@svadmin/ui 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,24 +8,47 @@
8
8
  import StatusBadge from './content/StatusBadge.svelte';
9
9
 
10
10
  const i18n = useTranslation();
11
- interface Integration { id: string; name: string; description: string; connected: boolean; Icon: Component; }
12
- let integrations = $state<Integration[]>([
13
- { id: 'source-control', name: 'Source Control', description: 'Sync repository metadata, deployments, and commit activity.', connected: true, Icon: FolderGit },
14
- { id: 'chatops', name: 'ChatOps', description: 'Send operational alerts and audit summaries to workspace channels.', connected: false, Icon: MessageCircle },
15
- { id: 'identity-cloud', name: 'Identity Cloud', description: 'Configure SSO and workspace identity handoff.', connected: true, Icon: Cloud },
16
- { id: 'webhooks', name: 'Webhooks', description: 'Register outbound webhooks for internal tools and monitors.', connected: false, Icon: Plug },
11
+
12
+ interface Integration {
13
+ id: string;
14
+ name: string;
15
+ description: string;
16
+ connected?: boolean;
17
+ Icon: Component;
18
+ }
19
+
20
+ interface Props {
21
+ integrations?: Integration[];
22
+ onConnectionChange?: (id: string, connected: boolean) => void | Promise<void>;
23
+ }
24
+
25
+ let { integrations, onConnectionChange }: Props = $props();
26
+ const defaultIntegrations = $derived<Integration[]>([
27
+ { id: 'source-control', name: i18n.t('integrations.sourceControl'), description: i18n.t('integrations.sourceControlDescription'), Icon: FolderGit },
28
+ { id: 'chatops', name: 'ChatOps', description: i18n.t('integrations.chatOpsDescription'), Icon: MessageCircle },
29
+ { id: 'identity-cloud', name: i18n.t('integrations.identityCloud'), description: i18n.t('integrations.identityCloudDescription'), Icon: Cloud },
30
+ { id: 'webhooks', name: 'Webhooks', description: i18n.t('integrations.webhooksDescription'), Icon: Plug },
17
31
  ]);
18
- function toggleConnection(id: string, connected: boolean) { integrations = integrations.map((item) => item.id === id ? { ...item, connected } : item); }
32
+ const displayedIntegrations = $derived(integrations ?? defaultIntegrations);
19
33
  </script>
20
34
 
21
35
  <div class="space-y-6">
22
36
  <div><h2 class="text-xl font-semibold text-foreground">{i18n.t('settings.integrations')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('settings.integrationsDescription')}</p></div>
23
37
  <SettingsGroup title={i18n.t('settings.integrations')} description={i18n.t('integrations.addMoreHint')}>
24
38
  <div class="divide-y divide-border">
25
- {#each integrations as integration (integration.id)}
39
+ {#each displayedIntegrations as integration (integration.id)}
26
40
  <SettingsFieldRow label={integration.name} description={integration.description}>
27
41
  {#snippet control()}
28
- <div class="flex items-center gap-3"><StatusBadge status={integration.connected ? 'success' : 'neutral'} label={integration.connected ? i18n.t('integrations.statusConnected') : i18n.t('integrations.statusDisconnected')} /><Switch checked={integration.connected} onCheckedChange={(checked) => toggleConnection(integration.id, checked)} aria-label={integration.name} /></div>
42
+ <div class="flex items-center gap-3">
43
+ {#if integration.connected !== undefined}
44
+ <StatusBadge status={integration.connected ? 'success' : 'neutral'} label={integration.connected ? i18n.t('integrations.statusConnected') : i18n.t('integrations.statusDisconnected')} />
45
+ {#if onConnectionChange}
46
+ <Switch checked={integration.connected} onCheckedChange={(checked) => onConnectionChange?.(integration.id, checked)} aria-label={integration.name} />
47
+ {/if}
48
+ {:else}
49
+ <span class="text-xs text-muted-foreground">{i18n.t('integrations.statusProvidedByHost')}</span>
50
+ {/if}
51
+ </div>
29
52
  {/snippet}
30
53
  </SettingsFieldRow>
31
54
  {/each}
@@ -1,4 +1,15 @@
1
1
  import type { Component } from 'svelte';
2
- declare const IntegrationsSettings: Component<Record<string, never>, {}, "">;
2
+ interface Integration {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ connected?: boolean;
7
+ Icon: Component;
8
+ }
9
+ interface Props {
10
+ integrations?: Integration[];
11
+ onConnectionChange?: (id: string, connected: boolean) => void | Promise<void>;
12
+ }
13
+ declare const IntegrationsSettings: Component<Props, {}, "">;
3
14
  type IntegrationsSettings = ReturnType<typeof IntegrationsSettings>;
4
15
  export default IntegrationsSettings;
@@ -54,7 +54,7 @@
54
54
  {#if bordered}
55
55
  <div class={cn('overflow-hidden rounded-lg border border-border bg-card text-foreground', className)}>
56
56
  <dl class={cn('grid divide-y divide-border/60 sm:divide-y-0', columnClass)}>
57
- {#each items as item (item.label)}
57
+ {#each items as item, idx (item.label ? `${item.label}-${idx}` : idx)}
58
58
  <div
59
59
  class={cn(
60
60
  'flex flex-col border-b border-r border-border/40 last:border-b-0',
@@ -99,7 +99,7 @@
99
99
  className,
100
100
  )}
101
101
  >
102
- {#each items as item (item.label)}
102
+ {#each items as item, idx (item.label ? `${item.label}-${idx}` : idx)}
103
103
  <div
104
104
  class={cn(
105
105
  'min-w-0',
@@ -3,10 +3,12 @@
3
3
  // Used by both ShowPage and FieldRenderer to avoid duplicate if/else chains.
4
4
  // Components are lazily resolved by field type at runtime.
5
5
  // Pre-import all field display components (tree-shaken by bundler)
6
+ import TextField from './fields/TextField.svelte';
6
7
  import BooleanField from './fields/BooleanField.svelte';
7
8
  import ImageField from './fields/ImageField.svelte';
8
9
  import TagField from './fields/TagField.svelte';
9
10
  import DateField from './fields/DateField.svelte';
11
+ import DateRangeField from './fields/DateRangeField.svelte';
10
12
  import EmailField from './fields/EmailField.svelte';
11
13
  import UrlField from './fields/UrlField.svelte';
12
14
  import SelectField from './fields/SelectField.svelte';
@@ -15,17 +17,27 @@ import RelationField from './fields/RelationField.svelte';
15
17
  import JsonField from './fields/JsonField.svelte';
16
18
  import RichTextField from './fields/RichTextField.svelte';
17
19
  import NumberField from './fields/NumberField.svelte';
20
+ import CurrencyField from './fields/CurrencyField.svelte';
21
+ import PhoneField from './fields/PhoneField.svelte';
22
+ import PercentField from './fields/PercentField.svelte';
23
+ import RatingField from './fields/RatingField.svelte';
24
+ import CodeField from './fields/CodeField.svelte';
25
+ import AvatarField from './fields/AvatarField.svelte';
26
+ import CopyField from './fields/CopyField.svelte';
18
27
  import FileField from './fields/FileField.svelte';
19
28
  /**
20
29
  * Built-in display components for show/detail pages.
21
30
  * Keys match FieldDefinition.type values.
22
31
  */
23
32
  export const builtinDisplayComponents = {
33
+ text: TextField,
34
+ string: TextField,
24
35
  boolean: BooleanField,
25
36
  image: ImageField,
26
37
  images: ImageField,
27
38
  tags: TagField,
28
39
  date: DateField,
40
+ daterange: DateRangeField,
29
41
  email: EmailField,
30
42
  url: UrlField,
31
43
  select: SelectField,
@@ -35,6 +47,13 @@ export const builtinDisplayComponents = {
35
47
  richtext: RichTextField,
36
48
  textarea: RichTextField,
37
49
  number: NumberField,
50
+ currency: CurrencyField,
51
+ phone: PhoneField,
52
+ percent: PercentField,
53
+ rating: RatingField,
54
+ code: CodeField,
55
+ avatar: AvatarField,
56
+ copy: CopyField,
38
57
  file: FileField,
39
58
  };
40
59
  // ─── Custom component registry ─────────────────────────────────
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { onDestroy } from 'svelte';
2
3
  import { Copy, Check } from '@lucide/svelte';
3
4
  import { Badge } from '../ui/badge/index.js';
4
5
  import { cn } from '../../utils.js';
@@ -27,6 +28,12 @@
27
28
 
28
29
  let copied = $state(false);
29
30
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
31
+ let destroyed = false;
32
+
33
+ onDestroy(() => {
34
+ destroyed = true;
35
+ if (timeoutId) clearTimeout(timeoutId);
36
+ });
30
37
 
31
38
  const formattedCode = $derived.by(() => {
32
39
  if (value == null || value === '') return '';
@@ -39,20 +46,25 @@
39
46
  }
40
47
  });
41
48
 
42
- async function handleCopy() {
49
+ async function handleCopy(event: MouseEvent) {
50
+ event.stopPropagation();
43
51
  if (!formattedCode || !copyable) return;
52
+
53
+ const writeText = globalThis.navigator?.clipboard?.writeText;
54
+ if (!writeText) return;
55
+
44
56
  try {
45
- if (typeof navigator !== 'undefined' && navigator.clipboard) {
46
- await navigator.clipboard.writeText(formattedCode);
47
- }
57
+ await writeText.call(globalThis.navigator.clipboard, formattedCode);
48
58
  } catch {
49
- // Fallback
59
+ return;
50
60
  }
51
61
 
62
+ if (destroyed) return;
52
63
  copied = true;
53
64
  if (timeoutId) clearTimeout(timeoutId);
54
65
  timeoutId = setTimeout(() => {
55
66
  copied = false;
67
+ timeoutId = null;
56
68
  }, 1500);
57
69
  }
58
70
  </script>
@@ -0,0 +1,103 @@
1
+ <script module lang="ts">
2
+ export type CurrencyTone = 'auto' | 'success' | 'warning' | 'destructive' | 'neutral';
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ import { cn } from '../../utils.js';
7
+
8
+ interface Props {
9
+ value?: number | string | null | undefined;
10
+ currency?: string;
11
+ locale?: string;
12
+ precision?: number;
13
+ compact?: boolean;
14
+ symbol?: string;
15
+ colored?: boolean;
16
+ tone?: CurrencyTone;
17
+ nullLabel?: string;
18
+ tabular?: boolean;
19
+ class?: string;
20
+ }
21
+
22
+ let {
23
+ value,
24
+ currency = 'USD',
25
+ locale = 'en-US',
26
+ precision = 2,
27
+ compact = false,
28
+ symbol,
29
+ colored = false,
30
+ tone = 'neutral',
31
+ nullLabel = '—',
32
+ tabular = true,
33
+ class: className = '',
34
+ }: Props = $props();
35
+
36
+ const normalizedPrecision = $derived(
37
+ Number.isFinite(precision) ? Math.min(20, Math.max(0, Math.trunc(precision))) : 2
38
+ );
39
+
40
+ const numericValue = $derived.by(() => {
41
+ if (value == null) return null;
42
+ if (typeof value === 'string' && value.trim() === '') return null;
43
+ const num = typeof value === 'number' ? value : Number(value);
44
+ return Number.isFinite(num) ? num : null;
45
+ });
46
+
47
+ const formatted = $derived.by(() => {
48
+ if (numericValue === null) return nullLabel;
49
+
50
+ const opts: Intl.NumberFormatOptions = {
51
+ style: symbol ? 'decimal' : 'currency',
52
+ currency: symbol ? undefined : currency,
53
+ minimumFractionDigits: normalizedPrecision,
54
+ maximumFractionDigits: normalizedPrecision,
55
+ };
56
+
57
+ if (compact) {
58
+ opts.notation = 'compact';
59
+ opts.compactDisplay = 'short';
60
+ }
61
+
62
+ try {
63
+ let res = new Intl.NumberFormat(locale, opts).format(numericValue);
64
+ if (symbol) {
65
+ res = `${symbol}${res}`;
66
+ }
67
+ return res;
68
+ } catch {
69
+ return String(numericValue);
70
+ }
71
+ });
72
+
73
+ const resolvedTone = $derived.by(() => {
74
+ if (tone !== 'auto' && tone !== 'neutral') return tone;
75
+ if ((!colored && tone !== 'auto') || numericValue === null) return 'neutral';
76
+ if (numericValue > 0) return 'success';
77
+ if (numericValue < 0) return 'destructive';
78
+ return 'neutral';
79
+ });
80
+
81
+ const toneClasses: Record<CurrencyTone, string> = {
82
+ auto: '',
83
+ success: 'text-success font-medium',
84
+ warning: 'text-warning-foreground font-medium',
85
+ destructive: 'text-destructive font-medium',
86
+ neutral: 'text-foreground',
87
+ };
88
+ </script>
89
+
90
+ {#if numericValue === null}
91
+ <span class={cn('field-currency text-muted-foreground text-sm', className)}>{nullLabel}</span>
92
+ {:else}
93
+ <span
94
+ class={cn(
95
+ 'field-currency inline-flex items-center text-sm',
96
+ tabular && 'font-mono tabular-nums',
97
+ toneClasses[resolvedTone],
98
+ className
99
+ )}
100
+ >
101
+ {formatted}
102
+ </span>
103
+ {/if}
@@ -0,0 +1,17 @@
1
+ export type CurrencyTone = 'auto' | 'success' | 'warning' | 'destructive' | 'neutral';
2
+ interface Props {
3
+ value?: number | string | null | undefined;
4
+ currency?: string;
5
+ locale?: string;
6
+ precision?: number;
7
+ compact?: boolean;
8
+ symbol?: string;
9
+ colored?: boolean;
10
+ tone?: CurrencyTone;
11
+ nullLabel?: string;
12
+ tabular?: boolean;
13
+ class?: string;
14
+ }
15
+ declare const CurrencyField: import("svelte").Component<Props, {}, "">;
16
+ type CurrencyField = ReturnType<typeof CurrencyField>;
17
+ export default CurrencyField;
@@ -25,23 +25,28 @@
25
25
  class: className = '',
26
26
  }: Props = $props();
27
27
 
28
+ const normalizedPrecision = $derived(
29
+ Math.min(20, Math.max(0, Math.trunc(Number.isFinite(precision) ? precision : 1)))
30
+ );
31
+
28
32
  const numericValue = $derived.by(() => {
29
- if (value == null || value === '') return null;
30
- const num = typeof value === 'number' ? value : Number(value);
31
- return isNaN(num) ? null : num;
33
+ if (value == null) return null;
34
+ if (typeof value === 'string' && value.trim() === '') return null;
35
+ const parsed = typeof value === 'number' ? value : Number(value);
36
+ return Number.isFinite(parsed) ? parsed : null;
32
37
  });
33
38
 
34
39
  const percentNumber = $derived.by(() => {
35
40
  if (numericValue === null) return null;
36
- return scale === '1' ? numericValue * 100 : numericValue;
41
+ const scaled = scale === '1' ? numericValue * 100 : numericValue;
42
+ return Number.isFinite(scaled) ? scaled : null;
37
43
  });
38
44
 
39
- const formatted = $derived.by(() => {
40
- if (percentNumber === null) return nullLabel;
41
- return `${percentNumber.toFixed(precision)}%`;
42
- });
45
+ const formatted = $derived(
46
+ percentNumber === null ? nullLabel : `${percentNumber.toFixed(normalizedPrecision)}%`
47
+ );
43
48
 
44
- const resolvedTone = $derived.by(() => {
49
+ const resolvedTone = $derived.by((): PercentTone => {
45
50
  if (tone !== 'auto') return tone;
46
51
  if (percentNumber === null) return 'neutral';
47
52
  if (percentNumber >= 80) return 'success';
@@ -67,13 +72,12 @@
67
72
  neutral: 'bg-primary',
68
73
  };
69
74
 
70
- const clampedProgress = $derived.by(() => {
71
- if (percentNumber === null) return 0;
72
- return Math.min(100, Math.max(0, percentNumber));
73
- });
75
+ const clampedProgress = $derived(
76
+ percentNumber === null ? 0 : Math.min(100, Math.max(0, percentNumber))
77
+ );
74
78
  </script>
75
79
 
76
- {#if numericValue === null}
80
+ {#if percentNumber === null}
77
81
  <span class={cn('field-percent text-muted-foreground text-sm', className)}>{nullLabel}</span>
78
82
  {:else}
79
83
  <div class={cn('field-percent inline-flex items-center gap-2 text-sm', className)}>
@@ -81,7 +85,14 @@
81
85
  {formatted}
82
86
  </span>
83
87
  {#if showProgress}
84
- <div class="h-1.5 w-16 overflow-hidden rounded-full bg-muted">
88
+ <div
89
+ class="h-1.5 w-16 overflow-hidden rounded-full bg-muted"
90
+ role="progressbar"
91
+ aria-label="Percentage"
92
+ aria-valuemin="0"
93
+ aria-valuemax="100"
94
+ aria-valuenow={clampedProgress}
95
+ >
85
96
  <div
86
97
  class={cn('h-full transition-all duration-300', toneBarClass[resolvedTone])}
87
98
  style="width: {clampedProgress}%"
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+ import { Phone, Copy, Check } from '@lucide/svelte';
4
+ import { cn } from '../../utils.js';
5
+
6
+ interface Props {
7
+ value?: string | number | null | undefined;
8
+ href?: string;
9
+ showIcon?: boolean;
10
+ clickable?: boolean;
11
+ copyable?: boolean;
12
+ nullLabel?: string;
13
+ oncopy?: (value: string) => void;
14
+ class?: string;
15
+ }
16
+
17
+ let {
18
+ value,
19
+ href,
20
+ showIcon = true,
21
+ clickable = true,
22
+ copyable = false,
23
+ nullLabel = '—',
24
+ oncopy,
25
+ class: className = '',
26
+ }: Props = $props();
27
+
28
+ let copied = $state(false);
29
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
30
+ let destroyed = false;
31
+
32
+ onDestroy(() => {
33
+ destroyed = true;
34
+ if (timeoutId) clearTimeout(timeoutId);
35
+ });
36
+
37
+ const stringValue = $derived.by(() => {
38
+ if (value == null || value === '') return null;
39
+ const str = String(value).trim();
40
+ return str.length > 0 ? str : null;
41
+ });
42
+
43
+ const telHref = $derived.by(() => {
44
+ if (!stringValue) return undefined;
45
+ const candidate = (href ?? stringValue).replace(/^tel:/i, '');
46
+ const normalized = candidate.replace(/[\s().-]/g, '');
47
+ if (!/^\+?\d+$/.test(normalized)) return undefined;
48
+ return `tel:${normalized}`;
49
+ });
50
+
51
+ async function handleCopy(e: MouseEvent) {
52
+ e.preventDefault();
53
+ e.stopPropagation();
54
+ if (!stringValue || !copyable) return;
55
+
56
+ const writeText = globalThis.navigator?.clipboard?.writeText;
57
+ if (!writeText) return;
58
+
59
+ try {
60
+ await writeText.call(globalThis.navigator.clipboard, stringValue);
61
+ } catch {
62
+ return;
63
+ }
64
+
65
+ if (destroyed) return;
66
+ copied = true;
67
+ oncopy?.(stringValue);
68
+
69
+ if (timeoutId) clearTimeout(timeoutId);
70
+ timeoutId = setTimeout(() => {
71
+ copied = false;
72
+ timeoutId = null;
73
+ }, 2000);
74
+ }
75
+ </script>
76
+
77
+ {#if !stringValue}
78
+ <span class={cn('field-phone text-muted-foreground text-sm', className)}>{nullLabel}</span>
79
+ {:else}
80
+ <div class={cn('field-phone inline-flex items-center gap-1.5 text-sm', className)}>
81
+ {#if showIcon}
82
+ <Phone class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
83
+ {/if}
84
+ {#if clickable && telHref}
85
+ <a
86
+ href={telHref}
87
+ class="font-medium text-foreground hover:text-primary transition-colors underline-offset-4 hover:underline"
88
+ >
89
+ {stringValue}
90
+ </a>
91
+ {:else}
92
+ <span class="text-foreground">{stringValue}</span>
93
+ {/if}
94
+ {#if copyable}
95
+ <button
96
+ type="button"
97
+ onclick={handleCopy}
98
+ class="inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
99
+ aria-label={copied ? 'Copied' : 'Copy phone number'}
100
+ title={copied ? 'Copied' : 'Copy phone number'}
101
+ >
102
+ {#if copied}
103
+ <Check class="h-3 w-3 text-success" />
104
+ {:else}
105
+ <Copy class="h-3 w-3" />
106
+ {/if}
107
+ </button>
108
+ {/if}
109
+ </div>
110
+ {/if}
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ value?: string | number | null | undefined;
3
+ href?: string;
4
+ showIcon?: boolean;
5
+ clickable?: boolean;
6
+ copyable?: boolean;
7
+ nullLabel?: string;
8
+ oncopy?: (value: string) => void;
9
+ class?: string;
10
+ }
11
+ declare const PhoneField: import("svelte").Component<Props, {}, "">;
12
+ type PhoneField = ReturnType<typeof PhoneField>;
13
+ export default PhoneField;
@@ -24,10 +24,16 @@
24
24
  class: className = '',
25
25
  }: Props = $props();
26
26
 
27
+ const normalizedMax = $derived(
28
+ Number.isFinite(max) ? Math.min(100, Math.max(1, Math.trunc(max))) : 5
29
+ );
30
+
27
31
  const numericValue = $derived.by(() => {
28
- if (value == null || value === '') return null;
29
- const num = typeof value === 'number' ? value : Number(value);
30
- return isNaN(num) ? null : Math.max(0, Math.min(max, num));
32
+ if (value == null) return null;
33
+ if (typeof value === 'string' && value.trim() === '') return null;
34
+ const parsed = typeof value === 'number' ? value : Number(value);
35
+ if (!Number.isFinite(parsed)) return null;
36
+ return Math.max(0, Math.min(normalizedMax, parsed));
31
37
  });
32
38
 
33
39
  const sizeClasses: Record<RatingSize, { icon: string; text: string }> = {
@@ -39,10 +45,10 @@
39
45
  const stars = $derived.by(() => {
40
46
  if (numericValue === null) return [];
41
47
  const result: ('full' | 'half' | 'empty')[] = [];
42
- for (let i = 1; i <= max; i++) {
43
- if (numericValue >= i) {
48
+ for (let index = 1; index <= normalizedMax; index += 1) {
49
+ if (numericValue >= index) {
44
50
  result.push('full');
45
- } else if (numericValue >= i - 0.5) {
51
+ } else if (numericValue >= index - 0.5) {
46
52
  result.push('half');
47
53
  } else {
48
54
  result.push('empty');
@@ -56,14 +62,18 @@
56
62
  <span class={cn('field-rating text-muted-foreground text-sm', className)}>{nullLabel}</span>
57
63
  {:else}
58
64
  <div class={cn('field-rating inline-flex items-center gap-1.5', className)}>
59
- <div class="inline-flex items-center gap-0.5 text-amber-500">
65
+ <div
66
+ class="inline-flex items-center gap-0.5 text-amber-500"
67
+ role="img"
68
+ aria-label={`${numericValue} out of ${normalizedMax}`}
69
+ >
60
70
  {#each stars as starType, idx (idx)}
61
71
  {#if starType === 'full'}
62
- <Star class={cn(sizeClasses[size].icon, 'fill-amber-500 text-amber-500')} />
72
+ <Star aria-hidden="true" class={cn(sizeClasses[size].icon, 'fill-amber-500 text-amber-500')} />
63
73
  {:else if starType === 'half'}
64
- <StarHalf class={cn(sizeClasses[size].icon, 'fill-amber-500 text-amber-500')} />
74
+ <StarHalf aria-hidden="true" class={cn(sizeClasses[size].icon, 'fill-amber-500 text-amber-500')} />
65
75
  {:else}
66
- <Star class={cn(sizeClasses[size].icon, 'text-muted-foreground/30')} />
76
+ <Star aria-hidden="true" class={cn(sizeClasses[size].icon, 'text-muted-foreground/30')} />
67
77
  {/if}
68
78
  {/each}
69
79
  </div>
package/dist/index.d.ts CHANGED
@@ -111,6 +111,8 @@ export { default as AvatarField } from './components/fields/AvatarField.svelte';
111
111
  export { default as CodeField } from './components/fields/CodeField.svelte';
112
112
  export { default as PercentField } from './components/fields/PercentField.svelte';
113
113
  export { default as RatingField } from './components/fields/RatingField.svelte';
114
+ export { default as CurrencyField } from './components/fields/CurrencyField.svelte';
115
+ export { default as PhoneField } from './components/fields/PhoneField.svelte';
114
116
  export { CreateButton, EditButton, DeleteButton, ShowButton, ListButton, RefreshButton, ExportButton, ImportButton, SaveButton, CloneButton, } from './components/buttons/index.js';
115
117
  export { Button, buttonVariants } from './components/ui/button/index.js';
116
118
  export { Input } from './components/ui/input/index.js';
@@ -221,3 +223,4 @@ export { default as BatchActionBar } from './components/BatchActionBar.svelte';
221
223
  export type { DateRangeFormat, DateRangeValue } from './components/fields/DateRangeField.svelte';
222
224
  export type { PercentTone } from './components/fields/PercentField.svelte';
223
225
  export type { RatingSize } from './components/fields/RatingField.svelte';
226
+ export type { CurrencyTone } from './components/fields/CurrencyField.svelte';
package/dist/index.js CHANGED
@@ -114,6 +114,8 @@ export { default as AvatarField } from './components/fields/AvatarField.svelte';
114
114
  export { default as CodeField } from './components/fields/CodeField.svelte';
115
115
  export { default as PercentField } from './components/fields/PercentField.svelte';
116
116
  export { default as RatingField } from './components/fields/RatingField.svelte';
117
+ export { default as CurrencyField } from './components/fields/CurrencyField.svelte';
118
+ export { default as PhoneField } from './components/fields/PhoneField.svelte';
117
119
  // Rich text editor (requires @svadmin/editor) as a direct import in consumer app
118
120
  // CRUD Buttons
119
121
  export { CreateButton, EditButton, DeleteButton, ShowButton, ListButton, RefreshButton, ExportButton, ImportButton, SaveButton, CloneButton, } from './components/buttons/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -39,7 +39,7 @@
39
39
  "./components/*": "./dist/components/*"
40
40
  },
41
41
  "peerDependencies": {
42
- "@svadmin/core": "^0.41.0",
42
+ "@svadmin/core": "^0.41.1",
43
43
  "@tanstack/svelte-query": "^6.1.38",
44
44
  "highlight.js": "^11.11.1",
45
45
  "isomorphic-dompurify": "^3.22.0",