@djangocfg/ui-core 2.1.426 → 2.1.428
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 +5 -3
- package/package.json +4 -4
- package/src/components/data/badge/index.tsx +1 -1
- package/src/components/data/calendar/calendar.tsx +2 -2
- package/src/components/data/stat/index.tsx +5 -5
- package/src/components/data/status/index.tsx +3 -3
- package/src/components/data/table/index.tsx +30 -11
- package/src/components/feedback/banner/index.tsx +8 -4
- package/src/components/forms/button/index.tsx +15 -5
- package/src/components/forms/button-download/index.tsx +2 -2
- package/src/components/forms/checkbox/index.tsx +1 -1
- package/src/components/forms/editable/index.tsx +19 -19
- package/src/components/forms/input/index.tsx +44 -9
- package/src/components/forms/otp/index.tsx +1 -1
- package/src/components/forms/setting-row/index.tsx +363 -0
- package/src/components/forms/switch/index.tsx +1 -1
- package/src/components/forms/tags-input/index.tsx +1 -1
- package/src/components/forms/textarea/index.tsx +3 -8
- package/src/components/index.ts +2 -0
- package/src/components/navigation/dropdown-menu/index.tsx +3 -1
- package/src/components/navigation/menu/menu-builder.tsx +7 -2
- package/src/components/navigation/stepper/index.tsx +1 -1
- package/src/components/navigation/tabs/index.tsx +3 -3
- package/src/components/overlay/dialog/index.tsx +8 -3
- package/src/components/overlay/sheet/index.tsx +1 -1
- package/src/components/overlay/tooltip/index.tsx +4 -1
- package/src/components/select/multi-select-pro-async.tsx +2 -2
- package/src/components/select/multi-select-pro.tsx +2 -2
- package/src/components/specialized/copy/index.tsx +2 -2
- package/src/components/specialized/item/index.tsx +1 -1
- package/src/hooks/router/README.md +4 -1
- package/src/lib/env.ts +6 -1
- package/src/styles/README.md +115 -22
- package/src/styles/base.css +18 -1
- package/src/styles/presets/index.ts +1 -0
- package/src/styles/presets/themes/dense.ts +11 -0
- package/src/styles/presets/themes/django-cfg.ts +43 -2
- package/src/styles/presets/themes/high-contrast.ts +25 -9
- package/src/styles/presets/themes/ios.ts +32 -0
- package/src/styles/presets/themes/macos.ts +36 -0
- package/src/styles/presets/themes/soft.ts +13 -0
- package/src/styles/presets/themes/windows.ts +34 -0
- package/src/styles/presets/types.ts +36 -2
- package/src/styles/theme/dark.css +48 -32
- package/src/styles/theme/light.css +21 -13
- package/src/styles/theme/tokens.css +23 -0
- package/src/styles/utilities/controls.css +12 -0
- package/src/styles/utilities/divider.css +23 -0
- package/src/styles/utilities.css +2 -0
- package/src/theme/ThemeSegmented.tsx +73 -0
- package/src/theme/index.ts +2 -0
- package/src/types/index.ts +0 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SettingRow — the universal settings line, macOS / Claude-settings style:
|
|
5
|
+
*
|
|
6
|
+
* [ label (+ description) ] ················· [ control ]
|
|
7
|
+
*
|
|
8
|
+
* separated from siblings by a single hairline (`--divider` token) — no card
|
|
9
|
+
* frame. This is the canonical building block for any settings surface (the
|
|
10
|
+
* SettingsLayout in @djangocfg/layouts is just one consumer).
|
|
11
|
+
*
|
|
12
|
+
* Props-driven control modes (precedence top→bottom):
|
|
13
|
+
* - `value` + `editable` + `onSave` → inline-editable chip (text/phone)
|
|
14
|
+
* - `value` → read-only value chip
|
|
15
|
+
* - `toggle` + `onToggle` → Switch
|
|
16
|
+
* - `navigation` (+ onClick) → full-row button with a trailing chevron
|
|
17
|
+
* - `action` → right-aligned node (button/link)
|
|
18
|
+
* - `children` → arbitrary control (select, segmented…)
|
|
19
|
+
*
|
|
20
|
+
* Styling uses ONLY design tokens / ui-core primitives (Input, Switch) — no
|
|
21
|
+
* arbitrary one-off classes — so it stays consistent and themeable everywhere.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as React from 'react';
|
|
25
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
26
|
+
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
|
27
|
+
|
|
28
|
+
import { cn } from '../../../lib/utils';
|
|
29
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../navigation/collapsible';
|
|
30
|
+
import { Editable, EditableInput, EditablePreview } from '../editable';
|
|
31
|
+
import { PhoneInput } from '../phone-input';
|
|
32
|
+
import { Switch } from '../switch';
|
|
33
|
+
|
|
34
|
+
function formatPhone(raw: string): string {
|
|
35
|
+
if (!raw) return '';
|
|
36
|
+
try {
|
|
37
|
+
return parsePhoneNumberFromString(raw)?.formatInternational() ?? raw;
|
|
38
|
+
} catch {
|
|
39
|
+
return raw;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read-only / preview value chip — recessed `muted` fill, right-aligned. */
|
|
44
|
+
const VALUE_CHIP =
|
|
45
|
+
'rounded-control inline-flex min-w-[7rem] items-center justify-end bg-muted px-3 py-1.5 text-right text-sm text-foreground';
|
|
46
|
+
|
|
47
|
+
export interface SettingRowProps {
|
|
48
|
+
/** Left-hand label. */
|
|
49
|
+
label: React.ReactNode;
|
|
50
|
+
/** Optional helper text under the label (muted, small). May contain links. */
|
|
51
|
+
description?: React.ReactNode;
|
|
52
|
+
|
|
53
|
+
// ── Control modes ──
|
|
54
|
+
/** Display a value (read-only chip, or inline-editable with `editable`). */
|
|
55
|
+
value?: string;
|
|
56
|
+
/** Make `value` inline-editable (Editable for text, PhoneInput for phone). */
|
|
57
|
+
editable?: boolean;
|
|
58
|
+
/** Commit handler for editable values. */
|
|
59
|
+
onSave?: (value: string) => Promise<void> | void;
|
|
60
|
+
/** Placeholder for an empty editable value. */
|
|
61
|
+
placeholder?: string;
|
|
62
|
+
/** Value kind — phone uses the country-aware input + formatting. */
|
|
63
|
+
type?: 'text' | 'phone';
|
|
64
|
+
/** Toggle mode — renders a Switch. */
|
|
65
|
+
toggle?: boolean;
|
|
66
|
+
/** Toggle state (with `toggle`). */
|
|
67
|
+
checked?: boolean;
|
|
68
|
+
/** Toggle change handler (with `toggle`). */
|
|
69
|
+
onToggle?: (checked: boolean) => void;
|
|
70
|
+
/** Navigation mode — whole row is a button with a trailing chevron. */
|
|
71
|
+
navigation?: boolean;
|
|
72
|
+
/** Right-aligned action node (button, link). */
|
|
73
|
+
action?: React.ReactNode;
|
|
74
|
+
/** Arbitrary right-hand control (select, segmented, custom). */
|
|
75
|
+
children?: React.ReactNode;
|
|
76
|
+
|
|
77
|
+
// ── Layout ──
|
|
78
|
+
/** Stack the control under the label (wide inputs, textareas). */
|
|
79
|
+
stacked?: boolean;
|
|
80
|
+
/** Row click (also used by `navigation`). */
|
|
81
|
+
onClick?: () => void;
|
|
82
|
+
disabled?: boolean;
|
|
83
|
+
className?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const SettingRow = React.forwardRef<HTMLDivElement, SettingRowProps>(
|
|
87
|
+
(
|
|
88
|
+
{
|
|
89
|
+
label,
|
|
90
|
+
description,
|
|
91
|
+
value,
|
|
92
|
+
editable = false,
|
|
93
|
+
onSave,
|
|
94
|
+
placeholder,
|
|
95
|
+
type = 'text',
|
|
96
|
+
toggle = false,
|
|
97
|
+
checked,
|
|
98
|
+
onToggle,
|
|
99
|
+
navigation = false,
|
|
100
|
+
action,
|
|
101
|
+
children,
|
|
102
|
+
stacked = false,
|
|
103
|
+
onClick,
|
|
104
|
+
disabled,
|
|
105
|
+
className,
|
|
106
|
+
},
|
|
107
|
+
ref,
|
|
108
|
+
) => {
|
|
109
|
+
const control = resolveControl({
|
|
110
|
+
value, editable, onSave, placeholder, type,
|
|
111
|
+
toggle, checked, onToggle, action, children, disabled,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const labelBlock = (
|
|
115
|
+
<div className={cn('min-w-0', stacked ? 'w-full' : 'flex-1')}>
|
|
116
|
+
<div className="text-sm text-foreground">{label}</div>
|
|
117
|
+
{description && (
|
|
118
|
+
// `div`, not `p`: `description` is arbitrary ReactNode and consumers
|
|
119
|
+
// legitimately embed block-level nodes (e.g. a `<Badge>` status pill,
|
|
120
|
+
// which renders a `<div>`) — a `<p>` cannot contain a `<div>` and
|
|
121
|
+
// triggers an invalid-nesting hydration error.
|
|
122
|
+
<div className="mt-0.5 text-[13px] leading-relaxed text-muted-foreground">{description}</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const base = cn(
|
|
128
|
+
'divider-b py-3.5',
|
|
129
|
+
stacked ? 'block' : 'flex items-center justify-between gap-4',
|
|
130
|
+
className,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Navigation row: the entire row is a button with a trailing chevron.
|
|
134
|
+
if (navigation) {
|
|
135
|
+
return (
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={onClick}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
className={cn(
|
|
141
|
+
base,
|
|
142
|
+
'group w-full rounded-control text-left transition-colors',
|
|
143
|
+
disabled ? 'cursor-default opacity-60' : 'hover:bg-accent/50',
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
{labelBlock}
|
|
147
|
+
<ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
|
148
|
+
</button>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Clickable (non-navigation) row.
|
|
153
|
+
if (onClick) {
|
|
154
|
+
return (
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={onClick}
|
|
158
|
+
disabled={disabled}
|
|
159
|
+
ref={ref as never}
|
|
160
|
+
className={cn(
|
|
161
|
+
base,
|
|
162
|
+
'w-full text-left transition-colors',
|
|
163
|
+
disabled ? 'cursor-default opacity-60' : 'hover:bg-accent/40',
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{labelBlock}
|
|
167
|
+
{control != null && <div className="flex shrink-0 items-center">{control}</div>}
|
|
168
|
+
</button>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div ref={ref} className={base}>
|
|
174
|
+
{labelBlock}
|
|
175
|
+
{control != null && (
|
|
176
|
+
<div className={cn('shrink-0', stacked ? 'mt-3 w-full' : 'flex items-center')}>{control}</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
SettingRow.displayName = 'SettingRow';
|
|
183
|
+
|
|
184
|
+
// ── Control resolver ──────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
interface ControlArgs {
|
|
187
|
+
value?: string;
|
|
188
|
+
editable: boolean;
|
|
189
|
+
onSave?: (value: string) => Promise<void> | void;
|
|
190
|
+
placeholder?: string;
|
|
191
|
+
type: 'text' | 'phone';
|
|
192
|
+
toggle: boolean;
|
|
193
|
+
checked?: boolean;
|
|
194
|
+
onToggle?: (checked: boolean) => void;
|
|
195
|
+
action?: React.ReactNode;
|
|
196
|
+
children?: React.ReactNode;
|
|
197
|
+
disabled?: boolean;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveControl(a: ControlArgs): React.ReactNode {
|
|
201
|
+
if (a.toggle) {
|
|
202
|
+
return <Switch checked={a.checked} onCheckedChange={a.onToggle} disabled={a.disabled} />;
|
|
203
|
+
}
|
|
204
|
+
if (a.value !== undefined && a.editable && a.onSave) {
|
|
205
|
+
return (
|
|
206
|
+
<EditableValue
|
|
207
|
+
value={a.value}
|
|
208
|
+
placeholder={a.placeholder ?? ''}
|
|
209
|
+
type={a.type}
|
|
210
|
+
onSave={a.onSave}
|
|
211
|
+
disabled={a.disabled}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (a.value !== undefined) {
|
|
216
|
+
const text = a.value ? (a.type === 'phone' ? formatPhone(a.value) : a.value) : a.placeholder ?? '';
|
|
217
|
+
return <span className={cn(VALUE_CHIP, !a.value && 'text-muted-foreground/60')}>{text}</span>;
|
|
218
|
+
}
|
|
219
|
+
if (a.action != null) return a.action;
|
|
220
|
+
return a.children ?? null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Inline editable value ──────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
interface EditableValueProps {
|
|
226
|
+
value: string;
|
|
227
|
+
placeholder: string;
|
|
228
|
+
type: 'text' | 'phone';
|
|
229
|
+
onSave: (value: string) => Promise<void> | void;
|
|
230
|
+
disabled?: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function EditableValue({ value, placeholder, type, onSave, disabled }: EditableValueProps) {
|
|
234
|
+
const submit = (next: string) => {
|
|
235
|
+
if (next !== value) onSave(next);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (type === 'phone') {
|
|
239
|
+
return <PhoneEditable value={value} placeholder={placeholder} onSubmit={submit} disabled={disabled} />;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<Editable
|
|
244
|
+
value={value}
|
|
245
|
+
placeholder={placeholder}
|
|
246
|
+
onValueSubmit={submit}
|
|
247
|
+
readOnly={disabled}
|
|
248
|
+
selectAllOnFocus
|
|
249
|
+
submitOnBlur
|
|
250
|
+
className="flex justify-end"
|
|
251
|
+
>
|
|
252
|
+
<EditablePreview
|
|
253
|
+
className={cn(
|
|
254
|
+
VALUE_CHIP,
|
|
255
|
+
'cursor-text transition-colors hover:bg-accent',
|
|
256
|
+
!value && 'text-muted-foreground/60',
|
|
257
|
+
disabled && 'cursor-default opacity-60 hover:bg-muted',
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
260
|
+
{value || placeholder}
|
|
261
|
+
</EditablePreview>
|
|
262
|
+
{/* EditableInput renders the global Input styling at `sm` density. */}
|
|
263
|
+
<EditableInput inputSize="sm" className="w-56" />
|
|
264
|
+
</Editable>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface PhoneEditableProps {
|
|
269
|
+
value: string;
|
|
270
|
+
placeholder: string;
|
|
271
|
+
onSubmit: (next: string) => void;
|
|
272
|
+
disabled?: boolean;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function PhoneEditable({ value, placeholder, onSubmit, disabled }: PhoneEditableProps) {
|
|
276
|
+
const [editing, setEditing] = React.useState(false);
|
|
277
|
+
const [draft, setDraft] = React.useState(value);
|
|
278
|
+
const wrapRef = React.useRef<HTMLDivElement>(null);
|
|
279
|
+
React.useEffect(() => setDraft(value), [value]);
|
|
280
|
+
React.useEffect(() => {
|
|
281
|
+
if (editing) wrapRef.current?.querySelector('input')?.focus();
|
|
282
|
+
}, [editing]);
|
|
283
|
+
|
|
284
|
+
if (!editing) {
|
|
285
|
+
return (
|
|
286
|
+
<button
|
|
287
|
+
type="button"
|
|
288
|
+
disabled={disabled}
|
|
289
|
+
onClick={() => setEditing(true)}
|
|
290
|
+
className={cn(VALUE_CHIP, 'transition-colors hover:bg-accent', !value && 'text-muted-foreground/60', disabled && 'opacity-60')}
|
|
291
|
+
>
|
|
292
|
+
{value ? formatPhone(value) : placeholder}
|
|
293
|
+
</button>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div
|
|
299
|
+
ref={wrapRef}
|
|
300
|
+
onBlur={(e) => {
|
|
301
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
302
|
+
onSubmit(draft);
|
|
303
|
+
setEditing(false);
|
|
304
|
+
}
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
<PhoneInput value={draft} onChange={(v) => setDraft(v ?? '')} placeholder={placeholder} />
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Titled block of rows ────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
export interface SettingsBlockProps {
|
|
315
|
+
/** Bold section heading (e.g. "Profile", "Preferences"). */
|
|
316
|
+
title?: React.ReactNode;
|
|
317
|
+
children: React.ReactNode;
|
|
318
|
+
className?: string;
|
|
319
|
+
/**
|
|
320
|
+
* Make the whole block collapsible — the title becomes a clickable header
|
|
321
|
+
* with a chevron, and the rows hide/reveal. Useful for rarely-needed or
|
|
322
|
+
* destructive sections (e.g. "Danger zone"). Requires a `title`.
|
|
323
|
+
*/
|
|
324
|
+
collapsible?: boolean;
|
|
325
|
+
/** Initial open state when `collapsible`. Default: false. */
|
|
326
|
+
defaultOpen?: boolean;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const BLOCK_TITLE = 'text-[13px] font-semibold uppercase tracking-wide text-muted-foreground';
|
|
330
|
+
|
|
331
|
+
export const SettingsBlock: React.FC<SettingsBlockProps> = ({
|
|
332
|
+
title,
|
|
333
|
+
children,
|
|
334
|
+
className,
|
|
335
|
+
collapsible = false,
|
|
336
|
+
defaultOpen = false,
|
|
337
|
+
}) => {
|
|
338
|
+
if (collapsible && title) {
|
|
339
|
+
return (
|
|
340
|
+
<Collapsible defaultOpen={defaultOpen} className={cn('space-y-0', className)}>
|
|
341
|
+
<CollapsibleTrigger
|
|
342
|
+
className={cn(
|
|
343
|
+
BLOCK_TITLE,
|
|
344
|
+
'group flex w-full items-center gap-1.5 pb-1 transition-colors hover:text-foreground',
|
|
345
|
+
)}
|
|
346
|
+
>
|
|
347
|
+
{title}
|
|
348
|
+
<ChevronDown className="size-3.5 transition-transform duration-200 group-data-[state=open]:rotate-180" />
|
|
349
|
+
</CollapsibleTrigger>
|
|
350
|
+
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down">
|
|
351
|
+
<div>{children}</div>
|
|
352
|
+
</CollapsibleContent>
|
|
353
|
+
</Collapsible>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<section className={cn('space-y-0', className)}>
|
|
359
|
+
{title && <h2 className={cn(BLOCK_TITLE, 'pb-1')}>{title}</h2>}
|
|
360
|
+
<div>{children}</div>
|
|
361
|
+
</section>
|
|
362
|
+
);
|
|
363
|
+
};
|
|
@@ -12,7 +12,7 @@ const Switch = React.forwardRef<
|
|
|
12
12
|
>(({ className, ...props }, ref) => (
|
|
13
13
|
<SwitchPrimitives.Root
|
|
14
14
|
className={cn(
|
|
15
|
-
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-
|
|
15
|
+
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
|
16
16
|
className
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -862,7 +862,7 @@ const TagsInputItemDelete = React.forwardRef<HTMLButtonElement, TagsInputItemDel
|
|
|
862
862
|
data-state={itemContext.isHighlighted ? "active" : "inactive"}
|
|
863
863
|
data-disabled={disabled ? "" : undefined}
|
|
864
864
|
className={cn(
|
|
865
|
-
"inline-flex items-center justify-center rounded-sm opacity-70
|
|
865
|
+
"inline-flex items-center justify-center rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring",
|
|
866
866
|
disabled && "pointer-events-none opacity-50",
|
|
867
867
|
props.className
|
|
868
868
|
)}
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks';
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
|
+
import { TEXTAREA_CLASS } from '../input';
|
|
5
6
|
|
|
6
7
|
export interface TextareaProps extends React.ComponentProps<"textarea"> {
|
|
7
8
|
/**
|
|
@@ -40,10 +41,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
40
41
|
if (value !== undefined) {
|
|
41
42
|
return (
|
|
42
43
|
<textarea
|
|
43
|
-
className={cn(
|
|
44
|
-
"flex min-h-[60px] w-full rounded-[var(--radius)] border border-input bg-transparent px-3 py-2 text-sm text-foreground shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
45
|
-
className
|
|
46
|
-
)}
|
|
44
|
+
className={cn(TEXTAREA_CLASS, className)}
|
|
47
45
|
ref={ref}
|
|
48
46
|
value={value}
|
|
49
47
|
onChange={storageKey ? handleChange : onChange}
|
|
@@ -54,10 +52,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
54
52
|
|
|
55
53
|
return (
|
|
56
54
|
<textarea
|
|
57
|
-
className={cn(
|
|
58
|
-
"flex min-h-[60px] w-full rounded-[var(--radius)] border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
59
|
-
className
|
|
60
|
-
)}
|
|
55
|
+
className={cn(TEXTAREA_CLASS, className)}
|
|
61
56
|
ref={ref}
|
|
62
57
|
defaultValue={storageKey && storedValue ? storedValue : defaultValue}
|
|
63
58
|
onChange={storageKey ? handleChange : onChange}
|
package/src/components/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export { CheckboxGroup, CheckboxGroupDescription, CheckboxGroupItem, CheckboxGro
|
|
|
13
13
|
export type { CheckboxGroupProps, CheckboxGroupItemProps, CheckboxGroupListProps } from './forms/checkbox-group';
|
|
14
14
|
export { RadioGroup, RadioGroupItem } from './forms/radio-group';
|
|
15
15
|
export { Switch } from './forms/switch';
|
|
16
|
+
export { SettingRow, SettingsBlock } from './forms/setting-row';
|
|
17
|
+
export type { SettingRowProps, SettingsBlockProps } from './forms/setting-row';
|
|
16
18
|
export { Slider } from './forms/slider';
|
|
17
19
|
export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants } from './forms/button-group';
|
|
18
20
|
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField } from './forms/form';
|
|
@@ -186,7 +186,9 @@ const DropdownMenuSeparator = React.forwardRef<
|
|
|
186
186
|
>(({ className, ...props }, ref) => (
|
|
187
187
|
<DropdownMenuPrimitive.Separator
|
|
188
188
|
ref={ref}
|
|
189
|
-
|
|
189
|
+
// Soft hairline via the --divider token (was bg-muted, which is darker than
|
|
190
|
+
// the popover surface and read as a hard dark groove).
|
|
191
|
+
className={cn("-mx-1 my-1 h-px bg-divider", className)}
|
|
190
192
|
{...props}
|
|
191
193
|
/>
|
|
192
194
|
))
|
|
@@ -41,13 +41,18 @@ export function MenuBuilder({
|
|
|
41
41
|
onOpenChange={onOpenChange}
|
|
42
42
|
defaultOpen={defaultOpen}
|
|
43
43
|
>
|
|
44
|
-
|
|
44
|
+
{/* The trigger opens the menu on click; suppress its focus-visible ring
|
|
45
|
+
so there's no stray outline around the avatar/button. */}
|
|
46
|
+
<DropdownMenuTrigger asChild className="focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0">
|
|
47
|
+
{children}
|
|
48
|
+
</DropdownMenuTrigger>
|
|
45
49
|
<DropdownMenuContent
|
|
46
50
|
side={side}
|
|
47
51
|
align={align}
|
|
48
52
|
sideOffset={sideOffset}
|
|
49
53
|
alignOffset={alignOffset}
|
|
50
|
-
|
|
54
|
+
// select-none: menu labels shouldn't be text-selectable (feels crisp).
|
|
55
|
+
className={cn('min-w-56 max-w-xs select-none', contentClassName)}
|
|
51
56
|
>
|
|
52
57
|
{renderItems(items, { showDescriptions })}
|
|
53
58
|
</DropdownMenuContent>
|
|
@@ -997,7 +997,7 @@ function StepperTrigger(props: ButtonProps) {
|
|
|
997
997
|
{...triggerProps}
|
|
998
998
|
ref={composedRef}
|
|
999
999
|
className={cn(
|
|
1000
|
-
"inline-flex items-center justify-center gap-3 rounded-md text-left
|
|
1000
|
+
"inline-flex items-center justify-center gap-3 rounded-md text-left transition-all focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
1001
1001
|
"not-has-data-[slot=description]:rounded-full not-has-data-[slot=title]:rounded-full",
|
|
1002
1002
|
className,
|
|
1003
1003
|
)}
|
|
@@ -285,8 +285,8 @@ const TabsTrigger = React.forwardRef<
|
|
|
285
285
|
].join(" ")
|
|
286
286
|
: [
|
|
287
287
|
"inline-flex items-center justify-center whitespace-nowrap rounded-[var(--radius-sm)] px-3 py-1",
|
|
288
|
-
"text-sm font-medium
|
|
289
|
-
"focus-visible:outline-none focus-visible:ring-
|
|
288
|
+
"text-sm font-medium transition-all",
|
|
289
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
290
290
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
291
291
|
"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
|
292
292
|
].join(" "),
|
|
@@ -306,7 +306,7 @@ const TabsContent = React.forwardRef<
|
|
|
306
306
|
<TabsPrimitive.Content
|
|
307
307
|
ref={ref}
|
|
308
308
|
className={cn(
|
|
309
|
-
"mt-2
|
|
309
|
+
"mt-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
310
310
|
className
|
|
311
311
|
)}
|
|
312
312
|
{...props}
|
|
@@ -60,17 +60,22 @@ const DialogContent = React.forwardRef<
|
|
|
60
60
|
ref={ref}
|
|
61
61
|
aria-describedby={undefined}
|
|
62
62
|
className={cn(
|
|
63
|
-
|
|
63
|
+
// The content auto-focuses on open; suppress the focus outline on the
|
|
64
|
+
// panel itself (a stray ring around the whole modal looks broken).
|
|
65
|
+
// Inner controls keep their own focus-visible rings.
|
|
66
|
+
"fixed z-600 bg-background duration-200 focus:outline-none focus-visible:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
64
67
|
fullscreen
|
|
65
68
|
? "left-0 top-0 grid h-[100dvh] w-screen max-w-none gap-0 border-0 p-0 shadow-none data-[state=closed]:zoom-out-[0.98] data-[state=open]:zoom-in-[0.98]"
|
|
66
|
-
|
|
69
|
+
// Rounded on ALL sizes — the radius used to be `sm:`-gated, which left
|
|
70
|
+
// phone dialogs with hard square corners. Centered, soft-cornered card.
|
|
71
|
+
: "left-1/2 top-1/2 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-[var(--radius-dialog)] border p-6 shadow-lg data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
67
72
|
className
|
|
68
73
|
)}
|
|
69
74
|
{...props}
|
|
70
75
|
>
|
|
71
76
|
{children}
|
|
72
77
|
{closeButton === undefined ? (
|
|
73
|
-
<DialogPrimitive.Close className="absolute right-4 top-4 cursor-pointer rounded-sm opacity-70
|
|
78
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 cursor-pointer rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
74
79
|
<Cross2Icon className="h-4 w-4" />
|
|
75
80
|
<span className="sr-only">Close</span>
|
|
76
81
|
</DialogPrimitive.Close>
|
|
@@ -67,7 +67,7 @@ const SheetContent = React.forwardRef<
|
|
|
67
67
|
className={cn(sheetVariants({ side }), className)}
|
|
68
68
|
{...props}
|
|
69
69
|
>
|
|
70
|
-
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70
|
|
70
|
+
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none data-[state=open]:bg-secondary">
|
|
71
71
|
<Cross2Icon className="h-4 w-4" />
|
|
72
72
|
<span className="sr-only">Close</span>
|
|
73
73
|
</SheetPrimitive.Close>
|
|
@@ -22,7 +22,10 @@ const TooltipContent = React.forwardRef<
|
|
|
22
22
|
aria-describedby={undefined}
|
|
23
23
|
sideOffset={sideOffset}
|
|
24
24
|
className={cn(
|
|
25
|
-
|
|
25
|
+
// INVERSE surface (foreground bg + background text) so the tooltip is
|
|
26
|
+
// always high-contrast against any panel — light tooltip on dark theme,
|
|
27
|
+
// dark tooltip on light theme (Claude/Vercel/macOS). No border needed.
|
|
28
|
+
"z-[700] overflow-hidden rounded-[var(--radius-tooltip)] bg-foreground px-3 py-1.5 text-xs font-medium text-background shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
26
29
|
className
|
|
27
30
|
)}
|
|
28
31
|
{...props}
|
|
@@ -381,7 +381,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
381
381
|
<span>{option.label}</span>
|
|
382
382
|
{!disabled && (
|
|
383
383
|
<button
|
|
384
|
-
className="ml-1 rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-
|
|
384
|
+
className="ml-1 rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
385
385
|
onClick={(e) => {
|
|
386
386
|
e.stopPropagation()
|
|
387
387
|
toggleOption(option.value)
|
|
@@ -556,7 +556,7 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
|
|
|
556
556
|
handleClearAll()
|
|
557
557
|
}
|
|
558
558
|
}}
|
|
559
|
-
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-
|
|
559
|
+
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
560
560
|
aria-label={translations.clearAll}
|
|
561
561
|
>
|
|
562
562
|
<XCircle className="h-4 w-4 shrink-0 opacity-50" />
|
|
@@ -398,7 +398,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
398
398
|
<span>{option.label}</span>
|
|
399
399
|
{!disabled && (
|
|
400
400
|
<button
|
|
401
|
-
className="ml-1 rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-
|
|
401
|
+
className="ml-1 rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
402
402
|
onClick={(e) => {
|
|
403
403
|
e.stopPropagation()
|
|
404
404
|
toggleOption(option.value)
|
|
@@ -573,7 +573,7 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
|
|
|
573
573
|
handleClearAll()
|
|
574
574
|
}
|
|
575
575
|
}}
|
|
576
|
-
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-
|
|
576
|
+
className="rounded-full hover:bg-muted-foreground/20 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
577
577
|
aria-label={translations.clearAll}
|
|
578
578
|
>
|
|
579
579
|
<XCircle className="h-4 w-4 shrink-0 opacity-50" />
|
|
@@ -70,11 +70,11 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
|
|
|
70
70
|
{...props}
|
|
71
71
|
>
|
|
72
72
|
{copied ? (
|
|
73
|
-
<Check className={cn(iconClassName, 'text-
|
|
73
|
+
<Check className={cn(iconClassName, 'text-success')} />
|
|
74
74
|
) : (
|
|
75
75
|
<Copy className={iconClassName} />
|
|
76
76
|
)}
|
|
77
|
-
{hasLabel && <span className={copied ? 'text-
|
|
77
|
+
{hasLabel && <span className={copied ? 'text-success' : undefined}>{copied ? copiedText : children}</span>}
|
|
78
78
|
</Button>
|
|
79
79
|
)
|
|
80
80
|
}
|
|
@@ -34,7 +34,7 @@ function ItemSeparator({
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const itemVariants = cva(
|
|
37
|
-
"group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring
|
|
37
|
+
"group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring [a]:transition-colors flex flex-wrap items-center rounded-[var(--radius)] border border-transparent text-sm transition-colors duration-100",
|
|
38
38
|
{
|
|
39
39
|
variants: {
|
|
40
40
|
variant: {
|
|
@@ -38,12 +38,15 @@ import { NextRouterAdapter, NextLinkProvider } from '@djangocfg/ui-core/adapters
|
|
|
38
38
|
<I18nProvider locale={locale} messages={messages}>
|
|
39
39
|
<NextRouterAdapter>
|
|
40
40
|
<NextLinkProvider>
|
|
41
|
-
|
|
41
|
+
{children}
|
|
42
42
|
</NextLinkProvider>
|
|
43
43
|
</NextRouterAdapter>
|
|
44
44
|
</I18nProvider>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
> Using `@djangocfg/layouts`? `BaseApp` already mounts both adapters — you don't
|
|
48
|
+
> need this manual wiring.
|
|
49
|
+
|
|
47
50
|
`next` is an **optional peer dependency** — the package never imports from `next/*` from the main entry. The Next adapter lives behind the `/adapters/nextjs` sub-path entry, so non-Next consumers don't pull `next` into their bundle.
|
|
48
51
|
|
|
49
52
|
For other routers (TanStack Router, wouter, Remix, custom transports) — write a ~20-line custom adapter:
|
package/src/lib/env.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Pure environment constants — shared across all @djangocfg UI packages.
|
|
3
|
+
*
|
|
4
|
+
* Keep this to framework-agnostic env primitives only. Feature flags that carry
|
|
5
|
+
* domain meaning (e.g. DPoP auth, static-build behaviour) live in the package
|
|
6
|
+
* that owns them — `dpopEnabled` / `isStaticBuild` are exported from
|
|
7
|
+
* `@djangocfg/api` (the lowest shared package, which implements DPoP).
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
export const nodeEnv = process.env.NODE_ENV || 'development';
|