@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,164 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DictationStatus — what dictation is doing, in words (see `Dictation`).
|
|
4
|
+
*
|
|
5
|
+
* "Listening…" (with the words heard so far) while the microphone is on,
|
|
6
|
+
* "Writing it down…" while a recording is turned into text, and
|
|
7
|
+
* a plain explanation when it cannot listen: the browser cannot turn speech
|
|
8
|
+
* into text, the microphone is blocked, or nothing was heard. Nothing while
|
|
9
|
+
* idle. Listening is announced politely; problems are announced as alerts.
|
|
10
|
+
*/
|
|
11
|
+
import { M } from '../../i18n/strings.ui.js';
|
|
12
|
+
import { useI18n } from '../../i18n/use-i18n.js';
|
|
13
|
+
import type { Dictation, DictationErrorKind } from './dictation.svelte.js';
|
|
14
|
+
|
|
15
|
+
export interface Props {
|
|
16
|
+
/** The dictation session whose listening state and messages this shows. */
|
|
17
|
+
dictation: Dictation;
|
|
18
|
+
/** Extra class names. */
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { dictation, class: className = '' }: Props = $props();
|
|
23
|
+
|
|
24
|
+
const { t } = useI18n();
|
|
25
|
+
const showLine = $derived(
|
|
26
|
+
dictation.state === 'starting' ||
|
|
27
|
+
dictation.state === 'listening' ||
|
|
28
|
+
dictation.state === 'stopping' ||
|
|
29
|
+
dictation.state === 'transcribing',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
function errorText(kind: DictationErrorKind | null): string {
|
|
33
|
+
switch (kind) {
|
|
34
|
+
case 'unsupported':
|
|
35
|
+
return t(M['ui.dictation.unsupported']);
|
|
36
|
+
case 'denied':
|
|
37
|
+
return t(M['ui.dictation.denied']);
|
|
38
|
+
case 'no-speech':
|
|
39
|
+
return t(M['ui.dictation.no_speech']);
|
|
40
|
+
case 'microphone':
|
|
41
|
+
return t(M['ui.dictation.microphone']);
|
|
42
|
+
case 'interrupted':
|
|
43
|
+
return t(M['ui.dictation.interrupted']);
|
|
44
|
+
case 'too-long':
|
|
45
|
+
return t(M['ui.dictation.too_long']);
|
|
46
|
+
case 'not-transcribed':
|
|
47
|
+
return t(M['ui.dictation.not_transcribed']);
|
|
48
|
+
case 'unavailable':
|
|
49
|
+
return t(M['ui.dictation.unavailable']);
|
|
50
|
+
case 'forbidden':
|
|
51
|
+
return t(M['ui.dictation.forbidden']);
|
|
52
|
+
default:
|
|
53
|
+
return t(M['ui.dictation.failed']);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div class={`smrt-dictation-status ${className}`.trim()}>
|
|
59
|
+
<p
|
|
60
|
+
class="smrt-dictation-status-line"
|
|
61
|
+
class:smrt-dictation-status--empty={!showLine}
|
|
62
|
+
role="status"
|
|
63
|
+
aria-live="polite"
|
|
64
|
+
>
|
|
65
|
+
{#if dictation.state === 'starting'}
|
|
66
|
+
{t(M['ui.dictation.starting'])}
|
|
67
|
+
{:else if dictation.state === 'listening' || dictation.state === 'stopping'}
|
|
68
|
+
<span class="smrt-dictation-status-dot" aria-hidden="true"></span>
|
|
69
|
+
{t(M['ui.dictation.listening'])}
|
|
70
|
+
{#if dictation.interim}
|
|
71
|
+
<span class="smrt-dictation-status-interim">“{dictation.interim}”</span>
|
|
72
|
+
{/if}
|
|
73
|
+
{:else if dictation.state === 'transcribing'}
|
|
74
|
+
<span class="smrt-dictation-status-spinner" aria-hidden="true"></span>
|
|
75
|
+
{t(M['ui.dictation.transcribing'])}
|
|
76
|
+
{/if}
|
|
77
|
+
</p>
|
|
78
|
+
<p
|
|
79
|
+
class="smrt-dictation-status-error"
|
|
80
|
+
class:smrt-dictation-status--empty={dictation.state !== 'error'}
|
|
81
|
+
role="alert"
|
|
82
|
+
data-dictation-error={dictation.state === 'error'
|
|
83
|
+
? (dictation.errorCode ?? dictation.errorKind)
|
|
84
|
+
: undefined}
|
|
85
|
+
>
|
|
86
|
+
{#if dictation.state === 'error'}
|
|
87
|
+
{errorText(dictation.errorKind)}
|
|
88
|
+
{/if}
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.smrt-dictation-status {
|
|
94
|
+
display: contents;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.smrt-dictation-status-line,
|
|
98
|
+
.smrt-dictation-status-error {
|
|
99
|
+
margin: 0;
|
|
100
|
+
font: var(--smrt-typography-body-small-font, 0.8125rem / 1.4 sans-serif);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
.smrt-dictation-status-line {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 0.4rem;
|
|
109
|
+
padding: 0.25rem 0;
|
|
110
|
+
color: var(--smrt-color-on-surface, #1a1c1e);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Kept mounted while empty (live regions must exist before they speak),
|
|
114
|
+
but take no room. */
|
|
115
|
+
.smrt-dictation-status--empty {
|
|
116
|
+
position: absolute;
|
|
117
|
+
inline-size: 1px;
|
|
118
|
+
block-size: 1px;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
clip-path: inset(50%);
|
|
121
|
+
padding: 0;
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.smrt-dictation-status-dot {
|
|
126
|
+
inline-size: 8px;
|
|
127
|
+
block-size: 8px;
|
|
128
|
+
border-radius: 50%;
|
|
129
|
+
background: var(--smrt-color-error, #ba1a1a);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.smrt-dictation-status-spinner {
|
|
133
|
+
inline-size: 12px;
|
|
134
|
+
block-size: 12px;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
border: 2px solid var(--smrt-color-outline-variant, #c4c6cf);
|
|
137
|
+
border-block-start-color: var(--smrt-color-primary, #2f5ea8);
|
|
138
|
+
animation: smrt-dictation-spin 0.9s linear infinite;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@keyframes smrt-dictation-spin {
|
|
142
|
+
to {
|
|
143
|
+
transform: rotate(360deg);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@media (prefers-reduced-motion: reduce) {
|
|
148
|
+
.smrt-dictation-status-spinner {
|
|
149
|
+
animation: none;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.smrt-dictation-status-interim {
|
|
154
|
+
color: var(--smrt-color-on-surface-variant, #43474e);
|
|
155
|
+
font-style: italic;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.smrt-dictation-status-error {
|
|
159
|
+
padding: 0.4rem 0.6rem;
|
|
160
|
+
border-radius: var(--smrt-radius-medium, 8px);
|
|
161
|
+
background: var(--smrt-color-error-container, #ffdad6);
|
|
162
|
+
color: var(--smrt-color-on-error-container, #410002);
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Dictation } from './dictation.svelte.js';
|
|
2
|
+
export interface Props {
|
|
3
|
+
/** The dictation session whose listening state and messages this shows. */
|
|
4
|
+
dictation: Dictation;
|
|
5
|
+
/** Extra class names. */
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const DictationStatus: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type DictationStatus = ReturnType<typeof DictationStatus>;
|
|
10
|
+
export default DictationStatus;
|
|
11
|
+
//# sourceMappingURL=DictationStatus.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DictationStatus.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DictationStatus.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,SAAS,EAAsB,MAAM,uBAAuB,CAAC;AAG3E,MAAM,WAAW,KAAK;IACpB,2EAA2E;IAC3E,SAAS,EAAE,SAAS,CAAC;IACrB,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAwED,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -8,6 +8,8 @@ export interface Props extends Omit<HTMLFieldsetAttributes, 'class'> {
|
|
|
8
8
|
description?: string;
|
|
9
9
|
/** Optional error message displayed in alert color. */
|
|
10
10
|
error?: string;
|
|
11
|
+
/** Lays the fields out in one column with a gap between them. */
|
|
12
|
+
stack?: boolean;
|
|
11
13
|
/** CSS class to apply to the fieldset. */
|
|
12
14
|
class?: string;
|
|
13
15
|
/** Form controls and content inside the fieldset. */
|
|
@@ -17,6 +19,7 @@ let {
|
|
|
17
19
|
legend,
|
|
18
20
|
description,
|
|
19
21
|
error,
|
|
22
|
+
stack = false,
|
|
20
23
|
class: className = '',
|
|
21
24
|
children,
|
|
22
25
|
...rest
|
|
@@ -28,10 +31,11 @@ const descriptionId = $derived(
|
|
|
28
31
|
const errorId = $derived(error ? `fieldset-${instanceId}-error` : undefined);
|
|
29
32
|
</script>
|
|
30
33
|
<fieldset class="fieldset {className}" aria-describedby={[descriptionId, errorId].filter(Boolean).join(' ') || undefined} {...rest}>
|
|
31
|
-
<legend>{legend}</legend>{#if description}<p id={descriptionId} class="description">{description}</p>{/if}<div class="content">{@render children()}</div>{#if error}<p id={errorId} class="error" role="alert">{error}</p>{/if}
|
|
34
|
+
<legend>{legend}</legend>{#if description}<p id={descriptionId} class="description">{description}</p>{/if}<div class="content" class:content--stack={stack}>{@render children()}</div>{#if error}<p id={errorId} class="error" role="alert">{error}</p>{/if}
|
|
32
35
|
</fieldset>
|
|
33
36
|
<style>
|
|
34
37
|
.fieldset { margin: 0; padding: var(--smrt-spacing-4); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-medium); }
|
|
35
38
|
legend { padding: 0 var(--smrt-spacing-2); color: var(--smrt-color-on-surface); font: var(--smrt-typography-title-small-font); }
|
|
39
|
+
.content--stack { display: grid; gap: var(--smrt-spacing-4); }
|
|
36
40
|
.description, .error { margin: 0 0 var(--smrt-spacing-3); font: var(--smrt-typography-body-small-font); color: var(--smrt-color-on-surface-variant); } .error { margin: var(--smrt-spacing-3) 0 0; color: var(--smrt-color-error); }
|
|
37
41
|
</style>
|
|
@@ -7,6 +7,8 @@ export interface Props extends Omit<HTMLFieldsetAttributes, 'class'> {
|
|
|
7
7
|
description?: string;
|
|
8
8
|
/** Optional error message displayed in alert color. */
|
|
9
9
|
error?: string;
|
|
10
|
+
/** Lays the fields out in one column with a gap between them. */
|
|
11
|
+
stack?: boolean;
|
|
10
12
|
/** CSS class to apply to the fieldset. */
|
|
11
13
|
class?: string;
|
|
12
14
|
/** Form controls and content inside the fieldset. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Fieldset.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Fieldset.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC;IAClE,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"Fieldset.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Fieldset.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC;IAClE,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAC;CACnB;AA2BD,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FormActionBar — a form's actions (Cancel · Save draft · Publish).
|
|
4
|
+
*
|
|
5
|
+
* Inline under the form on wider screens. On phones (up to 48rem) it is fixed
|
|
6
|
+
* to the bottom of the screen, padded for the safe area, and replaces the
|
|
7
|
+
* shell's phone bottom bar: `AdminShell` sees the `data-form-action-bar`
|
|
8
|
+
* attribute and hides its own bar. Hidden while the on-screen keyboard is open
|
|
9
|
+
* (`:root[data-keyboard-open]`, set by the shell's keyboard watcher). Put the
|
|
10
|
+
* primary action last.
|
|
11
|
+
*/
|
|
12
|
+
import type { Snippet } from 'svelte';
|
|
13
|
+
import { M } from '../../i18n/strings.ui.js';
|
|
14
|
+
import { useI18n } from '../../i18n/use-i18n.js';
|
|
15
|
+
|
|
16
|
+
export interface Props {
|
|
17
|
+
/** Accessible name of the action group (default "Form actions"). */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** Inside a toolbar the page already draws: no rule or margin on wider screens. */
|
|
20
|
+
plain?: boolean;
|
|
21
|
+
/** The action buttons, primary last. */
|
|
22
|
+
children: Snippet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let { label, plain = false, children }: Props = $props();
|
|
26
|
+
|
|
27
|
+
const { t } = useI18n();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
class="form-action-bar"
|
|
32
|
+
class:plain
|
|
33
|
+
role="group"
|
|
34
|
+
aria-label={label ?? t(M['ui.form_action_bar.label'])}
|
|
35
|
+
data-form-action-bar
|
|
36
|
+
data-testid="form-action-bar"
|
|
37
|
+
>
|
|
38
|
+
{@render children()}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.form-action-bar {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
justify-content: flex-end;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: var(--smrt-spacing-3);
|
|
48
|
+
margin-block-start: var(--smrt-spacing-6);
|
|
49
|
+
padding-block-start: var(--smrt-spacing-4);
|
|
50
|
+
border-block-start: 1px solid var(--smrt-color-outline-variant);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.form-action-bar.plain {
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
border: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (max-width: 48rem) {
|
|
60
|
+
.form-action-bar,
|
|
61
|
+
.form-action-bar.plain {
|
|
62
|
+
position: fixed;
|
|
63
|
+
inset: auto 0 0;
|
|
64
|
+
z-index: 12;
|
|
65
|
+
flex-wrap: nowrap;
|
|
66
|
+
gap: var(--smrt-spacing-2);
|
|
67
|
+
margin: 0;
|
|
68
|
+
padding: var(--smrt-spacing-2) var(--smrt-spacing-4)
|
|
69
|
+
calc(var(--smrt-spacing-2) + env(safe-area-inset-bottom));
|
|
70
|
+
border-block-start: 1px solid var(--smrt-color-outline-variant);
|
|
71
|
+
background: var(--smrt-color-surface);
|
|
72
|
+
box-shadow: var(--smrt-elevation-2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.form-action-bar > :global(*) {
|
|
76
|
+
flex: 1 1 0;
|
|
77
|
+
min-block-size: 2.75rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
:global(:root[data-keyboard-open]) .form-action-bar {
|
|
81
|
+
display: none;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormActionBar — a form's actions (Cancel · Save draft · Publish).
|
|
3
|
+
*
|
|
4
|
+
* Inline under the form on wider screens. On phones (up to 48rem) it is fixed
|
|
5
|
+
* to the bottom of the screen, padded for the safe area, and replaces the
|
|
6
|
+
* shell's phone bottom bar: `AdminShell` sees the `data-form-action-bar`
|
|
7
|
+
* attribute and hides its own bar. Hidden while the on-screen keyboard is open
|
|
8
|
+
* (`:root[data-keyboard-open]`, set by the shell's keyboard watcher). Put the
|
|
9
|
+
* primary action last.
|
|
10
|
+
*/
|
|
11
|
+
import type { Snippet } from 'svelte';
|
|
12
|
+
export interface Props {
|
|
13
|
+
/** Accessible name of the action group (default "Form actions"). */
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Inside a toolbar the page already draws: no rule or margin on wider screens. */
|
|
16
|
+
plain?: boolean;
|
|
17
|
+
/** The action buttons, primary last. */
|
|
18
|
+
children: Snippet;
|
|
19
|
+
}
|
|
20
|
+
declare const FormActionBar: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type FormActionBar = ReturnType<typeof FormActionBar>;
|
|
22
|
+
export default FormActionBar;
|
|
23
|
+
//# sourceMappingURL=FormActionBar.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormActionBar.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FormActionBar.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKtC,MAAM,WAAW,KAAK;IACpB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wCAAwC;IACxC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAqBD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -257,6 +257,15 @@ export function getElement(): HTMLInputElement | null {
|
|
|
257
257
|
color: var(--smrt-color-on-surface-variant, #9ca3af);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/* Phones: 16px text so iOS Safari doesn't zoom the page when the field
|
|
261
|
+
gets focus, and a 44px touch target. */
|
|
262
|
+
@media (max-width: 48rem) {
|
|
263
|
+
.input {
|
|
264
|
+
font-size: max(var(--smrt-typography-body-medium-size, 0.875rem), 16px);
|
|
265
|
+
min-height: 2.75rem;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
260
269
|
@media (prefers-reduced-motion: reduce) {
|
|
261
270
|
.input {
|
|
262
271
|
transition: none;
|
|
@@ -20,18 +20,29 @@ function handleChange(event: Event & { currentTarget: HTMLInputElement }) {
|
|
|
20
20
|
onchange?.(event);
|
|
21
21
|
}
|
|
22
22
|
</script>
|
|
23
|
-
<label class="radio" class:disabled={disabled || group.disabled}>
|
|
23
|
+
<label class="radio" class:disabled={disabled || group.disabled} data-smrt-hit-target>
|
|
24
24
|
<input id={resolvedId} type="radio" name={group.name} {value} disabled={disabled || group.disabled} required={group.required}
|
|
25
25
|
checked={group.value === value} onchange={handleChange} {...rest} />
|
|
26
|
-
<span class="radio__mark" aria-hidden="true"></span><span>{label}</span>
|
|
26
|
+
<span class="radio__mark" aria-hidden="true"></span><span class="radio__label">{label}</span>
|
|
27
27
|
</label>
|
|
28
28
|
<style>
|
|
29
|
-
|
|
29
|
+
/* The visible mark stays 18px; a transparent 44x44 ::before hit area (see Checkbox) makes the
|
|
30
|
+
touch target. Mark and label text sit above every hit area, so neighbours' hit areas never
|
|
31
|
+
steal their clicks. Beside other content (a group's other options, text, a link) it keeps to
|
|
32
|
+
the mark's column and grows vertically only. --smrt-control-hit-size resizes it (0px turns it
|
|
33
|
+
off). */
|
|
34
|
+
.radio { --_box: 1.15rem; --_hit: max(var(--smrt-control-hit-size, 2.75rem), var(--_box)); position: relative; display: inline-flex; align-items: center; gap: var(--smrt-spacing-2); cursor: pointer; }
|
|
35
|
+
.radio::before { content: ''; position: absolute; z-index: 0; inset-block-start: 50%; inset-inline-start: calc(var(--_box) / 2); inline-size: var(--_hit); block-size: var(--_hit); margin-block-start: calc(var(--_hit) / -2); margin-inline-start: calc(var(--_hit) / -2); }
|
|
36
|
+
.radio:not(:only-child)::before { inline-size: var(--_box); margin-inline-start: calc(var(--_box) / -2); }
|
|
30
37
|
.radio.disabled { opacity: .5; cursor: not-allowed; }
|
|
31
38
|
input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
|
32
|
-
.radio__mark { width: 1.15rem; height: 1.15rem; border: 2px solid var(--smrt-color-outline); border-radius: 50%; display: grid; place-items: center; }
|
|
39
|
+
.radio__mark { position: relative; z-index: 1; width: 1.15rem; height: 1.15rem; border: 2px solid var(--smrt-color-outline); border-radius: 50%; display: grid; place-items: center; }
|
|
40
|
+
.radio__label { position: relative; z-index: 1; }
|
|
33
41
|
.radio__mark::after { content: ''; width: .55rem; height: .55rem; border-radius: 50%; background: var(--smrt-color-primary); transform: scale(0); transition: transform var(--smrt-duration-short2); }
|
|
34
42
|
input:checked + .radio__mark { border-color: var(--smrt-color-primary); }
|
|
35
43
|
input:checked + .radio__mark::after { transform: scale(1); }
|
|
36
44
|
input:focus-visible + .radio__mark { outline: 2px solid var(--smrt-color-primary); outline-offset: 3px; }
|
|
45
|
+
|
|
46
|
+
/* Table cells: grow vertically only, never into a neighbouring cell. */
|
|
47
|
+
:global(:is(td, th)) .radio::before { inline-size: var(--_box); margin-inline-start: calc(var(--_box) / -2); }
|
|
37
48
|
</style>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SearchInput — the search box of a list's filter bar.
|
|
4
|
+
*
|
|
5
|
+
* It searches as you type (`debounceMs` after the last key; Enter searches at
|
|
6
|
+
* once) and reports the trimmed, single-line text through `onsearch`. The page
|
|
7
|
+
* keeps the applied search (a `?q=` URL parameter, say) and passes it back as
|
|
8
|
+
* `value`; the box follows it, so Back, a filter link or an agent's search
|
|
9
|
+
* update the text, and a search the box itself sent never undoes newer typing.
|
|
10
|
+
* The × clears it. The label ("Search") is read by screen readers; the
|
|
11
|
+
* placeholder says what the search matches. The field is a 44px target and
|
|
12
|
+
* 16px on phones so iOS does not zoom when it gets focus.
|
|
13
|
+
*/
|
|
14
|
+
import { onDestroy } from 'svelte';
|
|
15
|
+
import Input from './Input.svelte';
|
|
16
|
+
|
|
17
|
+
export interface Props {
|
|
18
|
+
/** The search the list is showing (from the URL or the page's state). */
|
|
19
|
+
value: string;
|
|
20
|
+
/** Called with the trimmed, single-line text (`''` to clear). */
|
|
21
|
+
onsearch: (query: string) => void | Promise<void>;
|
|
22
|
+
/** Placeholder text: what the search matches. */
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
/** Accessible name of the field. */
|
|
25
|
+
label?: string;
|
|
26
|
+
/** Accessible name of the clear button. */
|
|
27
|
+
clearLabel?: string;
|
|
28
|
+
/** Pause after the last key before searching, in milliseconds. */
|
|
29
|
+
debounceMs?: number;
|
|
30
|
+
/** Form field name. */
|
|
31
|
+
name?: string;
|
|
32
|
+
/** Additional CSS class names on the wrapper. */
|
|
33
|
+
class?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
value,
|
|
38
|
+
onsearch,
|
|
39
|
+
placeholder = 'Search',
|
|
40
|
+
label = 'Search',
|
|
41
|
+
clearLabel = 'Clear search',
|
|
42
|
+
debounceMs = 300,
|
|
43
|
+
name = 'q',
|
|
44
|
+
class: className = '',
|
|
45
|
+
}: Props = $props();
|
|
46
|
+
|
|
47
|
+
const instanceId = $props.id();
|
|
48
|
+
const inputId = `smrt-search-${instanceId}`;
|
|
49
|
+
let text = $state('');
|
|
50
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
51
|
+
/** The search this box last sent; its own round trip must not undo newer typing. */
|
|
52
|
+
let sent: string | null = null;
|
|
53
|
+
|
|
54
|
+
// Follow the applied search (back/forward, a filter link, an agent's search).
|
|
55
|
+
$effect(() => {
|
|
56
|
+
const next = value;
|
|
57
|
+
if (sent !== null && next === sent) {
|
|
58
|
+
sent = null;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Any other applied value supersedes what this box last sent (the host may
|
|
62
|
+
// have normalized it, or the search failed): never let a stale `sent`
|
|
63
|
+
// swallow a later value.
|
|
64
|
+
sent = null;
|
|
65
|
+
text = next;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function normalized(raw: string): string {
|
|
69
|
+
return raw.replace(/\s+/g, ' ').trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cancel() {
|
|
73
|
+
if (timer) clearTimeout(timer);
|
|
74
|
+
timer = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function search(raw: string) {
|
|
78
|
+
cancel();
|
|
79
|
+
const query = normalized(raw);
|
|
80
|
+
if (query === normalized(value)) return;
|
|
81
|
+
sent = query;
|
|
82
|
+
// A failed search is not the applied one.
|
|
83
|
+
const forget = () => {
|
|
84
|
+
if (sent === query) sent = null;
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
void Promise.resolve(onsearch(query)).catch(forget);
|
|
88
|
+
} catch {
|
|
89
|
+
forget();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleInput() {
|
|
94
|
+
cancel();
|
|
95
|
+
timer = setTimeout(() => search(text), debounceMs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Enter searches at once. The box is not a <form>: inside a host form a
|
|
99
|
+
// nested form is invalid and Enter would submit the outer one.
|
|
100
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
101
|
+
if (event.key !== 'Enter' || event.isComposing) return;
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
search(text);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clear() {
|
|
107
|
+
text = '';
|
|
108
|
+
search('');
|
|
109
|
+
document.getElementById(inputId)?.focus();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onDestroy(cancel);
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<div class="search-input {className}" role="search">
|
|
116
|
+
<label class="search-input__label" for={inputId}>{label}</label>
|
|
117
|
+
<span class="search-input__icon" aria-hidden="true">
|
|
118
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
|
|
119
|
+
</span>
|
|
120
|
+
<Input
|
|
121
|
+
id={inputId}
|
|
122
|
+
type="search"
|
|
123
|
+
{name}
|
|
124
|
+
{placeholder}
|
|
125
|
+
enterkeyhint="search"
|
|
126
|
+
autocomplete="off"
|
|
127
|
+
class="search-input__field"
|
|
128
|
+
bind:value={text}
|
|
129
|
+
oninput={handleInput}
|
|
130
|
+
onkeydown={handleKeydown}
|
|
131
|
+
/>
|
|
132
|
+
{#if text}
|
|
133
|
+
<button type="button" class="search-input__clear" aria-label={clearLabel} onclick={clear}>
|
|
134
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
135
|
+
</button>
|
|
136
|
+
{/if}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<style>
|
|
140
|
+
.search-input {
|
|
141
|
+
position: relative;
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
flex: 1 1 16rem;
|
|
145
|
+
max-inline-size: 28rem;
|
|
146
|
+
min-inline-size: 0;
|
|
147
|
+
}
|
|
148
|
+
.search-input__label {
|
|
149
|
+
position: absolute;
|
|
150
|
+
inline-size: 1px;
|
|
151
|
+
block-size: 1px;
|
|
152
|
+
margin: -1px;
|
|
153
|
+
padding: 0;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
clip: rect(0 0 0 0);
|
|
156
|
+
white-space: nowrap;
|
|
157
|
+
border: 0;
|
|
158
|
+
}
|
|
159
|
+
.search-input :global(.search-input__field) {
|
|
160
|
+
inline-size: 100%;
|
|
161
|
+
min-block-size: 2.75rem;
|
|
162
|
+
padding-inline: 2.4rem 2.75rem;
|
|
163
|
+
}
|
|
164
|
+
/* The browser's own clear button would sit on top of ours. */
|
|
165
|
+
.search-input :global(.search-input__field::-webkit-search-cancel-button) {
|
|
166
|
+
-webkit-appearance: none;
|
|
167
|
+
appearance: none;
|
|
168
|
+
}
|
|
169
|
+
.search-input__icon {
|
|
170
|
+
position: absolute;
|
|
171
|
+
inset-inline-start: 0.8rem;
|
|
172
|
+
z-index: 1;
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
color: var(--smrt-color-on-surface-variant);
|
|
175
|
+
pointer-events: none;
|
|
176
|
+
}
|
|
177
|
+
.search-input__clear {
|
|
178
|
+
position: absolute;
|
|
179
|
+
inset-inline-end: 0;
|
|
180
|
+
z-index: 1;
|
|
181
|
+
display: inline-flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
inline-size: 2.75rem;
|
|
185
|
+
block-size: 2.75rem;
|
|
186
|
+
padding: 0;
|
|
187
|
+
border: none;
|
|
188
|
+
border-radius: var(--smrt-radius-full, 999px);
|
|
189
|
+
background: transparent;
|
|
190
|
+
color: var(--smrt-color-on-surface-variant);
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
}
|
|
193
|
+
.search-input__clear:hover {
|
|
194
|
+
color: var(--smrt-color-on-surface);
|
|
195
|
+
}
|
|
196
|
+
.search-input__clear:focus-visible {
|
|
197
|
+
outline: 2px solid var(--smrt-color-primary);
|
|
198
|
+
outline-offset: -2px;
|
|
199
|
+
}
|
|
200
|
+
/* Phones: full width. */
|
|
201
|
+
@media (max-width: 48rem) {
|
|
202
|
+
.search-input {
|
|
203
|
+
flex-basis: 100%;
|
|
204
|
+
max-inline-size: none;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Props {
|
|
2
|
+
/** The search the list is showing (from the URL or the page's state). */
|
|
3
|
+
value: string;
|
|
4
|
+
/** Called with the trimmed, single-line text (`''` to clear). */
|
|
5
|
+
onsearch: (query: string) => void | Promise<void>;
|
|
6
|
+
/** Placeholder text: what the search matches. */
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Accessible name of the field. */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Accessible name of the clear button. */
|
|
11
|
+
clearLabel?: string;
|
|
12
|
+
/** Pause after the last key before searching, in milliseconds. */
|
|
13
|
+
debounceMs?: number;
|
|
14
|
+
/** Form field name. */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** Additional CSS class names on the wrapper. */
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const SearchInput: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type SearchInput = ReturnType<typeof SearchInput>;
|
|
21
|
+
export default SearchInput;
|
|
22
|
+
//# sourceMappingURL=SearchInput.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/SearchInput.svelte.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,KAAK;IACpB,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuGD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -190,6 +190,15 @@ export function getElement(): HTMLSelectElement | null {
|
|
|
190
190
|
opacity: 0.7;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/* Phones: 16px text so iOS Safari doesn't zoom the page when the field
|
|
194
|
+
gets focus, and a 44px touch target. */
|
|
195
|
+
@media (max-width: 48rem) {
|
|
196
|
+
.select {
|
|
197
|
+
font-size: max(var(--smrt-typography-body-medium-size, 0.875rem), 16px);
|
|
198
|
+
min-height: 2.75rem;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
193
202
|
@media (prefers-reduced-motion: reduce) {
|
|
194
203
|
.select {
|
|
195
204
|
transition: none;
|