@happyvertical/smrt-ui 0.34.5 → 0.34.7

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.
Files changed (49) hide show
  1. package/AGENTS.md +15 -5
  2. package/dist/components/chat/MessageBubble.svelte +123 -33
  3. package/dist/components/chat/MessageBubble.svelte.d.ts +33 -10
  4. package/dist/components/chat/MessageBubble.svelte.d.ts.map +1 -1
  5. package/dist/components/chat/ReactionPicker.svelte +50 -18
  6. package/dist/components/chat/ReactionPicker.svelte.d.ts +8 -8
  7. package/dist/components/chat/ReactionPicker.svelte.d.ts.map +1 -1
  8. package/dist/components/chat/TypingIndicator.svelte +42 -25
  9. package/dist/components/chat/TypingIndicator.svelte.d.ts +12 -5
  10. package/dist/components/chat/TypingIndicator.svelte.d.ts.map +1 -1
  11. package/dist/components/chat/__tests__/chat-primitives.test.js +52 -1
  12. package/dist/components/forms/Form.svelte +53 -0
  13. package/dist/components/forms/Form.svelte.d.ts +29 -0
  14. package/dist/components/forms/Form.svelte.d.ts.map +1 -0
  15. package/dist/components/forms/FormGroup.svelte +86 -0
  16. package/dist/components/forms/FormGroup.svelte.d.ts +13 -0
  17. package/dist/components/forms/FormGroup.svelte.d.ts.map +1 -0
  18. package/dist/components/forms/Input.svelte +89 -0
  19. package/dist/components/forms/Input.svelte.d.ts +9 -0
  20. package/dist/components/forms/Input.svelte.d.ts.map +1 -0
  21. package/dist/components/forms/Select.svelte +89 -0
  22. package/dist/components/forms/Select.svelte.d.ts +11 -0
  23. package/dist/components/forms/Select.svelte.d.ts.map +1 -0
  24. package/dist/components/forms/Textarea.svelte +91 -0
  25. package/dist/components/forms/Textarea.svelte.d.ts +10 -0
  26. package/dist/components/forms/Textarea.svelte.d.ts.map +1 -0
  27. package/dist/components/forms/Toggle.svelte +224 -0
  28. package/dist/components/forms/Toggle.svelte.d.ts +37 -0
  29. package/dist/components/forms/Toggle.svelte.d.ts.map +1 -0
  30. package/dist/components/forms/__tests__/Form.test.js +49 -0
  31. package/dist/components/forms/__tests__/FormGroup.test.js +48 -0
  32. package/dist/components/forms/__tests__/Input.test.js +49 -0
  33. package/dist/components/forms/__tests__/Select.test.js +37 -0
  34. package/dist/components/forms/__tests__/Textarea.test.js +39 -0
  35. package/dist/components/forms/__tests__/Toggle.test.js +87 -0
  36. package/dist/components/forms/__tests__/form-group-input.fixture.svelte +16 -0
  37. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +9 -0
  38. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +1 -0
  39. package/dist/components/forms/form-group-context.d.ts +13 -0
  40. package/dist/components/forms/form-group-context.d.ts.map +1 -0
  41. package/dist/components/forms/form-group-context.js +28 -0
  42. package/dist/components/forms/index.d.ts +21 -0
  43. package/dist/components/forms/index.d.ts.map +1 -0
  44. package/dist/components/forms/index.js +20 -0
  45. package/dist/components/ui/Button.svelte +16 -0
  46. package/dist/i18n/strings.ui.d.ts +2 -0
  47. package/dist/i18n/strings.ui.d.ts.map +1 -1
  48. package/dist/i18n/strings.ui.js +3 -0
  49. package/package.json +8 -2
@@ -29,12 +29,32 @@ describe('MessageBubble', () => {
29
29
  });
30
30
  expect(container.querySelector('time')).toHaveAttribute('datetime', '2026-01-01T10:00:00.000Z');
31
31
  });
32
- it('is axe-clean', async () => {
32
+ it('renders a bare bubble (no header/group) from plain content', () => {
33
+ render(MessageBubble, {
34
+ props: { content: 'Hello there', variant: 'default', own: false },
35
+ });
36
+ expect(screen.getByText('Hello there')).toBeInTheDocument();
37
+ // A bare bubble adds no labelled landmark — its host row owns the label.
38
+ expect(screen.queryByRole('group')).toBeNull();
39
+ });
40
+ it('renders a legacy card header even without an author', () => {
41
+ // The legacy card form (role, no styling axes) keeps its header + labelled
42
+ // group, falling back to the role label.
43
+ render(MessageBubble, { props: { role: 'agent', children: body('hi') } });
44
+ expect(screen.getByRole('group', { name: 'Assistant (agent)' })).toBeInTheDocument();
45
+ });
46
+ it('is axe-clean as a labelled card', async () => {
33
47
  const { container } = render(MessageBubble, {
34
48
  props: { role: 'user', author: 'You', children: body('hi') },
35
49
  });
36
50
  await expectNoA11yViolations(container);
37
51
  });
52
+ it('is axe-clean as an own bare bubble', async () => {
53
+ const { container } = render(MessageBubble, {
54
+ props: { content: 'Hi', variant: 'default', own: true },
55
+ });
56
+ await expectNoA11yViolations(container);
57
+ });
38
58
  });
39
59
  describe('ReactionPicker', () => {
40
60
  it('is a labelled group of named emoji buttons', () => {
@@ -48,6 +68,21 @@ describe('ReactionPicker', () => {
48
68
  await userEvent.click(screen.getByRole('button', { name: 'Heart' }));
49
69
  expect(onpick).toHaveBeenCalledWith('❤️');
50
70
  });
71
+ it('renders nothing when closed', () => {
72
+ render(ReactionPicker, { props: { isOpen: false } });
73
+ expect(screen.queryByRole('group')).toBeNull();
74
+ });
75
+ it('honors caller-supplied group + per-emoji labels', () => {
76
+ render(ReactionPicker, {
77
+ props: {
78
+ emojis: ['🚀'],
79
+ label: 'Emoji reactions',
80
+ emojiLabel: (emoji) => `React with ${emoji}`,
81
+ },
82
+ });
83
+ expect(screen.getByRole('group', { name: 'Emoji reactions' })).toBeInTheDocument();
84
+ expect(screen.getByRole('button', { name: 'React with 🚀' })).toBeInTheDocument();
85
+ });
51
86
  it('is axe-clean', async () => {
52
87
  const { container } = render(ReactionPicker);
53
88
  await expectNoA11yViolations(container);
@@ -58,6 +93,22 @@ describe('TypingIndicator', () => {
58
93
  render(TypingIndicator, { props: { name: 'Assistant' } });
59
94
  expect(screen.getByRole('status')).toHaveTextContent('Assistant is typing');
60
95
  });
96
+ it('names a single typist from a list', () => {
97
+ render(TypingIndicator, { props: { names: ['Ada'] } });
98
+ expect(screen.getByText('Ada is typing')).toBeInTheDocument();
99
+ });
100
+ it('names two typists from a list', () => {
101
+ render(TypingIndicator, { props: { names: ['Ada', 'Bob'] } });
102
+ expect(screen.getByText('Ada and Bob are typing')).toBeInTheDocument();
103
+ });
104
+ it('aggregates three or more typists', () => {
105
+ render(TypingIndicator, { props: { names: ['Ada', 'Bob', 'Cy'] } });
106
+ expect(screen.getByText('Ada and 2 others are typing')).toBeInTheDocument();
107
+ });
108
+ it('renders nothing when nobody is typing', () => {
109
+ const { container } = render(TypingIndicator, { props: { names: [] } });
110
+ expect(container.querySelector('.typing')).toBeNull();
111
+ });
61
112
  it('is axe-clean', async () => {
62
113
  const { container } = render(TypingIndicator, {
63
114
  props: { name: 'Assistant' },
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Form — the Provider-free base `<form>` primitive.
4
+ *
5
+ * A thin, dependency-free wrapper around the native `<form>` element so domain
6
+ * components have a primitive to adopt instead of hand-rolling raw `<form>`
7
+ * markup (issue #1589). It forwards every native form attribute (including
8
+ * `onsubmit`) and renders its children — no Provider, no i18n, no spoken-input
9
+ * logic.
10
+ *
11
+ * For the rich, Provider-backed form with field registration and voice input,
12
+ * use `Form` from `@happyvertical/smrt-svelte/forms` instead. This one is the
13
+ * leaf-level building block; that one composes app state on top.
14
+ *
15
+ * `preventDefault` (default `true`) calls `event.preventDefault()` before
16
+ * invoking the consumer's `onsubmit`, so a click/Enter submit runs the handler
17
+ * without a full-page navigation — the near-universal SPA pattern. Pass
18
+ * `preventDefault={false}` to keep native submission (e.g. a GET/POST `action`).
19
+ */
20
+ import type { Snippet } from 'svelte';
21
+ import type { HTMLFormAttributes } from 'svelte/elements';
22
+
23
+ export interface Props extends Omit<HTMLFormAttributes, 'class'> {
24
+ class?: string;
25
+ preventDefault?: boolean;
26
+ children: Snippet;
27
+ }
28
+
29
+ let {
30
+ class: className = '',
31
+ preventDefault = true,
32
+ onsubmit,
33
+ children,
34
+ ...rest
35
+ }: Props = $props();
36
+
37
+ function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
38
+ if (preventDefault) event.preventDefault();
39
+ onsubmit?.(event);
40
+ }
41
+ </script>
42
+
43
+ <form class="form {className}" onsubmit={handleSubmit} {...rest}>
44
+ {@render children()}
45
+ </form>
46
+
47
+ <!--
48
+ No base styles: a <form> is `display: block` by default, so an explicit
49
+ `.form { display: block }` rule would only add a specificity floor that ties
50
+ with a consumer's single-class layout override (e.g. `:global(.x){display:flex}`)
51
+ and can win by stylesheet order. The `form` class stays as a stable hook.
52
+ -->
53
+
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Form — the Provider-free base `<form>` primitive.
3
+ *
4
+ * A thin, dependency-free wrapper around the native `<form>` element so domain
5
+ * components have a primitive to adopt instead of hand-rolling raw `<form>`
6
+ * markup (issue #1589). It forwards every native form attribute (including
7
+ * `onsubmit`) and renders its children — no Provider, no i18n, no spoken-input
8
+ * logic.
9
+ *
10
+ * For the rich, Provider-backed form with field registration and voice input,
11
+ * use `Form` from `@happyvertical/smrt-svelte/forms` instead. This one is the
12
+ * leaf-level building block; that one composes app state on top.
13
+ *
14
+ * `preventDefault` (default `true`) calls `event.preventDefault()` before
15
+ * invoking the consumer's `onsubmit`, so a click/Enter submit runs the handler
16
+ * without a full-page navigation — the near-universal SPA pattern. Pass
17
+ * `preventDefault={false}` to keep native submission (e.g. a GET/POST `action`).
18
+ */
19
+ import type { Snippet } from 'svelte';
20
+ import type { HTMLFormAttributes } from 'svelte/elements';
21
+ export interface Props extends Omit<HTMLFormAttributes, 'class'> {
22
+ class?: string;
23
+ preventDefault?: boolean;
24
+ children: Snippet;
25
+ }
26
+ declare const Form: import("svelte").Component<Props, {}, "">;
27
+ type Form = ReturnType<typeof Form>;
28
+ export default Form;
29
+ //# sourceMappingURL=Form.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CACnB;AA8BD,QAAA,MAAM,IAAI,2CAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import {
4
+ type FormGroupContextValue,
5
+ nextFieldId,
6
+ setFormGroupContext,
7
+ } from './form-group-context.js';
8
+
9
+ export interface Props {
10
+ label: string;
11
+ id?: string;
12
+ error?: string;
13
+ hint?: string;
14
+ required?: boolean;
15
+ children: Snippet;
16
+ }
17
+
18
+ const { label, id, error, hint, required = false, children }: Props = $props();
19
+
20
+ // Stable id so the label's `for` and the wrapped input's `id` agree even when
21
+ // the consumer doesn't pass one.
22
+ const fallbackId = nextFieldId();
23
+ const fieldId = $derived(id ?? fallbackId);
24
+ const hintId = $derived(hint && !error ? `${fieldId}-hint` : undefined);
25
+ const errorId = $derived(error ? `${fieldId}-error` : undefined);
26
+ const describedBy = $derived(
27
+ [hintId, errorId].filter(Boolean).join(' ') || undefined,
28
+ );
29
+
30
+ // Publish the wiring a base input auto-applies (getter stays reactive as
31
+ // hint/error change).
32
+ setFormGroupContext(
33
+ (): FormGroupContextValue => ({
34
+ inputId: fieldId,
35
+ describedBy,
36
+ invalid: !!error,
37
+ }),
38
+ );
39
+ </script>
40
+
41
+ <div class="form-group">
42
+ <label for={fieldId} class="form-label">
43
+ {label}
44
+ {#if required}
45
+ <span class="required" aria-hidden="true">*</span>
46
+ {/if}
47
+ </label>
48
+ {@render children()}
49
+ {#if hintId}
50
+ <p id={hintId} class="form-hint">{hint}</p>
51
+ {/if}
52
+ {#if errorId}
53
+ <p id={errorId} class="form-error" role="alert">{error}</p>
54
+ {/if}
55
+ </div>
56
+
57
+ <style>
58
+ .form-group {
59
+ margin-bottom: 1rem;
60
+ }
61
+
62
+ .form-label {
63
+ display: block;
64
+ font-size: var(--smrt-typography-label-large-size, 0.875rem);
65
+ font-weight: var(--smrt-typography-weight-medium, 500);
66
+ color: var(--smrt-color-on-surface, #374151);
67
+ margin-bottom: 0.375rem;
68
+ }
69
+
70
+ .required {
71
+ color: var(--smrt-color-error, #ba1a1a);
72
+ margin-left: 0.125rem;
73
+ }
74
+
75
+ .form-hint {
76
+ margin: 0.25rem 0 0;
77
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
78
+ color: var(--smrt-color-on-surface-variant, #6b7280);
79
+ }
80
+
81
+ .form-error {
82
+ margin: 0.25rem 0 0;
83
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
84
+ color: var(--smrt-color-error, #ba1a1a);
85
+ }
86
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ export interface Props {
3
+ label: string;
4
+ id?: string;
5
+ error?: string;
6
+ hint?: string;
7
+ required?: boolean;
8
+ children: Snippet;
9
+ }
10
+ declare const FormGroup: import("svelte").Component<Props, {}, "">;
11
+ type FormGroup = ReturnType<typeof FormGroup>;
12
+ export default FormGroup;
13
+ //# sourceMappingURL=FormGroup.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormGroup.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FormGroup.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAQtC,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAiDD,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { tryGetFormGroupContext } from './form-group-context.js';
4
+
5
+ export interface Props extends Omit<HTMLInputAttributes, 'class' | 'value'> {
6
+ value?: string | number;
7
+ class?: string;
8
+ }
9
+
10
+ let {
11
+ id,
12
+ type = 'text',
13
+ value = $bindable(''),
14
+ placeholder,
15
+ disabled = false,
16
+ readonly = false,
17
+ required = false,
18
+ name,
19
+ class: className = '',
20
+ 'aria-describedby': ariaDescribedby,
21
+ 'aria-invalid': ariaInvalid,
22
+ ...rest
23
+ }: Props = $props();
24
+
25
+ // When wrapped in a <FormGroup>, inherit the id (so its <label for> resolves),
26
+ // the hint/error association, and the error state — unless set explicitly.
27
+ const formGroup = tryGetFormGroupContext();
28
+ const resolvedId = $derived(id ?? formGroup?.().inputId);
29
+ const resolvedDescribedBy = $derived(
30
+ ariaDescribedby ?? formGroup?.().describedBy,
31
+ );
32
+ const resolvedInvalid = $derived(
33
+ ariaInvalid ?? (formGroup?.().invalid ? 'true' : undefined),
34
+ );
35
+ </script>
36
+
37
+ <input
38
+ id={resolvedId}
39
+ {type}
40
+ bind:value
41
+ {placeholder}
42
+ {disabled}
43
+ {readonly}
44
+ {required}
45
+ {name}
46
+ aria-describedby={resolvedDescribedBy}
47
+ aria-invalid={resolvedInvalid}
48
+ class="input {className}"
49
+ {...rest}
50
+ />
51
+
52
+ <style>
53
+ .input {
54
+ display: block;
55
+ width: 100%;
56
+ padding: 0.5rem 0.75rem;
57
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
58
+ line-height: var(--smrt-typography-body-medium-line-height, 1.5);
59
+ color: var(--smrt-color-on-surface, #1f2937);
60
+ background-color: var(--smrt-color-surface, #fff);
61
+ border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
62
+ border-radius: var(--smrt-radius-small, 0.375rem);
63
+ transition:
64
+ border-color var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease-in-out),
65
+ box-shadow var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease-in-out);
66
+ }
67
+
68
+ .input:focus {
69
+ outline: none;
70
+ border-color: var(--smrt-color-primary, #005ac1);
71
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary, #005ac1) 10%, transparent);
72
+ }
73
+
74
+ .input:disabled {
75
+ background-color: var(--smrt-color-surface-container-high, #f3f4f6);
76
+ cursor: not-allowed;
77
+ opacity: 0.7;
78
+ }
79
+
80
+ .input::placeholder {
81
+ color: var(--smrt-color-on-surface-variant, #9ca3af);
82
+ }
83
+
84
+ @media (prefers-reduced-motion: reduce) {
85
+ .input {
86
+ transition: none;
87
+ }
88
+ }
89
+ </style>
@@ -0,0 +1,9 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ export interface Props extends Omit<HTMLInputAttributes, 'class' | 'value'> {
3
+ value?: string | number;
4
+ class?: string;
5
+ }
6
+ declare const Input: import("svelte").Component<Props, {}, "value">;
7
+ type Input = ReturnType<typeof Input>;
8
+ export default Input;
9
+ //# sourceMappingURL=Input.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Input.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Input.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAI3D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,OAAO,CAAC;IACzE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuCD,QAAA,MAAM,KAAK,gDAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLSelectAttributes } from 'svelte/elements';
4
+ import { tryGetFormGroupContext } from './form-group-context.js';
5
+
6
+ export interface Props extends Omit<HTMLSelectAttributes, 'class' | 'value'> {
7
+ value?: string;
8
+ class?: string;
9
+ children: Snippet;
10
+ }
11
+
12
+ let {
13
+ id,
14
+ value = $bindable(''),
15
+ disabled = false,
16
+ required = false,
17
+ name,
18
+ class: className = '',
19
+ children,
20
+ 'aria-describedby': ariaDescribedby,
21
+ 'aria-invalid': ariaInvalid,
22
+ ...rest
23
+ }: Props = $props();
24
+
25
+ // Inherit id / hint+error association / error state from a wrapping FormGroup.
26
+ const formGroup = tryGetFormGroupContext();
27
+ const resolvedId = $derived(id ?? formGroup?.().inputId);
28
+ const resolvedDescribedBy = $derived(
29
+ ariaDescribedby ?? formGroup?.().describedBy,
30
+ );
31
+ const resolvedInvalid = $derived(
32
+ ariaInvalid ?? (formGroup?.().invalid ? 'true' : undefined),
33
+ );
34
+ </script>
35
+
36
+ <select
37
+ id={resolvedId}
38
+ bind:value
39
+ {disabled}
40
+ {required}
41
+ {name}
42
+ aria-describedby={resolvedDescribedBy}
43
+ aria-invalid={resolvedInvalid}
44
+ class="select {className}"
45
+ {...rest}
46
+ >
47
+ {@render children()}
48
+ </select>
49
+
50
+ <style>
51
+ .select {
52
+ display: block;
53
+ width: 100%;
54
+ padding: 0.5rem 2rem 0.5rem 0.75rem;
55
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
56
+ line-height: var(--smrt-typography-body-medium-line-height, 1.5);
57
+ color: var(--smrt-color-on-surface, #1f2937);
58
+ background-color: var(--smrt-color-surface, #fff);
59
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2379747e' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
60
+ background-position: right 0.5rem center;
61
+ background-repeat: no-repeat;
62
+ background-size: 1.5em 1.5em;
63
+ border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
64
+ border-radius: 0.375rem;
65
+ appearance: none;
66
+ cursor: pointer;
67
+ transition:
68
+ border-color 0.15s ease-in-out,
69
+ box-shadow 0.15s ease-in-out;
70
+ }
71
+
72
+ .select:focus {
73
+ outline: none;
74
+ border-color: var(--smrt-color-primary, #005ac1);
75
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary, #005ac1) 10%, transparent);
76
+ }
77
+
78
+ .select:disabled {
79
+ background-color: var(--smrt-color-surface-container-high, #f3f4f6);
80
+ cursor: not-allowed;
81
+ opacity: 0.7;
82
+ }
83
+
84
+ @media (prefers-reduced-motion: reduce) {
85
+ .select {
86
+ transition: none;
87
+ }
88
+ }
89
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLSelectAttributes } from 'svelte/elements';
3
+ export interface Props extends Omit<HTMLSelectAttributes, 'class' | 'value'> {
4
+ value?: string;
5
+ class?: string;
6
+ children: Snippet;
7
+ }
8
+ declare const Select: import("svelte").Component<Props, {}, "value">;
9
+ type Select = ReturnType<typeof Select>;
10
+ export default Select;
11
+ //# sourceMappingURL=Select.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Select.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Select.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAI5D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,oBAAoB,EAAE,OAAO,GAAG,OAAO,CAAC;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAuCD,QAAA,MAAM,MAAM,gDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,91 @@
1
+ <script lang="ts">
2
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
3
+ import { tryGetFormGroupContext } from './form-group-context.js';
4
+
5
+ export interface Props extends Omit<HTMLTextareaAttributes, 'class' | 'value'> {
6
+ value?: string;
7
+ rows?: number;
8
+ class?: string;
9
+ }
10
+
11
+ let {
12
+ id,
13
+ value = $bindable(''),
14
+ placeholder,
15
+ disabled = false,
16
+ readonly = false,
17
+ required = false,
18
+ name,
19
+ rows = 4,
20
+ class: className = '',
21
+ 'aria-describedby': ariaDescribedby,
22
+ 'aria-invalid': ariaInvalid,
23
+ ...rest
24
+ }: Props = $props();
25
+
26
+ // Inherit id / hint+error association / error state from a wrapping FormGroup.
27
+ const formGroup = tryGetFormGroupContext();
28
+ const resolvedId = $derived(id ?? formGroup?.().inputId);
29
+ const resolvedDescribedBy = $derived(
30
+ ariaDescribedby ?? formGroup?.().describedBy,
31
+ );
32
+ const resolvedInvalid = $derived(
33
+ ariaInvalid ?? (formGroup?.().invalid ? 'true' : undefined),
34
+ );
35
+ </script>
36
+
37
+ <textarea
38
+ id={resolvedId}
39
+ bind:value
40
+ {placeholder}
41
+ {disabled}
42
+ {readonly}
43
+ {required}
44
+ {name}
45
+ {rows}
46
+ aria-describedby={resolvedDescribedBy}
47
+ aria-invalid={resolvedInvalid}
48
+ class="textarea {className}"
49
+ {...rest}
50
+ ></textarea>
51
+
52
+ <style>
53
+ .textarea {
54
+ display: block;
55
+ width: 100%;
56
+ padding: 0.5rem 0.75rem;
57
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
58
+ line-height: var(--smrt-typography-body-medium-line-height, 1.5);
59
+ color: var(--smrt-color-on-surface, #1f2937);
60
+ background-color: var(--smrt-color-surface, #fff);
61
+ border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
62
+ border-radius: 0.375rem;
63
+ resize: vertical;
64
+ min-height: 80px;
65
+ transition:
66
+ border-color 0.15s ease-in-out,
67
+ box-shadow 0.15s ease-in-out;
68
+ }
69
+
70
+ .textarea:focus {
71
+ outline: none;
72
+ border-color: var(--smrt-color-primary, #005ac1);
73
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary, #005ac1) 10%, transparent);
74
+ }
75
+
76
+ .textarea:disabled {
77
+ background-color: var(--smrt-color-surface-container-high, #f3f4f6);
78
+ cursor: not-allowed;
79
+ opacity: 0.7;
80
+ }
81
+
82
+ .textarea::placeholder {
83
+ color: var(--smrt-color-on-surface-variant, #9ca3af);
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .textarea {
88
+ transition: none;
89
+ }
90
+ }
91
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
2
+ export interface Props extends Omit<HTMLTextareaAttributes, 'class' | 'value'> {
3
+ value?: string;
4
+ rows?: number;
5
+ class?: string;
6
+ }
7
+ declare const Textarea: import("svelte").Component<Props, {}, "value">;
8
+ type Textarea = ReturnType<typeof Textarea>;
9
+ export default Textarea;
10
+ //# sourceMappingURL=Textarea.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Textarea.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Textarea.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAI9D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,OAAO,CAAC;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsCD,QAAA,MAAM,QAAQ,gDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}