@marianmeres/stuic 3.170.0 → 3.172.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 +10 -8
- package/API.md +90 -0
- package/README.md +1 -1
- package/dist/components/AssetsPreview/AssetsPreview.fixture.svelte +1 -1
- package/dist/components/Calendar/Calendar.svelte +799 -0
- package/dist/components/Calendar/Calendar.svelte.d.ts +135 -0
- package/dist/components/Calendar/README.md +294 -0
- package/dist/components/Calendar/calendar-i18n-sk.d.ts +20 -0
- package/dist/components/Calendar/calendar-i18n-sk.js +46 -0
- package/dist/components/Calendar/calendar-i18n.d.ts +61 -0
- package/dist/components/Calendar/calendar-i18n.js +73 -0
- package/dist/components/Calendar/index.css +307 -0
- package/dist/components/Calendar/index.d.ts +5 -0
- package/dist/components/Calendar/index.js +5 -0
- package/dist/components/Calendar/iso-date.d.ts +107 -0
- package/dist/components/Calendar/iso-date.js +247 -0
- package/dist/components/CommandMenu/CommandMenu.fixture.svelte +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +1 -1
- package/dist/components/ContextMenu/README.md +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.svelte +57 -3
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +4 -2
- package/dist/components/DropdownMenu/README.md +1 -0
- package/dist/components/Input/FieldDate.svelte +349 -0
- package/dist/components/Input/FieldDate.svelte.d.ts +79 -0
- package/dist/components/Input/FieldDateRange.svelte +373 -0
- package/dist/components/Input/FieldDateRange.svelte.d.ts +90 -0
- package/dist/components/Input/README.md +149 -16
- package/dist/components/Input/_internal/FieldDateShell.svelte +327 -0
- package/dist/components/Input/_internal/FieldDateShell.svelte.d.ts +66 -0
- package/dist/components/Input/index.css +119 -0
- package/dist/components/Input/index.d.ts +2 -0
- package/dist/components/Input/index.js +2 -0
- package/dist/components/ModalDialog/ModalDialog.fixture.svelte +1 -1
- package/dist/components/SlidingPanels/SlidingPanels.fixture.svelte +1 -1
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/docs/_archive/README.md +10 -0
- package/docs/{component-testing → _archive/component-testing}/00-overview-and-roadmap.md +8 -6
- package/docs/{component-testing → _archive/component-testing}/01-framework-setup.md +4 -2
- package/docs/{component-testing → _archive/component-testing}/03-component-coverage-roadmap.md +3 -1
- package/docs/{component-testing → _archive/component-testing}/04-hard-cases-and-e2e.md +5 -3
- package/docs/{component-testing → _archive/component-testing}/05-ci.md +2 -0
- package/docs/{component-testing → _archive/component-testing}/PROGRESS.md +3 -1
- package/docs/_archive/component-testing/README.md +28 -0
- package/docs/{upgrading.md → _archive/upgrading.md} +2 -0
- package/docs/architecture.md +19 -11
- package/docs/domains/actions.md +4 -3
- package/docs/domains/components.md +22 -19
- package/docs/domains/utils.md +2 -1
- package/docs/maybe-todo.md +6 -6
- package/docs/tasks.md +19 -9
- package/docs/{component-testing/02-test-conventions.md → testing-components.md} +30 -31
- package/docs/testing.md +3 -3
- package/package.json +2 -1
- package/docs/component-testing/README.md +0 -38
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Internal: the field scaffolding shared by FieldDate and FieldDateRange — the
|
|
3
|
+
InputWrap shell, the trigger button (icon + formatted value / placeholder + a
|
|
4
|
+
trailing × that clears), the hidden inputs that carry the ISO value(s) into
|
|
5
|
+
FormData (the first one runs the `validate` action), and the picker's two
|
|
6
|
+
homes: inline inside the field (`embedded`) or a native <dialog> opened by the
|
|
7
|
+
trigger. The picker itself is the `children` snippet; the owning field decides
|
|
8
|
+
what it is and what it means.
|
|
9
|
+
|
|
10
|
+
WHY hidden inputs: the visible trigger is a <button>, so nothing would be
|
|
11
|
+
submitted otherwise — and a hidden input is barred from native constraint
|
|
12
|
+
validation, which is why `required` / min / max are enforced by the owning
|
|
13
|
+
field's customValidator (same as FieldPhoneNumber / FieldCountry / …).
|
|
14
|
+
-->
|
|
15
|
+
<script lang="ts" module>
|
|
16
|
+
import type { Snippet } from "svelte";
|
|
17
|
+
import type { ValidateOptions } from "../../../actions/validate.svelte.js";
|
|
18
|
+
import type { THC } from "../../Thc/Thc.svelte";
|
|
19
|
+
import type { InputWrapClassProps } from "../types.js";
|
|
20
|
+
|
|
21
|
+
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
22
|
+
|
|
23
|
+
/** One hidden `<input>` the field submits. The first one carries the `validate` action. */
|
|
24
|
+
export interface FieldDateHiddenInput {
|
|
25
|
+
name?: string;
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Props extends InputWrapClassProps {
|
|
30
|
+
id: string;
|
|
31
|
+
label?: SnippetWithId | THC;
|
|
32
|
+
description?: SnippetWithId | THC;
|
|
33
|
+
labelAfter?: SnippetWithId | THC;
|
|
34
|
+
below?: SnippetWithId | THC;
|
|
35
|
+
class?: string;
|
|
36
|
+
style?: string;
|
|
37
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
38
|
+
required?: boolean;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
tabindex?: number;
|
|
41
|
+
labelLeft?: boolean;
|
|
42
|
+
labelLeftWidth?: "normal" | "wide";
|
|
43
|
+
labelLeftBreakpoint?: number;
|
|
44
|
+
/** Formatted value shown in the trigger (`""` = empty → placeholder) */
|
|
45
|
+
display: string;
|
|
46
|
+
placeholder: string;
|
|
47
|
+
/** Show the trailing × while there is a value */
|
|
48
|
+
clearable: boolean;
|
|
49
|
+
clearLabel: string;
|
|
50
|
+
/** Render the picker inline inside the field instead of trigger + dialog */
|
|
51
|
+
embedded: boolean;
|
|
52
|
+
inputs: FieldDateHiddenInput[];
|
|
53
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
54
|
+
dialogTitle: string;
|
|
55
|
+
closeLabel: string;
|
|
56
|
+
noScrollLock?: boolean;
|
|
57
|
+
/** Classes for the trigger button */
|
|
58
|
+
classInput?: string;
|
|
59
|
+
/** Classes for the dialog box */
|
|
60
|
+
classDialog?: string;
|
|
61
|
+
/** The first hidden input (bindable) */
|
|
62
|
+
input?: HTMLInputElement;
|
|
63
|
+
onOpen?: () => void;
|
|
64
|
+
onClose?: () => void;
|
|
65
|
+
onClear: () => void;
|
|
66
|
+
/** The picker */
|
|
67
|
+
children: Snippet;
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<script lang="ts">
|
|
72
|
+
import {
|
|
73
|
+
validate as validateAction,
|
|
74
|
+
type ValidationResult,
|
|
75
|
+
} from "../../../actions/validate.svelte.js";
|
|
76
|
+
import { iconCalendar, iconX } from "../../../icons/index.js";
|
|
77
|
+
import { twMerge } from "../../../utils/tw-merge.js";
|
|
78
|
+
import Button from "../../Button/Button.svelte";
|
|
79
|
+
import ModalDialog from "../../ModalDialog/ModalDialog.svelte";
|
|
80
|
+
import InputWrap from "./InputWrap.svelte";
|
|
81
|
+
|
|
82
|
+
let {
|
|
83
|
+
id,
|
|
84
|
+
label = "",
|
|
85
|
+
description,
|
|
86
|
+
labelAfter,
|
|
87
|
+
below,
|
|
88
|
+
class: classProp,
|
|
89
|
+
style,
|
|
90
|
+
renderSize = "md",
|
|
91
|
+
required = false,
|
|
92
|
+
disabled = false,
|
|
93
|
+
tabindex = 0,
|
|
94
|
+
labelLeft = false,
|
|
95
|
+
labelLeftWidth = "normal",
|
|
96
|
+
labelLeftBreakpoint = 480,
|
|
97
|
+
classLabel,
|
|
98
|
+
classLabelBox,
|
|
99
|
+
classInputBox,
|
|
100
|
+
classInputBoxWrap,
|
|
101
|
+
classInputBoxWrapInvalid,
|
|
102
|
+
classDescBox,
|
|
103
|
+
classDescBoxToggle,
|
|
104
|
+
classBelowBox,
|
|
105
|
+
classValidationBox,
|
|
106
|
+
display,
|
|
107
|
+
placeholder,
|
|
108
|
+
clearable,
|
|
109
|
+
clearLabel,
|
|
110
|
+
embedded,
|
|
111
|
+
inputs,
|
|
112
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
113
|
+
validate: validateProp,
|
|
114
|
+
dialogTitle,
|
|
115
|
+
closeLabel,
|
|
116
|
+
noScrollLock = false,
|
|
117
|
+
classInput,
|
|
118
|
+
classDialog,
|
|
119
|
+
input = $bindable(),
|
|
120
|
+
onOpen,
|
|
121
|
+
onClose,
|
|
122
|
+
onClear,
|
|
123
|
+
children,
|
|
124
|
+
}: Props = $props();
|
|
125
|
+
|
|
126
|
+
let validation: ValidationResult | undefined = $state();
|
|
127
|
+
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
128
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
129
|
+
|
|
130
|
+
// Internal refs; the public `input` bindable is a write-out mirror.
|
|
131
|
+
let hiddenEl: HTMLInputElement | undefined = $state();
|
|
132
|
+
let triggerEl: HTMLButtonElement | undefined = $state();
|
|
133
|
+
let embeddedEl: HTMLDivElement | undefined = $state();
|
|
134
|
+
let dialog: ModalDialog | undefined = $state();
|
|
135
|
+
|
|
136
|
+
$effect(() => {
|
|
137
|
+
input = hiddenEl;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
let isOpen = $derived(!embedded && !!dialog?.visibility().visible);
|
|
141
|
+
|
|
142
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
143
|
+
export function validate(): ValidationResult | undefined {
|
|
144
|
+
_doValidate?.();
|
|
145
|
+
return validation;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
149
|
+
export function clearValidation(): void {
|
|
150
|
+
validation = undefined;
|
|
151
|
+
hiddenEl?.setCustomValidity?.("");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
155
|
+
export function getValidation(): ValidationResult | undefined {
|
|
156
|
+
return validation;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Focus the trigger (dialog mode) or the picker's tabbable day (embedded). */
|
|
160
|
+
export function focus(): void {
|
|
161
|
+
if (embedded) embeddedEl?.querySelector<HTMLElement>('[tabindex="0"]')?.focus?.();
|
|
162
|
+
else triggerEl?.focus?.();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
166
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
167
|
+
(embedded ? embeddedEl : triggerEl)?.scrollIntoView?.({
|
|
168
|
+
behavior: "smooth",
|
|
169
|
+
block: "center",
|
|
170
|
+
...opts,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Re-run validation the way a user edit would (the action listens on `change`). */
|
|
175
|
+
export function dispatchChange(): void {
|
|
176
|
+
hiddenEl?.dispatchEvent(new Event("change", { bubbles: true }));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Open the picker dialog (no-op when `embedded` or `disabled`). */
|
|
180
|
+
export function open(): void {
|
|
181
|
+
if (embedded || disabled) return;
|
|
182
|
+
dialog?.open(triggerEl ?? null);
|
|
183
|
+
onOpen?.();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Close the picker dialog. */
|
|
187
|
+
export function close(): void {
|
|
188
|
+
dialog?.close();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Whether the picker dialog is currently open. */
|
|
192
|
+
export function isDialogOpen(): boolean {
|
|
193
|
+
return isOpen;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function clear() {
|
|
197
|
+
onClear();
|
|
198
|
+
triggerEl?.focus?.();
|
|
199
|
+
}
|
|
200
|
+
</script>
|
|
201
|
+
|
|
202
|
+
<InputWrap
|
|
203
|
+
{description}
|
|
204
|
+
class={classProp}
|
|
205
|
+
size={renderSize}
|
|
206
|
+
{id}
|
|
207
|
+
{label}
|
|
208
|
+
{labelAfter}
|
|
209
|
+
{below}
|
|
210
|
+
{required}
|
|
211
|
+
{disabled}
|
|
212
|
+
{labelLeft}
|
|
213
|
+
{labelLeftWidth}
|
|
214
|
+
{labelLeftBreakpoint}
|
|
215
|
+
{classLabel}
|
|
216
|
+
{classLabelBox}
|
|
217
|
+
{classInputBox}
|
|
218
|
+
{classInputBoxWrap}
|
|
219
|
+
{classInputBoxWrapInvalid}
|
|
220
|
+
{classDescBox}
|
|
221
|
+
{classDescBoxToggle}
|
|
222
|
+
{classBelowBox}
|
|
223
|
+
{classValidationBox}
|
|
224
|
+
{validation}
|
|
225
|
+
{style}
|
|
226
|
+
>
|
|
227
|
+
{#if embedded}
|
|
228
|
+
<div bind:this={embeddedEl} class="stuic-field-date-embedded" data-size={renderSize}>
|
|
229
|
+
{@render children()}
|
|
230
|
+
</div>
|
|
231
|
+
{:else}
|
|
232
|
+
<button
|
|
233
|
+
bind:this={triggerEl}
|
|
234
|
+
type="button"
|
|
235
|
+
{id}
|
|
236
|
+
class={twMerge("stuic-field-date-trigger", classInput)}
|
|
237
|
+
data-size={renderSize}
|
|
238
|
+
data-empty={display ? undefined : ""}
|
|
239
|
+
aria-haspopup="dialog"
|
|
240
|
+
aria-expanded={isOpen}
|
|
241
|
+
{disabled}
|
|
242
|
+
{tabindex}
|
|
243
|
+
onclick={open}
|
|
244
|
+
>
|
|
245
|
+
<span class="stuic-field-date-trigger-icon" aria-hidden="true">
|
|
246
|
+
{@html iconCalendar({ size: 18 })}
|
|
247
|
+
</span>
|
|
248
|
+
<span class="stuic-field-date-trigger-label">{display || placeholder}</span>
|
|
249
|
+
</button>
|
|
250
|
+
{/if}
|
|
251
|
+
|
|
252
|
+
{#snippet inputAfter()}
|
|
253
|
+
{#if !embedded && clearable && display && !disabled}
|
|
254
|
+
<div class="flex items-center pr-1">
|
|
255
|
+
<Button
|
|
256
|
+
iconButton
|
|
257
|
+
variant="ghost"
|
|
258
|
+
roundedFull
|
|
259
|
+
type="button"
|
|
260
|
+
size={renderSize === "lg" ? "md" : "sm"}
|
|
261
|
+
class="stuic-field-date-clear"
|
|
262
|
+
aria-label={clearLabel}
|
|
263
|
+
tooltip={clearLabel}
|
|
264
|
+
onclick={clear}
|
|
265
|
+
>
|
|
266
|
+
{@html iconX({ size: 16 })}
|
|
267
|
+
</Button>
|
|
268
|
+
</div>
|
|
269
|
+
{/if}
|
|
270
|
+
{/snippet}
|
|
271
|
+
|
|
272
|
+
{#each inputs as h, i (i)}
|
|
273
|
+
{#if i === 0}
|
|
274
|
+
<input
|
|
275
|
+
bind:this={hiddenEl}
|
|
276
|
+
type="hidden"
|
|
277
|
+
id={embedded ? id : undefined}
|
|
278
|
+
name={h.name}
|
|
279
|
+
value={h.value}
|
|
280
|
+
use:validateAction={() => ({
|
|
281
|
+
enabled: validateProp !== false,
|
|
282
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
283
|
+
setValidationResult,
|
|
284
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
285
|
+
})}
|
|
286
|
+
{required}
|
|
287
|
+
{disabled}
|
|
288
|
+
/>
|
|
289
|
+
{:else}
|
|
290
|
+
<input type="hidden" name={h.name} value={h.value} {disabled} />
|
|
291
|
+
{/if}
|
|
292
|
+
{/each}
|
|
293
|
+
</InputWrap>
|
|
294
|
+
|
|
295
|
+
{#if !embedded}
|
|
296
|
+
<ModalDialog
|
|
297
|
+
bind:this={dialog}
|
|
298
|
+
ariaLabelledby="{id}-dialog-title"
|
|
299
|
+
noAutoFocus
|
|
300
|
+
{noScrollLock}
|
|
301
|
+
preClose={() => {
|
|
302
|
+
onClose?.();
|
|
303
|
+
}}
|
|
304
|
+
class="bg-transparent shadow-none pointer-events-none size-auto max-w-full"
|
|
305
|
+
>
|
|
306
|
+
<div class={twMerge("stuic-field-date-dialog pointer-events-auto", classDialog)}>
|
|
307
|
+
<div class="stuic-field-date-dialog-header">
|
|
308
|
+
<span id="{id}-dialog-title" class="stuic-field-date-dialog-title"
|
|
309
|
+
>{dialogTitle}</span
|
|
310
|
+
>
|
|
311
|
+
<Button
|
|
312
|
+
x
|
|
313
|
+
size="sm"
|
|
314
|
+
variant="ghost"
|
|
315
|
+
roundedFull
|
|
316
|
+
type="button"
|
|
317
|
+
aria-label={closeLabel}
|
|
318
|
+
tooltip={closeLabel}
|
|
319
|
+
onclick={close}
|
|
320
|
+
/>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="stuic-field-date-dialog-body">
|
|
323
|
+
{@render children()}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</ModalDialog>
|
|
327
|
+
{/if}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ValidateOptions } from "../../../actions/validate.svelte.js";
|
|
3
|
+
import type { THC } from "../../Thc/Thc.svelte";
|
|
4
|
+
import type { InputWrapClassProps } from "../types.js";
|
|
5
|
+
type SnippetWithId = Snippet<[{
|
|
6
|
+
id: string;
|
|
7
|
+
}]>;
|
|
8
|
+
/** One hidden `<input>` the field submits. The first one carries the `validate` action. */
|
|
9
|
+
export interface FieldDateHiddenInput {
|
|
10
|
+
name?: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Props extends InputWrapClassProps {
|
|
14
|
+
id: string;
|
|
15
|
+
label?: SnippetWithId | THC;
|
|
16
|
+
description?: SnippetWithId | THC;
|
|
17
|
+
labelAfter?: SnippetWithId | THC;
|
|
18
|
+
below?: SnippetWithId | THC;
|
|
19
|
+
class?: string;
|
|
20
|
+
style?: string;
|
|
21
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
22
|
+
required?: boolean;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
tabindex?: number;
|
|
25
|
+
labelLeft?: boolean;
|
|
26
|
+
labelLeftWidth?: "normal" | "wide";
|
|
27
|
+
labelLeftBreakpoint?: number;
|
|
28
|
+
/** Formatted value shown in the trigger (`""` = empty → placeholder) */
|
|
29
|
+
display: string;
|
|
30
|
+
placeholder: string;
|
|
31
|
+
/** Show the trailing × while there is a value */
|
|
32
|
+
clearable: boolean;
|
|
33
|
+
clearLabel: string;
|
|
34
|
+
/** Render the picker inline inside the field instead of trigger + dialog */
|
|
35
|
+
embedded: boolean;
|
|
36
|
+
inputs: FieldDateHiddenInput[];
|
|
37
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
38
|
+
dialogTitle: string;
|
|
39
|
+
closeLabel: string;
|
|
40
|
+
noScrollLock?: boolean;
|
|
41
|
+
/** Classes for the trigger button */
|
|
42
|
+
classInput?: string;
|
|
43
|
+
/** Classes for the dialog box */
|
|
44
|
+
classDialog?: string;
|
|
45
|
+
/** The first hidden input (bindable) */
|
|
46
|
+
input?: HTMLInputElement;
|
|
47
|
+
onOpen?: () => void;
|
|
48
|
+
onClose?: () => void;
|
|
49
|
+
onClear: () => void;
|
|
50
|
+
/** The picker */
|
|
51
|
+
children: Snippet;
|
|
52
|
+
}
|
|
53
|
+
import { type ValidationResult } from "../../../actions/validate.svelte.js";
|
|
54
|
+
declare const FieldDateShell: import("svelte").Component<Props, {
|
|
55
|
+
validate: () => ValidationResult | undefined;
|
|
56
|
+
clearValidation: () => void;
|
|
57
|
+
getValidation: () => ValidationResult | undefined;
|
|
58
|
+
focus: () => void;
|
|
59
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
60
|
+
dispatchChange: () => void;
|
|
61
|
+
open: () => void;
|
|
62
|
+
close: () => void;
|
|
63
|
+
isDialogOpen: () => boolean;
|
|
64
|
+
}, "input">;
|
|
65
|
+
type FieldDateShell = ReturnType<typeof FieldDateShell>;
|
|
66
|
+
export default FieldDateShell;
|
|
@@ -83,6 +83,12 @@
|
|
|
83
83
|
--stuic-field-options-optgroup-text: var(--stuic-color-muted-foreground);
|
|
84
84
|
/* FieldOptions `chips` display mode */
|
|
85
85
|
--stuic-field-options-chips-gap: 0.25rem;
|
|
86
|
+
|
|
87
|
+
/* FieldDate / FieldDateRange specific */
|
|
88
|
+
--stuic-field-date-icon-text: var(--stuic-color-muted-foreground);
|
|
89
|
+
--stuic-field-date-placeholder-text: var(--stuic-input-placeholder);
|
|
90
|
+
--stuic-field-date-embedded-padding: 0.5rem;
|
|
91
|
+
--stuic-field-date-dialog-padding: 1rem;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
@layer components {
|
|
@@ -810,3 +816,116 @@
|
|
|
810
816
|
}
|
|
811
817
|
}
|
|
812
818
|
}
|
|
819
|
+
|
|
820
|
+
/* ============================================================================
|
|
821
|
+
FIELD DATE / FIELD DATE RANGE
|
|
822
|
+
The trigger is a <button> (so no iOS zoom guard needed); it takes the input
|
|
823
|
+
size tokens so the field lines up with its siblings.
|
|
824
|
+
============================================================================ */
|
|
825
|
+
|
|
826
|
+
@layer components {
|
|
827
|
+
.stuic-field-date-trigger {
|
|
828
|
+
display: flex;
|
|
829
|
+
flex: 1 1 0%;
|
|
830
|
+
min-width: 0;
|
|
831
|
+
width: 100%;
|
|
832
|
+
align-items: center;
|
|
833
|
+
gap: 0.5rem;
|
|
834
|
+
margin: 0;
|
|
835
|
+
border: 0;
|
|
836
|
+
background: transparent;
|
|
837
|
+
text-align: start;
|
|
838
|
+
font-family: var(--stuic-input-font-family);
|
|
839
|
+
color: var(--stuic-input-text);
|
|
840
|
+
letter-spacing: -0.025em;
|
|
841
|
+
border-radius: var(--stuic-input-radius, var(--stuic-radius));
|
|
842
|
+
cursor: pointer;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* InputWrap already shows the ring via `.input-wrap:focus-within` */
|
|
846
|
+
.stuic-field-date-trigger:focus,
|
|
847
|
+
.stuic-field-date-trigger:focus-visible {
|
|
848
|
+
outline: none;
|
|
849
|
+
box-shadow: none;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.stuic-field-date-trigger:disabled {
|
|
853
|
+
cursor: not-allowed;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.stuic-field-date-trigger[data-size="sm"] {
|
|
857
|
+
padding: var(--stuic-input-padding-y-sm) var(--stuic-input-padding-x-sm);
|
|
858
|
+
font-size: var(--stuic-input-font-size-sm);
|
|
859
|
+
min-height: var(--stuic-input-min-height-sm);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.stuic-field-date-trigger[data-size="md"],
|
|
863
|
+
.stuic-field-date-trigger:not([data-size]) {
|
|
864
|
+
padding: var(--stuic-input-padding-y-md) var(--stuic-input-padding-x-md);
|
|
865
|
+
font-size: var(--stuic-input-font-size-md);
|
|
866
|
+
min-height: var(--stuic-input-min-height-md);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.stuic-field-date-trigger[data-size="lg"] {
|
|
870
|
+
padding: var(--stuic-input-padding-y-lg) var(--stuic-input-padding-x-lg);
|
|
871
|
+
font-size: var(--stuic-input-font-size-lg);
|
|
872
|
+
min-height: var(--stuic-input-min-height-lg);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.stuic-field-date-trigger-icon {
|
|
876
|
+
display: inline-flex;
|
|
877
|
+
flex-shrink: 0;
|
|
878
|
+
color: var(--stuic-field-date-icon-text);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.stuic-field-date-trigger-label {
|
|
882
|
+
flex: 1;
|
|
883
|
+
min-width: 0;
|
|
884
|
+
overflow: hidden;
|
|
885
|
+
text-overflow: ellipsis;
|
|
886
|
+
white-space: nowrap;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.stuic-field-date-trigger[data-empty] .stuic-field-date-trigger-label {
|
|
890
|
+
color: var(--stuic-field-date-placeholder-text);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/* `embedded`: the calendar lives inside the input card */
|
|
894
|
+
.stuic-field-date-embedded {
|
|
895
|
+
flex: 1 1 0%;
|
|
896
|
+
min-width: 0;
|
|
897
|
+
display: flex;
|
|
898
|
+
justify-content: center;
|
|
899
|
+
padding: var(--stuic-field-date-embedded-padding);
|
|
900
|
+
overflow-x: auto;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/* the picker dialog card (inside a transparent, click-through ModalDialog box) */
|
|
904
|
+
.stuic-field-date-dialog {
|
|
905
|
+
background: var(--stuic-modal-dialog-bg);
|
|
906
|
+
color: var(--stuic-modal-dialog-text);
|
|
907
|
+
border-radius: var(--stuic-field-date-dialog-radius, var(--stuic-radius-container));
|
|
908
|
+
box-shadow: var(--stuic-field-date-dialog-shadow, var(--stuic-shadow-dialog));
|
|
909
|
+
padding: var(--stuic-field-date-dialog-padding);
|
|
910
|
+
max-width: 100%;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.stuic-field-date-dialog-header {
|
|
914
|
+
display: flex;
|
|
915
|
+
align-items: center;
|
|
916
|
+
justify-content: space-between;
|
|
917
|
+
gap: 0.5rem;
|
|
918
|
+
margin-bottom: 0.5rem;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.stuic-field-date-dialog-title {
|
|
922
|
+
font-weight: var(--font-weight-semibold);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.stuic-field-date-dialog-body {
|
|
926
|
+
display: flex;
|
|
927
|
+
justify-content: center;
|
|
928
|
+
max-width: 100%;
|
|
929
|
+
overflow-x: auto;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
@@ -21,3 +21,5 @@ export { default as FieldObject, type Props as FieldObjectProps, } from "./Field
|
|
|
21
21
|
export { default as FieldPhoneNumber, type Props as FieldPhoneNumberProps, } from "./FieldPhoneNumber.svelte";
|
|
22
22
|
export { default as FieldCountry, type Props as FieldCountryProps, } from "./FieldCountry.svelte";
|
|
23
23
|
export { validatePhoneNumber } from "./phone-validation.js";
|
|
24
|
+
export { default as FieldDate, type Props as FieldDateProps, type FieldDateCalendarProps, } from "./FieldDate.svelte";
|
|
25
|
+
export { default as FieldDateRange, type Props as FieldDateRangeProps, type FieldDateRangeValue, } from "./FieldDateRange.svelte";
|
|
@@ -21,3 +21,5 @@ export { default as FieldObject, } from "./FieldObject.svelte";
|
|
|
21
21
|
export { default as FieldPhoneNumber, } from "./FieldPhoneNumber.svelte";
|
|
22
22
|
export { default as FieldCountry, } from "./FieldCountry.svelte";
|
|
23
23
|
export { validatePhoneNumber } from "./phone-validation.js";
|
|
24
|
+
export { default as FieldDate, } from "./FieldDate.svelte";
|
|
25
|
+
export { default as FieldDateRange, } from "./FieldDateRange.svelte";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// Conventions escape hatch (docs/
|
|
2
|
+
// Conventions escape hatch (docs/testing-components.md):
|
|
3
3
|
// ModalDialog is imperative-only (open()/close() via a ref; no bindable
|
|
4
4
|
// `visible` prop), so a `.svelte.test.ts` file can't drive it directly. This
|
|
5
5
|
// fixture holds the `bind:this` ref and exposes an opener button that calls
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import SlidingPanels from "./SlidingPanels.svelte";
|
|
3
3
|
|
|
4
|
-
// Conventions escape hatch (docs/
|
|
4
|
+
// Conventions escape hatch (docs/testing-components.md):
|
|
5
5
|
// SlidingPanels exposes the panel content via snippets that receive an
|
|
6
6
|
// imperative `show(panel)` fn. A `.svelte.test.ts` file can't host markup that
|
|
7
7
|
// wires snippet args to a button, so we compose the real component here and
|
package/dist/icons/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { iconLucideTrendingDown as iconTrendingDown } from "@marianmeres/icons-f
|
|
|
31
31
|
export { iconLucideTrendingUp as iconTrendingUp } from "@marianmeres/icons-fns/lucide/iconLucideTrendingUp.js";
|
|
32
32
|
export { iconLucideZoomIn as iconZoomIn } from "@marianmeres/icons-fns/lucide/iconLucideZoomIn.js";
|
|
33
33
|
export { iconLucideZoomOut as iconZoomOut } from "@marianmeres/icons-fns/lucide/iconLucideZoomOut.js";
|
|
34
|
+
export { iconLucideCalendar as iconCalendar } from "@marianmeres/icons-fns/lucide/iconLucideCalendar.js";
|
|
34
35
|
export { iconLucideCheck as iconCheck } from "@marianmeres/icons-fns/lucide/iconLucideCheck.js";
|
|
35
36
|
export { iconLucideChevronDown as iconChevronDown } from "@marianmeres/icons-fns/lucide/iconLucideChevronDown.js";
|
|
36
37
|
export { iconLucideChevronLeft as iconChevronLeft } from "@marianmeres/icons-fns/lucide/iconLucideChevronLeft.js";
|
package/dist/icons/index.js
CHANGED
|
@@ -36,6 +36,7 @@ export { iconLucideTrendingUp as iconTrendingUp } from "@marianmeres/icons-fns/l
|
|
|
36
36
|
export { iconLucideZoomIn as iconZoomIn } from "@marianmeres/icons-fns/lucide/iconLucideZoomIn.js";
|
|
37
37
|
export { iconLucideZoomOut as iconZoomOut } from "@marianmeres/icons-fns/lucide/iconLucideZoomOut.js";
|
|
38
38
|
// UI Control Icons (Lucide)
|
|
39
|
+
export { iconLucideCalendar as iconCalendar } from "@marianmeres/icons-fns/lucide/iconLucideCalendar.js";
|
|
39
40
|
export { iconLucideCheck as iconCheck } from "@marianmeres/icons-fns/lucide/iconLucideCheck.js";
|
|
40
41
|
export { iconLucideChevronDown as iconChevronDown } from "@marianmeres/icons-fns/lucide/iconLucideChevronDown.js";
|
|
41
42
|
export { iconLucideChevronLeft as iconChevronLeft } from "@marianmeres/icons-fns/lucide/iconLucideChevronLeft.js";
|
package/dist/index.css
CHANGED
|
@@ -63,6 +63,7 @@ In practice:
|
|
|
63
63
|
@import "./components/Breadcrumbs/index.css";
|
|
64
64
|
@import "./components/Button/index.css";
|
|
65
65
|
@import "./components/ButtonGroupRadio/index.css";
|
|
66
|
+
@import "./components/Calendar/index.css";
|
|
66
67
|
@import "./components/Collapsible/index.css";
|
|
67
68
|
@import "./components/Cart/index.css";
|
|
68
69
|
@import "./components/Card/index.css";
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export * from "./components/Book/index.js";
|
|
|
32
32
|
export * from "./components/Breadcrumbs/index.js";
|
|
33
33
|
export * from "./components/Button/index.js";
|
|
34
34
|
export * from "./components/ButtonGroupRadio/index.js";
|
|
35
|
+
export * from "./components/Calendar/index.js";
|
|
35
36
|
export * from "./components/Cart/index.js";
|
|
36
37
|
export * from "./components/Card/index.js";
|
|
37
38
|
export * from "./components/Carousel/index.js";
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./components/Book/index.js";
|
|
|
33
33
|
export * from "./components/Breadcrumbs/index.js";
|
|
34
34
|
export * from "./components/Button/index.js";
|
|
35
35
|
export * from "./components/ButtonGroupRadio/index.js";
|
|
36
|
+
export * from "./components/Calendar/index.js";
|
|
36
37
|
export * from "./components/Cart/index.js";
|
|
37
38
|
export * from "./components/Card/index.js";
|
|
38
39
|
export * from "./components/Carousel/index.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Archived docs
|
|
2
|
+
|
|
3
|
+
Documents kept for provenance only. **Nothing here is current guidance** — do not follow it, and do
|
|
4
|
+
not update it. If something here turns out to still be true and useful, promote it back into `docs/`
|
|
5
|
+
rather than citing it from live code.
|
|
6
|
+
|
|
7
|
+
| Archived | Why |
|
|
8
|
+
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| [`component-testing/`](./component-testing/) | Planning set for introducing real-browser component tests (2026-06). The work is done and shipped; the roadmap, coverage tiers, CI plan and `PROGRESS.md` tracker are history. The one living piece was promoted to [`docs/testing-components.md`](../testing-components.md). |
|
|
10
|
+
| [`upgrading.md`](./upgrading.md) | One-time upgrade note for consumers moving off **v3.66.1**. The library is far past that; the deltas it lists are long since absorbed. |
|
|
@@ -7,6 +7,8 @@ vitest-browser-svelte docs. Planning artifact; no code was changed.
|
|
|
7
7
|
|
|
8
8
|
# @marianmeres/stuic — Component Testing: Overview & Roadmap
|
|
9
9
|
|
|
10
|
+
> **ARCHIVED — historical, not current guidance.** See [`docs/_archive/README.md`](../README.md) for why.
|
|
11
|
+
|
|
10
12
|
> **Verdict:** the proposed stack — Vitest Browser Mode + `vitest-browser-svelte` + Playwright/Chromium
|
|
11
13
|
> — is the right default for _this_ library, where the value being shipped is precisely the DOM/layout/
|
|
12
14
|
> focus/positioning behavior that the current node/server-build test setup **cannot exercise at all**.
|
|
@@ -24,13 +26,13 @@ vitest-browser-svelte docs. Planning artifact; no code was changed.
|
|
|
24
26
|
> node with the very server-build error this effort exists to escape.
|
|
25
27
|
>
|
|
26
28
|
> Read order: this file → [`01-framework-setup`](./01-framework-setup.md) for the exact config →
|
|
27
|
-
> [`02-test-conventions`](
|
|
29
|
+
> [`02-test-conventions`](../../testing-components.md) for how to write a test → then work
|
|
28
30
|
> [`PROGRESS.md`](./PROGRESS.md) top-down. [`03`](./03-component-coverage-roadmap.md) ranks all 74
|
|
29
31
|
> components; [`04`](./04-hard-cases-and-e2e.md) handles the hard 30; [`05`](./05-ci.md) is CI.
|
|
30
32
|
|
|
31
33
|
## A note on `docs/testing.md` (this is a partial reversal — by design)
|
|
32
34
|
|
|
33
|
-
[`docs/testing.md`](
|
|
35
|
+
[`docs/testing.md`](../../testing.md) records a deliberate decision **not** to test component rendering
|
|
34
36
|
("50+ components × prop combos = slow suite, tiny yield; rendering is gated by svelte-check + publint +
|
|
35
37
|
build"). That reasoning holds for _"does it render"_ and we keep it. What changes: browser mode lets
|
|
36
38
|
us test _"does it **behave**"_ — events, two-way binding, aria/disabled/active state, focus traps,
|
|
@@ -45,7 +47,7 @@ explicit sprint task so the docs don't contradict each other.
|
|
|
45
47
|
| 1 | Upgrade vitest 3→4, verify 9 suites green | [01](./01-framework-setup.md) | high | S | med | Gating prerequisite; nothing installs without it |
|
|
46
48
|
| 2 | Add `projects` split (node `server` + browser `client`) + Chromium | [01](./01-framework-setup.md) | high | S | med | The harness; routes by `*.svelte.test.ts` filename |
|
|
47
49
|
| 3 | Separator smoke test — prove client build + `$effect` actually run | [01](./01-framework-setup.md) | high | S | med | Disproves/confirms the documented server-build blocker |
|
|
48
|
-
| 4 | Reconcile `docs/testing.md` (behavior ✅, rendering still ❌) | [02](
|
|
50
|
+
| 4 | Reconcile `docs/testing.md` (behavior ✅, rendering still ❌) | [02](../../testing-components.md) | med | S | low | Keep docs internally consistent before scaling |
|
|
49
51
|
| 5 | Button — flagship; sets every assertion pattern | [03](./03-component-coverage-roadmap.md) | high | S | low | Most-used primitive; template for the rest |
|
|
50
52
|
| 6 | Pill, Switch — events + binding patterns | [03](./03-component-coverage-roadmap.md) | high | S | low | Cover dismiss/toggle/bind once, reuse everywhere |
|
|
51
53
|
| 7 | Spinner, Skeleton, DismissibleMessage, Avatar, Progress | [03](./03-component-coverage-roadmap.md) | high | S | low | Deterministic, high-traffic; quick wins |
|
|
@@ -68,7 +70,7 @@ Branch: `feat/component-testing`. One commit per task.
|
|
|
68
70
|
chromium`, fix the test scripts, and land the Separator smoke test. Unblocks all component tests
|
|
69
71
|
and proves the server-build blocker is gone. Detail in [01](./01-framework-setup.md).
|
|
70
72
|
3. **Reconcile `docs/testing.md` (#4)** — small doc edit so the philosophy matches reality.
|
|
71
|
-
4. **Button (#5)** — establishes the assertion vocabulary ([02](
|
|
73
|
+
4. **Button (#5)** — establishes the assertion vocabulary ([02](../../testing-components.md)) every later
|
|
72
74
|
test reuses. Highest-leverage single component.
|
|
73
75
|
5. **Pill → Switch → Spinner → Skeleton → DismissibleMessage → Avatar → Progress (#6, #7)** — the easy
|
|
74
76
|
tier, one commit each; fast, deterministic, high-traffic coverage.
|
|
@@ -105,7 +107,7 @@ flowchart TD
|
|
|
105
107
|
## Completeness check
|
|
106
108
|
|
|
107
109
|
- **Snippet-heavy components** (Button needs `children`): the `createRawSnippet` pattern is settled in
|
|
108
|
-
[02](
|
|
110
|
+
[02](../../testing-components.md); first real exercise is Button — watch for friction and codify a shared
|
|
109
111
|
helper home then.
|
|
110
112
|
- **SvelteKit-plugin-in-browser-mode** is the top unknown; the Separator smoke test (task 2) is the
|
|
111
113
|
designated early canary, with a documented fallback (plain `svelte()` plugin for the client project).
|
|
@@ -114,6 +116,6 @@ flowchart TD
|
|
|
114
116
|
- Not yet scoped: a dedicated `playwright.config.ts` for the standalone E2E layer — intentionally
|
|
115
117
|
deferred to its own future initiative ([04](./04-hard-cases-and-e2e.md)).
|
|
116
118
|
|
|
117
|
-
Source documents: [`01-framework-setup`](./01-framework-setup.md), [`02-test-conventions`](
|
|
119
|
+
Source documents: [`01-framework-setup`](./01-framework-setup.md), [`02-test-conventions`](../../testing-components.md),
|
|
118
120
|
[`03-component-coverage-roadmap`](./03-component-coverage-roadmap.md), [`04-hard-cases-and-e2e`](./04-hard-cases-and-e2e.md),
|
|
119
121
|
[`05-ci`](./05-ci.md).
|