@zeyos/zx 4.3.4 → 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 (128) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +7 -0
  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.d.ts +19 -0
  8. package/dist/types/components/card/card.d.ts +7 -0
  9. package/dist/types/components/card-view/card-view.d.ts +11 -3
  10. package/dist/types/components/card-view/record-card.d.ts +7 -1
  11. package/dist/types/components/code-editor/code-editor.d.ts +160 -0
  12. package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
  13. package/dist/types/components/context-menu/context-menu.d.ts +18 -2
  14. package/dist/types/components/date-picker/date-picker.d.ts +8 -0
  15. package/dist/types/components/date-picker/time-picker.d.ts +7 -0
  16. package/dist/types/components/dialog/dialog.d.ts +29 -0
  17. package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
  18. package/dist/types/components/file-item/file-item.d.ts +234 -0
  19. package/dist/types/components/file-list/file-list.d.ts +171 -0
  20. package/dist/types/components/filter/filter.d.ts +91 -36
  21. package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
  22. package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
  23. package/dist/types/components/kanban-view/kanban-view.d.ts +170 -4
  24. package/dist/types/components/launcher/launcher.d.ts +19 -12
  25. package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
  26. package/dist/types/components/menu-button/menu-button.d.ts +4 -37
  27. package/dist/types/components/message/message.d.ts +7 -0
  28. package/dist/types/components/modal/modal.d.ts +48 -0
  29. package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
  30. package/dist/types/components/panel/panel.d.ts +7 -0
  31. package/dist/types/components/search/search.d.ts +7 -0
  32. package/dist/types/components/select/select.d.ts +4 -0
  33. package/dist/types/components/sort-control/sort-control.d.ts +105 -0
  34. package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
  35. package/dist/types/components/tabbox/tabbox.d.ts +7 -0
  36. package/dist/types/components/table/table.d.ts +144 -0
  37. package/dist/types/components/table-view/table-view.d.ts +11 -5
  38. package/dist/types/components/timebox/timebox.d.ts +7 -0
  39. package/dist/types/components/value-list/value-list.d.ts +8 -0
  40. package/dist/types/components/view/record-view.d.ts +96 -5
  41. package/dist/types/core/icons.d.ts +17 -0
  42. package/dist/types/index.d.ts +11 -0
  43. package/dist/types/internal/app-rail.d.ts +10 -0
  44. package/dist/types/internal/menu-items.d.ts +134 -0
  45. package/dist/zx-zeyos.esm.js +616 -77
  46. package/dist/zx-zeyos.esm.min.js +8 -1
  47. package/dist/zx.css +2085 -238
  48. package/dist/zx.esm.js +6704 -1542
  49. package/dist/zx.esm.min.js +10 -3
  50. package/dist/zx.global.js +6704 -1542
  51. package/dist/zx.global.min.js +10 -3
  52. package/dist/zx.min.css +1 -1
  53. package/docs/api.json +1479 -127
  54. package/docs/llms.md +315 -10
  55. package/docs/llms.txt +11 -5
  56. package/package.json +1 -1
  57. package/src/components/activity-item/activity-item.css +137 -0
  58. package/src/components/activity-item/activity-item.js +599 -0
  59. package/src/components/activity-list/activity-list.css +64 -0
  60. package/src/components/activity-list/activity-list.js +375 -0
  61. package/src/components/app-rail/app-rail.css +227 -19
  62. package/src/components/app-sidebar/app-sidebar.css +414 -24
  63. package/src/components/app-sidebar/app-sidebar.js +65 -5
  64. package/src/components/button/button.css +8 -0
  65. package/src/components/button/button.js +6 -1
  66. package/src/components/calendar/calendar.js +33 -3
  67. package/src/components/card/card.js +12 -1
  68. package/src/components/card-view/card-view.css +1 -0
  69. package/src/components/card-view/card-view.js +25 -9
  70. package/src/components/card-view/record-card.js +7 -3
  71. package/src/components/code-editor/code-editor.css +67 -0
  72. package/src/components/code-editor/code-editor.js +288 -0
  73. package/src/components/code-editor/field-adapter.js +34 -0
  74. package/src/components/context-menu/context-menu.css +80 -3
  75. package/src/components/context-menu/context-menu.js +22 -30
  76. package/src/components/date-picker/date-picker.js +22 -8
  77. package/src/components/date-picker/month-picker.js +3 -3
  78. package/src/components/date-picker/time-picker.js +20 -8
  79. package/src/components/dialog/dialog.js +88 -66
  80. package/src/components/dropdown/dropdown.js +2 -2
  81. package/src/components/entity-ref/entity-ref.css +182 -0
  82. package/src/components/entity-ref/entity-ref.js +496 -0
  83. package/src/components/file-item/file-item.css +88 -0
  84. package/src/components/file-item/file-item.js +371 -0
  85. package/src/components/file-list/file-list.css +51 -0
  86. package/src/components/file-list/file-list.js +320 -0
  87. package/src/components/filter/filter.css +27 -1
  88. package/src/components/filter/filter.js +199 -45
  89. package/src/components/filter-panel/filter-panel.css +104 -0
  90. package/src/components/filter-panel/filter-panel.js +964 -0
  91. package/src/components/kanban-view/kanban-card.js +239 -0
  92. package/src/components/kanban-view/kanban-view.css +194 -0
  93. package/src/components/kanban-view/kanban-view.js +251 -8
  94. package/src/components/launcher/launcher.js +59 -27
  95. package/src/components/list-toolbar/list-toolbar.css +189 -0
  96. package/src/components/list-toolbar/list-toolbar.js +851 -0
  97. package/src/components/menu-button/menu-button.css +80 -3
  98. package/src/components/menu-button/menu-button.js +21 -37
  99. package/src/components/message/message.css +5 -2
  100. package/src/components/message/message.js +13 -2
  101. package/src/components/modal/modal.css +15 -0
  102. package/src/components/modal/modal.js +141 -0
  103. package/src/components/navigation-bar/navigation-bar.css +77 -2
  104. package/src/components/navigation-bar/navigation-bar.js +171 -4
  105. package/src/components/panel/panel.js +13 -2
  106. package/src/components/search/search.js +17 -5
  107. package/src/components/select/field-adapter.js +4 -1
  108. package/src/components/select/select.js +3 -0
  109. package/src/components/sort-control/sort-control.css +17 -0
  110. package/src/components/sort-control/sort-control.js +140 -0
  111. package/src/components/stat-tile/stat-tile.css +213 -0
  112. package/src/components/stat-tile/stat-tile.js +590 -0
  113. package/src/components/tabbox/tabbox.js +12 -1
  114. package/src/components/table/table.css +51 -2
  115. package/src/components/table/table.js +298 -19
  116. package/src/components/table-view/table-view.css +8 -2
  117. package/src/components/table-view/table-view.js +21 -6
  118. package/src/components/tag-picker/tag-picker.css +3 -1
  119. package/src/components/timebox/timebox.js +18 -5
  120. package/src/components/value-list/value-list.js +22 -6
  121. package/src/components/view/record-view.css +5 -0
  122. package/src/components/view/record-view.js +100 -7
  123. package/src/core/icons.js +74 -5
  124. package/src/index.js +25 -0
  125. package/src/internal/app-rail.js +19 -1
  126. package/src/internal/menu-items.js +142 -0
  127. package/styles/print.css +192 -0
  128. package/styles/zx.css +11 -0
@@ -0,0 +1,239 @@
1
+ // @ts-check
2
+ import { h, safeHref } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+ import { avatarInitials } from '../avatar/avatar.js';
5
+ import { badge } from '../badge/badge.js';
6
+
7
+ /** @typedef {Record<string, any>} KanbanRecord */
8
+
9
+ /**
10
+ * @typedef {Object} KanbanIndicator
11
+ * @property {string} label Visible text.
12
+ * @property {'neutral'|'accent'|'success'|'warning'|'danger'|'info'} [tone='neutral'] Semantic intent.
13
+ * @property {string|null} [icon=null] Optional leading icon name, used instead of the status dot.
14
+ * @property {boolean} [dot=true] Whether a leading status dot is drawn.
15
+ * @property {string} [title] Native title, useful when the label alone is terse.
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} KanbanProgress
20
+ * @property {number} value Completed amount, clamped into `[0, max]`.
21
+ * @property {number} max Amount that counts as finished.
22
+ * @property {number} percent Rounded completion percentage.
23
+ * @property {string|null} label Visible meter label, or null for the caller's default.
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} KanbanAssignee
28
+ * @property {string} name Person's name, used as the accessible label.
29
+ * @property {string|null} src Safe image URL, or null for the initials fallback.
30
+ * @property {string} initials Fallback initials.
31
+ */
32
+
33
+ /** Semantic intents an indicator may carry, in the order they are documented. */
34
+ const TONES = Object.freeze(['neutral', 'accent', 'success', 'warning', 'danger', 'info']);
35
+
36
+ /**
37
+ * Normalizes one indicator value into the descriptor the badge is built from. A plain value
38
+ * becomes the label and looks its tone up in the supplied map, so an application expresses
39
+ * "High means danger" as data rather than as a renderer.
40
+ * @param {unknown} value Raw field value, descriptor, or null.
41
+ * @param {Record<string, string>} [tones={}] Value-to-tone map.
42
+ * @returns {KanbanIndicator|null} Descriptor, or null when there is nothing to show.
43
+ */
44
+ export function resolveKanbanIndicator(value, tones = {}) {
45
+ if (value == null || value === '') return null;
46
+ const map = tones && typeof tones === 'object' ? tones : {};
47
+ const source = typeof value === 'object' && !('nodeType' in value)
48
+ ? /** @type {Record<string, any>} */ (value) : { label: value };
49
+ const label = source.label == null ? '' : String(source.label);
50
+ if (!label) return null;
51
+ const mapped = map[label];
52
+ const tone = TONES.includes(source.tone) ? source.tone
53
+ : TONES.includes(mapped) ? mapped : 'neutral';
54
+ return {
55
+ label,
56
+ tone: /** @type {KanbanIndicator['tone']} */ (tone),
57
+ icon: source.icon == null ? null : String(source.icon),
58
+ dot: source.dot !== false && source.icon == null,
59
+ ...(source.title == null ? {} : { title: String(source.title) })
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Builds one indicator badge.
65
+ * @param {KanbanIndicator} indicator Normalized descriptor.
66
+ * @returns {HTMLElement} Badge element.
67
+ */
68
+ export function createKanbanIndicator(indicator) {
69
+ return badge({
70
+ label: indicator.label,
71
+ kind: indicator.tone,
72
+ icon: indicator.icon ?? undefined,
73
+ dot: indicator.dot,
74
+ size: 'sm',
75
+ variant: 'soft',
76
+ ...(indicator.title == null ? {} : { title: indicator.title })
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Normalizes a completion value against an explicit scale. The scale is never guessed: a `0.72`
82
+ * on a 0–100 board is seven-tenths of a percent, and only the application knows which it meant.
83
+ * @param {unknown} value Raw field value, or `{value, max, label}`.
84
+ * @param {number} [max=100] Amount that counts as finished.
85
+ * @returns {KanbanProgress|null} Descriptor, or null when there is nothing to show.
86
+ */
87
+ export function resolveKanbanProgress(value, max = 100) {
88
+ if (value == null || value === '') return null;
89
+ const source = typeof value === 'object' && !('nodeType' in value)
90
+ ? /** @type {Record<string, any>} */ (value) : { value };
91
+ const limit = Number(source.max ?? max);
92
+ const amount = Number(source.value);
93
+ if (!Number.isFinite(amount) || !Number.isFinite(limit) || limit <= 0) return null;
94
+ const clamped = Math.max(0, Math.min(limit, amount));
95
+ // A blank label must not win over the localized default: it would become aria-label="".
96
+ const label = source.label == null ? '' : String(source.label).trim();
97
+ return {
98
+ value: clamped,
99
+ max: limit,
100
+ percent: Math.round(clamped / limit * 100),
101
+ label: label || null
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Builds the card's completion meter. The track is the accessible progress element itself, so the
107
+ * value is available without depending on the visible percentage text.
108
+ * @param {KanbanProgress} progress Normalized descriptor.
109
+ * @param {string} label Meter label.
110
+ * @param {string} valueText Formatted readout drawn opposite the label.
111
+ * @returns {HTMLElement} Meter element.
112
+ */
113
+ export function createKanbanProgress(progress, label, valueText) {
114
+ const track = h('div', {
115
+ class: 'zx-kanban-view__progress-track',
116
+ role: 'progressbar',
117
+ ariaValuemin: '0',
118
+ ariaValuemax: String(progress.max),
119
+ ariaValuenow: String(progress.value),
120
+ ariaValuetext: valueText,
121
+ ariaLabel: label
122
+ }, h('div', { class: 'zx-kanban-view__progress-fill' }));
123
+ // The fill is the only inline style on a card: a percentage cannot be expressed as a token.
124
+ /** @type {HTMLElement} */ (track.firstElementChild).style.inlineSize = `${progress.percent}%`;
125
+ return h('div', { class: 'zx-kanban-view__progress' },
126
+ h('div', { class: 'zx-kanban-view__progress-head' },
127
+ h('span', { class: 'zx-kanban-view__progress-label' }, label),
128
+ h('span', { class: 'zx-kanban-view__progress-value' }, valueText)),
129
+ track);
130
+ }
131
+
132
+ /**
133
+ * Normalizes one or more responsible people. Names, name lists, and descriptor objects all
134
+ * resolve to the same shape, and an unsafe image URL degrades to initials rather than failing.
135
+ * @param {unknown} value Raw field value.
136
+ * @returns {KanbanAssignee[]} Normalized people, in the order supplied.
137
+ */
138
+ export function resolveKanbanAssignees(value) {
139
+ if (value == null || value === '') return [];
140
+ const entries = Array.isArray(value) ? value : [value];
141
+ return /** @type {KanbanAssignee[]} */ (entries.map((entry) => {
142
+ if (entry == null || entry === '') return null;
143
+ const source = typeof entry === 'object' && !('nodeType' in entry)
144
+ ? /** @type {Record<string, any>} */ (entry) : { name: entry };
145
+ const name = String(source.name ?? source.label ?? '').trim();
146
+ // An avatar with no name cannot be announced: it would render as an unidentified face inside a
147
+ // group labelled "Assigned to ". A picture without a person is worse than no picture.
148
+ if (!name) return null;
149
+ const src = safeHref(source.src ?? source.image ?? null);
150
+ const initials = source.initials == null ? avatarInitials(name) : String(source.initials);
151
+ return { name, src, initials };
152
+ }).filter(Boolean));
153
+ }
154
+
155
+ /**
156
+ * Splits a list of people into the faces that are drawn and the number the overflow chip stands
157
+ * for. Exists as one function so the slice and the chip can never be computed from different
158
+ * normalizations of the same option — a fractional limit used to floor every face away while the
159
+ * chip still counted from the raw value.
160
+ * @param {number} max Requested face count.
161
+ * @param {number} total People available.
162
+ * @returns {{shown:number,hidden:number}} Normalized split.
163
+ */
164
+ export function kanbanAvatarLimit(max, total) {
165
+ // Exported, so a malformed total is normalized here rather than trusted from the caller.
166
+ const people = Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;
167
+ const limit = Number.isFinite(max) ? Math.max(1, Math.floor(max)) : people;
168
+ const shown = Math.min(people, limit);
169
+ return { shown, hidden: Math.max(0, people - shown) };
170
+ }
171
+
172
+ /**
173
+ * Builds the assignee avatar group. The group carries one accessible name listing everyone,
174
+ * including the people behind the overflow chip, and the chips themselves are hidden so a screen
175
+ * reader announces the list once rather than once per face.
176
+ * @param {KanbanAssignee[]} people Normalized people.
177
+ * @param {number} max Faces drawn before the overflow chip.
178
+ * @param {string} groupLabel Accessible name for the whole group.
179
+ * @param {(count: number) => string} overflowLabel Text for the overflow chip, given how many
180
+ * people it stands for. The count is derived here so it can never disagree with the slice.
181
+ * @returns {HTMLElement|null} Group element, or null when nobody is assigned.
182
+ */
183
+ export function createKanbanAvatars(people, max, groupLabel, overflowLabel) {
184
+ if (!people.length) return null;
185
+ const split = kanbanAvatarLimit(max, people.length);
186
+ const shown = people.slice(0, split.shown);
187
+ const hidden = split.hidden;
188
+ const faces = shown.map((person) => h('span', {
189
+ class: 'zx-kanban-view__avatar', ariaHidden: 'true', title: person.name || undefined
190
+ }, person.src
191
+ ? h('img', { class: 'zx-kanban-view__avatar-image', src: person.src, alt: '', draggable: false })
192
+ : h('span', { class: 'zx-kanban-view__avatar-initials' }, person.initials)));
193
+ if (hidden > 0) {
194
+ faces.push(h('span', {
195
+ class: 'zx-kanban-view__avatar', ariaHidden: 'true', dataset: { overflow: 'true' }
196
+ }, h('span', { class: 'zx-kanban-view__avatar-initials' }, overflowLabel(hidden))));
197
+ }
198
+ return h('span', { class: 'zx-kanban-view__avatars', role: 'img', ariaLabel: groupLabel }, faces);
199
+ }
200
+
201
+ /**
202
+ * Builds the card's head row: what the record is, and how urgent it is.
203
+ * @param {Object} parts Row content.
204
+ * @param {string|null} parts.entityIcon Icon name, or null.
205
+ * @param {string} parts.identifier Record key, or an empty string.
206
+ * @param {{indicator:Node|KanbanIndicator|null,prefix:string}[]} parts.flags Trailing indicators,
207
+ * each with the visually hidden text that names what it measures.
208
+ * @param {string} parts.identifierPrefix Visually hidden text read before the record key.
209
+ * @returns {HTMLElement|null} Head row, or null when it would be empty.
210
+ */
211
+ export function createKanbanCardHead(parts) {
212
+ const lead = [];
213
+ if (parts.entityIcon) {
214
+ lead.push(h('span', { class: 'zx-kanban-view__entity', ariaHidden: 'true' },
215
+ icon(parts.entityIcon, { size: 12 })));
216
+ }
217
+ if (parts.identifier) {
218
+ // `aria-label` is prohibited on the generic role, so the prefix is real, clipped text.
219
+ lead.push(h('span', { class: 'zx-kanban-view__identifier' },
220
+ parts.identifierPrefix
221
+ ? h('span', { class: 'zx-kanban-view__hint' }, `${parts.identifierPrefix} `) : null,
222
+ parts.identifier));
223
+ }
224
+ const trailing = (parts.flags ?? []).map((flag) => {
225
+ if (flag.indicator == null) return null;
226
+ const node = typeof flag.indicator === 'object' && 'nodeType' in flag.indicator
227
+ ? /** @type {Node} */ (flag.indicator)
228
+ : createKanbanIndicator(/** @type {KanbanIndicator} */ (flag.indicator));
229
+ // "High" alone does not say high what; the prefix names the axis without adding visible noise.
230
+ return flag.prefix
231
+ ? h('span', { class: 'zx-kanban-view__flag' },
232
+ h('span', { class: 'zx-kanban-view__hint' }, `${flag.prefix} `), node)
233
+ : node;
234
+ }).filter(Boolean);
235
+ if (!lead.length && !trailing.length) return null;
236
+ return h('div', { class: 'zx-kanban-view__card-head' },
237
+ lead.length ? h('span', { class: 'zx-kanban-view__card-lead' }, lead) : null,
238
+ trailing.length ? h('span', { class: 'zx-kanban-view__card-flags' }, trailing) : null);
239
+ }
@@ -95,6 +95,12 @@
95
95
  border: 0;
96
96
  border-radius: var(--zx-radius-sm);
97
97
  background: transparent;
98
+ /* Both are icon-only squares with an explicit size. base.css styles native buttons through
99
+ zero-specificity `:where()`, which loses the cascade but still floors the box through
100
+ *different* properties — `padding-inline` and `min-block-size`. Without resetting those, these
101
+ buttons are neither square nor able to shrink when the keyboard handle clips itself away. */
102
+ padding: 0;
103
+ min-block-size: 0;
98
104
  color: var(--zx-color-text-muted);
99
105
  cursor: pointer;
100
106
  }
@@ -565,10 +571,13 @@
565
571
 
566
572
  /* ── Empty column placeholder ─────────────────────────────────────────────────────────────── */
567
573
 
574
+ /* An empty column reads as somewhere a card can go, not as a rendering gap. */
568
575
  .zx-kanban-view__column-empty {
569
576
  display: grid;
570
577
  place-items: center;
571
578
  min-block-size: 4rem;
579
+ border: 1px dashed var(--zx-color-border);
580
+ border-radius: var(--zx-radius-md);
572
581
  padding: var(--zx-space-2);
573
582
  color: var(--zx-color-text-muted);
574
583
  font-size: var(--zx-text-xs);
@@ -658,3 +667,188 @@
658
667
  min-block-size: 2.75rem;
659
668
  }
660
669
  }
670
+
671
+ /* ── Card head: what the record is, and how urgent it is ──────────────────────────────────── */
672
+
673
+ .zx-kanban-view__card-head {
674
+ display: flex;
675
+ align-items: center;
676
+ gap: var(--zx-space-2);
677
+ min-block-size: 1.25rem;
678
+ min-inline-size: 0;
679
+ }
680
+
681
+ .zx-kanban-view__card-lead,
682
+ .zx-kanban-view__card-flags {
683
+ display: inline-flex;
684
+ align-items: center;
685
+ gap: var(--zx-space-1);
686
+ min-inline-size: 0;
687
+ }
688
+
689
+ .zx-kanban-view__card-flags {
690
+ flex: none;
691
+ margin-inline-start: auto;
692
+ }
693
+
694
+ .zx-kanban-view__entity {
695
+ display: inline-grid;
696
+ flex: none;
697
+ place-items: center;
698
+ color: var(--zx-color-text-muted);
699
+ }
700
+
701
+ .zx-kanban-view__identifier {
702
+ min-inline-size: 0;
703
+ overflow: hidden;
704
+ color: var(--zx-color-text-muted);
705
+ font-size: var(--zx-text-xs);
706
+ font-variant-numeric: tabular-nums;
707
+ letter-spacing: .02em;
708
+ text-overflow: ellipsis;
709
+ white-space: nowrap;
710
+ }
711
+
712
+ /* ── Card footer: completion, then the people responsible ─────────────────────────────────── */
713
+
714
+ .zx-kanban-view__card-footer {
715
+ display: flex;
716
+ flex-direction: column;
717
+ gap: var(--zx-space-2);
718
+ min-inline-size: 0;
719
+ }
720
+
721
+ .zx-kanban-view__progress-head {
722
+ display: flex;
723
+ align-items: baseline;
724
+ justify-content: space-between;
725
+ gap: var(--zx-space-2);
726
+ margin-block-end: calc(var(--zx-space-1) / 2);
727
+ }
728
+
729
+ .zx-kanban-view__progress-label,
730
+ .zx-kanban-view__progress-value {
731
+ color: var(--zx-color-text-muted);
732
+ font-size: var(--zx-text-xs);
733
+ line-height: 1.2;
734
+ white-space: nowrap;
735
+ }
736
+
737
+ .zx-kanban-view__progress-label {
738
+ min-inline-size: 0;
739
+ overflow: hidden;
740
+ text-overflow: ellipsis;
741
+ }
742
+
743
+ .zx-kanban-view__progress-value {
744
+ flex: none;
745
+ font-variant-numeric: tabular-nums;
746
+ }
747
+
748
+ .zx-kanban-view__progress-track {
749
+ block-size: 4px;
750
+ overflow: hidden;
751
+ border-radius: var(--zx-radius-full);
752
+ background: var(--zx-color-bg-muted);
753
+ }
754
+
755
+ .zx-kanban-view__progress-fill {
756
+ block-size: 100%;
757
+ border-radius: inherit;
758
+ background: var(--zx-color-accent);
759
+ }
760
+
761
+ /* ── Assignees ────────────────────────────────────────────────────────────────────────────── */
762
+
763
+ /* Faces overlap into a stack; each one lifts its own left edge clear of the previous. */
764
+ .zx-kanban-view__avatars {
765
+ display: inline-flex;
766
+ align-items: center;
767
+ min-inline-size: 0;
768
+ }
769
+
770
+ .zx-kanban-view__avatar {
771
+ display: inline-grid;
772
+ flex: none;
773
+ place-items: center;
774
+ inline-size: 1.5rem;
775
+ block-size: 1.5rem;
776
+ overflow: hidden;
777
+ border: 1px solid var(--zx-color-bg-surface);
778
+ border-radius: var(--zx-radius-full);
779
+ background: var(--zx-color-bg-muted);
780
+ color: var(--zx-color-text-muted);
781
+ }
782
+
783
+ .zx-kanban-view__avatar + .zx-kanban-view__avatar {
784
+ margin-inline-start: calc(var(--zx-space-2) * -1);
785
+ }
786
+
787
+ .zx-kanban-view__avatar[data-overflow="true"] {
788
+ background: var(--zx-color-accent-subtle);
789
+ color: var(--zx-color-accent);
790
+ }
791
+
792
+ .zx-kanban-view__avatar-image {
793
+ inline-size: 100%;
794
+ block-size: 100%;
795
+ object-fit: cover;
796
+ }
797
+
798
+ .zx-kanban-view__avatar-initials {
799
+ font-size: var(--zx-text-xs);
800
+ font-weight: 600;
801
+ line-height: 1;
802
+ text-transform: uppercase;
803
+ }
804
+
805
+ /* ── Move handle ──────────────────────────────────────────────────────────────────────────── */
806
+
807
+ /* The card is the pointer drag source, so the handle is keyboard furniture: announced and in tab
808
+ order, but out of sight and out of the layout until it takes focus or holds a grabbed card. */
809
+ .zx-kanban-view__move-handle[data-reveal="keyboard"] {
810
+ position: absolute;
811
+ /* base.css gives every native button a control-height floor; without resetting it the "hidden"
812
+ handle is still a full-height strip. */
813
+ inline-size: 1px;
814
+ block-size: 1px;
815
+ min-inline-size: 0;
816
+ min-block-size: 0;
817
+ overflow: hidden;
818
+ clip-path: inset(50%);
819
+ }
820
+
821
+ .zx-kanban-view__move-handle[data-reveal="keyboard"]:focus,
822
+ .zx-kanban-view__move-handle[data-reveal="keyboard"][aria-pressed="true"] {
823
+ position: static;
824
+ inline-size: calc(var(--zx-control-height) - var(--zx-space-1));
825
+ block-size: calc(var(--zx-control-height) - var(--zx-space-1));
826
+ min-block-size: 0;
827
+ overflow: visible;
828
+ clip-path: none;
829
+ }
830
+
831
+ @media (pointer: coarse) {
832
+ .zx-kanban-view__move-handle[data-reveal="keyboard"]:focus,
833
+ .zx-kanban-view__move-handle[data-reveal="keyboard"][aria-pressed="true"] {
834
+ inline-size: 2.75rem;
835
+ block-size: 2.75rem;
836
+ }
837
+ }
838
+
839
+ /* Text that names what a terse card value measures — "Priority" before "High" — carried as real
840
+ content because `aria-label` is prohibited on the generic role a span reports. */
841
+ .zx-kanban-view__hint {
842
+ position: absolute;
843
+ inline-size: 1px;
844
+ block-size: 1px;
845
+ overflow: hidden;
846
+ clip-path: inset(50%);
847
+ white-space: nowrap;
848
+ }
849
+
850
+ .zx-kanban-view__flag {
851
+ display: inline-flex;
852
+ align-items: center;
853
+ }
854
+ .zx-kanban-view .zx-record-card[data-selection-trigger="card"] { cursor: pointer; }