@nqmcreative/ui 0.2.3 → 0.4.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/README.md +209 -15
- package/cli/index.mjs +328 -112
- package/dist/core/color.d.ts +24 -0
- package/dist/core/color.js +39 -0
- package/dist/core/index.d.ts +8 -1
- package/dist/core/index.js +5 -1
- package/dist/core/locale.svelte.d.ts +8 -0
- package/dist/core/locale.svelte.js +14 -0
- package/dist/core/number.d.ts +22 -0
- package/dist/core/number.js +33 -0
- package/dist/core/pin.d.ts +23 -0
- package/dist/core/pin.js +38 -0
- package/dist/core/tags.d.ts +37 -0
- package/dist/core/tags.js +51 -0
- package/dist/core/time.d.ts +30 -0
- package/dist/core/time.js +78 -0
- package/dist/core/toast.svelte.d.ts +3 -0
- package/dist/styles/matte/AccordionItem.svelte +5 -0
- package/dist/styles/matte/AccordionItem.svelte.d.ts +2 -0
- package/dist/styles/matte/Avatar.svelte +15 -0
- package/dist/styles/matte/Avatar.svelte.d.ts +3 -0
- package/dist/styles/matte/Badge.svelte +5 -0
- package/dist/styles/matte/Badge.svelte.d.ts +2 -0
- package/dist/styles/matte/Breadcrumb.svelte +11 -4
- package/dist/styles/matte/Breadcrumb.svelte.d.ts +3 -0
- package/dist/styles/matte/Button.svelte +58 -7
- package/dist/styles/matte/Button.svelte.d.ts +14 -2
- package/dist/styles/matte/Card.svelte +6 -1
- package/dist/styles/matte/Card.svelte.d.ts +2 -0
- package/dist/styles/matte/CheckboxGroup.svelte +6 -0
- package/dist/styles/matte/CheckboxGroup.svelte.d.ts +2 -0
- package/dist/styles/matte/ColorInput.svelte +110 -0
- package/dist/styles/matte/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/matte/Combobox.svelte +5 -0
- package/dist/styles/matte/Combobox.svelte.d.ts +3 -0
- package/dist/styles/matte/CommandPalette.svelte +6 -0
- package/dist/styles/matte/CommandPalette.svelte.d.ts +3 -0
- package/dist/styles/matte/CurrencyInput.svelte +108 -0
- package/dist/styles/matte/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/matte/Drawer.svelte +5 -0
- package/dist/styles/matte/Drawer.svelte.d.ts +2 -0
- package/dist/styles/matte/FileInput.svelte +194 -0
- package/dist/styles/matte/FileInput.svelte.d.ts +32 -0
- package/dist/styles/matte/Footer.svelte +6 -2
- package/dist/styles/matte/Footer.svelte.d.ts +2 -0
- package/dist/styles/matte/InputAddon.svelte +36 -0
- package/dist/styles/matte/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/matte/Link.svelte +9 -0
- package/dist/styles/matte/Link.svelte.d.ts +4 -0
- package/dist/styles/matte/MenuItem.svelte +2 -1
- package/dist/styles/matte/Modal.svelte +5 -0
- package/dist/styles/matte/Modal.svelte.d.ts +2 -0
- package/dist/styles/matte/MultiSelect.svelte +7 -1
- package/dist/styles/matte/MultiSelect.svelte.d.ts +3 -0
- package/dist/styles/matte/Navbar.svelte +18 -4
- package/dist/styles/matte/Navbar.svelte.d.ts +2 -0
- package/dist/styles/matte/PinInput.svelte +137 -0
- package/dist/styles/matte/PinInput.svelte.d.ts +25 -0
- package/dist/styles/matte/PricingCard.svelte +16 -8
- package/dist/styles/matte/PricingCard.svelte.d.ts +2 -0
- package/dist/styles/matte/RadioGroup.svelte +6 -0
- package/dist/styles/matte/RadioGroup.svelte.d.ts +2 -0
- package/dist/styles/matte/SearchInput.svelte +87 -0
- package/dist/styles/matte/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/matte/SegmentedControl.svelte +16 -2
- package/dist/styles/matte/SegmentedControl.svelte.d.ts +5 -0
- package/dist/styles/matte/Sidebar.svelte +2 -1
- package/dist/styles/matte/Stat.svelte +8 -1
- package/dist/styles/matte/Stat.svelte.d.ts +3 -0
- package/dist/styles/matte/Steps.svelte +7 -0
- package/dist/styles/matte/Steps.svelte.d.ts +3 -0
- package/dist/styles/matte/Table.svelte +153 -97
- package/dist/styles/matte/Table.svelte.d.ts +12 -0
- package/dist/styles/matte/Tabs.svelte +5 -0
- package/dist/styles/matte/Tabs.svelte.d.ts +3 -0
- package/dist/styles/matte/TagsInput.svelte +173 -0
- package/dist/styles/matte/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/matte/TimeInput.svelte +129 -0
- package/dist/styles/matte/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/matte/Toaster.svelte +3 -0
- package/dist/styles/matte/icon.d.ts +21 -0
- package/dist/styles/matte/icon.js +21 -0
- package/dist/styles/matte/index.d.ts +8 -2
- package/dist/styles/matte/index.js +8 -2
- package/dist/styles/paper/AccordionItem.svelte +5 -0
- package/dist/styles/paper/AccordionItem.svelte.d.ts +2 -0
- package/dist/styles/paper/Avatar.svelte +15 -0
- package/dist/styles/paper/Avatar.svelte.d.ts +3 -0
- package/dist/styles/paper/Badge.svelte +5 -0
- package/dist/styles/paper/Badge.svelte.d.ts +2 -0
- package/dist/styles/paper/Breadcrumb.svelte +8 -4
- package/dist/styles/paper/Breadcrumb.svelte.d.ts +3 -0
- package/dist/styles/paper/Button.svelte +58 -7
- package/dist/styles/paper/Button.svelte.d.ts +14 -2
- package/dist/styles/paper/Card.svelte +6 -1
- package/dist/styles/paper/Card.svelte.d.ts +2 -0
- package/dist/styles/paper/CheckboxGroup.svelte +6 -0
- package/dist/styles/paper/CheckboxGroup.svelte.d.ts +2 -0
- package/dist/styles/paper/ColorInput.svelte +112 -0
- package/dist/styles/paper/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/paper/Combobox.svelte +5 -0
- package/dist/styles/paper/Combobox.svelte.d.ts +3 -0
- package/dist/styles/paper/CommandPalette.svelte +6 -0
- package/dist/styles/paper/CommandPalette.svelte.d.ts +3 -0
- package/dist/styles/paper/CurrencyInput.svelte +108 -0
- package/dist/styles/paper/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/paper/Drawer.svelte +5 -0
- package/dist/styles/paper/Drawer.svelte.d.ts +2 -0
- package/dist/styles/paper/FileInput.svelte +204 -0
- package/dist/styles/paper/FileInput.svelte.d.ts +32 -0
- package/dist/styles/paper/Footer.svelte +6 -2
- package/dist/styles/paper/Footer.svelte.d.ts +2 -0
- package/dist/styles/paper/InputAddon.svelte +36 -0
- package/dist/styles/paper/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/paper/Link.svelte +9 -0
- package/dist/styles/paper/Link.svelte.d.ts +4 -0
- package/dist/styles/paper/MenuItem.svelte +2 -1
- package/dist/styles/paper/Modal.svelte +5 -0
- package/dist/styles/paper/Modal.svelte.d.ts +2 -0
- package/dist/styles/paper/MultiSelect.svelte +7 -1
- package/dist/styles/paper/MultiSelect.svelte.d.ts +3 -0
- package/dist/styles/paper/Navbar.svelte +18 -4
- package/dist/styles/paper/Navbar.svelte.d.ts +2 -0
- package/dist/styles/paper/PinInput.svelte +137 -0
- package/dist/styles/paper/PinInput.svelte.d.ts +25 -0
- package/dist/styles/paper/PricingCard.svelte +23 -15
- package/dist/styles/paper/PricingCard.svelte.d.ts +2 -0
- package/dist/styles/paper/RadioGroup.svelte +6 -0
- package/dist/styles/paper/RadioGroup.svelte.d.ts +2 -0
- package/dist/styles/paper/SearchInput.svelte +92 -0
- package/dist/styles/paper/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/paper/SegmentedControl.svelte +16 -2
- package/dist/styles/paper/SegmentedControl.svelte.d.ts +5 -0
- package/dist/styles/paper/Sidebar.svelte +2 -1
- package/dist/styles/paper/Stat.svelte +8 -1
- package/dist/styles/paper/Stat.svelte.d.ts +3 -0
- package/dist/styles/paper/Steps.svelte +7 -0
- package/dist/styles/paper/Steps.svelte.d.ts +3 -0
- package/dist/styles/paper/Table.svelte +153 -97
- package/dist/styles/paper/Table.svelte.d.ts +12 -0
- package/dist/styles/paper/Tabs.svelte +5 -0
- package/dist/styles/paper/Tabs.svelte.d.ts +3 -0
- package/dist/styles/paper/TagsInput.svelte +178 -0
- package/dist/styles/paper/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/paper/TimeInput.svelte +155 -0
- package/dist/styles/paper/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/paper/Toaster.svelte +3 -0
- package/dist/styles/paper/icon.d.ts +21 -0
- package/dist/styles/paper/icon.js +21 -0
- package/dist/styles/paper/index.d.ts +8 -2
- package/dist/styles/paper/index.js +8 -2
- package/dist/styles/sprout/AccordionItem.svelte +5 -0
- package/dist/styles/sprout/AccordionItem.svelte.d.ts +2 -0
- package/dist/styles/sprout/Avatar.svelte +15 -0
- package/dist/styles/sprout/Avatar.svelte.d.ts +3 -0
- package/dist/styles/sprout/Badge.svelte +5 -0
- package/dist/styles/sprout/Badge.svelte.d.ts +2 -0
- package/dist/styles/sprout/Breadcrumb.svelte +8 -4
- package/dist/styles/sprout/Breadcrumb.svelte.d.ts +3 -0
- package/dist/styles/sprout/Button.svelte +58 -7
- package/dist/styles/sprout/Button.svelte.d.ts +14 -2
- package/dist/styles/sprout/Card.svelte +6 -1
- package/dist/styles/sprout/Card.svelte.d.ts +2 -0
- package/dist/styles/sprout/CheckboxGroup.svelte +6 -0
- package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +2 -0
- package/dist/styles/sprout/ColorInput.svelte +112 -0
- package/dist/styles/sprout/ColorInput.svelte.d.ts +16 -0
- package/dist/styles/sprout/Combobox.svelte +5 -0
- package/dist/styles/sprout/Combobox.svelte.d.ts +3 -0
- package/dist/styles/sprout/CommandPalette.svelte +6 -0
- package/dist/styles/sprout/CommandPalette.svelte.d.ts +3 -0
- package/dist/styles/sprout/CurrencyInput.svelte +108 -0
- package/dist/styles/sprout/CurrencyInput.svelte.d.ts +26 -0
- package/dist/styles/sprout/Drawer.svelte +5 -0
- package/dist/styles/sprout/Drawer.svelte.d.ts +2 -0
- package/dist/styles/sprout/FileInput.svelte +205 -0
- package/dist/styles/sprout/FileInput.svelte.d.ts +32 -0
- package/dist/styles/sprout/Footer.svelte +6 -2
- package/dist/styles/sprout/Footer.svelte.d.ts +2 -0
- package/dist/styles/sprout/InputAddon.svelte +37 -0
- package/dist/styles/sprout/InputAddon.svelte.d.ts +13 -0
- package/dist/styles/sprout/Link.svelte +9 -0
- package/dist/styles/sprout/Link.svelte.d.ts +4 -0
- package/dist/styles/sprout/MenuItem.svelte +2 -1
- package/dist/styles/sprout/Modal.svelte +5 -0
- package/dist/styles/sprout/Modal.svelte.d.ts +2 -0
- package/dist/styles/sprout/MultiSelect.svelte +7 -1
- package/dist/styles/sprout/MultiSelect.svelte.d.ts +3 -0
- package/dist/styles/sprout/Navbar.svelte +18 -4
- package/dist/styles/sprout/Navbar.svelte.d.ts +2 -0
- package/dist/styles/sprout/PinInput.svelte +138 -0
- package/dist/styles/sprout/PinInput.svelte.d.ts +25 -0
- package/dist/styles/sprout/PricingCard.svelte +23 -15
- package/dist/styles/sprout/PricingCard.svelte.d.ts +2 -0
- package/dist/styles/sprout/RadioGroup.svelte +6 -0
- package/dist/styles/sprout/RadioGroup.svelte.d.ts +2 -0
- package/dist/styles/sprout/SearchInput.svelte +92 -0
- package/dist/styles/sprout/SearchInput.svelte.d.ts +24 -0
- package/dist/styles/sprout/SegmentedControl.svelte +16 -2
- package/dist/styles/sprout/SegmentedControl.svelte.d.ts +5 -0
- package/dist/styles/sprout/Sidebar.svelte +2 -1
- package/dist/styles/sprout/Stat.svelte +8 -1
- package/dist/styles/sprout/Stat.svelte.d.ts +3 -0
- package/dist/styles/sprout/Steps.svelte +7 -0
- package/dist/styles/sprout/Steps.svelte.d.ts +3 -0
- package/dist/styles/sprout/Table.svelte +153 -97
- package/dist/styles/sprout/Table.svelte.d.ts +12 -0
- package/dist/styles/sprout/Tabs.svelte +5 -0
- package/dist/styles/sprout/Tabs.svelte.d.ts +3 -0
- package/dist/styles/sprout/TagsInput.svelte +179 -0
- package/dist/styles/sprout/TagsInput.svelte.d.ts +29 -0
- package/dist/styles/sprout/TimeInput.svelte +155 -0
- package/dist/styles/sprout/TimeInput.svelte.d.ts +21 -0
- package/dist/styles/sprout/Toaster.svelte +3 -0
- package/dist/styles/sprout/icon.d.ts +21 -0
- package/dist/styles/sprout/icon.js +21 -0
- package/dist/styles/sprout/index.d.ts +8 -2
- package/dist/styles/sprout/index.js +8 -2
- package/package.json +98 -26
- package/registry.json +143 -35
- package/dist/styles/matte/NumberInput.svelte +0 -96
- package/dist/styles/matte/NumberInput.svelte.d.ts +0 -15
- package/dist/styles/matte/PasswordInput.svelte +0 -95
- package/dist/styles/matte/PasswordInput.svelte.d.ts +0 -12
- package/dist/styles/paper/NumberInput.svelte +0 -96
- package/dist/styles/paper/NumberInput.svelte.d.ts +0 -15
- package/dist/styles/paper/PasswordInput.svelte +0 -100
- package/dist/styles/paper/PasswordInput.svelte.d.ts +0 -12
- package/dist/styles/sprout/NumberInput.svelte +0 -97
- package/dist/styles/sprout/NumberInput.svelte.d.ts +0 -15
- package/dist/styles/sprout/PasswordInput.svelte +0 -101
- package/dist/styles/sprout/PasswordInput.svelte.d.ts +0 -12
|
@@ -32,6 +32,16 @@
|
|
|
32
32
|
/** Header stays put while the body scrolls — pair with a `max-h-*` class. */
|
|
33
33
|
stickyHeader?: boolean;
|
|
34
34
|
|
|
35
|
+
/** Bar above the table — a title, a search field, a filter row. */
|
|
36
|
+
header?: Snippet;
|
|
37
|
+
/** Bar below the table — pagination, a total, a summary line. */
|
|
38
|
+
footer?: Snippet;
|
|
39
|
+
/**
|
|
40
|
+
* Shown in place of `header` while rows are ticked, after the count and the
|
|
41
|
+
* clear button. Passing it is what turns the bulk bar on.
|
|
42
|
+
*/
|
|
43
|
+
bulkActions?: Snippet<[{ selected: string[]; clear: () => void }]>;
|
|
44
|
+
|
|
35
45
|
/** Bindable. Set it to sort; leave `sortable` columns to drive it. */
|
|
36
46
|
sort?: TableSort | null;
|
|
37
47
|
/** Sort the rows here rather than refetching them server-side. Default true. */
|
|
@@ -57,6 +67,9 @@
|
|
|
57
67
|
hoverable = true,
|
|
58
68
|
compact = false,
|
|
59
69
|
stickyHeader = false,
|
|
70
|
+
header,
|
|
71
|
+
footer,
|
|
72
|
+
bulkActions,
|
|
60
73
|
sort = $bindable(null),
|
|
61
74
|
sortLocally = true,
|
|
62
75
|
onsort,
|
|
@@ -106,109 +119,152 @@
|
|
|
106
119
|
selected = toggleKey(selected, keyOf(row));
|
|
107
120
|
onselect?.(selected);
|
|
108
121
|
}
|
|
122
|
+
|
|
123
|
+
function clearSelection() {
|
|
124
|
+
selected = [];
|
|
125
|
+
onselect?.(selected);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The bulk bar takes the header's place, so only one of the two is up. */
|
|
129
|
+
const bulkBar = $derived(selectable && bulkActions !== undefined && selected.length > 0);
|
|
109
130
|
</script>
|
|
110
131
|
|
|
111
|
-
<div class="w-full overflow-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
<div class="flex w-full flex-col overflow-hidden border border-hairline {className}" {...rest}>
|
|
133
|
+
{#if bulkBar}
|
|
134
|
+
<div
|
|
135
|
+
class="flex flex-wrap items-center gap-3 border-b border-hairline bg-bg-inset px-3.5 py-2.5"
|
|
136
|
+
>
|
|
137
|
+
<span class="font-mono text-xs font-medium tracking-wide text-text uppercase">
|
|
138
|
+
{selected.length}
|
|
139
|
+
{t.current.selected}
|
|
140
|
+
</span>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onclick={clearSelection}
|
|
144
|
+
class="font-mono text-xs tracking-wide text-text-muted uppercase transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
116
145
|
>
|
|
117
|
-
{
|
|
118
|
-
</
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
{t.current.clear}
|
|
147
|
+
</button>
|
|
148
|
+
<div class="ml-auto flex flex-wrap items-center gap-2">
|
|
149
|
+
{@render bulkActions?.({ selected, clear: clearSelection })}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
{:else if header}
|
|
153
|
+
<div
|
|
154
|
+
class="flex flex-wrap items-center gap-3 border-b border-hairline px-3.5 py-2.5 font-sans text-sm text-text-secondary"
|
|
155
|
+
>
|
|
156
|
+
{@render header()}
|
|
157
|
+
</div>
|
|
158
|
+
{/if}
|
|
159
|
+
<div class="min-h-0 w-full flex-1 overflow-auto">
|
|
160
|
+
<table class="w-full border-collapse font-sans text-[15px]">
|
|
161
|
+
{#if caption}
|
|
162
|
+
<caption
|
|
163
|
+
class="border-b border-hairline px-3.5 py-2.5 text-left font-sans text-sm text-text-muted"
|
|
164
|
+
>
|
|
165
|
+
{caption}
|
|
166
|
+
</caption>
|
|
167
|
+
{/if}
|
|
168
|
+
<thead class={stickyHeader ? 'sticky top-0 z-10' : ''}>
|
|
169
|
+
<tr class="border-b border-hairline bg-bg-alt">
|
|
170
|
+
{#if selectable}
|
|
171
|
+
<th scope="col" class="w-10 {pad}">
|
|
172
|
+
<Checkbox
|
|
173
|
+
checked={allSelected}
|
|
174
|
+
indeterminate={someSelected}
|
|
175
|
+
{tone}
|
|
176
|
+
aria-label={t.current.selectAllRows}
|
|
177
|
+
onchange={toggleAll}
|
|
178
|
+
/>
|
|
179
|
+
</th>
|
|
180
|
+
{/if}
|
|
181
|
+
{#each columns as column (column.key)}
|
|
182
|
+
<th
|
|
183
|
+
scope="col"
|
|
184
|
+
aria-sort={sort?.key === column.key
|
|
185
|
+
? sort.direction === 'asc'
|
|
186
|
+
? 'ascending'
|
|
187
|
+
: 'descending'
|
|
188
|
+
: undefined}
|
|
189
|
+
class="font-mono text-xs font-medium tracking-wide text-text-muted uppercase {pad} {aligns[
|
|
190
|
+
column.align ?? 'left'
|
|
191
|
+
]} {column.class ?? ''}"
|
|
192
|
+
>
|
|
193
|
+
{#if column.sortable}
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onclick={() => toggleSort(column)}
|
|
197
|
+
class="inline-flex items-center gap-1.5 uppercase transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
|
|
150
198
|
{sort?.key === column.key ? 'text-text' : ''}"
|
|
151
|
-
|
|
199
|
+
>
|
|
200
|
+
{column.label}
|
|
201
|
+
<span class="text-[9px] leading-none" aria-hidden="true">
|
|
202
|
+
{#if sort?.key === column.key}
|
|
203
|
+
{sort.direction === 'asc' ? '▲' : '▼'}
|
|
204
|
+
{:else}
|
|
205
|
+
⇅
|
|
206
|
+
{/if}
|
|
207
|
+
</span>
|
|
208
|
+
</button>
|
|
209
|
+
{:else}
|
|
152
210
|
{column.label}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{:else}
|
|
157
|
-
⇅
|
|
158
|
-
{/if}
|
|
159
|
-
</span>
|
|
160
|
-
</button>
|
|
161
|
-
{:else}
|
|
162
|
-
{column.label}
|
|
163
|
-
{/if}
|
|
164
|
-
</th>
|
|
165
|
-
{/each}
|
|
166
|
-
</tr>
|
|
167
|
-
</thead>
|
|
168
|
-
<tbody>
|
|
169
|
-
{#if sorted.length === 0}
|
|
170
|
-
<tr>
|
|
171
|
-
<td
|
|
172
|
-
colspan={columns.length + (selectable ? 1 : 0)}
|
|
173
|
-
class="px-3.5 py-10 text-center text-sm text-text-muted"
|
|
174
|
-
>
|
|
175
|
-
{#if empty}{@render empty()}{:else}{t.current.noData}{/if}
|
|
176
|
-
</td>
|
|
211
|
+
{/if}
|
|
212
|
+
</th>
|
|
213
|
+
{/each}
|
|
177
214
|
</tr>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<tr
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
</thead>
|
|
216
|
+
<tbody>
|
|
217
|
+
{#if sorted.length === 0}
|
|
218
|
+
<tr>
|
|
219
|
+
<td
|
|
220
|
+
colspan={columns.length + (selectable ? 1 : 0)}
|
|
221
|
+
class="px-3.5 py-10 text-center text-sm text-text-muted"
|
|
222
|
+
>
|
|
223
|
+
{#if empty}{@render empty()}{:else}{t.current.noData}{/if}
|
|
224
|
+
</td>
|
|
225
|
+
</tr>
|
|
226
|
+
{:else}
|
|
227
|
+
{#each sorted as row, i (selectable ? keyOf(row) : i)}
|
|
228
|
+
{@const isSelected = selectable && selected.includes(keyOf(row))}
|
|
229
|
+
<tr
|
|
230
|
+
aria-selected={selectable ? isSelected : undefined}
|
|
231
|
+
class="border-b border-hairline last:border-b-0
|
|
184
232
|
{isSelected ? 'bg-bg-inset' : striped && i % 2 === 1 ? 'bg-bg-alt' : ''}
|
|
185
233
|
{hoverable ? 'transition-colors duration-150 hover:bg-bg-inset' : ''}"
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
234
|
+
>
|
|
235
|
+
{#if selectable}
|
|
236
|
+
<td class={pad}>
|
|
237
|
+
<Checkbox
|
|
238
|
+
checked={isSelected}
|
|
239
|
+
{tone}
|
|
240
|
+
aria-label={t.current.selectRow}
|
|
241
|
+
onchange={() => toggleRow(row)}
|
|
242
|
+
/>
|
|
243
|
+
</td>
|
|
244
|
+
{/if}
|
|
245
|
+
{#each columns as column (column.key)}
|
|
246
|
+
<td
|
|
247
|
+
class="text-text-secondary {pad} {aligns[column.align ?? 'left']} {column.class ??
|
|
248
|
+
''}"
|
|
249
|
+
>
|
|
250
|
+
{#if cell}
|
|
251
|
+
{@render cell({ row, column, value: row[column.key] })}
|
|
252
|
+
{:else}
|
|
253
|
+
{row[column.key] ?? ''}
|
|
254
|
+
{/if}
|
|
255
|
+
</td>
|
|
256
|
+
{/each}
|
|
257
|
+
</tr>
|
|
258
|
+
{/each}
|
|
259
|
+
{/if}
|
|
260
|
+
</tbody>
|
|
261
|
+
</table>
|
|
262
|
+
</div>
|
|
263
|
+
{#if footer}
|
|
264
|
+
<div
|
|
265
|
+
class="flex flex-wrap items-center gap-3 border-t border-hairline px-3.5 py-2.5 font-sans text-sm text-text-secondary"
|
|
266
|
+
>
|
|
267
|
+
{@render footer()}
|
|
268
|
+
</div>
|
|
269
|
+
{/if}
|
|
214
270
|
</div>
|
|
@@ -29,6 +29,18 @@ declare function $$render<T extends Record<string, unknown>>(): {
|
|
|
29
29
|
compact?: boolean;
|
|
30
30
|
/** Header stays put while the body scrolls — pair with a `max-h-*` class. */
|
|
31
31
|
stickyHeader?: boolean;
|
|
32
|
+
/** Bar above the table — a title, a search field, a filter row. */
|
|
33
|
+
header?: Snippet;
|
|
34
|
+
/** Bar below the table — pagination, a total, a summary line. */
|
|
35
|
+
footer?: Snippet;
|
|
36
|
+
/**
|
|
37
|
+
* Shown in place of `header` while rows are ticked, after the count and the
|
|
38
|
+
* clear button. Passing it is what turns the bulk bar on.
|
|
39
|
+
*/
|
|
40
|
+
bulkActions?: Snippet<[{
|
|
41
|
+
selected: string[];
|
|
42
|
+
clear: () => void;
|
|
43
|
+
}]>;
|
|
32
44
|
/** Bindable. Set it to sort; leave `sortable` columns to drive it. */
|
|
33
45
|
sort?: TableSort | null;
|
|
34
46
|
/** Sort the rows here rather than refetching them server-side. Default true. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
import {
|
|
4
5
|
focusRing,
|
|
@@ -8,10 +9,13 @@
|
|
|
8
9
|
toneText,
|
|
9
10
|
type Tone
|
|
10
11
|
} from '../../core/tones.js';
|
|
12
|
+
import { iconMd } from './icon.js';
|
|
11
13
|
|
|
12
14
|
export interface TabItem {
|
|
13
15
|
value: string;
|
|
14
16
|
label: string;
|
|
17
|
+
/** Leading icon, 16px. */
|
|
18
|
+
icon?: Snippet;
|
|
15
19
|
disabled?: boolean;
|
|
16
20
|
/** Small count/label rendered after the tab title. */
|
|
17
21
|
badge?: string | number;
|
|
@@ -89,6 +93,7 @@
|
|
|
89
93
|
onclick={() => select(item)}
|
|
90
94
|
class={tabClass(value === item.value)}
|
|
91
95
|
>
|
|
96
|
+
{#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
|
|
92
97
|
{item.label}
|
|
93
98
|
{#if item.badge !== undefined}
|
|
94
99
|
<span class="font-mono text-[11px] text-text-muted">{item.badge}</span>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
3
|
import { type Tone } from '../../core/tones.js';
|
|
3
4
|
export interface TabItem {
|
|
4
5
|
value: string;
|
|
5
6
|
label: string;
|
|
7
|
+
/** Leading icon, 16px. */
|
|
8
|
+
icon?: Snippet;
|
|
6
9
|
disabled?: boolean;
|
|
7
10
|
/** Small count/label rendered after the tab title. */
|
|
8
11
|
badge?: string | number;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useLocale } from '../../core/locale.svelte.js';
|
|
3
|
+
import { addTag, splitTags, type TagRejection } from '../../core/tags.js';
|
|
4
|
+
import { toneFocusWithinBorder, toneSoft, type Tone } from '../../core/tones.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Bindable list of tags, in the order they were added. */
|
|
8
|
+
tags?: string[];
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
/** Cap on how many may be held. `0` means no limit. */
|
|
11
|
+
max?: number;
|
|
12
|
+
/** Shortest a tag may be, after trimming. */
|
|
13
|
+
minLength?: number;
|
|
14
|
+
/** Keys that commit the tag under the caret, besides Enter. */
|
|
15
|
+
separators?: string[];
|
|
16
|
+
/** Refuse a tag that differs from an existing one only in case. */
|
|
17
|
+
caseInsensitive?: boolean;
|
|
18
|
+
/** Commit whatever is half-typed when the field loses focus. */
|
|
19
|
+
commitOnBlur?: boolean;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
invalid?: boolean;
|
|
22
|
+
tone?: Tone;
|
|
23
|
+
/** `name` for a plain form submit — one hidden field, comma-joined. */
|
|
24
|
+
name?: string;
|
|
25
|
+
onadd?: (tag: string) => void;
|
|
26
|
+
onremove?: (tag: string) => void;
|
|
27
|
+
onreject?: (reason: TagRejection, raw: string) => void;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
tags = $bindable([]),
|
|
33
|
+
placeholder,
|
|
34
|
+
max = 0,
|
|
35
|
+
minLength = 1,
|
|
36
|
+
separators = [','],
|
|
37
|
+
caseInsensitive = true,
|
|
38
|
+
commitOnBlur = true,
|
|
39
|
+
disabled = false,
|
|
40
|
+
invalid = false,
|
|
41
|
+
tone = 'brand',
|
|
42
|
+
name,
|
|
43
|
+
onadd,
|
|
44
|
+
onremove,
|
|
45
|
+
onreject,
|
|
46
|
+
class: className = ''
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
const t = useLocale();
|
|
50
|
+
|
|
51
|
+
let draft = $state('');
|
|
52
|
+
let field: HTMLInputElement | null = $state(null);
|
|
53
|
+
/** The last refusal, cleared as soon as the next keystroke lands. */
|
|
54
|
+
let refused = $state<TagRejection | null>(null);
|
|
55
|
+
|
|
56
|
+
const full = $derived(max > 0 && tags.length >= max);
|
|
57
|
+
const rules = $derived({ max, minLength, caseInsensitive });
|
|
58
|
+
|
|
59
|
+
function commit(raw: string) {
|
|
60
|
+
const { tags: next, rejected } = addTag(tags, raw, rules);
|
|
61
|
+
if (rejected) {
|
|
62
|
+
// An empty commit is the Enter key on an empty field, not a mistake.
|
|
63
|
+
if (rejected !== 'empty') {
|
|
64
|
+
refused = rejected;
|
|
65
|
+
onreject?.(rejected, raw);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
tags = next;
|
|
70
|
+
onadd?.(next[next.length - 1]);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function remove(index: number) {
|
|
75
|
+
const [gone] = tags.slice(index, index + 1);
|
|
76
|
+
tags = tags.filter((_, at) => at !== index);
|
|
77
|
+
if (gone !== undefined) onremove?.(gone);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onKeydown(event: KeyboardEvent) {
|
|
81
|
+
refused = null;
|
|
82
|
+
|
|
83
|
+
if (event.key === 'Enter' || separators.includes(event.key)) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
if (commit(draft)) draft = '';
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Empty field: Backspace peels the last chip off, the way chips behave
|
|
90
|
+
// everywhere else in this library.
|
|
91
|
+
if (event.key === 'Backspace' && draft === '' && tags.length) {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
remove(tags.length - 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onPaste(event: ClipboardEvent) {
|
|
98
|
+
const text = event.clipboardData?.getData('text') ?? '';
|
|
99
|
+
const parts = splitTags(text, separators);
|
|
100
|
+
if (parts.length <= 1) return;
|
|
101
|
+
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
for (const part of parts) commit(part);
|
|
104
|
+
draft = '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onBlur() {
|
|
108
|
+
if (commitOnBlur && draft.trim() && commit(draft)) draft = '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const messages: Record<TagRejection, string> = $derived({
|
|
112
|
+
empty: '',
|
|
113
|
+
short: `${minLength}+`,
|
|
114
|
+
duplicate: t.current.duplicateTag,
|
|
115
|
+
full: `${t.current.selected} ${tags.length}/${max}`
|
|
116
|
+
});
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<div class="flex w-full flex-col gap-1.5 {className}">
|
|
120
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
121
|
+
<div
|
|
122
|
+
onclick={() => field?.focus()}
|
|
123
|
+
class="flex w-full flex-wrap items-center gap-1.5 border bg-bg py-1.5 pr-1.5 pl-2 transition-colors duration-150 ease-brand-out
|
|
124
|
+
{invalid
|
|
125
|
+
? 'border-danger focus-within:border-danger'
|
|
126
|
+
: `border-hairline ${toneFocusWithinBorder[tone]}`}
|
|
127
|
+
{disabled ? 'pointer-events-none opacity-50' : ''}"
|
|
128
|
+
>
|
|
129
|
+
{#each tags as tag, index (tag)}
|
|
130
|
+
<span
|
|
131
|
+
class="inline-flex items-center gap-1.5 rounded-full py-1 pr-1 pl-2.5 font-mono text-xs {toneSoft[
|
|
132
|
+
tone
|
|
133
|
+
]}"
|
|
134
|
+
>
|
|
135
|
+
{tag}
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onclick={() => remove(index)}
|
|
139
|
+
aria-label="{t.current.removeItem} {tag}"
|
|
140
|
+
class="inline-flex size-4 items-center justify-center rounded-full transition-colors duration-150 hover:bg-bg focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-current"
|
|
141
|
+
>
|
|
142
|
+
<svg class="size-2.5" viewBox="0 0 10 10" fill="none" aria-hidden="true">
|
|
143
|
+
<path d="m1.5 1.5 7 7M8.5 1.5l-7 7" stroke="currentColor" stroke-width="1.5" />
|
|
144
|
+
</svg>
|
|
145
|
+
</button>
|
|
146
|
+
</span>
|
|
147
|
+
{/each}
|
|
148
|
+
|
|
149
|
+
<input
|
|
150
|
+
bind:this={field}
|
|
151
|
+
bind:value={draft}
|
|
152
|
+
{disabled}
|
|
153
|
+
readonly={full}
|
|
154
|
+
placeholder={tags.length === 0 ? (placeholder ?? t.current.addTag) : ''}
|
|
155
|
+
aria-label={placeholder ?? t.current.addTag}
|
|
156
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
157
|
+
onkeydown={onKeydown}
|
|
158
|
+
onpaste={onPaste}
|
|
159
|
+
onblur={onBlur}
|
|
160
|
+
class="h-7 min-w-24 flex-1 bg-transparent font-sans text-[15px] text-text placeholder:text-text-muted focus:outline-none"
|
|
161
|
+
/>
|
|
162
|
+
|
|
163
|
+
{#if name}
|
|
164
|
+
<input type="hidden" {name} value={tags.join(',')} />
|
|
165
|
+
{/if}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{#if refused && messages[refused]}
|
|
169
|
+
<span class="font-mono text-xs text-danger">{messages[refused]}</span>
|
|
170
|
+
{:else if max}
|
|
171
|
+
<span class="font-mono text-xs text-text-muted">{tags.length}/{max}</span>
|
|
172
|
+
{/if}
|
|
173
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type TagRejection } from '../../core/tags.js';
|
|
2
|
+
import { type Tone } from '../../core/tones.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Bindable list of tags, in the order they were added. */
|
|
5
|
+
tags?: string[];
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
/** Cap on how many may be held. `0` means no limit. */
|
|
8
|
+
max?: number;
|
|
9
|
+
/** Shortest a tag may be, after trimming. */
|
|
10
|
+
minLength?: number;
|
|
11
|
+
/** Keys that commit the tag under the caret, besides Enter. */
|
|
12
|
+
separators?: string[];
|
|
13
|
+
/** Refuse a tag that differs from an existing one only in case. */
|
|
14
|
+
caseInsensitive?: boolean;
|
|
15
|
+
/** Commit whatever is half-typed when the field loses focus. */
|
|
16
|
+
commitOnBlur?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
invalid?: boolean;
|
|
19
|
+
tone?: Tone;
|
|
20
|
+
/** `name` for a plain form submit — one hidden field, comma-joined. */
|
|
21
|
+
name?: string;
|
|
22
|
+
onadd?: (tag: string) => void;
|
|
23
|
+
onremove?: (tag: string) => void;
|
|
24
|
+
onreject?: (reason: TagRejection, raw: string) => void;
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
declare const TagsInput: import("svelte").Component<Props, {}, "tags">;
|
|
28
|
+
type TagsInput = ReturnType<typeof TagsInput>;
|
|
29
|
+
export default TagsInput;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { useLocale } from '../../core/locale.svelte.js';
|
|
4
|
+
import { clampTime, formatTime, parseTime, stepTime } from '../../core/time.js';
|
|
5
|
+
import type { Tone } from '../../core/tones.js';
|
|
6
|
+
import Input, { type InputSize } from './Input.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props extends Omit<
|
|
9
|
+
HTMLInputAttributes,
|
|
10
|
+
'size' | 'prefix' | 'suffix' | 'min' | 'max' | 'step'
|
|
11
|
+
> {
|
|
12
|
+
/** Bindable `HH:MM`, or `''` for empty. */
|
|
13
|
+
value?: string;
|
|
14
|
+
/** Inclusive bounds, `HH:MM`. */
|
|
15
|
+
min?: string;
|
|
16
|
+
max?: string;
|
|
17
|
+
/** Minutes one press of a stepper moves, and what typing snaps to. */
|
|
18
|
+
step?: number;
|
|
19
|
+
size?: InputSize;
|
|
20
|
+
tone?: Tone;
|
|
21
|
+
invalid?: boolean;
|
|
22
|
+
/** Hide the steppers and let the field be typed into only. */
|
|
23
|
+
steppers?: boolean;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
value = $bindable(''),
|
|
29
|
+
min,
|
|
30
|
+
max,
|
|
31
|
+
step = 15,
|
|
32
|
+
size = 'md',
|
|
33
|
+
tone = 'brand',
|
|
34
|
+
invalid = false,
|
|
35
|
+
disabled = false,
|
|
36
|
+
steppers = true,
|
|
37
|
+
class: className = '',
|
|
38
|
+
...rest
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
const t = useLocale();
|
|
42
|
+
|
|
43
|
+
// Typed text and the bound value are separate until the field settles:
|
|
44
|
+
// `9` is a valid thing to be halfway through typing and an invalid time.
|
|
45
|
+
let text = $state('');
|
|
46
|
+
let editing = $state(false);
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
if (!editing) text = value;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function settle() {
|
|
53
|
+
editing = false;
|
|
54
|
+
const minutes = parseTime(text);
|
|
55
|
+
value = minutes === null ? '' : formatTime(clampTime(minutes, min, max, step));
|
|
56
|
+
text = value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function nudge(direction: 1 | -1) {
|
|
60
|
+
value = stepTime(value, direction, step, min, max);
|
|
61
|
+
text = value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onKeydown(event: KeyboardEvent) {
|
|
65
|
+
if (event.key === 'ArrowUp') {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
nudge(1);
|
|
68
|
+
} else if (event.key === 'ArrowDown') {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
nudge(-1);
|
|
71
|
+
} else if (event.key === 'Enter') {
|
|
72
|
+
settle();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const stepper =
|
|
77
|
+
'grid size-6 place-items-center text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-current';
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<Input
|
|
81
|
+
bind:value={text}
|
|
82
|
+
{size}
|
|
83
|
+
{tone}
|
|
84
|
+
{invalid}
|
|
85
|
+
{disabled}
|
|
86
|
+
inputmode="numeric"
|
|
87
|
+
placeholder="--:--"
|
|
88
|
+
onfocus={() => (editing = true)}
|
|
89
|
+
onblur={settle}
|
|
90
|
+
onkeydown={onKeydown}
|
|
91
|
+
class="[&_input]:tabular-nums {className}"
|
|
92
|
+
{...rest}
|
|
93
|
+
>
|
|
94
|
+
{#snippet prefix()}
|
|
95
|
+
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
96
|
+
<circle cx="10" cy="10" r="6.5" stroke="currentColor" stroke-width="1.5" />
|
|
97
|
+
<path d="M10 6.5V10l2.5 1.5" stroke="currentColor" stroke-width="1.5" />
|
|
98
|
+
</svg>
|
|
99
|
+
{/snippet}
|
|
100
|
+
|
|
101
|
+
{#snippet suffix()}
|
|
102
|
+
{#if steppers}
|
|
103
|
+
<span class="-mr-2 flex flex-col">
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class={stepper}
|
|
107
|
+
{disabled}
|
|
108
|
+
onclick={() => nudge(1)}
|
|
109
|
+
aria-label={t.current.increase}
|
|
110
|
+
>
|
|
111
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
112
|
+
<path d="m3 7.5 3-3 3 3" stroke="currentColor" stroke-width="1.5" />
|
|
113
|
+
</svg>
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class={stepper}
|
|
118
|
+
{disabled}
|
|
119
|
+
onclick={() => nudge(-1)}
|
|
120
|
+
aria-label={t.current.decrease}
|
|
121
|
+
>
|
|
122
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
123
|
+
<path d="m3 4.5 3 3 3-3" stroke="currentColor" stroke-width="1.5" />
|
|
124
|
+
</svg>
|
|
125
|
+
</button>
|
|
126
|
+
</span>
|
|
127
|
+
{/if}
|
|
128
|
+
{/snippet}
|
|
129
|
+
</Input>
|