@svadmin/ui 0.22.0 → 0.23.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -308,9 +308,7 @@
308
308
  const ids = Object.keys(rowSelection);
309
309
  confirmMessage = t('common.batchDeleteConfirm', { count: ids.length });
310
310
  confirmAction = async () => {
311
- for (const id of ids) {
312
- await deleteMutation.mutateAsync({ id, resource: resourceName });
313
- }
311
+ await Promise.allSettled(ids.map(id => deleteMutation.mutateAsync({ id, resource: resourceName })));
314
312
  rowSelection = {};
315
313
  confirmOpen = false;
316
314
  };
@@ -78,7 +78,7 @@
78
78
  </script>
79
79
 
80
80
  <div class="space-y-1.5">
81
- <Label for={field.key}>
81
+ <Label for={field.key} id="label-{field.key}">
82
82
  {field.label}
83
83
  {#if field.required}
84
84
  <span class="text-destructive">*</span>
@@ -233,7 +233,11 @@
233
233
  {/if}
234
234
 
235
235
  {:else if field.type === 'multiselect'}
236
- <div class="space-y-2 rounded-lg border border-input p-3 max-h-48 overflow-y-auto">
236
+ <div
237
+ class="space-y-2 rounded-lg border border-input p-3 max-h-48 overflow-y-auto"
238
+ role="group"
239
+ aria-labelledby="label-{field.key}"
240
+ >
237
241
  {#each field.options ?? [] as opt}
238
242
  <label class="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 rounded px-1 py-0.5 transition-colors">
239
243
  <Checkbox
@@ -17,16 +17,23 @@
17
17
  ChevronLeft, ChevronDown, LogOut, Sun, Moon, Palette
18
18
  } from 'lucide-svelte';
19
19
 
20
- let { collapsed, identity, title, onToggle, onLogout, menu, legacyHashRouting = false }: {
20
+ let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
21
21
  collapsed: boolean;
22
22
  identity: Identity | null;
23
23
  title: string;
24
24
  onToggle: () => void;
25
25
  onLogout: () => void;
26
26
  menu?: MenuItem[];
27
- legacyHashRouting?: boolean;
27
+ /** 'hash' for SPA hash routing, 'path' for SvelteKit/filesystem routing, 'auto' to detect */
28
+ routeMode?: 'hash' | 'path' | 'auto';
28
29
  } = $props();
29
30
 
31
+ const effectiveRouteMode = $derived(
32
+ routeMode === 'auto'
33
+ ? (typeof window !== 'undefined' && window.location.hash.startsWith('#/') ? 'hash' : 'path')
34
+ : routeMode
35
+ );
36
+
30
37
  const resources = getResources();
31
38
 
32
39
  const iconMap: Record<string, typeof LayoutDashboard> = {
@@ -98,7 +105,7 @@
98
105
 
99
106
  function isActive(itemPath: string): boolean {
100
107
  if (itemPath === '/') return path === '/';
101
- return path.startsWith(itemPath);
108
+ return path === itemPath || path.startsWith(itemPath + "/");
102
109
  }
103
110
 
104
111
  /** Prefetch resource data on hover for instant navigation */
@@ -206,7 +213,7 @@
206
213
  {#each group.items as item}
207
214
  {@const active = isActive(item.path)}
208
215
  <a
209
- href={legacyHashRouting ? `#${item.path}` : item.path}
216
+ href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
210
217
  class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
211
218
  {active
212
219
  ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
@@ -229,7 +236,7 @@
229
236
  {#snippet child({ props }: { props: Record<string, unknown> })}
230
237
  <a
231
238
  {...props}
232
- href={legacyHashRouting ? `#${item.path}` : item.path}
239
+ href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
233
240
  class="flex items-center justify-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
234
241
  {active
235
242
  ? 'bg-sidebar-accent text-sidebar-accent-foreground'
@@ -245,7 +252,7 @@
245
252
  </Tooltip.Root>
246
253
  {:else}
247
254
  <a
248
- href={legacyHashRouting ? `#${item.path}` : item.path}
255
+ href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
249
256
  class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
250
257
  {active
251
258
  ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { cn, type WithElementRef } from "../../../utils.js";
3
3
  import type { HTMLAttributes } from "svelte/elements";
4
+ import { getContext } from "svelte";
4
5
 
5
6
  type Props = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
6
7
  align?: "start" | "center" | "end";
@@ -13,8 +14,11 @@
13
14
  children,
14
15
  ...restProps
15
16
  }: Props = $props();
17
+
18
+ let isOpen = $derived(getContext<() => boolean>("svadmin-dropdown-open")?.() ?? true);
16
19
  </script>
17
20
 
21
+ {#if isOpen}
18
22
  <div
19
23
  bind:this={ref}
20
24
  data-slot="dropdown-menu-content"
@@ -28,3 +32,4 @@
28
32
  >
29
33
  {@render children?.()}
30
34
  </div>
35
+ {/if}
@@ -16,6 +16,8 @@
16
16
  ...restProps
17
17
  }: Props = $props();
18
18
 
19
+ import { setContext } from "svelte";
20
+
19
21
  function handleClickOutside(e: MouseEvent) {
20
22
  if (ref && !ref.contains(e.target as Node)) {
21
23
  open = false;
@@ -23,6 +25,8 @@
23
25
  }
24
26
  }
25
27
 
28
+ setContext("svadmin-dropdown-open", () => open);
29
+
26
30
  $effect(() => {
27
31
  if (open) {
28
32
  document.addEventListener('click', handleClickOutside, true);