@zeyos/zx 3.0.0 → 4.1.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 (106) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/DESIGN-SYSTEM.md +87 -71
  3. package/README.md +16 -37
  4. package/dist/types/components/app-icon/app-icon.d.ts +88 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
  6. package/dist/types/components/card/card.d.ts +128 -0
  7. package/dist/types/components/chart/chart.d.ts +1 -0
  8. package/dist/types/components/filter/filter-model.d.ts +87 -0
  9. package/dist/types/components/filter/filter.d.ts +331 -0
  10. package/dist/types/components/launcher/launcher.d.ts +65 -6
  11. package/dist/types/components/select/select.d.ts +97 -4
  12. package/dist/types/components/table/table.d.ts +107 -3
  13. package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
  14. package/dist/types/core/dom.d.ts +9 -1
  15. package/dist/types/core/util.d.ts +9 -0
  16. package/dist/types/index.d.ts +6 -2
  17. package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
  18. package/dist/types/zeyos/icons.d.ts +24 -2
  19. package/dist/types/zeyos/index.d.ts +3 -2
  20. package/dist/types/zeyos/launcher.d.ts +147 -0
  21. package/dist/types/zeyos/select.d.ts +41 -0
  22. package/dist/zx-zeyos.esm.js +2018 -113
  23. package/dist/zx-zeyos.esm.min.js +1 -1
  24. package/dist/zx.css +815 -16
  25. package/dist/zx.esm.js +2661 -338
  26. package/dist/zx.esm.min.js +3 -3
  27. package/dist/zx.global.js +2661 -338
  28. package/dist/zx.global.min.js +3 -3
  29. package/dist/zx.min.css +1 -1
  30. package/docs/api.json +602 -110
  31. package/docs/llms.md +181 -123
  32. package/docs/llms.txt +24 -21
  33. package/package.json +8 -17
  34. package/src/components/app-icon/app-icon.css +105 -0
  35. package/src/components/app-icon/app-icon.js +134 -0
  36. package/src/components/app-sidebar/app-sidebar.css +12 -0
  37. package/src/components/app-sidebar/app-sidebar.js +310 -110
  38. package/src/components/card/card.css +149 -0
  39. package/src/components/card/card.js +285 -0
  40. package/src/components/chart/chart.js +8 -3
  41. package/src/components/filter/filter-model.js +226 -0
  42. package/src/components/filter/filter.css +147 -0
  43. package/src/components/filter/filter.js +554 -0
  44. package/src/components/grid/grid.css +1 -1
  45. package/src/components/grid/grid.js +14 -4
  46. package/src/components/launcher/launcher.css +113 -3
  47. package/src/components/launcher/launcher.js +234 -26
  48. package/src/components/message/message.css +36 -3
  49. package/src/components/select/field-adapter.js +9 -4
  50. package/src/components/select/select.css +161 -10
  51. package/src/components/select/select.js +361 -48
  52. package/src/components/table/table.css +85 -0
  53. package/src/components/table/table.js +426 -31
  54. package/src/components/tag-picker/tag-picker.css +19 -0
  55. package/src/components/tag-picker/tag-picker.js +42 -5
  56. package/src/core/dom.js +21 -1
  57. package/src/core/util.js +20 -0
  58. package/src/index.js +9 -2
  59. package/src/{components/app-rail → internal}/app-rail.js +80 -30
  60. package/src/zeyos/icons.js +38 -20
  61. package/src/zeyos/index.js +8 -2
  62. package/src/zeyos/launcher.js +338 -0
  63. package/src/zeyos/modules.js +1 -0
  64. package/src/zeyos/select.js +116 -1
  65. package/styles/tokens/global.css +1 -1
  66. package/styles/tokens/modules.css +1 -0
  67. package/styles/tokens/semantic.css +9 -0
  68. package/styles/tokens/themes.css +2 -2
  69. package/styles/zx.css +3 -0
  70. package/MIGRATION.md +0 -749
  71. package/dist/types/compat/base.d.ts +0 -156
  72. package/dist/types/compat/globals.d.ts +0 -20
  73. package/dist/types/compat/index.d.ts +0 -128
  74. package/dist/types/compat/map/basic.d.ts +0 -175
  75. package/dist/types/compat/map/data.d.ts +0 -103
  76. package/dist/types/compat/map/date-time.d.ts +0 -80
  77. package/dist/types/compat/map/factory.d.ts +0 -9
  78. package/dist/types/compat/map/form.d.ts +0 -98
  79. package/dist/types/compat/map/helpers.d.ts +0 -12
  80. package/dist/types/compat/map/http.d.ts +0 -36
  81. package/dist/types/compat/map/layout.d.ts +0 -106
  82. package/dist/types/compat/map/options.d.ts +0 -80
  83. package/dist/types/compat/map/select.d.ts +0 -95
  84. package/dist/types/compat/map/stubs.d.ts +0 -32
  85. package/dist/types/compat/map/utilities.d.ts +0 -37
  86. package/dist/types/compat-entry.d.ts +0 -5
  87. package/dist/zx-compat.esm.js +0 -17848
  88. package/dist/zx-compat.esm.min.js +0 -1
  89. package/dist/zx-compat.global.js +0 -29008
  90. package/dist/zx-compat.global.min.js +0 -3
  91. package/src/compat/base.js +0 -291
  92. package/src/compat/globals.js +0 -167
  93. package/src/compat/index.js +0 -84
  94. package/src/compat/map/basic.js +0 -464
  95. package/src/compat/map/data.js +0 -254
  96. package/src/compat/map/date-time.js +0 -177
  97. package/src/compat/map/factory.js +0 -45
  98. package/src/compat/map/form.js +0 -330
  99. package/src/compat/map/helpers.js +0 -46
  100. package/src/compat/map/http.js +0 -117
  101. package/src/compat/map/layout.js +0 -256
  102. package/src/compat/map/options.js +0 -267
  103. package/src/compat/map/select.js +0 -198
  104. package/src/compat/map/stubs.js +0 -26
  105. package/src/compat/map/utilities.js +0 -113
  106. package/src/compat-entry.js +0 -11
@@ -0,0 +1,226 @@
1
+ import { uid } from '../../core/util.js';
2
+
3
+ /** @typedef {null|boolean|number|string|FilterJson[]|{[key:string]: FilterJson}} FilterJson */
4
+ /** @typedef {{kind:'condition', id:string, field:string, operator:string, value:FilterJson}} FilterCondition */
5
+ /** @typedef {{kind:'group', id:string, logic:'and'|'or', children:FilterNode[]}} FilterGroup */
6
+ /** @typedef {FilterCondition|FilterGroup} FilterNode */
7
+ /** @typedef {{version:1, root:FilterGroup}} FilterAst */
8
+ /** @typedef {{id:string, label:string, arity:'none'|'single'|'pair'|'many', types:string[]}} FilterOperator */
9
+ /** @typedef {{id:string, label:string, type:string, operators?:string[], defaultOperator?:string}} FilterField */
10
+ /** @typedef {{nodeId:string, code:string, message:string}} FilterValidationError */
11
+
12
+ /** Core operators are application-neutral IDs; an adapter maps them to executable query syntax. */
13
+ export const filterOperators = Object.freeze([
14
+ op('contains', 'contains', 'single', ['text']),
15
+ op('notContains', 'does not contain', 'single', ['text']),
16
+ op('eq', 'is', 'single', ['text', 'number', 'money', 'boolean', 'enum', 'status', 'priority', 'country', 'currency', 'unit', 'entity', 'date', 'datetime']),
17
+ op('neq', 'is not', 'single', ['text', 'number', 'money', 'boolean', 'enum', 'status', 'priority', 'country', 'currency', 'unit', 'entity', 'date', 'datetime']),
18
+ op('startsWith', 'starts with', 'single', ['text']),
19
+ op('endsWith', 'ends with', 'single', ['text']),
20
+ op('lt', 'is less than', 'single', ['number', 'money']),
21
+ op('lte', 'is at most', 'single', ['number', 'money']),
22
+ op('gt', 'is greater than', 'single', ['number', 'money']),
23
+ op('gte', 'is at least', 'single', ['number', 'money']),
24
+ op('on', 'is on', 'single', ['date', 'datetime']),
25
+ op('notOn', 'is not on', 'single', ['date', 'datetime']),
26
+ op('before', 'is before', 'single', ['date', 'datetime']),
27
+ op('onOrBefore', 'is on or before', 'single', ['date', 'datetime']),
28
+ op('after', 'is after', 'single', ['date', 'datetime']),
29
+ op('onOrAfter', 'is on or after', 'single', ['date', 'datetime']),
30
+ op('between', 'is between', 'pair', ['number', 'money', 'date', 'datetime']),
31
+ op('notBetween', 'is not between', 'pair', ['number', 'money', 'date', 'datetime']),
32
+ op('withinLast', 'is within the last', 'pair', ['date', 'datetime']),
33
+ op('withinNext', 'is within the next', 'pair', ['date', 'datetime']),
34
+ op('anyOf', 'is any of', 'many', ['enum', 'status', 'priority', 'country', 'currency', 'unit', 'entity']),
35
+ op('noneOf', 'is none of', 'many', ['enum', 'status', 'priority', 'country', 'currency', 'unit', 'entity']),
36
+ op('containsAny', 'contains any', 'many', ['tags']),
37
+ op('containsAll', 'contains all', 'many', ['tags']),
38
+ op('containsNone', 'contains none', 'many', ['tags']),
39
+ op('isEmpty', 'is empty', 'none', ['text', 'number', 'money', 'date', 'datetime', 'boolean', 'enum', 'status', 'priority', 'country', 'currency', 'unit', 'entity', 'tags']),
40
+ op('isNotEmpty', 'is not empty', 'none', ['text', 'number', 'money', 'date', 'datetime', 'boolean', 'enum', 'status', 'priority', 'country', 'currency', 'unit', 'entity', 'tags'])
41
+ ]);
42
+
43
+ /** @returns {FilterAst} */
44
+ export function emptyFilterAst() {
45
+ return { version: 1, root: { kind: 'group', id: uid('filter-root'), logic: 'and', children: [] } };
46
+ }
47
+
48
+ /** @param {Partial<FilterCondition>} [initial={}] @returns {FilterCondition} */
49
+ export function filterCondition(initial = {}) {
50
+ return {
51
+ kind: 'condition',
52
+ id: String(initial.id || uid('filter-condition')),
53
+ field: String(initial.field ?? ''),
54
+ operator: String(initial.operator ?? ''),
55
+ value: cloneFilterValue(initial.value ?? null)
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Returns a defensive JSON-safe copy suitable for a condition value.
61
+ * @param {unknown} value
62
+ * @returns {FilterJson}
63
+ */
64
+ export function cloneFilterValue(value) {
65
+ return cloneJson(value);
66
+ }
67
+
68
+ /** @param {'and'|'or'} [logic='and'] @param {FilterNode[]} [children=[]] @returns {FilterGroup} */
69
+ export function filterGroup(logic = 'and', children = []) {
70
+ return { kind: 'group', id: uid('filter-group'), logic, children };
71
+ }
72
+
73
+ /**
74
+ * Parses untrusted state into a defensive JSON-safe AST. It rejects structural corruption but
75
+ * deliberately keeps unknown field/operator IDs for semantic validation and migration UI.
76
+ * @param {unknown} value
77
+ * @param {{maxDepth?:number, maxConditions?:number}} [limits={}]
78
+ * @returns {FilterAst}
79
+ */
80
+ export function parseFilterAst(value, limits = {}) {
81
+ const maxDepth = finiteLimit(limits.maxDepth, 3);
82
+ const maxConditions = finiteLimit(limits.maxConditions, 50);
83
+ if (!plainObject(value) || value.version !== 1 || !plainObject(value.root)) {
84
+ throw new TypeError('Filter AST must have version 1 and a root group');
85
+ }
86
+ const ids = new Set();
87
+ let conditions = 0;
88
+ const visit = (raw, depth, root = false) => {
89
+ if (!plainObject(raw)) throw new TypeError('Filter nodes must be plain objects');
90
+ const id = String(raw.id ?? '').trim();
91
+ if (!id || ids.has(id)) throw new TypeError('Filter node IDs must be non-empty and unique');
92
+ ids.add(id);
93
+ if (raw.kind === 'group') {
94
+ if (depth > maxDepth) throw new RangeError('Filter group depth exceeds maxDepth');
95
+ if (raw.logic !== 'and' && raw.logic !== 'or') throw new TypeError('Filter group logic must be and or or');
96
+ if (!Array.isArray(raw.children)) throw new TypeError('Filter group children must be an array');
97
+ return {
98
+ kind: 'group', id, logic: raw.logic,
99
+ children: raw.children.map((child) => visit(child, depth + 1, false))
100
+ };
101
+ }
102
+ if (root || raw.kind !== 'condition') throw new TypeError('Filter root must be a group');
103
+ conditions += 1;
104
+ if (conditions > maxConditions) throw new RangeError('Filter condition count exceeds maxConditions');
105
+ return {
106
+ kind: 'condition', id,
107
+ field: String(raw.field ?? ''), operator: String(raw.operator ?? ''),
108
+ value: cloneJson(raw.value)
109
+ };
110
+ };
111
+ const root = visit(value.root, 0, true);
112
+ if (root.kind !== 'group') throw new TypeError('Filter root must be a group');
113
+ return /** @type {FilterAst} */ ({ version: 1, root });
114
+ }
115
+
116
+ /** @param {FilterAst} value @returns {FilterAst} */
117
+ export function cloneFilterAst(value) {
118
+ return parseFilterAst(value, { maxDepth: Infinity, maxConditions: Infinity });
119
+ }
120
+
121
+ /** @param {FilterAst} value @returns {string} */
122
+ export function stringifyFilterAst(value) {
123
+ return JSON.stringify(cloneFilterAst(value));
124
+ }
125
+
126
+ /**
127
+ * Performs field/operator/arity validation without compiling or executing the expression.
128
+ * @param {FilterAst} ast
129
+ * @param {FilterField[]} fields
130
+ * @param {FilterOperator[]} [operators=filterOperators]
131
+ * @returns {{valid:boolean, errors:FilterValidationError[]}}
132
+ */
133
+ export function validateFilterAst(ast, fields, operators = filterOperators) {
134
+ const errors = [];
135
+ const fieldMap = new Map((Array.isArray(fields) ? fields : []).map((field) => [String(field.id), field]));
136
+ const operatorMap = new Map((Array.isArray(operators) ? operators : []).map((operator) => [String(operator.id), operator]));
137
+ const visit = (node, root = false) => {
138
+ if (node.kind === 'group') {
139
+ if (!root && node.children.length === 0) errors.push(error(node.id, 'empty-group', 'Nested groups need at least one condition'));
140
+ node.children.forEach((child) => visit(child));
141
+ return;
142
+ }
143
+ const field = fieldMap.get(node.field);
144
+ if (!field) errors.push(error(node.id, 'unknown-field', node.field ? `Unavailable field: ${node.field}` : 'Choose a field'));
145
+ const operator = operatorMap.get(node.operator);
146
+ if (!operator) {
147
+ errors.push(error(node.id, 'unknown-operator', node.operator ? `Unavailable operator: ${node.operator}` : 'Choose an operator'));
148
+ return;
149
+ }
150
+ if (field) {
151
+ const allowed = Array.isArray(field.operators) && field.operators.length
152
+ ? field.operators : operators.filter((candidate) => candidate.types.includes(field.type)).map((candidate) => candidate.id);
153
+ if (!allowed.includes(operator.id) || !operator.types.includes(field.type)) {
154
+ errors.push(error(node.id, 'operator-type', `${operator.label} is not available for ${field.label}`));
155
+ }
156
+ }
157
+ if (!validArity(operator.arity, node.value)) {
158
+ errors.push(error(node.id, 'value', operator.arity === 'pair' ? 'Enter both values'
159
+ : operator.arity === 'many' ? 'Choose at least one value'
160
+ : operator.arity === 'none' ? 'This operator does not accept a value' : 'Enter a value'));
161
+ }
162
+ };
163
+ visit(ast.root, true);
164
+ return { valid: errors.length === 0, errors };
165
+ }
166
+
167
+ /** @param {string} id @param {string} label @param {FilterOperator['arity']} arity @param {string[]} types @returns {FilterOperator} */
168
+ function op(id, label, arity, types) { return Object.freeze({ id, label, arity, types: Object.freeze(types) }); }
169
+
170
+ /** @param {string} nodeId @param {string} code @param {string} message @returns {FilterValidationError} */
171
+ function error(nodeId, code, message) { return { nodeId, code, message }; }
172
+
173
+ /** @param {FilterOperator['arity']} arity @param {FilterJson} value @returns {boolean} */
174
+ function validArity(arity, value) {
175
+ if (arity === 'none') return value === null;
176
+ if (arity === 'pair') return Array.isArray(value) && value.length === 2 && value.every(filled);
177
+ if (arity === 'many') return Array.isArray(value) && value.length > 0 && value.every(filled);
178
+ return filled(value);
179
+ }
180
+
181
+ /** @param {unknown} value @returns {boolean} */
182
+ function filled(value) { return value !== null && value !== undefined && !(typeof value === 'string' && value.trim() === ''); }
183
+
184
+ /** @param {unknown} value @param {WeakSet<object>} [seen=new WeakSet()] @returns {FilterJson} */
185
+ function cloneJson(value, seen = new WeakSet()) {
186
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
187
+ if (typeof value === 'number') {
188
+ if (!Number.isFinite(value)) throw new TypeError('Filter values cannot contain non-finite numbers');
189
+ return value;
190
+ }
191
+ if (typeof value !== 'object' || value instanceof Date || typeof value.nodeType === 'number') {
192
+ throw new TypeError('Filter values must be JSON-safe');
193
+ }
194
+ if (seen.has(value)) throw new TypeError('Filter values cannot contain cycles');
195
+ seen.add(value);
196
+ if (Array.isArray(value)) {
197
+ const result = value.map((item) => cloneJson(item, seen));
198
+ seen.delete(value);
199
+ return result;
200
+ }
201
+ if (!plainObject(value)) throw new TypeError('Filter values must be plain JSON objects');
202
+ const result = {};
203
+ for (const [key, item] of Object.entries(value)) {
204
+ // Assignment to `__proto__` is special on ordinary objects. Defining an own data property
205
+ // preserves the JSON member without changing the clone's prototype.
206
+ Object.defineProperty(result, key, {
207
+ value: cloneJson(item, seen), enumerable: true, configurable: true, writable: true
208
+ });
209
+ }
210
+ seen.delete(value);
211
+ return result;
212
+ }
213
+
214
+ /** @param {unknown} value @returns {boolean} */
215
+ function plainObject(value) {
216
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
217
+ const prototype = Object.getPrototypeOf(value);
218
+ return prototype === Object.prototype || prototype === null;
219
+ }
220
+
221
+ /** @param {unknown} value @param {number} fallback @returns {number} */
222
+ function finiteLimit(value, fallback) {
223
+ if (value === Infinity) return Infinity;
224
+ const number = Number(value);
225
+ return Number.isFinite(number) && number >= 0 ? Math.floor(number) : fallback;
226
+ }
@@ -0,0 +1,147 @@
1
+ .zx-filter {
2
+ display: grid;
3
+ gap: var(--zx-space-3);
4
+ color: var(--zx-color-text);
5
+ }
6
+
7
+ .zx-filter__status {
8
+ position: absolute;
9
+ inline-size: 1px;
10
+ block-size: 1px;
11
+ overflow: hidden;
12
+ clip-path: inset(50%);
13
+ }
14
+
15
+ .zx-filter__group,
16
+ .zx-filter__group > fieldset {
17
+ min-inline-size: 0;
18
+ margin: 0;
19
+ border: 1px solid var(--zx-color-border);
20
+ border-radius: var(--zx-radius-lg);
21
+ background: var(--zx-color-bg-surface);
22
+ padding: var(--zx-space-3);
23
+ }
24
+
25
+ .zx-filter__group .zx-filter__group {
26
+ border: 0;
27
+ border-inline-start: 3px solid var(--zx-color-border-strong);
28
+ border-radius: 0;
29
+ background: transparent;
30
+ padding: 0 0 0 var(--zx-space-3);
31
+ }
32
+
33
+ .zx-filter__legend {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ gap: var(--zx-space-2);
37
+ padding-inline: var(--zx-space-1);
38
+ color: var(--zx-color-text-muted);
39
+ font-size: var(--zx-text-sm);
40
+ }
41
+
42
+ .zx-filter__logic,
43
+ .zx-filter__field,
44
+ .zx-filter__operator,
45
+ .zx-filter__value input,
46
+ .zx-filter__value select {
47
+ min-block-size: var(--zx-control-height);
48
+ border: 1px solid var(--zx-color-border-control);
49
+ border-radius: var(--zx-control-radius);
50
+ background: var(--zx-color-bg-control);
51
+ padding-inline: var(--zx-control-pad-x);
52
+ color: var(--zx-color-text);
53
+ font: inherit;
54
+ }
55
+
56
+ .zx-filter__logic {
57
+ inline-size: auto;
58
+ min-block-size: 28px;
59
+ font-size: var(--zx-text-sm);
60
+ }
61
+
62
+ .zx-filter__children {
63
+ display: grid;
64
+ gap: var(--zx-space-2);
65
+ margin: var(--zx-space-2) 0;
66
+ padding: 0;
67
+ list-style: none;
68
+ }
69
+
70
+ .zx-filter__condition {
71
+ display: grid;
72
+ grid-template-columns: minmax(9rem, 1fr) minmax(9rem, 1fr) minmax(10rem, 1.4fr) auto;
73
+ align-items: start;
74
+ gap: var(--zx-space-2);
75
+ border-radius: var(--zx-radius-md);
76
+ }
77
+
78
+ .zx-filter__condition[data-invalid] {
79
+ outline: 1px solid var(--zx-color-danger);
80
+ outline-offset: var(--zx-space-1);
81
+ }
82
+
83
+ .zx-filter__value {
84
+ display: grid;
85
+ grid-template-columns: repeat(2, minmax(0, 1fr));
86
+ gap: var(--zx-space-2);
87
+ min-inline-size: 0;
88
+ }
89
+
90
+ .zx-filter__value-control,
91
+ .zx-filter__value-control > input {
92
+ inline-size: 100%;
93
+ min-inline-size: 0;
94
+ }
95
+
96
+ .zx-filter__value > :only-child,
97
+ .zx-filter__value--empty {
98
+ grid-column: 1 / -1;
99
+ }
100
+
101
+ .zx-filter__value--empty {
102
+ display: flex;
103
+ align-items: center;
104
+ min-block-size: var(--zx-control-height);
105
+ padding-inline: var(--zx-control-pad-x);
106
+ color: var(--zx-color-text-muted);
107
+ font-size: var(--zx-text-sm);
108
+ }
109
+
110
+ .zx-filter__group-actions,
111
+ .zx-filter__actions {
112
+ display: flex;
113
+ flex-wrap: wrap;
114
+ gap: var(--zx-space-2);
115
+ }
116
+
117
+ .zx-filter__actions {
118
+ justify-content: flex-end;
119
+ }
120
+
121
+ .zx-filter[data-readonly] .zx-filter__group-actions,
122
+ .zx-filter[data-readonly] .zx-filter__actions,
123
+ .zx-filter[data-disabled] .zx-filter__group-actions,
124
+ .zx-filter[data-disabled] .zx-filter__actions {
125
+ display: none;
126
+ }
127
+
128
+ @media (max-width: 680px) {
129
+ .zx-filter__condition {
130
+ grid-template-columns: minmax(0, 1fr);
131
+ }
132
+
133
+ .zx-filter__group .zx-filter__group {
134
+ padding-inline-start: var(--zx-space-2);
135
+ }
136
+
137
+ .zx-filter__value {
138
+ grid-template-columns: minmax(0, 1fr);
139
+ }
140
+ }
141
+
142
+ @media (forced-colors: active) {
143
+ .zx-filter__group,
144
+ .zx-filter__condition[data-invalid] {
145
+ border-color: CanvasText;
146
+ }
147
+ }