@kubex/zinc 1.1.94 → 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 +995 -196
- package/dist/vscode.html-custom-data.json +74 -19
- package/dist/web-types.json +147 -37
- package/dist/zn.d.ts +335 -59
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +467 -355
- package/docs/pages/components/page-builder.md +121 -19
- 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/button/button.scss +5 -2
- package/src/components/inline-edit/inline-edit.component.ts +6 -1
- package/src/components/input/input.component.ts +12 -2
- package/src/components/page/page.scss +7 -2
- 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 +483 -225
- package/src/components/page-builder/page-builder.scss +64 -10
- package/src/components/page-builder/page-builder.test.ts +656 -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 +75 -7
- 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/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/translations/translations.component.ts +5 -1
|
@@ -62,13 +62,24 @@ toggle to opt out.
|
|
|
62
62
|
|
|
63
63
|
Style the panel through `inspector`, `inspector-header` and `inspector-body` parts.
|
|
64
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
|
+
|
|
65
72
|
## JavaScript API
|
|
66
73
|
|
|
67
74
|
- `state` — get/set the current `PageState`. The getter returns a deep copy; the setter
|
|
68
75
|
replaces the state wholesale (like the `config` attribute, it does not emit `zn-page-change`).
|
|
69
76
|
- `addSection(type, index?)` — insert a new section of a registered type (default: at the end).
|
|
70
|
-
- `
|
|
71
|
-
|
|
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.
|
|
72
83
|
- `undo()` / `redo()` — step through edit history (bounded at 50 entries). There is no built-in
|
|
73
84
|
toolbar: wire these to your own header buttons or keyboard shortcuts.
|
|
74
85
|
- `registerSectionType(type)` / `registerSectionTypes(types)` — programmatic registration,
|
|
@@ -114,8 +125,8 @@ or call `restoreAutoSave()` yourself.
|
|
|
114
125
|
|
|
115
126
|
Every edit emits `zn-page-change` with the full page state (`event.detail.state`, also
|
|
116
127
|
readable via the `state` property) — plain JSON the host persists and later feeds back in
|
|
117
|
-
through the `config` attribute. Sections appear in page order; container
|
|
118
|
-
`
|
|
128
|
+
through the `config` attribute. Sections appear in page order; a container section additionally
|
|
129
|
+
carries `layout` and `cells`, per the Containers section below:
|
|
119
130
|
|
|
120
131
|
```json
|
|
121
132
|
{
|
|
@@ -130,10 +141,11 @@ through the `config` attribute. Sections appear in page order; container section
|
|
|
130
141
|
"type": "article-grid",
|
|
131
142
|
"label": "Popular articles",
|
|
132
143
|
"data": {"title": "Popular"},
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
{"id": "s-
|
|
136
|
-
|
|
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
|
+
[]
|
|
137
149
|
]
|
|
138
150
|
},
|
|
139
151
|
{
|
|
@@ -145,29 +157,119 @@ through the `config` attribute. Sections appear in page order; container section
|
|
|
145
157
|
}
|
|
146
158
|
```
|
|
147
159
|
|
|
148
|
-
|
|
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.
|
|
163
|
+
|
|
164
|
+
## A required first section
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
155
221
|
|
|
156
222
|
```html:preview
|
|
157
223
|
<zn-page-builder heading="KB Homepage" style="height: 560px"
|
|
158
|
-
config='{"sections":[{"id":"g1","type":"
|
|
159
|
-
<template type="
|
|
160
|
-
|
|
161
|
-
|
|
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>
|
|
162
231
|
<zn-input name="title" label="Grid title"></zn-input>
|
|
163
232
|
</template>
|
|
164
|
-
<template type="article
|
|
233
|
+
<template type="article" slot="config" label="Article" icon="article" category="Content"
|
|
165
234
|
description="A single article tile">
|
|
166
235
|
<zn-input name="article" label="Article id"></zn-input>
|
|
167
236
|
</template>
|
|
168
237
|
</zn-page-builder>
|
|
169
238
|
```
|
|
170
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
|
+
|
|
171
273
|
## List sections
|
|
172
274
|
|
|
173
275
|
Sections that show a set of existing items (categories, articles, …) reference them by id:
|
|
@@ -16,15 +16,16 @@ for you, as [`zn-textarea`](/components/textarea#slash-menu-quick-insertions) do
|
|
|
16
16
|
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}"></zn-textarea>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Slot one into a textarea when you want the panel's own settings — `
|
|
20
|
-
|
|
19
|
+
Slot one into a textarea when you want the panel's own settings — `max-items`, `placement`, `empty-text`, its width, or
|
|
20
|
+
the `heading` the list is announced by — declared in markup. The textarea then drives your menu instead of building its
|
|
21
|
+
own:
|
|
21
22
|
|
|
22
23
|
```html:preview
|
|
23
24
|
<zn-textarea label="Terms and conditions" rows="6" help-text="Type / to insert">
|
|
24
25
|
<zn-slash-menu slot="slash-menu" heading="Replacement strings" max-items="6" style="--slash-menu-width: 360px">
|
|
25
|
-
<zn-slash-item icon="tag@lu" label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
26
|
-
<zn-slash-item icon="building@lu" label="Legal entity" value="{{LEGAL_ENTITY}}"></zn-slash-item>
|
|
27
|
-
<zn-slash-item icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item>
|
|
26
|
+
<zn-slash-item group="Merchant" icon="tag@lu" label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
27
|
+
<zn-slash-item group="Merchant" icon="building@lu" label="Legal entity" value="{{LEGAL_ENTITY}}"></zn-slash-item>
|
|
28
|
+
<zn-slash-item group="Policy" icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item>
|
|
28
29
|
</zn-slash-menu>
|
|
29
30
|
</zn-textarea>
|
|
30
31
|
```
|
|
@@ -107,9 +108,134 @@ The menu doesn't listen for keys itself — whatever owns the field decides whic
|
|
|
107
108
|
</script>
|
|
108
109
|
```
|
|
109
110
|
|
|
111
|
+
### Grouping Items
|
|
112
|
+
|
|
113
|
+
An item's `group` puts a heading above it. The heading is drawn whenever the group changes going down the list, so
|
|
114
|
+
items sharing a group must be declared together — the menu lists them in the order it is given rather than gathering
|
|
115
|
+
them for you. Leave the group off and an item is listed under no heading at all; declared first, those lead the list,
|
|
116
|
+
which is how a handful of favourites can sit above named sections.
|
|
117
|
+
|
|
118
|
+
```html:preview
|
|
119
|
+
<zn-textarea label="Privacy policy" rows="7" help-text="Type / to see the sections, or 'company' to search across them">
|
|
120
|
+
<zn-slash-menu slot="slash-menu" style="--slash-menu-width: 340px">
|
|
121
|
+
<zn-slash-item icon="star@lu" label="Merchant block" value="{{MERCHANT_BLOCK}}"></zn-slash-item>
|
|
122
|
+
<zn-slash-item group="Merchant" icon="tag@lu" label="Brand name" keywords="company" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
123
|
+
<zn-slash-item group="Merchant" icon="building@lu" label="Legal entity" keywords="company" value="{{LEGAL_ENTITY}}"></zn-slash-item>
|
|
124
|
+
<zn-slash-item group="Customer" icon="user@lu" label="Customer name" value="{{CUSTOMER_NAME}}"></zn-slash-item>
|
|
125
|
+
<zn-slash-item group="Customer" icon="mail@lu" label="Customer email" value="{{CUSTOMER_EMAIL}}"></zn-slash-item>
|
|
126
|
+
<zn-slash-item group="Policy" icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item>
|
|
127
|
+
<zn-slash-item group="Policy" icon="calendar@lu" label="Refund window" value="{{REFUND_DAYS}} days"></zn-slash-item>
|
|
128
|
+
</zn-slash-menu>
|
|
129
|
+
</zn-textarea>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Group headings are only a structure for browsing: a query ranks every match on merit, and the headings follow whatever
|
|
133
|
+
order that leaves. Set `order` on an item to pin its place within a match band, and use `keywords` to make it findable
|
|
134
|
+
by terms that aren't in its label.
|
|
135
|
+
|
|
136
|
+
Driving the menu yourself, the same thing is a `group` on each item:
|
|
137
|
+
|
|
138
|
+
```html:preview
|
|
139
|
+
<zn-button id="grouped-anchor">Open the menu</zn-button>
|
|
140
|
+
<zn-slash-menu id="grouped-menu"></zn-slash-menu>
|
|
141
|
+
<div id="grouped-log" style="margin-top: 1rem; font-family: monospace; font-size: 0.875rem;"></div>
|
|
142
|
+
|
|
143
|
+
<script type="module">
|
|
144
|
+
const anchor = document.getElementById('grouped-anchor');
|
|
145
|
+
const menu = document.getElementById('grouped-menu');
|
|
146
|
+
const log = document.getElementById('grouped-log');
|
|
147
|
+
|
|
148
|
+
await customElements.whenDefined('zn-slash-menu');
|
|
149
|
+
|
|
150
|
+
menu.items = [
|
|
151
|
+
{label: 'Paragraph', value: '', icon: 'type@lu', group: 'Basic blocks'},
|
|
152
|
+
{label: 'Heading 1', value: '# ', icon: 'heading-1@lu', group: 'Basic blocks'},
|
|
153
|
+
{label: 'Heading 2', value: '## ', icon: 'heading-2@lu', group: 'Basic blocks'},
|
|
154
|
+
{label: 'To-do list', value: '- [ ] ', icon: 'square-check@lu', group: 'Lists'},
|
|
155
|
+
{label: 'Bulleted list', value: '- ', icon: 'list@lu', group: 'Lists'},
|
|
156
|
+
{label: 'Callout', value: 'NOTE: ', icon: 'info@lu', group: 'Advanced'},
|
|
157
|
+
{label: 'Quote', value: '> ', icon: 'quote@lu', group: 'Advanced'}
|
|
158
|
+
];
|
|
159
|
+
menu.hideKeys = true;
|
|
160
|
+
menu.anchor = anchor;
|
|
161
|
+
|
|
162
|
+
anchor.addEventListener('click', () => menu.open ? menu.hide() : menu.show());
|
|
163
|
+
|
|
164
|
+
menu.addEventListener('zn-slash-item-select', (event) => {
|
|
165
|
+
log.textContent = `selected ${event.detail.item.label} from ${event.detail.item.group}`;
|
|
166
|
+
menu.hide();
|
|
167
|
+
});
|
|
168
|
+
</script>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Keyboard Hints
|
|
172
|
+
|
|
173
|
+
A footer pinned to the bottom of the panel spells out the keys the menu answers to. The list scrolls beneath it, so
|
|
174
|
+
the hints stay in view. Use `hide-hints` on menus driven entirely by the mouse, or where the surrounding UI already
|
|
175
|
+
explains the shortcuts.
|
|
176
|
+
|
|
177
|
+
```html:preview
|
|
178
|
+
<zn-textarea label="Terms and conditions" rows="4" help-text="Type / to see the hints"
|
|
179
|
+
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}}">
|
|
180
|
+
<zn-slash-menu slot="slash-menu" heading="Replacement strings"></zn-slash-menu>
|
|
181
|
+
</zn-textarea>
|
|
182
|
+
|
|
183
|
+
<zn-textarea label="Internal note" rows="4" help-text="Type / — no hints"
|
|
184
|
+
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}}">
|
|
185
|
+
<zn-slash-menu slot="slash-menu" heading="Replacement strings" hide-hints></zn-slash-menu>
|
|
186
|
+
</zn-textarea>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Recently Used
|
|
190
|
+
|
|
191
|
+
Set `recent-key` and the menu remembers what was chosen there, listing the most recent of those items above the rest
|
|
192
|
+
under their own heading. The key is where the menu is used — `page-body`, `ticket-reply` — so each place keeps its own
|
|
193
|
+
history in `localStorage`, and two fields that should share one can share a key. `max-recent` caps the section
|
|
194
|
+
(3 by default), `recent-heading` names it, and `clearRecent()` forgets the lot. The section stands aside as soon as
|
|
195
|
+
there is a query, when the ranked matches are the better answer.
|
|
196
|
+
|
|
197
|
+
It reads as one more group, so it sits naturally above [grouped items](#grouping-items) — the section's own heading, then
|
|
198
|
+
the sections the list already had. Insert a few from the first field below to see it fill:
|
|
199
|
+
|
|
200
|
+
```html:preview
|
|
201
|
+
<zn-textarea label="Privacy policy" rows="6" help-text="Type / and insert a few — they come back to the top">
|
|
202
|
+
<zn-slash-menu slot="slash-menu" recent-key="docs-grouped" style="--slash-menu-width: 340px">
|
|
203
|
+
<zn-slash-item group="Merchant" icon="tag@lu" label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
204
|
+
<zn-slash-item group="Merchant" icon="building@lu" label="Legal entity" value="{{LEGAL_ENTITY}}"></zn-slash-item>
|
|
205
|
+
<zn-slash-item group="Customer" icon="user@lu" label="Customer name" value="{{CUSTOMER_NAME}}"></zn-slash-item>
|
|
206
|
+
<zn-slash-item group="Customer" icon="mail@lu" label="Customer email" value="{{CUSTOMER_EMAIL}}"></zn-slash-item>
|
|
207
|
+
<zn-slash-item group="Policy" icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item>
|
|
208
|
+
</zn-slash-menu>
|
|
209
|
+
</zn-textarea>
|
|
210
|
+
|
|
211
|
+
<zn-button id="forget-recent" style="margin-top: 1rem">Forget them</zn-button>
|
|
212
|
+
|
|
213
|
+
<script type="module">
|
|
214
|
+
import {clearRecentSlashItems} from '/dist/zn.min.js';
|
|
215
|
+
|
|
216
|
+
document.getElementById('forget-recent').addEventListener('click', () => {
|
|
217
|
+
clearRecentSlashItems('docs-grouped');
|
|
218
|
+
clearRecentSlashItems('docs-terms');
|
|
219
|
+
});
|
|
220
|
+
</script>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Where the items below carry no heading of their own, a rule closes the section off instead. This field shares nothing
|
|
224
|
+
with the one above — each key is its own history:
|
|
225
|
+
|
|
226
|
+
```html:preview
|
|
227
|
+
<zn-textarea label="Terms and conditions" rows="5" help-text="Type / and insert a few — the rule marks where they end"
|
|
228
|
+
slash-recent-key="docs-terms"
|
|
229
|
+
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}},
|
|
230
|
+
Customer name={{CUSTOMER_NAME}}, Support email={{SUPPORT_EMAIL}}"></zn-textarea>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`zn-input`, `zn-inline-edit`, `zn-translations` and `zn-remarkd-editor` take the same `slash-recent-key`. On a menu you
|
|
234
|
+
slot in yourself, or drive with `SlashMenuController`, set `recent-key` on the `zn-slash-menu` directly.
|
|
235
|
+
|
|
110
236
|
### Truncating Long Lists
|
|
111
237
|
|
|
112
|
-
`max-items` caps how many items are rendered; the rest are reported in a footer rather than silently dropped. The
|
|
238
|
+
`max-items` caps how many items are rendered; the rest are reported in a footer rather than silently dropped. The list
|
|
113
239
|
scrolls when its content exceeds `--slash-menu-max-height`.
|
|
114
240
|
|
|
115
241
|
```html:preview
|
|
@@ -501,6 +501,22 @@ the merge fields an application allows.
|
|
|
501
501
|
</script>
|
|
502
502
|
```
|
|
503
503
|
|
|
504
|
+
#### Recently Used Items
|
|
505
|
+
|
|
506
|
+
`slash-recent-key` remembers what was inserted here and lists those items above the rest next time, under a
|
|
507
|
+
"Recently used" heading. The key names the place the field is used, so each keeps its own history — see
|
|
508
|
+
[`zn-slash-menu`](/components/slash-menu#recently-used) for the menu's own settings.
|
|
509
|
+
|
|
510
|
+
```html:preview
|
|
511
|
+
<zn-textarea
|
|
512
|
+
label="Refund policy"
|
|
513
|
+
rows="5"
|
|
514
|
+
slash-recent-key="docs-textarea-refunds"
|
|
515
|
+
help-text="Type / and insert a couple — they come back to the top"
|
|
516
|
+
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}},
|
|
517
|
+
Refund window={{REFUND_DAYS}}, Support email={{SUPPORT_EMAIL}}"></zn-textarea>
|
|
518
|
+
```
|
|
519
|
+
|
|
504
520
|
#### Changing the Trigger
|
|
505
521
|
|
|
506
522
|
Set `slash-trigger` to any characters. Using `{{` lets someone who already knows the token they want type it directly
|
package/package.json
CHANGED
package/scss/_root.scss
CHANGED
|
@@ -350,8 +350,14 @@ ul.square {
|
|
|
350
350
|
|
|
351
351
|
// Frosted-glass inactive tabs for the zn-page tab navbar (consumed in
|
|
352
352
|
// components/navbar/navbar.scss, scoped there to :host-context(zn-page)).
|
|
353
|
+
//
|
|
354
|
+
// The translucent background carries the frosted look on its own. Do NOT put a
|
|
355
|
+
// backdrop-filter here: navbar.scss applies it per inactive tab, so each tab
|
|
356
|
+
// becomes its own render surface and re-samples + re-blurs its backdrop every
|
|
357
|
+
// frame. On a page with a normal number of tabs that alone pushed GPU draw to
|
|
358
|
+
// ~8ms/frame and dropped frames on every scroll.
|
|
353
359
|
--navbar-page-inactive-tab-background: rgba(255, 255, 255, 0.5);
|
|
354
|
-
--navbar-page-inactive-tab-filter:
|
|
360
|
+
--navbar-page-inactive-tab-filter: none;
|
|
355
361
|
|
|
356
362
|
.zn-primary {
|
|
357
363
|
color: rgb(var(--zn-color-primary));
|
|
@@ -456,8 +456,11 @@
|
|
|
456
456
|
inset-inline-start: 0;
|
|
457
457
|
bottom: 0;
|
|
458
458
|
border-left: solid 1px;
|
|
459
|
-
|
|
460
|
-
|
|
459
|
+
// PERF: a blend mode here gave every non-first button in every group its own
|
|
460
|
+
// render surface, and the :not(:hover) above meant hovering a group added and
|
|
461
|
+
// removed surfaces — re-running layerize and re-compositing on pointer move.
|
|
462
|
+
// A semi-transparent border reads the same over the button fills.
|
|
463
|
+
color: rgba(var(--zn-border-color), 0.85);
|
|
461
464
|
}
|
|
462
465
|
}
|
|
463
466
|
|
|
@@ -117,12 +117,15 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
117
117
|
/** The characters that open the slash menu. */
|
|
118
118
|
@property({ attribute: 'slash-trigger' }) slashTrigger = '/';
|
|
119
119
|
|
|
120
|
-
/** The
|
|
120
|
+
/** The name the slash menu's list is announced by. */
|
|
121
121
|
@property({ attribute: 'slash-heading' }) slashHeading = 'Insert';
|
|
122
122
|
|
|
123
123
|
/** Hides the insertion keys normally shown against the slash menu's items. */
|
|
124
124
|
@property({ attribute: 'slash-hide-keys', type: Boolean }) slashHideKeys = false;
|
|
125
125
|
|
|
126
|
+
/** Lists the slash menu items most recently chosen here above the rest, remembered under this key. */
|
|
127
|
+
@property({ attribute: 'slash-recent-key' }) slashRecentKey = '';
|
|
128
|
+
|
|
126
129
|
/** Resolves additional slash menu items each time the menu opens. JavaScript only. */
|
|
127
130
|
@property({ attribute: false }) slashItemsProvider?: (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
|
|
128
131
|
|
|
@@ -466,6 +469,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
466
469
|
slash-trigger="${this.slashTrigger}"
|
|
467
470
|
slash-heading="${this.slashHeading}"
|
|
468
471
|
slash-preset="${this.slashPreset}"
|
|
472
|
+
slash-recent-key="${this.slashRecentKey}"
|
|
469
473
|
?slash-hide-keys="${this.slashHideKeys}"
|
|
470
474
|
.slashItems="${this.slashItems}"
|
|
471
475
|
.slashItemsProvider="${this.slashItemsProvider}"
|
|
@@ -489,6 +493,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
489
493
|
slash-trigger="${this.slashTrigger}"
|
|
490
494
|
slash-heading="${this.slashHeading}"
|
|
491
495
|
slash-preset="${this.slashPreset}"
|
|
496
|
+
slash-recent-key="${this.slashRecentKey}"
|
|
492
497
|
?slash-hide-keys="${this.slashHideKeys}"
|
|
493
498
|
.slashItems="${this.slashItems}"
|
|
494
499
|
.slashItemsProvider="${this.slashItemsProvider}"
|
|
@@ -276,12 +276,18 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
276
276
|
/** The characters that open the slash menu. */
|
|
277
277
|
@property({attribute: 'slash-trigger'}) slashTrigger = '/';
|
|
278
278
|
|
|
279
|
-
/** The
|
|
279
|
+
/** The name the slash menu's list is announced by. */
|
|
280
280
|
@property({attribute: 'slash-heading'}) slashHeading = 'Insert';
|
|
281
281
|
|
|
282
282
|
/** Hides the insertion keys normally shown against the slash menu's items. */
|
|
283
283
|
@property({attribute: 'slash-hide-keys', type: Boolean}) slashHideKeys = false;
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Lists the items most recently chosen here above the rest, remembered in `localStorage` under this
|
|
287
|
+
* key. Share a key between the fields that should share a history; leave unset to offer no such list.
|
|
288
|
+
*/
|
|
289
|
+
@property({attribute: 'slash-recent-key'}) slashRecentKey = '';
|
|
290
|
+
|
|
285
291
|
/**
|
|
286
292
|
* Resolves additional items each time the menu opens, for lists that come from elsewhere (e.g. an
|
|
287
293
|
* API). Receives the current query and may return a promise. JavaScript only.
|
|
@@ -1100,7 +1106,11 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
1100
1106
|
</div>
|
|
1101
1107
|
${this.hasSlashMenu
|
|
1102
1108
|
? html`
|
|
1103
|
-
<zn-slash-menu
|
|
1109
|
+
<zn-slash-menu
|
|
1110
|
+
part="slash-menu"
|
|
1111
|
+
heading=${this.slashHeading}
|
|
1112
|
+
recent-key=${this.slashRecentKey}
|
|
1113
|
+
?hide-keys=${this.slashHideKeys}></zn-slash-menu>`
|
|
1104
1114
|
: ''}
|
|
1105
1115
|
<slot name="slash-menu"></slot>
|
|
1106
1116
|
<slot name="slash-items" hidden></slot>
|
|
@@ -93,8 +93,13 @@
|
|
|
93
93
|
background-repeat: no-repeat;
|
|
94
94
|
background-position: bottom center;
|
|
95
95
|
background-size: cover;
|
|
96
|
-
mix-blend-mode
|
|
97
|
-
|
|
96
|
+
// PERF: mix-blend-mode forces .page__header .header — which contains the tab
|
|
97
|
+
// navbar — into its own render surface with a backdrop readback. Dropped in
|
|
98
|
+
// favour of plain alpha compositing. This is a small visual change to the
|
|
99
|
+
// aurora wave (it no longer darkens what it sits on); restore the blend mode
|
|
100
|
+
// if the look regresses, but expect the GPU cost back with it.
|
|
101
|
+
//mix-blend-mode: color-burn;
|
|
102
|
+
opacity: 0.04;
|
|
98
103
|
pointer-events: none;
|
|
99
104
|
}
|
|
100
105
|
|
package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts
CHANGED
|
@@ -39,6 +39,8 @@ export default class ZnPageSectionCard extends ZincElement {
|
|
|
39
39
|
@property({type: Boolean, reflect: true}) selected = false;
|
|
40
40
|
/** Set when the section's type has no registered template — renders greyed. */
|
|
41
41
|
@property({type: Boolean, reflect: true}) unknown = false;
|
|
42
|
+
/** Set when the builder pins this section to the page — drops the remove action. */
|
|
43
|
+
@property({type: Boolean, reflect: true}) locked = false;
|
|
42
44
|
|
|
43
45
|
protected updated(changed: PropertyValues) {
|
|
44
46
|
super.updated(changed);
|
|
@@ -67,6 +69,10 @@ export default class ZnPageSectionCard extends ZincElement {
|
|
|
67
69
|
<span class="card__label">${this.label}</span>
|
|
68
70
|
${this.summary ? html`<span class="card__summary">${this.summary}</span>` : ''}
|
|
69
71
|
</span>
|
|
72
|
+
${this.locked ? html`
|
|
73
|
+
<span class="card__lock" title="This section cannot be removed">
|
|
74
|
+
<zn-icon src="lock" size="14"></zn-icon>
|
|
75
|
+
</span>` : ''}
|
|
70
76
|
<span class="card__actions" @keydown="${this._actionKeydown}">
|
|
71
77
|
<zn-button
|
|
72
78
|
class="card__action"
|
|
@@ -77,15 +83,16 @@ export default class ZnPageSectionCard extends ZincElement {
|
|
|
77
83
|
title="Duplicate section"
|
|
78
84
|
aria-label="Duplicate section"
|
|
79
85
|
@click="${(e: Event) => this._action(e, 'page-card-duplicate')}"></zn-button>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
${this.locked ? '' : html`
|
|
87
|
+
<zn-button
|
|
88
|
+
class="card__action"
|
|
89
|
+
icon-button="small"
|
|
90
|
+
plain
|
|
91
|
+
icon="delete"
|
|
92
|
+
icon-size="14"
|
|
93
|
+
title="Remove section"
|
|
94
|
+
aria-label="Remove section"
|
|
95
|
+
@click="${(e: Event) => this._action(e, 'page-card-remove')}"></zn-button>`}
|
|
89
96
|
</span>
|
|
90
97
|
</div>
|
|
91
98
|
`;
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
cursor: grabbing;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
:host([locked]),
|
|
19
|
+
:host([locked]:active) {
|
|
20
|
+
cursor: default;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
.card {
|
|
19
24
|
display: flex;
|
|
20
25
|
align-items: center;
|
|
@@ -77,6 +82,14 @@
|
|
|
77
82
|
text-overflow: ellipsis;
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
// Always visible, unlike the hover actions — it is what explains their absence.
|
|
86
|
+
.card__lock {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex: 0 0 auto;
|
|
89
|
+
align-items: center;
|
|
90
|
+
opacity: 0.45;
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
// Space is always reserved (visibility, not display) so the card never
|
|
81
94
|
// changes size or shifts its text when the actions appear on hover.
|
|
82
95
|
.card__actions {
|
|
@@ -41,6 +41,15 @@ describe('<zn-page-section-card>', () => {
|
|
|
41
41
|
expect(hostSawKeydown, 'host keydown suppressed for button activation keys').to.be.false;
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('should drop the remove action and show a lock when locked', async () => {
|
|
45
|
+
const el = await fixture<ZnPageSectionCard>(html`
|
|
46
|
+
<zn-page-section-card label="Hero" locked></zn-page-section-card>`);
|
|
47
|
+
expect(el.hasAttribute('locked')).to.be.true;
|
|
48
|
+
expect(el.shadowRoot!.querySelector('zn-button[title="Remove section"]'), 'remove gone').to.not.exist;
|
|
49
|
+
expect(el.shadowRoot!.querySelector('zn-button[title="Duplicate section"]'), 'duplicate kept').to.exist;
|
|
50
|
+
expect(el.shadowRoot!.querySelector('.card__lock'), 'lock indicator').to.exist;
|
|
51
|
+
});
|
|
52
|
+
|
|
44
53
|
it('should reflect selected and unknown states', async () => {
|
|
45
54
|
const el = await fixture<ZnPageSectionCard>(html`
|
|
46
55
|
<zn-page-section-card label="Hero" selected unknown></zn-page-section-card>`);
|