@classic-homes/theme-svelte 1.22.1 → 1.22.3
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/dist/lib/components/Combobox.svelte +2 -2
- package/dist/lib/components/DataTable.svelte +15 -6
- package/dist/lib/components/DataTable.svelte.d.ts +1 -1
- package/dist/lib/components/DateTimePicker.svelte +31 -34
- package/dist/lib/components/DropdownMenu.svelte +52 -30
- package/dist/lib/components/DropdownMenu.svelte.d.ts +5 -0
- package/dist/lib/components/FileUpload.svelte +5 -19
- package/dist/lib/components/MultiSelect.svelte +2 -2
- package/dist/lib/components/NumberInput.svelte +15 -2
- package/dist/lib/components/Select.svelte +19 -5
- package/dist/lib/components/Select.svelte.d.ts +4 -0
- package/dist/lib/components/Signature.svelte +15 -4
- package/dist/lib/components/Signature.svelte.d.ts +1 -1
- package/dist/lib/components/Slider.svelte +8 -6
- package/dist/lib/components/Tabs.svelte +10 -12
- package/dist/lib/components/Toast.svelte +12 -12
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +24 -26
- package/dist/lib/components/data-table/DataTableEditableCell.svelte +31 -18
- package/dist/lib/components/data-table/DataTableEditableCell.svelte.d.ts +0 -7
- package/dist/lib/components/data-table/DataTablePagination.svelte +5 -1
- package/dist/lib/components/data-table/DataTableVirtual.svelte +4 -7
- package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +3 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +6 -1
- package/dist/lib/components/data-table/createDataTable.svelte.js +22 -9
- package/dist/lib/components/layout/Sidebar.svelte +28 -0
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +66 -50
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +2 -0
- package/dist/lib/composables/useAsync.svelte.d.ts +1 -1
- package/dist/lib/composables/useAsync.svelte.js +33 -14
- package/dist/lib/composables/useForm.svelte.js +19 -7
- package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
- package/dist/lib/stores/sidebar.svelte.d.ts +6 -0
- package/dist/lib/stores/sidebar.svelte.js +15 -0
- package/dist/lib/stores/toast.svelte.js +12 -1
- package/dist/lib/utils/date.d.ts +3 -1
- package/dist/lib/utils/date.js +54 -34
- package/dist/lib/utils/form.d.ts +2 -1
- package/dist/lib/utils/form.js +3 -2
- package/package.json +1 -1
|
@@ -79,23 +79,21 @@
|
|
|
79
79
|
updateScrollShadows();
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
// Context for TabPanel children. Getters read the props directly, so the
|
|
83
|
+
// active panel is right on the first render and during SSR (an $effect
|
|
84
|
+
// would only fill it in after hydration).
|
|
85
|
+
setContext('tabs', {
|
|
86
|
+
get activeTab() {
|
|
87
|
+
return value;
|
|
88
|
+
},
|
|
89
|
+
get tabsId() {
|
|
90
|
+
return id;
|
|
91
|
+
},
|
|
90
92
|
});
|
|
91
93
|
|
|
92
|
-
// Set up context for TabPanel children (done once at component init)
|
|
93
|
-
setContext('tabs', tabsState);
|
|
94
|
-
|
|
95
94
|
function selectTab(tabId: string) {
|
|
96
95
|
if (tabs.find((t) => t.id === tabId && t.disabled)) return;
|
|
97
96
|
value = tabId;
|
|
98
|
-
tabsState.activeTab = tabId;
|
|
99
97
|
onValueChange?.(tabId);
|
|
100
98
|
}
|
|
101
99
|
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
8
8
|
|
|
9
|
+
// Status tokens (light/dark aware) over an opaque card surface: the tint is a
|
|
10
|
+
// ::before layer so the toast never shows the page through it, and text stays
|
|
11
|
+
// foreground-on-card for contrast in both modes.
|
|
9
12
|
const toastVariants = tv({
|
|
10
|
-
base:
|
|
13
|
+
base: "group pointer-events-auto relative isolate flex w-full items-start gap-3 overflow-hidden rounded-lg border bg-card p-4 text-card-foreground shadow-lg transition-all before:absolute before:inset-0 before:-z-10 before:content-['']",
|
|
11
14
|
variants: {
|
|
12
15
|
type: {
|
|
13
|
-
success:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
warning:
|
|
18
|
-
'bg-yellow-50 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800',
|
|
19
|
-
info: 'bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800',
|
|
16
|
+
success: 'border-success/40 before:bg-success/10',
|
|
17
|
+
error: 'border-destructive/40 before:bg-destructive/10',
|
|
18
|
+
warning: 'border-warning/40 before:bg-warning/10',
|
|
19
|
+
info: 'border-info/40 before:bg-info/10',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
defaultVariants: {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const iconColors: Record<ToastType, string> = {
|
|
39
|
-
success: 'text-
|
|
40
|
-
error: 'text-
|
|
41
|
-
warning: 'text-
|
|
42
|
-
info: 'text-
|
|
39
|
+
success: 'text-success',
|
|
40
|
+
error: 'text-destructive',
|
|
41
|
+
warning: 'text-warning',
|
|
42
|
+
info: 'text-info',
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
interface Props {
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
9
9
|
import DropdownMenu from '../DropdownMenu.svelte';
|
|
10
|
-
import Button from '../Button.svelte';
|
|
11
10
|
import { cn } from '../../utils.js';
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
@@ -19,13 +18,13 @@
|
|
|
19
18
|
|
|
20
19
|
let { table, class: className }: Props = $props();
|
|
21
20
|
|
|
22
|
-
//
|
|
21
|
+
// One checkbox item per column, checked while the column is visible
|
|
23
22
|
const menuItems = $derived.by(() => {
|
|
24
23
|
return table.allColumns.map((column) => {
|
|
25
|
-
const isVisible = !table.hiddenColumns.has(column.id);
|
|
26
24
|
return {
|
|
27
25
|
type: 'item' as const,
|
|
28
|
-
label:
|
|
26
|
+
label: column.header,
|
|
27
|
+
checked: !table.hiddenColumns.has(column.id),
|
|
29
28
|
onSelect: () => {
|
|
30
29
|
table.toggleColumnVisibility(column.id);
|
|
31
30
|
},
|
|
@@ -35,32 +34,31 @@
|
|
|
35
34
|
</script>
|
|
36
35
|
|
|
37
36
|
<div class={cn('relative', className)}>
|
|
37
|
+
<!-- DropdownMenu renders the trigger <button>; style it via triggerVariant
|
|
38
|
+
rather than nesting a Button inside it. -->
|
|
38
39
|
<DropdownMenu
|
|
39
40
|
items={[{ type: 'label', label: 'Toggle columns' }, { type: 'separator' }, ...menuItems]}
|
|
41
|
+
triggerVariant="outline"
|
|
42
|
+
triggerSize="sm"
|
|
43
|
+
triggerClass="ml-auto"
|
|
40
44
|
>
|
|
41
45
|
{#snippet trigger()}
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
stroke-linejoin="round"
|
|
59
|
-
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
60
|
-
/>
|
|
61
|
-
</svg>
|
|
62
|
-
Columns
|
|
63
|
-
</Button>
|
|
46
|
+
<svg
|
|
47
|
+
class="mr-2 h-4 w-4"
|
|
48
|
+
fill="none"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
stroke-width="2"
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
>
|
|
54
|
+
<path
|
|
55
|
+
stroke-linecap="round"
|
|
56
|
+
stroke-linejoin="round"
|
|
57
|
+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
58
|
+
/>
|
|
59
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
60
|
+
</svg>
|
|
61
|
+
Columns
|
|
64
62
|
{/snippet}
|
|
65
63
|
</DropdownMenu>
|
|
66
64
|
</div>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Supports text, number, and select inputs.
|
|
7
7
|
* Works with the native Svelte 5 DataTable implementation.
|
|
8
8
|
*/
|
|
9
|
+
import { tick } from 'svelte';
|
|
9
10
|
import type { DataTableColumn } from './types.js';
|
|
10
11
|
import type { DataTableRow } from './createDataTable.svelte.js';
|
|
11
12
|
import { cn } from '../../utils.js';
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
|
|
30
31
|
let isEditing = $state(false);
|
|
31
32
|
let editValue = $state<string>('');
|
|
33
|
+
let buttonEl = $state<HTMLButtonElement | null>(null);
|
|
32
34
|
|
|
33
35
|
// Get the current display value
|
|
34
36
|
const displayValue = $derived.by(() => {
|
|
@@ -70,13 +72,21 @@
|
|
|
70
72
|
editValue = '';
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
// After a keyboard save/cancel the input is gone; return focus to the cell.
|
|
76
|
+
async function refocusCell() {
|
|
77
|
+
await tick();
|
|
78
|
+
buttonEl?.focus();
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
function handleKeydown(e: KeyboardEvent) {
|
|
74
82
|
if (e.key === 'Enter') {
|
|
75
83
|
e.preventDefault();
|
|
76
84
|
saveEdit();
|
|
85
|
+
refocusCell();
|
|
77
86
|
} else if (e.key === 'Escape') {
|
|
78
87
|
e.preventDefault();
|
|
79
88
|
cancelEdit();
|
|
89
|
+
refocusCell();
|
|
80
90
|
} else if (e.key === 'Tab') {
|
|
81
91
|
saveEdit();
|
|
82
92
|
}
|
|
@@ -91,29 +101,32 @@
|
|
|
91
101
|
}
|
|
92
102
|
</script>
|
|
93
103
|
|
|
94
|
-
{#if isEditable}
|
|
104
|
+
{#if isEditable && isEditing}
|
|
105
|
+
<!-- The input sits beside, not inside, the button: an input in a button is
|
|
106
|
+
invalid, and its Enter would bubble to the button and reopen the editor. -->
|
|
107
|
+
<div class={cn('relative w-full', className)}>
|
|
108
|
+
<input
|
|
109
|
+
use:autofocus
|
|
110
|
+
type="text"
|
|
111
|
+
value={editValue}
|
|
112
|
+
oninput={(e: Event) => (editValue = (e.target as HTMLInputElement).value)}
|
|
113
|
+
onkeydown={handleKeydown}
|
|
114
|
+
onblur={handleBlur}
|
|
115
|
+
class="h-7 w-full rounded border border-input bg-background px-2 text-base sm:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
{:else if isEditable}
|
|
95
119
|
<button
|
|
120
|
+
bind:this={buttonEl}
|
|
96
121
|
type="button"
|
|
97
122
|
class={cn('relative w-full text-left hover:bg-muted-hover', className)}
|
|
98
123
|
ondblclick={handleDoubleClick}
|
|
99
|
-
onkeydown={(e: KeyboardEvent) =>
|
|
124
|
+
onkeydown={(e: KeyboardEvent) => e.key === 'Enter' && startEditing()}
|
|
100
125
|
>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
value={editValue}
|
|
106
|
-
oninput={(e: Event) => (editValue = (e.target as HTMLInputElement).value)}
|
|
107
|
-
onkeydown={handleKeydown}
|
|
108
|
-
onblur={handleBlur}
|
|
109
|
-
class="h-7 w-full rounded border border-input bg-background px-2 text-base sm:text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
110
|
-
/>
|
|
111
|
-
{:else}
|
|
112
|
-
<span class="border-b border-dashed border-transparent hover:border-primary">
|
|
113
|
-
{displayValue}
|
|
114
|
-
</span>
|
|
115
|
-
<span class="sr-only">(double-click to edit)</span>
|
|
116
|
-
{/if}
|
|
126
|
+
<span class="border-b border-dashed border-transparent hover:border-primary">
|
|
127
|
+
{displayValue}
|
|
128
|
+
</span>
|
|
129
|
+
<span class="sr-only">(double-click to edit)</span>
|
|
117
130
|
</button>
|
|
118
131
|
{:else}
|
|
119
132
|
<span class={cn('relative', className)}>
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DataTableEditableCell - Inline editable cell component
|
|
3
|
-
*
|
|
4
|
-
* Allows users to edit cell values in place.
|
|
5
|
-
* Supports text, number, and select inputs.
|
|
6
|
-
* Works with the native Svelte 5 DataTable implementation.
|
|
7
|
-
*/
|
|
8
1
|
import type { DataTableColumn } from './types.js';
|
|
9
2
|
import type { DataTableRow } from './createDataTable.svelte.js';
|
|
10
3
|
declare function $$render<T extends object>(): {
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
class: className,
|
|
29
29
|
}: Props = $props();
|
|
30
30
|
|
|
31
|
+
const uid = $props.id();
|
|
32
|
+
const pageSizeLabelId = `datatable-page-size-${uid}`;
|
|
33
|
+
|
|
31
34
|
function handlePageSizeChange(value: string) {
|
|
32
35
|
table.setPageSize(Number(value));
|
|
33
36
|
}
|
|
@@ -45,8 +48,9 @@
|
|
|
45
48
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 sm:gap-x-6 lg:gap-x-8">
|
|
46
49
|
<!-- Page size selector -->
|
|
47
50
|
<div class="flex items-center gap-2">
|
|
48
|
-
<span class="text-sm text-muted-foreground">Rows per page</span>
|
|
51
|
+
<span id={pageSizeLabelId} class="text-sm text-muted-foreground">Rows per page</span>
|
|
49
52
|
<Select
|
|
53
|
+
aria-labelledby={pageSizeLabelId}
|
|
50
54
|
value={String(table.pageSize)}
|
|
51
55
|
options={pageSizeOptions.map((size) => ({
|
|
52
56
|
value: String(size),
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* ```svelte
|
|
15
15
|
* <DataTableVirtual {table} height="600px" rowHeight={48} />
|
|
16
16
|
* ```
|
|
17
|
+
*
|
|
18
|
+
* Create the table with `enablePagination: false` so `table.rows` holds every
|
|
19
|
+
* row rather than one page.
|
|
17
20
|
*/
|
|
18
21
|
import type { DataTableInstance, DataTableRow } from './createDataTable.svelte.js';
|
|
19
22
|
import type { DataTableColumn } from './types.js';
|
|
@@ -96,12 +99,6 @@
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
// Handle row selection
|
|
100
|
-
function handleSelectRow(rowId: string, e: MouseEvent) {
|
|
101
|
-
e.stopPropagation();
|
|
102
|
-
table.toggleRowSelection(rowId);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
102
|
// Handle header click for sorting
|
|
106
103
|
function handleHeaderClick(columnId: string, sortable: boolean | undefined) {
|
|
107
104
|
if (!sortable) return;
|
|
@@ -184,7 +181,7 @@
|
|
|
184
181
|
checked={row.isSelected}
|
|
185
182
|
onCheckedChange={() => table.toggleRowSelection(row.id)}
|
|
186
183
|
aria-label={`Select row ${index + 1}`}
|
|
187
|
-
onclick={(e: MouseEvent) =>
|
|
184
|
+
onclick={(e: MouseEvent) => e.stopPropagation()}
|
|
188
185
|
/>
|
|
189
186
|
</div>
|
|
190
187
|
{/if}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* ```svelte
|
|
14
14
|
* <DataTableVirtual {table} height="600px" rowHeight={48} />
|
|
15
15
|
* ```
|
|
16
|
+
*
|
|
17
|
+
* Create the table with `enablePagination: false` so `table.rows` holds every
|
|
18
|
+
* row rather than one page.
|
|
16
19
|
*/
|
|
17
20
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
18
21
|
declare function $$render<T extends object>(): {
|
|
@@ -32,7 +32,7 @@ export interface DataTableOptions<T> {
|
|
|
32
32
|
data: T[];
|
|
33
33
|
/** Column definitions */
|
|
34
34
|
columns: DataTableColumn<T>[];
|
|
35
|
-
/** Get unique row ID (defaults to index) */
|
|
35
|
+
/** Get unique row ID (defaults to the row's index in `data`) */
|
|
36
36
|
getRowId?: (row: T, index: number) => string;
|
|
37
37
|
/** Initial sort column ID */
|
|
38
38
|
initialSortColumn?: string;
|
|
@@ -54,6 +54,11 @@ export interface DataTableOptions<T> {
|
|
|
54
54
|
manualFiltering?: boolean;
|
|
55
55
|
/** Use server-side pagination (skip client-side pagination) */
|
|
56
56
|
manualPagination?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Slice rows into pages (default true). Set false to get every row in
|
|
59
|
+
* `rows`, e.g. for a DataTable without a pager or for DataTableVirtual.
|
|
60
|
+
*/
|
|
61
|
+
enablePagination?: boolean;
|
|
57
62
|
/** Total page count (required for manual pagination) */
|
|
58
63
|
pageCount?: number;
|
|
59
64
|
/** Callback when sort changes */
|
|
@@ -162,16 +162,26 @@ export function createDataTable(optionsFn) {
|
|
|
162
162
|
: Math.ceil(totalRows / pageSize) || 1);
|
|
163
163
|
// Clamp pageIndex when data changes
|
|
164
164
|
const clampedPageIndex = $derived(Math.min(Math.max(0, pageIndex), Math.max(0, totalPages - 1)));
|
|
165
|
-
const paginatedData = $derived(options.manualPagination
|
|
165
|
+
const paginatedData = $derived(options.manualPagination || options.enablePagination === false
|
|
166
166
|
? sortedData // In manual mode, data is already paginated from server
|
|
167
167
|
: sortedData.slice(clampedPageIndex * pageSize, (clampedPageIndex + 1) * pageSize));
|
|
168
168
|
// === ROW HELPERS ===
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
// Index of each row in the source data, so row ids (and with them the
|
|
170
|
+
// selection) follow the row rather than its position after sort/filter/page.
|
|
171
|
+
const sourceIndex = $derived.by(() => {
|
|
172
|
+
const map = new Map();
|
|
173
|
+
data.forEach((row, i) => {
|
|
174
|
+
if (!map.has(row))
|
|
175
|
+
map.set(row, i);
|
|
176
|
+
});
|
|
177
|
+
return map;
|
|
178
|
+
});
|
|
179
|
+
const rows = $derived(paginatedData.map((row) => {
|
|
180
|
+
const index = sourceIndex.get(row) ?? -1;
|
|
181
|
+
const id = getRowId(row, index);
|
|
172
182
|
return {
|
|
173
183
|
id,
|
|
174
|
-
index
|
|
184
|
+
index,
|
|
175
185
|
original: row,
|
|
176
186
|
isSelected: selectedRows.has(id),
|
|
177
187
|
};
|
|
@@ -248,14 +258,17 @@ export function createDataTable(optionsFn) {
|
|
|
248
258
|
}
|
|
249
259
|
function nextPage() {
|
|
250
260
|
if (clampedPageIndex < totalPages - 1) {
|
|
251
|
-
|
|
252
|
-
|
|
261
|
+
// Read before assigning: clampedPageIndex recomputes from pageIndex.
|
|
262
|
+
const next = clampedPageIndex + 1;
|
|
263
|
+
pageIndex = next;
|
|
264
|
+
options.onPaginationChange?.({ pageIndex: next, pageSize });
|
|
253
265
|
}
|
|
254
266
|
}
|
|
255
267
|
function previousPage() {
|
|
256
268
|
if (clampedPageIndex > 0) {
|
|
257
|
-
|
|
258
|
-
|
|
269
|
+
const previous = clampedPageIndex - 1;
|
|
270
|
+
pageIndex = previous;
|
|
271
|
+
options.onPaginationChange?.({ pageIndex: previous, pageSize });
|
|
259
272
|
}
|
|
260
273
|
}
|
|
261
274
|
function firstPage() {
|
|
@@ -323,6 +323,34 @@
|
|
|
323
323
|
sidebarStore.setDefaultItems(defaultExpanded);
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
+
// Keep the active item visible: open the section and parent items holding
|
|
327
|
+
// it when it changes. Declared after the defaults effects so it runs after
|
|
328
|
+
// them: while nothing is saved, every new navigation re-applies the
|
|
329
|
+
// defaults, which would close it again. Once the user has toggled (saved),
|
|
330
|
+
// a section they closed stays closed until the active item moves.
|
|
331
|
+
let revealedActiveId: string | null = null;
|
|
332
|
+
$effect(() => {
|
|
333
|
+
type ActivePath = { id: string; parents: string[] };
|
|
334
|
+
const findActive = (items: NavItem[], parents: string[]): ActivePath | null => {
|
|
335
|
+
for (const item of items) {
|
|
336
|
+
if (item.active) return { id: item.id, parents };
|
|
337
|
+
const found = item.children && findActive(item.children, [...parents, item.id]);
|
|
338
|
+
if (found) return found;
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
};
|
|
342
|
+
for (const section of roleFilteredNavigation) {
|
|
343
|
+
const active = findActive(section.items, []);
|
|
344
|
+
if (!active) continue;
|
|
345
|
+
if (active.id !== revealedActiveId || !sidebarStore.hasPersistedExpansionState) {
|
|
346
|
+
sidebarStore.revealExpansion([section.id], active.parents);
|
|
347
|
+
}
|
|
348
|
+
revealedActiveId = active.id;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
revealedActiveId = null;
|
|
352
|
+
});
|
|
353
|
+
|
|
326
354
|
// Flyout state for collapsed sidebar
|
|
327
355
|
let hoveredFlyoutItem = $state<NavItem | null>(null);
|
|
328
356
|
let flyoutTop = $state(0);
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
autoCollapse?: boolean;
|
|
46
46
|
/** IDs of expandable items alongside this one */
|
|
47
47
|
siblingIds?: string[];
|
|
48
|
+
/** Rendered inside another item's submenu panel (internal) */
|
|
49
|
+
nested?: boolean;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
let {
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
onMouseLeave,
|
|
63
65
|
autoCollapse = false,
|
|
64
66
|
siblingIds = [],
|
|
67
|
+
nested = false,
|
|
65
68
|
}: Props = $props();
|
|
66
69
|
|
|
67
70
|
// Highlight matching text in item names
|
|
@@ -99,11 +102,11 @@
|
|
|
99
102
|
return cn(
|
|
100
103
|
isLight &&
|
|
101
104
|
!active &&
|
|
102
|
-
'text-foreground bg-
|
|
105
|
+
'text-foreground bg-transparent hover:bg-accent hover:text-accent-foreground',
|
|
103
106
|
isLight && active && 'bg-primary/10 text-primary',
|
|
104
107
|
!isLight &&
|
|
105
108
|
!active &&
|
|
106
|
-
'text-sidebar-foreground bg-
|
|
109
|
+
'text-sidebar-foreground bg-transparent hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
107
110
|
!isLight && active && 'bg-sidebar-primary text-sidebar-primary-foreground',
|
|
108
111
|
disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
|
|
109
112
|
);
|
|
@@ -266,9 +269,12 @@
|
|
|
266
269
|
<div
|
|
267
270
|
class={cn(
|
|
268
271
|
'group flex w-full items-center rounded-md',
|
|
269
|
-
|
|
272
|
+
// Inside a submenu panel rows sit on the panel's grey, so no rest fill
|
|
273
|
+
isLight && !item.active && (nested ? 'bg-transparent' : 'bg-background'),
|
|
274
|
+
isLight && !item.active && 'hover:bg-accent',
|
|
270
275
|
isLight && item.active && 'bg-primary/10',
|
|
271
|
-
!isLight && !item.active && 'bg-
|
|
276
|
+
!isLight && !item.active && (nested ? 'bg-transparent' : 'bg-sidebar'),
|
|
277
|
+
!isLight && !item.active && 'hover:bg-sidebar-accent',
|
|
272
278
|
!isLight && item.active && 'bg-sidebar-primary'
|
|
273
279
|
)}
|
|
274
280
|
>
|
|
@@ -372,54 +378,64 @@
|
|
|
372
378
|
</div>
|
|
373
379
|
|
|
374
380
|
{#if isExpanded}
|
|
375
|
-
|
|
376
|
-
|
|
381
|
+
<!-- Submenu panel: an inset grey surface at the top level so open children
|
|
382
|
+
read as separate from the main list. Deeper levels get a left rail
|
|
383
|
+
instead, so the grey never stacks darker -->
|
|
384
|
+
<div
|
|
385
|
+
class={cn(
|
|
386
|
+
'mt-1 overflow-hidden',
|
|
387
|
+
!nested && 'ml-3 rounded-md p-1',
|
|
388
|
+
!nested && (isLight ? 'bg-muted/50' : 'bg-sidebar-accent/30')
|
|
389
|
+
)}
|
|
377
390
|
transition:slide={{ duration: sidebarStore.animate ? 200 : 0 }}
|
|
378
391
|
>
|
|
379
|
-
{
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
392
|
+
<ul class={cn('space-y-1', nested && 'ml-3 border-l border-outline/40 pl-1')}>
|
|
393
|
+
{#each item.children ?? [] as child (child.id)}
|
|
394
|
+
<li>
|
|
395
|
+
{#if child.children?.length}
|
|
396
|
+
<!-- A nested category: same expandable row, one level deeper -->
|
|
397
|
+
<SidebarNavItem
|
|
398
|
+
item={child}
|
|
399
|
+
{isLight}
|
|
400
|
+
{isMobile}
|
|
401
|
+
{icon}
|
|
402
|
+
{searchQuery}
|
|
403
|
+
{onNavigate}
|
|
404
|
+
{autoCollapse}
|
|
405
|
+
siblingIds={expandableChildIds}
|
|
406
|
+
nested
|
|
407
|
+
/>
|
|
408
|
+
{:else}
|
|
409
|
+
<a
|
|
410
|
+
href={child.disabled ? undefined : child.href}
|
|
411
|
+
class={cn(
|
|
412
|
+
'flex min-w-0 items-center gap-3 rounded-md px-3 py-2 text-sm font-medium',
|
|
413
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
|
414
|
+
getItemStateClasses(child.active, child.disabled)
|
|
415
|
+
)}
|
|
416
|
+
target={child.external ? '_blank' : undefined}
|
|
417
|
+
rel={child.external ? 'noopener noreferrer' : undefined}
|
|
418
|
+
aria-current={child.active ? 'page' : undefined}
|
|
419
|
+
aria-disabled={child.disabled || undefined}
|
|
420
|
+
tabindex={child.disabled ? -1 : undefined}
|
|
421
|
+
onclick={(e) => {
|
|
422
|
+
if (child.disabled) {
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
onNavigate?.(child);
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
{@render navIcon(child)}
|
|
430
|
+
{@render highlightedName(child.name, highlightText(child.name, searchQuery))}
|
|
431
|
+
{@render navBadge(child.badge)}
|
|
432
|
+
{#if child.external}{@render externalIndicator()}{/if}
|
|
433
|
+
</a>
|
|
434
|
+
{/if}
|
|
435
|
+
</li>
|
|
436
|
+
{/each}
|
|
437
|
+
</ul>
|
|
438
|
+
</div>
|
|
423
439
|
{/if}
|
|
424
440
|
</div>
|
|
425
441
|
{:else}
|
|
@@ -32,6 +32,8 @@ interface Props {
|
|
|
32
32
|
autoCollapse?: boolean;
|
|
33
33
|
/** IDs of expandable items alongside this one */
|
|
34
34
|
siblingIds?: string[];
|
|
35
|
+
/** Rendered inside another item's submenu panel (internal) */
|
|
36
|
+
nested?: boolean;
|
|
35
37
|
}
|
|
36
38
|
declare const SidebarNavItem: import("svelte").Component<Props, {}, "">;
|
|
37
39
|
type SidebarNavItem = ReturnType<typeof SidebarNavItem>;
|