@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.
Files changed (136) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/README.md +7 -3
  3. package/dist/types/components/activity-item/activity-item.d.ts +351 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +195 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +28 -0
  6. package/dist/types/components/button/button.d.ts +8 -0
  7. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  8. package/dist/types/components/calendar/calendar.d.ts +19 -0
  9. package/dist/types/components/card/card.d.ts +7 -0
  10. package/dist/types/components/card-view/card-view.d.ts +12 -4
  11. package/dist/types/components/card-view/record-card.d.ts +53 -5
  12. package/dist/types/components/code-editor/code-editor.d.ts +160 -0
  13. package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
  14. package/dist/types/components/context-menu/context-menu.d.ts +18 -2
  15. package/dist/types/components/date-picker/date-picker.d.ts +8 -0
  16. package/dist/types/components/date-picker/time-picker.d.ts +7 -0
  17. package/dist/types/components/dialog/dialog.d.ts +29 -0
  18. package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
  19. package/dist/types/components/file-item/file-item.d.ts +234 -0
  20. package/dist/types/components/file-list/file-list.d.ts +171 -0
  21. package/dist/types/components/filter/filter.d.ts +91 -36
  22. package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
  23. package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
  24. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  25. package/dist/types/components/kanban-view/kanban-view.d.ts +837 -26
  26. package/dist/types/components/launcher/launcher.d.ts +19 -12
  27. package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
  28. package/dist/types/components/menu-button/menu-button.d.ts +4 -37
  29. package/dist/types/components/message/message.d.ts +7 -0
  30. package/dist/types/components/modal/modal.d.ts +48 -0
  31. package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
  32. package/dist/types/components/panel/panel.d.ts +7 -0
  33. package/dist/types/components/search/search.d.ts +7 -0
  34. package/dist/types/components/select/select.d.ts +4 -0
  35. package/dist/types/components/sort-control/sort-control.d.ts +105 -0
  36. package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
  37. package/dist/types/components/tabbox/tabbox.d.ts +7 -0
  38. package/dist/types/components/table/table.d.ts +144 -0
  39. package/dist/types/components/table-view/table-view.d.ts +11 -5
  40. package/dist/types/components/timebox/timebox.d.ts +7 -0
  41. package/dist/types/components/value-list/value-list.d.ts +8 -0
  42. package/dist/types/components/view/record-view.d.ts +96 -5
  43. package/dist/types/core/component.d.ts +6 -1
  44. package/dist/types/core/icons.d.ts +17 -0
  45. package/dist/types/index.d.ts +13 -1
  46. package/dist/types/internal/app-rail.d.ts +10 -0
  47. package/dist/types/internal/menu-items.d.ts +134 -0
  48. package/dist/zx-zeyos.esm.js +651 -88
  49. package/dist/zx-zeyos.esm.min.js +8 -1
  50. package/dist/zx.css +6266 -3900
  51. package/dist/zx.esm.js +8487 -1805
  52. package/dist/zx.esm.min.js +10 -3
  53. package/dist/zx.global.js +8487 -1805
  54. package/dist/zx.global.min.js +10 -3
  55. package/dist/zx.min.css +1 -1
  56. package/docs/api.json +1811 -247
  57. package/docs/llms.md +345 -40
  58. package/docs/llms.txt +11 -7
  59. package/package.json +1 -2
  60. package/src/components/activity-item/activity-item.css +137 -0
  61. package/src/components/activity-item/activity-item.js +599 -0
  62. package/src/components/activity-list/activity-list.css +64 -0
  63. package/src/components/activity-list/activity-list.js +375 -0
  64. package/src/components/app-rail/app-rail.css +227 -19
  65. package/src/components/app-sidebar/app-sidebar.css +414 -24
  66. package/src/components/app-sidebar/app-sidebar.js +65 -5
  67. package/src/components/button/button.css +8 -0
  68. package/src/components/button/button.js +6 -1
  69. package/src/components/calendar/calendar-model.js +45 -0
  70. package/src/components/calendar/calendar.css +30 -10
  71. package/src/components/calendar/calendar.js +41 -13
  72. package/src/components/card/card.js +12 -1
  73. package/src/components/card-view/card-view.css +212 -9
  74. package/src/components/card-view/card-view.js +32 -16
  75. package/src/components/card-view/record-card.js +98 -21
  76. package/src/components/code-editor/code-editor.css +67 -0
  77. package/src/components/code-editor/code-editor.js +288 -0
  78. package/src/components/code-editor/field-adapter.js +34 -0
  79. package/src/components/context-menu/context-menu.css +80 -3
  80. package/src/components/context-menu/context-menu.js +22 -30
  81. package/src/components/date-picker/date-picker.js +22 -8
  82. package/src/components/date-picker/month-picker.js +3 -3
  83. package/src/components/date-picker/time-picker.js +20 -8
  84. package/src/components/dialog/dialog.js +88 -66
  85. package/src/components/dropdown/dropdown.js +2 -2
  86. package/src/components/entity-ref/entity-ref.css +182 -0
  87. package/src/components/entity-ref/entity-ref.js +496 -0
  88. package/src/components/file-item/file-item.css +88 -0
  89. package/src/components/file-item/file-item.js +371 -0
  90. package/src/components/file-list/file-list.css +51 -0
  91. package/src/components/file-list/file-list.js +320 -0
  92. package/src/components/filter/filter.css +27 -1
  93. package/src/components/filter/filter.js +199 -45
  94. package/src/components/filter-panel/filter-panel.css +104 -0
  95. package/src/components/filter-panel/filter-panel.js +964 -0
  96. package/src/components/kanban-view/kanban-card.js +239 -0
  97. package/src/components/kanban-view/kanban-policy.js +412 -0
  98. package/src/components/kanban-view/kanban-view.css +674 -36
  99. package/src/components/kanban-view/kanban-view.js +1762 -240
  100. package/src/components/launcher/launcher.js +59 -27
  101. package/src/components/list-toolbar/list-toolbar.css +189 -0
  102. package/src/components/list-toolbar/list-toolbar.js +851 -0
  103. package/src/components/menu-button/menu-button.css +80 -3
  104. package/src/components/menu-button/menu-button.js +21 -37
  105. package/src/components/message/message.css +14 -12
  106. package/src/components/message/message.js +13 -2
  107. package/src/components/modal/modal.css +15 -0
  108. package/src/components/modal/modal.js +141 -0
  109. package/src/components/navigation-bar/navigation-bar.css +77 -2
  110. package/src/components/navigation-bar/navigation-bar.js +171 -4
  111. package/src/components/panel/panel.js +13 -2
  112. package/src/components/search/search.js +17 -5
  113. package/src/components/select/field-adapter.js +4 -1
  114. package/src/components/select/select.js +3 -0
  115. package/src/components/sort-control/sort-control.css +17 -0
  116. package/src/components/sort-control/sort-control.js +140 -0
  117. package/src/components/stat-tile/stat-tile.css +213 -0
  118. package/src/components/stat-tile/stat-tile.js +590 -0
  119. package/src/components/tabbox/tabbox.js +12 -1
  120. package/src/components/table/table.css +51 -2
  121. package/src/components/table/table.js +298 -19
  122. package/src/components/table-view/table-view.css +8 -2
  123. package/src/components/table-view/table-view.js +21 -6
  124. package/src/components/tag-picker/tag-picker.css +3 -1
  125. package/src/components/timebox/timebox.js +18 -5
  126. package/src/components/value-list/value-list.js +22 -6
  127. package/src/components/view/record-view.css +5 -0
  128. package/src/components/view/record-view.js +100 -7
  129. package/src/core/component.js +8 -3
  130. package/src/core/icons.js +74 -5
  131. package/src/index.js +33 -3
  132. package/src/internal/app-rail.js +19 -1
  133. package/src/internal/menu-items.js +142 -0
  134. package/styles/print.css +192 -0
  135. package/styles/zx.css +11 -0
  136. package/DESIGN-SYSTEM.md +0 -182
@@ -0,0 +1,182 @@
1
+ .zx-entity-ref {
2
+ --zx-entity-ref-visual-size: var(--zx-control-height);
3
+ display: grid;
4
+ grid-template-columns: auto minmax(0, 1fr) auto;
5
+ gap: var(--zx-space-2);
6
+ align-items: center;
7
+ min-inline-size: 0;
8
+ color: var(--zx-color-text);
9
+ }
10
+
11
+ .zx-entity-ref[data-size="sm"] {
12
+ --zx-entity-ref-visual-size: calc(var(--zx-control-height) * .75);
13
+ }
14
+
15
+ .zx-entity-ref[data-size="lg"] {
16
+ --zx-entity-ref-visual-size: calc(var(--zx-control-height) * 1.25);
17
+ }
18
+
19
+ .zx-entity-ref__icon {
20
+ display: inline-grid;
21
+ flex: none;
22
+ place-items: center;
23
+ inline-size: var(--zx-entity-ref-visual-size);
24
+ block-size: var(--zx-entity-ref-visual-size);
25
+ overflow: hidden;
26
+ border: 1px solid var(--zx-color-border);
27
+ border-radius: var(--zx-radius-md);
28
+ background: var(--zx-color-bg-muted);
29
+ color: var(--zx-color-text-muted);
30
+ }
31
+
32
+ .zx-entity-ref__icon[hidden],
33
+ .zx-entity-ref__title[hidden],
34
+ .zx-entity-ref__subtitle[hidden],
35
+ .zx-entity-ref__metadata[hidden],
36
+ .zx-entity-ref__actions[hidden] {
37
+ display: none;
38
+ }
39
+
40
+ .zx-entity-ref__body,
41
+ .zx-entity-ref__title,
42
+ .zx-entity-ref__subtitle,
43
+ .zx-entity-ref__primary,
44
+ .zx-entity-ref__name {
45
+ min-inline-size: 0;
46
+ }
47
+
48
+ .zx-entity-ref__body {
49
+ display: grid;
50
+ gap: var(--zx-space-1);
51
+ }
52
+
53
+ .zx-entity-ref__title {
54
+ color: var(--zx-color-text);
55
+ font-weight: 600;
56
+ line-height: 1.25;
57
+ }
58
+
59
+ .zx-entity-ref__primary,
60
+ .zx-entity-ref__name {
61
+ display: block;
62
+ color: inherit;
63
+ }
64
+
65
+ .zx-entity-ref__primary {
66
+ text-decoration-color: transparent;
67
+ text-underline-offset: .15em;
68
+ }
69
+
70
+ .zx-entity-ref__primary:hover {
71
+ color: var(--zx-color-accent-hover);
72
+ text-decoration-color: currentColor;
73
+ }
74
+
75
+ .zx-entity-ref__subtitle,
76
+ .zx-entity-ref__metadata {
77
+ color: var(--zx-color-text-muted);
78
+ font-size: var(--zx-text-sm);
79
+ line-height: 1.3;
80
+ }
81
+
82
+ .zx-entity-ref[data-wrap="false"] .zx-entity-ref__title,
83
+ .zx-entity-ref[data-wrap="false"] .zx-entity-ref__subtitle,
84
+ .zx-entity-ref[data-wrap="false"] .zx-entity-ref__primary,
85
+ .zx-entity-ref[data-wrap="false"] .zx-entity-ref__name {
86
+ overflow: hidden;
87
+ text-overflow: ellipsis;
88
+ white-space: nowrap;
89
+ }
90
+
91
+ .zx-entity-ref__metadata {
92
+ display: flex;
93
+ flex-wrap: wrap;
94
+ gap: var(--zx-space-1) var(--zx-space-3);
95
+ margin: 0;
96
+ }
97
+
98
+ .zx-entity-ref__field {
99
+ display: inline-flex;
100
+ gap: var(--zx-space-1);
101
+ align-items: center;
102
+ min-inline-size: 0;
103
+ }
104
+
105
+ .zx-entity-ref__label,
106
+ .zx-entity-ref__value {
107
+ margin: 0;
108
+ }
109
+
110
+ .zx-entity-ref__label::after {
111
+ content: ":";
112
+ }
113
+
114
+ .zx-entity-ref__field[data-label-visible="false"] .zx-entity-ref__label {
115
+ position: absolute;
116
+ inline-size: 1px;
117
+ block-size: 1px;
118
+ overflow: hidden;
119
+ clip-path: inset(50%);
120
+ }
121
+
122
+ .zx-entity-ref__value {
123
+ display: inline-flex;
124
+ gap: var(--zx-space-1);
125
+ align-items: center;
126
+ min-inline-size: 0;
127
+ }
128
+
129
+ .zx-entity-ref__actions {
130
+ display: inline-flex;
131
+ flex-wrap: wrap;
132
+ gap: var(--zx-space-1);
133
+ align-items: center;
134
+ justify-content: flex-end;
135
+ }
136
+
137
+ .zx-entity-ref__action {
138
+ display: inline-flex;
139
+ gap: var(--zx-space-1);
140
+ align-items: center;
141
+ justify-content: center;
142
+ min-block-size: calc(var(--zx-control-height) * .75);
143
+ padding: var(--zx-space-1) var(--zx-space-2);
144
+ border: 1px solid transparent;
145
+ border-radius: var(--zx-radius-md);
146
+ background: transparent;
147
+ color: var(--zx-color-text-muted);
148
+ font: inherit;
149
+ text-decoration: none;
150
+ cursor: pointer;
151
+ }
152
+
153
+ .zx-entity-ref__action:hover:not(:disabled) {
154
+ background: var(--zx-color-bg-hover);
155
+ color: var(--zx-color-text);
156
+ }
157
+
158
+ .zx-entity-ref__action[data-kind="primary"] {
159
+ color: var(--zx-color-accent);
160
+ }
161
+
162
+ .zx-entity-ref__action[data-kind="danger"] {
163
+ color: var(--zx-color-danger);
164
+ }
165
+
166
+ .zx-entity-ref__action[data-kind="default"] {
167
+ border-color: var(--zx-color-border-control);
168
+ background: var(--zx-color-bg-control);
169
+ color: var(--zx-color-text);
170
+ }
171
+
172
+ .zx-entity-ref__action:disabled {
173
+ opacity: .55;
174
+ cursor: default;
175
+ }
176
+
177
+ @media (forced-colors: active) {
178
+ .zx-entity-ref__icon,
179
+ .zx-entity-ref__action[data-kind="default"] {
180
+ border-color: CanvasText;
181
+ }
182
+ }
@@ -0,0 +1,496 @@
1
+ // @ts-check
2
+ import { Component } from '../../core/component.js';
3
+ import { h, restoreTarget, safeHref, snapshotTarget } from '../../core/dom.js';
4
+ import { icon as createIcon } from '../../core/icons.js';
5
+ import { isElement } from '../../core/util.js';
6
+
7
+ /** @typedef {string|number|Node|Component|null} EntityRefContent */
8
+ /** @typedef {string|Node|Component|(()=>string|Node|Component|null)|null} EntityRefIcon */
9
+
10
+ /**
11
+ * @typedef {Object} EntityRefLink
12
+ * @property {string} href Native primary-link destination.
13
+ * @property {string} [target] Native browsing context.
14
+ * @property {string} [rel] Native link relationship.
15
+ * @property {boolean|string} [download] Native download hint; a string supplies the filename.
16
+ * @property {(event: MouseEvent, entity: Readonly<EntityRefState>, component: EntityRef) => void} [onclick]
17
+ * Optional callback after an uncanceled activation; navigation remains native.
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} EntityRefMetadata
22
+ * @property {string} label Metadata label.
23
+ * @property {EntityRefContent} value Metadata value.
24
+ * @property {string|null} [icon=null] Optional Zx icon before the value.
25
+ * @property {boolean} [showLabel=true] Whether the label is visible instead of visually hidden.
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} EntityRefAction
30
+ * @property {string} id Stable action identifier.
31
+ * @property {string} [label=''] Visible label.
32
+ * @property {string|null} [icon=null] Optional Zx icon.
33
+ * @property {string} [title] Accessible/native title, required when the label is empty.
34
+ * @property {string} [href] Optional safe native link destination; otherwise a button is rendered.
35
+ * @property {string} [target] Native link browsing context.
36
+ * @property {string} [rel] Native link relationship.
37
+ * @property {'default'|'primary'|'danger'|'ghost'} [kind='ghost'] Visual intent.
38
+ * @property {boolean} [disabled=false] Whether activation is unavailable.
39
+ * @property {(id: string, action: Readonly<EntityRefAction>, entity: Readonly<EntityRefState>, component: EntityRef) => void} [onselect]
40
+ * Callback after an uncanceled activation.
41
+ */
42
+
43
+ /** @typedef {Element|EntityRefAction} EntityRefActionEntry */
44
+ /** @typedef {string|EntityRefLink|null} EntityRefLinkValue */
45
+
46
+ /**
47
+ * @typedef {Object} EntityRefOptions
48
+ * @property {unknown} [id=null] Application-owned entity identifier included in events.
49
+ * @property {string} [title=''] Primary identity text.
50
+ * @property {EntityRefContent} [subtitle=null] Secondary identity text or safe caller-supplied content.
51
+ * @property {EntityRefIcon} [icon=null] Icon name, node, component, or lazy renderer.
52
+ * @property {EntityRefMetadata[]} [metadata=[]] Labelled dense metadata.
53
+ * @property {EntityRefLinkValue} [link=null] Optional native primary link.
54
+ * @property {EntityRefActionEntry[]} [actions=[]] Secondary controls; element actions keep their own behavior.
55
+ * @property {'sm'|'md'|'lg'} [size='md'] Density-independent identity size.
56
+ * @property {boolean} [wrap=false] Whether long identity text may wrap instead of truncating.
57
+ * @property {string} [actionsLabel='Entity actions'] Accessible name for secondary actions.
58
+ * @property {(event: CustomEvent<{id: unknown, entity: Readonly<EntityRefState>, href: string, event: MouseEvent}>) => void} [onactivate]
59
+ * Preventable primary-link listener.
60
+ * @property {(event: CustomEvent<{id: string, action: Readonly<EntityRefAction>, entity: Readonly<EntityRefState>, event: MouseEvent}>) => void} [onaction]
61
+ * Preventable delegated-action listener.
62
+ */
63
+
64
+ /**
65
+ * @typedef {Object} EntityRefState
66
+ * @property {unknown} id Entity identifier.
67
+ * @property {string} title Primary identity.
68
+ * @property {EntityRefContent} subtitle Secondary identity.
69
+ * @property {EntityRefIcon} icon Identity icon.
70
+ * @property {EntityRefMetadata[]} metadata Normalized metadata copies.
71
+ * @property {EntityRefLink|null} link Safe native link.
72
+ * @property {EntityRefActionEntry[]} actions Normalized action copies or elements.
73
+ * @property {'sm'|'md'|'lg'} size Presentation size.
74
+ * @property {boolean} wrap Wrapping state.
75
+ * @property {string} actionsLabel Action-group label.
76
+ */
77
+
78
+ /**
79
+ * Dense entity identity for tables, feeds, selectors, and detail metadata. It owns presentation and
80
+ * safe native-link/action semantics; routing, permissions, context menus, and persistence remain
81
+ * application-owned.
82
+ * @fires EntityRef#activate
83
+ * @fires EntityRef#action
84
+ * @extends {Component<EntityRefOptions>}
85
+ */
86
+ export class EntityRef extends Component {
87
+ static cssName = 'entity-ref';
88
+
89
+ /** @type {Readonly<EntityRefOptions>} */
90
+ static defaults = {
91
+ id: null,
92
+ title: '',
93
+ subtitle: null,
94
+ icon: null,
95
+ metadata: [],
96
+ link: null,
97
+ actions: [],
98
+ size: 'md',
99
+ wrap: false,
100
+ actionsLabel: 'Entity actions'
101
+ };
102
+
103
+ /**
104
+ * Creates or enhances a dense entity reference.
105
+ * @param {Element|string|null} [target=null] Existing host, selector, or null.
106
+ * @param {EntityRefOptions} [options={}] Entity presentation.
107
+ */
108
+ constructor(target = null, options = {}) {
109
+ super(target, options);
110
+ }
111
+
112
+ /** @returns {HTMLElement} */
113
+ render() {
114
+ this._createdRoot = this.el === null;
115
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
116
+ this.el = root;
117
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
118
+ this._destroyed = false;
119
+ this._state = normalizeEntityRef(this.options);
120
+ this._renderedActions = [];
121
+
122
+ root.replaceChildren(
123
+ h('span', { ref: 'icon', class: 'zx-entity-ref__icon', ariaHidden: 'true' }),
124
+ h('div', { class: 'zx-entity-ref__body' },
125
+ h('span', { ref: 'title', class: 'zx-entity-ref__title' }),
126
+ h('span', { ref: 'subtitle', class: 'zx-entity-ref__subtitle' }),
127
+ h('dl', { ref: 'metadata', class: 'zx-entity-ref__metadata' })),
128
+ h('div', {
129
+ ref: 'actions',
130
+ class: 'zx-entity-ref__actions',
131
+ role: 'group'
132
+ })
133
+ );
134
+
135
+ this.listen(root, 'click', (event) => this._handleClick(/** @type {MouseEvent} */ (event)));
136
+ this._sync();
137
+ return root;
138
+ }
139
+
140
+ /**
141
+ * Updates any subset of the identity without replacing the root.
142
+ * @param {Partial<EntityRefOptions>} values Next values.
143
+ * @returns {this}
144
+ */
145
+ set(values = {}) {
146
+ if (!values || typeof values !== 'object' || Array.isArray(values)) return this;
147
+ this._state = normalizeEntityRef({ ...this._state, ...values });
148
+ this._sync();
149
+ return this;
150
+ }
151
+
152
+ /** Replaces the metadata collection. @param {EntityRefMetadata[]} metadata Metadata. @returns {this} */
153
+ setMetadata(metadata) {
154
+ return this.set({ metadata });
155
+ }
156
+
157
+ /** Replaces the secondary action controls. @param {EntityRefActionEntry[]} actions Actions. @returns {this} */
158
+ setActions(actions) {
159
+ return this.set({ actions });
160
+ }
161
+
162
+ /** Sets or clears the primary link. @param {EntityRefLinkValue} link Link. @returns {this} */
163
+ setLink(link) {
164
+ return this.set({ link });
165
+ }
166
+
167
+ /**
168
+ * Returns a defensive presentation snapshot. Caller-supplied Nodes and Components retain their
169
+ * identity; arrays and descriptors are copied.
170
+ * @returns {Readonly<EntityRefState>}
171
+ */
172
+ getEntity() {
173
+ return entitySnapshot(this._state);
174
+ }
175
+
176
+ /** Focuses the primary link or the first enabled action. @returns {this} */
177
+ focus() {
178
+ const control = this.refs.title.querySelector('a[href]')
179
+ ?? this.refs.actions.querySelector('a[href], button:not(:disabled)');
180
+ if (control && typeof /** @type {HTMLElement} */ (control).focus === 'function') {
181
+ /** @type {HTMLElement} */ (control).focus();
182
+ }
183
+ return this;
184
+ }
185
+
186
+ /** Restores an enhanced target exactly, or removes an owned reference. @returns {void} */
187
+ destroy() {
188
+ if (this._destroyed) return;
189
+ this._destroyed = true;
190
+ const root = this.el;
191
+ super.destroy();
192
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
193
+ }
194
+
195
+ /** @returns {void} */
196
+ _sync() {
197
+ const state = this._state;
198
+ const root = /** @type {HTMLElement} */ (this.el);
199
+ const iconHost = /** @type {HTMLElement} */ (this.refs.icon);
200
+ const titleHost = /** @type {HTMLElement} */ (this.refs.title);
201
+ const subtitleHost = /** @type {HTMLElement} */ (this.refs.subtitle);
202
+ const metadataHost = /** @type {HTMLElement} */ (this.refs.metadata);
203
+ const actionsHost = /** @type {HTMLElement} */ (this.refs.actions);
204
+ root.dataset.size = state.size;
205
+ root.dataset.wrap = String(state.wrap);
206
+ const visual = resolveEntityIcon(state.icon);
207
+ iconHost.replaceChildren(...(visual === null ? [] : [visual]));
208
+ iconHost.hidden = visual === null;
209
+
210
+ const title = state.link ? h('a', {
211
+ class: 'zx-entity-ref__primary',
212
+ href: state.link.href,
213
+ target: state.link.target,
214
+ rel: state.link.rel,
215
+ download: state.link.download === true ? '' : state.link.download,
216
+ dataset: { entityPrimary: '' }
217
+ }, state.title) : h('span', { class: 'zx-entity-ref__name' }, state.title);
218
+ titleHost.replaceChildren(title);
219
+ titleHost.hidden = state.title === '';
220
+
221
+ replaceContent(subtitleHost, state.subtitle);
222
+ subtitleHost.hidden = emptyContent(state.subtitle);
223
+
224
+ metadataHost.replaceChildren(...state.metadata.map((entry) => h('div', {
225
+ class: 'zx-entity-ref__field',
226
+ dataset: { labelVisible: String(entry.showLabel !== false) }
227
+ }, h('dt', { class: 'zx-entity-ref__label' }, entry.label),
228
+ h('dd', { class: 'zx-entity-ref__value' },
229
+ entry.icon ? createIcon(entry.icon, { size: 12 }) : null,
230
+ contentNode(entry.value)))));
231
+ metadataHost.hidden = state.metadata.length === 0;
232
+
233
+ this._renderedActions = state.actions;
234
+ actionsHost.setAttribute('aria-label', state.actionsLabel);
235
+ actionsHost.replaceChildren(...state.actions.map((action, index) => isElement(action)
236
+ ? action : renderAction(action, index)));
237
+ actionsHost.hidden = state.actions.length === 0;
238
+ }
239
+
240
+ /** @param {MouseEvent} event @returns {void} */
241
+ _handleClick(event) {
242
+ const target = event.target instanceof Element ? event.target : null;
243
+ if (!target) return;
244
+ const primary = target.closest('[data-entity-primary]');
245
+ if (primary && this.el.contains(primary) && this._state.link) {
246
+ const detail = {
247
+ id: this._state.id,
248
+ entity: this.getEntity(),
249
+ href: this._state.link.href,
250
+ event
251
+ };
252
+ const selected = this.emit('activate', detail, { honorDomCancellation: true });
253
+ if (selected.defaultPrevented) {
254
+ event.preventDefault();
255
+ return;
256
+ }
257
+ this._state.link.onclick?.(event, this.getEntity(), this);
258
+ return;
259
+ }
260
+
261
+ const control = target.closest('[data-entity-action]');
262
+ if (!control || !this.refs.actions.contains(control)) return;
263
+ const action = this._renderedActions[Number(/** @type {HTMLElement} */ (control).dataset.entityAction)];
264
+ if (!action || isElement(action) || action.disabled) return;
265
+ const selected = this.emit('action', {
266
+ id: action.id,
267
+ action: { ...action },
268
+ entity: this.getEntity(),
269
+ event
270
+ }, { honorDomCancellation: true });
271
+ if (selected.defaultPrevented) {
272
+ event.preventDefault();
273
+ return;
274
+ }
275
+ action.onselect?.(action.id, { ...action }, this.getEntity(), this);
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Normalizes an entity-reference contract without touching the DOM.
281
+ * @param {Partial<EntityRefOptions>|null|undefined} values Candidate values.
282
+ * @returns {EntityRefState}
283
+ */
284
+ export function normalizeEntityRef(values = {}) {
285
+ const source = values && typeof values === 'object' && !Array.isArray(values) ? values : {};
286
+ const title = String(source.title ?? '');
287
+ const link = normalizeEntityRefLink(source.link);
288
+ if (link && !title.trim()) throw new TypeError('A linked EntityRef requires a non-empty title');
289
+ return {
290
+ id: source.id ?? null,
291
+ title,
292
+ subtitle: normalizeContent(source.subtitle),
293
+ icon: normalizeIcon(source.icon),
294
+ metadata: normalizeEntityRefMetadata(source.metadata),
295
+ link,
296
+ actions: normalizeEntityRefActions(source.actions),
297
+ size: normalizeSize(source.size),
298
+ wrap: Boolean(source.wrap),
299
+ actionsLabel: String(source.actionsLabel || 'Entity actions')
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Normalizes and secures a primary native link.
305
+ * @param {EntityRefLinkValue|unknown} value Candidate link.
306
+ * @returns {EntityRefLink|null}
307
+ */
308
+ export function normalizeEntityRefLink(value) {
309
+ if (value == null) return null;
310
+ const descriptor = typeof value === 'string' ? { href: value } : value;
311
+ if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) return null;
312
+ const href = safeHref(/** @type {EntityRefLink} */ (descriptor).href);
313
+ if (href === null) return null;
314
+ const target = /** @type {EntityRefLink} */ (descriptor).target == null
315
+ ? undefined : String(/** @type {EntityRefLink} */ (descriptor).target);
316
+ const rel = safeRel(/** @type {EntityRefLink} */ (descriptor).rel, target);
317
+ const onclick = /** @type {EntityRefLink} */ (descriptor).onclick;
318
+ if (onclick !== undefined && typeof onclick !== 'function') {
319
+ throw new TypeError('EntityRef link onclick must be a function');
320
+ }
321
+ const rawDownload = /** @type {EntityRefLink} */ (descriptor).download;
322
+ const download = rawDownload === true ? true
323
+ : typeof rawDownload === 'string' ? rawDownload : undefined;
324
+ return {
325
+ href,
326
+ ...(target ? { target } : {}),
327
+ ...(rel ? { rel } : {}),
328
+ ...(download !== undefined ? { download } : {}),
329
+ ...(onclick ? { onclick } : {})
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Normalizes labelled metadata, omitting entries without a label or displayable value.
335
+ * @param {unknown} values Candidate metadata.
336
+ * @returns {EntityRefMetadata[]}
337
+ */
338
+ export function normalizeEntityRefMetadata(values) {
339
+ if (!Array.isArray(values)) return [];
340
+ return values.flatMap((entry) => {
341
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
342
+ const descriptor = /** @type {EntityRefMetadata} */ (entry);
343
+ const label = String(descriptor.label ?? '').trim();
344
+ const value = normalizeContent(descriptor.value);
345
+ if (!label || emptyContent(value)) return [];
346
+ return [{
347
+ label,
348
+ value,
349
+ icon: descriptor.icon == null ? null : String(descriptor.icon),
350
+ showLabel: descriptor.showLabel !== false
351
+ }];
352
+ });
353
+ }
354
+
355
+ /**
356
+ * Normalizes delegated actions while preserving caller-supplied Elements.
357
+ * @param {unknown} values Candidate actions.
358
+ * @returns {EntityRefActionEntry[]}
359
+ */
360
+ export function normalizeEntityRefActions(values) {
361
+ if (!Array.isArray(values)) return [];
362
+ const ids = new Set();
363
+ /** @type {EntityRefActionEntry[]} */
364
+ const normalized = [];
365
+ for (const entry of values) {
366
+ if (isElement(entry)) {
367
+ normalized.push(entry);
368
+ continue;
369
+ }
370
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
371
+ const action = /** @type {EntityRefAction} */ (entry);
372
+ const id = String(action.id ?? '').trim();
373
+ const label = String(action.label ?? '');
374
+ const title = String(action.title ?? label).trim();
375
+ if (!id || !title || ids.has(id)) continue;
376
+ if (action.href != null && safeHref(action.href) === null) continue;
377
+ if (action.onselect !== undefined && typeof action.onselect !== 'function') {
378
+ throw new TypeError(`EntityRef action ${id} onselect must be a function`);
379
+ }
380
+ ids.add(id);
381
+ const href = action.href == null ? undefined : /** @type {string} */ (safeHref(action.href));
382
+ const target = action.target == null ? undefined : String(action.target);
383
+ const rel = safeRel(action.rel, target);
384
+ normalized.push({
385
+ ...action,
386
+ id,
387
+ label,
388
+ title,
389
+ icon: action.icon == null ? null : String(action.icon),
390
+ kind: normalizeKind(action.kind),
391
+ disabled: Boolean(action.disabled),
392
+ ...(href ? { href } : {}),
393
+ ...(target ? { target } : {}),
394
+ ...(rel ? { rel } : {})
395
+ });
396
+ }
397
+ return normalized;
398
+ }
399
+
400
+ /** @param {EntityRefAction} action @param {number} index @returns {HTMLElement} */
401
+ function renderAction(action, index) {
402
+ const children = [
403
+ action.icon ? createIcon(action.icon, { size: 14 }) : null,
404
+ action.label ? h('span', { class: 'zx-entity-ref__action-label' }, action.label) : null
405
+ ];
406
+ const common = {
407
+ class: 'zx-entity-ref__action',
408
+ title: action.title,
409
+ ariaLabel: action.label || action.title,
410
+ dataset: { entityAction: String(index), kind: action.kind }
411
+ };
412
+ if (action.href && !action.disabled) {
413
+ return h('a', {
414
+ ...common,
415
+ href: action.href,
416
+ target: action.target,
417
+ rel: action.rel
418
+ }, children);
419
+ }
420
+ return h('button', { ...common, type: 'button', disabled: Boolean(action.disabled) }, children);
421
+ }
422
+
423
+ /** @param {EntityRefIcon} value @returns {EntityRefIcon} */
424
+ function normalizeIcon(value) {
425
+ if (value == null || typeof value === 'string' || typeof value === 'function') return value ?? null;
426
+ if (isNode(value) || value instanceof Component) return value;
427
+ return null;
428
+ }
429
+
430
+ /** @param {EntityRefIcon} value @returns {Node|string|null} */
431
+ function resolveEntityIcon(value) {
432
+ const candidate = typeof value === 'function' ? value() : value;
433
+ if (typeof candidate === 'string') return createIcon(candidate, { size: 18 });
434
+ if (candidate instanceof Component) return candidate.toElement();
435
+ return isNode(candidate) ? candidate : null;
436
+ }
437
+
438
+ /** @param {unknown} value @returns {EntityRefContent} */
439
+ function normalizeContent(value) {
440
+ if (value == null) return null;
441
+ if (typeof value === 'string' || typeof value === 'number') return value;
442
+ if (isNode(value) || value instanceof Component) return value;
443
+ return String(value);
444
+ }
445
+
446
+ /** @param {Element} host @param {EntityRefContent} content @returns {void} */
447
+ function replaceContent(host, content) {
448
+ host.replaceChildren(contentNode(content));
449
+ }
450
+
451
+ /** @param {EntityRefContent} content @returns {Node|string} */
452
+ function contentNode(content) {
453
+ if (content instanceof Component) return content.toElement();
454
+ return isNode(content) ? content : content == null ? '' : String(content);
455
+ }
456
+
457
+ /** @param {unknown} content @returns {boolean} */
458
+ function emptyContent(content) {
459
+ return content == null || (!isNode(content) && !(content instanceof Component) && String(content) === '');
460
+ }
461
+
462
+ /** @param {unknown} value @returns {value is Node} */
463
+ function isNode(value) {
464
+ return Boolean(value && typeof value === 'object'
465
+ && typeof /** @type {{nodeType?:unknown}} */ (value).nodeType === 'number');
466
+ }
467
+
468
+ /** @param {unknown} value @returns {'sm'|'md'|'lg'} */
469
+ function normalizeSize(value) {
470
+ return value === 'sm' || value === 'lg' ? value : 'md';
471
+ }
472
+
473
+ /** @param {unknown} value @returns {'default'|'primary'|'danger'|'ghost'} */
474
+ function normalizeKind(value) {
475
+ return value === 'default' || value === 'primary' || value === 'danger' ? value : 'ghost';
476
+ }
477
+
478
+ /** @param {unknown} value @param {string|undefined} target @returns {string|undefined} */
479
+ function safeRel(value, target) {
480
+ const tokens = String(value ?? '').split(/\s+/).filter((token) => /^[a-z-]+$/i.test(token));
481
+ if (target === '_blank' && !tokens.includes('noopener')) tokens.push('noopener');
482
+ return tokens.length ? [...new Set(tokens)].join(' ') : undefined;
483
+ }
484
+
485
+ /** @param {EntityRefState} state @returns {Readonly<EntityRefState>} */
486
+ function entitySnapshot(state) {
487
+ return Object.freeze({
488
+ ...state,
489
+ metadata: state.metadata.map((entry) => ({ ...entry })),
490
+ link: state.link ? { ...state.link } : null,
491
+ actions: state.actions.map((entry) => isElement(entry) ? entry : { ...entry })
492
+ });
493
+ }
494
+
495
+ /** Fired when the native primary link is activated. @event EntityRef#activate @type {CustomEvent<{id: unknown, entity: Readonly<EntityRefState>, href: string, event: MouseEvent}>} */
496
+ /** Fired when a delegated secondary action is activated. @event EntityRef#action @type {CustomEvent<{id: string, action: Readonly<EntityRefAction>, entity: Readonly<EntityRefState>, event: MouseEvent}>} */