@dorsk/tsumikit 0.44.1 → 0.46.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 CHANGED
@@ -164,7 +164,8 @@ Tabs, RadioGroup (`variant="rows"`: bordered rows, per-option `note`/`descriptio
164
164
  `action(option)` trailing control that never toggles, `below(option)` inline panel),
165
165
  Tooltip, Accordion, CopyButton, FileButton,
166
166
  Dropzone, CodeBlock, Callout, EmptyState, ConfirmModal, Pagination, Toaster,
167
- ThemePicker (popover grid of 2×2 palette swatches: bg · surface · text · accent per theme), FontScalePicker (popover with a stepped slider across the five text sizes), SectionHeader, KeyValue, LoadMore,
167
+ ThemePicker (popover grid of 2×2 palette swatches: bg · surface · text · accent per theme),
168
+ 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
169
  GitRef (branch chip + PR link tinted by state + `+N −N` diff; `collapse`
169
170
  auto/never/glyph degrades to icons inside a narrow `.cq` container),
170
171
  CapBar (consumption track with a draggable, keyboard-steppable cap handle that
@@ -9,6 +9,10 @@
9
9
  /** Muted, right-aligned secondary text (e.g. "62%"). */
10
10
  hint?: string;
11
11
  disabled?: boolean;
12
+ /** Section heading this option belongs under. Options sharing a group are
13
+ * emitted inside one `<optgroup label={group}>`, in first-seen group order.
14
+ * Ungrouped options render at the top level. */
15
+ group?: string;
12
16
  };
13
17
  </script>
14
18
 
@@ -42,6 +46,7 @@
42
46
  import type { HTMLSelectAttributes } from 'svelte/elements';
43
47
  import Icon from './Icon.svelte';
44
48
  import { getFieldContext, warnUnlabelled } from '../../field-context';
49
+ import { sectionOptions } from '../../select-options';
45
50
 
46
51
  // `size` shadows the native option-count attribute (unused in token layouts) to
47
52
  // expose the sm|md height scale instead.
@@ -99,14 +104,26 @@
99
104
  const selected = $derived(options?.find((o) => o.value === value));
100
105
  const hasFace = $derived(!!options && variant !== 'ghost');
101
106
 
107
+ const sections = $derived(sectionOptions(options ?? []));
108
+
102
109
  const optionText = (o: SelectOption) =>
103
110
  [o.emoji, o.label, o.hint && `· ${o.hint}`].filter(Boolean).join(' ');
104
111
  </script>
105
112
 
106
113
  {#snippet optionList()}
107
114
  {#if options}
108
- {#each options as o (o.value)}
109
- <option value={o.value} disabled={o.disabled}>{optionText(o)}</option>
115
+ {#each sections as section, i (section.group ?? `-${i}`)}
116
+ {#if section.group === undefined}
117
+ {#each section.options as o (o.value)}
118
+ <option value={o.value} disabled={o.disabled}>{optionText(o)}</option>
119
+ {/each}
120
+ {:else}
121
+ <optgroup label={section.group}>
122
+ {#each section.options as o (o.value)}
123
+ <option value={o.value} disabled={o.disabled}>{optionText(o)}</option>
124
+ {/each}
125
+ </optgroup>
126
+ {/if}
110
127
  {/each}
111
128
  {:else}
112
129
  {@render children?.()}
@@ -251,7 +268,8 @@
251
268
  .select.has-face {
252
269
  color: transparent;
253
270
  }
254
- .select.has-face option {
271
+ .select.has-face option,
272
+ .select.has-face optgroup {
255
273
  color: var(--text);
256
274
  background: var(--bg);
257
275
  }
@@ -7,6 +7,10 @@ export type SelectOption = {
7
7
  /** Muted, right-aligned secondary text (e.g. "62%"). */
8
8
  hint?: string;
9
9
  disabled?: boolean;
10
+ /** Section heading this option belongs under. Options sharing a group are
11
+ * emitted inside one `<optgroup label={group}>`, in first-seen group order.
12
+ * Ungrouped options render at the top level. */
13
+ group?: string;
10
14
  };
11
15
  import type { ControlSize } from '../../size';
12
16
  import type { Snippet } from 'svelte';
@@ -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;
@@ -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,9 +81,11 @@ 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
+ export { type OptionSection, sectionOptions } from './select-options';
86
89
  export type { ControlSize } from './size';
87
90
  export { fontScale, SCALE_LEVELS, type ScaleLevel } from './stores/fontscale.svelte';
88
91
  export { type Mode, THEMES, theme } from './stores/theme.svelte';
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,10 +91,12 @@ 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';
96
98
  export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, suggest, toSql, walk, } from './query';
99
+ export { sectionOptions } from './select-options';
97
100
  export { fontScale, SCALE_LEVELS } from './stores/fontscale.svelte';
98
101
  // ---- stores / actions ----
99
102
  export { THEMES, theme } from './stores/theme.svelte';
@@ -0,0 +1,6 @@
1
+ import type { SelectOption } from './components/atoms/Select.svelte';
2
+ export type OptionSection = {
3
+ group?: string;
4
+ options: SelectOption[];
5
+ };
6
+ export declare function sectionOptions(options: SelectOption[]): OptionSection[];
@@ -0,0 +1,22 @@
1
+ export function sectionOptions(options) {
2
+ const sections = [];
3
+ const byGroup = new Map();
4
+ for (const option of options) {
5
+ if (option.group === undefined) {
6
+ const tail = sections.at(-1);
7
+ if (tail && tail.group === undefined)
8
+ tail.options.push(option);
9
+ else
10
+ sections.push({ options: [option] });
11
+ continue;
12
+ }
13
+ let section = byGroup.get(option.group);
14
+ if (!section) {
15
+ section = { group: option.group, options: [] };
16
+ byGroup.set(option.group, section);
17
+ sections.push(section);
18
+ }
19
+ section.options.push(option);
20
+ }
21
+ return sections;
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.44.1",
3
+ "version": "0.46.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",