@marianmeres/stuic 3.167.0 → 3.169.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/AGENTS.md +6 -6
- package/dist/README.md +1 -1
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/long-press.d.ts +43 -0
- package/dist/attachments/long-press.js +107 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
- package/dist/components/Breadcrumbs/README.md +174 -0
- package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
- package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
- package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
- package/dist/components/Breadcrumbs/i18n.js +40 -0
- package/dist/components/Breadcrumbs/index.css +84 -0
- package/dist/components/Breadcrumbs/index.d.ts +4 -0
- package/dist/components/Breadcrumbs/index.js +4 -0
- package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
- package/dist/components/Breadcrumbs/json-ld.js +56 -0
- package/dist/components/Collapsible/Collapsible.svelte +84 -15
- package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
- package/dist/components/Collapsible/README.md +82 -23
- package/dist/components/Collapsible/index.css +38 -0
- package/dist/components/Collapsible/index.d.ts +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
- package/dist/components/ContextMenu/README.md +159 -0
- package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
- package/dist/components/ContextMenu/i18n-sk.js +18 -0
- package/dist/components/ContextMenu/i18n.d.ts +30 -0
- package/dist/components/ContextMenu/i18n.js +38 -0
- package/dist/components/ContextMenu/index.css +26 -0
- package/dist/components/ContextMenu/index.d.ts +3 -0
- package/dist/components/ContextMenu/index.js +3 -0
- package/dist/components/CopyButton/CopyButton.svelte +278 -0
- package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
- package/dist/components/CopyButton/README.md +241 -0
- package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
- package/dist/components/CopyButton/i18n-sk.js +20 -0
- package/dist/components/CopyButton/i18n.d.ts +35 -0
- package/dist/components/CopyButton/i18n.js +43 -0
- package/dist/components/CopyButton/index.css +67 -0
- package/dist/components/CopyButton/index.d.ts +3 -0
- package/dist/components/CopyButton/index.js +3 -0
- package/dist/components/DataTable/DataTable.svelte +41 -34
- package/dist/components/DataTable/README.md +45 -0
- package/dist/components/DataTable/i18n-sk.js +1 -0
- package/dist/components/DataTable/i18n.d.ts +1 -0
- package/dist/components/DataTable/i18n.js +3 -0
- package/dist/components/DataTable/index.css +41 -3
- package/dist/components/EmptyState/EmptyState.svelte +121 -0
- package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
- package/dist/components/EmptyState/README.md +188 -0
- package/dist/components/EmptyState/index.css +154 -0
- package/dist/components/EmptyState/index.d.ts +1 -0
- package/dist/components/EmptyState/index.js +1 -0
- package/dist/components/Input/FieldOptions.svelte +92 -2
- package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
- package/dist/components/Input/README.md +66 -31
- package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
- package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
- package/dist/components/Input/index.css +58 -0
- package/dist/components/Pagination/Pagination.svelte +250 -0
- package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
- package/dist/components/Pagination/README.md +152 -0
- package/dist/components/Pagination/i18n-sk.d.ts +17 -0
- package/dist/components/Pagination/i18n-sk.js +24 -0
- package/dist/components/Pagination/i18n.d.ts +41 -0
- package/dist/components/Pagination/i18n.js +49 -0
- package/dist/components/Pagination/index.css +50 -0
- package/dist/components/Pagination/index.d.ts +4 -0
- package/dist/components/Pagination/index.js +4 -0
- package/dist/components/Pagination/pagination-range.d.ts +21 -0
- package/dist/components/Pagination/pagination-range.js +41 -0
- package/dist/components/Pill/Pill.svelte +8 -2
- package/dist/components/Pill/Pill.svelte.d.ts +2 -0
- package/dist/components/Pill/README.md +35 -22
- package/dist/components/Pill/index.css +57 -18
- package/dist/components/Stat/README.md +157 -0
- package/dist/components/Stat/Stat.svelte +226 -0
- package/dist/components/Stat/Stat.svelte.d.ts +54 -0
- package/dist/components/Stat/index.css +217 -0
- package/dist/components/Stat/index.d.ts +1 -0
- package/dist/components/Stat/index.js +1 -0
- package/dist/components/Stepper/README.md +181 -0
- package/dist/components/Stepper/Stepper.svelte +240 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
- package/dist/components/Stepper/i18n-sk.d.ts +17 -0
- package/dist/components/Stepper/i18n-sk.js +21 -0
- package/dist/components/Stepper/i18n.d.ts +35 -0
- package/dist/components/Stepper/i18n.js +43 -0
- package/dist/components/Stepper/index.css +302 -0
- package/dist/components/Stepper/index.d.ts +3 -0
- package/dist/components/Stepper/index.js +3 -0
- package/dist/components/TrendChart/README.md +123 -0
- package/dist/components/TrendChart/TrendChart.svelte +114 -0
- package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
- package/dist/components/TrendChart/index.css +51 -0
- package/dist/components/TrendChart/index.d.ts +1 -0
- package/dist/components/TrendChart/index.js +1 -0
- package/dist/icons/index.d.ts +4 -0
- package/dist/icons/index.js +4 -0
- package/dist/index.css +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/copy-to-clipboard.d.ts +43 -0
- package/dist/utils/copy-to-clipboard.js +102 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/architecture.md +1 -1
- package/docs/domains/attachments.md +41 -4
- package/docs/domains/components.md +280 -13
- package/docs/domains/utils.md +8 -6
- package/docs/maybe-todo.md +132 -0
- package/docs/upgrading.md +6 -0
- package/package.json +11 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { ValidateOptions } from "../../../actions/validate.svelte.js";
|
|
4
|
+
import type { IntentColorKey } from "../../../utils/design-tokens.js";
|
|
5
|
+
import type { THC } from "../../Thc/Thc.svelte";
|
|
6
|
+
import type { InputWrapClassProps } from "../types.js";
|
|
7
|
+
|
|
8
|
+
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
9
|
+
|
|
10
|
+
/** One rendered chip. `key` must be unique within the row. */
|
|
11
|
+
export interface FieldLikeChip {
|
|
12
|
+
key: string | number;
|
|
13
|
+
label: string;
|
|
14
|
+
/** Accessible name of the chip's remove (×) button, e.g. "Remove Foo" */
|
|
15
|
+
removeLabel: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Internal: the `chips` trigger of `FieldOptions`. The FieldLikeButton counterpart for a
|
|
20
|
+
* selection shown as inline, individually removable Pills — same InputWrap shell, same
|
|
21
|
+
* hidden input + validate wiring, same imperative API.
|
|
22
|
+
*/
|
|
23
|
+
export interface Props extends InputWrapClassProps {
|
|
24
|
+
/** The hidden input carrying `value` + `name` (bindable) */
|
|
25
|
+
input?: HTMLInputElement;
|
|
26
|
+
value: string;
|
|
27
|
+
/** The chips to render (the parent derives them from `value`) */
|
|
28
|
+
chips: FieldLikeChip[];
|
|
29
|
+
label?: SnippetWithId | THC;
|
|
30
|
+
description?: SnippetWithId | THC;
|
|
31
|
+
class?: string;
|
|
32
|
+
id?: string;
|
|
33
|
+
tabindex?: number;
|
|
34
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
35
|
+
name?: string;
|
|
36
|
+
required?: boolean;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
39
|
+
labelAfter?: SnippetWithId | THC;
|
|
40
|
+
below?: SnippetWithId | THC;
|
|
41
|
+
labelLeft?: boolean;
|
|
42
|
+
labelLeftWidth?: "normal" | "wide";
|
|
43
|
+
labelLeftBreakpoint?: number;
|
|
44
|
+
style?: string;
|
|
45
|
+
/** Shown in the row while there are no chips */
|
|
46
|
+
placeholder?: string;
|
|
47
|
+
/** Accessible name (and tooltip) of the trailing "open" button */
|
|
48
|
+
openLabel: string;
|
|
49
|
+
/** Classes for each chip (Pill) */
|
|
50
|
+
classChip?: string;
|
|
51
|
+
/** Pill intent of the chips */
|
|
52
|
+
chipIntent?: IntentColorKey;
|
|
53
|
+
/** Open the picker (trailing button, or a click on the empty part of the row) */
|
|
54
|
+
onOpen: () => void;
|
|
55
|
+
/** A chip's × was pressed */
|
|
56
|
+
onRemove: (chip: FieldLikeChip, index: number) => void;
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<script lang="ts">
|
|
61
|
+
import { tick } from "svelte";
|
|
62
|
+
import { iconSearch } from "../../../icons/index.js";
|
|
63
|
+
import {
|
|
64
|
+
validate as validateAction,
|
|
65
|
+
type ValidationResult,
|
|
66
|
+
} from "../../../actions/validate.svelte.js";
|
|
67
|
+
import { getId } from "../../../utils/get-id.js";
|
|
68
|
+
import Button from "../../Button/Button.svelte";
|
|
69
|
+
import Pill from "../../Pill/Pill.svelte";
|
|
70
|
+
import InputWrap from "./InputWrap.svelte";
|
|
71
|
+
|
|
72
|
+
let {
|
|
73
|
+
input = $bindable(),
|
|
74
|
+
value = $bindable(),
|
|
75
|
+
chips,
|
|
76
|
+
label = "",
|
|
77
|
+
id = getId(),
|
|
78
|
+
tabindex = 0,
|
|
79
|
+
description,
|
|
80
|
+
class: classProp,
|
|
81
|
+
renderSize = "md",
|
|
82
|
+
name,
|
|
83
|
+
//
|
|
84
|
+
required = false,
|
|
85
|
+
disabled = false,
|
|
86
|
+
//
|
|
87
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
88
|
+
validate: validateProp,
|
|
89
|
+
//
|
|
90
|
+
labelAfter,
|
|
91
|
+
below,
|
|
92
|
+
//
|
|
93
|
+
labelLeft = false,
|
|
94
|
+
labelLeftWidth = "normal",
|
|
95
|
+
labelLeftBreakpoint = 480,
|
|
96
|
+
//
|
|
97
|
+
classLabel,
|
|
98
|
+
classLabelBox,
|
|
99
|
+
classInputBox,
|
|
100
|
+
classInputBoxWrap,
|
|
101
|
+
classInputBoxWrapInvalid,
|
|
102
|
+
classDescBox,
|
|
103
|
+
classDescBoxToggle,
|
|
104
|
+
classBelowBox,
|
|
105
|
+
classValidationBox,
|
|
106
|
+
style = "",
|
|
107
|
+
//
|
|
108
|
+
placeholder,
|
|
109
|
+
openLabel,
|
|
110
|
+
classChip,
|
|
111
|
+
chipIntent,
|
|
112
|
+
onOpen,
|
|
113
|
+
onRemove,
|
|
114
|
+
}: Props = $props();
|
|
115
|
+
|
|
116
|
+
// chips sit one step smaller than the field they live in
|
|
117
|
+
const PILL_SIZE: Record<string, string> = { sm: "sm", md: "sm", lg: "md" };
|
|
118
|
+
let pillSize = $derived(PILL_SIZE[renderSize] ?? "sm");
|
|
119
|
+
|
|
120
|
+
let validation: ValidationResult | undefined = $state();
|
|
121
|
+
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
122
|
+
|
|
123
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
124
|
+
let rowEl: HTMLDivElement | undefined = $state();
|
|
125
|
+
let openBtnEl: HTMLElement | undefined = $state();
|
|
126
|
+
|
|
127
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
128
|
+
export function validate(): ValidationResult | undefined {
|
|
129
|
+
_doValidate?.();
|
|
130
|
+
return validation;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
134
|
+
export function clearValidation(): void {
|
|
135
|
+
validation = undefined;
|
|
136
|
+
input?.setCustomValidity?.("");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
140
|
+
export function getValidation(): ValidationResult | undefined {
|
|
141
|
+
return validation;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Focus the trailing "open" button (the hidden input cannot be focused). */
|
|
145
|
+
export function focus(): void {
|
|
146
|
+
openBtnEl?.focus?.();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
150
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
151
|
+
rowEl?.scrollIntoView?.({
|
|
152
|
+
behavior: "smooth",
|
|
153
|
+
block: "center",
|
|
154
|
+
...opts,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// the empty part of the row is a pointer convenience: chips own their own clicks and
|
|
159
|
+
// the trailing button is the keyboard-reachable opener
|
|
160
|
+
function onRowClick(e: MouseEvent) {
|
|
161
|
+
if (disabled) return;
|
|
162
|
+
if ((e.target as Element | null)?.closest?.(".stuic-pill")) return;
|
|
163
|
+
onOpen();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// keep keyboard focus inside the field after a removal: the × now sitting at the same
|
|
167
|
+
// index, else the trailing button
|
|
168
|
+
function remove(chip: FieldLikeChip, index: number) {
|
|
169
|
+
onRemove(chip, index);
|
|
170
|
+
tick().then(() => {
|
|
171
|
+
const btns =
|
|
172
|
+
rowEl?.querySelectorAll<HTMLButtonElement>(".stuic-pill-dismiss") ?? [];
|
|
173
|
+
const next = btns[Math.min(index, btns.length - 1)];
|
|
174
|
+
(next ?? openBtnEl)?.focus?.();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<InputWrap
|
|
180
|
+
{description}
|
|
181
|
+
class={classProp}
|
|
182
|
+
size={renderSize}
|
|
183
|
+
{id}
|
|
184
|
+
{label}
|
|
185
|
+
{labelAfter}
|
|
186
|
+
{below}
|
|
187
|
+
{required}
|
|
188
|
+
{disabled}
|
|
189
|
+
{labelLeft}
|
|
190
|
+
{labelLeftWidth}
|
|
191
|
+
{labelLeftBreakpoint}
|
|
192
|
+
{classLabel}
|
|
193
|
+
{classLabelBox}
|
|
194
|
+
{classInputBox}
|
|
195
|
+
{classInputBoxWrap}
|
|
196
|
+
{classInputBoxWrapInvalid}
|
|
197
|
+
{classDescBox}
|
|
198
|
+
{classDescBoxToggle}
|
|
199
|
+
{classBelowBox}
|
|
200
|
+
{classValidationBox}
|
|
201
|
+
{validation}
|
|
202
|
+
{style}
|
|
203
|
+
>
|
|
204
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
205
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
206
|
+
<div
|
|
207
|
+
bind:this={rowEl}
|
|
208
|
+
class="stuic-field-options-chips"
|
|
209
|
+
data-size={renderSize}
|
|
210
|
+
data-empty={chips.length ? undefined : "true"}
|
|
211
|
+
onclick={onRowClick}
|
|
212
|
+
>
|
|
213
|
+
{#each chips as chip, i (chip.key)}
|
|
214
|
+
<Pill
|
|
215
|
+
dismissible
|
|
216
|
+
size={pillSize}
|
|
217
|
+
intent={chipIntent}
|
|
218
|
+
class={classChip}
|
|
219
|
+
dismissLabel={chip.removeLabel}
|
|
220
|
+
ondismiss={() => remove(chip, i)}
|
|
221
|
+
{disabled}
|
|
222
|
+
title={chip.label}
|
|
223
|
+
>
|
|
224
|
+
<!-- own span so an over-long label truncates (the Pill body is a flex row) -->
|
|
225
|
+
<span class="stuic-field-options-chip-label">{chip.label}</span>
|
|
226
|
+
</Pill>
|
|
227
|
+
{/each}
|
|
228
|
+
{#if !chips.length && placeholder}
|
|
229
|
+
<span class="stuic-field-options-chips-placeholder">{placeholder}</span>
|
|
230
|
+
{/if}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{#snippet inputAfter()}
|
|
234
|
+
<div class="flex items-center pr-1">
|
|
235
|
+
<Button
|
|
236
|
+
bind:el={openBtnEl}
|
|
237
|
+
iconButton
|
|
238
|
+
variant="ghost"
|
|
239
|
+
roundedFull
|
|
240
|
+
type="button"
|
|
241
|
+
size={renderSize === "lg" ? "md" : "sm"}
|
|
242
|
+
aria-label={openLabel}
|
|
243
|
+
tooltip={openLabel}
|
|
244
|
+
{disabled}
|
|
245
|
+
{tabindex}
|
|
246
|
+
onclick={() => onOpen()}
|
|
247
|
+
>
|
|
248
|
+
{@html iconSearch({ size: 18 })}
|
|
249
|
+
</Button>
|
|
250
|
+
</div>
|
|
251
|
+
{/snippet}
|
|
252
|
+
|
|
253
|
+
<input
|
|
254
|
+
bind:value
|
|
255
|
+
bind:this={input}
|
|
256
|
+
type="hidden"
|
|
257
|
+
{id}
|
|
258
|
+
{name}
|
|
259
|
+
use:validateAction={() => ({
|
|
260
|
+
enabled: validateProp !== false,
|
|
261
|
+
...(typeof validateProp === "boolean"
|
|
262
|
+
? {
|
|
263
|
+
// Return actual messages (not reason names) because hidden inputs
|
|
264
|
+
// don't support el.validationMessage - the validate action preserves
|
|
265
|
+
// our return value and uses it directly as the error message.
|
|
266
|
+
customValidator(val, ctx, el) {
|
|
267
|
+
if (required && !val)
|
|
268
|
+
return "This field requires attention. Please review and try again.";
|
|
269
|
+
|
|
270
|
+
// also, by default, JSON validation is built in
|
|
271
|
+
try {
|
|
272
|
+
JSON.parse(val as string);
|
|
273
|
+
return "";
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return "This field is invalid. Please review and try again.";
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
: validateProp),
|
|
280
|
+
setValidationResult,
|
|
281
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
282
|
+
})}
|
|
283
|
+
{required}
|
|
284
|
+
{disabled}
|
|
285
|
+
/>
|
|
286
|
+
</InputWrap>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ValidateOptions } from "../../../actions/validate.svelte.js";
|
|
3
|
+
import type { IntentColorKey } from "../../../utils/design-tokens.js";
|
|
4
|
+
import type { THC } from "../../Thc/Thc.svelte";
|
|
5
|
+
import type { InputWrapClassProps } from "../types.js";
|
|
6
|
+
type SnippetWithId = Snippet<[{
|
|
7
|
+
id: string;
|
|
8
|
+
}]>;
|
|
9
|
+
/** One rendered chip. `key` must be unique within the row. */
|
|
10
|
+
export interface FieldLikeChip {
|
|
11
|
+
key: string | number;
|
|
12
|
+
label: string;
|
|
13
|
+
/** Accessible name of the chip's remove (×) button, e.g. "Remove Foo" */
|
|
14
|
+
removeLabel: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Internal: the `chips` trigger of `FieldOptions`. The FieldLikeButton counterpart for a
|
|
18
|
+
* selection shown as inline, individually removable Pills — same InputWrap shell, same
|
|
19
|
+
* hidden input + validate wiring, same imperative API.
|
|
20
|
+
*/
|
|
21
|
+
export interface Props extends InputWrapClassProps {
|
|
22
|
+
/** The hidden input carrying `value` + `name` (bindable) */
|
|
23
|
+
input?: HTMLInputElement;
|
|
24
|
+
value: string;
|
|
25
|
+
/** The chips to render (the parent derives them from `value`) */
|
|
26
|
+
chips: FieldLikeChip[];
|
|
27
|
+
label?: SnippetWithId | THC;
|
|
28
|
+
description?: SnippetWithId | THC;
|
|
29
|
+
class?: string;
|
|
30
|
+
id?: string;
|
|
31
|
+
tabindex?: number;
|
|
32
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
33
|
+
name?: string;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
37
|
+
labelAfter?: SnippetWithId | THC;
|
|
38
|
+
below?: SnippetWithId | THC;
|
|
39
|
+
labelLeft?: boolean;
|
|
40
|
+
labelLeftWidth?: "normal" | "wide";
|
|
41
|
+
labelLeftBreakpoint?: number;
|
|
42
|
+
style?: string;
|
|
43
|
+
/** Shown in the row while there are no chips */
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
/** Accessible name (and tooltip) of the trailing "open" button */
|
|
46
|
+
openLabel: string;
|
|
47
|
+
/** Classes for each chip (Pill) */
|
|
48
|
+
classChip?: string;
|
|
49
|
+
/** Pill intent of the chips */
|
|
50
|
+
chipIntent?: IntentColorKey;
|
|
51
|
+
/** Open the picker (trailing button, or a click on the empty part of the row) */
|
|
52
|
+
onOpen: () => void;
|
|
53
|
+
/** A chip's × was pressed */
|
|
54
|
+
onRemove: (chip: FieldLikeChip, index: number) => void;
|
|
55
|
+
}
|
|
56
|
+
import { type ValidationResult } from "../../../actions/validate.svelte.js";
|
|
57
|
+
declare const FieldLikeChips: import("svelte").Component<Props, {
|
|
58
|
+
validate: () => ValidationResult | undefined;
|
|
59
|
+
clearValidation: () => void;
|
|
60
|
+
getValidation: () => ValidationResult | undefined;
|
|
61
|
+
focus: () => void;
|
|
62
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
63
|
+
}, "value" | "input">;
|
|
64
|
+
type FieldLikeChips = ReturnType<typeof FieldLikeChips>;
|
|
65
|
+
export default FieldLikeChips;
|
|
@@ -81,6 +81,8 @@
|
|
|
81
81
|
--stuic-field-options-control-ring: var(--stuic-color-ring);
|
|
82
82
|
--stuic-field-options-muted-text: var(--stuic-color-muted-foreground);
|
|
83
83
|
--stuic-field-options-optgroup-text: var(--stuic-color-muted-foreground);
|
|
84
|
+
/* FieldOptions `chips` display mode */
|
|
85
|
+
--stuic-field-options-chips-gap: 0.25rem;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
@layer components {
|
|
@@ -561,6 +563,62 @@
|
|
|
561
563
|
opacity: 1;
|
|
562
564
|
}
|
|
563
565
|
|
|
566
|
+
/* `chips` display mode: the selection as inline, removable Pills (FieldLikeChips) */
|
|
567
|
+
.stuic-field-options-chips {
|
|
568
|
+
display: flex;
|
|
569
|
+
flex: 1 1 0%;
|
|
570
|
+
min-width: 0;
|
|
571
|
+
flex-wrap: wrap;
|
|
572
|
+
align-items: center;
|
|
573
|
+
gap: var(--stuic-field-options-chips-gap);
|
|
574
|
+
cursor: pointer;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.stuic-input.disabled .stuic-field-options-chips {
|
|
578
|
+
cursor: not-allowed;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/* An over-long label must not push the chip out of the field: cap the chip at the row
|
|
582
|
+
width and let the label span truncate (the × keeps its size) */
|
|
583
|
+
.stuic-field-options-chips .stuic-pill {
|
|
584
|
+
max-width: 100%;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.stuic-field-options-chip-label {
|
|
588
|
+
min-width: 0;
|
|
589
|
+
overflow: hidden;
|
|
590
|
+
text-overflow: ellipsis;
|
|
591
|
+
white-space: nowrap;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* Row metrics follow the input size tokens so the field lines up with its siblings */
|
|
595
|
+
.stuic-field-options-chips[data-size="sm"] {
|
|
596
|
+
padding: calc(var(--stuic-input-padding-y-sm) / 2) var(--stuic-input-padding-x-sm);
|
|
597
|
+
font-size: var(--stuic-input-font-size-sm);
|
|
598
|
+
min-height: var(--stuic-input-min-height-sm);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.stuic-field-options-chips[data-size="md"],
|
|
602
|
+
.stuic-field-options-chips:not([data-size]) {
|
|
603
|
+
padding: calc(var(--stuic-input-padding-y-md) / 2) var(--stuic-input-padding-x-md);
|
|
604
|
+
font-size: var(--stuic-input-font-size-md);
|
|
605
|
+
min-height: var(--stuic-input-min-height-md);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.stuic-field-options-chips[data-size="lg"] {
|
|
609
|
+
padding: calc(var(--stuic-input-padding-y-lg) / 2) var(--stuic-input-padding-x-lg);
|
|
610
|
+
font-size: var(--stuic-input-font-size-lg);
|
|
611
|
+
min-height: var(--stuic-input-min-height-lg);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.stuic-field-options-chips-placeholder {
|
|
615
|
+
color: var(
|
|
616
|
+
--stuic-field-options-chips-placeholder-text,
|
|
617
|
+
var(--stuic-input-placeholder)
|
|
618
|
+
);
|
|
619
|
+
user-select: none;
|
|
620
|
+
}
|
|
621
|
+
|
|
564
622
|
/* ============================================================================
|
|
565
623
|
FIELD INPUT LOCALIZED
|
|
566
624
|
============================================================================ */
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import type { PagingCalcResult } from "@marianmeres/paging-store";
|
|
5
|
+
import type { TranslateFn } from "../../types.js";
|
|
6
|
+
import type { ButtonSize } from "../Button/Button.svelte";
|
|
7
|
+
|
|
8
|
+
export type PaginationVariant = "compact" | "numbers";
|
|
9
|
+
|
|
10
|
+
export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
|
|
11
|
+
/**
|
|
12
|
+
* Paging calculation result from `@marianmeres/paging-store` — the exact shape
|
|
13
|
+
* `DataTable` consumes, so both can be fed from the same store or `calculatePaging`
|
|
14
|
+
* call. Takes precedence over the `total`/`limit`/`offset` convenience props.
|
|
15
|
+
*/
|
|
16
|
+
paging?: PagingCalcResult;
|
|
17
|
+
/** Convenience alternative to `paging`: total number of items in the dataset. */
|
|
18
|
+
total?: number;
|
|
19
|
+
/** Convenience alternative to `paging`: items per page (default 10). */
|
|
20
|
+
limit?: number;
|
|
21
|
+
/** Convenience alternative to `paging`: current offset (default 0). */
|
|
22
|
+
offset?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Callback when the user navigates to a different page. First argument is the new
|
|
25
|
+
* offset (`DataTable`'s `onPageChange` signature), second is the 1-based target page.
|
|
26
|
+
*/
|
|
27
|
+
onPageChange?: (offset: number, page: number) => void;
|
|
28
|
+
/**
|
|
29
|
+
* `"compact"` (default): prev / "Page X of Y" / next — the `DataTable` pager.
|
|
30
|
+
* `"numbers"`: windowed page-number buttons with ellipsis gaps.
|
|
31
|
+
*/
|
|
32
|
+
variant?: PaginationVariant;
|
|
33
|
+
/** Numbers variant: pages always shown around the current page (default 1). */
|
|
34
|
+
siblingCount?: number;
|
|
35
|
+
/** Numbers variant: pages always shown at the start and at the end (default 1). */
|
|
36
|
+
boundaryCount?: number;
|
|
37
|
+
/** Render first/last jump buttons (default false). */
|
|
38
|
+
showFirstLast?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Render the "Page X of Y" info. Defaults per variant: `true` for compact (between
|
|
41
|
+
* prev/next), `false` for numbers (after the buttons when enabled).
|
|
42
|
+
*/
|
|
43
|
+
showInfo?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Render nothing when there is at most one page (default true — the `DataTable`
|
|
46
|
+
* rule: one page of results needs no pager). Set `false` to keep it visible.
|
|
47
|
+
*/
|
|
48
|
+
hideSinglePage?: boolean;
|
|
49
|
+
/** Disable all controls (e.g. while a page of data is loading). */
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
/** Size preset passed to the inner Buttons (default "sm", as in `DataTable`). */
|
|
52
|
+
size?: ButtonSize;
|
|
53
|
+
/** i18n translate function (see `createPaginationT`). */
|
|
54
|
+
t?: TranslateFn;
|
|
55
|
+
/** Override the "Page X of Y" info content. */
|
|
56
|
+
renderInfo?: Snippet<[PagingCalcResult]>;
|
|
57
|
+
/** Skip all default styling (inner Buttons keep their own Button styling) */
|
|
58
|
+
unstyled?: boolean;
|
|
59
|
+
/** Additional CSS classes */
|
|
60
|
+
class?: string;
|
|
61
|
+
/** Class for every inner button */
|
|
62
|
+
classButton?: string;
|
|
63
|
+
/** Extra class for the current-page button (numbers variant) */
|
|
64
|
+
classButtonCurrent?: string;
|
|
65
|
+
/** Class for the "Page X of Y" info */
|
|
66
|
+
classInfo?: string;
|
|
67
|
+
/** Bindable element reference */
|
|
68
|
+
el?: HTMLElement;
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<script lang="ts">
|
|
73
|
+
import { calculatePaging } from "@marianmeres/paging-store";
|
|
74
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
75
|
+
import Button from "../Button/Button.svelte";
|
|
76
|
+
import { t_default } from "./i18n.js";
|
|
77
|
+
import { paginationRange } from "./pagination-range.js";
|
|
78
|
+
|
|
79
|
+
let {
|
|
80
|
+
paging,
|
|
81
|
+
total,
|
|
82
|
+
limit,
|
|
83
|
+
offset,
|
|
84
|
+
onPageChange,
|
|
85
|
+
variant = "compact",
|
|
86
|
+
siblingCount = 1,
|
|
87
|
+
boundaryCount = 1,
|
|
88
|
+
showFirstLast = false,
|
|
89
|
+
showInfo,
|
|
90
|
+
hideSinglePage = true,
|
|
91
|
+
disabled = false,
|
|
92
|
+
size = "sm",
|
|
93
|
+
t = t_default,
|
|
94
|
+
renderInfo,
|
|
95
|
+
unstyled = false,
|
|
96
|
+
class: classProp,
|
|
97
|
+
classButton: classButtonProp,
|
|
98
|
+
classButtonCurrent: classButtonCurrentProp,
|
|
99
|
+
classInfo: classInfoProp,
|
|
100
|
+
el = $bindable(),
|
|
101
|
+
...rest
|
|
102
|
+
}: Props = $props();
|
|
103
|
+
|
|
104
|
+
let _paging: PagingCalcResult | undefined = $derived(
|
|
105
|
+
paging ??
|
|
106
|
+
(total != null || limit != null || offset != null
|
|
107
|
+
? calculatePaging({ total, limit, offset })
|
|
108
|
+
: undefined)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
let _render = $derived(!!_paging && (_paging.pageCount > 1 || !hideSinglePage));
|
|
112
|
+
let _showInfo = $derived(showInfo ?? variant === "compact");
|
|
113
|
+
|
|
114
|
+
let _pages = $derived(
|
|
115
|
+
variant === "numbers" && _paging
|
|
116
|
+
? paginationRange(_paging.currentPage, _paging.pageCount, {
|
|
117
|
+
siblingCount,
|
|
118
|
+
boundaryCount,
|
|
119
|
+
})
|
|
120
|
+
: []
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
let _class = $derived(unstyled ? classProp : twMerge("stuic-pagination", classProp));
|
|
124
|
+
let _classButton = $derived(
|
|
125
|
+
unstyled ? classButtonProp : twMerge("stuic-pagination-button", classButtonProp)
|
|
126
|
+
);
|
|
127
|
+
let _classInfo = $derived(
|
|
128
|
+
unstyled ? classInfoProp : twMerge("stuic-pagination-info", classInfoProp)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
function goToPage(page: number) {
|
|
132
|
+
if (!_paging || disabled) return;
|
|
133
|
+
if (page < 1 || page > _paging.pageCount || page === _paging.currentPage) return;
|
|
134
|
+
onPageChange?.((page - 1) * _paging.limit, page);
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
{#if _paging && _render}
|
|
139
|
+
<nav
|
|
140
|
+
bind:this={el}
|
|
141
|
+
class={_class}
|
|
142
|
+
data-variant={!unstyled ? variant : undefined}
|
|
143
|
+
aria-label={t("pagination", null, "Pagination")}
|
|
144
|
+
{...rest}
|
|
145
|
+
>
|
|
146
|
+
{#if showFirstLast}
|
|
147
|
+
<Button
|
|
148
|
+
variant="ghost"
|
|
149
|
+
{size}
|
|
150
|
+
class={_classButton}
|
|
151
|
+
disabled={disabled || !_paging.hasPrevious}
|
|
152
|
+
onclick={() => goToPage(1)}
|
|
153
|
+
aria-label={t("first_page")}
|
|
154
|
+
>
|
|
155
|
+
«
|
|
156
|
+
</Button>
|
|
157
|
+
{/if}
|
|
158
|
+
|
|
159
|
+
<Button
|
|
160
|
+
variant="ghost"
|
|
161
|
+
{size}
|
|
162
|
+
class={_classButton}
|
|
163
|
+
disabled={disabled || !_paging.hasPrevious}
|
|
164
|
+
onclick={() => goToPage(_paging!.currentPage - 1)}
|
|
165
|
+
aria-label={t("previous_page")}
|
|
166
|
+
>
|
|
167
|
+
{#if variant === "compact"}
|
|
168
|
+
‹ {t("previous_page")}
|
|
169
|
+
{:else}
|
|
170
|
+
‹
|
|
171
|
+
{/if}
|
|
172
|
+
</Button>
|
|
173
|
+
|
|
174
|
+
{#if variant === "numbers"}
|
|
175
|
+
{#each _pages as item, i (item === "ellipsis" ? `ellipsis-${i}` : item)}
|
|
176
|
+
{#if item === "ellipsis"}
|
|
177
|
+
<span
|
|
178
|
+
class={!unstyled ? "stuic-pagination-ellipsis" : undefined}
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
>
|
|
181
|
+
…
|
|
182
|
+
</span>
|
|
183
|
+
{:else}
|
|
184
|
+
{@const isCurrent = item === _paging.currentPage}
|
|
185
|
+
<Button
|
|
186
|
+
variant={isCurrent ? "solid" : "ghost"}
|
|
187
|
+
intent={isCurrent ? "primary" : undefined}
|
|
188
|
+
{size}
|
|
189
|
+
class={isCurrent
|
|
190
|
+
? twMerge(_classButton, classButtonCurrentProp)
|
|
191
|
+
: _classButton}
|
|
192
|
+
{disabled}
|
|
193
|
+
aria-label={t("go_to_page_x", { page: item })}
|
|
194
|
+
aria-current={isCurrent ? "page" : undefined}
|
|
195
|
+
data-current={isCurrent ? "true" : undefined}
|
|
196
|
+
onclick={() => goToPage(item)}
|
|
197
|
+
>
|
|
198
|
+
{item}
|
|
199
|
+
</Button>
|
|
200
|
+
{/if}
|
|
201
|
+
{/each}
|
|
202
|
+
{:else if _showInfo}
|
|
203
|
+
<span class={_classInfo}>
|
|
204
|
+
{#if renderInfo}
|
|
205
|
+
{@render renderInfo(_paging)}
|
|
206
|
+
{:else}
|
|
207
|
+
{t("page_x_of_y", { page: _paging.currentPage, pageCount: _paging.pageCount })}
|
|
208
|
+
{/if}
|
|
209
|
+
</span>
|
|
210
|
+
{/if}
|
|
211
|
+
|
|
212
|
+
<Button
|
|
213
|
+
variant="ghost"
|
|
214
|
+
{size}
|
|
215
|
+
class={_classButton}
|
|
216
|
+
disabled={disabled || !_paging.hasNext}
|
|
217
|
+
onclick={() => goToPage(_paging!.currentPage + 1)}
|
|
218
|
+
aria-label={t("next_page")}
|
|
219
|
+
>
|
|
220
|
+
{#if variant === "compact"}
|
|
221
|
+
{t("next_page")} ›
|
|
222
|
+
{:else}
|
|
223
|
+
›
|
|
224
|
+
{/if}
|
|
225
|
+
</Button>
|
|
226
|
+
|
|
227
|
+
{#if showFirstLast}
|
|
228
|
+
<Button
|
|
229
|
+
variant="ghost"
|
|
230
|
+
{size}
|
|
231
|
+
class={_classButton}
|
|
232
|
+
disabled={disabled || !_paging.hasNext}
|
|
233
|
+
onclick={() => goToPage(_paging!.pageCount)}
|
|
234
|
+
aria-label={t("last_page")}
|
|
235
|
+
>
|
|
236
|
+
»
|
|
237
|
+
</Button>
|
|
238
|
+
{/if}
|
|
239
|
+
|
|
240
|
+
{#if variant === "numbers" && _showInfo}
|
|
241
|
+
<span class={_classInfo}>
|
|
242
|
+
{#if renderInfo}
|
|
243
|
+
{@render renderInfo(_paging)}
|
|
244
|
+
{:else}
|
|
245
|
+
{t("page_x_of_y", { page: _paging.currentPage, pageCount: _paging.pageCount })}
|
|
246
|
+
{/if}
|
|
247
|
+
</span>
|
|
248
|
+
{/if}
|
|
249
|
+
</nav>
|
|
250
|
+
{/if}
|