@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.
- package/dist/custom-elements.json +2880 -453
- package/dist/vscode.html-custom-data.json +209 -22
- package/dist/web-types.json +452 -39
- package/dist/zn.d.ts +633 -61
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +865 -560
- package/docs/pages/components/page-builder.md +143 -19
- package/docs/pages/components/schedule-builder.md +345 -0
- package/docs/pages/components/slash-menu.md +132 -6
- package/docs/pages/components/textarea.md +16 -0
- package/package.json +1 -1
- package/scss/_root.scss +7 -1
- package/src/components/alert/alert.scss +9 -13
- package/src/components/button/button.scss +5 -2
- package/src/components/chip/chip.scss +1 -1
- package/src/components/icon-picker/icon-picker.component.ts +1 -1
- package/src/components/inline-edit/inline-edit.component.ts +6 -1
- package/src/components/input/input.component.ts +12 -2
- package/src/components/linked-select/linked-select.component.ts +22 -5
- package/src/components/page/page.scss +20 -9
- package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts +16 -9
- package/src/components/page-builder/modules/page-section-card/page-section-card.scss +13 -0
- package/src/components/page-builder/modules/page-section-card/page-section-card.test.ts +9 -0
- package/src/components/page-builder/page-builder.component.ts +535 -232
- package/src/components/page-builder/page-builder.scss +230 -19
- package/src/components/page-builder/page-builder.test.ts +790 -110
- package/src/components/page-builder/page-tree.test.ts +483 -0
- package/src/components/page-builder/page-tree.ts +329 -0
- package/src/components/page-builder/page.types.ts +98 -10
- package/src/components/page-nav/page-nav.scss +9 -1
- package/src/components/panel/panel.component.ts +5 -1
- package/src/components/priority-list/priority-list.component.ts +1 -0
- package/src/components/priority-list/priority-list.scss +2 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +198 -9
- package/src/components/remarkd-editor/remarkd-editor.scss +81 -0
- package/src/components/remarkd-editor/remarkd-editor.test.ts +179 -0
- package/src/components/schedule-builder/index.ts +12 -0
- package/src/components/schedule-builder/schedule-builder.component.ts +1543 -0
- package/src/components/schedule-builder/schedule-builder.scss +448 -0
- package/src/components/schedule-builder/schedule-builder.test.ts +344 -0
- package/src/components/settings-container/settings-container.scss +2 -1
- package/src/components/slash-item/slash-item.component.ts +1 -1
- package/src/components/slash-menu/slash-menu-items.ts +48 -0
- package/src/components/slash-menu/slash-menu.component.ts +134 -27
- package/src/components/slash-menu/slash-menu.scss +90 -12
- package/src/components/slash-menu/slash-menu.test.ts +107 -0
- package/src/components/textarea/textarea.component.ts +12 -2
- package/src/components/textarea/textarea.test.ts +2 -2
- package/src/components/toggle/toggle.component.ts +2 -1
- package/src/components/translations/translations.component.ts +5 -1
- package/src/zinc.ts +1 -0
- package/docs/superpowers/plans/2026-08-03-theme-editor.md +0 -1536
- package/docs/superpowers/specs/2026-08-03-theme-editor-design.md +0 -327
|
@@ -27,6 +27,10 @@ are bound by their `name` attribute: values prefill from the section's data and
|
|
|
27
27
|
description="Large banner with optional search">
|
|
28
28
|
<zn-input name="title" label="Title"></zn-input>
|
|
29
29
|
<zn-input name="subtitle" label="Subtitle"></zn-input>
|
|
30
|
+
<zn-input name="gradient" label="Background gradient" placeholder="linear-gradient(135deg, #00aaff, #aa00ff)"
|
|
31
|
+
help-text="CSS gradient painted behind the hero. Leave empty for the theme default."></zn-input>
|
|
32
|
+
<zn-toggle name="showSearch" label="Show search"
|
|
33
|
+
description="Draws the site search box in the banner."></zn-toggle>
|
|
30
34
|
</template>
|
|
31
35
|
<template type="article-list" slot="config" label="Article List" icon="list" category="Content"
|
|
32
36
|
description="A list of KB articles">
|
|
@@ -40,13 +44,42 @@ Types can also be registered programmatically via `sectionTypes` /
|
|
|
40
44
|
`registerSectionTypes()`, including a `renderConfig(section, update)` callback for
|
|
41
45
|
inspector bodies that need real logic.
|
|
42
46
|
|
|
47
|
+
## The inspector
|
|
48
|
+
|
|
49
|
+
Selecting a section opens the inspector, which heads the panel with the section type's
|
|
50
|
+
icon and colour, the section's name, its type (shown only where a renamed section makes
|
|
51
|
+
it useful), and a close button that clears the selection. Below that, a **Section name**
|
|
52
|
+
field renames the section (the card's label), followed by the type's stamped config form.
|
|
53
|
+
A type whose template has no content gets a hint in place of the form rather than an
|
|
54
|
+
empty panel.
|
|
55
|
+
|
|
56
|
+
Because the panel is narrow, the inspector sets its own form-label and help-text
|
|
57
|
+
typography rather than inheriting your app's full-width form styling, and its `gap` is
|
|
58
|
+
the single source of spacing between fields — a control's own outer margins are zeroed
|
|
59
|
+
so they can't stack with it. Stamped `<zn-toggle>`s are laid out as a row, with the
|
|
60
|
+
label and description left and the switch right; set `label-position` yourself on a
|
|
61
|
+
toggle to opt out.
|
|
62
|
+
|
|
63
|
+
Style the panel through `inspector`, `inspector-header` and `inspector-body` parts.
|
|
64
|
+
|
|
65
|
+
A container section additionally gets a **Layout** group at the top of the inspector: a column
|
|
66
|
+
count, a weight per column, a "Keep adding rows" toggle, and — when growing is off — a row count.
|
|
67
|
+
Every edit here is lossless: changing the column count re-chunks the same ordered list of
|
|
68
|
+
stacks into the new shape, changing the row count only pads or trims trailing empty rows
|
|
69
|
+
(clamping at the last row that still holds content), and changing a column's weight leaves
|
|
70
|
+
every cell's contents untouched. All of it is undoable like any other edit.
|
|
71
|
+
|
|
43
72
|
## JavaScript API
|
|
44
73
|
|
|
45
74
|
- `state` — get/set the current `PageState`. The getter returns a deep copy; the setter
|
|
46
75
|
replaces the state wholesale (like the `config` attribute, it does not emit `zn-page-change`).
|
|
47
76
|
- `addSection(type, index?)` — insert a new section of a registered type (default: at the end).
|
|
48
|
-
- `
|
|
49
|
-
|
|
77
|
+
- `addSectionToCell(type, containerId, cellIndex, insertIndex?)` — insert a new section of a
|
|
78
|
+
registered type into a container's cell. `cellIndex` is the position in the container's flat,
|
|
79
|
+
row-major `cells` list; `insertIndex` (default `0`, the top of the stack) is where in that
|
|
80
|
+
cell's stack it lands. Returns the new section, or `null` if the drop isn't allowed — because
|
|
81
|
+
the type isn't in the container's `accepts` list, or because it would exceed the two-level
|
|
82
|
+
nesting cap.
|
|
50
83
|
- `undo()` / `redo()` — step through edit history (bounded at 50 entries). There is no built-in
|
|
51
84
|
toolbar: wire these to your own header buttons or keyboard shortcuts.
|
|
52
85
|
- `registerSectionType(type)` / `registerSectionTypes(types)` — programmatic registration,
|
|
@@ -92,8 +125,8 @@ or call `restoreAutoSave()` yourself.
|
|
|
92
125
|
|
|
93
126
|
Every edit emits `zn-page-change` with the full page state (`event.detail.state`, also
|
|
94
127
|
readable via the `state` property) — plain JSON the host persists and later feeds back in
|
|
95
|
-
through the `config` attribute. Sections appear in page order; container
|
|
96
|
-
`
|
|
128
|
+
through the `config` attribute. Sections appear in page order; a container section additionally
|
|
129
|
+
carries `layout` and `cells`, per the Containers section below:
|
|
97
130
|
|
|
98
131
|
```json
|
|
99
132
|
{
|
|
@@ -108,10 +141,11 @@ through the `config` attribute. Sections appear in page order; container section
|
|
|
108
141
|
"type": "article-grid",
|
|
109
142
|
"label": "Popular articles",
|
|
110
143
|
"data": {"title": "Popular"},
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
{"id": "s-
|
|
114
|
-
|
|
144
|
+
"layout": {"widths": [1, 1, 1], "grow": false},
|
|
145
|
+
"cells": [
|
|
146
|
+
[{"id": "s-mc42h-2", "type": "article-tile", "data": {"article": "art_42"}}],
|
|
147
|
+
[{"id": "s-mc42p-3", "type": "article-tile", "data": {"article": "art_7"}}],
|
|
148
|
+
[]
|
|
115
149
|
]
|
|
116
150
|
},
|
|
117
151
|
{
|
|
@@ -123,29 +157,119 @@ through the `config` attribute. Sections appear in page order; container section
|
|
|
123
157
|
}
|
|
124
158
|
```
|
|
125
159
|
|
|
126
|
-
|
|
160
|
+
A host loading a config saved before this model — the old flat, null-padded `children` array —
|
|
161
|
+
still has it accepted and migrated on load; see "Legacy `slots`" further down for what that
|
|
162
|
+
older shape looked like and how it's converted.
|
|
127
163
|
|
|
128
|
-
A
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
164
|
+
## A required first section
|
|
165
|
+
|
|
166
|
+
Pages that must always open with a particular section — a hero banner, a masthead — set
|
|
167
|
+
`required-first` to that section type. The builder hoists an existing section of the type to
|
|
168
|
+
the top of the page, or inserts an empty one when there is none, and pins it there: it has no
|
|
169
|
+
remove action, ignores <kbd>Delete</kbd>, can't be dragged or moved into a container slot, and
|
|
170
|
+
nothing can be dropped above it. Its content stays fully editable in the inspector, and the
|
|
171
|
+
type stays in the palette, so further sections of it can still be added below.
|
|
172
|
+
|
|
173
|
+
Which section is pinned is derived from the state — `sections[0]` when its type matches — so
|
|
174
|
+
nothing about the lock is written into the persisted config.
|
|
175
|
+
|
|
176
|
+
```html:preview
|
|
177
|
+
<zn-page-builder heading="KB Homepage" required-first="hero" style="height: 420px"
|
|
178
|
+
config='{"sections":[{"id":"t1","type":"rich-text","data":{"content":"Welcome"}}]}'>
|
|
179
|
+
<template type="hero" slot="config" label="Hero" icon="star" category="Headers"
|
|
180
|
+
description="Banner with a heading and optional search">
|
|
181
|
+
<zn-input name="title" label="Title"></zn-input>
|
|
182
|
+
<zn-toggle name="showSearch" label="Show search"></zn-toggle>
|
|
183
|
+
</template>
|
|
184
|
+
<template type="rich-text" slot="config" label="Rich Text" icon="notes" category="Content"
|
|
185
|
+
description="A block of markdown content">
|
|
186
|
+
<zn-input name="content" label="Content"></zn-input>
|
|
187
|
+
</template>
|
|
188
|
+
</zn-page-builder>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The config above declares only a rich-text section, so the hero is inserted above it — loading a
|
|
192
|
+
page that lacks the required section normalises it rather than rejecting it. Two things follow
|
|
193
|
+
from that. The inserted section's `data` is empty, so a host that wants the pinned section
|
|
194
|
+
prefilled should put it into the `config` it hands over rather than rely on the insert. And the
|
|
195
|
+
guard is client-side, so a host that persists the config should enforce the same rule on save.
|
|
196
|
+
|
|
197
|
+
## Containers
|
|
198
|
+
|
|
199
|
+
A section type with the `container` attribute becomes a full-row container: its card renders a
|
|
200
|
+
grid of **cells** beneath it, and each cell holds an ordered **stack** of sections. The type
|
|
201
|
+
author only declares that it's a container and what it starts as; the editor reshapes it after
|
|
202
|
+
placing it, from the inspector's Layout group.
|
|
203
|
+
|
|
204
|
+
| Attribute | Meaning |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `container` | Marks the type a container. Required. |
|
|
207
|
+
| `columns="4"` | Seeds a new instance with 4 equal columns. |
|
|
208
|
+
| `widths="1 2 1"` | Seeds the column weights directly (comma- or whitespace-separated). Wins over `columns`. |
|
|
209
|
+
| `grow` | Seeds the instance growable — it always offers a further empty row. |
|
|
210
|
+
| `accepts="a,b"` | Restricts which types the cells take. Omit to allow any type, within the nesting cap below. |
|
|
211
|
+
|
|
212
|
+
If `widths` is present but unparsable — non-numeric tokens are discarded rather than kept as
|
|
213
|
+
columns — the container falls back to `columns`, and if that's absent too, to three equal
|
|
214
|
+
columns (`[1, 1, 1]`).
|
|
215
|
+
|
|
216
|
+
Drag sections from the palette into a cell, stack several sections in one cell, or drag them
|
|
217
|
+
between cells or out onto the page. Containers may nest **two levels deep** — a container inside
|
|
218
|
+
a cell, itself holding another container — and a drop that would nest a third level is refused.
|
|
219
|
+
That cap holds even when a container's `accepts` list names another container type: `accepts`
|
|
220
|
+
can't be used to bypass it.
|
|
133
221
|
|
|
134
222
|
```html:preview
|
|
135
223
|
<zn-page-builder heading="KB Homepage" style="height: 560px"
|
|
136
|
-
config='{"sections":[{"id":"g1","type":"
|
|
137
|
-
<template type="
|
|
138
|
-
|
|
139
|
-
|
|
224
|
+
config='{"sections":[{"id":"g1","type":"row","data":{},"layout":{"widths":[1,2,1],"grow":false},"cells":[[],[],[]]}]}'>
|
|
225
|
+
<template type="row" slot="config" label="Row" icon="view_column" category="Layout"
|
|
226
|
+
description="A row of columns you can weight" container widths="1 2 1">
|
|
227
|
+
<zn-input name="title" label="Row title"></zn-input>
|
|
228
|
+
</template>
|
|
229
|
+
<template type="grid" slot="config" label="Tile Grid" icon="grid_view" category="Layout"
|
|
230
|
+
description="Keeps adding rows as you fill it" container columns="3" grow>
|
|
140
231
|
<zn-input name="title" label="Grid title"></zn-input>
|
|
141
232
|
</template>
|
|
142
|
-
<template type="article
|
|
233
|
+
<template type="article" slot="config" label="Article" icon="article" category="Content"
|
|
143
234
|
description="A single article tile">
|
|
144
235
|
<zn-input name="article" label="Article id"></zn-input>
|
|
145
236
|
</template>
|
|
146
237
|
</zn-page-builder>
|
|
147
238
|
```
|
|
148
239
|
|
|
240
|
+
### The container config
|
|
241
|
+
|
|
242
|
+
A container persists its `layout` and its `cells` — a flat, row-major list of stacks whose
|
|
243
|
+
length is always a whole multiple of `layout.widths.length`. Rows are implicit
|
|
244
|
+
(`cells.length / layout.widths.length`), so there's no row count to keep in sync:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"id": "s-mc42a-1",
|
|
249
|
+
"type": "row",
|
|
250
|
+
"layout": { "widths": [1, 2, 1], "grow": false },
|
|
251
|
+
"cells": [
|
|
252
|
+
[ { "id": "s-1", "type": "nav", "data": {} },
|
|
253
|
+
{ "id": "s-2", "type": "links", "data": {} } ],
|
|
254
|
+
[ { "id": "s-3", "type": "hero", "data": {} } ],
|
|
255
|
+
[]
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Render it by mapping each weight to a grid track and each cell to a stack. A **growable**
|
|
261
|
+
container never persists a trailing all-empty row — the builder adds that row itself at render
|
|
262
|
+
time, so don't expect it in the JSON. A **fixed** container's trailing empty row, by contrast, is
|
|
263
|
+
part of its layout and does persist.
|
|
264
|
+
|
|
265
|
+
### Legacy `slots`
|
|
266
|
+
|
|
267
|
+
`slots="6"` still declares a container, and pages persisted with the old flat, null-padded
|
|
268
|
+
`children` array still load: they're migrated to `layout` + `cells` on load and re-saved in the
|
|
269
|
+
new shape — `children` is never written back. A `slots`-declared container that has no explicit
|
|
270
|
+
`accepts` also keeps its older, stricter rule of refusing container types in its cells (rather
|
|
271
|
+
than allowing anything up to the nesting cap). Prefer `container` going forward.
|
|
272
|
+
|
|
149
273
|
## List sections
|
|
150
274
|
|
|
151
275
|
Sections that show a set of existing items (categories, articles, …) reference them by id:
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
---
|
|
2
|
+
meta:
|
|
3
|
+
title: Schedule Builder
|
|
4
|
+
description: Builds a weekly opening-hours schedule as a drag-to-paint calendar or a compact list of time ranges, and posts the result as JSON.
|
|
5
|
+
layout: component
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
```html:preview
|
|
9
|
+
<zn-schedule-builder
|
|
10
|
+
name="opening-hours"
|
|
11
|
+
value='{
|
|
12
|
+
"timezone": "Europe/London",
|
|
13
|
+
"days": {
|
|
14
|
+
"mon": ["08:00-18:00"],
|
|
15
|
+
"tue": ["08:00-18:00"],
|
|
16
|
+
"wed": ["08:00-18:00"],
|
|
17
|
+
"thu": ["08:30-18:00"],
|
|
18
|
+
"fri": ["08:00-18:00"],
|
|
19
|
+
"sat": ["09:00-13:00"],
|
|
20
|
+
"sun": []
|
|
21
|
+
},
|
|
22
|
+
"exceptions": [
|
|
23
|
+
{"label": "All-hands offsite", "date": "2026-08-28", "ranges": ["08:00-13:00"]},
|
|
24
|
+
{"label": "Systems maintenance", "date": "2026-09-14", "ranges": ["10:00-18:00"]},
|
|
25
|
+
{"label": "Christmas Eve — early close", "date": "2026-12-24", "ranges": ["08:00-13:00"]}
|
|
26
|
+
]
|
|
27
|
+
}'>
|
|
28
|
+
</zn-schedule-builder>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
:::tip
|
|
32
|
+
This component works with standard `<form>` elements. Please refer to the section
|
|
33
|
+
on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
|
34
|
+
:::
|
|
35
|
+
|
|
36
|
+
## The Value
|
|
37
|
+
|
|
38
|
+
The schedule is serialised to JSON and exposed three ways:
|
|
39
|
+
|
|
40
|
+
- as the `value` attribute/property — a JSON string, which is what gets submitted with the form;
|
|
41
|
+
- as the `schedule` property — the same data as a plain object;
|
|
42
|
+
- as the `zn-change` event, emitted whenever the schedule changes.
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"timezone": "UTC",
|
|
47
|
+
"days": {
|
|
48
|
+
"mon": [{ "start": "08:00", "end": "18:00" }],
|
|
49
|
+
"tue": [{ "start": "08:00", "end": "12:00" }, { "start": "13:30", "end": "18:00" }],
|
|
50
|
+
"wed": [], "thu": [], "fri": [], "sat": [], "sun": []
|
|
51
|
+
},
|
|
52
|
+
"exceptions": [
|
|
53
|
+
{ "label": "Christmas Eve — early close", "date": "2026-12-24", "ranges": [{ "start": "08:00", "end": "13:00" }] }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Ranges are always sorted, and overlapping or touching ranges are merged, so `08:00–12:00` plus
|
|
59
|
+
`12:00–18:00` becomes a single `08:00–18:00`. Times are 24 hour `HH:MM` regardless of the
|
|
60
|
+
`time-format` used for display, and `timezone` names the timezone they are expressed in — see
|
|
61
|
+
[Timezones](#timezones).
|
|
62
|
+
|
|
63
|
+
When you write the value yourself you can use the `"08:00-18:00"` shorthand instead of
|
|
64
|
+
`{"start": "08:00", "end": "18:00"}`, and you can pass a bare day map without the `days` wrapper.
|
|
65
|
+
Both are normalised on the way in.
|
|
66
|
+
|
|
67
|
+
## Examples
|
|
68
|
+
|
|
69
|
+
### Views
|
|
70
|
+
|
|
71
|
+
The builder has two views. `calendar` is a drag-to-paint week grid; `form` is a compact list of time
|
|
72
|
+
ranges that fits inside a normal settings form. Use the `view` attribute to choose the starting
|
|
73
|
+
view, and `no-toggle` to remove the switcher and lock the component to one of them.
|
|
74
|
+
|
|
75
|
+
```html:preview
|
|
76
|
+
<zn-schedule-builder
|
|
77
|
+
view="form"
|
|
78
|
+
value='{"mon":["08:00-18:00"],"tue":["08:00-18:00"],"wed":["08:00-12:00","13:30-18:00"],"thu":["08:00-18:00"],"fri":["08:00-16:00"],"sat":["09:00-13:00"],"sun":[]}'>
|
|
79
|
+
</zn-schedule-builder>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
In the form view, click a range to edit it, and use **Add range** to split a day into multiple
|
|
83
|
+
periods. In the calendar view, drag across the grid to open hours and drag over open hours to close
|
|
84
|
+
them — a drag can span several days at once.
|
|
85
|
+
|
|
86
|
+
### Timezones
|
|
87
|
+
|
|
88
|
+
A schedule has two timezones: the one it is **stored** in, and the one it is **shown** in.
|
|
89
|
+
|
|
90
|
+
- `save-timezone` is the timezone the value is written in. It defaults to `UTC` as soon as the
|
|
91
|
+
schedule is timezone-aware, so hours land in your database in one canonical zone.
|
|
92
|
+
- `display-timezone` is the timezone the grid and the list are drawn in. It defaults to
|
|
93
|
+
`save-timezone`, and accepts `auto` for the viewer's own timezone.
|
|
94
|
+
- `show-timezone` adds a picker so the viewer can read the same schedule in any timezone.
|
|
95
|
+
|
|
96
|
+
Switching the display timezone never changes the value — the same hours are simply labelled in
|
|
97
|
+
another zone, and `zn-change` doesn't fire. Edits work the other way around: you drag or type in the
|
|
98
|
+
zone you are looking at, and the component converts back to `save-timezone` before storing.
|
|
99
|
+
|
|
100
|
+
```html:preview
|
|
101
|
+
<zn-schedule-builder
|
|
102
|
+
id="tz-demo"
|
|
103
|
+
show-timezone
|
|
104
|
+
display-timezone="Europe/London"
|
|
105
|
+
start-hour="0"
|
|
106
|
+
end-hour="24"
|
|
107
|
+
hide-summary
|
|
108
|
+
value='{"timezone":"UTC","days":{"mon":["08:00-18:00"],"tue":["08:00-18:00"],"wed":["08:00-18:00"],"thu":["08:00-18:00"],"fri":["08:00-16:00"]}}'>
|
|
109
|
+
</zn-schedule-builder>
|
|
110
|
+
|
|
111
|
+
<pre id="tz-demo-output" style="margin-top: 1rem"></pre>
|
|
112
|
+
|
|
113
|
+
<script>
|
|
114
|
+
const tzBuilder = document.querySelector('#tz-demo');
|
|
115
|
+
const tzOutput = document.querySelector('#tz-demo-output');
|
|
116
|
+
|
|
117
|
+
const printTz = () => (tzOutput.textContent = `stored (UTC):\n${JSON.stringify(tzBuilder.schedule.days, null, 2)}`);
|
|
118
|
+
|
|
119
|
+
tzBuilder.addEventListener('zn-change', printTz);
|
|
120
|
+
printTz();
|
|
121
|
+
</script>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Pick a timezone far from UTC and the blocks slide, wrapping onto the next day — or onto Monday, from
|
|
125
|
+
the end of Sunday — while the stored hours below stay put.
|
|
126
|
+
|
|
127
|
+
#### Which Timezones the Picker Offers
|
|
128
|
+
|
|
129
|
+
`timezones` takes IANA names, or one of three named sets:
|
|
130
|
+
|
|
131
|
+
| Set | Zones | What it is |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `en` | 6 | US Eastern, Central, Mountain and Pacific, the UK, and Australia — listed under those names rather than their IANA ones. For an English-speaking audience who would rather not read `America/Los_Angeles`. |
|
|
134
|
+
| `offsets` | ~42 | One zone per UTC offset. The default — enough to read a schedule from anywhere without a long list, though the city standing in for each offset is arbitrary. |
|
|
135
|
+
| `common` | ~90 | Every offset in use, plus the business and population centres that share one — so Berlin, Paris, Madrid and Rome all appear rather than one standing for the rest. Includes the half and quarter-hour zones (India, Iran, Nepal, Newfoundland, central Australia, Chatham). |
|
|
136
|
+
| `all` | ~420 | Everything `Intl.supportedValuesOf('timeZone')` reports. Complete, but full of aliases and zones nobody selects. |
|
|
137
|
+
|
|
138
|
+
`common` is the one to reach for whenever real users pick their own timezone; `offsets` suits an
|
|
139
|
+
internal tool where the offset is all that matters; `en` suits a product whose customers are all in
|
|
140
|
+
the US, the UK or Australia.
|
|
141
|
+
|
|
142
|
+
```html:preview
|
|
143
|
+
<zn-schedule-builder
|
|
144
|
+
show-timezone
|
|
145
|
+
timezones="en"
|
|
146
|
+
display-timezone="Europe/London"
|
|
147
|
+
view="form"
|
|
148
|
+
value='{"timezone":"UTC","days":{"mon":["13:00-21:00"],"tue":["13:00-21:00"]}}'>
|
|
149
|
+
</zn-schedule-builder>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```html:preview
|
|
153
|
+
<zn-schedule-builder
|
|
154
|
+
show-timezone
|
|
155
|
+
timezones="common"
|
|
156
|
+
display-timezone="auto"
|
|
157
|
+
view="form"
|
|
158
|
+
value='{"timezone":"UTC","days":{"mon":["13:00-21:00"],"tue":["13:00-21:00"]}}'>
|
|
159
|
+
</zn-schedule-builder>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Sets and explicit names can be mixed — `timezones="en Asia/Tokyo"` adds Tokyo to the six above, and a
|
|
163
|
+
set's friendly name wins over a later plain listing of the same zone. The viewer's own timezone plus
|
|
164
|
+
both configured zones are always added, so the current selection is never missing from the list.
|
|
165
|
+
|
|
166
|
+
```html:preview
|
|
167
|
+
<zn-schedule-builder
|
|
168
|
+
show-timezone
|
|
169
|
+
display-timezone="America/New_York"
|
|
170
|
+
timezones="UTC Europe/London America/New_York Asia/Tokyo"
|
|
171
|
+
view="form"
|
|
172
|
+
value='{"timezone":"UTC","days":{"mon":["13:00-21:00"],"tue":["13:00-21:00"]}}'>
|
|
173
|
+
</zn-schedule-builder>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Reading the hours in code follows the same split: `schedule`, `value` and `getDay()` are in the save
|
|
177
|
+
timezone, while `displayedDays` and `setDisplayDay()` are in the display timezone.
|
|
178
|
+
|
|
179
|
+
:::tip
|
|
180
|
+
A weekly pattern has no date of its own, so there is no way to know whether daylight saving applies
|
|
181
|
+
to it. Offsets are resolved against today by default; set `reference-date` to a `YYYY-MM-DD` date to
|
|
182
|
+
pin them to a specific point in the year.
|
|
183
|
+
:::
|
|
184
|
+
|
|
185
|
+
### Open and Closed Labels
|
|
186
|
+
|
|
187
|
+
`open-label` and `closed-label` name the two states. They appear in the calendar legend, against
|
|
188
|
+
days with no hours, and in the exception annotations, so use whatever wording fits the thing being
|
|
189
|
+
scheduled — availability, cover, staffing, opening hours.
|
|
190
|
+
|
|
191
|
+
```html:preview
|
|
192
|
+
<zn-schedule-builder
|
|
193
|
+
open-label="On call"
|
|
194
|
+
closed-label="Off"
|
|
195
|
+
start-hour="8"
|
|
196
|
+
end-hour="20"
|
|
197
|
+
hide-summary
|
|
198
|
+
value='{"mon":["09:00-17:00"],"tue":["09:00-17:00"],"wed":["09:00-17:00"],"thu":["09:00-17:00"],"fri":["09:00-13:00"]}'>
|
|
199
|
+
</zn-schedule-builder>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Labels and Help Text
|
|
203
|
+
|
|
204
|
+
```html:preview
|
|
205
|
+
<zn-schedule-builder
|
|
206
|
+
label="Opening hours"
|
|
207
|
+
help-text="Customers can only book appointments during these hours."
|
|
208
|
+
view="form"
|
|
209
|
+
value='{"mon":["09:00-17:00"],"tue":["09:00-17:00"],"wed":["09:00-17:00"],"thu":["09:00-17:00"],"fri":["09:00-17:00"]}'>
|
|
210
|
+
</zn-schedule-builder>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Grid Range and Granularity
|
|
214
|
+
|
|
215
|
+
`start-hour` and `end-hour` set the window the calendar draws, and `interval` sets both the grid
|
|
216
|
+
granularity and the step of the time inputs in the form view. The interval must be one of `5`, `10`,
|
|
217
|
+
`15`, `20`, `30` or `60` minutes.
|
|
218
|
+
|
|
219
|
+
```html:preview
|
|
220
|
+
<zn-schedule-builder
|
|
221
|
+
start-hour="7"
|
|
222
|
+
end-hour="13"
|
|
223
|
+
interval="15"
|
|
224
|
+
hide-summary
|
|
225
|
+
value='{"mon":["08:00-12:00"],"tue":["08:15-11:45"],"wed":["08:00-12:00"],"thu":["08:00-12:00"],"fri":["08:00-11:00"]}'>
|
|
226
|
+
</zn-schedule-builder>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Exceptions
|
|
230
|
+
|
|
231
|
+
Exceptions are dated deviations from the weekly pattern. The builder never edits them — it displays
|
|
232
|
+
them, and carries them through the value untouched — so the surrounding application stays in charge
|
|
233
|
+
of how they are created.
|
|
234
|
+
|
|
235
|
+
Hours an exception removes are drawn in a lighter shade in the calendar, and annotated against the
|
|
236
|
+
day in the form view. An exception can be a single `date`, or a `from`/`to` window, and can either
|
|
237
|
+
close the day outright (`closed`) or replace its hours (`ranges`).
|
|
238
|
+
|
|
239
|
+
Only exceptions that change the repeating weekly pattern are drawn against it: ones that name their
|
|
240
|
+
`days`, and ones whose date window covers at least a full week. A one-off date is a single
|
|
241
|
+
occurrence rather than a pattern, so it is carried through the value untouched but leaves the grid
|
|
242
|
+
alone.
|
|
243
|
+
|
|
244
|
+
```html:preview
|
|
245
|
+
<zn-schedule-builder
|
|
246
|
+
view="form"
|
|
247
|
+
value='{
|
|
248
|
+
"days": {
|
|
249
|
+
"mon": ["08:00-18:00"],
|
|
250
|
+
"tue": ["08:00-18:00"],
|
|
251
|
+
"wed": ["08:00-18:00"],
|
|
252
|
+
"thu": ["08:00-18:00"],
|
|
253
|
+
"fri": ["08:00-16:00"],
|
|
254
|
+
"sat": ["09:00-13:00"],
|
|
255
|
+
"sun": []
|
|
256
|
+
},
|
|
257
|
+
"exceptions": [
|
|
258
|
+
{"label": "Summer Fridays", "from": "2026-06-01", "to": "2026-08-29", "days": ["fri"], "ranges": ["08:00-16:00"]},
|
|
259
|
+
{"label": "Bank holiday", "date": "2026-08-31", "closed": true}
|
|
260
|
+
]
|
|
261
|
+
}'>
|
|
262
|
+
</zn-schedule-builder>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Week Start and Time Format
|
|
266
|
+
|
|
267
|
+
```html:preview
|
|
268
|
+
<zn-schedule-builder
|
|
269
|
+
week-start="sun"
|
|
270
|
+
time-format="12"
|
|
271
|
+
view="form"
|
|
272
|
+
value='{"mon":["08:00-18:00"],"sat":["09:00-13:00"],"sun":["10:00-16:00"]}'>
|
|
273
|
+
</zn-schedule-builder>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Readonly and Disabled
|
|
277
|
+
|
|
278
|
+
Use `readonly` to display a schedule without any editing affordances, and `disabled` to grey it out
|
|
279
|
+
and take it out of form submission.
|
|
280
|
+
|
|
281
|
+
```html:preview
|
|
282
|
+
<zn-schedule-builder
|
|
283
|
+
readonly
|
|
284
|
+
view="form"
|
|
285
|
+
value='{"mon":["08:00-18:00"],"tue":["08:00-18:00"],"wed":["08:00-18:00"],"thu":["08:00-18:00"],"fri":["08:00-18:00"],"sat":["09:00-13:00"]}'>
|
|
286
|
+
</zn-schedule-builder>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Reading the Value from JavaScript
|
|
290
|
+
|
|
291
|
+
```html:preview
|
|
292
|
+
<zn-schedule-builder
|
|
293
|
+
id="schedule-demo"
|
|
294
|
+
view="form"
|
|
295
|
+
value='{"mon":["09:00-17:00"],"tue":["09:00-17:00"]}'>
|
|
296
|
+
</zn-schedule-builder>
|
|
297
|
+
|
|
298
|
+
<pre id="schedule-demo-output" style="margin-top: 1rem"></pre>
|
|
299
|
+
|
|
300
|
+
<script>
|
|
301
|
+
const builder = document.querySelector('#schedule-demo');
|
|
302
|
+
const output = document.querySelector('#schedule-demo-output');
|
|
303
|
+
|
|
304
|
+
const print = () => (output.textContent = JSON.stringify(builder.schedule, null, 2));
|
|
305
|
+
|
|
306
|
+
builder.addEventListener('zn-change', print);
|
|
307
|
+
print();
|
|
308
|
+
</script>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### In a Form
|
|
312
|
+
|
|
313
|
+
The schedule posts as a single JSON field named after the control.
|
|
314
|
+
|
|
315
|
+
```html:preview
|
|
316
|
+
<form class="schedule-form">
|
|
317
|
+
<zn-schedule-builder
|
|
318
|
+
name="opening-hours"
|
|
319
|
+
label="Opening hours"
|
|
320
|
+
required
|
|
321
|
+
view="form"
|
|
322
|
+
value='{"mon":["09:00-17:00"],"tue":["09:00-17:00"],"wed":["09:00-17:00"],"thu":["09:00-17:00"],"fri":["09:00-17:00"]}'>
|
|
323
|
+
</zn-schedule-builder>
|
|
324
|
+
|
|
325
|
+
<br />
|
|
326
|
+
<zn-button type="submit">Submit</zn-button>
|
|
327
|
+
</form>
|
|
328
|
+
|
|
329
|
+
<script>
|
|
330
|
+
const form = document.querySelector('.schedule-form');
|
|
331
|
+
|
|
332
|
+
form.addEventListener('submit', event => {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
const data = new FormData(form);
|
|
335
|
+
alert(data.get('opening-hours'));
|
|
336
|
+
});
|
|
337
|
+
</script>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Accessibility
|
|
341
|
+
|
|
342
|
+
The form view is fully keyboard operable: ranges are buttons, editing a range gives you two native
|
|
343
|
+
time inputs, and <kbd>Enter</kbd> or <kbd>Escape</kbd> closes the editor. The calendar view is a
|
|
344
|
+
pointer-driven surface, so offer the form view (the default toggle does) wherever keyboard-only
|
|
345
|
+
users need to edit a schedule.
|