@bearmenu/ui 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/globals.css +162 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bearmenu/ui",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "BearMenu design system — shared UI tokens, primitives, and components",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/src/globals.css CHANGED
@@ -33,34 +33,77 @@ h6 {
33
33
  --sab: env(safe-area-inset-bottom);
34
34
  --sal: env(safe-area-inset-left);
35
35
 
36
- --background: oklch(0.97 0.012 80);
37
- --foreground: oklch(0.22 0.025 45);
38
- --card: oklch(0.99 0.006 70);
39
- --card-foreground: oklch(0.22 0.025 45);
40
- --popover: oklch(0.99 0.006 70);
41
- --popover-foreground: oklch(0.22 0.025 45);
36
+ /* ─── Light neutral ramp (0.8.3 — "warmth has edges") ─────────────────
37
+ Every surface and text token below is TRUE NEUTRAL: chroma exactly 0,
38
+ 8-bit R−B spread exactly 0. Grey. Not "warm grey", not "off-white" —
39
+ grey. That is deliberate and hard-won; read this before adding a tint.
40
+
41
+ These used to carry chroma 0.008-0.025 at hue 45-80, on the theory that a
42
+ faint warm tint everywhere expressed a warm brand. In practice a low-chroma
43
+ tint spread across large soft areas — page, chip fills, hover fills,
44
+ hairlines, shadows — does not read as warmth. It reads as a colour cast:
45
+ "tan", "peachy", "hurts my eyes", "unprofessional for a serious app".
46
+
47
+ It was walked down in stages — 0.012 → 0.005 → 0.0025 → 0.003 → 0 — and
48
+ was still being rejected at chroma 0.0025, an R−B spread of THREE, which
49
+ is about as small as an 8-bit tint gets. That is the finding: on a large
50
+ light field there is no amount of warm chroma small enough to be safe.
51
+ Any nonzero value at hue 45-80 eventually gets described as brown.
52
+
53
+ The rule that replaces it: warmth needs an EDGE and real saturation. It
54
+ lives in --accent, --gradient-primary, badges, CTAs and photography —
55
+ bounded things at chroma >= 0.09. Never in a fill, a blur or body text.
56
+ The contrast between a neutral field and a saturated accent is what makes
57
+ the accent read as intentional. Do not re-warm these. */
58
+ --background: oklch(0.986 0 0);
59
+ --foreground: oklch(0.18 0 0);
60
+ --card: oklch(1 0 0);
61
+ --card-foreground: oklch(0.18 0 0);
62
+ --popover: oklch(1 0 0);
63
+ --popover-foreground: oklch(0.18 0 0);
42
64
  --primary: oklch(0.25 0.05 35);
43
65
  --primary-foreground: oklch(0.98 0 0);
44
- --secondary: oklch(0.985 0.008 80);
45
- --secondary-foreground: oklch(0.22 0.025 45);
46
- --muted: oklch(0.985 0.008 80);
47
- --muted-foreground: oklch(0.62 0.015 45);
48
- --accent: oklch(0.94 0.04 50);
49
- --accent-foreground: oklch(0.22 0.025 45);
66
+ --secondary: oklch(0.955 0 0);
67
+ --secondary-foreground: oklch(0.18 0 0);
68
+ --muted: oklch(0.955 0 0);
69
+ --muted-foreground: oklch(0.52 0 0);
70
+ /* BREAKING in 0.8.3 — see CHANGELOG. --accent was oklch(0.94 0.04 50), a
71
+ pale peach FILL paired with a dark --accent-foreground. It is now the
72
+ brand terracotta, used as text and as a filled surface with WHITE
73
+ foreground. A consumer rendering `bg-accent text-accent-foreground` gets a
74
+ dark brown chip where it used to get a pale one. bearmenu-fe already ran
75
+ these values as a local override; promoting them stops Storybook and the
76
+ product disagreeing about what "accent" means. */
77
+ --accent: oklch(0.51 0.095 45);
78
+ --accent-foreground: oklch(1 0 0);
79
+ /* Recessed-well companion to --accent: a tinted surface for active chips,
80
+ wells and highlighted rows. Neutral on purpose — its call sites pair it
81
+ with `text-accent`, and the text is what carries the meaning. */
82
+ --accent-subtle: oklch(0.945 0 0);
50
83
  --destructive: oklch(0.577 0.245 27.325);
51
84
  --destructive-foreground: oklch(0.985 0 0);
52
- --border: oklch(0.9 0.012 60);
53
- --input: oklch(0.9 0.012 60);
85
+ --border: oklch(0.895 0 0);
86
+ --input: oklch(0.895 0 0);
54
87
  --ring: oklch(0.25 0.05 35);
55
88
  --chart-1: oklch(0.646 0.222 41.116);
56
89
  --chart-2: oklch(0.6 0.118 184.704);
57
90
  --chart-3: oklch(0.398 0.07 227.392);
58
91
  --chart-4: oklch(0.828 0.189 84.429);
59
92
  --chart-5: oklch(0.769 0.188 70.08);
60
- --surface: oklch(0.99 0.006 70);
93
+ --surface: oklch(1 0 0);
61
94
  --surface-foreground: var(--foreground);
62
- --surface-2: oklch(0.985 0.008 80);
63
- --line-soft: oklch(0.94 0.01 60);
95
+ --surface-2: oklch(0.968 0 0);
96
+ --line-soft: oklch(0.935 0 0);
97
+ /* The top plane. In light mode white IS the ceiling — "elevated" means card
98
+ + shadow, there is nothing above it to reach for — so this equals --card.
99
+ Dark reaches the same idea from below (see .dark). The asymmetry is
100
+ correct; do not "fix" the two into agreement. */
101
+ --surface-elevated: oklch(1 0 0);
102
+ /* Text tiers. --ink-secondary and --ink-tertiary sit between --foreground
103
+ and --muted-foreground for captions, counts and metadata. */
104
+ --ink-primary: var(--foreground);
105
+ --ink-secondary: oklch(0.45 0 0);
106
+ --ink-tertiary: oklch(0.52 0 0);
64
107
  --radius: 0.625rem;
65
108
  --sidebar: oklch(0.985 0 0);
66
109
  --sidebar-foreground: oklch(0.145 0 0);
@@ -75,6 +118,26 @@ h6 {
75
118
  --warning: oklch(0.72 0.18 68);
76
119
  --warning-foreground: oklch(0.18 0.04 45);
77
120
 
121
+ /* ─── Brand gradient ──────────────────────────────────────────────────
122
+ The terracotta ramp used by the FAB, primary buttons, banner CTAs and
123
+ featured surfaces. This — not any surface tint — is where the brand's
124
+ warmth lives: chroma 0.09-0.10, on bounded elements. */
125
+ --gradient-primary: linear-gradient(180deg, oklch(0.58 0.1 45) 0%, oklch(0.44 0.09 42) 100%);
126
+ --gradient-primary-from: oklch(0.58 0.1 45);
127
+ --gradient-primary-to: oklch(0.44 0.09 42);
128
+
129
+ /* ─── Generic elevation ───────────────────────────────────────────────
130
+ Pure neutral (chroma 0) at L 0.52. These were a saturated brown, which put
131
+ a soft-edged tan patch under every card, row and overlay — the single
132
+ largest warm area on a light page, and the hardest to attribute because a
133
+ blur has no boundary to blame it on. Alphas are the calibrated set; only
134
+ the tint changed. Apps compose these; they must not re-tint them. */
135
+ --shadow-card: 0 1px 2px oklch(0.52 0 0 / 0.05), 0 2px 6px -1px oklch(0.52 0 0 / 0.08);
136
+ --shadow-card-hover:
137
+ 0 2px 4px -1px oklch(0.52 0 0 / 0.07), 0 10px 24px -6px oklch(0.52 0 0 / 0.16);
138
+ --shadow-overlay:
139
+ 0 4px 12px -2px oklch(0.52 0 0 / 0.1), 0 16px 48px -12px oklch(0.52 0 0 / 0.2);
140
+
78
141
  /* Brown variant — replaces hardcoded #482E1F across button/badge */
79
142
  --brown: oklch(0.31 0.05 45);
80
143
  --brown-foreground: oklch(0.985 0 0);
@@ -90,10 +153,18 @@ h6 {
90
153
  --shadow-alpha-2xl: 0.25;
91
154
 
92
155
  /* ─── Card cover shadows (companion to lib/card-hover.ts) ─────────────
93
- The warm lift under a card's cover region, and its deepened hover state.
156
+ The lift under a card's cover region, and its deepened hover state.
94
157
  Deliberately separate from the --shadow-alpha-* ladder above: that is a
95
- neutral black ramp for surfaces, this is the warm brown cast that only
96
- reads correctly under photography and generated cover art.
158
+ pure-black ramp for surfaces, this is a softer, lighter cast tuned for
159
+ photography and generated cover art.
160
+
161
+ 0.8.3: was rgba(120, 80, 50, …) — an 8-bit R−B spread of 70, by far the
162
+ most saturated shadow in the system, compositing to spread 18-23 under
163
+ every card cover. It survived an entire light-theme neutralisation pass
164
+ because it lives here and nothing overrode it. Now oklch(0.45 0 0),
165
+ matched on LUMINANCE (0.091 vs the old 0.100) so the visual weight is
166
+ unchanged, with the tint gone entirely. Same reasoning as the light ramp
167
+ above: a wide blur cannot hold a tint without reading as a stain.
97
168
 
98
169
  --shadow-menu-cover-inset is the pressed-in "well" edge carried by covers
99
170
  that render a generated illustration instead of a photo. It has to live in
@@ -104,8 +175,8 @@ h6 {
104
175
  Consumer apps must not redefine these: their stylesheet is imported after
105
176
  this one and would silently win, which is the drift these tokens exist to
106
177
  end. */
107
- --shadow-card-image: 0 4px 14px rgba(120, 80, 50, 0.22);
108
- --shadow-card-image-hover: 0 12px 28px rgba(120, 80, 50, 0.3);
178
+ --shadow-card-image: 0 4px 14px oklch(0.45 0 0 / 0.22);
179
+ --shadow-card-image-hover: 0 12px 28px oklch(0.45 0 0 / 0.3);
109
180
  --shadow-menu-cover-inset:
110
181
  inset 0 1px 2px oklch(1 0 0 / 0.15), inset 0 -1px 4px oklch(0 0 0 / 0.18);
111
182
 
@@ -148,35 +219,52 @@ h6 {
148
219
  }
149
220
 
150
221
  .dark {
151
- --background: oklch(0.17 0.014 45);
152
- --foreground: oklch(0.97 0.008 60);
153
- --card: oklch(0.21 0.016 45);
154
- --card-foreground: oklch(0.97 0.008 60);
155
- --popover: oklch(0.21 0.016 45);
156
- --popover-foreground: oklch(0.97 0.008 60);
157
- --primary: oklch(0.98 0 0);
158
- --primary-foreground: oklch(0.25 0.05 35);
222
+ /* Dark keeps its warmth: on a dark canvas a low-chroma warm tint reads as
223
+ "espresso", not as a cast — the light-mode problem is a light-mode
224
+ problem. These are bearmenu-fe's shipped dark values, promoted in 0.8.3
225
+ alongside the light ramp so both themes have one source of truth. */
226
+ --background: oklch(0.155 0.01 45);
227
+ --foreground: oklch(0.96 0.005 60);
228
+ --card: oklch(0.205 0.01 45);
229
+ --card-foreground: oklch(0.96 0.005 60);
230
+ --popover: oklch(0.205 0.01 45);
231
+ --popover-foreground: oklch(0.96 0.005 60);
232
+ --primary: oklch(0.66 0.09 45);
233
+ --primary-foreground: oklch(0.155 0.01 45);
159
234
  --secondary: oklch(0.24 0.018 45);
160
- --secondary-foreground: oklch(0.97 0.008 60);
161
- --muted: oklch(0.24 0.018 45);
162
- --muted-foreground: oklch(0.6 0.012 60);
163
- --accent: oklch(0.3 0.04 50);
164
- --accent-foreground: oklch(0.97 0.008 60);
235
+ --secondary-foreground: oklch(0.96 0.005 60);
236
+ --muted: oklch(0.22 0.01 45);
237
+ --muted-foreground: oklch(0.72 0.008 60);
238
+ /* BREAKING alongside light: --accent is the terracotta, not a dark tint. */
239
+ --accent: oklch(0.66 0.09 45);
240
+ --accent-foreground: oklch(0.155 0.01 45);
241
+ --accent-subtle: oklch(0.255 0.022 45);
165
242
  /* AA-compliant on dark surfaces but dialed back from 0.68 to soften eye strain */
166
243
  --destructive: oklch(0.58 0.17 25.7);
167
244
  --destructive-foreground: oklch(0.985 0 0);
168
- --border: oklch(0.3 0.015 45);
169
- --input: oklch(0.3 0.015 45);
245
+ --border: oklch(0.32 0.01 45);
246
+ --input: oklch(0.32 0.01 45);
170
247
  --ring: oklch(0.98 0 0);
171
248
  --chart-1: oklch(0.488 0.243 264.376);
172
249
  --chart-2: oklch(0.696 0.17 162.48);
173
250
  --chart-3: oklch(0.769 0.188 70.08);
174
251
  --chart-4: oklch(0.627 0.265 303.9);
175
252
  --chart-5: oklch(0.645 0.246 16.439);
176
- --surface: oklch(0.21 0.016 45);
253
+ --surface: oklch(0.205 0.01 45);
177
254
  --surface-foreground: var(--foreground);
178
255
  --surface-2: oklch(0.24 0.018 45);
179
256
  --line-soft: oklch(0.26 0.014 45);
257
+ /* Dark reaches "top plane" from BELOW — a step up off the canvas — where
258
+ light reaches it by being white. Same token, opposite direction. */
259
+ --surface-elevated: oklch(0.225 0.012 45);
260
+ --ink-primary: var(--foreground);
261
+ --ink-secondary: oklch(0.78 0.005 60);
262
+ --ink-tertiary: oklch(0.6 0.006 60);
263
+ /* Black, not the light ramp's near-neutral grey: on a 0.155 canvas a grey
264
+ shadow has nothing to darken. */
265
+ --shadow-card: 0 1px 3px oklch(0 0 0 / 0.2), 0 1px 2px oklch(0 0 0 / 0.15);
266
+ --shadow-card-hover: 0 4px 16px oklch(0 0 0 / 0.3), 0 1px 4px oklch(0 0 0 / 0.2);
267
+ --shadow-overlay: 0 8px 32px oklch(0 0 0 / 0.4), 0 2px 8px oklch(0 0 0 / 0.25);
180
268
  --sidebar: oklch(0.205 0 0);
181
269
  --sidebar-foreground: oklch(0.985 0 0);
182
270
  --sidebar-primary: oklch(0.488 0.243 264.376);
@@ -262,7 +350,12 @@ h6 {
262
350
  --color-surface: var(--surface);
263
351
  --color-surface-foreground: var(--surface-foreground);
264
352
  --color-surface-2: var(--surface-2);
353
+ --color-surface-elevated: var(--surface-elevated);
265
354
  --color-line-soft: var(--line-soft);
355
+ --color-accent-subtle: var(--accent-subtle);
356
+ --color-ink-primary: var(--ink-primary);
357
+ --color-ink-secondary: var(--ink-secondary);
358
+ --color-ink-tertiary: var(--ink-tertiary);
266
359
 
267
360
  /* Theme-aware shadow utilities — override Tailwind defaults so they
268
361
  stay visible on dark surfaces */
@@ -294,15 +387,44 @@ h6 {
294
387
  @apply border-border outline-ring/50;
295
388
  }
296
389
 
390
+ /* Reserve the scrollbar gutter so content does not jump when a page grows
391
+ * past one viewport, or when a modal locks scrolling.
392
+ *
393
+ * This was `overflow-y: scroll`, which reserved the same space by forcing a
394
+ * permanent scrollbar — and silently disabled every scroll lock in every
395
+ * consumer app.
396
+ *
397
+ * WHY IT BROKE THEM. `react-remove-scroll` — which Radix Dialog and Sheet
398
+ * use, and which vaul's Drawer sits on top of — locks the page by setting
399
+ * `overflow: hidden !important` on BODY. That only reaches the viewport
400
+ * through CSS overflow propagation, and the propagation rule applies ONLY
401
+ * while the ROOT element's overflow is `visible`: give <html> any explicit
402
+ * overflow and the viewport takes its value from <html> instead, so the body
403
+ * rule merely clips the body box while the document scrolls on behind the
404
+ * open dialog. Symptom: the page scrolls under every drawer and sheet, in
405
+ * every browser except Safari — where vaul has a second, `position: fixed`
406
+ * lock that does not depend on propagation (`usePositionFixed`, Safari-gated),
407
+ * which is what makes this look intermittent.
408
+ *
409
+ * `scrollbar-gutter: stable` reserves the same space without touching
410
+ * overflow, so <html> stays `visible` and the lock works as designed. Safari
411
+ * below 18.2 ignores the property, but Safari's overlay scrollbars take no
412
+ * layout space there, so there is nothing to reserve and nothing to shift.
413
+ *
414
+ * DO NOT put an `overflow` of any kind back on <html>. An app that needs to
415
+ * clip an axis puts it on `body`, which propagates rather than blocking. */
297
416
  html {
298
- overflow-y: scroll; /* Always show vertical scrollbar to prevent layout shift */
417
+ scrollbar-gutter: stable;
299
418
  }
300
419
 
301
420
  body {
302
421
  @apply bg-background text-foreground;
303
422
  }
304
423
 
305
- /* Prevent Radix UI from adding padding when locking scroll */
424
+ /* `react-remove-scroll` compensates for the scrollbar it removes by adding an
425
+ * equal `margin-right`. The gutter above is reserved permanently and never
426
+ * goes away, so that compensation would double up and shove the page sideways
427
+ * by a scrollbar width every time a dialog opens. Keep this. */
306
428
  body[data-scroll-locked] {
307
429
  margin-right: 0px !important;
308
430
  }