@kubex/zinc 1.1.91 → 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.
- package/dist/custom-elements.json +1738 -110
- package/dist/vscode.html-custom-data.json +136 -4
- package/dist/web-types.json +307 -4
- package/dist/zn.d.ts +298 -2
- package/dist/zn.min.js +708 -516
- package/docs/pages/components/page-builder.md +22 -0
- package/docs/pages/components/schedule-builder.md +345 -0
- package/package.json +1 -1
- package/src/components/alert/alert.scss +9 -13
- package/src/components/chip/chip.scss +1 -1
- package/src/components/content-block/content-block.component.ts +3 -2
- package/src/components/icon-picker/icon-picker.component.ts +1 -1
- package/src/components/linked-select/linked-select.component.ts +22 -5
- package/src/components/page/page.scss +13 -7
- package/src/components/page-builder/page-builder.component.ts +52 -7
- package/src/components/page-builder/page-builder.scss +166 -9
- package/src/components/page-builder/page-builder.test.ts +134 -0
- package/src/components/page-builder/page.types.ts +23 -3
- 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/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/toggle/toggle.component.ts +2 -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,6 +44,24 @@ 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
|
+
|
|
43
65
|
## JavaScript API
|
|
44
66
|
|
|
45
67
|
- `state` — get/set the current `PageState`. The getter returns a deep copy; the setter
|
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
|
|
15
15
|
:host([appearance="solid"]) {
|
|
16
16
|
background-color: rgb(var(--zn-panel));
|
|
17
|
-
|
|
18
|
-
.wrapper:before {
|
|
19
|
-
backdrop-filter: none;
|
|
20
|
-
}
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
.alert-icon {
|
|
@@ -28,20 +24,20 @@ p {
|
|
|
28
24
|
margin: 0;
|
|
29
25
|
}
|
|
30
26
|
|
|
27
|
+
// No backdrop-filter here, deliberately. This used to carry a
|
|
28
|
+
// `::before { inset: 0; z-index: -1; backdrop-filter: blur(3px) }`, which forces
|
|
29
|
+
// a compositor render surface per alert and re-blurs whenever anything behind it
|
|
30
|
+
// repaints. Alerts are the one component that routinely appears dozens of times
|
|
31
|
+
// on a page (one per warning, per row), so that scaled into hundreds of render
|
|
32
|
+
// surfaces and could stop the compositor rasterising the page at all. The host
|
|
33
|
+
// background is `rgba(--zn-panel, 0.8)` over a flat panel, so the blur was not
|
|
34
|
+
// perceptible anyway — it only ever mattered over a high-frequency backdrop,
|
|
35
|
+
// which is not where alerts sit.
|
|
31
36
|
.wrapper {
|
|
32
37
|
position: relative;
|
|
33
38
|
border: 1px solid rgb(var(--zn-border-color)) !important;
|
|
34
39
|
width: 100%;
|
|
35
40
|
height: 100%;
|
|
36
|
-
|
|
37
|
-
&:before {
|
|
38
|
-
border-radius: 10px;
|
|
39
|
-
position: absolute;
|
|
40
|
-
content: '';
|
|
41
|
-
inset: 0;
|
|
42
|
-
z-index: -1;
|
|
43
|
-
backdrop-filter: blur(3px);
|
|
44
|
-
}
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
:host([center]) .content {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
font-size: var(--zn-text-chip-font-size);
|
|
11
11
|
font-family: var(--zn-font-family-sans);
|
|
12
12
|
font-weight: var(--zn-text-chip-font-weight);
|
|
13
|
-
border-radius: var(--zn-border-radius-
|
|
13
|
+
border-radius: var(--zn-border-radius-x-large);
|
|
14
14
|
padding-inline: var(--zn-spacing-x-small);
|
|
15
15
|
padding-block: 0;
|
|
16
16
|
height: var(--zn-spacing-medium);
|
|
@@ -4,6 +4,7 @@ import {HasSlotController} from "../../internal/slot";
|
|
|
4
4
|
import {html, unsafeCSS} from 'lit';
|
|
5
5
|
import {MutationController} from '@lit-labs/observers/mutation-controller.js';
|
|
6
6
|
import {property, queryAssignedNodes, queryAsync, state} from 'lit/decorators.js';
|
|
7
|
+
import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
|
|
7
8
|
import ZincElement from "../../internal/zinc-element";
|
|
8
9
|
import type {PropertyValues} from 'lit';
|
|
9
10
|
import type ZnTile from "../tile";
|
|
@@ -279,7 +280,7 @@ export default class ContentBlock extends ZincElement {
|
|
|
279
280
|
'text-section--text': section.type === 'text',
|
|
280
281
|
'hidden': section.type === 'reply'
|
|
281
282
|
})}>
|
|
282
|
-
${section.lines.map((line) => html`${line}<br>`)}
|
|
283
|
+
${section.lines.map((line) => html`${unsafeHTML(line)}<br>`)}
|
|
283
284
|
</div>
|
|
284
285
|
`)}
|
|
285
286
|
</div>
|
|
@@ -374,7 +375,7 @@ export default class ContentBlock extends ZincElement {
|
|
|
374
375
|
const textRows: TextRow[] = [];
|
|
375
376
|
|
|
376
377
|
if (textContent) {
|
|
377
|
-
const text = textContent.innerText
|
|
378
|
+
const text = textContent.innerText;
|
|
378
379
|
const rows = text.split('\n');
|
|
379
380
|
let previousType: TextRow['type'] | null = null;
|
|
380
381
|
let forceReply = false;
|
|
@@ -319,7 +319,7 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
319
319
|
|
|
320
320
|
return html`
|
|
321
321
|
<div part="form-control"
|
|
322
|
-
class="form-control ${hasLabel ? 'form-control--has-label' : ''} ${hasHelpText ? 'form-control--has-help-text' : ''}">
|
|
322
|
+
class="form-control form-control--medium ${hasLabel ? 'form-control--has-label' : ''} ${hasHelpText ? 'form-control--has-help-text' : ''}">
|
|
323
323
|
|
|
324
324
|
<label part="form-control-label" class="form-control__label"
|
|
325
325
|
aria-hidden=${hasLabel ? 'false' : 'true'}>
|
|
@@ -12,8 +12,9 @@ interface linkedSelectOption {
|
|
|
12
12
|
[key: string]: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Each group is a flat value -> label map, keyed on the linked select's value.
|
|
15
16
|
interface linkedSelectOptions {
|
|
16
|
-
[key: string]: linkedSelectOption
|
|
17
|
+
[key: string]: linkedSelectOption;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -46,6 +47,9 @@ export default class ZnLinkedSelect extends ZincElement implements ZincFormContr
|
|
|
46
47
|
@property({attribute: 'cache-key'}) cacheKey: string = "";
|
|
47
48
|
@property() label: string = "";
|
|
48
49
|
|
|
50
|
+
/** Automatically select the first option of the linked group when no value is set. */
|
|
51
|
+
@property({attribute: 'select-first', type: Boolean}) selectFirst = false;
|
|
52
|
+
|
|
49
53
|
@query('zn-select') input: ZnSelect;
|
|
50
54
|
|
|
51
55
|
private linkedSelectElement: HTMLSelectElement | ZnSelect;
|
|
@@ -123,7 +127,10 @@ export default class ZnLinkedSelect extends ZincElement implements ZincFormContr
|
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
public handleLinkedSelectChange = () => {
|
|
126
|
-
|
|
130
|
+
// The inner select only auto-selects on slotchange, which lit skips when it
|
|
131
|
+
// can reuse the option elements of the outgoing group, so the first option of
|
|
132
|
+
// the new group is resolved here rather than left to the select.
|
|
133
|
+
this.value = this.selectFirst ? Object.keys(this.currentOptions())[0] ?? "" : "";
|
|
127
134
|
this.requestUpdate();
|
|
128
135
|
this.formControlController.updateValidity();
|
|
129
136
|
};
|
|
@@ -137,19 +144,29 @@ export default class ZnLinkedSelect extends ZincElement implements ZincFormContr
|
|
|
137
144
|
this.value = (e.target as ZnSelect).value as string;
|
|
138
145
|
}
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
/** The options of the group the linked select currently points at. */
|
|
148
|
+
private currentOptions(): linkedSelectOption {
|
|
149
|
+
if (!this.options) {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
let selected = this.linkedSelectElement?.value as string;
|
|
142
|
-
if (!selected
|
|
154
|
+
if (!selected) {
|
|
143
155
|
selected = Object.keys(this.options)[0];
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
|
|
158
|
+
return (selected ? this.options[selected] : {}) ?? {};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
render() {
|
|
162
|
+
const options = this.currentOptions();
|
|
147
163
|
return html`
|
|
148
164
|
<zn-select part="select"
|
|
149
165
|
class="linked-select"
|
|
150
166
|
name="${this.name}"
|
|
151
167
|
id="main-input"
|
|
152
168
|
cache-key="${this.cacheKey}"
|
|
169
|
+
?select-first="${this.selectFirst}"
|
|
153
170
|
value="${this.value}"
|
|
154
171
|
@zn-change=${this.handleSelectChange}
|
|
155
172
|
label="${this.label}">
|
|
@@ -57,8 +57,20 @@
|
|
|
57
57
|
background: linear-gradient(225deg, rgba(233, 227, 255, 1) 0%, rgba(214, 246, 245, 1) 40%, rgba(233, 227, 255, 1) 100%), linear-gradient(180deg, rgba(129, 153, 217, 0) 0%, rgba(0, 0, 0, 1) 100%);
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
// Held still deliberately. This gradient used to drift via
|
|
61
|
+
// `animation: page-header 25s ease infinite`, which animates
|
|
62
|
+
// `background-position` — a paint-only property, so it can never be
|
|
63
|
+
// composited. That repainted the header every frame for as long as the page
|
|
64
|
+
// was open, and because the header is `position: sticky` its paint chunk
|
|
65
|
+
// spans the whole page, so every backdrop-filter behind it (zn-alert, for
|
|
66
|
+
// one) was re-blurred each frame as well. On content-heavy pages that put
|
|
67
|
+
// enough work into each frame that tiles never finished rasterising: blank
|
|
68
|
+
// regions, content appearing only as you scrolled, and a dead inspector
|
|
69
|
+
// overlay. `0% 7%` is frame zero of the old keyframes, so the header looks as
|
|
70
|
+
// it did. If the drift is wanted back it has to move a compositable property
|
|
71
|
+
// (transform on a clipped, contained layer), not background-position.
|
|
60
72
|
background-size: 150% 150%;
|
|
61
|
-
|
|
73
|
+
background-position: 0% 7%;
|
|
62
74
|
|
|
63
75
|
.header:not(.header--has-navigation) {
|
|
64
76
|
padding-bottom: var(--zn-base-gap);
|
|
@@ -91,12 +103,6 @@
|
|
|
91
103
|
}
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
@keyframes page-header {
|
|
95
|
-
0%{background-position:0% 7%}
|
|
96
|
-
50%{background-position:100% 94%}
|
|
97
|
-
100%{background-position:0% 7%}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
106
|
.header {
|
|
101
107
|
display: flex;
|
|
102
108
|
flex-direction: column;
|
|
@@ -77,6 +77,8 @@ function timeAgo(ms: number): string {
|
|
|
77
77
|
* @csspart palette - The left palette panel.
|
|
78
78
|
* @csspart canvas - The centre section-card canvas.
|
|
79
79
|
* @csspart inspector - The right panel while a section is selected.
|
|
80
|
+
* @csspart inspector-header - The inspector's fixed header (icon, section name, type, close).
|
|
81
|
+
* @csspart inspector-body - The inspector's scrolling form area.
|
|
80
82
|
*/
|
|
81
83
|
export default class ZnPageBuilder extends ZincElement {
|
|
82
84
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
@@ -1017,6 +1019,21 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
1017
1019
|
const form = document.createElement('div');
|
|
1018
1020
|
form.className = 'inspector__form';
|
|
1019
1021
|
form.append(type.configTemplate.content.cloneNode(true));
|
|
1022
|
+
// Custom elements cloned out of a <template> are inert until they are
|
|
1023
|
+
// connected, so their accessors do not exist yet. Assigning `value` to one
|
|
1024
|
+
// would define an own property that permanently shadows the setter the
|
|
1025
|
+
// element defines on upgrade — Lit only replays properties it declares
|
|
1026
|
+
// reactive, so a hand-written `value` accessor (zn-icon-picker) would keep
|
|
1027
|
+
// its default and the control would render empty. Upgrade first.
|
|
1028
|
+
//
|
|
1029
|
+
// Toggles default to a stacked label, which reads badly in a narrow panel —
|
|
1030
|
+
// put the switch on the label's right unless the host asked for a position.
|
|
1031
|
+
// Set before the upgrade, while `label-position` can only be an authored
|
|
1032
|
+
// attribute and never one zn-toggle reflected from its own default.
|
|
1033
|
+
form.querySelectorAll<HTMLElement>('zn-toggle:not([label-position])').forEach(toggle => {
|
|
1034
|
+
toggle.setAttribute('label-position', 'left');
|
|
1035
|
+
});
|
|
1036
|
+
customElements.upgrade(form);
|
|
1020
1037
|
form.querySelectorAll<HTMLElement>('[name]').forEach(control => {
|
|
1021
1038
|
const name = control.getAttribute('name')!;
|
|
1022
1039
|
const value = section.data[name];
|
|
@@ -1029,7 +1046,9 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
1029
1046
|
(control as HTMLInputElement).value = String(value);
|
|
1030
1047
|
}
|
|
1031
1048
|
});
|
|
1032
|
-
|
|
1049
|
+
// A content-free template stamps no controls — leave the form null so the
|
|
1050
|
+
// inspector shows its "no settings" hint rather than a blank gap.
|
|
1051
|
+
this._form = form.childElementCount ? form : null;
|
|
1033
1052
|
}
|
|
1034
1053
|
|
|
1035
1054
|
private _isBooleanControl(control: HTMLElement, value: unknown): boolean {
|
|
@@ -1069,22 +1088,48 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
1069
1088
|
const section = this._selectedSection();
|
|
1070
1089
|
if (!section) return html``;
|
|
1071
1090
|
const type = this.registry.get(section.type);
|
|
1091
|
+
const hasConfig = Boolean(type?.renderConfig) || this._form !== null;
|
|
1092
|
+
const title = section.label ?? type?.label ?? section.type;
|
|
1093
|
+
const typeLabel = type?.label ?? section.type;
|
|
1072
1094
|
return html`
|
|
1073
1095
|
<aside part="inspector" class="inspector">
|
|
1074
|
-
<
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1096
|
+
<div part="inspector-header" class="inspector-head">
|
|
1097
|
+
<span
|
|
1098
|
+
class="inspector-head__icon"
|
|
1099
|
+
style="--section-accent:${type?.color ?? 'rgb(var(--zn-color-primary))'}">
|
|
1100
|
+
<zn-icon src="${type?.icon ?? 'widgets'}" library="${ifDefined(type?.iconLibrary)}" size="18"></zn-icon>
|
|
1101
|
+
</span>
|
|
1102
|
+
<div class="inspector-head__text">
|
|
1103
|
+
<div class="inspector-head__title">${title}</div>
|
|
1104
|
+
${title === typeLabel ? '' : html`
|
|
1105
|
+
<div class="inspector-head__type">${typeLabel}</div>`}
|
|
1106
|
+
</div>
|
|
1107
|
+
<button
|
|
1108
|
+
type="button"
|
|
1109
|
+
class="inspector-close"
|
|
1110
|
+
title="Close section settings"
|
|
1111
|
+
aria-label="Close section settings"
|
|
1112
|
+
@click="${() => this._select(null)}">
|
|
1113
|
+
<zn-icon src="x@lu" size="18"></zn-icon>
|
|
1114
|
+
</button>
|
|
1115
|
+
</div>
|
|
1079
1116
|
<div
|
|
1080
|
-
|
|
1117
|
+
part="inspector-body"
|
|
1118
|
+
class="inspector-body"
|
|
1081
1119
|
@change="${this._onInspectorInput}"
|
|
1082
1120
|
@zn-change="${this._onInspectorInput}"
|
|
1083
1121
|
@input="${this._onInspectorInput}"
|
|
1084
1122
|
@zn-input="${this._onInspectorInput}">
|
|
1123
|
+
<zn-input
|
|
1124
|
+
class="inspector__rename"
|
|
1125
|
+
label="Section name"
|
|
1126
|
+
.value="${section.label ?? type?.label ?? ''}"
|
|
1127
|
+
@zn-change="${(e: Event) => this._renameSection(section.id, String((e.target as ZnInput).value ?? ''))}"></zn-input>
|
|
1085
1128
|
${type?.renderConfig
|
|
1086
1129
|
? type.renderConfig(section, data => this._updateSectionData(section.id, data))
|
|
1087
1130
|
: this._form}
|
|
1131
|
+
${hasConfig ? '' : html`
|
|
1132
|
+
<p class="inspector-hint">This section type has no settings.</p>`}
|
|
1088
1133
|
</div>
|
|
1089
1134
|
</aside>`;
|
|
1090
1135
|
}
|