feat(app-form): icon picker, tag/category autocomplete, typography

- Replace AppIconPicker text input with visual IconPickerButton for
  lucide icons (grid with search)
- Add AutocompleteInput component for category field with existing
  category suggestions
- Add TagsInput component for tags field with tag pills, autocomplete
  from existing tags, and keyboard navigation
- Add GET /api/apps/suggestions endpoint returning all categories/tags
- Add getAllTags() to appService (merges Tag model + comma-separated)
- Install @tailwindcss/typography plugin to fix prose rendering
  (headings, lists, blockquotes now render in Note/Markdown widgets)
- Fix note widget validator test for new html format
This commit is contained in:
2026-04-03 00:24:08 +03:00
parent a6b09aae9c
commit c5f5f84c79
10 changed files with 405 additions and 28 deletions
+31 -10
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import IconGrid from '$lib/components/ui/IconGrid.svelte';
import IconPickerButton from '$lib/components/ui/IconPickerButton.svelte';
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
import type { IconGridItem } from '$lib/components/ui/IconGrid.svelte';
interface Props {
@@ -29,6 +31,11 @@
iconValue = target.value;
onchange?.(iconType, iconValue);
}
function handleIconPickerChange(value: string) {
iconValue = value;
onchange?.(iconType, iconValue);
}
</script>
<div class="space-y-2">
@@ -44,22 +51,36 @@
/>
</div>
<input
type="text"
value={iconValue}
oninput={handleValueChange}
placeholder={iconType === 'lucide'
? $t('app.icon_lucide_placeholder')
: iconType === 'simple'
{#if iconType === 'lucide'}
<div class="flex flex-1 items-start">
<IconPickerButton
value={iconValue}
onchange={handleIconPickerChange}
placeholder={$t('app.icon_lucide_placeholder') ?? 'Select icon'}
/>
</div>
{:else}
<input
type="text"
value={iconValue}
oninput={handleValueChange}
placeholder={iconType === 'simple'
? $t('app.icon_simple_placeholder')
: iconType === 'url'
? $t('app.icon_url_placeholder')
: $t('app.icon_emoji_placeholder')}
class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
/>
class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
/>
{/if}
</div>
{#if iconType === 'emoji' && iconValue}
<!-- Preview -->
{#if iconType === 'lucide' && iconValue}
<div class="flex items-center gap-2">
<DynamicIcon name={iconValue} size={24} />
<span class="text-xs text-muted-foreground">{iconValue}</span>
</div>
{:else if iconType === 'emoji' && iconValue}
<div class="text-2xl">{iconValue}</div>
{:else if iconType === 'url' && iconValue}
<img src={iconValue} alt={$t('app.icon_preview')} class="h-8 w-8 rounded object-contain" />