@zeyos/zx 4.3.0 → 4.3.4

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +111 -1
  2. package/README.md +0 -3
  3. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  4. package/dist/types/components/card-view/card-view.d.ts +1 -1
  5. package/dist/types/components/card-view/record-card.d.ts +47 -5
  6. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  7. package/dist/types/components/kanban-view/kanban-view.d.ts +669 -24
  8. package/dist/types/core/component.d.ts +6 -1
  9. package/dist/types/index.d.ts +2 -1
  10. package/dist/zx-zeyos.esm.js +35 -11
  11. package/dist/zx-zeyos.esm.min.js +1 -1
  12. package/dist/zx.css +597 -78
  13. package/dist/zx.esm.js +1758 -238
  14. package/dist/zx.esm.min.js +3 -3
  15. package/dist/zx.global.js +1758 -238
  16. package/dist/zx.global.min.js +3 -3
  17. package/dist/zx.min.css +1 -1
  18. package/docs/api.json +214 -2
  19. package/docs/llms.md +30 -30
  20. package/docs/llms.txt +0 -2
  21. package/package.json +1 -2
  22. package/src/components/calendar/calendar-model.js +45 -0
  23. package/src/components/calendar/calendar.css +30 -10
  24. package/src/components/calendar/calendar.js +8 -10
  25. package/src/components/card-view/card-view.css +211 -9
  26. package/src/components/card-view/card-view.js +7 -7
  27. package/src/components/card-view/record-card.js +92 -19
  28. package/src/components/kanban-view/kanban-policy.js +412 -0
  29. package/src/components/kanban-view/kanban-view.css +480 -36
  30. package/src/components/kanban-view/kanban-view.js +1514 -235
  31. package/src/components/message/message.css +9 -10
  32. package/src/core/component.js +8 -3
  33. package/src/index.js +8 -3
  34. package/DESIGN-SYSTEM.md +0 -182
@@ -1,6 +1,10 @@
1
1
  .zx-card-view {
2
- --zx-card-view-min-card-width: 18rem;
2
+ --zx-card-view-min-card-width: 15rem;
3
3
  --zx-card-view-column-basis: 0%;
4
+ --zx-card-view-card-padding: var(--zx-space-3);
5
+ --zx-card-view-card-gap: var(--zx-space-2);
6
+ --zx-card-view-grid-gap: var(--zx-space-3);
7
+ --zx-card-view-preview-size: 1.75rem;
4
8
  inline-size: 100%;
5
9
  min-inline-size: 0;
6
10
  container-type: inline-size;
@@ -44,22 +48,23 @@
44
48
  }
45
49
 
46
50
  .zx-card-view__group + .zx-card-view__group {
47
- margin-block-start: var(--zx-space-6);
51
+ margin-block-start: var(--zx-space-4);
48
52
  }
49
53
 
50
54
  .zx-card-view__group-heading {
51
- margin: 0 0 var(--zx-space-3);
55
+ margin: 0 0 var(--zx-space-2);
52
56
  color: var(--zx-color-text);
53
- font-size: var(--zx-text-lg);
54
- line-height: 1.35;
57
+ font-size: var(--zx-text-sm);
58
+ font-weight: 600;
59
+ line-height: 1.3;
55
60
  }
56
61
 
57
62
  .zx-card-view__list {
58
63
  display: grid;
59
- grid-template-columns: repeat(auto-fit,
64
+ grid-template-columns: repeat(auto-fill,
60
65
  minmax(min(100%, max(var(--zx-card-view-min-card-width),
61
- calc(var(--zx-card-view-column-basis) - var(--zx-space-4)))), 1fr));
62
- gap: var(--zx-space-4);
66
+ calc(var(--zx-card-view-column-basis) - var(--zx-card-view-grid-gap)))), 1fr));
67
+ gap: var(--zx-card-view-grid-gap);
63
68
  min-inline-size: 0;
64
69
  margin: 0;
65
70
  padding: 0;
@@ -102,7 +107,7 @@
102
107
  box-shadow: var(--zx-focus-ring);
103
108
  }
104
109
 
105
- .zx-record-card[aria-selected="true"] {
110
+ .zx-record-card[data-selected="true"] {
106
111
  border-color: var(--zx-color-accent);
107
112
  box-shadow: inset 0 0 0 1px var(--zx-color-accent);
108
113
  }
@@ -181,6 +186,40 @@
181
186
  min-inline-size: 0;
182
187
  }
183
188
 
189
+ .zx-record-card__eyebrow {
190
+ display: flex;
191
+ align-items: flex-start;
192
+ gap: var(--zx-space-2);
193
+ min-inline-size: 0;
194
+ }
195
+
196
+ .zx-record-card__eyebrow-group,
197
+ .zx-record-card__title-prefix {
198
+ display: flex;
199
+ flex-wrap: wrap;
200
+ align-items: center;
201
+ gap: var(--zx-space-1);
202
+ min-inline-size: 0;
203
+ margin: 0;
204
+ }
205
+
206
+ .zx-record-card__eyebrow-group[data-card-slot="eyebrow-end"] {
207
+ justify-content: flex-end;
208
+ margin-inline-start: auto;
209
+ }
210
+
211
+ .zx-record-card__title-row {
212
+ display: flex;
213
+ align-items: flex-start;
214
+ gap: var(--zx-space-2);
215
+ min-inline-size: 0;
216
+ }
217
+
218
+ .zx-record-card__title-row > .zx-record-card__heading {
219
+ flex: 1 1 auto;
220
+ min-inline-size: 0;
221
+ }
222
+
184
223
  .zx-record-card__heading {
185
224
  margin: 0;
186
225
  overflow-wrap: anywhere;
@@ -387,3 +426,166 @@
387
426
  box-shadow var(--zx-dur-fast) var(--zx-ease);
388
427
  }
389
428
  }
429
+
430
+ /*
431
+ * CardView intentionally presents the shared record-card anatomy more densely than Kanban. Keep
432
+ * these selectors scoped: Kanban owns its own board-card rhythm while both views retain the same
433
+ * safe, semantic markup and delegated interaction hooks.
434
+ */
435
+ .zx-card-view .zx-record-card {
436
+ border-radius: var(--zx-radius-md);
437
+ }
438
+
439
+ .zx-card-view .zx-record-card:has(> .zx-record-card__preview) {
440
+ grid-template-columns: auto minmax(0, 1fr);
441
+ align-items: start;
442
+ }
443
+
444
+ .zx-card-view .zx-record-card__preview {
445
+ inline-size: var(--zx-card-view-preview-size);
446
+ block-size: var(--zx-card-view-preview-size);
447
+ margin-block-start: var(--zx-card-view-card-padding);
448
+ margin-inline-start: var(--zx-card-view-card-padding);
449
+ border: 0;
450
+ border-radius: var(--zx-radius-sm);
451
+ aspect-ratio: 1;
452
+ }
453
+
454
+ .zx-card-view .zx-record-card__preview .zx-icon {
455
+ inline-size: .875rem;
456
+ block-size: .875rem;
457
+ }
458
+
459
+ .zx-card-view .zx-record-card__body {
460
+ gap: var(--zx-card-view-card-gap);
461
+ padding: var(--zx-card-view-card-padding);
462
+ }
463
+
464
+ .zx-card-view .zx-record-card:has(> .zx-record-card__preview) .zx-record-card__body {
465
+ padding-inline-start: var(--zx-card-view-card-gap);
466
+ }
467
+
468
+ .zx-card-view .zx-record-card__header {
469
+ gap: var(--zx-space-2);
470
+ }
471
+
472
+ .zx-card-view .zx-record-card__selection {
473
+ min-block-size: 1.25rem;
474
+ }
475
+
476
+ .zx-card-view .zx-record-card__heading {
477
+ font-size: var(--zx-text-md);
478
+ font-weight: 600;
479
+ line-height: 1.3;
480
+ }
481
+
482
+ .zx-card-view .zx-record-card__primary,
483
+ .zx-card-view .zx-record-card__title {
484
+ display: -webkit-box;
485
+ overflow: hidden;
486
+ -webkit-box-orient: vertical;
487
+ -webkit-line-clamp: 2;
488
+ }
489
+
490
+ .zx-card-view .zx-record-card__subtitle {
491
+ margin-block-start: calc(var(--zx-space-1) / 2);
492
+ overflow: hidden;
493
+ font-size: var(--zx-text-xs);
494
+ line-height: 1.3;
495
+ text-overflow: ellipsis;
496
+ white-space: nowrap;
497
+ }
498
+
499
+ .zx-card-view .zx-record-card__actions {
500
+ flex-wrap: nowrap;
501
+ gap: calc(var(--zx-space-1) / 2);
502
+ }
503
+
504
+ .zx-card-view .zx-record-card__action {
505
+ min-block-size: 1.75rem;
506
+ border-color: transparent;
507
+ background: transparent;
508
+ padding-inline: var(--zx-space-2);
509
+ font-size: var(--zx-text-xs);
510
+ }
511
+
512
+ .zx-card-view .zx-record-card__metadata {
513
+ display: flex;
514
+ flex-wrap: wrap;
515
+ gap: var(--zx-space-1);
516
+ }
517
+
518
+ .zx-card-view .zx-record-card__field {
519
+ display: inline-flex;
520
+ flex: 0 1 auto;
521
+ align-items: center;
522
+ gap: var(--zx-space-1);
523
+ max-inline-size: 100%;
524
+ border: 1px solid var(--zx-color-border);
525
+ border-radius: var(--zx-radius-full);
526
+ background: var(--zx-color-bg-muted);
527
+ padding: calc(var(--zx-space-1) / 2) var(--zx-space-2);
528
+ }
529
+
530
+ .zx-card-view .zx-record-card__label {
531
+ flex: none;
532
+ font-size: var(--zx-text-xs);
533
+ font-weight: 500;
534
+ line-height: 1.2;
535
+ }
536
+
537
+ .zx-card-view .zx-record-card__value {
538
+ overflow: hidden;
539
+ font-size: var(--zx-text-xs);
540
+ line-height: 1.2;
541
+ text-overflow: ellipsis;
542
+ white-space: nowrap;
543
+ }
544
+
545
+ .zx-card-view .zx-card-view__group-empty {
546
+ padding: var(--zx-space-3);
547
+ font-size: var(--zx-text-sm);
548
+ }
549
+
550
+ .zx-card-view .zx-card-view__empty {
551
+ min-block-size: 6rem;
552
+ border-radius: var(--zx-radius-md);
553
+ padding: var(--zx-space-4);
554
+ font-size: var(--zx-text-sm);
555
+ }
556
+
557
+ .zx-card-view .zx-card-view__skeleton {
558
+ grid-template-columns: var(--zx-card-view-preview-size) minmax(0, 1fr);
559
+ gap: var(--zx-card-view-card-gap);
560
+ padding: var(--zx-card-view-card-padding);
561
+ }
562
+
563
+ .zx-card-view .zx-card-view__skeleton-preview {
564
+ grid-row: 1 / span 2;
565
+ inline-size: var(--zx-card-view-preview-size);
566
+ margin: 0;
567
+ aspect-ratio: 1;
568
+ border-radius: var(--zx-radius-sm);
569
+ }
570
+
571
+ .zx-card-view .zx-card-view__skeleton-line {
572
+ min-inline-size: 0;
573
+ block-size: .625rem;
574
+ }
575
+
576
+ .zx-card-view .zx-card-view__skeleton-line:nth-child(3) {
577
+ inline-size: 62%;
578
+ }
579
+
580
+ .zx-card-view .zx-card-view__skeleton-line:nth-child(4) {
581
+ grid-column: 1 / -1;
582
+ inline-size: 42%;
583
+ }
584
+
585
+ @media (pointer: coarse) {
586
+ .zx-card-view .zx-record-card__selection,
587
+ .zx-card-view .zx-record-card__action {
588
+ min-inline-size: 44px;
589
+ min-block-size: 44px;
590
+ }
591
+ }
@@ -39,7 +39,7 @@ import {
39
39
  * @property {string|((record:ViewRecord,index:number)=>unknown)|null} [groupBy=null]
40
40
  * Field id or local group resolver.
41
41
  * @property {unknown[]} [groupOrder=[]] Preferred group ids; missing ids render as empty groups.
42
- * @property {number|string} [minCardWidth='18rem'] Responsive minimum card width.
42
+ * @property {number|string} [minCardWidth='15rem'] Responsive minimum card width.
43
43
  * @property {number|null} [maxColumns=null] Maximum cards per row; null is unconstrained.
44
44
  * @property {'outlined'|'raised'|'filled'} [variant='outlined'] Card surface treatment.
45
45
  * @property {number} [loadingCount=6] Skeleton cards shown while loading.
@@ -87,7 +87,7 @@ export class CardView extends RecordView {
87
87
  actions: [],
88
88
  groupBy: null,
89
89
  groupOrder: [],
90
- minCardWidth: '18rem',
90
+ minCardWidth: '15rem',
91
91
  maxColumns: null,
92
92
  variant: 'outlined',
93
93
  loadingCount: 6,
@@ -329,7 +329,7 @@ export class CardView extends RecordView {
329
329
  for (const [id, card] of this._cardNodesById) {
330
330
  const selected = this._isViewSelected(id);
331
331
  card.dataset.selected = String(selected);
332
- if (this.options.selectable) card.setAttribute('aria-selected', String(selected));
332
+ if (this.options.selectable) card.setAttribute('aria-description', selected ? 'Selected' : 'Not selected');
333
333
  const checkbox = /** @type {HTMLInputElement|null} */ (card.querySelector('[data-record-selection]'));
334
334
  if (checkbox) checkbox.checked = selected;
335
335
  }
@@ -469,11 +469,11 @@ function normalizeGroupId(value) {
469
469
 
470
470
  /** @param {unknown} value @returns {string} */
471
471
  export function normalizeCardWidth(value) {
472
- if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? `${value}px` : '18rem';
472
+ if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? `${value}px` : '15rem';
473
473
  const text = String(value ?? '').trim();
474
- if (!text || text.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text)) return '18rem';
475
- if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text)) return '18rem';
476
- if (typeof globalThis.CSS?.supports === 'function' && !globalThis.CSS.supports('width', text)) return '18rem';
474
+ if (!text || text.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text)) return '15rem';
475
+ if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text)) return '15rem';
476
+ if (typeof globalThis.CSS?.supports === 'function' && !globalThis.CSS.supports('width', text)) return '15rem';
477
477
  return text;
478
478
  }
479
479
 
@@ -7,6 +7,15 @@ import { readViewField, renderViewField } from '../view/record-view.js';
7
7
  /** @typedef {Record<string, any>} ViewRecord */
8
8
  /** @typedef {import('../view/record-view.js').ViewField} ViewField */
9
9
  /** @typedef {string|((record:ViewRecord,index:number)=>unknown)|null} RecordCardValueSource */
10
+ /** @typedef {'eyebrow-start'|'eyebrow-end'|'title-prefix'} RecordCardFieldSlot */
11
+
12
+ /**
13
+ * @typedef {Object} RecordCardFieldGroups
14
+ * @property {ViewField[]} eyebrowStart Fields shown at the logical start of the eyebrow row.
15
+ * @property {ViewField[]} eyebrowEnd Fields shown at the logical end of the eyebrow row.
16
+ * @property {ViewField[]} titlePrefix Fields shown immediately before, but outside, the heading.
17
+ * @property {ViewField[]} metadata Remaining labelled metadata fields.
18
+ */
10
19
 
11
20
  /**
12
21
  * @typedef {Object} RecordCardPreviewDescriptor
@@ -73,11 +82,12 @@ import { readViewField, renderViewField } from '../view/record-view.js';
73
82
  * becomes text, URLs are normalized before reaching native attributes, and actions expose stable
74
83
  * data attributes for a component-owned delegated listener.
75
84
  *
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.
85
+ * The returned `<li>` belongs in a semantic `<ul>`/`<ol>`. When selectable its global
86
+ * `aria-description` announces selection without applying the unsupported `aria-selected` state
87
+ * to a listitem; multi-selection also adds a native checkbox. It deliberately remains a listitem
88
+ * rather than becoming an ARIA option because cards may contain native links and buttons, which
89
+ * the listbox pattern does not permit inside an option. Preview images are paired with a stable
90
+ * fallback. The owning component listens for image `error` and marks the fallback.
81
91
  *
82
92
  * @param {ViewRecord} record Record to render.
83
93
  * @param {number} index Current display index.
@@ -112,6 +122,13 @@ export function createRecordCard(record, index, options = {}) {
112
122
  class: 'zx-record-card__heading',
113
123
  id: headingId
114
124
  }, headingContent);
125
+ const fieldGroups = partitionRecordCardFields(visibleFields, titleSource, subtitleSource);
126
+ const eyebrow = createEyebrow(record, index, fieldGroups.eyebrowStart, fieldGroups.eyebrowEnd);
127
+ const titlePrefix = createFieldList(record, index, fieldGroups.titlePrefix,
128
+ 'zx-record-card__title-prefix', 'title-prefix');
129
+ const titleRow = titlePrefix
130
+ ? h('div', { class: 'zx-record-card__title-row' }, titlePrefix, heading)
131
+ : heading;
115
132
  const selection = selectable === 'multi' ? h('label', {
116
133
  class: 'zx-record-card__selection'
117
134
  }, h('input', {
@@ -127,15 +144,15 @@ export function createRecordCard(record, index, options = {}) {
127
144
  ariaLabel: `Actions for ${textAlternative(title) || 'record'}`
128
145
  }, controls, actionNodes) : null;
129
146
  const header = h('header', { class: 'zx-record-card__header' }, selection,
130
- h('div', { class: 'zx-record-card__titles' }, heading,
147
+ h('div', { class: 'zx-record-card__titles' }, titleRow,
131
148
  isEmptyContent(subtitle) ? null : h('p', { class: 'zx-record-card__subtitle' }, contentNode(subtitle))),
132
149
  actionArea);
133
- const metadata = createMetadata(record, index, visibleFields, titleSource, subtitleSource);
134
- const body = h('div', { class: 'zx-record-card__body' }, header, metadata);
150
+ const metadata = createFieldList(record, index, fieldGroups.metadata, 'zx-record-card__metadata');
151
+ const body = h('div', { class: 'zx-record-card__body' }, eyebrow, header, metadata);
135
152
  const card = /** @type {HTMLLIElement} */ (h('li', {
136
153
  class: 'zx-record-card',
137
154
  tabindex: 0,
138
- ariaSelected: selectable ? String(selected) : undefined,
155
+ ariaDescription: selectable ? selected ? 'Selected' : 'Not selected' : undefined,
139
156
  ariaLabelledby: headingId,
140
157
  dataset: {
141
158
  recordIndex: String(index),
@@ -146,6 +163,36 @@ export function createRecordCard(record, index, options = {}) {
146
163
  return card;
147
164
  }
148
165
 
166
+ /**
167
+ * Partitions ordered visible fields into the shared record-card slots. A recognized
168
+ * `field.view.card.slot` moves the field to that semantic region; absent or unknown values remain
169
+ * ordinary metadata. Title and subtitle fields retain the existing no-duplication rule.
170
+ *
171
+ * The input array and descriptors are never mutated. Each field appears in at most one result
172
+ * array, and the order within every result follows the supplied visible-field order.
173
+ *
174
+ * @param {ViewField[]} fields Ordered visible fields.
175
+ * @param {RecordCardValueSource} [titleSource=null] Active title field or resolver.
176
+ * @param {RecordCardValueSource} [subtitleSource=null] Active subtitle field or resolver.
177
+ * @returns {RecordCardFieldGroups} Partitioned fields.
178
+ */
179
+ export function partitionRecordCardFields(fields, titleSource = null, subtitleSource = null) {
180
+ /** @type {RecordCardFieldGroups} */
181
+ const groups = { eyebrowStart: [], eyebrowEnd: [], titlePrefix: [], metadata: [] };
182
+ const titleId = typeof titleSource === 'string' ? titleSource : null;
183
+ const subtitleId = typeof subtitleSource === 'string' ? subtitleSource : null;
184
+
185
+ for (const field of Array.isArray(fields) ? fields : []) {
186
+ if (!field.duplicate && (field.id === titleId || field.id === subtitleId)) continue;
187
+ const slot = resolveRecordCardFieldSlot(field);
188
+ if (slot === 'eyebrow-start') groups.eyebrowStart.push(field);
189
+ else if (slot === 'eyebrow-end') groups.eyebrowEnd.push(field);
190
+ else if (slot === 'title-prefix') groups.titlePrefix.push(field);
191
+ else groups.metadata.push(field);
192
+ }
193
+ return groups;
194
+ }
195
+
149
196
  /**
150
197
  * Resolves a preview without touching the DOM. Unsafe, malformed, and non-image URL schemes are
151
198
  * rejected into the same descriptor shape so callers can still render the stable fallback.
@@ -245,26 +292,52 @@ function createPreview(preview) {
245
292
  }, fallback, image);
246
293
  }
247
294
 
295
+ /**
296
+ * @param {ViewRecord} record
297
+ * @param {number} index
298
+ * @param {ViewField[]} eyebrowStart
299
+ * @param {ViewField[]} eyebrowEnd
300
+ * @returns {HTMLElement|null}
301
+ */
302
+ function createEyebrow(record, index, eyebrowStart, eyebrowEnd) {
303
+ const start = createFieldList(record, index, eyebrowStart,
304
+ 'zx-record-card__eyebrow-group', 'eyebrow-start');
305
+ const end = createFieldList(record, index, eyebrowEnd,
306
+ 'zx-record-card__eyebrow-group', 'eyebrow-end');
307
+ return start || end ? h('div', { class: 'zx-record-card__eyebrow' }, start, end) : null;
308
+ }
309
+
248
310
  /**
249
311
  * @param {ViewRecord} record
250
312
  * @param {number} index
251
313
  * @param {ViewField[]} fields
252
- * @param {RecordCardValueSource} titleSource
253
- * @param {RecordCardValueSource} subtitleSource
314
+ * @param {string} className
315
+ * @param {RecordCardFieldSlot|null} [slot=null]
254
316
  * @returns {HTMLElement|null}
255
317
  */
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', {
318
+ function createFieldList(record, index, fields, className, slot = null) {
319
+ if (fields.length === 0) return null;
320
+ return h('dl', {
321
+ class: className,
322
+ dataset: slot ? { cardSlot: slot } : undefined
323
+ }, fields.map((field) => h('div', {
262
324
  class: 'zx-record-card__field',
263
- dataset: { fieldId: field.id }
325
+ dataset: { fieldId: field.id, ...(slot ? { cardSlot: slot } : {}) }
264
326
  }, h('dt', { class: 'zx-record-card__label' }, field.label),
265
327
  h('dd', { class: 'zx-record-card__value' }, contentNode(renderViewField(field, record, index))))));
266
328
  }
267
329
 
330
+ /** @param {ViewField} field @returns {RecordCardFieldSlot|null} */
331
+ function resolveRecordCardFieldSlot(field) {
332
+ const view = field?.view;
333
+ if (!view || typeof view !== 'object' || Array.isArray(view)) return null;
334
+ const card = view.card;
335
+ if (!card || typeof card !== 'object' || Array.isArray(card)) return null;
336
+ const slot = /** @type {{slot?:unknown}} */ (card).slot;
337
+ return slot === 'eyebrow-start' || slot === 'eyebrow-end' || slot === 'title-prefix'
338
+ ? slot : null;
339
+ }
340
+
268
341
  /** @param {RecordCardAction} action @returns {HTMLElement} */
269
342
  function createAction(action) {
270
343
  const label = String(action.label ?? '');
@@ -356,7 +429,7 @@ function safePreviewUrl(value) {
356
429
  try {
357
430
  const base = globalThis.location?.href ?? 'https://zx.invalid/';
358
431
  const protocol = new URL(href, base).protocol.toLowerCase();
359
- return ['http:', 'https:', 'blob:', 'file:'].includes(protocol) ? href : null;
432
+ return ['http:', 'https:', 'blob:'].includes(protocol) ? href : null;
360
433
  } catch {
361
434
  return null;
362
435
  }