@gridui/radio-group 0.0.0-stage → 1.0.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/README.md CHANGED
@@ -1,3 +1,94 @@
1
- # Temporary Holding Version
1
+ # Radio group
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@gridui/radio-group` owns `RadioGroup`, `Radio`, and `RadioCard` as one
4
+ single-selection family. Import from the package root and load
5
+ `@gridui/radio-group/styles.css` after `@gridui/theme/styles.css`.
6
+ The package does not depend on Controls or application layout.
7
+
8
+ ## Selection and forms
9
+
10
+ `RadioGroup` retains string `value` / `onValueChange` for controlled state and
11
+ `defaultValue` for local state. `onValueChange(value, details)` receives the native
12
+ `details.event`; `details.cancel()` prevents an uncontrolled selection update.
13
+ Controlled callers own their state and must not commit a canceled change.
14
+ The shared event protocol comes from `@gridui/interaction`.
15
+
16
+ The group forwards `name`, `form`, `required`, `disabled`, orientation, loop, refs,
17
+ and native accessibility props. `readOnly` permits focus without changing the
18
+ selection. Items also respect disabled fieldsets. Native form reset restores the
19
+ initial uncontrolled value unless the reset event is canceled; controlled callers
20
+ reset their own state. Radix owns radio semantics, hidden form inputs, and keyboard
21
+ navigation. Provide an accessible group name independently of item labels.
22
+
23
+ ```tsx
24
+ import { Radio, RadioGroup } from "@gridui/radio-group";
25
+
26
+ <RadioGroup name="delivery" defaultValue="standard" aria-label="Delivery service">
27
+ <label><Radio value="standard" /> Standard delivery</label>
28
+ <label><Radio value="express" /> Express delivery</label>
29
+ </RadioGroup>
30
+ ```
31
+
32
+ Use comfortable visible label hit areas for compact `Radio` controls. `Radio`
33
+ and `RadioCard` can coexist in one group and share the same selection context.
34
+
35
+ ## Rich choices
36
+
37
+ `RadioCard` adds a required `label`, optional `description`, optional decorative
38
+ `leading` node, and `variant="outlined" | "plain"` to `RadioProps`, excluding
39
+ `asChild`. The default is `outlined`. Cards remain native radio buttons, not links
40
+ or independently managed toggles. Selection, form ownership, and callbacks stay
41
+ on `RadioGroup`; cards do not own columns or a second selection model.
42
+
43
+ The label and description have separate generated associations. Explicit
44
+ `aria-label` or `aria-labelledby` overrides the generated name. Supplied
45
+ `aria-describedby` is combined with the description association. `leading` is
46
+ hidden from assistive technology and must be decorative. Keep every slot free of
47
+ nested links, buttons, and inputs; essential information belongs in the label or
48
+ description. All slots accept phrasing content. Native refs point to the radio
49
+ button, and caller click/focus handlers can prevent a selection request.
50
+
51
+ ```tsx
52
+ <RadioGroup defaultValue="standard" aria-label="Delivery service">
53
+ <RadioCard value="standard" label="Standard delivery"
54
+ description="Delivered in three to five working days." />
55
+ <RadioCard value="express" label="Express delivery"
56
+ description="Delivered on the next working day." />
57
+ </RadioGroup>
58
+ ```
59
+
60
+ ## Shared grid boundaries
61
+
62
+ The outlined variant owns its functional border. The plain variant adds no outer
63
+ border declarations, so `GridItem asChild` retains its structural seams. It does
64
+ not apply `border: 0`, inspect Grid internals, or infer an enclosing layout.
65
+ Both variants retain the themed background so a grid seam surface does not show
66
+ through the card body.
67
+ `RadioGroup asChild` delegates layout to its child without imposing the group's
68
+ default vertical flex arrangement. Grid remains responsible for responsive tracks,
69
+ shared boundaries, and optional cell padding.
70
+
71
+ ```tsx
72
+ import { Grid, GridItem } from "@gridui/grid";
73
+ import { RadioCard, RadioGroup } from "@gridui/radio-group";
74
+
75
+ <RadioGroup asChild defaultValue="standard" aria-label="Delivery service">
76
+ <Grid columns={{ base: 1, md: 2 }} frame="cells">
77
+ <GridItem asChild padding={16}>
78
+ <RadioCard value="standard" variant="plain" label="Standard"
79
+ description="Three to five working days." />
80
+ </GridItem>
81
+ <GridItem asChild padding={16}>
82
+ <RadioCard value="express" variant="plain" label="Express"
83
+ description="Next working day." />
84
+ </GridItem>
85
+ </Grid>
86
+ </RadioGroup>
87
+ ```
88
+
89
+ ## Migration
90
+
91
+ Import `Radio`, `RadioGroup`, and their prop types from `@gridui/radio-group`
92
+ instead of `@gridui/controls`, and load the new stylesheet. Controls no longer
93
+ contains or re-exports this family. Existing values, callbacks, refs, forms,
94
+ read-only behavior, and keyboard contracts remain available.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Radio Group
4
+ * @namespace Root
5
+ * @description Context
6
+ */
7
+ export declare const RadioContext: import("react").Context<{
8
+ readonly readOnly: boolean;
9
+ readonly change: (value: string, event: Event) => void;
10
+ readonly navigation: {
11
+ current: Event | null;
12
+ };
13
+ } | null>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Radio Group
4
+ * @namespace Root
5
+ * @description Index
6
+ */
7
+ export type { RadioProps } from "./radio";
8
+ export { Radio } from "./radio";
9
+ export type { RadioCardProps } from "./radio-card";
10
+ export { RadioCard } from "./radio-card";
11
+ export type { RadioGroupProps } from "./radio-group";
12
+ export { RadioGroup } from "./radio-group";
package/dist/index.js ADDED
@@ -0,0 +1,252 @@
1
+ // src/style.ts
2
+ import { cva } from "class-variance-authority";
3
+ var radioGroupStyles = cva("flex flex-col gap-gridui-8");
4
+ var radioStyles = cva("inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full border border-control-border bg-background text-foreground align-middle data-[state=checked]:bg-accent-background data-[state=checked]:text-accent-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50 data-readonly:cursor-default aria-invalid:border-danger-foreground");
5
+ var radioCardStyles = cva("gridui-radio-card", {
6
+ variants: {
7
+ variant: {
8
+ outlined: "gridui-radio-card-outlined",
9
+ plain: "gridui-radio-card-plain"
10
+ }
11
+ },
12
+ defaultVariants: { variant: "outlined" }
13
+ });
14
+
15
+ // src/context.ts
16
+ import { createContext } from "react";
17
+ var RadioContext = createContext(null);
18
+
19
+ // src/use-radio-item.ts
20
+ import { useContext } from "react";
21
+ var useRadioItem = (props) => {
22
+ const context = useContext(RadioContext);
23
+ const onFocus = (event) => {
24
+ props.onFocus?.(event);
25
+ const navigation = context?.navigation.current;
26
+ if (navigation) {
27
+ context.navigation.current = null;
28
+ if (!event.defaultPrevented && !event.currentTarget.matches(":disabled")) {
29
+ context.change(props.value, navigation);
30
+ }
31
+ event.preventDefault();
32
+ }
33
+ };
34
+ const onClick = (event) => {
35
+ props.onClick?.(event);
36
+ if (!event.defaultPrevented && !event.currentTarget.matches(":disabled")) {
37
+ context?.change(props.value, event.nativeEvent);
38
+ }
39
+ event.preventDefault();
40
+ };
41
+ return {
42
+ readOnly: context?.readOnly ?? false,
43
+ onFocus,
44
+ onClick
45
+ };
46
+ };
47
+
48
+ // src/radio.tsx
49
+ import { cn } from "@gridui/style-utils";
50
+ import * as Primitive from "@radix-ui/react-radio-group";
51
+ import { jsx } from "react/jsx-runtime";
52
+ var Radio = (props) => {
53
+ const { className, ...restProps } = props;
54
+ const events = useRadioItem(props);
55
+ return /* @__PURE__ */ jsx(
56
+ Primitive.Item,
57
+ {
58
+ ...restProps,
59
+ className: cn(radioStyles(), className),
60
+ "data-readonly": events.readOnly ? "" : void 0,
61
+ onFocus: events.onFocus,
62
+ onClick: events.onClick,
63
+ children: /* @__PURE__ */ jsx(
64
+ Primitive.Indicator,
65
+ {
66
+ className: "size-2 rounded-full bg-current"
67
+ }
68
+ )
69
+ }
70
+ );
71
+ };
72
+
73
+ // src/radio-card.tsx
74
+ import { cn as cn2 } from "@gridui/style-utils";
75
+ import * as Primitive2 from "@radix-ui/react-radio-group";
76
+ import { useId } from "react";
77
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
78
+ var RadioCard = (props) => {
79
+ const {
80
+ label,
81
+ description,
82
+ leading,
83
+ variant = "outlined",
84
+ className,
85
+ "aria-label": ariaLabel,
86
+ "aria-labelledby": ariaLabelledBy,
87
+ "aria-describedby": ariaDescribedBy,
88
+ ...restProps
89
+ } = props;
90
+ const labelId = useId();
91
+ const descriptionId = useId();
92
+ const described = description !== void 0 && description !== null && description !== false;
93
+ const descriptionIds = [
94
+ ariaDescribedBy,
95
+ described ? descriptionId : void 0
96
+ ].filter(Boolean).join(" ") || void 0;
97
+ const events = useRadioItem(props);
98
+ return /* @__PURE__ */ jsxs(
99
+ Primitive2.Item,
100
+ {
101
+ ...restProps,
102
+ className: cn2(radioCardStyles({ variant }), className),
103
+ "aria-label": ariaLabel,
104
+ "aria-labelledby": ariaLabelledBy ?? (ariaLabel ? void 0 : labelId),
105
+ "aria-describedby": descriptionIds,
106
+ "data-readonly": events.readOnly ? "" : void 0,
107
+ onFocus: events.onFocus,
108
+ onClick: events.onClick,
109
+ children: [
110
+ leading !== void 0 && leading !== null && /* @__PURE__ */ jsx2(
111
+ "span",
112
+ {
113
+ className: "gridui-radio-card-leading",
114
+ "aria-hidden": "true",
115
+ children: leading
116
+ }
117
+ ),
118
+ /* @__PURE__ */ jsxs(
119
+ "span",
120
+ {
121
+ className: "gridui-radio-card-content",
122
+ children: [
123
+ /* @__PURE__ */ jsx2(
124
+ "span",
125
+ {
126
+ className: "type-ui",
127
+ id: labelId,
128
+ children: label
129
+ }
130
+ ),
131
+ described && /* @__PURE__ */ jsx2(
132
+ "span",
133
+ {
134
+ className: "gridui-radio-card-description type-supporting",
135
+ id: descriptionId,
136
+ children: description
137
+ }
138
+ )
139
+ ]
140
+ }
141
+ ),
142
+ /* @__PURE__ */ jsx2(
143
+ "span",
144
+ {
145
+ className: "gridui-radio-card-indicator",
146
+ "aria-hidden": "true",
147
+ children: /* @__PURE__ */ jsx2(
148
+ Primitive2.Indicator,
149
+ {
150
+ className: "gridui-radio-card-dot"
151
+ }
152
+ )
153
+ }
154
+ )
155
+ ]
156
+ }
157
+ );
158
+ };
159
+
160
+ // src/radio-group.tsx
161
+ import { changeDetails } from "@gridui/interaction";
162
+ import { cn as cn3 } from "@gridui/style-utils";
163
+ import * as Primitive3 from "@radix-ui/react-radio-group";
164
+ import { useEffect, useImperativeHandle, useRef, useState } from "react";
165
+ import { jsx as jsx3 } from "react/jsx-runtime";
166
+ var RadioGroup = (props) => {
167
+ const { asChild, className, value, defaultValue, onValueChange, readOnly = false, ref, onKeyDownCapture, onPointerDownCapture, ...restProps } = props;
168
+ const root = useRef(null);
169
+ const navigation = useRef(null);
170
+ const [
171
+ internalValue,
172
+ setInternalValue
173
+ ] = useState(defaultValue ?? null);
174
+ const initial = useRef(defaultValue ?? null);
175
+ useImperativeHandle(ref, () => {
176
+ return root.current;
177
+ });
178
+ useEffect(() => {
179
+ const form = restProps.form ? document.getElementById(restProps.form) : root.current?.closest("form");
180
+ if (!form || value !== void 0) {
181
+ return;
182
+ }
183
+ let mounted = true;
184
+ const reset = (event) => {
185
+ setTimeout(() => {
186
+ if (mounted && !event.defaultPrevented) {
187
+ setInternalValue(initial.current);
188
+ }
189
+ }, 0);
190
+ };
191
+ form.addEventListener("reset", reset);
192
+ return () => {
193
+ mounted = false;
194
+ form.removeEventListener("reset", reset);
195
+ };
196
+ }, [
197
+ restProps.form,
198
+ value
199
+ ]);
200
+ const change = (next, event) => {
201
+ if (readOnly || restProps.disabled) {
202
+ return;
203
+ }
204
+ const details = changeDetails(event);
205
+ onValueChange?.(next, details);
206
+ if (!details.isCanceled && value === void 0) {
207
+ setInternalValue(next);
208
+ }
209
+ };
210
+ return /* @__PURE__ */ jsx3(
211
+ RadioContext.Provider,
212
+ {
213
+ value: {
214
+ readOnly,
215
+ change,
216
+ navigation
217
+ },
218
+ children: /* @__PURE__ */ jsx3(
219
+ Primitive3.Root,
220
+ {
221
+ ...restProps,
222
+ asChild,
223
+ ref: root,
224
+ value: value === void 0 ? internalValue : value,
225
+ onKeyDownCapture: (event) => {
226
+ onKeyDownCapture?.(event);
227
+ navigation.current = !event.defaultPrevented && [
228
+ "ArrowUp",
229
+ "ArrowDown",
230
+ "ArrowLeft",
231
+ "ArrowRight",
232
+ "Home",
233
+ "End"
234
+ ].includes(event.key) ? event.nativeEvent : null;
235
+ },
236
+ onPointerDownCapture: (event) => {
237
+ navigation.current = null;
238
+ onPointerDownCapture?.(event);
239
+ },
240
+ "aria-readonly": readOnly,
241
+ className: cn3(asChild ? void 0 : radioGroupStyles(), className)
242
+ }
243
+ )
244
+ }
245
+ );
246
+ };
247
+ export {
248
+ Radio,
249
+ RadioCard,
250
+ RadioGroup
251
+ };
252
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/style.ts", "../src/context.ts", "../src/use-radio-item.ts", "../src/radio.tsx", "../src/radio-card.tsx", "../src/radio-group.tsx"],
4
+ "sourcesContent": ["/**\n * @author WMXPY\n * @package Radio Group\n * @namespace Root\n * @description Style\n */\n\nimport { cva } from \"class-variance-authority\";\n\nexport const radioGroupStyles = cva(\"flex flex-col gap-gridui-8\");\nexport const radioStyles = cva(\"inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full border border-control-border bg-background text-foreground align-middle data-[state=checked]:bg-accent-background data-[state=checked]:text-accent-foreground data-disabled:cursor-not-allowed data-disabled:opacity-50 data-readonly:cursor-default aria-invalid:border-danger-foreground\");\nexport const radioCardStyles = cva(\"gridui-radio-card\", {\n variants: {\n variant: {\n outlined: \"gridui-radio-card-outlined\",\n plain: \"gridui-radio-card-plain\",\n },\n },\n defaultVariants: { variant: \"outlined\" },\n});\n", "/**\n * @author WMXPY\n * @package Radio Group\n * @namespace Root\n * @description Context\n */\n\nimport { createContext } from \"react\";\n\nexport const RadioContext = createContext<{\n readonly readOnly: boolean;\n readonly change: (value: string, event: Event) => void;\n readonly navigation: {\n current: Event | null;\n };\n} | null>(null);\n", "/**\n * @author WMXPY\n * @package Radio Group\n * @namespace Root\n * @description Use Radio Item\n */\n\nimport { RadioContext } from \"./context\";\nimport type * as Primitive from \"@radix-ui/react-radio-group\";\nimport type { ComponentPropsWithRef, FocusEvent, MouseEvent } from \"react\";\nimport { useContext } from \"react\";\n\ntype RadioItemEvents = Pick<ComponentPropsWithRef<typeof Primitive.Item>, \"onClick\" | \"onFocus\" | \"value\">;\n\nexport const useRadioItem = (props: RadioItemEvents) => {\n const context = useContext(RadioContext);\n const onFocus = (event: FocusEvent<HTMLButtonElement>): void => {\n props.onFocus?.(event);\n const navigation = context?.navigation.current;\n if (navigation) {\n context.navigation.current = null;\n if (!event.defaultPrevented && !event.currentTarget.matches(\":disabled\")) {\n context.change(props.value, navigation);\n }\n event.preventDefault();\n }\n };\n const onClick = (event: MouseEvent<HTMLButtonElement>): void => {\n props.onClick?.(event);\n if (!event.defaultPrevented && !event.currentTarget.matches(\":disabled\")) {\n context?.change(props.value, event.nativeEvent);\n }\n event.preventDefault();\n };\n return {\n readOnly: context?.readOnly ?? false,\n onFocus,\n onClick,\n };\n};\n", "/**\n * @author WMXPY\n * @package Radio Group\n * @namespace Root\n * @description Radio\n */\n\nimport { radioStyles } from \"./style\";\nimport { useRadioItem } from \"./use-radio-item\";\nimport { cn } from \"@gridui/style-utils\";\nimport * as Primitive from \"@radix-ui/react-radio-group\";\nimport type { ComponentPropsWithRef, FC } from \"react\";\n\nexport type RadioProps = Omit<ComponentPropsWithRef<typeof Primitive.Item>, \"children\">;\n\nexport const Radio: FC<RadioProps> = (props: RadioProps) => {\n const { className, ...restProps } = props;\n const events = useRadioItem(props);\n return (<Primitive.Item\n {...restProps}\n className={cn(radioStyles(), className)}\n data-readonly={events.readOnly ? \"\" : undefined}\n onFocus={events.onFocus}\n onClick={events.onClick}\n >\n <Primitive.Indicator\n className=\"size-2 rounded-full bg-current\"\n />\n </Primitive.Item>);\n};\n", "/**\n * @author WMXPY\n * @package Radio Group\n * @namespace Root\n * @description Radio Card\n */\n\nimport type { RadioProps } from \"./radio\";\nimport { radioCardStyles } from \"./style\";\nimport { useRadioItem } from \"./use-radio-item\";\nimport { cn } from \"@gridui/style-utils\";\nimport * as Primitive from \"@radix-ui/react-radio-group\";\nimport type { FC, ReactNode } from \"react\";\nimport { useId } from \"react\";\n\nexport type RadioCardProps = Omit<RadioProps, \"asChild\"> & {\n readonly label: ReactNode;\n readonly description?: ReactNode;\n readonly leading?: ReactNode;\n readonly variant?: \"outlined\" | \"plain\";\n};\n\nexport const RadioCard: FC<RadioCardProps> = (props: RadioCardProps) => {\n const {\n label, description, leading, variant = \"outlined\", className,\n \"aria-label\": ariaLabel, \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy, ...restProps\n } = props;\n const labelId = useId();\n const descriptionId = useId();\n const described = description !== undefined && description !== null && description !== false;\n const descriptionIds = [\n ariaDescribedBy,\n described ? descriptionId : undefined,\n ].filter(Boolean).join(\" \") || undefined;\n const events = useRadioItem(props);\n return (<Primitive.Item\n {...restProps}\n className={cn(radioCardStyles({ variant }), className)}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy ?? (ariaLabel ? undefined : labelId)}\n aria-describedby={descriptionIds}\n data-readonly={events.readOnly ? \"\" : undefined}\n onFocus={events.onFocus}\n onClick={events.onClick}\n >\n {leading !== undefined && leading !== null && (<span\n className=\"gridui-radio-card-leading\"\n aria-hidden=\"true\"\n >\n {leading}\n </span>)}\n <span\n className=\"gridui-radio-card-content\"\n >\n <span\n className=\"type-ui\"\n id={labelId}\n >\n {label}\n </span>\n {described && (<span\n className=\"gridui-radio-card-description type-supporting\"\n id={descriptionId}\n >\n {description}\n </span>)}\n </span>\n <span\n className=\"gridui-radio-card-indicator\"\n aria-hidden=\"true\"\n >\n <Primitive.Indicator\n className=\"gridui-radio-card-dot\"\n />\n </span>\n </Primitive.Item>);\n};\n", "/**\n * @author WMXPY\n * @package Radio Group\n * @namespace Root\n * @description Radio Group\n */\n\nimport { RadioContext } from \"./context\";\nimport { radioGroupStyles } from \"./style\";\nimport type { ChangeDetails } from \"@gridui/interaction\";\nimport { changeDetails } from \"@gridui/interaction\";\nimport { cn } from \"@gridui/style-utils\";\nimport * as Primitive from \"@radix-ui/react-radio-group\";\nimport type { ComponentPropsWithRef, FC } from \"react\";\nimport { useEffect, useImperativeHandle, useRef, useState } from \"react\";\n\nexport type RadioGroupProps = Omit<ComponentPropsWithRef<typeof Primitive.Root>, \"onValueChange\" | \"defaultValue\"> & {\n readonly defaultValue?: string | null | undefined;\n readonly readOnly?: boolean | undefined;\n readonly onValueChange?: ((value: string, details: ChangeDetails) => void) | undefined;\n};\nexport const RadioGroup: FC<RadioGroupProps> = (props: RadioGroupProps) => {\n const { asChild, className, value, defaultValue, onValueChange, readOnly = false, ref, onKeyDownCapture, onPointerDownCapture, ...restProps } = props;\n const root = useRef<HTMLDivElement>(null);\n const navigation = useRef<Event | null>(null);\n const [\n internalValue,\n setInternalValue,\n ] = useState<string | null>(defaultValue ?? null);\n const initial = useRef(defaultValue ?? null);\n useImperativeHandle(ref, () => {\n return root.current as HTMLDivElement;\n });\n useEffect(() => {\n const form = restProps.form ? document.getElementById(restProps.form) : root.current?.closest(\"form\");\n if (!form || value !== undefined) {\n return;\n }\n let mounted = true;\n const reset = (event: Event) => {\n // Wait until React bubbling handlers can cancel the native reset.\n setTimeout(() => {\n if (mounted && !event.defaultPrevented) {\n setInternalValue(initial.current);\n }\n }, 0);\n };\n form.addEventListener(\"reset\", reset);\n return () => {\n mounted = false;\n form.removeEventListener(\"reset\", reset);\n };\n }, [\n restProps.form,\n value,\n ]);\n const change = (next: string, event: Event) => {\n if (readOnly || restProps.disabled) {\n return;\n }\n const details = changeDetails(event);\n onValueChange?.(next, details);\n if (!details.isCanceled && value === undefined) {\n setInternalValue(next);\n }\n };\n return (<RadioContext.Provider\n value={{\n readOnly,\n change,\n navigation,\n }}\n >\n <Primitive.Root\n {...restProps}\n asChild={asChild}\n ref={root}\n value={value === undefined ? internalValue : value}\n onKeyDownCapture={(event) => {\n onKeyDownCapture?.(event);\n navigation.current = !event.defaultPrevented && [\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n ].includes(event.key)\n ? event.nativeEvent : null;\n }}\n onPointerDownCapture={(event) => {\n navigation.current = null; onPointerDownCapture?.(event);\n }}\n aria-readonly={readOnly}\n className={cn(asChild ? undefined : radioGroupStyles(), className)}\n />\n </RadioContext.Provider>);\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["cn", "Primitive", "jsx", "cn", "Primitive", "jsx"]
7
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Radio Group
4
+ * @namespace Root
5
+ * @description Radio Card
6
+ */
7
+ import type { RadioProps } from "./radio";
8
+ import type { FC, ReactNode } from "react";
9
+ export type RadioCardProps = Omit<RadioProps, "asChild"> & {
10
+ readonly label: ReactNode;
11
+ readonly description?: ReactNode;
12
+ readonly leading?: ReactNode;
13
+ readonly variant?: "outlined" | "plain";
14
+ };
15
+ export declare const RadioCard: FC<RadioCardProps>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Radio Group
4
+ * @namespace Root
5
+ * @description Radio Group
6
+ */
7
+ import type { ChangeDetails } from "@gridui/interaction";
8
+ import * as Primitive from "@radix-ui/react-radio-group";
9
+ import type { ComponentPropsWithRef, FC } from "react";
10
+ export type RadioGroupProps = Omit<ComponentPropsWithRef<typeof Primitive.Root>, "onValueChange" | "defaultValue"> & {
11
+ readonly defaultValue?: string | null | undefined;
12
+ readonly readOnly?: boolean | undefined;
13
+ readonly onValueChange?: ((value: string, details: ChangeDetails) => void) | undefined;
14
+ };
15
+ export declare const RadioGroup: FC<RadioGroupProps>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Radio Group
4
+ * @namespace Root
5
+ * @description Radio
6
+ */
7
+ import * as Primitive from "@radix-ui/react-radio-group";
8
+ import type { ComponentPropsWithRef, FC } from "react";
9
+ export type RadioProps = Omit<ComponentPropsWithRef<typeof Primitive.Item>, "children">;
10
+ export declare const Radio: FC<RadioProps>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Radio Group
4
+ * @namespace Root
5
+ * @description Style
6
+ */
7
+ export declare const radioGroupStyles: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
8
+ export declare const radioStyles: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
9
+ export declare const radioCardStyles: (props?: ({
10
+ variant?: "outlined" | "plain" | null | undefined;
11
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -0,0 +1,185 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties;
3
+ @layer theme, base, components, utilities;
4
+ @layer theme {
5
+ :root, :host {
6
+ --spacing: 0.25rem;
7
+ }
8
+ }
9
+ @layer utilities {
10
+ .flex {
11
+ display: flex;
12
+ }
13
+ .inline-flex {
14
+ display: inline-flex;
15
+ }
16
+ .size-2 {
17
+ width: calc(var(--spacing) * 2);
18
+ height: calc(var(--spacing) * 2);
19
+ }
20
+ .size-5 {
21
+ width: calc(var(--spacing) * 5);
22
+ height: calc(var(--spacing) * 5);
23
+ }
24
+ .shrink-0 {
25
+ flex-shrink: 0;
26
+ }
27
+ .cursor-pointer {
28
+ cursor: pointer;
29
+ }
30
+ .flex-col {
31
+ flex-direction: column;
32
+ }
33
+ .items-center {
34
+ align-items: center;
35
+ }
36
+ .justify-center {
37
+ justify-content: center;
38
+ }
39
+ .gap-gridui-8 {
40
+ gap: var(--gridui-space-8);
41
+ }
42
+ .rounded-full {
43
+ border-radius: calc(infinity * 1px);
44
+ }
45
+ .border {
46
+ border-style: var(--tw-border-style);
47
+ border-width: 1px;
48
+ }
49
+ .border-control-border {
50
+ border-color: var(--gridui-control-border);
51
+ }
52
+ .bg-background {
53
+ background-color: var(--gridui-background);
54
+ }
55
+ .bg-current {
56
+ background-color: currentcolor;
57
+ }
58
+ .align-middle {
59
+ vertical-align: middle;
60
+ }
61
+ .type-supporting {
62
+ font: var(--gridui-type-supporting);
63
+ letter-spacing: normal;
64
+ }
65
+ .type-ui {
66
+ font: var(--gridui-type-ui);
67
+ letter-spacing: normal;
68
+ }
69
+ .text-foreground {
70
+ color: var(--gridui-foreground);
71
+ }
72
+ .aria-invalid\:border-danger-foreground[aria-invalid="true"] {
73
+ border-color: var(--gridui-danger-foreground);
74
+ }
75
+ .data-disabled\:cursor-not-allowed[data-disabled] {
76
+ cursor: not-allowed;
77
+ }
78
+ .data-disabled\:opacity-50[data-disabled] {
79
+ opacity: 50%;
80
+ }
81
+ .data-readonly\:cursor-default[data-readonly] {
82
+ cursor: default;
83
+ }
84
+ .data-\[state\=checked\]\:bg-accent-background[data-state="checked"] {
85
+ background-color: var(--gridui-accent-background);
86
+ }
87
+ .data-\[state\=checked\]\:text-accent-foreground[data-state="checked"] {
88
+ color: var(--gridui-accent-foreground);
89
+ }
90
+ }
91
+ @layer components {
92
+ .gridui-radio-card {
93
+ box-sizing: border-box;
94
+ position: relative;
95
+ display: flex;
96
+ align-items: flex-start;
97
+ gap: var(--gridui-space-16);
98
+ width: 100%;
99
+ min-width: 0;
100
+ min-height: 2.75rem;
101
+ padding: var(--gridui-space-16);
102
+ background: var(--gridui-background);
103
+ color: var(--gridui-foreground);
104
+ text-align: start;
105
+ cursor: pointer;
106
+ transition: background var(--gridui-motion-duration-quick) var(--gridui-motion-ease-out);
107
+ }
108
+ .gridui-radio-card-outlined {
109
+ border: 1px solid var(--gridui-control-border);
110
+ border-radius: var(--gridui-control-radius);
111
+ }
112
+ .gridui-radio-card[data-state="checked"] {
113
+ background: var(--gridui-surface);
114
+ }
115
+ .gridui-radio-card:disabled {
116
+ cursor: not-allowed;
117
+ opacity: 0.5;
118
+ }
119
+ .gridui-radio-card[data-readonly] {
120
+ cursor: default;
121
+ }
122
+ .gridui-radio-card:not(:disabled):not([data-readonly]):hover {
123
+ background: var(--gridui-surface-hover);
124
+ }
125
+ .gridui-radio-card-content {
126
+ display: flex;
127
+ flex: 1;
128
+ flex-direction: column;
129
+ gap: var(--gridui-space-8);
130
+ min-width: 0;
131
+ }
132
+ .gridui-radio-card-description, .gridui-radio-card-leading {
133
+ color: var(--gridui-muted);
134
+ }
135
+ .gridui-radio-card-leading {
136
+ display: inline-flex;
137
+ flex-shrink: 0;
138
+ }
139
+ .gridui-radio-card-leading svg {
140
+ width: 1.25rem;
141
+ height: 1.25rem;
142
+ }
143
+ .gridui-radio-card-indicator {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ flex-shrink: 0;
148
+ width: 1.25rem;
149
+ height: 1.25rem;
150
+ border: 1px solid var(--gridui-control-border);
151
+ border-radius: 50%;
152
+ background: var(--gridui-background);
153
+ }
154
+ .gridui-radio-card[data-state="checked"] .gridui-radio-card-indicator {
155
+ border-color: var(--gridui-accent-background);
156
+ background: var(--gridui-accent-background);
157
+ color: var(--gridui-accent-foreground);
158
+ }
159
+ .gridui-radio-card[aria-invalid="true"] .gridui-radio-card-indicator {
160
+ border-color: var(--gridui-danger-foreground);
161
+ }
162
+ .gridui-radio-card-dot {
163
+ width: 0.5rem;
164
+ height: 0.5rem;
165
+ border-radius: 50%;
166
+ background: currentcolor;
167
+ }
168
+ @media (prefers-reduced-motion: reduce) {
169
+ .gridui-radio-card {
170
+ transition: none;
171
+ }
172
+ }
173
+ }
174
+ @property --tw-border-style {
175
+ syntax: "*";
176
+ inherits: false;
177
+ initial-value: solid;
178
+ }
179
+ @layer properties {
180
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
181
+ *, ::before, ::after, ::backdrop {
182
+ --tw-border-style: solid;
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Radio Group
4
+ * @namespace Root
5
+ * @description Use Radio Item
6
+ */
7
+ import type * as Primitive from "@radix-ui/react-radio-group";
8
+ import type { ComponentPropsWithRef, FocusEvent, MouseEvent } from "react";
9
+ type RadioItemEvents = Pick<ComponentPropsWithRef<typeof Primitive.Item>, "onClick" | "onFocus" | "value">;
10
+ export declare const useRadioItem: (props: RadioItemEvents) => {
11
+ readOnly: boolean;
12
+ onFocus: (event: FocusEvent<HTMLButtonElement>) => void;
13
+ onClick: (event: MouseEvent<HTMLButtonElement>) => void;
14
+ };
15
+ export {};
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@gridui/radio-group",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "GridUI radio groups and rich single-choice cards",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/sudo-ui/GridUI-React-Monorepo.git",
10
+ "directory": "packages/radio-group"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "sideEffects": [
17
+ "**/*.css"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./styles.css": "./dist/styles.css"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@gridui/interaction": "^1.0.0",
31
+ "@gridui/style-utils": "^2.1.1",
32
+ "@gridui/theme": "^2.1.1",
33
+ "@radix-ui/react-radio-group": "1.5.1",
34
+ "class-variance-authority": "0.7.1"
35
+ },
36
+ "devDependencies": {
37
+ "@gridui-internal/configuration": "0.0.0",
38
+ "@gridui/grid": "2.1.2",
39
+ "@jest/globals": "30.5.2",
40
+ "@storybook/react-vite": "10.6.3",
41
+ "@types/jest": "30.0.0",
42
+ "@types/node": "26.6.5",
43
+ "@types/react": "19.3.0",
44
+ "@types/react-dom": "19.3.0",
45
+ "jest": "30.5.2",
46
+ "react": "19.3.1",
47
+ "react-dom": "19.3.1",
48
+ "typescript": "6.0.3"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^19.0.0",
52
+ "react-dom": "^19.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "node ../../scripts/compilation/package.ts",
56
+ "compile": "tsc --project tsconfig.json",
57
+ "test": "jest",
58
+ "coverage": "jest --coverage"
59
+ }
6
60
  }