@dorsk/tsumikit 0.45.0 → 0.47.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/README.md +14 -1
- package/dist/components/molecules/EmojiPicker.svelte +256 -0
- package/dist/components/molecules/EmojiPicker.svelte.d.ts +21 -0
- package/dist/components/molecules/ThemePicker.svelte +129 -10
- package/dist/components/molecules/ThemePicker.svelte.d.ts +7 -0
- package/dist/emoji.d.ts +18 -0
- package/dist/emoji.js +286 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/stores/theme.svelte.d.ts +23 -0
- package/dist/stores/theme.svelte.js +78 -6
- package/dist/theme-mode.d.ts +33 -0
- package/dist/theme-mode.js +45 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,18 @@ import { Button, Field, Input, Modal, ThemePicker } from '@dorsk/tsumikit';
|
|
|
64
64
|
- `<ThemePicker />` and `<FontScalePicker />` wire the stores to the UI. Theme
|
|
65
65
|
is persisted to `localStorage` and applied with no flash (head snippet in
|
|
66
66
|
`app.html`) and updates the mobile `<meta name="theme-color">`.
|
|
67
|
+
- The store keeps a **preference**, not just an id: one remembered light theme,
|
|
68
|
+
one remembered dark theme, and which of the two is pinned — or `auto`, which
|
|
69
|
+
follows `prefers-color-scheme` and repaints when the system flips.
|
|
70
|
+
`theme.choose(id | 'auto')`, `theme.mode`, `theme.pref`, `theme.resolved`,
|
|
71
|
+
`theme.hydrate(pref)` (replay a server-side preference) and
|
|
72
|
+
`theme.onchange = (pref) => …` (mirror it back). `theme.set(id)` and
|
|
73
|
+
`theme.current` are unchanged. `<ThemePicker auto />` adds the "Auto" row;
|
|
74
|
+
its labels (`autoLabel`, `autoHelp`, `lightLabel`, `darkLabel`) are props.
|
|
75
|
+
- **Storage format (0.47):** `localStorage['tsumikit-theme']` now holds
|
|
76
|
+
`{"mode","light","dark"}` instead of a bare theme id. A bare id left by an
|
|
77
|
+
older version still loads — it pins its own slot — but a downgrade will not
|
|
78
|
+
read the new blob and falls back to the default theme.
|
|
67
79
|
|
|
68
80
|
### Stylesheets
|
|
69
81
|
|
|
@@ -164,7 +176,8 @@ Tabs, RadioGroup (`variant="rows"`: bordered rows, per-option `note`/`descriptio
|
|
|
164
176
|
`action(option)` trailing control that never toggles, `below(option)` inline panel),
|
|
165
177
|
Tooltip, Accordion, CopyButton, FileButton,
|
|
166
178
|
Dropzone, CodeBlock, Callout, EmptyState, ConfirmModal, Pagination, Toaster,
|
|
167
|
-
ThemePicker (popover grid of 2×2 palette swatches: bg · surface · text · accent per theme),
|
|
179
|
+
ThemePicker (popover grid of 2×2 palette swatches: bg · surface · text · accent per theme),
|
|
180
|
+
EmojiPicker (popover with a searchable EN+FR glyph catalogue, grouped tabs, roving-tabindex grid; pass `groups` to swap the catalogue), FontScalePicker (popover with a stepped slider across the five text sizes), SectionHeader, KeyValue, LoadMore,
|
|
168
181
|
GitRef (branch chip + PR link tinted by state + `+N −N` diff; `collapse`
|
|
169
182
|
auto/never/glyph degrades to icons inside a narrow `.cq` container),
|
|
170
183
|
CapBar (consumption track with a draggable, keyboard-steppable cap handle that
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Emoji chooser: a Popover holding a search box, a tab strip of catalogue
|
|
3
|
+
// groups and a grid of glyphs. Desktop browsers have no emoji keyboard, so
|
|
4
|
+
// this is the click-only path next to a plain text field.
|
|
5
|
+
import Input from '../atoms/Input.svelte';
|
|
6
|
+
import Popover from './Popover.svelte';
|
|
7
|
+
import { type EmojiGroup, EMOJI_GROUPS, searchEmoji } from '../../emoji';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
value = '',
|
|
11
|
+
onselect,
|
|
12
|
+
groups = EMOJI_GROUPS,
|
|
13
|
+
columns = 8,
|
|
14
|
+
label = 'Choose an emoji',
|
|
15
|
+
searchLabel = 'Search an emoji…',
|
|
16
|
+
emptyLabel = 'No emoji matches.',
|
|
17
|
+
placement = 'bottom-start',
|
|
18
|
+
triggerClass = '',
|
|
19
|
+
disabled = false,
|
|
20
|
+
class: klass = '',
|
|
21
|
+
style: styleProp = '',
|
|
22
|
+
}: {
|
|
23
|
+
/** The current glyph, highlighted in the grid. */
|
|
24
|
+
value?: string;
|
|
25
|
+
onselect: (emoji: string) => void;
|
|
26
|
+
/** Catalogue to browse; defaults to the kit's EN+FR keyword set. */
|
|
27
|
+
groups?: readonly EmojiGroup[];
|
|
28
|
+
columns?: number;
|
|
29
|
+
/** Accessible name of the trigger and of the glyph grid. */
|
|
30
|
+
label?: string;
|
|
31
|
+
searchLabel?: string;
|
|
32
|
+
emptyLabel?: string;
|
|
33
|
+
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
34
|
+
triggerClass?: string;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
class?: string;
|
|
37
|
+
style?: string;
|
|
38
|
+
} = $props();
|
|
39
|
+
|
|
40
|
+
const uid = `emoji-${Math.random().toString(36).slice(2, 8)}`;
|
|
41
|
+
const tabId = (id: string) => `${uid}-tab-${id}`;
|
|
42
|
+
const panelId = `${uid}-panel`;
|
|
43
|
+
|
|
44
|
+
let query = $state('');
|
|
45
|
+
let groupId = $state<string | null>(null);
|
|
46
|
+
let cursor = $state(0);
|
|
47
|
+
let gridEl = $state<HTMLDivElement | null>(null);
|
|
48
|
+
|
|
49
|
+
const current = $derived(groups.find((g) => g.id === groupId) ?? groups[0]);
|
|
50
|
+
const searching = $derived(query.trim().length > 0);
|
|
51
|
+
const shown = $derived(searching ? searchEmoji(query, groups) : (current?.entries ?? []));
|
|
52
|
+
const active = $derived(Math.min(cursor, Math.max(shown.length - 1, 0)));
|
|
53
|
+
const picked = $derived(value.trim());
|
|
54
|
+
|
|
55
|
+
function focusCell(i: number) {
|
|
56
|
+
const cells = gridEl?.querySelectorAll<HTMLElement>('[role="option"]');
|
|
57
|
+
if (!cells?.length) return;
|
|
58
|
+
cursor = Math.max(0, Math.min(i, cells.length - 1));
|
|
59
|
+
cells[cursor].focus();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pick(emoji: string, close: () => void) {
|
|
63
|
+
onselect(emoji);
|
|
64
|
+
query = '';
|
|
65
|
+
close();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function onGridClick(e: MouseEvent, close: () => void) {
|
|
69
|
+
const cell = (e.target as HTMLElement | null)?.closest<HTMLElement>('[role="option"]');
|
|
70
|
+
if (cell?.dataset.emoji) pick(cell.dataset.emoji, close);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onGridKeydown(e: KeyboardEvent, close: () => void) {
|
|
74
|
+
const keys: Record<string, number> = {
|
|
75
|
+
ArrowRight: active + 1,
|
|
76
|
+
ArrowLeft: active - 1,
|
|
77
|
+
ArrowDown: active + columns,
|
|
78
|
+
ArrowUp: active - columns,
|
|
79
|
+
Home: 0,
|
|
80
|
+
End: shown.length - 1,
|
|
81
|
+
};
|
|
82
|
+
if (e.key in keys) {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
focusCell(keys[e.key]);
|
|
85
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const emoji = shown[active]?.emoji;
|
|
88
|
+
if (emoji) pick(emoji, close);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onTabsKeydown(e: KeyboardEvent) {
|
|
93
|
+
const i = groups.findIndex((g) => g.id === current?.id);
|
|
94
|
+
const next = e.key === 'ArrowRight' ? i + 1 : e.key === 'ArrowLeft' ? i - 1 : null;
|
|
95
|
+
if (next === null) return;
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
const g = groups[(next + groups.length) % groups.length];
|
|
98
|
+
groupId = g.id;
|
|
99
|
+
cursor = 0;
|
|
100
|
+
document.getElementById(tabId(g.id))?.focus();
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<Popover
|
|
105
|
+
{label}
|
|
106
|
+
{placement}
|
|
107
|
+
{triggerClass}
|
|
108
|
+
{disabled}
|
|
109
|
+
class={klass}
|
|
110
|
+
style={styleProp}
|
|
111
|
+
control
|
|
112
|
+
role="dialog"
|
|
113
|
+
onopen={() => {
|
|
114
|
+
query = '';
|
|
115
|
+
cursor = 0;
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{#snippet trigger()}
|
|
119
|
+
<span class="emoji-trigger" aria-hidden="true">{picked || '🙂'}</span>
|
|
120
|
+
{/snippet}
|
|
121
|
+
|
|
122
|
+
{#snippet children({ close }: { close: () => void })}
|
|
123
|
+
<div class="picker">
|
|
124
|
+
<Input
|
|
125
|
+
bind:value={query}
|
|
126
|
+
placeholder={searchLabel}
|
|
127
|
+
aria-label={searchLabel}
|
|
128
|
+
size="sm"
|
|
129
|
+
clearable
|
|
130
|
+
onclear={() => (query = '')}
|
|
131
|
+
style="width:100%"
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
{#if !searching}
|
|
135
|
+
<div class="tabs" role="tablist" tabindex={-1} aria-label={label} onkeydown={onTabsKeydown}>
|
|
136
|
+
{#each groups as g (g.id)}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
role="tab"
|
|
140
|
+
id={tabId(g.id)}
|
|
141
|
+
class="tab"
|
|
142
|
+
class:on={g.id === current?.id}
|
|
143
|
+
aria-selected={g.id === current?.id}
|
|
144
|
+
aria-controls={panelId}
|
|
145
|
+
aria-label={g.label}
|
|
146
|
+
tabindex={g.id === current?.id ? 0 : -1}
|
|
147
|
+
onclick={() => {
|
|
148
|
+
groupId = g.id;
|
|
149
|
+
cursor = 0;
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
<span aria-hidden="true">{g.icon}</span>
|
|
153
|
+
</button>
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
|
|
158
|
+
<div
|
|
159
|
+
id={panelId}
|
|
160
|
+
role={searching ? undefined : 'tabpanel'}
|
|
161
|
+
aria-labelledby={searching || !current ? undefined : tabId(current.id)}
|
|
162
|
+
>
|
|
163
|
+
{#if shown.length}
|
|
164
|
+
<!-- Options are not buttons: `option` is only valid as a listbox child,
|
|
165
|
+
and a roving tabindex keeps the grid to one tab stop. -->
|
|
166
|
+
<div
|
|
167
|
+
bind:this={gridEl}
|
|
168
|
+
class="grid"
|
|
169
|
+
role="listbox"
|
|
170
|
+
tabindex={-1}
|
|
171
|
+
aria-label={label}
|
|
172
|
+
style:--emoji-columns={columns}
|
|
173
|
+
onclick={(e) => onGridClick(e, close)}
|
|
174
|
+
onkeydown={(e) => onGridKeydown(e, close)}
|
|
175
|
+
>
|
|
176
|
+
{#each shown as x, i (x.emoji)}
|
|
177
|
+
<div
|
|
178
|
+
role="option"
|
|
179
|
+
class="cell"
|
|
180
|
+
class:on={x.emoji === picked}
|
|
181
|
+
aria-selected={x.emoji === picked}
|
|
182
|
+
data-emoji={x.emoji}
|
|
183
|
+
title={x.keys}
|
|
184
|
+
tabindex={i === active ? 0 : -1}
|
|
185
|
+
>
|
|
186
|
+
{x.emoji}
|
|
187
|
+
</div>
|
|
188
|
+
{/each}
|
|
189
|
+
</div>
|
|
190
|
+
{:else}
|
|
191
|
+
<p class="empty">{emptyLabel}</p>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
{/snippet}
|
|
196
|
+
</Popover>
|
|
197
|
+
|
|
198
|
+
<style>
|
|
199
|
+
.emoji-trigger {
|
|
200
|
+
font-size: var(--fs-md);
|
|
201
|
+
line-height: 1;
|
|
202
|
+
}
|
|
203
|
+
.picker {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
gap: var(--sp-2);
|
|
207
|
+
width: min(20rem, calc(100vw - var(--sp-6)));
|
|
208
|
+
}
|
|
209
|
+
.tabs {
|
|
210
|
+
display: flex;
|
|
211
|
+
gap: var(--sp-1);
|
|
212
|
+
}
|
|
213
|
+
.tab,
|
|
214
|
+
.cell {
|
|
215
|
+
display: flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: center;
|
|
218
|
+
padding: var(--sp-1);
|
|
219
|
+
border: 1px solid transparent;
|
|
220
|
+
border-radius: var(--r-sm);
|
|
221
|
+
background: none;
|
|
222
|
+
color: inherit;
|
|
223
|
+
font-size: var(--fs-lg);
|
|
224
|
+
line-height: 1;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
}
|
|
227
|
+
.tab.on,
|
|
228
|
+
.cell.on {
|
|
229
|
+
border-color: var(--accent);
|
|
230
|
+
background: var(--bg-elevated-2);
|
|
231
|
+
}
|
|
232
|
+
.tab:hover,
|
|
233
|
+
.cell:hover,
|
|
234
|
+
.tab:focus-visible,
|
|
235
|
+
.cell:focus-visible {
|
|
236
|
+
background: var(--bg-elevated-2);
|
|
237
|
+
}
|
|
238
|
+
.tab:focus-visible,
|
|
239
|
+
.cell:focus-visible {
|
|
240
|
+
outline: var(--focus-ring);
|
|
241
|
+
outline-offset: var(--focus-ring-offset);
|
|
242
|
+
}
|
|
243
|
+
.grid {
|
|
244
|
+
display: grid;
|
|
245
|
+
grid-template-columns: repeat(var(--emoji-columns), 1fr);
|
|
246
|
+
gap: var(--sp-1);
|
|
247
|
+
max-height: 14rem;
|
|
248
|
+
overflow-y: auto;
|
|
249
|
+
}
|
|
250
|
+
.empty {
|
|
251
|
+
margin: var(--sp-2) 0;
|
|
252
|
+
color: var(--text-muted);
|
|
253
|
+
font-size: var(--fs-xs);
|
|
254
|
+
text-align: center;
|
|
255
|
+
}
|
|
256
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type EmojiGroup } from '../../emoji';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** The current glyph, highlighted in the grid. */
|
|
4
|
+
value?: string;
|
|
5
|
+
onselect: (emoji: string) => void;
|
|
6
|
+
/** Catalogue to browse; defaults to the kit's EN+FR keyword set. */
|
|
7
|
+
groups?: readonly EmojiGroup[];
|
|
8
|
+
columns?: number;
|
|
9
|
+
/** Accessible name of the trigger and of the glyph grid. */
|
|
10
|
+
label?: string;
|
|
11
|
+
searchLabel?: string;
|
|
12
|
+
emptyLabel?: string;
|
|
13
|
+
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
14
|
+
triggerClass?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
class?: string;
|
|
17
|
+
style?: string;
|
|
18
|
+
};
|
|
19
|
+
declare const EmojiPicker: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
20
|
+
type EmojiPicker = ReturnType<typeof EmojiPicker>;
|
|
21
|
+
export default EmojiPicker;
|
|
@@ -7,17 +7,45 @@
|
|
|
7
7
|
// has no [data-theme] block, so its swatch carries the :root values.
|
|
8
8
|
import Popover from './Popover.svelte';
|
|
9
9
|
import { type ThemeDef, theme } from '../../stores/theme.svelte';
|
|
10
|
+
import { AUTO_THEME } from '../../theme-mode';
|
|
10
11
|
|
|
11
|
-
let {
|
|
12
|
+
let {
|
|
13
|
+
auto = false,
|
|
14
|
+
autoLabel = 'Auto',
|
|
15
|
+
autoHelp = 'Follow the system light/dark setting',
|
|
16
|
+
lightLabel = 'Light',
|
|
17
|
+
darkLabel = 'Dark',
|
|
18
|
+
class: klass = '',
|
|
19
|
+
}: {
|
|
20
|
+
/** Offer an "auto" row that follows `prefers-color-scheme`, remembering
|
|
21
|
+
* one light and one dark theme. */
|
|
22
|
+
auto?: boolean;
|
|
23
|
+
autoLabel?: string;
|
|
24
|
+
autoHelp?: string;
|
|
25
|
+
lightLabel?: string;
|
|
26
|
+
darkLabel?: string;
|
|
27
|
+
class?: string;
|
|
28
|
+
} = $props();
|
|
12
29
|
|
|
13
30
|
let hovered = $state<ThemeDef | null>(null);
|
|
31
|
+
let hoveredAuto = $state(false);
|
|
32
|
+
const isAuto = $derived(auto && theme.mode === AUTO_THEME);
|
|
14
33
|
const shown = $derived(hovered ?? theme.option);
|
|
15
34
|
const groups = $derived(
|
|
16
35
|
(['light', 'dark'] as const).map((mode) => ({
|
|
17
36
|
mode,
|
|
18
|
-
|
|
37
|
+
label: mode === 'light' ? lightLabel : darkLabel,
|
|
38
|
+
themes: theme.all.filter((t) => t.mode === mode),
|
|
19
39
|
}))
|
|
20
40
|
);
|
|
41
|
+
const named = (id: string) => theme.all.find((t) => t.id === id)?.label ?? id;
|
|
42
|
+
const autoCaption = $derived(`${autoLabel} · ${named(theme.pref.light)} / ${named(theme.pref.dark)}`);
|
|
43
|
+
const title = $derived(isAuto ? `${autoLabel} · ${theme.label}` : `Theme: ${theme.label}`);
|
|
44
|
+
const caption = $derived(
|
|
45
|
+
hoveredAuto || (isAuto && !hovered)
|
|
46
|
+
? autoCaption
|
|
47
|
+
: `${shown.icon ?? theme.fallbackIcon} ${shown.label}`
|
|
48
|
+
);
|
|
21
49
|
</script>
|
|
22
50
|
|
|
23
51
|
{#snippet swatch(id: string)}
|
|
@@ -26,18 +54,19 @@
|
|
|
26
54
|
</span>
|
|
27
55
|
{/snippet}
|
|
28
56
|
|
|
29
|
-
<Popover label=
|
|
30
|
-
{#snippet trigger()}<span class="trigger" data-tsu="ThemePicker" title
|
|
57
|
+
<Popover label={title} placement="bottom-end" triggerClass={klass} box="md">
|
|
58
|
+
{#snippet trigger()}<span class="trigger" data-tsu="ThemePicker" {title}>{@render swatch(theme.current)}{#if isAuto}<span class="auto-dot" aria-hidden="true">◐</span>{/if}</span>{/snippet}
|
|
31
59
|
<div class="panel">
|
|
32
60
|
{#each groups as g (g.mode)}
|
|
33
|
-
<div class="group-label">{g.
|
|
34
|
-
<div class="grid" role="group" aria-label="{g.
|
|
61
|
+
<div class="group-label">{g.label}</div>
|
|
62
|
+
<div class="grid" role="group" aria-label="{g.label} themes">
|
|
35
63
|
{#each g.themes as t (t.id)}
|
|
36
64
|
<button
|
|
37
65
|
type="button"
|
|
38
66
|
class="cell"
|
|
39
|
-
class:current={t.id === theme.current}
|
|
40
|
-
|
|
67
|
+
class:current={!isAuto && t.id === theme.current}
|
|
68
|
+
class:remembered={isAuto && t.id === theme.pref[g.mode]}
|
|
69
|
+
aria-pressed={!isAuto && t.id === theme.current}
|
|
41
70
|
aria-label={t.label}
|
|
42
71
|
title="{t.icon ?? theme.fallbackIcon} {t.label}"
|
|
43
72
|
onclick={() => theme.set(t.id)}
|
|
@@ -51,14 +80,50 @@
|
|
|
51
80
|
{/each}
|
|
52
81
|
</div>
|
|
53
82
|
{/each}
|
|
54
|
-
|
|
83
|
+
{#if auto}
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
class="auto"
|
|
87
|
+
class:current={isAuto}
|
|
88
|
+
aria-pressed={isAuto}
|
|
89
|
+
title={autoCaption}
|
|
90
|
+
onclick={() => theme.choose(AUTO_THEME)}
|
|
91
|
+
onpointerenter={() => (hoveredAuto = true)}
|
|
92
|
+
onpointerleave={() => (hoveredAuto = false)}
|
|
93
|
+
onfocus={() => (hoveredAuto = true)}
|
|
94
|
+
onblur={() => (hoveredAuto = false)}
|
|
95
|
+
>
|
|
96
|
+
<span class="auto-glyph" aria-hidden="true">◐</span>
|
|
97
|
+
<span class="auto-text">
|
|
98
|
+
<span class="auto-name">{autoLabel}</span>
|
|
99
|
+
<span class="auto-help">{autoHelp}</span>
|
|
100
|
+
</span>
|
|
101
|
+
<span class="auto-pair" aria-hidden="true">
|
|
102
|
+
{@render swatch(theme.pref.light)}
|
|
103
|
+
<span class="slash">/</span>
|
|
104
|
+
{@render swatch(theme.pref.dark)}
|
|
105
|
+
</span>
|
|
106
|
+
</button>
|
|
107
|
+
{/if}
|
|
108
|
+
<div class="caption" aria-live="polite">{caption}</div>
|
|
55
109
|
</div>
|
|
56
110
|
</Popover>
|
|
57
111
|
|
|
58
112
|
<style>
|
|
59
113
|
.trigger {
|
|
114
|
+
position: relative;
|
|
60
115
|
display: inline-flex;
|
|
61
116
|
}
|
|
117
|
+
.auto-dot {
|
|
118
|
+
position: absolute;
|
|
119
|
+
right: -0.35rem;
|
|
120
|
+
bottom: -0.35rem;
|
|
121
|
+
border-radius: 50%;
|
|
122
|
+
background: var(--bg);
|
|
123
|
+
color: var(--text-muted);
|
|
124
|
+
font-size: var(--fs-xs);
|
|
125
|
+
line-height: 1;
|
|
126
|
+
}
|
|
62
127
|
.swatch {
|
|
63
128
|
display: grid;
|
|
64
129
|
grid-template-columns: 1fr 1fr;
|
|
@@ -123,14 +188,68 @@
|
|
|
123
188
|
.cell.current {
|
|
124
189
|
border-color: var(--accent);
|
|
125
190
|
}
|
|
126
|
-
.cell
|
|
191
|
+
.cell.remembered {
|
|
192
|
+
border-color: var(--accent);
|
|
193
|
+
border-style: dashed;
|
|
194
|
+
}
|
|
195
|
+
.cell:focus-visible,
|
|
196
|
+
.auto:focus-visible {
|
|
127
197
|
outline: 2px solid var(--accent);
|
|
128
198
|
outline-offset: 1px;
|
|
129
199
|
}
|
|
200
|
+
.auto {
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: var(--sp-2);
|
|
204
|
+
width: 100%;
|
|
205
|
+
margin-top: var(--sp-2);
|
|
206
|
+
padding: var(--sp-1) var(--sp-2);
|
|
207
|
+
border: 2px solid transparent;
|
|
208
|
+
border-top: 1px solid var(--border);
|
|
209
|
+
border-radius: var(--r-md);
|
|
210
|
+
background: none;
|
|
211
|
+
color: inherit;
|
|
212
|
+
text-align: left;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
}
|
|
215
|
+
.auto:hover {
|
|
216
|
+
background: var(--bg-elevated-2);
|
|
217
|
+
}
|
|
218
|
+
.auto.current {
|
|
219
|
+
border-color: var(--accent);
|
|
220
|
+
}
|
|
221
|
+
.auto-glyph {
|
|
222
|
+
font-size: var(--fs-md);
|
|
223
|
+
line-height: 1;
|
|
224
|
+
}
|
|
225
|
+
.auto-text {
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
flex: 1 1 auto;
|
|
229
|
+
min-width: 0;
|
|
230
|
+
}
|
|
231
|
+
.auto-name {
|
|
232
|
+
font-size: var(--fs-sm);
|
|
233
|
+
}
|
|
234
|
+
.auto-help {
|
|
235
|
+
font-size: var(--fs-xs);
|
|
236
|
+
color: var(--text-faint);
|
|
237
|
+
white-space: normal;
|
|
238
|
+
}
|
|
239
|
+
.auto-pair {
|
|
240
|
+
display: inline-flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: var(--sp-1);
|
|
243
|
+
}
|
|
244
|
+
.slash {
|
|
245
|
+
color: var(--text-faint);
|
|
246
|
+
font-size: var(--fs-xs);
|
|
247
|
+
}
|
|
130
248
|
.caption {
|
|
131
249
|
margin-top: var(--sp-2);
|
|
132
250
|
text-align: center;
|
|
133
251
|
font-size: var(--fs-sm);
|
|
134
252
|
color: var(--text-muted);
|
|
253
|
+
white-space: normal;
|
|
135
254
|
}
|
|
136
255
|
</style>
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
type $$ComponentProps = {
|
|
2
|
+
/** Offer an "auto" row that follows `prefers-color-scheme`, remembering
|
|
3
|
+
* one light and one dark theme. */
|
|
4
|
+
auto?: boolean;
|
|
5
|
+
autoLabel?: string;
|
|
6
|
+
autoHelp?: string;
|
|
7
|
+
lightLabel?: string;
|
|
8
|
+
darkLabel?: string;
|
|
2
9
|
class?: string;
|
|
3
10
|
};
|
|
4
11
|
declare const ThemePicker: import("svelte").Component<$$ComponentProps, {}, "">;
|
package/dist/emoji.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface EmojiEntry {
|
|
2
|
+
emoji: string;
|
|
3
|
+
/** Search keywords, lower-case, both languages. */
|
|
4
|
+
keys: string;
|
|
5
|
+
}
|
|
6
|
+
export interface EmojiGroup {
|
|
7
|
+
id: string;
|
|
8
|
+
/** Group glyph shown in the tab strip. */
|
|
9
|
+
icon: string;
|
|
10
|
+
/** Accessible name of the tab. Translate it by supplying your own `groups`. */
|
|
11
|
+
label: string;
|
|
12
|
+
entries: EmojiEntry[];
|
|
13
|
+
}
|
|
14
|
+
export declare const EMOJI_GROUPS: readonly EmojiGroup[];
|
|
15
|
+
/** Entries matching a free-text query across every group; an empty query
|
|
16
|
+
* returns nothing (the caller shows the grouped grid instead). Accent-
|
|
17
|
+
* insensitive so « éléphant » and « elephant » both find the elephant. */
|
|
18
|
+
export declare function searchEmoji(query: string, groups?: readonly EmojiGroup[]): EmojiEntry[];
|
package/dist/emoji.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
const e = (emoji, keys) => ({ emoji, keys });
|
|
2
|
+
export const EMOJI_GROUPS = [
|
|
3
|
+
{
|
|
4
|
+
id: 'smileys',
|
|
5
|
+
label: 'Smileys',
|
|
6
|
+
icon: '😀',
|
|
7
|
+
entries: [
|
|
8
|
+
e('😀', 'grin smile sourire content happy'),
|
|
9
|
+
e('😄', 'smile laugh rire joie'),
|
|
10
|
+
e('😂', 'joy tears laugh rire larmes'),
|
|
11
|
+
e('🙂', 'slight smile sourire'),
|
|
12
|
+
e('😉', 'wink clin oeil'),
|
|
13
|
+
e('😍', 'heart eyes love amour'),
|
|
14
|
+
e('😎', 'cool sunglasses lunettes'),
|
|
15
|
+
e('🤓', 'nerd glasses lunettes geek'),
|
|
16
|
+
e('🧐', 'monocle inspect examiner'),
|
|
17
|
+
e('🤔', 'think réfléchir penser'),
|
|
18
|
+
e('🤖', 'robot bot machine'),
|
|
19
|
+
e('👻', 'ghost fantôme'),
|
|
20
|
+
e('💀', 'skull crâne tête de mort'),
|
|
21
|
+
e('👽', 'alien extraterrestre'),
|
|
22
|
+
e('🤡', 'clown'),
|
|
23
|
+
e('😈', 'devil diable démon'),
|
|
24
|
+
e('🥳', 'party fête célébration'),
|
|
25
|
+
e('😴', 'sleep dormir zzz'),
|
|
26
|
+
e('🤯', 'mind blown explosion tête'),
|
|
27
|
+
e('🫠', 'melt fondre'),
|
|
28
|
+
e('🙃', 'upside down envers'),
|
|
29
|
+
e('😇', 'angel ange halo'),
|
|
30
|
+
e('🤠', 'cowboy'),
|
|
31
|
+
e('🥸', 'disguise déguisement moustache'),
|
|
32
|
+
e('😶🌫️', 'fog brouillard nuage'),
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'people',
|
|
37
|
+
label: 'People',
|
|
38
|
+
icon: '🧑💻',
|
|
39
|
+
entries: [
|
|
40
|
+
e('🧑💻', 'technologist developer dev code ordinateur'),
|
|
41
|
+
e('👩💻', 'woman technologist développeuse'),
|
|
42
|
+
e('👨💻', 'man technologist développeur'),
|
|
43
|
+
e('🧑🔬', 'scientist science labo'),
|
|
44
|
+
e('🧑🚀', 'astronaut astronaute espace'),
|
|
45
|
+
e('🧑🎨', 'artist artiste peinture'),
|
|
46
|
+
e('🧑🍳', 'cook chef cuisine'),
|
|
47
|
+
e('🧑🏫', 'teacher prof enseignant'),
|
|
48
|
+
e('🧑⚕️', 'doctor médecin santé'),
|
|
49
|
+
e('🧑🚒', 'firefighter pompier'),
|
|
50
|
+
e('🕵️', 'detective détective enquête'),
|
|
51
|
+
e('🧙', 'wizard mage magicien sorcier'),
|
|
52
|
+
e('🧛', 'vampire'),
|
|
53
|
+
e('🧟', 'zombie'),
|
|
54
|
+
e('🦸', 'superhero héros super'),
|
|
55
|
+
e('🦹', 'villain méchant'),
|
|
56
|
+
e('🧞', 'genie génie'),
|
|
57
|
+
e('👑', 'crown couronne roi reine king queen'),
|
|
58
|
+
e('🎩', 'top hat chapeau haut de forme'),
|
|
59
|
+
e('🧢', 'cap casquette'),
|
|
60
|
+
e('👀', 'eyes yeux regarder'),
|
|
61
|
+
e('🧠', 'brain cerveau'),
|
|
62
|
+
e('💪', 'muscle biceps force'),
|
|
63
|
+
e('👋', 'wave hello bonjour salut'),
|
|
64
|
+
e('👍', 'thumbs up pouce ok'),
|
|
65
|
+
e('🙏', 'pray please merci thanks'),
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'animals',
|
|
70
|
+
label: 'Animals',
|
|
71
|
+
icon: '🐙',
|
|
72
|
+
entries: [
|
|
73
|
+
e('🐙', 'octopus pieuvre poulpe'),
|
|
74
|
+
e('🦀', 'crab crabe rust'),
|
|
75
|
+
e('🐍', 'snake serpent python'),
|
|
76
|
+
e('🐧', 'penguin pingouin manchot linux'),
|
|
77
|
+
e('🐳', 'whale baleine docker'),
|
|
78
|
+
e('🦊', 'fox renard'),
|
|
79
|
+
e('🐺', 'wolf loup'),
|
|
80
|
+
e('🦁', 'lion'),
|
|
81
|
+
e('🐯', 'tiger tigre'),
|
|
82
|
+
e('🐻', 'bear ours'),
|
|
83
|
+
e('🐼', 'panda'),
|
|
84
|
+
e('🐨', 'koala'),
|
|
85
|
+
e('🐸', 'frog grenouille'),
|
|
86
|
+
e('🦉', 'owl hibou chouette'),
|
|
87
|
+
e('🦅', 'eagle aigle'),
|
|
88
|
+
e('🦜', 'parrot perroquet'),
|
|
89
|
+
e('🐝', 'bee abeille'),
|
|
90
|
+
e('🦋', 'butterfly papillon'),
|
|
91
|
+
e('🐢', 'turtle tortue'),
|
|
92
|
+
e('🦈', 'shark requin'),
|
|
93
|
+
e('🐬', 'dolphin dauphin'),
|
|
94
|
+
e('🦄', 'unicorn licorne'),
|
|
95
|
+
e('🐉', 'dragon'),
|
|
96
|
+
e('🦖', 'dinosaur dinosaure t-rex'),
|
|
97
|
+
e('🐱', 'cat chat'),
|
|
98
|
+
e('🐶', 'dog chien'),
|
|
99
|
+
e('🐭', 'mouse souris'),
|
|
100
|
+
e('🐹', 'hamster'),
|
|
101
|
+
e('🐰', 'rabbit lapin'),
|
|
102
|
+
e('🦔', 'hedgehog hérisson'),
|
|
103
|
+
e('🐘', 'elephant éléphant postgres'),
|
|
104
|
+
e('🦒', 'giraffe girafe'),
|
|
105
|
+
e('🐎', 'horse cheval'),
|
|
106
|
+
e('🐐', 'goat chèvre'),
|
|
107
|
+
e('🐔', 'chicken poule'),
|
|
108
|
+
e('🦆', 'duck canard'),
|
|
109
|
+
e('🐌', 'snail escargot lent'),
|
|
110
|
+
e('🕷️', 'spider araignée'),
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'nature',
|
|
115
|
+
label: 'Nature',
|
|
116
|
+
icon: '🌿',
|
|
117
|
+
entries: [
|
|
118
|
+
e('🌿', 'herb plante feuille vert'),
|
|
119
|
+
e('🍀', 'clover trèfle chance luck'),
|
|
120
|
+
e('🌲', 'tree arbre sapin forêt'),
|
|
121
|
+
e('🌵', 'cactus'),
|
|
122
|
+
e('🌸', 'blossom fleur cerisier'),
|
|
123
|
+
e('🌻', 'sunflower tournesol'),
|
|
124
|
+
e('🌹', 'rose'),
|
|
125
|
+
e('🍄', 'mushroom champignon'),
|
|
126
|
+
e('🌍', 'earth terre monde globe'),
|
|
127
|
+
e('🌙', 'moon lune nuit'),
|
|
128
|
+
e('☀️', 'sun soleil jour'),
|
|
129
|
+
e('⭐', 'star étoile'),
|
|
130
|
+
e('🌟', 'glowing star étoile brillante'),
|
|
131
|
+
e('✨', 'sparkles étincelles magie'),
|
|
132
|
+
e('⚡', 'lightning éclair zap énergie'),
|
|
133
|
+
e('🔥', 'fire feu flamme hot'),
|
|
134
|
+
e('🌈', 'rainbow arc-en-ciel'),
|
|
135
|
+
e('☁️', 'cloud nuage'),
|
|
136
|
+
e('❄️', 'snowflake flocon neige froid'),
|
|
137
|
+
e('🌊', 'wave vague mer océan'),
|
|
138
|
+
e('🌋', 'volcano volcan'),
|
|
139
|
+
e('🪐', 'planet planète saturne'),
|
|
140
|
+
e('🚀', 'rocket fusée launch'),
|
|
141
|
+
e('🛸', 'ufo soucoupe ovni'),
|
|
142
|
+
e('☄️', 'comet comète'),
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'objects',
|
|
147
|
+
label: 'Objects',
|
|
148
|
+
icon: '🔧',
|
|
149
|
+
entries: [
|
|
150
|
+
e('🔧', 'wrench clé outil tool'),
|
|
151
|
+
e('🔨', 'hammer marteau build'),
|
|
152
|
+
e('🛠️', 'tools outils'),
|
|
153
|
+
e('⚙️', 'gear engrenage réglages settings'),
|
|
154
|
+
e('🔑', 'key clé password'),
|
|
155
|
+
e('🔒', 'lock cadenas verrou sécurité'),
|
|
156
|
+
e('🛡️', 'shield bouclier protection'),
|
|
157
|
+
e('⚔️', 'swords épées'),
|
|
158
|
+
e('🧰', 'toolbox boîte à outils'),
|
|
159
|
+
e('🧪', 'test tube éprouvette labo'),
|
|
160
|
+
e('🔬', 'microscope'),
|
|
161
|
+
e('🔭', 'telescope télescope'),
|
|
162
|
+
e('💻', 'laptop ordinateur portable'),
|
|
163
|
+
e('🖥️', 'desktop écran ordinateur'),
|
|
164
|
+
e('⌨️', 'keyboard clavier'),
|
|
165
|
+
e('🖱️', 'mouse souris'),
|
|
166
|
+
e('📱', 'phone téléphone mobile'),
|
|
167
|
+
e('💾', 'floppy disquette save'),
|
|
168
|
+
e('🗄️', 'cabinet classeur base données database'),
|
|
169
|
+
e('📦', 'package paquet box colis'),
|
|
170
|
+
e('📚', 'books livres docs'),
|
|
171
|
+
e('📝', 'memo note écrire'),
|
|
172
|
+
e('📌', 'pin punaise épingle'),
|
|
173
|
+
e('🔖', 'bookmark marque-page'),
|
|
174
|
+
e('💡', 'bulb idée ampoule'),
|
|
175
|
+
e('🔦', 'flashlight lampe torche'),
|
|
176
|
+
e('🧭', 'compass boussole'),
|
|
177
|
+
e('⏰', 'alarm réveil horloge'),
|
|
178
|
+
e('⏱️', 'stopwatch chrono'),
|
|
179
|
+
e('🎯', 'target cible dart'),
|
|
180
|
+
e('🎲', 'dice dé hasard'),
|
|
181
|
+
e('🎮', 'game manette jeu'),
|
|
182
|
+
e('🎸', 'guitar guitare musique'),
|
|
183
|
+
e('🎵', 'music note musique'),
|
|
184
|
+
e('🎧', 'headphones casque'),
|
|
185
|
+
e('📷', 'camera appareil photo'),
|
|
186
|
+
e('🎬', 'clapper cinéma film'),
|
|
187
|
+
e('🧩', 'puzzle pièce'),
|
|
188
|
+
e('🪄', 'magic wand baguette magique'),
|
|
189
|
+
e('🧲', 'magnet aimant'),
|
|
190
|
+
e('💎', 'gem diamant'),
|
|
191
|
+
e('💰', 'money bag argent sac'),
|
|
192
|
+
e('🪙', 'coin pièce'),
|
|
193
|
+
e('🏆', 'trophy trophée coupe'),
|
|
194
|
+
e('🥇', 'medal médaille or'),
|
|
195
|
+
e('🎁', 'gift cadeau'),
|
|
196
|
+
e('🎈', 'balloon ballon'),
|
|
197
|
+
e('☕', 'coffee café'),
|
|
198
|
+
e('🍕', 'pizza'),
|
|
199
|
+
e('🍔', 'burger'),
|
|
200
|
+
e('🍩', 'donut beignet'),
|
|
201
|
+
e('🍪', 'cookie biscuit'),
|
|
202
|
+
e('🍺', 'beer bière'),
|
|
203
|
+
e('🍷', 'wine vin'),
|
|
204
|
+
e('🚑', 'ambulance secours urgence rescue'),
|
|
205
|
+
e('🚒', 'fire engine pompiers'),
|
|
206
|
+
e('🚗', 'car voiture'),
|
|
207
|
+
e('🚲', 'bike vélo'),
|
|
208
|
+
e('✈️', 'plane avion'),
|
|
209
|
+
e('🚂', 'train locomotive'),
|
|
210
|
+
e('⛵', 'sailboat voilier bateau'),
|
|
211
|
+
e('🏠', 'house maison home'),
|
|
212
|
+
e('🏭', 'factory usine'),
|
|
213
|
+
e('🏰', 'castle château'),
|
|
214
|
+
e('🗼', 'tower tour'),
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'symbols',
|
|
219
|
+
label: 'Symbols',
|
|
220
|
+
icon: '❤️',
|
|
221
|
+
entries: [
|
|
222
|
+
e('❤️', 'heart coeur rouge love'),
|
|
223
|
+
e('🧡', 'orange heart coeur'),
|
|
224
|
+
e('💛', 'yellow heart coeur jaune'),
|
|
225
|
+
e('💚', 'green heart coeur vert'),
|
|
226
|
+
e('💙', 'blue heart coeur bleu'),
|
|
227
|
+
e('💜', 'purple heart coeur violet'),
|
|
228
|
+
e('🖤', 'black heart coeur noir'),
|
|
229
|
+
e('🤍', 'white heart coeur blanc'),
|
|
230
|
+
e('💯', 'hundred cent points'),
|
|
231
|
+
e('✅', 'check coche ok validé'),
|
|
232
|
+
e('❌', 'cross croix non erreur'),
|
|
233
|
+
e('⚠️', 'warning attention'),
|
|
234
|
+
e('🚫', 'forbidden interdit'),
|
|
235
|
+
e('♻️', 'recycle recyclage'),
|
|
236
|
+
e('🔴', 'red circle rond rouge'),
|
|
237
|
+
e('🟠', 'orange circle rond'),
|
|
238
|
+
e('🟡', 'yellow circle rond jaune'),
|
|
239
|
+
e('🟢', 'green circle rond vert'),
|
|
240
|
+
e('🔵', 'blue circle rond bleu'),
|
|
241
|
+
e('🟣', 'purple circle rond violet'),
|
|
242
|
+
e('⚫', 'black circle rond noir'),
|
|
243
|
+
e('⚪', 'white circle rond blanc'),
|
|
244
|
+
e('🟥', 'red square carré rouge'),
|
|
245
|
+
e('🟧', 'orange square carré'),
|
|
246
|
+
e('🟨', 'yellow square carré jaune'),
|
|
247
|
+
e('🟩', 'green square carré vert'),
|
|
248
|
+
e('🟦', 'blue square carré bleu'),
|
|
249
|
+
e('🟪', 'purple square carré violet'),
|
|
250
|
+
e('⬛', 'black square carré noir'),
|
|
251
|
+
e('⬜', 'white square carré blanc'),
|
|
252
|
+
e('🔶', 'orange diamond losange'),
|
|
253
|
+
e('🔷', 'blue diamond losange bleu'),
|
|
254
|
+
e('🔺', 'red triangle'),
|
|
255
|
+
e('♠️', 'spade pique'),
|
|
256
|
+
e('♣️', 'club trèfle'),
|
|
257
|
+
e('♥️', 'heart coeur cartes'),
|
|
258
|
+
e('♦️', 'diamond carreau'),
|
|
259
|
+
e('☮️', 'peace paix'),
|
|
260
|
+
e('☯️', 'yin yang'),
|
|
261
|
+
e('♾️', 'infinity infini'),
|
|
262
|
+
e('🔱', 'trident'),
|
|
263
|
+
e('⚓', 'anchor ancre'),
|
|
264
|
+
e('🏁', 'checkered flag drapeau arrivée'),
|
|
265
|
+
e('🚩', 'red flag drapeau rouge'),
|
|
266
|
+
e('🏴☠️', 'pirate flag drapeau pirate'),
|
|
267
|
+
e('🇫🇷', 'france drapeau flag'),
|
|
268
|
+
e('🇪🇺', 'europe eu drapeau flag'),
|
|
269
|
+
e('🇺🇸', 'usa états-unis flag drapeau'),
|
|
270
|
+
e('🇬🇧', 'uk royaume-uni flag drapeau'),
|
|
271
|
+
e('🇩🇪', 'germany allemagne flag drapeau'),
|
|
272
|
+
e('🇯🇵', 'japan japon flag drapeau'),
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
const fold = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
277
|
+
/** Entries matching a free-text query across every group; an empty query
|
|
278
|
+
* returns nothing (the caller shows the grouped grid instead). Accent-
|
|
279
|
+
* insensitive so « éléphant » and « elephant » both find the elephant. */
|
|
280
|
+
export function searchEmoji(query, groups = EMOJI_GROUPS) {
|
|
281
|
+
const q = fold(query.trim());
|
|
282
|
+
if (!q)
|
|
283
|
+
return [];
|
|
284
|
+
const words = q.split(/\s+/);
|
|
285
|
+
return groups.flatMap((g) => g.entries.filter((x) => words.every((w) => fold(x.keys).includes(w))));
|
|
286
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export { default as ConfirmModal } from './components/molecules/ConfirmModal.sve
|
|
|
48
48
|
export { default as CopyButton } from './components/molecules/CopyButton.svelte';
|
|
49
49
|
export { default as Drawer } from './components/molecules/Drawer.svelte';
|
|
50
50
|
export { default as Dropzone } from './components/molecules/Dropzone.svelte';
|
|
51
|
+
export { default as EmojiPicker } from './components/molecules/EmojiPicker.svelte';
|
|
51
52
|
export { default as EmptyState } from './components/molecules/EmptyState.svelte';
|
|
52
53
|
export { default as Field } from './components/molecules/Field.svelte';
|
|
53
54
|
export { default as Fieldset } from './components/molecules/Fieldset.svelte';
|
|
@@ -80,6 +81,7 @@ export { default as Truncate } from './components/molecules/Truncate.svelte';
|
|
|
80
81
|
export { default as WorkingDir } from './components/molecules/WorkingDir.svelte';
|
|
81
82
|
export { type Column, default as DataTable, type RowTone, } from './components/organisms/DataTable.svelte';
|
|
82
83
|
export { default as FilterSearchBar } from './components/organisms/FilterSearchBar.svelte';
|
|
84
|
+
export { EMOJI_GROUPS, type EmojiEntry, type EmojiGroup, searchEmoji } from './emoji';
|
|
83
85
|
export { FIELD_KEY, type FieldContext, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
|
|
84
86
|
export * as filterQuery from './query';
|
|
85
87
|
export { type AndNode, activeToken, compilePredicate, defaultOperator, type ExprNode, type FieldDef, type FieldType, type FilterNode, filters, findField, freeText, type LeafNode, type NotNode, OPERATORS, type Operator, type OperatorId, type OrNode, operatorByCode, operatorById, operatorsFor, parse, type Query, type QueryNode, resolveValues, type Schema, type Suggestion, type SuggestKind, type SuggestState, serialize, serializeFilter, suggest, type TextNode, toSql, type ValueContext, type ValueOption, type ValueProvider, walk, } from './query';
|
|
@@ -88,6 +90,7 @@ export type { ControlSize } from './size';
|
|
|
88
90
|
export { fontScale, SCALE_LEVELS, type ScaleLevel } from './stores/fontscale.svelte';
|
|
89
91
|
export { type Mode, THEMES, theme } from './stores/theme.svelte';
|
|
90
92
|
export { type Toast, type ToastAction, type ToastOptions, type ToastTone, type ToastToneInput, toasts, } from './stores/toast.svelte';
|
|
93
|
+
export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, type SlotOf, type ThemeChoice, type ThemePreference, type ThemeSlot, } from './theme-mode';
|
|
91
94
|
export { formatTimestamp, localTimeZone, relativeTime, type TimeInput, type TimestampMode, } from './timestamp';
|
|
92
95
|
export { canonicalTone, type Tone } from './tone';
|
|
93
96
|
export { pathCandidates, type TruncateMode, type TruncateOptions, truncate } from './truncate';
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,7 @@ export { default as ConfirmModal } from './components/molecules/ConfirmModal.sve
|
|
|
54
54
|
export { default as CopyButton } from './components/molecules/CopyButton.svelte';
|
|
55
55
|
export { default as Drawer } from './components/molecules/Drawer.svelte';
|
|
56
56
|
export { default as Dropzone } from './components/molecules/Dropzone.svelte';
|
|
57
|
+
export { default as EmojiPicker } from './components/molecules/EmojiPicker.svelte';
|
|
57
58
|
export { default as EmptyState } from './components/molecules/EmptyState.svelte';
|
|
58
59
|
// ---- molecules ----
|
|
59
60
|
export { default as Field } from './components/molecules/Field.svelte';
|
|
@@ -90,6 +91,7 @@ export { default as WorkingDir } from './components/molecules/WorkingDir.svelte'
|
|
|
90
91
|
// ---- organisms ----
|
|
91
92
|
export { default as DataTable, } from './components/organisms/DataTable.svelte';
|
|
92
93
|
export { default as FilterSearchBar } from './components/organisms/FilterSearchBar.svelte';
|
|
94
|
+
export { EMOJI_GROUPS, searchEmoji } from './emoji';
|
|
93
95
|
export { FIELD_KEY, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
|
|
94
96
|
// ---- query core (headless: schema / parser / AST / suggest / compilers) ----
|
|
95
97
|
export * as filterQuery from './query';
|
|
@@ -99,6 +101,7 @@ export { fontScale, SCALE_LEVELS } from './stores/fontscale.svelte';
|
|
|
99
101
|
// ---- stores / actions ----
|
|
100
102
|
export { THEMES, theme } from './stores/theme.svelte';
|
|
101
103
|
export { toasts, } from './stores/toast.svelte';
|
|
104
|
+
export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, } from './theme-mode';
|
|
102
105
|
export { formatTimestamp, localTimeZone, relativeTime, } from './timestamp';
|
|
103
106
|
export { canonicalTone } from './tone';
|
|
104
107
|
export { pathCandidates, truncate } from './truncate';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SlotOf, type ThemeChoice, type ThemePreference } from '../theme-mode';
|
|
1
2
|
export declare const THEMES: readonly [{
|
|
2
3
|
readonly id: "light";
|
|
3
4
|
readonly label: "Light";
|
|
@@ -155,16 +156,32 @@ export interface ThemeDef {
|
|
|
155
156
|
}
|
|
156
157
|
declare class Theme {
|
|
157
158
|
current: ThemeId;
|
|
159
|
+
/** True while the system asks for a dark scheme; only read in `auto`. */
|
|
160
|
+
systemDark: boolean;
|
|
161
|
+
pref: ThemePreference;
|
|
162
|
+
/** Called with the new preference whenever the user changes it, so an app
|
|
163
|
+
* can mirror it into its own (server-side) settings. */
|
|
164
|
+
onchange?: (pref: ThemePreference) => void;
|
|
158
165
|
readonly fallbackIcon = "\u25C8";
|
|
159
166
|
private registered;
|
|
160
167
|
private fallback;
|
|
161
168
|
private saved;
|
|
162
169
|
constructor();
|
|
170
|
+
get slotOf(): SlotOf;
|
|
171
|
+
/** `auto`, or the pinned slot. */
|
|
172
|
+
get mode(): ThemeChoice;
|
|
173
|
+
/** The theme id painted for the current preference. */
|
|
174
|
+
get resolved(): ThemeId;
|
|
175
|
+
/** What a picker shows as selected: `auto`, or the pinned slot's theme id. */
|
|
176
|
+
get choice(): string;
|
|
163
177
|
get all(): readonly ThemeDef[];
|
|
164
178
|
has(id: string | null | undefined): id is ThemeId;
|
|
165
179
|
register(defs: ThemeDef | ThemeDef[]): void;
|
|
166
180
|
setDefault(id: ThemeId): void;
|
|
167
181
|
private resolve;
|
|
182
|
+
private read;
|
|
183
|
+
private paint;
|
|
184
|
+
private persist;
|
|
168
185
|
private apply;
|
|
169
186
|
get option(): ThemeDef;
|
|
170
187
|
get label(): string;
|
|
@@ -172,6 +189,12 @@ declare class Theme {
|
|
|
172
189
|
get next(): ThemeDef;
|
|
173
190
|
toggle(): void;
|
|
174
191
|
set(mode: ThemeId): void;
|
|
192
|
+
/** A picker choice: `auto`, or a theme id, which also becomes its slot's
|
|
193
|
+
* remembered theme. An unregistered id is ignored. */
|
|
194
|
+
choose(choice: string): ThemePreference;
|
|
195
|
+
/** Replay a preference an app persisted itself, without echoing it back
|
|
196
|
+
* through `onchange`. */
|
|
197
|
+
hydrate(pref: ThemePreference): void;
|
|
175
198
|
}
|
|
176
199
|
export declare const theme: Theme;
|
|
177
200
|
export {};
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { browser } from '../env';
|
|
2
|
+
import { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, preferenceFrom, resolveTheme, } from '../theme-mode';
|
|
2
3
|
// Theme registry. Built-ins live in THEMES (+ one [data-theme="id"] block in
|
|
3
4
|
// styles/themes.css); consumers append their own with theme.register() and ship
|
|
4
5
|
// the matching block in their own stylesheet. `themeColor` drives the mobile
|
|
5
6
|
// browser-chrome <meta theme-color>; `mode` groups the theme into the picker's
|
|
6
7
|
// light/dark sections.
|
|
8
|
+
//
|
|
9
|
+
// `tsumikit-theme` holds a {mode,light,dark} blob; a bare theme id there is a
|
|
10
|
+
// legacy value and migrates through `preferenceFrom`.
|
|
7
11
|
const KEY = 'tsumikit-theme';
|
|
12
|
+
const SCHEME_QUERY = '(prefers-color-scheme: dark)';
|
|
8
13
|
export const THEMES = [
|
|
9
14
|
// ── Light ── bright, paper-white surfaces
|
|
10
15
|
{ id: 'light', label: 'Light', icon: '☀', themeColor: '#f6f7f9', mode: 'light' },
|
|
@@ -55,16 +60,43 @@ export const THEMES = [
|
|
|
55
60
|
const FALLBACK_ICON = '◈';
|
|
56
61
|
class Theme {
|
|
57
62
|
current = $state('dark');
|
|
63
|
+
/** True while the system asks for a dark scheme; only read in `auto`. */
|
|
64
|
+
systemDark = $state(false);
|
|
65
|
+
pref = $state(DEFAULT_THEME_PREFERENCE);
|
|
66
|
+
/** Called with the new preference whenever the user changes it, so an app
|
|
67
|
+
* can mirror it into its own (server-side) settings. */
|
|
68
|
+
onchange;
|
|
58
69
|
fallbackIcon = FALLBACK_ICON;
|
|
59
70
|
registered = $state([]);
|
|
60
71
|
fallback = 'dark';
|
|
61
72
|
saved = null;
|
|
62
73
|
constructor() {
|
|
63
74
|
if (browser) {
|
|
75
|
+
const mq = typeof matchMedia === 'function' ? matchMedia(SCHEME_QUERY) : null;
|
|
76
|
+
this.systemDark = mq?.matches ?? false;
|
|
77
|
+
mq?.addEventListener?.('change', (e) => {
|
|
78
|
+
this.systemDark = e.matches;
|
|
79
|
+
this.paint();
|
|
80
|
+
});
|
|
64
81
|
this.saved = localStorage.getItem(KEY);
|
|
65
82
|
this.resolve();
|
|
66
83
|
}
|
|
67
84
|
}
|
|
85
|
+
get slotOf() {
|
|
86
|
+
return (id) => this.all.find((t) => t.id === id)?.mode ?? null;
|
|
87
|
+
}
|
|
88
|
+
/** `auto`, or the pinned slot. */
|
|
89
|
+
get mode() {
|
|
90
|
+
return this.pref.mode;
|
|
91
|
+
}
|
|
92
|
+
/** The theme id painted for the current preference. */
|
|
93
|
+
get resolved() {
|
|
94
|
+
return resolveTheme(this.pref, this.systemDark);
|
|
95
|
+
}
|
|
96
|
+
/** What a picker shows as selected: `auto`, or the pinned slot's theme id. */
|
|
97
|
+
get choice() {
|
|
98
|
+
return this.pref.mode === AUTO_THEME ? AUTO_THEME : this.resolved;
|
|
99
|
+
}
|
|
68
100
|
get all() {
|
|
69
101
|
const byId = new Map();
|
|
70
102
|
for (const t of THEMES)
|
|
@@ -86,9 +118,38 @@ class Theme {
|
|
|
86
118
|
this.resolve();
|
|
87
119
|
}
|
|
88
120
|
resolve() {
|
|
89
|
-
this.
|
|
121
|
+
this.pref = this.read();
|
|
122
|
+
this.paint();
|
|
123
|
+
}
|
|
124
|
+
read() {
|
|
125
|
+
const seed = chooseTheme(DEFAULT_THEME_PREFERENCE, this.fallback, this.slotOf);
|
|
126
|
+
if (!this.saved)
|
|
127
|
+
return seed;
|
|
128
|
+
let blob = null;
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(this.saved);
|
|
131
|
+
if (parsed && typeof parsed === 'object')
|
|
132
|
+
blob = parsed;
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
return preferenceFrom(blob
|
|
136
|
+
? { themeMode: blob.mode, lightTheme: blob.light, darkTheme: blob.dark }
|
|
137
|
+
: { theme: this.saved }, this.slotOf, seed);
|
|
138
|
+
}
|
|
139
|
+
paint() {
|
|
140
|
+
this.current = this.resolved;
|
|
90
141
|
this.apply();
|
|
91
142
|
}
|
|
143
|
+
persist() {
|
|
144
|
+
this.saved = JSON.stringify(this.pref);
|
|
145
|
+
if (browser) {
|
|
146
|
+
try {
|
|
147
|
+
localStorage.setItem(KEY, this.saved);
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
}
|
|
151
|
+
this.onchange?.(this.pref);
|
|
152
|
+
}
|
|
92
153
|
apply() {
|
|
93
154
|
if (!browser)
|
|
94
155
|
return;
|
|
@@ -117,11 +178,22 @@ class Theme {
|
|
|
117
178
|
this.set(this.next.id);
|
|
118
179
|
}
|
|
119
180
|
set(mode) {
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
181
|
+
this.choose(mode);
|
|
182
|
+
}
|
|
183
|
+
/** A picker choice: `auto`, or a theme id, which also becomes its slot's
|
|
184
|
+
* remembered theme. An unregistered id is ignored. */
|
|
185
|
+
choose(choice) {
|
|
186
|
+
this.pref = chooseTheme(this.pref, choice, this.slotOf);
|
|
187
|
+
this.paint();
|
|
188
|
+
this.persist();
|
|
189
|
+
return this.pref;
|
|
190
|
+
}
|
|
191
|
+
/** Replay a preference an app persisted itself, without echoing it back
|
|
192
|
+
* through `onchange`. */
|
|
193
|
+
hydrate(pref) {
|
|
194
|
+
this.pref = pref;
|
|
195
|
+
this.saved = JSON.stringify(pref);
|
|
196
|
+
this.paint();
|
|
125
197
|
}
|
|
126
198
|
}
|
|
127
199
|
export const theme = new Theme();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ThemeSlot = 'light' | 'dark';
|
|
2
|
+
export type ThemeChoice = ThemeSlot | 'auto';
|
|
3
|
+
export interface ThemePreference {
|
|
4
|
+
/** `auto` follows the system; `light` / `dark` pin the matching slot. */
|
|
5
|
+
mode: ThemeChoice;
|
|
6
|
+
/** Last light theme the user picked. */
|
|
7
|
+
light: string;
|
|
8
|
+
/** Last dark theme the user picked. */
|
|
9
|
+
dark: string;
|
|
10
|
+
}
|
|
11
|
+
/** The picker value that means "follow the system". Never a theme id. */
|
|
12
|
+
export declare const AUTO_THEME = "auto";
|
|
13
|
+
export declare const DEFAULT_THEME_PREFERENCE: ThemePreference;
|
|
14
|
+
/** Which slot a theme id belongs to; `null` when the id is unknown. */
|
|
15
|
+
export type SlotOf = (id: string) => ThemeSlot | null;
|
|
16
|
+
/** The theme id to paint for a preference, given the system's current scheme. */
|
|
17
|
+
export declare function resolveTheme(pref: ThemePreference, systemDark: boolean): string;
|
|
18
|
+
/** Apply a picker choice: `auto` switches mode only; a theme id pins its slot
|
|
19
|
+
* AND becomes that slot's remembered theme. Unknown ids leave the preference
|
|
20
|
+
* untouched so a stale blob can never paint an unregistered theme. */
|
|
21
|
+
export declare function chooseTheme(pref: ThemePreference, choice: string, slotOf: SlotOf): ThemePreference;
|
|
22
|
+
/** Rebuild a preference from persisted fields. Older blobs only carried a
|
|
23
|
+
* single `theme`: it seeds whichever slot it belongs to and pins that mode, so
|
|
24
|
+
* nothing changes for a user who never touched the new picker. Unknown ids
|
|
25
|
+
* fall back to `defaults` rather than being trusted. */
|
|
26
|
+
export declare function preferenceFrom(raw: {
|
|
27
|
+
theme?: string | null;
|
|
28
|
+
themeMode?: string | null;
|
|
29
|
+
lightTheme?: string | null;
|
|
30
|
+
darkTheme?: string | null;
|
|
31
|
+
}, slotOf: SlotOf, defaults?: ThemePreference): ThemePreference;
|
|
32
|
+
/** The picker's current value: `auto`, or the pinned slot's theme id. */
|
|
33
|
+
export declare function pickerValue(pref: ThemePreference): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Pure logic behind the light/dark/auto theme preference: the last light and
|
|
2
|
+
// the last dark theme the user picked, plus an `auto` mode that follows the
|
|
3
|
+
// system's `prefers-color-scheme`. Rune-free so it unit-tests in isolation.
|
|
4
|
+
/** The picker value that means "follow the system". Never a theme id. */
|
|
5
|
+
export const AUTO_THEME = 'auto';
|
|
6
|
+
export const DEFAULT_THEME_PREFERENCE = {
|
|
7
|
+
mode: 'dark',
|
|
8
|
+
light: 'light',
|
|
9
|
+
dark: 'dark',
|
|
10
|
+
};
|
|
11
|
+
/** The theme id to paint for a preference, given the system's current scheme. */
|
|
12
|
+
export function resolveTheme(pref, systemDark) {
|
|
13
|
+
const slot = pref.mode === 'auto' ? (systemDark ? 'dark' : 'light') : pref.mode;
|
|
14
|
+
return slot === 'dark' ? pref.dark : pref.light;
|
|
15
|
+
}
|
|
16
|
+
/** Apply a picker choice: `auto` switches mode only; a theme id pins its slot
|
|
17
|
+
* AND becomes that slot's remembered theme. Unknown ids leave the preference
|
|
18
|
+
* untouched so a stale blob can never paint an unregistered theme. */
|
|
19
|
+
export function chooseTheme(pref, choice, slotOf) {
|
|
20
|
+
if (choice === AUTO_THEME)
|
|
21
|
+
return { ...pref, mode: 'auto' };
|
|
22
|
+
const slot = slotOf(choice);
|
|
23
|
+
if (!slot)
|
|
24
|
+
return pref;
|
|
25
|
+
return { ...pref, mode: slot, [slot]: choice };
|
|
26
|
+
}
|
|
27
|
+
/** Rebuild a preference from persisted fields. Older blobs only carried a
|
|
28
|
+
* single `theme`: it seeds whichever slot it belongs to and pins that mode, so
|
|
29
|
+
* nothing changes for a user who never touched the new picker. Unknown ids
|
|
30
|
+
* fall back to `defaults` rather than being trusted. */
|
|
31
|
+
export function preferenceFrom(raw, slotOf, defaults = DEFAULT_THEME_PREFERENCE) {
|
|
32
|
+
const light = raw.lightTheme && slotOf(raw.lightTheme) === 'light' ? raw.lightTheme : defaults.light;
|
|
33
|
+
const dark = raw.darkTheme && slotOf(raw.darkTheme) === 'dark' ? raw.darkTheme : defaults.dark;
|
|
34
|
+
const pref = { mode: defaults.mode, light, dark };
|
|
35
|
+
if (raw.themeMode === 'auto' || raw.themeMode === 'light' || raw.themeMode === 'dark') {
|
|
36
|
+
return { ...pref, mode: raw.themeMode };
|
|
37
|
+
}
|
|
38
|
+
if (raw.theme)
|
|
39
|
+
return chooseTheme(pref, raw.theme, slotOf);
|
|
40
|
+
return pref;
|
|
41
|
+
}
|
|
42
|
+
/** The picker's current value: `auto`, or the pinned slot's theme id. */
|
|
43
|
+
export function pickerValue(pref) {
|
|
44
|
+
return pref.mode === 'auto' ? AUTO_THEME : pref.mode === 'dark' ? pref.dark : pref.light;
|
|
45
|
+
}
|
package/package.json
CHANGED