@svadmin/ui 0.56.0 → 0.58.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 (52) hide show
  1. package/dist/app.css +28 -0
  2. package/dist/app.theme.css +28 -0
  3. package/dist/components/AICommandBar.svelte +33 -10
  4. package/dist/components/AdminApp.svelte +16 -5
  5. package/dist/components/AutoForm.svelte +4 -4
  6. package/dist/components/AutoTable.svelte +866 -204
  7. package/dist/components/AutoTable.svelte.d.ts +1 -1
  8. package/dist/components/CommandPalette.svelte +39 -17
  9. package/dist/components/ConfirmDialog.svelte +4 -3
  10. package/dist/components/ConfirmDialog.svelte.d.ts +1 -0
  11. package/dist/components/CopilotPanel.svelte +3 -3
  12. package/dist/components/DraggableHeader.svelte +15 -36
  13. package/dist/components/DraggableHeader.svelte.d.ts +13 -4
  14. package/dist/components/EditPage.svelte +4 -4
  15. package/dist/components/EmptyState.svelte +1 -1
  16. package/dist/components/ErrorComponent.svelte +1 -1
  17. package/dist/components/FieldRenderer.svelte +2 -1
  18. package/dist/components/ListPage.svelte +4 -12
  19. package/dist/components/ListPage.svelte.d.ts +1 -1
  20. package/dist/components/RecordDetailDrawer.svelte +121 -0
  21. package/dist/components/RecordDetailDrawer.svelte.d.ts +9 -0
  22. package/dist/components/ResourceAccessGuard.svelte +68 -0
  23. package/dist/components/ResourceAccessGuard.svelte.d.ts +12 -0
  24. package/dist/components/ResourceOperationsPage.svelte +8 -3
  25. package/dist/components/ShowPage.svelte +5 -6
  26. package/dist/components/TaskQueueDrawer.svelte +1 -1
  27. package/dist/components/TenantSwitcher.svelte +1 -0
  28. package/dist/components/Toast.svelte +35 -1
  29. package/dist/components/UndoableNotification.svelte +12 -6
  30. package/dist/components/UndoableNotification.svelte.d.ts +2 -0
  31. package/dist/components/account/MembersStarterPage.svelte +1 -1
  32. package/dist/components/buttons/CloneButton.svelte +9 -2
  33. package/dist/components/buttons/CreateButton.svelte +9 -2
  34. package/dist/components/buttons/DeleteButton.svelte +9 -2
  35. package/dist/components/buttons/EditButton.svelte +9 -2
  36. package/dist/components/buttons/ListButton.svelte +1 -1
  37. package/dist/components/buttons/ShowButton.svelte +9 -2
  38. package/dist/components/content/DataState.svelte +1 -1
  39. package/dist/components/content/FilterToolbar.svelte +6 -2
  40. package/dist/components/content/MediaThumbnail.svelte +109 -0
  41. package/dist/components/content/MediaThumbnail.svelte.d.ts +22 -0
  42. package/dist/components/content/ProjectCard.svelte +2 -1
  43. package/dist/components/content/SystemErrorState.svelte +1 -1
  44. package/dist/components/content/index.d.ts +2 -0
  45. package/dist/components/content/index.js +1 -0
  46. package/dist/components/fields/ImageField.svelte +19 -4
  47. package/dist/components/fields/ImageField.svelte.d.ts +4 -0
  48. package/dist/components/saved-list-views.d.ts +35 -0
  49. package/dist/components/saved-list-views.js +168 -0
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.js +3 -0
  52. package/package.json +2 -2
package/dist/app.css CHANGED
@@ -263,6 +263,27 @@
263
263
 
264
264
  }
265
265
 
266
+ @layer components {
267
+ /* Collapsed semantic containers must not retain surface chrome or spacing. */
268
+ details.svadmin-collapsible:not([open]),
269
+ [data-svadmin-collapsible]:not([data-open="true"]) {
270
+ margin: 0 !important;
271
+ padding: 0 !important;
272
+ border: 0 !important;
273
+ background: transparent !important;
274
+ box-shadow: none !important;
275
+ }
276
+
277
+ [data-svadmin-filter-toolbar] .svadmin-filter-toolbar-advanced[hidden] {
278
+ display: none !important;
279
+ }
280
+
281
+ [data-svadmin-filter-toolbar] .svadmin-filter-toolbar-advanced:not([hidden]) {
282
+ min-width: 0;
283
+ }
284
+
285
+ }
286
+
266
287
  body {
267
288
  background: var(--background);
268
289
  color: var(--foreground);
@@ -605,6 +626,13 @@ main {
605
626
  font-weight: 600;
606
627
  }
607
628
 
629
+ /* Collapsible details normalization stays scoped to svadmin-owned hooks. */
630
+ details[data-svadmin-filter]:not([open]) > :not(summary),
631
+ details.svadmin-collapsible-filter:not([open]) > :not(summary),
632
+ details.svadmin-collapsible:not([open]) > :not(summary) {
633
+ display: none !important;
634
+ }
635
+
608
636
  /* --- Stripe-first layout preset (clean-flat) --- */
609
637
  /* Keep this public class for layoutPreset API compatibility. The preset only
610
638
  * adjusts density and composition; color, focus, and elevation stay semantic. */
@@ -263,6 +263,27 @@
263
263
 
264
264
  }
265
265
 
266
+ @layer components {
267
+ /* Collapsed semantic containers must not retain surface chrome or spacing. */
268
+ details.svadmin-collapsible:not([open]),
269
+ [data-svadmin-collapsible]:not([data-open="true"]) {
270
+ margin: 0 !important;
271
+ padding: 0 !important;
272
+ border: 0 !important;
273
+ background: transparent !important;
274
+ box-shadow: none !important;
275
+ }
276
+
277
+ [data-svadmin-filter-toolbar] .svadmin-filter-toolbar-advanced[hidden] {
278
+ display: none !important;
279
+ }
280
+
281
+ [data-svadmin-filter-toolbar] .svadmin-filter-toolbar-advanced:not([hidden]) {
282
+ min-width: 0;
283
+ }
284
+
285
+ }
286
+
266
287
  body {
267
288
  background: var(--background);
268
289
  color: var(--foreground);
@@ -604,6 +625,13 @@ main {
604
625
  font-weight: 600;
605
626
  }
606
627
 
628
+ /* Collapsible details normalization stays scoped to svadmin-owned hooks. */
629
+ details[data-svadmin-filter]:not([open]) > :not(summary),
630
+ details.svadmin-collapsible-filter:not([open]) > :not(summary),
631
+ details.svadmin-collapsible:not([open]) > :not(summary) {
632
+ display: none !important;
633
+ }
634
+
607
635
  /* --- Stripe-first layout preset (clean-flat) --- */
608
636
  /* Keep this public class for layoutPreset API compatibility. The preset only
609
637
  * adjusts density and composition; color, focus, and elevation stay semantic. */
@@ -1,15 +1,17 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext, toggleTheme } from '@svadmin/core';
2
+ import { captureAdminContext, toggleTheme, useNavigation } from '@svadmin/core';
3
3
  import { useTranslation } from '@svadmin/core/i18n';
4
4
  import { Command } from './ui/command/index.js';
5
5
  import * as Dialog from './ui/dialog/index.js';
6
6
  import { Search, LayoutDashboard, Plus, Sun, FileText, Sparkles, Loader2 } from '@lucide/svelte';
7
7
  import MarkdownRenderer from './MarkdownRenderer.svelte';
8
+ import CanAccess from './CanAccess.svelte';
8
9
 
9
10
  const i18n = useTranslation();
10
11
 
11
12
  let { open = $bindable(false) } = $props<{ open?: boolean }>();
12
13
  const adminContext = captureAdminContext();
14
+ const navigation = useNavigation();
13
15
  let searchValue = $state('');
14
16
 
15
17
  const resources = $derived(adminContext.resources);
@@ -127,7 +129,8 @@
127
129
  />
128
130
  {#if searchValue.trim() && !aiMode}
129
131
  <button
130
- class="absolute right-3 flex items-center gap-1 text-[10px] font-medium text-warning bg-warning/10 hover:bg-warning/20 px-2 py-1 rounded transition-colors"
132
+ type="button"
133
+ class="absolute right-3 flex items-center gap-1 text-[10px] font-medium text-warning bg-warning/10 hover:bg-warning/20 px-2 py-1 rounded transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
131
134
  onclick={askAI}
132
135
  >
133
136
  <Sparkles class="h-3 w-3" />
@@ -149,10 +152,19 @@
149
152
  {i18n.t('common.home')}
150
153
  </Command.Item>
151
154
  {#each resources as r, _i (_i)}
152
- <Command.Item value={r.name} onSelect={() => act(() => adminContext.navigate(`/${r.name}`))} class={itemClass}>
153
- <FileText class="h-4 w-4 text-muted-foreground" />
154
- {r.label}
155
- </Command.Item>
155
+ {#if adminContext.accessControlProvider}
156
+ <CanAccess resource={r.name} action="list">
157
+ <Command.Item value={r.name} onSelect={() => act(() => navigation.list(r.name))} class={itemClass}>
158
+ <FileText class="h-4 w-4 text-muted-foreground" />
159
+ {r.label}
160
+ </Command.Item>
161
+ </CanAccess>
162
+ {:else}
163
+ <Command.Item value={r.name} onSelect={() => act(() => navigation.list(r.name))} class={itemClass}>
164
+ <FileText class="h-4 w-4 text-muted-foreground" />
165
+ {r.label}
166
+ </Command.Item>
167
+ {/if}
156
168
  {/each}
157
169
  </Command.Group>
158
170
 
@@ -161,10 +173,21 @@
161
173
  <!-- Actions -->
162
174
  <Command.Group heading={i18n.t('common.actions')}>
163
175
  {#each resources as r, _i (_i)}
164
- <Command.Item value={`create-${r.name}`} onSelect={() => act(() => adminContext.navigate(`/${r.name}/create`))} class={itemClass}>
165
- <Plus class="h-4 w-4 text-muted-foreground" />
166
- {i18n.t('common.create')} {r.label}
167
- </Command.Item>
176
+ {#if r.canCreate !== false}
177
+ {#if adminContext.accessControlProvider}
178
+ <CanAccess resource={r.name} action="create">
179
+ <Command.Item value={`create-${r.name}`} onSelect={() => act(() => navigation.create(r.name))} class={itemClass}>
180
+ <Plus class="h-4 w-4 text-muted-foreground" />
181
+ {i18n.t('common.create')} {r.label}
182
+ </Command.Item>
183
+ </CanAccess>
184
+ {:else}
185
+ <Command.Item value={`create-${r.name}`} onSelect={() => act(() => navigation.create(r.name))} class={itemClass}>
186
+ <Plus class="h-4 w-4 text-muted-foreground" />
187
+ {i18n.t('common.create')} {r.label}
188
+ </Command.Item>
189
+ {/if}
190
+ {/if}
168
191
  {/each}
169
192
  <Command.Item value="toggle-theme" onSelect={() => act(() => toggleTheme())} class={itemClass}>
170
193
  <Sun class="h-4 w-4 text-muted-foreground" />
@@ -73,6 +73,7 @@
73
73
  import ErrorPage from './ErrorPage.svelte';
74
74
  import SystemPageShell from './SystemPageShell.svelte';
75
75
  import LazyPage from './LazyPage.svelte';
76
+ import ResourceAccessGuard from './ResourceAccessGuard.svelte';
76
77
  import { Button } from './ui/button/index.js';
77
78
  import { Input } from './ui/input/index.js';
78
79
  import { Badge } from './ui/badge/index.js';
@@ -609,27 +610,37 @@
609
610
  {:else if (renderedRoute === '/:resource' || renderedRoute === '/:parent/:parentId/:resource') && renderedHasRouteResource}
610
611
  {#key renderedParams.resource}
611
612
  {@const Comp = renderedResourcePages?.list ?? mergedComponents.AutoTable}
612
- <Comp resourceName={renderedParams.resource} />
613
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="list">
614
+ <Comp resourceName={renderedParams.resource} />
615
+ </ResourceAccessGuard>
613
616
  {/key}
614
617
  {:else if (renderedRoute === '/:resource/create' || renderedRoute === '/:parent/:parentId/:resource/create') && renderedHasRouteResource}
615
618
  {#key renderedParams.resource}
616
619
  {@const Comp = renderedResourcePages?.create ?? mergedComponents.AutoForm}
617
- <Comp resourceName={renderedParams.resource} mode="create" />
620
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="create">
621
+ <Comp resourceName={renderedParams.resource} mode="create" />
622
+ </ResourceAccessGuard>
618
623
  {/key}
619
624
  {:else if (renderedRoute === '/:resource/edit/:id' || renderedRoute === '/:resource/:id/edit' || renderedRoute === '/:parent/:parentId/:resource/edit/:id') && renderedHasRouteResource}
620
625
  {#key `${renderedParams.resource}-${renderedParams.id}`}
621
626
  {@const Comp = renderedResourcePages?.edit ?? mergedComponents.AutoForm}
622
- <Comp resourceName={renderedParams.resource} mode="edit" id={renderedParams.id} />
627
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="edit" id={renderedParams.id}>
628
+ <Comp resourceName={renderedParams.resource} mode="edit" id={renderedParams.id} />
629
+ </ResourceAccessGuard>
623
630
  {/key}
624
631
  {:else if (renderedRoute === '/:resource/show/:id' || renderedRoute === '/:resource/:id' || renderedRoute === '/:parent/:parentId/:resource/show/:id') && renderedHasRouteResource}
625
632
  {#key `${renderedParams.resource}-${renderedParams.id}`}
626
633
  {@const Comp = renderedResourcePages?.show ?? mergedComponents.ShowPage}
627
- <Comp resourceName={renderedParams.resource} id={renderedParams.id} />
634
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="show" id={renderedParams.id}>
635
+ <Comp resourceName={renderedParams.resource} id={renderedParams.id} />
636
+ </ResourceAccessGuard>
628
637
  {/key}
629
638
  {:else if (renderedRoute === '/:resource/clone/:id' || renderedRoute === '/:parent/:parentId/:resource/clone/:id') && renderedHasRouteResource}
630
639
  {#key `${renderedParams.resource}-clone-${renderedParams.id}`}
631
640
  {@const Comp = renderedResourcePages?.clone ?? mergedComponents.AutoForm}
632
- <Comp resourceName={renderedParams.resource} mode="clone" id={renderedParams.id} />
641
+ <ResourceAccessGuard resourceName={renderedParams.resource} action="create" id={renderedParams.id} requireSourceRead>
642
+ <Comp resourceName={renderedParams.resource} mode="clone" id={renderedParams.id} />
643
+ </ResourceAccessGuard>
633
644
  {/key}
634
645
  {:else}
635
646
  {@const ErrorComp = mergedComponents.ErrorPage || ErrorPage}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext, useForm, getResource, deriveValidator } from '@svadmin/core';
2
+ import { useForm, getResource, deriveValidator, useNavigation } from '@svadmin/core';
3
3
  import { slide } from 'svelte/transition';
4
4
  import type { FieldDefinition } from '@svadmin/core';
5
5
  import { useTranslation } from '@svadmin/core/i18n';
@@ -45,7 +45,7 @@
45
45
  onSuccess,
46
46
  onNavigationGuardReady,
47
47
  }: Props = $props();
48
- const adminContext = captureAdminContext();
48
+ const navigation = useNavigation();
49
49
  const isReadonly = $derived(mode === 'show');
50
50
  const isCompact = $derived(density === 'compact');
51
51
 
@@ -196,7 +196,7 @@
196
196
  <div class="flex items-center gap-4">
197
197
  <TooltipButton
198
198
  tooltip={i18n.t('common.back')}
199
- onclick={() => guardNavigate(() => adminContext.navigate(`/${resourceName}`))}
199
+ onclick={() => guardNavigate(() => navigation.list(resourceName))}
200
200
  >
201
201
  <ArrowLeft class="h-5 w-5" />
202
202
  </TooltipButton>
@@ -311,7 +311,7 @@
311
311
  type="button"
312
312
  variant="outline"
313
313
  size={isCompact ? 'sm' : 'default'}
314
- onclick={() => guardNavigate(() => adminContext.navigate(`/${resourceName}`))}
314
+ onclick={() => guardNavigate(() => navigation.list(resourceName))}
315
315
  >
316
316
  {i18n.t('common.cancel')}
317
317
  </Button>