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:
@@ -6,6 +6,8 @@
|
||||
import AppIconPicker from './AppIconPicker.svelte';
|
||||
import IntegrationConfigFields from './IntegrationConfigFields.svelte';
|
||||
import AppUrlPreview from './AppUrlPreview.svelte';
|
||||
import AutocompleteInput from '$lib/components/ui/AutocompleteInput.svelte';
|
||||
import TagsInput from '$lib/components/ui/TagsInput.svelte';
|
||||
import IconGrid from '$lib/components/ui/IconGrid.svelte';
|
||||
import type { IconGridItem } from '$lib/components/ui/IconGrid.svelte';
|
||||
|
||||
@@ -25,6 +27,21 @@
|
||||
|
||||
let showAdvanced = $state(false);
|
||||
let showIntegration = $state(false);
|
||||
let categorySuggestions = $state<string[]>([]);
|
||||
let tagSuggestions = $state<string[]>([]);
|
||||
|
||||
// Fetch autocomplete suggestions
|
||||
$effect(() => {
|
||||
fetch('/api/apps/suggestions')
|
||||
.then((r) => r.json())
|
||||
.then((json) => {
|
||||
if (json.success) {
|
||||
categorySuggestions = json.data?.categories ?? [];
|
||||
tagSuggestions = json.data?.tags ?? [];
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
let availableIntegrations = $state<Array<{ id: string; name: string; icon: string; authConfigFields: any[]; extraConfigFields: any[] }>>([]);
|
||||
let integrationConfig = $state<Record<string, unknown>>({});
|
||||
let testingConnection = $state(false);
|
||||
@@ -148,13 +165,13 @@
|
||||
<label for="category" class="mb-1 block text-sm font-medium text-card-foreground">
|
||||
{$t('app.category')}
|
||||
</label>
|
||||
<input
|
||||
<AutocompleteInput
|
||||
id="category"
|
||||
name="category"
|
||||
type="text"
|
||||
bind:value={$form.category}
|
||||
class="w-full 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"
|
||||
suggestions={categorySuggestions}
|
||||
placeholder={$t('app.category_placeholder')}
|
||||
class="w-full 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -162,13 +179,13 @@
|
||||
<label for="tags" class="mb-1 block text-sm font-medium text-card-foreground">
|
||||
{$t('app.tags')}
|
||||
</label>
|
||||
<input
|
||||
<TagsInput
|
||||
id="tags"
|
||||
name="tags"
|
||||
type="text"
|
||||
bind:value={$form.tags}
|
||||
class="w-full 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"
|
||||
suggestions={tagSuggestions}
|
||||
placeholder={$t('app.tags_placeholder')}
|
||||
class="w-full 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user