@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
package/AGENTS.md
CHANGED
|
@@ -14,11 +14,20 @@ SMRT has one shared set of UI primitives, split across two packages by concern:
|
|
|
14
14
|
|
|
15
15
|
- **`smrt-ui` (here) owns the domain-agnostic VISUAL primitives** — `Button`,
|
|
16
16
|
`Card`, `Modal`/`ConfirmDialog`, `Badge`, `Avatar`, `Chip`, `Dropdown`,
|
|
17
|
-
`Tooltip`, `Skeleton`, `Tree`, `Pagination`, `DataTable`, …
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
`Tooltip`, `Skeleton`, `Tree`, `Pagination`, `DataTable`, … — **plus the
|
|
18
|
+
Provider-free base FORM primitives** under `./forms` (`Form`, `Input`,
|
|
19
|
+
`Select`, `Textarea`, `Toggle`, `FormGroup`), relocated here in #1589's
|
|
20
|
+
deferred-forms phase so domain packages can adopt them without pulling in the
|
|
21
|
+
smrt-svelte Provider or closing a build-graph cycle. These are dependency-free:
|
|
22
|
+
no Provider, no i18n, no spoken-input logic.
|
|
23
|
+
- **`smrt-svelte` owns the Provider-REQUIRED form primitives** — `CheckboxInput`,
|
|
24
|
+
the rich `Form` (field registration + voice), `TextInput`, `MoneyInput`, and
|
|
25
|
+
the specialized date/measurement/address/file inputs (they call `useAppState`
|
|
26
|
+
/ the AI hooks and carry i18n + spoken-input logic). It re-exports the base
|
|
27
|
+
**input** primitives (`Input`, `Select`, `Textarea`, `Toggle`, `FormGroup`)
|
|
28
|
+
from here so `@happyvertical/smrt-svelte/forms` stays the full barrel — but
|
|
29
|
+
**not** `Form`: that barrel's `Form` is the rich Provider-backed one, so the
|
|
30
|
+
Provider-free `Form` is only importable from `@happyvertical/smrt-ui/forms`.
|
|
22
31
|
|
|
23
32
|
**Domain packages import visual primitives from `smrt-ui` and form primitives
|
|
24
33
|
from `smrt-svelte`, and must not hand-roll raw `<button>` / `<input>` /
|
|
@@ -38,6 +47,7 @@ components are exempt — they *are* the primitives.
|
|
|
38
47
|
| `./layout` | `Container`, `Grid`, `Header`, `Footer`, `PageHeader`, `EmptyState`, … |
|
|
39
48
|
| `./calendar` | `Calendar`, `DayView` |
|
|
40
49
|
| `./chat` | `MessageBubble`, `ReactionPicker`, `TypingIndicator` |
|
|
50
|
+
| `./forms` | Provider-free base form primitives: `Form`, `Input`, `Select`, `Textarea`, `Toggle`, `FormGroup` (+ the FormGroup a11y context helpers) |
|
|
41
51
|
| `./i18n` | i18n **client**: `useI18n`, `<Trans>`, `defineMessages`, `renderTemplate` (no `smrt-languages` import — the server resolver stays in `smrt-svelte/i18n/server`) |
|
|
42
52
|
| `./registry` | `ModuleUIRegistry` for cross-package component discovery |
|
|
43
53
|
| `./theme` | simple `ThemeProvider` + context (`useTheme` consumes this from `smrt-svelte`) |
|
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* MessageBubble — a single chat message
|
|
3
|
+
* MessageBubble — a single chat message, tokenised and a11y-clean.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Two complementary forms share one styled container:
|
|
6
|
+
*
|
|
7
|
+
* - **Bare bubble** (the common case for a message list that renders its own
|
|
8
|
+
* author/time/reactions around each row): opt into the styling axes by passing
|
|
9
|
+
* `variant` and/or `own` (plus `content` or a `children` snippet). No header or
|
|
10
|
+
* labelled group is rendered — unless you also pass an `author` — so it nests
|
|
11
|
+
* cleanly inside an already-labelled message row without a redundant landmark.
|
|
12
|
+
* - **Self-contained card** (legacy form): pass `role` / `author` / `timestamp`
|
|
13
|
+
* (and optionally the `reactions` / `actions` snippets) without the styling
|
|
14
|
+
* axes. A header and a labelled `role="group"` (`<author|role> (<role>)`) are
|
|
15
|
+
* rendered so assistive tech can announce who sent each message; `author`
|
|
16
|
+
* falls back to a role label ("You" / "Assistant" / "System").
|
|
17
|
+
*
|
|
18
|
+
* `variant` + `own` are the canonical styling axes. The legacy `role` prop sets
|
|
19
|
+
* the header's role label and derives `variant`/`own` when those are not given.
|
|
9
20
|
*/
|
|
10
21
|
import type { Snippet } from 'svelte';
|
|
11
22
|
import type { ChatMessageRole } from '../../types-generic';
|
|
12
23
|
|
|
24
|
+
/** Visual tone: a peer message, an assistant message, or a centered notice. */
|
|
25
|
+
export type MessageBubbleVariant = 'default' | 'agent' | 'system';
|
|
26
|
+
|
|
13
27
|
export interface Props {
|
|
14
|
-
/**
|
|
28
|
+
/** Visual tone. Canonical styling axis. */
|
|
29
|
+
variant?: MessageBubbleVariant;
|
|
30
|
+
/** Whether the current viewer authored this message — drives alignment + own-color. */
|
|
31
|
+
own?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Legacy role. Prefer `variant` + `own`. Sets the header's role label and,
|
|
34
|
+
* when `variant`/`own` are unset, derives them (user → own default, agent →
|
|
35
|
+
* agent, system → system).
|
|
36
|
+
*/
|
|
15
37
|
role?: ChatMessageRole;
|
|
16
|
-
/**
|
|
38
|
+
/** Plain-text body. Ignored when a `children` snippet is provided. */
|
|
39
|
+
content?: string;
|
|
40
|
+
/** Body snippet (takes precedence over `content`). */
|
|
41
|
+
children?: Snippet;
|
|
42
|
+
/** Display name of the sender. When set, a header + labelled group render. */
|
|
17
43
|
author?: string;
|
|
18
|
-
/** Message time; rendered in a `<time datetime>` element. */
|
|
44
|
+
/** Message time; rendered in a `<time datetime>` element in the header. */
|
|
19
45
|
timestamp?: Date;
|
|
20
46
|
/** Override the visible timestamp text (defaults to a locale time). */
|
|
21
47
|
timestampLabel?: string;
|
|
22
|
-
/** Message body. */
|
|
23
|
-
children: Snippet;
|
|
24
48
|
/** Optional reactions row (e.g. a `ReactionPicker` or reaction chips). */
|
|
25
49
|
reactions?: Snippet;
|
|
26
50
|
/** Optional per-message actions (reply, copy, …). */
|
|
@@ -28,11 +52,14 @@ export interface Props {
|
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
const {
|
|
55
|
+
variant,
|
|
56
|
+
own,
|
|
31
57
|
role = 'user',
|
|
58
|
+
content,
|
|
59
|
+
children,
|
|
32
60
|
author,
|
|
33
61
|
timestamp,
|
|
34
62
|
timestampLabel,
|
|
35
|
-
children,
|
|
36
63
|
reactions,
|
|
37
64
|
actions,
|
|
38
65
|
}: Props = $props();
|
|
@@ -43,23 +70,50 @@ const roleNoun: Record<ChatMessageRole, string> = {
|
|
|
43
70
|
system: 'System',
|
|
44
71
|
};
|
|
45
72
|
|
|
46
|
-
|
|
73
|
+
// `variant`/`own` are canonical; derive them from the legacy `role` only when
|
|
74
|
+
// they are not explicitly supplied.
|
|
75
|
+
const effectiveVariant = $derived<MessageBubbleVariant>(
|
|
76
|
+
variant ??
|
|
77
|
+
(role === 'agent' ? 'agent' : role === 'system' ? 'system' : 'default'),
|
|
78
|
+
);
|
|
79
|
+
const effectiveOwn = $derived(own ?? role === 'user');
|
|
80
|
+
|
|
81
|
+
// The bare-bubble form is signalled by opting into the explicit styling axes
|
|
82
|
+
// (`variant`/`own`); the legacy card form (`role`/`author`/`timestamp`) is not.
|
|
83
|
+
const isBareForm = $derived(variant !== undefined || own !== undefined);
|
|
84
|
+
const headerName = $derived(author ?? roleNoun[role]);
|
|
85
|
+
// Render a header + labelled group for the legacy card form, or whenever an
|
|
86
|
+
// author is supplied. A bare bubble with no author stays a plain styled
|
|
87
|
+
// container (its host row owns the label).
|
|
88
|
+
const hasHeader = $derived(author !== undefined || !isBareForm);
|
|
89
|
+
const groupLabel = $derived(`${headerName} (${role})`);
|
|
47
90
|
const isoTime = $derived(timestamp ? timestamp.toISOString() : undefined);
|
|
48
91
|
const timeText = $derived(
|
|
49
92
|
timestampLabel ?? timestamp?.toLocaleTimeString() ?? '',
|
|
50
93
|
);
|
|
51
94
|
</script>
|
|
52
95
|
|
|
53
|
-
<div
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
96
|
+
<div
|
|
97
|
+
class="bubble bubble--{effectiveVariant}"
|
|
98
|
+
class:bubble--own={effectiveOwn}
|
|
99
|
+
role={hasHeader ? 'group' : undefined}
|
|
100
|
+
aria-label={hasHeader ? groupLabel : undefined}
|
|
101
|
+
>
|
|
102
|
+
{#if hasHeader}
|
|
103
|
+
<div class="bubble__header">
|
|
104
|
+
<span class="bubble__author">{headerName}</span>
|
|
105
|
+
{#if timeText}
|
|
106
|
+
<time class="bubble__time" datetime={isoTime}>{timeText}</time>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
109
|
+
{/if}
|
|
60
110
|
|
|
61
111
|
<div class="bubble__body">
|
|
62
|
-
{
|
|
112
|
+
{#if children}
|
|
113
|
+
{@render children()}
|
|
114
|
+
{:else if content !== undefined}
|
|
115
|
+
<p class="bubble__content">{content}</p>
|
|
116
|
+
{/if}
|
|
63
117
|
</div>
|
|
64
118
|
|
|
65
119
|
{#if reactions}
|
|
@@ -76,23 +130,57 @@ const timeText = $derived(
|
|
|
76
130
|
display: flex;
|
|
77
131
|
flex-direction: column;
|
|
78
132
|
gap: var(--smrt-spacing-1, 4px);
|
|
79
|
-
max-width:
|
|
80
|
-
padding: var(--smrt-spacing-
|
|
133
|
+
max-width: 75%;
|
|
134
|
+
padding: var(--smrt-spacing-3, 12px) var(--smrt-spacing-4, 16px);
|
|
81
135
|
border-radius: var(--smrt-radius-large, 12px);
|
|
82
|
-
|
|
83
|
-
|
|
136
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
137
|
+
line-height: 1.4;
|
|
138
|
+
word-break: break-word;
|
|
84
139
|
}
|
|
85
140
|
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
141
|
+
/* default — a peer message; `own` is the current viewer's message. */
|
|
142
|
+
.bubble--default {
|
|
143
|
+
background: var(--smrt-color-surface-container, #f3f4f6);
|
|
144
|
+
color: var(--smrt-color-on-surface, #1b1b1f);
|
|
145
|
+
border-bottom-left-radius: var(--smrt-radius-small, 4px);
|
|
146
|
+
}
|
|
147
|
+
.bubble--default.bubble--own {
|
|
148
|
+
background: var(--smrt-color-primary, #005ac1);
|
|
149
|
+
color: var(--smrt-color-on-primary, #ffffff);
|
|
150
|
+
border-bottom-right-radius: var(--smrt-radius-small, 4px);
|
|
151
|
+
border-bottom-left-radius: var(--smrt-radius-large, 12px);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* agent — assistant message, accented on the leading edge. */
|
|
155
|
+
.bubble--agent {
|
|
156
|
+
background: var(--smrt-color-tertiary-container, #ffd8e4);
|
|
157
|
+
color: var(--smrt-color-on-tertiary-container, #31111d);
|
|
158
|
+
border-bottom-left-radius: var(--smrt-radius-small, 4px);
|
|
159
|
+
border-left: 3px solid var(--smrt-color-tertiary, #7d5260);
|
|
160
|
+
}
|
|
161
|
+
.bubble--agent.bubble--own {
|
|
162
|
+
border-left: none;
|
|
163
|
+
border-right: 3px solid var(--smrt-color-tertiary, #7d5260);
|
|
164
|
+
border-bottom-right-radius: var(--smrt-radius-small, 4px);
|
|
165
|
+
border-bottom-left-radius: var(--smrt-radius-large, 12px);
|
|
90
166
|
}
|
|
167
|
+
|
|
168
|
+
/* system — centered notice. */
|
|
91
169
|
.bubble--system {
|
|
92
|
-
|
|
93
|
-
|
|
170
|
+
max-width: 100%;
|
|
171
|
+
background: transparent;
|
|
172
|
+
color: var(--smrt-color-on-surface-variant, #43474e);
|
|
173
|
+
text-align: center;
|
|
174
|
+
font-size: var(--smrt-typography-body-small-size, 0.75rem);
|
|
175
|
+
padding: var(--smrt-spacing-2, 6px) var(--smrt-spacing-4, 16px);
|
|
176
|
+
border-radius: 0;
|
|
94
177
|
align-self: center;
|
|
95
|
-
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Self-align own messages when laid out in a column (card form). In a host
|
|
181
|
+
row that already aligns own messages this is a harmless no-op. */
|
|
182
|
+
.bubble--own {
|
|
183
|
+
align-self: flex-end;
|
|
96
184
|
}
|
|
97
185
|
|
|
98
186
|
.bubble__header {
|
|
@@ -111,11 +199,13 @@ const timeText = $derived(
|
|
|
111
199
|
}
|
|
112
200
|
|
|
113
201
|
.bubble__body {
|
|
114
|
-
font-size: var(--smrt-typography-body-medium-size, 1rem);
|
|
115
|
-
line-height: var(--smrt-typography-body-medium-line-height, 1.5);
|
|
116
202
|
white-space: pre-wrap;
|
|
117
203
|
word-break: break-word;
|
|
118
204
|
}
|
|
205
|
+
.bubble__content {
|
|
206
|
+
margin: 0;
|
|
207
|
+
white-space: pre-wrap;
|
|
208
|
+
}
|
|
119
209
|
|
|
120
210
|
.bubble__reactions,
|
|
121
211
|
.bubble__actions {
|
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MessageBubble — a single chat message
|
|
2
|
+
* MessageBubble — a single chat message, tokenised and a11y-clean.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Two complementary forms share one styled container:
|
|
5
|
+
*
|
|
6
|
+
* - **Bare bubble** (the common case for a message list that renders its own
|
|
7
|
+
* author/time/reactions around each row): opt into the styling axes by passing
|
|
8
|
+
* `variant` and/or `own` (plus `content` or a `children` snippet). No header or
|
|
9
|
+
* labelled group is rendered — unless you also pass an `author` — so it nests
|
|
10
|
+
* cleanly inside an already-labelled message row without a redundant landmark.
|
|
11
|
+
* - **Self-contained card** (legacy form): pass `role` / `author` / `timestamp`
|
|
12
|
+
* (and optionally the `reactions` / `actions` snippets) without the styling
|
|
13
|
+
* axes. A header and a labelled `role="group"` (`<author|role> (<role>)`) are
|
|
14
|
+
* rendered so assistive tech can announce who sent each message; `author`
|
|
15
|
+
* falls back to a role label ("You" / "Assistant" / "System").
|
|
16
|
+
*
|
|
17
|
+
* `variant` + `own` are the canonical styling axes. The legacy `role` prop sets
|
|
18
|
+
* the header's role label and derives `variant`/`own` when those are not given.
|
|
8
19
|
*/
|
|
9
20
|
import type { Snippet } from 'svelte';
|
|
10
21
|
import type { ChatMessageRole } from '../../types-generic';
|
|
22
|
+
/** Visual tone: a peer message, an assistant message, or a centered notice. */
|
|
23
|
+
export type MessageBubbleVariant = 'default' | 'agent' | 'system';
|
|
11
24
|
export interface Props {
|
|
12
|
-
/**
|
|
25
|
+
/** Visual tone. Canonical styling axis. */
|
|
26
|
+
variant?: MessageBubbleVariant;
|
|
27
|
+
/** Whether the current viewer authored this message — drives alignment + own-color. */
|
|
28
|
+
own?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Legacy role. Prefer `variant` + `own`. Sets the header's role label and,
|
|
31
|
+
* when `variant`/`own` are unset, derives them (user → own default, agent →
|
|
32
|
+
* agent, system → system).
|
|
33
|
+
*/
|
|
13
34
|
role?: ChatMessageRole;
|
|
14
|
-
/**
|
|
35
|
+
/** Plain-text body. Ignored when a `children` snippet is provided. */
|
|
36
|
+
content?: string;
|
|
37
|
+
/** Body snippet (takes precedence over `content`). */
|
|
38
|
+
children?: Snippet;
|
|
39
|
+
/** Display name of the sender. When set, a header + labelled group render. */
|
|
15
40
|
author?: string;
|
|
16
|
-
/** Message time; rendered in a `<time datetime>` element. */
|
|
41
|
+
/** Message time; rendered in a `<time datetime>` element in the header. */
|
|
17
42
|
timestamp?: Date;
|
|
18
43
|
/** Override the visible timestamp text (defaults to a locale time). */
|
|
19
44
|
timestampLabel?: string;
|
|
20
|
-
/** Message body. */
|
|
21
|
-
children: Snippet;
|
|
22
45
|
/** Optional reactions row (e.g. a `ReactionPicker` or reaction chips). */
|
|
23
46
|
reactions?: Snippet;
|
|
24
47
|
/** Optional per-message actions (reply, copy, …). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageBubble.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/chat/MessageBubble.svelte.ts"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"MessageBubble.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/chat/MessageBubble.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,+EAA+E;AAC/E,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAElE,MAAM,WAAW,KAAK;IACpB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,uFAAuF;IACvF,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;OAIG;IACH,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qDAAqD;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAiFD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -2,15 +2,29 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ReactionPicker — a labelled group of emoji reaction buttons.
|
|
4
4
|
*
|
|
5
|
-
* Each button
|
|
6
|
-
* glyphs)
|
|
7
|
-
* activation.
|
|
5
|
+
* Each button carries a text accessible name (so the emoji isn't announced as
|
|
6
|
+
* raw glyphs) and the group carries an `aria-label`. Fires `onpick(emoji)` on
|
|
7
|
+
* activation. The group label and the fallback name for unrecognised emoji
|
|
8
|
+
* default to translated `ui.reaction_picker.*` strings; the common reactions use
|
|
9
|
+
* built-in English names unless a caller supplies `emojiLabel` (e.g. to route
|
|
10
|
+
* every label through a domain i18n catalog). Wraps gracefully for larger emoji
|
|
11
|
+
* sets.
|
|
8
12
|
*/
|
|
13
|
+
import { M } from '../../i18n/strings.ui.js';
|
|
14
|
+
import { useI18n } from '../../i18n/use-i18n.js';
|
|
15
|
+
|
|
9
16
|
export interface Props {
|
|
10
17
|
/** Emoji set to offer. */
|
|
11
18
|
emojis?: string[];
|
|
12
|
-
/** Accessible label for the group. */
|
|
19
|
+
/** Accessible label for the group. Defaults to a translated "Add reaction". */
|
|
13
20
|
label?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Accessible name for each emoji button. Defaults to a built-in name for the
|
|
23
|
+
* common reactions, else a translated "React with {emoji}".
|
|
24
|
+
*/
|
|
25
|
+
emojiLabel?: (emoji: string) => string;
|
|
26
|
+
/** Whether the picker is shown. */
|
|
27
|
+
isOpen?: boolean;
|
|
14
28
|
/** Fired with the chosen emoji. */
|
|
15
29
|
onpick?: (emoji: string) => void;
|
|
16
30
|
}
|
|
@@ -27,30 +41,48 @@ const EMOJI_LABELS: Record<string, string> = {
|
|
|
27
41
|
|
|
28
42
|
const {
|
|
29
43
|
emojis = DEFAULT_EMOJIS,
|
|
30
|
-
label
|
|
44
|
+
label,
|
|
45
|
+
emojiLabel,
|
|
46
|
+
isOpen = true,
|
|
31
47
|
onpick,
|
|
32
48
|
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
const { t } = useI18n();
|
|
51
|
+
|
|
52
|
+
const groupLabel = $derived(label ?? t(M['ui.reaction_picker.label']));
|
|
53
|
+
|
|
54
|
+
function nameFor(emoji: string): string {
|
|
55
|
+
return (
|
|
56
|
+
emojiLabel?.(emoji) ??
|
|
57
|
+
EMOJI_LABELS[emoji] ??
|
|
58
|
+
t(M['ui.reaction_picker.react_with'], { emoji })
|
|
59
|
+
);
|
|
60
|
+
}
|
|
33
61
|
</script>
|
|
34
62
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
{#if isOpen}
|
|
64
|
+
<div class="reactions" role="group" aria-label={groupLabel}>
|
|
65
|
+
{#each emojis as emoji (emoji)}
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
class="reactions__btn"
|
|
69
|
+
aria-label={nameFor(emoji)}
|
|
70
|
+
onclick={() => onpick?.(emoji)}
|
|
71
|
+
>
|
|
72
|
+
<span aria-hidden="true">{emoji}</span>
|
|
73
|
+
</button>
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
47
77
|
|
|
48
78
|
<style>
|
|
49
79
|
.reactions {
|
|
50
80
|
display: inline-flex;
|
|
81
|
+
flex-wrap: wrap;
|
|
51
82
|
gap: var(--smrt-spacing-1, 4px);
|
|
52
83
|
padding: var(--smrt-spacing-1, 4px);
|
|
53
|
-
|
|
84
|
+
max-width: 16rem;
|
|
85
|
+
border-radius: var(--smrt-radius-large, 12px);
|
|
54
86
|
background: var(--smrt-color-surface-container, #f3edf7);
|
|
55
87
|
}
|
|
56
88
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ReactionPicker — a labelled group of emoji reaction buttons.
|
|
3
|
-
*
|
|
4
|
-
* Each button has a text accessible name (so the emoji isn't announced as raw
|
|
5
|
-
* glyphs), and the group carries an `aria-label`. Fires `onpick(emoji)` on
|
|
6
|
-
* activation.
|
|
7
|
-
*/
|
|
8
1
|
export interface Props {
|
|
9
2
|
/** Emoji set to offer. */
|
|
10
3
|
emojis?: string[];
|
|
11
|
-
/** Accessible label for the group. */
|
|
4
|
+
/** Accessible label for the group. Defaults to a translated "Add reaction". */
|
|
12
5
|
label?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Accessible name for each emoji button. Defaults to a built-in name for the
|
|
8
|
+
* common reactions, else a translated "React with {emoji}".
|
|
9
|
+
*/
|
|
10
|
+
emojiLabel?: (emoji: string) => string;
|
|
11
|
+
/** Whether the picker is shown. */
|
|
12
|
+
isOpen?: boolean;
|
|
13
13
|
/** Fired with the chosen emoji. */
|
|
14
14
|
onpick?: (emoji: string) => void;
|
|
15
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactionPicker.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ReactionPicker.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReactionPicker.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/chat/ReactionPicker.svelte.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,KAAK;IACpB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,mCAAmC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mCAAmC;IACnC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAoDD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -2,51 +2,68 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* TypingIndicator — animated "…is typing" affordance.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Renders a visible label plus decorative animated dots (`aria-hidden`) inside a
|
|
6
|
+
* polite `role="status"` live region, so both sighted users and screen readers
|
|
7
|
+
* learn who is typing. Honors reduced-motion.
|
|
8
|
+
*
|
|
9
|
+
* Pass `names` to announce one or more typists ("Ada is typing", "Ada and Bob
|
|
10
|
+
* are typing", "Ada and 2 others are typing") — an empty list renders nothing.
|
|
11
|
+
* Or pass a single `name` / a full `label` override. The animated dots stand in
|
|
12
|
+
* for the trailing ellipsis, so the label text omits it.
|
|
8
13
|
*/
|
|
9
14
|
export interface Props {
|
|
10
|
-
/** Who is typing (
|
|
15
|
+
/** Who is typing (single). */
|
|
11
16
|
name?: string;
|
|
12
|
-
/**
|
|
17
|
+
/** Names of everyone currently typing — aggregated into the label. */
|
|
18
|
+
names?: string[];
|
|
19
|
+
/** Full label override; defaults to an aggregation of `names`/`name`. */
|
|
13
20
|
label?: string;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
const { name, label }: Props = $props();
|
|
17
|
-
|
|
23
|
+
const { name, names, label }: Props = $props();
|
|
24
|
+
|
|
25
|
+
// Names-mode (an explicit list) renders nothing when nobody is typing; the
|
|
26
|
+
// single-`name`/`label` form always renders.
|
|
27
|
+
const inNamesMode = $derived(names !== undefined);
|
|
28
|
+
const show = $derived(!inNamesMode || (names?.length ?? 0) > 0);
|
|
29
|
+
|
|
30
|
+
const text = $derived.by(() => {
|
|
31
|
+
if (label) return label;
|
|
32
|
+
if (names && names.length > 0) {
|
|
33
|
+
if (names.length === 1) return `${names[0]} is typing`;
|
|
34
|
+
if (names.length === 2) return `${names[0]} and ${names[1]} are typing`;
|
|
35
|
+
return `${names[0]} and ${names.length - 1} others are typing`;
|
|
36
|
+
}
|
|
37
|
+
return name ? `${name} is typing` : 'Typing';
|
|
38
|
+
});
|
|
18
39
|
</script>
|
|
19
40
|
|
|
20
|
-
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
<span class="
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
</
|
|
41
|
+
{#if show}
|
|
42
|
+
<div class="typing" role="status" aria-live="polite">
|
|
43
|
+
<span class="typing__text">{text}</span>
|
|
44
|
+
<span class="typing__dots" aria-hidden="true">
|
|
45
|
+
<span class="typing__dot"></span>
|
|
46
|
+
<span class="typing__dot"></span>
|
|
47
|
+
<span class="typing__dot"></span>
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
28
51
|
|
|
29
52
|
<style>
|
|
30
53
|
.typing {
|
|
31
54
|
display: inline-flex;
|
|
32
55
|
align-items: center;
|
|
33
|
-
gap: var(--smrt-spacing-
|
|
56
|
+
gap: var(--smrt-spacing-2, 8px);
|
|
34
57
|
padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
|
|
35
58
|
border-radius: var(--smrt-radius-large, 12px);
|
|
36
59
|
background: var(--smrt-color-surface-container, #f3edf7);
|
|
37
60
|
width: fit-content;
|
|
38
61
|
}
|
|
39
62
|
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
height: 1px;
|
|
44
|
-
padding: 0;
|
|
45
|
-
margin: -1px;
|
|
46
|
-
overflow: hidden;
|
|
47
|
-
clip: rect(0, 0, 0, 0);
|
|
63
|
+
.typing__text {
|
|
64
|
+
font-size: var(--smrt-typography-body-small-size, 0.75rem);
|
|
65
|
+
color: var(--smrt-color-on-surface-variant, #49454f);
|
|
48
66
|
white-space: nowrap;
|
|
49
|
-
border: 0;
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
.typing__dots {
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypingIndicator — animated "…is typing" affordance.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Renders a visible label plus decorative animated dots (`aria-hidden`) inside a
|
|
5
|
+
* polite `role="status"` live region, so both sighted users and screen readers
|
|
6
|
+
* learn who is typing. Honors reduced-motion.
|
|
7
|
+
*
|
|
8
|
+
* Pass `names` to announce one or more typists ("Ada is typing", "Ada and Bob
|
|
9
|
+
* are typing", "Ada and 2 others are typing") — an empty list renders nothing.
|
|
10
|
+
* Or pass a single `name` / a full `label` override. The animated dots stand in
|
|
11
|
+
* for the trailing ellipsis, so the label text omits it.
|
|
7
12
|
*/
|
|
8
13
|
export interface Props {
|
|
9
|
-
/** Who is typing (
|
|
14
|
+
/** Who is typing (single). */
|
|
10
15
|
name?: string;
|
|
11
|
-
/**
|
|
16
|
+
/** Names of everyone currently typing — aggregated into the label. */
|
|
17
|
+
names?: string[];
|
|
18
|
+
/** Full label override; defaults to an aggregation of `names`/`name`. */
|
|
12
19
|
label?: string;
|
|
13
20
|
}
|
|
14
21
|
declare const TypingIndicator: import("svelte").Component<Props, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TypingIndicator.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/chat/TypingIndicator.svelte.ts"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"TypingIndicator.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/chat/TypingIndicator.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,KAAK;IACpB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqCD,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|