@djangocfg/ui-core 2.1.465 → 2.1.468
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 -0
- package/package.json +6 -5
- package/src/components/navigation/dropdown-menu/index.tsx +9 -2
- package/src/components/navigation/menu/menu-builder.tsx +2 -1
- package/src/components/navigation/menu/render-items.tsx +9 -1
- package/src/components/navigation/menu/types.ts +15 -1
- package/src/styles/README.md +21 -4
- package/src/styles/css/presets/macos.css +14 -8
- package/src/styles/css/theme/light.css +5 -3
package/README.md
CHANGED
|
@@ -176,6 +176,11 @@ Tailwind v4 with semantic tokens, not raw color scales:
|
|
|
176
176
|
|
|
177
177
|
Tokens live in `:root` / `.dark` as fully-wrapped CSS colors; `@theme inline` exposes them as `--color-X` references, so opacity modifiers (`bg-card/40`, `border-foreground/20`) resolve via `color-mix` for **every** semantic token.
|
|
178
178
|
|
|
179
|
+
Preset text/fill pairs target WCAG AA for compact product UI. For the macOS
|
|
180
|
+
preset this is enforced by `pnpm check:contrast`; primary and destructive
|
|
181
|
+
foregrounds intentionally switch between light and dark ink where the system
|
|
182
|
+
fill's luminance requires it.
|
|
183
|
+
|
|
179
184
|
`@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.
|
|
180
185
|
|
|
181
186
|
Tailwind's `text-*` size utilities are bridged to the preset `--font-size-*` scale. Product-specific density belongs in a small CSS override after the static preset import — never in a runtime token builder. See `src/styles/README.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.468",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -124,10 +124,11 @@
|
|
|
124
124
|
],
|
|
125
125
|
"scripts": {
|
|
126
126
|
"lint": "eslint .",
|
|
127
|
-
"check": "tsc --noEmit"
|
|
127
|
+
"check": "tsc --noEmit",
|
|
128
|
+
"check:contrast": "node scripts/check-preset-contrast.mjs"
|
|
128
129
|
},
|
|
129
130
|
"peerDependencies": {
|
|
130
|
-
"@djangocfg/i18n": "^2.1.
|
|
131
|
+
"@djangocfg/i18n": "^2.1.468",
|
|
131
132
|
"consola": "^3.4.2",
|
|
132
133
|
"lucide-react": "^0.545.0",
|
|
133
134
|
"moment": "^2.30.1",
|
|
@@ -205,8 +206,8 @@
|
|
|
205
206
|
"@chenglou/pretext": "^0.0.8"
|
|
206
207
|
},
|
|
207
208
|
"devDependencies": {
|
|
208
|
-
"@djangocfg/i18n": "^2.1.
|
|
209
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
209
|
+
"@djangocfg/i18n": "^2.1.468",
|
|
210
|
+
"@djangocfg/typescript-config": "^2.1.468",
|
|
210
211
|
"@types/node": "^25.9.5",
|
|
211
212
|
"@types/react": "19.2.15",
|
|
212
213
|
"@types/react-dom": "19.2.3",
|
|
@@ -48,7 +48,12 @@ const DropdownMenuSubContent = React.forwardRef<
|
|
|
48
48
|
<DropdownMenuPrimitive.SubContent
|
|
49
49
|
ref={ref}
|
|
50
50
|
className={cn(
|
|
51
|
-
|
|
51
|
+
// Height/scroll mirrors DropdownMenuContent below: a long submenu (e.g. a
|
|
52
|
+
// 40-vendor model list) must cap and scroll INSIDE, not overflow off-screen
|
|
53
|
+
// and clip. `min()` of the space Radix measured to the viewport edge and a
|
|
54
|
+
// comfortable ceiling — a menu that spans the WHOLE screen reads as a page,
|
|
55
|
+
// not a menu, so we stop well short of the viewport even when it would fit.
|
|
56
|
+
"z-[700] max-h-[min(var(--radix-dropdown-menu-content-available-height),24rem)] min-w-32 overflow-y-auto overflow-x-hidden rounded-[var(--radius-popover)] border bg-popover backdrop-blur-xl p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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",
|
|
52
57
|
className
|
|
53
58
|
)}
|
|
54
59
|
{...props}
|
|
@@ -66,7 +71,9 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
66
71
|
ref={ref}
|
|
67
72
|
sideOffset={sideOffset}
|
|
68
73
|
className={cn(
|
|
69
|
-
|
|
74
|
+
// See DropdownMenuSubContent: cap at the SMALLER of the space to the
|
|
75
|
+
// viewport edge and a comfortable ceiling, then scroll inside.
|
|
76
|
+
"z-[700] max-h-[min(var(--radix-dropdown-menu-content-available-height),24rem)] min-w-32 overflow-y-auto overflow-x-hidden rounded-[var(--radius-popover)] border bg-popover backdrop-blur-xl p-1 text-popover-foreground shadow-md",
|
|
70
77
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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",
|
|
71
78
|
className
|
|
72
79
|
)}
|
|
@@ -33,6 +33,7 @@ export function MenuBuilder({
|
|
|
33
33
|
sideOffset,
|
|
34
34
|
alignOffset,
|
|
35
35
|
contentClassName,
|
|
36
|
+
submenuClassName,
|
|
36
37
|
showDescriptions = true,
|
|
37
38
|
}: MenuBuilderProps) {
|
|
38
39
|
return (
|
|
@@ -54,7 +55,7 @@ export function MenuBuilder({
|
|
|
54
55
|
// select-none: menu labels shouldn't be text-selectable (feels crisp).
|
|
55
56
|
className={cn('min-w-56 max-w-xs select-none', contentClassName)}
|
|
56
57
|
>
|
|
57
|
-
{renderItems(items, { showDescriptions })}
|
|
58
|
+
{renderItems(items, { showDescriptions, submenuClassName })}
|
|
58
59
|
</DropdownMenuContent>
|
|
59
60
|
</DropdownMenu>
|
|
60
61
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment, type ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
import { cn } from '../../../lib';
|
|
3
4
|
import {
|
|
4
5
|
DropdownMenuCheckboxItem,
|
|
5
6
|
DropdownMenuGroup,
|
|
@@ -18,6 +19,9 @@ import type { MenuItem } from './types';
|
|
|
18
19
|
|
|
19
20
|
interface RenderOptions {
|
|
20
21
|
showDescriptions: boolean;
|
|
22
|
+
/** Applied to every submenu panel at any depth; a `MenuSubmenuItem` may
|
|
23
|
+
* override it per-branch with its own `contentClassName`. */
|
|
24
|
+
submenuClassName?: string;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/** Drop `hidden` rows before rendering. */
|
|
@@ -83,7 +87,11 @@ export function renderItems(
|
|
|
83
87
|
/>
|
|
84
88
|
</DropdownMenuSubTrigger>
|
|
85
89
|
<DropdownMenuPortal>
|
|
86
|
-
|
|
90
|
+
{/* Per-branch class wins over the builder-wide one. Height +
|
|
91
|
+
scroll come from the primitive — see DropdownMenuSubContent. */}
|
|
92
|
+
<DropdownMenuSubContent
|
|
93
|
+
className={cn(opts.submenuClassName, item.contentClassName)}
|
|
94
|
+
>
|
|
87
95
|
{renderItems(item.items, opts)}
|
|
88
96
|
</DropdownMenuSubContent>
|
|
89
97
|
</DropdownMenuPortal>
|
|
@@ -37,6 +37,12 @@ export interface MenuActionItem extends MenuRowBase {
|
|
|
37
37
|
export interface MenuSubmenuItem extends MenuRowBase {
|
|
38
38
|
kind: 'submenu';
|
|
39
39
|
items: MenuItem[];
|
|
40
|
+
/**
|
|
41
|
+
* Extra class for THIS submenu's panel — overrides the builder-wide
|
|
42
|
+
* `submenuClassName` for one branch (e.g. only the vendor list needs to be
|
|
43
|
+
* wider). Height/scroll are already handled by the primitive.
|
|
44
|
+
*/
|
|
45
|
+
contentClassName?: string;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/** A single checkbox row. */
|
|
@@ -135,8 +141,16 @@ export interface MenuBuilderProps {
|
|
|
135
141
|
align?: 'start' | 'center' | 'end';
|
|
136
142
|
sideOffset?: number;
|
|
137
143
|
alignOffset?: number;
|
|
138
|
-
/** Extra class for the content panel. */
|
|
144
|
+
/** Extra class for the ROOT content panel. */
|
|
139
145
|
contentClassName?: string;
|
|
146
|
+
/**
|
|
147
|
+
* Extra class for EVERY submenu panel (applied at any nesting depth).
|
|
148
|
+
* Both root and sub panels already cap their height to the space Radix
|
|
149
|
+
* measured to the viewport edge and scroll inside, so this is for WIDTH and
|
|
150
|
+
* the like — e.g. `sm:max-w-md` when rows carry long ids that would
|
|
151
|
+
* otherwise be clipped by the default `max-w-xs`.
|
|
152
|
+
*/
|
|
153
|
+
submenuClassName?: string;
|
|
140
154
|
/** Render the muted description line on rows that have one. Default: true. */
|
|
141
155
|
showDescriptions?: boolean;
|
|
142
156
|
}
|
package/src/styles/README.md
CHANGED
|
@@ -56,7 +56,8 @@ not depend on the physical path. The preset defines both `:root` (light) and
|
|
|
56
56
|
For a product-specific adjustment, add a small override after the preset:
|
|
57
57
|
|
|
58
58
|
```css
|
|
59
|
-
:root,
|
|
59
|
+
:root,
|
|
60
|
+
.dark {
|
|
60
61
|
--font-size-base: 0.9375rem;
|
|
61
62
|
--font-size-sm: 0.875rem;
|
|
62
63
|
}
|
|
@@ -133,8 +134,12 @@ CSS rather than duplicating a pixel value in React.
|
|
|
133
134
|
Color variables are complete CSS colors, never bare HSL triplets:
|
|
134
135
|
|
|
135
136
|
```css
|
|
136
|
-
:root {
|
|
137
|
-
|
|
137
|
+
:root {
|
|
138
|
+
--background: hsl(240 17% 97%);
|
|
139
|
+
}
|
|
140
|
+
.dark {
|
|
141
|
+
--background: hsl(240 5% 8%);
|
|
142
|
+
}
|
|
138
143
|
```
|
|
139
144
|
|
|
140
145
|
Tailwind maps semantic utilities to these variables through `@theme inline`.
|
|
@@ -144,7 +149,8 @@ Use `var(--background)` or `bg-background`; do not write
|
|
|
144
149
|
Typography variables are ordinary CSS values:
|
|
145
150
|
|
|
146
151
|
```css
|
|
147
|
-
:root,
|
|
152
|
+
:root,
|
|
153
|
+
.dark {
|
|
148
154
|
--font-size-base: 0.8125rem;
|
|
149
155
|
--font-size-sm: 0.75rem;
|
|
150
156
|
}
|
|
@@ -157,6 +163,9 @@ Typography variables are ordinary CSS values:
|
|
|
157
163
|
CSS color values.
|
|
158
164
|
- Keep light and dark pairs together in the same file.
|
|
159
165
|
- A preset may omit tokens that intentionally inherit from `theme.css`.
|
|
166
|
+
- In light mode, `--muted` must be at least 2 HSL lightness points darker
|
|
167
|
+
than `--background`. This keeps translucent muted fills visible on the
|
|
168
|
+
canvas and is enforced by `check:contrast`.
|
|
160
169
|
- Changes to a preset require checking both light and dark modes.
|
|
161
170
|
- Product density overrides belong in the consuming app's CSS, not in a copied
|
|
162
171
|
TypeScript map.
|
|
@@ -164,9 +173,17 @@ Typography variables are ordinary CSS values:
|
|
|
164
173
|
## Verification
|
|
165
174
|
|
|
166
175
|
```bash
|
|
176
|
+
pnpm check:contrast
|
|
167
177
|
pnpm check
|
|
168
178
|
```
|
|
169
179
|
|
|
180
|
+
`check:contrast` merges every static preset over the base theme and validates
|
|
181
|
+
the light-mode background/muted surface hierarchy. It also keeps the complete
|
|
182
|
+
macOS compact-text audit: semantic pairs must reach WCAG AA 4.5:1 in both
|
|
183
|
+
modes, including primary, destructive, sidebar, status-surface, on-fill, and
|
|
184
|
+
muted text against every common surface. When tuning a token, change the
|
|
185
|
+
semantic pair together rather than overriding text in a consumer component.
|
|
186
|
+
|
|
170
187
|
At the consumer, build the app that imports the preset and verify both modes.
|
|
171
188
|
The important invariant is that changing a preset requires editing one CSS
|
|
172
189
|
file, and no React runtime code is involved in applying it.
|
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
--card-foreground: hsl(0 0% 7%);
|
|
7
7
|
--popover: hsl(0 0% 100%);
|
|
8
8
|
--popover-foreground: hsl(0 0% 7%);
|
|
9
|
-
|
|
9
|
+
/* Apple system-blue hue, lowered just enough for white small text (4.95:1). */
|
|
10
|
+
--primary: hsl(211 100% 44%);
|
|
10
11
|
--primary-foreground: hsl(0 0% 100%);
|
|
11
12
|
--secondary: hsl(240 12% 95%);
|
|
12
13
|
--secondary-foreground: hsl(0 0% 7%);
|
|
13
14
|
--muted: hsl(240 8% 91%);
|
|
14
|
-
|
|
15
|
+
/* Readable even when muted text sits on the muted fill itself (4.63:1). */
|
|
16
|
+
--muted-foreground: hsl(240 3% 41%);
|
|
15
17
|
--accent: hsl(211 50% 96%);
|
|
16
18
|
--accent-foreground: hsl(211 100% 36%);
|
|
17
|
-
|
|
19
|
+
/* Keep white destructive labels AA-readable in the light theme (5.69:1). */
|
|
20
|
+
--destructive: hsl(2 80% 44%);
|
|
18
21
|
--destructive-foreground: hsl(0 0% 100%);
|
|
22
|
+
--on-destructive: hsl(0 0% 100%);
|
|
19
23
|
--border: hsl(240 3% 78%);
|
|
20
24
|
--input: hsl(240 8% 93%);
|
|
21
25
|
--divider: hsl(240 4% 83%);
|
|
@@ -24,7 +28,7 @@
|
|
|
24
28
|
--overlay: hsl(240 6% 10% / 0.32);
|
|
25
29
|
--sidebar-background: hsl(240 12% 94%);
|
|
26
30
|
--sidebar-foreground: hsl(0 0% 7%);
|
|
27
|
-
--sidebar-primary: hsl(211 100%
|
|
31
|
+
--sidebar-primary: hsl(211 100% 44%);
|
|
28
32
|
--sidebar-primary-foreground: hsl(0 0% 100%);
|
|
29
33
|
--sidebar-accent: hsl(211 50% 93%);
|
|
30
34
|
--sidebar-accent-foreground: hsl(211 100% 36%);
|
|
@@ -76,15 +80,17 @@
|
|
|
76
80
|
--popover: hsl(240 3% 20%);
|
|
77
81
|
--popover-foreground: hsl(0 0% 95%);
|
|
78
82
|
--primary: hsl(211 100% 58%);
|
|
79
|
-
|
|
83
|
+
/* Bright system blue needs dark ink for compact controls (5.59:1). */
|
|
84
|
+
--primary-foreground: hsl(0 0% 9%);
|
|
80
85
|
--secondary: hsl(240 3% 20%);
|
|
81
86
|
--secondary-foreground: hsl(0 0% 88%);
|
|
82
87
|
--muted: hsl(240 3% 11%);
|
|
83
|
-
|
|
88
|
+
/* Clears AA on background, card, popover, muted, and accent surfaces. */
|
|
89
|
+
--muted-foreground: hsl(240 4% 62%);
|
|
84
90
|
--accent: hsl(211 25% 19%);
|
|
85
91
|
--accent-foreground: hsl(211 100% 72%);
|
|
86
92
|
--destructive: hsl(3 100% 62%);
|
|
87
|
-
--destructive-foreground: hsl(0 0%
|
|
93
|
+
--destructive-foreground: hsl(0 0% 9%);
|
|
88
94
|
--border: hsl(240 3% 22%);
|
|
89
95
|
--input: hsl(240 3% 22%);
|
|
90
96
|
--divider: hsl(240 4% 46% / 0.18);
|
|
@@ -94,7 +100,7 @@
|
|
|
94
100
|
--sidebar-background: hsl(240 5% 5%);
|
|
95
101
|
--sidebar-foreground: hsl(0 0% 90%);
|
|
96
102
|
--sidebar-primary: hsl(211 100% 58%);
|
|
97
|
-
--sidebar-primary-foreground: hsl(0 0%
|
|
103
|
+
--sidebar-primary-foreground: hsl(0 0% 9%);
|
|
98
104
|
--sidebar-accent: hsl(211 22% 16%);
|
|
99
105
|
--sidebar-accent-foreground: hsl(211 100% 72%);
|
|
100
106
|
--sidebar-border: hsl(240 3% 22%);
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
:root {
|
|
13
|
-
/* Base colors -
|
|
14
|
-
|
|
13
|
+
/* Base colors - an off-white canvas with darker neutral fills and a white
|
|
14
|
+
* raised card. Keep `muted` below `background` in light mode so chips,
|
|
15
|
+
* message bubbles, and secondary controls never dissolve into the page. */
|
|
16
|
+
--background: hsl(0 0% 98%);
|
|
15
17
|
--foreground: hsl(0 0% 9%);
|
|
16
18
|
--card: hsl(0 0% 100%);
|
|
17
19
|
--card-foreground: hsl(0 0% 9%);
|
|
@@ -22,7 +24,7 @@
|
|
|
22
24
|
--primary-foreground: hsl(0 0% 100%);
|
|
23
25
|
--secondary: hsl(0 0% 9%);
|
|
24
26
|
--secondary-foreground: hsl(0 0% 98%);
|
|
25
|
-
--muted: hsl(0 0%
|
|
27
|
+
--muted: hsl(0 0% 94%);
|
|
26
28
|
--muted-foreground: hsl(0 0% 45%);
|
|
27
29
|
/* Neutral interaction surface: a quiet step below the current light canvas.
|
|
28
30
|
* It deliberately keys off --background, not white --card, so hover remains
|