@kubex/zinc 1.1.92 → 1.1.94

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 (29) hide show
  1. package/dist/custom-elements.json +1738 -110
  2. package/dist/vscode.html-custom-data.json +136 -4
  3. package/dist/web-types.json +307 -4
  4. package/dist/zn.d.ts +298 -2
  5. package/dist/zn.min.js +708 -515
  6. package/docs/pages/components/page-builder.md +22 -0
  7. package/docs/pages/components/schedule-builder.md +345 -0
  8. package/package.json +1 -1
  9. package/src/components/alert/alert.scss +9 -13
  10. package/src/components/chip/chip.scss +1 -1
  11. package/src/components/icon-picker/icon-picker.component.ts +1 -1
  12. package/src/components/linked-select/linked-select.component.ts +22 -5
  13. package/src/components/page/page.scss +13 -7
  14. package/src/components/page-builder/page-builder.component.ts +52 -7
  15. package/src/components/page-builder/page-builder.scss +166 -9
  16. package/src/components/page-builder/page-builder.test.ts +134 -0
  17. package/src/components/page-builder/page.types.ts +23 -3
  18. package/src/components/page-nav/page-nav.scss +9 -1
  19. package/src/components/panel/panel.component.ts +5 -1
  20. package/src/components/priority-list/priority-list.component.ts +1 -0
  21. package/src/components/priority-list/priority-list.scss +2 -1
  22. package/src/components/schedule-builder/index.ts +12 -0
  23. package/src/components/schedule-builder/schedule-builder.component.ts +1543 -0
  24. package/src/components/schedule-builder/schedule-builder.scss +448 -0
  25. package/src/components/schedule-builder/schedule-builder.test.ts +344 -0
  26. package/src/components/toggle/toggle.component.ts +2 -1
  27. package/src/zinc.ts +1 -0
  28. package/docs/superpowers/plans/2026-08-03-theme-editor.md +0 -1536
  29. 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
+ });
@@ -26,6 +26,7 @@ import styles from './toggle.scss';
26
26
  *
27
27
  * @csspart base - The component's base wrapper containing the toggle switch.
28
28
  * @csspart control - The toggle switch control (the circular button that slides).
29
+ * @csspart label - The toggle's label.
29
30
  * @csspart description - The container that wraps the toggle's description.
30
31
  *
31
32
  * @cssproperty --zn-toggle-margin - The margin around the toggle switch. Defaults to `8px 0`.
@@ -218,7 +219,7 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
218
219
  <label>
219
220
  ${this.label || hasDescription ? html`
220
221
  <div class="switch__label-wrapper">
221
- ${this.label ? html`<p class="switch-label">${this.label}</p>` : ''}
222
+ ${this.label ? html`<p part="label" class="switch-label">${this.label}</p>` : ''}
222
223
  <div part="description"
223
224
  id="description"
224
225
  class="switch__description"
package/src/zinc.ts CHANGED
@@ -114,6 +114,7 @@ export { default as PreviewFrame } from './components/preview-frame';
114
114
  export { default as ThemeEditor } from './components/theme-editor';
115
115
  export { default as SlashMenu } from './components/slash-menu';
116
116
  export { default as SlashItem } from './components/slash-item';
117
+ export { default as ScheduleBuilder } from './components/schedule-builder';
117
118
  /* plop:component */
118
119
 
119
120
  // Base Component