@techsio/ui-kit 0.42.0 → 0.44.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.
@@ -0,0 +1,508 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { createContext, createElement, useContext, useEffect, useId, useMemo, useState } from "react";
3
+ import { collection as cascade_select_collection, connect, machine } from "@zag-js/cascade-select";
4
+ import { Portal, mergeProps, normalizeProps, useMachine } from "@zag-js/react";
5
+ import { ActionIcon } from "../atoms/action-icon.js";
6
+ import { Button } from "../atoms/button.js";
7
+ import { Icon } from "../atoms/icon.js";
8
+ import { Label } from "../atoms/label.js";
9
+ import { StatusText } from "../atoms/status-text.js";
10
+ import { tv } from "../utils.js";
11
+ const toControlSize = (size)=>"xs" === size ? "sm" : size;
12
+ const controlGlyphClass = {
13
+ sm: "text-icon-control-sm",
14
+ md: "text-icon-control-md",
15
+ lg: "text-icon-control-lg"
16
+ };
17
+ const cascadeSelectVariants = tv({
18
+ slots: {
19
+ root: "relative flex w-full flex-col gap-cascade-select",
20
+ control: "relative flex w-full items-center justify-between",
21
+ positioner: "isolate",
22
+ trigger: [
23
+ "form-control-base group w-full",
24
+ "flex items-center justify-between gap-0",
25
+ "border-cascade-select-trigger-border",
26
+ "text-left font-normal",
27
+ "hover:border-cascade-select-trigger-border-hover",
28
+ "hover:bg-cascade-select-trigger-bg-hover",
29
+ "focus:border-cascade-select-trigger-border-focus",
30
+ "focus-visible:outline-(style:--default-ring-style) focus-visible:outline-(length:--default-ring-width)",
31
+ "focus-visible:outline-cascade-select-ring",
32
+ "focus-visible:outline-offset-(length:--default-ring-offset)",
33
+ "data-disabled:cursor-not-allowed",
34
+ "data-disabled:border-cascade-select-border-disabled",
35
+ "data-disabled:bg-cascade-select-bg-disabled",
36
+ "data-disabled:text-cascade-select-fg-disabled",
37
+ "data-[validation=error]:border-(length:--border-width-validation)",
38
+ "data-[validation=error]:border-cascade-select-border-error data-[validation=error]:outline-cascade-select-border-error",
39
+ "data-[validation=error]:outline-(style:--default-ring-style) data-[validation=error]:outline-(length:--default-ring-width)",
40
+ "data-[validation=error]:outline-offset-(length:--default-ring-offset)",
41
+ "data-[validation=success]:border-(length:--border-width-validation)",
42
+ "data-[validation=success]:border-cascade-select-border-success data-[validation=success]:outline-cascade-select-border-success",
43
+ "data-[validation=success]:outline-(style:--default-ring-style) data-[validation=success]:outline-(length:--default-ring-width)",
44
+ "data-[validation=success]:outline-offset-(length:--default-ring-offset)",
45
+ "data-[validation=warning]:border-(length:--border-width-validation)",
46
+ "data-[validation=warning]:border-cascade-select-border-warning data-[validation=warning]:outline-cascade-select-border-warning",
47
+ "data-[validation=warning]:outline-(style:--default-ring-style) data-[validation=warning]:outline-(length:--default-ring-width)",
48
+ "data-[validation=warning]:outline-offset-(length:--default-ring-offset)",
49
+ "transition-colors duration-200 motion-reduce:transition-none"
50
+ ],
51
+ clearTrigger: "-translate-y-1/2 absolute top-1/2 right-cascade-select-right data-readonly:hidden",
52
+ content: [
53
+ "popup-surface-base flex w-max min-w-(--reference-width) max-w-(--available-width) overflow-x-auto",
54
+ "duration-200 ease-out motion-safe:transition-[opacity,display,translate,scale]",
55
+ "transition-discrete",
56
+ "starting:scale-98 starting:opacity-0",
57
+ "data-[state=open]:starting:scale-98 data-[state=open]:starting:opacity-0",
58
+ "data-[state=open]:scale-100 data-[state=open]:opacity-100",
59
+ "data-[state=closed]:scale-98 data-[state=closed]:opacity-0"
60
+ ],
61
+ list: "min-w-cascade-select-column grow basis-0",
62
+ item: [
63
+ "popup-item-base",
64
+ "hover:bg-popup-item-bg-hover",
65
+ "data-highlighted:bg-popup-item-bg-hover",
66
+ "data-[state=checked]:bg-popup-item-bg-selected",
67
+ "data-[state=checked]:text-popup-item-fg-selected",
68
+ "data-disabled:cursor-not-allowed data-disabled:text-popup-item-fg-disabled",
69
+ "transition-colors duration-200 motion-reduce:transition-none"
70
+ ],
71
+ itemText: "min-w-0 flex-grow truncate",
72
+ branchIndicator: [
73
+ "last:-me-(--size-popup-indicator) flex size-(--size-popup-indicator) shrink-0 items-center justify-center",
74
+ "rtl:rotate-180"
75
+ ],
76
+ itemIndicator: [
77
+ "last:-me-(--size-popup-indicator) flex size-(--size-popup-indicator) shrink-0 items-center justify-center",
78
+ "text-popup-item-fg-selected"
79
+ ],
80
+ valueText: [
81
+ "min-w-0 flex-grow truncate font-normal",
82
+ "data-placeholder:text-cascade-select-placeholder"
83
+ ],
84
+ indicator: [
85
+ "shrink-0 text-cascade-select-trigger-fg-base group-hover:text-cascade-select-trigger-fg-hover",
86
+ "data-[state=open]:rotate-180",
87
+ "motion-safe:transition-[transform,color] motion-safe:duration-200 motion-reduce:transition-none"
88
+ ]
89
+ },
90
+ variants: {
91
+ size: {
92
+ xs: {
93
+ trigger: "p-cascade-select-trigger-sm text-cascade-select-trigger-xs",
94
+ content: "popup-size-xs text-cascade-select-item-xs",
95
+ valueText: "text-cascade-select-value-xs"
96
+ },
97
+ sm: {
98
+ trigger: "h-form-control-sm rounded-cascade-select-sm p-cascade-select-trigger-sm text-cascade-select-trigger-sm",
99
+ content: "popup-size-sm text-cascade-select-item-sm",
100
+ valueText: "text-cascade-select-value-sm"
101
+ },
102
+ md: {
103
+ trigger: "h-form-control-md rounded-cascade-select-md p-cascade-select-trigger-md text-cascade-select-trigger-md",
104
+ content: "popup-size-md text-cascade-select-item-md",
105
+ valueText: "text-cascade-select-value-md"
106
+ },
107
+ lg: {
108
+ trigger: "p-cascade-select-trigger-md text-cascade-select-trigger-lg",
109
+ content: "popup-size-lg text-cascade-select-item-lg",
110
+ valueText: "text-cascade-select-value-lg"
111
+ }
112
+ }
113
+ },
114
+ defaultVariants: {
115
+ size: "md"
116
+ }
117
+ });
118
+ const pathsMatch = (api, selectedItems)=>selectedItems.length === api.value.length && selectedItems.every((items, pathIndex)=>items.length === api.value[pathIndex]?.length && items.every((item, itemIndex)=>api.collection.getNodeValue(item) === api.value[pathIndex]?.[itemIndex]));
119
+ const resolveSelectedItems = (api)=>{
120
+ if (pathsMatch(api, api.selectedItems)) return api.selectedItems;
121
+ return api.value.map((valuePath)=>{
122
+ const indexPath = api.collection.getIndexPath(valuePath);
123
+ return indexPath.map((_, index)=>api.collection.at(indexPath.slice(0, index + 1))).filter((item)=>void 0 !== item);
124
+ });
125
+ };
126
+ const formatSelectedItems = (api, selectedItems)=>{
127
+ const stringify = (item)=>api.collection.stringifyNode(item) ?? api.collection.getNodeValue(item);
128
+ if (api.multiple) return selectedItems.map((path)=>path.at(-1)).filter((item)=>void 0 !== item).map(stringify).join(", ");
129
+ return selectedItems.map((path)=>path.map(stringify).join(" / ")).join(", ");
130
+ };
131
+ const CascadeSelectContext = /*#__PURE__*/ createContext(null);
132
+ const CascadeSelectItemContext = /*#__PURE__*/ createContext(null);
133
+ function useCascadeSelectContext() {
134
+ const context = useContext(CascadeSelectContext);
135
+ if (!context) throw new Error("CascadeSelect parts must be used within CascadeSelect");
136
+ return context;
137
+ }
138
+ function useCascadeSelectItemContext() {
139
+ const context = useContext(CascadeSelectItemContext);
140
+ if (!context) throw new Error("CascadeSelect item parts must be used within CascadeSelect.Item");
141
+ return context;
142
+ }
143
+ function CascadeSelect({ children, className, id: providedId, items, ref, required = false, size = "md", validateStatus = "default", ...machineProps }) {
144
+ const generatedId = useId();
145
+ const id = providedId || generatedId;
146
+ const [statusTextIds, setStatusTextIds] = useState([]);
147
+ const collection = useMemo(()=>cascade_select_collection({
148
+ rootNode: {
149
+ children: items,
150
+ label: "",
151
+ value: "__cascade-select-root__"
152
+ },
153
+ isNodeDisabled: (item)=>item.disabled ?? false,
154
+ nodeToChildren: (item)=>item.children ?? [],
155
+ nodeToString: (item)=>item.label,
156
+ nodeToValue: (item)=>item.value
157
+ }), [
158
+ items
159
+ ]);
160
+ const service = useMachine(machine, {
161
+ ...machineProps,
162
+ collection,
163
+ id,
164
+ invalid: "error" === validateStatus,
165
+ positioning: {
166
+ fitViewport: true,
167
+ placement: "bottom-start",
168
+ ...machineProps.positioning
169
+ },
170
+ required
171
+ });
172
+ const api = connect(service, normalizeProps);
173
+ const styles = cascadeSelectVariants({
174
+ size
175
+ });
176
+ return /*#__PURE__*/ jsx(CascadeSelectContext.Provider, {
177
+ value: {
178
+ api,
179
+ formatValue: machineProps.formatValue,
180
+ required,
181
+ size,
182
+ statusTextIds,
183
+ setStatusTextIds,
184
+ validateStatus
185
+ },
186
+ children: /*#__PURE__*/ jsxs("div", {
187
+ ...api.getRootProps(),
188
+ className: styles.root({
189
+ className
190
+ }),
191
+ ref: ref,
192
+ children: [
193
+ /*#__PURE__*/ jsx("input", {
194
+ ...api.getHiddenInputProps()
195
+ }),
196
+ children
197
+ ]
198
+ })
199
+ });
200
+ }
201
+ CascadeSelect.Label = function({ children, ...props }) {
202
+ const { api, required } = useCascadeSelectContext();
203
+ return /*#__PURE__*/ jsx(Label, {
204
+ ...mergeProps(api.getLabelProps(), props),
205
+ disabled: api.disabled,
206
+ required: required,
207
+ children: children
208
+ });
209
+ };
210
+ CascadeSelect.Control = function({ className, ref, ...props }) {
211
+ const { api, size } = useCascadeSelectContext();
212
+ const styles = cascadeSelectVariants({
213
+ size
214
+ });
215
+ return /*#__PURE__*/ jsx("div", {
216
+ ...mergeProps(api.getControlProps(), props),
217
+ className: styles.control({
218
+ className
219
+ }),
220
+ ref: ref
221
+ });
222
+ };
223
+ CascadeSelect.Trigger = function({ className, ref, ...props }) {
224
+ const { api, size, statusTextIds, validateStatus } = useCascadeSelectContext();
225
+ const styles = cascadeSelectVariants({
226
+ size
227
+ });
228
+ const triggerProps = mergeProps(api.getTriggerProps(), props);
229
+ const ariaDescribedBy = [
230
+ triggerProps["aria-describedby"],
231
+ ...statusTextIds
232
+ ].filter(Boolean).join(" ") || void 0;
233
+ return /*#__PURE__*/ jsx(Button, {
234
+ ...triggerProps,
235
+ "aria-describedby": ariaDescribedBy,
236
+ className: styles.trigger({
237
+ className
238
+ }),
239
+ "data-validation": "default" === validateStatus ? void 0 : validateStatus,
240
+ ref: ref,
241
+ size: "current",
242
+ theme: "unstyled"
243
+ });
244
+ };
245
+ CascadeSelect.ValueText = function({ children, className, placeholder = "Select an option", ref, ...props }) {
246
+ const { api, formatValue, size } = useCascadeSelectContext();
247
+ const styles = cascadeSelectVariants({
248
+ size
249
+ });
250
+ const selectedItems = resolveSelectedItems(api);
251
+ const valueText = selectedItems === api.selectedItems ? api.valueAsString : formatValue?.(selectedItems) ?? formatSelectedItems(api, selectedItems);
252
+ const hasValue = api.hasSelectedItems;
253
+ let content = hasValue ? valueText : placeholder;
254
+ if (hasValue && "function" == typeof children) content = children(selectedItems);
255
+ else if (hasValue && void 0 !== children && "function" != typeof children) content = children;
256
+ return /*#__PURE__*/ jsx("span", {
257
+ ...mergeProps(api.getValueTextProps(), props),
258
+ className: styles.valueText({
259
+ className
260
+ }),
261
+ "data-placeholder": hasValue ? void 0 : "",
262
+ ref: ref,
263
+ children: content
264
+ });
265
+ };
266
+ CascadeSelect.Indicator = function({ children, className, iconSize = "current", ref, ...props }) {
267
+ const { api, size } = useCascadeSelectContext();
268
+ const styles = cascadeSelectVariants({
269
+ size
270
+ });
271
+ return /*#__PURE__*/ jsx("span", {
272
+ ...mergeProps(api.getIndicatorProps(), props),
273
+ className: styles.indicator({
274
+ className: `${controlGlyphClass[toControlSize(size)]} ${className ?? ""}`
275
+ }),
276
+ ref: ref,
277
+ children: children ?? /*#__PURE__*/ jsx(Icon, {
278
+ icon: "token-icon-cascade-select-indicator",
279
+ size: iconSize
280
+ })
281
+ });
282
+ };
283
+ CascadeSelect.ClearTrigger = function({ "aria-label": ariaLabel = "Clear selection", className, ref, ...props }) {
284
+ const { api, size } = useCascadeSelectContext();
285
+ const styles = cascadeSelectVariants({
286
+ size
287
+ });
288
+ return /*#__PURE__*/ jsx(ActionIcon, {
289
+ ...mergeProps(api.getClearTriggerProps(), props),
290
+ "aria-label": ariaLabel,
291
+ className: styles.clearTrigger({
292
+ className
293
+ }),
294
+ icon: "token-icon-cascade-select-clear",
295
+ ref: ref,
296
+ size: toControlSize(size),
297
+ tone: "neutral"
298
+ });
299
+ };
300
+ CascadeSelect.Positioner = function({ className, ref, ...props }) {
301
+ const { api, size } = useCascadeSelectContext();
302
+ const styles = cascadeSelectVariants({
303
+ size
304
+ });
305
+ return /*#__PURE__*/ jsx(Portal, {
306
+ children: /*#__PURE__*/ jsx("div", {
307
+ ...mergeProps(api.getPositionerProps(), props),
308
+ className: styles.positioner({
309
+ className
310
+ }),
311
+ ref: ref
312
+ })
313
+ });
314
+ };
315
+ CascadeSelect.Content = function({ className, ref, ...props }) {
316
+ const { api, size } = useCascadeSelectContext();
317
+ const styles = cascadeSelectVariants({
318
+ size
319
+ });
320
+ return /*#__PURE__*/ jsx("div", {
321
+ ...mergeProps(api.getContentProps(), props),
322
+ className: styles.content({
323
+ className
324
+ }),
325
+ ref: ref
326
+ });
327
+ };
328
+ CascadeSelect.List = function({ className, indexPath, item, ref, value, ...props }) {
329
+ const { api, size } = useCascadeSelectContext();
330
+ const styles = cascadeSelectVariants({
331
+ size
332
+ });
333
+ const itemProps = {
334
+ indexPath,
335
+ item,
336
+ value
337
+ };
338
+ return /*#__PURE__*/ jsx("ul", {
339
+ ...mergeProps(api.getListProps(itemProps), props),
340
+ className: styles.list({
341
+ className
342
+ }),
343
+ ref: ref
344
+ });
345
+ };
346
+ CascadeSelect.Item = function({ children, className, indexPath, item, ref, value, ...props }) {
347
+ const { api, size } = useCascadeSelectContext();
348
+ const styles = cascadeSelectVariants({
349
+ size
350
+ });
351
+ const itemProps = {
352
+ indexPath,
353
+ item,
354
+ value
355
+ };
356
+ const itemState = api.getItemState(itemProps);
357
+ return /*#__PURE__*/ jsx(CascadeSelectItemContext.Provider, {
358
+ value: {
359
+ itemProps,
360
+ itemState
361
+ },
362
+ children: /*#__PURE__*/ jsx("li", {
363
+ ...mergeProps(api.getItemProps(itemProps), props),
364
+ className: styles.item({
365
+ className
366
+ }),
367
+ ref: ref,
368
+ children: children
369
+ })
370
+ });
371
+ };
372
+ CascadeSelect.ItemText = function({ children, className, ref, ...props }) {
373
+ const { api, size } = useCascadeSelectContext();
374
+ const { itemProps } = useCascadeSelectItemContext();
375
+ const styles = cascadeSelectVariants({
376
+ size
377
+ });
378
+ return /*#__PURE__*/ jsx("span", {
379
+ ...mergeProps(api.getItemTextProps(itemProps), props),
380
+ className: styles.itemText({
381
+ className
382
+ }),
383
+ ref: ref,
384
+ children: children ?? itemProps.item.label
385
+ });
386
+ };
387
+ CascadeSelect.BranchIndicator = function({ children, className, iconSize = "current", ref, ...props }) {
388
+ const { size } = useCascadeSelectContext();
389
+ const { itemState } = useCascadeSelectItemContext();
390
+ const styles = cascadeSelectVariants({
391
+ size
392
+ });
393
+ if (!itemState.hasChildren) return null;
394
+ return /*#__PURE__*/ jsx("span", {
395
+ "aria-hidden": "true",
396
+ className: styles.branchIndicator({
397
+ className
398
+ }),
399
+ ref: ref,
400
+ ...props,
401
+ children: children ?? /*#__PURE__*/ jsx(Icon, {
402
+ icon: "token-icon-cascade-select-branch",
403
+ size: iconSize
404
+ })
405
+ });
406
+ };
407
+ CascadeSelect.ItemIndicator = function({ children, className, iconSize = "current", ref, ...props }) {
408
+ const { api, size } = useCascadeSelectContext();
409
+ const { itemProps } = useCascadeSelectItemContext();
410
+ const styles = cascadeSelectVariants({
411
+ size
412
+ });
413
+ return /*#__PURE__*/ jsx("span", {
414
+ ...mergeProps(api.getItemIndicatorProps(itemProps), props),
415
+ className: styles.itemIndicator({
416
+ className
417
+ }),
418
+ ref: ref,
419
+ children: children ?? /*#__PURE__*/ jsx(Icon, {
420
+ icon: "token-icon-cascade-select-check",
421
+ size: iconSize
422
+ })
423
+ });
424
+ };
425
+ function CascadeSelectTreeNode({ indexPath, node, value }) {
426
+ const { api } = useCascadeSelectContext();
427
+ const nodeProps = {
428
+ indexPath,
429
+ item: node,
430
+ value
431
+ };
432
+ const nodeState = api.getItemState(nodeProps);
433
+ const children = api.collection.getNodeChildren(node);
434
+ const highlightedChild = nodeState.highlightedChild;
435
+ return /*#__PURE__*/ jsxs(Fragment, {
436
+ children: [
437
+ /*#__PURE__*/ jsx(CascadeSelect.List, {
438
+ ...nodeProps,
439
+ children: children.map((item, index)=>{
440
+ const itemValue = api.collection.getNodeValue(item);
441
+ const itemProps = {
442
+ indexPath: [
443
+ ...indexPath,
444
+ index
445
+ ],
446
+ item,
447
+ value: [
448
+ ...value,
449
+ itemValue
450
+ ]
451
+ };
452
+ return /*#__PURE__*/ createElement(CascadeSelect.Item, {
453
+ ...itemProps,
454
+ key: itemValue,
455
+ children: [
456
+ /*#__PURE__*/ jsx(CascadeSelect.ItemText, {}, "text"),
457
+ /*#__PURE__*/ jsx(CascadeSelect.ItemIndicator, {}, "item-indicator"),
458
+ /*#__PURE__*/ jsx(CascadeSelect.BranchIndicator, {}, "branch-indicator")
459
+ ]
460
+ });
461
+ })
462
+ }),
463
+ highlightedChild && api.collection.isBranchNode(highlightedChild) && /*#__PURE__*/ jsx(CascadeSelectTreeNode, {
464
+ indexPath: [
465
+ ...indexPath,
466
+ nodeState.highlightedIndex
467
+ ],
468
+ node: highlightedChild,
469
+ value: [
470
+ ...value,
471
+ api.collection.getNodeValue(highlightedChild)
472
+ ]
473
+ })
474
+ ]
475
+ });
476
+ }
477
+ CascadeSelect.Node = function({ indexPath = [], node, value = [] }) {
478
+ const { api } = useCascadeSelectContext();
479
+ return /*#__PURE__*/ jsx(CascadeSelectTreeNode, {
480
+ indexPath: indexPath,
481
+ node: node ?? api.collection.rootNode,
482
+ value: value
483
+ });
484
+ };
485
+ CascadeSelect.StatusText = function({ id: providedId, size: sizeProp, status: statusProp, ...props }) {
486
+ const { setStatusTextIds, size, validateStatus } = useCascadeSelectContext();
487
+ const generatedId = useId();
488
+ const id = providedId || generatedId;
489
+ const effectiveSize = sizeProp ?? size;
490
+ useEffect(()=>{
491
+ setStatusTextIds((ids)=>[
492
+ ...ids,
493
+ id
494
+ ]);
495
+ return ()=>setStatusTextIds((ids)=>ids.filter((statusId)=>statusId !== id));
496
+ }, [
497
+ id,
498
+ setStatusTextIds
499
+ ]);
500
+ return /*#__PURE__*/ jsx(StatusText, {
501
+ ...props,
502
+ id: id,
503
+ size: "xs" === effectiveSize ? "sm" : effectiveSize,
504
+ status: statusProp ?? validateStatus
505
+ });
506
+ };
507
+ CascadeSelect.displayName = "CascadeSelect";
508
+ export { CascadeSelect, cascadeSelectVariants, useCascadeSelectContext };