@zeyos/zx 4.1.0 → 4.2.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 +61 -0
- package/DESIGN-SYSTEM.md +33 -3
- package/README.md +17 -5
- package/dist/types/components/app-icon/app-icon.d.ts +8 -1
- package/dist/types/components/aurora/aurora.d.ts +72 -0
- package/dist/types/components/calendar/calendar-model.d.ts +202 -0
- package/dist/types/components/calendar/calendar.d.ts +500 -0
- package/dist/types/components/card-view/card-view.d.ts +303 -0
- package/dist/types/components/card-view/record-card.d.ts +242 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +572 -0
- package/dist/types/components/table/table.d.ts +120 -31
- package/dist/types/components/table-view/table-view.d.ts +192 -0
- package/dist/types/components/view/record-view.d.ts +464 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/zeyos/calendar.d.ts +91 -0
- package/dist/types/zeyos/icons.d.ts +19 -3
- package/dist/types/zeyos/index.d.ts +4 -0
- package/dist/types/zeyos/legacy-saved-views.d.ts +66 -0
- package/dist/types/zeyos/saved-views.d.ts +331 -0
- package/dist/types/zeyos/view.d.ts +305 -0
- package/dist/zx-zeyos.esm.js +4627 -1460
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +2492 -410
- package/dist/zx.esm.js +4891 -115
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +4891 -115
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1702 -191
- package/docs/llms.md +207 -20
- package/docs/llms.txt +16 -6
- package/package.json +1 -1
- package/src/components/app-icon/app-icon.css +140 -30
- package/src/components/app-icon/app-icon.js +32 -5
- package/src/components/app-rail/app-rail.css +11 -2
- package/src/components/app-sidebar/app-sidebar.css +11 -2
- package/src/components/aurora/aurora.css +143 -0
- package/src/components/aurora/aurora.js +146 -0
- package/src/components/avatar/avatar.css +1 -1
- package/src/components/button/button.css +34 -2
- package/src/components/calendar/calendar-model.js +402 -0
- package/src/components/calendar/calendar.css +567 -0
- package/src/components/calendar/calendar.js +1524 -0
- package/src/components/card/card.css +25 -3
- package/src/components/card-view/card-view.css +389 -0
- package/src/components/card-view/card-view.js +527 -0
- package/src/components/card-view/record-card.js +392 -0
- package/src/components/context-menu/context-menu.css +2 -2
- package/src/components/date-picker/date-picker.css +15 -3
- package/src/components/datebox/date-range-box.css +15 -2
- package/src/components/datebox/datebox.css +16 -3
- package/src/components/dropdown/dropdown.css +15 -3
- package/src/components/kanban-view/kanban-view.css +216 -0
- package/src/components/kanban-view/kanban-view.js +1228 -0
- package/src/components/launcher/launcher.css +18 -5
- package/src/components/master-panel/master-panel.css +14 -3
- package/src/components/menu-button/menu-button.css +2 -2
- package/src/components/message/message.css +15 -10
- package/src/components/modal/modal.css +15 -3
- package/src/components/navigation-bar/navigation-bar.css +17 -5
- package/src/components/panel/panel.css +10 -2
- package/src/components/select/select.css +20 -4
- package/src/components/sheet/sheet.css +4 -1
- package/src/components/table/table.css +80 -4
- package/src/components/table/table.js +322 -37
- package/src/components/table-view/table-view.css +18 -0
- package/src/components/table-view/table-view.js +347 -0
- package/src/components/tag-picker/tag-picker.css +15 -3
- package/src/components/tooltip/tooltip.css +15 -3
- package/src/components/view/record-view.css +114 -0
- package/src/components/view/record-view.js +770 -0
- package/src/index.js +15 -0
- package/src/zeyos/calendar.js +115 -0
- package/src/zeyos/icons.js +15 -8
- package/src/zeyos/index.js +13 -0
- package/src/zeyos/launcher.js +2 -2
- package/src/zeyos/legacy-saved-views.js +155 -0
- package/src/zeyos/saved-views.js +815 -0
- package/src/zeyos/table.js +3 -1
- package/src/zeyos/view.js +543 -0
- package/styles/icons.css +3 -10
- package/styles/tokens/semantic.css +74 -2
- package/styles/zx.css +6 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { h, safeHref } from '../../core/dom.js';
|
|
3
|
+
import { icon } from '../../core/icons.js';
|
|
4
|
+
import { uid } from '../../core/util.js';
|
|
5
|
+
import { readViewField, renderViewField } from '../view/record-view.js';
|
|
6
|
+
|
|
7
|
+
/** @typedef {Record<string, any>} ViewRecord */
|
|
8
|
+
/** @typedef {import('../view/record-view.js').ViewField} ViewField */
|
|
9
|
+
/** @typedef {string|((record:ViewRecord,index:number)=>unknown)|null} RecordCardValueSource */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} RecordCardPreviewDescriptor
|
|
13
|
+
* @property {string} src Image URL.
|
|
14
|
+
* @property {string} [alt] Image alternative text.
|
|
15
|
+
* @property {'cover'|'contain'|'fill'|'none'|'scale-down'} [fit='cover'] Image fitting behavior.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @typedef {string|Node|RecordCardPreviewDescriptor|null} RecordCardPreview */
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} RecordCardLinkDescriptor
|
|
22
|
+
* @property {string} href Native link destination.
|
|
23
|
+
* @property {string} [target] Native browsing context.
|
|
24
|
+
* @property {string} [rel] Native link relationship.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** @typedef {string|RecordCardLinkDescriptor|null} RecordCardLink */
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} RecordCardAction
|
|
31
|
+
* @property {string} id Stable delegated-action identifier.
|
|
32
|
+
* @property {string} [label=''] Visible action label.
|
|
33
|
+
* @property {string|null} [icon=null] Optional Zx icon name.
|
|
34
|
+
* @property {string} [title] Accessible/native title, especially for an icon-only action.
|
|
35
|
+
* @property {string} [href] Optional safe native link destination; otherwise a button is rendered.
|
|
36
|
+
* @property {string} [target] Native link browsing context.
|
|
37
|
+
* @property {string} [rel] Native link relationship.
|
|
38
|
+
* @property {boolean} [disabled=false] Whether a button action is unavailable.
|
|
39
|
+
* @property {(record:ViewRecord,index:number,event:MouseEvent)=>void} [onclick]
|
|
40
|
+
* Callback invoked by an owning view's delegated listener; this helper never installs listeners.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} RecordCardOptions
|
|
45
|
+
* @property {ViewField[]} [fields=[]] Complete fields, used to resolve title and subtitle sources.
|
|
46
|
+
* @property {ViewField[]} [visibleFields=fields] Ordered fields rendered as labelled metadata.
|
|
47
|
+
* @property {RecordCardValueSource} [titleField=null] Field id or explicit value resolver.
|
|
48
|
+
* @property {RecordCardValueSource} [subtitleField=null] Field id or explicit value resolver.
|
|
49
|
+
* @property {string|((record:ViewRecord,index:number)=>RecordCardPreview)} [preview]
|
|
50
|
+
* Preview field id or explicit resolver.
|
|
51
|
+
* @property {string|((record:ViewRecord,index:number)=>unknown)|null} [previewAlt=null]
|
|
52
|
+
* Alternative-text field, literal text, or resolver.
|
|
53
|
+
* @property {RecordCardLink|((record:ViewRecord,index:number)=>RecordCardLink)} [link]
|
|
54
|
+
* @property {RecordCardAction[]|((record:ViewRecord,index:number)=>RecordCardAction[])} [actions=[]]
|
|
55
|
+
* @property {false|'single'|'multi'} [selectable=false] Selection presentation.
|
|
56
|
+
* @property {boolean} [selected=false] Current selection state.
|
|
57
|
+
* @property {'outlined'|'raised'|'filled'} [variant='outlined'] Surface treatment.
|
|
58
|
+
* @property {1|2|3|4|5|6} [headingLevel=3] Card title heading level.
|
|
59
|
+
* @property {Node[]} [controls=[]] Listener-free, owner-created controls such as a move handle.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} ResolvedRecordPreview
|
|
64
|
+
* @property {Node|null} node Explicit preview node.
|
|
65
|
+
* @property {string|null} src Safe image URL, or null when rejected.
|
|
66
|
+
* @property {string} alt Alternative text.
|
|
67
|
+
* @property {'cover'|'contain'|'fill'|'none'|'scale-down'} fit Image fitting behavior.
|
|
68
|
+
* @property {boolean} rejected Whether a supplied URL was unsafe or malformed.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Builds the listener-free card anatomy shared by CardView and KanbanView. All primitive content
|
|
73
|
+
* becomes text, URLs are normalized before reaching native attributes, and actions expose stable
|
|
74
|
+
* data attributes for a component-owned delegated listener.
|
|
75
|
+
*
|
|
76
|
+
* The returned `<li>` belongs in a semantic `<ul>`/`<ol>`. When selectable it carries
|
|
77
|
+
* `aria-selected`; multi-selection adds a native checkbox. It deliberately retains list-item
|
|
78
|
+
* semantics rather than becoming an ARIA option because cards may contain native links and
|
|
79
|
+
* buttons, which the listbox pattern does not permit inside an option. Preview images are paired
|
|
80
|
+
* with a stable fallback. The owning component listens for image `error` and marks the fallback.
|
|
81
|
+
*
|
|
82
|
+
* @param {ViewRecord} record Record to render.
|
|
83
|
+
* @param {number} index Current display index.
|
|
84
|
+
* @param {RecordCardOptions} [options={}] Rendering options.
|
|
85
|
+
* @returns {HTMLLIElement} Semantic record card list item.
|
|
86
|
+
*/
|
|
87
|
+
export function createRecordCard(record, index, options = {}) {
|
|
88
|
+
const fields = Array.isArray(options.fields) ? options.fields : [];
|
|
89
|
+
const visibleFields = Array.isArray(options.visibleFields) ? options.visibleFields : fields;
|
|
90
|
+
const titleSource = options.titleField ?? fields[0]?.id ?? null;
|
|
91
|
+
const subtitleSource = options.subtitleField ?? null;
|
|
92
|
+
const title = resolveRecordCardContent(record, index, titleSource, fields);
|
|
93
|
+
const subtitle = resolveRecordCardContent(record, index, subtitleSource, fields);
|
|
94
|
+
const preview = resolveRecordPreview(record, index, options.preview, options.previewAlt, fields);
|
|
95
|
+
const link = resolveRecordLink(record, index, options.link);
|
|
96
|
+
const actions = resolveRecordActions(record, index, options.actions);
|
|
97
|
+
const controls = normalizeControls(options.controls);
|
|
98
|
+
const selectable = normalizeSelectable(options.selectable);
|
|
99
|
+
const selected = Boolean(options.selected);
|
|
100
|
+
const headingId = uid('zx-record-card-title');
|
|
101
|
+
const headingLevel = normalizeHeadingLevel(options.headingLevel);
|
|
102
|
+
|
|
103
|
+
const headingContent = link
|
|
104
|
+
? h('a', {
|
|
105
|
+
class: 'zx-record-card__primary',
|
|
106
|
+
href: link.href,
|
|
107
|
+
target: link.target,
|
|
108
|
+
rel: link.rel
|
|
109
|
+
}, contentNode(title))
|
|
110
|
+
: h('span', { class: 'zx-record-card__title' }, contentNode(title));
|
|
111
|
+
const heading = h(`h${headingLevel}`, {
|
|
112
|
+
class: 'zx-record-card__heading',
|
|
113
|
+
id: headingId
|
|
114
|
+
}, headingContent);
|
|
115
|
+
const selection = selectable === 'multi' ? h('label', {
|
|
116
|
+
class: 'zx-record-card__selection'
|
|
117
|
+
}, h('input', {
|
|
118
|
+
type: 'checkbox',
|
|
119
|
+
checked: selected,
|
|
120
|
+
dataset: { recordSelection: '' },
|
|
121
|
+
ariaLabel: `Select ${textAlternative(title) || 'record'}`
|
|
122
|
+
})) : null;
|
|
123
|
+
const actionNodes = actions.map(createAction);
|
|
124
|
+
const actionArea = actionNodes.length || controls.length ? h('div', {
|
|
125
|
+
class: 'zx-record-card__actions',
|
|
126
|
+
role: 'group',
|
|
127
|
+
ariaLabel: `Actions for ${textAlternative(title) || 'record'}`
|
|
128
|
+
}, controls, actionNodes) : null;
|
|
129
|
+
const header = h('header', { class: 'zx-record-card__header' }, selection,
|
|
130
|
+
h('div', { class: 'zx-record-card__titles' }, heading,
|
|
131
|
+
isEmptyContent(subtitle) ? null : h('p', { class: 'zx-record-card__subtitle' }, contentNode(subtitle))),
|
|
132
|
+
actionArea);
|
|
133
|
+
const metadata = createMetadata(record, index, visibleFields, titleSource, subtitleSource);
|
|
134
|
+
const body = h('div', { class: 'zx-record-card__body' }, header, metadata);
|
|
135
|
+
const card = /** @type {HTMLLIElement} */ (h('li', {
|
|
136
|
+
class: 'zx-record-card',
|
|
137
|
+
tabindex: 0,
|
|
138
|
+
ariaSelected: selectable ? String(selected) : undefined,
|
|
139
|
+
ariaLabelledby: headingId,
|
|
140
|
+
dataset: {
|
|
141
|
+
recordIndex: String(index),
|
|
142
|
+
variant: normalizeVariant(options.variant),
|
|
143
|
+
selected: String(selected)
|
|
144
|
+
}
|
|
145
|
+
}, preview ? createPreview(preview) : null, body));
|
|
146
|
+
return card;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves a preview without touching the DOM. Unsafe, malformed, and non-image URL schemes are
|
|
151
|
+
* rejected into the same descriptor shape so callers can still render the stable fallback.
|
|
152
|
+
* @param {ViewRecord} record Record.
|
|
153
|
+
* @param {number} index Display index.
|
|
154
|
+
* @param {RecordCardOptions['preview']} preview Preview field or resolver.
|
|
155
|
+
* @param {RecordCardOptions['previewAlt']} previewAlt Alt field, literal, or resolver.
|
|
156
|
+
* @param {ViewField[]} [fields=[]] Complete fields.
|
|
157
|
+
* @returns {ResolvedRecordPreview|null} Resolved preview.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveRecordPreview(record, index, preview, previewAlt, fields = []) {
|
|
160
|
+
if (preview == null) return null;
|
|
161
|
+
const candidate = typeof preview === 'function'
|
|
162
|
+
? preview(record, index)
|
|
163
|
+
: readRecordSource(record, index, preview, fields, false);
|
|
164
|
+
if (candidate == null || candidate === '') return null;
|
|
165
|
+
const explicitAlt = resolvePreviewAlt(record, index, previewAlt, fields);
|
|
166
|
+
if (isNode(candidate)) {
|
|
167
|
+
return { node: candidate, src: null, alt: explicitAlt, fit: 'cover', rejected: false };
|
|
168
|
+
}
|
|
169
|
+
const descriptor = typeof candidate === 'object' && !Array.isArray(candidate)
|
|
170
|
+
? /** @type {RecordCardPreviewDescriptor} */ (candidate)
|
|
171
|
+
: { src: String(candidate) };
|
|
172
|
+
const source = typeof descriptor.src === 'string' ? descriptor.src : '';
|
|
173
|
+
const src = safePreviewUrl(source);
|
|
174
|
+
return {
|
|
175
|
+
node: null,
|
|
176
|
+
src,
|
|
177
|
+
alt: descriptor.alt == null ? explicitAlt : String(descriptor.alt),
|
|
178
|
+
fit: normalizePreviewFit(descriptor.fit),
|
|
179
|
+
rejected: src === null
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolves and validates a native title link without touching the DOM.
|
|
185
|
+
* @param {ViewRecord} record Record.
|
|
186
|
+
* @param {number} index Display index.
|
|
187
|
+
* @param {RecordCardOptions['link']} link Link value or resolver.
|
|
188
|
+
* @returns {RecordCardLinkDescriptor|null} Safe normalized link.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveRecordLink(record, index, link) {
|
|
191
|
+
const candidate = typeof link === 'function' ? link(record, index) : link;
|
|
192
|
+
if (candidate == null) return null;
|
|
193
|
+
const descriptor = typeof candidate === 'string' ? { href: candidate } : candidate;
|
|
194
|
+
if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) return null;
|
|
195
|
+
const href = safeHref(descriptor.href);
|
|
196
|
+
if (href === null) return null;
|
|
197
|
+
const target = descriptor.target == null ? undefined : String(descriptor.target);
|
|
198
|
+
const rel = safeRel(descriptor.rel, target);
|
|
199
|
+
return { href, ...(target ? { target } : {}), ...(rel ? { rel } : {}) };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolves and clones delegated action descriptors. Invalid entries are omitted rather than
|
|
204
|
+
* creating unnamed or ambiguously interactive controls.
|
|
205
|
+
* @param {ViewRecord} record Record.
|
|
206
|
+
* @param {number} index Display index.
|
|
207
|
+
* @param {RecordCardOptions['actions']} actions Actions or resolver.
|
|
208
|
+
* @returns {RecordCardAction[]} Normalized action copies.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveRecordActions(record, index, actions) {
|
|
211
|
+
const candidate = typeof actions === 'function' ? actions(record, index) : actions;
|
|
212
|
+
if (!Array.isArray(candidate)) return [];
|
|
213
|
+
return candidate.flatMap((action) => {
|
|
214
|
+
if (!action || typeof action !== 'object' || Array.isArray(action)) return [];
|
|
215
|
+
const id = String(action.id ?? '').trim();
|
|
216
|
+
const label = String(action.label ?? '');
|
|
217
|
+
const title = action.title == null ? '' : String(action.title);
|
|
218
|
+
if (!id || (!label && !title)) return [];
|
|
219
|
+
if (action.href != null && safeHref(action.href) === null) return [];
|
|
220
|
+
return [{ ...action, id, label, ...(title ? { title } : {}) }];
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @param {ResolvedRecordPreview} preview @returns {HTMLElement} */
|
|
225
|
+
function createPreview(preview) {
|
|
226
|
+
const fallback = h('span', {
|
|
227
|
+
class: 'zx-record-card__preview-fallback',
|
|
228
|
+
ariaHidden: 'true'
|
|
229
|
+
}, icon('file', { size: 24 }));
|
|
230
|
+
if (preview.node) {
|
|
231
|
+
return h('div', { class: 'zx-record-card__preview' }, preview.node);
|
|
232
|
+
}
|
|
233
|
+
const image = preview.src ? h('img', {
|
|
234
|
+
class: 'zx-record-card__preview-image',
|
|
235
|
+
src: preview.src,
|
|
236
|
+
alt: preview.alt,
|
|
237
|
+
loading: 'lazy',
|
|
238
|
+
decoding: 'async',
|
|
239
|
+
dataset: { recordPreview: '' },
|
|
240
|
+
style: { objectFit: preview.fit }
|
|
241
|
+
}) : null;
|
|
242
|
+
return h('div', {
|
|
243
|
+
class: 'zx-record-card__preview',
|
|
244
|
+
dataset: { failed: String(preview.rejected) }
|
|
245
|
+
}, fallback, image);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {ViewRecord} record
|
|
250
|
+
* @param {number} index
|
|
251
|
+
* @param {ViewField[]} fields
|
|
252
|
+
* @param {RecordCardValueSource} titleSource
|
|
253
|
+
* @param {RecordCardValueSource} subtitleSource
|
|
254
|
+
* @returns {HTMLElement|null}
|
|
255
|
+
*/
|
|
256
|
+
function createMetadata(record, index, fields, titleSource, subtitleSource) {
|
|
257
|
+
const titleId = typeof titleSource === 'string' ? titleSource : null;
|
|
258
|
+
const subtitleId = typeof subtitleSource === 'string' ? subtitleSource : null;
|
|
259
|
+
const visible = fields.filter((field) => field.duplicate || field.id !== titleId && field.id !== subtitleId);
|
|
260
|
+
if (visible.length === 0) return null;
|
|
261
|
+
return h('dl', { class: 'zx-record-card__metadata' }, visible.map((field) => h('div', {
|
|
262
|
+
class: 'zx-record-card__field',
|
|
263
|
+
dataset: { fieldId: field.id }
|
|
264
|
+
}, h('dt', { class: 'zx-record-card__label' }, field.label),
|
|
265
|
+
h('dd', { class: 'zx-record-card__value' }, contentNode(renderViewField(field, record, index))))));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** @param {RecordCardAction} action @returns {HTMLElement} */
|
|
269
|
+
function createAction(action) {
|
|
270
|
+
const label = String(action.label ?? '');
|
|
271
|
+
const title = String(action.title ?? (label || action.id));
|
|
272
|
+
const children = [action.icon ? icon(String(action.icon), { size: 14 }) : null,
|
|
273
|
+
label ? h('span', { class: 'zx-record-card__action-label' }, label) : null];
|
|
274
|
+
const href = action.href == null ? null : safeHref(action.href);
|
|
275
|
+
if (href && !action.disabled) {
|
|
276
|
+
const target = action.target == null ? undefined : String(action.target);
|
|
277
|
+
return h('a', {
|
|
278
|
+
class: 'zx-record-card__action',
|
|
279
|
+
href,
|
|
280
|
+
target,
|
|
281
|
+
rel: safeRel(action.rel, target),
|
|
282
|
+
title,
|
|
283
|
+
ariaLabel: label || title,
|
|
284
|
+
dataset: { recordAction: action.id }
|
|
285
|
+
}, children);
|
|
286
|
+
}
|
|
287
|
+
return h('button', {
|
|
288
|
+
class: 'zx-record-card__action',
|
|
289
|
+
type: 'button',
|
|
290
|
+
disabled: Boolean(action.disabled),
|
|
291
|
+
title,
|
|
292
|
+
ariaLabel: label || title,
|
|
293
|
+
dataset: { recordAction: action.id }
|
|
294
|
+
}, children);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** @param {ViewRecord} record @param {number} index @param {RecordCardValueSource} source @param {ViewField[]} fields @returns {unknown} */
|
|
298
|
+
function resolveRecordCardContent(record, index, source, fields) {
|
|
299
|
+
if (source == null) return '';
|
|
300
|
+
if (typeof source === 'function') return source(record, index);
|
|
301
|
+
const field = fields.find((candidate) => candidate.id === source);
|
|
302
|
+
return field ? renderViewField(field, record, index) : record?.[source] ?? '';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {ViewRecord} record @param {number} index @param {string} source @param {ViewField[]} fields
|
|
307
|
+
* @param {boolean} literalFallback @returns {unknown}
|
|
308
|
+
*/
|
|
309
|
+
function readRecordSource(record, index, source, fields, literalFallback) {
|
|
310
|
+
const field = fields.find((candidate) => candidate.id === source);
|
|
311
|
+
if (field) return readViewField(field, record, index);
|
|
312
|
+
if (Object.prototype.hasOwnProperty.call(record, source)) return record[source];
|
|
313
|
+
return literalFallback ? source : null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @param {ViewRecord} record @param {number} index @param {RecordCardOptions['previewAlt']} source @param {ViewField[]} fields @returns {string} */
|
|
317
|
+
function resolvePreviewAlt(record, index, source, fields) {
|
|
318
|
+
if (source == null) return '';
|
|
319
|
+
const value = typeof source === 'function'
|
|
320
|
+
? source(record, index)
|
|
321
|
+
: readRecordSource(record, index, source, fields, true);
|
|
322
|
+
return value == null ? '' : String(value);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** @param {unknown} content @returns {Node|string} */
|
|
326
|
+
function contentNode(content) {
|
|
327
|
+
return isNode(content) ? content : content == null ? '' : String(content);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** @param {unknown} content @returns {boolean} */
|
|
331
|
+
function isEmptyContent(content) {
|
|
332
|
+
return content == null || typeof content !== 'object' && String(content) === '';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** @param {unknown} content @returns {string} */
|
|
336
|
+
function textAlternative(content) {
|
|
337
|
+
if (isNode(content)) return content.textContent?.trim() ?? '';
|
|
338
|
+
return content == null ? '' : String(content).trim();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** @param {unknown} value @returns {value is Node} */
|
|
342
|
+
function isNode(value) {
|
|
343
|
+
return Boolean(value && typeof value === 'object'
|
|
344
|
+
&& typeof /** @type {{nodeType?:unknown}} */ (value).nodeType === 'number');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** @param {unknown} controls @returns {Node[]} */
|
|
348
|
+
function normalizeControls(controls) {
|
|
349
|
+
return Array.isArray(controls) ? controls.filter(isNode) : [];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** @param {unknown} value @returns {string|null} */
|
|
353
|
+
function safePreviewUrl(value) {
|
|
354
|
+
const href = safeHref(value);
|
|
355
|
+
if (!href) return null;
|
|
356
|
+
try {
|
|
357
|
+
const base = globalThis.location?.href ?? 'https://zx.invalid/';
|
|
358
|
+
const protocol = new URL(href, base).protocol.toLowerCase();
|
|
359
|
+
return ['http:', 'https:', 'blob:', 'file:'].includes(protocol) ? href : null;
|
|
360
|
+
} catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** @param {unknown} rel @param {string|undefined} target @returns {string|undefined} */
|
|
366
|
+
function safeRel(rel, target) {
|
|
367
|
+
const tokens = String(rel ?? '').split(/\s+/).filter((token) => /^[a-z-]+$/i.test(token));
|
|
368
|
+
if (target === '_blank' && !tokens.includes('noopener')) tokens.push('noopener');
|
|
369
|
+
return tokens.length ? [...new Set(tokens)].join(' ') : undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** @param {unknown} value @returns {false|'single'|'multi'} */
|
|
373
|
+
function normalizeSelectable(value) {
|
|
374
|
+
return value === 'single' || value === 'multi' ? value : false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** @param {unknown} value @returns {'outlined'|'raised'|'filled'} */
|
|
378
|
+
function normalizeVariant(value) {
|
|
379
|
+
return value === 'raised' || value === 'filled' ? value : 'outlined';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** @param {unknown} value @returns {1|2|3|4|5|6} */
|
|
383
|
+
function normalizeHeadingLevel(value) {
|
|
384
|
+
const number = Number(value ?? 3);
|
|
385
|
+
return /** @type {1|2|3|4|5|6} */ (Number.isInteger(number) && number >= 1 && number <= 6 ? number : 3);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** @param {unknown} value @returns {'cover'|'contain'|'fill'|'none'|'scale-down'} */
|
|
389
|
+
function normalizePreviewFit(value) {
|
|
390
|
+
return ['contain', 'fill', 'none', 'scale-down'].includes(String(value))
|
|
391
|
+
? /** @type {'contain'|'fill'|'none'|'scale-down'} */ (value) : 'cover';
|
|
392
|
+
}
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
gap: var(--zx-space-2);
|
|
22
22
|
inline-size: 100%;
|
|
23
23
|
min-block-size: var(--zx-control-height);
|
|
24
|
-
border-color:
|
|
25
|
-
background:
|
|
24
|
+
border-color: transparent;
|
|
25
|
+
background: transparent;
|
|
26
26
|
padding-inline: var(--zx-control-pad-x);
|
|
27
27
|
text-align: start;
|
|
28
28
|
}
|
|
@@ -123,13 +123,25 @@
|
|
|
123
123
|
position: absolute;
|
|
124
124
|
z-index: 2;
|
|
125
125
|
inset: var(--zx-space-3);
|
|
126
|
-
border: 1px solid var(--zx-color-border
|
|
126
|
+
border: 1px solid var(--zx-color-overlay-border);
|
|
127
127
|
border-radius: var(--zx-radius-md);
|
|
128
|
-
background: var(--zx-color-
|
|
129
|
-
|
|
128
|
+
background: var(--zx-color-overlay-surface);
|
|
129
|
+
-webkit-backdrop-filter: var(--zx-glass-filter-overlay);
|
|
130
|
+
backdrop-filter: var(--zx-glass-filter-overlay);
|
|
131
|
+
box-shadow: var(--zx-glass-raised-shadow);
|
|
130
132
|
padding: var(--zx-space-3);
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
136
|
+
.zx-date-picker__quick {
|
|
137
|
+
border-color: var(--zx-color-border-strong);
|
|
138
|
+
background: var(--zx-color-bg-raised);
|
|
139
|
+
-webkit-backdrop-filter: none;
|
|
140
|
+
backdrop-filter: none;
|
|
141
|
+
box-shadow: var(--zx-overlay-shadow);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
133
145
|
.zx-date-picker__quick[hidden] {
|
|
134
146
|
display: none;
|
|
135
147
|
}
|
|
@@ -123,6 +123,19 @@
|
|
|
123
123
|
|
|
124
124
|
.zx-date-range-box__popover .zx-date-range-picker__surface {
|
|
125
125
|
max-inline-size: none;
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
border-color: var(--zx-color-overlay-border);
|
|
127
|
+
background: var(--zx-color-overlay-surface);
|
|
128
|
+
-webkit-backdrop-filter: var(--zx-glass-filter-overlay);
|
|
129
|
+
backdrop-filter: var(--zx-glass-filter-overlay);
|
|
130
|
+
box-shadow: var(--zx-glass-raised-shadow);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
134
|
+
.zx-date-range-box__popover .zx-date-range-picker__surface {
|
|
135
|
+
border-color: var(--zx-color-border);
|
|
136
|
+
background: var(--zx-color-bg-raised);
|
|
137
|
+
-webkit-backdrop-filter: none;
|
|
138
|
+
backdrop-filter: none;
|
|
139
|
+
box-shadow: var(--zx-overlay-shadow);
|
|
140
|
+
}
|
|
128
141
|
}
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
overflow: visible;
|
|
91
91
|
max-inline-size: calc(100vw - var(--zx-space-4));
|
|
92
92
|
border: 0;
|
|
93
|
-
background:
|
|
93
|
+
background: none;
|
|
94
94
|
padding: 0;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -103,6 +103,19 @@
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
.zx-datebox__popover .zx-date-picker__surface {
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
border-color: var(--zx-color-overlay-border);
|
|
107
|
+
background: var(--zx-color-overlay-surface);
|
|
108
|
+
-webkit-backdrop-filter: var(--zx-glass-filter-overlay);
|
|
109
|
+
backdrop-filter: var(--zx-glass-filter-overlay);
|
|
110
|
+
box-shadow: var(--zx-glass-raised-shadow);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
114
|
+
.zx-datebox__popover .zx-date-picker__surface {
|
|
115
|
+
border-color: var(--zx-color-border);
|
|
116
|
+
background: var(--zx-color-bg-raised);
|
|
117
|
+
-webkit-backdrop-filter: none;
|
|
118
|
+
backdrop-filter: none;
|
|
119
|
+
box-shadow: var(--zx-overlay-shadow);
|
|
120
|
+
}
|
|
108
121
|
}
|
|
@@ -2,14 +2,26 @@
|
|
|
2
2
|
max-inline-size: calc(100vw - (2 * var(--zx-space-3)));
|
|
3
3
|
max-block-size: calc(100vh - (2 * var(--zx-space-3)));
|
|
4
4
|
overflow: auto;
|
|
5
|
-
border: 1px solid var(--zx-color-border);
|
|
5
|
+
border: 1px solid var(--zx-color-overlay-border);
|
|
6
6
|
border-radius: var(--zx-radius-md);
|
|
7
|
-
background: var(--zx-color-
|
|
8
|
-
|
|
7
|
+
background: var(--zx-color-overlay-surface);
|
|
8
|
+
-webkit-backdrop-filter: var(--zx-glass-filter-overlay);
|
|
9
|
+
backdrop-filter: var(--zx-glass-filter-overlay);
|
|
10
|
+
box-shadow: var(--zx-glass-raised-shadow);
|
|
9
11
|
color: var(--zx-color-text);
|
|
10
12
|
padding: var(--zx-space-3);
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
16
|
+
.zx-dropdown {
|
|
17
|
+
border-color: var(--zx-color-border);
|
|
18
|
+
background: var(--zx-color-bg-raised);
|
|
19
|
+
-webkit-backdrop-filter: none;
|
|
20
|
+
backdrop-filter: none;
|
|
21
|
+
box-shadow: var(--zx-overlay-shadow);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
@media (prefers-reduced-motion: no-preference) {
|
|
14
26
|
.zx-dropdown:popover-open {
|
|
15
27
|
animation: zx-dropdown-enter var(--zx-dur-fast) var(--zx-ease);
|