@djangocfg/ui-core 2.1.426 → 2.1.427

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.
Files changed (40) hide show
  1. package/package.json +4 -4
  2. package/src/components/data/badge/index.tsx +1 -1
  3. package/src/components/data/calendar/calendar.tsx +2 -2
  4. package/src/components/data/stat/index.tsx +5 -5
  5. package/src/components/data/status/index.tsx +3 -3
  6. package/src/components/data/table/index.tsx +30 -11
  7. package/src/components/feedback/banner/index.tsx +5 -4
  8. package/src/components/forms/button/index.tsx +15 -5
  9. package/src/components/forms/button-download/index.tsx +2 -2
  10. package/src/components/forms/checkbox/index.tsx +1 -1
  11. package/src/components/forms/editable/index.tsx +19 -19
  12. package/src/components/forms/input/index.tsx +44 -9
  13. package/src/components/forms/otp/index.tsx +1 -1
  14. package/src/components/forms/setting-row/index.tsx +359 -0
  15. package/src/components/forms/switch/index.tsx +1 -1
  16. package/src/components/forms/tags-input/index.tsx +1 -1
  17. package/src/components/forms/textarea/index.tsx +3 -8
  18. package/src/components/index.ts +2 -0
  19. package/src/components/navigation/dropdown-menu/index.tsx +3 -1
  20. package/src/components/navigation/menu/menu-builder.tsx +7 -2
  21. package/src/components/navigation/stepper/index.tsx +1 -1
  22. package/src/components/navigation/tabs/index.tsx +3 -3
  23. package/src/components/overlay/dialog/index.tsx +8 -3
  24. package/src/components/overlay/sheet/index.tsx +1 -1
  25. package/src/components/overlay/tooltip/index.tsx +4 -1
  26. package/src/components/select/multi-select-pro-async.tsx +2 -2
  27. package/src/components/select/multi-select-pro.tsx +2 -2
  28. package/src/components/specialized/copy/index.tsx +2 -2
  29. package/src/components/specialized/item/index.tsx +1 -1
  30. package/src/styles/README.md +49 -10
  31. package/src/styles/base.css +18 -1
  32. package/src/styles/theme/dark.css +40 -26
  33. package/src/styles/theme/light.css +13 -7
  34. package/src/styles/theme/tokens.css +1 -0
  35. package/src/styles/utilities/controls.css +12 -0
  36. package/src/styles/utilities/divider.css +23 -0
  37. package/src/styles/utilities.css +2 -0
  38. package/src/theme/ThemeSegmented.tsx +73 -0
  39. package/src/theme/index.ts +2 -0
  40. package/src/types/index.ts +0 -0
@@ -0,0 +1,359 @@
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
+ <p className="mt-0.5 text-[13px] leading-relaxed text-muted-foreground">{description}</p>
119
+ )}
120
+ </div>
121
+ );
122
+
123
+ const base = cn(
124
+ 'divider-b py-3.5',
125
+ stacked ? 'block' : 'flex items-center justify-between gap-4',
126
+ className,
127
+ );
128
+
129
+ // Navigation row: the entire row is a button with a trailing chevron.
130
+ if (navigation) {
131
+ return (
132
+ <button
133
+ type="button"
134
+ onClick={onClick}
135
+ disabled={disabled}
136
+ className={cn(
137
+ base,
138
+ 'group w-full rounded-control text-left transition-colors',
139
+ disabled ? 'cursor-default opacity-60' : 'hover:bg-accent/50',
140
+ )}
141
+ >
142
+ {labelBlock}
143
+ <ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
144
+ </button>
145
+ );
146
+ }
147
+
148
+ // Clickable (non-navigation) row.
149
+ if (onClick) {
150
+ return (
151
+ <button
152
+ type="button"
153
+ onClick={onClick}
154
+ disabled={disabled}
155
+ ref={ref as never}
156
+ className={cn(
157
+ base,
158
+ 'w-full text-left transition-colors',
159
+ disabled ? 'cursor-default opacity-60' : 'hover:bg-accent/40',
160
+ )}
161
+ >
162
+ {labelBlock}
163
+ {control != null && <div className="flex shrink-0 items-center">{control}</div>}
164
+ </button>
165
+ );
166
+ }
167
+
168
+ return (
169
+ <div ref={ref} className={base}>
170
+ {labelBlock}
171
+ {control != null && (
172
+ <div className={cn('shrink-0', stacked ? 'mt-3 w-full' : 'flex items-center')}>{control}</div>
173
+ )}
174
+ </div>
175
+ );
176
+ },
177
+ );
178
+ SettingRow.displayName = 'SettingRow';
179
+
180
+ // ── Control resolver ──────────────────────────────────────────────────────────
181
+
182
+ interface ControlArgs {
183
+ value?: string;
184
+ editable: boolean;
185
+ onSave?: (value: string) => Promise<void> | void;
186
+ placeholder?: string;
187
+ type: 'text' | 'phone';
188
+ toggle: boolean;
189
+ checked?: boolean;
190
+ onToggle?: (checked: boolean) => void;
191
+ action?: React.ReactNode;
192
+ children?: React.ReactNode;
193
+ disabled?: boolean;
194
+ }
195
+
196
+ function resolveControl(a: ControlArgs): React.ReactNode {
197
+ if (a.toggle) {
198
+ return <Switch checked={a.checked} onCheckedChange={a.onToggle} disabled={a.disabled} />;
199
+ }
200
+ if (a.value !== undefined && a.editable && a.onSave) {
201
+ return (
202
+ <EditableValue
203
+ value={a.value}
204
+ placeholder={a.placeholder ?? ''}
205
+ type={a.type}
206
+ onSave={a.onSave}
207
+ disabled={a.disabled}
208
+ />
209
+ );
210
+ }
211
+ if (a.value !== undefined) {
212
+ const text = a.value ? (a.type === 'phone' ? formatPhone(a.value) : a.value) : a.placeholder ?? '';
213
+ return <span className={cn(VALUE_CHIP, !a.value && 'text-muted-foreground/60')}>{text}</span>;
214
+ }
215
+ if (a.action != null) return a.action;
216
+ return a.children ?? null;
217
+ }
218
+
219
+ // ── Inline editable value ──────────────────────────────────────────────────────
220
+
221
+ interface EditableValueProps {
222
+ value: string;
223
+ placeholder: string;
224
+ type: 'text' | 'phone';
225
+ onSave: (value: string) => Promise<void> | void;
226
+ disabled?: boolean;
227
+ }
228
+
229
+ function EditableValue({ value, placeholder, type, onSave, disabled }: EditableValueProps) {
230
+ const submit = (next: string) => {
231
+ if (next !== value) onSave(next);
232
+ };
233
+
234
+ if (type === 'phone') {
235
+ return <PhoneEditable value={value} placeholder={placeholder} onSubmit={submit} disabled={disabled} />;
236
+ }
237
+
238
+ return (
239
+ <Editable
240
+ value={value}
241
+ placeholder={placeholder}
242
+ onValueSubmit={submit}
243
+ readOnly={disabled}
244
+ selectAllOnFocus
245
+ submitOnBlur
246
+ className="flex justify-end"
247
+ >
248
+ <EditablePreview
249
+ className={cn(
250
+ VALUE_CHIP,
251
+ 'cursor-text transition-colors hover:bg-accent',
252
+ !value && 'text-muted-foreground/60',
253
+ disabled && 'cursor-default opacity-60 hover:bg-muted',
254
+ )}
255
+ >
256
+ {value || placeholder}
257
+ </EditablePreview>
258
+ {/* EditableInput renders the global Input styling at `sm` density. */}
259
+ <EditableInput inputSize="sm" className="w-56" />
260
+ </Editable>
261
+ );
262
+ }
263
+
264
+ interface PhoneEditableProps {
265
+ value: string;
266
+ placeholder: string;
267
+ onSubmit: (next: string) => void;
268
+ disabled?: boolean;
269
+ }
270
+
271
+ function PhoneEditable({ value, placeholder, onSubmit, disabled }: PhoneEditableProps) {
272
+ const [editing, setEditing] = React.useState(false);
273
+ const [draft, setDraft] = React.useState(value);
274
+ const wrapRef = React.useRef<HTMLDivElement>(null);
275
+ React.useEffect(() => setDraft(value), [value]);
276
+ React.useEffect(() => {
277
+ if (editing) wrapRef.current?.querySelector('input')?.focus();
278
+ }, [editing]);
279
+
280
+ if (!editing) {
281
+ return (
282
+ <button
283
+ type="button"
284
+ disabled={disabled}
285
+ onClick={() => setEditing(true)}
286
+ className={cn(VALUE_CHIP, 'transition-colors hover:bg-accent', !value && 'text-muted-foreground/60', disabled && 'opacity-60')}
287
+ >
288
+ {value ? formatPhone(value) : placeholder}
289
+ </button>
290
+ );
291
+ }
292
+
293
+ return (
294
+ <div
295
+ ref={wrapRef}
296
+ onBlur={(e) => {
297
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
298
+ onSubmit(draft);
299
+ setEditing(false);
300
+ }
301
+ }}
302
+ >
303
+ <PhoneInput value={draft} onChange={(v) => setDraft(v ?? '')} placeholder={placeholder} />
304
+ </div>
305
+ );
306
+ }
307
+
308
+ // ── Titled block of rows ────────────────────────────────────────────────────────
309
+
310
+ export interface SettingsBlockProps {
311
+ /** Bold section heading (e.g. "Profile", "Preferences"). */
312
+ title?: React.ReactNode;
313
+ children: React.ReactNode;
314
+ className?: string;
315
+ /**
316
+ * Make the whole block collapsible — the title becomes a clickable header
317
+ * with a chevron, and the rows hide/reveal. Useful for rarely-needed or
318
+ * destructive sections (e.g. "Danger zone"). Requires a `title`.
319
+ */
320
+ collapsible?: boolean;
321
+ /** Initial open state when `collapsible`. Default: false. */
322
+ defaultOpen?: boolean;
323
+ }
324
+
325
+ const BLOCK_TITLE = 'text-[13px] font-semibold uppercase tracking-wide text-muted-foreground';
326
+
327
+ export const SettingsBlock: React.FC<SettingsBlockProps> = ({
328
+ title,
329
+ children,
330
+ className,
331
+ collapsible = false,
332
+ defaultOpen = false,
333
+ }) => {
334
+ if (collapsible && title) {
335
+ return (
336
+ <Collapsible defaultOpen={defaultOpen} className={cn('space-y-0', className)}>
337
+ <CollapsibleTrigger
338
+ className={cn(
339
+ BLOCK_TITLE,
340
+ 'group flex w-full items-center gap-1.5 pb-1 transition-colors hover:text-foreground',
341
+ )}
342
+ >
343
+ {title}
344
+ <ChevronDown className="size-3.5 transition-transform duration-200 group-data-[state=open]:rotate-180" />
345
+ </CollapsibleTrigger>
346
+ <CollapsibleContent className="overflow-hidden data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down">
347
+ <div>{children}</div>
348
+ </CollapsibleContent>
349
+ </Collapsible>
350
+ );
351
+ }
352
+
353
+ return (
354
+ <section className={cn('space-y-0', className)}>
355
+ {title && <h2 className={cn(BLOCK_TITLE, 'pb-1')}>{title}</h2>}
356
+ <div>{children}</div>
357
+ </section>
358
+ );
359
+ };
@@ -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-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
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 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
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}
@@ -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
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
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
- <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
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
- className={cn('min-w-56 max-w-xs', contentClassName)}
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 outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 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",
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 ring-offset-background transition-all",
289
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
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 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-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
- "fixed z-600 bg-background duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
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
- : "left-1/2 top-1/2 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border p-6 shadow-lg sm:rounded-[var(--radius-dialog)] data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
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 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
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 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
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
- "z-[700] overflow-hidden rounded-[var(--radius-tooltip)] border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground 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",
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-2 focus:ring-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-2 focus:ring-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-2 focus:ring-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-2 focus:ring-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-green-500')} />
73
+ <Check className={cn(iconClassName, 'text-success')} />
74
74
  ) : (
75
75
  <Copy className={iconClassName} />
76
76
  )}
77
- {hasLabel && <span className={copied ? 'text-green-500' : undefined}>{copied ? copiedText : children}</span>}
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/50 [a]:transition-colors flex flex-wrap items-center rounded-[var(--radius)] border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
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: {