@happyvertical/smrt-ui 0.51.38 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -2
- package/README.md +257 -5
- package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
- package/dist/actions/swipe-dismiss.d.ts +42 -0
- package/dist/actions/swipe-dismiss.d.ts.map +1 -0
- package/dist/actions/swipe-dismiss.js +69 -0
- package/dist/components/calendar/Calendar.svelte +5 -0
- package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
- package/dist/components/calendar/CalendarView.svelte +1250 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
- package/dist/components/calendar/DayView.svelte +5 -0
- package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
- package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
- package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
- package/dist/components/calendar/calendar-model.d.ts +178 -0
- package/dist/components/calendar/calendar-model.d.ts.map +1 -0
- package/dist/components/calendar/calendar-model.js +362 -0
- package/dist/components/calendar/index.d.ts +12 -1
- package/dist/components/calendar/index.d.ts.map +1 -1
- package/dist/components/calendar/index.js +11 -1
- package/dist/components/data/CollectionList.svelte +23 -5
- package/dist/components/data/CollectionList.svelte.d.ts +8 -2
- package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTable.svelte +118 -40
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +4 -0
- package/dist/components/data/DataTableController.d.ts.map +1 -1
- package/dist/components/data/DataTableController.js +11 -4
- package/dist/components/data/ListSortSelect.svelte +121 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
- package/dist/components/data/SortableHeader.svelte +169 -0
- package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
- package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
- package/dist/components/data/__tests__/CollectionList.test.js +37 -0
- package/dist/components/data/__tests__/DataTable.test.js +76 -0
- package/dist/components/data/__tests__/DataTableController.test.js +35 -0
- package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
- package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
- package/dist/components/data/__tests__/list-sort.test.js +91 -0
- package/dist/components/data/__tests__/surface-navigation.test.js +121 -0
- package/dist/components/data/data-surface.d.ts +1 -0
- package/dist/components/data/data-surface.d.ts.map +1 -1
- package/dist/components/data/data-surface.js +1 -0
- package/dist/components/data/index.d.ts +6 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +6 -0
- package/dist/components/data/link-surface.d.ts +79 -0
- package/dist/components/data/link-surface.d.ts.map +1 -0
- package/dist/components/data/link-surface.js +155 -0
- package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
- package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
- package/dist/components/data/list-sort-state.svelte.js +47 -0
- package/dist/components/data/list-sort.d.ts +94 -0
- package/dist/components/data/list-sort.d.ts.map +1 -0
- package/dist/components/data/list-sort.js +182 -0
- package/dist/components/data/step-surface.d.ts +73 -0
- package/dist/components/data/step-surface.d.ts.map +1 -0
- package/dist/components/data/step-surface.js +177 -0
- package/dist/components/data/surface-navigation.d.ts +49 -0
- package/dist/components/data/surface-navigation.d.ts.map +1 -0
- package/dist/components/data/surface-navigation.js +100 -0
- package/dist/components/data/types.d.ts +19 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/feedback/PhoneSheet.svelte +224 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
- package/dist/components/feedback/WorkingStrip.svelte +426 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
- package/dist/components/feedback/index.d.ts +4 -0
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/index.js +5 -0
- package/dist/components/feedback/working-status.d.ts +34 -0
- package/dist/components/feedback/working-status.d.ts.map +1 -0
- package/dist/components/feedback/working-status.js +5 -0
- package/dist/components/forms/Checkbox.svelte +26 -2
- package/dist/components/forms/Combobox.svelte +68 -12
- package/dist/components/forms/Combobox.svelte.d.ts +6 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/DictationButton.svelte +128 -0
- package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
- package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
- package/dist/components/forms/DictationStatus.svelte +164 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
- package/dist/components/forms/Fieldset.svelte +5 -1
- package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
- package/dist/components/forms/Fieldset.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormActionBar.svelte +84 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +9 -0
- package/dist/components/forms/Radio.svelte +15 -4
- package/dist/components/forms/SearchInput.svelte +207 -0
- package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
- package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/Select.svelte +9 -0
- package/dist/components/forms/StagedControlReview.svelte +21 -12
- package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
- package/dist/components/forms/Switch.svelte +11 -5
- package/dist/components/forms/TagsInput.svelte +8 -1
- package/dist/components/forms/Textarea.svelte +8 -0
- package/dist/components/forms/__tests__/Combobox.test.js +87 -0
- package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
- package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
- package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
- package/dist/components/forms/__tests__/FormActionBar.test.js +22 -0
- package/dist/components/forms/__tests__/SearchInput.test.js +92 -0
- package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
- package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
- package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
- package/dist/components/forms/__tests__/dictation.test.js +486 -0
- package/dist/components/forms/__tests__/long-press.test.js +175 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte +33 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.d.ts +69 -0
- package/dist/components/forms/audio-capture.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.js +193 -0
- package/dist/components/forms/control-interaction.d.ts +6 -0
- package/dist/components/forms/control-interaction.d.ts.map +1 -1
- package/dist/components/forms/control-interaction.js +12 -1
- package/dist/components/forms/control-proposals.d.ts +55 -0
- package/dist/components/forms/control-proposals.d.ts.map +1 -0
- package/dist/components/forms/control-proposals.js +188 -0
- package/dist/components/forms/dictation-transcribe.d.ts +36 -0
- package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
- package/dist/components/forms/dictation-transcribe.js +65 -0
- package/dist/components/forms/dictation.svelte.d.ts +200 -0
- package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
- package/dist/components/forms/dictation.svelte.js +686 -0
- package/dist/components/forms/index.d.ts +14 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +14 -1
- package/dist/components/forms/insert-text.d.ts +12 -0
- package/dist/components/forms/insert-text.d.ts.map +1 -0
- package/dist/components/forms/insert-text.js +38 -0
- package/dist/components/forms/long-press.d.ts +88 -0
- package/dist/components/forms/long-press.d.ts.map +1 -0
- package/dist/components/forms/long-press.js +213 -0
- package/dist/components/forms/ready-beep.d.ts +24 -0
- package/dist/components/forms/ready-beep.d.ts.map +1 -0
- package/dist/components/forms/ready-beep.js +82 -0
- package/dist/components/layout/PageHeader.svelte +151 -17
- package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
- package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
- package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +1 -0
- package/dist/components/layout/page-header-context.d.ts +25 -0
- package/dist/components/layout/page-header-context.d.ts.map +1 -0
- package/dist/components/layout/page-header-context.js +16 -0
- package/dist/components/nav/Tabs.svelte +241 -0
- package/dist/components/nav/Tabs.svelte.d.ts +15 -0
- package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
- package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
- package/dist/components/nav/index.d.ts +1 -0
- package/dist/components/nav/index.d.ts.map +1 -1
- package/dist/components/nav/index.js +1 -0
- package/dist/components/nav/tabs-overflow.d.ts +12 -0
- package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
- package/dist/components/nav/tabs-overflow.js +17 -0
- package/dist/components/nav/types.d.ts +8 -0
- package/dist/components/nav/types.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +7 -0
- package/dist/components/ui/Card.svelte +14 -5
- package/dist/components/ui/Dropdown.svelte +55 -1
- package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
- package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
- package/dist/components/ui/IconToggle.svelte +139 -0
- package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
- package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
- package/dist/components/ui/IconToggleGroup.svelte +56 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
- package/dist/components/ui/Pagination.svelte +1 -1
- package/dist/components/ui/Popover.svelte +17 -1
- package/dist/components/ui/Popover.svelte.d.ts +8 -0
- package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
- package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
- package/dist/components/ui/__tests__/Popover.test.js +44 -0
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/i18n/strings.ui.d.ts +41 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +46 -0
- package/dist/svelte/__tests__/playground.test.js +1 -0
- package/dist/svelte/playground/CalendarPreview.svelte +140 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/types-generic.d.ts +2 -0
- package/dist/types-generic.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* WorkingStrip — "something is working" status.
|
|
4
|
+
*
|
|
5
|
+
* - `variant="strip"`: a thin bar (place it above a phone bottom bar); with
|
|
6
|
+
* `onopen` the whole strip is a button that reopens the surface doing the
|
|
7
|
+
* work (e.g. an assistant sheet);
|
|
8
|
+
* - `variant="floating"`: the same content as a pill the host floats over
|
|
9
|
+
* the page (desktop and tablet), with the goal above the current step;
|
|
10
|
+
* - `variant="inline"`: a compact line at the top of a pane.
|
|
11
|
+
*
|
|
12
|
+
* Phases (see `WorkingStatus`): a spinner while `working`; `paused`;
|
|
13
|
+
* `waiting` (the person is needed, with a prominent Review button); `done`
|
|
14
|
+
* (success colors), `failed` (error colors) and `cancelled`, each with an
|
|
15
|
+
* icon and text, never color alone; nothing while `idle`.
|
|
16
|
+
*
|
|
17
|
+
* Accessibility: two live regions stay mounted. Step changes are announced
|
|
18
|
+
* politely, at most once per `announceIntervalMs`; `waiting` and `failed`
|
|
19
|
+
* are announced assertively. The strip never takes focus by itself. Escape
|
|
20
|
+
* inside it pauses the work when `onpause` is given. Every control is at
|
|
21
|
+
* least 44px (strip and floating).
|
|
22
|
+
*/
|
|
23
|
+
import { onDestroy, untrack } from 'svelte';
|
|
24
|
+
import { M } from '../../i18n/strings.ui.js';
|
|
25
|
+
import { useI18n } from '../../i18n/use-i18n.js';
|
|
26
|
+
import {
|
|
27
|
+
WORKING_ASSERTIVE_PHASES,
|
|
28
|
+
type WorkingPhase,
|
|
29
|
+
type WorkingStatus,
|
|
30
|
+
} from './working-status.js';
|
|
31
|
+
|
|
32
|
+
export interface Props {
|
|
33
|
+
/** What is happening. */
|
|
34
|
+
status: WorkingStatus;
|
|
35
|
+
/** `strip` (above a bottom bar), `floating` (a pill), or `inline`. */
|
|
36
|
+
variant?: 'strip' | 'floating' | 'inline';
|
|
37
|
+
/** Show a Stop button while the work runs, is paused, or waits. */
|
|
38
|
+
canStop?: boolean;
|
|
39
|
+
/** Stop the work. */
|
|
40
|
+
onstop?: () => void;
|
|
41
|
+
/** Pause the work (shows Pause while working; Escape pauses too). */
|
|
42
|
+
onpause?: () => void;
|
|
43
|
+
/** Continue paused work (shows Continue while paused). */
|
|
44
|
+
onresume?: () => void;
|
|
45
|
+
/** Open what the work is waiting on (shows Review while waiting). */
|
|
46
|
+
onreview?: () => void;
|
|
47
|
+
/** Reopen the surface doing the work (strip and floating). */
|
|
48
|
+
onopen?: () => void;
|
|
49
|
+
/** Hide a finished status (shows Close once done, failed or cancelled). */
|
|
50
|
+
ondismiss?: () => void;
|
|
51
|
+
/** Accessible hint for `onopen` (default "Open"). */
|
|
52
|
+
openLabel?: string;
|
|
53
|
+
/** Stop button text (default "Stop"). */
|
|
54
|
+
stopLabel?: string;
|
|
55
|
+
/** Pause button text (default "Pause"). */
|
|
56
|
+
pauseLabel?: string;
|
|
57
|
+
/** Continue button text (default "Continue"). */
|
|
58
|
+
resumeLabel?: string;
|
|
59
|
+
/** Review button text (default "Review"). */
|
|
60
|
+
reviewLabel?: string;
|
|
61
|
+
/** Minimum time between two polite step announcements. Default 2000ms. */
|
|
62
|
+
announceIntervalMs?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let {
|
|
66
|
+
status,
|
|
67
|
+
variant = 'strip',
|
|
68
|
+
canStop = false,
|
|
69
|
+
onstop,
|
|
70
|
+
onpause,
|
|
71
|
+
onresume,
|
|
72
|
+
onreview,
|
|
73
|
+
onopen,
|
|
74
|
+
ondismiss,
|
|
75
|
+
openLabel,
|
|
76
|
+
stopLabel,
|
|
77
|
+
pauseLabel,
|
|
78
|
+
resumeLabel,
|
|
79
|
+
reviewLabel,
|
|
80
|
+
announceIntervalMs = 2000,
|
|
81
|
+
}: Props = $props();
|
|
82
|
+
|
|
83
|
+
const { t } = useI18n();
|
|
84
|
+
|
|
85
|
+
const DEFAULT_LABEL = {
|
|
86
|
+
working: M['ui.working_strip.working'],
|
|
87
|
+
paused: M['ui.working_strip.paused'],
|
|
88
|
+
waiting: M['ui.working_strip.waiting'],
|
|
89
|
+
done: M['ui.working_strip.done'],
|
|
90
|
+
failed: M['ui.working_strip.failed'],
|
|
91
|
+
cancelled: M['ui.working_strip.cancelled'],
|
|
92
|
+
} satisfies Record<Exclude<WorkingPhase, 'idle'>, unknown>;
|
|
93
|
+
|
|
94
|
+
const phase = $derived(status.phase);
|
|
95
|
+
const visible = $derived(phase !== 'idle');
|
|
96
|
+
const label = $derived(
|
|
97
|
+
phase === 'idle' ? '' : (status.label ?? t(DEFAULT_LABEL[phase])),
|
|
98
|
+
);
|
|
99
|
+
const goal = $derived(status.goal?.trim() || null);
|
|
100
|
+
const assertive = $derived(WORKING_ASSERTIVE_PHASES.includes(phase));
|
|
101
|
+
const terminal = $derived(
|
|
102
|
+
phase === 'done' || phase === 'failed' || phase === 'cancelled',
|
|
103
|
+
);
|
|
104
|
+
const running = $derived(
|
|
105
|
+
phase === 'working' || phase === 'paused' || phase === 'waiting',
|
|
106
|
+
);
|
|
107
|
+
const accessibleSummary = $derived(goal ? `${goal}. ${label}` : label);
|
|
108
|
+
|
|
109
|
+
// Polite announcements, throttled so a fast run does not chatter.
|
|
110
|
+
let politeText = $state('');
|
|
111
|
+
let lastAnnouncedAt = 0;
|
|
112
|
+
let announceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
113
|
+
|
|
114
|
+
function announce(text: string) {
|
|
115
|
+
if (announceTimer) clearTimeout(announceTimer);
|
|
116
|
+
announceTimer = null;
|
|
117
|
+
const wait = Math.max(0, lastAnnouncedAt + announceIntervalMs - Date.now());
|
|
118
|
+
if (wait === 0 || text === '') {
|
|
119
|
+
politeText = text;
|
|
120
|
+
lastAnnouncedAt = text ? Date.now() : lastAnnouncedAt;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
announceTimer = setTimeout(() => {
|
|
124
|
+
announceTimer = null;
|
|
125
|
+
politeText = text;
|
|
126
|
+
lastAnnouncedAt = Date.now();
|
|
127
|
+
}, wait);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$effect(() => {
|
|
131
|
+
const text = visible && !assertive ? label : '';
|
|
132
|
+
untrack(() => announce(text));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
onDestroy(() => {
|
|
136
|
+
if (announceTimer) clearTimeout(announceTimer);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
140
|
+
if (event.key === 'Escape' && phase === 'working' && onpause) {
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
onpause();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<div class="working-strip__live" aria-live="polite" aria-atomic="true">
|
|
148
|
+
{politeText}
|
|
149
|
+
</div>
|
|
150
|
+
<div class="working-strip__live" aria-live="assertive" aria-atomic="true">
|
|
151
|
+
{visible && assertive ? accessibleSummary : ''}
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{#if visible}
|
|
155
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
156
|
+
<div
|
|
157
|
+
class="working-strip working-strip--{variant}"
|
|
158
|
+
data-phase={phase}
|
|
159
|
+
data-testid="working-strip"
|
|
160
|
+
onkeydown={handleKeydown}
|
|
161
|
+
>
|
|
162
|
+
{#snippet content()}
|
|
163
|
+
<span class="working-strip__icon" aria-hidden="true">
|
|
164
|
+
{#if phase === 'working'}
|
|
165
|
+
<span class="working-strip__spinner"></span>
|
|
166
|
+
{:else if phase === 'paused'}
|
|
167
|
+
<span class="working-strip__glyph">❚❚</span>
|
|
168
|
+
{:else if phase === 'waiting'}
|
|
169
|
+
<span class="working-strip__glyph">!</span>
|
|
170
|
+
{:else if phase === 'done'}
|
|
171
|
+
<span class="working-strip__glyph">✓</span>
|
|
172
|
+
{:else if phase === 'failed'}
|
|
173
|
+
<span class="working-strip__glyph">✕</span>
|
|
174
|
+
{:else if phase === 'cancelled'}
|
|
175
|
+
<span class="working-strip__glyph">■</span>
|
|
176
|
+
{/if}
|
|
177
|
+
</span>
|
|
178
|
+
<span class="working-strip__text" aria-hidden="true">
|
|
179
|
+
{#if goal && variant === 'floating'}
|
|
180
|
+
<span class="working-strip__goal">{goal}</span>
|
|
181
|
+
{/if}
|
|
182
|
+
<span class="working-strip__label">{label}</span>
|
|
183
|
+
</span>
|
|
184
|
+
{/snippet}
|
|
185
|
+
|
|
186
|
+
{#if variant !== 'inline' && onopen}
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
class="working-strip__main"
|
|
190
|
+
aria-label={`${accessibleSummary}. ${openLabel ?? t(M['ui.working_strip.open'])}`}
|
|
191
|
+
onclick={onopen}
|
|
192
|
+
>
|
|
193
|
+
{@render content()}
|
|
194
|
+
</button>
|
|
195
|
+
{:else}
|
|
196
|
+
<div class="working-strip__main">
|
|
197
|
+
<span class="working-strip__sr">{accessibleSummary}</span>
|
|
198
|
+
{@render content()}
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
|
|
202
|
+
<div class="working-strip__actions">
|
|
203
|
+
{#if phase === 'waiting' && onreview}
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
class="working-strip__button working-strip__button--primary"
|
|
207
|
+
onclick={onreview}
|
|
208
|
+
>
|
|
209
|
+
{reviewLabel ?? t(M['ui.working_strip.review'])}
|
|
210
|
+
</button>
|
|
211
|
+
{/if}
|
|
212
|
+
{#if phase === 'working' && onpause}
|
|
213
|
+
<button type="button" class="working-strip__button" onclick={onpause}>
|
|
214
|
+
{pauseLabel ?? t(M['ui.working_strip.pause'])}
|
|
215
|
+
</button>
|
|
216
|
+
{/if}
|
|
217
|
+
{#if phase === 'paused' && onresume}
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
class="working-strip__button working-strip__button--primary"
|
|
221
|
+
onclick={onresume}
|
|
222
|
+
>
|
|
223
|
+
{resumeLabel ?? t(M['ui.working_strip.resume'])}
|
|
224
|
+
</button>
|
|
225
|
+
{/if}
|
|
226
|
+
{#if canStop && running}
|
|
227
|
+
<button type="button" class="working-strip__button" onclick={onstop}>
|
|
228
|
+
{stopLabel ?? t(M['ui.working_strip.stop'])}
|
|
229
|
+
</button>
|
|
230
|
+
{/if}
|
|
231
|
+
{#if terminal && ondismiss}
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
class="working-strip__button working-strip__button--icon"
|
|
235
|
+
aria-label={t(M['ui.working_strip.dismiss'])}
|
|
236
|
+
onclick={ondismiss}
|
|
237
|
+
>
|
|
238
|
+
<span aria-hidden="true">✕</span>
|
|
239
|
+
</button>
|
|
240
|
+
{/if}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
{/if}
|
|
244
|
+
|
|
245
|
+
<style>
|
|
246
|
+
.working-strip__live,
|
|
247
|
+
.working-strip__sr {
|
|
248
|
+
position: absolute;
|
|
249
|
+
inline-size: 1px;
|
|
250
|
+
block-size: 1px;
|
|
251
|
+
overflow: hidden;
|
|
252
|
+
clip-path: inset(50%);
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.working-strip {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: var(--smrt-spacing-2);
|
|
260
|
+
min-inline-size: 0;
|
|
261
|
+
background: var(--smrt-color-surface-container-high);
|
|
262
|
+
color: var(--smrt-color-on-surface);
|
|
263
|
+
font: var(--smrt-typography-body-medium-font);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.working-strip--strip {
|
|
267
|
+
box-sizing: border-box;
|
|
268
|
+
min-block-size: 2.75rem;
|
|
269
|
+
padding-inline-end: var(--smrt-spacing-2);
|
|
270
|
+
border-block-start: 1px solid var(--smrt-color-outline-variant);
|
|
271
|
+
box-shadow: var(--smrt-elevation-2);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.working-strip--floating {
|
|
275
|
+
box-sizing: border-box;
|
|
276
|
+
min-block-size: 3.5rem;
|
|
277
|
+
max-inline-size: min(34rem, calc(100vw - 2rem));
|
|
278
|
+
padding-block: var(--smrt-spacing-1);
|
|
279
|
+
padding-inline-end: var(--smrt-spacing-2);
|
|
280
|
+
border: 1px solid var(--smrt-color-outline-variant);
|
|
281
|
+
border-radius: var(--smrt-radius-xl, 1.25rem);
|
|
282
|
+
box-shadow: var(--smrt-elevation-3);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.working-strip--inline {
|
|
286
|
+
padding: var(--smrt-spacing-2) var(--smrt-spacing-3);
|
|
287
|
+
border-block-end: 1px solid var(--smrt-color-outline-variant);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.working-strip[data-phase='waiting'] {
|
|
291
|
+
background: var(--smrt-color-tertiary-container, var(--smrt-color-primary-container));
|
|
292
|
+
color: var(--smrt-color-on-tertiary-container, var(--smrt-color-on-primary-container));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.working-strip[data-phase='done'] {
|
|
296
|
+
background: var(--smrt-color-success-container, var(--smrt-color-primary-container));
|
|
297
|
+
color: var(--smrt-color-on-success-container, var(--smrt-color-on-primary-container));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.working-strip[data-phase='failed'] {
|
|
301
|
+
background: var(--smrt-color-error-container);
|
|
302
|
+
color: var(--smrt-color-on-error-container);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.working-strip__main {
|
|
306
|
+
display: flex;
|
|
307
|
+
flex: 1;
|
|
308
|
+
align-items: center;
|
|
309
|
+
gap: var(--smrt-spacing-2);
|
|
310
|
+
min-inline-size: 0;
|
|
311
|
+
min-block-size: 2.75rem;
|
|
312
|
+
padding: 0 var(--smrt-spacing-4);
|
|
313
|
+
border: 0;
|
|
314
|
+
border-radius: inherit;
|
|
315
|
+
background: transparent;
|
|
316
|
+
color: inherit;
|
|
317
|
+
font: inherit;
|
|
318
|
+
text-align: start;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
button.working-strip__main {
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.working-strip--inline .working-strip__main {
|
|
326
|
+
min-block-size: 0;
|
|
327
|
+
padding: 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.working-strip__main:focus-visible,
|
|
331
|
+
.working-strip__button:focus-visible {
|
|
332
|
+
outline: 2px solid currentColor;
|
|
333
|
+
outline-offset: -2px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.working-strip__icon {
|
|
337
|
+
display: inline-grid;
|
|
338
|
+
flex: none;
|
|
339
|
+
place-items: center;
|
|
340
|
+
inline-size: 1.25rem;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.working-strip__glyph {
|
|
344
|
+
font-weight: 700;
|
|
345
|
+
line-height: 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.working-strip__text {
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
min-inline-size: 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.working-strip__goal {
|
|
355
|
+
overflow: hidden;
|
|
356
|
+
font: var(--smrt-typography-label-medium-font);
|
|
357
|
+
opacity: 0.8;
|
|
358
|
+
text-overflow: ellipsis;
|
|
359
|
+
white-space: nowrap;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.working-strip__label {
|
|
363
|
+
overflow: hidden;
|
|
364
|
+
text-overflow: ellipsis;
|
|
365
|
+
white-space: nowrap;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.working-strip__spinner {
|
|
369
|
+
display: block;
|
|
370
|
+
inline-size: 1rem;
|
|
371
|
+
block-size: 1rem;
|
|
372
|
+
border: 2px solid
|
|
373
|
+
color-mix(in srgb, currentColor 25%, transparent);
|
|
374
|
+
border-block-start-color: currentColor;
|
|
375
|
+
border-radius: var(--smrt-radius-full);
|
|
376
|
+
animation: working-strip-spin 0.8s linear infinite;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.working-strip__actions {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex: none;
|
|
382
|
+
align-items: center;
|
|
383
|
+
gap: var(--smrt-spacing-1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.working-strip__button {
|
|
387
|
+
flex: none;
|
|
388
|
+
min-inline-size: 2.75rem;
|
|
389
|
+
min-block-size: 2.75rem;
|
|
390
|
+
padding: 0 var(--smrt-spacing-4);
|
|
391
|
+
border: 1px solid var(--smrt-color-outline-variant);
|
|
392
|
+
border-radius: var(--smrt-radius-full);
|
|
393
|
+
background: var(--smrt-color-surface);
|
|
394
|
+
color: var(--smrt-color-on-surface);
|
|
395
|
+
font: var(--smrt-typography-label-large-font);
|
|
396
|
+
cursor: pointer;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.working-strip__button--primary {
|
|
400
|
+
border-color: transparent;
|
|
401
|
+
background: var(--smrt-color-primary);
|
|
402
|
+
color: var(--smrt-color-on-primary);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.working-strip__button--icon {
|
|
406
|
+
padding: 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.working-strip--inline .working-strip__button {
|
|
410
|
+
min-block-size: 2rem;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@keyframes working-strip-spin {
|
|
414
|
+
to {
|
|
415
|
+
transform: rotate(360deg);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@media (prefers-reduced-motion: reduce) {
|
|
420
|
+
.working-strip__spinner {
|
|
421
|
+
animation: none;
|
|
422
|
+
border-block-start-color: color-mix(in srgb, currentColor 25%, transparent);
|
|
423
|
+
border-inline-start-color: currentColor;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type WorkingStatus } from './working-status.js';
|
|
2
|
+
export interface Props {
|
|
3
|
+
/** What is happening. */
|
|
4
|
+
status: WorkingStatus;
|
|
5
|
+
/** `strip` (above a bottom bar), `floating` (a pill), or `inline`. */
|
|
6
|
+
variant?: 'strip' | 'floating' | 'inline';
|
|
7
|
+
/** Show a Stop button while the work runs, is paused, or waits. */
|
|
8
|
+
canStop?: boolean;
|
|
9
|
+
/** Stop the work. */
|
|
10
|
+
onstop?: () => void;
|
|
11
|
+
/** Pause the work (shows Pause while working; Escape pauses too). */
|
|
12
|
+
onpause?: () => void;
|
|
13
|
+
/** Continue paused work (shows Continue while paused). */
|
|
14
|
+
onresume?: () => void;
|
|
15
|
+
/** Open what the work is waiting on (shows Review while waiting). */
|
|
16
|
+
onreview?: () => void;
|
|
17
|
+
/** Reopen the surface doing the work (strip and floating). */
|
|
18
|
+
onopen?: () => void;
|
|
19
|
+
/** Hide a finished status (shows Close once done, failed or cancelled). */
|
|
20
|
+
ondismiss?: () => void;
|
|
21
|
+
/** Accessible hint for `onopen` (default "Open"). */
|
|
22
|
+
openLabel?: string;
|
|
23
|
+
/** Stop button text (default "Stop"). */
|
|
24
|
+
stopLabel?: string;
|
|
25
|
+
/** Pause button text (default "Pause"). */
|
|
26
|
+
pauseLabel?: string;
|
|
27
|
+
/** Continue button text (default "Continue"). */
|
|
28
|
+
resumeLabel?: string;
|
|
29
|
+
/** Review button text (default "Review"). */
|
|
30
|
+
reviewLabel?: string;
|
|
31
|
+
/** Minimum time between two polite step announcements. Default 2000ms. */
|
|
32
|
+
announceIntervalMs?: number;
|
|
33
|
+
}
|
|
34
|
+
declare const WorkingStrip: import("svelte").Component<Props, {}, "">;
|
|
35
|
+
type WorkingStrip = ReturnType<typeof WorkingStrip>;
|
|
36
|
+
export default WorkingStrip;
|
|
37
|
+
//# sourceMappingURL=WorkingStrip.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkingStrip.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/WorkingStrip.svelte.ts"],"names":[],"mappings":"AA2BA,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,KAAK;IACpB,yBAAyB;IACzB,MAAM,EAAE,aAAa,CAAC;IACtB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC1C,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qBAAqB;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AA0KD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoneSheet: a non-modal phone surface (full-screen page or bottom sheet)
|
|
3
|
+
* that stays mounted while closed, closes on its button / Escape / swipe
|
|
4
|
+
* down, and manages focus.
|
|
5
|
+
*/
|
|
6
|
+
import { render, screen } from '@testing-library/svelte';
|
|
7
|
+
import userEvent from '@testing-library/user-event';
|
|
8
|
+
import { createRawSnippet, flushSync, tick } from 'svelte';
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
11
|
+
import PhoneSheet from '../PhoneSheet.svelte';
|
|
12
|
+
const body = createRawSnippet(() => ({
|
|
13
|
+
render: () => '<p>Sheet body</p>',
|
|
14
|
+
}));
|
|
15
|
+
describe('PhoneSheet', () => {
|
|
16
|
+
it('renders a labelled dialog with a named close button when open', async () => {
|
|
17
|
+
const { container } = render(PhoneSheet, {
|
|
18
|
+
props: {
|
|
19
|
+
open: true,
|
|
20
|
+
title: 'Notifications',
|
|
21
|
+
onclose: () => { },
|
|
22
|
+
children: body,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const dialog = screen.getByRole('dialog', { name: 'Notifications' });
|
|
26
|
+
expect(dialog).toHaveAttribute('aria-hidden', 'false');
|
|
27
|
+
expect(screen.getByRole('button', { name: 'Close Notifications' })).toBeInTheDocument();
|
|
28
|
+
expect(screen.getByText('Sheet body')).toBeInTheDocument();
|
|
29
|
+
await expectNoA11yViolations(container);
|
|
30
|
+
});
|
|
31
|
+
it('stays mounted but hidden and inert while closed', () => {
|
|
32
|
+
render(PhoneSheet, {
|
|
33
|
+
props: {
|
|
34
|
+
open: false,
|
|
35
|
+
title: 'Assistant',
|
|
36
|
+
onclose: () => { },
|
|
37
|
+
children: body,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const sheet = screen.getByTestId('phone-sheet');
|
|
41
|
+
expect(sheet).toHaveAttribute('aria-hidden', 'true');
|
|
42
|
+
// jsdom has no `inert` IDL attribute, so Svelte sets it as a property.
|
|
43
|
+
expect(sheet.hasAttribute('inert') ||
|
|
44
|
+
sheet.inert).toBe(true);
|
|
45
|
+
expect(sheet.classList.contains('open')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('closes on the close button and on Escape', async () => {
|
|
48
|
+
const onclose = vi.fn();
|
|
49
|
+
render(PhoneSheet, {
|
|
50
|
+
props: {
|
|
51
|
+
open: true,
|
|
52
|
+
title: 'Assistant',
|
|
53
|
+
onclose,
|
|
54
|
+
closeLabel: 'Close the assistant',
|
|
55
|
+
children: body,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
await userEvent.click(screen.getByRole('button', { name: 'Close the assistant' }));
|
|
59
|
+
expect(onclose).toHaveBeenCalledTimes(1);
|
|
60
|
+
screen.getByRole('dialog').dispatchEvent(new KeyboardEvent('keydown', {
|
|
61
|
+
key: 'Escape',
|
|
62
|
+
bubbles: true,
|
|
63
|
+
cancelable: true,
|
|
64
|
+
}));
|
|
65
|
+
expect(onclose).toHaveBeenCalledTimes(2);
|
|
66
|
+
});
|
|
67
|
+
it('moves focus in on open and back to the opener on close', async () => {
|
|
68
|
+
const opener = document.createElement('button');
|
|
69
|
+
document.body.appendChild(opener);
|
|
70
|
+
opener.focus();
|
|
71
|
+
const { rerender } = render(PhoneSheet, {
|
|
72
|
+
props: {
|
|
73
|
+
open: false,
|
|
74
|
+
title: 'Assistant',
|
|
75
|
+
onclose: () => { },
|
|
76
|
+
children: body,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
await rerender({ open: true });
|
|
80
|
+
await tick();
|
|
81
|
+
await tick();
|
|
82
|
+
expect(document.activeElement).toBe(screen.getByTestId('phone-sheet'));
|
|
83
|
+
await rerender({ open: false });
|
|
84
|
+
flushSync();
|
|
85
|
+
expect(document.activeElement).toBe(opener);
|
|
86
|
+
opener.remove();
|
|
87
|
+
});
|
|
88
|
+
it('draws a grabber only for the sheet variant', () => {
|
|
89
|
+
const { container } = render(PhoneSheet, {
|
|
90
|
+
props: {
|
|
91
|
+
open: true,
|
|
92
|
+
title: 'Assistant',
|
|
93
|
+
variant: 'sheet',
|
|
94
|
+
onclose: () => { },
|
|
95
|
+
children: body,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
expect(container.querySelector('.phone-sheet__grabber')).not.toBeNull();
|
|
99
|
+
expect(container.querySelector('.phone-sheet--sheet')).not.toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|