@viax.io/uxm 4.45.0 → 4.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/studio/lib/registry/inputs/radio-group.cjs +33 -1
  3. package/dist/studio/lib/registry/inputs/radio-group.cjs.map +1 -1
  4. package/dist/studio/lib/registry/inputs/radio-group.d.ts.map +1 -1
  5. package/dist/studio/lib/registry/inputs/radio-group.js +33 -1
  6. package/dist/studio/lib/registry/inputs/radio-group.js.map +1 -1
  7. package/dist/ui/index.cjs +2 -0
  8. package/dist/ui/index.cjs.map +1 -1
  9. package/dist/ui/index.d.ts +2 -2
  10. package/dist/ui/index.d.ts.map +1 -1
  11. package/dist/ui/index.js +2 -1
  12. package/dist/ui/index.js.map +1 -1
  13. package/dist/ui/menu/index.cjs +3 -1
  14. package/dist/ui/menu/index.cjs.map +1 -1
  15. package/dist/ui/menu/index.d.ts +1 -0
  16. package/dist/ui/menu/index.d.ts.map +1 -1
  17. package/dist/ui/menu/index.js +1 -0
  18. package/dist/ui/menu/index.js.map +1 -1
  19. package/dist/ui/menu/menu-button.cjs +89 -0
  20. package/dist/ui/menu/menu-button.cjs.map +1 -0
  21. package/dist/ui/menu/menu-button.css +30 -0
  22. package/dist/ui/menu/menu-button.d.ts +48 -0
  23. package/dist/ui/menu/menu-button.d.ts.map +1 -0
  24. package/dist/ui/menu/menu-button.js +70 -0
  25. package/dist/ui/menu/menu-button.js.map +1 -0
  26. package/dist/ui/radio-group/radio-group-preview.cjs +86 -35
  27. package/dist/ui/radio-group/radio-group-preview.cjs.map +1 -1
  28. package/dist/ui/radio-group/radio-group-preview.d.ts.map +1 -1
  29. package/dist/ui/radio-group/radio-group-preview.js +87 -36
  30. package/dist/ui/radio-group/radio-group-preview.js.map +1 -1
  31. package/dist/ui/radio-group/radio-group.cjs +46 -18
  32. package/dist/ui/radio-group/radio-group.cjs.map +1 -1
  33. package/dist/ui/radio-group/radio-group.css +82 -0
  34. package/dist/ui/radio-group/radio-group.d.ts +24 -1
  35. package/dist/ui/radio-group/radio-group.d.ts.map +1 -1
  36. package/dist/ui/radio-group/radio-group.js +46 -18
  37. package/dist/ui/radio-group/radio-group.js.map +1 -1
  38. package/dist/ui/styles.css +1 -0
  39. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/menu/menu-button.tsx"],"sourcesContent":["import { cn } from '@/helpers';\nimport {\n ButtonGhost,\n ButtonPrimary,\n ButtonSecondary,\n ButtonTertiary,\n} from '@/ui/button';\nimport { Icon } from '@/ui/icon';\nimport type { PopoverPlacement } from '@/ui/popover';\n\nimport { Menu } from './menu';\n\nimport type { MenuEntry } from './menu';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\n\n/** Which shipped button renders the trigger. */\nexport type MenuButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost';\n\nconst TRIGGER: Record<\n MenuButtonVariant,\n (props: ComponentPropsWithRef<'button'>) => ReactNode\n> = {\n primary: ButtonPrimary,\n secondary: ButtonSecondary,\n tertiary: ButtonTertiary,\n ghost: ButtonGhost,\n};\n\nexport interface MenuButtonProps {\n /** Menu entries — the same `MenuEntry[]` `Menu` takes. */\n items: MenuEntry[];\n /** Button label. */\n children: ReactNode;\n /** Which shipped button renders the trigger. Default `secondary`. */\n variant?: MenuButtonVariant;\n /** Optional leading icon, before the label. */\n icon?: ReactNode;\n /**\n * Trailing dropdown affordance. Default a `chevron-down` that rotates while\n * the menu is open; pass `false` to omit it (for a trigger whose label\n * already reads as a menu, or where space is tight).\n */\n chevron?: boolean;\n /** Forwarded to `Menu`. Default `bottom-end`, as `Menu`'s own default. */\n placement?: PopoverPlacement;\n /** Disables the trigger. The menu cannot be opened. */\n disabled?: boolean;\n /** Accessible name for the menu panel. Forwarded to `Menu`. */\n 'aria-label'?: string;\n /** Class on the trigger button. */\n className?: string;\n}\n\n/**\n * The labelled dropdown button — a `Menu` whose trigger is one of the shipped\n * `Button*` atoms, with the chevron, the ARIA and the trigger ref already\n * wired.\n *\n * `Menu` is deliberately trigger-agnostic: `renderTrigger` is required so a ⋮\n * `IconButton`, an avatar or a field can all open one. That is the right\n * primitive, but it means the single most common shape — a button that says\n * what it does and drops a menu — is re-derived at every call site, along with\n * a hand-added chevron and the `triggerProps` plumbing. This is that shape,\n * shipped once.\n *\n * It is a composition, not a new visual primitive: the button is a real\n * `Button*`, the menu is a real `Menu`. Anything this does not expose —\n * controlled `open`, `matchAnchorWidth`, a bespoke trigger — is a sign to drop\n * to `Menu` + `renderTrigger`, which is unchanged and always available.\n */\nexport function MenuButton({\n items,\n children,\n variant = 'secondary',\n icon,\n chevron = true,\n placement,\n disabled,\n 'aria-label': ariaLabel,\n className,\n}: MenuButtonProps) {\n const Trigger = TRIGGER[variant];\n\n return (\n <Menu\n items={items}\n placement={placement}\n aria-label={ariaLabel}\n renderTrigger={({ open, triggerProps }) => (\n // `triggerProps` spreads straight onto the button — its `ref` included.\n // Before 4.45.0 the button family did not declare `ref`, which is why\n // consumers wrapped the trigger in a `<span ref>`; that wrapper is what\n // this composite exists to stop re-deriving.\n <Trigger\n {...triggerProps}\n disabled={disabled}\n className={cn('uxm-menu-button', className)}\n >\n {icon}\n {children}\n {chevron && (\n <Icon\n glyph=\"chevron-down\"\n size={14}\n className={cn(\n 'uxm-menu-button__chevron',\n open && 'uxm-menu-button__chevron--open',\n )}\n />\n )}\n </Trigger>\n )}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6FQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA7FR,qBAAmB;AACnB,oBAKO;AACP,kBAAqB;AAGrB,kBAAqB;AAQrB,MAAM,UAGF;AAAA,EACF,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,OAAO;AACT;AA4CO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAoB;AAClB,QAAM,UAAU,QAAQ,OAAO;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,cAAY;AAAA,MACZ,eAAe,CAAC,EAAE,MAAM,aAAa,MAKnC;AAAA,QAAC;AAAA;AAAA,UACE,GAAG;AAAA,UACJ;AAAA,UACA,eAAW,mBAAG,mBAAmB,SAAS;AAAA,UAEzC;AAAA;AAAA,YACA;AAAA,YACA,WACC;AAAA,cAAC;AAAA;AAAA,gBACC,OAAM;AAAA,gBACN,MAAM;AAAA,gBACN,eAAW;AAAA,kBACT;AAAA,kBACA,QAAQ;AAAA,gBACV;AAAA;AAAA,YACF;AAAA;AAAA;AAAA,MAEJ;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -0,0 +1,30 @@
1
+ /* ── menu-button ── */
2
+ /* `MenuButton` is a composition: the trigger IS a shipped `Button*`, so the
3
+ whole row — `display: inline-flex`, the alignment, and
4
+ `gap: var(--uxm-button-{variant}-gap, 8px)` — already comes from that atom.
5
+ Nothing here re-declares any of it.
6
+
7
+ That is not tidiness, it is the difference between the button's gap knob
8
+ working and not: `styles.css` imports `button.css` at line 16 and this file
9
+ at 65, so a `gap` declared here would win the cascade at equal specificity
10
+ and quietly pin a MenuButton to its own value while every other button in
11
+ the app followed `--uxm-button-{variant}-gap`.
12
+
13
+ So this block owns exactly one thing the button cannot know about: the
14
+ chevron's open state. */
15
+ .uxm-menu-button__chevron {
16
+ flex-shrink: 0;
17
+ /* Turns to point at the open panel. Purely decorative — `aria-expanded` on
18
+ the trigger is what actually announces the state. */
19
+ transition: transform 0.15s ease;
20
+ }
21
+ .uxm-menu-button__chevron--open {
22
+ transform: rotate(180deg);
23
+ }
24
+
25
+ /* The rotation is decoration; the chevron must still point the right way. */
26
+ @media (prefers-reduced-motion: reduce) {
27
+ .uxm-menu-button__chevron {
28
+ transition: none;
29
+ }
30
+ }
@@ -0,0 +1,48 @@
1
+ import type { PopoverPlacement } from '../popover/index.js';
2
+ import type { MenuEntry } from './menu.js';
3
+ import type { ReactNode } from 'react';
4
+ /** Which shipped button renders the trigger. */
5
+ export type MenuButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost';
6
+ export interface MenuButtonProps {
7
+ /** Menu entries — the same `MenuEntry[]` `Menu` takes. */
8
+ items: MenuEntry[];
9
+ /** Button label. */
10
+ children: ReactNode;
11
+ /** Which shipped button renders the trigger. Default `secondary`. */
12
+ variant?: MenuButtonVariant;
13
+ /** Optional leading icon, before the label. */
14
+ icon?: ReactNode;
15
+ /**
16
+ * Trailing dropdown affordance. Default a `chevron-down` that rotates while
17
+ * the menu is open; pass `false` to omit it (for a trigger whose label
18
+ * already reads as a menu, or where space is tight).
19
+ */
20
+ chevron?: boolean;
21
+ /** Forwarded to `Menu`. Default `bottom-end`, as `Menu`'s own default. */
22
+ placement?: PopoverPlacement;
23
+ /** Disables the trigger. The menu cannot be opened. */
24
+ disabled?: boolean;
25
+ /** Accessible name for the menu panel. Forwarded to `Menu`. */
26
+ 'aria-label'?: string;
27
+ /** Class on the trigger button. */
28
+ className?: string;
29
+ }
30
+ /**
31
+ * The labelled dropdown button — a `Menu` whose trigger is one of the shipped
32
+ * `Button*` atoms, with the chevron, the ARIA and the trigger ref already
33
+ * wired.
34
+ *
35
+ * `Menu` is deliberately trigger-agnostic: `renderTrigger` is required so a ⋮
36
+ * `IconButton`, an avatar or a field can all open one. That is the right
37
+ * primitive, but it means the single most common shape — a button that says
38
+ * what it does and drops a menu — is re-derived at every call site, along with
39
+ * a hand-added chevron and the `triggerProps` plumbing. This is that shape,
40
+ * shipped once.
41
+ *
42
+ * It is a composition, not a new visual primitive: the button is a real
43
+ * `Button*`, the menu is a real `Menu`. Anything this does not expose —
44
+ * controlled `open`, `matchAnchorWidth`, a bespoke trigger — is a sign to drop
45
+ * to `Menu` + `renderTrigger`, which is unchanged and always available.
46
+ */
47
+ export declare function MenuButton({ items, children, variant, icon, chevron, placement, disabled, 'aria-label': ariaLabel, className, }: MenuButtonProps): import("react").JSX.Element;
48
+ //# sourceMappingURL=menu-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu-button.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/menu-button.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAyB,SAAS,EAAE,MAAM,OAAO,CAAC;AAE9D,gDAAgD;AAChD,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;AAY/E,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,oBAAoB;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,qEAAqE;IACrE,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,OAAqB,EACrB,IAAI,EACJ,OAAc,EACd,SAAS,EACT,QAAQ,EACR,YAAY,EAAE,SAAS,EACvB,SAAS,GACV,EAAE,eAAe,+BAkCjB"}
@@ -0,0 +1,70 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../../helpers/index.js";
3
+ import {
4
+ ButtonGhost,
5
+ ButtonPrimary,
6
+ ButtonSecondary,
7
+ ButtonTertiary
8
+ } from "../button/index.js";
9
+ import { Icon } from "../icon/index.js";
10
+ import { Menu } from "./menu.js";
11
+ const TRIGGER = {
12
+ primary: ButtonPrimary,
13
+ secondary: ButtonSecondary,
14
+ tertiary: ButtonTertiary,
15
+ ghost: ButtonGhost
16
+ };
17
+ function MenuButton({
18
+ items,
19
+ children,
20
+ variant = "secondary",
21
+ icon,
22
+ chevron = true,
23
+ placement,
24
+ disabled,
25
+ "aria-label": ariaLabel,
26
+ className
27
+ }) {
28
+ const Trigger = TRIGGER[variant];
29
+ return /* @__PURE__ */ jsx(
30
+ Menu,
31
+ {
32
+ items,
33
+ placement,
34
+ "aria-label": ariaLabel,
35
+ renderTrigger: ({ open, triggerProps }) => (
36
+ // `triggerProps` spreads straight onto the button — its `ref` included.
37
+ // Before 4.45.0 the button family did not declare `ref`, which is why
38
+ // consumers wrapped the trigger in a `<span ref>`; that wrapper is what
39
+ // this composite exists to stop re-deriving.
40
+ /* @__PURE__ */ jsxs(
41
+ Trigger,
42
+ {
43
+ ...triggerProps,
44
+ disabled,
45
+ className: cn("uxm-menu-button", className),
46
+ children: [
47
+ icon,
48
+ children,
49
+ chevron && /* @__PURE__ */ jsx(
50
+ Icon,
51
+ {
52
+ glyph: "chevron-down",
53
+ size: 14,
54
+ className: cn(
55
+ "uxm-menu-button__chevron",
56
+ open && "uxm-menu-button__chevron--open"
57
+ )
58
+ }
59
+ )
60
+ ]
61
+ }
62
+ )
63
+ )
64
+ }
65
+ );
66
+ }
67
+ export {
68
+ MenuButton
69
+ };
70
+ //# sourceMappingURL=menu-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/menu/menu-button.tsx"],"sourcesContent":["import { cn } from '@/helpers';\nimport {\n ButtonGhost,\n ButtonPrimary,\n ButtonSecondary,\n ButtonTertiary,\n} from '@/ui/button';\nimport { Icon } from '@/ui/icon';\nimport type { PopoverPlacement } from '@/ui/popover';\n\nimport { Menu } from './menu';\n\nimport type { MenuEntry } from './menu';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\n\n/** Which shipped button renders the trigger. */\nexport type MenuButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost';\n\nconst TRIGGER: Record<\n MenuButtonVariant,\n (props: ComponentPropsWithRef<'button'>) => ReactNode\n> = {\n primary: ButtonPrimary,\n secondary: ButtonSecondary,\n tertiary: ButtonTertiary,\n ghost: ButtonGhost,\n};\n\nexport interface MenuButtonProps {\n /** Menu entries — the same `MenuEntry[]` `Menu` takes. */\n items: MenuEntry[];\n /** Button label. */\n children: ReactNode;\n /** Which shipped button renders the trigger. Default `secondary`. */\n variant?: MenuButtonVariant;\n /** Optional leading icon, before the label. */\n icon?: ReactNode;\n /**\n * Trailing dropdown affordance. Default a `chevron-down` that rotates while\n * the menu is open; pass `false` to omit it (for a trigger whose label\n * already reads as a menu, or where space is tight).\n */\n chevron?: boolean;\n /** Forwarded to `Menu`. Default `bottom-end`, as `Menu`'s own default. */\n placement?: PopoverPlacement;\n /** Disables the trigger. The menu cannot be opened. */\n disabled?: boolean;\n /** Accessible name for the menu panel. Forwarded to `Menu`. */\n 'aria-label'?: string;\n /** Class on the trigger button. */\n className?: string;\n}\n\n/**\n * The labelled dropdown button — a `Menu` whose trigger is one of the shipped\n * `Button*` atoms, with the chevron, the ARIA and the trigger ref already\n * wired.\n *\n * `Menu` is deliberately trigger-agnostic: `renderTrigger` is required so a ⋮\n * `IconButton`, an avatar or a field can all open one. That is the right\n * primitive, but it means the single most common shape — a button that says\n * what it does and drops a menu — is re-derived at every call site, along with\n * a hand-added chevron and the `triggerProps` plumbing. This is that shape,\n * shipped once.\n *\n * It is a composition, not a new visual primitive: the button is a real\n * `Button*`, the menu is a real `Menu`. Anything this does not expose —\n * controlled `open`, `matchAnchorWidth`, a bespoke trigger — is a sign to drop\n * to `Menu` + `renderTrigger`, which is unchanged and always available.\n */\nexport function MenuButton({\n items,\n children,\n variant = 'secondary',\n icon,\n chevron = true,\n placement,\n disabled,\n 'aria-label': ariaLabel,\n className,\n}: MenuButtonProps) {\n const Trigger = TRIGGER[variant];\n\n return (\n <Menu\n items={items}\n placement={placement}\n aria-label={ariaLabel}\n renderTrigger={({ open, triggerProps }) => (\n // `triggerProps` spreads straight onto the button — its `ref` included.\n // Before 4.45.0 the button family did not declare `ref`, which is why\n // consumers wrapped the trigger in a `<span ref>`; that wrapper is what\n // this composite exists to stop re-deriving.\n <Trigger\n {...triggerProps}\n disabled={disabled}\n className={cn('uxm-menu-button', className)}\n >\n {icon}\n {children}\n {chevron && (\n <Icon\n glyph=\"chevron-down\"\n size={14}\n className={cn(\n 'uxm-menu-button__chevron',\n open && 'uxm-menu-button__chevron--open',\n )}\n />\n )}\n </Trigger>\n )}\n />\n );\n}\n"],"mappings":"AA6FQ,SAQI,KARJ;AA7FR,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AAGrB,SAAS,YAAY;AAQrB,MAAM,UAGF;AAAA,EACF,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,OAAO;AACT;AA4CO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAoB;AAClB,QAAM,UAAU,QAAQ,OAAO;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,cAAY;AAAA,MACZ,eAAe,CAAC,EAAE,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,QAKnC;AAAA,UAAC;AAAA;AAAA,YACE,GAAG;AAAA,YACJ;AAAA,YACA,WAAW,GAAG,mBAAmB,SAAS;AAAA,YAEzC;AAAA;AAAA,cACA;AAAA,cACA,WACC;AAAA,gBAAC;AAAA;AAAA,kBACC,OAAM;AAAA,kBACN,MAAM;AAAA,kBACN,WAAW;AAAA,oBACT;AAAA,oBACA,QAAQ;AAAA,kBACV;AAAA;AAAA,cACF;AAAA;AAAA;AAAA,QAEJ;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -27,16 +27,80 @@ function ShowcaseRow({
27
27
  selected,
28
28
  state,
29
29
  styles,
30
- label
30
+ label,
31
+ variant,
32
+ indicator
31
33
  }) {
32
34
  const isHover = state === "hover";
33
35
  const isFocus = state === "focus";
34
36
  const isDisabled = state === "disabled";
37
+ const isCard = variant === "card";
35
38
  const bg = isHover ? styles.hoverUnselectedBg : styles.unselectedBg;
36
39
  const border = isHover ? selected ? styles.hoverSelectedBorder : styles.hoverUnselectedBorder : selected ? styles.selectedBorder : styles.unselectedBorder;
37
40
  const dot = isHover ? styles.hoverDotColor : styles.dotColor;
38
41
  const ring = styles.focusRing;
39
42
  const size = styles.size ?? 20;
43
+ const circle = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
44
+ "span",
45
+ {
46
+ "aria-hidden": "true",
47
+ style: {
48
+ width: size,
49
+ height: size,
50
+ flexShrink: 0,
51
+ borderRadius: "50%",
52
+ border: `2px solid ${border}`,
53
+ backgroundColor: bg,
54
+ display: "inline-flex",
55
+ alignItems: "center",
56
+ justifyContent: "center",
57
+ transition: "border-color 0.15s, background-color 0.15s",
58
+ ...isFocus && !isCard ? { outline: `2px solid ${ring}`, outlineOffset: 2 } : {},
59
+ ...isCard ? { position: "absolute", top: styles.cardPadding, right: styles.cardPadding } : {}
60
+ },
61
+ children: selected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
62
+ "span",
63
+ {
64
+ style: {
65
+ width: size * 0.5,
66
+ height: size * 0.5,
67
+ borderRadius: "50%",
68
+ backgroundColor: dot
69
+ }
70
+ }
71
+ )
72
+ }
73
+ );
74
+ if (isCard) {
75
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
76
+ "div",
77
+ {
78
+ style: {
79
+ position: "relative",
80
+ display: "flex",
81
+ flexDirection: "column",
82
+ gap: styles.cardGap,
83
+ minWidth: 150,
84
+ boxSizing: "border-box",
85
+ padding: styles.cardPadding,
86
+ borderRadius: styles.cardRadius,
87
+ border: `${styles.cardBorderWidth}px solid ${border}`,
88
+ backgroundColor: styles.cardBg,
89
+ fontSize: 14,
90
+ color: "var(--color-text)",
91
+ opacity: isDisabled ? styles.disabledOpacity ?? 0.4 : 1,
92
+ cursor: isDisabled ? "not-allowed" : "pointer",
93
+ transition: "border-color 0.15s, background-color 0.15s",
94
+ ...isFocus ? { outline: `2px solid ${ring}`, outlineOffset: 2 } : {}
95
+ },
96
+ children: [
97
+ indicator === "corner" && circle,
98
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: label }),
99
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--color-text-muted)", fontSize: 12 }, children: "What this option does" })
100
+ ]
101
+ }
102
+ );
103
+ }
40
104
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
41
105
  "div",
42
106
  {
@@ -50,36 +114,7 @@ function ShowcaseRow({
50
114
  cursor: isDisabled ? "not-allowed" : "pointer"
51
115
  },
52
116
  children: [
53
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
54
- "span",
55
- {
56
- "aria-hidden": "true",
57
- style: {
58
- width: size,
59
- height: size,
60
- flexShrink: 0,
61
- borderRadius: "50%",
62
- border: `2px solid ${border}`,
63
- backgroundColor: bg,
64
- display: "inline-flex",
65
- alignItems: "center",
66
- justifyContent: "center",
67
- transition: "border-color 0.15s, background-color 0.15s",
68
- ...isFocus ? { outline: `2px solid ${ring}`, outlineOffset: 2 } : {}
69
- },
70
- children: selected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
71
- "span",
72
- {
73
- style: {
74
- width: size * 0.5,
75
- height: size * 0.5,
76
- borderRadius: "50%",
77
- backgroundColor: dot
78
- }
79
- }
80
- )
81
- }
82
- ),
117
+ circle,
83
118
  label
84
119
  ]
85
120
  }
@@ -89,6 +124,8 @@ const INTERACTIVE_OPTIONS = ["Small", "Medium", "Large"];
89
124
  function RadioGroupPreview({ styles, variants }) {
90
125
  const state = variants.state ?? "default";
91
126
  const direction = variants.direction ?? "vertical";
127
+ const variant = variants.variant ?? "default";
128
+ const indicator = variants.indicator ?? "hidden";
92
129
  const cssVars = {
93
130
  "--uxm-radio-group-size": styles.size != null ? `${styles.size}px` : void 0,
94
131
  "--uxm-radio-group-gap": styles.gap != null ? `${styles.gap}px` : void 0,
@@ -103,7 +140,12 @@ function RadioGroupPreview({ styles, variants }) {
103
140
  "--uxm-radio-group-focus-ring": styles.focusRing,
104
141
  "--uxm-radio-group-disabled-opacity": styles.disabledOpacity != null ? String(styles.disabledOpacity) : void 0,
105
142
  "--uxm-radio-group-error-color": styles.errorColor,
106
- "--uxm-radio-group-error-message-size": styles.errorMessageSize != null ? `${styles.errorMessageSize}px` : void 0
143
+ "--uxm-radio-group-error-message-size": styles.errorMessageSize != null ? `${styles.errorMessageSize}px` : void 0,
144
+ "--uxm-radio-card-padding": `${styles.cardPadding}px`,
145
+ "--uxm-radio-card-radius": `${styles.cardRadius}px`,
146
+ "--uxm-radio-card-gap": `${styles.cardGap}px`,
147
+ "--uxm-radio-card-border-width": `${styles.cardBorderWidth}px`,
148
+ "--uxm-radio-card-bg": styles.cardBg
107
149
  };
108
150
  const sectionLabel = {
109
151
  fontSize: 11,
@@ -121,8 +163,8 @@ function RadioGroupPreview({ styles, variants }) {
121
163
  " state"
122
164
  ] }),
123
165
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
124
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShowcaseRow, { selected: false, state, styles, label: "Unselected" }),
125
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShowcaseRow, { selected: true, state, styles, label: "Selected" })
166
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShowcaseRow, { selected: false, state, styles, label: "Unselected", variant, indicator }),
167
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShowcaseRow, { selected: true, state, styles, label: "Selected", variant, indicator })
126
168
  ] })
127
169
  ] }),
128
170
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: "1px solid var(--color-border)", paddingTop: 16 }, children: [
@@ -132,6 +174,8 @@ function RadioGroupPreview({ styles, variants }) {
132
174
  {
133
175
  name: `uxm-radio-preview-${interactiveKey}`,
134
176
  direction,
177
+ variant,
178
+ indicator,
135
179
  error: state === "error" ? "Pick an option to continue" : void 0,
136
180
  children: INTERACTIVE_OPTIONS.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
137
181
  import_ui.RadioOption,
@@ -139,7 +183,14 @@ function RadioGroupPreview({ styles, variants }) {
139
183
  name: `uxm-radio-preview-${interactiveKey}`,
140
184
  value: opt,
141
185
  disabled: interactiveDisabled,
142
- children: opt
186
+ children: variant === "card" ? (
187
+ // Card mode is for rich content — a one-word label in a tile
188
+ // would demo a box, not the shape the variant exists for.
189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: opt }),
191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--color-text-muted)", fontSize: 12 }, children: "What this option does" })
192
+ ] })
193
+ ) : opt
143
194
  },
144
195
  opt
145
196
  ))
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/radio-group/radio-group-preview.tsx"],"sourcesContent":["import { type CSSProperties } from 'react';\n\nimport type { PreviewProps } from '@/previews/types';\nimport { RadioGroup, RadioOption } from '@/ui';\n\ntype Styles = PreviewProps['styles'];\n\nfunction ShowcaseRow({\n selected,\n state,\n styles,\n label,\n}: {\n selected: boolean;\n state: string;\n styles: Styles;\n label: string;\n}) {\n // Static showcase — paints the selected forced state regardless of\n // pointer interaction. Inline JSX (not the real <RadioOption>) so each\n // forced state can override the circle + dot directly; the interactive\n // instance below is where production CSS rules exercise.\n const isHover = state === 'hover';\n const isFocus = state === 'focus';\n const isDisabled = state === 'disabled';\n\n const bg = isHover\n ? styles.hoverUnselectedBg\n : styles.unselectedBg;\n const border = isHover\n ? (selected ? styles.hoverSelectedBorder : styles.hoverUnselectedBorder)\n : (selected ? styles.selectedBorder : styles.unselectedBorder);\n const dot = isHover ? styles.hoverDotColor : styles.dotColor;\n const ring = styles.focusRing;\n const size = (styles.size as number) ?? 20;\n\n return (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 10,\n fontSize: 14,\n color: 'var(--color-text)',\n opacity: isDisabled ? ((styles.disabledOpacity as number) ?? 0.4) : 1,\n cursor: isDisabled ? 'not-allowed' : 'pointer',\n }}\n >\n <span\n aria-hidden=\"true\"\n style={{\n width: size,\n height: size,\n flexShrink: 0,\n borderRadius: '50%',\n border: `2px solid ${border as string}`,\n backgroundColor: bg as string,\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n transition: 'border-color 0.15s, background-color 0.15s',\n ...(isFocus\n ? { outline: `2px solid ${ring as string}`, outlineOffset: 2 }\n : {}),\n }}\n >\n {selected && (\n <span\n style={{\n width: size * 0.5,\n height: size * 0.5,\n borderRadius: '50%',\n backgroundColor: dot as string,\n }}\n />\n )}\n </span>\n {label}\n </div>\n );\n}\n\nconst INTERACTIVE_OPTIONS = ['Small', 'Medium', 'Large'];\n\nexport function RadioGroupPreview({ styles, variants }: PreviewProps) {\n const state = (variants.state as string) ?? 'default';\n const direction = (variants.direction as 'vertical' | 'horizontal' | undefined) ?? 'vertical';\n\n // Project every registry knob as a `--uxm-radio-group-*` custom property\n // on the wrapper. The interactive <RadioGroup> below renders the real\n // component whose styles.css rules read these vars — live editor edits\n // paint immediately, and production `:hover` / `:focus-visible` rules\n // exercise exactly as they would in prod.\n const cssVars: Record<string, string | undefined> = {\n '--uxm-radio-group-size': styles.size != null ? `${styles.size}px` : undefined,\n '--uxm-radio-group-gap': styles.gap != null ? `${styles.gap}px` : undefined,\n '--uxm-radio-group-unselected-bg': styles.unselectedBg as string | undefined,\n '--uxm-radio-group-unselected-border': styles.unselectedBorder as string | undefined,\n '--uxm-radio-group-selected-border': styles.selectedBorder as string | undefined,\n '--uxm-radio-group-dot-color': styles.dotColor as string | undefined,\n '--uxm-radio-group-hover-unselected-bg': styles.hoverUnselectedBg as string | undefined,\n '--uxm-radio-group-hover-unselected-border': styles.hoverUnselectedBorder as string | undefined,\n '--uxm-radio-group-hover-selected-border': styles.hoverSelectedBorder as string | undefined,\n '--uxm-radio-group-hover-dot-color': styles.hoverDotColor as string | undefined,\n '--uxm-radio-group-focus-ring': styles.focusRing as string | undefined,\n '--uxm-radio-group-disabled-opacity':\n styles.disabledOpacity != null ? String(styles.disabledOpacity) : undefined,\n '--uxm-radio-group-error-color': styles.errorColor as string | undefined,\n '--uxm-radio-group-error-message-size':\n styles.errorMessageSize != null ? `${styles.errorMessageSize}px` : undefined,\n };\n\n const sectionLabel = {\n fontSize: 11,\n color: 'var(--color-text-muted)',\n marginBottom: 12,\n letterSpacing: 0.5,\n textTransform: 'uppercase' as const,\n };\n\n // Pair the state and direction into the interactive `key` so that\n // toggling either variant resets the radio group cleanly (matches the\n // freshly-{re,dis}abled UX and clears any stale selection when the user\n // flips layout).\n const interactiveKey = `${state}-${direction}`;\n const interactiveDisabled = state === 'disabled';\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 32, ...cssVars } as CSSProperties}>\n <div>\n <div style={sectionLabel}>{state} state</div>\n <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>\n <ShowcaseRow selected={false} state={state} styles={styles} label=\"Unselected\" />\n <ShowcaseRow selected={true} state={state} styles={styles} label=\"Selected\" />\n </div>\n </div>\n\n {/* Interactive instance — uncontrolled so the browser owns the\n checked state. React (and the canvas's event-capture re-renders)\n can't fight the DOM here, so click/keyboard navigation toggle\n reliably between options. Three options share a unique `name`\n so the browser's radio-group semantics work natively. */}\n <div style={{ borderTop: '1px solid var(--color-border)', paddingTop: 16 }}>\n <div style={sectionLabel}>Interactive</div>\n <RadioGroup\n key={interactiveKey}\n name={`uxm-radio-preview-${interactiveKey}`}\n direction={direction}\n error={state === 'error' ? 'Pick an option to continue' : undefined}\n >\n {INTERACTIVE_OPTIONS.map((opt) => (\n <RadioOption\n key={opt}\n name={`uxm-radio-preview-${interactiveKey}`}\n value={opt}\n disabled={interactiveDisabled}\n >\n {opt}\n </RadioOption>\n ))}\n </RadioGroup>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/radio-group/radio-group-preview.tsx"],"sourcesContent":["import { type CSSProperties } from 'react';\n\nimport type { PreviewProps } from '@/previews/types';\nimport { RadioGroup, RadioOption } from '@/ui';\n\ntype Styles = PreviewProps['styles'];\n\nfunction ShowcaseRow({\n selected,\n state,\n styles,\n label,\n variant,\n indicator,\n}: {\n selected: boolean;\n state: string;\n styles: Styles;\n label: string;\n variant: 'default' | 'card';\n indicator: 'hidden' | 'corner';\n}) {\n // Static showcase — paints the selected forced state regardless of\n // pointer interaction. Inline JSX (not the real <RadioOption>) so each\n // forced state can override the circle + dot directly; the interactive\n // instance below is where production CSS rules exercise.\n //\n // It has to follow `variant`: this is the ONLY thing that paints the forced\n // hover / focus / disabled states — the interactive group below shows the\n // resting state and real input. Left as a row while the variant is `card`,\n // the States picker would show a hovered CIRCLE for a control that is a\n // tile, and the hover / focus knobs (which the card frame genuinely reads)\n // would be tuned against feedback that is not what ships.\n const isHover = state === 'hover';\n const isFocus = state === 'focus';\n const isDisabled = state === 'disabled';\n const isCard = variant === 'card';\n\n const bg = isHover\n ? styles.hoverUnselectedBg\n : styles.unselectedBg;\n const border = isHover\n ? (selected ? styles.hoverSelectedBorder : styles.hoverUnselectedBorder)\n : (selected ? styles.selectedBorder : styles.unselectedBorder);\n const dot = isHover ? styles.hoverDotColor : styles.dotColor;\n const ring = styles.focusRing;\n const size = (styles.size as number) ?? 20;\n\n const circle = (\n <span\n aria-hidden=\"true\"\n style={{\n width: size,\n height: size,\n flexShrink: 0,\n borderRadius: '50%',\n border: `2px solid ${border as string}`,\n backgroundColor: bg as string,\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n transition: 'border-color 0.15s, background-color 0.15s',\n ...(isFocus && !isCard\n ? { outline: `2px solid ${ring as string}`, outlineOffset: 2 }\n : {}),\n ...(isCard\n ? { position: 'absolute' as const, top: styles.cardPadding as number, right: styles.cardPadding as number }\n : {}),\n }}\n >\n {selected && (\n <span\n style={{\n width: size * 0.5,\n height: size * 0.5,\n borderRadius: '50%',\n backgroundColor: dot as string,\n }}\n />\n )}\n </span>\n );\n\n if (isCard) {\n // Mirrors `.uxm-radio--card`: the tile carries the frame and the focus\n // ring, and the circle is either gone or pinned to the corner.\n return (\n <div\n style={{\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n gap: styles.cardGap as number,\n minWidth: 150,\n boxSizing: 'border-box',\n padding: styles.cardPadding as number,\n borderRadius: styles.cardRadius as number,\n border: `${styles.cardBorderWidth}px solid ${border as string}`,\n backgroundColor: styles.cardBg as string,\n fontSize: 14,\n color: 'var(--color-text)',\n opacity: isDisabled ? ((styles.disabledOpacity as number) ?? 0.4) : 1,\n cursor: isDisabled ? 'not-allowed' : 'pointer',\n transition: 'border-color 0.15s, background-color 0.15s',\n ...(isFocus ? { outline: `2px solid ${ring as string}`, outlineOffset: 2 } : {}),\n }}\n >\n {indicator === 'corner' && circle}\n <strong>{label}</strong>\n <span style={{ color: 'var(--color-text-muted)', fontSize: 12 }}>\n What this option does\n </span>\n </div>\n );\n }\n\n return (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: 10,\n fontSize: 14,\n color: 'var(--color-text)',\n opacity: isDisabled ? ((styles.disabledOpacity as number) ?? 0.4) : 1,\n cursor: isDisabled ? 'not-allowed' : 'pointer',\n }}\n >\n {circle}\n {label}\n </div>\n );\n}\n\nconst INTERACTIVE_OPTIONS = ['Small', 'Medium', 'Large'];\n\nexport function RadioGroupPreview({ styles, variants }: PreviewProps) {\n const state = (variants.state as string) ?? 'default';\n const direction = (variants.direction as 'vertical' | 'horizontal' | undefined) ?? 'vertical';\n const variant = (variants.variant as 'default' | 'card' | undefined) ?? 'default';\n const indicator = (variants.indicator as 'hidden' | 'corner' | undefined) ?? 'hidden';\n\n // Project every registry knob as a `--uxm-radio-group-*` custom property\n // on the wrapper. The interactive <RadioGroup> below renders the real\n // component whose styles.css rules read these vars — live editor edits\n // paint immediately, and production `:hover` / `:focus-visible` rules\n // exercise exactly as they would in prod.\n const cssVars: Record<string, string | undefined> = {\n '--uxm-radio-group-size': styles.size != null ? `${styles.size}px` : undefined,\n '--uxm-radio-group-gap': styles.gap != null ? `${styles.gap}px` : undefined,\n '--uxm-radio-group-unselected-bg': styles.unselectedBg as string | undefined,\n '--uxm-radio-group-unselected-border': styles.unselectedBorder as string | undefined,\n '--uxm-radio-group-selected-border': styles.selectedBorder as string | undefined,\n '--uxm-radio-group-dot-color': styles.dotColor as string | undefined,\n '--uxm-radio-group-hover-unselected-bg': styles.hoverUnselectedBg as string | undefined,\n '--uxm-radio-group-hover-unselected-border': styles.hoverUnselectedBorder as string | undefined,\n '--uxm-radio-group-hover-selected-border': styles.hoverSelectedBorder as string | undefined,\n '--uxm-radio-group-hover-dot-color': styles.hoverDotColor as string | undefined,\n '--uxm-radio-group-focus-ring': styles.focusRing as string | undefined,\n '--uxm-radio-group-disabled-opacity':\n styles.disabledOpacity != null ? String(styles.disabledOpacity) : undefined,\n '--uxm-radio-group-error-color': styles.errorColor as string | undefined,\n '--uxm-radio-group-error-message-size':\n styles.errorMessageSize != null ? `${styles.errorMessageSize}px` : undefined,\n '--uxm-radio-card-padding': `${styles.cardPadding}px`,\n '--uxm-radio-card-radius': `${styles.cardRadius}px`,\n '--uxm-radio-card-gap': `${styles.cardGap}px`,\n '--uxm-radio-card-border-width': `${styles.cardBorderWidth}px`,\n '--uxm-radio-card-bg': styles.cardBg as string,\n };\n\n const sectionLabel = {\n fontSize: 11,\n color: 'var(--color-text-muted)',\n marginBottom: 12,\n letterSpacing: 0.5,\n textTransform: 'uppercase' as const,\n };\n\n // Pair the state and direction into the interactive `key` so that\n // toggling either variant resets the radio group cleanly (matches the\n // freshly-{re,dis}abled UX and clears any stale selection when the user\n // flips layout).\n const interactiveKey = `${state}-${direction}`;\n const interactiveDisabled = state === 'disabled';\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 32, ...cssVars } as CSSProperties}>\n <div>\n <div style={sectionLabel}>{state} state</div>\n <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>\n <ShowcaseRow selected={false} state={state} styles={styles} label=\"Unselected\" variant={variant} indicator={indicator} />\n <ShowcaseRow selected={true} state={state} styles={styles} label=\"Selected\" variant={variant} indicator={indicator} />\n </div>\n </div>\n\n {/* Interactive instance — uncontrolled so the browser owns the\n checked state. React (and the canvas's event-capture re-renders)\n can't fight the DOM here, so click/keyboard navigation toggle\n reliably between options. Three options share a unique `name`\n so the browser's radio-group semantics work natively. */}\n <div style={{ borderTop: '1px solid var(--color-border)', paddingTop: 16 }}>\n <div style={sectionLabel}>Interactive</div>\n <RadioGroup\n key={interactiveKey}\n name={`uxm-radio-preview-${interactiveKey}`}\n direction={direction}\n variant={variant}\n indicator={indicator}\n error={state === 'error' ? 'Pick an option to continue' : undefined}\n >\n {INTERACTIVE_OPTIONS.map((opt) => (\n <RadioOption\n key={opt}\n name={`uxm-radio-preview-${interactiveKey}`}\n value={opt}\n disabled={interactiveDisabled}\n >\n {variant === 'card' ? (\n // Card mode is for rich content — a one-word label in a tile\n // would demo a box, not the shape the variant exists for.\n <>\n <strong>{opt}</strong>\n <span style={{ color: 'var(--color-text-muted)', fontSize: 12 }}>\n What this option does\n </span>\n </>\n ) : (\n opt\n )}\n </RadioOption>\n ))}\n </RadioGroup>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"radio-group-preview.d.ts","sourceRoot":"","sources":["../../../src/ui/radio-group/radio-group-preview.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAkFrD,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,YAAY,+BAgFnE"}
1
+ {"version":3,"file":"radio-group-preview.d.ts","sourceRoot":"","sources":["../../../src/ui/radio-group/radio-group-preview.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAsIrD,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,YAAY,+BAoGnE"}
@@ -1,19 +1,83 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { RadioGroup, RadioOption } from "../index.js";
3
3
  function ShowcaseRow({
4
4
  selected,
5
5
  state,
6
6
  styles,
7
- label
7
+ label,
8
+ variant,
9
+ indicator
8
10
  }) {
9
11
  const isHover = state === "hover";
10
12
  const isFocus = state === "focus";
11
13
  const isDisabled = state === "disabled";
14
+ const isCard = variant === "card";
12
15
  const bg = isHover ? styles.hoverUnselectedBg : styles.unselectedBg;
13
16
  const border = isHover ? selected ? styles.hoverSelectedBorder : styles.hoverUnselectedBorder : selected ? styles.selectedBorder : styles.unselectedBorder;
14
17
  const dot = isHover ? styles.hoverDotColor : styles.dotColor;
15
18
  const ring = styles.focusRing;
16
19
  const size = styles.size ?? 20;
20
+ const circle = /* @__PURE__ */ jsx(
21
+ "span",
22
+ {
23
+ "aria-hidden": "true",
24
+ style: {
25
+ width: size,
26
+ height: size,
27
+ flexShrink: 0,
28
+ borderRadius: "50%",
29
+ border: `2px solid ${border}`,
30
+ backgroundColor: bg,
31
+ display: "inline-flex",
32
+ alignItems: "center",
33
+ justifyContent: "center",
34
+ transition: "border-color 0.15s, background-color 0.15s",
35
+ ...isFocus && !isCard ? { outline: `2px solid ${ring}`, outlineOffset: 2 } : {},
36
+ ...isCard ? { position: "absolute", top: styles.cardPadding, right: styles.cardPadding } : {}
37
+ },
38
+ children: selected && /* @__PURE__ */ jsx(
39
+ "span",
40
+ {
41
+ style: {
42
+ width: size * 0.5,
43
+ height: size * 0.5,
44
+ borderRadius: "50%",
45
+ backgroundColor: dot
46
+ }
47
+ }
48
+ )
49
+ }
50
+ );
51
+ if (isCard) {
52
+ return /* @__PURE__ */ jsxs(
53
+ "div",
54
+ {
55
+ style: {
56
+ position: "relative",
57
+ display: "flex",
58
+ flexDirection: "column",
59
+ gap: styles.cardGap,
60
+ minWidth: 150,
61
+ boxSizing: "border-box",
62
+ padding: styles.cardPadding,
63
+ borderRadius: styles.cardRadius,
64
+ border: `${styles.cardBorderWidth}px solid ${border}`,
65
+ backgroundColor: styles.cardBg,
66
+ fontSize: 14,
67
+ color: "var(--color-text)",
68
+ opacity: isDisabled ? styles.disabledOpacity ?? 0.4 : 1,
69
+ cursor: isDisabled ? "not-allowed" : "pointer",
70
+ transition: "border-color 0.15s, background-color 0.15s",
71
+ ...isFocus ? { outline: `2px solid ${ring}`, outlineOffset: 2 } : {}
72
+ },
73
+ children: [
74
+ indicator === "corner" && circle,
75
+ /* @__PURE__ */ jsx("strong", { children: label }),
76
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--color-text-muted)", fontSize: 12 }, children: "What this option does" })
77
+ ]
78
+ }
79
+ );
80
+ }
17
81
  return /* @__PURE__ */ jsxs(
18
82
  "div",
19
83
  {
@@ -27,36 +91,7 @@ function ShowcaseRow({
27
91
  cursor: isDisabled ? "not-allowed" : "pointer"
28
92
  },
29
93
  children: [
30
- /* @__PURE__ */ jsx(
31
- "span",
32
- {
33
- "aria-hidden": "true",
34
- style: {
35
- width: size,
36
- height: size,
37
- flexShrink: 0,
38
- borderRadius: "50%",
39
- border: `2px solid ${border}`,
40
- backgroundColor: bg,
41
- display: "inline-flex",
42
- alignItems: "center",
43
- justifyContent: "center",
44
- transition: "border-color 0.15s, background-color 0.15s",
45
- ...isFocus ? { outline: `2px solid ${ring}`, outlineOffset: 2 } : {}
46
- },
47
- children: selected && /* @__PURE__ */ jsx(
48
- "span",
49
- {
50
- style: {
51
- width: size * 0.5,
52
- height: size * 0.5,
53
- borderRadius: "50%",
54
- backgroundColor: dot
55
- }
56
- }
57
- )
58
- }
59
- ),
94
+ circle,
60
95
  label
61
96
  ]
62
97
  }
@@ -66,6 +101,8 @@ const INTERACTIVE_OPTIONS = ["Small", "Medium", "Large"];
66
101
  function RadioGroupPreview({ styles, variants }) {
67
102
  const state = variants.state ?? "default";
68
103
  const direction = variants.direction ?? "vertical";
104
+ const variant = variants.variant ?? "default";
105
+ const indicator = variants.indicator ?? "hidden";
69
106
  const cssVars = {
70
107
  "--uxm-radio-group-size": styles.size != null ? `${styles.size}px` : void 0,
71
108
  "--uxm-radio-group-gap": styles.gap != null ? `${styles.gap}px` : void 0,
@@ -80,7 +117,12 @@ function RadioGroupPreview({ styles, variants }) {
80
117
  "--uxm-radio-group-focus-ring": styles.focusRing,
81
118
  "--uxm-radio-group-disabled-opacity": styles.disabledOpacity != null ? String(styles.disabledOpacity) : void 0,
82
119
  "--uxm-radio-group-error-color": styles.errorColor,
83
- "--uxm-radio-group-error-message-size": styles.errorMessageSize != null ? `${styles.errorMessageSize}px` : void 0
120
+ "--uxm-radio-group-error-message-size": styles.errorMessageSize != null ? `${styles.errorMessageSize}px` : void 0,
121
+ "--uxm-radio-card-padding": `${styles.cardPadding}px`,
122
+ "--uxm-radio-card-radius": `${styles.cardRadius}px`,
123
+ "--uxm-radio-card-gap": `${styles.cardGap}px`,
124
+ "--uxm-radio-card-border-width": `${styles.cardBorderWidth}px`,
125
+ "--uxm-radio-card-bg": styles.cardBg
84
126
  };
85
127
  const sectionLabel = {
86
128
  fontSize: 11,
@@ -98,8 +140,8 @@ function RadioGroupPreview({ styles, variants }) {
98
140
  " state"
99
141
  ] }),
100
142
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
101
- /* @__PURE__ */ jsx(ShowcaseRow, { selected: false, state, styles, label: "Unselected" }),
102
- /* @__PURE__ */ jsx(ShowcaseRow, { selected: true, state, styles, label: "Selected" })
143
+ /* @__PURE__ */ jsx(ShowcaseRow, { selected: false, state, styles, label: "Unselected", variant, indicator }),
144
+ /* @__PURE__ */ jsx(ShowcaseRow, { selected: true, state, styles, label: "Selected", variant, indicator })
103
145
  ] })
104
146
  ] }),
105
147
  /* @__PURE__ */ jsxs("div", { style: { borderTop: "1px solid var(--color-border)", paddingTop: 16 }, children: [
@@ -109,6 +151,8 @@ function RadioGroupPreview({ styles, variants }) {
109
151
  {
110
152
  name: `uxm-radio-preview-${interactiveKey}`,
111
153
  direction,
154
+ variant,
155
+ indicator,
112
156
  error: state === "error" ? "Pick an option to continue" : void 0,
113
157
  children: INTERACTIVE_OPTIONS.map((opt) => /* @__PURE__ */ jsx(
114
158
  RadioOption,
@@ -116,7 +160,14 @@ function RadioGroupPreview({ styles, variants }) {
116
160
  name: `uxm-radio-preview-${interactiveKey}`,
117
161
  value: opt,
118
162
  disabled: interactiveDisabled,
119
- children: opt
163
+ children: variant === "card" ? (
164
+ // Card mode is for rich content — a one-word label in a tile
165
+ // would demo a box, not the shape the variant exists for.
166
+ /* @__PURE__ */ jsxs(Fragment, { children: [
167
+ /* @__PURE__ */ jsx("strong", { children: opt }),
168
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--color-text-muted)", fontSize: 12 }, children: "What this option does" })
169
+ ] })
170
+ ) : opt
120
171
  },
121
172
  opt
122
173
  ))