@djangocfg/ui-core 2.1.531 → 2.1.533

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.531",
3
+ "version": "2.1.533",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -128,7 +128,7 @@
128
128
  "check:contrast": "node scripts/check-preset-contrast.mjs"
129
129
  },
130
130
  "peerDependencies": {
131
- "@djangocfg/i18n": "^2.1.531",
131
+ "@djangocfg/i18n": "^2.1.533",
132
132
  "consola": "^3.4.2",
133
133
  "lucide-react": "^0.545.0",
134
134
  "moment": "^2.30.1",
@@ -206,8 +206,8 @@
206
206
  "@chenglou/pretext": "^0.0.8"
207
207
  },
208
208
  "devDependencies": {
209
- "@djangocfg/i18n": "^2.1.531",
210
- "@djangocfg/typescript-config": "^2.1.531",
209
+ "@djangocfg/i18n": "^2.1.533",
210
+ "@djangocfg/typescript-config": "^2.1.533",
211
211
  "@types/node": "^24.13.3",
212
212
  "@types/react": "19.2.15",
213
213
  "@types/react-dom": "19.2.3",
@@ -18,12 +18,32 @@ export interface SliderProps extends React.ComponentPropsWithoutRef<typeof Slide
18
18
  storageType?: StorageType
19
19
  /** TTL in ms */
20
20
  storageTtl?: number
21
+ /**
22
+ * Rendered inside the track, behind the filled range.
23
+ *
24
+ * For a second quantity that shares the track's scale — a media player's
25
+ * buffered extent is the case this exists for. Positioned by the caller and
26
+ * never interactive: the slider owns every pointer event on the track.
27
+ */
28
+ trackDecoration?: React.ReactNode
29
+ /**
30
+ * Extra classes for the track, the filled range and the thumb.
31
+ *
32
+ * These parts live inside this component, so a consumer cannot reach them:
33
+ * a descendant selector would have to name classes it does not own, and
34
+ * arbitrary Tailwind variants targeting them are not reliably generated.
35
+ * A media scrubber needs a hairline track and a thumb that only appears on
36
+ * hover, which is what these exist for.
37
+ */
38
+ trackClassName?: string
39
+ rangeClassName?: string
40
+ thumbClassName?: string
21
41
  }
22
42
 
23
43
  const Slider = React.forwardRef<
24
44
  React.ElementRef<typeof SliderPrimitive.Root>,
25
45
  SliderProps
26
- >(({ className, orientation = 'horizontal', storageKey, storageType, storageTtl, onValueChange, ...props }, ref) => {
46
+ >(({ className, orientation = 'horizontal', storageKey, storageType, storageTtl, trackDecoration, trackClassName, rangeClassName, thumbClassName, onValueChange, ...props }, ref) => {
27
47
  const isVertical = orientation === 'vertical';
28
48
 
29
49
  const storageOptions: UseStoredValueOptions | undefined =
@@ -55,6 +75,7 @@ const Slider = React.forwardRef<
55
75
  return (
56
76
  <SliderPrimitive.Root
57
77
  ref={ref}
78
+ data-slot="slider"
58
79
  orientation={orientation}
59
80
  className={cn(
60
81
  "relative flex touch-none select-none",
@@ -66,19 +87,39 @@ const Slider = React.forwardRef<
66
87
  {...sliderProps}
67
88
  >
68
89
  <SliderPrimitive.Track
90
+ data-slot="slider-track"
69
91
  className={cn(
70
92
  "relative overflow-hidden rounded-full bg-primary/20",
71
- isVertical ? "w-1.5 h-full grow" : "h-1.5 w-full grow"
93
+ isVertical ? "w-1.5 h-full grow" : "h-1.5 w-full grow",
94
+ trackClassName
72
95
  )}
73
96
  >
97
+ {/* Renders BEHIND the range, inside the track, so a consumer that has a
98
+ secondary quantity to show (a media player's buffered extent) does not
99
+ have to fork this component or overlay a second element. */}
100
+ {trackDecoration}
74
101
  <SliderPrimitive.Range
102
+ data-slot="slider-range"
75
103
  className={cn(
76
104
  "absolute bg-primary",
77
- isVertical ? "w-full bottom-0" : "h-full left-0"
105
+ isVertical ? "w-full bottom-0" : "h-full left-0",
106
+ rangeClassName
78
107
  )}
79
108
  />
80
109
  </SliderPrimitive.Track>
81
- <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
110
+ {/* The accessible name and value text belong on the THUMB: that is where
111
+ Radix puts `role="slider"`, so a label left on the Root names a
112
+ generic element and the control itself stays anonymous. */}
113
+ <SliderPrimitive.Thumb
114
+ data-slot="slider-thumb"
115
+ aria-label={props['aria-label']}
116
+ aria-labelledby={props['aria-labelledby']}
117
+ aria-valuetext={props['aria-valuetext']}
118
+ className={cn(
119
+ "block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
120
+ thumbClassName
121
+ )}
122
+ />
82
123
  </SliderPrimitive.Root>
83
124
  );
84
125
  })
@@ -99,7 +99,7 @@ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './over
99
99
  // ─────────────────────────────────────────────────────────────────────────────
100
100
  export { navigationMenuTriggerStyle, NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuContent, NavigationMenuTrigger, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport } from './navigation/navigation-menu';
101
101
  export { Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem, MenubarSeparator, MenubarLabel, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarPortal, MenubarSubContent, MenubarSubTrigger, MenubarGroup, MenubarSub, MenubarShortcut } from './navigation/menubar';
102
- export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup } from './navigation/dropdown-menu';
102
+ export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, revealOnRowInteraction } from './navigation/dropdown-menu';
103
103
  export { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from './navigation/context-menu';
104
104
  export { PopoverRowButton } from './navigation/popover-row-button';
105
105
  export type { PopoverRowButtonProps } from './navigation/popover-row-button';
@@ -11,6 +11,24 @@ const DropdownMenu = DropdownMenuPrimitive.Root
11
11
 
12
12
  const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
13
13
 
14
+ /**
15
+ * Classes for a trigger that stays hidden until the row is hovered or focused
16
+ * — the `…` on a list row, a tab, a tree item.
17
+ *
18
+ * The last clause is the one that is easy to miss and impossible to see in a
19
+ * screenshot of a closed menu: while the menu is OPEN, Radix has moved focus
20
+ * into the portalled panel and the pointer has left the row, so both reveal
21
+ * conditions are false and the trigger fades out under the menu it just
22
+ * opened. `data-state` is set by Radix on any trigger, so this composes with
23
+ * the primitive and with `MenuBuilder` alike.
24
+ *
25
+ * The row must be a `group`. Compose with `cn()` and the row's own sizing.
26
+ */
27
+ const revealOnRowInteraction =
28
+ "opacity-0 transition-opacity group-hover:opacity-100 " +
29
+ "group-focus-within:opacity-100 focus-visible:opacity-100 " +
30
+ "data-[state=open]:opacity-100 motion-reduce:transition-none"
31
+
14
32
  const DropdownMenuGroup = DropdownMenuPrimitive.Group
15
33
 
16
34
  const DropdownMenuPortal = DropdownMenuPrimitive.Portal
@@ -232,6 +250,7 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
232
250
  export {
233
251
  DropdownMenu,
234
252
  DropdownMenuTrigger,
253
+ revealOnRowInteraction,
235
254
  DropdownMenuContent,
236
255
  DropdownMenuItem,
237
256
  DropdownMenuCheckboxItem,
@@ -1,217 +1,156 @@
1
1
  # ui-core styles
2
2
 
3
- `@djangocfg/ui-core` owns the semantic CSS token contract used by every
4
- frontend. CSS is the single source of truth for colors, typography, radius,
5
- status surfaces, charts, and sidebar tokens.
3
+ CSS is the single source of truth for colors, typography, radius, status
4
+ surfaces, charts, and sidebar tokens. There are no TypeScript color maps and no
5
+ runtime token generator do not add either.
6
6
 
7
7
  ## Layout
8
8
 
9
9
  ```text
10
10
  styles/
11
- ├── css/ # complete CSS layer
12
- │ ├── full.css # Tailwind v4 + tokens + base + utilities
13
- │ ├── index.css # unlayered compatibility entry
14
- │ ├── globals.css # backwards-compatible global entry
15
- │ ├── base.css # resets and document defaults
16
- │ ├── sources.css # Tailwind @source directives
17
- ├── theme.css # token/animation aggregator
18
- ├── theme/ # tokens, light, dark, animations
19
- │ ├── utilities.css # ui-core utility aggregator
20
- │ ├── utilities/ # focused utility modules
21
- │ └── presets/ # authoritative static preset token sheets
22
- │ ├── default.css
23
- │ ├── django-cfg.css
24
- │ ├── ios.css
25
- │ ├── macos.css
26
- │ ├── windows.css
27
- │ ├── soft.css
28
- │ ├── dense.css
29
- │ └── high-contrast.css
30
- ├── palette/ # runtime readers of computed CSS variables
31
- └── presets/ # TypeScript names/order only
32
- ├── index.ts
33
- ├── presets.ts
34
- └── types.ts
11
+ ├── css/
12
+ │ ├── full.css # THE entry: Tailwind + tokens + base + utilities
13
+ │ ├── base.css # resets
14
+ │ ├── sources.css # Tailwind @source directives
15
+ │ ├── theme/ # tokens.css (the @theme map), light.css, dark.css
16
+ │ ├── utilities/ # focused utility modules
17
+ └── presets/ # the authoritative token values, one file per preset
18
+ ├── palette/ # runtime readers of computed CSS variables
19
+ └── presets/ # TypeScript names and ordering only — never values
35
20
  ```
36
21
 
37
- There are deliberately no TypeScript color maps and no runtime CSS generator.
38
- Do not add a second palette in React or TypeScript.
22
+ `index.css` and `globals.css` are compatibility entries. New consumers import
23
+ `full.css`.
39
24
 
40
- `styles/presets/` contains only the TypeScript preset names and ordering used by
41
- the public API. The actual values always live in `styles/css/presets/*.css`.
42
-
43
- ## Consumer contract
44
-
45
- Import the golden path first, then exactly one product preset:
25
+ ## Consumer setup
46
26
 
47
27
  ```css
48
28
  @import "@djangocfg/ui-core/styles/full";
29
+ @plugin "tailwindcss-animate";
49
30
  @import "@djangocfg/ui-core/styles/presets/macos";
31
+ @import "@djangocfg/ui-tools/styles";
50
32
  ```
51
33
 
52
- The preset export resolves to `src/styles/css/presets/macos.css`; consumers do
53
- not depend on the physical path. The preset defines both `:root` (light) and
54
- `.dark` (dark) values. `ThemeProvider` only controls the `html.dark` class.
55
-
56
- For a product-specific adjustment, add a small override after the preset:
34
+ Order is load-bearing: the preset must come after `full.css` so its `:root` and
35
+ `.dark` values win. Application overrides come last.
57
36
 
58
- ```css
59
- :root,
60
- .dark {
61
- --font-size-base: 0.9375rem;
62
- --font-size-sm: 0.875rem;
63
- }
64
- ```
65
-
66
- Do not create a `theme-preset.ts`, append a `<style>` tag, or call a token
67
- builder during application startup.
37
+ `full.css` already imports Tailwind, tokens, base and utilities — a second
38
+ `@import "tailwindcss"` or `base.css` in the consumer is a duplicate. The
39
+ `tailwindcss-animate` plugin is registered by the consumer, not the package.
68
40
 
69
- ### Import order
41
+ A preset defines both `:root` (light) and `.dark`. `ThemeProvider` only toggles
42
+ the `html.dark` class.
70
43
 
71
- Keep the application entrypoint minimal and deterministic:
44
+ For a product adjustment, override after the preset:
72
45
 
73
46
  ```css
74
- @import "@djangocfg/ui-core/styles/full";
75
- @plugin "tailwindcss-animate";
76
- @import "@djangocfg/ui-core/styles/presets/macos";
77
- @import "@djangocfg/layouts/styles";
78
- @import "@djangocfg/ui-tools/styles";
47
+ :root, .dark { --font-size-base: 0.9375rem; }
79
48
  ```
80
49
 
81
- `full.css` already imports Tailwind, the theme tokens, base styles, and
82
- ui-core utilities. Do not add a second `@import "tailwindcss"`, `base.css`,
83
- or `utilities.css` in the consumer. Load the preset after `full.css`, so its
84
- `:root` and `.dark` variables win over defaults. Keep application overrides
85
- after the preset.
86
-
87
- The `tailwindcss-animate` plugin is registered once by the consumer. It
88
- provides classes such as `animate-in`, `fade-in-0`, and `zoom-in-95`; the
89
- package's component source uses those classes but does not register the
90
- plugin itself.
50
+ Do not build tokens at startup, append a `<style>` tag, or copy values into a
51
+ `theme-preset.ts`.
91
52
 
92
53
  ### Tailwind source scanning
93
54
 
94
- `css/sources.css` is part of the `full` entry and must scan the package source
95
- root from its own location:
55
+ `css/sources.css` must scan the package root from its own location:
96
56
 
97
57
  ```css
98
58
  @source "../../**/*.{ts,tsx}";
99
59
  ```
100
60
 
101
- This is important. Scanning only `../` scans `src/styles` and silently drops
102
- utilities used by components, including `bg-overlay` and state/animation
103
- utilities. A consumer may add extra `@source` directives for local packages,
104
- but should not replace the ui-core source directive.
61
+ Scanning `../` instead reaches only `src/styles` and silently drops every
62
+ utility used by components, `bg-overlay` among them. Consumers may add their own
63
+ `@source` lines but must not replace this one.
105
64
 
106
- ### Overlays and glass surfaces
65
+ ## Token format
107
66
 
108
- Dialog, alert-dialog, sheet, and drawer backdrops use the semantic
109
- `bg-overlay` class. The class gets its color from `--overlay` and its frosting
110
- from `--overlay-blur` (the default is `5px`):
67
+ Color variables are complete CSS colors, never bare HSL triplets. Use
68
+ `var(--background)` or `bg-background`; never `hsl(var(--background))`.
111
69
 
112
70
  ```css
113
- .bg-overlay {
114
- background: var(--overlay);
115
- backdrop-filter: blur(var(--overlay-blur));
116
- }
71
+ :root { --background: hsl(240 17% 97%); }
72
+ .dark { --background: hsl(240 5% 8%); }
117
73
  ```
118
74
 
119
- Glass surfaces must have a translucent background; an opaque `bg-card` or
120
- `bg-popover` makes the blur invisible. A dropdown rendered inside an ancestor
121
- that already has `backdrop-filter` is also unable to blur the page behind that
122
- ancestor because it becomes a backdrop root. PublicLayout desktop dropdowns
123
- are therefore rendered through a body portal and positioned from the trigger.
124
- Use the same pattern for new floating surfaces that must blur content outside
125
- their parent stacking context.
75
+ Typography variables are ordinary CSS values (`--font-size-base: 0.8125rem`).
126
76
 
127
- For custom glass utilities, prefer a Tailwind utility such as
128
- `backdrop-blur-[5px]` when the consuming build must emit the standard
129
- `backdrop-filter` declaration. Keep the visual token (`--overlay-blur`) in
130
- CSS rather than duplicating a pixel value in React.
77
+ ### Raised and recessed surfaces
131
78
 
132
- ## Token format
79
+ | Token | Reads as | Use for |
80
+ |---|---|---|
81
+ | `--surface-hover` | a step toward the reader | pointer feedback on a resting surface |
82
+ | `--surface-recessed` | sunk below the page | chrome framing content: a tab strip, a rail, a footer band |
133
83
 
134
- Color variables are complete CSS colors, never bare HSL triplets:
84
+ `--surface-recessed` is a translucent black, not a mix with `--background`. A
85
+ percentage mix takes its step from what is already there, and a dark page has
86
+ almost nothing to take: 6% black into `hsl(240 5% 8%)` moved it two bytes out of
87
+ 255, which no eye resolves. A fixed overlay steps by the same amount whatever
88
+ the page — and the alpha differs per theme, because the same overlay costs far
89
+ more contrast on a near-white ground.
135
90
 
136
- ```css
137
- :root {
138
- --background: hsl(240 17% 97%);
139
- }
140
- .dark {
141
- --background: hsl(240 5% 8%);
142
- }
143
- ```
91
+ **Never recess with `--muted` or `--card`.** Their direction is not guaranteed:
92
+ `theme/dark.css` keeps `--muted` below `--background`, while the macOS preset
93
+ raises every resting surface (background 8%, muted 11%, card 12%). A tab strip
94
+ painted `bg-muted` floated toward the reader on that preset and looked correct
95
+ in the base theme.
144
96
 
145
- Tailwind maps semantic utilities to these variables through `@theme inline`.
146
- Use `var(--background)` or `bg-background`; do not write
147
- `hsl(var(--background))`.
97
+ ### Overlays and glass
148
98
 
149
- Typography variables are ordinary CSS values:
99
+ `bg-overlay` takes its color from `--overlay` and its frosting from
100
+ `--overlay-blur` (default `5px`). A glass surface needs a translucent
101
+ background — an opaque `bg-card` makes the blur invisible.
150
102
 
151
- ```css
152
- :root,
153
- .dark {
154
- --font-size-base: 0.8125rem;
155
- --font-size-sm: 0.75rem;
156
- }
157
- ```
103
+ An element inside an ancestor that already has `backdrop-filter` cannot blur the
104
+ page behind that ancestor: the ancestor becomes a backdrop root. Render such
105
+ surfaces through a body portal, positioned from their trigger.
158
106
 
159
107
  ## Preset rules
160
108
 
161
- - Presets are authored directly in `styles/css/presets/`.
162
- - Every preset must define valid `hsl(...)`, `color-mix(...)`, or other complete
163
- CSS color values.
164
- - Keep light and dark pairs together in the same file.
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`.
169
- - An interactive surface must sit at least 4 HSL lightness points away from the
170
- surface it lands on, **in both modes**: `--accent` vs `--background`, and
171
- `--sidebar-accent` vs `--sidebar-background`. Direction is deliberately not
172
- asserted (light darkens its hover, dark lightens it) only the size of the
173
- step, so both modes do comparable work for the same interaction. Enforced by
174
- `check:contrast`.
175
-
176
- This rule exists because the one above it was not enough. A sidebar hover
177
- lands on `--sidebar-background`, which nothing checked, so presets shipped
178
- one-point steps in light against eleven in dark and one stepped the wrong
179
- way, its hover lighter than the rail it highlighted. Every one passed the
180
- `--muted`/`--background` gate. **Check the surface the user actually sees,
181
- not a neighbouring one that happens to be nearby.**
182
- - Changes to a preset require checking both light and dark modes.
183
- - Product density overrides belong in the consuming app's CSS, not in a copied
184
- TypeScript map.
109
+ - Values live in `styles/css/presets/*.css`. `styles/presets/*.ts` holds names
110
+ and ordering only.
111
+ - Every value must be a complete CSS color. Keep a preset's light and dark pairs
112
+ in one file. A preset may omit tokens that inherit from `theme.css`.
113
+ - **Light mode:** `--muted` at least 2 lightness points darker than
114
+ `--background`, so translucent muted fills stay visible.
115
+ - **Both modes:** an interactive surface sits at least 4 lightness points from
116
+ the surface it lands on — `--accent` vs `--background`, `--sidebar-accent` vs
117
+ `--sidebar-background`.
118
+
119
+ Direction is not asserted, only the step size. The second pair exists because
120
+ the first was not enough: a sidebar hover lands on `--sidebar-background`,
121
+ which nothing checked, so presets shipped one-point steps in light against
122
+ eleven in dark, and one hover came out lighter than the rail it highlighted —
123
+ all while passing the `--muted` gate. **Check the surface the user sees, not a
124
+ neighbour that happens to be nearby.**
125
+
126
+ Because direction is free, product code must not infer it from a token's
127
+ value. That is what `--surface-recessed` and `--surface-hover` are for.
128
+ - Density overrides belong in the consuming app's CSS.
185
129
 
186
130
  ## Verification
187
131
 
188
132
  ```bash
189
- pnpm check:contrast
190
- pnpm check
133
+ pnpm check:contrast # preset gates + the macOS AA compact-text audit
134
+ pnpm check # tsc --noEmit
191
135
  ```
192
136
 
193
- `check:contrast` merges every static preset over the base theme and validates
194
- the light-mode background/muted surface hierarchy. It also keeps the complete
195
- macOS compact-text audit: semantic pairs must reach WCAG AA 4.5:1 in both
196
- modes, including primary, destructive, sidebar, status-surface, on-fill, and
197
- muted text against every common surface. When tuning a token, change the
198
- semantic pair together rather than overriding text in a consumer component.
137
+ `check:contrast` merges each static preset over the base theme, enforces the
138
+ rules above, and requires WCAG AA 4.5:1 for semantic pairs in both modes. When
139
+ tuning a token, move the semantic pair together rather than overriding text in a
140
+ consumer component.
199
141
 
200
- At the consumer, build the app that imports the preset and verify both modes.
201
- The important invariant is that changing a preset requires editing one CSS
202
- file, and no React runtime code is involved in applying it.
142
+ Then build a consumer and look at both modes.
203
143
 
204
- For a local cross-repository change, use the consumer's sync command and
205
- restart its dev server afterward:
144
+ ## Cross-repository changes
206
145
 
207
146
  ```bash
208
147
  pnpm sync:cfg:one ui-core
209
- pnpm sync:cfg:one layouts
210
- pnpm sync:cfg:one i18n # required peer when ui-core is synced locally
211
- rm -rf apps/web/.next
148
+ pnpm sync:cfg:one i18n # required peer when ui-core is synced locally
212
149
  ```
213
150
 
214
- Do not delete `pnpm-lock.yaml` to force a package update. Update the package
215
- range with pnpm, then let the lockfile record the resolved version. A clean
216
- install restores published packages and therefore requires publishing a new
217
- package version for source changes to survive.
151
+ Restart the consumer's dev server afterward.
152
+
153
+ A sync is local and temporary: a clean install restores the published package,
154
+ so a source change survives only once a new version is published. Update the
155
+ dependency range with pnpm and let the lockfile record the resolution — never
156
+ delete `pnpm-lock.yaml` to force an update.
@@ -37,6 +37,18 @@
37
37
  --muted-foreground: hsl(48 5% 59%);
38
38
  /* Neutral interaction surface: a quiet lift above the current dark page. */
39
39
  --surface-hover: color-mix(in oklab, var(--card) 96%, var(--foreground));
40
+ /* The counterpart to --surface-hover: a surface that reads as sunk BELOW the
41
+ * page — a tab strip under a document, a rail beside it. Every other surface
42
+ * token raises, so a chrome band painted with one of them floats toward the
43
+ * viewer instead of receding, and a preset whose --muted sits above
44
+ * --background has no recessed fill at all.
45
+ *
46
+ * A translucent BLACK, not a mix with --background. A percentage mix takes its
47
+ * step from what is already there, and a dark page has almost nothing to take:
48
+ * mixing 6% black into hsl(240 5% 8%) moved it two bytes out of 255, which no
49
+ * eye resolves. A fixed overlay steps by the same amount whatever the page,
50
+ * and composites over whatever surface the band happens to sit on. */
51
+ --surface-recessed: rgb(0 0 0 / 0.35);
40
52
  /* Hover/active surface (rails, menus, tabs) — a quiet warm lift above the page. */
41
53
  --accent: hsl(48 3% 24%);
42
54
  --accent-foreground: hsl(48 33% 97%);
@@ -34,6 +34,11 @@
34
34
  * It deliberately keys off --background, not white --card, so hover remains
35
35
  * visible on translucent menus and on plain pages alike. */
36
36
  --surface-hover: color-mix(in oklab, var(--background) 96%, var(--foreground));
37
+ /* The counterpart to --surface-hover: a surface that reads as sunk BELOW the
38
+ * page. A translucent black for the same reason as the dark theme's, but much
39
+ * weaker: the same overlay costs far more contrast on a near-white ground,
40
+ * where it goes muddy rather than quiet. */
41
+ --surface-recessed: rgb(0 0 0 / 0.045);
37
42
  /* Neutral hover/active surface — quiet warm-gray lift (Claude bg-200), a
38
43
  * clear step (≥4L) below the 97% canvas. */
39
44
  --accent: hsl(60 11% 92%);
@@ -39,6 +39,7 @@
39
39
  --color-muted: var(--muted);
40
40
  --color-muted-foreground: var(--muted-foreground);
41
41
  --color-surface-hover: var(--surface-hover);
42
+ --color-surface-recessed: var(--surface-recessed);
42
43
  --color-accent: var(--accent);
43
44
  --color-accent-foreground: var(--accent-foreground);
44
45
  --color-destructive: var(--destructive);