@jbpark/ui-kit 6.0.1 → 6.1.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.
package/dist/Button.d.mts CHANGED
@@ -11,31 +11,26 @@ interface Props extends Omit<NativeButtonProps, 'size' | 'variant' | 'type'> {
11
11
  disabled?: boolean;
12
12
  size?: 'small' | 'middle' | 'large';
13
13
  /**
14
- * @deprecated Use `variant` instead. `type` is an alias kept for backwards
15
- * compatibility and will be removed in the next major.
14
+ * Syntactic sugar for a `(color, variant)` pair. **Will follow `variant` and
15
+ * `color` if provided** — `type` only fills in the axes you leave unset.
16
16
  *
17
- * The two props address the same axis, and 3 of the 5 values (`dashed`,
18
- * `text`, `link`) are already spelled identically — `<Button type="text">`
19
- * and `<Button variant="text">` render the same thing. The only values
20
- * `type` contributes are the aliases `primary` → `solid` and
21
- * `default` → `outlined`. `variant` additionally offers `filled`, which
22
- * `type` cannot express.
17
+ * | `type` | equivalent to |
18
+ * | --------- | -------------------------------------- |
19
+ * | `primary` | `color="primary" variant="solid"` |
20
+ * | `default` | `color="default" variant="outlined"` |
21
+ * | `dashed` | `color="default" variant="dashed"` |
22
+ * | `text` | `color="default" variant="text"` |
23
+ * | `link` | `color="primary" variant="link"` |
23
24
  *
24
- * Migration:
25
- * | `type` | `variant` |
26
- * | --------- | ----------- |
27
- * | `primary` | `solid` |
28
- * | `default` | `outlined` |
29
- * | `dashed` | `dashed` |
30
- * | `text` | `text` |
31
- * | `link` | `link` |
32
- *
33
- * Passing both still lets `variant` win.
25
+ * So `<Button type="primary">` is the same as
26
+ * `<Button color="primary" variant="solid">`, and
27
+ * `<Button type="primary" variant="filled">` keeps the primary color but
28
+ * renders filled. `danger` still overrides the resolved color.
34
29
  */
35
30
  type?: 'primary' | 'default' | 'dashed' | 'text' | 'link';
36
31
  /**
37
- * Visual fill style. This is the canonical prop — prefer it over `type`.
38
- * Defaults to `'outlined'` (matching the legacy `type="default"`).
32
+ * Visual fill style, independent of `color`. Defaults to `'outlined'`.
33
+ * Takes precedence over whatever `type` would have supplied.
39
34
  */
40
35
  variant?: 'solid' | 'outlined' | 'dashed' | 'filled' | 'text' | 'link';
41
36
  /**
@@ -45,6 +40,10 @@ interface Props extends Omit<NativeButtonProps, 'size' | 'variant' | 'type'> {
45
40
  */
46
41
  htmlType?: 'button' | 'submit' | 'reset';
47
42
  shape?: 'default' | 'circle' | 'round';
43
+ /**
44
+ * Color, independent of `variant`. Defaults to `'default'`, or to whatever
45
+ * `type` maps to when `type` is set. `danger` overrides this.
46
+ */
48
47
  color?: PresetColors | 'default' | 'primary' | 'danger';
49
48
  loading?: boolean | {
50
49
  icon: React$1.ReactNode;
@@ -1 +1 @@
1
- {"version":3,"file":"Button.d.mts","names":[],"sources":["../src/components/atoms/button.tsx"],"mappings":";;KAYK,oBAAoB,QAAM;EAC7B;;KAGG;UAeY,cAAc,KAC7B;EAGA,OAAO,QAAM;EACb;EACA;EACA;EACA;;;;;;;;;;;;;;;;;;;;;;;EAuBA;;;;;EAKA;;;;;;EAMA;EACA;EACA,QAAQ;EACR;IAAsB,MAAM,QAAM;;;cA2E9B,WAAU,MAAA,WAAA,MAAA,SAAA,UAAA,MAAA,OAAA,OAAA,OAAA,UAAA,SAAA,QAAA,SAAA,UAAA,gBAAA,SAiBb,UAAK,QAAA,IAAA"}
1
+ {"version":3,"file":"Button.d.mts","names":[],"sources":["../src/components/atoms/button.tsx"],"mappings":";;KAYK,oBAAoB,QAAM;EAC7B;;KAGG;UAeY,cAAc,KAC7B;EAGA,OAAO,QAAM;EACb;EACA;EACA;EACA;;;;;;;;;;;;;;;;;;EAkBA;;;;;EAKA;;;;;;EAMA;EACA;;;;;EAKA,QAAQ;EACR;IAAsB,MAAM,QAAM;;;cAqF9B,WAAU,MAAA,WAAA,MAAA,SAAA,UAAA,MAAA,OAAA,OAAA,OAAA,UAAA,SAAA,QAAA,SAAA,UAAA,gBAAA,SAiBb,UAAK,QAAA,IAAA"}
package/dist/Button.mjs CHANGED
@@ -17,12 +17,34 @@ const variantClasses = {
17
17
  text: cn("bg-transparent text-foreground", "hover:bg-accent"),
18
18
  link: cn("bg-transparent text-primary", "underline-offset-4 hover:underline", "hover:bg-primary/10")
19
19
  };
20
- const typeToVariant = {
21
- primary: "solid",
22
- default: "outlined",
23
- dashed: "dashed",
24
- text: "text",
25
- link: "link"
20
+ /**
21
+ * antd-style syntactic sugar: each `type` expands to a `(color, variant)` pair.
22
+ * Explicit `color`/`variant` props win over whatever the `type` maps to, so
23
+ * `type` only fills in the axes the caller left unset.
24
+ *
25
+ * Mirrors antd: `<Button type="primary">` ≡ `<Button color="primary" variant="solid">`.
26
+ */
27
+ const typeToColorVariant = {
28
+ primary: {
29
+ color: "primary",
30
+ variant: "solid"
31
+ },
32
+ default: {
33
+ color: "default",
34
+ variant: "outlined"
35
+ },
36
+ dashed: {
37
+ color: "default",
38
+ variant: "dashed"
39
+ },
40
+ text: {
41
+ color: "default",
42
+ variant: "text"
43
+ },
44
+ link: {
45
+ color: "primary",
46
+ variant: "link"
47
+ }
26
48
  };
27
49
  const sizesClasses = {
28
50
  small: "h-6 px-3 text-sm",
@@ -39,15 +61,16 @@ const shapesClasses = {
39
61
  circle: "rounded-full",
40
62
  round: "rounded-2xl"
41
63
  };
42
- const Button = ({ icon, className, type, variant, htmlType = "button", size, color = "default", shape, block = false, disabled, loading, danger, asChild = false, children, onMouseDown, ...props }) => {
64
+ const Button = ({ icon, className, type, variant, htmlType = "button", size, color, shape, block = false, disabled, loading, danger, asChild = false, children, onMouseDown, ...props }) => {
43
65
  const { componentSize, defaultProps } = useConfig();
44
66
  const buttonDefaults = defaultProps?.button;
45
67
  const resolvedSize = size ?? componentSize ?? "middle";
46
68
  const resolvedShape = shape ?? buttonDefaults?.shape ?? "default";
47
69
  const iconOnly = icon && !children;
48
- const computedColor = danger ? "danger" : color;
49
- const colored = computedColor && computedColor !== "default";
50
- const resolvedVariant = variant ?? (type ? typeToVariant[type] : void 0) ?? "outlined";
70
+ const typeDefaults = type ? typeToColorVariant[type] : void 0;
71
+ const resolvedVariant = variant ?? typeDefaults?.variant ?? "outlined";
72
+ const computedColor = danger ? "danger" : color ?? typeDefaults?.color ?? "default";
73
+ const colored = computedColor !== "default";
51
74
  const isLoading = !!loading;
52
75
  const Comp = asChild ? Slot : "button";
53
76
  const displayIcon = loading ? typeof loading === "object" ? loading.icon : /* @__PURE__ */ jsx(LoaderCircle, { className: "animate-spin" }) : icon;
@@ -1 +1 @@
1
- {"version":3,"file":"Button.mjs","names":[],"sources":["../src/components/atoms/button.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\n\nimport { Slot } from '@radix-ui/react-slot';\nimport { LoaderCircle } from 'lucide-react';\n\nimport { useConfig } from '@repo/ui/providers';\nimport { cn } from '@repo/ui/utils';\n\nimport { INTERACTIVE_CHASSIS } from '../../lib/chassis';\n\ntype NativeButtonProps = React.ComponentProps<'button'> & {\n asChild?: boolean;\n};\n\ntype PresetColors =\n | 'blue'\n | 'purple'\n | 'cyan'\n | 'green'\n | 'magenta'\n | 'pink'\n | 'red'\n | 'orange'\n | 'yellow'\n | 'volcano'\n | 'geekblue'\n | 'lime'\n | 'gold';\n\nexport interface Props extends Omit<\n NativeButtonProps,\n 'size' | 'variant' | 'type'\n> {\n icon?: React.ReactNode;\n block?: boolean;\n danger?: boolean;\n disabled?: boolean;\n size?: 'small' | 'middle' | 'large';\n /**\n * @deprecated Use `variant` instead. `type` is an alias kept for backwards\n * compatibility and will be removed in the next major.\n *\n * The two props address the same axis, and 3 of the 5 values (`dashed`,\n * `text`, `link`) are already spelled identically — `<Button type=\"text\">`\n * and `<Button variant=\"text\">` render the same thing. The only values\n * `type` contributes are the aliases `primary` → `solid` and\n * `default` → `outlined`. `variant` additionally offers `filled`, which\n * `type` cannot express.\n *\n * Migration:\n * | `type` | `variant` |\n * | --------- | ----------- |\n * | `primary` | `solid` |\n * | `default` | `outlined` |\n * | `dashed` | `dashed` |\n * | `text` | `text` |\n * | `link` | `link` |\n *\n * Passing both still lets `variant` win.\n */\n type?: 'primary' | 'default' | 'dashed' | 'text' | 'link';\n /**\n * Visual fill style. This is the canonical prop — prefer it over `type`.\n * Defaults to `'outlined'` (matching the legacy `type=\"default\"`).\n */\n variant?: 'solid' | 'outlined' | 'dashed' | 'filled' | 'text' | 'link';\n /**\n * Native `<button>` `type` (`button`/`submit`/`reset`), kept separate\n * from the antd-style `type` above. Defaults to `'button'` so a Button\n * placed inside a `<form>` doesn't submit it unless explicitly opted in.\n */\n htmlType?: 'button' | 'submit' | 'reset';\n shape?: 'default' | 'circle' | 'round';\n color?: PresetColors | 'default' | 'primary' | 'danger';\n loading?: boolean | { icon: React.ReactNode };\n}\n\n// Chassis absorbed from core/button's cva base (#278 ③ / #294 Phase 4). The\n// shared focus/aria set lives in INTERACTIVE_CHASSIS; a Button additionally\n// disables and sizes its icons. Kept as constants so the absorb relocates this\n// treatment rather than dropping it (the #177 class of silent regression the\n// #300/#301 net guards).\nconst BUTTON_CHASSIS = cn(\n INTERACTIVE_CHASSIS,\n 'outline-none',\n 'disabled:pointer-events-none disabled:opacity-50',\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n);\n\n// Presentation the atom builds on. The size/variant/shape classes below still\n// override the incidental leftovers exactly as they did when this wrapped\n// core/button, so the rendered output is unchanged (verified against a\n// pre-refactor render matrix).\nconst BUTTON_BASE = cn(\n BUTTON_CHASSIS,\n 'gap-2 whitespace-nowrap font-medium transition-all',\n 'has-[>svg]:px-3',\n);\n\nconst variantClasses: Record<string, string> = {\n // solid used to inherit its fill from core/button's default variant; carry it\n // explicitly now that the primitive is absorbed. A colored solid overrides\n // this with the --btn-* custom properties further down.\n solid: cn('bg-primary text-primary-foreground', 'hover:bg-primary/90'),\n outlined: cn(\n 'border border-[color-mix(in_oklch,var(--btn-border),transparent_50%)]',\n 'bg-background text-foreground',\n 'hover:bg-accent',\n ),\n dashed: cn(\n 'border border-dashed border-[color-mix(in_oklch,var(--btn-border),transparent_50%)]',\n 'bg-background text-foreground',\n 'hover:bg-accent',\n ),\n filled: cn('bg-muted text-foreground', 'hover:bg-muted/80'),\n text: cn('bg-transparent text-foreground', 'hover:bg-accent'),\n link: cn(\n 'bg-transparent text-primary',\n 'underline-offset-4 hover:underline',\n 'hover:bg-primary/10',\n ),\n};\n\nconst typeToVariant: Record<string, string> = {\n primary: 'solid',\n default: 'outlined',\n dashed: 'dashed',\n text: 'text',\n link: 'link',\n};\n\nconst sizesClasses: Record<string, string> = {\n small: 'h-6 px-3 text-sm',\n middle: 'h-8 px-4 text-base',\n large: 'h-10 px-5 text-lg',\n};\n\nconst iconClasses: Record<string, string> = {\n small: `size-6 [&_svg:not([class*='size-'])]:size-3`,\n middle: `size-8 [&_svg:not([class*='size-'])]:size-4`,\n large: `size-10 [&_svg:not([class*='size-'])]:size-5`,\n};\n\nconst shapesClasses = {\n default: 'rounded-sm',\n circle: 'rounded-full',\n round: 'rounded-2xl',\n};\n\nconst Button = ({\n icon,\n className,\n type,\n variant,\n htmlType = 'button',\n size,\n color = 'default',\n shape,\n block = false,\n disabled,\n loading,\n danger,\n asChild = false,\n children,\n onMouseDown,\n ...props\n}: Props) => {\n const { componentSize, defaultProps } = useConfig();\n const buttonDefaults = defaultProps?.button as\n Partial<Pick<Props, 'shape'>> | undefined;\n const resolvedSize = size ?? componentSize ?? 'middle';\n const resolvedShape = shape ?? buttonDefaults?.shape ?? 'default';\n const iconOnly = icon && !children;\n const computedColor = danger ? 'danger' : color;\n const colored = computedColor && computedColor !== 'default';\n // `variant` is canonical; `type` is a deprecated alias resolved through\n // typeToVariant. The default lives here (not on the `type` parameter) so the\n // deprecated prop stays off the default path — a Button with neither prop\n // resolves straight to 'outlined'.\n const resolvedVariant =\n variant ?? (type ? typeToVariant[type] : undefined) ?? 'outlined';\n const isLoading = !!loading;\n\n const Comp = asChild ? Slot : 'button';\n\n const displayIcon = loading ? (\n typeof loading === 'object' ? (\n loading.icon\n ) : (\n <LoaderCircle className=\"animate-spin\" />\n )\n ) : (\n icon\n );\n\n return (\n <Comp\n data-slot=\"button\"\n className={cn(\n BUTTON_BASE,\n 'inline-flex items-center justify-center gap-x-2',\n 'rounded-lg',\n 'cursor-pointer',\n 'h-auto py-0',\n 'transition-all',\n variantClasses[resolvedVariant],\n sizesClasses[resolvedSize],\n iconOnly && ['p-0', iconClasses[resolvedSize]],\n shapesClasses[resolvedShape],\n block && 'w-full',\n colored &&\n (resolvedVariant === 'solid'\n ? [\n 'bg-(--btn-bg)',\n 'hover:bg-(--btn-bg-hover)',\n 'active:bg-(--btn-bg-active)',\n 'text-(--btn-fg)',\n 'border-(--btn-border)',\n ]\n : [\n 'text-(--btn-bg)',\n 'border-[color-mix(in_oklch,var(--btn-border),transparent_50%)]',\n //\n ]),\n className,\n //\n )}\n type={htmlType}\n disabled={disabled || isLoading}\n aria-busy={isLoading}\n data-color={computedColor}\n onMouseDown={e => {\n onMouseDown?.(e);\n }}\n {...props}\n >\n {displayIcon}\n {children}\n </Comp>\n );\n};\n\nexport default Button;\n"],"mappings":";;;;;;;;;AAoFA,MAAM,iBAAiB,GACrB,qBACA,gBACA,oDACA,mFACF;AAMA,MAAM,cAAc,GAClB,gBACA,sDACA,iBACF;AAEA,MAAM,iBAAyC;CAI7C,OAAO,GAAG,sCAAsC,qBAAqB;CACrE,UAAU,GACR,yEACA,iCACA,iBACF;CACA,QAAQ,GACN,uFACA,iCACA,iBACF;CACA,QAAQ,GAAG,4BAA4B,mBAAmB;CAC1D,MAAM,GAAG,kCAAkC,iBAAiB;CAC5D,MAAM,GACJ,+BACA,sCACA,qBACF;AACF;AAEA,MAAM,gBAAwC;CAC5C,SAAS;CACT,SAAS;CACT,QAAQ;CACR,MAAM;CACN,MAAM;AACR;AAEA,MAAM,eAAuC;CAC3C,OAAO;CACP,QAAQ;CACR,OAAO;AACT;AAEA,MAAM,cAAsC;CAC1C,OAAO;CACP,QAAQ;CACR,OAAO;AACT;AAEA,MAAM,gBAAgB;CACpB,SAAS;CACT,QAAQ;CACR,OAAO;AACT;AAEA,MAAM,UAAU,EACd,MACA,WACA,MACA,SACA,WAAW,UACX,MACA,QAAQ,WACR,OACA,QAAQ,OACR,UACA,SACA,QACA,UAAU,OACV,UACA,aACA,GAAG,YACQ;CACX,MAAM,EAAE,eAAe,iBAAiB,UAAU;CAClD,MAAM,iBAAiB,cAAc;CAErC,MAAM,eAAe,QAAQ,iBAAiB;CAC9C,MAAM,gBAAgB,SAAS,gBAAgB,SAAS;CACxD,MAAM,WAAW,QAAQ,CAAC;CAC1B,MAAM,gBAAgB,SAAS,WAAW;CAC1C,MAAM,UAAU,iBAAiB,kBAAkB;CAKnD,MAAM,kBACJ,YAAY,OAAO,cAAc,QAAQ,KAAA,MAAc;CACzD,MAAM,YAAY,CAAC,CAAC;CAEpB,MAAM,OAAO,UAAU,OAAO;CAE9B,MAAM,cAAc,UAClB,OAAO,YAAY,WACjB,QAAQ,OAER,oBAAC,cAAD,EAAc,WAAU,eAAgB,CAAA,IAG1C;CAGF,OACE,qBAAC,MAAD;EACE,aAAU;EACV,WAAW,GACT,aACA,mDACA,cACA,kBACA,eACA,kBACA,eAAe,kBACf,aAAa,eACb,YAAY,CAAC,OAAO,YAAY,aAAa,GAC7C,cAAc,gBACd,SAAS,UACT,YACG,oBAAoB,UACjB;GACE;GACA;GACA;GACA;GACA;EACF,IACA,CACE,mBACA,gEAEF,IACN,SAEF;EACA,MAAM;EACN,UAAU,YAAY;EACtB,aAAW;EACX,cAAY;EACZ,cAAa,MAAK;GAChB,cAAc,CAAC;EACjB;EACA,GAAI;EAtCN,UAAA,CAwCG,aACA,QACG;;AAEV"}
1
+ {"version":3,"file":"Button.mjs","names":[],"sources":["../src/components/atoms/button.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\n\nimport { Slot } from '@radix-ui/react-slot';\nimport { LoaderCircle } from 'lucide-react';\n\nimport { useConfig } from '@repo/ui/providers';\nimport { cn } from '@repo/ui/utils';\n\nimport { INTERACTIVE_CHASSIS } from '../../lib/chassis';\n\ntype NativeButtonProps = React.ComponentProps<'button'> & {\n asChild?: boolean;\n};\n\ntype PresetColors =\n | 'blue'\n | 'purple'\n | 'cyan'\n | 'green'\n | 'magenta'\n | 'pink'\n | 'red'\n | 'orange'\n | 'yellow'\n | 'volcano'\n | 'geekblue'\n | 'lime'\n | 'gold';\n\nexport interface Props extends Omit<\n NativeButtonProps,\n 'size' | 'variant' | 'type'\n> {\n icon?: React.ReactNode;\n block?: boolean;\n danger?: boolean;\n disabled?: boolean;\n size?: 'small' | 'middle' | 'large';\n /**\n * Syntactic sugar for a `(color, variant)` pair. **Will follow `variant` and\n * `color` if provided** — `type` only fills in the axes you leave unset.\n *\n * | `type` | equivalent to |\n * | --------- | -------------------------------------- |\n * | `primary` | `color=\"primary\" variant=\"solid\"` |\n * | `default` | `color=\"default\" variant=\"outlined\"` |\n * | `dashed` | `color=\"default\" variant=\"dashed\"` |\n * | `text` | `color=\"default\" variant=\"text\"` |\n * | `link` | `color=\"primary\" variant=\"link\"` |\n *\n * So `<Button type=\"primary\">` is the same as\n * `<Button color=\"primary\" variant=\"solid\">`, and\n * `<Button type=\"primary\" variant=\"filled\">` keeps the primary color but\n * renders filled. `danger` still overrides the resolved color.\n */\n type?: 'primary' | 'default' | 'dashed' | 'text' | 'link';\n /**\n * Visual fill style, independent of `color`. Defaults to `'outlined'`.\n * Takes precedence over whatever `type` would have supplied.\n */\n variant?: 'solid' | 'outlined' | 'dashed' | 'filled' | 'text' | 'link';\n /**\n * Native `<button>` `type` (`button`/`submit`/`reset`), kept separate\n * from the antd-style `type` above. Defaults to `'button'` so a Button\n * placed inside a `<form>` doesn't submit it unless explicitly opted in.\n */\n htmlType?: 'button' | 'submit' | 'reset';\n shape?: 'default' | 'circle' | 'round';\n /**\n * Color, independent of `variant`. Defaults to `'default'`, or to whatever\n * `type` maps to when `type` is set. `danger` overrides this.\n */\n color?: PresetColors | 'default' | 'primary' | 'danger';\n loading?: boolean | { icon: React.ReactNode };\n}\n\n// Chassis absorbed from core/button's cva base (#278 ③ / #294 Phase 4). The\n// shared focus/aria set lives in INTERACTIVE_CHASSIS; a Button additionally\n// disables and sizes its icons. Kept as constants so the absorb relocates this\n// treatment rather than dropping it (the #177 class of silent regression the\n// #300/#301 net guards).\nconst BUTTON_CHASSIS = cn(\n INTERACTIVE_CHASSIS,\n 'outline-none',\n 'disabled:pointer-events-none disabled:opacity-50',\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n);\n\n// Presentation the atom builds on. The size/variant/shape classes below still\n// override the incidental leftovers exactly as they did when this wrapped\n// core/button, so the rendered output is unchanged (verified against a\n// pre-refactor render matrix).\nconst BUTTON_BASE = cn(\n BUTTON_CHASSIS,\n 'gap-2 whitespace-nowrap font-medium transition-all',\n 'has-[>svg]:px-3',\n);\n\nconst variantClasses: Record<string, string> = {\n // solid used to inherit its fill from core/button's default variant; carry it\n // explicitly now that the primitive is absorbed. A colored solid overrides\n // this with the --btn-* custom properties further down.\n solid: cn('bg-primary text-primary-foreground', 'hover:bg-primary/90'),\n outlined: cn(\n 'border border-[color-mix(in_oklch,var(--btn-border),transparent_50%)]',\n 'bg-background text-foreground',\n 'hover:bg-accent',\n ),\n dashed: cn(\n 'border border-dashed border-[color-mix(in_oklch,var(--btn-border),transparent_50%)]',\n 'bg-background text-foreground',\n 'hover:bg-accent',\n ),\n filled: cn('bg-muted text-foreground', 'hover:bg-muted/80'),\n text: cn('bg-transparent text-foreground', 'hover:bg-accent'),\n link: cn(\n 'bg-transparent text-primary',\n 'underline-offset-4 hover:underline',\n 'hover:bg-primary/10',\n ),\n};\n\n/**\n * antd-style syntactic sugar: each `type` expands to a `(color, variant)` pair.\n * Explicit `color`/`variant` props win over whatever the `type` maps to, so\n * `type` only fills in the axes the caller left unset.\n *\n * Mirrors antd: `<Button type=\"primary\">` ≡ `<Button color=\"primary\" variant=\"solid\">`.\n */\nconst typeToColorVariant: Record<\n NonNullable<Props['type']>,\n { color: NonNullable<Props['color']>; variant: NonNullable<Props['variant']> }\n> = {\n primary: { color: 'primary', variant: 'solid' },\n default: { color: 'default', variant: 'outlined' },\n dashed: { color: 'default', variant: 'dashed' },\n text: { color: 'default', variant: 'text' },\n link: { color: 'primary', variant: 'link' },\n};\n\nconst sizesClasses: Record<string, string> = {\n small: 'h-6 px-3 text-sm',\n middle: 'h-8 px-4 text-base',\n large: 'h-10 px-5 text-lg',\n};\n\nconst iconClasses: Record<string, string> = {\n small: `size-6 [&_svg:not([class*='size-'])]:size-3`,\n middle: `size-8 [&_svg:not([class*='size-'])]:size-4`,\n large: `size-10 [&_svg:not([class*='size-'])]:size-5`,\n};\n\nconst shapesClasses = {\n default: 'rounded-sm',\n circle: 'rounded-full',\n round: 'rounded-2xl',\n};\n\nconst Button = ({\n icon,\n className,\n type,\n variant,\n htmlType = 'button',\n size,\n color,\n shape,\n block = false,\n disabled,\n loading,\n danger,\n asChild = false,\n children,\n onMouseDown,\n ...props\n}: Props) => {\n const { componentSize, defaultProps } = useConfig();\n const buttonDefaults = defaultProps?.button as\n Partial<Pick<Props, 'shape'>> | undefined;\n const resolvedSize = size ?? componentSize ?? 'middle';\n const resolvedShape = shape ?? buttonDefaults?.shape ?? 'default';\n const iconOnly = icon && !children;\n // `type` is syntactic sugar that expands to a (color, variant) pair; explicit\n // `color`/`variant` win over it, so `type` only fills the axes left unset.\n // `danger` still trumps everything, matching antd. Defaults live here rather\n // than on the parameters so `type` gets a chance to supply them first.\n const typeDefaults = type ? typeToColorVariant[type] : undefined;\n const resolvedVariant = variant ?? typeDefaults?.variant ?? 'outlined';\n const computedColor = danger\n ? 'danger'\n : (color ?? typeDefaults?.color ?? 'default');\n const colored = computedColor !== 'default';\n const isLoading = !!loading;\n\n const Comp = asChild ? Slot : 'button';\n\n const displayIcon = loading ? (\n typeof loading === 'object' ? (\n loading.icon\n ) : (\n <LoaderCircle className=\"animate-spin\" />\n )\n ) : (\n icon\n );\n\n return (\n <Comp\n data-slot=\"button\"\n className={cn(\n BUTTON_BASE,\n 'inline-flex items-center justify-center gap-x-2',\n 'rounded-lg',\n 'cursor-pointer',\n 'h-auto py-0',\n 'transition-all',\n variantClasses[resolvedVariant],\n sizesClasses[resolvedSize],\n iconOnly && ['p-0', iconClasses[resolvedSize]],\n shapesClasses[resolvedShape],\n block && 'w-full',\n colored &&\n (resolvedVariant === 'solid'\n ? [\n 'bg-(--btn-bg)',\n 'hover:bg-(--btn-bg-hover)',\n 'active:bg-(--btn-bg-active)',\n 'text-(--btn-fg)',\n 'border-(--btn-border)',\n ]\n : [\n 'text-(--btn-bg)',\n 'border-[color-mix(in_oklch,var(--btn-border),transparent_50%)]',\n //\n ]),\n className,\n //\n )}\n type={htmlType}\n disabled={disabled || isLoading}\n aria-busy={isLoading}\n data-color={computedColor}\n onMouseDown={e => {\n onMouseDown?.(e);\n }}\n {...props}\n >\n {displayIcon}\n {children}\n </Comp>\n );\n};\n\nexport default Button;\n"],"mappings":";;;;;;;;;AAmFA,MAAM,iBAAiB,GACrB,qBACA,gBACA,oDACA,mFACF;AAMA,MAAM,cAAc,GAClB,gBACA,sDACA,iBACF;AAEA,MAAM,iBAAyC;CAI7C,OAAO,GAAG,sCAAsC,qBAAqB;CACrE,UAAU,GACR,yEACA,iCACA,iBACF;CACA,QAAQ,GACN,uFACA,iCACA,iBACF;CACA,QAAQ,GAAG,4BAA4B,mBAAmB;CAC1D,MAAM,GAAG,kCAAkC,iBAAiB;CAC5D,MAAM,GACJ,+BACA,sCACA,qBACF;AACF;;;;;;;;AASA,MAAM,qBAGF;CACF,SAAS;EAAE,OAAO;EAAW,SAAS;CAAQ;CAC9C,SAAS;EAAE,OAAO;EAAW,SAAS;CAAW;CACjD,QAAQ;EAAE,OAAO;EAAW,SAAS;CAAS;CAC9C,MAAM;EAAE,OAAO;EAAW,SAAS;CAAO;CAC1C,MAAM;EAAE,OAAO;EAAW,SAAS;CAAO;AAC5C;AAEA,MAAM,eAAuC;CAC3C,OAAO;CACP,QAAQ;CACR,OAAO;AACT;AAEA,MAAM,cAAsC;CAC1C,OAAO;CACP,QAAQ;CACR,OAAO;AACT;AAEA,MAAM,gBAAgB;CACpB,SAAS;CACT,QAAQ;CACR,OAAO;AACT;AAEA,MAAM,UAAU,EACd,MACA,WACA,MACA,SACA,WAAW,UACX,MACA,OACA,OACA,QAAQ,OACR,UACA,SACA,QACA,UAAU,OACV,UACA,aACA,GAAG,YACQ;CACX,MAAM,EAAE,eAAe,iBAAiB,UAAU;CAClD,MAAM,iBAAiB,cAAc;CAErC,MAAM,eAAe,QAAQ,iBAAiB;CAC9C,MAAM,gBAAgB,SAAS,gBAAgB,SAAS;CACxD,MAAM,WAAW,QAAQ,CAAC;CAK1B,MAAM,eAAe,OAAO,mBAAmB,QAAQ,KAAA;CACvD,MAAM,kBAAkB,WAAW,cAAc,WAAW;CAC5D,MAAM,gBAAgB,SAClB,WACC,SAAS,cAAc,SAAS;CACrC,MAAM,UAAU,kBAAkB;CAClC,MAAM,YAAY,CAAC,CAAC;CAEpB,MAAM,OAAO,UAAU,OAAO;CAE9B,MAAM,cAAc,UAClB,OAAO,YAAY,WACjB,QAAQ,OAER,oBAAC,cAAD,EAAc,WAAU,eAAgB,CAAA,IAG1C;CAGF,OACE,qBAAC,MAAD;EACE,aAAU;EACV,WAAW,GACT,aACA,mDACA,cACA,kBACA,eACA,kBACA,eAAe,kBACf,aAAa,eACb,YAAY,CAAC,OAAO,YAAY,aAAa,GAC7C,cAAc,gBACd,SAAS,UACT,YACG,oBAAoB,UACjB;GACE;GACA;GACA;GACA;GACA;EACF,IACA,CACE,mBACA,gEAEF,IACN,SAEF;EACA,MAAM;EACN,UAAU,YAAY;EACtB,aAAW;EACX,cAAY;EACZ,cAAa,MAAK;GAChB,cAAc,CAAC;EACjB;EACA,GAAI;EAtCN,UAAA,CAwCG,aACA,QACG;;AAEV"}
@@ -19,11 +19,11 @@ const Group = ({ orientation = "vertical", placement = "left", className, classN
19
19
  };
20
20
  return /* @__PURE__ */ jsx("ul", {
21
21
  ...props,
22
- className: cn("m-0 list-none p-0", orientation === "vertical" ? "space-y-2" : "flex gap-2", className),
22
+ className: cn("m-0 flex list-none gap-2 p-0", orientation === "vertical" && "flex-col", className),
23
23
  children: options.map((item, index) => {
24
24
  const checked = value.includes(item.value);
25
25
  return /* @__PURE__ */ jsx("li", {
26
- className: cn("flex", classNames?.wrapper),
26
+ className: cn("m-0 flex", classNames?.wrapper),
27
27
  children: /* @__PURE__ */ jsx(Checkbox, {
28
28
  placement,
29
29
  className: cn(classNames?.item),
@@ -1 +1 @@
1
- {"version":3,"file":"group.mjs","names":[],"sources":["../../../../src/components/atoms/checkbox/group.tsx"],"sourcesContent":["'use client';\n\nimport { useControllableState } from '@jbpark/use-hooks';\n\nimport { cn } from '@repo/ui/utils';\n\nimport {\n type Option,\n type OptionGroupClassNames,\n type OptionValue,\n type Options,\n normalizeOptions,\n warnOnDuplicateOptionValues,\n} from '../option-group';\nimport Checkbox from './checkbox';\n\nexport type { OptionValue };\n\nexport interface Props extends Omit<\n React.ComponentPropsWithoutRef<'ul'>,\n 'onChange' | 'defaultValue' | 'value'\n> {\n options?: Options;\n orientation?: 'vertical' | 'horizontal';\n placement?: 'left' | 'right';\n classNames?: OptionGroupClassNames;\n defaultValue?: OptionValue[];\n value?: OptionValue[];\n /** Native form field name, shared across every option's checkbox. */\n name?: string;\n disabled?: boolean;\n onChange?: (values: OptionValue[]) => void;\n}\n\nconst Group = ({\n orientation = 'vertical',\n placement = 'left',\n className,\n classNames = {},\n options: _options = [],\n name,\n disabled,\n defaultValue,\n value: _value,\n onChange: _onChange = () => {},\n ...props\n}: Props) => {\n const [value, setValue] = useControllableState<OptionValue[]>({\n value: _value,\n defaultValue: defaultValue ?? [],\n onChange: _onChange,\n });\n\n const options: Option[] = normalizeOptions(_options);\n warnOnDuplicateOptionValues(options, 'Checkbox.Group');\n\n const onChange = (checked: boolean, optionValue: OptionValue) => {\n const nextValue = checked\n ? [...value, optionValue]\n : value.filter(v => v !== optionValue);\n\n setValue(nextValue);\n };\n\n return (\n <ul\n {...props}\n className={cn(\n 'm-0 list-none p-0',\n orientation === 'vertical' ? 'space-y-2' : 'flex gap-2',\n className,\n )}\n >\n {options.map((item: Option, index) => {\n const checked = value.includes(item.value);\n\n return (\n <li\n key={`${index}-${String(item.value)}`}\n className={cn('flex', classNames?.wrapper)}\n >\n <Checkbox\n placement={placement}\n className={cn(classNames?.item)}\n name={name}\n value={item.value}\n checked={checked}\n disabled={disabled || item.disabled}\n onChange={checked => onChange(checked, item.value)}\n >\n {item.label}\n </Checkbox>\n </li>\n );\n })}\n </ul>\n );\n};\n\nexport default Group;\n"],"mappings":";;;;;;;AAkCA,MAAM,SAAS,EACb,cAAc,YACd,YAAY,QACZ,WACA,aAAa,CAAC,GACd,SAAS,WAAW,CAAC,GACrB,MACA,UACA,cACA,OAAO,QACP,UAAU,kBAAkB,CAAC,GAC7B,GAAG,YACQ;CACX,MAAM,CAAC,OAAO,YAAY,qBAAoC;EAC5D,OAAO;EACP,cAAc,gBAAgB,CAAC;EAC/B,UAAU;CACZ,CAAC;CAED,MAAM,UAAoB,iBAAiB,QAAQ;CACnD,4BAA4B,SAAS,gBAAgB;CAErD,MAAM,YAAY,SAAkB,gBAA6B;EAC/D,MAAM,YAAY,UACd,CAAC,GAAG,OAAO,WAAW,IACtB,MAAM,QAAO,MAAK,MAAM,WAAW;EAEvC,SAAS,SAAS;CACpB;CAEA,OACE,oBAAC,MAAD;EACE,GAAI;EACJ,WAAW,GACT,qBACA,gBAAgB,aAAa,cAAc,cAC3C,SACF;EAEC,UAAA,QAAQ,KAAK,MAAc,UAAU;GACpC,MAAM,UAAU,MAAM,SAAS,KAAK,KAAK;GAEzC,OACE,oBAAC,MAAD;IAEE,WAAW,GAAG,QAAQ,YAAY,OAAO;IAEzC,UAAA,oBAAC,UAAD;KACa;KACX,WAAW,GAAG,YAAY,IAAI;KACxB;KACN,OAAO,KAAK;KACH;KACT,UAAU,YAAY,KAAK;KAC3B,WAAU,YAAW,SAAS,SAAS,KAAK,KAAK;KAEhD,UAAA,KAAK;IACE,CAAA;GACR,GAdG,GAAG,MAAM,GAAG,OAAO,KAAK,KAAK,GAchC;EAER,CAAC;CACC,CAAA;AAER"}
1
+ {"version":3,"file":"group.mjs","names":[],"sources":["../../../../src/components/atoms/checkbox/group.tsx"],"sourcesContent":["'use client';\n\nimport { useControllableState } from '@jbpark/use-hooks';\n\nimport { cn } from '@repo/ui/utils';\n\nimport {\n type Option,\n type OptionGroupClassNames,\n type OptionValue,\n type Options,\n normalizeOptions,\n warnOnDuplicateOptionValues,\n} from '../option-group';\nimport Checkbox from './checkbox';\n\nexport type { OptionValue };\n\nexport interface Props extends Omit<\n React.ComponentPropsWithoutRef<'ul'>,\n 'onChange' | 'defaultValue' | 'value'\n> {\n options?: Options;\n orientation?: 'vertical' | 'horizontal';\n placement?: 'left' | 'right';\n classNames?: OptionGroupClassNames;\n defaultValue?: OptionValue[];\n value?: OptionValue[];\n /** Native form field name, shared across every option's checkbox. */\n name?: string;\n disabled?: boolean;\n onChange?: (values: OptionValue[]) => void;\n}\n\nconst Group = ({\n orientation = 'vertical',\n placement = 'left',\n className,\n classNames = {},\n options: _options = [],\n name,\n disabled,\n defaultValue,\n value: _value,\n onChange: _onChange = () => {},\n ...props\n}: Props) => {\n const [value, setValue] = useControllableState<OptionValue[]>({\n value: _value,\n defaultValue: defaultValue ?? [],\n onChange: _onChange,\n });\n\n const options: Option[] = normalizeOptions(_options);\n warnOnDuplicateOptionValues(options, 'Checkbox.Group');\n\n const onChange = (checked: boolean, optionValue: OptionValue) => {\n const nextValue = checked\n ? [...value, optionValue]\n : value.filter(v => v !== optionValue);\n\n setValue(nextValue);\n };\n\n return (\n <ul\n {...props}\n className={cn(\n // Spacing comes from `gap`, not `space-y`'s child margins, so the\n // per-item `m-0` reset below can neutralise host list styles without\n // also wiping out the group's own spacing.\n 'm-0 flex list-none gap-2 p-0',\n orientation === 'vertical' && 'flex-col',\n className,\n )}\n >\n {options.map((item: Option, index) => {\n const checked = value.includes(item.value);\n\n return (\n <li\n key={`${index}-${String(item.value)}`}\n // `m-0` completes the list reset the `ul` above starts (#253) —\n // see Radio.Group for the host rule this guards against.\n className={cn('m-0 flex', classNames?.wrapper)}\n >\n <Checkbox\n placement={placement}\n className={cn(classNames?.item)}\n name={name}\n value={item.value}\n checked={checked}\n disabled={disabled || item.disabled}\n onChange={checked => onChange(checked, item.value)}\n >\n {item.label}\n </Checkbox>\n </li>\n );\n })}\n </ul>\n );\n};\n\nexport default Group;\n"],"mappings":";;;;;;;AAkCA,MAAM,SAAS,EACb,cAAc,YACd,YAAY,QACZ,WACA,aAAa,CAAC,GACd,SAAS,WAAW,CAAC,GACrB,MACA,UACA,cACA,OAAO,QACP,UAAU,kBAAkB,CAAC,GAC7B,GAAG,YACQ;CACX,MAAM,CAAC,OAAO,YAAY,qBAAoC;EAC5D,OAAO;EACP,cAAc,gBAAgB,CAAC;EAC/B,UAAU;CACZ,CAAC;CAED,MAAM,UAAoB,iBAAiB,QAAQ;CACnD,4BAA4B,SAAS,gBAAgB;CAErD,MAAM,YAAY,SAAkB,gBAA6B;EAC/D,MAAM,YAAY,UACd,CAAC,GAAG,OAAO,WAAW,IACtB,MAAM,QAAO,MAAK,MAAM,WAAW;EAEvC,SAAS,SAAS;CACpB;CAEA,OACE,oBAAC,MAAD;EACE,GAAI;EACJ,WAAW,GAIT,gCACA,gBAAgB,cAAc,YAC9B,SACF;EAEC,UAAA,QAAQ,KAAK,MAAc,UAAU;GACpC,MAAM,UAAU,MAAM,SAAS,KAAK,KAAK;GAEzC,OACE,oBAAC,MAAD;IAIE,WAAW,GAAG,YAAY,YAAY,OAAO;IAE7C,UAAA,oBAAC,UAAD;KACa;KACX,WAAW,GAAG,YAAY,IAAI;KACxB;KACN,OAAO,KAAK;KACH;KACT,UAAU,YAAY,KAAK;KAC3B,WAAU,YAAW,SAAS,SAAS,KAAK,KAAK;KAEhD,UAAA,KAAK;IACE,CAAA;GACR,GAhBG,GAAG,MAAM,GAAG,OAAO,KAAK,KAAK,GAgBhC;EAER,CAAC;CACC,CAAA;AAER"}
@@ -37,7 +37,7 @@ const RadioGroup = ({ orientation = "horizontal", placement = "left", className,
37
37
  children: options.map((item, index) => {
38
38
  const checked = value === item.value;
39
39
  return /* @__PURE__ */ jsx("li", {
40
- className: cn("flex", classNames?.wrapper),
40
+ className: cn("m-0 flex", classNames?.wrapper),
41
41
  children: isButton ? /* @__PURE__ */ jsx(Item, {
42
42
  asChild: true,
43
43
  value: String(item.value),
@@ -1 +1 @@
1
- {"version":3,"file":"group.mjs","names":["radio"],"sources":["../../../../src/components/atoms/radio/group.tsx"],"sourcesContent":["'use client';\n\nimport { useId } from 'react';\n\nimport { useControllableState } from '@jbpark/use-hooks';\n\nimport { cn } from '@repo/ui/utils';\n\nimport { radio } from '../../../core';\nimport Button from '../button';\nimport {\n type Option,\n type OptionGroupClassNames,\n type OptionValue,\n type Options,\n normalizeOptions,\n warnOnDuplicateOptionValues,\n} from '../option-group';\nimport { RadioGroupContext } from './context';\nimport Radio from './radio';\n\nconst { RadioGroup: Core, RadioGroupItem: Item } = radio;\n\nexport type { OptionValue };\n\nexport interface Props extends Omit<\n React.ComponentPropsWithoutRef<'ul'>,\n 'onChange' | 'defaultValue' | 'value'\n> {\n options?: Options;\n orientation?: 'vertical' | 'horizontal';\n placement?: 'left' | 'right';\n classNames?: OptionGroupClassNames;\n defaultValue?: OptionValue;\n value?: OptionValue;\n /** Native form field name — participates in `FormData` when set. */\n name?: string;\n optionType?: 'default' | 'button';\n buttonStyle?: 'solid' | 'outlined';\n size?: 'small' | 'middle' | 'large';\n disabled?: boolean;\n onChange?: (value: OptionValue) => void;\n}\n\nconst RadioGroup = ({\n orientation = 'horizontal',\n placement = 'left',\n className,\n classNames = {},\n options: _options = [],\n name,\n optionType = 'default',\n buttonStyle = 'solid',\n size,\n disabled,\n defaultValue,\n value: _value,\n onChange: _onChange = () => {},\n ...props\n}: Props) => {\n const [value, setValue] = useControllableState<OptionValue>({\n value: _value,\n defaultValue,\n onChange: _onChange,\n });\n\n const options: Option[] = normalizeOptions(_options);\n warnOnDuplicateOptionValues(options, 'Radio.Group');\n\n const isButton = optionType === 'button';\n\n const onChange = (checked: boolean, optionValue: OptionValue) => {\n if (!checked) {\n return;\n }\n setValue(optionValue);\n };\n\n // A single Radix radiogroup root owns every option so that arrow keys move\n // focus across the whole group. `id` stays index-derived (for DOM\n // id/label pairing), but Radix itself now addresses/submits items by\n // their real (stringified) `value` — see radio.tsx.\n const baseId = useId();\n const getOptionId = (index: number) => `${baseId}-${index}`;\n const checkedItemValue = value === undefined ? '' : String(value);\n\n const onValueChange = (itemValue: string) => {\n const item = options.find(option => String(option.value) === itemValue);\n\n if (!item) {\n return;\n }\n\n onChange(true, item.value);\n };\n\n const list = (\n <ul\n {...props}\n className={cn(\n 'm-0 flex list-none gap-4 p-0',\n className,\n orientation === 'vertical' && 'flex-col',\n isButton && 'gap-0',\n )}\n >\n {options.map((item: Option, index) => {\n const checked = value === item.value;\n\n return (\n <li\n key={`${index}-${String(item.value)}`}\n className={cn('flex', classNames?.wrapper)}\n >\n {isButton ? (\n // `asChild` gives the Button real radio semantics (role,\n // aria-checked, roving tabindex, arrow-key nav, click/keyboard\n // selection via the shared Core's onValueChange below) instead\n // of a plain list of buttons with none of that.\n <Item\n asChild\n value={String(item.value)}\n disabled={disabled || item.disabled}\n >\n <Button\n variant={checked ? buttonStyle : 'outlined'}\n size={size}\n disabled={disabled || item.disabled}\n className={cn(\n // The `solid` variant (the checked item, by default)\n // renders no border at all while `outlined` (every\n // unchecked item) renders a 1px one — reserving the\n // border unconditionally keeps every option's box the\n // same size regardless of which variant it's in.\n 'rounded-none border',\n checked &&\n (buttonStyle === 'outlined'\n ? 'border-primary'\n : 'border-transparent'),\n index === 0 &&\n (orientation === 'vertical'\n ? 'rounded-t-lg'\n : 'rounded-l-lg'),\n index === options.length - 1 &&\n (orientation === 'vertical'\n ? 'rounded-b-lg'\n : 'rounded-r-lg'),\n )}\n >\n {item.label}\n </Button>\n </Item>\n ) : (\n <Radio\n id={getOptionId(index)}\n placement={placement}\n className={cn(classNames?.item)}\n value={item.value}\n checked={checked}\n disabled={disabled || item.disabled}\n onChange={checked => onChange(checked, item.value)}\n >\n {item.label}\n </Radio>\n )}\n </li>\n );\n })}\n </ul>\n );\n\n return (\n <RadioGroupContext.Provider value={true}>\n <Core\n className=\"block\"\n name={name}\n value={checkedItemValue}\n onValueChange={onValueChange}\n >\n {list}\n </Core>\n </RadioGroupContext.Provider>\n );\n};\n\nexport default RadioGroup;\n"],"mappings":";;;;;;;;;;;AAqBA,MAAM,EAAE,YAAY,MAAM,gBAAgB,SAASA;AAuBnD,MAAM,cAAc,EAClB,cAAc,cACd,YAAY,QACZ,WACA,aAAa,CAAC,GACd,SAAS,WAAW,CAAC,GACrB,MACA,aAAa,WACb,cAAc,SACd,MACA,UACA,cACA,OAAO,QACP,UAAU,kBAAkB,CAAC,GAC7B,GAAG,YACQ;CACX,MAAM,CAAC,OAAO,YAAY,qBAAkC;EAC1D,OAAO;EACP;EACA,UAAU;CACZ,CAAC;CAED,MAAM,UAAoB,iBAAiB,QAAQ;CACnD,4BAA4B,SAAS,aAAa;CAElD,MAAM,WAAW,eAAe;CAEhC,MAAM,YAAY,SAAkB,gBAA6B;EAC/D,IAAI,CAAC,SACH;EAEF,SAAS,WAAW;CACtB;CAMA,MAAM,SAAS,MAAM;CACrB,MAAM,eAAe,UAAkB,GAAG,OAAO,GAAG;CACpD,MAAM,mBAAmB,UAAU,KAAA,IAAY,KAAK,OAAO,KAAK;CAEhE,MAAM,iBAAiB,cAAsB;EAC3C,MAAM,OAAO,QAAQ,MAAK,WAAU,OAAO,OAAO,KAAK,MAAM,SAAS;EAEtE,IAAI,CAAC,MACH;EAGF,SAAS,MAAM,KAAK,KAAK;CAC3B;CAEA,MAAM,OACJ,oBAAC,MAAD;EACE,GAAI;EACJ,WAAW,GACT,gCACA,WACA,gBAAgB,cAAc,YAC9B,YAAY,OACd;EAEC,UAAA,QAAQ,KAAK,MAAc,UAAU;GACpC,MAAM,UAAU,UAAU,KAAK;GAE/B,OACE,oBAAC,MAAD;IAEE,WAAW,GAAG,QAAQ,YAAY,OAAO;IAExC,UAAA,WAKC,oBAAC,MAAD;KACE,SAAA;KACA,OAAO,OAAO,KAAK,KAAK;KACxB,UAAU,YAAY,KAAK;KAE3B,UAAA,oBAAC,QAAD;MACE,SAAS,UAAU,cAAc;MAC3B;MACN,UAAU,YAAY,KAAK;MAC3B,WAAW,GAMT,uBACA,YACG,gBAAgB,aACb,mBACA,uBACN,UAAU,MACP,gBAAgB,aACb,iBACA,iBACN,UAAU,QAAQ,SAAS,MACxB,gBAAgB,aACb,iBACA,eACR;MAEC,UAAA,KAAK;KACA,CAAA;IACJ,CAAA,IAEN,oBAAC,OAAD;KACE,IAAI,YAAY,KAAK;KACV;KACX,WAAW,GAAG,YAAY,IAAI;KAC9B,OAAO,KAAK;KACH;KACT,UAAU,YAAY,KAAK;KAC3B,WAAU,YAAW,SAAS,SAAS,KAAK,KAAK;KAEhD,UAAA,KAAK;IACD,CAAA;GAEP,GAtDG,GAAG,MAAM,GAAG,OAAO,KAAK,KAAK,GAsDhC;EAER,CAAC;CACC,CAAA;CAGN,OACE,oBAAC,kBAAkB,UAAnB;EAA4B,OAAO;EACjC,UAAA,oBAAC,MAAD;GACE,WAAU;GACJ;GACN,OAAO;GACQ;GAEd,UAAA;EACG,CAAA;CACoB,CAAA;AAEhC"}
1
+ {"version":3,"file":"group.mjs","names":["radio"],"sources":["../../../../src/components/atoms/radio/group.tsx"],"sourcesContent":["'use client';\n\nimport { useId } from 'react';\n\nimport { useControllableState } from '@jbpark/use-hooks';\n\nimport { cn } from '@repo/ui/utils';\n\nimport { radio } from '../../../core';\nimport Button from '../button';\nimport {\n type Option,\n type OptionGroupClassNames,\n type OptionValue,\n type Options,\n normalizeOptions,\n warnOnDuplicateOptionValues,\n} from '../option-group';\nimport { RadioGroupContext } from './context';\nimport Radio from './radio';\n\nconst { RadioGroup: Core, RadioGroupItem: Item } = radio;\n\nexport type { OptionValue };\n\nexport interface Props extends Omit<\n React.ComponentPropsWithoutRef<'ul'>,\n 'onChange' | 'defaultValue' | 'value'\n> {\n options?: Options;\n orientation?: 'vertical' | 'horizontal';\n placement?: 'left' | 'right';\n classNames?: OptionGroupClassNames;\n defaultValue?: OptionValue;\n value?: OptionValue;\n /** Native form field name — participates in `FormData` when set. */\n name?: string;\n optionType?: 'default' | 'button';\n buttonStyle?: 'solid' | 'outlined';\n size?: 'small' | 'middle' | 'large';\n disabled?: boolean;\n onChange?: (value: OptionValue) => void;\n}\n\nconst RadioGroup = ({\n orientation = 'horizontal',\n placement = 'left',\n className,\n classNames = {},\n options: _options = [],\n name,\n optionType = 'default',\n buttonStyle = 'solid',\n size,\n disabled,\n defaultValue,\n value: _value,\n onChange: _onChange = () => {},\n ...props\n}: Props) => {\n const [value, setValue] = useControllableState<OptionValue>({\n value: _value,\n defaultValue,\n onChange: _onChange,\n });\n\n const options: Option[] = normalizeOptions(_options);\n warnOnDuplicateOptionValues(options, 'Radio.Group');\n\n const isButton = optionType === 'button';\n\n const onChange = (checked: boolean, optionValue: OptionValue) => {\n if (!checked) {\n return;\n }\n setValue(optionValue);\n };\n\n // A single Radix radiogroup root owns every option so that arrow keys move\n // focus across the whole group. `id` stays index-derived (for DOM\n // id/label pairing), but Radix itself now addresses/submits items by\n // their real (stringified) `value` — see radio.tsx.\n const baseId = useId();\n const getOptionId = (index: number) => `${baseId}-${index}`;\n const checkedItemValue = value === undefined ? '' : String(value);\n\n const onValueChange = (itemValue: string) => {\n const item = options.find(option => String(option.value) === itemValue);\n\n if (!item) {\n return;\n }\n\n onChange(true, item.value);\n };\n\n const list = (\n <ul\n {...props}\n className={cn(\n 'm-0 flex list-none gap-4 p-0',\n className,\n orientation === 'vertical' && 'flex-col',\n isButton && 'gap-0',\n )}\n >\n {options.map((item: Option, index) => {\n const checked = value === item.value;\n\n return (\n <li\n key={`${index}-${String(item.value)}`}\n // `m-0` completes the list reset the `ul` above starts (#253):\n // without preflight, hosts that style prose lists still reach the\n // items — Docusaurus/Infima's `.markdown li + li` gives every item\n // but the first a `margin-top`, which in horizontal orientation\n // drops options 2..n by that much and stretches the first one,\n // making it look misaligned.\n className={cn('m-0 flex', classNames?.wrapper)}\n >\n {isButton ? (\n // `asChild` gives the Button real radio semantics (role,\n // aria-checked, roving tabindex, arrow-key nav, click/keyboard\n // selection via the shared Core's onValueChange below) instead\n // of a plain list of buttons with none of that.\n <Item\n asChild\n value={String(item.value)}\n disabled={disabled || item.disabled}\n >\n <Button\n variant={checked ? buttonStyle : 'outlined'}\n size={size}\n disabled={disabled || item.disabled}\n className={cn(\n // The `solid` variant (the checked item, by default)\n // renders no border at all while `outlined` (every\n // unchecked item) renders a 1px one — reserving the\n // border unconditionally keeps every option's box the\n // same size regardless of which variant it's in.\n 'rounded-none border',\n checked &&\n (buttonStyle === 'outlined'\n ? 'border-primary'\n : 'border-transparent'),\n index === 0 &&\n (orientation === 'vertical'\n ? 'rounded-t-lg'\n : 'rounded-l-lg'),\n index === options.length - 1 &&\n (orientation === 'vertical'\n ? 'rounded-b-lg'\n : 'rounded-r-lg'),\n )}\n >\n {item.label}\n </Button>\n </Item>\n ) : (\n <Radio\n id={getOptionId(index)}\n placement={placement}\n className={cn(classNames?.item)}\n value={item.value}\n checked={checked}\n disabled={disabled || item.disabled}\n onChange={checked => onChange(checked, item.value)}\n >\n {item.label}\n </Radio>\n )}\n </li>\n );\n })}\n </ul>\n );\n\n return (\n <RadioGroupContext.Provider value={true}>\n <Core\n className=\"block\"\n name={name}\n value={checkedItemValue}\n onValueChange={onValueChange}\n >\n {list}\n </Core>\n </RadioGroupContext.Provider>\n );\n};\n\nexport default RadioGroup;\n"],"mappings":";;;;;;;;;;;AAqBA,MAAM,EAAE,YAAY,MAAM,gBAAgB,SAASA;AAuBnD,MAAM,cAAc,EAClB,cAAc,cACd,YAAY,QACZ,WACA,aAAa,CAAC,GACd,SAAS,WAAW,CAAC,GACrB,MACA,aAAa,WACb,cAAc,SACd,MACA,UACA,cACA,OAAO,QACP,UAAU,kBAAkB,CAAC,GAC7B,GAAG,YACQ;CACX,MAAM,CAAC,OAAO,YAAY,qBAAkC;EAC1D,OAAO;EACP;EACA,UAAU;CACZ,CAAC;CAED,MAAM,UAAoB,iBAAiB,QAAQ;CACnD,4BAA4B,SAAS,aAAa;CAElD,MAAM,WAAW,eAAe;CAEhC,MAAM,YAAY,SAAkB,gBAA6B;EAC/D,IAAI,CAAC,SACH;EAEF,SAAS,WAAW;CACtB;CAMA,MAAM,SAAS,MAAM;CACrB,MAAM,eAAe,UAAkB,GAAG,OAAO,GAAG;CACpD,MAAM,mBAAmB,UAAU,KAAA,IAAY,KAAK,OAAO,KAAK;CAEhE,MAAM,iBAAiB,cAAsB;EAC3C,MAAM,OAAO,QAAQ,MAAK,WAAU,OAAO,OAAO,KAAK,MAAM,SAAS;EAEtE,IAAI,CAAC,MACH;EAGF,SAAS,MAAM,KAAK,KAAK;CAC3B;CAEA,MAAM,OACJ,oBAAC,MAAD;EACE,GAAI;EACJ,WAAW,GACT,gCACA,WACA,gBAAgB,cAAc,YAC9B,YAAY,OACd;EAEC,UAAA,QAAQ,KAAK,MAAc,UAAU;GACpC,MAAM,UAAU,UAAU,KAAK;GAE/B,OACE,oBAAC,MAAD;IAQE,WAAW,GAAG,YAAY,YAAY,OAAO;IAE5C,UAAA,WAKC,oBAAC,MAAD;KACE,SAAA;KACA,OAAO,OAAO,KAAK,KAAK;KACxB,UAAU,YAAY,KAAK;KAE3B,UAAA,oBAAC,QAAD;MACE,SAAS,UAAU,cAAc;MAC3B;MACN,UAAU,YAAY,KAAK;MAC3B,WAAW,GAMT,uBACA,YACG,gBAAgB,aACb,mBACA,uBACN,UAAU,MACP,gBAAgB,aACb,iBACA,iBACN,UAAU,QAAQ,SAAS,MACxB,gBAAgB,aACb,iBACA,eACR;MAEC,UAAA,KAAK;KACA,CAAA;IACJ,CAAA,IAEN,oBAAC,OAAD;KACE,IAAI,YAAY,KAAK;KACV;KACX,WAAW,GAAG,YAAY,IAAI;KAC9B,OAAO,KAAK;KACH;KACT,UAAU,YAAY,KAAK;KAC3B,WAAU,YAAW,SAAS,SAAS,KAAK,KAAK;KAEhD,UAAA,KAAK;IACD,CAAA;GAEP,GA5DG,GAAG,MAAM,GAAG,OAAO,KAAK,KAAK,GA4DhC;EAER,CAAC;CACC,CAAA;CAGN,OACE,oBAAC,kBAAkB,UAAnB;EAA4B,OAAO;EACjC,UAAA,oBAAC,MAAD;GACE,WAAU;GACJ;GACN,OAAO;GACQ;GAEd,UAAA;EACG,CAAA;CACoB,CAAA;AAEhC"}
@@ -70,7 +70,7 @@ const Item = ({ itemKey, keyPath, root = false, index, mode, label, children, se
70
70
  return /* @__PURE__ */ jsxs("li", {
71
71
  ref: itemRef,
72
72
  role: "none",
73
- className: cn("group", "relative", root && isHorizontal ? "inline-block" : "block"),
73
+ className: cn("group", "relative", "m-0", root && isHorizontal ? "inline-block" : "block"),
74
74
  onMouseEnter: () => {
75
75
  if (isInline) return;
76
76
  setOpen(true);
@@ -1 +1 @@
1
- {"version":3,"file":"item.mjs","names":[],"sources":["../../../../../src/components/molecules/menu/item/item.tsx"],"sourcesContent":["'use client';\n\nimport { useRef, useState } from 'react';\n\nimport { ChevronDown } from 'lucide-react';\nimport { AnimatePresence, motion } from 'motion/react';\n\nimport { cn } from '@repo/ui/utils';\n\nimport { MENU_CLASSNAMES, MenuItem, Props } from '../menu';\nimport Label from './label';\n\ninterface ItemProps extends Props, MenuItem {\n itemKey: React.Key;\n keyPath: React.Key[];\n root?: boolean;\n index: number;\n}\nconst OFFSET: [number, number] = [8, 8];\nconst INLINE_OFFSET = 16;\n\nconst STYLES = {\n hover: {\n classes: ['hover:underline', 'hover:underline-offset-8'],\n },\n open: {\n classes: ['underline', 'underline-offset-8'],\n },\n} as const;\n\nconst getItemClasses = (\n root: boolean,\n isHorizontal: boolean,\n isInline: boolean,\n hasChildren: boolean,\n customClasses?: string,\n) =>\n cn(\n 'px-4 py-2',\n 'cursor-pointer',\n 'whitespace-nowrap',\n root && isHorizontal ? 'inline-flex' : 'flex',\n ...(root\n ? [\n 'h-full',\n 'rounded-none border-transparent',\n hasChildren && 'hover:border-current',\n isHorizontal ? 'border-b-2' : 'border-r-2',\n isInline && 'border-none',\n ]\n : ['rounded-md', ...STYLES.hover.classes]),\n customClasses,\n );\n\nconst Item = ({\n itemKey,\n keyPath,\n root = false,\n index,\n mode,\n label,\n children,\n selectionMap,\n classNames,\n styles,\n offset = OFFSET,\n inlineOffset = INLINE_OFFSET,\n onClick,\n onSelect,\n ...props\n}: ItemProps) => {\n const [open, setOpen] = useState<boolean>(false);\n\n const itemRef = useRef<HTMLLIElement | null>(null);\n\n const isHorizontal = mode === 'horizontal';\n const isInline = mode === 'inline';\n\n const [left, top] = offset;\n\n const selected = selectionMap?.get(itemKey) ?? false;\n\n const item = {\n ...props,\n label,\n key: itemKey,\n children,\n };\n\n const itemClassNames = getItemClasses(\n root,\n isHorizontal,\n isInline,\n !!children?.length,\n root ? classNames?.rootItem : classNames?.item,\n );\n\n const handleSelect = (domEvent: React.SyntheticEvent) => {\n domEvent.stopPropagation();\n\n if (isInline) {\n setOpen(!open);\n }\n\n // Next.js's types pull in React's `react/experimental` augmentation\n // (adding `useOptimistic`'s internal key symbol to `React.Key`), which\n // widens `itemKey`/`item.key` here beyond what `ClickEventHandler`\n // expects when this file is type-checked as part of a Next.js app —\n // real menu item keys are always string/number, never that symbol.\n const params = {\n domEvent,\n key: itemKey as Exclude<React.Key, symbol>,\n keyPath,\n item: item as unknown as MenuItem,\n };\n\n onClick?.(params);\n\n if (!selected) {\n onSelect?.(params);\n }\n };\n\n // Roving tabindex (WAI-ARIA menu pattern): only the first item in each\n // menu/submenu level is a Tab stop (below); arrow keys move real DOM\n // focus between sibling <li>s within that same level, which is what\n // ends up mattering for actual keyboard use even though — unlike a\n // fully state-tracked implementation — focus resets to the first item\n // if the user Tabs out and back in rather than resuming where they left\n // off.\n //\n // Deliberately not `useKeyPress` (unlike dropdown.tsx's trigger and\n // radio.tsx/checkbox.tsx's icon mode, #229's action item 5): that hook\n // is a static combo -> handler binding, one listener per combo. This\n // handler is a single stateful branch — Enter/Space select, Escape\n // conditionally closes a submenu and stops the event from also closing\n // an ancestor Dropdown, and the arrow-key branch reads `itemRef` at\n // fire time to focus a *sibling* `<li>`. Splitting that into 3-4\n // separate useKeyPress calls (each its own listener) per menu item\n // wouldn't fix a bug — the existing logic is already correct — it would\n // just multiply listeners across every item in a menu for no benefit.\n const handleKeyDown = (domEvent: React.KeyboardEvent) => {\n if (domEvent.key === 'Enter' || domEvent.key === ' ') {\n domEvent.preventDefault();\n handleSelect(domEvent);\n return;\n }\n\n if (domEvent.key === 'Escape') {\n if (children?.length && open) {\n domEvent.stopPropagation();\n setOpen(false);\n }\n return;\n }\n\n const isNext = isHorizontal\n ? domEvent.key === 'ArrowRight'\n : domEvent.key === 'ArrowDown';\n const isPrev = isHorizontal\n ? domEvent.key === 'ArrowLeft'\n : domEvent.key === 'ArrowUp';\n\n if (!isNext && !isPrev) {\n return;\n }\n\n domEvent.preventDefault();\n const sibling = isNext\n ? itemRef.current?.nextElementSibling\n : itemRef.current?.previousElementSibling;\n sibling?.querySelector<HTMLElement>('[role=\"menuitem\"]')?.focus();\n };\n\n const childrenContainerClassNames = cn(\n isInline ? 'relative overflow-hidden' : 'absolute min-w-[200px]',\n !isInline && root && isHorizontal\n ? 'left-0 top-full pt-2 pl-0'\n : !isInline\n ? 'left-full top-0 pl-2 pt-0'\n : 'pl-4',\n );\n\n const childrenContainerStyle = isInline\n ? {\n paddingLeft: inlineOffset,\n }\n : root && isHorizontal\n ? {\n paddingTop: top,\n }\n : {\n paddingLeft: left,\n };\n\n return (\n <li\n ref={itemRef}\n role=\"none\"\n className={cn(\n 'group',\n 'relative',\n root && isHorizontal ? 'inline-block' : 'block',\n )}\n onMouseEnter={() => {\n if (isInline) {\n return;\n }\n setOpen(true);\n }}\n onMouseLeave={() => {\n if (isInline) {\n return;\n }\n setOpen(false);\n }}\n onFocus={() => {\n if (isInline) {\n return;\n }\n setOpen(true);\n }}\n onBlur={e => {\n if (isInline) {\n return;\n }\n if (!itemRef.current?.contains(e.relatedTarget as Node)) {\n setOpen(false);\n }\n }}\n >\n <div\n role=\"menuitem\"\n tabIndex={index === 0 ? 0 : -1}\n aria-expanded={children?.length ? open : undefined}\n aria-haspopup={children?.length ? 'menu' : undefined}\n aria-current={selected ? true : undefined}\n className={cn(\n itemClassNames,\n ...(selected\n ? root\n ? ['border-current']\n : [...STYLES.open.classes, ...STYLES.hover.classes]\n : root && !!children?.length\n ? ['group-hover:border-current']\n : []),\n //\n )}\n style={root ? styles?.rootItem : styles?.item}\n onClick={handleSelect}\n onKeyDown={handleKeyDown}\n >\n <div\n className={cn(\n 'w-full',\n 'inline-flex items-center justify-between',\n //\n )}\n >\n <Label className={cn(classNames?.label)} style={styles?.label}>\n {label}\n </Label>\n {!root && !isInline && !!children?.length && (\n <ChevronDown className={cn('-rotate-90')} />\n )}\n {isInline && !!children?.length && (\n <ChevronDown\n className={cn(\n 'transition-all',\n open ? '-rotate-180' : 'rotate-0',\n )}\n />\n )}\n </div>\n </div>\n <AnimatePresence>\n {!!children?.length && open && (\n <motion.div\n className={cn(\n childrenContainerClassNames,\n //\n )}\n style={childrenContainerStyle}\n initial={isInline ? { height: 0, opacity: 0 } : { opacity: 0 }}\n animate={isInline ? { height: 'auto', opacity: 1 } : { opacity: 1 }}\n exit={isInline ? { height: 0, opacity: 0 } : { opacity: 0 }}\n transition={{ duration: 0.2 }}\n >\n <ul\n role=\"menu\"\n className={cn(\n MENU_CLASSNAMES,\n isInline && 'shadow-none',\n classNames?.subMenu,\n //\n )}\n style={styles?.subMenu}\n >\n {children.map((child, childIndex) => (\n <Item\n {...child}\n key={child.key}\n itemKey={child.key}\n keyPath={[...keyPath, child.key]}\n index={childIndex}\n selectionMap={selectionMap}\n mode={mode}\n classNames={classNames}\n styles={styles}\n offset={offset}\n inlineOffset={inlineOffset}\n onClick={onClick}\n onSelect={onSelect}\n />\n ))}\n </ul>\n </motion.div>\n )}\n </AnimatePresence>\n </li>\n );\n};\n\nexport default Item;\n"],"mappings":"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"}
1
+ {"version":3,"file":"item.mjs","names":[],"sources":["../../../../../src/components/molecules/menu/item/item.tsx"],"sourcesContent":["'use client';\n\nimport { useRef, useState } from 'react';\n\nimport { ChevronDown } from 'lucide-react';\nimport { AnimatePresence, motion } from 'motion/react';\n\nimport { cn } from '@repo/ui/utils';\n\nimport { MENU_CLASSNAMES, MenuItem, Props } from '../menu';\nimport Label from './label';\n\ninterface ItemProps extends Props, MenuItem {\n itemKey: React.Key;\n keyPath: React.Key[];\n root?: boolean;\n index: number;\n}\nconst OFFSET: [number, number] = [8, 8];\nconst INLINE_OFFSET = 16;\n\nconst STYLES = {\n hover: {\n classes: ['hover:underline', 'hover:underline-offset-8'],\n },\n open: {\n classes: ['underline', 'underline-offset-8'],\n },\n} as const;\n\nconst getItemClasses = (\n root: boolean,\n isHorizontal: boolean,\n isInline: boolean,\n hasChildren: boolean,\n customClasses?: string,\n) =>\n cn(\n 'px-4 py-2',\n 'cursor-pointer',\n 'whitespace-nowrap',\n root && isHorizontal ? 'inline-flex' : 'flex',\n ...(root\n ? [\n 'h-full',\n 'rounded-none border-transparent',\n hasChildren && 'hover:border-current',\n isHorizontal ? 'border-b-2' : 'border-r-2',\n isInline && 'border-none',\n ]\n : ['rounded-md', ...STYLES.hover.classes]),\n customClasses,\n );\n\nconst Item = ({\n itemKey,\n keyPath,\n root = false,\n index,\n mode,\n label,\n children,\n selectionMap,\n classNames,\n styles,\n offset = OFFSET,\n inlineOffset = INLINE_OFFSET,\n onClick,\n onSelect,\n ...props\n}: ItemProps) => {\n const [open, setOpen] = useState<boolean>(false);\n\n const itemRef = useRef<HTMLLIElement | null>(null);\n\n const isHorizontal = mode === 'horizontal';\n const isInline = mode === 'inline';\n\n const [left, top] = offset;\n\n const selected = selectionMap?.get(itemKey) ?? false;\n\n const item = {\n ...props,\n label,\n key: itemKey,\n children,\n };\n\n const itemClassNames = getItemClasses(\n root,\n isHorizontal,\n isInline,\n !!children?.length,\n root ? classNames?.rootItem : classNames?.item,\n );\n\n const handleSelect = (domEvent: React.SyntheticEvent) => {\n domEvent.stopPropagation();\n\n if (isInline) {\n setOpen(!open);\n }\n\n // Next.js's types pull in React's `react/experimental` augmentation\n // (adding `useOptimistic`'s internal key symbol to `React.Key`), which\n // widens `itemKey`/`item.key` here beyond what `ClickEventHandler`\n // expects when this file is type-checked as part of a Next.js app —\n // real menu item keys are always string/number, never that symbol.\n const params = {\n domEvent,\n key: itemKey as Exclude<React.Key, symbol>,\n keyPath,\n item: item as unknown as MenuItem,\n };\n\n onClick?.(params);\n\n if (!selected) {\n onSelect?.(params);\n }\n };\n\n // Roving tabindex (WAI-ARIA menu pattern): only the first item in each\n // menu/submenu level is a Tab stop (below); arrow keys move real DOM\n // focus between sibling <li>s within that same level, which is what\n // ends up mattering for actual keyboard use even though — unlike a\n // fully state-tracked implementation — focus resets to the first item\n // if the user Tabs out and back in rather than resuming where they left\n // off.\n //\n // Deliberately not `useKeyPress` (unlike dropdown.tsx's trigger and\n // radio.tsx/checkbox.tsx's icon mode, #229's action item 5): that hook\n // is a static combo -> handler binding, one listener per combo. This\n // handler is a single stateful branch — Enter/Space select, Escape\n // conditionally closes a submenu and stops the event from also closing\n // an ancestor Dropdown, and the arrow-key branch reads `itemRef` at\n // fire time to focus a *sibling* `<li>`. Splitting that into 3-4\n // separate useKeyPress calls (each its own listener) per menu item\n // wouldn't fix a bug — the existing logic is already correct — it would\n // just multiply listeners across every item in a menu for no benefit.\n const handleKeyDown = (domEvent: React.KeyboardEvent) => {\n if (domEvent.key === 'Enter' || domEvent.key === ' ') {\n domEvent.preventDefault();\n handleSelect(domEvent);\n return;\n }\n\n if (domEvent.key === 'Escape') {\n if (children?.length && open) {\n domEvent.stopPropagation();\n setOpen(false);\n }\n return;\n }\n\n const isNext = isHorizontal\n ? domEvent.key === 'ArrowRight'\n : domEvent.key === 'ArrowDown';\n const isPrev = isHorizontal\n ? domEvent.key === 'ArrowLeft'\n : domEvent.key === 'ArrowUp';\n\n if (!isNext && !isPrev) {\n return;\n }\n\n domEvent.preventDefault();\n const sibling = isNext\n ? itemRef.current?.nextElementSibling\n : itemRef.current?.previousElementSibling;\n sibling?.querySelector<HTMLElement>('[role=\"menuitem\"]')?.focus();\n };\n\n const childrenContainerClassNames = cn(\n isInline ? 'relative overflow-hidden' : 'absolute min-w-[200px]',\n !isInline && root && isHorizontal\n ? 'left-0 top-full pt-2 pl-0'\n : !isInline\n ? 'left-full top-0 pl-2 pt-0'\n : 'pl-4',\n );\n\n const childrenContainerStyle = isInline\n ? {\n paddingLeft: inlineOffset,\n }\n : root && isHorizontal\n ? {\n paddingTop: top,\n }\n : {\n paddingLeft: left,\n };\n\n return (\n <li\n ref={itemRef}\n role=\"none\"\n className={cn(\n 'group',\n 'relative',\n // `m-0` completes MENU_CLASSNAMES' list reset — see Radio.Group for\n // the host rule (`.markdown li + li`) this guards against.\n 'm-0',\n root && isHorizontal ? 'inline-block' : 'block',\n )}\n onMouseEnter={() => {\n if (isInline) {\n return;\n }\n setOpen(true);\n }}\n onMouseLeave={() => {\n if (isInline) {\n return;\n }\n setOpen(false);\n }}\n onFocus={() => {\n if (isInline) {\n return;\n }\n setOpen(true);\n }}\n onBlur={e => {\n if (isInline) {\n return;\n }\n if (!itemRef.current?.contains(e.relatedTarget as Node)) {\n setOpen(false);\n }\n }}\n >\n <div\n role=\"menuitem\"\n tabIndex={index === 0 ? 0 : -1}\n aria-expanded={children?.length ? open : undefined}\n aria-haspopup={children?.length ? 'menu' : undefined}\n aria-current={selected ? true : undefined}\n className={cn(\n itemClassNames,\n ...(selected\n ? root\n ? ['border-current']\n : [...STYLES.open.classes, ...STYLES.hover.classes]\n : root && !!children?.length\n ? ['group-hover:border-current']\n : []),\n //\n )}\n style={root ? styles?.rootItem : styles?.item}\n onClick={handleSelect}\n onKeyDown={handleKeyDown}\n >\n <div\n className={cn(\n 'w-full',\n 'inline-flex items-center justify-between',\n //\n )}\n >\n <Label className={cn(classNames?.label)} style={styles?.label}>\n {label}\n </Label>\n {!root && !isInline && !!children?.length && (\n <ChevronDown className={cn('-rotate-90')} />\n )}\n {isInline && !!children?.length && (\n <ChevronDown\n className={cn(\n 'transition-all',\n open ? '-rotate-180' : 'rotate-0',\n )}\n />\n )}\n </div>\n </div>\n <AnimatePresence>\n {!!children?.length && open && (\n <motion.div\n className={cn(\n childrenContainerClassNames,\n //\n )}\n style={childrenContainerStyle}\n initial={isInline ? { height: 0, opacity: 0 } : { opacity: 0 }}\n animate={isInline ? { height: 'auto', opacity: 1 } : { opacity: 1 }}\n exit={isInline ? { height: 0, opacity: 0 } : { opacity: 0 }}\n transition={{ duration: 0.2 }}\n >\n <ul\n role=\"menu\"\n className={cn(\n MENU_CLASSNAMES,\n isInline && 'shadow-none',\n classNames?.subMenu,\n //\n )}\n style={styles?.subMenu}\n >\n {children.map((child, childIndex) => (\n <Item\n {...child}\n key={child.key}\n itemKey={child.key}\n keyPath={[...keyPath, child.key]}\n index={childIndex}\n selectionMap={selectionMap}\n mode={mode}\n classNames={classNames}\n styles={styles}\n offset={offset}\n inlineOffset={inlineOffset}\n onClick={onClick}\n onSelect={onSelect}\n />\n ))}\n </ul>\n </motion.div>\n )}\n </AnimatePresence>\n </li>\n );\n};\n\nexport default Item;\n"],"mappings":"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"}
@@ -81,7 +81,7 @@ const Upload$1 = ({ defaultValue = [], value: _value, multiple = true, accept =
81
81
  }), files.length > 0 && /* @__PURE__ */ jsx("ul", {
82
82
  className: cn("m-0 flex list-none flex-col gap-2 p-0", classNames?.list),
83
83
  children: files.map((file) => /* @__PURE__ */ jsxs("li", {
84
- className: cn("flex items-center gap-2 rounded-md border p-2", classNames?.item),
84
+ className: cn("m-0 flex items-center gap-2 rounded-md border p-2", classNames?.item),
85
85
  children: [
86
86
  isImage(file) && /* @__PURE__ */ jsx("img", {
87
87
  src: file.url,
@@ -1 +1 @@
1
- {"version":3,"file":"upload.mjs","names":["Upload","UploadIcon"],"sources":["../../../src/components/molecules/upload.tsx"],"sourcesContent":["'use client';\n\nimport { useId } from 'react';\n\nimport {\n useControllableState,\n useFileDrop,\n useFileToDataUrl,\n} from '@jbpark/use-hooks';\nimport { Upload as UploadIcon, X } from 'lucide-react';\n\nimport { cn } from '@repo/ui/utils';\n\nimport Button from '../atoms/button';\n\n// Monotonic per-session counter used as a uniqueness tiebreaker for file\n// uids. Guarantees a unique suffix even for the same file (identical\n// name/lastModified/size) added twice, which name+lastModified+size alone\n// can't. These uids are internal React keys, not cryptographic, so a plain\n// counter is enough — and it avoids a runtime `uuid` dependency.\nlet uidCounter = 0;\nconst nextUid = () => (uidCounter += 1);\n\nexport interface UploadFile {\n uid: string;\n name: string;\n url: string;\n}\n\nexport interface Props {\n defaultValue?: UploadFile[];\n value?: UploadFile[];\n multiple?: boolean;\n accept?: string;\n maxCount?: number;\n disabled?: boolean;\n className?: string;\n classNames?: {\n dropzone?: string;\n list?: string;\n item?: string;\n };\n onChange?: (files: UploadFile[]) => void;\n /**\n * Called once per file that didn't make it in — either bumped by\n * `maxCount` or failed to read.\n */\n onReject?: (info: { file: File; reason: 'max-count' | 'read-error' }) => void;\n}\n\nconst isImage = (file: UploadFile) =>\n /^data:image\\//.test(file.url) ||\n /\\.(png|jpe?g|gif|webp|svg)$/i.test(file.name);\n\nconst Upload = ({\n defaultValue = [],\n value: _value,\n multiple = true,\n accept = 'image/*',\n maxCount,\n disabled,\n className,\n classNames,\n onChange: _onChange = () => {},\n onReject,\n}: Props) => {\n const inputId = useId();\n const readAsDataUrl = useFileToDataUrl();\n const [files, setFiles] = useControllableState<UploadFile[]>({\n value: _value,\n defaultValue,\n onChange: _onChange,\n });\n\n const addFiles = async (incoming: File[]) => {\n const remaining =\n typeof maxCount === 'number'\n ? Math.max(0, maxCount - files.length)\n : incoming.length;\n\n const accepted = incoming.slice(0, remaining);\n const rejected = incoming.slice(remaining);\n\n rejected.forEach(file => onReject?.({ file, reason: 'max-count' }));\n\n if (accepted.length === 0) {\n return;\n }\n\n const uploaded = (\n await Promise.all(\n accepted.map(async file => {\n try {\n return {\n // `nextUid()` guarantees uniqueness even for the same file\n // added twice (identical name/lastModified/size), which\n // name+lastModified+size alone can't — a collision there\n // corrupted React's item keys and made removeFile's\n // uid-based filter delete both copies at once.\n uid: `${file.name}-${file.lastModified}-${file.size}-${nextUid()}`,\n name: file.name,\n url: await readAsDataUrl(file),\n };\n } catch {\n onReject?.({ file, reason: 'read-error' });\n return null;\n }\n }),\n )\n ).filter((file): file is UploadFile => file !== null);\n\n if (uploaded.length === 0) {\n return;\n }\n\n setFiles([...files, ...uploaded]);\n };\n\n const removeFile = (uid: string) => {\n setFiles(files.filter(file => file.uid !== uid));\n };\n\n const { dropRef, isDragging } = useFileDrop<HTMLLabelElement>({\n accept,\n multiple,\n disabled,\n onDrop: addFiles,\n });\n\n return (\n <div\n className={cn(\n 'flex flex-col gap-3',\n className,\n //\n )}\n >\n <label\n ref={dropRef}\n htmlFor={inputId}\n className={cn(\n 'flex cursor-pointer flex-col items-center justify-center gap-2',\n 'rounded-md border border-dashed p-6 text-center',\n 'text-muted-foreground text-sm',\n 'transition-colors',\n isDragging && 'border-primary bg-accent',\n disabled && 'pointer-events-none cursor-not-allowed opacity-50',\n classNames?.dropzone,\n //\n )}\n >\n <UploadIcon className=\"size-6\" />\n <span>Click or drag file{multiple ? 's' : ''} to upload</span>\n <input\n id={inputId}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n className=\"hidden\"\n onChange={e => {\n if (e.target.files?.length) {\n addFiles(Array.from(e.target.files));\n }\n e.target.value = '';\n }}\n />\n </label>\n\n {files.length > 0 && (\n <ul\n className={cn(\n 'm-0 flex list-none flex-col gap-2 p-0',\n classNames?.list,\n //\n )}\n >\n {files.map(file => (\n <li\n key={file.uid}\n className={cn(\n 'flex items-center gap-2 rounded-md border p-2',\n classNames?.item,\n //\n )}\n >\n {isImage(file) && (\n <img\n src={file.url}\n alt=\"\"\n className=\"size-10 shrink-0 rounded object-cover\"\n />\n )}\n <span className=\"flex-1 truncate text-sm\">{file.name}</span>\n <Button\n variant=\"text\"\n shape=\"circle\"\n size=\"small\"\n icon={<X className=\"size-4\" />}\n onClick={() => removeFile(file.uid)}\n className=\"text-muted-foreground hover:text-foreground shrink-0\"\n aria-label={`Remove ${file.name}`}\n />\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n\nexport default Upload;\n"],"mappings":";;;;;;;;AAoBA,IAAI,aAAa;AACjB,MAAM,gBAAiB,cAAc;AA6BrC,MAAM,WAAW,SACf,gBAAgB,KAAK,KAAK,GAAG,KAC7B,+BAA+B,KAAK,KAAK,IAAI;AAE/C,MAAMA,YAAU,EACd,eAAe,CAAC,GAChB,OAAO,QACP,WAAW,MACX,SAAS,WACT,UACA,UACA,WACA,YACA,UAAU,kBAAkB,CAAC,GAC7B,eACW;CACX,MAAM,UAAU,MAAM;CACtB,MAAM,gBAAgB,iBAAiB;CACvC,MAAM,CAAC,OAAO,YAAY,qBAAmC;EAC3D,OAAO;EACP;EACA,UAAU;CACZ,CAAC;CAED,MAAM,WAAW,OAAO,aAAqB;EAC3C,MAAM,YACJ,OAAO,aAAa,WAChB,KAAK,IAAI,GAAG,WAAW,MAAM,MAAM,IACnC,SAAS;EAEf,MAAM,WAAW,SAAS,MAAM,GAAG,SAAS;EAG5C,SAF0B,MAAM,SAEzB,CAAC,CAAC,SAAQ,SAAQ,WAAW;GAAE;GAAM,QAAQ;EAAY,CAAC,CAAC;EAElE,IAAI,SAAS,WAAW,GACtB;EAGF,MAAM,YACJ,MAAM,QAAQ,IACZ,SAAS,IAAI,OAAM,SAAQ;GACzB,IAAI;IACF,OAAO;KAML,KAAK,GAAG,KAAK,KAAK,GAAG,KAAK,aAAa,GAAG,KAAK,KAAK,GAAG,QAAQ;KAC/D,MAAM,KAAK;KACX,KAAK,MAAM,cAAc,IAAI;IAC/B;GACF,QAAQ;IACN,WAAW;KAAE;KAAM,QAAQ;IAAa,CAAC;IACzC,OAAO;GACT;EACF,CAAC,CACH,EAAA,CACA,QAAQ,SAA6B,SAAS,IAAI;EAEpD,IAAI,SAAS,WAAW,GACtB;EAGF,SAAS,CAAC,GAAG,OAAO,GAAG,QAAQ,CAAC;CAClC;CAEA,MAAM,cAAc,QAAgB;EAClC,SAAS,MAAM,QAAO,SAAQ,KAAK,QAAQ,GAAG,CAAC;CACjD;CAEA,MAAM,EAAE,SAAS,eAAe,YAA8B;EAC5D;EACA;EACA;EACA,QAAQ;CACV,CAAC;CAED,OACE,qBAAC,OAAD;EACE,WAAW,GACT,uBACA,SAEF;EALF,UAAA,CAOE,qBAAC,SAAD;GACE,KAAK;GACL,SAAS;GACT,WAAW,GACT,kEACA,mDACA,iCACA,qBACA,cAAc,4BACd,YAAY,qDACZ,YAAY,QAEd;GAZF,UAAA;IAcE,oBAACC,QAAD,EAAY,WAAU,SAAU,CAAA;IAChC,qBAAC,QAAD,EAAA,UAAA;KAAM;KAAmB,WAAW,MAAM;KAAG;IAAgB,EAAA,CAAA;IAC7D,oBAAC,SAAD;KACE,IAAI;KACJ,MAAK;KACG;KACE;KACA;KACV,WAAU;KACV,WAAU,MAAK;MACb,IAAI,EAAE,OAAO,OAAO,QAClB,SAAS,MAAM,KAAK,EAAE,OAAO,KAAK,CAAC;MAErC,EAAE,OAAO,QAAQ;KACnB;IACD,CAAA;GACI;EAEN,CAAA,GAAA,MAAM,SAAS,KACd,oBAAC,MAAD;GACE,WAAW,GACT,yCACA,YAAY,IAEd;GAEC,UAAA,MAAM,KAAI,SACT,qBAAC,MAAD;IAEE,WAAW,GACT,iDACA,YAAY,IAEd;IANF,UAAA;KAQG,QAAQ,IAAI,KACX,oBAAC,OAAD;MACE,KAAK,KAAK;MACV,KAAI;MACJ,WAAU;KACX,CAAA;KAEH,oBAAC,QAAD;MAAM,WAAU;MAA2B,UAAA,KAAK;KAAW,CAAA;KAC3D,oBAAC,QAAD;MACE,SAAQ;MACR,OAAM;MACN,MAAK;MACL,MAAM,oBAAC,GAAD,EAAG,WAAU,SAAU,CAAA;MAC7B,eAAe,WAAW,KAAK,GAAG;MAClC,WAAU;MACV,cAAY,UAAU,KAAK;KAC5B,CAAA;IACC;GAxBG,GAAA,KAAK,GAwBR,CACL;EACC,CAAA,CAEH;;AAET"}
1
+ {"version":3,"file":"upload.mjs","names":["Upload","UploadIcon"],"sources":["../../../src/components/molecules/upload.tsx"],"sourcesContent":["'use client';\n\nimport { useId } from 'react';\n\nimport {\n useControllableState,\n useFileDrop,\n useFileToDataUrl,\n} from '@jbpark/use-hooks';\nimport { Upload as UploadIcon, X } from 'lucide-react';\n\nimport { cn } from '@repo/ui/utils';\n\nimport Button from '../atoms/button';\n\n// Monotonic per-session counter used as a uniqueness tiebreaker for file\n// uids. Guarantees a unique suffix even for the same file (identical\n// name/lastModified/size) added twice, which name+lastModified+size alone\n// can't. These uids are internal React keys, not cryptographic, so a plain\n// counter is enough — and it avoids a runtime `uuid` dependency.\nlet uidCounter = 0;\nconst nextUid = () => (uidCounter += 1);\n\nexport interface UploadFile {\n uid: string;\n name: string;\n url: string;\n}\n\nexport interface Props {\n defaultValue?: UploadFile[];\n value?: UploadFile[];\n multiple?: boolean;\n accept?: string;\n maxCount?: number;\n disabled?: boolean;\n className?: string;\n classNames?: {\n dropzone?: string;\n list?: string;\n item?: string;\n };\n onChange?: (files: UploadFile[]) => void;\n /**\n * Called once per file that didn't make it in — either bumped by\n * `maxCount` or failed to read.\n */\n onReject?: (info: { file: File; reason: 'max-count' | 'read-error' }) => void;\n}\n\nconst isImage = (file: UploadFile) =>\n /^data:image\\//.test(file.url) ||\n /\\.(png|jpe?g|gif|webp|svg)$/i.test(file.name);\n\nconst Upload = ({\n defaultValue = [],\n value: _value,\n multiple = true,\n accept = 'image/*',\n maxCount,\n disabled,\n className,\n classNames,\n onChange: _onChange = () => {},\n onReject,\n}: Props) => {\n const inputId = useId();\n const readAsDataUrl = useFileToDataUrl();\n const [files, setFiles] = useControllableState<UploadFile[]>({\n value: _value,\n defaultValue,\n onChange: _onChange,\n });\n\n const addFiles = async (incoming: File[]) => {\n const remaining =\n typeof maxCount === 'number'\n ? Math.max(0, maxCount - files.length)\n : incoming.length;\n\n const accepted = incoming.slice(0, remaining);\n const rejected = incoming.slice(remaining);\n\n rejected.forEach(file => onReject?.({ file, reason: 'max-count' }));\n\n if (accepted.length === 0) {\n return;\n }\n\n const uploaded = (\n await Promise.all(\n accepted.map(async file => {\n try {\n return {\n // `nextUid()` guarantees uniqueness even for the same file\n // added twice (identical name/lastModified/size), which\n // name+lastModified+size alone can't — a collision there\n // corrupted React's item keys and made removeFile's\n // uid-based filter delete both copies at once.\n uid: `${file.name}-${file.lastModified}-${file.size}-${nextUid()}`,\n name: file.name,\n url: await readAsDataUrl(file),\n };\n } catch {\n onReject?.({ file, reason: 'read-error' });\n return null;\n }\n }),\n )\n ).filter((file): file is UploadFile => file !== null);\n\n if (uploaded.length === 0) {\n return;\n }\n\n setFiles([...files, ...uploaded]);\n };\n\n const removeFile = (uid: string) => {\n setFiles(files.filter(file => file.uid !== uid));\n };\n\n const { dropRef, isDragging } = useFileDrop<HTMLLabelElement>({\n accept,\n multiple,\n disabled,\n onDrop: addFiles,\n });\n\n return (\n <div\n className={cn(\n 'flex flex-col gap-3',\n className,\n //\n )}\n >\n <label\n ref={dropRef}\n htmlFor={inputId}\n className={cn(\n 'flex cursor-pointer flex-col items-center justify-center gap-2',\n 'rounded-md border border-dashed p-6 text-center',\n 'text-muted-foreground text-sm',\n 'transition-colors',\n isDragging && 'border-primary bg-accent',\n disabled && 'pointer-events-none cursor-not-allowed opacity-50',\n classNames?.dropzone,\n //\n )}\n >\n <UploadIcon className=\"size-6\" />\n <span>Click or drag file{multiple ? 's' : ''} to upload</span>\n <input\n id={inputId}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n className=\"hidden\"\n onChange={e => {\n if (e.target.files?.length) {\n addFiles(Array.from(e.target.files));\n }\n e.target.value = '';\n }}\n />\n </label>\n\n {files.length > 0 && (\n <ul\n className={cn(\n 'm-0 flex list-none flex-col gap-2 p-0',\n classNames?.list,\n //\n )}\n >\n {files.map(file => (\n <li\n key={file.uid}\n className={cn(\n // `m-0` completes the list reset the `ul` above starts — see\n // Radio.Group for the host rule this guards against.\n 'm-0 flex items-center gap-2 rounded-md border p-2',\n classNames?.item,\n //\n )}\n >\n {isImage(file) && (\n <img\n src={file.url}\n alt=\"\"\n className=\"size-10 shrink-0 rounded object-cover\"\n />\n )}\n <span className=\"flex-1 truncate text-sm\">{file.name}</span>\n <Button\n variant=\"text\"\n shape=\"circle\"\n size=\"small\"\n icon={<X className=\"size-4\" />}\n onClick={() => removeFile(file.uid)}\n className=\"text-muted-foreground hover:text-foreground shrink-0\"\n aria-label={`Remove ${file.name}`}\n />\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n\nexport default Upload;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbpark/ui-kit",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "Modern React UI component library built with TypeScript, Tailwind CSS, and Radix UI. Featuring atoms, molecules, organisms and layout templates for building beautiful interfaces.",
5
5
  "keywords": [
6
6
  "react",