cursedbelt 5.26.0 → 5.28.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.
Files changed (98) hide show
  1. package/dist/react/charts/IntervalTimeline.d.ts.map +1 -1
  2. package/dist/react/charts/IntervalTimeline.js +6 -2
  3. package/dist/react/charts/IntervalTimeline.js.map +1 -1
  4. package/dist/react/components/Button.d.ts +1 -1
  5. package/dist/react/components/Button.d.ts.map +1 -1
  6. package/dist/react/components/Button.js +4 -0
  7. package/dist/react/components/Button.js.map +1 -1
  8. package/dist/react/components/CodeArea.d.ts.map +1 -1
  9. package/dist/react/components/CodeArea.js +3 -1
  10. package/dist/react/components/CodeArea.js.map +1 -1
  11. package/dist/react/components/DataTable/DataTable.js +2 -2
  12. package/dist/react/components/DataTable/DataTable.js.map +1 -1
  13. package/dist/react/components/DataTable/TableFilterBuilder.d.ts.map +1 -1
  14. package/dist/react/components/DataTable/TableFilterBuilder.js +2 -1
  15. package/dist/react/components/DataTable/TableFilterBuilder.js.map +1 -1
  16. package/dist/react/components/FindingsList.d.ts.map +1 -1
  17. package/dist/react/components/FindingsList.js +2 -1
  18. package/dist/react/components/FindingsList.js.map +1 -1
  19. package/dist/react/components/InfoHint.d.ts.map +1 -1
  20. package/dist/react/components/InfoHint.js +3 -1
  21. package/dist/react/components/InfoHint.js.map +1 -1
  22. package/dist/react/components/Section.d.ts.map +1 -1
  23. package/dist/react/components/Section.js +3 -1
  24. package/dist/react/components/Section.js.map +1 -1
  25. package/dist/react/components/calendar/MonthGrid.d.ts.map +1 -1
  26. package/dist/react/components/calendar/MonthGrid.js +33 -32
  27. package/dist/react/components/calendar/MonthGrid.js.map +1 -1
  28. package/dist/react/components/calendar/WeekView.d.ts.map +1 -1
  29. package/dist/react/components/calendar/WeekView.js +2 -1
  30. package/dist/react/components/calendar/WeekView.js.map +1 -1
  31. package/dist/react/components/sharing/RecipientPicker.d.ts.map +1 -1
  32. package/dist/react/components/sharing/RecipientPicker.js +3 -1
  33. package/dist/react/components/sharing/RecipientPicker.js.map +1 -1
  34. package/dist/react/dashboard-grid/WidgetFrame.d.ts.map +1 -1
  35. package/dist/react/dashboard-grid/WidgetFrame.js +2 -1
  36. package/dist/react/dashboard-grid/WidgetFrame.js.map +1 -1
  37. package/dist/react/diff-viewer/TextComparePane.d.ts.map +1 -1
  38. package/dist/react/diff-viewer/TextComparePane.js +2 -1
  39. package/dist/react/diff-viewer/TextComparePane.js.map +1 -1
  40. package/dist/react/emoji/EmojiAutocomplete.d.ts.map +1 -1
  41. package/dist/react/emoji/EmojiAutocomplete.js +3 -1
  42. package/dist/react/emoji/EmojiAutocomplete.js.map +1 -1
  43. package/dist/react/folder-tree/FolderPicker.d.ts.map +1 -1
  44. package/dist/react/folder-tree/FolderPicker.js +3 -2
  45. package/dist/react/folder-tree/FolderPicker.js.map +1 -1
  46. package/dist/react/layoutEngine/LayoutCanvas.d.ts.map +1 -1
  47. package/dist/react/layoutEngine/LayoutCanvas.js.map +1 -1
  48. package/dist/react/layoutEngine/NodeInspector.d.ts.map +1 -1
  49. package/dist/react/layoutEngine/NodeInspector.js +2 -1
  50. package/dist/react/layoutEngine/NodeInspector.js.map +1 -1
  51. package/dist/react/media/TrimRangeSlider.d.ts.map +1 -1
  52. package/dist/react/media/TrimRangeSlider.js +3 -1
  53. package/dist/react/media/TrimRangeSlider.js.map +1 -1
  54. package/dist/react/media-gallery/MediaGallery.d.ts +14 -1
  55. package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
  56. package/dist/react/media-gallery/MediaGallery.js +11 -7
  57. package/dist/react/media-gallery/MediaGallery.js.map +1 -1
  58. package/dist/react/media-gallery/index.d.ts +1 -1
  59. package/dist/react/media-gallery/index.d.ts.map +1 -1
  60. package/dist/react/media-gallery/index.js +1 -1
  61. package/dist/react/media-gallery/index.js.map +1 -1
  62. package/dist/react/problem-detail/ProblemDetailView.d.ts.map +1 -1
  63. package/dist/react/problem-detail/ProblemDetailView.js +2 -1
  64. package/dist/react/problem-detail/ProblemDetailView.js.map +1 -1
  65. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  66. package/dist/styles-areas/core.css +29 -28
  67. package/dist/styles-utilities.css +9 -0
  68. package/dist/theme.css +11 -0
  69. package/package.json +8 -2
  70. package/scripts/guardrails-baseline.json +2 -1
  71. package/scripts/guardrailsEnforce.spec.ts +36 -0
  72. package/scripts/guardrailsEnforce.ts +66 -0
  73. package/src/react/charts/IntervalTimeline.tsx +2 -0
  74. package/src/react/components/Button.spec.tsx +8 -0
  75. package/src/react/components/Button.tsx +4 -0
  76. package/src/react/components/CodeArea.tsx +1 -0
  77. package/src/react/components/DataTable/DataTable.tsx +4 -4
  78. package/src/react/components/DataTable/TableFilterBuilder.tsx +3 -2
  79. package/src/react/components/FindingsList.tsx +9 -5
  80. package/src/react/components/InfoHint.tsx +1 -0
  81. package/src/react/components/Section.tsx +1 -0
  82. package/src/react/components/calendar/MonthGrid.tsx +3 -1
  83. package/src/react/components/calendar/WeekView.tsx +3 -2
  84. package/src/react/components/sharing/RecipientPicker.tsx +4 -1
  85. package/src/react/dashboard-grid/WidgetFrame.tsx +4 -6
  86. package/src/react/diff-viewer/TextComparePane.tsx +4 -6
  87. package/src/react/emoji/EmojiAutocomplete.tsx +1 -0
  88. package/src/react/folder-tree/FolderPicker.tsx +5 -4
  89. package/src/react/layoutEngine/LayoutCanvas.tsx +1 -0
  90. package/src/react/layoutEngine/NodeInspector.tsx +3 -2
  91. package/src/react/media/TrimRangeSlider.tsx +1 -0
  92. package/src/react/media-gallery/MediaGallery.spec.tsx +22 -0
  93. package/src/react/media-gallery/MediaGallery.tsx +27 -6
  94. package/src/react/media-gallery/index.ts +2 -0
  95. package/src/react/problem-detail/ProblemDetailView.tsx +14 -5
  96. package/src/styles-areas/core.css +29 -28
  97. package/src/styles-utilities.css +9 -0
  98. package/src/theme.css +11 -0
@@ -16,6 +16,7 @@ import { EventChip } from './EventChip.js';
16
16
  import { filterOccurrences } from './occurrences.js';
17
17
  import type { CalendarEventBase, OccurrenceFilters } from './types.js';
18
18
  import { TAP_TARGET_ALWAYS } from '../../lib/tapTarget.js';
19
+ import { Surface } from '../Surface.js';
19
20
 
20
21
  export interface WeekViewProps {
21
22
  /** Any date within the week to render. */
@@ -62,7 +63,7 @@ export function WeekView({
62
63
  const labels = WEEKDAY_LABELS[weekStart];
63
64
 
64
65
  return (
65
- <div className={cn('overflow-hidden rounded-xl border border-border bg-card', className)}>
66
+ <Surface level="flat" pad="none" radius="xl" className={cn('overflow-hidden bg-card', className)}>
66
67
  {/* Weekday header with date numbers */}
67
68
  <div className='grid grid-cols-[repeat(7,minmax(0,1fr))] border-b border-border bg-muted/40'>
68
69
  {days.map((dayKey, i) => (
@@ -133,6 +134,6 @@ export function WeekView({
133
134
  );
134
135
  })}
135
136
  </div>
136
- </div>
137
+ </Surface>
137
138
  );
138
139
  }
@@ -113,7 +113,10 @@ export function RecipientPicker({
113
113
  </div>
114
114
 
115
115
  {showDropdown && (
116
- <div className='mt-1 overflow-hidden rounded-md border border-border bg-popover shadow-overlay'>
116
+ <div
117
+ // guardrails-ignore no-hand-rolled-surface: the suggestions dropdown under the field — an overlay listbox (bg-popover, shadow-overlay), not a card
118
+ className='mt-1 overflow-hidden rounded-md border border-border bg-popover shadow-overlay'
119
+ >
117
120
  <ScrollArea className='max-h-56'>
118
121
  <div role='listbox' className='py-1'>
119
122
  {visible.map((r) => {
@@ -3,6 +3,7 @@ import { DismissButton } from '../components/DismissButton.js';
3
3
  import { ScrollArea } from '../components/ScrollArea.js';
4
4
  import { WidgetHeader } from '../components/WidgetHeader.js';
5
5
  import { cn } from '../lib/cn.js';
6
+ import { Surface } from '../components/Surface.js';
6
7
 
7
8
  export interface WidgetFrameProps {
8
9
  /** Widget title shown in the chrome header. */
@@ -59,11 +60,8 @@ export function WidgetFrame({
59
60
  onRemove !== undefined;
60
61
 
61
62
  return (
62
- <div
63
- className={cn(
64
- 'flex h-full flex-col overflow-hidden rounded-md border border-border bg-surface',
65
- className,
66
- )}
63
+ <Surface
64
+ level="flat" pad="none" radius="md" className={cn('flex h-full flex-col overflow-hidden', className)}
67
65
  >
68
66
  {hasHeader && (
69
67
  <WidgetHeader
@@ -101,6 +99,6 @@ export function WidgetFrame({
101
99
  <ScrollArea axis='y' space='none' safeArea={false} className='p-3'>
102
100
  {children}
103
101
  </ScrollArea>
104
- </div>
102
+ </Surface>
105
103
  );
106
104
  }
@@ -16,6 +16,7 @@ import { useMemo } from 'react';
16
16
  import { cn } from '../lib/cn.js';
17
17
  import { DiffViewer } from './DiffViewer.js';
18
18
  import { diffStats } from './diff.js';
19
+ import { Surface } from '../components/Surface.js';
19
20
 
20
21
  /** One text source in the comparison. */
21
22
  export interface TextSource {
@@ -89,15 +90,12 @@ export function TextComparePane({
89
90
 
90
91
  if (sources.length === 0) {
91
92
  return (
92
- <div
93
- className={cn(
94
- 'cb-text-compare rounded-md border border-border bg-muted p-4 text-center text-sm text-muted-foreground',
95
- className,
96
- )}
93
+ <Surface
94
+ level="flat" pad="none" radius="md" className={cn('cb-text-compare bg-muted p-4 text-center text-sm text-muted-foreground', className)}
97
95
  style={style}
98
96
  >
99
97
  Nothing to compare.
100
- </div>
98
+ </Surface>
101
99
  );
102
100
  }
103
101
 
@@ -111,6 +111,7 @@ export function EmojiAutocomplete({
111
111
  return (
112
112
  <FloatingPortal>
113
113
  <div
114
+ // guardrails-ignore no-hand-rolled-surface: a floating-ui popup panel, positioned by its own `floatingStyles`; Popover owns its own frame the same way
114
115
  ref={refs.setFloating}
115
116
  style={floatingStyles}
116
117
  // floating-ui's own positioning `style` (set above) is required on this exact
@@ -18,6 +18,7 @@ import { Input } from "../components/Input.js";
18
18
  import { FolderBreadcrumb } from "./FolderBreadcrumb.js";
19
19
  import { formatFolderPath, isValidFolderName, normalizeFolderName } from "cursedbelt-core/folder-tree/path";
20
20
  import { type FolderNode, flattenFolderTree } from "cursedbelt-core/folder-tree/tree";
21
+ import { Surface } from "../components/Surface.js";
21
22
 
22
23
  export interface FolderPickerProps {
23
24
  /** The current derived tree (from the items' folder paths). */
@@ -111,7 +112,7 @@ export function FolderPicker({
111
112
  </button>
112
113
 
113
114
  {open && (
114
- <div className="flex flex-col gap-1 rounded-md border border-[var(--border)] bg-[var(--card)] p-1.5">
115
+ <Surface level="flat" pad="none" radius="md" className="flex flex-col gap-1 bg-[var(--card)] p-1.5">
115
116
  {allowNone && (
116
117
  <button
117
118
  // guardrails-ignore no-raw-action-button: option selection, no data mutation
@@ -165,7 +166,7 @@ export function FolderPicker({
165
166
  + New folder…
166
167
  </Button>
167
168
  ) : (
168
- <div className="flex flex-col gap-1 rounded-md border border-[var(--border)] bg-[var(--muted)]/10 p-2">
169
+ <Surface level="flat" pad="none" radius="md" className="flex flex-col gap-1 bg-[var(--muted)]/10 p-2">
169
170
  <span className="text-xs text-[var(--muted-foreground)]">
170
171
  New folder in{" "}
171
172
  {creatingUnder.length === 0 ? (
@@ -210,9 +211,9 @@ export function FolderPicker({
210
211
  Cancel
211
212
  </Button>
212
213
  </div>
213
- </div>
214
+ </Surface>
214
215
  )}
215
- </div>
216
+ </Surface>
216
217
  )}
217
218
  </div>
218
219
  );
@@ -421,6 +421,7 @@ export function LayoutCanvas<F extends LayoutField = LayoutField>({
421
421
  </RootZone>
422
422
  {activeNode && pointer && (
423
423
  <div
424
+ // guardrails-ignore no-hand-rolled-surface: the drag ghost follows the pointer (position: fixed) — a transient cursor companion, not a container
424
425
  aria-hidden
425
426
  style={{
426
427
  position: 'fixed',
@@ -18,6 +18,7 @@ import { cn } from '../lib/cn.js';
18
18
  import type { BindingResolver } from './LayoutRenderer.js';
19
19
  import { compatibleWidgets, widgetDef } from './registry.js';
20
20
  import type { WidgetRegistry } from './types.js';
21
+ import { Surface } from '../components/Surface.js';
21
22
 
22
23
  // The property panel for the selected node — rebuilt for NON-TECHNICAL editing. The
23
24
  // primary layer answers "what should this block look like?" in plain language: pick a
@@ -177,7 +178,7 @@ function Preview<F extends LayoutField>({
177
178
  resolved.kind === 'column' || resolved.kind === 'aggregate' ? resolved.field : undefined;
178
179
  return (
179
180
  <Labeled label='Preview'>
180
- <div className='min-w-0 break-words rounded-md border border-border bg-surface p-3 text-sm text-foreground'>
181
+ <Surface level="flat" pad="sm" radius="md" className='min-w-0 break-words text-sm text-foreground'>
181
182
  {def?.render({
182
183
  node,
183
184
  resolved,
@@ -187,7 +188,7 @@ function Preview<F extends LayoutField>({
187
188
  row: sampleRow,
188
189
  rows: sampleRows,
189
190
  }) ?? <span className='text-muted-foreground'>No preview</span>}
190
- </div>
191
+ </Surface>
191
192
  </Labeled>
192
193
  );
193
194
  }
@@ -92,6 +92,7 @@ export function TrimRangeSlider({
92
92
  return (
93
93
  <div className='select-none' data-cb-trim-slider>
94
94
  <div
95
+ // guardrails-ignore no-hand-rolled-surface: the trim slider's TRACK — a pointer-driven control, not a container
95
96
  ref={trackRef}
96
97
  className='relative h-16 w-full cursor-pointer overflow-hidden rounded-md border border-border bg-muted'
97
98
  onPointerMove={onPointerMove}
@@ -474,6 +474,28 @@ describe('MediaGallery navigation and orientation', () => {
474
474
  fireEvent.click(second.getByRole('button', { name: /Pinned/ }));
475
475
  expect(second.container.querySelector('.cbgd-watch-sticky')).toBeNull();
476
476
  });
477
+
478
+ test('🔴 `pinStage` hands the pin to the APP — its own chrome pins it, and the titlebar button asks', () => {
479
+ const asked: boolean[] = [];
480
+ function App({ pinned }: { pinned: boolean }) {
481
+ return <MediaGallery items={ITEMS} pinStage={pinned} onPinStageChange={(next) => asked.push(next)} />;
482
+ }
483
+ const { container, getByRole, rerender } = render(<App pinned={false} />);
484
+ fireEvent.click(getByRole('button', { name: 'Open clip.mp4' }));
485
+ expect(container.querySelector('.cbgd-watch-sticky')).toBeNull();
486
+
487
+ // The app's own control (collections' rail) flips the prop — the stage pins with no click inside.
488
+ rerender(<App pinned={true} />);
489
+ expect(container.querySelector('.cbgd-watch-sticky')).not.toBeNull();
490
+ expect(getByRole('button', { name: /Pinned/ }).getAttribute('aria-pressed')).toBe('true');
491
+
492
+ // The titlebar button is now a REQUEST: it asks, and until the app answers nothing moves.
493
+ fireEvent.click(getByRole('button', { name: /Pinned/ }));
494
+ expect(asked).toEqual([false]);
495
+ expect(container.querySelector('.cbgd-watch-sticky')).not.toBeNull();
496
+ // Controlled does not move the remembering — the gallery still wrote the shared key.
497
+ expect(globalThis.localStorage.getItem('cbmg:pin-stage')).toBe('0');
498
+ });
477
499
  });
478
500
 
479
501
  /**
@@ -1555,6 +1555,19 @@ export interface MediaGalleryProps {
1555
1555
  * detail view from its own chrome — see the note by `selectedId` in the body.
1556
1556
  */
1557
1557
  selectedId?: string | null;
1558
+ /**
1559
+ * Whether the stage is PINNED to the top while the related grid scrolls under it, when the
1560
+ * consumer owns that — the titlebar's pin button then asks through `onPinStageChange`.
1561
+ *
1562
+ * The case that needed it (collections, 2026-09-29): *"add a … sidebar icon button to press
1563
+ * that toggles locking the media player at the top of the screen"* — the app's own chrome
1564
+ * has to reach a switch the gallery used to hold privately. Same shape as `viewMode`, and
1565
+ * 🔴 the same rule: controlled does NOT move the remembering. The gallery writes
1566
+ * `readPinStage`'s key when its own button is pressed, in both modes; an app toggling it from
1567
+ * its own chrome calls `writePinStage` itself.
1568
+ */
1569
+ pinStage?: boolean;
1570
+ onPinStageChange?: (pinned: boolean) => void;
1558
1571
  /** Custom byte loader for the pdf/sheet/text/html stages (default: `fetch(item.url)`). */
1559
1572
  loadBytes?: (item: MediaGalleryItem) => Promise<ArrayBuffer>;
1560
1573
  /** Shown when there are no items and not loading. */
@@ -1833,6 +1846,8 @@ export function MediaGallery({
1833
1846
  onQueryChange,
1834
1847
  onSelectedChange,
1835
1848
  selectedId: controlledSelectedId,
1849
+ pinStage: controlledPinStage,
1850
+ onPinStageChange,
1836
1851
  loadBytes,
1837
1852
  empty,
1838
1853
  emptyAction,
@@ -1888,7 +1903,17 @@ export function MediaGallery({
1888
1903
  // What happens when a video ends — remembered across visits. See playbackPreferences.ts.
1889
1904
  const [playbackEnd, setPlaybackEnd] = useState<PlaybackEndBehavior>(readPlaybackEnd);
1890
1905
  // Whether the stage stays pinned at the top while the related grid scrolls under it.
1891
- const [pinStage, setPinStage] = useState<boolean>(readPinStage);
1906
+ // Controlled when the consumer passes `pinStage` — see the prop.
1907
+ const [uncontrolledPinStage, setUncontrolledPinStage] = useState<boolean>(readPinStage);
1908
+ const pinStage = controlledPinStage !== undefined ? controlledPinStage : uncontrolledPinStage;
1909
+ const setPinStage = useCallback(
1910
+ (next: boolean): void => {
1911
+ if (controlledPinStage === undefined) setUncontrolledPinStage(next);
1912
+ writePinStage(next);
1913
+ onPinStageChange?.(next);
1914
+ },
1915
+ [controlledPinStage, onPinStageChange],
1916
+ );
1892
1917
  /*
1893
1918
  * 🔴 The shell's `⤢ Fullscreen` lives HERE rather than inside `DenseGalleryDetail`,
1894
1919
  * because the video stage has to know about it.
@@ -2712,11 +2737,7 @@ export function MediaGallery({
2712
2737
  className={`cbgd-order-btn is-icon${pinStage ? ' is-active' : ''}`}
2713
2738
  aria-pressed={pinStage}
2714
2739
  aria-label={pinStage ? 'Pinned — unpin the stage' : 'Pin the stage'}
2715
- onClick={() => {
2716
- const next = !pinStage;
2717
- setPinStage(next);
2718
- writePinStage(next);
2719
- }}
2740
+ onClick={() => setPinStage(!pinStage)}
2720
2741
  >
2721
2742
  <Pin aria-hidden />
2722
2743
  </button>
@@ -50,11 +50,13 @@ export {
50
50
  type PlaybackEndBehavior,
51
51
  nextPlaybackEnd,
52
52
  readImageDwell,
53
+ readPinStage,
53
54
  readPlaybackEnd,
54
55
  type GalleryViewMode,
55
56
  readThumbAutoplay,
56
57
  readViewMode,
57
58
  writeImageDwell,
59
+ writePinStage,
58
60
  writePlaybackEnd,
59
61
  writeThumbAutoplay,
60
62
  writeViewMode,
@@ -5,6 +5,7 @@ import { Badge } from '../components/Badge.js';
5
5
  import { CopyButton } from '../components/CopyButton.js';
6
6
  import { PathText } from '../components/PathText.js';
7
7
  import { cn } from '../lib/cn.js';
8
+ import { Surface } from '../components/Surface.js';
8
9
 
9
10
  /**
10
11
  * A reusable, presentational "why is this problem happening" drill-down.
@@ -153,13 +154,13 @@ export function ProblemDetailView({
153
154
  <section className='grid gap-2 sm:grid-cols-2'>
154
155
  {EXPLAIN_ROWS.map(({ key, label, icon }) =>
155
156
  explain[key] ? (
156
- <div key={key} className='rounded-lg border border-border bg-card p-3'>
157
+ <Surface key={key} level="flat" pad="sm" radius="lg" className='bg-card'>
157
158
  <div className='mb-1 flex items-center gap-1.5 text-muted-foreground'>
158
159
  <span className='shrink-0'>{icon}</span>
159
160
  <span className='text-caption font-semibold uppercase tracking-wide'>{label}</span>
160
161
  </div>
161
162
  <p className='leading-snug text-foreground'>{explain[key]}</p>
162
- </div>
163
+ </Surface>
163
164
  ) : null,
164
165
  )}
165
166
  </section>
@@ -208,10 +209,15 @@ export function ProblemDetailView({
208
209
  </div>
209
210
  <ul className='flex flex-col gap-1'>
210
211
  {ready.slice(0, 12).map((t) => (
211
- <li
212
+ <Surface
212
213
  key={t.file}
213
- className='flex items-center gap-2 rounded border border-border/60 bg-card/50 px-2 py-1'
214
+ asChild
215
+ level='flat'
216
+ pad='none'
217
+ radius='md'
218
+ className='flex items-center gap-2 rounded border-border/60 bg-card/50 px-2 py-1'
214
219
  >
220
+ <li>
215
221
  <span className='min-w-0 flex-1 truncate font-mono text-caption text-foreground'>
216
222
  {t.file}
217
223
  </span>
@@ -221,6 +227,7 @@ export function ProblemDetailView({
221
227
  </Badge>
222
228
  )}
223
229
  </li>
230
+ </Surface>
224
231
  ))}
225
232
  </ul>
226
233
  {ready.length > 12 && (
@@ -268,7 +275,8 @@ export function ProblemDetailView({
268
275
  )}
269
276
 
270
277
  {triage && (
271
- <section className='rounded-lg border border-border bg-muted/30 p-3 text-xs'>
278
+ <Surface asChild level='flat' pad='sm' radius='lg' className='bg-muted/30 text-xs'>
279
+ <section>
272
280
  <span className='text-caption font-semibold uppercase tracking-wide text-muted-foreground'>
273
281
  System verdict · {triage.status}
274
282
  </span>
@@ -278,6 +286,7 @@ export function ProblemDetailView({
278
286
  <p className='mt-1 text-caption text-muted-foreground'>decided by {triage.decidedBy}</p>
279
287
  )}
280
288
  </section>
289
+ </Surface>
281
290
  )}
282
291
 
283
292
  {detail?.trim() && (
@@ -15,7 +15,7 @@
15
15
  * after an app's responsive variant (task 2124). An app that imports every area gets exactly
16
16
  * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
17
17
  * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
18
- * 715 candidate(s).
18
+ * 718 candidate(s).
19
19
  *
20
20
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
21
21
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -43,21 +43,21 @@
43
43
  @source inline("bg-background bg-background/80 bg-background/90 bg-background/95 bg-black/50 bg-border bg-card");
44
44
  @source inline("bg-current bg-destructive bg-destructive-solid bg-destructive/15 bg-destructive/5");
45
45
  @source inline("bg-destructive/8 bg-emerald-100 bg-field-bg bg-muted bg-muted-foreground/40");
46
- @source inline("bg-muted-foreground/50 bg-muted/20 bg-popover bg-success bg-success/15 bg-success/5 bg-surface");
47
- @source inline("bg-surface-sunken bg-tone-amber-bg bg-tone-cyan-bg bg-tone-emerald-bg bg-tone-fuchsia-bg");
48
- @source inline("bg-tone-lime-bg bg-tone-orange-bg bg-tone-purple-bg bg-tone-rose-bg bg-tone-sky-bg");
49
- @source inline("bg-tone-slate-bg bg-tone-teal-bg bg-tone-violet-bg bg-transparent bg-warning bg-warning/15");
50
- @source inline("bg-warning/5 bg-warning/8 bg-white block blur border border-accent/40 border-accent/50 border-b");
51
- @source inline("border-b-2 border-b-[7px] border-b-current border-border border-border-strong border-border/60");
52
- @source inline("border-collapse border-current border-destructive/30 border-destructive/40 border-destructive/50");
53
- @source inline("border-field-border border-success/30 border-success/40 border-success/50 border-t");
54
- @source inline("border-tone-sky-fg/30 border-tone-slate-fg/30 border-transparent border-warning/30");
55
- @source inline("border-warning/40 border-warning/50 border-x-4 border-x-transparent bot bottom bottom-0 break");
56
- @source inline("break-all break-words c ca cap capitalize co com contain container content contents cursor");
57
- @source inline("cursor-col-resize cursor-default cursor-ew-resize cursor-pointer cursor-row-resize d dark");
58
- @source inline("dark:bg-card-overlay dark:bg-card-raised dark:bg-gray-800 data data-[disabled]:hover:bg-border");
59
- @source inline("data-[disabled]:opacity-50 data-[disabled]:pointer-events-none");
60
- @source inline("data-[interactive=true]:duration-[var(--duration-base)]");
46
+ @source inline("bg-muted-foreground/50 bg-muted/20 bg-popover bg-success bg-success-solid bg-success/15");
47
+ @source inline("bg-success/5 bg-surface bg-surface-sunken bg-tone-amber-bg bg-tone-cyan-bg bg-tone-emerald-bg");
48
+ @source inline("bg-tone-fuchsia-bg bg-tone-lime-bg bg-tone-orange-bg bg-tone-purple-bg bg-tone-rose-bg");
49
+ @source inline("bg-tone-sky-bg bg-tone-slate-bg bg-tone-teal-bg bg-tone-violet-bg bg-transparent bg-warning");
50
+ @source inline("bg-warning/15 bg-warning/5 bg-warning/8 bg-white block blur border border-accent/40");
51
+ @source inline("border-accent/50 border-b border-b-2 border-b-[7px] border-b-current border-border");
52
+ @source inline("border-border-strong border-border/60 border-collapse border-current border-destructive/30");
53
+ @source inline("border-destructive/40 border-destructive/50 border-field-border border-success/30");
54
+ @source inline("border-success/40 border-success/50 border-t border-tone-sky-fg/30 border-tone-slate-fg/30");
55
+ @source inline("border-transparent border-warning/30 border-warning/40 border-warning/50 border-x-4");
56
+ @source inline("border-x-transparent bot bottom bottom-0 break break-all break-words c ca cap capitalize co com");
57
+ @source inline("contain container content contents cursor cursor-col-resize cursor-default cursor-ew-resize");
58
+ @source inline("cursor-pointer cursor-row-resize d dark dark:bg-card-overlay dark:bg-card-raised");
59
+ @source inline("dark:bg-gray-800 data data-[disabled]:hover:bg-border data-[disabled]:opacity-50");
60
+ @source inline("data-[disabled]:pointer-events-none data-[interactive=true]:duration-[var(--duration-base)]");
61
61
  @source inline("data-[interactive=true]:ease-[var(--ease-out)] data-[interactive=true]:hover:-translate-y-0.5");
62
62
  @source inline("data-[interactive=true]:hover:border-border-strong data-[interactive=true]:hover:shadow-overlay");
63
63
  @source inline("data-[interactive=true]:transition-all data-[placeholder]:text-muted-foreground");
@@ -86,11 +86,11 @@
86
86
  @source inline("focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent");
87
87
  @source inline("focus-visible:ring-accent-solid focus-visible:ring-destructive focus-visible:ring-field-ring/30");
88
88
  @source inline("focus-visible:ring-offset-1 focus-visible:ring-offset-2 focus-visible:ring-offset-background");
89
- @source inline("focus-visible:ring-ring focus-visible:text-destructive-foreground focus:bg-accent/10");
90
- @source inline("focus:bg-destructive/10 focus:outline-none focus:ring-2 focus:ring-accent focus:text-accent");
91
- @source inline("focus:text-destructive font-bold font-medium font-mono font-normal font-semibold gap gap-0");
92
- @source inline("gap-0.5 gap-1 gap-1.5 gap-12 gap-16 gap-2 gap-3 gap-4 gap-6 gap-8 gap-x-2.5 gap-x-3 gap-y-1");
93
- @source inline("gap-y-2 gap-y-4 grid grid-cols-2");
89
+ @source inline("focus-visible:ring-ring focus-visible:ring-success focus-visible:text-destructive-foreground");
90
+ @source inline("focus:bg-accent/10 focus:bg-destructive/10 focus:outline-none focus:ring-2 focus:ring-accent");
91
+ @source inline("focus:text-accent focus:text-destructive font-bold font-medium font-mono font-normal");
92
+ @source inline("font-semibold gap gap-0 gap-0.5 gap-1 gap-1.5 gap-12 gap-16 gap-2 gap-3 gap-4 gap-6 gap-8");
93
+ @source inline("gap-x-2.5 gap-x-3 gap-y-1 gap-y-2 gap-y-4 grid grid-cols-2");
94
94
  @source inline("grid-cols-[repeat(auto-fit,minmax(min(var(--cb-grid-min),100%),1fr))] group");
95
95
  @source inline("group-active/sep:opacity-100 group-data-[state=open]:rotate-180");
96
96
  @source inline("group-focus-visible/sep:opacity-100 group-hover group-hover/path:opacity-100");
@@ -99,13 +99,14 @@
99
99
  @source inline("h-[var(--control-h-lg)] h-[var(--control-h-md)] h-[var(--control-h-sm)] h-auto h-full h-px hi");
100
100
  @source inline("hid hidden hover hover:bg-accent hover:bg-accent-solid-hover hover:bg-accent/10");
101
101
  @source inline("hover:bg-accent/20 hover:bg-destructive-solid hover:bg-destructive-solid-hover hover:bg-muted");
102
- @source inline("hover:bg-transparent hover:border-accent hover:border-destructive-solid hover:opacity-70");
103
- @source inline("hover:text-accent hover:text-destructive hover:text-destructive-foreground hover:text-foreground");
104
- @source inline("hover:underline i in inline inline-block inline-flex inset inset-0 inset-x-0 inset-y-0 invisible");
105
- @source inline("is iso isolate it item items items-baseline items-center items-end items-start just");
106
- @source inline("justify-between justify-center justify-end l leading leading-none leading-relaxed leading-snug");
107
- @source inline("left left-1/2 left-2 lg lg:px-8 lower lowercase m m-0 max max-h max-h-[80vh]");
108
- @source inline("max-h-[calc(100dvh-2rem)] max-h-[calc(var(--radix-popper-available-height,100dvh)-1rem)]");
102
+ @source inline("hover:bg-success-solid-hover hover:bg-transparent hover:border-accent");
103
+ @source inline("hover:border-destructive-solid hover:opacity-70 hover:text-accent hover:text-destructive");
104
+ @source inline("hover:text-destructive-foreground hover:text-foreground hover:underline i in inline inline-block");
105
+ @source inline("inline-flex inset inset-0 inset-x-0 inset-y-0 invisible is iso isolate it item items");
106
+ @source inline("items-baseline items-center items-end items-start just justify-between justify-center");
107
+ @source inline("justify-end l leading leading-none leading-relaxed leading-snug left left-1/2 left-2 lg lg:px-8");
108
+ @source inline("lower lowercase m m-0 max max-h max-h-[80vh] max-h-[calc(100dvh-2rem)]");
109
+ @source inline("max-h-[calc(var(--radix-popper-available-height,100dvh)-1rem)]");
109
110
  @source inline("max-h-[min(90dvh,calc(100dvh-2rem))] max-h-[var(--radix-select-content-available-height)]");
110
111
  @source inline("max-w-2xl max-w-4xl max-w-5xl max-w-[10rem] max-w-[calc(100vw-2rem)]");
111
112
  @source inline("max-w-[var(--reading-width)] max-w-full max-w-lg max-w-sm max-w-xl max-w-xs mb-0.5 mb-1 md");
@@ -2098,6 +2098,9 @@
2098
2098
  .bg-success {
2099
2099
  background-color: var(--success);
2100
2100
  }
2101
+ .bg-success-solid {
2102
+ background-color: var(--success-solid);
2103
+ }
2101
2104
  .bg-success\/5 {
2102
2105
  background-color: var(--success);
2103
2106
  @supports (color: color-mix(in lab, red, red)) {
@@ -3520,6 +3523,9 @@
3520
3523
  background-color: color-mix(in oklab, var(--muted) 80%, transparent);
3521
3524
  }
3522
3525
  }
3526
+ .hover\:bg-success-solid-hover:hover {
3527
+ background-color: var(--success-solid-hover);
3528
+ }
3523
3529
  .hover\:bg-transparent:hover {
3524
3530
  background-color: transparent;
3525
3531
  }
@@ -3667,6 +3673,9 @@
3667
3673
  .focus-visible\:ring-ring:focus-visible {
3668
3674
  --tw-ring-color: var(--ring);
3669
3675
  }
3676
+ .focus-visible\:ring-success:focus-visible {
3677
+ --tw-ring-color: var(--success);
3678
+ }
3670
3679
  .focus-visible\:ring-offset-1:focus-visible {
3671
3680
  --tw-ring-offset-width: 1px;
3672
3681
  --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
package/src/theme.css CHANGED
@@ -169,6 +169,13 @@
169
169
  --destructive-solid: var(--destructive); /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
170
170
  --destructive-solid-hover: #991b1b; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
171
171
  --success: #16a34a; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
172
+ /* Solid-fill success, for a green button carrying WHITE text — the `destructive-solid`
173
+ split, for the same reason: --success is tuned as a status colour, and white on it
174
+ is 3.3:1. green-700 is 5.02:1 with white and its hover 7.1:1, in both modes, so it
175
+ is mode-stable like --accent-solid. First consumer: station's Resume, the green half
176
+ of a run/pause toggle whose Pause is `destructive-solid` (owner, 2026-09-29). */
177
+ --success-solid: #15803d; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
178
+ --success-solid-hover: #166534; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
172
179
  --warning: #d97706; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
173
180
  /* The gold a filled star is painted in (StarRating). Light needs a DARKER gold
174
181
  than dark mode does, and this is measured, not taste: the neon below scores
@@ -470,6 +477,8 @@
470
477
  --destructive-solid: #dc2626; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
471
478
  --destructive-solid-hover: #b91c1c; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
472
479
  --success: #22c55e; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
480
+ --success-solid: #15803d; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
481
+ --success-solid-hover: #166534; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
473
482
  --warning: #f59e0b; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
474
483
  /* The neon the star always had — correct against a near-black canvas at 10.76:1,
475
484
  and the half of the pair that never needed changing. See the note at :root. */
@@ -845,6 +854,8 @@ option:disabled {
845
854
  --color-destructive-solid: var(--destructive-solid);
846
855
  --color-destructive-solid-hover: var(--destructive-solid-hover);
847
856
  --color-success: var(--success);
857
+ --color-success-solid: var(--success-solid);
858
+ --color-success-solid-hover: var(--success-solid-hover);
848
859
  --color-warning: var(--warning);
849
860
  --color-star: var(--star);
850
861
  /* AA-safe status TEXT over the 15% tinted badge fills */