entasis 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Avatar/avatar.theme.js +3 -1
- package/dist/components/Avatar/avatarGroup.theme.js +4 -1
- package/dist/components/Form/Ask/ask.mcp.d.ts +1 -1
- package/dist/components/Form/Ask/ask.mcp.js +1 -1
- package/dist/components/Form/Field/Field.svelte +2 -2
- package/dist/components/Form/Field/Field.svelte.d.ts +6 -6
- package/dist/components/Form/Field/field.d.ts +12 -3
- package/dist/components/Form/Field/field.state.svelte.d.ts +8 -37
- package/dist/components/Form/Field/field.state.svelte.js +21 -4
- package/dist/components/Form/Form/Form.svelte +2 -1
- package/dist/components/Form/Form/FormFieldRenderer.svelte +57 -11
- package/dist/components/Form/Form/FormFieldRenderer.svelte.d.ts +3 -2
- package/dist/components/Form/Form/form.d.ts +52 -8
- package/dist/components/Form/Form/form.mcp.d.ts +1 -1
- package/dist/components/Form/Form/form.mcp.js +66 -0
- package/dist/components/Form/Form/form.registry.d.ts +28 -0
- package/dist/components/Form/Form/form.registry.js +62 -0
- package/dist/components/Form/Form/form.state.svelte.d.ts +49 -8
- package/dist/components/Form/Form/form.state.svelte.js +3 -2
- package/dist/components/Form/Form/index.d.ts +2 -1
- package/dist/components/Form/Form/index.js +1 -0
- package/dist/components/Form/Form/visibility.d.ts +32 -30
- package/dist/components/Form/MultiStepForm/multiStepForm.state.svelte.d.ts +28 -0
- package/dist/components/Marquee/Marquee.svelte +8 -6
- package/dist/components/Marquee/marquee.mcp.d.ts +1 -1
- package/dist/components/Marquee/marquee.mcp.js +11 -11
- package/dist/components/Marquee/marquee.theme.js +17 -8
- package/dist/components/Sidebar/SidebarMenuItem.svelte +9 -1
- package/dist/components/Sidebar/SidebarMenuSubItem.svelte +71 -6
- package/dist/components/Sidebar/SidebarMenuSubItem.svelte.d.ts +4 -1
- package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
- package/dist/components/Sidebar/sidebar.mcp.js +62 -0
- package/dist/components/Sidebar/sidebar.props.d.ts +18 -0
- package/dist/components/Sidebar/sidebar.theme.d.ts +60 -0
- package/dist/components/Sidebar/sidebar.theme.js +40 -2
- package/dist/generated/componentContract.d.ts +1 -1
- package/dist/generated/componentContract.js +6 -0
- package/dist/generated/componentMcpRegistry.d.ts +4 -4
- package/package.json +1 -1
|
@@ -215,6 +215,72 @@ Use type: 'field' when Form must own a value but a custom control must render it
|
|
|
215
215
|
|
|
216
216
|
The controller exposes value, disabled, required, validation state, accessible ids, controlAttrs, and the control attachment. The entry participates in bind:value, visibility, validation, submission, and InferFormValue through the same FieldState owner as built-in controls.
|
|
217
217
|
|
|
218
|
+
## App-defined input types
|
|
219
|
+
|
|
220
|
+
An app can add its own input types to every Form and ask() dialog, by name, so entries stay plain data that serializes (JSON from a server, a tool call, a database). Two steps, once per app.
|
|
221
|
+
|
|
222
|
+
Type it: augment \`FormInputRegistry\` with the value the input holds and the props its entries take.
|
|
223
|
+
|
|
224
|
+
\`\`\`ts
|
|
225
|
+
// doc-fence: skip (imports a sibling component; tooling/component-contract/consumer-form-input.ts checks this exact setup against the package)
|
|
226
|
+
import { registerFormInputs } from './index.ts';
|
|
227
|
+
import MoneyInput from './MoneyInput.svelte';
|
|
228
|
+
|
|
229
|
+
export type Money = { amount: number; currency: string };
|
|
230
|
+
|
|
231
|
+
declare module 'entasis/form' {
|
|
232
|
+
interface FormInputRegistry {
|
|
233
|
+
money: { value: Money; props: { currencies?: string[] } };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
registerFormInputs({
|
|
238
|
+
money: {
|
|
239
|
+
component: MoneyInput,
|
|
240
|
+
isEmpty: (value) => value?.amount === undefined,
|
|
241
|
+
validate: (value) => (value.amount < 0 ? 'Must not be negative' : null)
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
\`\`\`
|
|
245
|
+
|
|
246
|
+
Register it: call registerFormInputs at the top level of a module the server and the browser both load before a form renders, such as one the root layout imports. Its keys are checked against the registry; built-in type names are refused, and registering a type again replaces it.
|
|
247
|
+
|
|
248
|
+
The component renders the control alone. Form wraps it in Field (label, description, errors) and hands it the field it registered, typed from the registry, plus the entry's own props:
|
|
249
|
+
|
|
250
|
+
\`\`\`svelte
|
|
251
|
+
<!-- doc-fence: skip -->
|
|
252
|
+
<!-- Needs the money registration above; tooling/component-contract/consumer-money-input.svelte is the checked copy. -->
|
|
253
|
+
<script lang="ts">
|
|
254
|
+
import type { FormInputComponentProps } from './index.ts';
|
|
255
|
+
|
|
256
|
+
let { field, currencies = ['EUR'] }: FormInputComponentProps<'money'> = $props();
|
|
257
|
+
const currency = $derived(field.value?.currency ?? currencies[0] ?? 'EUR');
|
|
258
|
+
</script>
|
|
259
|
+
|
|
260
|
+
<input
|
|
261
|
+
type="number"
|
|
262
|
+
{...field.controlAttrs}
|
|
263
|
+
{@attach field.control}
|
|
264
|
+
value={field.value?.amount ?? ''}
|
|
265
|
+
oninput={(event) => field.setValue({ amount: Number(event.currentTarget.value), currency })}
|
|
266
|
+
/>
|
|
267
|
+
\`\`\`
|
|
268
|
+
|
|
269
|
+
Entries then use the type like a built-in one, in Form inputs, groups, and ask():
|
|
270
|
+
|
|
271
|
+
\`\`\`ts
|
|
272
|
+
// doc-fence: skip (needs the money registration above)
|
|
273
|
+
const result = await ask({
|
|
274
|
+
title: 'Set a price',
|
|
275
|
+
confirm: 'Save',
|
|
276
|
+
cancel: 'Cancel',
|
|
277
|
+
inputs: { price: { type: 'money', label: 'Price', required: true, currencies: ['EUR', 'USD'] } }
|
|
278
|
+
});
|
|
279
|
+
if (result.submitted) result.value.price; // Money
|
|
280
|
+
\`\`\`
|
|
281
|
+
|
|
282
|
+
Every Field entry option applies (label, description, required, disabled, size, density, visible, defaultValue, onValidate, onValueChange, slots); any other key goes to the component. required rejects what isEmpty reports empty (by default null, undefined, an empty string, or an empty array); validate runs for every entry of the type, before the entry's own onValidate. The value takes part in bind:value, visibility, validation, submission, and InferFormValue like a built-in input's. An entry whose type is neither built-in nor registered throws when the form reads it.
|
|
283
|
+
|
|
218
284
|
## Non-value entries
|
|
219
285
|
|
|
220
286
|
Use type: 'action' to place a labelled row of buttons in the ordered inputs flow. It accepts optional label and description content plus the same Form-aware action objects as the top-level actions prop. Its label position follows the Form layout unless labelPosition overrides it. Form size is the default button size, action density follows the Form, and loading or disabled protection cannot be overridden while the Form is submitting.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import * as v from 'valibot';
|
|
3
|
+
import type { RegisteredInputType } from '../Field/field.js';
|
|
4
|
+
import type { FieldValidationResult } from '../Field/field.state.svelte.js';
|
|
5
|
+
import type { FormInputDefinition, FormRegisteredInput, FormRenderableInput } from './form.js';
|
|
6
|
+
/** The definitions `registerFormInputs` takes: one per `FormInputRegistry` key, all optional. */
|
|
7
|
+
export type FormInputDefinitions = {
|
|
8
|
+
[T in RegisteredInputType]?: FormInputDefinition<T>;
|
|
9
|
+
};
|
|
10
|
+
/** A definition with its value type erased, as the registry stores and Form renders it. */
|
|
11
|
+
export type RegisteredFormInputDefinition = {
|
|
12
|
+
component: Component<Record<string, unknown>>;
|
|
13
|
+
isEmpty: (value: unknown) => boolean;
|
|
14
|
+
validate?: (value: unknown) => FieldValidationResult;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Registers the components behind the app's `FormInputRegistry` types, so entries such as
|
|
18
|
+
* `{ type: 'money', label: 'Price' }` render in every Form and ask() dialog. Call it at the top
|
|
19
|
+
* level of a module the server and the browser both load before a form renders, such as one the
|
|
20
|
+
* root layout imports. Registering a type again replaces it, so hot reloads keep working.
|
|
21
|
+
*
|
|
22
|
+
* Returns a function that removes these registrations.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerFormInputs(inputs: FormInputDefinitions): () => void;
|
|
25
|
+
export declare const getFormInputDefinition: (type: string) => RegisteredFormInputDefinition | undefined;
|
|
26
|
+
export declare const isRegisteredFormInput: (input: FormRenderableInput) => input is FormRegisteredInput;
|
|
27
|
+
/** The `required`/optional check a registered type gets, in the built-in schemas' terms. */
|
|
28
|
+
export declare function getRegisteredInputSchema(definition: RegisteredFormInputDefinition, required: boolean): v.GenericSchema;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
import { schemas } from '../Field/schemas.js';
|
|
3
|
+
// Form's own entry kinds; the built-in input types are rejected through the schema table.
|
|
4
|
+
const ENTRY_KINDS = new Set(['field', 'custom', 'action', 'group']);
|
|
5
|
+
const definitions = new Map();
|
|
6
|
+
const isEmptyValue = (value) => value === null ||
|
|
7
|
+
value === undefined ||
|
|
8
|
+
value === '' ||
|
|
9
|
+
(Array.isArray(value) && value.length === 0);
|
|
10
|
+
/**
|
|
11
|
+
* Registers the components behind the app's `FormInputRegistry` types, so entries such as
|
|
12
|
+
* `{ type: 'money', label: 'Price' }` render in every Form and ask() dialog. Call it at the top
|
|
13
|
+
* level of a module the server and the browser both load before a form renders, such as one the
|
|
14
|
+
* root layout imports. Registering a type again replaces it, so hot reloads keep working.
|
|
15
|
+
*
|
|
16
|
+
* Returns a function that removes these registrations.
|
|
17
|
+
*/
|
|
18
|
+
export function registerFormInputs(inputs) {
|
|
19
|
+
const added = [];
|
|
20
|
+
// Erased: inside the library the registry is empty, so each definition's own type is `never`.
|
|
21
|
+
const entries = Object.entries(inputs);
|
|
22
|
+
for (const [type, definition] of entries) {
|
|
23
|
+
if (!definition)
|
|
24
|
+
continue;
|
|
25
|
+
if (ENTRY_KINDS.has(type) || Object.hasOwn(schemas.required, type)) {
|
|
26
|
+
throw new Error(`[Form] "${type}" is a built-in input type and cannot be registered.`);
|
|
27
|
+
}
|
|
28
|
+
added.push([
|
|
29
|
+
type,
|
|
30
|
+
{
|
|
31
|
+
// The registry erases each definition's value type; Form hands the component the
|
|
32
|
+
// field it registered for that type, which restores it.
|
|
33
|
+
component: definition.component,
|
|
34
|
+
isEmpty: definition.isEmpty ?? isEmptyValue,
|
|
35
|
+
validate: definition.validate
|
|
36
|
+
}
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
for (const [type, definition] of added)
|
|
40
|
+
definitions.set(type, definition);
|
|
41
|
+
return () => {
|
|
42
|
+
for (const [type, definition] of added) {
|
|
43
|
+
if (definitions.get(type) === definition)
|
|
44
|
+
definitions.delete(type);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export const getFormInputDefinition = (type) => definitions.get(type);
|
|
49
|
+
export const isRegisteredFormInput = (input) => definitions.has(input.type);
|
|
50
|
+
const registeredSchemas = new WeakMap();
|
|
51
|
+
/** The `required`/optional check a registered type gets, in the built-in schemas' terms. */
|
|
52
|
+
export function getRegisteredInputSchema(definition, required) {
|
|
53
|
+
let entry = registeredSchemas.get(definition);
|
|
54
|
+
if (!entry) {
|
|
55
|
+
entry = {
|
|
56
|
+
required: v.pipe(v.unknown(), v.check((value) => !definition.isEmpty(value))),
|
|
57
|
+
optional: v.unknown()
|
|
58
|
+
};
|
|
59
|
+
registeredSchemas.set(definition, entry);
|
|
60
|
+
}
|
|
61
|
+
return required ? entry.required : entry.optional;
|
|
62
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Attachment } from 'svelte/attachments';
|
|
2
2
|
import { SvelteMap } from 'svelte/reactivity';
|
|
3
3
|
import type { FieldState } from '../Field/field.state.svelte.js';
|
|
4
|
-
import type { FieldValue, InputType } from '../Field/field.js';
|
|
4
|
+
import type { FieldType, FieldValue, InputType } from '../Field/field.js';
|
|
5
5
|
import type { FormGroup, FormInputs, FormRenderableInput, FormSubmitHandler, FormValueRecord, InferFormValue, LiveFormValue } from './form.js';
|
|
6
6
|
type FormStateOptions<I extends FormInputs> = {
|
|
7
7
|
inputs: I;
|
|
@@ -26,18 +26,31 @@ export declare class FormState<I extends FormInputs = FormInputs> {
|
|
|
26
26
|
get controlAttrs(): import("../Field/field.state.svelte.js").FieldControlAttributes;
|
|
27
27
|
setValue: (nextValue: FieldValue<T> | null | undefined) => void;
|
|
28
28
|
syncValue: (nextValue: FieldValue<T> | null | undefined) => void;
|
|
29
|
-
|
|
29
|
+
get definition(): import("./form.registry.js").RegisteredFormInputDefinition | undefined;
|
|
30
|
+
checkSchema(value?: string | number | boolean | string[] | number[] | Date | Blob | File | File[] | [Date | null, Date | null] | import("../Field/field.js").KeyValuePair[] | {
|
|
31
|
+
amount: number;
|
|
32
|
+
currency: string;
|
|
33
|
+
} | null | undefined): import("valibot", { with: { "resolution-mode": "require" } }).SafeParseResult<import("valibot", { with: { "resolution-mode": "require" } }).BaseSchema<unknown, unknown, import("valibot", { with: { "resolution-mode": "require" } }).BaseIssue<unknown>>>;
|
|
30
34
|
get isValid(): boolean;
|
|
31
35
|
validate: (value?: FieldValue<T> | null | undefined) => readonly [boolean, unknown];
|
|
32
|
-
value?: string | number | boolean | string[] | number[] | Date | Blob | File | File[] | [Date | null, Date | null] | import("../Field/field.js").KeyValuePair[] |
|
|
36
|
+
value?: string | number | boolean | string[] | number[] | Date | Blob | File | File[] | [Date | null, Date | null] | import("../Field/field.js").KeyValuePair[] | {
|
|
37
|
+
amount: number;
|
|
38
|
+
currency: string;
|
|
39
|
+
} | null | undefined;
|
|
33
40
|
errors: string[] | boolean;
|
|
34
41
|
focused: boolean;
|
|
35
|
-
type:
|
|
42
|
+
type: FieldType;
|
|
36
43
|
required?: boolean;
|
|
37
44
|
disabled?: boolean;
|
|
38
45
|
visible?: boolean;
|
|
39
|
-
onValidate?: ((value: string | number | boolean | string[] | number[] | Date | Blob | File | File[] | [Date | null, Date | null] | import("../Field/field.js").KeyValuePair[] |
|
|
40
|
-
|
|
46
|
+
onValidate?: ((value: string | number | boolean | string[] | number[] | Date | Blob | File | File[] | [Date | null, Date | null] | import("../Field/field.js").KeyValuePair[] | {
|
|
47
|
+
amount: number;
|
|
48
|
+
currency: string;
|
|
49
|
+
} | null) => import("../Field/field.state.svelte.js").FieldValidationResult) | undefined;
|
|
50
|
+
onValueChange?: ((value: string | number | boolean | string[] | number[] | Date | Blob | File | File[] | [Date | null, Date | null] | import("../Field/field.js").KeyValuePair[] | {
|
|
51
|
+
amount: number;
|
|
52
|
+
currency: string;
|
|
53
|
+
} | null) => void) | undefined;
|
|
41
54
|
id: string;
|
|
42
55
|
}>;
|
|
43
56
|
loading: boolean;
|
|
@@ -853,6 +866,13 @@ export declare class FormState<I extends FormInputs = FormInputs> {
|
|
|
853
866
|
theme?: import("../PinInput/pinInput.theme.js").PinInputThemeProps & import("../Field/field.js").InputProps<"pin">["theme"];
|
|
854
867
|
}, "visible"> & {
|
|
855
868
|
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
869
|
+
}) | ({
|
|
870
|
+
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
871
|
+
class?: string;
|
|
872
|
+
} & Omit<import("../Field/field.js").InputProps<"test-money">, "class" | "name" | "visible"> & {
|
|
873
|
+
currencies?: readonly string[];
|
|
874
|
+
} & {
|
|
875
|
+
type: "test-money";
|
|
856
876
|
}) ? T_9["type"] : never : never : never>> : FieldValue<Extract<T_1[K_1], import("./form.js").FormValueInput> extends infer T_10 ? T_10 extends Extract<T_1[K_1], import("./form.js").FormValueInput> ? T_10 extends import("./form.js").FormFieldEntry<infer Input extends InputType> ? Input : T_10 extends (Omit<{
|
|
857
877
|
type: "time";
|
|
858
878
|
} & {
|
|
@@ -1625,6 +1645,13 @@ export declare class FormState<I extends FormInputs = FormInputs> {
|
|
|
1625
1645
|
theme?: import("../PinInput/pinInput.theme.js").PinInputThemeProps & import("../Field/field.js").InputProps<"pin">["theme"];
|
|
1626
1646
|
}, "visible"> & {
|
|
1627
1647
|
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
1648
|
+
}) | ({
|
|
1649
|
+
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
1650
|
+
class?: string;
|
|
1651
|
+
} & Omit<import("../Field/field.js").InputProps<"test-money">, "class" | "name" | "visible"> & {
|
|
1652
|
+
currencies?: readonly string[];
|
|
1653
|
+
} & {
|
|
1654
|
+
type: "test-money";
|
|
1628
1655
|
}) ? T_10["type"] : never : never : never> | null; } : never) & (((({ [K_3 in keyof I]: I[K_3] extends infer T_6 ? T_6 extends I[K_3] ? T_6 extends FormGroup<infer Inputs extends import("./form.js").FormGroupInputs> ? (T_6 extends infer T_7 ? T_7 extends T_6 ? T_7 extends {
|
|
1629
1656
|
visible: infer Visibility;
|
|
1630
1657
|
} ? boolean extends Visibility ? true : Extract<Visibility, false | ((value: FormValueRecord) => boolean)> extends never ? false : true : false : never : never) extends true ? { [K_6 in keyof Inputs]: Omit<Inputs[K_6], "visible"> & {
|
|
@@ -2431,6 +2458,13 @@ export declare class FormState<I extends FormInputs = FormInputs> {
|
|
|
2431
2458
|
theme?: import("../PinInput/pinInput.theme.js").PinInputThemeProps & import("../Field/field.js").InputProps<"pin">["theme"];
|
|
2432
2459
|
}, "visible"> & {
|
|
2433
2460
|
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
2461
|
+
}) | ({
|
|
2462
|
+
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
2463
|
+
class?: string;
|
|
2464
|
+
} & Omit<import("../Field/field.js").InputProps<"test-money">, "class" | "name" | "visible"> & {
|
|
2465
|
+
currencies?: readonly string[];
|
|
2466
|
+
} & {
|
|
2467
|
+
type: "test-money";
|
|
2434
2468
|
}) ? T_12["type"] : never : never : never>> : FieldValue<Extract<T_4[K_4], import("./form.js").FormValueInput> extends infer T_13 ? T_13 extends Extract<T_4[K_4], import("./form.js").FormValueInput> ? T_13 extends import("./form.js").FormFieldEntry<infer Input extends InputType> ? Input : T_13 extends (Omit<{
|
|
2435
2469
|
type: "time";
|
|
2436
2470
|
} & {
|
|
@@ -3203,10 +3237,17 @@ export declare class FormState<I extends FormInputs = FormInputs> {
|
|
|
3203
3237
|
theme?: import("../PinInput/pinInput.theme.js").PinInputThemeProps & import("../Field/field.js").InputProps<"pin">["theme"];
|
|
3204
3238
|
}, "visible"> & {
|
|
3205
3239
|
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
3240
|
+
}) | ({
|
|
3241
|
+
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
3242
|
+
class?: string;
|
|
3243
|
+
} & Omit<import("../Field/field.js").InputProps<"test-money">, "class" | "name" | "visible"> & {
|
|
3244
|
+
currencies?: readonly string[];
|
|
3245
|
+
} & {
|
|
3246
|
+
type: "test-money";
|
|
3206
3247
|
}) ? T_13["type"] : never : never : never> | null) | undefined; } : never) extends infer T ? { [K in keyof T]: T[K]; } : never>;
|
|
3207
3248
|
constructor(options: FormStateOptions<I>);
|
|
3208
|
-
registerField<T extends
|
|
3209
|
-
updateFieldValue<T extends
|
|
3249
|
+
registerField<T extends FieldType>(field: FieldState<T>): () => void;
|
|
3250
|
+
updateFieldValue<T extends FieldType>(field: FieldState<T>, notify?: boolean): void;
|
|
3210
3251
|
keyboardNavigation: Attachment<HTMLElement>;
|
|
3211
3252
|
private handleKeydown;
|
|
3212
3253
|
isInputVisible(name: string): boolean;
|
|
@@ -2,6 +2,7 @@ import { setContext, untrack } from 'svelte';
|
|
|
2
2
|
import { on } from 'svelte/events';
|
|
3
3
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
4
4
|
import { flattenFormInputs, getFormValueInputType } from './form.js';
|
|
5
|
+
import { getFormInputDefinition } from './form.registry.js';
|
|
5
6
|
import { isFieldVisible } from './visibility.js';
|
|
6
7
|
const focusableControl = 'input:not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
|
|
7
8
|
const enterNavigationInputTypes = new Set([
|
|
@@ -220,8 +221,8 @@ export class FormState {
|
|
|
220
221
|
}
|
|
221
222
|
for (const { name, input } of definitions) {
|
|
222
223
|
const inputType = getFormValueInputType(input);
|
|
223
|
-
if (!supportedInputTypes.has(inputType)) {
|
|
224
|
-
throw new Error(`Form field "${name}" uses unsupported input type "${String(inputType)}".`);
|
|
224
|
+
if (!supportedInputTypes.has(inputType) && !getFormInputDefinition(inputType)) {
|
|
225
|
+
throw new Error(`Form field "${name}" uses unsupported input type "${String(inputType)}". Register app-defined types with registerFormInputs().`);
|
|
225
226
|
}
|
|
226
227
|
if (!hasOwn(externalValue, name))
|
|
227
228
|
continue;
|
|
@@ -3,6 +3,7 @@ export { Ask, ask } from '../Ask/index.js';
|
|
|
3
3
|
export type { AskButton, AskDialogOptions, AskOptions, AskProps, AskResult } from '../Ask/index.js';
|
|
4
4
|
export type { FormLayout, FormProps, FormVariant } from './form.props.js';
|
|
5
5
|
export { FormState } from './form.state.svelte.js';
|
|
6
|
+
export { registerFormInputs, type FormInputDefinitions } from './form.registry.js';
|
|
6
7
|
export type { FieldLabelPosition } from '../Field/field.js';
|
|
7
8
|
export { formTheme, setFormTheme, useFormTheme, type FormTheme, type FormThemeProps } from './form.theme.js';
|
|
8
|
-
export type { FormInput, FormAction, FormActionInput, FormCustomInput, FormInputAction, FormInputState, FormFieldController, FormFieldInput, FormFieldEntry, FormGroup, FormGroupColumns, FormGroupInputs, FormInputsWithState, FormInputs, FormRenderableInput, FormValueInput, FlattenFormInputs, InferFormValue, LiveFormValue, FormSubmitHandler, MaybePromise } from './form.js';
|
|
9
|
+
export type { FormInput, FormAction, FormActionInput, FormCustomInput, FormInputAction, FormInputState, FormFieldController, FormFieldInput, FormFieldEntry, FormInputComponentProps, FormInputDefinition, FormInputRegistry, FormRegisteredInput, FormGroup, FormGroupColumns, FormGroupInputs, FormInputsWithState, FormInputs, FormRenderableInput, FormValueInput, FlattenFormInputs, InferFormValue, LiveFormValue, FormSubmitHandler, MaybePromise } from './form.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as Form } from './Form.svelte';
|
|
2
2
|
export { Ask, ask } from '../Ask/index.js';
|
|
3
3
|
export { FormState } from './form.state.svelte.js';
|
|
4
|
+
export { registerFormInputs } from './form.registry.js';
|
|
4
5
|
export { formTheme, setFormTheme, useFormTheme } from './form.theme.js';
|
|
@@ -4801,21 +4801,6 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
|
|
|
4801
4801
|
name?: string | undefined;
|
|
4802
4802
|
label?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4803
4803
|
items: import("../TagGroup/tagGroup.props.js").TagGroupOption[];
|
|
4804
|
-
header?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4805
|
-
actions?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4806
|
-
description?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4807
|
-
helper?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4808
|
-
footer?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4809
|
-
errorsContainer?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4810
|
-
onValueChange?: ((value: import("../TagGroup/tagGroup.props.js").TagGroupValue) => void) | undefined;
|
|
4811
|
-
defaultValue?: import("../TagGroup/tagGroup.props.js").TagGroupValue | undefined;
|
|
4812
|
-
required?: boolean | undefined;
|
|
4813
|
-
density?: import("../../../types/theme.js").Density | undefined;
|
|
4814
|
-
labelPosition?: FieldLabelPosition;
|
|
4815
|
-
onValidate?: ((value: import("../TagGroup/tagGroup.props.js").TagGroupValue) => string[] | boolean) | undefined;
|
|
4816
|
-
fieldAttrs?: import("../Field/field.js").FieldAttributes | undefined;
|
|
4817
|
-
errors?: string[] | boolean | undefined;
|
|
4818
|
-
focused?: boolean | undefined;
|
|
4819
4804
|
field?: {
|
|
4820
4805
|
name: string;
|
|
4821
4806
|
size: Sizes;
|
|
@@ -4831,6 +4816,7 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
|
|
|
4831
4816
|
get controlAttrs(): import("../Field/field.state.svelte.js").FieldControlAttributes;
|
|
4832
4817
|
setValue: (nextValue: import("../Field/field.js").FieldValue<T> | null | undefined) => void;
|
|
4833
4818
|
syncValue: (nextValue: import("../Field/field.js").FieldValue<T> | null | undefined) => void;
|
|
4819
|
+
get definition(): import("./form.registry.js").RegisteredFormInputDefinition | undefined;
|
|
4834
4820
|
checkSchema(value?: string | string[] | null | undefined): import("valibot", { with: { "resolution-mode": "require" } }).SafeParseResult<import("valibot", { with: { "resolution-mode": "require" } }).BaseSchema<unknown, unknown, import("valibot", { with: { "resolution-mode": "require" } }).BaseIssue<unknown>>>;
|
|
4835
4821
|
get isValid(): boolean;
|
|
4836
4822
|
validate: (value?: import("../Field/field.js").FieldValue<T> | null | undefined) => readonly [boolean, unknown];
|
|
@@ -4845,6 +4831,21 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
|
|
|
4845
4831
|
onValueChange?: ((value: string | string[] | null) => void) | undefined;
|
|
4846
4832
|
id: string;
|
|
4847
4833
|
} | undefined;
|
|
4834
|
+
header?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4835
|
+
actions?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4836
|
+
description?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4837
|
+
helper?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4838
|
+
footer?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4839
|
+
errorsContainer?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
4840
|
+
onValueChange?: ((value: import("../TagGroup/tagGroup.props.js").TagGroupValue) => void) | undefined;
|
|
4841
|
+
defaultValue?: import("../TagGroup/tagGroup.props.js").TagGroupValue | undefined;
|
|
4842
|
+
required?: boolean | undefined;
|
|
4843
|
+
density?: import("../../../types/theme.js").Density | undefined;
|
|
4844
|
+
labelPosition?: FieldLabelPosition;
|
|
4845
|
+
onValidate?: ((value: import("../TagGroup/tagGroup.props.js").TagGroupValue) => string[] | boolean) | undefined;
|
|
4846
|
+
fieldAttrs?: import("../Field/field.js").FieldAttributes | undefined;
|
|
4847
|
+
errors?: string[] | boolean | undefined;
|
|
4848
|
+
focused?: boolean | undefined;
|
|
4848
4849
|
labelFor?: string | false | undefined;
|
|
4849
4850
|
multiple?: boolean | undefined;
|
|
4850
4851
|
unselectedColor?: import("../../../types/theme.js").Colors | undefined;
|
|
@@ -10417,21 +10418,6 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
|
|
|
10417
10418
|
name?: string | undefined;
|
|
10418
10419
|
label?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10419
10420
|
items: import("../TagGroup/tagGroup.props.js").TagGroupOption[];
|
|
10420
|
-
header?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10421
|
-
actions?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10422
|
-
description?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10423
|
-
helper?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10424
|
-
footer?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10425
|
-
errorsContainer?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10426
|
-
onValueChange?: ((value: import("../TagGroup/tagGroup.props.js").TagGroupValue) => void) | undefined;
|
|
10427
|
-
defaultValue?: import("../TagGroup/tagGroup.props.js").TagGroupValue | undefined;
|
|
10428
|
-
required?: boolean | undefined;
|
|
10429
|
-
density?: import("../../../types/theme.js").Density | undefined;
|
|
10430
|
-
labelPosition?: FieldLabelPosition;
|
|
10431
|
-
onValidate?: ((value: import("../TagGroup/tagGroup.props.js").TagGroupValue) => string[] | boolean) | undefined;
|
|
10432
|
-
fieldAttrs?: import("../Field/field.js").FieldAttributes | undefined;
|
|
10433
|
-
errors?: string[] | boolean | undefined;
|
|
10434
|
-
focused?: boolean | undefined;
|
|
10435
10421
|
field?: {
|
|
10436
10422
|
name: string;
|
|
10437
10423
|
size: Sizes;
|
|
@@ -10447,6 +10433,7 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
|
|
|
10447
10433
|
get controlAttrs(): import("../Field/field.state.svelte.js").FieldControlAttributes;
|
|
10448
10434
|
setValue: (nextValue: import("../Field/field.js").FieldValue<T> | null | undefined) => void;
|
|
10449
10435
|
syncValue: (nextValue: import("../Field/field.js").FieldValue<T> | null | undefined) => void;
|
|
10436
|
+
get definition(): import("./form.registry.js").RegisteredFormInputDefinition | undefined;
|
|
10450
10437
|
checkSchema(value?: string | string[] | null | undefined): import("valibot", { with: { "resolution-mode": "require" } }).SafeParseResult<import("valibot", { with: { "resolution-mode": "require" } }).BaseSchema<unknown, unknown, import("valibot", { with: { "resolution-mode": "require" } }).BaseIssue<unknown>>>;
|
|
10451
10438
|
get isValid(): boolean;
|
|
10452
10439
|
validate: (value?: import("../Field/field.js").FieldValue<T> | null | undefined) => readonly [boolean, unknown];
|
|
@@ -10461,6 +10448,21 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
|
|
|
10461
10448
|
onValueChange?: ((value: string | string[] | null) => void) | undefined;
|
|
10462
10449
|
id: string;
|
|
10463
10450
|
} | undefined;
|
|
10451
|
+
header?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10452
|
+
actions?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10453
|
+
description?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10454
|
+
helper?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10455
|
+
footer?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10456
|
+
errorsContainer?: import("../../Slot/slot.js").Slot<undefined> | undefined;
|
|
10457
|
+
onValueChange?: ((value: import("../TagGroup/tagGroup.props.js").TagGroupValue) => void) | undefined;
|
|
10458
|
+
defaultValue?: import("../TagGroup/tagGroup.props.js").TagGroupValue | undefined;
|
|
10459
|
+
required?: boolean | undefined;
|
|
10460
|
+
density?: import("../../../types/theme.js").Density | undefined;
|
|
10461
|
+
labelPosition?: FieldLabelPosition;
|
|
10462
|
+
onValidate?: ((value: import("../TagGroup/tagGroup.props.js").TagGroupValue) => string[] | boolean) | undefined;
|
|
10463
|
+
fieldAttrs?: import("../Field/field.js").FieldAttributes | undefined;
|
|
10464
|
+
errors?: string[] | boolean | undefined;
|
|
10465
|
+
focused?: boolean | undefined;
|
|
10464
10466
|
labelFor?: string | false | undefined;
|
|
10465
10467
|
multiple?: boolean | undefined;
|
|
10466
10468
|
unselectedColor?: import("../../../types/theme.js").Colors | undefined;
|
|
@@ -833,6 +833,13 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
|
|
|
833
833
|
theme?: import("../PinInput/pinInput.theme.js").PinInputThemeProps & import("../Field/field.js").InputProps<"pin">["theme"];
|
|
834
834
|
}, "visible"> & {
|
|
835
835
|
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
836
|
+
}) | ({
|
|
837
|
+
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
838
|
+
class?: string;
|
|
839
|
+
} & Omit<import("../Field/field.js").InputProps<"test-money">, "class" | "name" | "visible"> & {
|
|
840
|
+
currencies?: readonly string[];
|
|
841
|
+
} & {
|
|
842
|
+
type: "test-money";
|
|
836
843
|
}) ? T_15["type"] : never : never : never>> : import("../Field/field.js").FieldValue<Extract<T_1[K_1], import("../Form/form.js").FormValueInput> extends infer T_16 ? T_16 extends Extract<T_1[K_1], import("../Form/form.js").FormValueInput> ? T_16 extends import("../Form/form.js").FormFieldEntry<infer Input extends import("../Field/field.js").InputType> ? Input : T_16 extends (Omit<{
|
|
837
844
|
type: "time";
|
|
838
845
|
} & {
|
|
@@ -1605,6 +1612,13 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
|
|
|
1605
1612
|
theme?: import("../PinInput/pinInput.theme.js").PinInputThemeProps & import("../Field/field.js").InputProps<"pin">["theme"];
|
|
1606
1613
|
}, "visible"> & {
|
|
1607
1614
|
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
1615
|
+
}) | ({
|
|
1616
|
+
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
1617
|
+
class?: string;
|
|
1618
|
+
} & Omit<import("../Field/field.js").InputProps<"test-money">, "class" | "name" | "visible"> & {
|
|
1619
|
+
currencies?: readonly string[];
|
|
1620
|
+
} & {
|
|
1621
|
+
type: "test-money";
|
|
1608
1622
|
}) ? T_16["type"] : never : never : never> | null; } : never) & (((((MergedMultiStepFormInputs<I> extends infer T_8 extends FormInputs ? { [K_3 in keyof T_8]: T_8[K_3] extends infer T_9 ? T_9 extends T_8[K_3] ? T_9 extends import("../Form/form.js").FormGroup<infer Inputs extends import("../Form/form.js").FormGroupInputs> ? (T_9 extends infer T_10 ? T_10 extends T_9 ? T_10 extends {
|
|
1609
1623
|
visible: infer Visibility;
|
|
1610
1624
|
} ? boolean extends Visibility ? true : Extract<Visibility, false | ((value: FormValueRecord) => boolean)> extends never ? false : true : false : never : never) extends true ? { [K_6 in keyof Inputs]: Omit<Inputs[K_6], "visible"> & {
|
|
@@ -2411,6 +2425,13 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
|
|
|
2411
2425
|
theme?: import("../PinInput/pinInput.theme.js").PinInputThemeProps & import("../Field/field.js").InputProps<"pin">["theme"];
|
|
2412
2426
|
}, "visible"> & {
|
|
2413
2427
|
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
2428
|
+
}) | ({
|
|
2429
|
+
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
2430
|
+
class?: string;
|
|
2431
|
+
} & Omit<import("../Field/field.js").InputProps<"test-money">, "class" | "name" | "visible"> & {
|
|
2432
|
+
currencies?: readonly string[];
|
|
2433
|
+
} & {
|
|
2434
|
+
type: "test-money";
|
|
2414
2435
|
}) ? T_20["type"] : never : never : never>> : import("../Field/field.js").FieldValue<Extract<T_6[K_4], import("../Form/form.js").FormValueInput> extends infer T_21 ? T_21 extends Extract<T_6[K_4], import("../Form/form.js").FormValueInput> ? T_21 extends import("../Form/form.js").FormFieldEntry<infer Input extends import("../Field/field.js").InputType> ? Input : T_21 extends (Omit<{
|
|
2415
2436
|
type: "time";
|
|
2416
2437
|
} & {
|
|
@@ -3183,6 +3204,13 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
|
|
|
3183
3204
|
theme?: import("../PinInput/pinInput.theme.js").PinInputThemeProps & import("../Field/field.js").InputProps<"pin">["theme"];
|
|
3184
3205
|
}, "visible"> & {
|
|
3185
3206
|
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
3207
|
+
}) | ({
|
|
3208
|
+
visible?: boolean | ((value: FormValueRecord) => boolean);
|
|
3209
|
+
class?: string;
|
|
3210
|
+
} & Omit<import("../Field/field.js").InputProps<"test-money">, "class" | "name" | "visible"> & {
|
|
3211
|
+
currencies?: readonly string[];
|
|
3212
|
+
} & {
|
|
3213
|
+
type: "test-money";
|
|
3186
3214
|
}) ? T_21["type"] : never : never : never> | null) | undefined; } : never) extends infer T ? { [K in keyof T]: T[K]; } : never>;
|
|
3187
3215
|
progress: {
|
|
3188
3216
|
value: number;
|
|
@@ -33,11 +33,6 @@
|
|
|
33
33
|
return `${Math.max(0.1, speed)}s`;
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
const gap = $derived.by(() => {
|
|
37
|
-
if (size === 'small') return '0.5rem';
|
|
38
|
-
if (size === 'large') return '1.5rem';
|
|
39
|
-
return '1rem';
|
|
40
|
-
});
|
|
41
36
|
const configuredCopyCount = $derived.by(() => {
|
|
42
37
|
if (!Number.isFinite(numberOfCopies)) return 2;
|
|
43
38
|
return Math.min(MAX_COPY_COUNT, Math.max(2, Math.floor(numberOfCopies)));
|
|
@@ -80,7 +75,7 @@
|
|
|
80
75
|
data-reverse={reverse ? 'true' : undefined}
|
|
81
76
|
data-fade={fade ? 'true' : undefined}
|
|
82
77
|
class={classes.root({ direction, size, fade, class: className })}
|
|
83
|
-
style="--animation-duration: {animationDuration};
|
|
78
|
+
style="--animation-duration: {animationDuration};"
|
|
84
79
|
{@attach observeRoot}
|
|
85
80
|
{...attachments}
|
|
86
81
|
>
|
|
@@ -144,4 +139,11 @@
|
|
|
144
139
|
:global(html[data-entasis-reduce-motion] .animate-marquee-up) {
|
|
145
140
|
animation: none;
|
|
146
141
|
}
|
|
142
|
+
|
|
143
|
+
/* A still marquee starts at its first item, so only its trailing edge, where the row is cut,
|
|
144
|
+
keeps the fade; a faded leading edge would half-hide the first item. */
|
|
145
|
+
:global(html[data-entasis-reduce-motion]) div[data-slot='marquee'][data-fade] {
|
|
146
|
+
--scroll-fade-s: 0px;
|
|
147
|
+
--scroll-fade-t: 0px;
|
|
148
|
+
}
|
|
147
149
|
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const marqueeDescription = "\n# Marquee Component\n\nThe Marquee component creates an infinite scrolling animation for displaying content. It automatically duplicates the slot content to create a seamless infinite loop effect and supports customizable speed, direction, fade effects, and pause-on-hover functionality.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst items = [\n\t\t{ id: 1, text: 'Item 1' },\n\t\t{ id: 2, text: 'Item 2' },\n\t\t{ id: 3, text: 'Item 3' }\n\t];\n</script>\n\n<Marquee>\n\t{#each items as item}\n\t\t<div class=\"px-4 py-2 bg-surface rounded\">\n\t\t\t{item.text}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Props\n\n### Core Props\n- **children**: Snippet (required)\n - Content to display in the marquee\n - Parent component handles iteration over items\n - Content is automatically duplicated based on `numberOfCopies`\n\n- **direction**: 'left' | 'up' (default: 'left')\n - `'left'`: Horizontal scrolling from right to left\n - `'up'`: Vertical scrolling from bottom to top\n\n- **numberOfCopies**: number (default: 2)\n - Minimum number of times to duplicate the content for a seamless loop\n - The component measures the viewport and first copy, then adds copies when the content is too short to cover the viewport continuously\n - The effective count is bounded to 100 copies\n\n### Animation Props\n- **speed**: 'fast' | 'normal' | 'slow' | number (default: 'fast')\n - Animation speed control\n - `'fast'`: 20 seconds per cycle\n - `'normal'`: 40 seconds per cycle\n - `'slow'`: 80 seconds per cycle\n - `number`: Custom duration in seconds (e.g., `30` for 30 seconds)\n\n- **reverse**: boolean (default: false)\n - If true, animation runs in reverse direction\n - For 'left': scrolls left to right\n - For 'up': scrolls top to bottom\n\n- **pauseOnHover**: boolean (default: true)\n - If true, animation pauses when mouse hovers over the marquee\n - If false, animation continues regardless of hover state\n\n### Visual Props\n- **fade**: boolean (default: true)\n - If true, applies the shared scroll-fade utility at the marquee edges\n - Uses a static scroll-fade mode because the marquee is clipped animation, not a native scroll container\n\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n -
|
|
1
|
+
export declare const marqueeDescription = "\n# Marquee Component\n\nThe Marquee component creates an infinite scrolling animation for displaying content. It automatically duplicates the slot content to create a seamless infinite loop effect and supports customizable speed, direction, fade effects, and pause-on-hover functionality.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst items = [\n\t\t{ id: 1, text: 'Item 1' },\n\t\t{ id: 2, text: 'Item 2' },\n\t\t{ id: 3, text: 'Item 3' }\n\t];\n</script>\n\n<Marquee>\n\t{#each items as item}\n\t\t<div class=\"px-4 py-2 bg-surface rounded\">\n\t\t\t{item.text}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Props\n\n### Core Props\n- **children**: Snippet (required)\n - Content to display in the marquee\n - Parent component handles iteration over items\n - Content is automatically duplicated based on `numberOfCopies`\n\n- **direction**: 'left' | 'up' (default: 'left')\n - `'left'`: Horizontal scrolling from right to left\n - `'up'`: Vertical scrolling from bottom to top\n\n- **numberOfCopies**: number (default: 2)\n - Minimum number of times to duplicate the content for a seamless loop\n - The component measures the viewport and first copy, then adds copies when the content is too short to cover the viewport continuously\n - The effective count is bounded to 100 copies\n\n### Animation Props\n- **speed**: 'fast' | 'normal' | 'slow' | number (default: 'fast')\n - Animation speed control\n - `'fast'`: 20 seconds per cycle\n - `'normal'`: 40 seconds per cycle\n - `'slow'`: 80 seconds per cycle\n - `number`: Custom duration in seconds (e.g., `30` for 30 seconds)\n\n- **reverse**: boolean (default: false)\n - If true, animation runs in reverse direction\n - For 'left': scrolls left to right\n - For 'up': scrolls top to bottom\n\n- **pauseOnHover**: boolean (default: true)\n - If true, animation pauses when mouse hovers over the marquee\n - If false, animation continues regardless of hover state\n\n### Visual Props\n- **fade**: boolean (default: true)\n - If true, applies the shared scroll-fade utility at the marquee edges\n - Uses a static scroll-fade mode because the marquee is clipped animation, not a native scroll container\n\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - Sets the marquee's one spacing value, `--gap`, from the theme spacing scale\n - `small`: `--space-md`\n - `normal`: `--space-xl`\n - `large`: `--layout-space-md`\n - The copies, the items inside each copy and the loop step all use `--gap`, so the loop stays\n seamless at every size and theme spacing. To respace a marquee, set `--gap` once on the root\n (`class=\"[--gap:var(--space-lg)]\"`); never redeclare it on the copies.\n\n### Styling Props\n- **ref**: HTMLDivElement | null (bindable)\n - Reference to the marquee container\n\n- **class**: string\n - Additional CSS classes for the marquee container\n\n- **innerClass**: string\n - Additional CSS classes for each duplicated copy of the content\n\n- **theme**: MarqueeThemeProps\n - Custom theme overrides for advanced styling\n\n## Examples\n\n### Basic Horizontal Marquee\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst testimonials = [\n\t\t{ author: 'John Doe', quote: 'Amazing product!' },\n\t\t{ author: 'Jane Smith', quote: 'Highly recommended!' },\n\t\t{ author: 'Bob Johnson', quote: 'Best purchase ever!' }\n\t];\n</script>\n\n<Marquee>\n\t{#each testimonials as item}\n\t\t<div class=\"px-8 py-4 bg-surface rounded-lg\">\n\t\t\t<p class=\"text-lg font-semibold\">\"{item.quote}\"</p>\n\t\t\t<p class=\"text-sm text-muted\">\u2014 {item.author}</p>\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### Logo Carousel\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst logos = [\n\t\t{ name: 'Company 1', logo: '/logo1.png' },\n\t\t{ name: 'Company 2', logo: '/logo2.png' },\n\t\t{ name: 'Company 3', logo: '/logo3.png' }\n\t];\n</script>\n\n<Marquee speed=\"normal\" size=\"large\">\n\t{#each logos as logo}\n\t\t<img src={logo.logo} alt={logo.name} class=\"h-12 w-auto\" />\n\t{/each}\n</Marquee>\n```\n\n### Vertical Marquee\n\n```svelte\n<Marquee direction=\"up\" class=\"h-[400px]\">\n\t{#each items as item}\n\t\t<div class=\"px-6 py-4 bg-surface rounded\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### With Fade Effect\n\n```svelte\n<Marquee fade={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Reverse Direction\n\n```svelte\n<Marquee reverse={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Speed\n\n```svelte\n<Marquee speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Different Sizes\n\n```svelte\n<!-- Small -->\n<Marquee size=\"small\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Normal (default) -->\n<Marquee size=\"normal\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Large -->\n<Marquee size=\"large\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Number of Copies\n\n```svelte\n<!-- More copies for smoother appearance -->\n<Marquee numberOfCopies={4} speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Disable Pause on Hover\n\n```svelte\n<Marquee pauseOnHover={false}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Combined Effects\n\n```svelte\n<Marquee \n\tfade={true}\n\treverse={true}\n\tspeed=\"slow\"\n\tsize=\"large\"\n>\n\t{#each items as item}\n\t\t<div class=\"px-8 py-6 bg-surface rounded-lg\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Structure\n\nThe Marquee component renders:\n- A container `<div>` with overflow hidden and optional scroll-fade utility classes\n- Multiple copies of the content (default 2) based on `numberOfCopies`\n- Each copy has the animation applied independently\n- Parent component iterates over items in the slot\n\n## Animation Details\n\n- Uses CSS keyframe animations for smooth GPU-accelerated performance\n- Content is duplicated `numberOfCopies` times to create seamless infinite scroll\n- Animation translates each copy by -100% of its width/height plus `--gap`, the same value the copies are spaced with\n- Animation duration is controlled via CSS custom property `--animation-duration`\n- Pause on hover uses CSS `animation-play-state` for instant response\n- Reverse mode uses CSS `direction: reverse` for the animation\n\n## Accessibility\n\n- Uses `role=\"presentation\"` on the container as the marquee is decorative\n- Consider providing alternative static content for screen readers\n- Ensure sufficient color foreground for text content\n- Pause on hover helps users interact with and read content\n- Animation is disabled when the user requests reduced motion; the still row then fades only its trailing edge\n\n## Notes\n\n- Parent component is responsible for iterating over items (similar to Carousel pattern)\n- Content is automatically duplicated internally based on `numberOfCopies`\n- The component uses CSS animations for optimal performance\n- Custom speed values are specified in seconds (e.g., `30` = 30 seconds per cycle)\n- For vertical marquees, ensure the container has a defined height\n- Increasing `numberOfCopies` creates smoother appearance but uses more DOM elements\n- The fade effect uses the shared scroll-fade utility classes\n\n## Theme Customization\n\nThe Marquee component uses a theme object that can be customized using the `theme` prop or by setting a global theme.\n\n### Theme Structure\n\nThe theme object contains the following parts:\n- **root**: Main marquee container styles\n- **inner**: Inner scrolling content container styles\n\n### Available Variants\n\n**root**:\n- base: Base classes for main container\n- Variants:\n - direction: 'left' | 'up' - Scroll direction\n - size: 'small' | 'normal' | 'large' - Gap between duplicated content\n - fade: boolean - Applies shared scroll-fade utility classes\n\n**inner**:\n- base: Base classes for scrolling content\n- Variants:\n - direction: 'left' | 'up' - Animation direction\n - size: 'small' | 'normal' | 'large' - Gap size\n - pauseOnHover: boolean - Pause animation on hover\n - reverse: boolean - Reverse animation direction\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<Marquee \n theme={{\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: '[--gap:var(--space-lg)]'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Custom Animation Styling**:\n```svelte\n<Marquee \n theme={{\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setMarqueeTheme } from './index.ts';\n \n setMarqueeTheme({\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: '[--gap:var(--space-lg)]'\n }\n },\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n }\n }\n });\n</script>\n```\n";
|
|
@@ -65,10 +65,13 @@ The Marquee component creates an infinite scrolling animation for displaying con
|
|
|
65
65
|
- Uses a static scroll-fade mode because the marquee is clipped animation, not a native scroll container
|
|
66
66
|
|
|
67
67
|
- **size**: 'small' | 'normal' | 'large' (default: 'normal')
|
|
68
|
-
-
|
|
69
|
-
- \`small\`:
|
|
70
|
-
- \`normal\`:
|
|
71
|
-
- \`large\`:
|
|
68
|
+
- Sets the marquee's one spacing value, \`--gap\`, from the theme spacing scale
|
|
69
|
+
- \`small\`: \`--space-md\`
|
|
70
|
+
- \`normal\`: \`--space-xl\`
|
|
71
|
+
- \`large\`: \`--layout-space-md\`
|
|
72
|
+
- The copies, the items inside each copy and the loop step all use \`--gap\`, so the loop stays
|
|
73
|
+
seamless at every size and theme spacing. To respace a marquee, set \`--gap\` once on the root
|
|
74
|
+
(\`class="[--gap:var(--space-lg)]"\`); never redeclare it on the copies.
|
|
72
75
|
|
|
73
76
|
### Styling Props
|
|
74
77
|
- **ref**: HTMLDivElement | null (bindable)
|
|
@@ -245,7 +248,7 @@ The Marquee component renders:
|
|
|
245
248
|
|
|
246
249
|
- Uses CSS keyframe animations for smooth GPU-accelerated performance
|
|
247
250
|
- Content is duplicated \`numberOfCopies\` times to create seamless infinite scroll
|
|
248
|
-
- Animation translates each copy by -100% of its width/height plus gap
|
|
251
|
+
- Animation translates each copy by -100% of its width/height plus \`--gap\`, the same value the copies are spaced with
|
|
249
252
|
- Animation duration is controlled via CSS custom property \`--animation-duration\`
|
|
250
253
|
- Pause on hover uses CSS \`animation-play-state\` for instant response
|
|
251
254
|
- Reverse mode uses CSS \`direction: reverse\` for the animation
|
|
@@ -256,7 +259,7 @@ The Marquee component renders:
|
|
|
256
259
|
- Consider providing alternative static content for screen readers
|
|
257
260
|
- Ensure sufficient color foreground for text content
|
|
258
261
|
- Pause on hover helps users interact with and read content
|
|
259
|
-
- Animation is disabled when the user requests reduced motion
|
|
262
|
+
- Animation is disabled when the user requests reduced motion; the still row then fades only its trailing edge
|
|
260
263
|
|
|
261
264
|
## Notes
|
|
262
265
|
|
|
@@ -307,7 +310,7 @@ The theme object contains the following parts:
|
|
|
307
310
|
left: 'flex-row'
|
|
308
311
|
},
|
|
309
312
|
size: {
|
|
310
|
-
normal: 'gap-
|
|
313
|
+
normal: '[--gap:var(--space-lg)]'
|
|
311
314
|
}
|
|
312
315
|
}
|
|
313
316
|
}}
|
|
@@ -329,9 +332,6 @@ The theme object contains the following parts:
|
|
|
329
332
|
},
|
|
330
333
|
pauseOnHover: {
|
|
331
334
|
true: 'group-hover:[animation-play-state:paused]'
|
|
332
|
-
},
|
|
333
|
-
size: {
|
|
334
|
-
large: 'gap-6'
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
}}
|
|
@@ -354,7 +354,7 @@ The theme object contains the following parts:
|
|
|
354
354
|
left: 'flex-row'
|
|
355
355
|
},
|
|
356
356
|
size: {
|
|
357
|
-
normal: 'gap-
|
|
357
|
+
normal: '[--gap:var(--space-lg)]'
|
|
358
358
|
}
|
|
359
359
|
},
|
|
360
360
|
inner: {
|