@zeyos/zx 4.3.0 → 4.4.0
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/CHANGELOG.md +266 -1
- package/README.md +7 -3
- package/dist/types/components/activity-item/activity-item.d.ts +351 -0
- package/dist/types/components/activity-list/activity-list.d.ts +195 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +28 -0
- package/dist/types/components/button/button.d.ts +8 -0
- package/dist/types/components/calendar/calendar-model.d.ts +22 -0
- package/dist/types/components/calendar/calendar.d.ts +19 -0
- package/dist/types/components/card/card.d.ts +7 -0
- package/dist/types/components/card-view/card-view.d.ts +12 -4
- package/dist/types/components/card-view/record-card.d.ts +53 -5
- package/dist/types/components/code-editor/code-editor.d.ts +160 -0
- package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +18 -2
- package/dist/types/components/date-picker/date-picker.d.ts +8 -0
- package/dist/types/components/date-picker/time-picker.d.ts +7 -0
- package/dist/types/components/dialog/dialog.d.ts +29 -0
- package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
- package/dist/types/components/file-item/file-item.d.ts +234 -0
- package/dist/types/components/file-list/file-list.d.ts +171 -0
- package/dist/types/components/filter/filter.d.ts +91 -36
- package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
- package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
- package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +837 -26
- package/dist/types/components/launcher/launcher.d.ts +19 -12
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
- package/dist/types/components/menu-button/menu-button.d.ts +4 -37
- package/dist/types/components/message/message.d.ts +7 -0
- package/dist/types/components/modal/modal.d.ts +48 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
- package/dist/types/components/panel/panel.d.ts +7 -0
- package/dist/types/components/search/search.d.ts +7 -0
- package/dist/types/components/select/select.d.ts +4 -0
- package/dist/types/components/sort-control/sort-control.d.ts +105 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
- package/dist/types/components/tabbox/tabbox.d.ts +7 -0
- package/dist/types/components/table/table.d.ts +144 -0
- package/dist/types/components/table-view/table-view.d.ts +11 -5
- package/dist/types/components/timebox/timebox.d.ts +7 -0
- package/dist/types/components/value-list/value-list.d.ts +8 -0
- package/dist/types/components/view/record-view.d.ts +96 -5
- package/dist/types/core/component.d.ts +6 -1
- package/dist/types/core/icons.d.ts +17 -0
- package/dist/types/index.d.ts +13 -1
- package/dist/types/internal/app-rail.d.ts +10 -0
- package/dist/types/internal/menu-items.d.ts +134 -0
- package/dist/zx-zeyos.esm.js +651 -88
- package/dist/zx-zeyos.esm.min.js +8 -1
- package/dist/zx.css +6266 -3900
- package/dist/zx.esm.js +8487 -1805
- package/dist/zx.esm.min.js +10 -3
- package/dist/zx.global.js +8487 -1805
- package/dist/zx.global.min.js +10 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1811 -247
- package/docs/llms.md +345 -40
- package/docs/llms.txt +11 -7
- package/package.json +1 -2
- package/src/components/activity-item/activity-item.css +137 -0
- package/src/components/activity-item/activity-item.js +599 -0
- package/src/components/activity-list/activity-list.css +64 -0
- package/src/components/activity-list/activity-list.js +375 -0
- package/src/components/app-rail/app-rail.css +227 -19
- package/src/components/app-sidebar/app-sidebar.css +414 -24
- package/src/components/app-sidebar/app-sidebar.js +65 -5
- package/src/components/button/button.css +8 -0
- package/src/components/button/button.js +6 -1
- package/src/components/calendar/calendar-model.js +45 -0
- package/src/components/calendar/calendar.css +30 -10
- package/src/components/calendar/calendar.js +41 -13
- package/src/components/card/card.js +12 -1
- package/src/components/card-view/card-view.css +212 -9
- package/src/components/card-view/card-view.js +32 -16
- package/src/components/card-view/record-card.js +98 -21
- package/src/components/code-editor/code-editor.css +67 -0
- package/src/components/code-editor/code-editor.js +288 -0
- package/src/components/code-editor/field-adapter.js +34 -0
- package/src/components/context-menu/context-menu.css +80 -3
- package/src/components/context-menu/context-menu.js +22 -30
- package/src/components/date-picker/date-picker.js +22 -8
- package/src/components/date-picker/month-picker.js +3 -3
- package/src/components/date-picker/time-picker.js +20 -8
- package/src/components/dialog/dialog.js +88 -66
- package/src/components/dropdown/dropdown.js +2 -2
- package/src/components/entity-ref/entity-ref.css +182 -0
- package/src/components/entity-ref/entity-ref.js +496 -0
- package/src/components/file-item/file-item.css +88 -0
- package/src/components/file-item/file-item.js +371 -0
- package/src/components/file-list/file-list.css +51 -0
- package/src/components/file-list/file-list.js +320 -0
- package/src/components/filter/filter.css +27 -1
- package/src/components/filter/filter.js +199 -45
- package/src/components/filter-panel/filter-panel.css +104 -0
- package/src/components/filter-panel/filter-panel.js +964 -0
- package/src/components/kanban-view/kanban-card.js +239 -0
- package/src/components/kanban-view/kanban-policy.js +412 -0
- package/src/components/kanban-view/kanban-view.css +674 -36
- package/src/components/kanban-view/kanban-view.js +1762 -240
- package/src/components/launcher/launcher.js +59 -27
- package/src/components/list-toolbar/list-toolbar.css +189 -0
- package/src/components/list-toolbar/list-toolbar.js +851 -0
- package/src/components/menu-button/menu-button.css +80 -3
- package/src/components/menu-button/menu-button.js +21 -37
- package/src/components/message/message.css +14 -12
- package/src/components/message/message.js +13 -2
- package/src/components/modal/modal.css +15 -0
- package/src/components/modal/modal.js +141 -0
- package/src/components/navigation-bar/navigation-bar.css +77 -2
- package/src/components/navigation-bar/navigation-bar.js +171 -4
- package/src/components/panel/panel.js +13 -2
- package/src/components/search/search.js +17 -5
- package/src/components/select/field-adapter.js +4 -1
- package/src/components/select/select.js +3 -0
- package/src/components/sort-control/sort-control.css +17 -0
- package/src/components/sort-control/sort-control.js +140 -0
- package/src/components/stat-tile/stat-tile.css +213 -0
- package/src/components/stat-tile/stat-tile.js +590 -0
- package/src/components/tabbox/tabbox.js +12 -1
- package/src/components/table/table.css +51 -2
- package/src/components/table/table.js +298 -19
- package/src/components/table-view/table-view.css +8 -2
- package/src/components/table-view/table-view.js +21 -6
- package/src/components/tag-picker/tag-picker.css +3 -1
- package/src/components/timebox/timebox.js +18 -5
- package/src/components/value-list/value-list.js +22 -6
- package/src/components/view/record-view.css +5 -0
- package/src/components/view/record-view.js +100 -7
- package/src/core/component.js +8 -3
- package/src/core/icons.js +74 -5
- package/src/index.js +33 -3
- package/src/internal/app-rail.js +19 -1
- package/src/internal/menu-items.js +142 -0
- package/styles/print.css +192 -0
- package/styles/zx.css +11 -0
- package/DESIGN-SYSTEM.md +0 -182
|
@@ -1,4 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* AppSidebar — the surface an application is judged by before it is read.
|
|
3
|
+
*
|
|
4
|
+
* Everything here hangs off one spacing scale declared on the root. Item height, icon gutter,
|
|
5
|
+
* row gap, group gap and indentation are derived from it rather than picked per rule, which is
|
|
6
|
+
* what makes a density switch a real change of rhythm instead of a smaller font.
|
|
7
|
+
*
|
|
8
|
+
* Two deliberate choices worth stating:
|
|
9
|
+
*
|
|
10
|
+
* - **Labels wrap, they never truncate.** German runs 20-35% longer than English and a nav is
|
|
11
|
+
* where that shows first: "Belege & Rechnungen" is two lines in a 16.5rem sidebar. An ellipsis
|
|
12
|
+
* there hides the word that distinguishes one destination from another, so the row grows
|
|
13
|
+
* instead and the fixed icon column keeps the second line aligned under the first.
|
|
14
|
+
* - **The active state never rests on colour alone.** It is a filled pill, a marker docked to the
|
|
15
|
+
* sidebar's outer edge, and a heavier label — three signals, one of which survives greyscale,
|
|
16
|
+
* forced colours and a reduced-transparency page.
|
|
17
|
+
*/
|
|
18
|
+
|
|
1
19
|
.zx-app-sidebar {
|
|
20
|
+
/* The scale. `--zx-control-height` and `--zx-space-1..4` are density-aware, so compact
|
|
21
|
+
tightens the item, the gaps and the padding together while `--zx-space-5/6` hold the icon
|
|
22
|
+
column and the indent steady — a narrower rhythm, not a smaller one. */
|
|
23
|
+
--zx-app-sidebar-gutter: var(--zx-space-2);
|
|
24
|
+
--zx-app-sidebar-icon: var(--zx-space-6);
|
|
25
|
+
--zx-app-sidebar-item-gap: var(--zx-space-3);
|
|
26
|
+
--zx-app-sidebar-item-height: calc(var(--zx-control-height) + var(--zx-space-2));
|
|
27
|
+
--zx-app-sidebar-pad-x: var(--zx-space-3);
|
|
28
|
+
--zx-app-sidebar-row-gap: var(--zx-space-1);
|
|
29
|
+
--zx-app-sidebar-group-gap: var(--zx-space-4);
|
|
30
|
+
--zx-app-sidebar-indent: var(--zx-space-5);
|
|
31
|
+
--zx-app-sidebar-marker: 3px;
|
|
32
|
+
--zx-app-sidebar-radius: var(--zx-radius-xl);
|
|
33
|
+
/* One overlay expression for hover and press. Mixing the *text* colour into the surface moves
|
|
34
|
+
the fill away from the page in whichever direction the theme runs, so a single declaration
|
|
35
|
+
is correct in light and in dark instead of needing a per-theme override. */
|
|
36
|
+
--zx-app-sidebar-hover: color-mix(in srgb, var(--zx-color-text) 7%, transparent);
|
|
37
|
+
--zx-app-sidebar-press: color-mix(in srgb, var(--zx-color-text) 14%, transparent);
|
|
2
38
|
display: flex;
|
|
3
39
|
min-block-size: 0;
|
|
4
40
|
border: 1px solid var(--zx-color-glass-border);
|
|
@@ -7,6 +43,11 @@
|
|
|
7
43
|
color: var(--zx-color-text);
|
|
8
44
|
}
|
|
9
45
|
|
|
46
|
+
[data-zx-density="compact"] .zx-app-sidebar,
|
|
47
|
+
.zx-app-sidebar[data-zx-density="compact"] {
|
|
48
|
+
--zx-app-sidebar-group-gap: var(--zx-space-3);
|
|
49
|
+
}
|
|
50
|
+
|
|
10
51
|
.zx-app-sidebar[data-orientation="horizontal"] {
|
|
11
52
|
inline-size: 100%;
|
|
12
53
|
}
|
|
@@ -29,57 +70,406 @@
|
|
|
29
70
|
.zx-app-sidebar__expanded[hidden],
|
|
30
71
|
.zx-app-sidebar__rail[hidden] { display: none; }
|
|
31
72
|
|
|
73
|
+
/* The header carries no rule. It is a brand slot above a nav, not a boundary between two kinds
|
|
74
|
+
of content, and a hairline there reads as leftover scaffolding. The footer keeps its rule:
|
|
75
|
+
an identity control below navigation is a different region and the line is doing work. */
|
|
32
76
|
.zx-app-sidebar__header {
|
|
33
77
|
display: grid;
|
|
34
78
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
35
79
|
align-items: center;
|
|
36
80
|
gap: var(--zx-space-2);
|
|
37
|
-
min-block-size: calc(var(--zx-control-height) + var(--zx-space-
|
|
38
|
-
|
|
39
|
-
padding: var(--zx-space-2);
|
|
81
|
+
min-block-size: calc(var(--zx-control-height) + var(--zx-space-4));
|
|
82
|
+
padding: var(--zx-space-2) var(--zx-space-3);
|
|
40
83
|
}
|
|
41
84
|
|
|
42
85
|
.zx-app-sidebar__header-content { min-inline-size: 0; }
|
|
43
|
-
|
|
86
|
+
|
|
87
|
+
/* The block padding is not decoration: the focus ring is a 3px outer glow, and a nav that padded
|
|
88
|
+
its scroll box by less would clip the ring off the first and last row. */
|
|
89
|
+
.zx-app-sidebar__nav {
|
|
90
|
+
min-block-size: 0;
|
|
91
|
+
overflow: auto;
|
|
92
|
+
padding: var(--zx-space-2) var(--zx-app-sidebar-gutter) var(--zx-space-3);
|
|
93
|
+
}
|
|
94
|
+
|
|
44
95
|
.zx-app-sidebar__list,
|
|
45
|
-
.zx-app-sidebar__sublist {
|
|
46
|
-
|
|
96
|
+
.zx-app-sidebar__sublist {
|
|
97
|
+
display: grid;
|
|
98
|
+
gap: var(--zx-app-sidebar-row-gap);
|
|
99
|
+
margin: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
list-style: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* A sidebar whose items carry no icons should not pay for an empty gutter; one that has even a
|
|
105
|
+
single icon keeps the column on every row so the labels stay in one line down the list. */
|
|
106
|
+
.zx-app-sidebar__list:not(:has(.zx-app-sidebar__item-icon)) {
|
|
107
|
+
--zx-app-sidebar-icon: 0px;
|
|
108
|
+
--zx-app-sidebar-item-gap: 0px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.zx-app-sidebar__sublist { position: relative; margin-block-start: var(--zx-app-sidebar-row-gap); }
|
|
47
112
|
.zx-app-sidebar__sublist[hidden] { display: none; }
|
|
48
113
|
|
|
114
|
+
/* The one rule in the component that carries meaning: a guide dropped from the centre of the
|
|
115
|
+
parent's icon that scopes the open branch to the row that opened it. */
|
|
116
|
+
.zx-app-sidebar__sublist::before {
|
|
117
|
+
content: "";
|
|
118
|
+
position: absolute;
|
|
119
|
+
inset-block: var(--zx-space-1);
|
|
120
|
+
inset-inline-start: calc(var(--zx-app-sidebar-pad-x) + var(--zx-app-sidebar-icon) / 2);
|
|
121
|
+
inline-size: 1px;
|
|
122
|
+
border-radius: var(--zx-radius-full);
|
|
123
|
+
background: var(--zx-color-border);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Groups are separated by space, not by rules: a top-level branch opens a group, and the row
|
|
127
|
+
after that group closes it. The first row never gets the gap. */
|
|
128
|
+
.zx-app-sidebar__list > .zx-app-sidebar__row:has(> .zx-app-sidebar__sublist),
|
|
129
|
+
.zx-app-sidebar__list > .zx-app-sidebar__row:has(> .zx-app-sidebar__sublist) + .zx-app-sidebar__row {
|
|
130
|
+
margin-block-start: var(--zx-app-sidebar-group-gap);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.zx-app-sidebar__list > .zx-app-sidebar__row:first-child { margin-block-start: 0; }
|
|
134
|
+
|
|
49
135
|
.zx-app-sidebar__item {
|
|
136
|
+
position: relative;
|
|
50
137
|
display: grid;
|
|
51
|
-
|
|
138
|
+
/* The icon track is composed here and not in a variable on the root: a custom property
|
|
139
|
+
substitutes its `var()`s where it is declared, so a track precomputed on the component would
|
|
140
|
+
ignore the list-level override below and keep an empty 36px gutter on an icon-less sidebar. */
|
|
141
|
+
grid-template-columns: calc(var(--zx-app-sidebar-icon) + var(--zx-app-sidebar-item-gap)) minmax(0, 1fr) auto auto;
|
|
52
142
|
align-items: center;
|
|
53
|
-
gap:
|
|
143
|
+
column-gap: 0;
|
|
54
144
|
inline-size: 100%;
|
|
55
|
-
min-block-size: var(--zx-
|
|
56
|
-
border
|
|
57
|
-
border-radius: var(--zx-radius
|
|
145
|
+
min-block-size: var(--zx-app-sidebar-item-height);
|
|
146
|
+
border: 0;
|
|
147
|
+
border-radius: var(--zx-app-sidebar-radius);
|
|
58
148
|
background: transparent;
|
|
59
|
-
padding-
|
|
60
|
-
|
|
149
|
+
padding-block: var(--zx-space-1);
|
|
150
|
+
padding-inline: var(--zx-app-sidebar-pad-x);
|
|
151
|
+
color: var(--zx-color-text);
|
|
152
|
+
font-size: var(--zx-text-md);
|
|
153
|
+
font-weight: 500;
|
|
154
|
+
line-height: 1.35;
|
|
61
155
|
text-align: start;
|
|
62
156
|
text-decoration: none;
|
|
63
157
|
}
|
|
64
158
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.zx-app-sidebar__item:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.zx-app-
|
|
72
|
-
.zx-app-sidebar__item-
|
|
73
|
-
|
|
74
|
-
.
|
|
159
|
+
/* A nav is a touch target before it is a dense list. Compact tightens the drawn row, and a
|
|
160
|
+
coarse pointer puts the hit area back to 40px whatever the density says. */
|
|
161
|
+
@media (pointer: coarse) {
|
|
162
|
+
.zx-app-sidebar__item { min-block-size: 40px; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.zx-app-sidebar__row[data-level="2"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-app-sidebar-pad-x) + var(--zx-app-sidebar-indent)); }
|
|
166
|
+
.zx-app-sidebar__row[data-level="3"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-app-sidebar-pad-x) + 2 * var(--zx-app-sidebar-indent)); }
|
|
167
|
+
|
|
168
|
+
/* The active marker. It is anchored to the control and not to the row, because a row is the
|
|
169
|
+
branch *and everything open under it* — a marker measured against that would grow into a
|
|
170
|
+
bracket down the whole group. It sits in the nav's own gutter, docked to the sidebar's outer
|
|
171
|
+
edge, so every marker lines up whatever the nesting depth, and it is the signal that survives
|
|
172
|
+
when colour does not. A collapsed branch that merely *contains* the active route gets a short
|
|
173
|
+
stub of the same marker: the same vocabulary, a quieter sentence. */
|
|
174
|
+
.zx-app-sidebar__item::before {
|
|
175
|
+
content: "";
|
|
176
|
+
position: absolute;
|
|
177
|
+
inset-block: 50%;
|
|
178
|
+
inset-inline-start: calc(-1 * var(--zx-app-sidebar-gutter));
|
|
179
|
+
inline-size: var(--zx-app-sidebar-marker);
|
|
180
|
+
border-start-end-radius: var(--zx-radius-full);
|
|
181
|
+
border-end-end-radius: var(--zx-radius-full);
|
|
182
|
+
background: var(--zx-color-accent);
|
|
183
|
+
opacity: 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.zx-app-sidebar[data-side="right"] .zx-app-sidebar__item::before {
|
|
187
|
+
inset-inline: auto calc(-1 * var(--zx-app-sidebar-gutter));
|
|
188
|
+
border-radius: 0;
|
|
189
|
+
border-start-start-radius: var(--zx-radius-full);
|
|
190
|
+
border-end-start-radius: var(--zx-radius-full);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.zx-app-sidebar__item[aria-current="page"]::before {
|
|
194
|
+
inset-block: 25%;
|
|
195
|
+
opacity: 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.zx-app-sidebar__item[data-active-descendant="true"][aria-expanded="false"]::before {
|
|
199
|
+
inset-block: 38%;
|
|
200
|
+
opacity: .55;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.zx-app-sidebar__item-icon {
|
|
204
|
+
grid-column: 1;
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
inline-size: var(--zx-app-sidebar-icon);
|
|
209
|
+
color: var(--zx-color-text-muted);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* No nowrap and no ellipsis: the row grows instead. `hyphens` is what makes that bearable for a
|
|
213
|
+
German compound — where the document declares a language the browser breaks "Rechnungsausgang"
|
|
214
|
+
at a syllable, and `overflow-wrap` is only the last resort behind it for a word that fits
|
|
215
|
+
nowhere at all. */
|
|
216
|
+
.zx-app-sidebar__item-label {
|
|
217
|
+
grid-column: 2;
|
|
218
|
+
min-inline-size: 0;
|
|
219
|
+
hyphens: auto;
|
|
220
|
+
overflow-wrap: break-word;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.zx-app-sidebar__item-badge {
|
|
224
|
+
grid-column: 3;
|
|
225
|
+
margin-inline-start: var(--zx-space-2);
|
|
226
|
+
color: var(--zx-color-text-muted);
|
|
227
|
+
font-size: var(--zx-text-xs);
|
|
228
|
+
font-variant-numeric: tabular-nums;
|
|
229
|
+
line-height: 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.zx-app-sidebar__item-disclosure {
|
|
233
|
+
grid-column: 4;
|
|
234
|
+
display: inline-flex;
|
|
235
|
+
margin-inline-start: var(--zx-space-1);
|
|
236
|
+
color: var(--zx-color-text-muted);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.zx-app-sidebar__item:hover { background: var(--zx-app-sidebar-hover); }
|
|
240
|
+
.zx-app-sidebar__item:hover .zx-app-sidebar__item-icon,
|
|
241
|
+
.zx-app-sidebar__item:hover .zx-app-sidebar__item-disclosure { color: var(--zx-color-text); }
|
|
242
|
+
.zx-app-sidebar__item:active { background: var(--zx-app-sidebar-press); }
|
|
243
|
+
|
|
244
|
+
/* A branch that is closed over the active route keeps the accent in the label and the marker
|
|
245
|
+
stub outside it; a branch that is open says so with its chevron and needs neither. */
|
|
246
|
+
.zx-app-sidebar__item[data-active-descendant="true"][aria-expanded="false"] {
|
|
247
|
+
color: var(--zx-color-accent);
|
|
248
|
+
font-weight: 600;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.zx-app-sidebar__item[aria-current="page"] {
|
|
252
|
+
background: var(--zx-color-accent);
|
|
253
|
+
color: var(--zx-color-on-accent);
|
|
254
|
+
font-weight: 600;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Hover and press on the pill move the fill toward the text colour, which raises the contrast
|
|
258
|
+
with `--zx-color-on-accent` in both polarities instead of dropping it in one of them. */
|
|
259
|
+
.zx-app-sidebar__item[aria-current="page"]:hover {
|
|
260
|
+
background: color-mix(in srgb, var(--zx-color-text) 8%, var(--zx-color-accent));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.zx-app-sidebar__item[aria-current="page"]:active {
|
|
264
|
+
background: color-mix(in srgb, var(--zx-color-text) 16%, var(--zx-color-accent));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Focus is a ring, never a fill: a keyboard user arrowing past the active route must still be
|
|
268
|
+
able to tell which row is current. Ordinary rows take the shared `--zx-focus-ring` from
|
|
269
|
+
`base.css` untouched; only the filled pill needs help, because an accent glow around an accent
|
|
270
|
+
fill is a ring nobody can see. The inner line is drawn in the pill's own text colour, which the
|
|
271
|
+
preset contract already guarantees against that fill. */
|
|
272
|
+
.zx-app-sidebar__item[aria-current="page"]:focus-visible {
|
|
273
|
+
box-shadow: inset 0 0 0 2px var(--zx-color-on-accent), var(--zx-focus-ring);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.zx-app-sidebar__item[aria-current="page"] .zx-app-sidebar__item-icon,
|
|
277
|
+
.zx-app-sidebar__item[aria-current="page"] .zx-app-sidebar__item-badge,
|
|
278
|
+
.zx-app-sidebar__item[aria-current="page"] .zx-app-sidebar__item-disclosure {
|
|
279
|
+
color: inherit;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Unavailable, and still readable. A blanket `opacity` on the row took the label under 3:1, which
|
|
283
|
+
is the one thing a destination a user cannot reach still has to do: say where it would go. The
|
|
284
|
+
icon carries the dimming instead, and the label keeps the muted text role. */
|
|
285
|
+
.zx-app-sidebar__item[aria-disabled="true"] {
|
|
286
|
+
color: var(--zx-color-text-muted);
|
|
287
|
+
cursor: not-allowed;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.zx-app-sidebar__item[aria-disabled="true"] .zx-app-sidebar__item-icon { opacity: .55; }
|
|
291
|
+
|
|
292
|
+
.zx-app-sidebar__item[aria-disabled="true"]:hover,
|
|
293
|
+
.zx-app-sidebar__item[aria-disabled="true"]:active { background: transparent; }
|
|
75
294
|
|
|
76
295
|
.zx-app-sidebar__footer {
|
|
77
296
|
border-block-start: 1px solid var(--zx-color-border);
|
|
78
|
-
padding: var(--zx-space-2);
|
|
297
|
+
padding: var(--zx-space-2) var(--zx-space-3);
|
|
79
298
|
}
|
|
80
299
|
.zx-app-sidebar__footer[hidden] { display: none; }
|
|
300
|
+
|
|
81
301
|
.zx-app-sidebar__rail-header { display: grid; gap: var(--zx-space-1); }
|
|
82
302
|
|
|
303
|
+
/*
|
|
304
|
+
* The minimized rail. Only the presentation changes; the same vocabulary applies — filled pill,
|
|
305
|
+
* docked marker, badge that survives the loss of the label.
|
|
306
|
+
*/
|
|
307
|
+
.zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"] {
|
|
308
|
+
background: var(--zx-color-accent);
|
|
309
|
+
color: var(--zx-color-on-accent);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"]:hover {
|
|
313
|
+
background: color-mix(in srgb, var(--zx-color-text) 8%, var(--zx-color-accent));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.zx-app-sidebar__rail .zx-app-rail__item:not([aria-current="page"]):hover,
|
|
317
|
+
.zx-app-sidebar__rail .zx-app-rail__item:not([aria-current="page"])[aria-expanded="true"] {
|
|
318
|
+
background: var(--zx-app-sidebar-hover);
|
|
319
|
+
color: var(--zx-color-text);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.zx-app-sidebar__rail .zx-app-rail__item:not([aria-current="page"]):active {
|
|
323
|
+
background: var(--zx-app-sidebar-press);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"]:focus-visible {
|
|
327
|
+
box-shadow: inset 0 0 0 2px var(--zx-color-on-accent), var(--zx-focus-ring);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.zx-app-sidebar__rail .zx-app-rail__item::after {
|
|
331
|
+
content: "";
|
|
332
|
+
position: absolute;
|
|
333
|
+
inset-block: 22%;
|
|
334
|
+
inset-inline-start: calc(-1 * var(--zx-space-1));
|
|
335
|
+
inline-size: var(--zx-app-sidebar-marker);
|
|
336
|
+
border-start-end-radius: var(--zx-radius-full);
|
|
337
|
+
border-end-end-radius: var(--zx-radius-full);
|
|
338
|
+
background: var(--zx-color-accent);
|
|
339
|
+
opacity: 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.zx-app-sidebar[data-side="right"] .zx-app-rail__item::after {
|
|
343
|
+
inset-inline: auto calc(-1 * var(--zx-space-1));
|
|
344
|
+
border-radius: 0;
|
|
345
|
+
border-start-start-radius: var(--zx-radius-full);
|
|
346
|
+
border-end-start-radius: var(--zx-radius-full);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.zx-app-sidebar[data-orientation="horizontal"] .zx-app-rail__item::after {
|
|
350
|
+
inset-block: auto calc(-1 * var(--zx-space-1));
|
|
351
|
+
inset-inline: 22%;
|
|
352
|
+
inline-size: auto;
|
|
353
|
+
block-size: var(--zx-app-sidebar-marker);
|
|
354
|
+
border-radius: 0;
|
|
355
|
+
border-start-start-radius: var(--zx-radius-full);
|
|
356
|
+
border-start-end-radius: var(--zx-radius-full);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.zx-app-sidebar[data-side="bottom"] .zx-app-rail__item::after {
|
|
360
|
+
inset-block: calc(-1 * var(--zx-space-1)) auto;
|
|
361
|
+
border-radius: 0;
|
|
362
|
+
border-end-start-radius: var(--zx-radius-full);
|
|
363
|
+
border-end-end-radius: var(--zx-radius-full);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"]::after { opacity: 1; }
|
|
367
|
+
.zx-app-sidebar__rail .zx-app-rail__item[data-active-descendant="true"]:not([aria-current="page"])::after {
|
|
368
|
+
inset-block: 34%;
|
|
369
|
+
opacity: .55;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.zx-app-sidebar[data-orientation="horizontal"] .zx-app-rail__item[data-active-descendant="true"]:not([aria-current="page"])::after {
|
|
373
|
+
inset-block: auto calc(-1 * var(--zx-space-1));
|
|
374
|
+
inset-inline: 34%;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/*
|
|
378
|
+
* The count a minimized rail would otherwise lose. `data-zx-badge` is set by the component from
|
|
379
|
+
* the item's own badge: a number that fits stays a number, and one that does not becomes a dot,
|
|
380
|
+
* because a three-digit pill on a 32px target stops being a badge and starts being a label. The
|
|
381
|
+
* figure itself moves into the control's accessible name either way, so nothing is dropped —
|
|
382
|
+
* only redrawn.
|
|
383
|
+
*/
|
|
384
|
+
.zx-app-sidebar__rail .zx-app-rail__item-badge {
|
|
385
|
+
inset-block-start: 0;
|
|
386
|
+
inset-inline-end: 0;
|
|
387
|
+
min-inline-size: var(--zx-space-4);
|
|
388
|
+
block-size: var(--zx-space-4);
|
|
389
|
+
border-radius: var(--zx-radius-full);
|
|
390
|
+
background: var(--zx-color-accent);
|
|
391
|
+
box-shadow: 0 0 0 2px var(--zx-color-bg-surface);
|
|
392
|
+
padding-inline: var(--zx-space-1);
|
|
393
|
+
color: var(--zx-color-on-accent);
|
|
394
|
+
font-size: var(--zx-text-xs);
|
|
395
|
+
font-variant-numeric: tabular-nums;
|
|
396
|
+
font-weight: 600;
|
|
397
|
+
line-height: var(--zx-space-4);
|
|
398
|
+
text-align: center;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.zx-app-sidebar__rail .zx-app-rail__item-badge[data-zx-badge="dot"] {
|
|
402
|
+
min-inline-size: 0;
|
|
403
|
+
inline-size: var(--zx-space-2);
|
|
404
|
+
block-size: var(--zx-space-2);
|
|
405
|
+
padding: 0;
|
|
406
|
+
font-size: 0;
|
|
407
|
+
line-height: 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* On the filled pill the badge would be accent-on-accent. Invert it so the count keeps a fill
|
|
411
|
+
of its own without introducing a third colour. */
|
|
412
|
+
.zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"] .zx-app-rail__item-badge {
|
|
413
|
+
background: var(--zx-color-on-accent);
|
|
414
|
+
box-shadow: none;
|
|
415
|
+
color: var(--zx-color-accent);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/*
|
|
419
|
+
* Motion. Everything here is decoration on a state change that is already expressed statically,
|
|
420
|
+
* so all of it is opt-in.
|
|
421
|
+
*
|
|
422
|
+
* The expanded tree is re-rendered wholesale on every branch toggle, which is why the disclosure
|
|
423
|
+
* animation is driven by `data-branch-motion` — an attribute the component sets on exactly the
|
|
424
|
+
* sublist it just opened, so the branches that were already open do not replay it.
|
|
425
|
+
*/
|
|
426
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
427
|
+
.zx-app-sidebar__item,
|
|
428
|
+
.zx-app-sidebar__rail .zx-app-rail__item {
|
|
429
|
+
transition: background-color var(--zx-dur-fast) var(--zx-ease),
|
|
430
|
+
color var(--zx-dur-fast) var(--zx-ease),
|
|
431
|
+
box-shadow var(--zx-dur-fast) var(--zx-ease);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.zx-app-sidebar__item::before,
|
|
435
|
+
.zx-app-sidebar__rail .zx-app-rail__item::after {
|
|
436
|
+
transition: opacity var(--zx-dur-base) var(--zx-ease),
|
|
437
|
+
inset-block var(--zx-dur-base) var(--zx-ease);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.zx-app-sidebar__item:active,
|
|
441
|
+
.zx-app-sidebar__rail .zx-app-rail__item:active { transform: scale(.985); }
|
|
442
|
+
|
|
443
|
+
.zx-app-sidebar__expanded,
|
|
444
|
+
.zx-app-sidebar__rail { animation: zx-app-sidebar-mode var(--zx-dur-base) var(--zx-ease); }
|
|
445
|
+
|
|
446
|
+
.zx-app-sidebar__sublist[data-branch-motion="expand"] { animation: zx-app-sidebar-branch var(--zx-dur-base) var(--zx-ease); }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
@keyframes zx-app-sidebar-mode {
|
|
450
|
+
from { opacity: 0; }
|
|
451
|
+
to { opacity: 1; }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@keyframes zx-app-sidebar-branch {
|
|
455
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
456
|
+
to { opacity: 1; transform: translateY(0); }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* Forced colours drop backgrounds and box-shadows, so the pill and the marker both disappear.
|
|
460
|
+
The system's own selected-item pair carries the state there instead. */
|
|
461
|
+
@media (forced-colors: active) {
|
|
462
|
+
.zx-app-sidebar__item[aria-current="page"],
|
|
463
|
+
.zx-app-sidebar__rail .zx-app-rail__item[aria-current="page"],
|
|
464
|
+
.zx-app-sidebar__rail .zx-app-rail__item-badge {
|
|
465
|
+
background: Highlight;
|
|
466
|
+
color: HighlightText;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.zx-app-sidebar__item::before,
|
|
470
|
+
.zx-app-sidebar__rail .zx-app-rail__item::after { background: Highlight; }
|
|
471
|
+
}
|
|
472
|
+
|
|
83
473
|
@media (prefers-reduced-transparency: reduce) {
|
|
84
474
|
.zx-app-sidebar {
|
|
85
475
|
border-color: var(--zx-color-border);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h } from '../../core/dom.js';
|
|
3
|
+
import { printf } from '../../core/i18n.js';
|
|
3
4
|
import { icon } from '../../core/icons.js';
|
|
4
5
|
import { uid } from '../../core/util.js';
|
|
5
6
|
import {
|
|
@@ -99,8 +100,10 @@ export class AppSidebar extends Component {
|
|
|
99
100
|
setItems(items) {
|
|
100
101
|
this._items = normalizeAppItems(items);
|
|
101
102
|
this._reconcileExpanded();
|
|
102
|
-
if (this._rail)
|
|
103
|
-
|
|
103
|
+
if (this._rail) {
|
|
104
|
+
this._rail.setItems(this._items);
|
|
105
|
+
this._decorateRail();
|
|
106
|
+
} else this._renderTree();
|
|
104
107
|
return this;
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -151,7 +154,10 @@ export class AppSidebar extends Component {
|
|
|
151
154
|
const expanded = !this._expanded.has(key);
|
|
152
155
|
if (expanded) this._expanded.add(key);
|
|
153
156
|
else this._expanded.delete(key);
|
|
154
|
-
if (!this._rail)
|
|
157
|
+
if (!this._rail) {
|
|
158
|
+
this._renderTree();
|
|
159
|
+
if (expanded) this._markBranchMotion(key);
|
|
160
|
+
}
|
|
155
161
|
if (!silent) this.emit('branchchange', {
|
|
156
162
|
item,
|
|
157
163
|
id: item.id,
|
|
@@ -298,7 +304,7 @@ export class AppSidebar extends Component {
|
|
|
298
304
|
const collapseButton = h('button', {
|
|
299
305
|
class: 'zx-app-sidebar__collapse zx-icon-btn',
|
|
300
306
|
type: 'button',
|
|
301
|
-
ariaLabel: 'Minimize application sidebar'
|
|
307
|
+
ariaLabel: this._message('appSidebar.minimize', 'Minimize application sidebar')
|
|
302
308
|
}, icon(this._side === 'right' ? 'chevron-right' : 'chevron-left'));
|
|
303
309
|
collapseButton.hidden = !this.options.collapsible;
|
|
304
310
|
const footer = h('footer', { class: 'zx-app-sidebar__footer' });
|
|
@@ -330,7 +336,7 @@ export class AppSidebar extends Component {
|
|
|
330
336
|
const expandButton = h('button', {
|
|
331
337
|
class: 'zx-app-sidebar__rail-toggle zx-icon-btn',
|
|
332
338
|
type: 'button',
|
|
333
|
-
ariaLabel: 'Expand application sidebar'
|
|
339
|
+
ariaLabel: this._message('appSidebar.expand', 'Expand application sidebar')
|
|
334
340
|
}, icon(this._side === 'right' ? 'chevron-left' : 'chevron-right'));
|
|
335
341
|
railHeader.append(expandButton);
|
|
336
342
|
this._railExpandButton = expandButton;
|
|
@@ -356,6 +362,31 @@ export class AppSidebar extends Component {
|
|
|
356
362
|
});
|
|
357
363
|
this._listenMode(railHost, 'zx-select', (event) => event.stopPropagation());
|
|
358
364
|
this._listenMode(railHost, 'zx-flyoutchange', (event) => event.stopPropagation());
|
|
365
|
+
this._decorateRail();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Keeps a minimized rail's counts readable and audible.
|
|
370
|
+
*
|
|
371
|
+
* A rail destination is icon-only, so its accessible name is the label alone and the badge the
|
|
372
|
+
* expanded tree showed as text is announced by nothing. Here the figure is folded back into the
|
|
373
|
+
* name, and `data-zx-badge` tells the stylesheet whether the count still fits the target or has
|
|
374
|
+
* to be drawn as a dot. Presentation only: no item, option, event, or listener is touched.
|
|
375
|
+
* @returns {void}
|
|
376
|
+
*/
|
|
377
|
+
_decorateRail() {
|
|
378
|
+
const rail = this.refs.rail;
|
|
379
|
+
if (!rail) return;
|
|
380
|
+
for (const control of rail.querySelectorAll('.zx-app-rail__item[data-app-nav-value-id]')) {
|
|
381
|
+
const badge = control.querySelector(':scope > .zx-app-rail__item-badge');
|
|
382
|
+
if (!badge) continue;
|
|
383
|
+
const item = findItem(this._items, control.getAttribute('data-app-nav-value-id'));
|
|
384
|
+
if (!item || item.badge == null) continue;
|
|
385
|
+
const count = String(item.badge);
|
|
386
|
+
badge.dataset.zxBadge = count.length > 2 ? 'dot' : 'count';
|
|
387
|
+
control.setAttribute('aria-label',
|
|
388
|
+
this._message('appSidebar.itemBadge', '%1 (%2)', item.label, count));
|
|
389
|
+
}
|
|
359
390
|
}
|
|
360
391
|
|
|
361
392
|
/** @param {EventTarget} target @param {string} type @param {EventListener} listener @returns {void} */
|
|
@@ -494,6 +525,35 @@ export class AppSidebar extends Component {
|
|
|
494
525
|
}
|
|
495
526
|
}
|
|
496
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Flags the sublist a branch toggle just opened so the stylesheet can animate that one branch.
|
|
530
|
+
*
|
|
531
|
+
* `_renderTree()` rebuilds every row on every toggle, so a transition declared on the sublist
|
|
532
|
+
* alone would replay on branches that never moved. The attribute is written after the rebuild
|
|
533
|
+
* and disappears with the element on the next one, which is why it needs no teardown.
|
|
534
|
+
* @param {string} key Branch id as a string.
|
|
535
|
+
* @returns {void}
|
|
536
|
+
*/
|
|
537
|
+
_markBranchMotion(key) {
|
|
538
|
+
if (!this.refs.list) return;
|
|
539
|
+
const control = [...this.refs.list.querySelectorAll('[data-app-nav-id]')]
|
|
540
|
+
.find((candidate) => candidate.getAttribute('data-app-nav-id') === key) ?? null;
|
|
541
|
+
const sublist = control?.parentElement?.querySelector(':scope > .zx-app-sidebar__sublist');
|
|
542
|
+
if (sublist instanceof HTMLElement) sublist.dataset.branchMotion = 'expand';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Resolves a message through the host translator, falling back to the built-in English text.
|
|
547
|
+
* @param {string} key Message key.
|
|
548
|
+
* @param {string} fallback Built-in text, with `%1`-style placeholders.
|
|
549
|
+
* @param {...unknown} args Interpolation values.
|
|
550
|
+
* @returns {string}
|
|
551
|
+
*/
|
|
552
|
+
_message(key, fallback, ...args) {
|
|
553
|
+
const message = this.msg(key, ...args);
|
|
554
|
+
return message === key ? printf(fallback, args) : message;
|
|
555
|
+
}
|
|
556
|
+
|
|
497
557
|
/** @returns {void} */
|
|
498
558
|
_reconcileExpanded() {
|
|
499
559
|
const branchIds = new Set();
|
|
@@ -93,6 +93,14 @@
|
|
|
93
93
|
display: inline-flex;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
.zx-btn__shortcut {
|
|
97
|
+
font: inherit;
|
|
98
|
+
font-size: var(--zx-text-xs);
|
|
99
|
+
opacity: .7;
|
|
100
|
+
margin-inline-start: var(--zx-space-1);
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
.zx-btn-group > .zx-btn:not(:first-child) {
|
|
97
105
|
margin-inline-start: -1px;
|
|
98
106
|
border-start-start-radius: 0;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { h } from '../../core/dom.js';
|
|
2
2
|
import { icon as createIcon } from '../../core/icons.js';
|
|
3
3
|
|
|
4
|
+
/** @typedef {{label:string,keys:string}} ButtonShortcut Visible hint and ARIA key combination; the host binds the action. */
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* @typedef {Object} ButtonOptions
|
|
6
8
|
* @property {string} [label=''] Visible button label.
|
|
@@ -9,6 +11,7 @@ import { icon as createIcon } from '../../core/icons.js';
|
|
|
9
11
|
* @property {'md'|'sm'} [size='md'] Control size.
|
|
10
12
|
* @property {boolean} [disabled=false] Whether the button is disabled.
|
|
11
13
|
* @property {string} [title] Native title text.
|
|
14
|
+
* @property {ButtonShortcut} [shortcut] Keyboard hint; does not register a keyboard handler.
|
|
12
15
|
* @property {(event: MouseEvent) => void} [onclick] Click callback.
|
|
13
16
|
*/
|
|
14
17
|
|
|
@@ -32,7 +35,8 @@ export function button(opts = {}) {
|
|
|
32
35
|
class: 'zx-btn',
|
|
33
36
|
type: 'button',
|
|
34
37
|
disabled: Boolean(options.disabled),
|
|
35
|
-
title: options.title,
|
|
38
|
+
title: options.title ?? (options.shortcut ? `${options.label} (${options.shortcut.label})` : undefined),
|
|
39
|
+
ariaKeyshortcuts: options.shortcut?.keys,
|
|
36
40
|
dataset: {
|
|
37
41
|
kind: normalizeKind(options.kind),
|
|
38
42
|
size: normalizeSize(options.size)
|
|
@@ -45,6 +49,7 @@ export function button(opts = {}) {
|
|
|
45
49
|
if (options.label !== '') {
|
|
46
50
|
element.append(h('span', { class: 'zx-btn__label' }, String(options.label)));
|
|
47
51
|
}
|
|
52
|
+
if (options.shortcut) element.append(h('kbd', { class: 'zx-btn__shortcut', ariaHidden: 'true' }, options.shortcut.label));
|
|
48
53
|
if (!options.label && options.title) element.setAttribute('aria-label', options.title);
|
|
49
54
|
if (typeof options.onclick === 'function') element.onclick = options.onclick;
|
|
50
55
|
return element;
|