@mks2508/mks-ui 0.5.1 → 0.5.2

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.
@@ -68,6 +68,8 @@ import { dataCardStyles, dataCardVariants } from "./ui/DataCard/DataCard.styles.
68
68
  import { DataCard, DataCardActions, DataCardBracket, DataCardLabel, DataCardToggle, DataCardValue, useDataCard } from "./ui/DataCard/index.js";
69
69
  import { useListFormat } from "./hooks/Formatting/UseListFormat.js";
70
70
  import { TextFlow } from "./ui/TextFlow/index.js";
71
+ import { dynamicToggleStyles } from "./ui/DynamicToggle/DynamicToggle.styles.js";
72
+ import { DynamicToggle, DynamicToggleGroup, DynamicToggleOption } from "./ui/DynamicToggle/index.js";
71
73
  import "./ui/index.js";
72
74
  import { MorphingPopover, MorphingPopoverWithTarget } from "./components/MorphingPopover/index.js";
73
75
  import "./components/index.js";
@@ -108,4 +110,4 @@ import { HugeIcons } from "./icons/index.js";
108
110
  import { IconWrapper } from "./lib/icon-wrapper.js";
109
111
  import "./lib/index.js";
110
112
 
111
- export { ANIMATION_CONFIGS, ANIMATION_DEFAULTS, Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionStyles, AccordionTrigger, ActivityIcon, AlertDialog, AlertDialogBackdrop, AlertDialogClose, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogPopup, AlertDialogPortal, AlertDialogStyles, AlertDialogTitle, AlertDialogTrigger, ArrowDownToLineIcon, ArrowUpIcon, AutoHeight, Badge, BellElectricIcon, BellIcon, BotIcon, BoxIcon, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Check, Checkbox, CheckboxIndicator, CircleCheckIcon, Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, ComboboxSeparator, ComboboxTrigger, ComboboxValue, CornerBracket, CountingNumber, DataCard, DataCardActions, DataCardBracket, DataCardLabel, DataCardToggle, DataCardValue, DeleteIcon, Dialog, DialogBackdrop, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogPopup, DialogPortal, DialogTitle, DialogTrigger, DownloadIcon, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EASINGS, EFFECTS, Edit2, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle, Globe, Highlight, HighlightItem, HomeIcon, HugeIcons, IconWrapper, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea, Label, Layers, LayoutPanelTopIcon, ListIcon, Menu, MenuArrow, MenuCheckboxItem, MenuCheckboxItemIndicator, MenuGroup, MenuGroupLabel, MenuHighlight, MenuHighlightItem, MenuItem, MenuPopup, MenuPortal, MenuPositioner, MenuRadioGroup, MenuRadioItem, MenuRadioItemIndicator, MenuSeparator, MenuShortcut, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger, Morph, MorphingPopover, MorphingPopoverWithTarget, PRESETS, Package, Palette, PlusIcon, Popover, PopoverArrow, PopoverBackdrop, PopoverClose, PopoverDescription, PopoverPopup, PopoverPortal, PopoverPositioner, PopoverTitle, PopoverTrigger, Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue, RESPONSIVE_CONFIGS, RefreshCw, ReorderRoot as Reorder, Rocket, Save, SearchIcon, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SettingsIcon, SlidingNumber, SlidingText, Slot, Switch, SwitchIcon, SwitchThumb, TIMING, TRANSFORMS, Tabs, TabsHighlight, TabsHighlightItem, TabsList, TabsPanel, TabsPanels, TabsTab, TerminalIcon, TextFlow, Textarea, Tooltip, TooltipArrow, TooltipPopup, TooltipPortal, TooltipPositioner, TooltipProvider, TooltipTrigger, Trash2, TrendingDownIcon, TrendingUpIcon, Type, Upload, XIcon, accordionVariants, badgeVariants, bracketVariants, buttonStateStyles, buttonVariants, calculateSeparatorCoordination, cardVariants, checkboxStyles, cn, dataCardStyles, dataCardVariants, dialogStyles, fieldVariants, getResponsiveDuration, getResponsiveStagger, getStrictContext, inputGroupAddonVariants, inputGroupButtonVariants, popoverStyles, progressStyles, shouldShowSeparator, switchStyles, tabsIndicatorVariants, tabsListVariants, tabsTabVariants, tooltipStyles, useAccordionItem, useAlertDialog, useAnimationOrchestrator, useAutoHeight, useCSSGridMorph, useCheckbox, useComboboxAnchor, useControlledState, useDataCard, useDataState, useDialog, useElementRegistry, useFLIPAnimation, useFLIPClipPath, useHighlight, useIsInView, useListFormat, useMenu, useMenuActiveValue, useMorph, useMorphContext, usePopover, usePositionCapture, useProgress, useReorder, useReorderPresence, useSwitch, useTooltip, useViewTransitions };
113
+ export { ANIMATION_CONFIGS, ANIMATION_DEFAULTS, Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionStyles, AccordionTrigger, ActivityIcon, AlertDialog, AlertDialogBackdrop, AlertDialogClose, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogPopup, AlertDialogPortal, AlertDialogStyles, AlertDialogTitle, AlertDialogTrigger, ArrowDownToLineIcon, ArrowUpIcon, AutoHeight, Badge, BellElectricIcon, BellIcon, BotIcon, BoxIcon, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Check, Checkbox, CheckboxIndicator, CircleCheckIcon, Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, ComboboxSeparator, ComboboxTrigger, ComboboxValue, CornerBracket, CountingNumber, DataCard, DataCardActions, DataCardBracket, DataCardLabel, DataCardToggle, DataCardValue, DeleteIcon, Dialog, DialogBackdrop, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogPopup, DialogPortal, DialogTitle, DialogTrigger, DownloadIcon, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, DynamicToggle, DynamicToggleGroup, DynamicToggleOption, EASINGS, EFFECTS, Edit2, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle, Globe, Highlight, HighlightItem, HomeIcon, HugeIcons, IconWrapper, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputGroupTextarea, Label, Layers, LayoutPanelTopIcon, ListIcon, Menu, MenuArrow, MenuCheckboxItem, MenuCheckboxItemIndicator, MenuGroup, MenuGroupLabel, MenuHighlight, MenuHighlightItem, MenuItem, MenuPopup, MenuPortal, MenuPositioner, MenuRadioGroup, MenuRadioItem, MenuRadioItemIndicator, MenuSeparator, MenuShortcut, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger, Morph, MorphingPopover, MorphingPopoverWithTarget, PRESETS, Package, Palette, PlusIcon, Popover, PopoverArrow, PopoverBackdrop, PopoverClose, PopoverDescription, PopoverPopup, PopoverPortal, PopoverPositioner, PopoverTitle, PopoverTrigger, Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue, RESPONSIVE_CONFIGS, RefreshCw, ReorderRoot as Reorder, Rocket, Save, SearchIcon, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SettingsIcon, SlidingNumber, SlidingText, Slot, Switch, SwitchIcon, SwitchThumb, TIMING, TRANSFORMS, Tabs, TabsHighlight, TabsHighlightItem, TabsList, TabsPanel, TabsPanels, TabsTab, TerminalIcon, TextFlow, Textarea, Tooltip, TooltipArrow, TooltipPopup, TooltipPortal, TooltipPositioner, TooltipProvider, TooltipTrigger, Trash2, TrendingDownIcon, TrendingUpIcon, Type, Upload, XIcon, accordionVariants, badgeVariants, bracketVariants, buttonStateStyles, buttonVariants, calculateSeparatorCoordination, cardVariants, checkboxStyles, cn, dataCardStyles, dataCardVariants, dialogStyles, dynamicToggleStyles, fieldVariants, getResponsiveDuration, getResponsiveStagger, getStrictContext, inputGroupAddonVariants, inputGroupButtonVariants, popoverStyles, progressStyles, shouldShowSeparator, switchStyles, tabsIndicatorVariants, tabsListVariants, tabsTabVariants, tooltipStyles, useAccordionItem, useAlertDialog, useAnimationOrchestrator, useAutoHeight, useCSSGridMorph, useCheckbox, useComboboxAnchor, useControlledState, useDataCard, useDataState, useDialog, useElementRegistry, useFLIPAnimation, useFLIPClipPath, useHighlight, useIsInView, useListFormat, useMenu, useMenuActiveValue, useMorph, useMorphContext, usePopover, usePositionCapture, useProgress, useReorder, useReorderPresence, useSwitch, useTooltip, useViewTransitions };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * DynamicToggle style slots and types.
3
+ *
4
+ * Uses semantic CSS variables for all colors.
5
+ * Animation handled in DynamicToggle.css (clip-path, transitions).
6
+ *
7
+ * @module @mks2508/mks-ui/react/ui/DynamicToggle
8
+ */
9
+ import type { StyleSlots } from '../../../core/types';
10
+ /** Slot names for DynamicToggle */
11
+ export type DynamicToggleSlot = 'root' | 'track' | 'option' | 'indicator' | 'group' | 'groupLabel' | 'groupIndicator';
12
+ /**
13
+ * Default styles for each DynamicToggle slot.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <DynamicToggle slots={{ root: 'w-72', indicator: 'bg-primary' }}>
18
+ * ...
19
+ * </DynamicToggle>
20
+ * ```
21
+ */
22
+ export declare const dynamicToggleStyles: StyleSlots<DynamicToggleSlot>;
23
+ //# sourceMappingURL=DynamicToggle.styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DynamicToggle.styles.d.ts","sourceRoot":"","sources":["../../../../src/react-ui/ui/DynamicToggle/DynamicToggle.styles.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,mCAAmC;AACnC,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,OAAO,GACP,QAAQ,GACR,WAAW,GACX,OAAO,GACP,YAAY,GACZ,gBAAgB,CAAC;AAErB;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,EAAE,UAAU,CAAC,iBAAiB,CAiC7D,CAAC"}
@@ -0,0 +1,39 @@
1
+ //#region src/react-ui/ui/DynamicToggle/DynamicToggle.styles.ts
2
+ /**
3
+ * Default styles for each DynamicToggle slot.
4
+ *
5
+ * @example
6
+ * ```tsx
7
+ * <DynamicToggle slots={{ root: 'w-72', indicator: 'bg-primary' }}>
8
+ * ...
9
+ * </DynamicToggle>
10
+ * ```
11
+ */
12
+ const dynamicToggleStyles = {
13
+ root: ["relative rounded-full border border-border bg-card", "p-[2px] select-none"].join(" "),
14
+ track: ["relative grid place-items-center", "w-full h-full"].join(" "),
15
+ option: [
16
+ "inline-grid place-items-center cursor-pointer",
17
+ "text-xs font-medium z-[2] h-full w-full",
18
+ "transition-[color,opacity] duration-[220ms]"
19
+ ].join(" "),
20
+ indicator: [
21
+ "absolute w-1/2 left-0 top-0 bottom-0",
22
+ "bg-foreground rounded-full",
23
+ "pointer-events-none z-0"
24
+ ].join(" "),
25
+ group: ["relative w-full h-full grid", "border border-transparent"].join(" "),
26
+ groupLabel: [
27
+ "absolute left-1/2 top-1/2 z-[2]",
28
+ "text-xs font-medium text-foreground",
29
+ "pointer-events-none"
30
+ ].join(" "),
31
+ groupIndicator: [
32
+ "absolute left-1/2 top-0 bottom-0",
33
+ "bg-foreground rounded-full",
34
+ "pointer-events-none z-0"
35
+ ].join(" ")
36
+ };
37
+
38
+ //#endregion
39
+ export { dynamicToggleStyles };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * DynamicToggle type definitions.
3
+ *
4
+ * A CSS-animated toggle where one option can expand into sub-options.
5
+ * Uses hidden radio inputs + `:has(:checked)` for zero-JS animation.
6
+ *
7
+ * @module @mks2508/mks-ui/react/ui/DynamicToggle
8
+ */
9
+ import type { SlotOverrides } from '../../../core/types';
10
+ import type { DynamicToggleSlot } from './DynamicToggle.styles';
11
+ /**
12
+ * Context shared between DynamicToggle root and its children.
13
+ */
14
+ export type DynamicToggleContextType = {
15
+ /** Current selected value */
16
+ value: string;
17
+ /** Update selected value */
18
+ setValue: (value: string) => void;
19
+ /** Auto-generated radio group name */
20
+ groupName: string;
21
+ /** Whether a group child is active */
22
+ groupActive: boolean;
23
+ };
24
+ /**
25
+ * Props for the DynamicToggle root container.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * <DynamicToggle value="tree" onValueChange={setVal}>
30
+ * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
31
+ * <DynamicToggleGroup label="Changes">
32
+ * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
33
+ * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
34
+ * </DynamicToggleGroup>
35
+ * </DynamicToggle>
36
+ * ```
37
+ */
38
+ export interface IDynamicToggleProps {
39
+ /** Controlled value */
40
+ value?: string;
41
+ /** Uncontrolled default value */
42
+ defaultValue?: string;
43
+ /** Change callback */
44
+ onValueChange?: (value: string) => void;
45
+ /** Slot class overrides */
46
+ slots?: SlotOverrides<DynamicToggleSlot>;
47
+ /** Additional class for the root */
48
+ className?: string;
49
+ /** DynamicToggleOption and DynamicToggleGroup children */
50
+ children: React.ReactNode;
51
+ }
52
+ /**
53
+ * Props for a single toggle option (top-level or inside a group).
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
58
+ * ```
59
+ */
60
+ export interface IDynamicToggleOptionProps {
61
+ /** Value this option represents */
62
+ value: string;
63
+ /** Label content */
64
+ children: React.ReactNode;
65
+ /** Additional class */
66
+ className?: string;
67
+ }
68
+ /**
69
+ * Props for an expandable group of options.
70
+ * When none of the group's options are active, shows the collapsed `label`.
71
+ * When one is active, expands to show all sub-options.
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * <DynamicToggleGroup label="Changes">
76
+ * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
77
+ * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
78
+ * </DynamicToggleGroup>
79
+ * ```
80
+ */
81
+ export interface IDynamicToggleGroupProps {
82
+ /** Label shown when collapsed */
83
+ label: string;
84
+ /** DynamicToggleOption children */
85
+ children: React.ReactNode;
86
+ /** Additional class */
87
+ className?: string;
88
+ }
89
+ //# sourceMappingURL=DynamicToggle.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DynamicToggle.types.d.ts","sourceRoot":"","sources":["../../../../src/react-ui/ui/DynamicToggle/DynamicToggle.types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,mBAAmB;IAClC,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,2BAA2B;IAC3B,KAAK,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IACzC,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,yBAAyB;IACxC,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,wBAAwB;IACvC,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
@@ -0,0 +1,29 @@
1
+ import type { IDynamicToggleProps, IDynamicToggleOptionProps, IDynamicToggleGroupProps } from './DynamicToggle.types';
2
+ /**
3
+ * Root container — pill-shaped toggle control.
4
+ *
5
+ * @param props - Component props
6
+ * @returns React component
7
+ */
8
+ declare function DynamicToggle({ slots, className, children, ...props }: IDynamicToggleProps): import("react/jsx-runtime").JSX.Element;
9
+ /**
10
+ * A single toggle option — renders a hidden radio + visible label.
11
+ * Can be used at the top level or inside a DynamicToggleGroup.
12
+ *
13
+ * @param props - Component props
14
+ * @returns React component
15
+ */
16
+ declare function DynamicToggleOption({ value, children, className }: IDynamicToggleOptionProps): import("react/jsx-runtime").JSX.Element;
17
+ /**
18
+ * An expandable group of options. Shows `label` when collapsed,
19
+ * expands to show sub-options when one is active.
20
+ *
21
+ * @param props - Component props
22
+ * @returns React component
23
+ */
24
+ declare function DynamicToggleGroup({ label, children, className }: IDynamicToggleGroupProps): import("react/jsx-runtime").JSX.Element;
25
+ export { DynamicToggle, DynamicToggleOption, DynamicToggleGroup };
26
+ export type { IDynamicToggleProps, IDynamicToggleOptionProps, IDynamicToggleGroupProps, DynamicToggleContextType, } from './DynamicToggle.types';
27
+ export type { DynamicToggleSlot } from './DynamicToggle.styles';
28
+ export { dynamicToggleStyles } from './DynamicToggle.styles';
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/react-ui/ui/DynamicToggle/index.tsx"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAEV,mBAAmB,EACnB,yBAAyB,EACzB,wBAAwB,EACzB,MAAM,uBAAuB,CAAC;AAyD/B;;;;;GAKG;AACH,iBAAS,aAAa,CAAC,EACrB,KAAK,EACL,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,mBAAmB,2CAiDrB;AAMD;;;;;;GAMG;AACH,iBAAS,mBAAmB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,yBAAyB,2CA0BrF;AAMD;;;;;;GAMG;AACH,iBAAS,kBAAkB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,wBAAwB,2CAqBnF;AAMD,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;AAElE,YAAY,EACV,mBAAmB,EACnB,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,169 @@
1
+ 'use client';
2
+
3
+ import { cn } from "../../lib/utils.js";
4
+ import { getStrictContext } from "../../lib/get-strict-context.js";
5
+ import { useControlledState } from "../../hooks/State/UseControlledState.js";
6
+ import { dynamicToggleStyles } from "./DynamicToggle.styles.js";
7
+ import * as React$1 from "react";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+
10
+ //#region src/react-ui/ui/DynamicToggle/index.tsx
11
+ /**
12
+ * DynamicToggle — CSS-animated toggle where one option expands into sub-options.
13
+ *
14
+ * Uses hidden radio inputs + `:has(:checked)` for zero-JS animation.
15
+ * The indicator slides between options, and group options expand/collapse
16
+ * with a clip-path reveal animation.
17
+ *
18
+ * @module @mks2508/mks-ui/react/ui/DynamicToggle
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <DynamicToggle value="tree" onValueChange={setVal}>
23
+ * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
24
+ * <DynamicToggleGroup label="Changes">
25
+ * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
26
+ * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
27
+ * </DynamicToggleGroup>
28
+ * </DynamicToggle>
29
+ * ```
30
+ */
31
+ const DT_CSS_ID = "mks-dynamic-toggle-css";
32
+ /** Reads the CSS file content at build time via rolldown */
33
+ const DT_CSS = `
34
+ :root{--dt-duration:0.22s;--dt-ease:cubic-bezier(0.22,0.61,0.36,1);--dt-drop-off:0.45}
35
+ [data-slot="dt-track"]{grid-template-columns:repeat(4,1fr)}
36
+ [data-slot="dt-track"]>label:first-of-type,[data-slot="dt-group"]{grid-column:span 2}
37
+ [data-slot="dt-indicator"]{transition:translate var(--dt-duration) var(--dt-ease)}
38
+ [data-slot="dt-track"]:has(>input:checked) [data-slot="dt-indicator"]{translate:0 0}
39
+ [data-slot="dt-track"]:not(:has(>input:checked)) [data-slot="dt-indicator"]{translate:100% 0}
40
+ [data-slot="dt-track"]:has(>input:checked)>label{color:var(--card)}
41
+ [data-slot="dt-track"]:not(:has(>input:checked))>label{color:var(--foreground);opacity:var(--dt-drop-off)}
42
+ [data-slot="dt-group"]{container-type:size;grid-template-columns:1fr 1fr}
43
+ [data-slot="dt-group-label"]{translate:-50% -80%;transition:translate var(--dt-duration) var(--dt-ease),scale var(--dt-duration) var(--dt-ease)}
44
+ [data-slot="dt-group"]:has(input:checked) [data-slot="dt-group-label"]{translate:-50% -250%;scale:0.85}
45
+ [data-slot="dt-group-indicator"]{transition:translate var(--dt-duration) var(--dt-ease),clip-path var(--dt-duration) var(--dt-ease),background var(--dt-duration) var(--dt-ease);clip-path:inset(73cqh calc(50% + 1px) calc(27cqh - 2px) calc(50% - 3px) round 100px);translate:-50% 0;background:var(--foreground)}
46
+ [data-slot="dt-group"]:has(input:checked) [data-slot="dt-group-indicator"]{background:var(--card);clip-path:inset(0 0 0 0 round 100px)}
47
+ [data-slot="dt-group"]:has(input:nth-of-type(1):checked) [data-slot="dt-group-indicator"]{translate:-100% 0}
48
+ [data-slot="dt-group"]:has(input:nth-of-type(2):checked) [data-slot="dt-group-indicator"]{translate:0 0}
49
+ [data-slot="dt-group"] label{color:var(--muted-foreground);transition:color var(--dt-duration) var(--dt-ease),opacity var(--dt-duration) var(--dt-ease)}
50
+ [data-slot="dt-group"] label span{transition:scale var(--dt-duration) var(--dt-ease)}
51
+ [data-slot="dt-track"]:has(>input:checked) [data-slot="dt-group"] label{color:var(--muted-foreground)}
52
+ [data-slot="dt-group"]:has(input:checked) label{color:var(--muted-foreground);opacity:0.75}
53
+ [data-slot="dt-group"]:has(input:nth-of-type(1):checked) label:nth-of-type(1),[data-slot="dt-group"]:has(input:nth-of-type(2):checked) label:nth-of-type(2){color:var(--foreground);opacity:1}
54
+ [data-slot="dt-group"] label:nth-of-type(1) span{scale:0.75;transform-origin:150% 150%}
55
+ [data-slot="dt-group"] label:nth-of-type(2) span{scale:0.75;transform-origin:-65% 150%}
56
+ [data-slot="dt-group"]:has(input:checked) label span{scale:1}
57
+ [data-slot="dt-radio"]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}
58
+ `;
59
+ function useDynamicToggleCSS() {
60
+ React$1.useEffect(() => {
61
+ if (typeof document === "undefined") return;
62
+ if (document.getElementById(DT_CSS_ID)) return;
63
+ const style = document.createElement("style");
64
+ style.id = DT_CSS_ID;
65
+ style.textContent = DT_CSS;
66
+ document.head.appendChild(style);
67
+ }, []);
68
+ }
69
+ const [DynamicToggleProvider, useDynamicToggle] = getStrictContext("DynamicToggleContext");
70
+ /**
71
+ * Root container — pill-shaped toggle control.
72
+ *
73
+ * @param props - Component props
74
+ * @returns React component
75
+ */
76
+ function DynamicToggle({ slots, className, children, ...props }) {
77
+ useDynamicToggleCSS();
78
+ const [value, setValue] = useControlledState({
79
+ value: props.value,
80
+ defaultValue: props.defaultValue ?? "",
81
+ onChange: props.onValueChange
82
+ });
83
+ const groupName = React$1.useId();
84
+ const groupActive = React$1.useMemo(() => {
85
+ const vals = [];
86
+ React$1.Children.forEach(children, (child) => {
87
+ if (React$1.isValidElement(child) && child.type === DynamicToggleGroup) React$1.Children.forEach(child.props.children, (gc) => {
88
+ if (React$1.isValidElement(gc) && gc.props.value) vals.push(gc.props.value);
89
+ });
90
+ });
91
+ return vals;
92
+ }, [children]).includes(value);
93
+ return /* @__PURE__ */ jsx(DynamicToggleProvider, {
94
+ value: {
95
+ value,
96
+ setValue,
97
+ groupName,
98
+ groupActive
99
+ },
100
+ children: /* @__PURE__ */ jsx("div", {
101
+ "data-slot": "dt-root",
102
+ "data-group-active": groupActive || void 0,
103
+ className: cn(dynamicToggleStyles.root, slots?.root, className),
104
+ children: /* @__PURE__ */ jsxs("div", {
105
+ "data-slot": "dt-track",
106
+ className: cn(dynamicToggleStyles.track, slots?.track),
107
+ children: [/* @__PURE__ */ jsx("div", {
108
+ "data-slot": "dt-indicator",
109
+ className: cn(dynamicToggleStyles.indicator, slots?.indicator)
110
+ }), children]
111
+ })
112
+ })
113
+ });
114
+ }
115
+ /**
116
+ * A single toggle option — renders a hidden radio + visible label.
117
+ * Can be used at the top level or inside a DynamicToggleGroup.
118
+ *
119
+ * @param props - Component props
120
+ * @returns React component
121
+ */
122
+ function DynamicToggleOption({ value, children, className }) {
123
+ const ctx = useDynamicToggle();
124
+ const id = React$1.useId();
125
+ const isActive = ctx.value === value;
126
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("label", {
127
+ htmlFor: id,
128
+ "data-slot": "dt-option",
129
+ "data-active": isActive || void 0,
130
+ className: cn(dynamicToggleStyles.option, className),
131
+ children: /* @__PURE__ */ jsx("span", { children })
132
+ }), /* @__PURE__ */ jsx("input", {
133
+ "data-slot": "dt-radio",
134
+ type: "radio",
135
+ name: ctx.groupName,
136
+ id,
137
+ value,
138
+ checked: isActive,
139
+ onChange: () => ctx.setValue(value)
140
+ })] });
141
+ }
142
+ /**
143
+ * An expandable group of options. Shows `label` when collapsed,
144
+ * expands to show sub-options when one is active.
145
+ *
146
+ * @param props - Component props
147
+ * @returns React component
148
+ */
149
+ function DynamicToggleGroup({ label, children, className }) {
150
+ return /* @__PURE__ */ jsxs("div", {
151
+ "data-slot": "dt-group",
152
+ className: cn(dynamicToggleStyles.group, className),
153
+ children: [
154
+ /* @__PURE__ */ jsx("span", {
155
+ "data-slot": "dt-group-label",
156
+ className: dynamicToggleStyles.groupLabel,
157
+ children: label
158
+ }),
159
+ /* @__PURE__ */ jsx("div", {
160
+ "data-slot": "dt-group-indicator",
161
+ className: dynamicToggleStyles.groupIndicator
162
+ }),
163
+ children
164
+ ]
165
+ });
166
+ }
167
+
168
+ //#endregion
169
+ export { DynamicToggle, DynamicToggleGroup, DynamicToggleOption };
@@ -30,4 +30,5 @@ export * from './Separator';
30
30
  export * from './Textarea';
31
31
  export * from './DataCard';
32
32
  export * from './TextFlow';
33
+ export * from './DynamicToggle';
33
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react-ui/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAG1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAG3B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react-ui/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAG1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAG3B,cAAc,YAAY,CAAC;AAG3B,cAAc,iBAAiB,CAAC"}
@@ -39,3 +39,5 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScro
39
39
  import { dataCardStyles, dataCardVariants } from "./DataCard/DataCard.styles.js";
40
40
  import { DataCard, DataCardActions, DataCardBracket, DataCardLabel, DataCardToggle, DataCardValue, useDataCard } from "./DataCard/index.js";
41
41
  import { TextFlow } from "./TextFlow/index.js";
42
+ import { dynamicToggleStyles } from "./DynamicToggle/DynamicToggle.styles.js";
43
+ import { DynamicToggle, DynamicToggleGroup, DynamicToggleOption } from "./DynamicToggle/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mks2508/mks-ui",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "UI component library - Shadcn/Animate UI based with DevEnv components",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,150 @@
1
+ /**
2
+ * DynamicToggle — CSS-only animated transitions.
3
+ *
4
+ * Ported from @jh3yy codepen. All colors via CSS variables (semantic tokens).
5
+ * Uses :has(:checked) for state-driven animation.
6
+ * container-type: size on group for cqh units in clip-path.
7
+ */
8
+
9
+ :root {
10
+ --dt-duration: 0.22s;
11
+ --dt-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
12
+ --dt-drop-off: 0.45;
13
+ }
14
+
15
+ /* ── Track layout ── */
16
+ [data-slot="dt-track"] {
17
+ grid-template-columns: repeat(4, 1fr);
18
+ }
19
+
20
+ [data-slot="dt-track"] > label:first-of-type,
21
+ [data-slot="dt-group"] {
22
+ grid-column: span 2;
23
+ }
24
+
25
+ /* ── Main indicator slide ── */
26
+ [data-slot="dt-indicator"] {
27
+ transition: translate var(--dt-duration) var(--dt-ease);
28
+ }
29
+
30
+ /* When primary (non-group) is checked → indicator on left */
31
+ [data-slot="dt-track"]:has(> input:checked) [data-slot="dt-indicator"] {
32
+ translate: 0 0;
33
+ }
34
+
35
+ /* When nothing in track is checked → group is active → indicator slides right */
36
+ [data-slot="dt-track"]:not(:has(> input:checked)) [data-slot="dt-indicator"] {
37
+ translate: 100% 0;
38
+ }
39
+
40
+ /* ── Primary option text ── */
41
+ [data-slot="dt-track"]:has(> input:checked) > label {
42
+ color: var(--card);
43
+ }
44
+
45
+ [data-slot="dt-track"]:not(:has(> input:checked)) > label {
46
+ color: var(--foreground);
47
+ opacity: var(--dt-drop-off);
48
+ }
49
+
50
+ /* ── Group container ── */
51
+ [data-slot="dt-group"] {
52
+ container-type: size;
53
+ grid-template-columns: 1fr 1fr;
54
+ }
55
+
56
+ /* ── Group collapsed label ("Changes") ── */
57
+ [data-slot="dt-group-label"] {
58
+ translate: -50% -80%;
59
+ transition: translate var(--dt-duration) var(--dt-ease),
60
+ scale var(--dt-duration) var(--dt-ease);
61
+ }
62
+
63
+ /* When group is active → label moves up and shrinks */
64
+ [data-slot="dt-group"]:has(input:checked) [data-slot="dt-group-label"] {
65
+ translate: -50% -250%;
66
+ scale: 0.85;
67
+ }
68
+
69
+ /* ── Group internal indicator ── */
70
+ [data-slot="dt-group-indicator"] {
71
+ transition: translate var(--dt-duration) var(--dt-ease),
72
+ clip-path var(--dt-duration) var(--dt-ease),
73
+ background var(--dt-duration) var(--dt-ease);
74
+ /* Collapsed: clip to tiny pill in center */
75
+ clip-path: inset(73cqh calc(50% + 1px) calc(27cqh - 2px) calc(50% - 3px) round 100px);
76
+ translate: -50% 0;
77
+ background: var(--foreground);
78
+ }
79
+
80
+ /* When group is active → indicator expands and positions */
81
+ [data-slot="dt-group"]:has(input:checked) [data-slot="dt-group-indicator"] {
82
+ background: var(--card);
83
+ clip-path: inset(0 0 0 0 round 100px);
84
+ }
85
+
86
+ /* First sub-option checked → indicator left */
87
+ [data-slot="dt-group"]:has(input:nth-of-type(1):checked) [data-slot="dt-group-indicator"] {
88
+ translate: -100% 0;
89
+ }
90
+
91
+ /* Second sub-option checked → indicator right */
92
+ [data-slot="dt-group"]:has(input:nth-of-type(2):checked) [data-slot="dt-group-indicator"] {
93
+ translate: 0 0;
94
+ }
95
+
96
+ /* ── Group option labels ── */
97
+ [data-slot="dt-group"] label {
98
+ color: var(--muted-foreground);
99
+ transition: color var(--dt-duration) var(--dt-ease),
100
+ opacity var(--dt-duration) var(--dt-ease);
101
+ }
102
+
103
+ [data-slot="dt-group"] label span {
104
+ transition: scale var(--dt-duration) var(--dt-ease);
105
+ }
106
+
107
+ /* When group collapsed: hide sub-option labels */
108
+ [data-slot="dt-track"]:has(> input:checked) [data-slot="dt-group"] label {
109
+ color: var(--muted-foreground);
110
+ }
111
+
112
+ /* When group active: show labels, highlight active one */
113
+ [data-slot="dt-group"]:has(input:checked) label {
114
+ color: var(--muted-foreground);
115
+ opacity: 0.75;
116
+ }
117
+
118
+ [data-slot="dt-group"]:has(input:nth-of-type(1):checked) label:nth-of-type(1),
119
+ [data-slot="dt-group"]:has(input:nth-of-type(2):checked) label:nth-of-type(2) {
120
+ color: var(--foreground);
121
+ opacity: 1;
122
+ }
123
+
124
+ /* Sub-option label scale animation */
125
+ [data-slot="dt-group"] label:nth-of-type(1) span {
126
+ scale: 0.75;
127
+ transform-origin: 150% 150%;
128
+ }
129
+
130
+ [data-slot="dt-group"] label:nth-of-type(2) span {
131
+ scale: 0.75;
132
+ transform-origin: -65% 150%;
133
+ }
134
+
135
+ [data-slot="dt-group"]:has(input:checked) label span {
136
+ scale: 1;
137
+ }
138
+
139
+ /* ── Screen reader only (hidden radios) ── */
140
+ [data-slot="dt-radio"] {
141
+ position: absolute;
142
+ width: 1px;
143
+ height: 1px;
144
+ padding: 0;
145
+ margin: -1px;
146
+ overflow: hidden;
147
+ clip: rect(0, 0, 0, 0);
148
+ white-space: nowrap;
149
+ border-width: 0;
150
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * DynamicToggle style slots and types.
3
+ *
4
+ * Uses semantic CSS variables for all colors.
5
+ * Animation handled in DynamicToggle.css (clip-path, transitions).
6
+ *
7
+ * @module @mks2508/mks-ui/react/ui/DynamicToggle
8
+ */
9
+
10
+ import type { StyleSlots } from '@/core/types';
11
+
12
+ /** Slot names for DynamicToggle */
13
+ export type DynamicToggleSlot =
14
+ | 'root'
15
+ | 'track'
16
+ | 'option'
17
+ | 'indicator'
18
+ | 'group'
19
+ | 'groupLabel'
20
+ | 'groupIndicator';
21
+
22
+ /**
23
+ * Default styles for each DynamicToggle slot.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <DynamicToggle slots={{ root: 'w-72', indicator: 'bg-primary' }}>
28
+ * ...
29
+ * </DynamicToggle>
30
+ * ```
31
+ */
32
+ export const dynamicToggleStyles: StyleSlots<DynamicToggleSlot> = {
33
+ root: [
34
+ 'relative rounded-full border border-border bg-card',
35
+ 'p-[2px] select-none',
36
+ ].join(' '),
37
+ track: [
38
+ 'relative grid place-items-center',
39
+ 'w-full h-full',
40
+ ].join(' '),
41
+ option: [
42
+ 'inline-grid place-items-center cursor-pointer',
43
+ 'text-xs font-medium z-[2] h-full w-full',
44
+ 'transition-[color,opacity] duration-[220ms]',
45
+ ].join(' '),
46
+ indicator: [
47
+ 'absolute w-1/2 left-0 top-0 bottom-0',
48
+ 'bg-foreground rounded-full',
49
+ 'pointer-events-none z-0',
50
+ ].join(' '),
51
+ group: [
52
+ 'relative w-full h-full grid',
53
+ 'border border-transparent',
54
+ ].join(' '),
55
+ groupLabel: [
56
+ 'absolute left-1/2 top-1/2 z-[2]',
57
+ 'text-xs font-medium text-foreground',
58
+ 'pointer-events-none',
59
+ ].join(' '),
60
+ groupIndicator: [
61
+ 'absolute left-1/2 top-0 bottom-0',
62
+ 'bg-foreground rounded-full',
63
+ 'pointer-events-none z-0',
64
+ ].join(' '),
65
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * DynamicToggle type definitions.
3
+ *
4
+ * A CSS-animated toggle where one option can expand into sub-options.
5
+ * Uses hidden radio inputs + `:has(:checked)` for zero-JS animation.
6
+ *
7
+ * @module @mks2508/mks-ui/react/ui/DynamicToggle
8
+ */
9
+
10
+ import type { SlotOverrides } from '@/core/types';
11
+ import type { DynamicToggleSlot } from './DynamicToggle.styles';
12
+
13
+ /**
14
+ * Context shared between DynamicToggle root and its children.
15
+ */
16
+ export type DynamicToggleContextType = {
17
+ /** Current selected value */
18
+ value: string;
19
+ /** Update selected value */
20
+ setValue: (value: string) => void;
21
+ /** Auto-generated radio group name */
22
+ groupName: string;
23
+ /** Whether a group child is active */
24
+ groupActive: boolean;
25
+ };
26
+
27
+ /**
28
+ * Props for the DynamicToggle root container.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <DynamicToggle value="tree" onValueChange={setVal}>
33
+ * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
34
+ * <DynamicToggleGroup label="Changes">
35
+ * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
36
+ * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
37
+ * </DynamicToggleGroup>
38
+ * </DynamicToggle>
39
+ * ```
40
+ */
41
+ export interface IDynamicToggleProps {
42
+ /** Controlled value */
43
+ value?: string;
44
+ /** Uncontrolled default value */
45
+ defaultValue?: string;
46
+ /** Change callback */
47
+ onValueChange?: (value: string) => void;
48
+ /** Slot class overrides */
49
+ slots?: SlotOverrides<DynamicToggleSlot>;
50
+ /** Additional class for the root */
51
+ className?: string;
52
+ /** DynamicToggleOption and DynamicToggleGroup children */
53
+ children: React.ReactNode;
54
+ }
55
+
56
+ /**
57
+ * Props for a single toggle option (top-level or inside a group).
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
62
+ * ```
63
+ */
64
+ export interface IDynamicToggleOptionProps {
65
+ /** Value this option represents */
66
+ value: string;
67
+ /** Label content */
68
+ children: React.ReactNode;
69
+ /** Additional class */
70
+ className?: string;
71
+ }
72
+
73
+ /**
74
+ * Props for an expandable group of options.
75
+ * When none of the group's options are active, shows the collapsed `label`.
76
+ * When one is active, expands to show all sub-options.
77
+ *
78
+ * @example
79
+ * ```tsx
80
+ * <DynamicToggleGroup label="Changes">
81
+ * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
82
+ * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
83
+ * </DynamicToggleGroup>
84
+ * ```
85
+ */
86
+ export interface IDynamicToggleGroupProps {
87
+ /** Label shown when collapsed */
88
+ label: string;
89
+ /** DynamicToggleOption children */
90
+ children: React.ReactNode;
91
+ /** Additional class */
92
+ className?: string;
93
+ }
@@ -0,0 +1,240 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * DynamicToggle — CSS-animated toggle where one option expands into sub-options.
5
+ *
6
+ * Uses hidden radio inputs + `:has(:checked)` for zero-JS animation.
7
+ * The indicator slides between options, and group options expand/collapse
8
+ * with a clip-path reveal animation.
9
+ *
10
+ * @module @mks2508/mks-ui/react/ui/DynamicToggle
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <DynamicToggle value="tree" onValueChange={setVal}>
15
+ * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
16
+ * <DynamicToggleGroup label="Changes">
17
+ * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
18
+ * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
19
+ * </DynamicToggleGroup>
20
+ * </DynamicToggle>
21
+ * ```
22
+ */
23
+
24
+ import * as React from 'react';
25
+ import { useControlledState } from '@/react-ui/hooks/State/UseControlledState';
26
+ import { getStrictContext } from '@/react-ui/lib/get-strict-context';
27
+ import { cn } from '@/react-ui/lib/utils';
28
+ import { dynamicToggleStyles } from './DynamicToggle.styles';
29
+ import type {
30
+ DynamicToggleContextType,
31
+ IDynamicToggleProps,
32
+ IDynamicToggleOptionProps,
33
+ IDynamicToggleGroupProps,
34
+ } from './DynamicToggle.types';
35
+ // ---------------------------------------------------------------------------
36
+ // CSS injection (same pattern as Tabs — inject once into document head)
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const DT_CSS_ID = 'mks-dynamic-toggle-css';
40
+
41
+ /** Reads the CSS file content at build time via rolldown */
42
+ const DT_CSS = `
43
+ :root{--dt-duration:0.22s;--dt-ease:cubic-bezier(0.22,0.61,0.36,1);--dt-drop-off:0.45}
44
+ [data-slot="dt-track"]{grid-template-columns:repeat(4,1fr)}
45
+ [data-slot="dt-track"]>label:first-of-type,[data-slot="dt-group"]{grid-column:span 2}
46
+ [data-slot="dt-indicator"]{transition:translate var(--dt-duration) var(--dt-ease)}
47
+ [data-slot="dt-track"]:has(>input:checked) [data-slot="dt-indicator"]{translate:0 0}
48
+ [data-slot="dt-track"]:not(:has(>input:checked)) [data-slot="dt-indicator"]{translate:100% 0}
49
+ [data-slot="dt-track"]:has(>input:checked)>label{color:var(--card)}
50
+ [data-slot="dt-track"]:not(:has(>input:checked))>label{color:var(--foreground);opacity:var(--dt-drop-off)}
51
+ [data-slot="dt-group"]{container-type:size;grid-template-columns:1fr 1fr}
52
+ [data-slot="dt-group-label"]{translate:-50% -80%;transition:translate var(--dt-duration) var(--dt-ease),scale var(--dt-duration) var(--dt-ease)}
53
+ [data-slot="dt-group"]:has(input:checked) [data-slot="dt-group-label"]{translate:-50% -250%;scale:0.85}
54
+ [data-slot="dt-group-indicator"]{transition:translate var(--dt-duration) var(--dt-ease),clip-path var(--dt-duration) var(--dt-ease),background var(--dt-duration) var(--dt-ease);clip-path:inset(73cqh calc(50% + 1px) calc(27cqh - 2px) calc(50% - 3px) round 100px);translate:-50% 0;background:var(--foreground)}
55
+ [data-slot="dt-group"]:has(input:checked) [data-slot="dt-group-indicator"]{background:var(--card);clip-path:inset(0 0 0 0 round 100px)}
56
+ [data-slot="dt-group"]:has(input:nth-of-type(1):checked) [data-slot="dt-group-indicator"]{translate:-100% 0}
57
+ [data-slot="dt-group"]:has(input:nth-of-type(2):checked) [data-slot="dt-group-indicator"]{translate:0 0}
58
+ [data-slot="dt-group"] label{color:var(--muted-foreground);transition:color var(--dt-duration) var(--dt-ease),opacity var(--dt-duration) var(--dt-ease)}
59
+ [data-slot="dt-group"] label span{transition:scale var(--dt-duration) var(--dt-ease)}
60
+ [data-slot="dt-track"]:has(>input:checked) [data-slot="dt-group"] label{color:var(--muted-foreground)}
61
+ [data-slot="dt-group"]:has(input:checked) label{color:var(--muted-foreground);opacity:0.75}
62
+ [data-slot="dt-group"]:has(input:nth-of-type(1):checked) label:nth-of-type(1),[data-slot="dt-group"]:has(input:nth-of-type(2):checked) label:nth-of-type(2){color:var(--foreground);opacity:1}
63
+ [data-slot="dt-group"] label:nth-of-type(1) span{scale:0.75;transform-origin:150% 150%}
64
+ [data-slot="dt-group"] label:nth-of-type(2) span{scale:0.75;transform-origin:-65% 150%}
65
+ [data-slot="dt-group"]:has(input:checked) label span{scale:1}
66
+ [data-slot="dt-radio"]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}
67
+ `;
68
+
69
+ function useDynamicToggleCSS() {
70
+ React.useEffect(() => {
71
+ if (typeof document === 'undefined') return;
72
+ if (document.getElementById(DT_CSS_ID)) return;
73
+ const style = document.createElement('style');
74
+ style.id = DT_CSS_ID;
75
+ style.textContent = DT_CSS;
76
+ document.head.appendChild(style);
77
+ }, []);
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Context
82
+ // ---------------------------------------------------------------------------
83
+
84
+ const [DynamicToggleProvider, useDynamicToggle] =
85
+ getStrictContext<DynamicToggleContextType>('DynamicToggleContext');
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // DynamicToggle (Root)
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Root container — pill-shaped toggle control.
93
+ *
94
+ * @param props - Component props
95
+ * @returns React component
96
+ */
97
+ function DynamicToggle({
98
+ slots,
99
+ className,
100
+ children,
101
+ ...props
102
+ }: IDynamicToggleProps) {
103
+ useDynamicToggleCSS();
104
+
105
+ const [value, setValue] = useControlledState({
106
+ value: props.value,
107
+ defaultValue: props.defaultValue ?? '',
108
+ onChange: props.onValueChange,
109
+ });
110
+
111
+ const groupName = React.useId();
112
+
113
+ // Detect if group child is active (check children for group values)
114
+ const groupValues = React.useMemo(() => {
115
+ const vals: string[] = [];
116
+ React.Children.forEach(children, (child) => {
117
+ if (React.isValidElement<IDynamicToggleGroupProps>(child) && child.type === DynamicToggleGroup) {
118
+ React.Children.forEach(child.props.children, (gc) => {
119
+ if (React.isValidElement<IDynamicToggleOptionProps>(gc) && gc.props.value) {
120
+ vals.push(gc.props.value);
121
+ }
122
+ });
123
+ }
124
+ });
125
+ return vals;
126
+ }, [children]);
127
+
128
+ const groupActive = groupValues.includes(value);
129
+
130
+ return (
131
+ <DynamicToggleProvider value={{ value, setValue, groupName, groupActive }}>
132
+ <div
133
+ data-slot="dt-root"
134
+ data-group-active={groupActive || undefined}
135
+ className={cn(dynamicToggleStyles.root, slots?.root, className)}
136
+ >
137
+ <div
138
+ data-slot="dt-track"
139
+ className={cn(dynamicToggleStyles.track, slots?.track)}
140
+ >
141
+ {/* Main sliding indicator */}
142
+ <div
143
+ data-slot="dt-indicator"
144
+ className={cn(dynamicToggleStyles.indicator, slots?.indicator)}
145
+ />
146
+ {children}
147
+ </div>
148
+ </div>
149
+ </DynamicToggleProvider>
150
+ );
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // DynamicToggleOption
155
+ // ---------------------------------------------------------------------------
156
+
157
+ /**
158
+ * A single toggle option — renders a hidden radio + visible label.
159
+ * Can be used at the top level or inside a DynamicToggleGroup.
160
+ *
161
+ * @param props - Component props
162
+ * @returns React component
163
+ */
164
+ function DynamicToggleOption({ value, children, className }: IDynamicToggleOptionProps) {
165
+ const ctx = useDynamicToggle();
166
+ const id = React.useId();
167
+ const isActive = ctx.value === value;
168
+
169
+ return (
170
+ <>
171
+ <label
172
+ htmlFor={id}
173
+ data-slot="dt-option"
174
+ data-active={isActive || undefined}
175
+ className={cn(dynamicToggleStyles.option, className)}
176
+ >
177
+ <span>{children}</span>
178
+ </label>
179
+ <input
180
+ data-slot="dt-radio"
181
+ type="radio"
182
+ name={ctx.groupName}
183
+ id={id}
184
+ value={value}
185
+ checked={isActive}
186
+ onChange={() => ctx.setValue(value)}
187
+ />
188
+ </>
189
+ );
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // DynamicToggleGroup
194
+ // ---------------------------------------------------------------------------
195
+
196
+ /**
197
+ * An expandable group of options. Shows `label` when collapsed,
198
+ * expands to show sub-options when one is active.
199
+ *
200
+ * @param props - Component props
201
+ * @returns React component
202
+ */
203
+ function DynamicToggleGroup({ label, children, className }: IDynamicToggleGroupProps) {
204
+ return (
205
+ <div
206
+ data-slot="dt-group"
207
+ className={cn(dynamicToggleStyles.group, className)}
208
+ >
209
+ {/* Collapsed label */}
210
+ <span
211
+ data-slot="dt-group-label"
212
+ className={dynamicToggleStyles.groupLabel}
213
+ >
214
+ {label}
215
+ </span>
216
+ {/* Internal sliding indicator */}
217
+ <div
218
+ data-slot="dt-group-indicator"
219
+ className={dynamicToggleStyles.groupIndicator}
220
+ />
221
+ {children}
222
+ </div>
223
+ );
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Exports
228
+ // ---------------------------------------------------------------------------
229
+
230
+ export { DynamicToggle, DynamicToggleOption, DynamicToggleGroup };
231
+
232
+ export type {
233
+ IDynamicToggleProps,
234
+ IDynamicToggleOptionProps,
235
+ IDynamicToggleGroupProps,
236
+ DynamicToggleContextType,
237
+ } from './DynamicToggle.types';
238
+
239
+ export type { DynamicToggleSlot } from './DynamicToggle.styles';
240
+ export { dynamicToggleStyles } from './DynamicToggle.styles';
@@ -36,3 +36,6 @@ export * from './DataCard';
36
36
 
37
37
  // WAAPI-based components
38
38
  export * from './TextFlow';
39
+
40
+ // CSS-animated components
41
+ export * from './DynamicToggle';