@kubex/zinc 1.1.92 → 1.1.95

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 (53) hide show
  1. package/dist/custom-elements.json +2880 -453
  2. package/dist/vscode.html-custom-data.json +209 -22
  3. package/dist/web-types.json +452 -39
  4. package/dist/zn.d.ts +633 -61
  5. package/dist/zn.min.css +1 -1
  6. package/dist/zn.min.js +865 -560
  7. package/docs/pages/components/page-builder.md +143 -19
  8. package/docs/pages/components/schedule-builder.md +345 -0
  9. package/docs/pages/components/slash-menu.md +132 -6
  10. package/docs/pages/components/textarea.md +16 -0
  11. package/package.json +1 -1
  12. package/scss/_root.scss +7 -1
  13. package/src/components/alert/alert.scss +9 -13
  14. package/src/components/button/button.scss +5 -2
  15. package/src/components/chip/chip.scss +1 -1
  16. package/src/components/icon-picker/icon-picker.component.ts +1 -1
  17. package/src/components/inline-edit/inline-edit.component.ts +6 -1
  18. package/src/components/input/input.component.ts +12 -2
  19. package/src/components/linked-select/linked-select.component.ts +22 -5
  20. package/src/components/page/page.scss +20 -9
  21. package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts +16 -9
  22. package/src/components/page-builder/modules/page-section-card/page-section-card.scss +13 -0
  23. package/src/components/page-builder/modules/page-section-card/page-section-card.test.ts +9 -0
  24. package/src/components/page-builder/page-builder.component.ts +535 -232
  25. package/src/components/page-builder/page-builder.scss +230 -19
  26. package/src/components/page-builder/page-builder.test.ts +790 -110
  27. package/src/components/page-builder/page-tree.test.ts +483 -0
  28. package/src/components/page-builder/page-tree.ts +329 -0
  29. package/src/components/page-builder/page.types.ts +98 -10
  30. package/src/components/page-nav/page-nav.scss +9 -1
  31. package/src/components/panel/panel.component.ts +5 -1
  32. package/src/components/priority-list/priority-list.component.ts +1 -0
  33. package/src/components/priority-list/priority-list.scss +2 -1
  34. package/src/components/remarkd-editor/remarkd-editor.component.ts +198 -9
  35. package/src/components/remarkd-editor/remarkd-editor.scss +81 -0
  36. package/src/components/remarkd-editor/remarkd-editor.test.ts +179 -0
  37. package/src/components/schedule-builder/index.ts +12 -0
  38. package/src/components/schedule-builder/schedule-builder.component.ts +1543 -0
  39. package/src/components/schedule-builder/schedule-builder.scss +448 -0
  40. package/src/components/schedule-builder/schedule-builder.test.ts +344 -0
  41. package/src/components/settings-container/settings-container.scss +2 -1
  42. package/src/components/slash-item/slash-item.component.ts +1 -1
  43. package/src/components/slash-menu/slash-menu-items.ts +48 -0
  44. package/src/components/slash-menu/slash-menu.component.ts +134 -27
  45. package/src/components/slash-menu/slash-menu.scss +90 -12
  46. package/src/components/slash-menu/slash-menu.test.ts +107 -0
  47. package/src/components/textarea/textarea.component.ts +12 -2
  48. package/src/components/textarea/textarea.test.ts +2 -2
  49. package/src/components/toggle/toggle.component.ts +2 -1
  50. package/src/components/translations/translations.component.ts +5 -1
  51. package/src/zinc.ts +1 -0
  52. package/docs/superpowers/plans/2026-08-03-theme-editor.md +0 -1536
  53. package/docs/superpowers/specs/2026-08-03-theme-editor-design.md +0 -327
@@ -0,0 +1,344 @@
1
+ import '../../../dist/zn.min.js';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type {ScheduleValue} from './schedule-builder.component';
4
+ import type ZnScheduleBuilder from './schedule-builder.component';
5
+
6
+ const parseSchedule = (value: string): ScheduleValue => JSON.parse(value) as ScheduleValue;
7
+
8
+ const scheduleValue = JSON.stringify({
9
+ days: {
10
+ mon: [{start: '08:00', end: '18:00'}],
11
+ tue: [], wed: [], thu: [], fri: [], sat: [], sun: []
12
+ },
13
+ exceptions: []
14
+ });
15
+
16
+ describe('<zn-schedule-builder>', () => {
17
+ it('should render a component', async () => {
18
+ const el = await fixture(html`
19
+ <zn-schedule-builder></zn-schedule-builder>`);
20
+
21
+ expect(el).to.exist;
22
+ });
23
+
24
+ it('should expose an empty schedule by default', async () => {
25
+ const el: ZnScheduleBuilder = await fixture(html`
26
+ <zn-schedule-builder></zn-schedule-builder>`);
27
+
28
+ expect(el.schedule.days.mon).to.deep.equal([]);
29
+ expect(el.schedule.exceptions).to.deep.equal([]);
30
+ });
31
+
32
+ it('should parse a JSON value into the schedule', async () => {
33
+ const el: ZnScheduleBuilder = await fixture(html`
34
+ <zn-schedule-builder value="${scheduleValue}"></zn-schedule-builder>`);
35
+
36
+ expect(el.getDay('mon')).to.deep.equal([{start: '08:00', end: '18:00'}]);
37
+ expect(el.getDay('sun')).to.deep.equal([]);
38
+ });
39
+
40
+ it('should accept the shorthand range syntax', async () => {
41
+ const el: ZnScheduleBuilder = await fixture(html`
42
+ <zn-schedule-builder value='{"mon":["09:00-17:00"]}'></zn-schedule-builder>`);
43
+
44
+ expect(el.getDay('mon')).to.deep.equal([{start: '09:00', end: '17:00'}]);
45
+ });
46
+
47
+ it('should serialise assignments back into the value', async () => {
48
+ const el: ZnScheduleBuilder = await fixture(html`
49
+ <zn-schedule-builder></zn-schedule-builder>`);
50
+
51
+ el.setDay('tue', [{start: '10:00', end: '12:00'}]);
52
+ await el.updateComplete;
53
+
54
+ expect(parseSchedule(el.value).days.tue).to.deep.equal([{start: '10:00', end: '12:00'}]);
55
+ });
56
+
57
+ it('should merge overlapping and touching ranges', async () => {
58
+ const el: ZnScheduleBuilder = await fixture(html`
59
+ <zn-schedule-builder></zn-schedule-builder>`);
60
+
61
+ el.setDay('wed', [{start: '13:00', end: '18:00'}, {start: '08:00', end: '13:00'}]);
62
+ await el.updateComplete;
63
+
64
+ expect(el.getDay('wed')).to.deep.equal([{start: '08:00', end: '18:00'}]);
65
+ });
66
+
67
+ it('should drop invalid ranges', async () => {
68
+ const el: ZnScheduleBuilder = await fixture(html`
69
+ <zn-schedule-builder></zn-schedule-builder>`);
70
+
71
+ el.setDay('thu', [{start: '18:00', end: '09:00'}, {start: 'nope', end: '10:00'}]);
72
+ await el.updateComplete;
73
+
74
+ expect(el.getDay('thu')).to.deep.equal([]);
75
+ });
76
+
77
+ it('should emit zn-change when the schedule changes', async () => {
78
+ const el: ZnScheduleBuilder = await fixture(html`
79
+ <zn-schedule-builder></zn-schedule-builder>`);
80
+
81
+ let changed = false;
82
+ el.addEventListener('zn-change', () => (changed = true));
83
+
84
+ el.setDay('fri', [{start: '09:00', end: '17:00'}]);
85
+ await el.updateComplete;
86
+
87
+ expect(changed).to.be.true;
88
+ });
89
+
90
+ it('should render the list view when view is form', async () => {
91
+ const el: ZnScheduleBuilder = await fixture(html`
92
+ <zn-schedule-builder view="form" value="${scheduleValue}"></zn-schedule-builder>`);
93
+
94
+ expect(el.shadowRoot?.querySelector('.list')).to.exist;
95
+ expect(el.shadowRoot?.querySelector('.calendar')).to.not.exist;
96
+ });
97
+
98
+ it('should render the calendar view by default', async () => {
99
+ const el: ZnScheduleBuilder = await fixture(html`
100
+ <zn-schedule-builder value="${scheduleValue}"></zn-schedule-builder>`);
101
+
102
+ expect(el.shadowRoot?.querySelector('.calendar')).to.exist;
103
+ expect(el.shadowRoot?.querySelectorAll('.calendar__col').length).to.equal(7);
104
+ });
105
+
106
+ it('should shade hours a weekday exception removes', async () => {
107
+ const el: ZnScheduleBuilder = await fixture(html`
108
+ <zn-schedule-builder value='{
109
+ "days": {"fri": ["08:00-18:00"]},
110
+ "exceptions": [{"label": "Summer Fridays", "days": ["fri"], "ranges": ["08:00-16:00"]}]
111
+ }'></zn-schedule-builder>`);
112
+
113
+ const friday = el.shadowRoot?.querySelectorAll('.calendar__col')[4];
114
+ expect(friday?.querySelectorAll('.calendar__slot--open').length).to.be.greaterThan(0);
115
+ expect(friday?.querySelectorAll('.calendar__slot--reduced').length).to.equal(4); // 16:00–18:00
116
+ });
117
+
118
+ it('should not shade the weekly grid for a one-off dated exception', async () => {
119
+ const el: ZnScheduleBuilder = await fixture(html`
120
+ <zn-schedule-builder value='{
121
+ "days": {"mon": ["08:00-18:00"]},
122
+ "exceptions": [{"label": "Offsite", "date": "2026-09-14", "ranges": ["08:00-13:00"]}]
123
+ }'></zn-schedule-builder>`);
124
+
125
+ expect(el.shadowRoot?.querySelectorAll('.calendar__slot--reduced').length).to.equal(0);
126
+ });
127
+
128
+ describe('timezones', () => {
129
+ it('should not convert anything when no timezone is configured', async () => {
130
+ const el: ZnScheduleBuilder = await fixture(html`
131
+ <zn-schedule-builder value='{"mon":["09:00-17:00"]}'></zn-schedule-builder>`);
132
+
133
+ expect(el.displayedDays.mon).to.deep.equal([{start: '09:00', end: '17:00'}]);
134
+ expect(parseSchedule(el.value).timezone).to.be.undefined;
135
+ });
136
+
137
+ it('should default to storing in UTC once a timezone is in play', async () => {
138
+ const el: ZnScheduleBuilder = await fixture(html`
139
+ <zn-schedule-builder show-timezone value='{"mon":["09:00-17:00"]}'></zn-schedule-builder>`);
140
+
141
+ expect(parseSchedule(el.value).timezone).to.equal('UTC');
142
+ });
143
+
144
+ it('should show stored hours in the display timezone without changing the value', async () => {
145
+ const el: ZnScheduleBuilder = await fixture(html`
146
+ <zn-schedule-builder
147
+ display-timezone="Europe/London"
148
+ reference-date="2026-07-15"
149
+ value='{"timezone":"UTC","days":{"mon":["09:00-17:00"]}}'></zn-schedule-builder>`);
150
+
151
+ // London is UTC+1 in July.
152
+ expect(el.displayedDays.mon).to.deep.equal([{start: '10:00', end: '18:00'}]);
153
+ expect(el.getDay('mon')).to.deep.equal([{start: '09:00', end: '17:00'}]);
154
+ });
155
+
156
+ it('should follow daylight saving via the reference date', async () => {
157
+ const el: ZnScheduleBuilder = await fixture(html`
158
+ <zn-schedule-builder
159
+ display-timezone="Europe/London"
160
+ reference-date="2026-01-15"
161
+ value='{"timezone":"UTC","days":{"mon":["09:00-17:00"]}}'></zn-schedule-builder>`);
162
+
163
+ // London is UTC+0 in January.
164
+ expect(el.displayedDays.mon).to.deep.equal([{start: '09:00', end: '17:00'}]);
165
+ });
166
+
167
+ it('should roll hours onto the next day when the offset pushes them past midnight', async () => {
168
+ const el: ZnScheduleBuilder = await fixture(html`
169
+ <zn-schedule-builder
170
+ display-timezone="Asia/Tokyo"
171
+ value='{"timezone":"UTC","days":{"mon":["23:00-24:00"]}}'></zn-schedule-builder>`);
172
+
173
+ expect(el.displayedDays.mon).to.deep.equal([]);
174
+ expect(el.displayedDays.tue).to.deep.equal([{start: '08:00', end: '09:00'}]);
175
+ });
176
+
177
+ it('should wrap Sunday hours around to Monday', async () => {
178
+ const el: ZnScheduleBuilder = await fixture(html`
179
+ <zn-schedule-builder
180
+ display-timezone="Asia/Tokyo"
181
+ value='{"timezone":"UTC","days":{"sun":["20:00-22:00"]}}'></zn-schedule-builder>`);
182
+
183
+ expect(el.displayedDays.mon).to.deep.equal([{start: '05:00', end: '07:00'}]);
184
+ expect(el.displayedDays.sun).to.deep.equal([]);
185
+ });
186
+
187
+ it('should convert edits made in the display timezone back to UTC', async () => {
188
+ const el: ZnScheduleBuilder = await fixture(html`
189
+ <zn-schedule-builder display-timezone="Asia/Tokyo" save-timezone="UTC"></zn-schedule-builder>`);
190
+
191
+ el.setDisplayDay('tue', [{start: '09:00', end: '17:00'}]);
192
+ await el.updateComplete;
193
+
194
+ expect(el.getDay('tue')).to.deep.equal([{start: '00:00', end: '08:00'}]);
195
+ expect(el.displayedDays.tue).to.deep.equal([{start: '09:00', end: '17:00'}]);
196
+ });
197
+
198
+ it('should convert incoming data expressed in another timezone', async () => {
199
+ const el: ZnScheduleBuilder = await fixture(html`
200
+ <zn-schedule-builder
201
+ save-timezone="UTC"
202
+ reference-date="2026-07-15"
203
+ value='{"timezone":"Europe/London","days":{"mon":["09:00-17:00"]}}'></zn-schedule-builder>`);
204
+
205
+ expect(el.getDay('mon')).to.deep.equal([{start: '08:00', end: '16:00'}]);
206
+ expect(parseSchedule(el.value).timezone).to.equal('UTC');
207
+ });
208
+
209
+ it('should adopt the value timezone when none is configured', async () => {
210
+ const el: ZnScheduleBuilder = await fixture(html`
211
+ <zn-schedule-builder value='{"timezone":"Europe/London","days":{"mon":["09:00-17:00"]}}'></zn-schedule-builder>`);
212
+
213
+ expect(el.getDay('mon')).to.deep.equal([{start: '09:00', end: '17:00'}]);
214
+ expect(parseSchedule(el.value).timezone).to.equal('Europe/London');
215
+ });
216
+
217
+ it('should render the timezone picker only when asked', async () => {
218
+ const without: ZnScheduleBuilder = await fixture(html`
219
+ <zn-schedule-builder></zn-schedule-builder>`);
220
+ expect(without.shadowRoot?.querySelector('zn-select')).to.not.exist;
221
+
222
+ const el: ZnScheduleBuilder = await fixture(html`
223
+ <zn-schedule-builder show-timezone display-timezone="Europe/London"></zn-schedule-builder>`);
224
+ const select = el.shadowRoot?.querySelector('zn-select');
225
+
226
+ expect(select).to.exist;
227
+ expect(el.shadowRoot?.querySelectorAll('zn-option').length).to.be.greaterThan(1);
228
+ });
229
+
230
+ const optionValues = (el: ZnScheduleBuilder) =>
231
+ [...(el.shadowRoot?.querySelectorAll('zn-option') ?? [])].map(option => option.getAttribute('value') ?? '');
232
+
233
+ it('should expand the named timezone sets', async () => {
234
+ const offsets: ZnScheduleBuilder = await fixture(html`
235
+ <zn-schedule-builder show-timezone timezones="offsets"></zn-schedule-builder>`);
236
+ const common: ZnScheduleBuilder = await fixture(html`
237
+ <zn-schedule-builder show-timezone timezones="common"></zn-schedule-builder>`);
238
+ const all: ZnScheduleBuilder = await fixture(html`
239
+ <zn-schedule-builder show-timezone timezones="all"></zn-schedule-builder>`);
240
+
241
+ expect(optionValues(offsets).length).to.be.greaterThan(20);
242
+ expect(optionValues(common).length).to.be.greaterThan(optionValues(offsets).length);
243
+ expect(optionValues(all).length).to.be.greaterThan(optionValues(common).length);
244
+ expect(optionValues(common)).to.include('Asia/Kathmandu');
245
+ expect(optionValues(offsets)).to.not.include('Asia/Kathmandu');
246
+ });
247
+
248
+ it('should cover every offset in the offsets set from the common set', async () => {
249
+ const offsets: ZnScheduleBuilder = await fixture(html`
250
+ <zn-schedule-builder show-timezone timezones="offsets"></zn-schedule-builder>`);
251
+ const common: ZnScheduleBuilder = await fixture(html`
252
+ <zn-schedule-builder show-timezone timezones="common"></zn-schedule-builder>`);
253
+
254
+ const offsetsOf = (zones: string[]) => new Set(zones.flatMap(zone =>
255
+ ['2026-01-15T12:00:00Z', '2026-07-15T12:00:00Z'].map(when => {
256
+ const parts = new Intl.DateTimeFormat('en-US', {
257
+ timeZone: zone, hour12: false,
258
+ year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
259
+ }).formatToParts(new Date(when));
260
+ const read = (type: string) => Number(parts.find(part => part.type === type)?.value);
261
+ const utc = Date.UTC(read('year'), read('month') - 1, read('day'), read('hour') % 24, read('minute'));
262
+ return Math.round((utc - new Date(when).getTime()) / 60000);
263
+ })));
264
+
265
+ const covered = offsetsOf(optionValues(common));
266
+ [...offsetsOf(optionValues(offsets))].forEach(offset => expect(covered).to.include(offset));
267
+ });
268
+
269
+ it('should offer the English-speaking set under friendly names', async () => {
270
+ const el: ZnScheduleBuilder = await fixture(html`
271
+ <zn-schedule-builder show-timezone timezones="en" display-timezone="Europe/London"></zn-schedule-builder>`);
272
+
273
+ expect(optionValues(el)).to.include.members([
274
+ 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles',
275
+ 'Europe/London', 'Australia/Sydney'
276
+ ]);
277
+
278
+ const labels = [...(el.shadowRoot?.querySelectorAll('zn-option') ?? [])].map(o => o.textContent?.trim() ?? '');
279
+ expect(labels.some(label => label.startsWith('US Eastern ('))).to.be.true;
280
+ expect(labels.some(label => label.startsWith('US Mountain ('))).to.be.true;
281
+ expect(labels.some(label => label.startsWith('UK ('))).to.be.true;
282
+ expect(labels.some(label => label.startsWith('Australia ('))).to.be.true;
283
+ });
284
+
285
+ it('should keep the friendly label when a set is combined with plain zones', async () => {
286
+ const el: ZnScheduleBuilder = await fixture(html`
287
+ <zn-schedule-builder show-timezone
288
+ timezones="en America/New_York Asia/Tokyo"
289
+ display-timezone="Europe/London"></zn-schedule-builder>`);
290
+
291
+ const newYork = [...(el.shadowRoot?.querySelectorAll('zn-option') ?? [])]
292
+ .filter(option => option.getAttribute('value') === 'America/New_York');
293
+
294
+ expect(newYork).to.have.lengthOf(1);
295
+ expect(newYork[0].textContent?.trim()).to.match(/^US Eastern \(/);
296
+ });
297
+
298
+ it('should mix named sets with explicit zones', async () => {
299
+ const el: ZnScheduleBuilder = await fixture(html`
300
+ <zn-schedule-builder show-timezone timezones="UTC Europe/London Asia/Tokyo"></zn-schedule-builder>`);
301
+
302
+ // The viewer's own zone is always added so the current selection is never unreachable.
303
+ expect(optionValues(el)).to.include.members(['UTC', 'Europe/London', 'Asia/Tokyo']);
304
+ expect(optionValues(el).length).to.be.lessThan(10);
305
+ });
306
+
307
+ it('should not emit zn-change when only the display timezone changes', async () => {
308
+ const el: ZnScheduleBuilder = await fixture(html`
309
+ <zn-schedule-builder show-timezone value='{"mon":["09:00-17:00"]}'></zn-schedule-builder>`);
310
+
311
+ const before = el.value;
312
+ let changed = false;
313
+ el.addEventListener('zn-change', () => (changed = true));
314
+
315
+ el.displayTimezone = 'Asia/Tokyo';
316
+ await el.updateComplete;
317
+
318
+ expect(changed).to.be.false;
319
+ expect(el.value).to.equal(before);
320
+ });
321
+ });
322
+
323
+ it('should be invalid when required and empty', async () => {
324
+ const el: ZnScheduleBuilder = await fixture(html`
325
+ <zn-schedule-builder required></zn-schedule-builder>`);
326
+
327
+ expect(el.checkValidity()).to.be.false;
328
+
329
+ el.setDay('mon', [{start: '09:00', end: '17:00'}]);
330
+ await el.updateComplete;
331
+
332
+ expect(el.checkValidity()).to.be.true;
333
+ });
334
+
335
+ it('should submit the schedule as JSON with the form', async () => {
336
+ const form: HTMLFormElement = await fixture(html`
337
+ <form>
338
+ <zn-schedule-builder name="hours" value="${scheduleValue}"></zn-schedule-builder>
339
+ </form>`);
340
+
341
+ const data = new FormData(form);
342
+ expect(parseSchedule(String(data.get('hours'))).days.mon).to.deep.equal([{start: '08:00', end: '18:00'}]);
343
+ });
344
+ });
@@ -105,7 +105,8 @@ zn-button {
105
105
  border-bottom-width: 0;
106
106
  border-radius: 0 0 0 var(--zn-border-radius);
107
107
  transition: grid-template-rows 0.22s ease, border-bottom-width 0s linear 0.22s;
108
- will-change: grid-template-rows;
108
+ // PERF: grid-template-rows is not a compositable property, so will-change
109
+ // cannot help it — it only promoted a layer per settings container for free.
109
110
  }
110
111
 
111
112
  &--open &__panel {
@@ -2,7 +2,7 @@ import {html, unsafeCSS} from 'lit';
2
2
  import {property} from 'lit/decorators.js';
3
3
  import ZincElement from '../../internal/zinc-element';
4
4
  import type {CSSResultGroup} from 'lit';
5
- import type {SlashMenuItem} from '../slash-menu/slash-menu-items';
5
+ import type {SlashMenuItem} from '../slash-menu';
6
6
 
7
7
  import styles from './slash-item.scss';
8
8
 
@@ -120,3 +120,51 @@ export function filterSlashItems(items: SlashMenuItem[], query: string): SlashMe
120
120
  a.index - b.index)
121
121
  .map(entry => entry.item);
122
122
  }
123
+
124
+ const RECENT_PREFIX = 'zn-slash-recent:';
125
+ /** Kept deeper than any menu shows, so history survives items that aren't in the current list. */
126
+ const RECENT_LIMIT = 10;
127
+
128
+ /** The identity an item is remembered by in a menu's recently used list. */
129
+ export function slashItemKey(item: SlashMenuItem): string {
130
+ return item.action ? `action:${item.action}` : `value:${item.value || item.label}`;
131
+ }
132
+
133
+ /** The keys of the items most recently chosen from the menu stored under `key`, newest first. */
134
+ export function readRecentSlashItems(key: string): string[] {
135
+ if (!key) return [];
136
+
137
+ try {
138
+ const parsed: unknown = JSON.parse(localStorage.getItem(RECENT_PREFIX + key) ?? '[]');
139
+ return Array.isArray(parsed) ? parsed.filter((entry): entry is string => typeof entry === 'string') : [];
140
+ } catch {
141
+ return [];
142
+ }
143
+ }
144
+
145
+ /** Moves an item to the front of the recently used list stored under `key`, and returns the list. */
146
+ export function recordRecentSlashItem(key: string, item: SlashMenuItem): string[] {
147
+ if (!key) return [];
148
+
149
+ const itemKey = slashItemKey(item);
150
+ const keys = [itemKey, ...readRecentSlashItems(key).filter(entry => entry !== itemKey)].slice(0, RECENT_LIMIT);
151
+
152
+ try {
153
+ localStorage.setItem(RECENT_PREFIX + key, JSON.stringify(keys));
154
+ } catch {
155
+ // No storage (private browsing, quota) — the list just doesn't outlive the page
156
+ }
157
+
158
+ return keys;
159
+ }
160
+
161
+ /** Forgets the recently used items stored under `key`. */
162
+ export function clearRecentSlashItems(key: string) {
163
+ if (!key) return;
164
+
165
+ try {
166
+ localStorage.removeItem(RECENT_PREFIX + key);
167
+ } catch {
168
+ // As above
169
+ }
170
+ }