@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
|
@@ -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}>} */
|