fluid-primitives 0.16.0 → 0.17.2-next.4.ea462ad
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/README.md +41 -3
- package/dist/Client-D-GbQqXf.js +240 -0
- package/dist/accordion.d.ts +3 -3
- package/dist/accordion.js +48 -1
- package/dist/checkbox-group.d.ts +99 -0
- package/dist/checkbox-group.js +117 -0
- package/dist/checkbox-group.registry-CGwuF7SF.js +112 -0
- package/dist/checkbox.d.ts +26 -3
- package/dist/checkbox.js +148 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.js +3 -1
- package/dist/clipboard.d.ts +3 -3
- package/dist/clipboard.js +32 -1
- package/dist/collapsible.d.ts +3 -3
- package/dist/collapsible.js +28 -1
- package/dist/dialog.d.ts +3 -3
- package/dist/dialog.js +34 -1
- package/dist/field.d.ts +2 -2
- package/dist/field.dom-CJQXpQbZ.js +11 -0
- package/dist/field.js +211 -1
- package/dist/form.d.ts +3 -3
- package/dist/form.js +432 -1
- package/dist/form.registry-CmpTny_s.js +51 -0
- package/dist/{form.registry-DdLr3dbX.d.ts → form.registry-DjhMF6Ny.d.ts} +2 -2
- package/dist/{index-BpcuNDMI.d.ts → index-DxZhm-zT.d.ts} +10 -1
- package/dist/navigation-menu.d.ts +15 -0
- package/dist/navigation-menu.js +71 -0
- package/dist/number-input.d.ts +3 -3
- package/dist/number-input.js +55 -1
- package/dist/popover.d.ts +3 -3
- package/dist/popover.js +42 -1
- package/dist/radio-group.d.ts +3 -3
- package/dist/radio-group.js +67 -1
- package/dist/scroll-area.d.ts +1 -1
- package/dist/scroll-area.js +32 -1
- package/dist/select.d.ts +6 -5
- package/dist/select.js +84 -1
- package/dist/switch.d.ts +53 -0
- package/dist/switch.js +61 -0
- package/dist/tabs.d.ts +3 -3
- package/dist/tabs.js +33 -1
- package/dist/tooltip.d.ts +3 -3
- package/dist/tooltip.js +36 -1
- package/package.json +44 -22
- package/dist/Client-CygooKu8.js +0 -1
- package/dist/field.dom-etI2JxSW.js +0 -1
- package/dist/form.registry-Bv4jZGjo.js +0 -1
package/README.md
CHANGED
|
@@ -2,12 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
# Fluid Primitives
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Unstyled, flexible and accessible UI Primitives that provide a foundation for building your own component library in Fluid.
|
|
6
|
+
|
|
7
|
+
Fluid Primitives brings modern component patterns to TYPO3. Build accessible, composable UI components with the same developer experience you'd expect from React libraries like Radix or Base UI - but for Fluid templates.
|
|
6
8
|
|
|
7
9
|
## Documentation
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Full documentation can be found at [fluid-primitives.com](https://fluid-primitives.com).
|
|
12
|
+
|
|
13
|
+
## What You Get
|
|
14
|
+
|
|
15
|
+
**Accessible by default.** Every interactive component handles keyboard navigation, focus management, and ARIA attributes automatically via [Zag.js](https://zagjs.com/) state machines.
|
|
16
|
+
|
|
17
|
+
**Composable API.** No more prop drilling. Build complex UIs by composing small, focused parts that work together.
|
|
18
|
+
|
|
19
|
+
**Unstyled.** Zero design opinions. Use Tailwind, vanilla CSS, or any styling approach. You control every pixel.
|
|
20
|
+
|
|
21
|
+
**Server-rendered.** Components render on the server with PHP/Fluid, then hydrate on the client. No layout shift, great for SEO.
|
|
22
|
+
|
|
23
|
+
## Quick Example
|
|
24
|
+
|
|
25
|
+
A tooltip with full accessibility support in just a few lines:
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<ui:tooltip.root>
|
|
29
|
+
<ui:tooltip.trigger>Hover me</ui:tooltip.trigger>
|
|
30
|
+
<ui:tooltip.content>Tooltip content here</ui:tooltip.content>
|
|
31
|
+
</ui:tooltip.root>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's it. Keyboard support, focus handling, proper ARIA attributes - all handled.
|
|
35
|
+
|
|
36
|
+
## Why This Exists
|
|
37
|
+
|
|
38
|
+
TYPO3 Fluid lacked an elegant solution for building robust, interactive components. The typical approach leads to bloated templates with complex conditional logic, poor accessibility, and custom JavaScript that's hard to maintain.
|
|
39
|
+
|
|
40
|
+
Fluid Primitives solves this by bringing proven patterns from the modern frontend ecosystem to TYPO3, while respecting its server-first architecture.
|
|
41
|
+
|
|
42
|
+
## Acknowledgments
|
|
43
|
+
|
|
44
|
+
- [Zag.js](https://zagjs.com/) - The state machine foundation
|
|
45
|
+
- [Radix UI](https://www.radix-ui.com/primitives) - API design inspiration
|
|
46
|
+
- [Base UI](https://base-ui.com/) - Component behavior patterns
|
|
47
|
+
- [Ark UI](https://ark-ui.com/) - Zag.js integration patterns
|
|
10
48
|
|
|
11
49
|
## Development
|
|
12
50
|
|
|
13
|
-
See [github.com/jramke/fluid-primitives.com](https://github.com/jramke/fluid-primitives.com) for the development setup.
|
|
51
|
+
See [github.com/jramke/fluid-primitives.com](https://github.com/jramke/fluid-primitives.com) monorepo for the development setup.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { ListCollection } from "@zag-js/collection";
|
|
2
|
+
import { VanillaMachine, VanillaMachine as Machine, mergeProps, normalizeProps, spreadProps } from "@zag-js/vanilla";
|
|
3
|
+
import { uuid } from "@zag-js/utils";
|
|
4
|
+
|
|
5
|
+
//#region Resources/Private/Client/src/lib/component.ts
|
|
6
|
+
var Component = class {
|
|
7
|
+
document;
|
|
8
|
+
machine;
|
|
9
|
+
api;
|
|
10
|
+
hydrator = null;
|
|
11
|
+
userProps;
|
|
12
|
+
static name;
|
|
13
|
+
get doc() {
|
|
14
|
+
return this.document;
|
|
15
|
+
}
|
|
16
|
+
constructor(props, userDocument = document) {
|
|
17
|
+
this.document = userDocument;
|
|
18
|
+
this.userProps = this.transformProps(props);
|
|
19
|
+
this.hydrator = this.initHydrator(props);
|
|
20
|
+
this.machine = this.initMachine(props);
|
|
21
|
+
this.api = this.initApi();
|
|
22
|
+
}
|
|
23
|
+
initHydrator(props) {
|
|
24
|
+
const id = props.id;
|
|
25
|
+
if (!id) throw new Error("ComponentHydrator requires an id prop to initialize.");
|
|
26
|
+
return new ComponentHydrator(this.getName(), id, props.ids, this.doc);
|
|
27
|
+
}
|
|
28
|
+
init() {
|
|
29
|
+
this.render();
|
|
30
|
+
this.machine.subscribe(() => {
|
|
31
|
+
this.api = this.initApi();
|
|
32
|
+
this.render();
|
|
33
|
+
});
|
|
34
|
+
this.machine.start();
|
|
35
|
+
}
|
|
36
|
+
getName() {
|
|
37
|
+
return this.constructor.name;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Override in consumer for example when a getter is used for collection
|
|
41
|
+
* Needs to be used manually inside the initMachine method
|
|
42
|
+
*/
|
|
43
|
+
transformProps(props) {
|
|
44
|
+
return props;
|
|
45
|
+
}
|
|
46
|
+
updateProps(newProps) {
|
|
47
|
+
this.machine.updateProps(newProps);
|
|
48
|
+
}
|
|
49
|
+
destroy() {
|
|
50
|
+
this.machine.stop();
|
|
51
|
+
this.hydrator?.destroy();
|
|
52
|
+
}
|
|
53
|
+
spreadProps(node, attrs) {
|
|
54
|
+
spreadProps(node, attrs, this.machine.scope.id);
|
|
55
|
+
}
|
|
56
|
+
getElement(part, parent) {
|
|
57
|
+
return this.hydrator?.getElement(part, parent) || null;
|
|
58
|
+
}
|
|
59
|
+
getElements(part, parent) {
|
|
60
|
+
return this.hydrator?.getElements(part, parent) || [];
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region Resources/Private/Primitives/Field/src/field.registry.ts
|
|
66
|
+
const registry = /* @__PURE__ */ new WeakMap();
|
|
67
|
+
function registerFieldMachine(field, service) {
|
|
68
|
+
if (!field) return;
|
|
69
|
+
registry.set(field, service);
|
|
70
|
+
field.dispatchEvent(new CustomEvent("fluid-primitives:field:registered", { bubbles: true }));
|
|
71
|
+
}
|
|
72
|
+
function getFieldMachineFor(el) {
|
|
73
|
+
if (!el) return;
|
|
74
|
+
return registry.get(el);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region Resources/Private/Client/src/lib/field-aware-component.ts
|
|
79
|
+
const fieldProps = [
|
|
80
|
+
"invalid",
|
|
81
|
+
"disabled",
|
|
82
|
+
"readOnly",
|
|
83
|
+
"required"
|
|
84
|
+
];
|
|
85
|
+
const fieldAccessors = {
|
|
86
|
+
disabled: (s) => s.context.get("disabled"),
|
|
87
|
+
readOnly: (s) => s.context.get("readOnly"),
|
|
88
|
+
required: (s) => s.context.get("required"),
|
|
89
|
+
invalid: (s) => s.context.get("invalid")
|
|
90
|
+
};
|
|
91
|
+
var FieldAwareComponent = class extends Component {
|
|
92
|
+
subscribedToField = false;
|
|
93
|
+
fieldMachine;
|
|
94
|
+
closestField = null;
|
|
95
|
+
getClosestField() {
|
|
96
|
+
return this.closestField || this.getElement("root")?.closest("[data-scope=\"field\"][data-part=\"root\"]") || null;
|
|
97
|
+
}
|
|
98
|
+
withFieldProps(props) {
|
|
99
|
+
this.closestField = this.getClosestField();
|
|
100
|
+
if (!this.closestField) return props;
|
|
101
|
+
this.fieldMachine = getFieldMachineFor(this.closestField);
|
|
102
|
+
if (this.fieldMachine) return this.propsWithField(props, this.fieldMachine);
|
|
103
|
+
else {
|
|
104
|
+
const handler = () => {
|
|
105
|
+
this.fieldMachine = getFieldMachineFor(this.closestField);
|
|
106
|
+
this.updateProps(this.propsWithField(this.userProps, this.fieldMachine));
|
|
107
|
+
this.closestField?.removeEventListener("fluid-primitives:field:registered", handler);
|
|
108
|
+
};
|
|
109
|
+
this.closestField.addEventListener("fluid-primitives:field:registered", handler);
|
|
110
|
+
}
|
|
111
|
+
return props;
|
|
112
|
+
}
|
|
113
|
+
subscribeToFieldService() {
|
|
114
|
+
if (this.subscribedToField) return;
|
|
115
|
+
this.closestField = this.getClosestField();
|
|
116
|
+
if (!this.closestField) return;
|
|
117
|
+
if (!this.fieldMachine) this.fieldMachine = getFieldMachineFor(this.closestField);
|
|
118
|
+
if (this.fieldMachine) {
|
|
119
|
+
this.fieldMachine.subscribe((snapshot) => {
|
|
120
|
+
queueMicrotask(() => {
|
|
121
|
+
let propsToUpdate = {};
|
|
122
|
+
for (const prop of fieldProps) {
|
|
123
|
+
const newValue = !!fieldAccessors[prop](snapshot);
|
|
124
|
+
if (newValue !== !!this.machine.prop(prop)) propsToUpdate[prop] = newValue;
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(propsToUpdate).length > 0) this.updateProps(propsToUpdate);
|
|
127
|
+
else this.machine.notify();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
this.subscribedToField = true;
|
|
131
|
+
} else {
|
|
132
|
+
const handler = () => {
|
|
133
|
+
this.subscribeToFieldService();
|
|
134
|
+
this.closestField.removeEventListener("fluid-primitives:field:registered", handler);
|
|
135
|
+
};
|
|
136
|
+
this.closestField.addEventListener("fluid-primitives:field:registered", handler);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region Resources/Private/Client/src/lib/hydration.ts
|
|
143
|
+
function getHydrationData(component, id) {
|
|
144
|
+
const hydrationData = window.FluidPrimitives?.hydrationData;
|
|
145
|
+
if (!hydrationData || typeof hydrationData !== "object") return null;
|
|
146
|
+
if (!component) return hydrationData;
|
|
147
|
+
if (!hydrationData[component]) return null;
|
|
148
|
+
if (!id) return hydrationData[component];
|
|
149
|
+
return hydrationData[component][id] || null;
|
|
150
|
+
}
|
|
151
|
+
function mount(componentName, callback) {
|
|
152
|
+
const hydrationInstances = getHydrationData(componentName);
|
|
153
|
+
if (!hydrationInstances) return;
|
|
154
|
+
Object.keys(hydrationInstances).forEach((id) => {
|
|
155
|
+
if (hydrationInstances[id].controlled) return;
|
|
156
|
+
const instance = callback({
|
|
157
|
+
...hydrationInstances[id],
|
|
158
|
+
createHydrator: () => new ComponentHydrator(componentName, id, hydrationInstances[id].props.ids)
|
|
159
|
+
});
|
|
160
|
+
if (!instance) return;
|
|
161
|
+
if (!window.FluidPrimitives.uncontrolledInstances[componentName]) window.FluidPrimitives.uncontrolledInstances[componentName] = {};
|
|
162
|
+
window.FluidPrimitives.uncontrolledInstances[componentName][id] = instance;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
var ComponentHydrator = class {
|
|
166
|
+
componentName;
|
|
167
|
+
doc;
|
|
168
|
+
rootId;
|
|
169
|
+
ids;
|
|
170
|
+
elementRefs = /* @__PURE__ */ new Map();
|
|
171
|
+
constructor(componentName, rootId, ids = {}, doc = document) {
|
|
172
|
+
this.componentName = componentName;
|
|
173
|
+
this.doc = doc;
|
|
174
|
+
if (!rootId) throw new Error(`Root ID is required for component hydration: ${componentName}`);
|
|
175
|
+
this.rootId = rootId;
|
|
176
|
+
this.ids = ids;
|
|
177
|
+
}
|
|
178
|
+
getElement(part, parent = this.doc) {
|
|
179
|
+
if (this.elementRefs.has(part)) return this.elementRefs.get(part) || null;
|
|
180
|
+
let element = null;
|
|
181
|
+
if (this.ids[part]) element = parent.querySelector(`#${this.ids[part]}`);
|
|
182
|
+
else element = parent.querySelector(`[data-hydrate-${this.componentName}="${this.rootId}"][data-part="${part}"][data-scope="${this.componentName}"]`);
|
|
183
|
+
if (element) {
|
|
184
|
+
if (parent === this.doc) this.elementRefs.set(part, element);
|
|
185
|
+
element.removeAttribute(`data-hydrate-${this.componentName}`);
|
|
186
|
+
element.__rootId = this.rootId;
|
|
187
|
+
}
|
|
188
|
+
return element;
|
|
189
|
+
}
|
|
190
|
+
getElements(part, parent = this.doc) {
|
|
191
|
+
if (this.elementRefs.has(part)) return this.elementRefs.get(part);
|
|
192
|
+
let elements = [];
|
|
193
|
+
if (this.ids[part]) elements = Array.from(parent.querySelectorAll(`#${this.ids[part]}`));
|
|
194
|
+
else elements = Array.from(parent.querySelectorAll(`[data-hydrate-${this.componentName}="${this.rootId}"][data-part="${part}"][data-scope="${this.componentName}"]`));
|
|
195
|
+
if (parent === this.doc) this.elementRefs.set(part, elements);
|
|
196
|
+
elements.forEach((el) => el.removeAttribute(`data-hydrate-${this.componentName}`));
|
|
197
|
+
return elements;
|
|
198
|
+
}
|
|
199
|
+
generateRefAttributesString(part) {
|
|
200
|
+
const id = this.ids[part] || `${this.rootId}-${part}`;
|
|
201
|
+
return `data-scope="${this.componentName}" data-part="${part}" data-hydrate-${this.componentName}="${id}"`;
|
|
202
|
+
}
|
|
203
|
+
setRefAttributes(element, part) {
|
|
204
|
+
this.generateRefAttributesString(part).split(" ").map((attr) => attr.trim()).forEach((attr) => {
|
|
205
|
+
const [key, value] = attr.split("=");
|
|
206
|
+
element.setAttribute(key, value.replace(/"/g, ""));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
destroy() {
|
|
210
|
+
this.elementRefs.forEach((el) => {
|
|
211
|
+
if (el instanceof Element) el.setAttribute(`data-hydrate-${this.componentName}`, this.rootId);
|
|
212
|
+
else el.forEach((e) => e.setAttribute(`data-hydrate-${this.componentName}`, this.rootId));
|
|
213
|
+
});
|
|
214
|
+
this.elementRefs.clear();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
function getListCollectionFromHydrationData(hydrationCollection) {
|
|
218
|
+
if (hydrationCollection instanceof ListCollection) return hydrationCollection;
|
|
219
|
+
return new ListCollection({
|
|
220
|
+
items: hydrationCollection.items,
|
|
221
|
+
itemToValue: hydrationCollection.itemToValueKey ? (item) => item?.[hydrationCollection.itemToValueKey] : void 0,
|
|
222
|
+
itemToString: hydrationCollection.itemToStringKey ? (item) => item?.[hydrationCollection.itemToStringKey] : void 0,
|
|
223
|
+
isItemDisabled: hydrationCollection.isItemDisabledKey ? (item) => item?.[hydrationCollection.isItemDisabledKey] : void 0,
|
|
224
|
+
groupBy: hydrationCollection.groupByKey ? (item) => {
|
|
225
|
+
const key = hydrationCollection.groupByKey;
|
|
226
|
+
if (!key) return void 0;
|
|
227
|
+
if (key.includes(".")) return key.split(".").reduce((obj, k) => obj ? obj[k] : void 0, item);
|
|
228
|
+
return item?.[key];
|
|
229
|
+
} : void 0
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region Resources/Private/Client/src/lib/uid.ts
|
|
235
|
+
function uid(prefix = "f") {
|
|
236
|
+
return "«" + prefix + uuid() + "»";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//#endregion
|
|
240
|
+
export { Machine as a, getListCollectionFromHydrationData as c, registerFieldMachine as d, Component as f, mergeProps as i, mount as l, spreadProps as n, ComponentHydrator as o, normalizeProps as r, getHydrationData as s, uid as t, FieldAwareComponent as u };
|
package/dist/accordion.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as Machine, d as Component } from "./index-DxZhm-zT.js";
|
|
2
2
|
import * as accordion from "@zag-js/accordion";
|
|
3
|
-
import * as
|
|
3
|
+
import * as _$_zag_js_types0 from "@zag-js/types";
|
|
4
4
|
|
|
5
5
|
//#region Resources/Private/Primitives/Accordion/Accordion.d.ts
|
|
6
6
|
declare class Accordion extends Component<accordion.Props, accordion.Api> {
|
|
7
7
|
static name: string;
|
|
8
8
|
initMachine(props: accordion.Props): Machine<any>;
|
|
9
|
-
initApi(): accordion.Api<
|
|
9
|
+
initApi(): accordion.Api<_$_zag_js_types0.PropTypes<{
|
|
10
10
|
[x: string]: any;
|
|
11
11
|
}>>;
|
|
12
12
|
render(): void;
|
package/dist/accordion.js
CHANGED
|
@@ -1 +1,48 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import { a as Machine, f as Component, r as normalizeProps } from "./Client-D-GbQqXf.js";
|
|
2
|
+
import * as accordion from "@zag-js/accordion";
|
|
3
|
+
|
|
4
|
+
//#region Resources/Private/Primitives/Accordion/Accordion.ts
|
|
5
|
+
var Accordion = class extends Component {
|
|
6
|
+
static name = "accordion";
|
|
7
|
+
initMachine(props) {
|
|
8
|
+
return new Machine(accordion.machine, {
|
|
9
|
+
collapsible: true,
|
|
10
|
+
...props
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
initApi() {
|
|
14
|
+
return accordion.connect(this.machine.service, normalizeProps);
|
|
15
|
+
}
|
|
16
|
+
render() {
|
|
17
|
+
const rootEl = this.getElement("root");
|
|
18
|
+
if (rootEl) this.spreadProps(rootEl, this.api.getRootProps());
|
|
19
|
+
this.getElements("item").forEach((itemEl) => {
|
|
20
|
+
this.spreadProps(itemEl, this.api.getItemProps({
|
|
21
|
+
value: itemEl.getAttribute("data-value"),
|
|
22
|
+
disabled: itemEl.hasAttribute("data-disabled")
|
|
23
|
+
}));
|
|
24
|
+
});
|
|
25
|
+
this.getElements("item-trigger").forEach((trigger) => {
|
|
26
|
+
this.spreadProps(trigger, this.api.getItemTriggerProps({
|
|
27
|
+
value: trigger.getAttribute("data-value"),
|
|
28
|
+
disabled: trigger.hasAttribute("data-disabled")
|
|
29
|
+
}));
|
|
30
|
+
});
|
|
31
|
+
this.getElements("item-content").forEach((contentEl) => {
|
|
32
|
+
this.spreadProps(contentEl, this.api.getItemContentProps({
|
|
33
|
+
value: contentEl.getAttribute("data-value"),
|
|
34
|
+
disabled: contentEl.hasAttribute("data-disabled")
|
|
35
|
+
}));
|
|
36
|
+
});
|
|
37
|
+
this.getElements("item-indicator").forEach((indicatorEl) => {
|
|
38
|
+
this.spreadProps(indicatorEl, this.api.getItemIndicatorProps({
|
|
39
|
+
value: indicatorEl.getAttribute("data-value"),
|
|
40
|
+
disabled: indicatorEl.hasAttribute("data-disabled")
|
|
41
|
+
}));
|
|
42
|
+
});
|
|
43
|
+
this.getElements("item-header");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { Accordion };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { a as Machine, l as FieldAwareComponent } from "./index-DxZhm-zT.js";
|
|
2
|
+
import { t as FieldMachine } from "./form.registry-DjhMF6Ny.js";
|
|
3
|
+
import { EventObject } from "@zag-js/core";
|
|
4
|
+
import { PropTypes } from "@zag-js/types";
|
|
5
|
+
|
|
6
|
+
//#region Resources/Private/Primitives/CheckboxGroup/src/checkbox-group.types.d.ts
|
|
7
|
+
interface CheckboxGroupProps {
|
|
8
|
+
id: string;
|
|
9
|
+
ids?: Record<string, string>;
|
|
10
|
+
/** The initial value of the checkbox group (uncontrolled) */
|
|
11
|
+
defaultValue?: string[];
|
|
12
|
+
/** The controlled value of the checkbox group */
|
|
13
|
+
value?: string[];
|
|
14
|
+
/** The name of the input fields in the checkbox group (for form submission) */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** The form id the checkbox group belongs to */
|
|
17
|
+
form?: string;
|
|
18
|
+
/** If true, the checkbox group is disabled */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** If true, the checkbox group is read-only */
|
|
21
|
+
readOnly?: boolean;
|
|
22
|
+
/** If true, the checkbox group is required */
|
|
23
|
+
required?: boolean;
|
|
24
|
+
/** If true, the checkbox group is invalid */
|
|
25
|
+
invalid?: boolean;
|
|
26
|
+
/** The maximum number of selected values */
|
|
27
|
+
maxSelectedValues?: number;
|
|
28
|
+
/** Called when the value changes */
|
|
29
|
+
onValueChange?: (details: {
|
|
30
|
+
value: string[];
|
|
31
|
+
}) => void;
|
|
32
|
+
}
|
|
33
|
+
interface CheckboxGroupSchema {
|
|
34
|
+
props: CheckboxGroupProps;
|
|
35
|
+
context: {
|
|
36
|
+
value: string[];
|
|
37
|
+
};
|
|
38
|
+
computed: {
|
|
39
|
+
isAtMax: boolean;
|
|
40
|
+
isInteractive: boolean;
|
|
41
|
+
};
|
|
42
|
+
state: 'ready';
|
|
43
|
+
event: EventObject;
|
|
44
|
+
action: string;
|
|
45
|
+
effect: string;
|
|
46
|
+
}
|
|
47
|
+
interface CheckboxGroupItemProps {
|
|
48
|
+
value: string;
|
|
49
|
+
}
|
|
50
|
+
/** API returned by getItemProps for checkbox items */
|
|
51
|
+
interface CheckboxGroupItemState {
|
|
52
|
+
checked: boolean;
|
|
53
|
+
onCheckedChange: () => void;
|
|
54
|
+
name: string | undefined;
|
|
55
|
+
disabled: boolean;
|
|
56
|
+
readOnly: boolean;
|
|
57
|
+
invalid: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Public API for CheckboxGroup - used by Checkbox to get item props */
|
|
60
|
+
interface CheckboxGroupApi {
|
|
61
|
+
/** The current value of the checkbox group */
|
|
62
|
+
value: string[];
|
|
63
|
+
/** The name for form submission */
|
|
64
|
+
name: string | undefined;
|
|
65
|
+
/** Whether the checkbox group is disabled */
|
|
66
|
+
disabled: boolean;
|
|
67
|
+
/** Whether the checkbox group is read-only */
|
|
68
|
+
readOnly: boolean;
|
|
69
|
+
/** Whether the checkbox group is invalid */
|
|
70
|
+
invalid: boolean;
|
|
71
|
+
/** Check if a value is selected */
|
|
72
|
+
isChecked(value: string): boolean;
|
|
73
|
+
/** Set the entire value array */
|
|
74
|
+
setValue(value: string[]): void;
|
|
75
|
+
/** Add a value to the selection */
|
|
76
|
+
addValue(value: string): void;
|
|
77
|
+
/** Remove a value from the selection */
|
|
78
|
+
removeValue(value: string): void;
|
|
79
|
+
/** Toggle a value */
|
|
80
|
+
toggleValue(value: string): void;
|
|
81
|
+
/** Get props to merge into a checkbox item */
|
|
82
|
+
getItemProps(props: CheckboxGroupItemProps): CheckboxGroupItemState;
|
|
83
|
+
/** Get root element props */
|
|
84
|
+
getRootProps(): PropTypes['element'];
|
|
85
|
+
/** Get label element props */
|
|
86
|
+
getLabelProps(): PropTypes['element'];
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region Resources/Private/Primitives/CheckboxGroup/CheckboxGroup.d.ts
|
|
90
|
+
declare class CheckboxGroup extends FieldAwareComponent<CheckboxGroupProps, CheckboxGroupApi> {
|
|
91
|
+
static name: string;
|
|
92
|
+
propsWithField(props: CheckboxGroupProps, fieldMachine: FieldMachine): CheckboxGroupProps;
|
|
93
|
+
initMachine(props: CheckboxGroupProps): Machine<CheckboxGroupSchema>;
|
|
94
|
+
initApi(): CheckboxGroupApi;
|
|
95
|
+
render(): void;
|
|
96
|
+
destroy(): void;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
export { CheckboxGroup };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { a as Machine, i as mergeProps, r as normalizeProps, u as FieldAwareComponent } from "./Client-D-GbQqXf.js";
|
|
2
|
+
import { i as connect, n as registerCheckboxGroup, r as unregisterCheckboxGroup } from "./checkbox-group.registry-CGwuF7SF.js";
|
|
3
|
+
import { a as getLabelId } from "./field.dom-CJQXpQbZ.js";
|
|
4
|
+
import { createMachine } from "@zag-js/core";
|
|
5
|
+
|
|
6
|
+
//#region Resources/Private/Primitives/CheckboxGroup/src/checkbox-group.machine.ts
|
|
7
|
+
const machine = createMachine({
|
|
8
|
+
initialState() {
|
|
9
|
+
return "ready";
|
|
10
|
+
},
|
|
11
|
+
context({ bindable, prop }) {
|
|
12
|
+
return { value: bindable(() => ({
|
|
13
|
+
defaultValue: prop("defaultValue") ?? [],
|
|
14
|
+
value: prop("value"),
|
|
15
|
+
onChange(value) {
|
|
16
|
+
prop("onValueChange")?.({ value });
|
|
17
|
+
}
|
|
18
|
+
})) };
|
|
19
|
+
},
|
|
20
|
+
states: { ready: { on: {
|
|
21
|
+
"VALUE.SET": { actions: ["setValue"] },
|
|
22
|
+
"VALUE.ADD": { actions: ["addValue"] },
|
|
23
|
+
"VALUE.REMOVE": { actions: ["removeValue"] },
|
|
24
|
+
"VALUE.TOGGLE": { actions: ["toggleValue"] }
|
|
25
|
+
} } },
|
|
26
|
+
computed: {
|
|
27
|
+
isAtMax({ prop, context }) {
|
|
28
|
+
const max = prop("maxSelectedValues");
|
|
29
|
+
if (max == null) return false;
|
|
30
|
+
return context.get("value").length >= max;
|
|
31
|
+
},
|
|
32
|
+
isInteractive({ prop }) {
|
|
33
|
+
return !prop("disabled") && !prop("readOnly");
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
implementations: { actions: {
|
|
37
|
+
setValue({ context, event }) {
|
|
38
|
+
const newValue = event.value;
|
|
39
|
+
context.set("value", newValue);
|
|
40
|
+
},
|
|
41
|
+
addValue({ context, event, computed }) {
|
|
42
|
+
if (!computed("isInteractive")) return;
|
|
43
|
+
const val = event.value;
|
|
44
|
+
const currentValue = context.get("value");
|
|
45
|
+
if (currentValue.includes(val)) return;
|
|
46
|
+
if (computed("isAtMax")) return;
|
|
47
|
+
const newValue = [...currentValue, val];
|
|
48
|
+
context.set("value", newValue);
|
|
49
|
+
},
|
|
50
|
+
removeValue({ context, event, computed }) {
|
|
51
|
+
if (!computed("isInteractive")) return;
|
|
52
|
+
const val = event.value;
|
|
53
|
+
const newValue = context.get("value").filter((v) => v !== val);
|
|
54
|
+
context.set("value", newValue);
|
|
55
|
+
},
|
|
56
|
+
toggleValue({ context, event, computed }) {
|
|
57
|
+
const val = event.value;
|
|
58
|
+
const currentValue = context.get("value");
|
|
59
|
+
if (currentValue.includes(val)) {
|
|
60
|
+
if (!computed("isInteractive")) return;
|
|
61
|
+
const newValue = currentValue.filter((v) => v !== val);
|
|
62
|
+
context.set("value", newValue);
|
|
63
|
+
} else {
|
|
64
|
+
if (!computed("isInteractive")) return;
|
|
65
|
+
if (computed("isAtMax")) return;
|
|
66
|
+
const newValue = [...currentValue, val];
|
|
67
|
+
context.set("value", newValue);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} }
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region Resources/Private/Primitives/CheckboxGroup/CheckboxGroup.ts
|
|
75
|
+
var CheckboxGroup = class extends FieldAwareComponent {
|
|
76
|
+
static name = "checkbox-group";
|
|
77
|
+
propsWithField(props, fieldMachine) {
|
|
78
|
+
return {
|
|
79
|
+
...props,
|
|
80
|
+
disabled: props.disabled ?? fieldMachine.context.get("disabled"),
|
|
81
|
+
readOnly: props.readOnly ?? fieldMachine.context.get("readOnly"),
|
|
82
|
+
required: props.required ?? fieldMachine.context.get("required"),
|
|
83
|
+
invalid: props.invalid ?? fieldMachine.context.get("invalid"),
|
|
84
|
+
name: props.name ?? fieldMachine.prop("name"),
|
|
85
|
+
ids: {
|
|
86
|
+
...props.ids,
|
|
87
|
+
label: getLabelId(fieldMachine.scope)
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
initMachine(props) {
|
|
92
|
+
props = this.withFieldProps(props);
|
|
93
|
+
const createdMachine = new Machine(machine, props);
|
|
94
|
+
registerCheckboxGroup(this.getElement("root"), createdMachine);
|
|
95
|
+
return createdMachine;
|
|
96
|
+
}
|
|
97
|
+
initApi() {
|
|
98
|
+
return connect(this.machine.service, normalizeProps);
|
|
99
|
+
}
|
|
100
|
+
render() {
|
|
101
|
+
this.subscribeToFieldService();
|
|
102
|
+
const rootEl = this.getElement("root");
|
|
103
|
+
if (rootEl) {
|
|
104
|
+
const mergedProps = mergeProps(this.api.getRootProps(), { "aria-describedby": this.fieldMachine?.context.get("describeIds") || void 0 });
|
|
105
|
+
this.spreadProps(rootEl, mergedProps);
|
|
106
|
+
}
|
|
107
|
+
const labelEl = this.getElement("label");
|
|
108
|
+
if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps());
|
|
109
|
+
}
|
|
110
|
+
destroy() {
|
|
111
|
+
unregisterCheckboxGroup(this.getElement("root"));
|
|
112
|
+
super.destroy();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { CheckboxGroup };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//#region Resources/Private/Primitives/CheckboxGroup/src/checkbox-group.dom.ts
|
|
2
|
+
const getRootId = (scope) => scope.ids?.root ?? `checkbox-group:${scope.id}:root`;
|
|
3
|
+
const getLabelId = (scope) => scope.ids?.label ?? `checkbox-group:${scope.id}:label`;
|
|
4
|
+
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region Resources/Private/Primitives/CheckboxGroup/src/checkbox-group.connect.ts
|
|
7
|
+
function connect(service, normalize) {
|
|
8
|
+
const { scope, context, computed, send, prop } = service;
|
|
9
|
+
const disabled = !!prop("disabled");
|
|
10
|
+
const readOnly = !!prop("readOnly");
|
|
11
|
+
const invalid = !!prop("invalid");
|
|
12
|
+
const required = !!prop("required");
|
|
13
|
+
const name = prop("name");
|
|
14
|
+
return {
|
|
15
|
+
get value() {
|
|
16
|
+
return context.get("value");
|
|
17
|
+
},
|
|
18
|
+
name,
|
|
19
|
+
disabled,
|
|
20
|
+
readOnly,
|
|
21
|
+
invalid,
|
|
22
|
+
isChecked(val) {
|
|
23
|
+
return context.get("value").includes(val);
|
|
24
|
+
},
|
|
25
|
+
setValue(val) {
|
|
26
|
+
send({
|
|
27
|
+
type: "VALUE.SET",
|
|
28
|
+
value: val
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
addValue(val) {
|
|
32
|
+
send({
|
|
33
|
+
type: "VALUE.ADD",
|
|
34
|
+
value: val
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
removeValue(val) {
|
|
38
|
+
send({
|
|
39
|
+
type: "VALUE.REMOVE",
|
|
40
|
+
value: val
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
toggleValue(val) {
|
|
44
|
+
send({
|
|
45
|
+
type: "VALUE.TOGGLE",
|
|
46
|
+
value: val
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
getItemProps(props) {
|
|
50
|
+
const checked = context.get("value").includes(props.value);
|
|
51
|
+
const isAtMax = computed("isAtMax");
|
|
52
|
+
return {
|
|
53
|
+
checked,
|
|
54
|
+
onCheckedChange: () => send({
|
|
55
|
+
type: "VALUE.TOGGLE",
|
|
56
|
+
value: props.value
|
|
57
|
+
}),
|
|
58
|
+
name,
|
|
59
|
+
disabled: disabled || isAtMax && !checked,
|
|
60
|
+
readOnly,
|
|
61
|
+
invalid
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
getRootProps() {
|
|
65
|
+
return normalize.element({
|
|
66
|
+
id: getRootId(scope),
|
|
67
|
+
role: "group",
|
|
68
|
+
"data-scope": "checkbox-group",
|
|
69
|
+
"data-part": "root",
|
|
70
|
+
"data-disabled": disabled ? "" : void 0,
|
|
71
|
+
"data-readonly": readOnly ? "" : void 0,
|
|
72
|
+
"data-invalid": invalid ? "" : void 0,
|
|
73
|
+
"aria-labelledby": getLabelId(scope),
|
|
74
|
+
"aria-disabled": disabled || void 0,
|
|
75
|
+
"aria-invalid": invalid || void 0,
|
|
76
|
+
"aria-required": required || void 0
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
getLabelProps() {
|
|
80
|
+
return normalize.element({
|
|
81
|
+
id: getLabelId(scope),
|
|
82
|
+
"data-scope": "checkbox-group",
|
|
83
|
+
"data-part": "label",
|
|
84
|
+
"data-disabled": disabled ? "" : void 0,
|
|
85
|
+
"data-readonly": readOnly ? "" : void 0,
|
|
86
|
+
"data-invalid": invalid ? "" : void 0
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region Resources/Private/Primitives/CheckboxGroup/src/checkbox-group.registry.ts
|
|
94
|
+
const registry = /* @__PURE__ */ new WeakMap();
|
|
95
|
+
function registerCheckboxGroup(root, machine) {
|
|
96
|
+
if (!root) return;
|
|
97
|
+
registry.set(root, machine);
|
|
98
|
+
root.dispatchEvent(new CustomEvent("fluid-primitives:checkbox-group:registered", { bubbles: true }));
|
|
99
|
+
}
|
|
100
|
+
function unregisterCheckboxGroup(root) {
|
|
101
|
+
if (!root) return;
|
|
102
|
+
registry.delete(root);
|
|
103
|
+
}
|
|
104
|
+
function getCheckboxGroupMachineFor(el) {
|
|
105
|
+
if (!el) return void 0;
|
|
106
|
+
const root = el.closest("[data-scope=\"checkbox-group\"][data-part=\"root\"]");
|
|
107
|
+
if (!root) return void 0;
|
|
108
|
+
return registry.get(root);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
export { connect as i, registerCheckboxGroup as n, unregisterCheckboxGroup as r, getCheckboxGroupMachineFor as t };
|