@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.
- package/AGENTS.md +15 -5
- package/dist/components/chat/MessageBubble.svelte +123 -33
- package/dist/components/chat/MessageBubble.svelte.d.ts +33 -10
- package/dist/components/chat/MessageBubble.svelte.d.ts.map +1 -1
- package/dist/components/chat/ReactionPicker.svelte +50 -18
- package/dist/components/chat/ReactionPicker.svelte.d.ts +8 -8
- package/dist/components/chat/ReactionPicker.svelte.d.ts.map +1 -1
- package/dist/components/chat/TypingIndicator.svelte +42 -25
- package/dist/components/chat/TypingIndicator.svelte.d.ts +12 -5
- package/dist/components/chat/TypingIndicator.svelte.d.ts.map +1 -1
- package/dist/components/chat/__tests__/chat-primitives.test.js +52 -1
- package/dist/components/forms/Form.svelte +53 -0
- package/dist/components/forms/Form.svelte.d.ts +29 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -0
- package/dist/components/forms/FormGroup.svelte +86 -0
- package/dist/components/forms/FormGroup.svelte.d.ts +13 -0
- package/dist/components/forms/FormGroup.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +89 -0
- package/dist/components/forms/Input.svelte.d.ts +9 -0
- package/dist/components/forms/Input.svelte.d.ts.map +1 -0
- package/dist/components/forms/Select.svelte +89 -0
- package/dist/components/forms/Select.svelte.d.ts +11 -0
- package/dist/components/forms/Select.svelte.d.ts.map +1 -0
- package/dist/components/forms/Textarea.svelte +91 -0
- package/dist/components/forms/Textarea.svelte.d.ts +10 -0
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -0
- package/dist/components/forms/Toggle.svelte +224 -0
- package/dist/components/forms/Toggle.svelte.d.ts +37 -0
- package/dist/components/forms/Toggle.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/Form.test.js +49 -0
- package/dist/components/forms/__tests__/FormGroup.test.js +48 -0
- package/dist/components/forms/__tests__/Input.test.js +49 -0
- package/dist/components/forms/__tests__/Select.test.js +37 -0
- package/dist/components/forms/__tests__/Textarea.test.js +39 -0
- package/dist/components/forms/__tests__/Toggle.test.js +87 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte +16 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/form-group-context.d.ts +13 -0
- package/dist/components/forms/form-group-context.d.ts.map +1 -0
- package/dist/components/forms/form-group-context.js +28 -0
- package/dist/components/forms/index.d.ts +21 -0
- package/dist/components/forms/index.d.ts.map +1 -0
- package/dist/components/forms/index.js +20 -0
- package/dist/components/ui/Button.svelte +16 -0
- package/dist/i18n/strings.ui.d.ts +2 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +3 -0
- 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('
|
|
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"}
|