@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.
- package/CHANGELOG.md +155 -0
- package/README.md +7 -0
- 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.d.ts +19 -0
- package/dist/types/components/card/card.d.ts +7 -0
- package/dist/types/components/card-view/card-view.d.ts +11 -3
- package/dist/types/components/card-view/record-card.d.ts +7 -1
- 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-view.d.ts +170 -4
- 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/icons.d.ts +17 -0
- package/dist/types/index.d.ts +11 -0
- 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 +616 -77
- package/dist/zx-zeyos.esm.min.js +8 -1
- package/dist/zx.css +2085 -238
- package/dist/zx.esm.js +6704 -1542
- package/dist/zx.esm.min.js +10 -3
- package/dist/zx.global.js +6704 -1542
- package/dist/zx.global.min.js +10 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1479 -127
- package/docs/llms.md +315 -10
- package/docs/llms.txt +11 -5
- package/package.json +1 -1
- 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.js +33 -3
- package/src/components/card/card.js +12 -1
- package/src/components/card-view/card-view.css +1 -0
- package/src/components/card-view/card-view.js +25 -9
- package/src/components/card-view/record-card.js +7 -3
- 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-view.css +194 -0
- package/src/components/kanban-view/kanban-view.js +251 -8
- 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 +5 -2
- 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/icons.js +74 -5
- package/src/index.js +25 -0
- 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
|
@@ -6,7 +6,11 @@ import { uid } from '../../core/util.js';
|
|
|
6
6
|
import { badge } from '../badge/badge.js';
|
|
7
7
|
import { createRecordCard, resolveRecordActions } from '../card-view/record-card.js';
|
|
8
8
|
import { ContextMenu } from '../context-menu/context-menu.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createKanbanAvatars, createKanbanCardHead, createKanbanProgress, resolveKanbanAssignees,
|
|
11
|
+
resolveKanbanIndicator, resolveKanbanProgress
|
|
12
|
+
} from './kanban-card.js';
|
|
13
|
+
import { RecordView, normalizeViewFieldControls, readViewField, viewRecordId } from '../view/record-view.js';
|
|
10
14
|
import {
|
|
11
15
|
absoluteKanbanIndex, allowsKanbanTransition, clampKanbanIndex, createKanbanHistory,
|
|
12
16
|
evaluateKanbanMove, kanbanSearchTerms, locateKanbanRecord, matchesKanbanSearch, normalizeAxisId,
|
|
@@ -20,6 +24,12 @@ import {
|
|
|
20
24
|
/** @typedef {import('../card-view/record-card.js').RecordCardAction} RecordCardAction */
|
|
21
25
|
/** @typedef {import('../menu-button/menu-button.js').MenuItem} MenuItem */
|
|
22
26
|
/** @typedef {import('./kanban-policy.js').KanbanRule} KanbanRule */
|
|
27
|
+
/** @typedef {import('./kanban-card.js').KanbanIndicator} KanbanIndicator */
|
|
28
|
+
/**
|
|
29
|
+
* A card indicator source: a field id, a reader, or a literal descriptor used for every card.
|
|
30
|
+
* @typedef {string|KanbanIndicator|((record:KanbanRecord,index:number)=>KanbanIndicator|Node|unknown)|null} KanbanIndicatorSource
|
|
31
|
+
*/
|
|
32
|
+
/** @typedef {import('./kanban-card.js').KanbanAssignee} KanbanAssignee */
|
|
23
33
|
/** @typedef {import('./kanban-policy.js').KanbanRejectReason} KanbanRejectReason */
|
|
24
34
|
/** @typedef {import('./kanban-policy.js').KanbanMoveEvaluation} KanbanMoveEvaluation */
|
|
25
35
|
/** @typedef {string|((record: KanbanRecord, index: number) => unknown)|null} KanbanAccessor */
|
|
@@ -116,10 +126,13 @@ import {
|
|
|
116
126
|
* @property {{id:string,dir:'asc'|'desc'}|null} [sort=null] Shared initial sort.
|
|
117
127
|
* @property {'local'|'server'} [sortMode='local'] Shared sort mode.
|
|
118
128
|
* @property {false|'single'|'multi'} [selectable=false] Shared selection mode.
|
|
129
|
+
* @property {'checkbox'|'card'} [selectionTrigger='checkbox'] Select through a checkbox or the non-interactive card surface.
|
|
119
130
|
* @property {unknown[]} [selection=[]] Initial record ids.
|
|
120
131
|
* @property {string[]} [fieldOrder=[]] Shared field order.
|
|
121
132
|
* @property {string[]} [hiddenFields=[]] Shared hidden fields.
|
|
122
|
-
* @property {boolean} [fieldControls=true]
|
|
133
|
+
* @property {boolean|import('../view/record-view.js').ViewFieldControlsConfig} [fieldControls=true]
|
|
134
|
+
* Show shared field controls. Redeclared narrower than `RecordViewOptions` it fails declaration
|
|
135
|
+
* emit, because the two option types are intersected.
|
|
123
136
|
* @property {string|Node|(() => string|Node)|null} [emptyText=null] Empty-board content.
|
|
124
137
|
* @property {KanbanAccessor} [columnBy='status'] Column accessor.
|
|
125
138
|
* @property {KanbanColumn[]|null} [columns=null] Explicit columns, or null to derive them.
|
|
@@ -134,6 +147,21 @@ import {
|
|
|
134
147
|
* @property {'local'|'external'} [moveMode='local'] Whether accepted moves update local data.
|
|
135
148
|
* @property {'card'|'handle'} [dragFrom='card'] Whether the whole card or only its handle starts a
|
|
136
149
|
* pointer drag. Touch drags from the card body require a long press so the board still scrolls.
|
|
150
|
+
* @property {'keyboard'|'always'} [moveHandle='keyboard'] Move-handle presentation. `keyboard`
|
|
151
|
+
* keeps it announced and tabbable but out of sight until it takes focus, because the card itself
|
|
152
|
+
* is already the pointer drag source; `always` shows it alongside the card's other controls.
|
|
153
|
+
* `dragFrom:'handle'` overrides this — a drag source a pointer user cannot see is not a control.
|
|
154
|
+
* @property {string|((record:KanbanRecord,index:number)=>string|null)|null} [entityIcon=null] Icon
|
|
155
|
+
* naming what the record is, drawn before the identifier.
|
|
156
|
+
* @property {KanbanCardField} [identifier=null] Stable record key shown on the card head.
|
|
157
|
+
* @property {KanbanIndicatorSource} [status=null] Status indicator source.
|
|
158
|
+
* @property {Record<string,string>} [statusTones={}] Maps a status value to a semantic tone.
|
|
159
|
+
* @property {KanbanIndicatorSource} [priority=null] Priority indicator source.
|
|
160
|
+
* @property {Record<string,string>} [priorityTones={}] Maps a priority value to a semantic tone.
|
|
161
|
+
* @property {KanbanCardField} [progress=null] Completion source; a number or `{value,max,label}`.
|
|
162
|
+
* @property {number} [progressMax=100] Amount that counts as finished.
|
|
163
|
+
* @property {KanbanCardField} [assignees=null] Responsible people; a name, a list, or descriptors.
|
|
164
|
+
* @property {number} [maxAvatars=3] Faces drawn before the overflow chip.
|
|
137
165
|
* @property {boolean} [multiMove=true] Move the whole selection when a selected card is picked up.
|
|
138
166
|
* @property {boolean} [autoScroll=true] Scroll the board and columns while dragging near an edge.
|
|
139
167
|
* @property {'warn'|'block'} [wipPolicy='warn'] Whether exceeding a limit warns or refuses.
|
|
@@ -151,7 +179,10 @@ import {
|
|
|
151
179
|
* Render a per-column add control that emits `recordadd`.
|
|
152
180
|
* @property {boolean|MenuItem[]|((context:KanbanCardContext)=>MenuItem[])} [contextMenu=false]
|
|
153
181
|
* Card context menu. `true` offers move commands; an array or function appends application items.
|
|
154
|
-
* @property {((context:KanbanCardContext)=>Node|null|undefined)|null} [renderCard=null] Replaces
|
|
182
|
+
* @property {((context:KanbanCardContext)=>Node|null|undefined)|null} [renderCard=null] Replaces the
|
|
183
|
+
* shared record-card content — eyebrow, titles, and metadata. The preview, selection control,
|
|
184
|
+
* action group, move handle, rule badges, and any configured card head and footer still render,
|
|
185
|
+
* because each of those answers to its own option; clear those options to remove them.
|
|
155
186
|
* @property {((context:KanbanHeaderContext)=>Node|null|undefined)|null} [renderColumnHeader=null] Replaces column header content.
|
|
156
187
|
* @property {((context:KanbanHeaderContext)=>Node|null|undefined)|null} [renderSwimlaneHeader=null] Replaces lane header content.
|
|
157
188
|
* @property {((context:KanbanHeaderContext)=>Node|null|undefined)|null} [renderColumnEmpty=null] Empty-column placeholder.
|
|
@@ -176,6 +207,18 @@ import {
|
|
|
176
207
|
/** @typedef {{record:KanbanRecord,id:unknown,index:number,column:string,lane:string|null}} KanbanCardMeta */
|
|
177
208
|
/** @typedef {{column:string,lane:string|null,list:HTMLElement}} KanbanSectionMeta */
|
|
178
209
|
/** @typedef {{ids:unknown[],from:KanbanMovePoint,to:KanbanMovePoint}} KanbanKeyboardMove */
|
|
210
|
+
/** @typedef {((record:KanbanRecord,index:number)=>unknown)|null} KanbanCardReader */
|
|
211
|
+
/**
|
|
212
|
+
* @typedef {Object} KanbanCardSources
|
|
213
|
+
* @property {KanbanCardReader} identifier Record-key reader.
|
|
214
|
+
* @property {KanbanCardReader} status Status reader.
|
|
215
|
+
* @property {KanbanCardReader} priority Priority reader.
|
|
216
|
+
* @property {KanbanCardReader} progress Completion reader.
|
|
217
|
+
* @property {KanbanCardReader} assignees Responsible-people reader.
|
|
218
|
+
* @property {KanbanCardReader} entityIcon Entity-icon reader.
|
|
219
|
+
* @property {Set<string>} fieldIds Field ids the anatomy consumes.
|
|
220
|
+
* @property {boolean} any Whether any source is configured.
|
|
221
|
+
*/
|
|
179
222
|
|
|
180
223
|
const INTERACTIVE = 'a, button, input, select, textarea, summary, [contenteditable="true"]';
|
|
181
224
|
|
|
@@ -217,7 +260,14 @@ const DEFAULT_LABELS = Object.freeze({
|
|
|
217
260
|
countLimit: '%count% records, work-in-progress limit %limit%',
|
|
218
261
|
countLimitInLane: '%count% records in this lane, %total% of work-in-progress limit %limit% in the column',
|
|
219
262
|
empty: 'No records',
|
|
220
|
-
columnEmpty: '',
|
|
263
|
+
columnEmpty: 'Drop a card here',
|
|
264
|
+
identifier: 'Record',
|
|
265
|
+
priorityLabel: 'Priority',
|
|
266
|
+
statusLabel: 'Status',
|
|
267
|
+
progress: 'Progress',
|
|
268
|
+
progressValue: '%percent%%',
|
|
269
|
+
assignees: 'Assigned to %names%',
|
|
270
|
+
assigneeOverflow: '+%count%',
|
|
221
271
|
selected: 'Selected',
|
|
222
272
|
notSelected: 'Not selected',
|
|
223
273
|
noMatches: 'No cards match the current search.',
|
|
@@ -247,6 +297,11 @@ const DEFAULT_LABELS = Object.freeze({
|
|
|
247
297
|
});
|
|
248
298
|
|
|
249
299
|
/**
|
|
300
|
+
* A field consumed by the card head or footer — the `identifier`, `status`, `priority`,
|
|
301
|
+
* `progress`, or `assignees` source — is dropped from the shared metadata list so the same value
|
|
302
|
+
* is never shown twice. `field.duplicate === true` opts it back in, extending the rule the shared
|
|
303
|
+
* record card already applies to title and subtitle fields.
|
|
304
|
+
*
|
|
250
305
|
* Configurable record board with semantic columns, optional swim lanes, and equivalent pointer,
|
|
251
306
|
* touch, keyboard, context-menu, and programmatic movement. Local moves clone records; callback
|
|
252
307
|
* grouping remains fully usable for display and external moves but cannot be written safely by
|
|
@@ -265,6 +320,7 @@ export class KanbanView extends RecordView {
|
|
|
265
320
|
/** @type {Readonly<KanbanViewOptions & RecordViewOptions>} */
|
|
266
321
|
static defaults = {
|
|
267
322
|
...RecordView.defaults,
|
|
323
|
+
selectionTrigger: 'checkbox',
|
|
268
324
|
columnBy: 'status',
|
|
269
325
|
columns: null,
|
|
270
326
|
swimlaneBy: null,
|
|
@@ -277,6 +333,17 @@ export class KanbanView extends RecordView {
|
|
|
277
333
|
actions: [],
|
|
278
334
|
moveMode: 'local',
|
|
279
335
|
dragFrom: 'card',
|
|
336
|
+
moveHandle: 'keyboard',
|
|
337
|
+
entityIcon: null,
|
|
338
|
+
identifier: null,
|
|
339
|
+
status: null,
|
|
340
|
+
statusTones: {},
|
|
341
|
+
priority: null,
|
|
342
|
+
priorityTones: {},
|
|
343
|
+
progress: null,
|
|
344
|
+
progressMax: 100,
|
|
345
|
+
assignees: null,
|
|
346
|
+
maxAvatars: 3,
|
|
280
347
|
multiMove: true,
|
|
281
348
|
autoScroll: true,
|
|
282
349
|
wipPolicy: 'warn',
|
|
@@ -354,13 +421,24 @@ export class KanbanView extends RecordView {
|
|
|
354
421
|
this._sectionMeta = new WeakMap();
|
|
355
422
|
this._cardsById = new Map();
|
|
356
423
|
this._visibleIds = new Set();
|
|
424
|
+
this._cardSources = /** @type {KanbanCardSources} */ ({
|
|
425
|
+
identifier: null, status: null, priority: null, progress: null, assignees: null,
|
|
426
|
+
entityIcon: null, fieldIds: new Set(), any: false
|
|
427
|
+
});
|
|
428
|
+
this._fieldIndex = /** @type {Map<string, any>|null} */ (null);
|
|
429
|
+
this._cardFields = /** @type {{all:any[],metadata:any[]}|null} */ (null);
|
|
357
430
|
this._kanbanMenu = /** @type {ContextMenu|null} */ (null);
|
|
358
431
|
this._searchInput = /** @type {HTMLInputElement|null} */ (null);
|
|
359
432
|
this._undoButton = /** @type {HTMLButtonElement|null} */ (null);
|
|
360
433
|
this._redoButton = /** @type {HTMLButtonElement|null} */ (null);
|
|
361
434
|
|
|
362
435
|
this._toolbar = h('div', { class: 'zx-record-view__toolbar zx-kanban-view__toolbar' });
|
|
363
|
-
|
|
436
|
+
// A label the host passed in the object form wins; otherwise Kanban's own
|
|
437
|
+
// message bag supplies it, which is where this label has always come from.
|
|
438
|
+
if (options.fieldControls) {
|
|
439
|
+
const configured = normalizeViewFieldControls(options.fieldControls).label;
|
|
440
|
+
this._toolbar.append(this._createViewFieldControls(configured ?? this._text('fields')));
|
|
441
|
+
}
|
|
364
442
|
if (options.searchControl) this._toolbar.append(this._createSearchControl());
|
|
365
443
|
if (options.historyControls) this._toolbar.append(this._createHistoryControls());
|
|
366
444
|
this._board = h('div', {
|
|
@@ -881,8 +959,12 @@ export class KanbanView extends RecordView {
|
|
|
881
959
|
id: String(/** @type {HTMLElement} */ (activeCollapse).dataset.kanbanId),
|
|
882
960
|
lane: activeSection ? this._sectionMeta.get(activeSection)?.lane ?? null : null
|
|
883
961
|
} : null;
|
|
962
|
+
// Any change that alters field descriptors also refreshes, so this is the invalidation point.
|
|
963
|
+
this._fieldIndex = null;
|
|
964
|
+
this._cardFields = null;
|
|
884
965
|
const columns = this._resolvedColumns();
|
|
885
966
|
const lanes = this._resolvedSwimlanes();
|
|
967
|
+
this._resolveCardSources();
|
|
886
968
|
this._cardMeta = new WeakMap();
|
|
887
969
|
this._sectionMeta = new WeakMap();
|
|
888
970
|
this._cardsById = new Map();
|
|
@@ -1034,16 +1116,21 @@ export class KanbanView extends RecordView {
|
|
|
1034
1116
|
const grabbed = Boolean(this._keyboardMove?.ids.some((candidate) => Object.is(candidate, id)));
|
|
1035
1117
|
const rules = resolveKanbanRules(this._kanbanRules, record, { index, column, lane });
|
|
1036
1118
|
const multiple = grabbed && this._keyboardMove ? this._keyboardMove.ids.length : 1;
|
|
1119
|
+
// A handle that is the only drag source must be visible, whatever `moveHandle` asks for.
|
|
1120
|
+
const reveal = options.moveHandle === 'always' || options.dragFrom === 'handle'
|
|
1121
|
+
? 'always' : 'keyboard';
|
|
1037
1122
|
const handle = h('button', {
|
|
1038
1123
|
class: 'zx-kanban-view__move-handle', type: 'button',
|
|
1039
1124
|
ariaPressed: String(grabbed),
|
|
1125
|
+
dataset: { reveal },
|
|
1040
1126
|
ariaLabel: multiple > 1
|
|
1041
1127
|
? this._text('moveMultiple', { count: multiple })
|
|
1042
1128
|
: this._text('move', { name: this._recordName(record, index) })
|
|
1043
1129
|
}, icon('drag', { size: 13 }));
|
|
1130
|
+
const cardFields = this._resolvedCardFields();
|
|
1044
1131
|
const card = createRecordCard(record, index, {
|
|
1045
|
-
fields:
|
|
1046
|
-
visibleFields:
|
|
1132
|
+
fields: cardFields.all,
|
|
1133
|
+
visibleFields: cardFields.metadata,
|
|
1047
1134
|
titleField: options.titleField,
|
|
1048
1135
|
subtitleField: options.subtitleField,
|
|
1049
1136
|
preview: options.preview ?? undefined,
|
|
@@ -1051,6 +1138,7 @@ export class KanbanView extends RecordView {
|
|
|
1051
1138
|
link: options.link ?? undefined,
|
|
1052
1139
|
actions: options.actions,
|
|
1053
1140
|
selectable: options.selectable,
|
|
1141
|
+
selectionTrigger: options.selectionTrigger,
|
|
1054
1142
|
selected: this._isViewSelected(id),
|
|
1055
1143
|
variant: options.variant,
|
|
1056
1144
|
headingLevel: hasLanes ? 4 : 3,
|
|
@@ -1060,6 +1148,7 @@ export class KanbanView extends RecordView {
|
|
|
1060
1148
|
if (grabbed) card.dataset.grabbed = 'true';
|
|
1061
1149
|
this._applyCardContent(card, { record, id, index, column, lane, rules, selected: this._isViewSelected(id) });
|
|
1062
1150
|
this._applyCardRules(card, rules);
|
|
1151
|
+
this._applyCardAnatomy(card, record, index);
|
|
1063
1152
|
this._applyCardDescription(card, id);
|
|
1064
1153
|
const meta = { record, id, index, column, lane };
|
|
1065
1154
|
this._cardMeta.set(card, meta);
|
|
@@ -1067,6 +1156,137 @@ export class KanbanView extends RecordView {
|
|
|
1067
1156
|
return card;
|
|
1068
1157
|
}
|
|
1069
1158
|
|
|
1159
|
+
/**
|
|
1160
|
+
* Builds the two field arrays every card needs, once per refresh.
|
|
1161
|
+
* @returns {{all:import('../view/record-view.js').ViewField[],metadata:import('../view/record-view.js').ViewField[]}}
|
|
1162
|
+
*/
|
|
1163
|
+
_resolvedCardFields() {
|
|
1164
|
+
if (!this._cardFields) {
|
|
1165
|
+
this._cardFields = { all: this.getFields(), metadata: this._cardMetadataFields() };
|
|
1166
|
+
}
|
|
1167
|
+
return this._cardFields;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Returns the fields the shared metadata list may show. A field the card head or footer already
|
|
1172
|
+
* projects is dropped, because repeating it as a labelled row is the same value twice; a field
|
|
1173
|
+
* marked `duplicate` opts back in, matching how title and subtitle already behave.
|
|
1174
|
+
* @returns {import('../view/record-view.js').ViewField[]} Metadata fields.
|
|
1175
|
+
*/
|
|
1176
|
+
_cardMetadataFields() {
|
|
1177
|
+
const consumed = this._cardSources?.fieldIds;
|
|
1178
|
+
const visible = this.getVisibleFields();
|
|
1179
|
+
if (!consumed?.size) return visible;
|
|
1180
|
+
return visible.filter((field) => !consumed.has(field.id) || field.duplicate === true);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Resolves the card-anatomy readers once per refresh. A field id would otherwise be looked up
|
|
1185
|
+
* through `getFields()` — which clones every descriptor — once per source per card, so a board
|
|
1186
|
+
* with six anatomy sources rebuilt the whole field array six times for every card it drew.
|
|
1187
|
+
* @returns {void}
|
|
1188
|
+
*/
|
|
1189
|
+
_resolveCardSources() {
|
|
1190
|
+
const options = this._kanbanOptions();
|
|
1191
|
+
const fields = this.getFields();
|
|
1192
|
+
/** @type {Set<string>} */
|
|
1193
|
+
const fieldIds = new Set();
|
|
1194
|
+
/** @param {unknown} source @returns {((record:KanbanRecord,index:number)=>unknown)|null} */
|
|
1195
|
+
const reader = (source) => {
|
|
1196
|
+
if (source == null) return null;
|
|
1197
|
+
if (typeof source === 'function') return /** @type {any} */ (source);
|
|
1198
|
+
// A literal descriptor is content, not an accessor: reading it as a property name would
|
|
1199
|
+
// silently resolve undefined and render nothing.
|
|
1200
|
+
if (typeof source === 'object') return () => source;
|
|
1201
|
+
const id = String(source);
|
|
1202
|
+
const field = fields.find((candidate) => candidate.id === id);
|
|
1203
|
+
fieldIds.add(id);
|
|
1204
|
+
return field
|
|
1205
|
+
? (record, index) => readViewField(field, record, index) : (record) => record?.[id];
|
|
1206
|
+
};
|
|
1207
|
+
const sources = {
|
|
1208
|
+
identifier: reader(options.identifier),
|
|
1209
|
+
status: reader(options.status),
|
|
1210
|
+
priority: reader(options.priority),
|
|
1211
|
+
progress: reader(options.progress),
|
|
1212
|
+
assignees: reader(options.assignees),
|
|
1213
|
+
entityIcon: typeof options.entityIcon === 'function' ? options.entityIcon
|
|
1214
|
+
: options.entityIcon == null ? null : () => options.entityIcon
|
|
1215
|
+
};
|
|
1216
|
+
this._cardSources = /** @type {KanbanCardSources} */ ({
|
|
1217
|
+
...sources,
|
|
1218
|
+
fieldIds,
|
|
1219
|
+
any: Object.values(sources).some(Boolean)
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Adds the two Kanban-owned regions that bracket the shared record-card anatomy: a head naming
|
|
1225
|
+
* what the record is and how urgent it is, and a footer carrying completion and the people
|
|
1226
|
+
* responsible. Each part is omitted when its option is unset, so a board that configures none of
|
|
1227
|
+
* them renders exactly the card it rendered before.
|
|
1228
|
+
* @param {HTMLElement} card Card element.
|
|
1229
|
+
* @param {KanbanRecord} record Record.
|
|
1230
|
+
* @param {number} index Display index.
|
|
1231
|
+
* @returns {void}
|
|
1232
|
+
*/
|
|
1233
|
+
_applyCardAnatomy(card, record, index) {
|
|
1234
|
+
const options = this._kanbanOptions();
|
|
1235
|
+
const sources = this._cardSources;
|
|
1236
|
+
if (!sources?.any) return;
|
|
1237
|
+
const body = card.querySelector('.zx-record-card__body');
|
|
1238
|
+
if (!body) return;
|
|
1239
|
+
const entityIcon = sources.entityIcon?.(record, index);
|
|
1240
|
+
const identifier = sources.identifier?.(record, index);
|
|
1241
|
+
const head = createKanbanCardHead({
|
|
1242
|
+
entityIcon: entityIcon == null || entityIcon === '' ? null : String(entityIcon),
|
|
1243
|
+
identifier: identifier == null ? '' : String(identifier),
|
|
1244
|
+
identifierPrefix: this._text('identifier'),
|
|
1245
|
+
flags: [
|
|
1246
|
+
{
|
|
1247
|
+
indicator: this._cardIndicator(sources.status, options.statusTones, record, index),
|
|
1248
|
+
prefix: this._text('statusLabel')
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
indicator: this._cardIndicator(sources.priority, options.priorityTones, record, index),
|
|
1252
|
+
prefix: this._text('priorityLabel')
|
|
1253
|
+
}
|
|
1254
|
+
]
|
|
1255
|
+
});
|
|
1256
|
+
if (head) body.prepend(head);
|
|
1257
|
+
|
|
1258
|
+
const footer = [];
|
|
1259
|
+
const progress = resolveKanbanProgress(
|
|
1260
|
+
sources.progress?.(record, index), Number(options.progressMax));
|
|
1261
|
+
if (progress) {
|
|
1262
|
+
footer.push(createKanbanProgress(progress,
|
|
1263
|
+
progress.label ?? this._text('progress'),
|
|
1264
|
+
this._text('progressValue', { percent: progress.percent })));
|
|
1265
|
+
}
|
|
1266
|
+
const people = resolveKanbanAssignees(sources.assignees?.(record, index));
|
|
1267
|
+
const avatars = createKanbanAvatars(people, Number(options.maxAvatars),
|
|
1268
|
+
this._text('assignees', { names: people.map((person) => person.name).filter(Boolean).join(', ') }),
|
|
1269
|
+
(count) => this._text('assigneeOverflow', { count }));
|
|
1270
|
+
if (avatars) footer.push(avatars);
|
|
1271
|
+
if (footer.length) body.append(h('div', { class: 'zx-kanban-view__card-footer' }, footer));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Resolves one indicator from an already-resolved reader. A reader may return a ready-made
|
|
1276
|
+
* `Node`, which is adopted rather than described, so an application keeps full control.
|
|
1277
|
+
* @param {((record:KanbanRecord,index:number)=>unknown)|null} source Resolved reader.
|
|
1278
|
+
* @param {unknown} tones Value-to-tone map.
|
|
1279
|
+
* @param {KanbanRecord} record Record.
|
|
1280
|
+
* @param {number} index Display index.
|
|
1281
|
+
* @returns {KanbanIndicator|Node|null} Indicator, node, or null.
|
|
1282
|
+
*/
|
|
1283
|
+
_cardIndicator(source, tones, record, index) {
|
|
1284
|
+
if (!source) return null;
|
|
1285
|
+
const value = source(record, index);
|
|
1286
|
+
if (value && typeof value === 'object' && 'nodeType' in value) return /** @type {Node} */ (value);
|
|
1287
|
+
return resolveKanbanIndicator(value, /** @type {Record<string,string>} */ (tones ?? {}));
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1070
1290
|
/**
|
|
1071
1291
|
* Hands card content to `renderCard` while keeping the managed shell: the list item, its
|
|
1072
1292
|
* selection control, the action group, and the move handle are never given away, because they
|
|
@@ -1220,10 +1440,24 @@ export class KanbanView extends RecordView {
|
|
|
1220
1440
|
_readAxis(accessor, record, index) {
|
|
1221
1441
|
if (typeof accessor === 'function') return accessor(record, index);
|
|
1222
1442
|
if (accessor == null) return null;
|
|
1223
|
-
const field = this.
|
|
1443
|
+
const field = this._axisField(String(accessor));
|
|
1224
1444
|
return field ? readViewField(field, record, index) : record?.[accessor];
|
|
1225
1445
|
}
|
|
1226
1446
|
|
|
1447
|
+
/**
|
|
1448
|
+
* Resolves one field descriptor by id through a per-refresh index. `getFields()` rebuilds a Map
|
|
1449
|
+
* and clones every descriptor on each call, and a string column or lane accessor is read once
|
|
1450
|
+
* per record per column per lane — which made this the most expensive line in a board render.
|
|
1451
|
+
* @param {string} id Field id.
|
|
1452
|
+
* @returns {import('../view/record-view.js').ViewField|undefined} Descriptor, if any.
|
|
1453
|
+
*/
|
|
1454
|
+
_axisField(id) {
|
|
1455
|
+
if (!this._fieldIndex) {
|
|
1456
|
+
this._fieldIndex = new Map(this.getFields().map((field) => [field.id, field]));
|
|
1457
|
+
}
|
|
1458
|
+
return this._fieldIndex.get(id);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1227
1461
|
/** @param {string} column @param {string|null} lane @returns {{record:KanbanRecord,index:number}[]} */
|
|
1228
1462
|
_recordsIn(column, lane) {
|
|
1229
1463
|
const options = this._kanbanOptions();
|
|
@@ -1689,6 +1923,11 @@ export class KanbanView extends RecordView {
|
|
|
1689
1923
|
}
|
|
1690
1924
|
if (target?.closest('.zx-kanban-view__move-handle, .zx-record-card__selection, [data-record-select]')) return;
|
|
1691
1925
|
if (target?.closest(INTERACTIVE) || hasTextSelection()) return;
|
|
1926
|
+
if (this.options.selectable && this._kanbanOptions().selectionTrigger === 'card') {
|
|
1927
|
+
this.toggleSelection(meta.id, { range: event.shiftKey });
|
|
1928
|
+
/** @type {HTMLElement|null} */ (target?.closest('.zx-record-card'))?.focus({ preventScroll: true });
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1692
1931
|
this.emit('recordclick', { ...meta, event });
|
|
1693
1932
|
if (this.options.selectable === 'single' && !this._isViewSelected(meta.id)) {
|
|
1694
1933
|
this.setSelection([meta.id]);
|
|
@@ -1724,6 +1963,10 @@ export class KanbanView extends RecordView {
|
|
|
1724
1963
|
if (!cardMeta || target !== card) return;
|
|
1725
1964
|
if (event.key === 'Enter') {
|
|
1726
1965
|
event.preventDefault();
|
|
1966
|
+
if (this.options.selectable && this._kanbanOptions().selectionTrigger === 'card') {
|
|
1967
|
+
this.toggleSelection(cardMeta.id);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1727
1970
|
this.emit('recordclick', { ...cardMeta, event });
|
|
1728
1971
|
if (this.options.selectable === 'single') this.setSelection([cardMeta.id]);
|
|
1729
1972
|
} else if (event.key === ' ' && this.options.selectable !== false) {
|
|
@@ -42,12 +42,12 @@ import { uid } from '../../core/util.js';
|
|
|
42
42
|
* @property {number} [debounce=250] Source-query delay in milliseconds.
|
|
43
43
|
* @property {number} [minQuery=0] Global source minimum query length.
|
|
44
44
|
* @property {number} [maxResults=100] Maximum combined results.
|
|
45
|
-
* @property {string} [placeholder
|
|
46
|
-
* @property {string} [label
|
|
47
|
-
* @property {string} [closeLabel
|
|
48
|
-
* @property {string} [resultsLabel
|
|
49
|
-
* @property {string} [emptyText
|
|
50
|
-
* @property {string} [loadingText
|
|
45
|
+
* @property {string} [placeholder] Search-field placeholder. Omitted resolves `launcher.placeholder`, English `Search applications and records`.
|
|
46
|
+
* @property {string} [label] Dialog and field label. Omitted resolves `launcher.label`, English `Launcher`.
|
|
47
|
+
* @property {string} [closeLabel] Close-button accessible label. Omitted resolves `launcher.close`, English `Close launcher`.
|
|
48
|
+
* @property {string} [resultsLabel] Results-region accessible label. Omitted resolves `launcher.results`, English `Launcher results`.
|
|
49
|
+
* @property {string} [emptyText] Empty-state text. Omitted resolves `launcher.empty`, English `No results`.
|
|
50
|
+
* @property {string} [loadingText] Loading-state text. Omitted resolves `launcher.loading`, English `Searching…`.
|
|
51
51
|
* @property {'mod+k'|false} [shortcut='mod+k'] Optional global keyboard shortcut.
|
|
52
52
|
* @property {false|{move?: string, open?: string, close?: string}} [hints] Visible keyboard hints, or false.
|
|
53
53
|
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns an internally created launcher; defaults to the opener.
|
|
@@ -80,14 +80,20 @@ export class Launcher extends Component {
|
|
|
80
80
|
debounce: 250,
|
|
81
81
|
minQuery: 0,
|
|
82
82
|
maxResults: 100,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
/*
|
|
84
|
+
* The six labels default to null rather than to their English text so that an application
|
|
85
|
+
* which installs a translator and passes no labels still gets a translated launcher. The
|
|
86
|
+
* documented default is what `_message()` falls back to, so an untranslated application
|
|
87
|
+
* renders exactly the same words as before.
|
|
88
|
+
*/
|
|
89
|
+
placeholder: null,
|
|
90
|
+
label: null,
|
|
91
|
+
closeLabel: null,
|
|
92
|
+
resultsLabel: null,
|
|
93
|
+
emptyText: null,
|
|
94
|
+
loadingText: null,
|
|
89
95
|
shortcut: 'mod+k',
|
|
90
|
-
hints:
|
|
96
|
+
hints: null,
|
|
91
97
|
scope: null
|
|
92
98
|
};
|
|
93
99
|
|
|
@@ -126,6 +132,7 @@ export class Launcher extends Component {
|
|
|
126
132
|
this._destroyed = false;
|
|
127
133
|
|
|
128
134
|
const listId = uid('zx-launcher-results');
|
|
135
|
+
const label = String(this.options.label ?? this._message('launcher.label', 'Launcher'));
|
|
129
136
|
const input = h('input', {
|
|
130
137
|
ref: 'input',
|
|
131
138
|
class: 'zx-launcher__input',
|
|
@@ -136,15 +143,15 @@ export class Launcher extends Component {
|
|
|
136
143
|
ariaAutocomplete: 'list',
|
|
137
144
|
ariaControls: listId,
|
|
138
145
|
ariaExpanded: 'false',
|
|
139
|
-
ariaLabel:
|
|
140
|
-
placeholder: this.options.placeholder,
|
|
146
|
+
ariaLabel: label,
|
|
147
|
+
placeholder: this.options.placeholder ?? this._message('launcher.placeholder', 'Search applications and records'),
|
|
141
148
|
value: this._query
|
|
142
149
|
});
|
|
143
150
|
const close = h('button', {
|
|
144
151
|
ref: 'close',
|
|
145
152
|
class: 'zx-icon-btn zx-launcher__close',
|
|
146
153
|
type: 'button',
|
|
147
|
-
ariaLabel: this.options.closeLabel
|
|
154
|
+
ariaLabel: this.options.closeLabel ?? this._message('launcher.close', 'Close launcher')
|
|
148
155
|
}, icon('x'));
|
|
149
156
|
const status = h('div', {
|
|
150
157
|
ref: 'status',
|
|
@@ -157,14 +164,18 @@ export class Launcher extends Component {
|
|
|
157
164
|
class: 'zx-launcher__results',
|
|
158
165
|
id: listId,
|
|
159
166
|
role: 'listbox',
|
|
160
|
-
ariaLabel: this.options.resultsLabel
|
|
167
|
+
ariaLabel: this.options.resultsLabel ?? this._message('launcher.results', 'Launcher results')
|
|
161
168
|
});
|
|
162
169
|
const shortcut = this.options.shortcut === 'mod+k' ? h('kbd', {
|
|
163
170
|
class: 'zx-launcher__shortcut',
|
|
164
171
|
ariaHidden: 'true'
|
|
165
172
|
}, launcherShortcutLabel()) : null;
|
|
166
|
-
const footer = launcherHints(this.options.hints
|
|
167
|
-
|
|
173
|
+
const footer = launcherHints(this.options.hints, {
|
|
174
|
+
move: this._message('launcher.hintMove', 'Move'),
|
|
175
|
+
open: this._message('launcher.hintOpen', 'Open'),
|
|
176
|
+
close: this._message('launcher.hintClose', 'Close')
|
|
177
|
+
});
|
|
178
|
+
dialog.setAttribute('aria-label', label);
|
|
168
179
|
dialog.replaceChildren(
|
|
169
180
|
h('div', { class: 'zx-launcher__surface' },
|
|
170
181
|
h('div', {
|
|
@@ -458,9 +469,15 @@ export class Launcher extends Component {
|
|
|
458
469
|
|
|
459
470
|
/** @returns {void} */
|
|
460
471
|
_renderStatus() {
|
|
461
|
-
if (this._busy)
|
|
462
|
-
|
|
463
|
-
|
|
472
|
+
if (this._busy) {
|
|
473
|
+
this.refs.status.textContent = String(this.options.loadingText
|
|
474
|
+
?? this._message('launcher.loading', 'Searching…'));
|
|
475
|
+
} else if (this._results.length === 0) {
|
|
476
|
+
this.refs.status.textContent = String(this.options.emptyText
|
|
477
|
+
?? this._message('launcher.empty', 'No results'));
|
|
478
|
+
} else {
|
|
479
|
+
this.refs.status.textContent = '';
|
|
480
|
+
}
|
|
464
481
|
this.refs.status.hidden = !this.refs.status.textContent;
|
|
465
482
|
}
|
|
466
483
|
|
|
@@ -566,6 +583,17 @@ export class Launcher extends Component {
|
|
|
566
583
|
event.preventDefault();
|
|
567
584
|
this.open();
|
|
568
585
|
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Resolves a message through the host translator, falling back to the built-in English text.
|
|
589
|
+
* @param {string} key Message key.
|
|
590
|
+
* @param {string} fallback English default.
|
|
591
|
+
* @returns {string}
|
|
592
|
+
*/
|
|
593
|
+
_message(key, fallback) {
|
|
594
|
+
const message = this.msg(key);
|
|
595
|
+
return message === key ? fallback : message;
|
|
596
|
+
}
|
|
569
597
|
}
|
|
570
598
|
|
|
571
599
|
/** @returns {string} */
|
|
@@ -575,14 +603,18 @@ function launcherShortcutLabel() {
|
|
|
575
603
|
return /mac|iphone|ipad/i.test(platform) ? '⌘ K' : 'Ctrl K';
|
|
576
604
|
}
|
|
577
605
|
|
|
578
|
-
/**
|
|
579
|
-
|
|
606
|
+
/**
|
|
607
|
+
* @param {LauncherOptions['hints']} hints Host-supplied hint labels, or false to omit the row.
|
|
608
|
+
* @param {{move: string, open: string, close: string}} defaults Resolved built-in labels.
|
|
609
|
+
* @returns {HTMLElement|null}
|
|
610
|
+
*/
|
|
611
|
+
function launcherHints(hints, defaults) {
|
|
580
612
|
if (hints === false) return null;
|
|
581
613
|
const labels = hints && typeof hints === 'object' ? hints : {};
|
|
582
614
|
return h('footer', { class: 'zx-launcher__hints', ariaHidden: 'true' },
|
|
583
|
-
h('span', {}, h('kbd', {}, '↑↓←→'), labels.move ??
|
|
584
|
-
h('span', {}, h('kbd', {}, '↵'), labels.open ??
|
|
585
|
-
h('span', {}, h('kbd', {}, 'Esc'), labels.close ??
|
|
615
|
+
h('span', {}, h('kbd', {}, '↑↓←→'), labels.move ?? defaults.move),
|
|
616
|
+
h('span', {}, h('kbd', {}, '↵'), labels.open ?? defaults.open),
|
|
617
|
+
h('span', {}, h('kbd', {}, 'Esc'), labels.close ?? defaults.close));
|
|
586
618
|
}
|
|
587
619
|
|
|
588
620
|
/** @param {LauncherItem} item @returns {Node|null} */
|