@asnewyla/radio 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @asnewyla/radio
2
+
3
+ Styled radio built on `@asnewyla/unstyled-radio`. Label association, inline
4
+ validation error, full keyboard/focus accessibility.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @asnewyla/radio
10
+ ```
11
+
12
+ Import the stylesheet once, anywhere in your app:
13
+
14
+ ```ts
15
+ import '@asnewyla/radio/styles.css';
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```tsx
21
+ import { Radio } from '@asnewyla/radio';
22
+
23
+ <Radio label="Option A" name="choice" defaultChecked />
24
+ <Radio label="Option B" name="choice" />
25
+
26
+ <Radio label="Option A" error="You must choose an option" />
27
+
28
+ // Controlled
29
+ <Radio
30
+ label="Option A"
31
+ checked={selected === 'a'}
32
+ onChange={() => setSelected('a')}
33
+ />
34
+ ```
35
+
36
+ ### Props
37
+
38
+ | Prop | Type | Default |
39
+ |---|---|---|
40
+ | `label` | `string` (**required**) | — |
41
+ | `error` | `string` | — |
42
+ | `checked` / `defaultChecked` | `boolean` | — |
43
+ | `onChange` | `(event: React.ChangeEvent<HTMLInputElement>) => void` | — |
44
+ | `disabled` | `boolean` | `false` |
45
+ | `id` | `string` | auto-generated via `useId()` |
46
+
47
+ `error` doubles as the invalid-state flag — there's no separate `invalid`
48
+ prop. Setting `error` sets `aria-invalid` on the input and links it via
49
+ `aria-describedby` to the rendered error message; there's no way to end up
50
+ with one without the other. Same convention `@asnewyla/input` and
51
+ `@asnewyla/checkbox` use.
52
+
53
+ ### Grouping radios
54
+
55
+ Two `Radio`s sharing a native `name` get correct browser-level exclusivity
56
+ for free — clicking one genuinely unchecks the other at the DOM level.
57
+ That's enough for a *display-only* group, or one you fully control
58
+ yourself via `checked`/`onChange` on every member. For a group where you
59
+ want React to own "which value is selected" as shared state, use
60
+ `RadioGroup` from `@asnewyla/radio-group` instead of wiring several
61
+ `Radio`s up by hand:
62
+
63
+ ```tsx
64
+ import { RadioGroup } from '@asnewyla/radio-group';
65
+ import { Radio } from '@asnewyla/radio';
66
+
67
+ <RadioGroup name="size" defaultValue="md">
68
+ <Radio label="Small" value="sm" />
69
+ <Radio label="Medium" value="md" />
70
+ <Radio label="Large" value="lg" />
71
+ </RadioGroup>
72
+ ```
73
+
74
+ `Radio` exports `RadioGroupProvider`/`useRadioGroupContext` itself (that's
75
+ how it consumes an ancestor group), but you'd only reach for those
76
+ directly if you were building something like `RadioGroup` yourself — see
77
+ `@asnewyla/radio-group`'s README for the underlying limitation it exists
78
+ to fix (an uncontrolled radio's own state can go stale when a sibling is
79
+ selected instead, since browsers don't fire a change event on the
80
+ deselected radio).
81
+
82
+ ### Theming
83
+
84
+ Override the CSS custom properties — defaults adapt automatically to
85
+ `prefers-color-scheme` via `@asnewyla/tokens`:
86
+
87
+ ```css
88
+ :root {
89
+ --xd-color-primary: #0d9488;
90
+ --xd-color-destructive: #dc2626;
91
+ }
92
+ ```
93
+
94
+ ## License
95
+
96
+ MIT
package/dist/index.css ADDED
@@ -0,0 +1,63 @@
1
+ /* src/components/Radio.css */
2
+ .xd-radio {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: var(--xd-space-sm, 0.375rem);
6
+ flex-wrap: wrap;
7
+ }
8
+ .xd-radio-input {
9
+ appearance: none;
10
+ -webkit-appearance: none;
11
+ margin: 0;
12
+ flex-shrink: 0;
13
+ width: 1rem;
14
+ height: 1rem;
15
+ position: relative;
16
+ border-radius: 50%;
17
+ border: var(--xd-border-width-thin, 1px) solid var(--xd-color-border, #cbd5e1);
18
+ background-color: transparent;
19
+ cursor: pointer;
20
+ transition: border-color var(--xd-motion-fast, 120ms ease-out);
21
+ }
22
+ .xd-radio-input[data-checked=true] {
23
+ border-color: var(--xd-color-primary, #0d9488);
24
+ }
25
+ .xd-radio-input[data-checked=true]::after {
26
+ content: "";
27
+ position: absolute;
28
+ top: 50%;
29
+ left: 50%;
30
+ width: 0.5rem;
31
+ height: 0.5rem;
32
+ border-radius: 50%;
33
+ background-color: var(--xd-color-primary, #0d9488);
34
+ transform: translate(-50%, -50%);
35
+ }
36
+ .xd-radio-input:focus-visible {
37
+ outline: var(--xd-focus-ring-width, 2px) solid var(--xd-color-focus, var(--xd-color-primary, #0d9488));
38
+ outline-offset: var(--xd-focus-ring-offset, 2px);
39
+ }
40
+ .xd-radio-label {
41
+ font-size: var(--xd-font-size-md, 0.875rem);
42
+ cursor: pointer;
43
+ }
44
+ .xd-radio-input[data-invalid=true] {
45
+ border-color: var(--xd-color-destructive, #dc2626);
46
+ }
47
+ .xd-radio-input[data-invalid=true]:focus-visible {
48
+ outline-color: var(--xd-color-destructive, #dc2626);
49
+ }
50
+ .xd-radio-error {
51
+ flex-basis: 100%;
52
+ font-size: var(--xd-font-size-sm, 0.8125rem);
53
+ color: var(--xd-color-destructive, #dc2626);
54
+ }
55
+ .xd-radio-input:disabled {
56
+ opacity: var(--xd-opacity-disabled, 0.5);
57
+ cursor: not-allowed;
58
+ }
59
+ .xd-radio-input:disabled + .xd-radio-label {
60
+ opacity: var(--xd-opacity-disabled, 0.5);
61
+ cursor: not-allowed;
62
+ }
63
+ /*# sourceMappingURL=index.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Radio.css"],"sourcesContent":[".xd-radio {\n display: inline-flex;\n align-items: center;\n gap: var(--xd-space-sm, 0.375rem);\n flex-wrap: wrap;\n}\n\n.xd-radio-input {\n appearance: none;\n -webkit-appearance: none;\n margin: 0;\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n position: relative;\n border-radius: 50%;\n border: var(--xd-border-width-thin, 1px) solid var(--xd-color-border, #cbd5e1);\n background-color: transparent;\n cursor: pointer;\n transition: border-color var(--xd-motion-fast, 120ms ease-out);\n}\n\n.xd-radio-input[data-checked=\"true\"] {\n border-color: var(--xd-color-primary, #0d9488);\n}\n\n.xd-radio-input[data-checked=\"true\"]::after {\n content: \"\";\n position: absolute;\n top: 50%;\n left: 50%;\n width: 0.5rem;\n height: 0.5rem;\n border-radius: 50%;\n background-color: var(--xd-color-primary, #0d9488);\n transform: translate(-50%, -50%);\n}\n\n.xd-radio-input:focus-visible {\n outline: var(--xd-focus-ring-width, 2px) solid\n var(--xd-color-focus, var(--xd-color-primary, #0d9488));\n outline-offset: var(--xd-focus-ring-offset, 2px);\n}\n\n.xd-radio-label {\n font-size: var(--xd-font-size-md, 0.875rem);\n cursor: pointer;\n}\n\n/* --- Invalid --- */\n\n.xd-radio-input[data-invalid=\"true\"] {\n border-color: var(--xd-color-destructive, #dc2626);\n}\n\n.xd-radio-input[data-invalid=\"true\"]:focus-visible {\n outline-color: var(--xd-color-destructive, #dc2626);\n}\n\n.xd-radio-error {\n flex-basis: 100%;\n font-size: var(--xd-font-size-sm, 0.8125rem);\n color: var(--xd-color-destructive, #dc2626);\n}\n\n/* --- Disabled --- */\n\n.xd-radio-input:disabled {\n opacity: var(--xd-opacity-disabled, 0.5);\n cursor: not-allowed;\n}\n\n.xd-radio-input:disabled + .xd-radio-label {\n opacity: var(--xd-opacity-disabled, 0.5);\n cursor: not-allowed;\n}\n"],"mappings":";AAAA,CAAC;AACC,WAAS;AACT,eAAa;AACb,OAAK,IAAI,aAAa,EAAE;AACxB,aAAW;AACb;AAEA,CAAC;AACC,cAAY;AACZ,sBAAoB;AACpB,UAAQ;AACR,eAAa;AACb,SAAO;AACP,UAAQ;AACR,YAAU;AACV,iBAAe;AACf,UAAQ,IAAI,sBAAsB,EAAE,KAAK,MAAM,IAAI,iBAAiB,EAAE;AACtE,oBAAkB;AAClB,UAAQ;AACR,cAAY,aAAa,IAAI,gBAAgB,EAAE,MAAM;AACvD;AAEA,CAfC,cAec,CAAC;AACd,gBAAc,IAAI,kBAAkB,EAAE;AACxC;AAEA,CAnBC,cAmBc,CAAC,kBAAoB;AAClC,WAAS;AACT,YAAU;AACV,OAAK;AACL,QAAM;AACN,SAAO;AACP,UAAQ;AACR,iBAAe;AACf,oBAAkB,IAAI,kBAAkB,EAAE;AAC1C,aAAW,UAAU,IAAI,EAAE;AAC7B;AAEA,CA/BC,cA+Bc;AACb,WAAS,IAAI,qBAAqB,EAAE,KAAK,MACvC,IAAI,gBAAgB,EAAE,IAAI,kBAAkB,EAAE;AAChD,kBAAgB,IAAI,sBAAsB,EAAE;AAC9C;AAEA,CAAC;AACC,aAAW,IAAI,iBAAiB,EAAE;AAClC,UAAQ;AACV;AAIA,CA5CC,cA4Cc,CAAC;AACd,gBAAc,IAAI,sBAAsB,EAAE;AAC5C;AAEA,CAhDC,cAgDc,CAAC,kBAAoB;AAClC,iBAAe,IAAI,sBAAsB,EAAE;AAC7C;AAEA,CAAC;AACC,cAAY;AACZ,aAAW,IAAI,iBAAiB,EAAE;AAClC,SAAO,IAAI,sBAAsB,EAAE;AACrC;AAIA,CA5DC,cA4Dc;AACb,WAAS,IAAI,qBAAqB,EAAE;AACpC,UAAQ;AACV;AAEA,CAjEC,cAiEc,UAAU,EAAE,CA5B1B;AA6BC,WAAS,IAAI,qBAAqB,EAAE;AACpC,UAAQ;AACV;","names":[]}
@@ -0,0 +1,21 @@
1
+ import * as react from 'react';
2
+ import { UnstyledRadioProps } from '@asnewyla/unstyled-radio';
3
+
4
+ type RadioProps = Omit<UnstyledRadioProps, 'invalid'> & {
5
+ label: string;
6
+ error?: string;
7
+ };
8
+ declare const Radio: react.ForwardRefExoticComponent<Omit<UnstyledRadioProps, "invalid"> & {
9
+ label: string;
10
+ error?: string;
11
+ } & react.RefAttributes<HTMLInputElement>>;
12
+
13
+ interface RadioGroupContextValue {
14
+ name: string;
15
+ value: string | undefined;
16
+ onChange: (value: string) => void;
17
+ }
18
+ declare const RadioGroupProvider: react.Provider<RadioGroupContextValue | null>;
19
+ declare function useRadioGroupContext(): RadioGroupContextValue | null;
20
+
21
+ export { Radio, type RadioGroupContextValue, RadioGroupProvider, type RadioProps, useRadioGroupContext };
@@ -0,0 +1,21 @@
1
+ import * as react from 'react';
2
+ import { UnstyledRadioProps } from '@asnewyla/unstyled-radio';
3
+
4
+ type RadioProps = Omit<UnstyledRadioProps, 'invalid'> & {
5
+ label: string;
6
+ error?: string;
7
+ };
8
+ declare const Radio: react.ForwardRefExoticComponent<Omit<UnstyledRadioProps, "invalid"> & {
9
+ label: string;
10
+ error?: string;
11
+ } & react.RefAttributes<HTMLInputElement>>;
12
+
13
+ interface RadioGroupContextValue {
14
+ name: string;
15
+ value: string | undefined;
16
+ onChange: (value: string) => void;
17
+ }
18
+ declare const RadioGroupProvider: react.Provider<RadioGroupContextValue | null>;
19
+ declare function useRadioGroupContext(): RadioGroupContextValue | null;
20
+
21
+ export { Radio, type RadioGroupContextValue, RadioGroupProvider, type RadioProps, useRadioGroupContext };
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ var unstyledRadio = require('@asnewyla/unstyled-radio');
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/components/Radio.tsx
8
+ var RadioGroupContext = react.createContext(null);
9
+ var RadioGroupProvider = RadioGroupContext.Provider;
10
+ function useRadioGroupContext() {
11
+ return react.useContext(RadioGroupContext);
12
+ }
13
+ function RadioInner({
14
+ id,
15
+ label,
16
+ error,
17
+ disabled,
18
+ className,
19
+ checked,
20
+ onChange,
21
+ value,
22
+ name,
23
+ ...rest
24
+ }, ref) {
25
+ const group = useRadioGroupContext();
26
+ const generatedId = react.useId();
27
+ const radioId = id ?? generatedId;
28
+ const errorId = `${radioId}-error`;
29
+ const resolvedChecked = checked ?? (group ? group.value === value : void 0);
30
+ const resolvedName = name ?? group?.name;
31
+ const handleChange = (e) => {
32
+ onChange?.(e);
33
+ if (group && value !== void 0) {
34
+ group.onChange(String(value));
35
+ }
36
+ };
37
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "xd-radio", children: [
38
+ /* @__PURE__ */ jsxRuntime.jsx(
39
+ unstyledRadio.UnstyledRadio,
40
+ {
41
+ id: radioId,
42
+ ...rest,
43
+ name: resolvedName,
44
+ value,
45
+ checked: resolvedChecked,
46
+ onChange: handleChange,
47
+ invalid: !!error,
48
+ disabled,
49
+ "aria-describedby": error ? errorId : void 0,
50
+ className: className ? `xd-radio-input ${className}` : "xd-radio-input",
51
+ ref
52
+ }
53
+ ),
54
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "xd-radio-label", htmlFor: radioId, children: label }),
55
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "xd-radio-error", id: errorId, children: error })
56
+ ] });
57
+ }
58
+ var Radio = react.forwardRef(RadioInner);
59
+
60
+ exports.Radio = Radio;
61
+ exports.RadioGroupProvider = RadioGroupProvider;
62
+ exports.useRadioGroupContext = useRadioGroupContext;
63
+ //# sourceMappingURL=index.js.map
64
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/RadioGroupContext.tsx","../src/components/Radio.tsx"],"names":["createContext","useContext","useId","jsxs","jsx","UnstyledRadio","forwardRef"],"mappings":";;;;;;;AAQA,IAAM,iBAAA,GAAoBA,oBAA6C,IAAI,CAAA;AAEpE,IAAM,qBAAqB,iBAAA,CAAkB;AAE7C,SAAS,oBAAA,GAAsD;AACpE,EAAA,OAAOC,iBAAW,iBAAiB,CAAA;AACrC;ACDA,SAAS,UAAA,CACP;AAAA,EACE,EAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAG;AACL,CAAA,EACA,GAAA,EACA;AACA,EAAA,MAAM,QAAQ,oBAAA,EAAqB;AACnC,EAAA,MAAM,cAAcC,WAAA,EAAM;AAC1B,EAAA,MAAM,UAAU,EAAA,IAAM,WAAA;AACtB,EAAA,MAAM,OAAA,GAAU,GAAG,OAAO,CAAA,MAAA,CAAA;AAO1B,EAAA,MAAM,eAAA,GACJ,OAAA,KAAY,KAAA,GAAQ,KAAA,CAAM,UAAU,KAAA,GAAQ,MAAA,CAAA;AAC9C,EAAA,MAAM,YAAA,GAAe,QAAQ,KAAA,EAAO,IAAA;AAEpC,EAAA,MAAM,YAAA,GAAe,CAAC,CAAA,KAA2C;AAC/D,IAAA,QAAA,GAAW,CAAC,CAAA;AACZ,IAAA,IAAI,KAAA,IAAS,UAAU,MAAA,EAAW;AAChC,MAAA,KAAA,CAAM,QAAA,CAAS,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,IAC9B;AAAA,EACF,CAAA;AAEA,EAAA,uBACEC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,UAAA,EACb,QAAA,EAAA;AAAA,oBAAAC,cAAA;AAAA,MAACC,2BAAA;AAAA,MAAA;AAAA,QACC,EAAA,EAAI,OAAA;AAAA,QACH,GAAG,IAAA;AAAA,QACJ,IAAA,EAAM,YAAA;AAAA,QACN,KAAA;AAAA,QACA,OAAA,EAAS,eAAA;AAAA,QACT,QAAA,EAAU,YAAA;AAAA,QACV,OAAA,EAAS,CAAC,CAAC,KAAA;AAAA,QACX,QAAA;AAAA,QACA,kBAAA,EAAkB,QAAQ,OAAA,GAAU,MAAA;AAAA,QACpC,SAAA,EAAW,SAAA,GAAY,CAAA,eAAA,EAAkB,SAAS,CAAA,CAAA,GAAK,gBAAA;AAAA,QACvD;AAAA;AAAA,KACF;AAAA,mCACC,OAAA,EAAA,EAAM,SAAA,EAAU,gBAAA,EAAiB,OAAA,EAAS,SACxC,QAAA,EAAA,KAAA,EACH,CAAA;AAAA,IACC,yBACCD,cAAA,CAAC,KAAA,EAAA,EAAI,WAAU,gBAAA,EAAiB,EAAA,EAAI,SACjC,QAAA,EAAA,KAAA,EACH;AAAA,GAAA,EAEJ,CAAA;AAEJ;AAEO,IAAM,KAAA,GAAQE,iBAAyC,UAAU","file":"index.js","sourcesContent":["import { createContext, useContext } from 'react';\n\nexport interface RadioGroupContextValue {\n name: string;\n value: string | undefined;\n onChange: (value: string) => void;\n}\n\nconst RadioGroupContext = createContext<RadioGroupContextValue | null>(null);\n\nexport const RadioGroupProvider = RadioGroupContext.Provider;\n\nexport function useRadioGroupContext(): RadioGroupContextValue | null {\n return useContext(RadioGroupContext);\n}\n","import {\n UnstyledRadio,\n type UnstyledRadioProps,\n} from '@asnewyla/unstyled-radio';\nimport { type ForwardedRef, forwardRef, useId } from 'react';\nimport { useRadioGroupContext } from './RadioGroupContext';\nimport './Radio.css';\n\nexport type RadioProps = Omit<UnstyledRadioProps, 'invalid'> & {\n label: string;\n error?: string;\n};\n\nfunction RadioInner(\n {\n id,\n label,\n error,\n disabled,\n className,\n checked,\n onChange,\n value,\n name,\n ...rest\n }: RadioProps,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n const group = useRadioGroupContext();\n const generatedId = useId();\n const radioId = id ?? generatedId;\n const errorId = `${radioId}-error`;\n\n // Own `checked` wins if given (same prop ?? group?.x precedence Button\n // uses for ButtonGroupContext); otherwise, inside a RadioGroup, checked\n // is derived from whether this radio's own value matches the group's\n // selected value — that's the group's whole point: one shared source of\n // truth instead of several independently-tracked radios.\n const resolvedChecked =\n checked ?? (group ? group.value === value : undefined);\n const resolvedName = name ?? group?.name;\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n if (group && value !== undefined) {\n group.onChange(String(value));\n }\n };\n\n return (\n <div className=\"xd-radio\">\n <UnstyledRadio\n id={radioId}\n {...rest}\n name={resolvedName}\n value={value}\n checked={resolvedChecked}\n onChange={handleChange}\n invalid={!!error}\n disabled={disabled}\n aria-describedby={error ? errorId : undefined}\n className={className ? `xd-radio-input ${className}` : 'xd-radio-input'}\n ref={ref}\n />\n <label className=\"xd-radio-label\" htmlFor={radioId}>\n {label}\n </label>\n {error && (\n <div className=\"xd-radio-error\" id={errorId}>\n {error}\n </div>\n )}\n </div>\n );\n}\n\nexport const Radio = forwardRef<HTMLInputElement, RadioProps>(RadioInner);\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,60 @@
1
+ import { UnstyledRadio } from '@asnewyla/unstyled-radio';
2
+ import { createContext, forwardRef, useId, useContext } from 'react';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/components/Radio.tsx
6
+ var RadioGroupContext = createContext(null);
7
+ var RadioGroupProvider = RadioGroupContext.Provider;
8
+ function useRadioGroupContext() {
9
+ return useContext(RadioGroupContext);
10
+ }
11
+ function RadioInner({
12
+ id,
13
+ label,
14
+ error,
15
+ disabled,
16
+ className,
17
+ checked,
18
+ onChange,
19
+ value,
20
+ name,
21
+ ...rest
22
+ }, ref) {
23
+ const group = useRadioGroupContext();
24
+ const generatedId = useId();
25
+ const radioId = id ?? generatedId;
26
+ const errorId = `${radioId}-error`;
27
+ const resolvedChecked = checked ?? (group ? group.value === value : void 0);
28
+ const resolvedName = name ?? group?.name;
29
+ const handleChange = (e) => {
30
+ onChange?.(e);
31
+ if (group && value !== void 0) {
32
+ group.onChange(String(value));
33
+ }
34
+ };
35
+ return /* @__PURE__ */ jsxs("div", { className: "xd-radio", children: [
36
+ /* @__PURE__ */ jsx(
37
+ UnstyledRadio,
38
+ {
39
+ id: radioId,
40
+ ...rest,
41
+ name: resolvedName,
42
+ value,
43
+ checked: resolvedChecked,
44
+ onChange: handleChange,
45
+ invalid: !!error,
46
+ disabled,
47
+ "aria-describedby": error ? errorId : void 0,
48
+ className: className ? `xd-radio-input ${className}` : "xd-radio-input",
49
+ ref
50
+ }
51
+ ),
52
+ /* @__PURE__ */ jsx("label", { className: "xd-radio-label", htmlFor: radioId, children: label }),
53
+ error && /* @__PURE__ */ jsx("div", { className: "xd-radio-error", id: errorId, children: error })
54
+ ] });
55
+ }
56
+ var Radio = forwardRef(RadioInner);
57
+
58
+ export { Radio, RadioGroupProvider, useRadioGroupContext };
59
+ //# sourceMappingURL=index.mjs.map
60
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/RadioGroupContext.tsx","../src/components/Radio.tsx"],"names":[],"mappings":";;;;;AAQA,IAAM,iBAAA,GAAoB,cAA6C,IAAI,CAAA;AAEpE,IAAM,qBAAqB,iBAAA,CAAkB;AAE7C,SAAS,oBAAA,GAAsD;AACpE,EAAA,OAAO,WAAW,iBAAiB,CAAA;AACrC;ACDA,SAAS,UAAA,CACP;AAAA,EACE,EAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAG;AACL,CAAA,EACA,GAAA,EACA;AACA,EAAA,MAAM,QAAQ,oBAAA,EAAqB;AACnC,EAAA,MAAM,cAAc,KAAA,EAAM;AAC1B,EAAA,MAAM,UAAU,EAAA,IAAM,WAAA;AACtB,EAAA,MAAM,OAAA,GAAU,GAAG,OAAO,CAAA,MAAA,CAAA;AAO1B,EAAA,MAAM,eAAA,GACJ,OAAA,KAAY,KAAA,GAAQ,KAAA,CAAM,UAAU,KAAA,GAAQ,MAAA,CAAA;AAC9C,EAAA,MAAM,YAAA,GAAe,QAAQ,KAAA,EAAO,IAAA;AAEpC,EAAA,MAAM,YAAA,GAAe,CAAC,CAAA,KAA2C;AAC/D,IAAA,QAAA,GAAW,CAAC,CAAA;AACZ,IAAA,IAAI,KAAA,IAAS,UAAU,MAAA,EAAW;AAChC,MAAA,KAAA,CAAM,QAAA,CAAS,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,IAC9B;AAAA,EACF,CAAA;AAEA,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,UAAA,EACb,QAAA,EAAA;AAAA,oBAAA,GAAA;AAAA,MAAC,aAAA;AAAA,MAAA;AAAA,QACC,EAAA,EAAI,OAAA;AAAA,QACH,GAAG,IAAA;AAAA,QACJ,IAAA,EAAM,YAAA;AAAA,QACN,KAAA;AAAA,QACA,OAAA,EAAS,eAAA;AAAA,QACT,QAAA,EAAU,YAAA;AAAA,QACV,OAAA,EAAS,CAAC,CAAC,KAAA;AAAA,QACX,QAAA;AAAA,QACA,kBAAA,EAAkB,QAAQ,OAAA,GAAU,MAAA;AAAA,QACpC,SAAA,EAAW,SAAA,GAAY,CAAA,eAAA,EAAkB,SAAS,CAAA,CAAA,GAAK,gBAAA;AAAA,QACvD;AAAA;AAAA,KACF;AAAA,wBACC,OAAA,EAAA,EAAM,SAAA,EAAU,gBAAA,EAAiB,OAAA,EAAS,SACxC,QAAA,EAAA,KAAA,EACH,CAAA;AAAA,IACC,yBACC,GAAA,CAAC,KAAA,EAAA,EAAI,WAAU,gBAAA,EAAiB,EAAA,EAAI,SACjC,QAAA,EAAA,KAAA,EACH;AAAA,GAAA,EAEJ,CAAA;AAEJ;AAEO,IAAM,KAAA,GAAQ,WAAyC,UAAU","file":"index.mjs","sourcesContent":["import { createContext, useContext } from 'react';\n\nexport interface RadioGroupContextValue {\n name: string;\n value: string | undefined;\n onChange: (value: string) => void;\n}\n\nconst RadioGroupContext = createContext<RadioGroupContextValue | null>(null);\n\nexport const RadioGroupProvider = RadioGroupContext.Provider;\n\nexport function useRadioGroupContext(): RadioGroupContextValue | null {\n return useContext(RadioGroupContext);\n}\n","import {\n UnstyledRadio,\n type UnstyledRadioProps,\n} from '@asnewyla/unstyled-radio';\nimport { type ForwardedRef, forwardRef, useId } from 'react';\nimport { useRadioGroupContext } from './RadioGroupContext';\nimport './Radio.css';\n\nexport type RadioProps = Omit<UnstyledRadioProps, 'invalid'> & {\n label: string;\n error?: string;\n};\n\nfunction RadioInner(\n {\n id,\n label,\n error,\n disabled,\n className,\n checked,\n onChange,\n value,\n name,\n ...rest\n }: RadioProps,\n ref: ForwardedRef<HTMLInputElement>,\n) {\n const group = useRadioGroupContext();\n const generatedId = useId();\n const radioId = id ?? generatedId;\n const errorId = `${radioId}-error`;\n\n // Own `checked` wins if given (same prop ?? group?.x precedence Button\n // uses for ButtonGroupContext); otherwise, inside a RadioGroup, checked\n // is derived from whether this radio's own value matches the group's\n // selected value — that's the group's whole point: one shared source of\n // truth instead of several independently-tracked radios.\n const resolvedChecked =\n checked ?? (group ? group.value === value : undefined);\n const resolvedName = name ?? group?.name;\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n if (group && value !== undefined) {\n group.onChange(String(value));\n }\n };\n\n return (\n <div className=\"xd-radio\">\n <UnstyledRadio\n id={radioId}\n {...rest}\n name={resolvedName}\n value={value}\n checked={resolvedChecked}\n onChange={handleChange}\n invalid={!!error}\n disabled={disabled}\n aria-describedby={error ? errorId : undefined}\n className={className ? `xd-radio-input ${className}` : 'xd-radio-input'}\n ref={ref}\n />\n <label className=\"xd-radio-label\" htmlFor={radioId}>\n {label}\n </label>\n {error && (\n <div className=\"xd-radio-error\" id={errorId}>\n {error}\n </div>\n )}\n </div>\n );\n}\n\nexport const Radio = forwardRef<HTMLInputElement, RadioProps>(RadioInner);\n"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@asnewyla/radio",
3
+ "version": "0.2.0",
4
+ "description": "Styled radio with label, validation error display, and accessibility",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/xavierDelaFuente/xd-components.git",
9
+ "directory": "packages/radio"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "main": "dist/index.js",
15
+ "module": "dist/index.mjs",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.mjs",
21
+ "require": "./dist/index.js"
22
+ },
23
+ "./styles.css": "./dist/index.css"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "sideEffects": [
29
+ "*.css"
30
+ ],
31
+ "keywords": [
32
+ "react",
33
+ "radio",
34
+ "form",
35
+ "styled",
36
+ "accessible"
37
+ ],
38
+ "peerDependencies": {
39
+ "react": "^18.0.0 || ^19.0.0",
40
+ "react-dom": "^18.0.0 || ^19.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@asnewyla/unstyled-radio": "0.2.0"
44
+ },
45
+ "devDependencies": {
46
+ "@testing-library/jest-dom": "^6.4.0",
47
+ "@testing-library/react": "^16.0.0",
48
+ "@testing-library/user-event": "^14.5.0",
49
+ "@vitest/coverage-v8": "^2.0.0",
50
+ "jsdom": "^24.0.0",
51
+ "react": "^18.3.0",
52
+ "react-dom": "^18.3.0",
53
+ "tsup": "^8.1.0",
54
+ "typescript": "^5.5.0",
55
+ "vitest": "^2.0.0"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "test:coverage": "vitest run --coverage",
62
+ "type-check": "tsc --noEmit"
63
+ }
64
+ }