@djangocfg/ui-core 2.1.544 → 2.1.546
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.546",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"check:contrast": "node scripts/check-preset-contrast.mjs"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
|
-
"@djangocfg/i18n": "^2.1.
|
|
133
|
+
"@djangocfg/i18n": "^2.1.546",
|
|
134
134
|
"consola": "^3.4.2",
|
|
135
135
|
"lucide-react": "^0.545.0",
|
|
136
136
|
"moment": "^2.30.1",
|
|
@@ -206,9 +206,9 @@
|
|
|
206
206
|
"vaul": "1.1.2"
|
|
207
207
|
},
|
|
208
208
|
"devDependencies": {
|
|
209
|
-
"@djangocfg/eslint-config": "^2.1.
|
|
210
|
-
"@djangocfg/i18n": "^2.1.
|
|
211
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
209
|
+
"@djangocfg/eslint-config": "^2.1.546",
|
|
210
|
+
"@djangocfg/i18n": "^2.1.546",
|
|
211
|
+
"@djangocfg/typescript-config": "^2.1.546",
|
|
212
212
|
"@storybook/react-vite": "^10.5.0",
|
|
213
213
|
"@types/node": "^24.13.3",
|
|
214
214
|
"@types/react": "19.2.15",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { Check } from "lucide-react"
|
|
4
3
|
import * as React from "react"
|
|
5
4
|
|
|
6
5
|
import { cn } from "../../../lib/utils"
|
|
6
|
+
import { Checkbox } from "../checkbox"
|
|
7
7
|
|
|
8
8
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
9
|
// Types
|
|
@@ -11,9 +11,13 @@ import { cn } from "../../../lib/utils"
|
|
|
11
11
|
|
|
12
12
|
interface CheckboxGroupContextValue {
|
|
13
13
|
value: string[]
|
|
14
|
-
|
|
14
|
+
toggle: (value: string) => void
|
|
15
15
|
disabled: boolean
|
|
16
16
|
name?: string
|
|
17
|
+
/** Ties `CheckboxGroupLabel` to the group's `aria-labelledby`. */
|
|
18
|
+
labelId: string
|
|
19
|
+
/** Ties every item's `aria-describedby` to the group's description. */
|
|
20
|
+
descriptionId: string
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
const CheckboxGroupContext = React.createContext<CheckboxGroupContextValue | null>(null)
|
|
@@ -45,6 +49,7 @@ function CheckboxGroup({
|
|
|
45
49
|
onValueChange,
|
|
46
50
|
disabled = false,
|
|
47
51
|
name,
|
|
52
|
+
"aria-labelledby": ariaLabelledBy,
|
|
48
53
|
...props
|
|
49
54
|
}: CheckboxGroupProps) {
|
|
50
55
|
const isControlled = valueProp !== undefined
|
|
@@ -52,32 +57,53 @@ function CheckboxGroup({
|
|
|
52
57
|
|
|
53
58
|
const value = isControlled ? valueProp : uncontrolledValue
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
// Ids for the label and description, so the group announces as one thing
|
|
61
|
+
// rather than as a run of unrelated checkboxes.
|
|
62
|
+
const id = React.useId()
|
|
63
|
+
const labelId = `${id}-label`
|
|
64
|
+
const descriptionId = `${id}-description`
|
|
65
|
+
|
|
66
|
+
// The membership rule lives here, not on the item: an item that computed the
|
|
67
|
+
// next array from a captured `value` toggled against a stale snapshot when
|
|
68
|
+
// two arrived in one tick.
|
|
69
|
+
const toggle = React.useCallback(
|
|
70
|
+
(member: string) => {
|
|
71
|
+
const from = isControlled ? valueProp : undefined
|
|
72
|
+
const next = (current: string[]) =>
|
|
73
|
+
current.includes(member)
|
|
74
|
+
? current.filter((entry) => entry !== member)
|
|
75
|
+
: [...current, member]
|
|
76
|
+
|
|
77
|
+
if (isControlled) {
|
|
78
|
+
onValueChange?.(next(from ?? []))
|
|
79
|
+
return
|
|
59
80
|
}
|
|
60
|
-
|
|
81
|
+
setUncontrolledValue((current) => {
|
|
82
|
+
const value = next(current)
|
|
83
|
+
onValueChange?.(value)
|
|
84
|
+
return value
|
|
85
|
+
})
|
|
61
86
|
},
|
|
62
|
-
[isControlled, onValueChange],
|
|
87
|
+
[isControlled, valueProp, onValueChange],
|
|
63
88
|
)
|
|
64
89
|
|
|
65
90
|
const contextValue = React.useMemo<CheckboxGroupContextValue>(
|
|
66
|
-
() => ({
|
|
67
|
-
|
|
68
|
-
onValueChange: handleValueChange,
|
|
69
|
-
disabled,
|
|
70
|
-
name,
|
|
71
|
-
}),
|
|
72
|
-
[value, handleValueChange, disabled, name],
|
|
91
|
+
() => ({ value, toggle, disabled, name, labelId, descriptionId }),
|
|
92
|
+
[value, toggle, disabled, name, labelId, descriptionId],
|
|
73
93
|
)
|
|
74
94
|
|
|
75
95
|
return (
|
|
76
96
|
<CheckboxGroupContext.Provider value={contextValue}>
|
|
97
|
+
{/* `role="group"`: without it a screen reader reads the items as
|
|
98
|
+
unrelated checkboxes and never says what they have in common.
|
|
99
|
+
`CheckboxGroupLabel` names it; a caller passing its own
|
|
100
|
+
`aria-labelledby` wins. */}
|
|
77
101
|
<div
|
|
102
|
+
role="group"
|
|
103
|
+
aria-labelledby={ariaLabelledBy ?? labelId}
|
|
78
104
|
data-slot="checkbox-group"
|
|
79
105
|
data-disabled={disabled ? "" : undefined}
|
|
80
|
-
className={cn("peer flex flex-col gap-
|
|
106
|
+
className={cn("peer flex flex-col gap-2", className)}
|
|
81
107
|
{...props}
|
|
82
108
|
/>
|
|
83
109
|
</CheckboxGroupContext.Provider>
|
|
@@ -88,9 +114,11 @@ function CheckboxGroup({
|
|
|
88
114
|
// CheckboxGroupLabel
|
|
89
115
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
116
|
|
|
91
|
-
function CheckboxGroupLabel({ className, ...props }: React.ComponentProps<"div">) {
|
|
117
|
+
function CheckboxGroupLabel({ className, id, ...props }: React.ComponentProps<"div">) {
|
|
118
|
+
const context = useCheckboxGroupContext("CheckboxGroupLabel")
|
|
92
119
|
return (
|
|
93
120
|
<div
|
|
121
|
+
id={id ?? context.labelId}
|
|
94
122
|
data-slot="checkbox-group-label"
|
|
95
123
|
className={cn(
|
|
96
124
|
"text-foreground/70 text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
@@ -115,12 +143,16 @@ function CheckboxGroupList({
|
|
|
115
143
|
...props
|
|
116
144
|
}: CheckboxGroupListProps) {
|
|
117
145
|
return (
|
|
146
|
+
// Optional: the root already stacks its children, so this exists for the
|
|
147
|
+
// horizontal case and for separating the items from a label and a
|
|
148
|
+
// description that share the root. Same `gap-2` either way — one spacing
|
|
149
|
+
// for one component.
|
|
118
150
|
<div
|
|
119
151
|
data-slot="checkbox-group-list"
|
|
120
152
|
data-orientation={orientation}
|
|
121
153
|
className={cn(
|
|
122
|
-
"flex gap-
|
|
123
|
-
orientation === "horizontal" ? "flex-row" : "flex-col",
|
|
154
|
+
"flex gap-2",
|
|
155
|
+
orientation === "horizontal" ? "flex-row flex-wrap items-center" : "flex-col",
|
|
124
156
|
className,
|
|
125
157
|
)}
|
|
126
158
|
{...props}
|
|
@@ -132,67 +164,58 @@ function CheckboxGroupList({
|
|
|
132
164
|
// CheckboxGroupItem
|
|
133
165
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
134
166
|
|
|
135
|
-
interface CheckboxGroupItemProps
|
|
167
|
+
interface CheckboxGroupItemProps
|
|
168
|
+
extends Omit<React.ComponentProps<typeof Checkbox>, "value" | "checked" | "onCheckedChange"> {
|
|
136
169
|
value: string
|
|
137
170
|
disabled?: boolean
|
|
171
|
+
/** Classes for the ROW. The box itself takes `checkboxClassName`. */
|
|
172
|
+
className?: string
|
|
173
|
+
checkboxClassName?: string
|
|
138
174
|
}
|
|
139
175
|
|
|
140
176
|
function CheckboxGroupItem({
|
|
141
177
|
className,
|
|
178
|
+
checkboxClassName,
|
|
142
179
|
children,
|
|
143
180
|
value,
|
|
144
181
|
disabled: disabledProp,
|
|
145
|
-
onClick,
|
|
146
182
|
...props
|
|
147
183
|
}: CheckboxGroupItemProps) {
|
|
148
184
|
const context = useCheckboxGroupContext("CheckboxGroupItem")
|
|
149
185
|
const isDisabled = disabledProp || context.disabled
|
|
150
186
|
const isChecked = context.value.includes(value)
|
|
151
187
|
|
|
152
|
-
const handleClick = React.useCallback(
|
|
153
|
-
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
154
|
-
onClick?.(event)
|
|
155
|
-
if (isDisabled) return
|
|
156
|
-
|
|
157
|
-
const nextValue = isChecked
|
|
158
|
-
? context.value.filter((v) => v !== value)
|
|
159
|
-
: [...context.value, value]
|
|
160
|
-
|
|
161
|
-
context.onValueChange(nextValue)
|
|
162
|
-
},
|
|
163
|
-
[onClick, isDisabled, isChecked, context, value],
|
|
164
|
-
)
|
|
165
|
-
|
|
166
188
|
return (
|
|
189
|
+
// The whole row is the target, not just the box: a 16px hit area for a
|
|
190
|
+
// choice the row already names is the commonest reason a list like this
|
|
191
|
+
// feels unresponsive. `w-full`, so the row spans its list.
|
|
167
192
|
<label
|
|
193
|
+
data-slot="checkbox-group-row"
|
|
194
|
+
data-state={isChecked ? "checked" : "unchecked"}
|
|
195
|
+
data-disabled={isDisabled ? "" : undefined}
|
|
168
196
|
className={cn(
|
|
169
|
-
"flex w-
|
|
170
|
-
"
|
|
197
|
+
"flex w-full select-none items-center gap-2 text-sm leading-none",
|
|
198
|
+
isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
|
|
199
|
+
className,
|
|
171
200
|
)}
|
|
172
201
|
>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
202
|
+
{/* The shared `Checkbox`, not a second implementation of one. The
|
|
203
|
+
hand-rolled button here carried its own border, size and tick, so a
|
|
204
|
+
form holding both drew two different checkboxes. */}
|
|
205
|
+
<Checkbox
|
|
177
206
|
data-slot="checkbox-group-item"
|
|
178
|
-
|
|
179
|
-
data-disabled={isDisabled ? "" : undefined}
|
|
180
|
-
disabled={isDisabled}
|
|
181
|
-
onClick={handleClick}
|
|
182
|
-
className={cn(
|
|
183
|
-
"size-4 shrink-0 rounded-sm border border-primary shadow-sm",
|
|
184
|
-
"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring",
|
|
185
|
-
"data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
186
|
-
className,
|
|
187
|
-
)}
|
|
207
|
+
aria-describedby={context.descriptionId}
|
|
188
208
|
{...props}
|
|
189
|
-
|
|
190
|
-
{
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
209
|
+
checked={isChecked}
|
|
210
|
+
onCheckedChange={() => context.toggle(value)}
|
|
211
|
+
disabled={isDisabled}
|
|
212
|
+
className={checkboxClassName}
|
|
213
|
+
/>
|
|
214
|
+
{/* Submitted with the form. `name` was accepted and then dropped, so a
|
|
215
|
+
native submit sent nothing for a group the caller had named. */}
|
|
216
|
+
{context.name && isChecked ? (
|
|
217
|
+
<input type="hidden" name={context.name} value={value} />
|
|
218
|
+
) : null}
|
|
196
219
|
{children}
|
|
197
220
|
</label>
|
|
198
221
|
)
|
|
@@ -202,9 +225,11 @@ function CheckboxGroupItem({
|
|
|
202
225
|
// CheckboxGroupDescription
|
|
203
226
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
227
|
|
|
205
|
-
function CheckboxGroupDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
228
|
+
function CheckboxGroupDescription({ className, id, ...props }: React.ComponentProps<"div">) {
|
|
229
|
+
const context = useCheckboxGroupContext("CheckboxGroupDescription")
|
|
206
230
|
return (
|
|
207
231
|
<div
|
|
232
|
+
id={id ?? context.descriptionId}
|
|
208
233
|
data-slot="checkbox-group-description"
|
|
209
234
|
className={cn(
|
|
210
235
|
"text-[0.8rem] text-muted-foreground leading-none",
|