entasis 0.13.6 → 0.13.8

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.
@@ -527,6 +527,7 @@ export declare class FormState<I extends FormInputs = FormInputs> {
527
527
  } & Omit<import("../Field/field.js").InputProps<"signature">, "theme"> & {
528
528
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat;
529
529
  inkColor?: string;
530
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk;
530
531
  strokeWidth?: number;
531
532
  placeholder?: string;
532
533
  clearLabel?: string;
@@ -1412,6 +1413,7 @@ export declare class FormState<I extends FormInputs = FormInputs> {
1412
1413
  } & Omit<import("../Field/field.js").InputProps<"signature">, "theme"> & {
1413
1414
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat;
1414
1415
  inkColor?: string;
1416
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk;
1415
1417
  strokeWidth?: number;
1416
1418
  placeholder?: string;
1417
1419
  clearLabel?: string;
@@ -2493,6 +2495,7 @@ export declare class FormState<I extends FormInputs = FormInputs> {
2493
2495
  } & Omit<import("../Field/field.js").InputProps<"signature">, "theme"> & {
2494
2496
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat;
2495
2497
  inkColor?: string;
2498
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk;
2496
2499
  strokeWidth?: number;
2497
2500
  placeholder?: string;
2498
2501
  clearLabel?: string;
@@ -3378,6 +3381,7 @@ export declare class FormState<I extends FormInputs = FormInputs> {
3378
3381
  } & Omit<import("../Field/field.js").InputProps<"signature">, "theme"> & {
3379
3382
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat;
3380
3383
  inkColor?: string;
3384
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk;
3381
3385
  strokeWidth?: number;
3382
3386
  placeholder?: string;
3383
3387
  clearLabel?: string;
@@ -1400,6 +1400,7 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
1400
1400
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat | undefined;
1401
1401
  clearLabel?: string | undefined;
1402
1402
  inkColor?: string | undefined;
1403
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk | undefined;
1403
1404
  strokeWidth?: number | undefined;
1404
1405
  undoLabel?: string | undefined;
1405
1406
  display?: string;
@@ -7755,6 +7756,7 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
7755
7756
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat | undefined;
7756
7757
  clearLabel?: string | undefined;
7757
7758
  inkColor?: string | undefined;
7759
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk | undefined;
7758
7760
  strokeWidth?: number | undefined;
7759
7761
  undoLabel?: string | undefined;
7760
7762
  display?: string;
@@ -442,6 +442,7 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
442
442
  } & Omit<import("../Field/field.ts").InputProps<"signature">, "theme"> & {
443
443
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat;
444
444
  inkColor?: string;
445
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk;
445
446
  strokeWidth?: number;
446
447
  placeholder?: string;
447
448
  clearLabel?: string;
@@ -1327,6 +1328,7 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
1327
1328
  } & Omit<import("../Field/field.ts").InputProps<"signature">, "theme"> & {
1328
1329
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat;
1329
1330
  inkColor?: string;
1331
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk;
1330
1332
  strokeWidth?: number;
1331
1333
  placeholder?: string;
1332
1334
  clearLabel?: string;
@@ -2408,6 +2410,7 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
2408
2410
  } & Omit<import("../Field/field.ts").InputProps<"signature">, "theme"> & {
2409
2411
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat;
2410
2412
  inkColor?: string;
2413
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk;
2411
2414
  strokeWidth?: number;
2412
2415
  placeholder?: string;
2413
2416
  clearLabel?: string;
@@ -3293,6 +3296,7 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
3293
3296
  } & Omit<import("../Field/field.ts").InputProps<"signature">, "theme"> & {
3294
3297
  format?: import("../SignatureInput/signatureInput.strokes.ts").SignatureFormat;
3295
3298
  inkColor?: string;
3299
+ imageInk?: import("../SignatureInput/signatureInput.props.ts").SignatureImageInk;
3296
3300
  strokeWidth?: number;
3297
3301
  placeholder?: string;
3298
3302
  clearLabel?: string;
@@ -33,6 +33,7 @@
33
33
  size = 'normal',
34
34
  format = 'png',
35
35
  inkColor = '#000000',
36
+ imageInk = 'theme',
36
37
  strokeWidth = 2.5,
37
38
  placeholder,
38
39
  clearLabel,
@@ -125,6 +126,27 @@
125
126
 
126
127
  const hasInk = $derived(strokes.length > 0 || current !== null);
127
128
  const image = $derived(hasInk ? null : (field.value ?? null));
129
+
130
+ // A mask image is fetched in CORS mode: a URL from another origin that does not allow it would
131
+ // paint nothing. Such a URL is probed once and, when refused, shown as the image itself.
132
+ let refusedMask = $state<string | null>(null);
133
+ const ownsImage = (url: string) =>
134
+ /^(data|blob):/.test(url) || new URL(url, location.href).origin === location.origin;
135
+ $effect(() => {
136
+ if (imageInk !== 'theme' || !image || ownsImage(image)) return;
137
+ const probe = new Image();
138
+ probe.crossOrigin = 'anonymous';
139
+ probe.onerror = () => (refusedMask = image);
140
+ probe.src = image;
141
+ return () => {
142
+ probe.onerror = null;
143
+ };
144
+ });
145
+ const imageMode = $derived(imageInk === 'theme' && refusedMask !== image ? 'theme' : 'as-is');
146
+ // Quoted for `url()`: a data URL may hold characters CSS would end the string on.
147
+ const maskImage = $derived(
148
+ image ? `url("${image.replace(/["\\\n]/g, (char) => encodeURIComponent(char))}")` : undefined
149
+ );
128
150
  const shapes = $derived(
129
151
  [...strokes, ...(current ? [current] : [])].flatMap((stroke) =>
130
152
  strokeShapes(stroke, strokeWidth)
@@ -277,7 +299,23 @@
277
299
  </div>
278
300
  {/if}
279
301
  {#if image}
280
- <img data-slot="signature-image" src={image} alt="" class={classes.image()} />
302
+ {#if imageMode === 'theme'}
303
+ <span
304
+ data-slot="signature-image"
305
+ data-ink="theme"
306
+ class={classes.image({ ink: 'theme' })}
307
+ style:mask-image={maskImage}
308
+ style:-webkit-mask-image={maskImage}
309
+ ></span>
310
+ {:else}
311
+ <img
312
+ data-slot="signature-image"
313
+ data-ink="as-is"
314
+ src={image}
315
+ alt=""
316
+ class={classes.image({ ink: 'as-is' })}
317
+ />
318
+ {/if}
281
319
  {/if}
282
320
  <svg data-slot="signature-ink" class={classes.ink()} aria-hidden="true" focusable="false">
283
321
  <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
@@ -1,4 +1,4 @@
1
1
  export { default as SignatureInput } from './SignatureInput.svelte';
2
- export type { SignatureInputProps } from './signatureInput.props.js';
2
+ export type { SignatureImageInk, SignatureInputProps } from './signatureInput.props.js';
3
3
  export type { SignatureFormat, SignaturePoint, SignatureStroke } from './signatureInput.strokes.js';
4
4
  export { signatureInputTheme, setSignatureInputTheme, useSignatureInputTheme, type SignatureInputTheme, type SignatureInputThemeProps } from './signatureInput.theme.js';
@@ -1 +1 @@
1
- export declare const signatureInputDescription = "\n# SignatureInput Component\n\nSignatureInput is a form field where a person signs with a mouse, a finger or a pen. Its value is the signature as an image: a data URL cropped to the ink (PNG by default, or SVG), ready to store, submit or place in a document.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n import { SignatureInput } from './index.ts';\n\n let signature = $state<string | null>(null);\n</script>\n\n<SignatureInput label=\"Signature\" required bind:value={signature} />\n```\n\nIn a Form or `ask()`, use `type: 'signature'`:\n\n```ts\nimport { ask } from 'entasis/ask';\n\nconst result = await ask({\n title: 'Sign the mandate',\n inputs: { signature: { type: 'signature', label: 'Signature', required: true } },\n confirm: 'Sign',\n cancel: 'Cancel'\n});\n```\n\n## Props\n\nExtends the shared Field props (`label`, `description`, `helper`, `required`, `disabled`, `readOnly`, `name`, `size`, `density`, `labelPosition`, `messages`, `onValidate`, `errors`, `focused`, `visible`) plus:\n\n- **value**: `string | null` (bindable) \u2014 the signature as a data URL, or `null` while the pad is empty.\n- **defaultValue**: `string | null` \u2014 initial value when `value` is omitted. Any image data URL works: a saved signature shows in the pad, scaled to fit.\n- **onValueChange**: `(value: string | null) => void` \u2014 called once per finished stroke, undo and clear.\n- **format**: `'png' | 'svg'` (default `'png'`) \u2014 the image the value holds. A PNG is drawn at twice the pad's resolution; an SVG keeps every stroke as a vector path.\n- **inkColor**: `string` (default `'#000000'`) \u2014 CSS color of the ink in the exported image, so a signature reads on a white document whatever the app's theme. The pad itself draws in the theme's text color.\n- **strokeWidth**: `number` (default `2.5`) \u2014 line width in CSS pixels at a medium pen pressure. A pen's pressure makes the line thinner or thicker (half to one and a half times); a mouse or a finger draws at this width.\n- **placeholder**: `string` \u2014 hint on the signing line while the pad is empty. Defaults to the i18n `signatureInputPlaceholder` (\"Sign here\").\n- **clearLabel**, **undoLabel**: `string` \u2014 accessible labels and tooltips of the two buttons.\n- **i18n**: `Partial<Messages>` \u2014 per-instance translations.\n- **size**: `'small' | 'normal' | 'large'` \u2014 pad height (112, 144 or 176 px) and placeholder text size. Override the height with the `pad` theme part.\n- **theme**: `SignatureInputThemeProps` and Field theme parts.\n\n## Behaviour\n\n- Each stroke runs from pointer down to pointer up and is smoothed through its samples. The value updates when a stroke ends, not while it is drawn.\n- The undo button (or Ctrl/Cmd+Z on the pad) removes the last stroke; the clear button (or Delete/Backspace on the pad) empties the pad and sets the value to `null`. Both show only while the pad is editable and has a signature.\n- A value set from outside (a saved signature, a Form reset) replaces the strokes and shows as an image. Drawing over it starts a new signature: the image is replaced, not added to. Undo only steps back through strokes drawn since.\n- `disabled` and `readOnly` stop drawing and hide the buttons; a read-only pad still shows, focuses, validates and submits its value. A stroke in progress is dropped when the pad becomes disabled or read-only.\n- Required: an empty pad (`null` or `''`) fails `required`.\n- Native forms: a hidden input (`FieldHiddenInputs`) submits the data URL under `name`.\n\n## Accessibility\n\n- The pad is the field's control: it is focusable, labelled by the field label (or \"Signature\" without one), and has `role=\"img\"` with a description of its state (\"No signature yet\u2026\" or \"Signed.\"). Errors and descriptions are linked by Field.\n- Drawing is a path-based gesture with no keyboard equivalent: signing by hand is the purpose of the field. Keyboard users can clear and undo with the buttons and keys above. When a keyboard-only path to consent is needed, pair the pad with another field (a typed full name, a consent checkbox).\n- The ink, placeholder and image are decorative (`aria-hidden`, empty `alt`).\n\n## Styling hooks\n\n- `data-slot`: `signature-frame`, `signature-pad`, `signature-ink`, `signature-image`, `signature-placeholder`, `signature-actions`.\n- On the pad: `data-filled=\"true\"` while it holds a signature, `data-drawing=\"true\"` during a stroke.\n\n## Theme parts\n\n`frame` (the pad and its buttons), `pad` (variants `size`, `interactive`, `disabled`, `hasError`), `ink` (the SVG of strokes), `image` (a value set from outside), `placeholder` (the signing line and hint), `actions` (the undo and clear buttons).\n\n## Exports\n\n`SignatureInput`, `SignatureInputProps`, `SignatureFormat`, `SignaturePoint`, `SignatureStroke`, `signatureInputTheme`, `setSignatureInputTheme`, `useSignatureInputTheme`, `SignatureInputTheme`, `SignatureInputThemeProps`.\n";
1
+ export declare const signatureInputDescription = "\n# SignatureInput Component\n\nSignatureInput is a form field where a person signs with a mouse, a finger or a pen. Its value is the signature as an image: a data URL cropped to the ink (PNG by default, or SVG), ready to store, submit or place in a document.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n import { SignatureInput } from './index.ts';\n\n let signature = $state<string | null>(null);\n</script>\n\n<SignatureInput label=\"Signature\" required bind:value={signature} />\n```\n\nIn a Form or `ask()`, use `type: 'signature'`:\n\n```ts\nimport { ask } from 'entasis/ask';\n\nconst result = await ask({\n title: 'Sign the mandate',\n inputs: { signature: { type: 'signature', label: 'Signature', required: true } },\n confirm: 'Sign',\n cancel: 'Cancel'\n});\n```\n\n## Props\n\nExtends the shared Field props (`label`, `description`, `helper`, `required`, `disabled`, `readOnly`, `name`, `size`, `density`, `labelPosition`, `messages`, `onValidate`, `errors`, `focused`, `visible`) plus:\n\n- **value**: `string | null` (bindable) \u2014 the signature as a data URL, or `null` while the pad is empty.\n- **defaultValue**: `string | null` \u2014 initial value when `value` is omitted. Any image data URL works: a saved signature shows in the pad, scaled to fit.\n- **onValueChange**: `(value: string | null) => void` \u2014 called once per finished stroke, undo and clear.\n- **format**: `'png' | 'svg'` (default `'png'`) \u2014 the image the value holds. A PNG is drawn at twice the pad's resolution; an SVG keeps every stroke as a vector path.\n- **inkColor**: `string` (default `'#000000'`) \u2014 CSS color of the ink in the exported image, so a signature reads on a white document whatever the app's theme. The pad itself draws in the theme's text color.\n- **imageInk**: `'theme' | 'as-is'` (default `'theme'`) \u2014 how a value set from outside (a saved signature) shows in the pad. `'theme'` paints its shape in the pad's ink color, like live strokes (the image becomes a CSS mask over `bg-current`; only its transparency counts), so a black signature saved for paper reads on a dark theme without the app knowing which mode is on. `'as-is'` shows the image unchanged: for a signature with several colors or an opaque background. The value is never changed. A URL from another origin is fetched in CORS mode for the mask; when that origin refuses CORS, the pad falls back to the image as it is.\n- **strokeWidth**: `number` (default `2.5`) \u2014 line width in CSS pixels at a medium pen pressure. A pen's pressure makes the line thinner or thicker (half to one and a half times); a mouse or a finger draws at this width.\n- **placeholder**: `string` \u2014 hint on the signing line while the pad is empty. Defaults to the i18n `signatureInputPlaceholder` (\"Sign here\").\n- **clearLabel**, **undoLabel**: `string` \u2014 accessible labels and tooltips of the two buttons.\n- **i18n**: `Partial<Messages>` \u2014 per-instance translations.\n- **size**: `'small' | 'normal' | 'large'` \u2014 pad height (112, 144 or 176 px) and placeholder text size. Override the height with the `pad` theme part.\n- **theme**: `SignatureInputThemeProps` and Field theme parts.\n\n## Behaviour\n\n- Each stroke runs from pointer down to pointer up and is smoothed through its samples. The value updates when a stroke ends, not while it is drawn.\n- The undo button (or Ctrl/Cmd+Z on the pad) removes the last stroke; the clear button (or Delete/Backspace on the pad) empties the pad and sets the value to `null`. Both show only while the pad is editable and has a signature.\n- A value set from outside (a saved signature, a Form reset) replaces the strokes and shows as an image. Drawing over it starts a new signature: the image is replaced, not added to. Undo only steps back through strokes drawn since.\n- `disabled` and `readOnly` stop drawing and hide the buttons; a read-only pad still shows, focuses, validates and submits its value. A stroke in progress is dropped when the pad becomes disabled or read-only.\n- Required: an empty pad (`null` or `''`) fails `required`.\n- Native forms: a hidden input (`FieldHiddenInputs`) submits the data URL under `name`.\n\n## Accessibility\n\n- The pad is the field's control: it is focusable, labelled by the field label (or \"Signature\" without one), and has `role=\"img\"` with a description of its state (\"No signature yet\u2026\" or \"Signed.\"). Errors and descriptions are linked by Field.\n- Drawing is a path-based gesture with no keyboard equivalent: signing by hand is the purpose of the field. Keyboard users can clear and undo with the buttons and keys above. When a keyboard-only path to consent is needed, pair the pad with another field (a typed full name, a consent checkbox).\n- The ink, placeholder and image are decorative (`aria-hidden`, empty `alt`).\n\n## Styling hooks\n\n- `data-slot`: `signature-frame`, `signature-pad`, `signature-ink`, `signature-image`, `signature-placeholder`, `signature-actions`.\n- On the pad: `data-filled=\"true\"` while it holds a signature, `data-drawing=\"true\"` during a stroke.\n\n## Theme parts\n\n`frame` (the pad and its buttons), `pad` (variants `size`, `interactive`, `disabled`, `hasError`), `ink` (the SVG of strokes), `image` (a value set from outside), `placeholder` (the signing line and hint), `actions` (the undo and clear buttons).\n\n## Exports\n\n`SignatureInput`, `SignatureInputProps`, `SignatureFormat`, `SignaturePoint`, `SignatureStroke`, `signatureInputTheme`, `setSignatureInputTheme`, `useSignatureInputTheme`, `SignatureInputTheme`, `SignatureInputThemeProps`.\n";
@@ -37,6 +37,7 @@ Extends the shared Field props (\`label\`, \`description\`, \`helper\`, \`requir
37
37
  - **onValueChange**: \`(value: string | null) => void\` — called once per finished stroke, undo and clear.
38
38
  - **format**: \`'png' | 'svg'\` (default \`'png'\`) — the image the value holds. A PNG is drawn at twice the pad's resolution; an SVG keeps every stroke as a vector path.
39
39
  - **inkColor**: \`string\` (default \`'#000000'\`) — CSS color of the ink in the exported image, so a signature reads on a white document whatever the app's theme. The pad itself draws in the theme's text color.
40
+ - **imageInk**: \`'theme' | 'as-is'\` (default \`'theme'\`) — how a value set from outside (a saved signature) shows in the pad. \`'theme'\` paints its shape in the pad's ink color, like live strokes (the image becomes a CSS mask over \`bg-current\`; only its transparency counts), so a black signature saved for paper reads on a dark theme without the app knowing which mode is on. \`'as-is'\` shows the image unchanged: for a signature with several colors or an opaque background. The value is never changed. A URL from another origin is fetched in CORS mode for the mask; when that origin refuses CORS, the pad falls back to the image as it is.
40
41
  - **strokeWidth**: \`number\` (default \`2.5\`) — line width in CSS pixels at a medium pen pressure. A pen's pressure makes the line thinner or thicker (half to one and a half times); a mouse or a finger draws at this width.
41
42
  - **placeholder**: \`string\` — hint on the signing line while the pad is empty. Defaults to the i18n \`signatureInputPlaceholder\` ("Sign here").
42
43
  - **clearLabel**, **undoLabel**: \`string\` — accessible labels and tooltips of the two buttons.
@@ -3,6 +3,7 @@ import type { WithAttachments } from '../../../types/props.js';
3
3
  import type { InputProps } from '../Field/field.js';
4
4
  import type { SignatureFormat } from './signatureInput.strokes.js';
5
5
  import type { SignatureInputThemeProps } from './signatureInput.theme.js';
6
+ export type SignatureImageInk = 'theme' | 'as-is';
6
7
  export type SignatureInputProps = WithAttachments<Omit<InputProps<'signature'>, 'theme'> & {
7
8
  /**
8
9
  * Image the value holds, as a data URL cropped to the ink: `'png'` (default, drawn at twice
@@ -14,6 +15,14 @@ export type SignatureInputProps = WithAttachments<Omit<InputProps<'signature'>,
14
15
  * white document whatever the app's theme; the pad itself draws in the theme's text color.
15
16
  */
16
17
  inkColor?: string;
18
+ /**
19
+ * How a value set from outside (a saved signature) shows in the pad. `'theme'` (default)
20
+ * paints its shape in the pad's ink color, as live strokes are, so a black signature saved
21
+ * for paper still reads on a dark theme; only its transparency counts. `'as-is'` shows the
22
+ * image unchanged: for a signature with several colors or an opaque background. The value
23
+ * itself is never changed.
24
+ */
25
+ imageInk?: SignatureImageInk;
17
26
  /** Line width in CSS pixels at a medium pen pressure. Defaults to 2.5. */
18
27
  strokeWidth?: number;
19
28
  /** Hint on the signing line while the pad is empty. Defaults to the i18n `signatureInputPlaceholder`. */
@@ -21,7 +21,12 @@ export declare const signatureInputTheme: {
21
21
  };
22
22
  }> & import("../../../utils/cva/types.ts").CVAClassProp) | undefined) => string;
23
23
  ink: (props?: import("../../../utils/cva/types.ts").CVAClassProp | undefined) => string;
24
- image: (props?: import("../../../utils/cva/types.ts").CVAClassProp | undefined) => string;
24
+ image: (props?: (import("../../../utils/cva/types.ts").CVAVariantSchema<{
25
+ ink: {
26
+ theme: string;
27
+ 'as-is': string;
28
+ };
29
+ }> & import("../../../utils/cva/types.ts").CVAClassProp) | undefined) => string;
25
30
  placeholder: (props?: (import("../../../utils/cva/types.ts").CVAVariantSchema<{
26
31
  size: {
27
32
  small: string;
@@ -55,7 +60,12 @@ export declare const setSignatureInputTheme: (theme: InferComponentTheme<{
55
60
  };
56
61
  }> & import("../../../utils/cva/types.ts").CVAClassProp) | undefined) => string;
57
62
  ink: (props?: import("../../../utils/cva/types.ts").CVAClassProp | undefined) => string;
58
- image: (props?: import("../../../utils/cva/types.ts").CVAClassProp | undefined) => string;
63
+ image: (props?: (import("../../../utils/cva/types.ts").CVAVariantSchema<{
64
+ ink: {
65
+ theme: string;
66
+ 'as-is': string;
67
+ };
68
+ }> & import("../../../utils/cva/types.ts").CVAClassProp) | undefined) => string;
59
69
  placeholder: (props?: (import("../../../utils/cva/types.ts").CVAVariantSchema<{
60
70
  size: {
61
71
  small: string;
@@ -87,7 +97,12 @@ export declare const useSignatureInputTheme: import("../../../utils/cva/theme.ts
87
97
  };
88
98
  }> & import("../../../utils/cva/types.ts").CVAClassProp) | undefined) => string;
89
99
  ink: (props?: import("../../../utils/cva/types.ts").CVAClassProp | undefined) => string;
90
- image: (props?: import("../../../utils/cva/types.ts").CVAClassProp | undefined) => string;
100
+ image: (props?: (import("../../../utils/cva/types.ts").CVAVariantSchema<{
101
+ ink: {
102
+ theme: string;
103
+ 'as-is': string;
104
+ };
105
+ }> & import("../../../utils/cva/types.ts").CVAClassProp) | undefined) => string;
91
106
  placeholder: (props?: (import("../../../utils/cva/types.ts").CVAVariantSchema<{
92
107
  size: {
93
108
  small: string;
@@ -37,9 +37,20 @@ const defaultPad = cva({
37
37
  const defaultInk = cva({
38
38
  base: 'pointer-events-none absolute inset-0 size-full overflow-visible'
39
39
  });
40
- // A value set from outside (a saved signature) shows as an image, scaled to fit the pad.
40
+ // A value set from outside (a saved signature) shows scaled to fit the pad. With `imageInk`
41
+ // 'theme' it is a mask over the pad's ink color (`bg-current`), so a black signature saved for
42
+ // paper reads on a dark surface; 'as-is' is the image itself.
41
43
  const defaultImage = cva({
42
- base: 'pointer-events-none absolute inset-0 size-full object-contain p-md'
44
+ base: 'pointer-events-none absolute inset-0 size-full p-md',
45
+ variants: {
46
+ ink: {
47
+ theme: 'bg-current [mask-clip:content-box] [mask-origin:content-box] [mask-position:center] [mask-repeat:no-repeat] [mask-size:contain]',
48
+ 'as-is': 'object-contain'
49
+ }
50
+ },
51
+ defaultVariants: {
52
+ ink: 'theme'
53
+ }
43
54
  });
44
55
  // The line to sign on and its hint: decorative, gone as soon as there is ink.
45
56
  const defaultPlaceholder = cva({
@@ -266,7 +266,7 @@
266
266
  });
267
267
 
268
268
  function hasIcon(entry: SidebarMenuEntry) {
269
- return entry.icon != null;
269
+ return entry.icon != null || entry.media != null;
270
270
  }
271
271
 
272
272
  function hasCollapsibleMedia(button: SidebarMenuButtonItem | undefined) {
@@ -28,6 +28,7 @@
28
28
  import SidebarFlyout from './SidebarFlyout.svelte';
29
29
  import SidebarIcon from './SidebarIcon.svelte';
30
30
  import SidebarMenuSubItem from './SidebarMenuSubItem.svelte';
31
+ import SidebarMenuText from './SidebarMenuText.svelte';
31
32
  import { useSidebarSuffixWidth } from './sidebar.suffix.svelte.js';
32
33
  import { useSidebarMotion, useSidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
33
34
 
@@ -82,6 +83,13 @@
82
83
  );
83
84
  const hasSubmenu = $derived(!!item.items?.length);
84
85
  const showBadge = $derived(item.badge != null && !isIconCollapsed);
86
+ // A description stacks a second line under the label, so the row takes the large height
87
+ // unless the entry sets its own size.
88
+ const rowSize = $derived(item.size ?? (item.description != null ? 'large' : undefined));
89
+ const lead = $derived(item.media ? 'media' : 'icon');
90
+ // On a two-line row the suffix and the badge sit on the description's line, so the label's
91
+ // line, and its meta, run to the same end on every row.
92
+ const twoLine = $derived(item.description != null);
85
93
  // An in-flow chevron or dots after the badge: the row then keeps its action's room at every
86
94
  // width, since the action would otherwise cover that glyph when it shows.
87
95
  const hasTrailingGlyph = $derived(
@@ -94,8 +102,12 @@
94
102
  const endReserve = $derived(
95
103
  item.suffix
96
104
  ? item.action
97
- ? 'suffixAction'
98
- : 'suffix'
105
+ ? twoLine
106
+ ? 'suffixLineAction'
107
+ : 'suffixAction'
108
+ : twoLine
109
+ ? 'suffixLine'
110
+ : 'suffix'
99
111
  : !item.action
100
112
  ? 'none'
101
113
  : showBadge && !hasTrailingGlyph
@@ -176,7 +188,11 @@
176
188
  </script>
177
189
 
178
190
  {#snippet entryContent()}
179
- {#if hasIconSurface}
191
+ {#if item.media}
192
+ <span data-slot="sidebar-menu-media" data-sidebar="menu-media" class={classes.menuMedia()}>
193
+ {@render item.media(api, item)}
194
+ </span>
195
+ {:else if hasIconSurface}
180
196
  <span
181
197
  data-slot="sidebar-menu-icon"
182
198
  data-color={item.iconColor}
@@ -187,7 +203,25 @@
187
203
  {:else}
188
204
  <SidebarIcon icon={item.icon} class={back ? 'rtl:-scale-x-100' : undefined} />
189
205
  {/if}
190
- <span class={classes.menuLabel()}>{item.label}</span>
206
+ {#if item.description != null || item.meta != null}
207
+ <SidebarMenuText
208
+ label={item.label}
209
+ description={item.description}
210
+ meta={isIconCollapsed ? undefined : item.meta}
211
+ suffix={!!item.suffix}
212
+ trailing={twoLine ? badge : undefined}
213
+ {size}
214
+ {theme}
215
+ />
216
+ {:else}
217
+ <span class={classes.menuLabel()}>{item.label}</span>
218
+ {/if}
219
+ {#if !twoLine}
220
+ {@render badge()}
221
+ {/if}
222
+ {/snippet}
223
+
224
+ {#snippet badge()}
191
225
  {#if showBadge}
192
226
  <span
193
227
  data-slot="sidebar-menu-badge"
@@ -224,7 +258,7 @@
224
258
  role={item.disabled ? 'link' : undefined}
225
259
  data-slot="sidebar-menu-button"
226
260
  data-sidebar="menu-button"
227
- data-size={item.size ?? 'normal'}
261
+ data-size={rowSize ?? 'normal'}
228
262
  data-active={item.isActive ? 'true' : undefined}
229
263
  data-active-variant={activeVariant}
230
264
  aria-current={item.isActive ? 'page' : undefined}
@@ -236,7 +270,8 @@
236
270
  activeVariant,
237
271
  size,
238
272
  density,
239
- itemSize: item.size,
273
+ itemSize: rowSize,
274
+ lead,
240
275
  endReserve,
241
276
  className: item.class
242
277
  })}
@@ -254,7 +289,7 @@
254
289
  type="button"
255
290
  data-slot="sidebar-menu-button"
256
291
  data-sidebar="menu-button"
257
- data-size={item.size ?? 'normal'}
292
+ data-size={rowSize ?? 'normal'}
258
293
  data-active={item.isActive ? 'true' : undefined}
259
294
  data-active-variant={activeVariant}
260
295
  aria-current={item.isActive ? 'page' : undefined}
@@ -267,7 +302,8 @@
267
302
  activeVariant,
268
303
  size,
269
304
  density,
270
- itemSize: item.size,
305
+ itemSize: rowSize,
306
+ lead,
271
307
  endReserve,
272
308
  className: item.class
273
309
  })}
@@ -302,7 +338,7 @@
302
338
  type="button"
303
339
  data-slot="sidebar-menu-button"
304
340
  data-sidebar="menu-button"
305
- data-size={item.size ?? 'normal'}
341
+ data-size={rowSize ?? 'normal'}
306
342
  data-active-variant={activeVariant}
307
343
  disabled={item.disabled || undefined}
308
344
  class={classes.menuButton({
@@ -310,7 +346,8 @@
310
346
  activeVariant,
311
347
  size,
312
348
  density,
313
- itemSize: item.size,
349
+ itemSize: rowSize,
350
+ lead,
314
351
  endReserve,
315
352
  className: ['aria-expanded:bg-neutral-muted', item.class]
316
353
  })}
@@ -339,7 +376,7 @@
339
376
  type="button"
340
377
  data-slot="sidebar-menu-button"
341
378
  data-sidebar="menu-button"
342
- data-size={item.size ?? 'normal'}
379
+ data-size={rowSize ?? 'normal'}
343
380
  data-active={item.isActive ? 'true' : undefined}
344
381
  data-active-variant={activeVariant}
345
382
  class={classes.menuButton({
@@ -347,7 +384,8 @@
347
384
  activeVariant,
348
385
  size,
349
386
  density,
350
- itemSize: item.size,
387
+ itemSize: rowSize,
388
+ lead,
351
389
  endReserve,
352
390
  className: item.class
353
391
  })}
@@ -434,7 +472,7 @@
434
472
  type="button"
435
473
  data-slot="sidebar-menu-button"
436
474
  data-sidebar="menu-button"
437
- data-size={item.size ?? 'normal'}
475
+ data-size={rowSize ?? 'normal'}
438
476
  data-active={item.isActive ? 'true' : undefined}
439
477
  data-active-variant={activeVariant}
440
478
  disabled={item.disabled || undefined}
@@ -443,7 +481,8 @@
443
481
  activeVariant,
444
482
  size,
445
483
  density,
446
- itemSize: item.size,
484
+ itemSize: rowSize,
485
+ lead,
447
486
  endReserve,
448
487
  className: item.class
449
488
  })}
@@ -473,7 +512,12 @@
473
512
  data-sidebar="menu-suffix"
474
513
  inert={isIconCollapsed ? true : undefined}
475
514
  aria-hidden={isIconCollapsed ? 'true' : undefined}
476
- class={classes.suffix({ size, density, action: item.action ? 'action' : 'none' })}
515
+ class={classes.suffix({
516
+ size,
517
+ density,
518
+ action: item.action ? 'action' : 'none',
519
+ line: twoLine ? 'two' : 'one'
520
+ })}
477
521
  {@attach suffixWidth.measure}
478
522
  >
479
523
  {@render item.suffix(api, item)}
@@ -11,6 +11,7 @@
11
11
  import { sidebarInwardPhysicalSide, sidebarTooltip } from './sidebar-position.js';
12
12
  import SidebarAction from './SidebarAction.svelte';
13
13
  import SidebarIcon from './SidebarIcon.svelte';
14
+ import SidebarMenuText from './SidebarMenuText.svelte';
14
15
  import { useSidebarSuffixWidth } from './sidebar.suffix.svelte.js';
15
16
  import { useSidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
16
17
 
@@ -39,6 +40,9 @@
39
40
  // tile, so an untinted row never inherits the ambient `data-color`.
40
41
  const hasIconSurface = $derived(!!sub.icon && (sub.iconVariant === 'tile' || !!sub.iconColor));
41
42
  const hasBadge = $derived(sub.badge != null);
43
+ // On a two-line row the suffix and the badge sit on the description's line, so the label's
44
+ // line, and its meta, run to the same end on every row.
45
+ const twoLine = $derived(sub.description != null);
42
46
  const suffixWidth = useSidebarSuffixWidth();
43
47
  const uid = $props.id();
44
48
  const suffixId = `${uid}-suffix`;
@@ -63,13 +67,18 @@
63
67
  // stays visible before the action, so a row with one reserves both at every width.
64
68
  endReserve: sub.suffix
65
69
  ? sub.action
66
- ? 'suffixAction'
67
- : 'suffix'
70
+ ? twoLine
71
+ ? 'suffixLineAction'
72
+ : 'suffixAction'
73
+ : twoLine
74
+ ? 'suffixLine'
75
+ : 'suffix'
68
76
  : !sub.action
69
77
  ? 'none'
70
78
  : hasBadge
71
79
  ? 'actionBelowMd'
72
80
  : 'action',
81
+ tall: sub.description != null || sub.media != null,
73
82
  className: sub.class
74
83
  })
75
84
  );
@@ -85,7 +94,11 @@
85
94
  </script>
86
95
 
87
96
  {#snippet entryContent()}
88
- {#if hasIconSurface}
97
+ {#if sub.media}
98
+ <span data-slot="sidebar-menu-media" data-sidebar="menu-media" class={classes.menuMedia()}>
99
+ {@render sub.media(api, sub)}
100
+ </span>
101
+ {:else if hasIconSurface}
89
102
  <span
90
103
  data-slot="sidebar-menu-icon"
91
104
  data-color={sub.iconColor}
@@ -100,20 +113,37 @@
100
113
  {:else}
101
114
  <SidebarIcon icon={sub.icon} />
102
115
  {/if}
103
- {#if hasBadge}
116
+ {#if sub.description != null || sub.meta != null}
117
+ <SidebarMenuText
118
+ label={sub.label}
119
+ description={sub.description}
120
+ meta={sub.meta}
121
+ suffix={!!sub.suffix}
122
+ trailing={twoLine && hasBadge ? badge : undefined}
123
+ size={subSize}
124
+ {theme}
125
+ />
126
+ {#if !twoLine && hasBadge}
127
+ {@render badge()}
128
+ {/if}
129
+ {:else if hasBadge}
104
130
  <span data-slot="sidebar-menu-sub-label">{sub.label}</span>
105
- <span
106
- data-slot="sidebar-menu-sub-badge"
107
- data-sidebar="menu-sub-badge"
108
- class={classes.badge({ size: subSize, action: sub.action && !sub.suffix ? 'sub' : 'none' })}
109
- >
110
- {sub.badge}
111
- </span>
131
+ {@render badge()}
112
132
  {:else}
113
133
  <span>{sub.label}</span>
114
134
  {/if}
115
135
  {/snippet}
116
136
 
137
+ {#snippet badge()}
138
+ <span
139
+ data-slot="sidebar-menu-sub-badge"
140
+ data-sidebar="menu-sub-badge"
141
+ class={classes.badge({ size: subSize, action: sub.action && !sub.suffix ? 'sub' : 'none' })}
142
+ >
143
+ {sub.badge}
144
+ </span>
145
+ {/snippet}
146
+
117
147
  <li
118
148
  bind:this={itemRef}
119
149
  data-slot="sidebar-menu-sub-item"
@@ -175,7 +205,8 @@
175
205
  class={classes.suffix({
176
206
  size: subSize,
177
207
  density,
178
- action: sub.action ? 'action' : 'none'
208
+ action: sub.action ? 'action' : 'none',
209
+ line: twoLine ? 'two' : 'one'
179
210
  })}
180
211
  {@attach suffixWidth.measure}
181
212
  >
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { SidebarSize } from './sidebar.props.js';
4
+ import { useSidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
5
+
6
+ let {
7
+ label,
8
+ description,
9
+ meta,
10
+ size,
11
+ suffix = false,
12
+ trailing,
13
+ theme
14
+ }: {
15
+ label: string;
16
+ description?: string | Snippet;
17
+ /** Left out while the Sidebar is collapsed to icons. */
18
+ meta?: string | Snippet;
19
+ size: SidebarSize;
20
+ /** The row has a suffix, centred on the description's line. */
21
+ suffix?: boolean;
22
+ /** End of the description's line: the row's badge. */
23
+ trailing?: Snippet;
24
+ theme?: SidebarThemeProps;
25
+ } = $props();
26
+
27
+ const classes = $derived(useSidebarTheme(theme));
28
+ </script>
29
+
30
+ {#snippet text(content: string | Snippet)}
31
+ {#if typeof content === 'string'}{content}{:else}{@render content()}{/if}
32
+ {/snippet}
33
+
34
+ <!-- The label's line (the label, then the meta at its end) over the description's line. -->
35
+ <span data-slot="sidebar-menu-text" class={classes.menuText()}>
36
+ <span class={classes.menuTitleRow()}>
37
+ <span data-slot="sidebar-menu-label" class={classes.menuTitle()}>{label}</span>
38
+ {#if meta != null}
39
+ <span
40
+ data-slot="sidebar-menu-meta"
41
+ data-sidebar="menu-meta"
42
+ class={classes.menuMeta({ size })}
43
+ >
44
+ {@render text(meta)}
45
+ </span>
46
+ {/if}
47
+ </span>
48
+ {#if description != null}
49
+ <span class={classes.menuDescriptionRow({ suffix })}>
50
+ <span
51
+ data-slot="sidebar-menu-description"
52
+ data-sidebar="menu-description"
53
+ class={classes.menuSecondary({ size })}
54
+ >
55
+ {@render text(description)}
56
+ </span>
57
+ {@render trailing?.()}
58
+ </span>
59
+ {/if}
60
+ </span>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { SidebarSize } from './sidebar.props.js';
3
+ import { type SidebarThemeProps } from './sidebar.theme.js';
4
+ type $$ComponentProps = {
5
+ label: string;
6
+ description?: string | Snippet;
7
+ /** Left out while the Sidebar is collapsed to icons. */
8
+ meta?: string | Snippet;
9
+ size: SidebarSize;
10
+ /** The row has a suffix, centred on the description's line. */
11
+ suffix?: boolean;
12
+ /** End of the description's line: the row's badge. */
13
+ trailing?: Snippet;
14
+ theme?: SidebarThemeProps;
15
+ };
16
+ declare const SidebarMenuText: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type SidebarMenuText = ReturnType<typeof SidebarMenuText>;
18
+ export default SidebarMenuText;