@fusion.dev/helix-headless 0.22.0 → 0.24.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 (103) hide show
  1. package/custom-elements.json +48397 -26631
  2. package/dist/announcement/root/announcement-root.d.ts +19 -5
  3. package/dist/area-chart/index.d.ts +1 -0
  4. package/dist/area-chart/index.js +1 -0
  5. package/dist/area-chart/root/area-chart-root.d.ts +182 -0
  6. package/dist/area-chart/root/area-chart-root.js +205 -0
  7. package/dist/carousel/root/carousel-root.d.ts +1 -0
  8. package/dist/carousel/root/carousel-root.js +18 -2
  9. package/dist/chat/index.d.ts +1 -1
  10. package/dist/chat/message/chat-message.d.ts +7 -0
  11. package/dist/chat/message/chat-message.js +2 -1
  12. package/dist/chat/root/chat-root.d.ts +195 -4
  13. package/dist/chat/root/chat-root.js +316 -23
  14. package/dist/checkbox-group/root/checkbox-group-root.d.ts +2 -0
  15. package/dist/checkbox-group/root/checkbox-group-root.js +12 -2
  16. package/dist/code-block/index.d.ts +1 -0
  17. package/dist/code-block/index.js +1 -0
  18. package/dist/code-block/patch.d.ts +62 -0
  19. package/dist/code-block/patch.js +93 -0
  20. package/dist/code-block/root/code-block-root.d.ts +82 -17
  21. package/dist/code-block/root/code-block-root.js +83 -6
  22. package/dist/color-picker/root/color-picker-root.d.ts +4 -3
  23. package/dist/color-picker/root/color-picker-root.js +4 -1
  24. package/dist/data-table/root/data-table-root.d.ts +195 -23
  25. package/dist/data-table/root/data-table-root.js +308 -27
  26. package/dist/description-list/index.d.ts +2 -0
  27. package/dist/description-list/index.js +2 -0
  28. package/dist/description-list/item/description-list-item.d.ts +39 -0
  29. package/dist/description-list/item/description-list-item.js +26 -0
  30. package/dist/description-list/root/description-list-root.d.ts +56 -0
  31. package/dist/description-list/root/description-list-root.js +21 -0
  32. package/dist/element-events.generated.js +27 -6
  33. package/dist/filter-input/chip/filter-chip.d.ts +114 -0
  34. package/dist/filter-input/chip/filter-chip.js +61 -0
  35. package/dist/filter-input/field/filter-field.d.ts +128 -0
  36. package/dist/filter-input/field/filter-field.js +70 -0
  37. package/dist/filter-input/grammar.d.ts +96 -0
  38. package/dist/filter-input/grammar.js +133 -0
  39. package/dist/filter-input/index.d.ts +5 -0
  40. package/dist/filter-input/index.js +5 -0
  41. package/dist/filter-input/menu/filter-menu.d.ts +153 -0
  42. package/dist/filter-input/menu/filter-menu.js +164 -0
  43. package/dist/filter-input/operator/filter-operator.d.ts +105 -0
  44. package/dist/filter-input/operator/filter-operator.js +39 -0
  45. package/dist/filter-input/root/filter-input-root.d.ts +118 -9
  46. package/dist/filter-input/root/filter-input-root.js +96 -2
  47. package/dist/format-date/root/format-date-root.js +1 -1
  48. package/dist/index.d.ts +5 -0
  49. package/dist/index.js +5 -0
  50. package/dist/line-chart/root/line-chart-root.d.ts +9 -2
  51. package/dist/listbox/root/listbox-root.d.ts +8 -7
  52. package/dist/listbox/root/listbox-root.js +7 -2
  53. package/dist/menu/item/menu-item.d.ts +54 -6
  54. package/dist/menu/item/menu-item.js +31 -6
  55. package/dist/menu/root/menu-root.d.ts +23 -0
  56. package/dist/menu/root/menu-root.js +13 -2
  57. package/dist/notifications/group/notification-group.d.ts +129 -0
  58. package/dist/notifications/group/notification-group.js +106 -0
  59. package/dist/notifications/index.d.ts +1 -0
  60. package/dist/notifications/index.js +1 -0
  61. package/dist/notifications/notification/notification.d.ts +39 -13
  62. package/dist/notifications/notification/notification.js +11 -8
  63. package/dist/notifications/root/notifications-root.d.ts +57 -1
  64. package/dist/notifications/root/notifications-root.js +35 -4
  65. package/dist/number-input/root/number-input-root.d.ts +21 -1
  66. package/dist/number-input/root/number-input-root.js +18 -5
  67. package/dist/object-inspector/index.d.ts +1 -0
  68. package/dist/object-inspector/index.js +1 -0
  69. package/dist/object-inspector/root/object-inspector-root.d.ts +183 -0
  70. package/dist/object-inspector/root/object-inspector-root.js +348 -0
  71. package/dist/outline/root/outline-root.d.ts +16 -1
  72. package/dist/outline/root/outline-root.js +27 -9
  73. package/dist/radio/group/radio-group.d.ts +1 -0
  74. package/dist/scatter-chart/index.d.ts +1 -0
  75. package/dist/scatter-chart/index.js +1 -0
  76. package/dist/scatter-chart/root/scatter-chart-root.d.ts +144 -0
  77. package/dist/scatter-chart/root/scatter-chart-root.js +205 -0
  78. package/dist/skip-link/index.d.ts +1 -0
  79. package/dist/skip-link/index.js +1 -0
  80. package/dist/skip-link/root/skip-link-root.d.ts +81 -0
  81. package/dist/skip-link/root/skip-link-root.js +58 -0
  82. package/dist/speed-dial/root/speed-dial-root.d.ts +1 -0
  83. package/dist/speed-dial/root/speed-dial-root.js +6 -2
  84. package/dist/steps/root/steps-root.d.ts +1 -0
  85. package/dist/table/column/table-column.d.ts +79 -0
  86. package/dist/table/column/table-column.js +125 -5
  87. package/dist/table/root/table-root.d.ts +2 -0
  88. package/dist/table/root/table-root.js +12 -3
  89. package/dist/time-picker/root/time-picker-root.d.ts +9 -0
  90. package/dist/time-picker/root/time-picker-root.js +5 -0
  91. package/dist/toast/root/toast-root.d.ts +2 -0
  92. package/dist/tooltip/root/tooltip-root.js +1 -1
  93. package/dist/tour/root/tour-root.d.ts +1 -0
  94. package/dist/tree/item/tree-item.d.ts +7 -4
  95. package/dist/tree/item/tree-item.js +4 -1
  96. package/dist/video/root/video-root.d.ts +23 -1
  97. package/dist/video/root/video-root.js +42 -22
  98. package/dist/virtual-list/root/virtual-list-root.d.ts +10 -6
  99. package/dist/virtual-list/root/virtual-list-root.js +10 -4
  100. package/package.json +5 -5
  101. package/types/jsx.d.ts +4535 -346
  102. package/types/svelte.d.ts +11 -0
  103. package/types/vue.d.ts +737 -39
@@ -0,0 +1,133 @@
1
+ export const DEFAULT_FILTER_OPERATORS = [
2
+ { arity: 'many', default: true, label: 'is any of', name: 'any' },
3
+ { arity: 'many', default: false, label: 'is none of', name: 'none' },
4
+ ];
5
+ const NEGATION = 'none';
6
+ const same = (written, declared) => declared !== '' && written.toLowerCase() === declared.trim().toLowerCase();
7
+ const unquote = (piece) => piece.length > 1 && piece.startsWith('"') && piece.endsWith('"') ? piece.slice(1, -1) : piece;
8
+ function split(text, separator) {
9
+ const parts = [];
10
+ let current = '';
11
+ let quoted = false;
12
+ for (const char of text) {
13
+ if (char === '"') {
14
+ quoted = !quoted;
15
+ current += char;
16
+ continue;
17
+ }
18
+ if (!quoted && (separator === ' ' ? /\s/.test(char) : char === separator)) {
19
+ if (current !== '') {
20
+ parts.push(current);
21
+ }
22
+ current = '';
23
+ continue;
24
+ }
25
+ current += char;
26
+ }
27
+ if (current !== '') {
28
+ parts.push(current);
29
+ }
30
+ return parts;
31
+ }
32
+ function atColon(term) {
33
+ let quoted = false;
34
+ for (let at = 0; at < term.length; at += 1) {
35
+ const char = term[at];
36
+ if (char === '"') {
37
+ quoted = !quoted;
38
+ continue;
39
+ }
40
+ if (!quoted && char === ':') {
41
+ return [term.slice(0, at), term.slice(at + 1)];
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ function readPiece(written, fields) {
47
+ const negated = written.startsWith('-');
48
+ const parts = atColon(negated ? written.slice(1) : written);
49
+ if (!parts) {
50
+ return null;
51
+ }
52
+ const key = unquote(parts[0]);
53
+ const field = fields.find((one) => same(key, one.name) || same(key, one.label));
54
+ if (!field) {
55
+ return null;
56
+ }
57
+ const operator = negated
58
+ ? field.operators.find((one) => one.name === NEGATION)
59
+ : (field.operators.find((one) => one.default) ?? field.operators[0]);
60
+ if (!operator || operator.arity === 'none') {
61
+ return null;
62
+ }
63
+ const values = [];
64
+ for (const raw of split(parts[1], ',').map(unquote)) {
65
+ if (raw === '') {
66
+ continue;
67
+ }
68
+ if (field.values.length === 0) {
69
+ values.push(raw);
70
+ continue;
71
+ }
72
+ const found = field.values.find((one) => same(raw, one.value) || same(raw, one.label));
73
+ if (!found) {
74
+ return null;
75
+ }
76
+ values.push(found.value);
77
+ }
78
+ if (values.length === 0) {
79
+ return null;
80
+ }
81
+ if (operator.arity === 'one' && values.length > 1) {
82
+ return null;
83
+ }
84
+ return { field, operator, values };
85
+ }
86
+ export function readFilterText(text, fields) {
87
+ const order = [];
88
+ const held = new Map();
89
+ const spare = [];
90
+ for (const written of split(text, ' ')) {
91
+ const read = readPiece(written, fields);
92
+ if (!read) {
93
+ spare.push(written);
94
+ continue;
95
+ }
96
+ const standing = held.get(read.field.name);
97
+ if (standing && standing.operator.name !== read.operator.name) {
98
+ spare.push(written);
99
+ continue;
100
+ }
101
+ if (standing) {
102
+ for (const value of read.values) {
103
+ if (!standing.values.includes(value)) {
104
+ standing.values.push(value);
105
+ }
106
+ }
107
+ standing.written.push(written);
108
+ continue;
109
+ }
110
+ const term = {
111
+ field: read.field,
112
+ operator: read.operator,
113
+ values: [...new Set(read.values)],
114
+ written: [written],
115
+ };
116
+ order.push(term);
117
+ held.set(read.field.name, term);
118
+ }
119
+ return {
120
+ terms: order.map((term) => ({
121
+ facet: {
122
+ arity: term.operator.arity,
123
+ field: term.field.name,
124
+ label: term.field.label,
125
+ operator: term.operator.name,
126
+ operatorLabel: term.operator.default ? '' : term.operator.label,
127
+ values: term.values,
128
+ },
129
+ written: term.written,
130
+ })),
131
+ text: spare.join(' '),
132
+ };
133
+ }
@@ -1 +1,6 @@
1
+ export * from './chip/filter-chip.js';
2
+ export * from './field/filter-field.js';
3
+ export * from './grammar.js';
4
+ export * from './menu/filter-menu.js';
5
+ export * from './operator/filter-operator.js';
1
6
  export * from './root/filter-input-root.js';
@@ -1 +1,6 @@
1
+ export * from './chip/filter-chip.js';
2
+ export * from './field/filter-field.js';
3
+ export * from './grammar.js';
4
+ export * from './menu/filter-menu.js';
5
+ export * from './operator/filter-operator.js';
1
6
  export * from './root/filter-input-root.js';
@@ -0,0 +1,153 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
+ import type { HelixHeadlessFilterField } from '../field/filter-field.js';
4
+ import type { FilterGrammarOperator } from '../grammar.js';
5
+ import '../../divider/index.js';
6
+ import '../../menu/index.js';
7
+ /**
8
+ * @summary The control that builds a filter: pick a field, tick its values.
9
+ *
10
+ * @description
11
+ * **It reads the declarations and renders a menu.** A consumer writes their
12
+ * fields as `FilterField`s inside this, and what comes out is a two level menu:
13
+ * the fields, and under each one its comparisons and its values. Nothing here
14
+ * knows what a field means, which is the line `FilterField` already draws.
15
+ *
16
+ * ```html
17
+ * <helix-headless-filter-menu label="Filter">
18
+ * <helix-headless-filter-field name="disposition" label="Disposition">
19
+ * <helix-headless-option value="refunded">Refunded</helix-headless-option>
20
+ * </helix-headless-filter-field>
21
+ * </helix-headless-filter-menu>
22
+ * ```
23
+ *
24
+ * That field declares no comparisons and so is offered `is any of` and `is none
25
+ * of`, which are the two the typed grammar can say. The menu and the text end up
26
+ * offering the same filters because both read `FilterField.vocabulary`, and a
27
+ * consumer whose field needs other words declares them there.
28
+ *
29
+ * **A tick is a filter, immediately.** There is no Apply: ticking a value
30
+ * announces the field's whole set as it now stands, so a queue narrows as the
31
+ * reader works rather than after they have finished. Unticking the last value
32
+ * announces an empty set, which is a consumer's cue to drop the chip.
33
+ *
34
+ * **The operator is chosen here and written nowhere else.** `helix-facet` carries
35
+ * `operatorLabel` empty when the comparison is the field's default, which is
36
+ * exactly what a chip's `operator-label` wants: the segment appears only where
37
+ * somebody picked something worth noticing. Deciding it here is the point, since
38
+ * this is the only thing that has read both the field and its operators.
39
+ *
40
+ * **How many values a comparison takes decides what is under it.** A field
41
+ * whose comparison is `arity="none"`, `is empty` or `is set`, is offered no
42
+ * values at all and gives back any that were ticked: the comparison is the whole
43
+ * filter. One at `arity="one"` offers its values as a single choice, so pressing
44
+ * another replaces it and pressing the one already on clears it. Both announce
45
+ * `arity` in the facet, which is what lets a consumer tell an empty filter from
46
+ * no filter at all.
47
+ *
48
+ * **It applies nothing.** The chips are the consumer's markup and the rows are
49
+ * their data, so this announces and stops, the way `Notification` reports a read
50
+ * rather than inventing one. Refusing `helix-facet` refuses the change.
51
+ *
52
+ * @since 0.25.0
53
+ * @status experimental
54
+ * @category forms
55
+ * @aliases FacetPicker, FilterBuilder
56
+ * @documentation https://helix.fusion.dev/components/filter-input
57
+ *
58
+ * @slot - The fields this can filter by, as `FilterField`s. Read rather than rendered.
59
+ * @slot trigger - The control that opens the menu. A `Button` is the usual one.
60
+ *
61
+ * @event {HelixFacetEvent} helix-facet - Fired when a field's values change. Cancelable: refusing it takes the decision and nothing moves.
62
+ */
63
+ export declare class HelixHeadlessFilterMenu extends HelixElement {
64
+ /**
65
+ * Accessible name for the menu.
66
+ */
67
+ label: string;
68
+ /**
69
+ * What is ticked, per field.
70
+ *
71
+ * **Held here rather than read back off the menu**, because the menu's rows
72
+ * are rebuilt every time a page changes and a tick read from a row that has
73
+ * been re-rendered is a tick nobody made. This is the answer; the rows are a
74
+ * view of it.
75
+ */
76
+ private readonly picked;
77
+ /** Which comparison each field is using, where the reader chose one. */
78
+ private readonly comparisons;
79
+ /**
80
+ * The fields declared inside this, in the order they were written.
81
+ *
82
+ * What a render may know of them: none on a server, whose shim has no tree
83
+ * and threw here, and none while hydrating what one sent.
84
+ */
85
+ get fields(): HelixHeadlessFilterField[];
86
+ protected firstUpdated(changed: PropertyValues): void;
87
+ /** What is ticked for one field, as the consumer's own values. */
88
+ valuesFor(field: string): readonly string[];
89
+ /**
90
+ * Which comparison a field is using: the reader's choice, then the one the
91
+ * field marked, then the first one it supports.
92
+ *
93
+ * **The last of those is not a fallback for nothing, it is the scalar case.**
94
+ * A date supports `after` and nothing else and marks no default, because there
95
+ * is nothing for a default to distinguish it from. Answering `null` there left
96
+ * a chip built from it with no comparison at all: `Created` and a date, with
97
+ * the word that made it mean anything missing. It is not marked `default`, so
98
+ * it is not the unwritten one and the chip says it, which is right.
99
+ *
100
+ * Read off `vocabulary` rather than off the operator elements, so a field that
101
+ * declared none is offered the defaults here and parses them in the text, from
102
+ * one answer rather than two that can drift.
103
+ */
104
+ operatorFor(field: HelixHeadlessFilterField): FilterGrammarOperator | null;
105
+ /**
106
+ * Announces a field's set as it now stands, and reports whether it stood.
107
+ *
108
+ * The whole set rather than the one value that moved, because a filter is the
109
+ * set: a consumer redrawing a chip from a single tick would have to keep their
110
+ * own copy of everything else, which is the second source of truth this exists
111
+ * to avoid.
112
+ */
113
+ private announce;
114
+ /**
115
+ * Filters a field by these values, announcing it the way a tick does.
116
+ *
117
+ * **The one way this element's state changes**, whether the move came from a
118
+ * row here, from a filter typed into the field around it, or from a chip
119
+ * somebody dismissed. Before this existed the ticks were only ever written by
120
+ * a press, so a chip removed anywhere else left the menu still showing it
121
+ * ticked, and the reader's next press on that value read as *unticking* it:
122
+ * they pressed `Refunded`, an empty set was announced, and nothing appeared.
123
+ *
124
+ * Refusing the `helix-facet` puts everything back, so a consumer who declines
125
+ * a filter is never left looking at a menu that says it took.
126
+ */
127
+ apply(field: string, values: readonly string[], operator?: string): boolean;
128
+ /**
129
+ * A row was chosen: a value to tick, or a comparison to use.
130
+ *
131
+ * **Read off the row rather than encoded into its `value`.** Packing the three
132
+ * facts into one string and splitting them apart again was the first idea, and
133
+ * it does not survive the template: a nested template literal inside a
134
+ * `static-html` template comes back carrying Lit's own binding marker where
135
+ * the separator was, so `field disposition` arrived with a null byte in the
136
+ * middle of it and every split produced nonsense. Three data attributes cost
137
+ * nothing and there is nothing to parse.
138
+ *
139
+ * Stopped here rather than let past, because a consumer listening for
140
+ * `helix-select` on their own page should not receive one for a row inside a
141
+ * control they did not write. Only the two rows that choose something need it:
142
+ * a row that opens a page is stopped by `Menu` itself, in a capture-phase
143
+ * handler, on the ground that looking into a submenu is not a choice. Measured
144
+ * both ways, and widening this to every `data-facet` row changed nothing.
145
+ */
146
+ private readonly handleSelect;
147
+ protected render(): TemplateResult;
148
+ }
149
+ declare global {
150
+ interface HTMLElementTagNameMap {
151
+ 'helix-headless-filter-menu': HelixHeadlessFilterMenu;
152
+ }
153
+ }
@@ -0,0 +1,164 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { customElement, HelixElement, HelixFacetEvent } from '@fusion.dev/helix-core';
8
+ import { nothing } from 'lit';
9
+ import { property } from 'lit/decorators.js';
10
+ import { html } from 'lit/static-html.js';
11
+ import '../../divider/index.js';
12
+ import '../../menu/index.js';
13
+ let HelixHeadlessFilterMenu = class HelixHeadlessFilterMenu extends HelixElement {
14
+ constructor() {
15
+ super(...arguments);
16
+ this.label = 'Filter';
17
+ this.picked = new Map();
18
+ this.comparisons = new Map();
19
+ this.handleSelect = (event) => {
20
+ const row = event.composedPath()[0];
21
+ if (!(row instanceof HTMLElement)) {
22
+ return;
23
+ }
24
+ const kind = row.dataset.facet;
25
+ if (kind !== 'value' && kind !== 'operator') {
26
+ return;
27
+ }
28
+ event.stopPropagation();
29
+ const field = row.dataset.field ?? '';
30
+ const value = row.dataset.value ?? '';
31
+ if (kind === 'operator') {
32
+ this.apply(field, this.valuesFor(field), value);
33
+ return;
34
+ }
35
+ const declared = this.fields.find((one) => (one.name ?? '') === field);
36
+ const held = this.picked.get(field) ?? new Set();
37
+ if (declared && this.operatorFor(declared)?.arity === 'one') {
38
+ this.apply(field, held.has(value) && held.size === 1 ? [] : [value]);
39
+ return;
40
+ }
41
+ const ticked = new Set(held);
42
+ if (ticked.has(value)) {
43
+ ticked.delete(value);
44
+ }
45
+ else {
46
+ ticked.add(value);
47
+ }
48
+ this.apply(field, [...ticked]);
49
+ };
50
+ }
51
+ get fields() {
52
+ return this.knownChildren.filter((child) => ['helix-headless-filter-field', 'helix-filter-field'].includes(child.localName));
53
+ }
54
+ firstUpdated(changed) {
55
+ super.firstUpdated(changed);
56
+ this.finishHydration();
57
+ }
58
+ valuesFor(field) {
59
+ return [...(this.picked.get(field) ?? [])];
60
+ }
61
+ operatorFor(field) {
62
+ const { operators } = field.vocabulary;
63
+ const chosen = this.comparisons.get(field.name ?? '');
64
+ const declared = operators.find((operator) => operator.default) ?? operators[0] ?? null;
65
+ if (chosen === undefined) {
66
+ return declared;
67
+ }
68
+ return operators.find((operator) => operator.name === chosen) ?? declared;
69
+ }
70
+ announce(field) {
71
+ const operator = this.operatorFor(field);
72
+ return this.dispatchEvent(new HelixFacetEvent({
73
+ arity: operator?.arity ?? 'many',
74
+ field: field.name ?? '',
75
+ label: field.label,
76
+ operator: operator?.name ?? '',
77
+ operatorLabel: operator?.default ? '' : (operator?.label ?? ''),
78
+ values: this.valuesFor(field.name ?? ''),
79
+ }));
80
+ }
81
+ apply(field, values, operator) {
82
+ const declared = this.fields.find((one) => (one.name ?? '') === field);
83
+ if (!declared) {
84
+ return false;
85
+ }
86
+ const heldValues = this.picked.get(field);
87
+ const heldOperator = this.comparisons.get(field);
88
+ if (operator !== undefined) {
89
+ this.comparisons.set(field, operator);
90
+ }
91
+ else if (values.length === 0) {
92
+ this.comparisons.delete(field);
93
+ }
94
+ const arity = this.operatorFor(declared)?.arity ?? 'many';
95
+ this.picked.set(field, new Set(arity === 'none' ? [] : values));
96
+ const stood = this.announce(declared);
97
+ if (!stood) {
98
+ if (heldValues === undefined) {
99
+ this.picked.delete(field);
100
+ }
101
+ else {
102
+ this.picked.set(field, heldValues);
103
+ }
104
+ if (heldOperator === undefined) {
105
+ this.comparisons.delete(field);
106
+ }
107
+ else {
108
+ this.comparisons.set(field, heldOperator);
109
+ }
110
+ }
111
+ this.requestUpdate();
112
+ return stood;
113
+ }
114
+ render() {
115
+ const divider = this.tag('divider');
116
+ const menu = this.tag('menu');
117
+ const item = this.tag('menu-item');
118
+ return html `<${menu} label=${this.label} @helix-select=${this.handleSelect}>
119
+ <slot name="trigger" slot="trigger"></slot>
120
+ ${this.fields.map((field) => {
121
+ const { label, name, operators, values } = field.vocabulary;
122
+ const ticked = this.picked.get(name) ?? new Set();
123
+ const using = this.operatorFor(field);
124
+ const offered = using?.arity === 'none' ? [] : values;
125
+ return html `<${item} data-facet="field" data-field=${name}>
126
+ ${label}
127
+ ${operators.length > 1
128
+ ? html `<${item} slot="submenu" data-facet="comparison" data-field=${name}>
129
+ ${using?.label ?? ''}
130
+ ${operators.map((operator) => html `<${item}
131
+ slot="submenu"
132
+ checkable
133
+ exclusive
134
+ ?checked=${operator.name === using?.name}
135
+ data-facet="operator"
136
+ data-field=${name}
137
+ data-value=${operator.name}
138
+ >${operator.label}</${item}>`)}
139
+ </${item}>`
140
+ : nothing}
141
+ ${operators.length > 1 && offered.length > 0
142
+ ? html `<${divider} slot="submenu" announced></${divider}>`
143
+ : nothing}
144
+ ${offered.map((option) => html `<${item}
145
+ slot="submenu"
146
+ checkable
147
+ ?exclusive=${using?.arity === 'one'}
148
+ ?checked=${ticked.has(option.value)}
149
+ data-facet="value"
150
+ data-field=${name}
151
+ data-value=${option.value}
152
+ >${option.label}</${item}>`)}
153
+ </${item}>`;
154
+ })}
155
+ </${menu}>`;
156
+ }
157
+ };
158
+ __decorate([
159
+ property()
160
+ ], HelixHeadlessFilterMenu.prototype, "label", void 0);
161
+ HelixHeadlessFilterMenu = __decorate([
162
+ customElement('helix-headless-filter-menu')
163
+ ], HelixHeadlessFilterMenu);
164
+ export { HelixHeadlessFilterMenu };
@@ -0,0 +1,105 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import { nothing, type PropertyValues } from 'lit';
3
+ /**
4
+ * @summary One way a filter can compare a field to its values.
5
+ *
6
+ * @description
7
+ * **A declaration, not a control.** It renders nothing and is never seen: it is
8
+ * how a consumer says which comparisons their field supports, in markup, beside
9
+ * the values it supports them on. `Field` reads them; the chip shows the one in
10
+ * force; the menu offers the rest.
11
+ *
12
+ * **The vocabulary is the consumer's and this element never learns it.** What
13
+ * `is none of` means against a queue of refunds is a question about refunds, so
14
+ * nothing here compares anything or knows what `name` will be handed to. Helix
15
+ * owns the shape a filter has, which is a field, an operator and some values,
16
+ * and owns the interaction that builds one. What the words mean is the domain's,
17
+ * which is the same line `FilterInput` already draws when it declines to learn a
18
+ * grammar.
19
+ *
20
+ * **`default` is what makes a chip readable.** The operator a field is usually
21
+ * filtered by is the one nobody needs told, so a chip drawn with it says only
22
+ * the field and the values. Naming a second operator `default` on one field is a
23
+ * consumer bug and the field reports it rather than guessing.
24
+ *
25
+ * @since 0.23.0
26
+ * @status experimental
27
+ * @category forms
28
+ * @aliases Comparator, Predicate
29
+ * @documentation https://helix.fusion.dev/components/filter-input
30
+ */
31
+ export declare class HelixHeadlessFilterOperator extends HelixElement {
32
+ /**
33
+ * How many values this comparison takes.
34
+ *
35
+ * `none` is a comparison that is complete on its own, which is what `is empty`
36
+ * and `is set` are: a chip drawn for one shows no values because there are
37
+ * none to show, and the menu that builds it offers nothing to pick.
38
+ *
39
+ * `one` takes a single value, which is every scalar comparison: `is after` a
40
+ * date, `is above` a number. `many` takes a set, which is what `is any of` and
41
+ * `is none of` are, and is the only arity a multi-select panel makes sense
42
+ * for.
43
+ *
44
+ * Read rather than enforced. This element applies nothing, so the arity is a
45
+ * fact the picker uses to decide what to offer, not a rule it polices. What
46
+ * the picker does with it: `none` offers no values at all and takes back any
47
+ * that were ticked, since the comparison is the whole filter; `one` replaces
48
+ * rather than adds, and its rows announce as one of a set. The typed grammar
49
+ * reads it too, refusing a list where a scalar was declared and never writing
50
+ * a comparison that takes nothing.
51
+ *
52
+ * The arity reaches a consumer in `helix-facet`, which is what tells an empty
53
+ * filter from no filter: `values` empty means the field is no longer narrowing
54
+ * anything, unless the comparison is one that never had values to give.
55
+ *
56
+ * **Undefined reads as `many`, and takes no default**, which is the rule every
57
+ * reflected property here answers to: a default writes an attribute on upgrade
58
+ * that the server never wrote, and the two markups differ before a reader has
59
+ * touched anything.
60
+ */
61
+ arity?: 'many' | 'none' | 'one';
62
+ /**
63
+ * Whether this is the comparison the field is filtered by unless asked
64
+ * otherwise.
65
+ *
66
+ * The chip leaves a default operator unwritten, so the common filter reads as
67
+ * a field and its values and nothing else, and the word only appears once
68
+ * somebody has chosen a comparison worth noticing.
69
+ */
70
+ default: boolean;
71
+ /**
72
+ * What a reader is shown, in the chip and in the menu that changes it.
73
+ *
74
+ * Words rather than symbols, and the reason is that a set has more than one
75
+ * negation: `!=` cannot tell "is none of" from "is not all of", and both are
76
+ * filters somebody wants. A chip is read, so it takes the reading. The text
77
+ * grammar a consumer types is theirs and may use whatever symbols it likes.
78
+ */
79
+ label: string;
80
+ /**
81
+ * What the consumer's own model calls this comparison.
82
+ *
83
+ * Never shown. It is what arrives in the filter this element helps build, so
84
+ * a consumer matches on it rather than on the words a reader sees, and
85
+ * translating the label breaks nothing.
86
+ *
87
+ * Reflected and so undefaulted, for the reason `arity` gives above.
88
+ */
89
+ name?: string;
90
+ protected willUpdate(changed: PropertyValues): void;
91
+ /**
92
+ * Nothing, which is what makes this a declaration.
93
+ *
94
+ * **No `<slot>`, deliberately.** An element that renders no slot renders none
95
+ * of its children, so the words a consumer writes inside one are held for
96
+ * whoever reads them and never painted. That is the whole mechanism, and it
97
+ * costs no `display: none` in a tier that has no stylesheet to put one in.
98
+ */
99
+ protected render(): typeof nothing;
100
+ }
101
+ declare global {
102
+ interface HTMLElementTagNameMap {
103
+ 'helix-headless-filter-operator': HelixHeadlessFilterOperator;
104
+ }
105
+ }
@@ -0,0 +1,39 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { customElement, HelixElement } from '@fusion.dev/helix-core';
8
+ import { nothing } from 'lit';
9
+ import { property } from 'lit/decorators.js';
10
+ let HelixHeadlessFilterOperator = class HelixHeadlessFilterOperator extends HelixElement {
11
+ constructor() {
12
+ super(...arguments);
13
+ this.default = false;
14
+ this.label = '';
15
+ }
16
+ willUpdate(changed) {
17
+ super.willUpdate(changed);
18
+ this.internals.role = 'none';
19
+ }
20
+ render() {
21
+ return nothing;
22
+ }
23
+ };
24
+ __decorate([
25
+ property({ reflect: true })
26
+ ], HelixHeadlessFilterOperator.prototype, "arity", void 0);
27
+ __decorate([
28
+ property({ reflect: true, type: Boolean })
29
+ ], HelixHeadlessFilterOperator.prototype, "default", void 0);
30
+ __decorate([
31
+ property()
32
+ ], HelixHeadlessFilterOperator.prototype, "label", void 0);
33
+ __decorate([
34
+ property({ reflect: true })
35
+ ], HelixHeadlessFilterOperator.prototype, "name", void 0);
36
+ HelixHeadlessFilterOperator = __decorate([
37
+ customElement('helix-headless-filter-operator')
38
+ ], HelixHeadlessFilterOperator);
39
+ export { HelixHeadlessFilterOperator };