@djangocfg/ui-core 2.1.464 → 2.1.466

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 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.464",
3
+ "version": "2.1.466",
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.464",
131
+ "@djangocfg/i18n": "^2.1.466",
131
132
  "consola": "^3.4.2",
132
133
  "lucide-react": "^0.545.0",
133
134
  "moment": "^2.30.1",
@@ -150,68 +151,68 @@
150
151
  }
151
152
  },
152
153
  "dependencies": {
153
- "@hookform/resolvers": "^5.2.2",
154
- "@radix-ui/react-accordion": "^1.2.12",
155
- "@radix-ui/react-alert-dialog": "^1.1.15",
156
- "@radix-ui/react-aspect-ratio": "^1.1.7",
157
- "@radix-ui/react-avatar": "^1.1.10",
158
- "@radix-ui/react-checkbox": "^1.3.3",
159
- "@radix-ui/react-collapsible": "^1.1.12",
160
- "@radix-ui/react-context-menu": "^2.2.16",
161
- "@radix-ui/react-dialog": "^1.1.15",
162
- "@radix-ui/react-dropdown-menu": "^2.1.16",
163
- "@radix-ui/react-hover-card": "^1.1.15",
154
+ "@hookform/resolvers": "^5.4.0",
155
+ "@radix-ui/react-accordion": "^1.2.16",
156
+ "@radix-ui/react-alert-dialog": "^1.1.19",
157
+ "@radix-ui/react-aspect-ratio": "^1.1.11",
158
+ "@radix-ui/react-avatar": "^1.2.2",
159
+ "@radix-ui/react-checkbox": "^1.3.7",
160
+ "@radix-ui/react-collapsible": "^1.1.16",
161
+ "@radix-ui/react-context-menu": "^2.3.3",
162
+ "@radix-ui/react-dialog": "^1.1.19",
163
+ "@radix-ui/react-dropdown-menu": "^2.1.20",
164
+ "@radix-ui/react-hover-card": "^1.1.19",
164
165
  "@radix-ui/react-icons": "^1.3.2",
165
- "@radix-ui/react-label": "^2.1.7",
166
- "@radix-ui/react-menubar": "^1.1.16",
167
- "@radix-ui/react-navigation-menu": "^1.2.14",
168
- "@radix-ui/react-popover": "^1.1.15",
169
- "@radix-ui/react-progress": "^1.1.7",
170
- "@radix-ui/react-radio-group": "^1.3.8",
171
- "@radix-ui/react-scroll-area": "^1.2.10",
172
- "@radix-ui/react-select": "^2.2.6",
173
- "@radix-ui/react-separator": "^1.1.7",
174
- "@radix-ui/react-slider": "^1.3.6",
175
- "@radix-ui/react-slot": "^1.2.4",
176
- "@radix-ui/react-switch": "^1.2.6",
177
- "@radix-ui/react-tabs": "^1.1.13",
178
- "@radix-ui/react-toast": "^1.2.15",
179
- "@radix-ui/react-toggle": "^1.1.10",
180
- "@radix-ui/react-toggle-group": "^1.1.11",
181
- "@radix-ui/react-tooltip": "^1.2.8",
182
- "@web3icons/react": "^4.0.26",
166
+ "@radix-ui/react-label": "^2.1.11",
167
+ "@radix-ui/react-menubar": "^1.1.20",
168
+ "@radix-ui/react-navigation-menu": "^1.2.18",
169
+ "@radix-ui/react-popover": "^1.1.19",
170
+ "@radix-ui/react-progress": "^1.1.12",
171
+ "@radix-ui/react-radio-group": "^1.4.3",
172
+ "@radix-ui/react-scroll-area": "^1.2.14",
173
+ "@radix-ui/react-select": "^2.3.3",
174
+ "@radix-ui/react-separator": "^1.1.11",
175
+ "@radix-ui/react-slider": "^1.4.3",
176
+ "@radix-ui/react-slot": "^1.3.0",
177
+ "@radix-ui/react-switch": "^1.3.3",
178
+ "@radix-ui/react-tabs": "^1.1.17",
179
+ "@radix-ui/react-toast": "^1.2.19",
180
+ "@radix-ui/react-toggle": "^1.1.14",
181
+ "@radix-ui/react-toggle-group": "^1.1.15",
182
+ "@radix-ui/react-tooltip": "^1.2.12",
183
+ "@web3icons/react": "^4.1.17",
183
184
  "class-variance-authority": "^0.7.1",
184
185
  "clsx": "^2.1.1",
185
186
  "cmdk": "1.1.1",
186
- "countries-list": "^3.2.2",
187
- "country-flag-icons": "^1.6.16",
188
- "date-fns": "^4.1.0",
187
+ "countries-list": "^3.4.0",
188
+ "country-flag-icons": "^1.6.20",
189
+ "date-fns": "^4.4.0",
189
190
  "embla-carousel-react": "8.6.0",
190
191
  "i18n-iso-countries": "^7.14.0",
191
192
  "input-otp": "1.4.2",
192
- "libphonenumber-js": "^1.12.24",
193
+ "libphonenumber-js": "^1.13.8",
193
194
  "next-themes": "^0.4.6",
194
195
  "nextjs-toploader": "^3.9.17",
195
196
  "react-day-picker": "9.11.1",
196
- "react-hotkeys-hook": "^4.6.1",
197
+ "react-hotkeys-hook": "^4.6.2",
197
198
  "react-resizable-panels": "3.0.6",
198
199
  "react-sticky-box": "^2.0.5",
199
200
  "recharts": "2.15.4",
200
201
  "sonner": "2.0.7",
201
- "tailwind-merge": "^3.3.1",
202
+ "tailwind-merge": "^3.6.0",
202
203
  "vaul": "1.1.2"
203
204
  },
204
205
  "optionalDependencies": {
205
- "@chenglou/pretext": "*"
206
+ "@chenglou/pretext": "^0.0.8"
206
207
  },
207
208
  "devDependencies": {
208
- "@djangocfg/i18n": "^2.1.464",
209
- "@djangocfg/typescript-config": "^2.1.464",
210
- "@types/node": "^25.2.3",
211
- "@types/react": "^19.2.15",
212
- "@types/react-dom": "^19.2.3",
209
+ "@djangocfg/i18n": "^2.1.466",
210
+ "@djangocfg/typescript-config": "^2.1.466",
211
+ "@types/node": "^25.9.5",
212
+ "@types/react": "19.2.15",
213
+ "@types/react-dom": "19.2.3",
213
214
  "lucide-react": "^0.545.0",
214
- "next": "^16.2.2",
215
+ "next": "^16.2.10",
215
216
  "typescript": "^5.9.3"
216
217
  },
217
218
  "publishConfig": {
@@ -48,7 +48,12 @@ const DropdownMenuSubContent = React.forwardRef<
48
48
  <DropdownMenuPrimitive.SubContent
49
49
  ref={ref}
50
50
  className={cn(
51
- "z-[700] min-w-32 overflow-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",
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
- "z-[700] max-h-[var(--radix-dropdown-menu-content-available-height)] 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",
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
- <DropdownMenuSubContent>
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
  }
@@ -164,9 +164,16 @@ Typography variables are ordinary CSS values:
164
164
  ## Verification
165
165
 
166
166
  ```bash
167
+ pnpm check:contrast
167
168
  pnpm check
168
169
  ```
169
170
 
171
+ `check:contrast` validates the macOS preset's compact-text semantic pairs at
172
+ WCAG AA 4.5:1 in both modes. It includes primary, destructive, sidebar,
173
+ status-surface, and muted text against every common surface. When tuning a
174
+ token, change the semantic pair together rather than overriding text in a
175
+ consumer component.
176
+
170
177
  At the consumer, build the app that imports the preset and verify both modes.
171
178
  The important invariant is that changing a preset requires editing one CSS
172
179
  file, and no React runtime code is involved in applying it.
@@ -6,15 +6,18 @@
6
6
  --card-foreground: hsl(0 0% 7%);
7
7
  --popover: hsl(0 0% 100%);
8
8
  --popover-foreground: hsl(0 0% 7%);
9
- --primary: hsl(211 100% 50%);
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
- --muted-foreground: hsl(240 3% 44%);
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
- --destructive: hsl(2 100% 59%);
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%);
19
22
  --border: hsl(240 3% 78%);
20
23
  --input: hsl(240 8% 93%);
@@ -24,7 +27,7 @@
24
27
  --overlay: hsl(240 6% 10% / 0.32);
25
28
  --sidebar-background: hsl(240 12% 94%);
26
29
  --sidebar-foreground: hsl(0 0% 7%);
27
- --sidebar-primary: hsl(211 100% 50%);
30
+ --sidebar-primary: hsl(211 100% 44%);
28
31
  --sidebar-primary-foreground: hsl(0 0% 100%);
29
32
  --sidebar-accent: hsl(211 50% 93%);
30
33
  --sidebar-accent-foreground: hsl(211 100% 36%);
@@ -76,15 +79,17 @@
76
79
  --popover: hsl(240 3% 20%);
77
80
  --popover-foreground: hsl(0 0% 95%);
78
81
  --primary: hsl(211 100% 58%);
79
- --primary-foreground: hsl(0 0% 100%);
82
+ /* Bright system blue needs dark ink for compact controls (5.59:1). */
83
+ --primary-foreground: hsl(0 0% 9%);
80
84
  --secondary: hsl(240 3% 20%);
81
85
  --secondary-foreground: hsl(0 0% 88%);
82
86
  --muted: hsl(240 3% 11%);
83
- --muted-foreground: hsl(240 4% 58%);
87
+ /* Clears AA on background, card, popover, muted, and accent surfaces. */
88
+ --muted-foreground: hsl(240 4% 62%);
84
89
  --accent: hsl(211 25% 19%);
85
90
  --accent-foreground: hsl(211 100% 72%);
86
91
  --destructive: hsl(3 100% 62%);
87
- --destructive-foreground: hsl(0 0% 100%);
92
+ --destructive-foreground: hsl(0 0% 9%);
88
93
  --border: hsl(240 3% 22%);
89
94
  --input: hsl(240 3% 22%);
90
95
  --divider: hsl(240 4% 46% / 0.18);
@@ -94,7 +99,7 @@
94
99
  --sidebar-background: hsl(240 5% 5%);
95
100
  --sidebar-foreground: hsl(0 0% 90%);
96
101
  --sidebar-primary: hsl(211 100% 58%);
97
- --sidebar-primary-foreground: hsl(0 0% 100%);
102
+ --sidebar-primary-foreground: hsl(0 0% 9%);
98
103
  --sidebar-accent: hsl(211 22% 16%);
99
104
  --sidebar-accent-foreground: hsl(211 100% 72%);
100
105
  --sidebar-border: hsl(240 3% 22%);