@zeyos/zx 4.3.4 → 5.0.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 +438 -0
- package/README.md +49 -10
- package/dist/types/components/activity-item/activity-item.d.ts +351 -0
- package/dist/types/components/activity-list/activity-list.d.ts +217 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +33 -3
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +23 -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/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +149 -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/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +75 -6
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
- package/dist/types/components/field/field.d.ts +24 -2
- package/dist/types/components/fieldset/fieldset.d.ts +19 -1
- package/dist/types/components/file-item/file-item.d.ts +262 -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/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +146 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +170 -4
- package/dist/types/components/launcher/launcher.d.ts +278 -26
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
- package/dist/types/components/menu-button/menu-button.d.ts +37 -42
- package/dist/types/components/message/message.d.ts +126 -17
- package/dist/types/components/modal/modal.d.ts +61 -2
- 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/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +15 -3
- package/dist/types/components/sort-control/sort-control.d.ts +105 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +288 -0
- package/dist/types/components/tabbox/tabbox.d.ts +16 -0
- package/dist/types/components/table/table.d.ts +190 -1
- package/dist/types/components/table-view/table-view.d.ts +11 -5
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/timebox/timebox.d.ts +7 -0
- package/dist/types/components/tree/tree.d.ts +13 -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/breakpoint.d.ts +1 -1
- package/dist/types/core/icons.d.ts +37 -22
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +15 -1
- package/dist/types/index.d.ts +16 -3
- package/dist/types/internal/app-rail.d.ts +10 -0
- package/dist/types/internal/menu-items.d.ts +134 -0
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +1 -1
- package/dist/types/zeyos/query.d.ts +79 -0
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +100 -14
- package/dist/types/zeyos/view.d.ts +10 -0
- package/dist/zx-zeyos.esm.js +2206 -489
- package/dist/zx-zeyos.esm.min.js +56 -1
- package/dist/zx.css +3322 -724
- package/dist/zx.esm.js +12244 -5809
- package/dist/zx.esm.min.js +58 -3
- package/dist/zx.global.js +12246 -5811
- package/dist/zx.global.min.js +58 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1683 -146
- package/docs/llms.md +777 -110
- package/docs/llms.txt +14 -8
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/activity-item/activity-item.css +137 -0
- package/src/components/activity-item/activity-item.js +602 -0
- package/src/components/activity-list/activity-list.css +99 -0
- package/src/components/activity-list/activity-list.js +409 -0
- package/src/components/app-icon/app-icon.css +1 -1
- package/src/components/app-rail/app-rail.css +191 -22
- package/src/components/app-sidebar/app-sidebar.css +349 -24
- package/src/components/app-sidebar/app-sidebar.js +86 -11
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/button/button.css +74 -8
- package/src/components/button/button.js +44 -1
- package/src/components/calendar/calendar.css +46 -46
- package/src/components/calendar/calendar.js +33 -3
- package/src/components/card/card.css +2 -2
- package/src/components/card/card.js +12 -1
- package/src/components/card-view/card-view.css +10 -7
- package/src/components/card-view/card-view.js +25 -9
- package/src/components/card-view/record-card.js +7 -3
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +68 -0
- package/src/components/code-editor/code-editor.js +306 -0
- package/src/components/code-editor/field-adapter.js +34 -0
- package/src/components/context-menu/context-menu.css +87 -5
- package/src/components/context-menu/context-menu.js +22 -30
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +8 -3
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-picker.js +22 -8
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/month-picker.js +3 -3
- package/src/components/date-picker/time-picker.css +1 -1
- package/src/components/date-picker/time-picker.js +20 -8
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +174 -70
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +7 -5
- package/src/components/empty-state/empty-state.css +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/field/field.css +28 -9
- package/src/components/field/field.js +68 -26
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +28 -5
- package/src/components/file-item/file-item.css +88 -0
- package/src/components/file-item/file-item.js +505 -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 +205 -47
- package/src/components/filter-panel/filter-panel.css +104 -0
- package/src/components/filter-panel/filter-panel.js +964 -0
- package/src/components/finder/finder.css +15 -13
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +247 -0
- package/src/components/kanban-view/kanban-view.css +226 -8
- package/src/components/kanban-view/kanban-view.js +255 -12
- package/src/components/launcher/launcher.css +107 -25
- package/src/components/launcher/launcher.js +557 -63
- package/src/components/list-toolbar/list-toolbar.css +194 -0
- package/src/components/list-toolbar/list-toolbar.js +851 -0
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/menu-button/menu-button.css +87 -5
- package/src/components/menu-button/menu-button.js +86 -46
- package/src/components/message/message.css +38 -5
- package/src/components/message/message.js +230 -17
- package/src/components/modal/modal.css +15 -0
- package/src/components/modal/modal.js +159 -3
- package/src/components/navigation-bar/navigation-bar.css +95 -11
- package/src/components/navigation-bar/navigation-bar.js +171 -4
- package/src/components/number-field/field-adapter.js +1 -0
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +8 -6
- package/src/components/panel/panel.css +1 -1
- package/src/components/panel/panel.js +13 -2
- package/src/components/rating/rating.css +7 -0
- package/src/components/search/search.js +17 -5
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +6 -1
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +86 -47
- package/src/components/sheet/sheet.css +11 -8
- package/src/components/slider/slider.css +16 -2
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +20 -0
- package/src/components/sort-control/sort-control.js +140 -0
- package/src/components/stat-tile/stat-tile.css +208 -0
- package/src/components/stat-tile/stat-tile.js +601 -0
- package/src/components/tabbox/tabbox.css +32 -27
- package/src/components/tabbox/tabbox.js +17 -2
- package/src/components/table/table.css +155 -31
- package/src/components/table/table.js +386 -33
- 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 +86 -6
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/timebox/timebox.js +18 -5
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +10 -3
- package/src/components/tree/tree.css +25 -16
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- 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/breakpoint.js +1 -1
- package/src/core/icons.js +259 -57
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +21 -2
- package/src/index.js +30 -3
- package/src/internal/app-rail.js +18 -1
- package/src/internal/menu-items.js +142 -0
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +67 -27
- package/src/zeyos/index.js +1 -1
- package/src/zeyos/query.js +153 -0
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +68 -5
- package/src/zeyos/view.js +6 -0
- package/styles/base.css +56 -0
- package/styles/icons.css +11 -1
- package/styles/print.css +192 -0
- package/styles/tokens/dark.css +113 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +100 -31
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +11 -1
- package/styles/tokens/density.css +0 -9
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
flex: none;
|
|
18
18
|
min-inline-size: 0;
|
|
19
19
|
flex-direction: column;
|
|
20
|
-
border-inline-end: 1px solid var(--zx-color-border);
|
|
20
|
+
border-inline-end: 1px solid var(--zx-color-border-subtle);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
.zx-finder__list {
|
|
@@ -32,9 +32,12 @@
|
|
|
32
32
|
display: flex;
|
|
33
33
|
align-items: center;
|
|
34
34
|
gap: var(--zx-space-2);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
/* A browsing row: a space-1 margin round one line of text, and the smallest radius, so a
|
|
36
|
+
column of them reads as a list rather than a stack of buttons. */
|
|
37
|
+
min-block-size: calc(var(--zx-control-height) - var(--zx-space-1));
|
|
38
|
+
padding: var(--zx-space-1) var(--zx-space-2);
|
|
39
|
+
border-radius: var(--zx-radius-sm);
|
|
40
|
+
line-height: 1.25;
|
|
38
41
|
cursor: pointer;
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -42,10 +45,12 @@
|
|
|
42
45
|
background: var(--zx-color-bg-hover);
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
/* The selected row of each column is the path you are on, not a pick: the neutral current fill,
|
|
49
|
+
the full text colour and a heavier label. */
|
|
45
50
|
.zx-finder__row[aria-selected="true"] {
|
|
46
|
-
background: var(--zx-color-bg-
|
|
47
|
-
color: var(--zx-color-
|
|
48
|
-
font-weight:
|
|
51
|
+
background: var(--zx-color-bg-current);
|
|
52
|
+
color: var(--zx-color-text);
|
|
53
|
+
font-weight: var(--zx-weight-semibold);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
.zx-finder__row:focus-visible {
|
|
@@ -53,19 +58,16 @@
|
|
|
53
58
|
box-shadow: var(--zx-focus-ring);
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/* Icons are quieter than labels, folders included; the current row's icon takes the text colour. */
|
|
56
62
|
.zx-finder__icon {
|
|
57
63
|
display: inline-grid;
|
|
58
64
|
place-items: center;
|
|
59
65
|
flex: none;
|
|
60
|
-
color: var(--zx-color-text-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.zx-finder__row[data-branch] .zx-finder__icon {
|
|
64
|
-
color: var(--zx-color-warning);
|
|
66
|
+
color: var(--zx-color-text-subtle);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
.zx-finder__row[aria-selected="true"] .zx-finder__icon {
|
|
68
|
-
color:
|
|
70
|
+
color: var(--zx-color-text);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
.zx-finder__label {
|
|
@@ -29,11 +29,12 @@
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
.zx-form__actions .zx-btn[type="submit"] {
|
|
32
|
-
border-color: var(--zx-color-accent);
|
|
33
|
-
background: var(--zx-color-accent);
|
|
34
|
-
color: var(--zx-color-on-accent);
|
|
32
|
+
border-color: var(--zx-color-accent-solid);
|
|
33
|
+
background: var(--zx-color-accent-solid);
|
|
34
|
+
color: var(--zx-color-on-accent-solid);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.zx-form__actions .zx-btn[type="submit"]:hover {
|
|
38
|
-
|
|
38
|
+
border-color: var(--zx-color-accent-solid-hover);
|
|
39
|
+
background: var(--zx-color-accent-solid-hover);
|
|
39
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h } from '../../core/dom.js';
|
|
3
3
|
import { icon as createIcon } from '../../core/icons.js';
|
|
4
|
+
import { uid } from '../../core/util.js';
|
|
4
5
|
import { Fieldset } from '../fieldset/fieldset.js';
|
|
5
6
|
import { coerceFloat, coerceInteger } from '../field/field.js';
|
|
6
7
|
|
|
@@ -22,6 +23,9 @@ import { coerceFloat, coerceInteger } from '../field/field.js';
|
|
|
22
23
|
* @property {Array<Fieldset|import('../fieldset/fieldset.js').FieldsetOptions>} [fieldsets=[]] Initial fieldsets.
|
|
23
24
|
* @property {FormAction[]} [actions=[]] Form action descriptors.
|
|
24
25
|
* @property {boolean} [novalidate=true] Native `novalidate` state.
|
|
26
|
+
* @property {string|null} [idPrefix=null] Prefix of the DOM ids generated for the fields of the
|
|
27
|
+
* fieldsets this form builds, as `<idPrefix>-<key>`. Null generates one per form instance, so a
|
|
28
|
+
* form mounted twice never repeats an id. A fieldset's own `idPrefix` wins.
|
|
25
29
|
* @property {(event: CustomEvent<{values: Record<string, unknown>}>) => void} [onsubmit] Submit listener.
|
|
26
30
|
* @property {(event: CustomEvent<{errors: Record<string, string>}>) => void} [oninvalid] Invalid listener.
|
|
27
31
|
* @property {(event: CustomEvent<{id: string, value: unknown}>) => void} [onchange] Field change listener.
|
|
@@ -42,6 +46,7 @@ export class Form extends Component {
|
|
|
42
46
|
fieldsets: [],
|
|
43
47
|
actions: [],
|
|
44
48
|
novalidate: true,
|
|
49
|
+
idPrefix: null,
|
|
45
50
|
msg: {
|
|
46
51
|
'form.required': 'This field is required.',
|
|
47
52
|
'form.int': 'Enter a whole number.',
|
|
@@ -56,6 +61,7 @@ export class Form extends Component {
|
|
|
56
61
|
this._ownedFieldsets = new Set();
|
|
57
62
|
this._origins = new Map();
|
|
58
63
|
this._ownedNodes = [];
|
|
64
|
+
this._idPrefix = this.options.idPrefix ? String(this.options.idPrefix) : uid('zx-form');
|
|
59
65
|
this._novalidateAttribute = root.getAttribute('novalidate');
|
|
60
66
|
root.toggleAttribute('novalidate', Boolean(this.options.novalidate));
|
|
61
67
|
|
|
@@ -81,13 +87,16 @@ export class Form extends Component {
|
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
/**
|
|
84
|
-
* Adds an existing fieldset or constructs one from options.
|
|
90
|
+
* Adds an existing fieldset or constructs one from options. A constructed fieldset generates its
|
|
91
|
+
* field ids from the form's `idPrefix` unless its options carry their own.
|
|
85
92
|
* @param {Fieldset|import('../fieldset/fieldset.js').FieldsetOptions} fieldset Fieldset source.
|
|
86
93
|
* @returns {Fieldset}
|
|
87
94
|
*/
|
|
88
95
|
addFieldset(fieldset) {
|
|
89
96
|
const owned = !(fieldset instanceof Fieldset);
|
|
90
|
-
const instance = owned
|
|
97
|
+
const instance = owned
|
|
98
|
+
? new Fieldset(null, { ...(fieldset ?? {}), idPrefix: fieldset?.idPrefix ?? this._idPrefix })
|
|
99
|
+
: fieldset;
|
|
91
100
|
if (this._fieldsets.includes(instance)) return instance;
|
|
92
101
|
if (owned) this._ownedFieldsets.add(instance);
|
|
93
102
|
else this._origins.set(instance, { parent: instance.el.parentNode, next: instance.el.nextSibling });
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { h, safeHref } from '../../core/dom.js';
|
|
3
|
+
import { icon } from '../../core/icons.js';
|
|
4
|
+
import { AVATAR_TONES, avatarInitials, avatarTone } 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
|
+
* @property {import('../avatar/avatar.js').AvatarTone} tone Fallback tint: a descriptor's own
|
|
32
|
+
* `tone` when it names one of `AVATAR_TONES`, otherwise `avatarTone(name)` — the colour the same
|
|
33
|
+
* person has in every Avatar.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** Semantic intents an indicator may carry, in the order they are documented. */
|
|
37
|
+
const TONES = Object.freeze(['neutral', 'accent', 'success', 'warning', 'danger', 'info']);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes one indicator value into the descriptor the badge is built from. A plain value
|
|
41
|
+
* becomes the label and looks its tone up in the supplied map, so an application expresses
|
|
42
|
+
* "High means danger" as data rather than as a renderer.
|
|
43
|
+
* @param {unknown} value Raw field value, descriptor, or null.
|
|
44
|
+
* @param {Record<string, string>} [tones={}] Value-to-tone map.
|
|
45
|
+
* @returns {KanbanIndicator|null} Descriptor, or null when there is nothing to show.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveKanbanIndicator(value, tones = {}) {
|
|
48
|
+
if (value == null || value === '') return null;
|
|
49
|
+
const map = tones && typeof tones === 'object' ? tones : {};
|
|
50
|
+
const source = typeof value === 'object' && !('nodeType' in value)
|
|
51
|
+
? /** @type {Record<string, any>} */ (value) : { label: value };
|
|
52
|
+
const label = source.label == null ? '' : String(source.label);
|
|
53
|
+
if (!label) return null;
|
|
54
|
+
const mapped = map[label];
|
|
55
|
+
const tone = TONES.includes(source.tone) ? source.tone
|
|
56
|
+
: TONES.includes(mapped) ? mapped : 'neutral';
|
|
57
|
+
return {
|
|
58
|
+
label,
|
|
59
|
+
tone: /** @type {KanbanIndicator['tone']} */ (tone),
|
|
60
|
+
icon: source.icon == null ? null : String(source.icon),
|
|
61
|
+
dot: source.dot !== false && source.icon == null,
|
|
62
|
+
...(source.title == null ? {} : { title: String(source.title) })
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds one indicator badge.
|
|
68
|
+
* @param {KanbanIndicator} indicator Normalized descriptor.
|
|
69
|
+
* @returns {HTMLElement} Badge element.
|
|
70
|
+
*/
|
|
71
|
+
export function createKanbanIndicator(indicator) {
|
|
72
|
+
return badge({
|
|
73
|
+
label: indicator.label,
|
|
74
|
+
kind: indicator.tone,
|
|
75
|
+
icon: indicator.icon ?? undefined,
|
|
76
|
+
dot: indicator.dot,
|
|
77
|
+
size: 'sm',
|
|
78
|
+
variant: 'soft',
|
|
79
|
+
...(indicator.title == null ? {} : { title: indicator.title })
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalizes a completion value against an explicit scale. The scale is never guessed: a `0.72`
|
|
85
|
+
* on a 0–100 board is seven-tenths of a percent, and only the application knows which it meant.
|
|
86
|
+
* @param {unknown} value Raw field value, or `{value, max, label}`.
|
|
87
|
+
* @param {number} [max=100] Amount that counts as finished.
|
|
88
|
+
* @returns {KanbanProgress|null} Descriptor, or null when there is nothing to show.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveKanbanProgress(value, max = 100) {
|
|
91
|
+
if (value == null || value === '') return null;
|
|
92
|
+
const source = typeof value === 'object' && !('nodeType' in value)
|
|
93
|
+
? /** @type {Record<string, any>} */ (value) : { value };
|
|
94
|
+
const limit = Number(source.max ?? max);
|
|
95
|
+
const amount = Number(source.value);
|
|
96
|
+
if (!Number.isFinite(amount) || !Number.isFinite(limit) || limit <= 0) return null;
|
|
97
|
+
const clamped = Math.max(0, Math.min(limit, amount));
|
|
98
|
+
// A blank label must not win over the localized default: it would become aria-label="".
|
|
99
|
+
const label = source.label == null ? '' : String(source.label).trim();
|
|
100
|
+
return {
|
|
101
|
+
value: clamped,
|
|
102
|
+
max: limit,
|
|
103
|
+
percent: Math.round(clamped / limit * 100),
|
|
104
|
+
label: label || null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds the card's completion meter. The track is the accessible progress element itself, so the
|
|
110
|
+
* value is available without depending on the visible percentage text.
|
|
111
|
+
* @param {KanbanProgress} progress Normalized descriptor.
|
|
112
|
+
* @param {string} label Meter label.
|
|
113
|
+
* @param {string} valueText Formatted readout drawn opposite the label.
|
|
114
|
+
* @returns {HTMLElement} Meter element.
|
|
115
|
+
*/
|
|
116
|
+
export function createKanbanProgress(progress, label, valueText) {
|
|
117
|
+
const track = h('div', {
|
|
118
|
+
class: 'zx-kanban-view__progress-track',
|
|
119
|
+
role: 'progressbar',
|
|
120
|
+
ariaValuemin: '0',
|
|
121
|
+
ariaValuemax: String(progress.max),
|
|
122
|
+
ariaValuenow: String(progress.value),
|
|
123
|
+
ariaValuetext: valueText,
|
|
124
|
+
ariaLabel: label
|
|
125
|
+
}, h('div', { class: 'zx-kanban-view__progress-fill' }));
|
|
126
|
+
// The fill is the only inline style on a card: a percentage cannot be expressed as a token.
|
|
127
|
+
/** @type {HTMLElement} */ (track.firstElementChild).style.inlineSize = `${progress.percent}%`;
|
|
128
|
+
return h('div', { class: 'zx-kanban-view__progress' },
|
|
129
|
+
h('div', { class: 'zx-kanban-view__progress-head' },
|
|
130
|
+
h('span', { class: 'zx-kanban-view__progress-label' }, label),
|
|
131
|
+
h('span', { class: 'zx-kanban-view__progress-value' }, valueText)),
|
|
132
|
+
track);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Normalizes one or more responsible people. Names, name lists, and descriptor objects all
|
|
137
|
+
* resolve to the same shape, and an unsafe image URL degrades to initials rather than failing.
|
|
138
|
+
* A descriptor may name its `tone`; otherwise the tone is derived from the name exactly as Avatar
|
|
139
|
+
* derives it.
|
|
140
|
+
* @param {unknown} value Raw field value.
|
|
141
|
+
* @returns {KanbanAssignee[]} Normalized people, in the order supplied.
|
|
142
|
+
*/
|
|
143
|
+
export function resolveKanbanAssignees(value) {
|
|
144
|
+
if (value == null || value === '') return [];
|
|
145
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
146
|
+
return /** @type {KanbanAssignee[]} */ (entries.map((entry) => {
|
|
147
|
+
if (entry == null || entry === '') return null;
|
|
148
|
+
const source = typeof entry === 'object' && !('nodeType' in entry)
|
|
149
|
+
? /** @type {Record<string, any>} */ (entry) : { name: entry };
|
|
150
|
+
const name = String(source.name ?? source.label ?? '').trim();
|
|
151
|
+
// An avatar with no name cannot be announced: it would render as an unidentified face inside a
|
|
152
|
+
// group labelled "Assigned to ". A picture without a person is worse than no picture.
|
|
153
|
+
if (!name) return null;
|
|
154
|
+
const src = safeHref(source.src ?? source.image ?? null);
|
|
155
|
+
const initials = source.initials == null ? avatarInitials(name) : String(source.initials);
|
|
156
|
+
const tone = AVATAR_TONES.includes(source.tone) ? source.tone : avatarTone(name);
|
|
157
|
+
return { name, src, initials, tone };
|
|
158
|
+
}).filter(Boolean));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Splits a list of people into the faces that are drawn and the number the overflow chip stands
|
|
163
|
+
* for. Exists as one function so the slice and the chip can never be computed from different
|
|
164
|
+
* normalizations of the same option — a fractional limit used to floor every face away while the
|
|
165
|
+
* chip still counted from the raw value.
|
|
166
|
+
* @param {number} max Requested face count.
|
|
167
|
+
* @param {number} total People available.
|
|
168
|
+
* @returns {{shown:number,hidden:number}} Normalized split.
|
|
169
|
+
*/
|
|
170
|
+
export function kanbanAvatarLimit(max, total) {
|
|
171
|
+
// Exported, so a malformed total is normalized here rather than trusted from the caller.
|
|
172
|
+
const people = Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;
|
|
173
|
+
const limit = Number.isFinite(max) ? Math.max(1, Math.floor(max)) : people;
|
|
174
|
+
const shown = Math.min(people, limit);
|
|
175
|
+
return { shown, hidden: Math.max(0, people - shown) };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Builds the assignee avatar group. The group carries one accessible name listing everyone,
|
|
180
|
+
* including the people behind the overflow chip, and the chips themselves are hidden so a screen
|
|
181
|
+
* reader announces the list once rather than once per face.
|
|
182
|
+
* @param {KanbanAssignee[]} people Normalized people.
|
|
183
|
+
* @param {number} max Faces drawn before the overflow chip.
|
|
184
|
+
* @param {string} groupLabel Accessible name for the whole group.
|
|
185
|
+
* @param {(count: number) => string} overflowLabel Text for the overflow chip, given how many
|
|
186
|
+
* people it stands for. The count is derived here so it can never disagree with the slice.
|
|
187
|
+
* @returns {HTMLElement|null} Group element, or null when nobody is assigned.
|
|
188
|
+
*/
|
|
189
|
+
export function createKanbanAvatars(people, max, groupLabel, overflowLabel) {
|
|
190
|
+
if (!people.length) return null;
|
|
191
|
+
const split = kanbanAvatarLimit(max, people.length);
|
|
192
|
+
const shown = people.slice(0, split.shown);
|
|
193
|
+
const hidden = split.hidden;
|
|
194
|
+
// Each face carries its tone, so the initials sit on the tint the person has in every Avatar.
|
|
195
|
+
const faces = shown.map((person) => h('span', {
|
|
196
|
+
class: 'zx-kanban-view__avatar', ariaHidden: 'true', title: person.name || undefined,
|
|
197
|
+
dataset: { tone: person.tone ?? avatarTone(person.name) }
|
|
198
|
+
}, person.src
|
|
199
|
+
? h('img', { class: 'zx-kanban-view__avatar-image', src: person.src, alt: '', draggable: false })
|
|
200
|
+
: h('span', { class: 'zx-kanban-view__avatar-initials' }, person.initials)));
|
|
201
|
+
if (hidden > 0) {
|
|
202
|
+
faces.push(h('span', {
|
|
203
|
+
class: 'zx-kanban-view__avatar', ariaHidden: 'true', dataset: { overflow: 'true' }
|
|
204
|
+
}, h('span', { class: 'zx-kanban-view__avatar-initials' }, overflowLabel(hidden))));
|
|
205
|
+
}
|
|
206
|
+
return h('span', { class: 'zx-kanban-view__avatars', role: 'img', ariaLabel: groupLabel }, faces);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Builds the card's head row: what the record is, and how urgent it is.
|
|
211
|
+
* @param {Object} parts Row content.
|
|
212
|
+
* @param {string|null} parts.entityIcon Icon name, or null.
|
|
213
|
+
* @param {string} parts.identifier Record key, or an empty string.
|
|
214
|
+
* @param {{indicator:Node|KanbanIndicator|null,prefix:string}[]} parts.flags Trailing indicators,
|
|
215
|
+
* each with the visually hidden text that names what it measures.
|
|
216
|
+
* @param {string} parts.identifierPrefix Visually hidden text read before the record key.
|
|
217
|
+
* @returns {HTMLElement|null} Head row, or null when it would be empty.
|
|
218
|
+
*/
|
|
219
|
+
export function createKanbanCardHead(parts) {
|
|
220
|
+
const lead = [];
|
|
221
|
+
if (parts.entityIcon) {
|
|
222
|
+
lead.push(h('span', { class: 'zx-kanban-view__entity', ariaHidden: 'true' },
|
|
223
|
+
icon(parts.entityIcon, { size: 12 })));
|
|
224
|
+
}
|
|
225
|
+
if (parts.identifier) {
|
|
226
|
+
// `aria-label` is prohibited on the generic role, so the prefix is real, clipped text.
|
|
227
|
+
lead.push(h('span', { class: 'zx-kanban-view__identifier' },
|
|
228
|
+
parts.identifierPrefix
|
|
229
|
+
? h('span', { class: 'zx-kanban-view__hint' }, `${parts.identifierPrefix} `) : null,
|
|
230
|
+
parts.identifier));
|
|
231
|
+
}
|
|
232
|
+
const trailing = (parts.flags ?? []).map((flag) => {
|
|
233
|
+
if (flag.indicator == null) return null;
|
|
234
|
+
const node = typeof flag.indicator === 'object' && 'nodeType' in flag.indicator
|
|
235
|
+
? /** @type {Node} */ (flag.indicator)
|
|
236
|
+
: createKanbanIndicator(/** @type {KanbanIndicator} */ (flag.indicator));
|
|
237
|
+
// "High" alone does not say high what; the prefix names the axis without adding visible noise.
|
|
238
|
+
return flag.prefix
|
|
239
|
+
? h('span', { class: 'zx-kanban-view__flag' },
|
|
240
|
+
h('span', { class: 'zx-kanban-view__hint' }, `${flag.prefix} `), node)
|
|
241
|
+
: node;
|
|
242
|
+
}).filter(Boolean);
|
|
243
|
+
if (!lead.length && !trailing.length) return null;
|
|
244
|
+
return h('div', { class: 'zx-kanban-view__card-head' },
|
|
245
|
+
lead.length ? h('span', { class: 'zx-kanban-view__card-lead' }, lead) : null,
|
|
246
|
+
trailing.length ? h('span', { class: 'zx-kanban-view__card-flags' }, trailing) : null);
|
|
247
|
+
}
|
|
@@ -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
|
}
|
|
@@ -114,8 +120,8 @@
|
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
.zx-kanban-view__move-handle[aria-pressed="true"] {
|
|
117
|
-
background: var(--zx-color-
|
|
118
|
-
color: var(--zx-color-
|
|
123
|
+
background: var(--zx-color-bg-current);
|
|
124
|
+
color: var(--zx-color-text);
|
|
119
125
|
cursor: grabbing;
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -342,7 +348,7 @@
|
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
.zx-kanban-view .zx-record-card__eyebrow-group[data-card-slot="eyebrow-start"] .zx-record-card__field {
|
|
345
|
-
border-radius: var(--zx-radius-
|
|
351
|
+
border-radius: var(--zx-radius-sm);
|
|
346
352
|
background: var(--zx-color-bg-muted);
|
|
347
353
|
padding: calc(var(--zx-space-1) / 2) var(--zx-space-2);
|
|
348
354
|
}
|
|
@@ -413,6 +419,12 @@
|
|
|
413
419
|
.zx-kanban-view .zx-record-card[data-grabbed="true"] {
|
|
414
420
|
outline: 2px solid var(--zx-color-accent);
|
|
415
421
|
outline-offset: 2px;
|
|
422
|
+
/* A card picked up from the keyboard is the item being dragged, so it lifts like one. */
|
|
423
|
+
box-shadow: var(--zx-drag-shadow);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.zx-kanban-view .zx-record-card[data-grabbed="true"]:focus-visible {
|
|
427
|
+
box-shadow: var(--zx-focus-ring), var(--zx-drag-shadow);
|
|
416
428
|
}
|
|
417
429
|
|
|
418
430
|
.zx-kanban-view__empty {
|
|
@@ -464,10 +476,6 @@
|
|
|
464
476
|
}
|
|
465
477
|
}
|
|
466
478
|
|
|
467
|
-
[data-zx-density="compact"] .zx-kanban-view {
|
|
468
|
-
--zx-kanban-card-padding-inline: var(--zx-space-2);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
479
|
@media (prefers-reduced-motion: no-preference) {
|
|
472
480
|
.zx-kanban-view__collapse,
|
|
473
481
|
.zx-kanban-view__move-handle,
|
|
@@ -565,10 +573,13 @@
|
|
|
565
573
|
|
|
566
574
|
/* ── Empty column placeholder ─────────────────────────────────────────────────────────────── */
|
|
567
575
|
|
|
576
|
+
/* An empty column reads as somewhere a card can go, not as a rendering gap. */
|
|
568
577
|
.zx-kanban-view__column-empty {
|
|
569
578
|
display: grid;
|
|
570
579
|
place-items: center;
|
|
571
580
|
min-block-size: 4rem;
|
|
581
|
+
border: 1px dashed var(--zx-color-border);
|
|
582
|
+
border-radius: var(--zx-radius-md);
|
|
572
583
|
padding: var(--zx-space-2);
|
|
573
584
|
color: var(--zx-color-text-muted);
|
|
574
585
|
font-size: var(--zx-text-xs);
|
|
@@ -642,7 +653,8 @@
|
|
|
642
653
|
border: 1px solid var(--zx-color-border);
|
|
643
654
|
border-radius: var(--zx-radius-md);
|
|
644
655
|
background: var(--zx-color-bg-raised);
|
|
645
|
-
|
|
656
|
+
/* The item in flight: lifted above every resting card by the shared drag shadow. */
|
|
657
|
+
box-shadow: var(--zx-drag-shadow);
|
|
646
658
|
padding: var(--zx-space-1) var(--zx-space-2);
|
|
647
659
|
color: var(--zx-color-text);
|
|
648
660
|
font-size: var(--zx-text-xs);
|
|
@@ -658,3 +670,209 @@
|
|
|
658
670
|
min-block-size: 2.75rem;
|
|
659
671
|
}
|
|
660
672
|
}
|
|
673
|
+
|
|
674
|
+
/* ── Card head: what the record is, and how urgent it is ──────────────────────────────────── */
|
|
675
|
+
|
|
676
|
+
.zx-kanban-view__card-head {
|
|
677
|
+
display: flex;
|
|
678
|
+
align-items: center;
|
|
679
|
+
gap: var(--zx-space-2);
|
|
680
|
+
min-block-size: 1.25rem;
|
|
681
|
+
min-inline-size: 0;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.zx-kanban-view__card-lead,
|
|
685
|
+
.zx-kanban-view__card-flags {
|
|
686
|
+
display: inline-flex;
|
|
687
|
+
align-items: center;
|
|
688
|
+
gap: var(--zx-space-1);
|
|
689
|
+
min-inline-size: 0;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.zx-kanban-view__card-flags {
|
|
693
|
+
flex: none;
|
|
694
|
+
margin-inline-start: auto;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.zx-kanban-view__entity {
|
|
698
|
+
display: inline-grid;
|
|
699
|
+
flex: none;
|
|
700
|
+
place-items: center;
|
|
701
|
+
color: var(--zx-color-text-muted);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.zx-kanban-view__identifier {
|
|
705
|
+
min-inline-size: 0;
|
|
706
|
+
overflow: hidden;
|
|
707
|
+
color: var(--zx-color-text-muted);
|
|
708
|
+
font-size: var(--zx-text-xs);
|
|
709
|
+
font-variant-numeric: tabular-nums;
|
|
710
|
+
text-overflow: ellipsis;
|
|
711
|
+
white-space: nowrap;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* ── Card footer: completion, then the people responsible ─────────────────────────────────── */
|
|
715
|
+
|
|
716
|
+
.zx-kanban-view__card-footer {
|
|
717
|
+
display: flex;
|
|
718
|
+
flex-direction: column;
|
|
719
|
+
gap: var(--zx-space-2);
|
|
720
|
+
min-inline-size: 0;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.zx-kanban-view__progress-head {
|
|
724
|
+
display: flex;
|
|
725
|
+
align-items: baseline;
|
|
726
|
+
justify-content: space-between;
|
|
727
|
+
gap: var(--zx-space-2);
|
|
728
|
+
margin-block-end: calc(var(--zx-space-1) / 2);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.zx-kanban-view__progress-label,
|
|
732
|
+
.zx-kanban-view__progress-value {
|
|
733
|
+
color: var(--zx-color-text-muted);
|
|
734
|
+
font-size: var(--zx-text-xs);
|
|
735
|
+
line-height: 1.2;
|
|
736
|
+
white-space: nowrap;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.zx-kanban-view__progress-label {
|
|
740
|
+
min-inline-size: 0;
|
|
741
|
+
overflow: hidden;
|
|
742
|
+
text-overflow: ellipsis;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.zx-kanban-view__progress-value {
|
|
746
|
+
flex: none;
|
|
747
|
+
font-variant-numeric: tabular-nums;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.zx-kanban-view__progress-track {
|
|
751
|
+
block-size: 4px;
|
|
752
|
+
overflow: hidden;
|
|
753
|
+
border-radius: var(--zx-radius-full);
|
|
754
|
+
background: var(--zx-color-bg-muted);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.zx-kanban-view__progress-fill {
|
|
758
|
+
block-size: 100%;
|
|
759
|
+
border-radius: inherit;
|
|
760
|
+
background: var(--zx-color-accent);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* ── Assignees ────────────────────────────────────────────────────────────────────────────── */
|
|
764
|
+
|
|
765
|
+
/* Faces overlap into a stack; each one lifts its own left edge clear of the previous. */
|
|
766
|
+
.zx-kanban-view__avatars {
|
|
767
|
+
display: inline-flex;
|
|
768
|
+
align-items: center;
|
|
769
|
+
min-inline-size: 0;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/*
|
|
773
|
+
* A face is a small Avatar: the initials on the tint of the tone the component derives from the
|
|
774
|
+
* name with Avatar's own hash (`data-tone`), so a person has the same colour on the board as
|
|
775
|
+
* everywhere else. The surface-coloured edge is what separates overlapping faces.
|
|
776
|
+
*/
|
|
777
|
+
.zx-kanban-view__avatar {
|
|
778
|
+
display: inline-grid;
|
|
779
|
+
flex: none;
|
|
780
|
+
place-items: center;
|
|
781
|
+
inline-size: 1.5rem;
|
|
782
|
+
block-size: 1.5rem;
|
|
783
|
+
overflow: hidden;
|
|
784
|
+
border: 1px solid var(--zx-color-bg-surface);
|
|
785
|
+
border-radius: var(--zx-radius-full);
|
|
786
|
+
background: var(--zx-kanban-avatar-bg, var(--zx-color-bg-muted));
|
|
787
|
+
color: var(--zx-kanban-avatar-fg, var(--zx-color-text-muted));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.zx-kanban-view__avatar[data-tone="blue"] { --zx-kanban-avatar-bg: var(--zx-color-tone-blue-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-blue-fg); }
|
|
791
|
+
.zx-kanban-view__avatar[data-tone="teal"] { --zx-kanban-avatar-bg: var(--zx-color-tone-teal-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-teal-fg); }
|
|
792
|
+
.zx-kanban-view__avatar[data-tone="green"] { --zx-kanban-avatar-bg: var(--zx-color-tone-green-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-green-fg); }
|
|
793
|
+
.zx-kanban-view__avatar[data-tone="amber"] { --zx-kanban-avatar-bg: var(--zx-color-tone-amber-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-amber-fg); }
|
|
794
|
+
.zx-kanban-view__avatar[data-tone="orange"] { --zx-kanban-avatar-bg: var(--zx-color-tone-orange-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-orange-fg); }
|
|
795
|
+
.zx-kanban-view__avatar[data-tone="red"] { --zx-kanban-avatar-bg: var(--zx-color-tone-red-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-red-fg); }
|
|
796
|
+
.zx-kanban-view__avatar[data-tone="pink"] { --zx-kanban-avatar-bg: var(--zx-color-tone-pink-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-pink-fg); }
|
|
797
|
+
.zx-kanban-view__avatar[data-tone="violet"] { --zx-kanban-avatar-bg: var(--zx-color-tone-violet-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-violet-fg); }
|
|
798
|
+
.zx-kanban-view__avatar[data-tone="slate"] { --zx-kanban-avatar-bg: var(--zx-color-tone-slate-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-slate-fg); }
|
|
799
|
+
|
|
800
|
+
.zx-kanban-view__avatar + .zx-kanban-view__avatar {
|
|
801
|
+
margin-inline-start: calc(var(--zx-space-2) * -1);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.zx-kanban-view__avatar[data-overflow="true"] {
|
|
805
|
+
background: var(--zx-color-bg-current);
|
|
806
|
+
color: var(--zx-color-text-muted);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.zx-kanban-view__avatar-image {
|
|
810
|
+
inline-size: 100%;
|
|
811
|
+
block-size: 100%;
|
|
812
|
+
object-fit: cover;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* Avatar's initials at the small size: 0.4 of the face, never under 10px, semibold. */
|
|
816
|
+
.zx-kanban-view__avatar-initials {
|
|
817
|
+
font-size: max(10px, calc(1.5rem * .4));
|
|
818
|
+
font-weight: var(--zx-weight-semibold);
|
|
819
|
+
letter-spacing: 0;
|
|
820
|
+
line-height: 1;
|
|
821
|
+
text-transform: uppercase;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* The overflow chip is a count, not a person: it keeps the text size it can be read at. */
|
|
825
|
+
.zx-kanban-view__avatar[data-overflow="true"] .zx-kanban-view__avatar-initials {
|
|
826
|
+
font-size: var(--zx-text-xs);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/* ── Move handle ──────────────────────────────────────────────────────────────────────────── */
|
|
830
|
+
|
|
831
|
+
/* The card is the pointer drag source, so the handle is keyboard furniture: announced and in tab
|
|
832
|
+
order, but out of sight and out of the layout until it takes focus or holds a grabbed card. */
|
|
833
|
+
.zx-kanban-view__move-handle[data-reveal="keyboard"] {
|
|
834
|
+
position: absolute;
|
|
835
|
+
/* base.css gives every native button a control-height floor; without resetting it the "hidden"
|
|
836
|
+
handle is still a full-height strip. */
|
|
837
|
+
inline-size: 1px;
|
|
838
|
+
block-size: 1px;
|
|
839
|
+
min-inline-size: 0;
|
|
840
|
+
min-block-size: 0;
|
|
841
|
+
overflow: hidden;
|
|
842
|
+
clip-path: inset(50%);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.zx-kanban-view__move-handle[data-reveal="keyboard"]:focus,
|
|
846
|
+
.zx-kanban-view__move-handle[data-reveal="keyboard"][aria-pressed="true"] {
|
|
847
|
+
position: static;
|
|
848
|
+
inline-size: calc(var(--zx-control-height) - var(--zx-space-1));
|
|
849
|
+
block-size: calc(var(--zx-control-height) - var(--zx-space-1));
|
|
850
|
+
min-block-size: 0;
|
|
851
|
+
overflow: visible;
|
|
852
|
+
clip-path: none;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
@media (pointer: coarse) {
|
|
856
|
+
.zx-kanban-view__move-handle[data-reveal="keyboard"]:focus,
|
|
857
|
+
.zx-kanban-view__move-handle[data-reveal="keyboard"][aria-pressed="true"] {
|
|
858
|
+
inline-size: 2.75rem;
|
|
859
|
+
block-size: 2.75rem;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/* Text that names what a terse card value measures — "Priority" before "High" — carried as real
|
|
864
|
+
content because `aria-label` is prohibited on the generic role a span reports. */
|
|
865
|
+
.zx-kanban-view__hint {
|
|
866
|
+
position: absolute;
|
|
867
|
+
inline-size: 1px;
|
|
868
|
+
block-size: 1px;
|
|
869
|
+
overflow: hidden;
|
|
870
|
+
clip-path: inset(50%);
|
|
871
|
+
white-space: nowrap;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.zx-kanban-view__flag {
|
|
875
|
+
display: inline-flex;
|
|
876
|
+
align-items: center;
|
|
877
|
+
}
|
|
878
|
+
.zx-kanban-view .zx-record-card[data-selection-trigger="card"] { cursor: pointer; }
|