@svadmin/ui 0.0.6 → 0.3.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.
Files changed (41) hide show
  1. package/package.json +3 -2
  2. package/src/components/AdminApp.svelte +4 -3
  3. package/src/components/Authenticated.svelte +44 -13
  4. package/src/components/AutoForm.svelte +5 -5
  5. package/src/components/AutoSaveIndicator.svelte +30 -0
  6. package/src/components/AutoTable.svelte +81 -10
  7. package/src/components/Breadcrumbs.svelte +2 -0
  8. package/src/components/CatchAllNavigate.svelte +21 -0
  9. package/src/components/ConfirmDialog.svelte +1 -1
  10. package/src/components/CreatePage.svelte +42 -0
  11. package/src/components/EditPage.svelte +52 -0
  12. package/src/components/EmptyState.svelte +17 -31
  13. package/src/components/ErrorComponent.svelte +35 -0
  14. package/src/components/Header.svelte +28 -59
  15. package/src/components/ListPage.svelte +47 -0
  16. package/src/components/LiveIndicator.svelte +115 -0
  17. package/src/components/NavigateToResource.svelte +20 -0
  18. package/src/components/PageSkeleton.svelte +70 -0
  19. package/src/components/ShowPage.svelte +3 -3
  20. package/src/components/Sidebar.svelte +29 -10
  21. package/src/components/ThemedTitle.svelte +32 -0
  22. package/src/components/Toast.svelte +21 -11
  23. package/src/components/buttons/CloneButton.svelte +1 -1
  24. package/src/components/buttons/CreateButton.svelte +1 -1
  25. package/src/components/buttons/DeleteButton.svelte +3 -4
  26. package/src/components/buttons/EditButton.svelte +1 -1
  27. package/src/components/buttons/ImportButton.svelte +29 -7
  28. package/src/components/buttons/RefreshButton.svelte +5 -4
  29. package/src/components/fields/BooleanField.svelte +13 -0
  30. package/src/components/fields/DateField.svelte +6 -0
  31. package/src/components/fields/EmailField.svelte +15 -0
  32. package/src/components/fields/FileField.svelte +19 -0
  33. package/src/components/fields/ImageField.svelte +14 -0
  34. package/src/components/fields/MarkdownField.svelte +19 -0
  35. package/src/components/fields/NumberField.svelte +6 -0
  36. package/src/components/fields/TagField.svelte +17 -0
  37. package/src/components/fields/TextField.svelte +16 -0
  38. package/src/components/fields/UrlField.svelte +20 -0
  39. package/src/components/ui/avatar/avatar.svelte +2 -1
  40. package/src/index.ts +22 -0
  41. package/src/router-state.svelte.ts +16 -9
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ interface Props { value: string | null | undefined }
3
+ let { value }: Props = $props();
4
+ </script>
5
+ {#if value}
6
+ <a href="mailto:{value}" class="field-email">{value}</a>
7
+ {:else}
8
+ <span class="field-email field-empty">—</span>
9
+ {/if}
10
+
11
+ <style>
12
+ .field-email { color: var(--color-primary, #3b82f6); text-decoration: none; }
13
+ .field-email:hover { text-decoration: underline; }
14
+ .field-empty { color: var(--color-text-muted, #9ca3af); }
15
+ </style>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ interface Props { value: string | null | undefined; title?: string }
3
+ let { value, title }: Props = $props();
4
+ const fileName = $derived(value ? value.split('/').pop() ?? value : null);
5
+ </script>
6
+ {#if value}
7
+ <a href={value} download class="field-file" title={title ?? fileName ?? ''}>
8
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
9
+ {fileName}
10
+ </a>
11
+ {:else}
12
+ <span class="field-file field-empty">—</span>
13
+ {/if}
14
+
15
+ <style>
16
+ .field-file { color: var(--color-primary, #3b82f6); text-decoration: none; display: inline-flex; align-items: center; gap: 4px; }
17
+ .field-file:hover { text-decoration: underline; }
18
+ .field-empty { color: var(--color-text-muted, #9ca3af); }
19
+ </style>
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ interface Props { value: string | null | undefined; alt?: string; width?: number; height?: number }
3
+ let { value, alt = '', width = 80, height = 80 }: Props = $props();
4
+ </script>
5
+ {#if value}
6
+ <img src={value} {alt} {width} {height} class="field-image" />
7
+ {:else}
8
+ <span class="field-image field-empty">—</span>
9
+ {/if}
10
+
11
+ <style>
12
+ .field-image { border-radius: 8px; object-fit: cover; border: 1px solid var(--color-border, #e5e7eb); }
13
+ .field-empty { color: var(--color-text-muted, #9ca3af); display: inline-flex; align-items: center; justify-content: center; width: 80px; height: 80px; background: var(--color-surface, #f3f4f6); border-radius: 8px; }
14
+ </style>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ interface Props { value: string | null | undefined }
3
+ let { value }: Props = $props();
4
+ </script>
5
+ {#if value}
6
+ <div class="field-markdown">{@html value}</div>
7
+ {:else}
8
+ <span class="field-empty">—</span>
9
+ {/if}
10
+
11
+ <style>
12
+ .field-markdown { line-height: 1.6; }
13
+ .field-markdown :global(h1), .field-markdown :global(h2), .field-markdown :global(h3) { margin-top: 0.5em; margin-bottom: 0.25em; }
14
+ .field-markdown :global(p) { margin: 0.5em 0; }
15
+ .field-markdown :global(code) { background: var(--color-surface, #f3f4f6); padding: 2px 6px; border-radius: 4px; font-size: 0.875em; }
16
+ .field-markdown :global(pre) { background: var(--color-surface, #f3f4f6); padding: 12px; border-radius: 8px; overflow-x: auto; }
17
+ .field-markdown :global(a) { color: var(--color-primary, #3b82f6); }
18
+ .field-empty { color: var(--color-text-muted, #9ca3af); }
19
+ </style>
@@ -0,0 +1,6 @@
1
+ <script lang="ts">
2
+ interface Props { value: number | string | null | undefined; locale?: string; options?: Intl.NumberFormatOptions }
3
+ let { value, locale = 'en-US', options = {} }: Props = $props();
4
+ const formatted = $derived(value != null ? new Intl.NumberFormat(locale, options).format(Number(value)) : '—');
5
+ </script>
6
+ <span class="field-number">{formatted}</span>
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ interface Props { value: string | string[] | null | undefined; color?: string }
3
+ let { value, color = 'var(--color-primary, #3b82f6)' }: Props = $props();
4
+ const tags = $derived(Array.isArray(value) ? value : value ? [value] : []);
5
+ </script>
6
+ <span class="field-tags">
7
+ {#each tags as tag}
8
+ <span class="tag" style="--tag-color: {color}">{tag}</span>
9
+ {/each}
10
+ {#if tags.length === 0}<span class="field-empty">—</span>{/if}
11
+ </span>
12
+
13
+ <style>
14
+ .field-tags { display: inline-flex; flex-wrap: wrap; gap: 4px; }
15
+ .tag { display: inline-block; padding: 2px 8px; border-radius: 9999px; font-size: 12px; background: color-mix(in srgb, var(--tag-color) 15%, transparent); color: var(--tag-color); border: 1px solid color-mix(in srgb, var(--tag-color) 30%, transparent); }
16
+ .field-empty { color: var(--color-text-muted, #9ca3af); }
17
+ </style>
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ interface Props { value: string | null | undefined; maxLength?: number }
3
+ let { value, maxLength }: Props = $props();
4
+ const display = $derived(value && maxLength && value.length > maxLength ? value.slice(0, maxLength) + '…' : value);
5
+ const needsTooltip = $derived(value && maxLength && value.length > maxLength);
6
+ </script>
7
+ {#if value}
8
+ <span class="field-text" title={needsTooltip ? value : undefined}>{display}</span>
9
+ {:else}
10
+ <span class="field-text field-empty">—</span>
11
+ {/if}
12
+
13
+ <style>
14
+ .field-text { display: inline-block; }
15
+ .field-empty { color: var(--color-text-muted, #9ca3af); }
16
+ </style>
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ interface Props { value: string | null | undefined; label?: string }
3
+ let { value, label }: Props = $props();
4
+ const display = $derived(label ?? value ?? '');
5
+ </script>
6
+ {#if value}
7
+ <a href={value} target="_blank" rel="noopener noreferrer" class="field-url">
8
+ {display}
9
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
10
+ </a>
11
+ {:else}
12
+ <span class="field-url field-empty">—</span>
13
+ {/if}
14
+
15
+ <style>
16
+ .field-url { color: var(--color-primary, #3b82f6); text-decoration: none; display: inline-flex; align-items: center; gap: 4px; }
17
+ .field-url:hover { text-decoration: underline; }
18
+ .field-url svg { opacity: 0.6; }
19
+ .field-empty { color: var(--color-text-muted, #9ca3af); }
20
+ </style>
@@ -16,7 +16,8 @@
16
16
  defaultVariants: { size: "default" },
17
17
  });
18
18
 
19
- export type AvatarSize = VariantProps<typeof avatarVariants>["size"];
19
+ // AvatarSize type is exported from the package index.ts
20
+ type AvatarSize = VariantProps<typeof avatarVariants>["size"];
20
21
 
21
22
  type Props = WithElementRef<HTMLImgAttributes> & {
22
23
  size?: AvatarSize;
package/src/index.ts CHANGED
@@ -30,6 +30,28 @@ export { default as Authenticated } from './components/Authenticated.svelte';
30
30
  export { default as UpdatePasswordPage } from './components/UpdatePasswordPage.svelte';
31
31
  export { default as ConfigErrorScreen } from './components/ConfigErrorScreen.svelte';
32
32
  export { default as InferencerPanel } from './components/InferencerPanel.svelte';
33
+ export { default as LiveIndicator } from './components/LiveIndicator.svelte';
34
+ export { default as ListPage } from './components/ListPage.svelte';
35
+ export { default as CreatePage } from './components/CreatePage.svelte';
36
+ export { default as EditPage } from './components/EditPage.svelte';
37
+ export { default as AutoSaveIndicator } from './components/AutoSaveIndicator.svelte';
38
+ export { default as ErrorComponent } from './components/ErrorComponent.svelte';
39
+ export { default as PageSkeleton } from './components/PageSkeleton.svelte';
40
+ export { default as NavigateToResource } from './components/NavigateToResource.svelte';
41
+ export { default as CatchAllNavigate } from './components/CatchAllNavigate.svelte';
42
+ export { default as ThemedTitle } from './components/ThemedTitle.svelte';
43
+
44
+ // Field display components
45
+ export { default as NumberField } from './components/fields/NumberField.svelte';
46
+ export { default as DateField } from './components/fields/DateField.svelte';
47
+ export { default as EmailField } from './components/fields/EmailField.svelte';
48
+ export { default as UrlField } from './components/fields/UrlField.svelte';
49
+ export { default as BooleanField } from './components/fields/BooleanField.svelte';
50
+ export { default as TagField } from './components/fields/TagField.svelte';
51
+ export { default as FileField } from './components/fields/FileField.svelte';
52
+ export { default as MarkdownField } from './components/fields/MarkdownField.svelte';
53
+ export { default as TextField } from './components/fields/TextField.svelte';
54
+ export { default as ImageField } from './components/fields/ImageField.svelte';
33
55
 
34
56
  // CRUD Buttons
35
57
  export {
@@ -1,5 +1,5 @@
1
1
  // Shared reactive hash router state — .svelte.ts enables $state at module level
2
- import { matchRoute } from '@svadmin/core/router';
2
+ import { matchRoute, setActiveRouterProvider } from '@svadmin/core/router';
3
3
  import type { RouterProvider } from '@svadmin/core';
4
4
 
5
5
  let _route = $state('/');
@@ -17,20 +17,24 @@ const ROUTES = [
17
17
  '/:resource/create',
18
18
  '/:resource/edit/:id',
19
19
  '/:resource/show/:id',
20
+ '/:parent/:parentId/:resource',
21
+ '/:parent/:parentId/:resource/create',
22
+ '/:parent/:parentId/:resource/edit/:id',
23
+ '/:parent/:parentId/:resource/show/:id',
20
24
  ];
21
25
 
22
26
  function sync() {
27
+ let path: string;
28
+
23
29
  if (_provider) {
24
- // Use RouterProvider for parsing
25
30
  const parsed = _provider.parse();
26
- const path = parsed.pathname || '/';
27
- const m = matchRoute(path.startsWith('#') ? path : `#${path}`, ROUTES);
31
+ path = parsed.pathname || '/';
32
+ const m = matchRoute(path, ROUTES);
28
33
  _route = m?.route ?? '/';
29
34
  _params = { ...(m?.params ?? {}), ...parsed.params };
30
35
  } else {
31
- // Fallback: direct hash parsing
32
- const hash = window.location.hash;
33
- const m = matchRoute(hash, ROUTES);
36
+ path = typeof window !== 'undefined' ? window.location.hash.replace(/^#/, '') || '/' : '/';
37
+ const m = matchRoute(path, ROUTES);
34
38
  _route = m?.route ?? '/';
35
39
  _params = m?.params ?? {};
36
40
  }
@@ -40,9 +44,12 @@ export function initRouter(provider?: RouterProvider) {
40
44
  if (_initialized) return;
41
45
  _initialized = true;
42
46
  _provider = provider;
47
+ setActiveRouterProvider(provider);
43
48
  sync();
44
- window.addEventListener('hashchange', sync);
45
- window.addEventListener('popstate', sync);
49
+ if (typeof window !== 'undefined') {
50
+ window.addEventListener('hashchange', sync);
51
+ window.addEventListener('popstate', sync);
52
+ }
46
53
  }
47
54
 
48
55
  export function getRoute(): string {