@mindlogic-ai/logician-ui 3.1.0-alpha.8 → 3.1.0
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/dist/components/InfoSprinkle/InfoSprinkle.d.ts +1 -1
- package/dist/components/InfoSprinkle/InfoSprinkle.d.ts.map +1 -1
- package/dist/components/InfoSprinkle/InfoSprinkle.js +25 -2
- package/dist/components/InfoSprinkle/InfoSprinkle.js.map +1 -1
- package/dist/components/InfoSprinkle/InfoSprinkle.mjs +25 -2
- package/dist/components/InfoSprinkle/InfoSprinkle.mjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.js +20 -4
- package/dist/components/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.mjs +20 -4
- package/dist/components/SegmentedControl/SegmentedControl.mjs.map +1 -1
- package/dist/hooks/useHasHover.d.ts +13 -0
- package/dist/hooks/useHasHover.d.ts.map +1 -0
- package/dist/hooks/useHasHover.js +34 -0
- package/dist/hooks/useHasHover.js.map +1 -0
- package/dist/hooks/useHasHover.mjs +29 -0
- package/dist/hooks/useHasHover.mjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/theme/colors.d.ts +196 -44
- package/dist/theme/colors.d.ts.map +1 -1
- package/dist/theme/colors.js +184 -22
- package/dist/theme/colors.js.map +1 -1
- package/dist/theme/colors.mjs +184 -22
- package/dist/theme/colors.mjs.map +1 -1
- package/dist/theme/global.d.ts.map +1 -1
- package/dist/theme/global.js +30 -2
- package/dist/theme/global.js.map +1 -1
- package/dist/theme/global.mjs +30 -2
- package/dist/theme/global.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/InfoSprinkle/InfoSprinkle.tsx +32 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +21 -4
- package/src/hooks/useHasHover.ts +32 -0
- package/src/index.ts +1 -0
- package/src/theme/SemanticTokens.mdx +61 -7
- package/src/theme/colors.ts +216 -26
- package/src/theme/global.ts +31 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns whether the primary pointing device is capable of hovering
|
|
7
|
+
* (i.e. a mouse/trackpad rather than a touchscreen).
|
|
8
|
+
*
|
|
9
|
+
* Useful for components that rely on hover interactions on desktop but need a
|
|
10
|
+
* tap/click affordance on touch devices, where hover events never fire.
|
|
11
|
+
*
|
|
12
|
+
* Defaults to `true` so server-rendered markup matches the most common
|
|
13
|
+
* (desktop) case, then resolves to the real value after mount.
|
|
14
|
+
*/
|
|
15
|
+
export const useHasHover = () => {
|
|
16
|
+
const [hasHover, setHasHover] = useState(true);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (typeof window === 'undefined' || !window.matchMedia) return;
|
|
20
|
+
|
|
21
|
+
const mql = window.matchMedia('(hover: hover)');
|
|
22
|
+
const update = () => setHasHover(mql.matches);
|
|
23
|
+
|
|
24
|
+
update();
|
|
25
|
+
mql.addEventListener('change', update);
|
|
26
|
+
return () => mql.removeEventListener('change', update);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
return hasHover;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default useHasHover;
|
package/src/index.ts
CHANGED
|
@@ -102,6 +102,7 @@ export {
|
|
|
102
102
|
} from './components/Typography';
|
|
103
103
|
|
|
104
104
|
// Hooks
|
|
105
|
+
export { useHasHover } from './hooks/useHasHover';
|
|
105
106
|
export type { LanguageContextValue } from './hooks/useLanguage';
|
|
106
107
|
export { LanguageContext, useLanguage } from './hooks/useLanguage';
|
|
107
108
|
|
|
@@ -34,7 +34,7 @@ primitive depending on the active color mode.
|
|
|
34
34
|
Two payoffs:
|
|
35
35
|
|
|
36
36
|
1. **Dark mode for free.** Add `color="fg.default"` and it's `gray.1300` in
|
|
37
|
-
light and
|
|
37
|
+
light and a light neutral in dark — no `useColorMode`, no conditionals.
|
|
38
38
|
2. **Intent, not value.** `border.subtle` always reads as "a low-emphasis
|
|
39
39
|
divider," whatever the mode. The intent is encoded once, in the theme.
|
|
40
40
|
|
|
@@ -49,21 +49,38 @@ have no semantic meaning.
|
|
|
49
49
|
These cover text, surfaces, and borders — the bulk of any UI. Each flips
|
|
50
50
|
between a light and a dark value.
|
|
51
51
|
|
|
52
|
+
> **Dark neutrals are desaturated.** The `Dark` columns below name the `gray`
|
|
53
|
+
> step each token mirrors, but in dark mode every neutral actually resolves to a
|
|
54
|
+
> _halved-saturation_ counterpart of that step (the blue-tinted `gray` ramp is
|
|
55
|
+
> right for light surfaces but muddy as a dark one). Light values are exactly the
|
|
56
|
+
> named `gray` step. You never reference the desaturated values directly — use
|
|
57
|
+
> the tokens.
|
|
58
|
+
|
|
52
59
|
### Foreground — `fg.*` (text & icons)
|
|
53
60
|
|
|
54
61
|
| Token | Light | Dark | Use for |
|
|
55
62
|
| --- | --- | --- | --- |
|
|
56
|
-
| `fg.
|
|
57
|
-
| `fg.
|
|
58
|
-
| `fg.
|
|
63
|
+
| `fg.emphasized` | `gray.1300` | `gray.200` | Headings, titles, key figures, strong emphasis |
|
|
64
|
+
| `fg.default` | `gray.1000` | `gray.300` | Primary **body** text |
|
|
65
|
+
| `fg.muted` | `gray.900` | `gray.400` | Secondary text, captions, supporting icons |
|
|
66
|
+
| `fg.subtle` | `gray.700` | `gray.600`† | Tertiary text, placeholders, icons |
|
|
59
67
|
| `fg.inverse` | `gray.0` | `gray.1400` | Text on an **inverse** surface (e.g. tooltip) |
|
|
60
68
|
|
|
69
|
+
† `fg.subtle`'s dark value is bumped one step lighter than the straight mirror so
|
|
70
|
+
helper/secondary text clears WCAG AA 4.5:1 on `bg.muted`.
|
|
71
|
+
|
|
72
|
+
> `fg.default` is the body weight; `fg.emphasized` is the strongest step (the
|
|
73
|
+
> near-black `gray.1300` `fg.default` used to be). Reach for `emphasized` only
|
|
74
|
+
> when you want maximum contrast — headings, key numbers.
|
|
75
|
+
|
|
61
76
|
### Background — `bg.*` (surfaces & fills)
|
|
62
77
|
|
|
63
78
|
| Token | Light | Dark | Use for |
|
|
64
79
|
| --- | --- | --- | --- |
|
|
65
80
|
| `bg.canvas` | `gray.0` | `gray.1500` | App/page background |
|
|
66
81
|
| `bg.surface` | `white` | `gray.1400` | Raised surface — cards, popovers, menus, modals |
|
|
82
|
+
| `bg.raised` | `white` | `gray.1100` | Strongly-raised neutral — one level above `surface` (e.g. a selected segment); tops the dark ramp so it reads as lifted |
|
|
83
|
+
| `bg.sunken` | `gray.50` | `gray.1500` | Sunken page wash for list/overview surfaces, so `bg.surface` cards read as raised above it |
|
|
67
84
|
| `bg.subtle` | `gray.50` | `gray.1300` | Subtle fill, hover background, secondary surface |
|
|
68
85
|
| `bg.muted` | `gray.100` | `gray.1200` | Muted fill, tertiary surface, neutral chips |
|
|
69
86
|
| `bg.inverse` | `gray.1300` | `gray.50` | High-contrast/inverse surface (flips with mode) |
|
|
@@ -86,6 +103,30 @@ between a light and a dark value.
|
|
|
86
103
|
</Box>
|
|
87
104
|
```
|
|
88
105
|
|
|
106
|
+
### `slate.*` — foundational mode-aware neutral family
|
|
107
|
+
|
|
108
|
+
`slate.0`–`slate.1500` is a **first-class neutral palette**, sitting alongside
|
|
109
|
+
the raw `gray.*` primitives but **mode-aware**: `slate.N` is `gray.N` in light
|
|
110
|
+
and the desaturated counterpart in dark, so one token holds the same tonal level
|
|
111
|
+
in both modes (`slate.300` is a light divider in light and the matching dark
|
|
112
|
+
divider in dark — no `_dark={{…}}` at the call site). It lives under
|
|
113
|
+
`semanticTokens` only because Chakra requires that for the `_dark` flip;
|
|
114
|
+
conceptually it's a **foundation** (a tonal scale), not a **role**.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// Use a role token when one matches the intent (carries semantics + AA tuning):
|
|
118
|
+
<Text color="fg.muted" />
|
|
119
|
+
|
|
120
|
+
// Reach for slate when you need a specific neutral step no role names — the
|
|
121
|
+
// mode-aware equivalent of dropping to a raw gray.N:
|
|
122
|
+
<Box borderColor="slate.300" bg="slate.50" />
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> `slate` and the role tokens (`fg`/`bg`/`border`) are **distinct ramps** —
|
|
126
|
+
> `slate` is a mechanical light↔dark mirror, while the roles carry hand-tuned
|
|
127
|
+
> dark values (AA bumps, hierarchy). They share light values but **diverge in
|
|
128
|
+
> dark**, so they are not drop-in interchangeable. Pick by intent, not by value.
|
|
129
|
+
|
|
89
130
|
---
|
|
90
131
|
|
|
91
132
|
## The brand tokens
|
|
@@ -110,6 +151,18 @@ flips in dark mode:
|
|
|
110
151
|
In dark mode these steps lighten so they keep contrast on dark surfaces — e.g.
|
|
111
152
|
`primary.main` is `blue.500` in light and lifts to `blue.300` in dark.
|
|
112
153
|
|
|
154
|
+
### Solid brand fills — `primary.fill` / `primary.fillStrong`
|
|
155
|
+
|
|
156
|
+
For a **solid blue surface with white text/icons on top** (modal headers, hero
|
|
157
|
+
banners, brand badges), use `primary.fill` (or `primary.fillStrong` for a deeper
|
|
158
|
+
shade) rather than `primary.main`. The `*.main`/`*.dark` steps lighten in dark —
|
|
159
|
+
right for foreground, but too light to carry white text as a fill — so the
|
|
160
|
+
`fill*` tokens stay deep blue in both modes.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<ModalHeader bg="primary.fill" color="white">Upgrade</ModalHeader>
|
|
164
|
+
```
|
|
165
|
+
|
|
113
166
|
---
|
|
114
167
|
|
|
115
168
|
## Migration cheat-sheet
|
|
@@ -119,9 +172,10 @@ the nearest semantic token:
|
|
|
119
172
|
|
|
120
173
|
| Was (primitive) | Use (semantic) |
|
|
121
174
|
| --- | --- |
|
|
122
|
-
| `gray.
|
|
123
|
-
| `gray.
|
|
124
|
-
| `gray.
|
|
175
|
+
| `gray.1200`–`gray.1500` as heading/strong text | `fg.emphasized` |
|
|
176
|
+
| `gray.1000`–`gray.1100` as body text | `fg.default` |
|
|
177
|
+
| `gray.900` as secondary text | `fg.muted` |
|
|
178
|
+
| `gray.500`–`gray.800` as tertiary text/icon | `fg.subtle` |
|
|
125
179
|
| `white` / `gray.0` surface | `bg.surface` / `bg.canvas` |
|
|
126
180
|
| `gray.50` / `gray.100` fill | `bg.subtle` / `bg.muted` |
|
|
127
181
|
| `gray.200` / `gray.300` border | `border.subtle` / `border.default` |
|
package/src/theme/colors.ts
CHANGED
|
@@ -60,6 +60,37 @@
|
|
|
60
60
|
* <Badge bgColor="success.lightest" color="success.dark" />
|
|
61
61
|
* ```
|
|
62
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* Halved-saturation counterpart of each primitive `gray.N` — same hue and
|
|
65
|
+
* lightness, HSL saturation cut by ~50% (e.g. `gray.1300` #1E2433 @ 26% →
|
|
66
|
+
* #23262E @ 13%). The blue-tinted `gray` ramp reads as the right neutral in
|
|
67
|
+
* light mode but turns muddy/over-chromatic as a dark surface, so every
|
|
68
|
+
* dark-mode neutral below (`slate.*`, and the `_dark` of `bg`/`fg`/`border`)
|
|
69
|
+
* resolves to this desaturated mirror instead of the raw `gray` step. Light
|
|
70
|
+
* mode is untouched — it keeps referencing `{colors.gray.*}` verbatim.
|
|
71
|
+
*
|
|
72
|
+
* Single source of truth: change a dark neutral here, not at each token.
|
|
73
|
+
*/
|
|
74
|
+
const desaturatedGray = {
|
|
75
|
+
0: '#FEFEFF',
|
|
76
|
+
50: '#F8F9FB',
|
|
77
|
+
100: '#F2F4F7',
|
|
78
|
+
200: '#E5E8EC',
|
|
79
|
+
300: '#D2D5DB',
|
|
80
|
+
400: '#B6BAC3',
|
|
81
|
+
500: '#A2A6B1',
|
|
82
|
+
600: '#8E939F',
|
|
83
|
+
700: '#7C818D',
|
|
84
|
+
800: '#6A6F7C',
|
|
85
|
+
900: '#595E6B',
|
|
86
|
+
1000: '#4A4E5A',
|
|
87
|
+
1100: '#3C404B',
|
|
88
|
+
1200: '#30343C',
|
|
89
|
+
1300: '#23262E',
|
|
90
|
+
1400: '#181A20',
|
|
91
|
+
1500: '#0E1014',
|
|
92
|
+
} as const;
|
|
93
|
+
|
|
63
94
|
export const semanticTokens = {
|
|
64
95
|
colors: {
|
|
65
96
|
/**
|
|
@@ -97,6 +128,18 @@ export const semanticTokens = {
|
|
|
97
128
|
darker: {
|
|
98
129
|
value: { base: '{colors.blue.900}', _dark: '{colors.blue.100}' },
|
|
99
130
|
}, // high-contrast text
|
|
131
|
+
// Solid brand-blue *fills* for surfaces with white text/icons on top
|
|
132
|
+
// (modal headers, hero/banner gradients, brand badges). `primary.main`/
|
|
133
|
+
// `primary.dark` lighten ~2 stops in dark — right for foreground, too
|
|
134
|
+
// light as a fill — so these stay a deep blue in dark instead. `base`
|
|
135
|
+
// repeats the old main/dark values, so light is unchanged. Bare accents
|
|
136
|
+
// (dots, progress bars) keep `primary.main`.
|
|
137
|
+
fill: {
|
|
138
|
+
value: { base: '{colors.blue.500}', _dark: '{colors.blue.700}' },
|
|
139
|
+
},
|
|
140
|
+
fillStrong: {
|
|
141
|
+
value: { base: '{colors.blue.700}', _dark: '{colors.blue.800}' },
|
|
142
|
+
},
|
|
100
143
|
},
|
|
101
144
|
|
|
102
145
|
/**
|
|
@@ -234,6 +277,77 @@ export const semanticTokens = {
|
|
|
234
277
|
},
|
|
235
278
|
},
|
|
236
279
|
|
|
280
|
+
/**
|
|
281
|
+
* `slate.*` — the foundational **mode-aware neutral family**.
|
|
282
|
+
*
|
|
283
|
+
* A first-class neutral palette alongside the raw `gray.*` primitives, but
|
|
284
|
+
* mode-aware: each `slate.N` resolves to `gray.N` in light and to the
|
|
285
|
+
* desaturated counterpart of the *mirrored* step in dark, so a single token
|
|
286
|
+
* carries the same tonal level in both modes (e.g. `slate.300` is a light
|
|
287
|
+
* divider in light and the equivalent dark divider in dark — no `_dark={{…}}`
|
|
288
|
+
* at the call site). It lives under `semanticTokens` only because Chakra
|
|
289
|
+
* requires that for the `_dark` flip; conceptually it is a *foundation*
|
|
290
|
+
* (a tonal scale), not a *role*.
|
|
291
|
+
*
|
|
292
|
+
* When to use which:
|
|
293
|
+
* - Prefer the **role tokens** (`fg`/`bg`/`border`) when one matches the
|
|
294
|
+
* intent — they carry semantics and AA-tuned dark values.
|
|
295
|
+
* - Reach for **`slate.N`** when you need a specific neutral tonal step that
|
|
296
|
+
* no role names (mirroring how you'd otherwise drop to a raw `gray.N`, but
|
|
297
|
+
* keeping the dark flip). `slate` and the role tokens are *distinct* ramps
|
|
298
|
+
* (slate is a mechanical mirror; roles are hand-tuned), so they are not
|
|
299
|
+
* interchangeable in dark mode.
|
|
300
|
+
*
|
|
301
|
+
* `600`/`700` are lifted off the straight mirror (#8E939F/#7C818D) so the
|
|
302
|
+
* secondary/muted text they most often carry clears WCAG AA 4.5:1 on the
|
|
303
|
+
* dark canvas/surface — the straight mirrors measured ~3.0–3.9 there.
|
|
304
|
+
*/
|
|
305
|
+
slate: {
|
|
306
|
+
0: { value: { base: '{colors.gray.0}', _dark: desaturatedGray[1500] } },
|
|
307
|
+
50: { value: { base: '{colors.gray.50}', _dark: desaturatedGray[1400] } },
|
|
308
|
+
100: {
|
|
309
|
+
value: { base: '{colors.gray.100}', _dark: desaturatedGray[1300] },
|
|
310
|
+
},
|
|
311
|
+
200: {
|
|
312
|
+
value: { base: '{colors.gray.200}', _dark: desaturatedGray[1200] },
|
|
313
|
+
},
|
|
314
|
+
300: {
|
|
315
|
+
value: { base: '{colors.gray.300}', _dark: desaturatedGray[1100] },
|
|
316
|
+
},
|
|
317
|
+
400: {
|
|
318
|
+
value: { base: '{colors.gray.400}', _dark: desaturatedGray[1000] },
|
|
319
|
+
},
|
|
320
|
+
500: {
|
|
321
|
+
value: { base: '{colors.gray.500}', _dark: desaturatedGray[900] },
|
|
322
|
+
},
|
|
323
|
+
600: { value: { base: '{colors.gray.600}', _dark: '#898E99' } },
|
|
324
|
+
700: { value: { base: '{colors.gray.700}', _dark: '#8D919D' } },
|
|
325
|
+
800: {
|
|
326
|
+
value: { base: '{colors.gray.800}', _dark: desaturatedGray[600] },
|
|
327
|
+
},
|
|
328
|
+
900: {
|
|
329
|
+
value: { base: '{colors.gray.900}', _dark: desaturatedGray[500] },
|
|
330
|
+
},
|
|
331
|
+
1000: {
|
|
332
|
+
value: { base: '{colors.gray.1000}', _dark: desaturatedGray[400] },
|
|
333
|
+
},
|
|
334
|
+
1100: {
|
|
335
|
+
value: { base: '{colors.gray.1100}', _dark: desaturatedGray[300] },
|
|
336
|
+
},
|
|
337
|
+
1200: {
|
|
338
|
+
value: { base: '{colors.gray.1200}', _dark: desaturatedGray[200] },
|
|
339
|
+
},
|
|
340
|
+
1300: {
|
|
341
|
+
value: { base: '{colors.gray.1300}', _dark: desaturatedGray[100] },
|
|
342
|
+
},
|
|
343
|
+
1400: {
|
|
344
|
+
value: { base: '{colors.gray.1400}', _dark: desaturatedGray[50] },
|
|
345
|
+
},
|
|
346
|
+
1500: {
|
|
347
|
+
value: { base: '{colors.gray.1500}', _dark: desaturatedGray[0] },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
|
|
237
351
|
/**
|
|
238
352
|
* Neutral background tokens — map onto the gray.0–1500 scale.
|
|
239
353
|
* Use for: page/canvas, raised surfaces (cards, menus), subtle/muted fills,
|
|
@@ -246,27 +360,55 @@ export const semanticTokens = {
|
|
|
246
360
|
* - inverse: high-contrast surface (flips to light in dark mode)
|
|
247
361
|
*/
|
|
248
362
|
bg: {
|
|
363
|
+
// Chakra's own global css paints `html { background: bg }` — the *plain*
|
|
364
|
+
// `bg` token, not `bg.canvas` — so this is the actual page background
|
|
365
|
+
// wherever no component paints over it. `_light` is pure white (Chakra's
|
|
366
|
+
// value, so light is untouched); `_dark` rejoins our neutral floor instead
|
|
367
|
+
// of Chakra's off-palette black.
|
|
368
|
+
DEFAULT: {
|
|
369
|
+
value: { _light: '{colors.white}', _dark: desaturatedGray[1500] },
|
|
370
|
+
},
|
|
249
371
|
canvas: {
|
|
250
|
-
value: { base: '{colors.gray.0}', _dark:
|
|
372
|
+
value: { base: '{colors.gray.0}', _dark: desaturatedGray[1500] },
|
|
251
373
|
},
|
|
252
374
|
surface: {
|
|
253
|
-
value: { base: '{colors.white}', _dark:
|
|
375
|
+
value: { base: '{colors.white}', _dark: desaturatedGray[1400] },
|
|
376
|
+
},
|
|
377
|
+
// Strongly-raised neutral surface — one level above `surface` (e.g. the
|
|
378
|
+
// selected thumb of a SegmentedControl). In dark this is the *lightest*
|
|
379
|
+
// neutral bg token so a raised element reads as lifted toward the light,
|
|
380
|
+
// not recessed. (The `bg.*` dark ramp is otherwise compressed such that
|
|
381
|
+
// `surface` sits below `subtle`/`muted`; `raised` deliberately tops the
|
|
382
|
+
// scale so "raised" has a token that behaves correctly in dark.)
|
|
383
|
+
// NB: named `raised`, not Chakra's `emphasized` — that default token name
|
|
384
|
+
// cannot be overridden via semanticTokens in this setup (it keeps
|
|
385
|
+
// resolving to Chakra's own gray.200), whereas a fresh name is honoured.
|
|
386
|
+
raised: {
|
|
387
|
+
value: { base: '{colors.white}', _dark: desaturatedGray[1100] },
|
|
254
388
|
},
|
|
255
389
|
subtle: {
|
|
256
|
-
value: { base: '{colors.gray.50}', _dark:
|
|
390
|
+
value: { base: '{colors.gray.50}', _dark: desaturatedGray[1300] },
|
|
257
391
|
},
|
|
258
392
|
muted: {
|
|
259
|
-
value: { base: '{colors.gray.100}', _dark:
|
|
393
|
+
value: { base: '{colors.gray.100}', _dark: desaturatedGray[1200] },
|
|
260
394
|
},
|
|
261
395
|
inverse: {
|
|
262
|
-
value: { base: '{colors.gray.1300}', _dark:
|
|
396
|
+
value: { base: '{colors.gray.1300}', _dark: desaturatedGray[50] },
|
|
397
|
+
},
|
|
398
|
+
// Sunken page wash for list/overview surfaces: a gray floor in light so
|
|
399
|
+
// `bg.surface` cards read as raised above it. In dark the `bg.*` ramp is
|
|
400
|
+
// compressed (`subtle` sits *lighter* than `surface`), which would invert
|
|
401
|
+
// that elevation — so the dark value drops to the canvas floor instead.
|
|
402
|
+
// Component-level fills (hover, chips, inner blocks) keep using `bg.subtle`.
|
|
403
|
+
sunken: {
|
|
404
|
+
value: { base: '{colors.gray.50}', _dark: desaturatedGray[1500] },
|
|
263
405
|
},
|
|
264
406
|
// Override Chakra's default `bg.panel` (whose `_dark` resolves to Chakra's
|
|
265
407
|
// own gray.950 = #111111, off our slate palette). Light value is white —
|
|
266
408
|
// identical to Chakra's default — so this only realigns dark overlay
|
|
267
409
|
// surfaces (Menu / Modal / Popover / Toast) onto our gray scale.
|
|
268
410
|
panel: {
|
|
269
|
-
value: { base: '{colors.white}', _dark:
|
|
411
|
+
value: { base: '{colors.white}', _dark: desaturatedGray[1400] },
|
|
270
412
|
},
|
|
271
413
|
/**
|
|
272
414
|
* Row/selection state tints. Use these for selected rows,
|
|
@@ -300,26 +442,42 @@ export const semanticTokens = {
|
|
|
300
442
|
* - inverse: text on inverse surfaces (flips with mode)
|
|
301
443
|
*/
|
|
302
444
|
fg: {
|
|
445
|
+
// Plain `fg` is Chakra's html-level text color (`html { color: fg }`).
|
|
446
|
+
// `_light` repeats Chakra's value (black); `_dark` rejoins our desaturated
|
|
447
|
+
// neutral so legacy html-level text tracks `fg.default`.
|
|
448
|
+
DEFAULT: {
|
|
449
|
+
value: { _light: '{colors.black}', _dark: desaturatedGray[50] },
|
|
450
|
+
},
|
|
451
|
+
// Strongest text — headings, titles, key figures, emphasis. This is the
|
|
452
|
+
// near-black step that `fg.default` used to be; `default` is now re-pegged
|
|
453
|
+
// to a lighter body weight (see below), so reach for `emphasized` when you
|
|
454
|
+
// specifically want maximum contrast.
|
|
455
|
+
emphasized: {
|
|
456
|
+
value: { base: '{colors.gray.1300}', _dark: desaturatedGray[200] },
|
|
457
|
+
},
|
|
303
458
|
default: {
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
459
|
+
// Primary *body* text. Re-pegged from gray.1300 → gray.1000: near-black
|
|
460
|
+
// (gray.1300, ~14:1 on white) is unusually heavy for running copy, and
|
|
461
|
+
// real product usage clustered well below it. gray.1000 (~9:1) is a
|
|
462
|
+
// comfortable AAA body weight; the old near-black step lives on as
|
|
463
|
+
// `fg.emphasized`. _dark drops one step from emphasized for hierarchy.
|
|
464
|
+
value: { base: '{colors.gray.1000}', _dark: desaturatedGray[300] },
|
|
309
465
|
},
|
|
310
466
|
muted: {
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
// (gray.900) is unchanged.
|
|
316
|
-
value: { base: '{colors.gray.900}', _dark: '{colors.gray.300}' },
|
|
467
|
+
// Secondary text. _dark sits one step below `default` (~9.5:1 on the dark
|
|
468
|
+
// canvas) to keep the default→muted hierarchy gap. Light value (gray.900)
|
|
469
|
+
// is unchanged.
|
|
470
|
+
value: { base: '{colors.gray.900}', _dark: desaturatedGray[400] },
|
|
317
471
|
},
|
|
318
472
|
subtle: {
|
|
319
|
-
|
|
473
|
+
// Tertiary / placeholder / icon text. _dark a11y-bumped from the straight
|
|
474
|
+
// mirror (desaturatedGray[600] #8E939F, ~4.06:1 on bg.muted) to #989DA9
|
|
475
|
+
// (~4.6:1) so it clears AA while staying below fg.muted. Light value
|
|
476
|
+
// (gray.700) is unchanged.
|
|
477
|
+
value: { base: '{colors.gray.700}', _dark: '#989DA9' },
|
|
320
478
|
},
|
|
321
479
|
inverse: {
|
|
322
|
-
value: { base: '{colors.gray.0}', _dark:
|
|
480
|
+
value: { base: '{colors.gray.0}', _dark: desaturatedGray[1400] },
|
|
323
481
|
},
|
|
324
482
|
},
|
|
325
483
|
|
|
@@ -331,14 +489,21 @@ export const semanticTokens = {
|
|
|
331
489
|
* - strong: high-emphasis borders, focus outlines on neutral
|
|
332
490
|
*/
|
|
333
491
|
border: {
|
|
492
|
+
// Plain `border` feeds Chakra's `--global-color-border` (the implicit
|
|
493
|
+
// default border color). Not a text color, so its `_dark` takes the
|
|
494
|
+
// straight halved-saturation mirror (no a11y bump). `_light` repeats
|
|
495
|
+
// Chakra's value so light is untouched.
|
|
496
|
+
DEFAULT: {
|
|
497
|
+
value: { _light: '{colors.gray.200}', _dark: desaturatedGray[800] },
|
|
498
|
+
},
|
|
334
499
|
default: {
|
|
335
|
-
value: { base: '{colors.gray.300}', _dark:
|
|
500
|
+
value: { base: '{colors.gray.300}', _dark: desaturatedGray[1100] },
|
|
336
501
|
},
|
|
337
502
|
subtle: {
|
|
338
|
-
value: { base: '{colors.gray.200}', _dark:
|
|
503
|
+
value: { base: '{colors.gray.200}', _dark: desaturatedGray[1300] },
|
|
339
504
|
},
|
|
340
505
|
strong: {
|
|
341
|
-
value: { base: '{colors.gray.500}', _dark:
|
|
506
|
+
value: { base: '{colors.gray.500}', _dark: desaturatedGray[900] },
|
|
342
507
|
},
|
|
343
508
|
},
|
|
344
509
|
},
|
|
@@ -352,22 +517,47 @@ export const semanticTokens = {
|
|
|
352
517
|
* by reviewers. It is intentionally hand-maintained alongside `semanticTokens`
|
|
353
518
|
* so a rename here is a visible, reviewable diff.
|
|
354
519
|
*
|
|
355
|
-
* Note: `bg.panel`
|
|
356
|
-
*
|
|
357
|
-
*
|
|
520
|
+
* Note: `bg.panel` and the bare `bg`/`fg`/`border` DEFAULT tokens are
|
|
521
|
+
* deliberately omitted — they are internal realignments of Chakra defaults
|
|
522
|
+
* (overlay surfaces and html-level globals), not part of the public migration
|
|
523
|
+
* contract. App code should use `bg.surface`/`bg.canvas`, `fg.default`, etc.
|
|
358
524
|
*/
|
|
359
525
|
export type SemanticColorToken =
|
|
360
526
|
| `bg.${
|
|
361
527
|
| 'canvas'
|
|
362
528
|
| 'surface'
|
|
529
|
+
| 'raised'
|
|
363
530
|
| 'subtle'
|
|
364
531
|
| 'muted'
|
|
532
|
+
| 'sunken'
|
|
365
533
|
| 'inverse'
|
|
366
534
|
| 'selected'
|
|
367
535
|
| 'highlighted'}`
|
|
368
536
|
| 'bg.invalid.subtle'
|
|
369
|
-
| `fg.${'default' | 'muted' | 'subtle' | 'inverse'}`
|
|
537
|
+
| `fg.${'emphasized' | 'default' | 'muted' | 'subtle' | 'inverse'}`
|
|
370
538
|
| `border.${'default' | 'subtle' | 'strong'}`
|
|
539
|
+
// `slate.*` — foundational mode-aware neutral family (a tonal scale, not a
|
|
540
|
+
// role). Prefer a `fg`/`bg`/`border` role token when one fits; reach for
|
|
541
|
+
// `slate.N` when you need a specific neutral step no role names.
|
|
542
|
+
| `slate.${
|
|
543
|
+
| 0
|
|
544
|
+
| 50
|
|
545
|
+
| 100
|
|
546
|
+
| 200
|
|
547
|
+
| 300
|
|
548
|
+
| 400
|
|
549
|
+
| 500
|
|
550
|
+
| 600
|
|
551
|
+
| 700
|
|
552
|
+
| 800
|
|
553
|
+
| 900
|
|
554
|
+
| 1000
|
|
555
|
+
| 1100
|
|
556
|
+
| 1200
|
|
557
|
+
| 1300
|
|
558
|
+
| 1400
|
|
559
|
+
| 1500}`
|
|
560
|
+
| `primary.${'fill' | 'fillStrong'}`
|
|
371
561
|
| `${'primary' | 'secondary' | 'danger' | 'success' | 'warning'}.${
|
|
372
562
|
| 'lightest'
|
|
373
563
|
| 'extralight'
|
package/src/theme/global.ts
CHANGED
|
@@ -20,8 +20,8 @@ export const globalCss = defineGlobalStyles({
|
|
|
20
20
|
// Dark mode body fallbacks. Only activates under the `.dark` class set by the
|
|
21
21
|
// color-mode provider, so light-mode rendering is byte-for-byte identical.
|
|
22
22
|
'.dark': {
|
|
23
|
-
'--chakra-colors-chakra-body-text': '#
|
|
24
|
-
'--chakra-colors-chakra-body-bg': '#
|
|
23
|
+
'--chakra-colors-chakra-body-text': '#E5E8EC', // desaturated gray.200 - Primary text (dark); matches fg.default's _dark
|
|
24
|
+
'--chakra-colors-chakra-body-bg': '#0E1014', // desaturated gray.1500 - Background (dark); matches bg.canvas's _dark
|
|
25
25
|
},
|
|
26
26
|
|
|
27
27
|
html: {
|
|
@@ -49,6 +49,35 @@ export const globalCss = defineGlobalStyles({
|
|
|
49
49
|
"body[data-lang='es']": {
|
|
50
50
|
fontFamily: inter.style.fontFamily,
|
|
51
51
|
},
|
|
52
|
+
|
|
53
|
+
// Global scrollbar styling. Without this, scrollbars fall back to the raw
|
|
54
|
+
// browser chrome — a bright, square, high-contrast track that stands out
|
|
55
|
+
// badly in dark mode. A thin, transparent-track, rounded thumb (mode-aware
|
|
56
|
+
// via the `slate` ramp) is unobtrusive in both modes. Components that opt out
|
|
57
|
+
// (e.g. hidden scrollbars) still override locally.
|
|
58
|
+
'*': {
|
|
59
|
+
scrollbarWidth: 'thin',
|
|
60
|
+
scrollbarColor: 'var(--chakra-colors-slate-300) transparent',
|
|
61
|
+
},
|
|
62
|
+
'::-webkit-scrollbar': {
|
|
63
|
+
width: '10px',
|
|
64
|
+
height: '10px',
|
|
65
|
+
},
|
|
66
|
+
'::-webkit-scrollbar-track': {
|
|
67
|
+
background: 'transparent',
|
|
68
|
+
},
|
|
69
|
+
'::-webkit-scrollbar-thumb': {
|
|
70
|
+
backgroundColor: 'var(--chakra-colors-slate-300)',
|
|
71
|
+
borderRadius: '9999px',
|
|
72
|
+
border: '2px solid transparent',
|
|
73
|
+
backgroundClip: 'content-box',
|
|
74
|
+
},
|
|
75
|
+
'::-webkit-scrollbar-thumb:hover': {
|
|
76
|
+
backgroundColor: 'var(--chakra-colors-slate-400)',
|
|
77
|
+
},
|
|
78
|
+
'::-webkit-scrollbar-corner': {
|
|
79
|
+
background: 'transparent',
|
|
80
|
+
},
|
|
52
81
|
});
|
|
53
82
|
|
|
54
83
|
// Legacy export for backwards compatibility
|