@mks2508/mks-ui 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/react-ui/index.js +8 -3
  2. package/dist/react-ui/primitives/index.js +5 -0
  3. package/dist/react-ui/primitives/waapi/Gooey/Gooey.types.d.ts +103 -0
  4. package/dist/react-ui/primitives/waapi/Gooey/Gooey.types.d.ts.map +1 -0
  5. package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.d.ts +10 -0
  6. package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.d.ts.map +1 -0
  7. package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.js +59 -0
  8. package/dist/react-ui/primitives/waapi/Gooey/GooeyFilter.d.ts +7 -0
  9. package/dist/react-ui/primitives/waapi/Gooey/GooeyFilter.d.ts.map +1 -0
  10. package/dist/react-ui/primitives/waapi/Gooey/GooeyFilter.js +78 -0
  11. package/dist/react-ui/primitives/waapi/Gooey/MorphPath.d.ts +7 -0
  12. package/dist/react-ui/primitives/waapi/Gooey/MorphPath.d.ts.map +1 -0
  13. package/dist/react-ui/primitives/waapi/Gooey/MorphPath.js +51 -0
  14. package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.d.ts +87 -0
  15. package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.d.ts.map +1 -0
  16. package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.js +177 -0
  17. package/dist/react-ui/primitives/waapi/Gooey/index.d.ts +28 -0
  18. package/dist/react-ui/primitives/waapi/Gooey/index.d.ts.map +1 -0
  19. package/dist/react-ui/primitives/waapi/Gooey/index.js +5 -0
  20. package/dist/react-ui/primitives/waapi/Gooey/useMorphPath.d.ts +7 -0
  21. package/dist/react-ui/primitives/waapi/Gooey/useMorphPath.d.ts.map +1 -0
  22. package/dist/react-ui/primitives/waapi/Gooey/useMorphPath.js +47 -0
  23. package/dist/react-ui/primitives/waapi/index.d.ts +2 -0
  24. package/dist/react-ui/primitives/waapi/index.d.ts.map +1 -1
  25. package/dist/react-ui/primitives/waapi/index.js +6 -0
  26. package/dist/react-ui/ui/DataCard/DataCard.styles.d.ts +26 -16
  27. package/dist/react-ui/ui/DataCard/DataCard.styles.d.ts.map +1 -1
  28. package/dist/react-ui/ui/DataCard/DataCard.styles.js +36 -74
  29. package/dist/react-ui/ui/DataCard/DataCard.types.d.ts +50 -70
  30. package/dist/react-ui/ui/DataCard/DataCard.types.d.ts.map +1 -1
  31. package/dist/react-ui/ui/DataCard/index.d.ts +24 -93
  32. package/dist/react-ui/ui/DataCard/index.d.ts.map +1 -1
  33. package/dist/react-ui/ui/DataCard/index.js +76 -118
  34. package/dist/react-ui/ui/DynamicToggle/DynamicToggle-Cm6-VceQ.css +304 -0
  35. package/dist/react-ui/ui/DynamicToggle/DynamicToggle.css +303 -0
  36. package/dist/react-ui/ui/DynamicToggle/DynamicToggle.js +0 -0
  37. package/dist/react-ui/ui/DynamicToggle/DynamicToggle.styles.d.ts +20 -8
  38. package/dist/react-ui/ui/DynamicToggle/DynamicToggle.styles.d.ts.map +1 -1
  39. package/dist/react-ui/ui/DynamicToggle/DynamicToggle.styles.js +55 -27
  40. package/dist/react-ui/ui/DynamicToggle/DynamicToggle.types.d.ts +63 -14
  41. package/dist/react-ui/ui/DynamicToggle/DynamicToggle.types.d.ts.map +1 -1
  42. package/dist/react-ui/ui/DynamicToggle/index.d.ts +22 -20
  43. package/dist/react-ui/ui/DynamicToggle/index.d.ts.map +1 -1
  44. package/dist/react-ui/ui/DynamicToggle/index.js +115 -96
  45. package/dist/react-ui/ui/Switch/index.js +1 -1
  46. package/dist/react-ui/ui/index.js +2 -2
  47. package/package.json +2 -2
  48. package/src/css.d.ts +1 -0
  49. package/src/react-ui/primitives/waapi/Gooey/Gooey.types.ts +123 -0
  50. package/src/react-ui/primitives/waapi/Gooey/GooeyCanvas.tsx +80 -0
  51. package/src/react-ui/primitives/waapi/Gooey/GooeyFilter.tsx +77 -0
  52. package/src/react-ui/primitives/waapi/Gooey/MorphPath.tsx +58 -0
  53. package/src/react-ui/primitives/waapi/Gooey/gooey-utils.ts +244 -0
  54. package/src/react-ui/primitives/waapi/Gooey/index.ts +50 -0
  55. package/src/react-ui/primitives/waapi/Gooey/useMorphPath.ts +48 -0
  56. package/src/react-ui/primitives/waapi/index.ts +23 -0
  57. package/src/react-ui/ui/DataCard/DataCard.styles.ts +45 -101
  58. package/src/react-ui/ui/DataCard/DataCard.types.ts +52 -73
  59. package/src/react-ui/ui/DataCard/index.tsx +118 -184
  60. package/src/react-ui/ui/DynamicToggle/DynamicToggle.css +244 -91
  61. package/src/react-ui/ui/DynamicToggle/DynamicToggle.styles.ts +60 -40
  62. package/src/react-ui/ui/DynamicToggle/DynamicToggle.types.ts +95 -14
  63. package/src/react-ui/ui/DynamicToggle/index.tsx +150 -96
  64. package/src/react-ui/ui/DynamicToggle/prototype-v7.html +615 -0
  65. package/src/react-ui/ui/DynamicToggle/prototype.html +419 -0
  66. package/src/react-ui/ui/Switch/index.tsx +1 -1
  67. /package/dist/react-ui/blocks/Terminal/panel/{terminal-filter-dropdown.module-DAcl_XQZ.css → terminal-filter-dropdown.module-C6oDcFBS.css} +0 -0
  68. /package/dist/react-ui/blocks/Terminal/panel/{terminal-session-tabs.module-DNAop5e3.css → terminal-session-tabs.module-D_-sgyza.css} +0 -0
  69. /package/dist/react-ui/components/MorphingPopover/{morphing-popover.module-BJrjXisF.css → morphing-popover.module-B1ftlaYj.css} +0 -0
@@ -1,14 +1,52 @@
1
1
  /**
2
2
  * DynamicToggle type definitions.
3
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.
4
+ * CSS-animated toggle where one option expands into sub-options.
5
+ * Uses hidden radios + `:has(:checked)` for zero-JS animation.
6
+ * Optional gooey morph via `GooeyCanvas` or `MorphPath` primitives.
6
7
  *
7
8
  * @module @mks2508/mks-ui/react/ui/DynamicToggle
8
9
  */
9
10
 
10
- import type { SlotOverrides } from '@/core/types';
11
- import type { DynamicToggleSlot } from './DynamicToggle.styles';
11
+ import type { SlotOverrides, IBaseConfig } from '@/core/types';
12
+ import type { DynamicToggleSlot, DynamicToggleVariantProps } from './DynamicToggle.styles';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Shared types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** How the group appears when collapsed (standalone option active). */
19
+ export type DynamicToggleCollapsedMode = 'title' | 'opts' | 'title-opts';
20
+
21
+ /** Morph technique for the pill↔groupLabel junction. */
22
+ export type DynamicToggleMorphMode = 'none' | 'filter' | 'path';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Config
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /**
29
+ * Configuration for DynamicToggle animation behavior.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * <DynamicToggle config={{ morphMode: 'filter', duration: 0.3 }}>
34
+ * ...
35
+ * </DynamicToggle>
36
+ * ```
37
+ */
38
+ export interface IDynamicToggleConfig extends IBaseConfig {
39
+ /** CSS transition duration in seconds (default: 0.22) */
40
+ duration?: number;
41
+ /** Label animation style (default: 'morph') */
42
+ labelAnimation?: 'morph' | 'float' | 'none';
43
+ /** Gooey morph mode for the pill↔groupLabel junction (default: 'none') */
44
+ morphMode?: DynamicToggleMorphMode;
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Context
49
+ // ---------------------------------------------------------------------------
12
50
 
13
51
  /**
14
52
  * Context shared between DynamicToggle root and its children.
@@ -22,37 +60,66 @@ export type DynamicToggleContextType = {
22
60
  groupName: string;
23
61
  /** Whether a group child is active */
24
62
  groupActive: boolean;
63
+ /** Whether the toggle is disabled */
64
+ disabled: boolean;
65
+ /** Register group info (called by DynamicToggleGroup on mount) */
66
+ registerGroup: (
67
+ label: string,
68
+ values: string[],
69
+ position: 'top' | 'bottom' | 'hidden',
70
+ collapsedMode: DynamicToggleCollapsedMode,
71
+ ) => void;
25
72
  };
26
73
 
74
+ // ---------------------------------------------------------------------------
75
+ // Root
76
+ // ---------------------------------------------------------------------------
77
+
27
78
  /**
28
79
  * Props for the DynamicToggle root container.
29
80
  *
81
+ * Supports exactly one `DynamicToggleOption` + one `DynamicToggleGroup`.
82
+ * The group expands into sub-options when one of its children is active.
83
+ *
30
84
  * @example
31
85
  * ```tsx
32
- * <DynamicToggle value="tree" onValueChange={setVal}>
86
+ * <DynamicToggle
87
+ * value="tree" onValueChange={setVal}
88
+ * size="sm" config={{ morphMode: 'filter' }}
89
+ * >
33
90
  * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
34
- * <DynamicToggleGroup label="Changes">
91
+ * <DynamicToggleGroup label="Changes" collapsedMode="title">
35
92
  * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
36
93
  * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
37
94
  * </DynamicToggleGroup>
38
95
  * </DynamicToggle>
39
96
  * ```
40
97
  */
41
- export interface IDynamicToggleProps {
98
+ export interface IDynamicToggleProps extends DynamicToggleVariantProps {
42
99
  /** Controlled value */
43
100
  value?: string;
44
101
  /** Uncontrolled default value */
45
102
  defaultValue?: string;
46
103
  /** Change callback */
47
104
  onValueChange?: (value: string) => void;
105
+ /** Disable the entire toggle */
106
+ disabled?: boolean;
48
107
  /** Slot class overrides */
49
108
  slots?: SlotOverrides<DynamicToggleSlot>;
109
+ /** Animation/behavior configuration */
110
+ config?: IDynamicToggleConfig;
111
+ /** Accessible label for the radio group */
112
+ 'aria-label'?: string;
50
113
  /** Additional class for the root */
51
114
  className?: string;
52
115
  /** DynamicToggleOption and DynamicToggleGroup children */
53
116
  children: React.ReactNode;
54
117
  }
55
118
 
119
+ // ---------------------------------------------------------------------------
120
+ // Option
121
+ // ---------------------------------------------------------------------------
122
+
56
123
  /**
57
124
  * Props for a single toggle option (top-level or inside a group).
58
125
  *
@@ -70,23 +137,37 @@ export interface IDynamicToggleOptionProps {
70
137
  className?: string;
71
138
  }
72
139
 
140
+ // ---------------------------------------------------------------------------
141
+ // Group
142
+ // ---------------------------------------------------------------------------
143
+
73
144
  /**
74
145
  * 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.
146
+ *
147
+ * When none of the group's options are active, shows collapsed content
148
+ * based on `collapsedMode`:
149
+ * - `'title'` — only the group label
150
+ * - `'opts'` — only the combined sub-option text ("Solo · Team")
151
+ * - `'title-opts'` — WIP: currently falls back to 'title' behavior
152
+ *
153
+ * When one is active, expands with a clip-path reveal animation.
77
154
  *
78
155
  * @example
79
156
  * ```tsx
80
- * <DynamicToggleGroup label="Changes">
81
- * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
82
- * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
157
+ * <DynamicToggleGroup label="Premium" collapsedMode="title">
158
+ * <DynamicToggleOption value="solo">Solo</DynamicToggleOption>
159
+ * <DynamicToggleOption value="team">Team</DynamicToggleOption>
83
160
  * </DynamicToggleGroup>
84
161
  * ```
85
162
  */
86
163
  export interface IDynamicToggleGroupProps {
87
- /** Label shown when collapsed */
164
+ /** Label shown as group title / group label text */
88
165
  label: string;
89
- /** DynamicToggleOption children */
166
+ /** Group label position relative to the pill (default: 'top') */
167
+ labelPosition?: 'top' | 'bottom' | 'hidden';
168
+ /** How the group appears when collapsed (default: 'title') */
169
+ collapsedMode?: DynamicToggleCollapsedMode;
170
+ /** Exactly 2 DynamicToggleOption children */
90
171
  children: React.ReactNode;
91
172
  /** Additional class */
92
173
  className?: string;
@@ -1,19 +1,21 @@
1
1
  'use client';
2
2
 
3
3
  /**
4
- * DynamicToggle — CSS-animated toggle where one option expands into sub-options.
4
+ * DynamicToggle — CSS-animated toggle with expanding sub-options and group label.
5
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.
6
+ * Pure CSS animation via `:has(:checked)` on hidden radio inputs.
7
+ * When the group is active, a group label grows out of the pill.
8
+ * Optional gooey morph via `config.morphMode` for organic junction.
9
+ *
10
+ * Supports exactly 1 `DynamicToggleOption` + 1 `DynamicToggleGroup` (with 2 sub-options).
9
11
  *
10
12
  * @module @mks2508/mks-ui/react/ui/DynamicToggle
11
13
  *
12
14
  * @example
13
15
  * ```tsx
14
- * <DynamicToggle value="tree" onValueChange={setVal}>
16
+ * <DynamicToggle value="tree" onValueChange={setVal} size="sm" shape="pill">
15
17
  * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
16
- * <DynamicToggleGroup label="Changes">
18
+ * <DynamicToggleGroup label="Changes" collapsedMode="title" labelPosition="top">
17
19
  * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
18
20
  * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
19
21
  * </DynamicToggleGroup>
@@ -25,57 +27,16 @@ import * as React from 'react';
25
27
  import { useControlledState } from '@/react-ui/hooks/State/UseControlledState';
26
28
  import { getStrictContext } from '@/react-ui/lib/get-strict-context';
27
29
  import { cn } from '@/react-ui/lib/utils';
28
- import { dynamicToggleStyles } from './DynamicToggle.styles';
30
+ import { GooeyCanvas } from '@/react-ui/primitives/waapi/Gooey/GooeyCanvas';
31
+ import './DynamicToggle.css';
32
+ import { dynamicToggleStyles, dynamicToggleVariants } from './DynamicToggle.styles';
29
33
  import type {
30
34
  DynamicToggleContextType,
35
+ DynamicToggleCollapsedMode,
31
36
  IDynamicToggleProps,
32
37
  IDynamicToggleOptionProps,
33
38
  IDynamicToggleGroupProps,
34
39
  } 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
40
 
80
41
  // ---------------------------------------------------------------------------
81
42
  // Context
@@ -84,24 +45,42 @@ function useDynamicToggleCSS() {
84
45
  const [DynamicToggleProvider, useDynamicToggle] =
85
46
  getStrictContext<DynamicToggleContextType>('DynamicToggleContext');
86
47
 
48
+ // ---------------------------------------------------------------------------
49
+ // SSR-safe layout effect — useLayoutEffect on client, useEffect on server
50
+ // ---------------------------------------------------------------------------
51
+
52
+ const useIsomorphicLayoutEffect =
53
+ typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Size height map (for auto-blur calculation)
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const SIZE_HEIGHT_PX: Record<string, number> = {
60
+ sm: 30,
61
+ default: 38,
62
+ lg: 44,
63
+ };
64
+
87
65
  // ---------------------------------------------------------------------------
88
66
  // DynamicToggle (Root)
89
67
  // ---------------------------------------------------------------------------
90
68
 
91
69
  /**
92
- * Root container — pill-shaped toggle control.
93
- *
94
- * @param props - Component props
95
- * @returns React component
70
+ * Root container — pill-shaped toggle with expanding sub-options and group label.
96
71
  */
97
72
  function DynamicToggle({
73
+ variant,
74
+ size,
75
+ shape,
98
76
  slots,
77
+ config,
78
+ disabled = false,
99
79
  className,
100
80
  children,
81
+ 'aria-label': ariaLabel,
101
82
  ...props
102
83
  }: IDynamicToggleProps) {
103
- useDynamicToggleCSS();
104
-
105
84
  const [value, setValue] = useControlledState({
106
85
  value: props.value,
107
86
  defaultValue: props.defaultValue ?? '',
@@ -110,35 +89,75 @@ function DynamicToggle({
110
89
 
111
90
  const groupName = React.useId();
112
91
 
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]);
92
+ // Group info registered by DynamicToggleGroup child via context + useLayoutEffect
93
+ const [groupLabel, setGroupLabel] = React.useState('');
94
+ const [groupValues, setGroupValues] = React.useState<string[]>([]);
95
+ const [groupPosition, setGroupPosition] = React.useState<'top' | 'bottom' | 'hidden'>('top');
96
+ const [groupCollapsedMode, setGroupCollapsedMode] = React.useState<DynamicToggleCollapsedMode>('title');
97
+
98
+ const registerGroup = React.useCallback(
99
+ (label: string, values: string[], position: 'top' | 'bottom' | 'hidden', collapsedMode: DynamicToggleCollapsedMode) => {
100
+ setGroupLabel(label);
101
+ setGroupValues(values);
102
+ setGroupPosition(position);
103
+ setGroupCollapsedMode(collapsedMode);
104
+ },
105
+ [],
106
+ );
127
107
 
128
108
  const groupActive = groupValues.includes(value);
129
109
 
110
+ // Config
111
+ const morphMode = config?.morphMode ?? 'none';
112
+ const resolvedVariant = variant ?? 'default';
113
+ const effectiveMorphMode = (resolvedVariant === 'ghost' || resolvedVariant === 'outline') ? 'none' : morphMode;
114
+ const labelAnimation = config?.labelAnimation ?? 'morph';
115
+ const showGroupLabel = labelAnimation !== 'none' && groupPosition !== 'hidden' && groupLabel;
116
+ const heightPx = SIZE_HEIGHT_PX[size ?? 'default'] ?? 32;
117
+
118
+ const style = config?.duration
119
+ ? { '--dt-dur': `${config.duration}s` } as React.CSSProperties
120
+ : undefined;
121
+
122
+ // Group label element (shared between modes)
123
+ const groupLabelElement = showGroupLabel ? (
124
+ <div
125
+ data-slot="dt-group-label"
126
+ data-position={groupPosition}
127
+ className={cn(dynamicToggleStyles.groupLabel, slots?.groupLabel)}
128
+ >
129
+ <span>{groupLabel || '\u00A0'}</span>
130
+ </div>
131
+ ) : null;
132
+
130
133
  return (
131
- <DynamicToggleProvider value={{ value, setValue, groupName, groupActive }}>
134
+ <DynamicToggleProvider value={{ value, setValue, groupName, groupActive, disabled, registerGroup }}>
132
135
  <div
133
136
  data-slot="dt-root"
137
+ data-morph={effectiveMorphMode !== 'none' ? effectiveMorphMode : undefined}
134
138
  data-group-active={groupActive || undefined}
135
- className={cn(dynamicToggleStyles.root, slots?.root, className)}
139
+ data-disabled={disabled || undefined}
140
+ role="radiogroup"
141
+ aria-label={ariaLabel}
142
+ style={style}
143
+ className={cn(dynamicToggleVariants({ variant, size, shape }), slots?.root, className)}
136
144
  >
145
+ {/* Filter morph: GooeyCanvas wraps backgrounds only */}
146
+ {effectiveMorphMode === 'filter' && (
147
+ <GooeyCanvas height={heightPx}>
148
+ <div className="absolute inset-0 rounded-[inherit] bg-card" />
149
+ {groupLabelElement}
150
+ </GooeyCanvas>
151
+ )}
152
+
153
+ {/* Path morph: group label rendered directly (no gooey filter) */}
154
+ {effectiveMorphMode === 'path' && groupLabelElement}
155
+
156
+ {/* Track — always rendered, z-indexed above gooey layer */}
137
157
  <div
138
158
  data-slot="dt-track"
139
159
  className={cn(dynamicToggleStyles.track, slots?.track)}
140
160
  >
141
- {/* Main sliding indicator */}
142
161
  <div
143
162
  data-slot="dt-indicator"
144
163
  className={cn(dynamicToggleStyles.indicator, slots?.indicator)}
@@ -155,11 +174,7 @@ function DynamicToggle({
155
174
  // ---------------------------------------------------------------------------
156
175
 
157
176
  /**
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
177
+ * A single toggle option — hidden radio + visible label.
163
178
  */
164
179
  function DynamicToggleOption({ value, children, className }: IDynamicToggleOptionProps) {
165
180
  const ctx = useDynamicToggle();
@@ -177,12 +192,13 @@ function DynamicToggleOption({ value, children, className }: IDynamicToggleOptio
177
192
  <span>{children}</span>
178
193
  </label>
179
194
  <input
180
- data-slot="dt-radio"
195
+ className="sr-only"
181
196
  type="radio"
182
197
  name={ctx.groupName}
183
198
  id={id}
184
199
  value={value}
185
200
  checked={isActive}
201
+ disabled={ctx.disabled}
186
202
  onChange={() => ctx.setValue(value)}
187
203
  />
188
204
  </>
@@ -194,26 +210,50 @@ function DynamicToggleOption({ value, children, className }: IDynamicToggleOptio
194
210
  // ---------------------------------------------------------------------------
195
211
 
196
212
  /**
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
213
+ * Expandable group registers with root on mount, renders sub-options.
214
+ * The group label is rendered by the root based on registered info.
202
215
  */
203
- function DynamicToggleGroup({ label, children, className }: IDynamicToggleGroupProps) {
216
+ function DynamicToggleGroup({
217
+ label,
218
+ labelPosition = 'top',
219
+ collapsedMode = 'title',
220
+ children,
221
+ className,
222
+ }: IDynamicToggleGroupProps) {
223
+ const ctx = useDynamicToggle();
224
+
225
+ // Build combined opts text from children
226
+ const optsText = React.useMemo(() => {
227
+ const labels: string[] = [];
228
+ React.Children.forEach(children, (child) => {
229
+ if (React.isValidElement(child)) {
230
+ const p = child.props as { children?: React.ReactNode };
231
+ if (p.children) labels.push(String(p.children));
232
+ }
233
+ });
234
+ return labels.join(' · ');
235
+ }, [children]);
236
+
237
+ // Register group info with root — useLayoutEffect prevents visual flash
238
+ useIsomorphicLayoutEffect(() => {
239
+ const values: string[] = [];
240
+ React.Children.forEach(children, (child) => {
241
+ if (React.isValidElement(child)) {
242
+ const p = child.props as { value?: string };
243
+ if (p.value) values.push(p.value);
244
+ }
245
+ });
246
+ ctx.registerGroup(label, values, labelPosition, collapsedMode);
247
+ }, [label, labelPosition, collapsedMode, children, ctx.registerGroup]);
248
+
204
249
  return (
205
250
  <div
206
251
  data-slot="dt-group"
252
+ data-collapsed={collapsedMode}
253
+ data-label={label}
254
+ data-opts={collapsedMode !== 'title' ? optsText : undefined}
207
255
  className={cn(dynamicToggleStyles.group, className)}
208
256
  >
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
257
  <div
218
258
  data-slot="dt-group-indicator"
219
259
  className={dynamicToggleStyles.groupIndicator}
@@ -223,18 +263,32 @@ function DynamicToggleGroup({ label, children, className }: IDynamicToggleGroupP
223
263
  );
224
264
  }
225
265
 
266
+ // Display names
267
+ DynamicToggle.displayName = 'DynamicToggle';
268
+ DynamicToggleOption.displayName = 'DynamicToggleOption';
269
+ DynamicToggleGroup.displayName = 'DynamicToggleGroup';
270
+
226
271
  // ---------------------------------------------------------------------------
227
272
  // Exports
228
273
  // ---------------------------------------------------------------------------
229
274
 
230
- export { DynamicToggle, DynamicToggleOption, DynamicToggleGroup };
275
+ export {
276
+ DynamicToggle,
277
+ DynamicToggleOption,
278
+ DynamicToggleGroup,
279
+ useDynamicToggle,
280
+ dynamicToggleVariants,
281
+ };
231
282
 
232
283
  export type {
233
284
  IDynamicToggleProps,
234
285
  IDynamicToggleOptionProps,
235
286
  IDynamicToggleGroupProps,
287
+ IDynamicToggleConfig,
236
288
  DynamicToggleContextType,
289
+ DynamicToggleCollapsedMode,
290
+ DynamicToggleMorphMode,
237
291
  } from './DynamicToggle.types';
238
292
 
239
- export type { DynamicToggleSlot } from './DynamicToggle.styles';
293
+ export type { DynamicToggleSlot, DynamicToggleVariantProps } from './DynamicToggle.styles';
240
294
  export { dynamicToggleStyles } from './DynamicToggle.styles';