@marianmeres/stuic 3.66.1 → 3.68.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/dist/actions/autoscroll.d.ts +7 -0
- package/dist/actions/autoscroll.js +7 -0
- package/dist/actions/focus-trap.d.ts +7 -0
- package/dist/actions/focus-trap.js +8 -3
- package/dist/actions/typeahead.svelte.js +40 -4
- package/dist/components/Carousel/Carousel.svelte +9 -2
- package/dist/components/Carousel/README.md +8 -2
- package/dist/components/Cart/Cart.svelte +3 -0
- package/dist/components/Cart/README.md +18 -1
- package/dist/components/Checkout/CheckoutOrderReview.svelte +4 -14
- package/dist/components/Checkout/README.md +184 -0
- package/dist/components/Checkout/_internal/checkout-utils.d.ts +6 -0
- package/dist/components/Checkout/_internal/checkout-utils.js +24 -0
- package/dist/components/Checkout/index.d.ts +1 -1
- package/dist/components/Checkout/index.js +1 -1
- package/dist/components/CommandMenu/CommandMenu.svelte +23 -7
- package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +2 -0
- package/dist/components/CronInput/CronInput.svelte +44 -9
- package/dist/components/CronInput/CronInput.svelte.d.ts +2 -0
- package/dist/components/CronInput/README.md +145 -0
- package/dist/components/CronInput/cron-next-run.svelte.d.ts +11 -0
- package/dist/components/CronInput/cron-next-run.svelte.js +11 -0
- package/dist/components/CronInput/index.css +0 -8
- package/dist/components/DataTable/DataTable.svelte +276 -83
- package/dist/components/DataTable/DataTable.svelte.d.ts +58 -6
- package/dist/components/DataTable/README.md +155 -25
- package/dist/components/DataTable/index.css +31 -0
- package/dist/components/DropdownMenu/DropdownMenu.svelte +43 -26
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +5 -1
- package/dist/components/DropdownMenu/README.md +37 -9
- package/dist/components/Input/FieldAssets.svelte +9 -7
- package/dist/components/Input/FieldAssets.svelte.d.ts +3 -7
- package/dist/components/Input/FieldFile.svelte +13 -7
- package/dist/components/Input/FieldFile.svelte.d.ts +4 -7
- package/dist/components/Input/FieldInput.svelte +10 -8
- package/dist/components/Input/FieldInput.svelte.d.ts +3 -8
- package/dist/components/Input/FieldInputLocalized.svelte +8 -7
- package/dist/components/Input/FieldInputLocalized.svelte.d.ts +2 -7
- package/dist/components/Input/FieldKeyValues.svelte +8 -7
- package/dist/components/Input/FieldKeyValues.svelte.d.ts +2 -7
- package/dist/components/Input/FieldLikeButton.svelte +9 -7
- package/dist/components/Input/FieldLikeButton.svelte.d.ts +3 -7
- package/dist/components/Input/FieldObject.svelte +8 -7
- package/dist/components/Input/FieldObject.svelte.d.ts +2 -7
- package/dist/components/Input/FieldOptions.svelte +9 -7
- package/dist/components/Input/FieldOptions.svelte.d.ts +3 -7
- package/dist/components/Input/FieldPhoneNumber.svelte +7 -8
- package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +3 -8
- package/dist/components/Input/FieldSelect.svelte +9 -8
- package/dist/components/Input/FieldSelect.svelte.d.ts +3 -8
- package/dist/components/Input/FieldSwitch.svelte +9 -7
- package/dist/components/Input/FieldSwitch.svelte.d.ts +3 -7
- package/dist/components/Input/FieldTextarea.svelte +7 -8
- package/dist/components/Input/FieldTextarea.svelte.d.ts +3 -8
- package/dist/components/Input/README.md +20 -0
- package/dist/components/Input/_internal/InputWrap.svelte +2 -10
- package/dist/components/Input/_internal/InputWrap.svelte.d.ts +2 -10
- package/dist/components/Input/types.d.ts +28 -0
- package/dist/components/Nav/Nav.svelte +5 -4
- package/dist/components/Nav/Nav.svelte.d.ts +2 -2
- package/dist/components/Nav/README.md +2 -2
- package/dist/components/Nav/index.css +4 -0
- package/dist/components/Tree/README.md +189 -0
- package/dist/components/Tree/Tree.svelte +46 -2
- package/dist/components/Tree/Tree.svelte.d.ts +5 -0
- package/dist/utils/input-history.svelte.d.ts +12 -0
- package/dist/utils/input-history.svelte.js +12 -0
- package/dist/utils/observe-exists.svelte.d.ts +1 -0
- package/dist/utils/observe-exists.svelte.js +11 -3
- package/dist/utils/switch.svelte.d.ts +12 -0
- package/dist/utils/switch.svelte.js +12 -1
- package/docs/architecture.md +0 -1
- package/docs/testing.md +72 -0
- package/docs/upgrading.md +281 -0
- package/package.json +12 -13
|
@@ -28,14 +28,21 @@ loading state, and mobile card layout.
|
|
|
28
28
|
|
|
29
29
|
### With Paging
|
|
30
30
|
|
|
31
|
+
DataTable integrates with [`@marianmeres/paging-store`](https://github.com/marianmeres/paging-store). Pass its computed result and receive offsets back via `onPageChange`:
|
|
32
|
+
|
|
31
33
|
```svelte
|
|
34
|
+
<script lang="ts">
|
|
35
|
+
import { createPagingStore } from "@marianmeres/paging-store";
|
|
36
|
+
|
|
37
|
+
const paging = createPagingStore({ pageSize: 20, totalItems: 100 });
|
|
38
|
+
$: pagingResult = $paging; // or via $derived in Svelte 5 rune mode
|
|
39
|
+
</script>
|
|
40
|
+
|
|
32
41
|
<DataTable
|
|
33
42
|
{columns}
|
|
34
43
|
{data}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
totalItems={100}
|
|
38
|
-
onPageChange={(p) => fetchPage(p)}
|
|
44
|
+
paging={pagingResult}
|
|
45
|
+
onPageChange={(offset) => paging.setOffset(offset)}
|
|
39
46
|
/>
|
|
40
47
|
```
|
|
41
48
|
|
|
@@ -55,13 +62,83 @@ loading state, and mobile card layout.
|
|
|
55
62
|
</DataTable>
|
|
56
63
|
```
|
|
57
64
|
|
|
65
|
+
Shift+click a checkbox to toggle a range of rows from the last clicked checkbox to the current one. Disabled rows in the range are skipped. The anchor resets when `data` changes (e.g. on page navigation).
|
|
66
|
+
|
|
67
|
+
### Select All Across Pages
|
|
68
|
+
|
|
69
|
+
When your data is paged, DataTable can offer a "select all results" affordance that expands selection beyond the current page. Enable it with `allowSelectAllPages`. When the user opts in, selection flips to **all-pages mode**: every row is implicitly selected, and `excluded` holds IDs the user has explicitly deselected.
|
|
70
|
+
|
|
71
|
+
```svelte
|
|
72
|
+
<script lang="ts">
|
|
73
|
+
let selected = $state(new Set<string | number>());
|
|
74
|
+
let selectedAll = $state(false);
|
|
75
|
+
let excluded = $state(new Set<string | number>());
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<DataTable
|
|
79
|
+
{columns}
|
|
80
|
+
{data}
|
|
81
|
+
{paging}
|
|
82
|
+
{onPageChange}
|
|
83
|
+
selectable
|
|
84
|
+
allowSelectAllPages
|
|
85
|
+
bind:selected
|
|
86
|
+
bind:selectedAll
|
|
87
|
+
bind:excluded
|
|
88
|
+
getRowId={(row) => row.id}
|
|
89
|
+
>
|
|
90
|
+
{#snippet batchActions({ selectedAll, excluded, effectiveCount, totalCount, clearSelection })}
|
|
91
|
+
<span>{effectiveCount} selected{selectedAll ? ` of ${totalCount}` : ""}</span>
|
|
92
|
+
<Button onclick={() => deleteSelection({ selectedAll, excluded })}>Delete</Button>
|
|
93
|
+
<Button variant="ghost" onclick={clearSelection}>Clear</Button>
|
|
94
|
+
{/snippet}
|
|
95
|
+
</DataTable>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Important — executing batch operations:** in all-pages mode the off-page rows are not loaded locally, so consumers cannot iterate IDs. Execute operations server-side using the active filter minus `excluded`:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
function deleteSelection({ selectedAll, excluded }) {
|
|
102
|
+
if (selectedAll) {
|
|
103
|
+
// Server-side: DELETE FROM rows WHERE <currentFilter> AND id NOT IN excluded
|
|
104
|
+
return api.delete({ filter: currentFilter, exclude: [...excluded] });
|
|
105
|
+
}
|
|
106
|
+
return api.delete({ ids: [...selected] });
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
New records inserted while all-pages mode is active are implicitly selected (they aren't in `excluded`). This matches the conventional intent — "delete everything matching X" should include matches that arrive before the operation runs. If you need snapshot-at-click semantics, capture a timestamp or ID list in your consumer.
|
|
111
|
+
|
|
112
|
+
**Customising the banner:** the default banner uses the built-in `t()` keys `select_all_on_page_x`, `select_all_results`, `all_results_selected`, and `clear_selection`. Override markup entirely with the `selectAllBanner` snippet:
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<DataTable {columns} {data} {paging} selectable allowSelectAllPages bind:selected bind:selectedAll bind:excluded>
|
|
116
|
+
{#snippet selectAllBanner({ selectedAll, totalCount, selectAll, clearSelection })}
|
|
117
|
+
<div class="my-banner">
|
|
118
|
+
{#if selectedAll}
|
|
119
|
+
<span>All {totalCount} selected.</span>
|
|
120
|
+
<button onclick={clearSelection}>Undo</button>
|
|
121
|
+
{:else}
|
|
122
|
+
<button onclick={selectAll}>Select all {totalCount}</button>
|
|
123
|
+
{/if}
|
|
124
|
+
</div>
|
|
125
|
+
{/snippet}
|
|
126
|
+
</DataTable>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Filter changes:** when filters change in the consumer, reset the bound selection stores (`selected`, `selectedAll`, `excluded`) explicitly — DataTable doesn't track which filter produced the current state.
|
|
130
|
+
|
|
58
131
|
### Custom Cell Rendering
|
|
59
132
|
|
|
133
|
+
The `cell` snippet is used for both desktop and mobile layouts. Use the `variant` param if rendering differs per layout.
|
|
134
|
+
|
|
60
135
|
```svelte
|
|
61
136
|
<DataTable {columns} {data}>
|
|
62
|
-
{#snippet cell({ column, row, value })}
|
|
137
|
+
{#snippet cell({ column, row, value, rowIndex, variant })}
|
|
63
138
|
{#if column.key === "status"}
|
|
64
139
|
<span class="badge">{value}</span>
|
|
140
|
+
{:else if variant === "mobile"}
|
|
141
|
+
<em>{value}</em>
|
|
65
142
|
{:else}
|
|
66
143
|
{value}
|
|
67
144
|
{/if}
|
|
@@ -69,6 +146,34 @@ loading state, and mobile card layout.
|
|
|
69
146
|
</DataTable>
|
|
70
147
|
```
|
|
71
148
|
|
|
149
|
+
### Custom Desktop Row
|
|
150
|
+
|
|
151
|
+
Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable does not render the default row markup (checkbox, cells) — you own it all. Use for custom expandable rows, row grouping, etc.
|
|
152
|
+
|
|
153
|
+
```svelte
|
|
154
|
+
<DataTable {columns} {data}>
|
|
155
|
+
{#snippet row({ row, columns, rowIndex, isSelected })}
|
|
156
|
+
<tr data-custom-row>
|
|
157
|
+
{#each columns as col}
|
|
158
|
+
<td>{row[col.key]}</td>
|
|
159
|
+
{/each}
|
|
160
|
+
</tr>
|
|
161
|
+
{/snippet}
|
|
162
|
+
</DataTable>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Disabling Selection Per Row
|
|
166
|
+
|
|
167
|
+
```svelte
|
|
168
|
+
<DataTable
|
|
169
|
+
{columns}
|
|
170
|
+
{data}
|
|
171
|
+
selectable
|
|
172
|
+
bind:selected
|
|
173
|
+
selectDisabledBy={(row) => row.locked === true}
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
72
177
|
### Custom Mobile Layout
|
|
73
178
|
|
|
74
179
|
```svelte
|
|
@@ -84,26 +189,48 @@ loading state, and mobile card layout.
|
|
|
84
189
|
|
|
85
190
|
## Props
|
|
86
191
|
|
|
87
|
-
| Prop
|
|
88
|
-
|
|
|
89
|
-
| `columns`
|
|
90
|
-
| `data`
|
|
91
|
-
| `getRowId`
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
192
|
+
| Prop | Type | Default | Description |
|
|
193
|
+
| ------------------- | ------------------------------------------ | ------------- | ----------------------------------------------------------------- |
|
|
194
|
+
| `columns` | `DataTableColumn<T>[]` | required | Column definitions |
|
|
195
|
+
| `data` | `T[]` | required | Array of row data |
|
|
196
|
+
| `getRowId` | `(row, index) => string \| number` | `(_, i) => i` | Row ID extractor |
|
|
197
|
+
| `paging` | `PagingCalcResult` | - | Paging state (from `@marianmeres/paging-store`) |
|
|
198
|
+
| `onPageChange` | `(offset: number) => void` | - | Called with the new offset when the user navigates pages |
|
|
199
|
+
| `selectable` | `boolean` | `false` | Enable selection checkboxes |
|
|
200
|
+
| `selected` | `Set<string \| number>` | `new Set()` | Selected row IDs (bindable) |
|
|
201
|
+
| `selectOnRowClick` | `boolean` | `false` | Clicking anywhere on a row toggles its selection |
|
|
202
|
+
| `selectDisabledBy` | `(row, index) => boolean` | - | Return `true` to disable selection for a specific row |
|
|
203
|
+
| `allowSelectAllPages` | `boolean` | `false` | Show a banner offering "select all results" across paged data |
|
|
204
|
+
| `selectedAll` | `boolean` | `false` | All-pages mode flag (bindable). In this mode `excluded` drives selection |
|
|
205
|
+
| `excluded` | `Set<string \| number>` | `new Set()` | Deselected row IDs while in all-pages mode (bindable) |
|
|
206
|
+
| `onRowClick` | `(row, index) => void` | - | Row click callback |
|
|
207
|
+
| `loading` | `boolean` | `false` | Show loading overlay |
|
|
208
|
+
| `small` | `boolean` | `false` | Force mobile/card layout regardless of viewport |
|
|
209
|
+
| `t` | `TranslateFn` | built-in | Optional translation function |
|
|
210
|
+
| `cell` | `Snippet` | - | Custom cell renderer (desktop + mobile) |
|
|
211
|
+
| `row` | `Snippet` | - | Custom desktop `<tr>` renderer (overrides default row) |
|
|
212
|
+
| `mobileRow` | `Snippet` | - | Custom mobile card renderer |
|
|
213
|
+
| `batchActions` | `Snippet` | - | Batch action bar content |
|
|
214
|
+
| `selectAllBanner` | `Snippet` | - | Override default "select all across pages" banner |
|
|
215
|
+
| `empty` | `Snippet` | - | Custom empty state |
|
|
216
|
+
| `unstyled` | `boolean` | `false` | Skip default styling |
|
|
217
|
+
| `class` | `string` | - | Additional CSS classes |
|
|
218
|
+
| `el` | `HTMLDivElement` | - | Bindable element ref |
|
|
219
|
+
|
|
220
|
+
### Snippet signatures
|
|
221
|
+
|
|
222
|
+
| Snippet | Props |
|
|
223
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
224
|
+
| `cell` | `{ column, row, value, rowIndex, variant: "desktop" \| "mobile" }` |
|
|
225
|
+
| `row` | `{ row, columns, rowIndex, isSelected }` — desktop only |
|
|
226
|
+
| `mobileRow` | `{ row, columns, rowIndex }` — mobile only |
|
|
227
|
+
| `batchActions` | `{ selected, selectedRows, selectedAll, excluded, effectiveCount, totalCount, clearSelection }` |
|
|
228
|
+
| `selectAllBanner` | `{ selectedAll, effectiveCount, totalCount, pageCount, selectAll, clearSelection }` |
|
|
229
|
+
| `empty` | — |
|
|
230
|
+
|
|
231
|
+
> **Note:** "Select all rows" affects only the rows currently in `data` (i.e. the current page when using external paging). Rows for which `selectDisabledBy` returns `true` are excluded from "select all". To select across pages, enable `allowSelectAllPages` and use the banner that appears.
|
|
232
|
+
>
|
|
233
|
+
> **Note:** `clearSelection` (exposed by the `batchActions` snippet) resets all selection state — `selected`, `selectedAll`, and `excluded` — including exiting all-pages mode.
|
|
107
234
|
|
|
108
235
|
## DataTableColumn
|
|
109
236
|
|
|
@@ -143,3 +270,6 @@ loading state, and mobile card layout.
|
|
|
143
270
|
| `--stuic-data-table-card-radius` | `var(--radius-md)` | Mobile card radius |
|
|
144
271
|
| `--stuic-data-table-card-padding` | `0.75rem` | Mobile card padding |
|
|
145
272
|
| `--stuic-data-table-card-gap` | `0.5rem` | Gap between mobile cards |
|
|
273
|
+
| `--stuic-data-table-select-all-bg` | `color-mix(primary 10%)` | Select-all banner background |
|
|
274
|
+
| `--stuic-data-table-select-all-padding-x` | `0.75rem` | Banner horizontal padding |
|
|
275
|
+
| `--stuic-data-table-select-all-padding-y` | `0.5rem` | Banner vertical padding |
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
--stuic-data-table-batch-padding-x: 0.75rem;
|
|
39
39
|
--stuic-data-table-batch-padding-y: 0.5rem;
|
|
40
40
|
|
|
41
|
+
/* Select-all-across-pages banner */
|
|
42
|
+
--stuic-data-table-select-all-bg: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
|
|
43
|
+
--stuic-data-table-select-all-padding-x: 0.75rem;
|
|
44
|
+
--stuic-data-table-select-all-padding-y: 0.5rem;
|
|
45
|
+
|
|
41
46
|
/* Checkbox column */
|
|
42
47
|
--stuic-data-table-checkbox-width: 3rem;
|
|
43
48
|
|
|
@@ -210,6 +215,25 @@
|
|
|
210
215
|
margin-bottom: 0.5rem;
|
|
211
216
|
}
|
|
212
217
|
|
|
218
|
+
/* ============================================================================
|
|
219
|
+
SELECT-ALL-ACROSS-PAGES BANNER
|
|
220
|
+
============================================================================ */
|
|
221
|
+
|
|
222
|
+
.stuic-data-table-select-all-banner {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
justify-content: center;
|
|
226
|
+
gap: var(--stuic-data-table-paging-gap);
|
|
227
|
+
padding: var(--stuic-data-table-select-all-padding-y)
|
|
228
|
+
var(--stuic-data-table-select-all-padding-x);
|
|
229
|
+
background: var(--stuic-data-table-select-all-bg);
|
|
230
|
+
border-radius: var(--stuic-data-table-radius, var(--stuic-radius));
|
|
231
|
+
margin-bottom: 0.5rem;
|
|
232
|
+
font-size: var(--stuic-data-table-cell-font-size);
|
|
233
|
+
color: var(--stuic-data-table-header-color);
|
|
234
|
+
text-align: center;
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
/* ============================================================================
|
|
214
238
|
LOADING SPINNER OVERLAY
|
|
215
239
|
============================================================================ */
|
|
@@ -278,6 +302,13 @@
|
|
|
278
302
|
background: var(--stuic-data-table-card-bg-selected);
|
|
279
303
|
}
|
|
280
304
|
|
|
305
|
+
.stuic-data-table-card-checkbox {
|
|
306
|
+
display: flex;
|
|
307
|
+
align-items: center;
|
|
308
|
+
gap: 0.5rem;
|
|
309
|
+
margin-bottom: 0.25rem;
|
|
310
|
+
}
|
|
311
|
+
|
|
281
312
|
.stuic-data-table-card-row {
|
|
282
313
|
display: flex;
|
|
283
314
|
justify-content: space-between;
|
|
@@ -201,6 +201,10 @@
|
|
|
201
201
|
triggerEl?: HTMLButtonElement;
|
|
202
202
|
/** Reference to dropdown element */
|
|
203
203
|
dropdownEl?: HTMLDivElement;
|
|
204
|
+
/** Reference to the wrapper element */
|
|
205
|
+
el?: HTMLDivElement;
|
|
206
|
+
/** Opt out of stuic base classes for full styling control */
|
|
207
|
+
unstyled?: boolean;
|
|
204
208
|
/** Optional, used only when css positioning not supported (iPhone)*/
|
|
205
209
|
noScrollLock?: boolean;
|
|
206
210
|
}
|
|
@@ -298,6 +302,8 @@
|
|
|
298
302
|
onSelect,
|
|
299
303
|
triggerEl = $bindable(),
|
|
300
304
|
dropdownEl = $bindable(),
|
|
305
|
+
el = $bindable(),
|
|
306
|
+
unstyled = false,
|
|
301
307
|
scrollbarGutter,
|
|
302
308
|
noScrollLock,
|
|
303
309
|
...rest
|
|
@@ -309,7 +315,6 @@
|
|
|
309
315
|
const anchorName = `--dropdown-anchor-${triggerId}`;
|
|
310
316
|
|
|
311
317
|
// State
|
|
312
|
-
let wrapperEl: HTMLDivElement = $state()!;
|
|
313
318
|
let activeItemEl: HTMLButtonElement | undefined = $state();
|
|
314
319
|
const reducedMotion = prefersReducedMotion();
|
|
315
320
|
|
|
@@ -615,7 +620,7 @@
|
|
|
615
620
|
|
|
616
621
|
// Click outside handler — only active when open (prevents stale refs on destroy)
|
|
617
622
|
const _clickOutside = onClickOutside(
|
|
618
|
-
() =>
|
|
623
|
+
() => el,
|
|
619
624
|
() => {
|
|
620
625
|
if (closeOnClickOutside && isOpen) {
|
|
621
626
|
isOpen = false;
|
|
@@ -626,7 +631,7 @@
|
|
|
626
631
|
);
|
|
627
632
|
|
|
628
633
|
$effect(() => {
|
|
629
|
-
if (isOpen &&
|
|
634
|
+
if (isOpen && el) _clickOutside.start();
|
|
630
635
|
else _clickOutside.stop();
|
|
631
636
|
});
|
|
632
637
|
|
|
@@ -808,8 +813,8 @@
|
|
|
808
813
|
/>
|
|
809
814
|
|
|
810
815
|
<div
|
|
811
|
-
bind:this={
|
|
812
|
-
class={twMerge(DROPDOWN_MENU_BASE_CLASSES, classProp)}
|
|
816
|
+
bind:this={el}
|
|
817
|
+
class={unstyled ? classProp : twMerge(DROPDOWN_MENU_BASE_CLASSES, classProp)}
|
|
813
818
|
style:anchor-name={isSupported ? anchorName : undefined}
|
|
814
819
|
>
|
|
815
820
|
<!-- Trigger -->
|
|
@@ -830,7 +835,7 @@
|
|
|
830
835
|
<button
|
|
831
836
|
bind:this={triggerEl}
|
|
832
837
|
id={triggerId}
|
|
833
|
-
class={twMerge(DROPDOWN_MENU_TRIGGER_CLASSES, classTrigger)}
|
|
838
|
+
class={unstyled ? classTrigger : twMerge(DROPDOWN_MENU_TRIGGER_CLASSES, classTrigger)}
|
|
834
839
|
onclick={() => (isOpen = !isOpen)}
|
|
835
840
|
aria-haspopup="menu"
|
|
836
841
|
aria-expanded={isOpen}
|
|
@@ -851,15 +856,15 @@
|
|
|
851
856
|
|
|
852
857
|
<!-- Backdrop (fallback mode only) -->
|
|
853
858
|
{#if isOpen && !isSupported && showBackdrop}
|
|
859
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
854
860
|
<div
|
|
855
|
-
class={twMerge(DROPDOWN_MENU_BACKDROP_CLASSES, classBackdrop)}
|
|
861
|
+
class={unstyled ? classBackdrop : twMerge(DROPDOWN_MENU_BACKDROP_CLASSES, classBackdrop)}
|
|
856
862
|
onclick={() => {
|
|
857
863
|
if (closeOnClickOutside) {
|
|
858
864
|
isOpen = false;
|
|
859
865
|
triggerEl?.focus();
|
|
860
866
|
}
|
|
861
867
|
}}
|
|
862
|
-
onkeydown={() => {}}
|
|
863
868
|
role="presentation"
|
|
864
869
|
transition:fade={{ duration: transitionDuration }}
|
|
865
870
|
></div>
|
|
@@ -872,11 +877,13 @@
|
|
|
872
877
|
id={dropdownId}
|
|
873
878
|
role="menu"
|
|
874
879
|
aria-labelledby={triggerId}
|
|
875
|
-
class={
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
+
class={unstyled
|
|
881
|
+
? twMerge(!isSupported && "w-4/5 max-w-sm", classDropdown)
|
|
882
|
+
: twMerge(
|
|
883
|
+
DROPDOWN_MENU_DROPDOWN_CLASSES,
|
|
884
|
+
!isSupported && "w-4/5 max-w-sm",
|
|
885
|
+
classDropdown
|
|
886
|
+
)}
|
|
880
887
|
style={dropdownStyle}
|
|
881
888
|
transition:slide={{ duration: transitionDuration }}
|
|
882
889
|
>
|
|
@@ -886,7 +893,9 @@
|
|
|
886
893
|
<button
|
|
887
894
|
type="button"
|
|
888
895
|
aria-label="Close"
|
|
889
|
-
class=
|
|
896
|
+
class={unstyled
|
|
897
|
+
? "absolute right-0 top-0 pointer-events-auto"
|
|
898
|
+
: "stuic-close-button absolute right-0 top-0 pointer-events-auto"}
|
|
890
899
|
onclick={() => {
|
|
891
900
|
isOpen = false;
|
|
892
901
|
triggerEl?.focus();
|
|
@@ -957,12 +966,16 @@
|
|
|
957
966
|
{:else if item.type === "divider"}
|
|
958
967
|
<div
|
|
959
968
|
role="separator"
|
|
960
|
-
class={
|
|
969
|
+
class={unstyled
|
|
970
|
+
? twMerge(classDivider, item.class)
|
|
971
|
+
: twMerge(DROPDOWN_MENU_DIVIDER_CLASSES, classDivider, item.class)}
|
|
961
972
|
></div>
|
|
962
973
|
{:else if item.type === "header"}
|
|
963
974
|
<div
|
|
964
975
|
role="presentation"
|
|
965
|
-
class={
|
|
976
|
+
class={unstyled
|
|
977
|
+
? twMerge(classHeader, item.class)
|
|
978
|
+
: twMerge(DROPDOWN_MENU_HEADER_CLASSES, classHeader, item.class)}
|
|
966
979
|
>
|
|
967
980
|
<Thc thc={item.label} />
|
|
968
981
|
</div>
|
|
@@ -1043,20 +1056,24 @@
|
|
|
1043
1056
|
{#if childItem.type === "divider"}
|
|
1044
1057
|
<div
|
|
1045
1058
|
role="separator"
|
|
1046
|
-
class={
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1059
|
+
class={unstyled
|
|
1060
|
+
? twMerge(classDivider, childItem.class)
|
|
1061
|
+
: twMerge(
|
|
1062
|
+
DROPDOWN_MENU_DIVIDER_CLASSES,
|
|
1063
|
+
classDivider,
|
|
1064
|
+
childItem.class
|
|
1065
|
+
)}
|
|
1051
1066
|
></div>
|
|
1052
1067
|
{:else if childItem.type === "header"}
|
|
1053
1068
|
<div
|
|
1054
1069
|
role="presentation"
|
|
1055
|
-
class={
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1070
|
+
class={unstyled
|
|
1071
|
+
? twMerge(classHeader, childItem.class)
|
|
1072
|
+
: twMerge(
|
|
1073
|
+
DROPDOWN_MENU_HEADER_CLASSES,
|
|
1074
|
+
classHeader,
|
|
1075
|
+
childItem.class
|
|
1076
|
+
)}
|
|
1060
1077
|
>
|
|
1061
1078
|
<Thc thc={childItem.label} />
|
|
1062
1079
|
</div>
|
|
@@ -170,6 +170,10 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
|
170
170
|
triggerEl?: HTMLButtonElement;
|
|
171
171
|
/** Reference to dropdown element */
|
|
172
172
|
dropdownEl?: HTMLDivElement;
|
|
173
|
+
/** Reference to the wrapper element */
|
|
174
|
+
el?: HTMLDivElement;
|
|
175
|
+
/** Opt out of stuic base classes for full styling control */
|
|
176
|
+
unstyled?: boolean;
|
|
173
177
|
/** Optional, used only when css positioning not supported (iPhone)*/
|
|
174
178
|
noScrollLock?: boolean;
|
|
175
179
|
}
|
|
@@ -179,6 +183,6 @@ export declare const DROPDOWN_MENU_DROPDOWN_CLASSES = "stuic-dropdown-menu-dropd
|
|
|
179
183
|
export declare const DROPDOWN_MENU_DIVIDER_CLASSES = "stuic-dropdown-menu-divider";
|
|
180
184
|
export declare const DROPDOWN_MENU_HEADER_CLASSES = "stuic-dropdown-menu-header";
|
|
181
185
|
export declare const DROPDOWN_MENU_BACKDROP_CLASSES = "stuic-dropdown-menu-backdrop";
|
|
182
|
-
declare const DropdownMenu: import("svelte").Component<Props, {}, "isOpen" | "triggerEl" | "dropdownEl">;
|
|
186
|
+
declare const DropdownMenu: import("svelte").Component<Props, {}, "el" | "isOpen" | "triggerEl" | "dropdownEl">;
|
|
183
187
|
type DropdownMenu = ReturnType<typeof DropdownMenu>;
|
|
184
188
|
export default DropdownMenu;
|
|
@@ -8,13 +8,15 @@ A feature-rich dropdown menu component with CSS Anchor Positioning (with fallbac
|
|
|
8
8
|
| ------------------------ | ---------------------- | -------------------- | ---------------------------------------- |
|
|
9
9
|
| `items` | `DropdownMenuItem[]` | - | Menu items to display |
|
|
10
10
|
| `isOpen` | `boolean` | `false` | Controlled open state (bindable) |
|
|
11
|
-
| `position` | `DropdownMenuPosition` | `"bottom-span-
|
|
11
|
+
| `position` | `DropdownMenuPosition` | `"bottom-span-right"` | Popover position relative to trigger |
|
|
12
12
|
| `offset` | `string` | `"0.25rem"` | Offset from trigger element (CSS value) |
|
|
13
13
|
| `maxHeight` | `string` | `"300px"` | Max height of dropdown |
|
|
14
14
|
| `closeOnSelect` | `boolean` | `true` | Close menu when action item is selected |
|
|
15
15
|
| `closeOnClickOutside` | `boolean` | `true` | Close on click outside |
|
|
16
16
|
| `closeOnEscape` | `boolean` | `true` | Close on Escape key |
|
|
17
17
|
| `forceFallback` | `boolean` | `false` | Force fallback positioning (for testing) |
|
|
18
|
+
| `search` | `boolean \| DropdownMenuSearchConfig` | - | Enable search/filter input (see [Search](#search)) |
|
|
19
|
+
| `unstyled` | `boolean` | `false` | Opt out of stuic base classes |
|
|
18
20
|
| `class` | `string` | - | Classes for wrapper element |
|
|
19
21
|
| `classTrigger` | `string` | - | Classes for trigger button |
|
|
20
22
|
| `classDropdown` | `string` | - | Classes for dropdown container |
|
|
@@ -25,6 +27,7 @@ A feature-rich dropdown menu component with CSS Anchor Positioning (with fallbac
|
|
|
25
27
|
| `classHeader` | `string` | - | Classes for header items |
|
|
26
28
|
| `classExpandable` | `string` | - | Classes for expandable section header |
|
|
27
29
|
| `classExpandableContent` | `string` | - | Classes for expandable section content |
|
|
30
|
+
| `el` | `HTMLDivElement` | - | Wrapper element reference (bindable) |
|
|
28
31
|
| `triggerEl` | `HTMLButtonElement` | - | Trigger element reference (bindable) |
|
|
29
32
|
| `dropdownEl` | `HTMLDivElement` | - | Dropdown element reference (bindable) |
|
|
30
33
|
|
|
@@ -39,15 +42,15 @@ A feature-rich dropdown menu component with CSS Anchor Positioning (with fallbac
|
|
|
39
42
|
|
|
40
43
|
### Action Item
|
|
41
44
|
|
|
42
|
-
Clickable menu item with optional
|
|
45
|
+
Clickable menu item with optional leading/trailing content.
|
|
43
46
|
|
|
44
47
|
```typescript
|
|
45
48
|
interface DropdownMenuActionItem {
|
|
46
49
|
type: "action";
|
|
47
50
|
id: string | number;
|
|
48
51
|
label: THC; // Text, HTML, or component
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
contentBefore?: THC; // Optional leading content (icon, etc.)
|
|
53
|
+
contentAfter?: THC; // Optional trailing content (shortcut hint, etc.)
|
|
51
54
|
disabled?: boolean;
|
|
52
55
|
onSelect?: () => void | boolean;
|
|
53
56
|
href?: string; // Render as <a> link instead of <button>
|
|
@@ -104,7 +107,7 @@ interface DropdownMenuExpandableItem {
|
|
|
104
107
|
type: "expandable";
|
|
105
108
|
id: string | number;
|
|
106
109
|
label: THC;
|
|
107
|
-
|
|
110
|
+
contentBefore?: THC;
|
|
108
111
|
items: DropdownMenuFlatItem[]; // Nested items (no nested expandables)
|
|
109
112
|
defaultExpanded?: boolean;
|
|
110
113
|
disabled?: boolean;
|
|
@@ -131,6 +134,29 @@ interface DropdownMenuExpandableItem {
|
|
|
131
134
|
| `onClose` | - | Called when menu closes |
|
|
132
135
|
| `onSelect` | `(item: DropdownMenuActionItem)` | Called when action item selected (fallback) |
|
|
133
136
|
|
|
137
|
+
## Search
|
|
138
|
+
|
|
139
|
+
Pass `search` to enable an in-menu filter input. Use `true` for defaults or a config object:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
interface DropdownMenuSearchConfig {
|
|
143
|
+
placeholder?: string;
|
|
144
|
+
/** Match algorithm */
|
|
145
|
+
strategy?: "prefix" | "exact" | "fuzzy";
|
|
146
|
+
/** Custom function to extract searchable text from an item */
|
|
147
|
+
getContent?: (item: DropdownMenuActionItem | DropdownMenuExpandableItem) => string;
|
|
148
|
+
/** Auto-focus search input when menu opens */
|
|
149
|
+
autoFocus?: boolean;
|
|
150
|
+
/** Message shown when no results found */
|
|
151
|
+
noResultsMessage?: string;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```svelte
|
|
156
|
+
<DropdownMenu {items} search />
|
|
157
|
+
<DropdownMenu {items} search={{ placeholder: "Filter…", strategy: "fuzzy" }} />
|
|
158
|
+
```
|
|
159
|
+
|
|
134
160
|
## Keyboard Navigation
|
|
135
161
|
|
|
136
162
|
| Key | Action |
|
|
@@ -186,6 +212,8 @@ When `href` is set, the item renders as an `<a>` element instead of `<button>`.
|
|
|
186
212
|
|
|
187
213
|
### With Icons and Shortcuts
|
|
188
214
|
|
|
215
|
+
Use `contentBefore` for leading content (icons) and `contentAfter` for trailing content (shortcut hints). Both accept any `THC` value — string, HTML, or component.
|
|
216
|
+
|
|
189
217
|
```svelte
|
|
190
218
|
<script lang="ts">
|
|
191
219
|
import { DropdownMenu } from "stuic";
|
|
@@ -197,8 +225,8 @@ When `href` is set, the item renders as an `<a>` element instead of `<button>`.
|
|
|
197
225
|
type: "action",
|
|
198
226
|
id: "edit",
|
|
199
227
|
label: "Edit",
|
|
200
|
-
|
|
201
|
-
|
|
228
|
+
contentBefore: { html: iconLucideEdit({ size: 16 }) },
|
|
229
|
+
contentAfter: "Cmd+E",
|
|
202
230
|
onSelect: () => handleEdit(),
|
|
203
231
|
},
|
|
204
232
|
{ type: "divider" },
|
|
@@ -206,8 +234,8 @@ When `href` is set, the item renders as an `<a>` element instead of `<button>`.
|
|
|
206
234
|
type: "action",
|
|
207
235
|
id: "delete",
|
|
208
236
|
label: "Delete",
|
|
209
|
-
|
|
210
|
-
|
|
237
|
+
contentBefore: { html: iconLucideTrash({ size: 16 }) },
|
|
238
|
+
contentAfter: "Cmd+D",
|
|
211
239
|
onSelect: () => handleDelete(),
|
|
212
240
|
},
|
|
213
241
|
];
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import SpinnerCircleOscillate from "../Spinner/SpinnerCircleOscillate.svelte";
|
|
38
38
|
import { isTHCNotEmpty, type THC } from "../Thc/Thc.svelte";
|
|
39
39
|
import InputWrap from "./_internal/InputWrap.svelte";
|
|
40
|
+
import type { InputWrapClassProps } from "./types.js";
|
|
40
41
|
import Button from "../Button/Button.svelte";
|
|
41
42
|
|
|
42
43
|
const clog = createClog("FieldAssets");
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
|
|
127
128
|
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
128
129
|
|
|
129
|
-
export interface Props extends Record<string, any> {
|
|
130
|
+
export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
130
131
|
value: string;
|
|
131
132
|
label?: SnippetWithId | THC;
|
|
132
133
|
type?: string;
|
|
@@ -145,13 +146,8 @@
|
|
|
145
146
|
labelLeft?: boolean;
|
|
146
147
|
labelLeftWidth?: "normal" | "wide";
|
|
147
148
|
labelLeftBreakpoint?: number;
|
|
149
|
+
/** Classes for the hidden <input> element */
|
|
148
150
|
classInput?: string;
|
|
149
|
-
classLabel?: string;
|
|
150
|
-
classLabelBox?: string;
|
|
151
|
-
classInputBox?: string;
|
|
152
|
-
classInputBoxWrap?: string;
|
|
153
|
-
classDescBox?: string;
|
|
154
|
-
classBelowBox?: string;
|
|
155
151
|
classOption?: string;
|
|
156
152
|
classOptionActive?: string;
|
|
157
153
|
classOptgroup?: string;
|
|
@@ -205,8 +201,11 @@
|
|
|
205
201
|
classLabelBox,
|
|
206
202
|
classInputBox,
|
|
207
203
|
classInputBoxWrap,
|
|
204
|
+
classInputBoxWrapInvalid,
|
|
208
205
|
classDescBox,
|
|
206
|
+
classDescBoxToggle,
|
|
209
207
|
classBelowBox,
|
|
208
|
+
classValidationBox,
|
|
210
209
|
//
|
|
211
210
|
classOption,
|
|
212
211
|
classOptionActive,
|
|
@@ -503,8 +502,11 @@
|
|
|
503
502
|
{classLabelBox}
|
|
504
503
|
{classInputBox}
|
|
505
504
|
{classInputBoxWrap}
|
|
505
|
+
{classInputBoxWrapInvalid}
|
|
506
506
|
{classDescBox}
|
|
507
|
+
{classDescBoxToggle}
|
|
507
508
|
{classBelowBox}
|
|
509
|
+
{classValidationBox}
|
|
508
510
|
{validation}
|
|
509
511
|
{style}
|
|
510
512
|
>
|
|
@@ -3,6 +3,7 @@ import { type ValidateOptions } from "../../actions/validate.svelte.js";
|
|
|
3
3
|
import type { TranslateFn } from "../../types.js";
|
|
4
4
|
import { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
5
5
|
import { type THC } from "../Thc/Thc.svelte";
|
|
6
|
+
import type { InputWrapClassProps } from "./types.js";
|
|
6
7
|
export type FieldAssetUrlObj = {
|
|
7
8
|
thumb: string;
|
|
8
9
|
full: string;
|
|
@@ -23,7 +24,7 @@ export declare function getAssetIcon(ext?: string): CallableFunction;
|
|
|
23
24
|
type SnippetWithId = Snippet<[{
|
|
24
25
|
id: string;
|
|
25
26
|
}]>;
|
|
26
|
-
export interface Props extends Record<string, any> {
|
|
27
|
+
export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
27
28
|
value: string;
|
|
28
29
|
label?: SnippetWithId | THC;
|
|
29
30
|
type?: string;
|
|
@@ -42,13 +43,8 @@ export interface Props extends Record<string, any> {
|
|
|
42
43
|
labelLeft?: boolean;
|
|
43
44
|
labelLeftWidth?: "normal" | "wide";
|
|
44
45
|
labelLeftBreakpoint?: number;
|
|
46
|
+
/** Classes for the hidden <input> element */
|
|
45
47
|
classInput?: string;
|
|
46
|
-
classLabel?: string;
|
|
47
|
-
classLabelBox?: string;
|
|
48
|
-
classInputBox?: string;
|
|
49
|
-
classInputBoxWrap?: string;
|
|
50
|
-
classDescBox?: string;
|
|
51
|
-
classBelowBox?: string;
|
|
52
48
|
classOption?: string;
|
|
53
49
|
classOptionActive?: string;
|
|
54
50
|
classOptgroup?: string;
|