@social.io/catalog 1.3.0 → 2.1.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.
Files changed (130) hide show
  1. package/.smartconfig.json +51 -0
  2. package/dist_bundle/bundle.js +5455 -3248
  3. package/dist_ts_web/00_commitinfo_data.js +3 -3
  4. package/dist_ts_web/elements/index.d.ts +21 -3
  5. package/dist_ts_web/elements/index.js +28 -7
  6. package/dist_ts_web/elements/sio-ai-card.d.ts +39 -0
  7. package/dist_ts_web/elements/sio-ai-card.js +229 -0
  8. package/dist_ts_web/elements/sio-avatar.d.ts +46 -0
  9. package/dist_ts_web/elements/sio-avatar.js +379 -0
  10. package/dist_ts_web/elements/sio-bucket-header.d.ts +36 -0
  11. package/dist_ts_web/elements/sio-bucket-header.js +197 -0
  12. package/dist_ts_web/elements/sio-button.d.ts +7 -3
  13. package/dist_ts_web/elements/sio-button.js +253 -152
  14. package/dist_ts_web/elements/sio-channel-rail.d.ts +38 -0
  15. package/dist_ts_web/elements/sio-channel-rail.js +288 -0
  16. package/dist_ts_web/elements/sio-chip.d.ts +25 -0
  17. package/dist_ts_web/elements/sio-chip.js +188 -0
  18. package/dist_ts_web/elements/sio-cmdk.d.ts +59 -0
  19. package/dist_ts_web/elements/sio-cmdk.js +471 -0
  20. package/dist_ts_web/elements/sio-combox.d.ts +55 -1
  21. package/dist_ts_web/elements/sio-combox.js +458 -261
  22. package/dist_ts_web/elements/sio-composer.d.ts +48 -0
  23. package/dist_ts_web/elements/sio-composer.js +357 -0
  24. package/dist_ts_web/elements/sio-conversation-selector.d.ts +41 -1
  25. package/dist_ts_web/elements/sio-conversation-selector.js +426 -238
  26. package/dist_ts_web/elements/sio-conversation-view.d.ts +34 -6
  27. package/dist_ts_web/elements/sio-conversation-view.js +738 -564
  28. package/dist_ts_web/elements/sio-dropdown-menu.d.ts +6 -6
  29. package/dist_ts_web/elements/sio-dropdown-menu.js +223 -169
  30. package/dist_ts_web/elements/sio-fab.d.ts +23 -5
  31. package/dist_ts_web/elements/sio-fab.js +259 -235
  32. package/dist_ts_web/elements/sio-glass-toolbar.d.ts +22 -0
  33. package/dist_ts_web/elements/sio-glass-toolbar.js +178 -0
  34. package/dist_ts_web/elements/sio-icon.d.ts +1 -0
  35. package/dist_ts_web/elements/sio-icon.js +254 -94
  36. package/dist_ts_web/elements/sio-image-lightbox.d.ts +3 -4
  37. package/dist_ts_web/elements/sio-image-lightbox.js +282 -191
  38. package/dist_ts_web/elements/sio-inset-list.d.ts +28 -0
  39. package/dist_ts_web/elements/sio-inset-list.js +191 -0
  40. package/dist_ts_web/elements/sio-kbd.d.ts +16 -0
  41. package/dist_ts_web/elements/sio-kbd.js +117 -0
  42. package/dist_ts_web/elements/sio-list-row.d.ts +55 -0
  43. package/dist_ts_web/elements/sio-list-row.js +462 -0
  44. package/dist_ts_web/elements/sio-logo-render.d.ts +42 -0
  45. package/dist_ts_web/elements/sio-logo-render.js +306 -0
  46. package/dist_ts_web/elements/sio-logo.d.ts +64 -0
  47. package/dist_ts_web/elements/sio-logo.js +393 -0
  48. package/dist_ts_web/elements/sio-message-input.d.ts +25 -4
  49. package/dist_ts_web/elements/sio-message-input.js +231 -226
  50. package/dist_ts_web/elements/sio-paper-thumb.d.ts +24 -0
  51. package/dist_ts_web/elements/sio-paper-thumb.js +293 -0
  52. package/dist_ts_web/elements/sio-pdf-viewer.d.ts +2 -0
  53. package/dist_ts_web/elements/sio-pdf-viewer.js +344 -244
  54. package/dist_ts_web/elements/sio-segmented.d.ts +32 -0
  55. package/dist_ts_web/elements/sio-segmented.js +217 -0
  56. package/dist_ts_web/elements/sio-step-timeline.d.ts +28 -0
  57. package/dist_ts_web/elements/sio-step-timeline.js +222 -0
  58. package/dist_ts_web/elements/sio-tab-bar.d.ts +36 -0
  59. package/dist_ts_web/elements/sio-tab-bar.js +232 -0
  60. package/dist_ts_web/elements/sio-unread-badge.d.ts +25 -0
  61. package/dist_ts_web/elements/sio-unread-badge.js +203 -0
  62. package/dist_ts_web/elements/sio-waveform.d.ts +34 -0
  63. package/dist_ts_web/elements/sio-waveform.js +304 -0
  64. package/dist_ts_web/elements/tokens.d.ts +70 -0
  65. package/dist_ts_web/elements/tokens.js +277 -0
  66. package/dist_ts_web/pages/firstpage.d.ts +5 -0
  67. package/dist_ts_web/pages/firstpage.js +21 -2
  68. package/dist_ts_web/pages/mainpage.d.ts +6 -0
  69. package/dist_ts_web/pages/mainpage.js +111 -39
  70. package/{license → license.md} +3 -1
  71. package/package.json +23 -29
  72. package/readme.hints.md +82 -0
  73. package/readme.md +113 -146
  74. package/ts_web/00_commitinfo_data.ts +2 -2
  75. package/ts_web/elements/index.ts +31 -7
  76. package/ts_web/elements/sio-ai-card.ts +175 -0
  77. package/ts_web/elements/sio-avatar.ts +295 -0
  78. package/ts_web/elements/sio-bucket-header.ts +157 -0
  79. package/ts_web/elements/sio-button.ts +149 -127
  80. package/ts_web/elements/sio-channel-rail.ts +256 -0
  81. package/ts_web/elements/sio-chip.ts +134 -0
  82. package/ts_web/elements/sio-cmdk.ts +461 -0
  83. package/ts_web/elements/sio-combox.ts +327 -204
  84. package/ts_web/elements/sio-composer.ts +306 -0
  85. package/ts_web/elements/sio-conversation-selector.ts +350 -228
  86. package/ts_web/elements/sio-conversation-view.ts +573 -457
  87. package/ts_web/elements/sio-dropdown-menu.ts +112 -130
  88. package/ts_web/elements/sio-fab.ts +192 -227
  89. package/ts_web/elements/sio-glass-toolbar.ts +140 -0
  90. package/ts_web/elements/sio-icon.ts +147 -25
  91. package/ts_web/elements/sio-image-lightbox.ts +62 -64
  92. package/ts_web/elements/sio-inset-list.ts +140 -0
  93. package/ts_web/elements/sio-kbd.ts +80 -0
  94. package/ts_web/elements/sio-list-row.ts +393 -0
  95. package/ts_web/elements/sio-logo-render.ts +367 -0
  96. package/ts_web/elements/sio-logo.ts +311 -0
  97. package/ts_web/elements/sio-message-input.ts +142 -221
  98. package/ts_web/elements/sio-paper-thumb.ts +244 -0
  99. package/ts_web/elements/sio-pdf-viewer.ts +58 -60
  100. package/ts_web/elements/sio-segmented.ts +183 -0
  101. package/ts_web/elements/sio-step-timeline.ts +179 -0
  102. package/ts_web/elements/sio-tab-bar.ts +195 -0
  103. package/ts_web/elements/sio-unread-badge.ts +147 -0
  104. package/ts_web/elements/sio-waveform.ts +239 -0
  105. package/ts_web/elements/tokens.ts +297 -0
  106. package/ts_web/pages/firstpage.ts +20 -1
  107. package/ts_web/pages/mainpage.ts +111 -39
  108. package/dist_bundle/bundle.js.map +0 -7
  109. package/dist_ts_web/elements/00colors.d.ts +0 -108
  110. package/dist_ts_web/elements/00colors.js +0 -137
  111. package/dist_ts_web/elements/00fonts.d.ts +0 -60
  112. package/dist_ts_web/elements/00fonts.js +0 -129
  113. package/dist_ts_web/elements/00tokens.d.ts +0 -123
  114. package/dist_ts_web/elements/00tokens.js +0 -176
  115. package/dist_ts_web/elements/sio-conversation-list.d.ts +0 -20
  116. package/dist_ts_web/elements/sio-conversation-list.js +0 -495
  117. package/dist_ts_web/elements/sio-recorder.d.ts +0 -60
  118. package/dist_ts_web/elements/sio-recorder.js +0 -173
  119. package/dist_ts_web/elements/sio-subwidget-conversations.d.ts +0 -16
  120. package/dist_ts_web/elements/sio-subwidget-conversations.js +0 -350
  121. package/dist_ts_web/elements/sio-subwidget-onboardme.d.ts +0 -7
  122. package/dist_ts_web/elements/sio-subwidget-onboardme.js +0 -243
  123. package/dist_watch/bundle.js +0 -81694
  124. package/dist_watch/bundle.js.map +0 -7
  125. package/dist_watch/index.html +0 -25
  126. package/npmextra.json +0 -19
  127. package/ts_web/elements/00colors.ts +0 -164
  128. package/ts_web/elements/00fonts.ts +0 -145
  129. package/ts_web/elements/00tokens.ts +0 -197
  130. package/ts_web/elements/sio-recorder.ts +0 -213
@@ -0,0 +1,134 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ property,
5
+ customElement,
6
+ css,
7
+ type TemplateResult,
8
+ } from '@design.estate/dees-element';
9
+
10
+ import { sioElementStyles } from './tokens.js';
11
+ import './sio-icon.js';
12
+
13
+ export type TSioChipVariant = 'channel' | 'lane' | 'status' | 'generic';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'sio-chip': SioChip;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * variant:value → palette token.
23
+ * channel mail | chats | calls | letters
24
+ * lane feed | paper | people
25
+ * status now | today | later | done (unified-feed triage buckets)
26
+ */
27
+ const chipPalette: Record<string, string> = {
28
+ 'channel:mail': 'var(--sio-ch-mail)',
29
+ 'channel:chats': 'var(--sio-ch-chats)',
30
+ 'channel:calls': 'var(--sio-ch-calls)',
31
+ 'channel:letters': 'var(--sio-ch-letters)',
32
+ 'lane:feed': 'var(--sio-lane-feed)',
33
+ 'lane:paper': 'var(--sio-lane-paper)',
34
+ 'lane:people': 'var(--sio-lane-people)',
35
+ 'status:now': 'var(--sio-bucket-now)',
36
+ 'status:today': 'var(--sio-bucket-today)',
37
+ 'status:later': 'var(--sio-bucket-later)',
38
+ 'status:done': 'var(--sio-bucket-done)',
39
+ };
40
+
41
+ /**
42
+ * sio-chip — uppercase 11px/600 label on a 14% color-mix tint of the
43
+ * palette color (v7 `.sio-lane-chip` and the channel chips in list rows).
44
+ */
45
+ @customElement('sio-chip')
46
+ export class SioChip extends DeesElement {
47
+ public static demo = () => html`
48
+ <div style="display: flex; flex-direction: column; gap: 14px;">
49
+ <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
50
+ <sio-chip variant="channel" value="mail" icon="envelope">Mail</sio-chip>
51
+ <sio-chip variant="channel" value="chats" icon="message-circle">Chat</sio-chip>
52
+ <sio-chip variant="channel" value="calls" icon="phone">Call</sio-chip>
53
+ <sio-chip variant="channel" value="letters" icon="file-text">Letter</sio-chip>
54
+ </div>
55
+ <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
56
+ <sio-chip variant="lane" value="feed">Feed</sio-chip>
57
+ <sio-chip variant="lane" value="paper">Paper trail</sio-chip>
58
+ <sio-chip variant="lane" value="people">People</sio-chip>
59
+ </div>
60
+ <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
61
+ <sio-chip variant="status" value="now">Now</sio-chip>
62
+ <sio-chip variant="status" value="today">Today</sio-chip>
63
+ <sio-chip variant="status" value="later">Later</sio-chip>
64
+ <sio-chip variant="status" value="done">Done</sio-chip>
65
+ </div>
66
+ <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
67
+ <sio-chip icon="sparkles">AI summary</sio-chip>
68
+ <sio-chip color="var(--sio-ch-letters)">Suggested</sio-chip>
69
+ </div>
70
+ </div>
71
+ `;
72
+
73
+ @property({ type: String })
74
+ public accessor variant: TSioChipVariant = 'generic';
75
+
76
+ /** palette key within the variant, e.g. "mail", "feed", "now" */
77
+ @property({ type: String })
78
+ public accessor value = '';
79
+
80
+ /** custom CSS color for variant="generic"; defaults to the system tint */
81
+ @property({ type: String })
82
+ public accessor color = '';
83
+
84
+ /** optional leading glyph (sio-icon name) */
85
+ @property({ type: String })
86
+ public accessor icon = '';
87
+
88
+ public static styles = [
89
+ ...sioElementStyles,
90
+ css`
91
+ :host {
92
+ display: inline-flex;
93
+ flex-shrink: 0;
94
+ }
95
+
96
+ /* v7 .sio-lane-chip */
97
+ .chip {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ gap: 6px;
101
+ padding: 3px 9px;
102
+ border-radius: var(--sio-radius-sm);
103
+ background: color-mix(in oklab, var(--chip-color) 14%, transparent);
104
+ color: var(--chip-color);
105
+ font-family: var(--sio-font-sf);
106
+ font-size: 11px;
107
+ font-weight: 600;
108
+ letter-spacing: 0.04em;
109
+ text-transform: uppercase;
110
+ white-space: nowrap;
111
+ user-select: none;
112
+ }
113
+
114
+ sio-icon {
115
+ flex: 0 0 auto;
116
+ }
117
+ `,
118
+ ];
119
+
120
+ private get resolvedColor(): string {
121
+ return (
122
+ chipPalette[`${this.variant}:${this.value}`] || this.color || 'var(--sio-tint)'
123
+ );
124
+ }
125
+
126
+ public render(): TemplateResult {
127
+ return html`
128
+ <span class="chip" style="--chip-color: ${this.resolvedColor};">
129
+ ${this.icon ? html`<sio-icon icon=${this.icon} size="10"></sio-icon>` : ''}
130
+ <slot></slot>
131
+ </span>
132
+ `;
133
+ }
134
+ }
@@ -0,0 +1,461 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ css,
5
+ customElement,
6
+ property,
7
+ state,
8
+ type TemplateResult,
9
+ } from '@design.estate/dees-element';
10
+ import { sioElementStyles } from './tokens.js';
11
+ import './sio-icon.js';
12
+ import './sio-kbd.js';
13
+
14
+ /**
15
+ * sio-cmdk — v7 command palette overlay (SocialIODesktop.jsx `SIOCmdK`
16
+ * + socialio.css `.sio-cmdk*`).
17
+ *
18
+ * Scrim rgba(0,0,0,.32) + blur(6px), 640px panel at 14vh on --sio-surface,
19
+ * radius 14, deep shadow + 0.5px hairline ring. Search row with magnifier,
20
+ * query input and an `esc` kbd hint; grouped results with uppercase 11/600
21
+ * section headers; rows of 18px glyph + 14px label + right kbd chord.
22
+ * Active row = tint-soft wash + tint text.
23
+ *
24
+ * Keyboard: ↑/↓ move (wrapping), Enter runs the active item, Esc closes.
25
+ * Typing filters items across all sections; empty sections disappear.
26
+ *
27
+ * Events: `cmdk-run` { id } · `cmdk-close` (both bubble + composed).
28
+ * The host owns the `open` flag — the palette never hides itself.
29
+ */
30
+
31
+ export interface ISioCmdkItem {
32
+ id: string;
33
+ /** lucide icon name rendered through sio-icon (18px glyph slot). */
34
+ icon: string;
35
+ label: string;
36
+ /** keyboard chord hint, e.g. '⇧R' or 'G I'. */
37
+ shortcut?: string;
38
+ }
39
+
40
+ export interface ISioCmdkSection {
41
+ name: string;
42
+ items: ISioCmdkItem[];
43
+ }
44
+
45
+ declare global {
46
+ interface HTMLElementTagNameMap {
47
+ 'sio-cmdk': SioCmdk;
48
+ }
49
+ }
50
+
51
+ @customElement('sio-cmdk')
52
+ export class SioCmdk extends DeesElement {
53
+ public static demo = () => {
54
+ const demoSections: ISioCmdkSection[] = [
55
+ {
56
+ name: 'Actions',
57
+ items: [
58
+ { id: 'action.reply', icon: 'reply', label: 'Reply', shortcut: 'R' },
59
+ { id: 'action.reply-all', icon: 'reply-all', label: 'Reply all', shortcut: '⇧R' },
60
+ { id: 'action.forward', icon: 'forward', label: 'Forward', shortcut: 'F' },
61
+ { id: 'action.archive', icon: 'archive', label: 'Archive', shortcut: 'E' },
62
+ ],
63
+ },
64
+ {
65
+ name: 'Jump to',
66
+ items: [
67
+ { id: 'goto.inbox-people', icon: 'inbox', label: 'Inbox · People', shortcut: 'G I' },
68
+ { id: 'goto.starred', icon: 'star', label: 'Starred', shortcut: 'G S' },
69
+ { id: 'goto.sent', icon: 'send', label: 'Sent', shortcut: 'G T' },
70
+ ],
71
+ },
72
+ {
73
+ name: 'Snooze',
74
+ items: [
75
+ { id: 'snooze.tomorrow', icon: 'sunset', label: 'Tomorrow morning', shortcut: 'H 1' },
76
+ { id: 'snooze.evening', icon: 'clock', label: 'This evening', shortcut: 'H 2' },
77
+ { id: 'snooze.next-week', icon: 'calendar', label: 'Next week', shortcut: 'H 3' },
78
+ ],
79
+ },
80
+ ];
81
+ // The wrapper's transform contains the fixed-position overlay so each
82
+ // frame keeps its own palette (same trick as the prototype's mac window).
83
+ const frameStyle = `
84
+ position: relative; transform: translateZ(0); overflow: hidden;
85
+ border-radius: 12px; background: var(--sio-bg);
86
+ box-shadow: 0 0 0 0.5px var(--sio-separator);
87
+ `;
88
+ const mockRow = (from: string, subject: string, time: string) => html`
89
+ <div style="display: flex; align-items: center; gap: 12px; padding: 12px 18px; border-bottom: 0.5px solid var(--sio-separator);">
90
+ <span style="width: 8px; height: 8px; border-radius: 50%; background: var(--sio-tint); flex-shrink: 0;"></span>
91
+ <span style="width: 140px; font-size: 14px; font-weight: 600; color: var(--sio-label); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${from}</span>
92
+ <span style="flex: 1; font-size: 14px; color: var(--sio-label-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${subject}</span>
93
+ <span style="font-size: 12px; color: var(--sio-label-3); font-variant-numeric: tabular-nums;">${time}</span>
94
+ </div>
95
+ `;
96
+ const mockWindow = html`
97
+ <div style="height: 44px; display: flex; align-items: center; gap: 8px; padding: 0 18px; border-bottom: 0.5px solid var(--sio-separator); background: var(--sio-surface);">
98
+ <span style="width: 12px; height: 12px; border-radius: 50%; background: #FF5F57;"></span>
99
+ <span style="width: 12px; height: 12px; border-radius: 50%; background: #FEBC2E;"></span>
100
+ <span style="width: 12px; height: 12px; border-radius: 50%; background: #28C840;"></span>
101
+ <span style="margin-left: 10px; font-size: 15px; font-weight: 700; letter-spacing: -0.3px; color: var(--sio-label);">People</span>
102
+ <span style="font-size: 12px; font-weight: 500; color: var(--sio-label-3);">3 unread</span>
103
+ </div>
104
+ ${mockRow('Maya Lindqvist', 'Launch copy — final pass before Thursday', '09:41')}
105
+ ${mockRow('Jonas Weber', 'Re: Offsite agenda + travel booking', 'Yesterday')}
106
+ ${mockRow('Priya Nair', 'Voicemail transcript: callback re: contract', 'Tue')}
107
+ `;
108
+ return html`
109
+ <div style="display: flex; flex-direction: column; gap: 28px; font-family: var(--sio-font-sf);">
110
+ <div>
111
+ <div style="font-size: 12px; font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase; color: var(--sio-label-3); margin: 0 0 10px 2px;">
112
+ ⌘K over the desktop — grouped results, first row active
113
+ </div>
114
+ <div style="${frameStyle} height: 480px;">
115
+ <div>${mockWindow}</div>
116
+ <sio-cmdk
117
+ open
118
+ .sections=${demoSections}
119
+ @cmdk-run=${(eventArg: CustomEvent<{ id: string }>) => console.log('cmdk-run', eventArg.detail.id)}
120
+ @cmdk-close=${() => console.log('cmdk-close')}
121
+ ></sio-cmdk>
122
+ </div>
123
+ </div>
124
+ <div>
125
+ <div style="font-size: 12px; font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase; color: var(--sio-label-3); margin: 0 0 10px 2px;">
126
+ Typed query "re" — sections filter down, empty groups drop out
127
+ </div>
128
+ <div style="${frameStyle} height: 340px;">
129
+ <div>${mockWindow}</div>
130
+ <sio-cmdk open query="re" .sections=${demoSections}></sio-cmdk>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ `;
135
+ };
136
+
137
+ // --------------------------------------------------------------------- api
138
+
139
+ /** Grouped commands; filtered live against `query`. */
140
+ @property({ type: Array })
141
+ public accessor sections: ISioCmdkSection[] = [];
142
+
143
+ /** Host-owned visibility flag (wire ⌘K / cmdk-close in the app shell). */
144
+ @property({ type: Boolean, reflect: true })
145
+ public accessor open = false;
146
+
147
+ /** Current filter text — settable to preseed, updated as the user types. */
148
+ @property({ type: String })
149
+ public accessor query = '';
150
+
151
+ @property({ type: String })
152
+ public accessor placeholder = 'Type a command…';
153
+
154
+ @state()
155
+ private accessor activeIndex = 0;
156
+
157
+ // ------------------------------------------------------------------ styles
158
+
159
+ public static styles = [
160
+ ...sioElementStyles,
161
+ css`
162
+ :host {
163
+ display: none;
164
+ }
165
+ :host([open]) {
166
+ display: block;
167
+ position: fixed;
168
+ inset: 0;
169
+ z-index: 100;
170
+ }
171
+ /* scrim + shadow values are spec'd black in the v7 prototype
172
+ (socialio.css .sio-cmdk*) — they are not theme surfaces. */
173
+ .backdrop {
174
+ position: absolute;
175
+ inset: 0;
176
+ background: rgba(0, 0, 0, 0.32);
177
+ backdrop-filter: blur(6px);
178
+ -webkit-backdrop-filter: blur(6px);
179
+ display: flex;
180
+ justify-content: center;
181
+ align-items: flex-start;
182
+ padding-top: 14vh;
183
+ }
184
+ .panel {
185
+ width: 640px;
186
+ max-width: calc(100% - 80px);
187
+ background: var(--sio-surface);
188
+ border-radius: var(--sio-radius-lg);
189
+ box-shadow:
190
+ 0 30px 80px rgba(0, 0, 0, 0.35),
191
+ 0 0 0 var(--sio-hairline) var(--sio-separator);
192
+ overflow: hidden;
193
+ }
194
+
195
+ /* search row */
196
+ .search-row {
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 10px;
200
+ padding: 14px 16px;
201
+ border-bottom: var(--sio-hairline) solid var(--sio-separator);
202
+ }
203
+ .search-icon {
204
+ color: var(--sio-label-3);
205
+ flex-shrink: 0;
206
+ }
207
+ .search-input {
208
+ flex: 1;
209
+ min-width: 0;
210
+ padding: 0;
211
+ border: none;
212
+ outline: none;
213
+ background: transparent;
214
+ font-family: var(--sio-font-sf);
215
+ font-size: 15px;
216
+ line-height: 20px;
217
+ letter-spacing: -0.24px;
218
+ color: var(--sio-label);
219
+ }
220
+ .search-input::placeholder {
221
+ color: var(--sio-label-4);
222
+ }
223
+ .esc-hint {
224
+ flex-shrink: 0;
225
+ cursor: pointer;
226
+ }
227
+
228
+ /* grouped results */
229
+ .results {
230
+ padding: 6px 0;
231
+ max-height: 360px;
232
+ overflow-y: auto;
233
+ overscroll-behavior: contain;
234
+ }
235
+ .section {
236
+ padding: 4px 0;
237
+ }
238
+ .section-header {
239
+ padding: 6px 18px;
240
+ font-size: 11px;
241
+ line-height: 13px;
242
+ font-weight: 600;
243
+ letter-spacing: 0.06px;
244
+ text-transform: uppercase;
245
+ color: var(--sio-label-3);
246
+ }
247
+ .row {
248
+ display: flex;
249
+ align-items: center;
250
+ gap: 12px;
251
+ padding: 8px 18px;
252
+ color: var(--sio-label);
253
+ cursor: default;
254
+ transition: background 0.08s ease;
255
+ }
256
+ .row.active {
257
+ background: var(--sio-tint-soft);
258
+ color: var(--sio-tint);
259
+ }
260
+ .glyph {
261
+ width: 18px;
262
+ display: inline-flex;
263
+ align-items: center;
264
+ justify-content: center;
265
+ flex-shrink: 0;
266
+ color: inherit;
267
+ }
268
+ .label {
269
+ flex: 1;
270
+ min-width: 0;
271
+ font-size: 14px;
272
+ line-height: 19px;
273
+ letter-spacing: -0.15px;
274
+ white-space: nowrap;
275
+ overflow: hidden;
276
+ text-overflow: ellipsis;
277
+ }
278
+ .row sio-kbd {
279
+ flex-shrink: 0;
280
+ }
281
+ .empty {
282
+ padding: 18px;
283
+ text-align: center;
284
+ font-size: 14px;
285
+ color: var(--sio-label-3);
286
+ }
287
+ `,
288
+ ];
289
+
290
+ // --------------------------------------------------------------- filtering
291
+
292
+ private getFilteredSections(): ISioCmdkSection[] {
293
+ const normalizedQuery = this.query.trim().toLowerCase();
294
+ if (!normalizedQuery) {
295
+ return this.sections;
296
+ }
297
+ return this.sections
298
+ .map((sectionArg) => ({
299
+ ...sectionArg,
300
+ items: sectionArg.items.filter((itemArg) =>
301
+ itemArg.label.toLowerCase().includes(normalizedQuery),
302
+ ),
303
+ }))
304
+ .filter((sectionArg) => sectionArg.items.length > 0);
305
+ }
306
+
307
+ private getFlatItems(): ISioCmdkItem[] {
308
+ return this.getFilteredSections().flatMap((sectionArg) => sectionArg.items);
309
+ }
310
+
311
+ // -------------------------------------------------------------- lifecycle
312
+
313
+ public willUpdate(): void {
314
+ // keep the active row inside the (possibly shrunken) filtered list
315
+ const totalItems = this.getFlatItems().length;
316
+ if (this.activeIndex >= totalItems) {
317
+ this.activeIndex = totalItems > 0 ? totalItems - 1 : 0;
318
+ }
319
+ }
320
+
321
+ public updated(changedPropertiesArg: Map<PropertyKey, unknown>): void {
322
+ if (changedPropertiesArg.has('open') && this.open) {
323
+ const inputElement = this.shadowRoot?.querySelector<HTMLInputElement>('.search-input');
324
+ inputElement?.focus({ preventScroll: true });
325
+ }
326
+ }
327
+
328
+ // ------------------------------------------------------------ interaction
329
+
330
+ private handleInput(eventArg: Event): void {
331
+ this.query = (eventArg.target as HTMLInputElement).value;
332
+ this.activeIndex = 0;
333
+ }
334
+
335
+ private handleKeydown(eventArg: KeyboardEvent): void {
336
+ const flatItems = this.getFlatItems();
337
+ switch (eventArg.key) {
338
+ case 'ArrowDown':
339
+ eventArg.preventDefault();
340
+ if (flatItems.length > 0) {
341
+ this.activeIndex = (this.activeIndex + 1) % flatItems.length;
342
+ this.scrollActiveRowIntoView();
343
+ }
344
+ break;
345
+ case 'ArrowUp':
346
+ eventArg.preventDefault();
347
+ if (flatItems.length > 0) {
348
+ this.activeIndex = (this.activeIndex - 1 + flatItems.length) % flatItems.length;
349
+ this.scrollActiveRowIntoView();
350
+ }
351
+ break;
352
+ case 'Enter':
353
+ eventArg.preventDefault();
354
+ if (flatItems[this.activeIndex]) {
355
+ this.runItem(flatItems[this.activeIndex]);
356
+ }
357
+ break;
358
+ case 'Escape':
359
+ eventArg.preventDefault();
360
+ this.close();
361
+ break;
362
+ }
363
+ }
364
+
365
+ private async scrollActiveRowIntoView(): Promise<void> {
366
+ await this.updateComplete;
367
+ this.shadowRoot?.querySelector('.row.active')?.scrollIntoView({ block: 'nearest' });
368
+ }
369
+
370
+ private handleBackdropClick(eventArg: MouseEvent): void {
371
+ if (eventArg.target === eventArg.currentTarget) {
372
+ this.close();
373
+ }
374
+ }
375
+
376
+ private runItem(itemArg: ISioCmdkItem): void {
377
+ this.dispatchEvent(
378
+ new CustomEvent<{ id: string }>('cmdk-run', {
379
+ detail: { id: itemArg.id },
380
+ bubbles: true,
381
+ composed: true,
382
+ }),
383
+ );
384
+ }
385
+
386
+ private close(): void {
387
+ this.dispatchEvent(
388
+ new CustomEvent('cmdk-close', {
389
+ bubbles: true,
390
+ composed: true,
391
+ }),
392
+ );
393
+ }
394
+
395
+ // ----------------------------------------------------------------- render
396
+
397
+ public render(): TemplateResult {
398
+ if (!this.open) {
399
+ return html``;
400
+ }
401
+ const filteredSections = this.getFilteredSections();
402
+ let runningIndex = -1;
403
+ return html`
404
+ <div class="backdrop" @click=${this.handleBackdropClick} @keydown=${this.handleKeydown}>
405
+ <div class="panel" role="dialog" aria-modal="true" aria-label="Command palette">
406
+ <div class="search-row">
407
+ <sio-icon class="search-icon" icon="search" size="16"></sio-icon>
408
+ <input
409
+ class="search-input"
410
+ type="text"
411
+ .value=${this.query}
412
+ placeholder=${this.placeholder}
413
+ spellcheck="false"
414
+ autocomplete="off"
415
+ aria-label="Search commands"
416
+ @input=${this.handleInput}
417
+ />
418
+ <sio-kbd class="esc-hint" @click=${this.close}>esc</sio-kbd>
419
+ </div>
420
+ <div class="results" role="listbox" aria-label="Commands">
421
+ ${filteredSections.length === 0
422
+ ? html`<div class="empty">No matching commands</div>`
423
+ : filteredSections.map(
424
+ (sectionArg) => html`
425
+ <div class="section">
426
+ <div class="section-header">${sectionArg.name}</div>
427
+ ${sectionArg.items.map((itemArg) => {
428
+ runningIndex += 1;
429
+ const itemIndex = runningIndex;
430
+ const isActive = itemIndex === this.activeIndex;
431
+ return html`
432
+ <div
433
+ class="row ${isActive ? 'active' : ''}"
434
+ role="option"
435
+ aria-selected=${isActive ? 'true' : 'false'}
436
+ @pointerenter=${() => {
437
+ this.activeIndex = itemIndex;
438
+ }}
439
+ @click=${() => this.runItem(itemArg)}
440
+ >
441
+ <span class="glyph">
442
+ ${itemArg.icon
443
+ ? html`<sio-icon icon=${itemArg.icon} size="16"></sio-icon>`
444
+ : html``}
445
+ </span>
446
+ <span class="label">${itemArg.label}</span>
447
+ ${itemArg.shortcut
448
+ ? html`<sio-kbd>${itemArg.shortcut}</sio-kbd>`
449
+ : html``}
450
+ </div>
451
+ `;
452
+ })}
453
+ </div>
454
+ `,
455
+ )}
456
+ </div>
457
+ </div>
458
+ </div>
459
+ `;
460
+ }
461
+ }