@mks2508/mks-ui 0.5.2 → 0.5.7

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 (72) 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 +120 -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 +190 -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 +94 -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 +182 -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-DOR3Ld-k.css +376 -0
  35. package/dist/react-ui/ui/DynamicToggle/DynamicToggle.css +376 -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 +69 -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 +133 -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 +141 -0
  50. package/src/react-ui/primitives/waapi/Gooey/GooeyCanvas.tsx +217 -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 +253 -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 +320 -94
  61. package/src/react-ui/ui/DynamicToggle/DynamicToggle.styles.ts +60 -40
  62. package/src/react-ui/ui/DynamicToggle/DynamicToggle.types.ts +101 -14
  63. package/src/react-ui/ui/DynamicToggle/index.tsx +172 -96
  64. package/src/react-ui/ui/DynamicToggle/prototype-v7-ios.html +413 -0
  65. package/src/react-ui/ui/DynamicToggle/prototype-v7.html +615 -0
  66. package/src/react-ui/ui/DynamicToggle/prototype-v8-gooey-safari.html +560 -0
  67. package/src/react-ui/ui/DynamicToggle/prototype-v8b-react-structure.html +227 -0
  68. package/src/react-ui/ui/DynamicToggle/prototype.html +419 -0
  69. package/src/react-ui/ui/Switch/index.tsx +1 -1
  70. /package/dist/react-ui/blocks/Terminal/panel/{terminal-filter-dropdown.module-DAcl_XQZ.css → terminal-filter-dropdown.module-C6oDcFBS.css} +0 -0
  71. /package/dist/react-ui/blocks/Terminal/panel/{terminal-session-tabs.module-DNAop5e3.css → terminal-session-tabs.module-D_-sgyza.css} +0 -0
  72. /package/dist/react-ui/components/MorphingPopover/{morphing-popover.module-BJrjXisF.css → morphing-popover.module-B1ftlaYj.css} +0 -0
@@ -1,14 +1,58 @@
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
+ /** Indicator slide duration in seconds (default: 0.3) */
46
+ indicatorDuration?: number;
47
+ /** Indicator slide easing (default: material standard cubic-bezier) */
48
+ indicatorEasing?: string;
49
+ /** Force translate-based indicator instead of CSS Anchor (debug/compat) */
50
+ forceTranslateIndicator?: boolean;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Context
55
+ // ---------------------------------------------------------------------------
12
56
 
13
57
  /**
14
58
  * Context shared between DynamicToggle root and its children.
@@ -22,37 +66,66 @@ export type DynamicToggleContextType = {
22
66
  groupName: string;
23
67
  /** Whether a group child is active */
24
68
  groupActive: boolean;
69
+ /** Whether the toggle is disabled */
70
+ disabled: boolean;
71
+ /** Register group info (called by DynamicToggleGroup on mount) */
72
+ registerGroup: (
73
+ label: string,
74
+ values: string[],
75
+ position: 'top' | 'bottom' | 'hidden',
76
+ collapsedMode: DynamicToggleCollapsedMode,
77
+ ) => void;
25
78
  };
26
79
 
80
+ // ---------------------------------------------------------------------------
81
+ // Root
82
+ // ---------------------------------------------------------------------------
83
+
27
84
  /**
28
85
  * Props for the DynamicToggle root container.
29
86
  *
87
+ * Supports exactly one `DynamicToggleOption` + one `DynamicToggleGroup`.
88
+ * The group expands into sub-options when one of its children is active.
89
+ *
30
90
  * @example
31
91
  * ```tsx
32
- * <DynamicToggle value="tree" onValueChange={setVal}>
92
+ * <DynamicToggle
93
+ * value="tree" onValueChange={setVal}
94
+ * size="sm" config={{ morphMode: 'filter' }}
95
+ * >
33
96
  * <DynamicToggleOption value="tree">Tree</DynamicToggleOption>
34
- * <DynamicToggleGroup label="Changes">
97
+ * <DynamicToggleGroup label="Changes" collapsedMode="title">
35
98
  * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
36
99
  * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
37
100
  * </DynamicToggleGroup>
38
101
  * </DynamicToggle>
39
102
  * ```
40
103
  */
41
- export interface IDynamicToggleProps {
104
+ export interface IDynamicToggleProps extends DynamicToggleVariantProps {
42
105
  /** Controlled value */
43
106
  value?: string;
44
107
  /** Uncontrolled default value */
45
108
  defaultValue?: string;
46
109
  /** Change callback */
47
110
  onValueChange?: (value: string) => void;
111
+ /** Disable the entire toggle */
112
+ disabled?: boolean;
48
113
  /** Slot class overrides */
49
114
  slots?: SlotOverrides<DynamicToggleSlot>;
115
+ /** Animation/behavior configuration */
116
+ config?: IDynamicToggleConfig;
117
+ /** Accessible label for the radio group */
118
+ 'aria-label'?: string;
50
119
  /** Additional class for the root */
51
120
  className?: string;
52
121
  /** DynamicToggleOption and DynamicToggleGroup children */
53
122
  children: React.ReactNode;
54
123
  }
55
124
 
125
+ // ---------------------------------------------------------------------------
126
+ // Option
127
+ // ---------------------------------------------------------------------------
128
+
56
129
  /**
57
130
  * Props for a single toggle option (top-level or inside a group).
58
131
  *
@@ -70,23 +143,37 @@ export interface IDynamicToggleOptionProps {
70
143
  className?: string;
71
144
  }
72
145
 
146
+ // ---------------------------------------------------------------------------
147
+ // Group
148
+ // ---------------------------------------------------------------------------
149
+
73
150
  /**
74
151
  * 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.
152
+ *
153
+ * When none of the group's options are active, shows collapsed content
154
+ * based on `collapsedMode`:
155
+ * - `'title'` — only the group label
156
+ * - `'opts'` — only the combined sub-option text ("Solo · Team")
157
+ * - `'title-opts'` — WIP: currently falls back to 'title' behavior
158
+ *
159
+ * When one is active, expands with a clip-path reveal animation.
77
160
  *
78
161
  * @example
79
162
  * ```tsx
80
- * <DynamicToggleGroup label="Changes">
81
- * <DynamicToggleOption value="flat">Flat</DynamicToggleOption>
82
- * <DynamicToggleOption value="grouped">Grouped</DynamicToggleOption>
163
+ * <DynamicToggleGroup label="Premium" collapsedMode="title">
164
+ * <DynamicToggleOption value="solo">Solo</DynamicToggleOption>
165
+ * <DynamicToggleOption value="team">Team</DynamicToggleOption>
83
166
  * </DynamicToggleGroup>
84
167
  * ```
85
168
  */
86
169
  export interface IDynamicToggleGroupProps {
87
- /** Label shown when collapsed */
170
+ /** Label shown as group title / group label text */
88
171
  label: string;
89
- /** DynamicToggleOption children */
172
+ /** Group label position relative to the pill (default: 'top') */
173
+ labelPosition?: 'top' | 'bottom' | 'hidden';
174
+ /** How the group appears when collapsed (default: 'title') */
175
+ collapsedMode?: DynamicToggleCollapsedMode;
176
+ /** Exactly 2 DynamicToggleOption children */
90
177
  children: React.ReactNode;
91
178
  /** Additional class */
92
179
  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,54 @@ 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
+
65
+ const SIZE_WIDTH_PX: Record<string, number> = {
66
+ sm: 210,
67
+ default: 260,
68
+ lg: 320,
69
+ };
70
+
71
+ const SHAPE_RADIUS: Record<string, number> = {
72
+ pill: 9999,
73
+ rounded: 12,
74
+ square: 6,
75
+ };
76
+
87
77
  // ---------------------------------------------------------------------------
88
78
  // DynamicToggle (Root)
89
79
  // ---------------------------------------------------------------------------
90
80
 
91
81
  /**
92
- * Root container — pill-shaped toggle control.
93
- *
94
- * @param props - Component props
95
- * @returns React component
82
+ * Root container — pill-shaped toggle with expanding sub-options and group label.
96
83
  */
97
84
  function DynamicToggle({
85
+ variant,
86
+ size,
87
+ shape,
98
88
  slots,
89
+ config,
90
+ disabled = false,
99
91
  className,
100
92
  children,
93
+ 'aria-label': ariaLabel,
101
94
  ...props
102
95
  }: IDynamicToggleProps) {
103
- useDynamicToggleCSS();
104
-
105
96
  const [value, setValue] = useControlledState({
106
97
  value: props.value,
107
98
  defaultValue: props.defaultValue ?? '',
@@ -110,35 +101,85 @@ function DynamicToggle({
110
101
 
111
102
  const groupName = React.useId();
112
103
 
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]);
104
+ // Group info registered by DynamicToggleGroup child via context + useLayoutEffect
105
+ const [groupLabel, setGroupLabel] = React.useState('');
106
+ const [groupValues, setGroupValues] = React.useState<string[]>([]);
107
+ const [groupPosition, setGroupPosition] = React.useState<'top' | 'bottom' | 'hidden'>('top');
108
+ const [groupCollapsedMode, setGroupCollapsedMode] = React.useState<DynamicToggleCollapsedMode>('title');
109
+
110
+ const registerGroup = React.useCallback(
111
+ (label: string, values: string[], position: 'top' | 'bottom' | 'hidden', collapsedMode: DynamicToggleCollapsedMode) => {
112
+ setGroupLabel(label);
113
+ setGroupValues(values);
114
+ setGroupPosition(position);
115
+ setGroupCollapsedMode(collapsedMode);
116
+ },
117
+ [],
118
+ );
127
119
 
128
120
  const groupActive = groupValues.includes(value);
129
121
 
122
+ // Config
123
+ const morphMode = config?.morphMode ?? 'none';
124
+ const resolvedVariant = variant ?? 'default';
125
+ const effectiveMorphMode = (resolvedVariant === 'ghost' || resolvedVariant === 'outline') ? 'none' : morphMode;
126
+ const labelAnimation = config?.labelAnimation ?? 'morph';
127
+ const showGroupLabel = labelAnimation !== 'none' && groupPosition !== 'hidden' && groupLabel;
128
+ const heightPx = SIZE_HEIGHT_PX[size ?? 'default'] ?? 32;
129
+
130
+ const style = {
131
+ ...(config?.duration && { '--dt-dur': `${config.duration}s` }),
132
+ ...(config?.indicatorDuration && { '--dt-indicator-dur': `${config.indicatorDuration}s` }),
133
+ ...(config?.indicatorEasing && { '--dt-indicator-ease': config.indicatorEasing }),
134
+ } as React.CSSProperties;
135
+ const hasStyle = Object.keys(style).length > 0;
136
+
137
+ // Group label element (shared between modes)
138
+ const groupLabelElement = showGroupLabel ? (
139
+ <div
140
+ data-slot="dt-group-label"
141
+ data-position={groupPosition}
142
+ className={cn(dynamicToggleStyles.groupLabel, slots?.groupLabel)}
143
+ >
144
+ <span>{groupLabel || '\u00A0'}</span>
145
+ </div>
146
+ ) : null;
147
+
130
148
  return (
131
- <DynamicToggleProvider value={{ value, setValue, groupName, groupActive }}>
149
+ <DynamicToggleProvider value={{ value, setValue, groupName, groupActive, disabled, registerGroup }}>
132
150
  <div
133
151
  data-slot="dt-root"
152
+ data-morph={effectiveMorphMode !== 'none' ? effectiveMorphMode : undefined}
153
+ data-indicator={config?.forceTranslateIndicator ? 'translate' : undefined}
134
154
  data-group-active={groupActive || undefined}
135
- className={cn(dynamicToggleStyles.root, slots?.root, className)}
155
+ data-disabled={disabled || undefined}
156
+ role="radiogroup"
157
+ aria-label={ariaLabel}
158
+ style={hasStyle ? style : undefined}
159
+ className={cn(dynamicToggleVariants({ variant, size, shape }), slots?.root, className)}
136
160
  >
161
+ {/* Filter morph: GooeyCanvas with SVG rects (Safari-safe) */}
162
+ {effectiveMorphMode === 'filter' && (
163
+ <>
164
+ <GooeyCanvas
165
+ height={heightPx}
166
+ width={SIZE_WIDTH_PX[size ?? 'default'] ?? 260}
167
+ radius={SHAPE_RADIUS[shape ?? 'pill'] ?? 9999}
168
+ expanded={groupActive}
169
+ />
170
+ {/* Group label text — outside the filtered canvas */}
171
+ {groupLabelElement}
172
+ </>
173
+ )}
174
+
175
+ {/* Path morph: group label rendered directly (no gooey filter) */}
176
+ {effectiveMorphMode === 'path' && groupLabelElement}
177
+
178
+ {/* Track — always rendered, z-indexed above gooey layer */}
137
179
  <div
138
180
  data-slot="dt-track"
139
181
  className={cn(dynamicToggleStyles.track, slots?.track)}
140
182
  >
141
- {/* Main sliding indicator */}
142
183
  <div
143
184
  data-slot="dt-indicator"
144
185
  className={cn(dynamicToggleStyles.indicator, slots?.indicator)}
@@ -155,11 +196,7 @@ function DynamicToggle({
155
196
  // ---------------------------------------------------------------------------
156
197
 
157
198
  /**
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
199
+ * A single toggle option — hidden radio + visible label.
163
200
  */
164
201
  function DynamicToggleOption({ value, children, className }: IDynamicToggleOptionProps) {
165
202
  const ctx = useDynamicToggle();
@@ -177,12 +214,13 @@ function DynamicToggleOption({ value, children, className }: IDynamicToggleOptio
177
214
  <span>{children}</span>
178
215
  </label>
179
216
  <input
180
- data-slot="dt-radio"
217
+ className="sr-only"
181
218
  type="radio"
182
219
  name={ctx.groupName}
183
220
  id={id}
184
221
  value={value}
185
222
  checked={isActive}
223
+ disabled={ctx.disabled}
186
224
  onChange={() => ctx.setValue(value)}
187
225
  />
188
226
  </>
@@ -194,26 +232,50 @@ function DynamicToggleOption({ value, children, className }: IDynamicToggleOptio
194
232
  // ---------------------------------------------------------------------------
195
233
 
196
234
  /**
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
235
+ * Expandable group registers with root on mount, renders sub-options.
236
+ * The group label is rendered by the root based on registered info.
202
237
  */
203
- function DynamicToggleGroup({ label, children, className }: IDynamicToggleGroupProps) {
238
+ function DynamicToggleGroup({
239
+ label,
240
+ labelPosition = 'top',
241
+ collapsedMode = 'title',
242
+ children,
243
+ className,
244
+ }: IDynamicToggleGroupProps) {
245
+ const ctx = useDynamicToggle();
246
+
247
+ // Build combined opts text from children
248
+ const optsText = React.useMemo(() => {
249
+ const labels: string[] = [];
250
+ React.Children.forEach(children, (child) => {
251
+ if (React.isValidElement(child)) {
252
+ const p = child.props as { children?: React.ReactNode };
253
+ if (p.children) labels.push(String(p.children));
254
+ }
255
+ });
256
+ return labels.join(' · ');
257
+ }, [children]);
258
+
259
+ // Register group info with root — useLayoutEffect prevents visual flash
260
+ useIsomorphicLayoutEffect(() => {
261
+ const values: string[] = [];
262
+ React.Children.forEach(children, (child) => {
263
+ if (React.isValidElement(child)) {
264
+ const p = child.props as { value?: string };
265
+ if (p.value) values.push(p.value);
266
+ }
267
+ });
268
+ ctx.registerGroup(label, values, labelPosition, collapsedMode);
269
+ }, [label, labelPosition, collapsedMode, children, ctx.registerGroup]);
270
+
204
271
  return (
205
272
  <div
206
273
  data-slot="dt-group"
274
+ data-collapsed={collapsedMode}
275
+ data-label={label}
276
+ data-opts={collapsedMode !== 'title' ? optsText : undefined}
207
277
  className={cn(dynamicToggleStyles.group, className)}
208
278
  >
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
279
  <div
218
280
  data-slot="dt-group-indicator"
219
281
  className={dynamicToggleStyles.groupIndicator}
@@ -223,18 +285,32 @@ function DynamicToggleGroup({ label, children, className }: IDynamicToggleGroupP
223
285
  );
224
286
  }
225
287
 
288
+ // Display names
289
+ DynamicToggle.displayName = 'DynamicToggle';
290
+ DynamicToggleOption.displayName = 'DynamicToggleOption';
291
+ DynamicToggleGroup.displayName = 'DynamicToggleGroup';
292
+
226
293
  // ---------------------------------------------------------------------------
227
294
  // Exports
228
295
  // ---------------------------------------------------------------------------
229
296
 
230
- export { DynamicToggle, DynamicToggleOption, DynamicToggleGroup };
297
+ export {
298
+ DynamicToggle,
299
+ DynamicToggleOption,
300
+ DynamicToggleGroup,
301
+ useDynamicToggle,
302
+ dynamicToggleVariants,
303
+ };
231
304
 
232
305
  export type {
233
306
  IDynamicToggleProps,
234
307
  IDynamicToggleOptionProps,
235
308
  IDynamicToggleGroupProps,
309
+ IDynamicToggleConfig,
236
310
  DynamicToggleContextType,
311
+ DynamicToggleCollapsedMode,
312
+ DynamicToggleMorphMode,
237
313
  } from './DynamicToggle.types';
238
314
 
239
- export type { DynamicToggleSlot } from './DynamicToggle.styles';
315
+ export type { DynamicToggleSlot, DynamicToggleVariantProps } from './DynamicToggle.styles';
240
316
  export { dynamicToggleStyles } from './DynamicToggle.styles';