@nocobase/app-template-default 1.0.0-beta.67 → 1.0.0-beta.68

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 (36) hide show
  1. package/.agents/rules/components.md +1 -1
  2. package/.agents/rules/shell.md +3 -1
  3. package/client/components/page-header.tsx +1 -1
  4. package/client/components/status-badge.tsx +7 -1
  5. package/client/extensions/nocobase-inbox/inbox-item.tsx +44 -58
  6. package/client/extensions/nocobase-inbox/inbox-list.tsx +73 -75
  7. package/client/index.tsx +4 -0
  8. package/client/layouts/components/app-sidebar.tsx +5 -5
  9. package/client/layouts/components/layout-header.tsx +1 -1
  10. package/client/layouts/components/navigation-menu.tsx +223 -12
  11. package/client/layouts/components/sidebar-footer.tsx +1 -1
  12. package/client/layouts/components/user-menu.tsx +1 -1
  13. package/client/lib/navigation-transform.ts +8 -0
  14. package/client/lib/scrollbar.ts +88 -0
  15. package/client/locales/system/en-US.ts +4 -0
  16. package/client/locales/system/zh-CN.ts +15 -12
  17. package/client/pages/auth/auth-page.css +37 -0
  18. package/client/pages/auth/reset-password.tsx +57 -14
  19. package/client/pages/auth/shared.tsx +24 -24
  20. package/client/pages/home/index.tsx +2 -2
  21. package/client/routing/route-navigation.ts +10 -1
  22. package/client/styles.css +253 -17
  23. package/client/theme/theme-settings.tsx +12 -7
  24. package/client/theme/themes/compact.css +9 -14
  25. package/client/theme/themes/default.css +7 -12
  26. package/package.json +7 -7
  27. package/server/app.ts +2 -0
  28. package/server/config/notification.ts +4 -7
  29. package/tests/components/auth-i18n.test.tsx +57 -5
  30. package/tests/components/layout-i18n.test.tsx +2 -2
  31. package/tests/components/navigation-menu.test.tsx +133 -4
  32. package/tests/components/sidebar-permissions.test.tsx +5 -15
  33. package/tests/components/status-badge.test.tsx +22 -0
  34. package/tests/logic/scrollbar.test.ts +88 -0
  35. package/tests/logic/theme-tokens.test.ts +1 -0
  36. package/vitest.config.ts +1 -0
@@ -5,6 +5,8 @@ import type { ReactElement, ReactNode } from 'react';
5
5
 
6
6
  import { AuthSplitLayout } from '#extensions/nocobase-auth-split-layout/auth-split-layout';
7
7
 
8
+ import './auth-page.css';
9
+
8
10
  export interface AuthPageProps {
9
11
  readonly title: ReactNode;
10
12
  readonly description: ReactNode;
@@ -26,6 +28,7 @@ export function AuthPage({
26
28
  <AuthSplitLayout
27
29
  aside={<BrandPanel />}
28
30
  asideLabel={t('auth.about')}
31
+ className='auth-page'
29
32
  description={description}
30
33
  logo={
31
34
  <>
@@ -68,42 +71,39 @@ function BrandPanel(): ReactElement {
68
71
  ];
69
72
 
70
73
  return (
71
- <div className='relative grid h-full place-items-center overflow-hidden bg-(--auth-brand-background) p-12 text-white'>
72
- <div className='pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,color-mix(in_srgb,var(--primary)_22%,transparent),transparent_65%)]' />
73
- <div className='relative w-full max-w-xl'>
74
+ <div className='auth-brand-panel relative grid h-full place-items-center overflow-hidden p-12 text-white'>
75
+ <div className='relative w-full max-w-md'>
74
76
  <p className='text-xs font-semibold tracking-[0.16em] text-(--auth-brand-highlight) uppercase'>
75
77
  {t('auth.platform')}
76
78
  </p>
77
- <h2 className='mt-4 text-5xl leading-[1.05] font-semibold tracking-[-0.045em]'>
79
+ <h2 className='mt-6 text-4xl leading-tight font-semibold tracking-tight'>
78
80
  {t('auth.marketingTitleFirst')}
79
81
  <br />
80
82
  {t('auth.marketingTitleSecond')}
81
83
  <br />
82
84
  {t('auth.marketingTitleThird')}
83
85
  </h2>
84
- <p className='mt-5 text-sm leading-6 text-(--auth-brand-muted)'>
86
+ <p className='mt-6 text-sm leading-7 text-(--auth-brand-muted)'>
85
87
  {t('auth.marketingDescription')}
86
88
  </p>
87
- <div className='mt-8 overflow-hidden rounded-2xl border border-white/10 bg-white/5 text-white shadow-lg backdrop-blur-sm'>
88
- <div className='space-y-5 p-6'>
89
- {features.map(({ description, icon: Icon, id, title }) => (
90
- <div className='flex gap-4' key={id}>
91
- <span className='grid size-11 shrink-0 place-items-center rounded-xl bg-(--auth-brand-highlight)/10 text-(--auth-brand-highlight)'>
92
- <Icon aria-hidden='true' className='size-5' />
93
- </span>
94
- <div>
95
- <p className='font-semibold'>{title}</p>
96
- <p className='mt-1 text-sm leading-6 text-(--auth-brand-muted)'>
97
- {description}
98
- </p>
99
- </div>
89
+ <div className='mt-12 space-y-8'>
90
+ {features.map(({ description, icon: Icon, id, title }) => (
91
+ <div className='flex gap-4' key={id}>
92
+ <span className='grid size-10 shrink-0 place-items-center rounded-xl bg-(--auth-brand-highlight)/8 text-(--auth-brand-highlight)'>
93
+ <Icon aria-hidden='true' className='size-5' />
94
+ </span>
95
+ <div>
96
+ <p className='text-sm font-medium'>{title}</p>
97
+ <p className='mt-1.5 text-sm leading-6 text-(--auth-brand-muted)'>
98
+ {description}
99
+ </p>
100
100
  </div>
101
- ))}
102
- </div>
103
- <div className='flex items-center gap-3 border-t border-white/10 bg-white/3 px-6 py-4 text-sm font-medium text-(--auth-brand-muted)'>
104
- <Blocks aria-hidden='true' className='size-4' />
105
- <span>{t('auth.marketingFooter')}</span>
106
- </div>
101
+ </div>
102
+ ))}
103
+ </div>
104
+ <div className='mt-12 flex items-center gap-2.5 border-t border-white/8 pt-6 text-xs text-(--auth-brand-muted)'>
105
+ <Blocks aria-hidden='true' className='size-4' />
106
+ <span>{t('auth.marketingFooter')}</span>
107
107
  </div>
108
108
  </div>
109
109
  </div>
@@ -160,11 +160,11 @@ function CapabilityCard({
160
160
  onClick={onOpen}
161
161
  className='group flex w-full cursor-pointer rounded-xl text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50'
162
162
  >
163
- <Card className='w-full transition-[translate,box-shadow] duration-200 ease-out group-hover:-translate-y-0.5 group-hover:shadow-md group-hover:ring-foreground/20 motion-reduce:transition-none motion-reduce:group-hover:translate-y-0'>
163
+ <Card className='w-full transition-[translate,box-shadow,background-color] duration-200 ease-out group-hover:-translate-y-0.5 group-hover:bg-hover group-hover:shadow-md motion-reduce:transition-none motion-reduce:group-hover:translate-y-0'>
164
164
  <CardHeader className='gap-3'>
165
165
  {/* The chip only deepens its tint on hover rather than inverting: swapping a light background and a dark icon
166
166
  passes both through the same color halfway, where the icon vanishes for a moment and reads as a flicker. */}
167
- <span className='flex size-9 items-center justify-center rounded-lg bg-primary/5 text-primary ring-1 ring-transparent transition-[background-color,box-shadow] duration-200 ease-out group-hover:bg-primary/10 group-hover:ring-primary/20 motion-reduce:transition-none'>
167
+ <span className='flex size-9 items-center justify-center rounded-lg bg-primary/5 text-primary ring-1 ring-transparent transition-[background-color,box-shadow] duration-200 ease-out group-hover:bg-primary/10 group-hover:ring-border motion-reduce:transition-none'>
168
168
  <Icon
169
169
  className='size-4 transition-transform duration-200 ease-out group-hover:scale-110 motion-reduce:transition-none'
170
170
  aria-hidden
@@ -8,8 +8,10 @@ import { useEffect, useMemo, useState } from 'react';
8
8
  import { matchPath, matchRoutes, type RouteObject } from 'react-router';
9
9
 
10
10
  import { EMPTY_ARRAY } from '#lib/constants';
11
+ import { transformNavigation } from '#lib/navigation-transform';
11
12
 
12
13
  export interface RouteNavigationItem {
14
+ readonly section?: boolean;
13
15
  readonly route: AppClientRegisteredRoute;
14
16
  readonly children: readonly RouteNavigationItem[];
15
17
  }
@@ -17,10 +19,17 @@ export interface RouteNavigationItem {
17
19
  export function buildRouteNavigation(
18
20
  routes: readonly AppClientRegisteredRoute[],
19
21
  denied: ReadonlySet<string>,
22
+ ): RouteNavigationItem[] {
23
+ return transformNavigation(visibleRouteNavigation(routes, denied));
24
+ }
25
+
26
+ function visibleRouteNavigation(
27
+ routes: readonly AppClientRegisteredRoute[],
28
+ denied: ReadonlySet<string>,
20
29
  ): RouteNavigationItem[] {
21
30
  return routes.flatMap((route) => {
22
31
  if (denied.has(routeKey(route))) return [];
23
- const children = buildRouteNavigation(
32
+ const children = visibleRouteNavigation(
24
33
  route.children ?? EMPTY_ARRAY,
25
34
  denied,
26
35
  );
package/client/styles.css CHANGED
@@ -50,6 +50,7 @@
50
50
  --color-muted-foreground: var(--muted-foreground);
51
51
  --color-accent: var(--accent);
52
52
  --color-accent-foreground: var(--accent-foreground);
53
+ --color-hover: var(--hover);
53
54
  --color-destructive: var(--destructive);
54
55
  --color-border: var(--border);
55
56
  --color-input: var(--input);
@@ -138,16 +139,28 @@
138
139
 
139
140
  /* Application-wide interaction and badge treatment, outside generated primitives. */
140
141
  @layer base {
142
+ /* Whatever may be clicked shows the pointer, the plugins' pages included.
143
+ Tailwind 4 leaves buttons with the arrow; a control disabled (natively,
144
+ or as Base UI and ARIA mark it) keeps it, and a utility on an element
145
+ (`cursor-default`, `cursor-not-allowed`) still wins over this layer. */
141
146
  :is(
142
147
  button,
143
148
  [role='button'],
144
149
  [role='menuitem'],
150
+ [role='menuitemcheckbox'],
151
+ [role='menuitemradio'],
145
152
  [role='option'],
146
153
  [role='tab'],
147
154
  [role='checkbox'],
148
155
  [role='radio'],
149
156
  [role='switch'],
150
157
  [role='combobox'],
158
+ summary,
159
+ select,
160
+ label[for],
161
+ input[type='checkbox'],
162
+ input[type='radio'],
163
+ input[type='file'],
151
164
  a[href]
152
165
  ):not(:disabled):not([aria-disabled='true']):not([data-disabled]) {
153
166
  cursor: pointer;
@@ -161,14 +174,64 @@
161
174
  padding-inline: calc(var(--spacing) * 2.5);
162
175
  line-height: 1.4;
163
176
  }
177
+ /* The small badge, `data-size='sm'`: beside a title or a name, on a card or
178
+ a board's card, wherever several sit in a dense row. The default one is a
179
+ status standing on its own, as in a table's column or a record's
180
+ particulars. */
181
+ [data-slot='badge'][data-size='sm'] {
182
+ min-height: calc(var(--spacing) * 5.5);
183
+ padding-block: 0;
184
+ padding-inline: calc(var(--spacing) * 1.5);
185
+ font-size: var(--text-xs);
186
+ line-height: 1.3;
187
+ }
164
188
 
165
- /* Shared overlay treatment lives outside generated shadcn primitives. */
189
+ /* One focus for every control, across the application and the plugins'
190
+ pages: a half-strength border and a 2px halo at a fifth of the ring colour,
191
+ in place of the 3px ring at half of it that the primitives and pages draw,
192
+ which reads heavy. Unlayered, so it wins over their utilities without
193
+ editing them. A label wrapping a visually hidden control, such as an
194
+ option card, and a rich text editor's frame show their control's focus.
195
+ An invalid field keeps its red, and a destructive button its own colour. */
196
+ :is(
197
+ [data-slot='input'],
198
+ [data-slot='textarea'],
199
+ [data-slot='select-trigger'],
200
+ button,
201
+ [role='button'],
202
+ [role='tab'],
203
+ [role='menuitem'],
204
+ [role='checkbox'],
205
+ [role='switch'],
206
+ [role='radio'],
207
+ input[type='checkbox'],
208
+ input[type='radio'],
209
+ a[href]
210
+ ):focus-visible:not([aria-invalid='true']),
211
+ label:has(> .sr-only:focus-visible),
212
+ [data-slot='rich-text-editor']:focus-within,
213
+ [data-slot='input-group']:has(
214
+ [data-slot='input-group-control']:focus-visible
215
+ ):not(:has([aria-invalid='true'])) {
216
+ border-color: color-mix(in oklch, var(--ring) 50%, transparent);
217
+ box-shadow: 0 0 0 2px color-mix(in oklch, var(--ring) 20%, transparent);
218
+ }
219
+ [data-slot='button'].text-destructive:focus-visible:not([aria-invalid='true']) {
220
+ border-color: color-mix(in oklch, var(--destructive) 40%, transparent);
221
+ box-shadow: 0 0 0 2px color-mix(in oklch, var(--destructive) 20%, transparent);
222
+ }
223
+
224
+ /* Overlays in a light frame: a faint, blurred backdrop, and the content
225
+ framed by the general border alone, as a card is, without the primitive's
226
+ ring or a heavy shadow. The plugins' copies of these primitives take it
227
+ too; unlayered, so it wins over their utilities. */
166
228
  :is(
167
229
  [data-slot='dialog-overlay'],
168
230
  [data-slot='alert-dialog-overlay'],
169
231
  [data-slot='sheet-overlay']
170
232
  ) {
171
- background: var(--overlay-backdrop);
233
+ background: rgb(0 0 0 / 0.1);
234
+ backdrop-filter: blur(4px);
172
235
  }
173
236
  :is(
174
237
  [data-slot='dialog-content'],
@@ -176,7 +239,7 @@
176
239
  [data-slot='route-overlay-content']
177
240
  ) {
178
241
  border: 1px solid var(--border);
179
- box-shadow: var(--overlay-shadow);
242
+ box-shadow: none;
180
243
  }
181
244
  [data-slot='route-overlay-content'][data-presentation='dialog'] {
182
245
  border-radius: var(--radius-xl);
@@ -308,14 +371,65 @@
308
371
  padding-right: calc(var(--spacing) * 24);
309
372
  }
310
373
 
311
- /* Compact content can stay dense while the primary navigation remains comfortable.
312
- The primitive's important icon-mode sizes still control the collapsed rail. */
313
- :root[data-theme='compact'] .app-navigation [data-slot='sidebar-menu-button'] {
314
- height: calc(var(--spacing) * 12);
315
- padding-inline: calc(var(--spacing) * 4);
374
+ /* The navigation's rows are roomier than the sidebar primitive's: 10 units
375
+ high with half a unit between them, nested rows 8.5 high, in a sidebar 64
376
+ units wide (layouts/components/app-sidebar.tsx). The primitive's important
377
+ icon-mode sizes still control the collapsed rail. */
378
+ .app-navigation [data-slot='sidebar-menu'] {
379
+ gap: calc(var(--spacing) * 0.5);
380
+ }
381
+ /* Top-level groups only: a section nested in a menu sits flush. */
382
+ .app-navigation > nav > [data-slot='sidebar-group'] {
383
+ padding: calc(var(--spacing) * 2);
384
+ }
385
+ /* A section's heading reads as a heading over its entries, not as one of
386
+ them: half the menu's foreground, mixed toward its ground, so both colour
387
+ schemes follow. */
388
+ .app-navigation [data-sidebar='group-label'] {
389
+ height: calc(var(--spacing) * 8);
390
+ padding-inline: calc(var(--spacing) * 3.5);
391
+ color: color-mix(in oklch, var(--sidebar-foreground) 50%, var(--sidebar));
392
+ }
393
+ .app-navigation [data-slot='sidebar-menu-button'] {
394
+ height: calc(var(--spacing) * 10);
395
+ gap: calc(var(--spacing) * 2);
396
+ padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3.5);
397
+ }
398
+ /* The primitive places a row's count and its expand button for 8-unit rows;
399
+ both are 5 units high, so this centres them on the taller rows. */
400
+ .app-navigation
401
+ [data-slot='sidebar-menu-item']
402
+ > :is([data-slot='sidebar-menu-badge'], [data-slot='sidebar-menu-action']) {
403
+ top: calc(var(--spacing) * 2.5);
404
+ }
405
+ .app-navigation
406
+ [data-slot='sidebar-menu-sub-item']
407
+ > [data-slot='sidebar-menu-action'] {
408
+ top: calc(var(--spacing) * 1.75);
409
+ }
410
+ .app-navigation [data-slot='sidebar-menu-button'] > svg {
411
+ width: calc(var(--spacing) * 4);
412
+ height: calc(var(--spacing) * 4);
413
+ }
414
+ /* An entry with a count (navigation-menu.tsx) leaves room for it at the
415
+ row's end, so a long label truncates before reaching it. */
416
+ .app-navigation
417
+ [data-slot='sidebar-menu-item']:has(> [data-slot='sidebar-menu-badge'])
418
+ > [data-slot='sidebar-menu-button'] {
419
+ padding-right: calc(var(--spacing) * 10);
420
+ }
421
+ .app-navigation [data-slot='sidebar-menu-sub'] {
422
+ gap: calc(var(--spacing) * 0.5);
423
+ padding-block: calc(var(--spacing) * 1);
424
+ }
425
+ .app-navigation [data-slot='sidebar-menu-sub-button'] {
426
+ height: calc(var(--spacing) * 8.5);
427
+ gap: calc(var(--spacing) * 2);
428
+ padding-inline: calc(var(--spacing) * 2.5);
316
429
  }
317
- :root[data-theme='compact'] .app-navigation [data-slot='sidebar-menu'] {
318
- gap: calc(var(--spacing) * 1.5);
430
+ .app-navigation [data-slot='sidebar-menu-sub-button'] > svg {
431
+ width: calc(var(--spacing) * 4);
432
+ height: calc(var(--spacing) * 4);
319
433
  }
320
434
 
321
435
  /* Brand tint belongs to the application shell, independent of each page's content. */
@@ -331,11 +445,20 @@
331
445
 
332
446
  /* Shared table hierarchy stays neutral so semantic status colors remain distinct. */
333
447
  [data-slot='table-header'] {
334
- background: color-mix(in srgb, var(--muted) 70%, var(--card));
448
+ background: var(--hover);
335
449
  }
336
450
  [data-slot='table-header'] [data-slot='table-row']:hover {
337
451
  background: transparent;
338
452
  }
453
+ /* A row under the pointer takes the --hover grey, as every hovered row and
454
+ list item does, rather than the primitive's half of --muted. Matched by
455
+ that class, so a row given another hover keeps it, and a selected row
456
+ keeps its own ground. */
457
+ [data-slot='table-row'][class~='hover:bg-muted/50']:hover:not(
458
+ [data-state='selected']
459
+ ) {
460
+ background: var(--hover);
461
+ }
339
462
  [data-slot='table-head'] {
340
463
  height: calc(var(--spacing) * 12);
341
464
  padding-inline: calc(var(--spacing) * 3);
@@ -355,12 +478,125 @@
355
478
  color: var(--primary);
356
479
  }
357
480
 
358
- /* Nested items keep a comfortable rhythm independently of compact content. */
359
- .app-navigation [data-slot='sidebar-menu-sub-button'] {
481
+ /* A card's frame in the general border, as every other line: the card
482
+ primitive and the plugins' copies draw their ring at a tenth of the
483
+ foreground. Matched by that class, so a card given another ring colour
484
+ keeps it; unlayered, so it wins over the utility. */
485
+ [data-slot='card'][class~='ring-foreground/10'] {
486
+ --tw-ring-color: var(--border);
487
+ }
488
+
489
+ /* Line tabs as a record page's tabs: from the left, the open one in the
490
+ foreground over a 2px line that sits on a thin rule under the row, the
491
+ others muted until hovered. Unlayered, so it wins over the primitive's
492
+ utilities. A row whose tabs keep off its edges sets --tabs-inset; its rule
493
+ still runs edge to edge. */
494
+ [data-slot='tabs-list'][data-variant='line'][data-orientation='horizontal'] {
495
+ width: 100%;
496
+ height: auto;
497
+ justify-content: flex-start;
498
+ gap: calc(var(--spacing) * 5);
499
+ padding: 0 var(--tabs-inset, 0px);
500
+ border-bottom: 1px solid var(--border);
501
+ }
502
+ [data-slot='tabs-list'][data-variant='line'][data-orientation='horizontal']
503
+ > [data-slot='tabs-trigger'] {
504
+ flex: none;
360
505
  height: calc(var(--spacing) * 10);
361
- padding-inline: calc(var(--spacing) * 3);
506
+ padding-inline: 0;
507
+ border-inline-width: 0;
508
+ color: var(--muted-foreground);
362
509
  }
363
- .app-navigation [data-slot='sidebar-menu-sub'] {
364
- gap: calc(var(--spacing) * 1.5);
365
- padding-block: calc(var(--spacing) * 1.5);
510
+ [data-slot='tabs-list'][data-variant='line'][data-orientation='horizontal']
511
+ > [data-slot='tabs-trigger']:is(:hover, [data-active]) {
512
+ color: var(--foreground);
513
+ }
514
+ [data-slot='tabs-list'][data-variant='line'][data-orientation='horizontal']
515
+ > [data-slot='tabs-trigger']::after {
516
+ bottom: -1px;
517
+ height: 2px;
518
+ }
519
+
520
+ /* Every scrollbar, across and down: a thin rounded bar in a faint grey on no
521
+ track, drawn only while its element scrolls or the mouse is near the edge
522
+ it runs along (revealScrollbarsWhileScrolling
523
+ in client/lib/scrollbar.ts marks it `data-scrolling` until it has been
524
+ still for a moment) and transparent otherwise, so showing it moves nothing;
525
+ a little firmer under the pointer. Chromium and Safari draw it through the
526
+ -webkit- pseudo-elements, which allow a thinner bar than
527
+ `scrollbar-width: thin`; they ignore those wherever the standard properties
528
+ are set (scrollbar-color is inherited), so the standard ones are given only
529
+ to a browser without them (Firefox). */
530
+ ::-webkit-scrollbar {
531
+ width: 6px;
532
+ height: 6px;
533
+ }
534
+ ::-webkit-scrollbar-track,
535
+ ::-webkit-scrollbar-corner {
536
+ background: transparent;
537
+ }
538
+ ::-webkit-scrollbar-thumb {
539
+ border-radius: 9999px;
540
+ background-color: transparent;
541
+ }
542
+ [data-scrolling]::-webkit-scrollbar-thumb {
543
+ background-color: color-mix(
544
+ in oklab,
545
+ var(--muted-foreground) 20%,
546
+ transparent
547
+ );
548
+ }
549
+ [data-scrolling]::-webkit-scrollbar-thumb:hover {
550
+ background-color: color-mix(
551
+ in oklab,
552
+ var(--muted-foreground) 30%,
553
+ transparent
554
+ );
555
+ }
556
+ @supports not selector(::-webkit-scrollbar) {
557
+ * {
558
+ scrollbar-width: thin;
559
+ scrollbar-color: transparent transparent;
560
+ }
561
+ [data-scrolling] {
562
+ scrollbar-color: color-mix(
563
+ in oklab,
564
+ var(--muted-foreground) 20%,
565
+ transparent
566
+ )
567
+ transparent;
568
+ }
569
+ }
570
+
571
+ /* A scroller whose bar always shows, thicker, to be dragged: one wider than
572
+ the page, such as a board, which a mouse without a sideways wheel moves
573
+ only by its bar. Plain rules beside the ones above rather than a utility,
574
+ which their unlayered rules would outrank. */
575
+ .scroll-shown::-webkit-scrollbar {
576
+ width: 8px;
577
+ height: 8px;
578
+ }
579
+ .scroll-shown::-webkit-scrollbar-thumb {
580
+ background-color: color-mix(
581
+ in oklab,
582
+ var(--muted-foreground) 25%,
583
+ transparent
584
+ );
585
+ }
586
+ .scroll-shown::-webkit-scrollbar-thumb:hover {
587
+ background-color: color-mix(
588
+ in oklab,
589
+ var(--muted-foreground) 40%,
590
+ transparent
591
+ );
592
+ }
593
+ @supports not selector(::-webkit-scrollbar) {
594
+ .scroll-shown {
595
+ scrollbar-color: color-mix(
596
+ in oklab,
597
+ var(--muted-foreground) 25%,
598
+ transparent
599
+ )
600
+ transparent;
601
+ }
366
602
  }
@@ -36,7 +36,7 @@ export function ThemeSettings(): ReactElement {
36
36
  aria-label={title}
37
37
  size='icon'
38
38
  variant='outline'
39
- className='size-10 rounded-xl border-border/70 bg-background/60 hover:bg-accent/50'
39
+ className='size-10 rounded-xl border-border bg-background/60 hover:bg-accent/50'
40
40
  />
41
41
  }
42
42
  >
@@ -70,13 +70,15 @@ export function ThemeSettings(): ReactElement {
70
70
  <label
71
71
  key={mode}
72
72
  className={cn(
73
- 'relative flex min-w-0 cursor-pointer items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-colors select-none has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-ring',
73
+ 'relative flex min-w-0 cursor-pointer items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-colors select-none',
74
74
  isSelected
75
75
  ? 'bg-background text-foreground shadow-xs'
76
76
  : 'text-muted-foreground hover:bg-background/40 hover:text-foreground',
77
77
  )}
78
78
  >
79
- <RadioGroupItem value={mode} className='sr-only' />
79
+ {/* `absolute` over the primitive's `relative`, which would keep
80
+ the hidden radio in the row and push the words aside. */}
81
+ <RadioGroupItem value={mode} className='absolute sr-only' />
80
82
  <ModeIcon aria-hidden='true' className='size-3.5 shrink-0' />
81
83
  <span className='whitespace-nowrap'>
82
84
  {t('appearance.' + mode, {
@@ -109,18 +111,21 @@ export function ThemeSettings(): ReactElement {
109
111
  <label
110
112
  key={item.id}
111
113
  className={cn(
112
- 'group relative flex cursor-pointer flex-col rounded-xl border p-2 text-left transition-all select-none has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-ring',
114
+ 'group relative flex cursor-pointer flex-col rounded-xl border p-2 text-left transition-all select-none',
113
115
  isSelected
114
116
  ? 'border-primary ring-2 ring-primary/20 bg-primary/5 shadow-xs'
115
- : 'border-border/70 hover:border-border hover:bg-muted/30',
117
+ : 'border-border hover:bg-muted/30',
116
118
  )}
117
119
  >
118
- <RadioGroupItem value={item.id} className='sr-only' />
120
+ <RadioGroupItem
121
+ value={item.id}
122
+ className='absolute sr-only'
123
+ />
119
124
  <div className='mb-2 h-16 w-full'>
120
125
  <div
121
126
  aria-hidden='true'
122
127
  data-theme={item.id}
123
- className='theme-preview relative flex h-full overflow-hidden rounded-lg border border-border/80 bg-background shadow-2xs'
128
+ className='theme-preview relative flex h-full overflow-hidden rounded-lg border border-border bg-background shadow-2xs'
124
129
  >
125
130
  {/* Mini Sidebar */}
126
131
  <div className='flex w-6 flex-col gap-1 border-r border-sidebar-border/50 bg-sidebar p-1'>
@@ -9,7 +9,7 @@
9
9
  ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
10
10
  'Courier New', monospace;
11
11
  --font-heading: var(--font-sans);
12
- --spacing: 0.2rem;
12
+ --spacing: 0.225rem;
13
13
  --text-xs: 0.75rem;
14
14
  --text-xs--line-height: calc(1 / 0.75);
15
15
  --text-sm: 0.875rem;
@@ -45,15 +45,11 @@
45
45
  --shadow-xl:
46
46
  0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
47
47
  --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
48
- --radius: 0.375rem;
48
+ --radius: 0.5rem;
49
49
  --background: oklch(0.985 0 0);
50
50
  --foreground: oklch(0.16 0 0);
51
51
  --card: oklch(1 0 0);
52
52
  --card-foreground: oklch(0.16 0 0);
53
- --overlay-backdrop: rgb(15 23 42 / 0.28);
54
- --overlay-shadow:
55
- 0 24px 64px -16px rgb(15 23 42 / 0.22),
56
- 0 8px 24px -12px rgb(15 23 42 / 0.16);
57
53
  --popover: oklch(1 0 0);
58
54
  --popover-foreground: oklch(0.16 0 0);
59
55
  --primary: #002ad1;
@@ -71,8 +67,9 @@
71
67
  --muted-foreground: oklch(0.52 0 0);
72
68
  --accent: rgb(237 242 255);
73
69
  --accent-foreground: rgb(0 42 209);
70
+ --hover: color-mix(in srgb, var(--muted) 70%, var(--card));
74
71
  --destructive: oklch(0.577 0.245 27.325);
75
- --border: oklch(0.9 0 0);
72
+ --border: oklch(0.9 0 0 / 60%);
76
73
  --input: oklch(0.9 0 0);
77
74
  --ring: var(--primary);
78
75
  --chart-1: oklch(0.646 0.222 41.116);
@@ -84,8 +81,8 @@
84
81
  --sidebar-foreground: var(--card-foreground);
85
82
  --sidebar-primary: color-mix(in srgb, var(--primary) 8%, var(--sidebar));
86
83
  --sidebar-primary-foreground: var(--primary);
87
- --sidebar-accent: var(--accent);
88
- --sidebar-accent-foreground: var(--accent-foreground);
84
+ --sidebar-accent: var(--hover);
85
+ --sidebar-accent-foreground: var(--sidebar-foreground);
89
86
  --sidebar-border: var(--border);
90
87
  --sidebar-ring: var(--ring);
91
88
  }
@@ -96,9 +93,6 @@
96
93
  --foreground: oklch(0.96 0 0);
97
94
  --card: oklch(0.18 0 0);
98
95
  --card-foreground: oklch(0.985 0 0);
99
- --overlay-backdrop: rgb(0 0 0 / 0.58);
100
- --overlay-shadow:
101
- 0 24px 64px -16px rgb(0 0 0 / 0.6), 0 8px 24px -12px rgb(0 0 0 / 0.4);
102
96
  --popover: oklch(0.18 0 0);
103
97
  --popover-foreground: oklch(0.985 0 0);
104
98
  --primary: #8ca9ff;
@@ -113,6 +107,7 @@
113
107
  --muted-foreground: oklch(0.7 0 0);
114
108
  --accent: rgb(30 42 72);
115
109
  --accent-foreground: rgb(174 194 255);
110
+ --hover: color-mix(in srgb, var(--muted) 70%, var(--card));
116
111
  --destructive: oklch(0.704 0.191 22.216);
117
112
  --border: oklch(1 0 0 / 10%);
118
113
  --input: oklch(1 0 0 / 15%);
@@ -126,8 +121,8 @@
126
121
  --sidebar-foreground: var(--card-foreground);
127
122
  --sidebar-primary: color-mix(in srgb, var(--primary) 8%, var(--sidebar));
128
123
  --sidebar-primary-foreground: var(--primary);
129
- --sidebar-accent: var(--accent);
130
- --sidebar-accent-foreground: var(--accent-foreground);
124
+ --sidebar-accent: var(--hover);
125
+ --sidebar-accent-foreground: var(--sidebar-foreground);
131
126
  --sidebar-border: var(--border);
132
127
  --sidebar-ring: var(--ring);
133
128
  }
@@ -51,10 +51,6 @@
51
51
  --foreground: oklch(0.16 0 0);
52
52
  --card: oklch(1 0 0);
53
53
  --card-foreground: oklch(0.16 0 0);
54
- --overlay-backdrop: rgb(15 23 42 / 0.28);
55
- --overlay-shadow:
56
- 0 24px 64px -16px rgb(15 23 42 / 0.22),
57
- 0 8px 24px -12px rgb(15 23 42 / 0.16);
58
54
  --popover: oklch(1 0 0);
59
55
  --popover-foreground: oklch(0.16 0 0);
60
56
  --primary: #002ad1;
@@ -72,8 +68,9 @@
72
68
  --muted-foreground: oklch(0.52 0 0);
73
69
  --accent: rgb(237 242 255);
74
70
  --accent-foreground: rgb(0 42 209);
71
+ --hover: color-mix(in srgb, var(--muted) 70%, var(--card));
75
72
  --destructive: oklch(0.577 0.245 27.325);
76
- --border: oklch(0.9 0 0);
73
+ --border: oklch(0.9 0 0 / 60%);
77
74
  --input: oklch(0.9 0 0);
78
75
  --ring: var(--primary);
79
76
  --chart-1: oklch(0.646 0.222 41.116);
@@ -85,8 +82,8 @@
85
82
  --sidebar-foreground: var(--card-foreground);
86
83
  --sidebar-primary: color-mix(in srgb, var(--primary) 8%, var(--sidebar));
87
84
  --sidebar-primary-foreground: var(--primary);
88
- --sidebar-accent: var(--accent);
89
- --sidebar-accent-foreground: var(--accent-foreground);
85
+ --sidebar-accent: var(--hover);
86
+ --sidebar-accent-foreground: var(--sidebar-foreground);
90
87
  --sidebar-border: var(--border);
91
88
  --sidebar-ring: var(--ring);
92
89
  }
@@ -98,9 +95,6 @@
98
95
  --foreground: oklch(0.96 0 0);
99
96
  --card: oklch(0.18 0 0);
100
97
  --card-foreground: oklch(0.985 0 0);
101
- --overlay-backdrop: rgb(0 0 0 / 0.58);
102
- --overlay-shadow:
103
- 0 24px 64px -16px rgb(0 0 0 / 0.6), 0 8px 24px -12px rgb(0 0 0 / 0.4);
104
98
  --popover: oklch(0.18 0 0);
105
99
  --popover-foreground: oklch(0.985 0 0);
106
100
  --primary: #8ca9ff;
@@ -115,6 +109,7 @@
115
109
  --muted-foreground: oklch(0.7 0 0);
116
110
  --accent: rgb(30 42 72);
117
111
  --accent-foreground: rgb(174 194 255);
112
+ --hover: color-mix(in srgb, var(--muted) 70%, var(--card));
118
113
  --destructive: oklch(0.704 0.191 22.216);
119
114
  --border: oklch(1 0 0 / 10%);
120
115
  --input: oklch(1 0 0 / 15%);
@@ -128,8 +123,8 @@
128
123
  --sidebar-foreground: var(--card-foreground);
129
124
  --sidebar-primary: color-mix(in srgb, var(--primary) 8%, var(--sidebar));
130
125
  --sidebar-primary-foreground: var(--primary);
131
- --sidebar-accent: var(--accent);
132
- --sidebar-accent-foreground: var(--accent-foreground);
126
+ --sidebar-accent: var(--hover);
127
+ --sidebar-accent-foreground: var(--sidebar-foreground);
133
128
  --sidebar-border: var(--border);
134
129
  --sidebar-ring: var(--ring);
135
130
  }