@djangocfg/ui-core 2.1.430 → 2.1.432
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 +4 -2
- package/package.json +4 -4
- package/src/components/forms/popover-action-button/index.tsx +47 -0
- package/src/components/index.ts +4 -0
- package/src/components/navigation/context-menu/index.tsx +2 -2
- package/src/components/navigation/popover-row-button/index.tsx +54 -0
- package/src/components/overlay/alert-dialog/index.tsx +2 -2
- package/src/components/overlay/dialog/index.tsx +2 -2
- package/src/components/overlay/drawer/index.tsx +2 -2
- package/src/components/overlay/sheet/index.tsx +2 -2
- package/src/components/select/select.tsx +12 -2
- package/src/hooks/media/index.ts +1 -0
- package/src/hooks/media/useIsTouch.ts +23 -0
- package/src/lib/styles.ts +1 -1
- package/src/providers/UiProviders.tsx +39 -2
- package/src/styles/README.md +91 -0
- package/src/styles/base.css +32 -0
- package/src/styles/presets/themes/windows.ts +7 -0
- package/src/styles/presets/types.ts +12 -1
- package/src/styles/theme/dark.css +20 -0
- package/src/styles/theme/light.css +21 -0
- package/src/styles/theme/tokens.css +10 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ import { UiProviders, Button, Card } from '@djangocfg/ui-core';
|
|
|
44
44
|
</UiProviders>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
`<UiProviders>` mounts Tooltip / Dialog / Toast
|
|
47
|
+
`<UiProviders>` mounts Tooltip / Dialog / Toast in the right order **and a top-level error `Boundary`** (a crash shows a recoverable fallback, not a white screen). **Mount it once at the root** — library components (and everything in `@djangocfg/ui-tools`) trust it to be there and never nest their own (a second `TooltipProvider` is the canonical "Tooltip must be used within TooltipProvider" trap). Pass `onError` to forward crashes to your logger, `errorFallback` for a custom (e.g. i18n) crash screen, or `errorBoundary={false}` to opt out.
|
|
48
48
|
|
|
49
49
|
## Catalogue
|
|
50
50
|
|
|
@@ -68,7 +68,7 @@ Imports stay flat — group folders are organisational.
|
|
|
68
68
|
| Topic | Hooks |
|
|
69
69
|
|---|---|
|
|
70
70
|
| `dom/` | `useSize` · `useResizeObserver` · `useMeasure` · `useMutationObserver` · `useIntersection` |
|
|
71
|
-
| `device/` | `useIsMobile` · `useMediaQuery` · `useOnline` · `useViewportSize` · `useOrientation` |
|
|
71
|
+
| `device/` | `useIsMobile` · `useIsTouch` · `useMediaQuery` · `useOnline` · `useViewportSize` · `useOrientation` |
|
|
72
72
|
| `state/` | `useLocalStorage` · `useSessionStorage` · `useToggle` · `useCounter` · `useDebouncedValue` |
|
|
73
73
|
| `events/` | `useEventListener` · `useClickOutside` · `useKeyPress` · `useFocusWithin` |
|
|
74
74
|
| `theme/` | `useTheme` · `useResolvedTheme` · `useThemePreset` |
|
|
@@ -112,6 +112,8 @@ Tokens live in `:root` / `.dark` as fully-wrapped CSS colors; `@theme inline` ex
|
|
|
112
112
|
|
|
113
113
|
`@custom-variant dark (&:where(.dark, .dark *))` binds the `dark:` variant to the `.dark` class on `<html>` (not `prefers-color-scheme`) — every theme-switcher in this monorepo toggles that class.
|
|
114
114
|
|
|
115
|
+
Tailwind's `text-*` size utilities are bridged to the preset-overridable `--font-size-*` scale, so overriding those vars (globally via `buildThemeStyleSheet({ vars })` or scoped on a selector) re-sizes all `text-*` at once — no per-component edits. See `src/styles/README.md` § Typography tokens.
|
|
116
|
+
|
|
115
117
|
**Programmatic theme colors** for Canvas / SVG / Mermaid:
|
|
116
118
|
|
|
117
119
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.432",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"check": "tsc --noEmit"
|
|
107
107
|
},
|
|
108
108
|
"peerDependencies": {
|
|
109
|
-
"@djangocfg/i18n": "^2.1.
|
|
109
|
+
"@djangocfg/i18n": "^2.1.432",
|
|
110
110
|
"consola": "^3.4.2",
|
|
111
111
|
"lucide-react": "^0.545.0",
|
|
112
112
|
"moment": "^2.30.1",
|
|
@@ -180,8 +180,8 @@
|
|
|
180
180
|
"@chenglou/pretext": "*"
|
|
181
181
|
},
|
|
182
182
|
"devDependencies": {
|
|
183
|
-
"@djangocfg/i18n": "^2.1.
|
|
184
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
183
|
+
"@djangocfg/i18n": "^2.1.432",
|
|
184
|
+
"@djangocfg/typescript-config": "^2.1.432",
|
|
185
185
|
"@types/node": "^25.2.3",
|
|
186
186
|
"@types/react": "^19.2.15",
|
|
187
187
|
"@types/react-dom": "^19.2.3",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../../../lib/utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PopoverActionButton — the full-width icon+label list/footer row, macOS style:
|
|
9
|
+
*
|
|
10
|
+
* [ icon ] label…
|
|
11
|
+
*
|
|
12
|
+
* The canonical "jump to settings" / footer-link control inside popovers and
|
|
13
|
+
* menus (e.g. "Connection settings…", "Manage providers…"). Replaces the
|
|
14
|
+
* hand-rolled `<button className="flex w-full items-center gap-2 rounded px-2
|
|
15
|
+
* py-1.5 …">` that was copied across popover footers.
|
|
16
|
+
*
|
|
17
|
+
* Plain `<button>` underneath — extend it with any native button prop. The
|
|
18
|
+
* incoming `className` is merged last, so callers can still add layout tweaks
|
|
19
|
+
* (`mt-1`, etc.) without re-stating the base styling.
|
|
20
|
+
*/
|
|
21
|
+
export interface PopoverActionButtonProps
|
|
22
|
+
extends React.ComponentProps<"button"> {
|
|
23
|
+
/** Icon rendered before the label, in a fixed `h-3.5 w-3.5` slot. */
|
|
24
|
+
icon?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PopoverActionButton = React.forwardRef<HTMLButtonElement, PopoverActionButtonProps>(
|
|
28
|
+
({ className, icon, type = "button", children, ...props }, ref) => {
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
ref={ref}
|
|
32
|
+
type={type}
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{icon != null && <span className="h-3.5 w-3.5 shrink-0">{icon}</span>}
|
|
40
|
+
{children}
|
|
41
|
+
</button>
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
PopoverActionButton.displayName = "PopoverActionButton";
|
|
46
|
+
|
|
47
|
+
export { PopoverActionButton };
|
package/src/components/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type { SmartOTPProps as OTPInputProps, OTPValidationMode, OTPPasteBehavio
|
|
|
27
27
|
export { ButtonGroup as ButtonGroupComponent } from './forms/button-group';
|
|
28
28
|
export { DownloadButton } from './forms/button-download';
|
|
29
29
|
export type { DownloadButtonProps } from './forms/button-download';
|
|
30
|
+
export { PopoverActionButton } from './forms/popover-action-button';
|
|
31
|
+
export type { PopoverActionButtonProps } from './forms/popover-action-button';
|
|
30
32
|
|
|
31
33
|
// Mask Input
|
|
32
34
|
export { MaskInput } from './forms/mask-input';
|
|
@@ -79,6 +81,8 @@ export { navigationMenuTriggerStyle, NavigationMenu, NavigationMenuList, Navigat
|
|
|
79
81
|
export { Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem, MenubarSeparator, MenubarLabel, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarPortal, MenubarSubContent, MenubarSubTrigger, MenubarGroup, MenubarSub, MenubarShortcut } from './navigation/menubar';
|
|
80
82
|
export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup } from './navigation/dropdown-menu';
|
|
81
83
|
export { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from './navigation/context-menu';
|
|
84
|
+
export { PopoverRowButton } from './navigation/popover-row-button';
|
|
85
|
+
export type { PopoverRowButtonProps } from './navigation/popover-row-button';
|
|
82
86
|
export { MenuBuilder } from './navigation/menu';
|
|
83
87
|
export type { MenuItem, MenuActionItem, MenuSubmenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioOption, MenuSeparator, MenuLabel, MenuSection, MenuCustom, MenuRowBase, MenuBuilderProps } from './navigation/menu';
|
|
84
88
|
export { Tabs, TabsContent, TabsList, TabsTrigger } from './navigation/tabs';
|
|
@@ -47,7 +47,7 @@ const ContextMenuSubContent = React.forwardRef<
|
|
|
47
47
|
<ContextMenuPrimitive.SubContent
|
|
48
48
|
ref={ref}
|
|
49
49
|
className={cn(
|
|
50
|
-
"z-[700] min-w-32 overflow-hidden rounded-[var(--radius-popover)] border bg-popover
|
|
50
|
+
"z-[700] min-w-32 overflow-hidden rounded-[var(--radius-popover)] border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:duration-75 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-98 motion-reduce:data-[state=open]:animate-none",
|
|
51
51
|
className
|
|
52
52
|
)}
|
|
53
53
|
{...props}
|
|
@@ -63,7 +63,7 @@ const ContextMenuContent = React.forwardRef<
|
|
|
63
63
|
<ContextMenuPrimitive.Content
|
|
64
64
|
ref={ref}
|
|
65
65
|
className={cn(
|
|
66
|
-
"z-[700] max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-[var(--radius-popover)] border bg-popover
|
|
66
|
+
"z-[700] max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-[var(--radius-popover)] border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:duration-75 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-98 motion-reduce:data-[state=open]:animate-none",
|
|
67
67
|
className
|
|
68
68
|
)}
|
|
69
69
|
{...props}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Check } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PopoverRowButton — a selectable list-row button, macOS picker style:
|
|
10
|
+
*
|
|
11
|
+
* [ ✓ ] label…
|
|
12
|
+
*
|
|
13
|
+
* The canonical row for model / machine / provider pickers: full-width, left
|
|
14
|
+
* aligned, with a trailing-leading check that fades in on `selected`. Replaces
|
|
15
|
+
* the hand-rolled `<button className={cn("flex w-full items-center gap-2 px-3
|
|
16
|
+
* py-1.5 …", active && "bg-muted/60")}>` copied across pickers.
|
|
17
|
+
*
|
|
18
|
+
* Plain `<button>` underneath — extend it with any native button prop. The
|
|
19
|
+
* incoming `className` is merged last so callers can compose freely.
|
|
20
|
+
*/
|
|
21
|
+
export interface PopoverRowButtonProps
|
|
22
|
+
extends React.ComponentProps<"button"> {
|
|
23
|
+
/** Marks the row active — fills the background and shows the check. */
|
|
24
|
+
selected?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PopoverRowButton = React.forwardRef<HTMLButtonElement, PopoverRowButtonProps>(
|
|
28
|
+
({ className, selected = false, type = "button", children, ...props }, ref) => {
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
ref={ref}
|
|
32
|
+
type={type}
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs",
|
|
35
|
+
"hover:bg-muted disabled:cursor-not-allowed disabled:opacity-40",
|
|
36
|
+
selected && "bg-muted/60",
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
<Check
|
|
42
|
+
className={cn(
|
|
43
|
+
"h-3.5 w-3.5 shrink-0 text-primary",
|
|
44
|
+
selected ? "opacity-100" : "opacity-0",
|
|
45
|
+
)}
|
|
46
|
+
/>
|
|
47
|
+
{children}
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
PopoverRowButton.displayName = "PopoverRowButton";
|
|
53
|
+
|
|
54
|
+
export { PopoverRowButton };
|
|
@@ -19,10 +19,10 @@ const AlertDialogOverlay = React.forwardRef<
|
|
|
19
19
|
>(({ className, style, ...props }, ref) => (
|
|
20
20
|
<AlertDialogPrimitive.Overlay
|
|
21
21
|
className={cn(
|
|
22
|
-
"fixed inset-0 z-600 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
22
|
+
"fixed inset-0 z-600 bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
23
23
|
className
|
|
24
24
|
)}
|
|
25
|
-
style={
|
|
25
|
+
style={style}
|
|
26
26
|
{...props}
|
|
27
27
|
ref={ref}
|
|
28
28
|
/>
|
|
@@ -22,10 +22,10 @@ const DialogOverlay = React.forwardRef<
|
|
|
22
22
|
<DialogPrimitive.Overlay
|
|
23
23
|
ref={ref}
|
|
24
24
|
className={cn(
|
|
25
|
-
"fixed inset-0 z-600 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
25
|
+
"fixed inset-0 z-600 bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
26
26
|
className
|
|
27
27
|
)}
|
|
28
|
-
style={
|
|
28
|
+
style={style}
|
|
29
29
|
{...props}
|
|
30
30
|
/>
|
|
31
31
|
))
|
|
@@ -50,8 +50,8 @@ const DrawerOverlay = React.forwardRef<
|
|
|
50
50
|
>(({ className, style, ...props }, ref) => (
|
|
51
51
|
<DrawerPrimitive.Overlay
|
|
52
52
|
ref={ref}
|
|
53
|
-
className={cn("fixed inset-0 z-500 transition-opacity duration-200", className)}
|
|
54
|
-
style={
|
|
53
|
+
className={cn("fixed inset-0 z-500 bg-overlay transition-opacity duration-200", className)}
|
|
54
|
+
style={style}
|
|
55
55
|
{...props}
|
|
56
56
|
/>
|
|
57
57
|
))
|
|
@@ -22,10 +22,10 @@ const SheetOverlay = React.forwardRef<
|
|
|
22
22
|
>(({ className, style, ...props }, ref) => (
|
|
23
23
|
<SheetPrimitive.Overlay
|
|
24
24
|
className={cn(
|
|
25
|
-
"fixed inset-0 z-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
25
|
+
"fixed inset-0 z-200 bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
26
26
|
className
|
|
27
27
|
)}
|
|
28
|
-
style={
|
|
28
|
+
style={style}
|
|
29
29
|
{...props}
|
|
30
30
|
ref={ref}
|
|
31
31
|
/>
|
|
@@ -53,12 +53,22 @@ const SelectTrigger = React.forwardRef<
|
|
|
53
53
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
54
54
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
|
55
55
|
icon?: React.ComponentType<{ className?: string }>;
|
|
56
|
+
/**
|
|
57
|
+
* `ghost` strips the bordered field chrome (border, bg, shadow, fixed
|
|
58
|
+
* height) for use as a flat inline control — e.g. a compact dropdown in a
|
|
59
|
+
* status bar or toolbar where the trigger should read like a plain button.
|
|
60
|
+
*/
|
|
61
|
+
variant?: "default" | "ghost";
|
|
56
62
|
}
|
|
57
|
-
>(({ className, children, icon: Icon, ...props }, ref) => (
|
|
63
|
+
>(({ className, children, icon: Icon, variant = "default", ...props }, ref) => (
|
|
58
64
|
<SelectPrimitive.Trigger
|
|
59
65
|
ref={ref}
|
|
60
66
|
className={cn(
|
|
61
|
-
"flex
|
|
67
|
+
"flex items-center justify-between whitespace-nowrap text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
68
|
+
variant === "default" &&
|
|
69
|
+
"h-10 w-full rounded-[var(--radius)] border border-input bg-transparent px-3 py-2 shadow-sm focus:ring-1 focus:ring-ring",
|
|
70
|
+
variant === "ghost" &&
|
|
71
|
+
"rounded-md px-2 py-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground",
|
|
62
72
|
className
|
|
63
73
|
)}
|
|
64
74
|
{...props}
|
package/src/hooks/media/index.ts
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMediaQuery } from './useMediaQuery';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Is the primary pointer a touch (coarse) pointer?
|
|
7
|
+
*
|
|
8
|
+
* Reads `(pointer: coarse)` via `matchMedia` — NOT the userAgent. A media
|
|
9
|
+
* query reflects real touchscreens, DevTools device emulation and updates
|
|
10
|
+
* live; UA sniffing misses touch laptops, mis-detects iPadOS, and never
|
|
11
|
+
* reacts to a change. SSR-safe (false until mounted).
|
|
12
|
+
*
|
|
13
|
+
* For "touch OR narrow viewport" (e.g. lock an embedded map to a tap-to-
|
|
14
|
+
* expand affordance), compose with `useMediaQuery`:
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const isTouch = useIsTouch()
|
|
18
|
+
* const isNarrow = useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`)
|
|
19
|
+
* const isMobileSurface = isTouch || isNarrow
|
|
20
|
+
*/
|
|
21
|
+
export function useIsTouch(): boolean {
|
|
22
|
+
return useMediaQuery('(pointer: coarse)');
|
|
23
|
+
}
|
package/src/lib/styles.ts
CHANGED
|
@@ -24,7 +24,7 @@ const visuallyHidden: React.CSSProperties = {
|
|
|
24
24
|
const overlay: React.CSSProperties = {
|
|
25
25
|
position: "fixed",
|
|
26
26
|
inset: 0,
|
|
27
|
-
backgroundColor: "
|
|
27
|
+
backgroundColor: "var(--overlay)",
|
|
28
28
|
backdropFilter: "blur(2px)",
|
|
29
29
|
zIndex: 50,
|
|
30
30
|
} as const;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import type { ErrorInfo } from 'react';
|
|
5
5
|
|
|
6
6
|
import { TooltipProvider } from '../components/overlay/tooltip';
|
|
7
7
|
import { Toaster } from '../components/feedback/sonner';
|
|
8
|
+
import { Boundary, type BoundaryProps } from '../components/boundary';
|
|
8
9
|
import { DialogProvider } from '../lib/dialog-service/DialogProvider';
|
|
9
10
|
|
|
10
11
|
export interface UiProvidersProps {
|
|
@@ -15,6 +16,23 @@ export interface UiProvidersProps {
|
|
|
15
16
|
noToaster?: boolean;
|
|
16
17
|
/** Disable the imperative `window.dialog` service + its renderer. */
|
|
17
18
|
noDialogService?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Top-level crash protection. By default UiProviders wraps the whole tree in
|
|
21
|
+
* a fullscreen `<Boundary>` so any uncaught React error shows a recoverable
|
|
22
|
+
* fallback instead of a white screen. Pass `onError` to forward the crash to
|
|
23
|
+
* your logger / monitor (the host owns logging; the boundary just calls it).
|
|
24
|
+
* Set `errorBoundary={false}` if the host installs its own top-level boundary.
|
|
25
|
+
*/
|
|
26
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
27
|
+
/** Disable the built-in top-level error boundary. @default false (boundary on) */
|
|
28
|
+
errorBoundary?: false;
|
|
29
|
+
/**
|
|
30
|
+
* Custom fallback for the built-in boundary. Receives `{ error, errorInfo,
|
|
31
|
+
* reset }`. Use it to render an app-specific (e.g. i18n'd) crash screen
|
|
32
|
+
* instead of the default fullscreen one — lets hosts keep a single boundary
|
|
33
|
+
* (this one) rather than wrapping their own on top.
|
|
34
|
+
*/
|
|
35
|
+
errorFallback?: BoundaryProps['fallback'];
|
|
18
36
|
/**
|
|
19
37
|
* SSR-safe mount strategy. Default `true` — skips providers on the
|
|
20
38
|
* initial server render and remounts after `useEffect` so Radix
|
|
@@ -60,6 +78,9 @@ export function UiProviders({
|
|
|
60
78
|
tooltipDelay = 100,
|
|
61
79
|
noToaster,
|
|
62
80
|
noDialogService,
|
|
81
|
+
onError,
|
|
82
|
+
errorBoundary,
|
|
83
|
+
errorFallback,
|
|
63
84
|
}: UiProvidersProps) {
|
|
64
85
|
// No SSR-skip on purpose: any nested library component that renders
|
|
65
86
|
// `<Tooltip>` on its first paint expects to find `<TooltipProvider>`
|
|
@@ -67,7 +88,23 @@ export function UiProviders({
|
|
|
67
88
|
// "Tooltip must be used within TooltipProvider" before hydration.
|
|
68
89
|
// Radix's own provider tolerates the SSR pass — no hydration
|
|
69
90
|
// mismatches observed; the safe wrapper was over-engineering.
|
|
70
|
-
const
|
|
91
|
+
const dialogTree = noDialogService ? children : <DialogProvider>{children}</DialogProvider>;
|
|
92
|
+
// Top-level crash protection by default. The Boundary catches uncaught React
|
|
93
|
+
// errors and shows a recoverable fullscreen fallback; `onError` forwards the
|
|
94
|
+
// crash to the host's logger/monitor. Opt out with `errorBoundary={false}`.
|
|
95
|
+
const tree =
|
|
96
|
+
errorBoundary === false ? (
|
|
97
|
+
dialogTree
|
|
98
|
+
) : (
|
|
99
|
+
<Boundary
|
|
100
|
+
variant="fullscreen"
|
|
101
|
+
name="ui-providers"
|
|
102
|
+
onError={onError}
|
|
103
|
+
fallback={errorFallback}
|
|
104
|
+
>
|
|
105
|
+
{dialogTree}
|
|
106
|
+
</Boundary>
|
|
107
|
+
);
|
|
71
108
|
return (
|
|
72
109
|
<TooltipProvider delayDuration={tooltipDelay}>
|
|
73
110
|
{tree}
|
package/src/styles/README.md
CHANGED
|
@@ -60,6 +60,7 @@ This makes opacity modifiers (`bg-card/40`, `border-foreground/20`) resolve thro
|
|
|
60
60
|
| `bg-destructive` / `text-destructive-foreground` | `--destructive` | Error / delete filled controls |
|
|
61
61
|
| `border-border` | `--border` | Card outlines, control borders |
|
|
62
62
|
| `border-divider` / `.divider-b` | `--divider` | **Hairline between rows** — deliberately *lighter than `--card`* so it stays visible on elevated surfaces (a `--border` line vanishes on a card) |
|
|
63
|
+
| `bg-overlay` | `--overlay` | Modal scrim / backdrop behind dialogs, drawers, sheets — black scrim in both themes, the token owns the opacity |
|
|
63
64
|
| `bg-input` | `--input` | Input **fill** — a notch off the panel so fields read as real controls (not flush holes). The input *border* uses `--border`, not `--input` |
|
|
64
65
|
| `ring-ring` | `--ring` | Focus rings, selected outlines — **blue** (system-accent feel), independent of the cyan brand |
|
|
65
66
|
|
|
@@ -84,6 +85,29 @@ Available statuses: **`warning`** · **`success`** · **`destructive`** · **`in
|
|
|
84
85
|
</div>
|
|
85
86
|
```
|
|
86
87
|
|
|
88
|
+
#### On-fill text — `*-foreground` vs `on-*` (read this before styling a badge)
|
|
89
|
+
|
|
90
|
+
There are **two** text tokens per status and they are NOT interchangeable:
|
|
91
|
+
|
|
92
|
+
| Token | Class | Sits on | Use for |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| `--{status}-foreground` | `text-success-foreground` | the tinted **`*-background`** | banner / alert copy (a *colored* tint) |
|
|
95
|
+
| `--on-{status}` | `text-on-success` | the **solid `*` fill** | text/icon on a filled badge, unread pill, filled chip |
|
|
96
|
+
|
|
97
|
+
`*-foreground` is a *status-colored* tint tuned for the faint banner surface — on the solid fill it produces **green-text-on-green-fill** (unreadable). `on-*` is a near-black / near-white contrast ink (the WhatsApp/Telegram pattern: dark text on the green pill) that meets WCAG AA against the fill in both themes.
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
{/* ✅ unread count pill — dark ink on the green fill */}
|
|
101
|
+
<span className="rounded-full bg-success px-2 py-0.5 text-xs font-semibold text-on-success">
|
|
102
|
+
{unread}
|
|
103
|
+
</span>
|
|
104
|
+
|
|
105
|
+
{/* ❌ green-on-green — *-foreground is for banners, not the fill */}
|
|
106
|
+
<span className="bg-success text-success-foreground">{unread}</span>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
On-fill tokens exist for **`success`** · **`warning`** · **`info`** · **`destructive`** in both themes. (`--primary-foreground` / `--secondary-foreground` / `--destructive-foreground` already play the on-fill role for those base fills — they're genuine contrast colors, not tints, so no `on-*` is needed for them.) The base `on-*` is a near-black; a preset only re-declares it when it retints a fill dark enough that near-black fails — e.g. **`windows`** light mode sets `on-success` / `on-info` to white because Fluent's light green/blue fills are very dark.
|
|
110
|
+
|
|
87
111
|
The **brand presets** (`macos` / `ios` / `windows` / `django-cfg`) retint the full
|
|
88
112
|
status set to their own canvas, so banners read correctly on their custom
|
|
89
113
|
backgrounds. The **modifier presets** (`soft` / `dense` / `high-contrast`) and
|
|
@@ -139,6 +163,73 @@ so `font-sans` / `font-mono` and `text-xs … text-xl` follow the active preset
|
|
|
139
163
|
instead of Tailwind's hardcoded defaults. `body` applies font-sans + base
|
|
140
164
|
size/line-height/tracking directly.
|
|
141
165
|
|
|
166
|
+
#### The font-size scale → `text-*` bridge (one source of truth)
|
|
167
|
+
|
|
168
|
+
The size tokens are plain rem values. The `macos` preset
|
|
169
|
+
(`presets/themes/macos.ts`) sets them to the Apple HIG point scale:
|
|
170
|
+
|
|
171
|
+
| Token | macos value | px (@1×) | Used for |
|
|
172
|
+
|---|---|---|---|
|
|
173
|
+
| `--font-size-xs` | `0.6875rem` | 11px | captions, timestamps |
|
|
174
|
+
| `--font-size-sm` | `0.75rem` | 12px | footnotes, secondary labels |
|
|
175
|
+
| `--font-size-base` | `0.8125rem` | 13px | HIG default body |
|
|
176
|
+
| `--font-size-lg` | `0.9375rem` | 15px | subheadings |
|
|
177
|
+
| `--font-size-xl` | `1.0625rem` | 17px | titles, nav bar |
|
|
178
|
+
|
|
179
|
+
**The key fact:** Tailwind's `text-*` utilities don't read their own hardcoded
|
|
180
|
+
sizes — they're bridged to these vars in `tokens.css` via `@theme inline`:
|
|
181
|
+
|
|
182
|
+
```css
|
|
183
|
+
@theme inline {
|
|
184
|
+
--text-xs: var(--font-size-xs);
|
|
185
|
+
--text-sm: var(--font-size-sm);
|
|
186
|
+
--text-base: var(--font-size-base);
|
|
187
|
+
--text-lg: var(--font-size-lg);
|
|
188
|
+
--text-xl: var(--font-size-xl);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
So `--font-size-*` is the **single source of truth** for text sizing: change one
|
|
193
|
+
var and **every** `text-sm` / `text-base` / … in that scope re-sizes uniformly —
|
|
194
|
+
no per-component edits, no chasing `text-[15px]` literals across the tree.
|
|
195
|
+
|
|
196
|
+
#### Override recipe (a) — global bump via `buildThemeStyleSheet`
|
|
197
|
+
|
|
198
|
+
To lift the whole UI a notch (e.g. a desktop consumer that wants 15px body),
|
|
199
|
+
pass `vars` alongside the preset — they merge on top of the preset's values per
|
|
200
|
+
mode (`buildThemeStyleSheet` → `mergeLayer`), emitting `:root` (light) and
|
|
201
|
+
`.dark` blocks. Every `text-*` utility moves with them:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { buildThemeStyleSheet } from '@djangocfg/ui-core/styles/presets';
|
|
205
|
+
|
|
206
|
+
const css = buildThemeStyleSheet({
|
|
207
|
+
preset: 'macos',
|
|
208
|
+
vars: {
|
|
209
|
+
light: { 'font-size-base': '0.9375rem', 'font-size-sm': '0.8125rem' },
|
|
210
|
+
dark: { 'font-size-base': '0.9375rem', 'font-size-sm': '0.8125rem' },
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
// inject `css` after ui-core/styles (cmdop does exactly this)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
> Keys are the bare token name (no `--` prefix); `buildThemeStyleSheet` adds it.
|
|
217
|
+
|
|
218
|
+
#### Override recipe (b) — scoped bump on a selector
|
|
219
|
+
|
|
220
|
+
To re-size only a subtree, re-declare the font-size vars on a selector. The
|
|
221
|
+
bridge re-points `text-*` for everything inside it — no preset rebuild:
|
|
222
|
+
|
|
223
|
+
```css
|
|
224
|
+
.compact-panel {
|
|
225
|
+
--font-size-base: 0.75rem; /* 12px */
|
|
226
|
+
--font-size-sm: 0.6875rem; /* 11px */
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Prefer either recipe over per-component `text-[15px]` hacks** — those drift
|
|
231
|
+
from the scale and don't follow the preset or theme.
|
|
232
|
+
|
|
142
233
|
## Radius tokens
|
|
143
234
|
|
|
144
235
|
The radius **scale** (`--radius`, `--radius-sm`, …) is theme-independent and lives in `base.css`; presets that set a semantic `radius` regenerate the scale via `presets/build.ts`. A few named radii are fixed defaults (default preset only):
|
package/src/styles/base.css
CHANGED
|
@@ -46,6 +46,38 @@
|
|
|
46
46
|
outline: none;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/* Pointer cursor on every interactive control.
|
|
50
|
+
*
|
|
51
|
+
* Tailwind v4 preflight sets `cursor: default` on <button>; a browser adds
|
|
52
|
+
* the implicit pointer back, but a native webview (Wails/Electron WKWebView)
|
|
53
|
+
* does NOT — buttons then feel "dead" (arrow cursor on hover). Restore it
|
|
54
|
+
* here, in the design system, so every consumer gets it without sprinkling
|
|
55
|
+
* `cursor-pointer` on each control. `:where()` keeps specificity at zero, so
|
|
56
|
+
* a component's explicit `cursor-*` (text inputs, drag handles, the
|
|
57
|
+
* `disabled:` states below) always wins. Disabled controls get `not-allowed`.
|
|
58
|
+
*/
|
|
59
|
+
:where(
|
|
60
|
+
button,
|
|
61
|
+
a[href],
|
|
62
|
+
[role="button"],
|
|
63
|
+
[role="tab"],
|
|
64
|
+
[role="menuitem"],
|
|
65
|
+
[role="option"],
|
|
66
|
+
[role="switch"],
|
|
67
|
+
[role="checkbox"],
|
|
68
|
+
[role="radio"],
|
|
69
|
+
summary,
|
|
70
|
+
label[for],
|
|
71
|
+
select
|
|
72
|
+
):not(:disabled):not([aria-disabled="true"]) {
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:where(button, a, [role="button"]):disabled,
|
|
77
|
+
:where(button, a, [role="button"])[aria-disabled="true"] {
|
|
78
|
+
cursor: not-allowed;
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
html {
|
|
50
82
|
font-weight: 400;
|
|
51
83
|
}
|
|
@@ -77,6 +77,13 @@ export const windowsPreset: ThemePreset = {
|
|
|
77
77
|
'info-background': 'hsl(210 100% 95%)',
|
|
78
78
|
'info-foreground': 'hsl(210 90% 28%)',
|
|
79
79
|
'info-border': 'hsl(210 80% 78%)',
|
|
80
|
+
// On-fill ink: Fluent's light success/info fills are DARK (27%/38% L), so
|
|
81
|
+
// near-black (base --on-*) fails — these two need WHITE ink instead.
|
|
82
|
+
// on-success vs success (120 78% 27%) → 5.5:1 (AA)
|
|
83
|
+
// on-info vs info (210 100% 38%) → 6.0:1 (AA)
|
|
84
|
+
// warning/destructive light fills stay near-black (base value is correct).
|
|
85
|
+
'on-success': 'hsl(0 0% 100%)',
|
|
86
|
+
'on-info': 'hsl(0 0% 100%)',
|
|
80
87
|
...fluentTypography,
|
|
81
88
|
},
|
|
82
89
|
dark: {
|
|
@@ -44,6 +44,13 @@ export type ThemeCssVarChromeKey = 'border' | 'input' | 'divider' | 'ring' | 'ra
|
|
|
44
44
|
* custom `background` should re-tint these so banners don't clash with the
|
|
45
45
|
* canvas; presets that keep the default canvas can omit them. All are
|
|
46
46
|
* **fully-wrapped CSS colors** (same rule as `ThemeCssVarColorKey`).
|
|
47
|
+
*
|
|
48
|
+
* `*-foreground` = status TEXT on the tinted `*-background` (banners).
|
|
49
|
+
* `on-*` = readable text/icon ON the solid `*` fill (badges, pills, filled
|
|
50
|
+
* chips) — a near-black/near-white contrast color, NOT the banner tint. The
|
|
51
|
+
* base `on-*` (near-black) is safe for most fills; a preset only needs to set
|
|
52
|
+
* `on-*` when it retints a fill DARK enough that near-black fails (e.g.
|
|
53
|
+
* Fluent's dark light-mode green/blue → white ink).
|
|
47
54
|
*/
|
|
48
55
|
export type ThemeCssVarStatusKey =
|
|
49
56
|
| 'warning'
|
|
@@ -59,7 +66,11 @@ export type ThemeCssVarStatusKey =
|
|
|
59
66
|
| 'info'
|
|
60
67
|
| 'info-background'
|
|
61
68
|
| 'info-foreground'
|
|
62
|
-
| 'info-border'
|
|
69
|
+
| 'info-border'
|
|
70
|
+
| 'on-success'
|
|
71
|
+
| 'on-warning'
|
|
72
|
+
| 'on-info'
|
|
73
|
+
| 'on-destructive';
|
|
63
74
|
|
|
64
75
|
/**
|
|
65
76
|
* Typography tokens — override system font stack and scale per preset.
|
|
@@ -47,6 +47,9 @@
|
|
|
47
47
|
/* Divider — hairline; slightly softer than --border so rows don't read as a
|
|
48
48
|
* hard rule (Claude menus/rows). */
|
|
49
49
|
--divider: hsl(48 3% 24%);
|
|
50
|
+
/* Overlay — modal scrim / backdrop behind dialogs, drawers, sheets. Black in
|
|
51
|
+
* both themes; slightly darker here so it still reads on the dark page. */
|
|
52
|
+
--overlay: hsl(0 0% 0% / 0.7);
|
|
50
53
|
/* Focus ring — blue (system-accent feel), independent of the cyan brand. */
|
|
51
54
|
--ring: hsl(217 91% 60%);
|
|
52
55
|
--shadow-card: none;
|
|
@@ -76,6 +79,11 @@
|
|
|
76
79
|
* Dark variants: dim backgrounds (~8-12% lightness) so banners
|
|
77
80
|
* don't blow out against the near-black page. Foreground raised
|
|
78
81
|
* to ~70-80% lightness for contrast. Borders kept muted.
|
|
82
|
+
*
|
|
83
|
+
* `--{status}-foreground` is the LIGHT status tint for text on the
|
|
84
|
+
* `*-background` banner — NOT a contrast color for the solid `*` fill
|
|
85
|
+
* (light-green-on-green is unreadable). For text/icons ON the solid
|
|
86
|
+
* fill use `--on-{status}` below.
|
|
79
87
|
* ─────────────────────────────────────────────────────────────── */
|
|
80
88
|
|
|
81
89
|
/* Warning — amber */
|
|
@@ -96,6 +104,18 @@
|
|
|
96
104
|
--info-background: hsl(200 80% 8%);
|
|
97
105
|
--info-foreground: hsl(200 80% 75%);
|
|
98
106
|
--info-border: hsl(200 60% 25%);
|
|
107
|
+
|
|
108
|
+
/* On-fill text/icon — readable color ON the solid `*` fill (badges,
|
|
109
|
+
* unread pills, filled chips). The status fills here are bright/mid
|
|
110
|
+
* (45-60% L) so near-black ink contrasts best (WhatsApp pattern).
|
|
111
|
+
* on-success vs --success (142 60% 45%) → 6.9:1 (AA)
|
|
112
|
+
* on-warning vs --warning (38 92% 50%) → 8.4:1 (AA)
|
|
113
|
+
* on-info vs --info (200 80% 55%) → 6.9:1 (AA)
|
|
114
|
+
* on-destructive vs --destructive (0 67% 60%) → 4.7:1 (AA) */
|
|
115
|
+
--on-success: hsl(0 0% 9%);
|
|
116
|
+
--on-warning: hsl(0 0% 9%);
|
|
117
|
+
--on-info: hsl(0 0% 9%);
|
|
118
|
+
--on-destructive: hsl(0 0% 9%);
|
|
99
119
|
/* Surface gradient dark */
|
|
100
120
|
--surface-gradient: linear-gradient(to bottom, var(--background), color-mix(in oklab, var(--background) 80%, transparent));
|
|
101
121
|
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
/* Divider — hairline between rows; slightly darker than --border so it reads
|
|
37
37
|
* on white cards. */
|
|
38
38
|
--divider: hsl(0 0% 88%);
|
|
39
|
+
/* Overlay — modal scrim / backdrop behind dialogs, drawers, sheets. A scrim
|
|
40
|
+
* is black in both themes; the token owns the opacity. */
|
|
41
|
+
--overlay: hsl(0 0% 0% / 0.6);
|
|
39
42
|
/* Focus ring — blue (system-accent feel), independent of the cyan brand. */
|
|
40
43
|
--ring: hsl(217 91% 55%);
|
|
41
44
|
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.06), 0 1px 2px -1px rgb(0 0 0 / 0.04);
|
|
@@ -69,6 +72,11 @@
|
|
|
69
72
|
* readable foreground text, and border ring.
|
|
70
73
|
* Use bg-warning-background / text-warning-foreground / border-warning-border
|
|
71
74
|
* in components — never raw amber-* or green-* classes.
|
|
75
|
+
*
|
|
76
|
+
* `--{status}-foreground` is status TEXT on the tinted `*-background`
|
|
77
|
+
* (banners) — it is a colored tint, NOT a contrast color for the solid
|
|
78
|
+
* `*` fill. For text/icons sitting ON the solid fill (badges, pills,
|
|
79
|
+
* filled chips) use the `--on-{status}` tokens below instead.
|
|
72
80
|
* ─────────────────────────────────────────────────────────────── */
|
|
73
81
|
|
|
74
82
|
/* Warning — amber */
|
|
@@ -89,6 +97,19 @@
|
|
|
89
97
|
--info-background: hsl(200 100% 97%);
|
|
90
98
|
--info-foreground: hsl(200 80% 20%);
|
|
91
99
|
--info-border: hsl(200 80% 65%);
|
|
100
|
+
|
|
101
|
+
/* On-fill text/icon — readable color sitting ON the solid `*` fill
|
|
102
|
+
* (badges, unread pills, filled chips), distinct from the `*-foreground`
|
|
103
|
+
* banner tints above. The mid-lightness green/amber/blue/red fills all
|
|
104
|
+
* take a near-black ink (WhatsApp/Telegram pattern) for the best contrast.
|
|
105
|
+
* on-success vs --success (142 71% 45%) → 7.8:1 (AA)
|
|
106
|
+
* on-warning vs --warning (38 92% 50%) → 8.4:1 (AA)
|
|
107
|
+
* on-info vs --info (200 90% 40%) → 4.4:1 (AA-large / borderline AA)
|
|
108
|
+
* on-destructive vs --destructive (0 84% 60%) → 4.7:1 (AA) */
|
|
109
|
+
--on-success: hsl(0 0% 9%);
|
|
110
|
+
--on-warning: hsl(0 0% 9%);
|
|
111
|
+
--on-info: hsl(0 0% 9%);
|
|
112
|
+
--on-destructive: hsl(0 0% 9%);
|
|
92
113
|
/* Surface gradient */
|
|
93
114
|
--surface-gradient: linear-gradient(to bottom, var(--background), color-mix(in oklab, var(--background) 80%, transparent));
|
|
94
115
|
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
--color-border: var(--border);
|
|
46
46
|
--color-input: var(--input);
|
|
47
47
|
--color-divider: var(--divider);
|
|
48
|
+
--color-overlay: var(--overlay);
|
|
48
49
|
--color-ring: var(--ring);
|
|
49
50
|
|
|
50
51
|
/* Status surfaces */
|
|
@@ -66,6 +67,15 @@
|
|
|
66
67
|
--color-info-foreground: var(--info-foreground);
|
|
67
68
|
--color-info-border: var(--info-border);
|
|
68
69
|
|
|
70
|
+
/* On-fill text/icon — contrast color sitting ON the solid `*` fill
|
|
71
|
+
* (badges, unread pills, filled chips). Distinct from `*-foreground`,
|
|
72
|
+
* which is the status tint on the `*-background` banner surface.
|
|
73
|
+
* Backs `text-on-success` / `bg-on-warning` etc. (+ opacity modifiers). */
|
|
74
|
+
--color-on-success: var(--on-success);
|
|
75
|
+
--color-on-warning: var(--on-warning);
|
|
76
|
+
--color-on-info: var(--on-info);
|
|
77
|
+
--color-on-destructive: var(--on-destructive);
|
|
78
|
+
|
|
69
79
|
/* Code surface */
|
|
70
80
|
--color-code: var(--code);
|
|
71
81
|
--color-code-foreground: var(--code-foreground);
|