@proteos/ui 0.31.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 (226) hide show
  1. package/LICENSE +105 -0
  2. package/dist/chunk-IKAX76MB.js +113 -0
  3. package/dist/chunk-IKAX76MB.js.map +1 -0
  4. package/dist/chunk-MWAXK66H.cjs +119 -0
  5. package/dist/chunk-MWAXK66H.cjs.map +1 -0
  6. package/dist/index.cjs +6246 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +2126 -0
  9. package/dist/index.d.ts +2126 -0
  10. package/dist/index.js +6069 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/pdf-view-3ZMMS6ML.cjs +95 -0
  13. package/dist/pdf-view-3ZMMS6ML.cjs.map +1 -0
  14. package/dist/pdf-view-BV5ZD6OC.js +93 -0
  15. package/dist/pdf-view-BV5ZD6OC.js.map +1 -0
  16. package/package.json +96 -0
  17. package/src/components/attachment-chip/attachment-chip.tsx +129 -0
  18. package/src/components/attachment-chip/index.ts +2 -0
  19. package/src/components/attribute-type-icon/attribute-type-icon.stories.tsx +70 -0
  20. package/src/components/attribute-type-icon/attribute-type-icon.tsx +125 -0
  21. package/src/components/attribute-type-icon/index.ts +8 -0
  22. package/src/components/avatar/avatar.stories.tsx +42 -0
  23. package/src/components/avatar/avatar.tsx +48 -0
  24. package/src/components/avatar/index.ts +2 -0
  25. package/src/components/avatar-group/avatar-group.test.tsx +38 -0
  26. package/src/components/avatar-group/avatar-group.tsx +60 -0
  27. package/src/components/avatar-group/index.ts +2 -0
  28. package/src/components/badge/badge.stories.tsx +62 -0
  29. package/src/components/badge/badge.test.tsx +36 -0
  30. package/src/components/badge/badge.tsx +54 -0
  31. package/src/components/badge/index.ts +2 -0
  32. package/src/components/badge-multi-select/badge-multi-select.stories.tsx +177 -0
  33. package/src/components/badge-multi-select/badge-multi-select.test.tsx +155 -0
  34. package/src/components/badge-multi-select/badge-multi-select.tsx +272 -0
  35. package/src/components/badge-multi-select/index.ts +2 -0
  36. package/src/components/breadcrumbs/breadcrumbs.stories.tsx +43 -0
  37. package/src/components/breadcrumbs/breadcrumbs.tsx +86 -0
  38. package/src/components/breadcrumbs/index.ts +2 -0
  39. package/src/components/button/button.stories.tsx +290 -0
  40. package/src/components/button/button.test.tsx +59 -0
  41. package/src/components/button/button.tsx +86 -0
  42. package/src/components/button/index.ts +2 -0
  43. package/src/components/card/card.stories.tsx +351 -0
  44. package/src/components/card/card.test.tsx +141 -0
  45. package/src/components/card/card.tsx +173 -0
  46. package/src/components/card/index.ts +13 -0
  47. package/src/components/checkbox/checkbox-group.tsx +52 -0
  48. package/src/components/checkbox/checkbox.stories.tsx +48 -0
  49. package/src/components/checkbox/checkbox.test.tsx +50 -0
  50. package/src/components/checkbox/checkbox.tsx +71 -0
  51. package/src/components/checkbox/index.ts +4 -0
  52. package/src/components/combobox/combobox.stories.tsx +97 -0
  53. package/src/components/combobox/combobox.test.tsx +123 -0
  54. package/src/components/combobox/combobox.tsx +239 -0
  55. package/src/components/combobox/index.ts +2 -0
  56. package/src/components/command/command-hint.tsx +46 -0
  57. package/src/components/command/command.stories.tsx +88 -0
  58. package/src/components/command/command.test.tsx +102 -0
  59. package/src/components/command/command.tsx +76 -0
  60. package/src/components/command/index.ts +16 -0
  61. package/src/components/currency-input/currency-input.tsx +192 -0
  62. package/src/components/currency-input/index.ts +6 -0
  63. package/src/components/data-table/cells.stories.tsx +120 -0
  64. package/src/components/data-table/cells.tsx +165 -0
  65. package/src/components/data-table/data-table.stories.tsx +371 -0
  66. package/src/components/data-table/data-table.tsx +556 -0
  67. package/src/components/data-table/index.ts +22 -0
  68. package/src/components/data-table/make-actions-column.ts +30 -0
  69. package/src/components/data-table/states/data-table-empty.tsx +40 -0
  70. package/src/components/data-table/states/data-table-error.tsx +30 -0
  71. package/src/components/data-table/states/data-table-skeleton.tsx +42 -0
  72. package/src/components/data-table/use-infinite-scroll-sentinel.ts +51 -0
  73. package/src/components/date-picker/calendar-internals.tsx +138 -0
  74. package/src/components/date-picker/date-picker.stories.tsx +78 -0
  75. package/src/components/date-picker/date-picker.test.tsx +71 -0
  76. package/src/components/date-picker/date-picker.tsx +140 -0
  77. package/src/components/date-picker/index.ts +2 -0
  78. package/src/components/date-time-picker/date-time-picker.stories.tsx +96 -0
  79. package/src/components/date-time-picker/date-time-picker.test.tsx +87 -0
  80. package/src/components/date-time-picker/date-time-picker.tsx +203 -0
  81. package/src/components/date-time-picker/index.ts +2 -0
  82. package/src/components/empty-state/empty-state.tsx +63 -0
  83. package/src/components/empty-state/index.ts +2 -0
  84. package/src/components/error-animation/error-animation.stories.tsx +50 -0
  85. package/src/components/error-animation/error-animation.test.tsx +22 -0
  86. package/src/components/error-animation/error-animation.tsx +26 -0
  87. package/src/components/error-animation/index.ts +2 -0
  88. package/src/components/field/field.tsx +142 -0
  89. package/src/components/field/index.ts +8 -0
  90. package/src/components/file-field/file-field.tsx +285 -0
  91. package/src/components/file-field/index.ts +2 -0
  92. package/src/components/file-viewer/fallback-card.tsx +68 -0
  93. package/src/components/file-viewer/file-viewer.stories.tsx +67 -0
  94. package/src/components/file-viewer/file-viewer.test.tsx +81 -0
  95. package/src/components/file-viewer/file-viewer.tsx +116 -0
  96. package/src/components/file-viewer/image-view.tsx +38 -0
  97. package/src/components/file-viewer/index.ts +7 -0
  98. package/src/components/file-viewer/pdf-view.tsx +122 -0
  99. package/src/components/file-viewer/video-view.tsx +16 -0
  100. package/src/components/image-attachment/image-attachment.stories.tsx +39 -0
  101. package/src/components/image-attachment/image-attachment.test.tsx +54 -0
  102. package/src/components/image-attachment/image-attachment.tsx +77 -0
  103. package/src/components/image-attachment/index.ts +3 -0
  104. package/src/components/image-attachment/is-previewable-image-name.ts +14 -0
  105. package/src/components/image-lightbox/image-lightbox.stories.tsx +38 -0
  106. package/src/components/image-lightbox/image-lightbox.test.tsx +72 -0
  107. package/src/components/image-lightbox/image-lightbox.tsx +92 -0
  108. package/src/components/image-lightbox/index.ts +2 -0
  109. package/src/components/input/index.ts +2 -0
  110. package/src/components/input/input.stories.tsx +71 -0
  111. package/src/components/input/input.test.tsx +36 -0
  112. package/src/components/input/input.tsx +76 -0
  113. package/src/components/kbd/index.ts +2 -0
  114. package/src/components/kbd/kbd.stories.tsx +34 -0
  115. package/src/components/kbd/kbd.tsx +27 -0
  116. package/src/components/menu/index.ts +2 -0
  117. package/src/components/menu/menu.tsx +121 -0
  118. package/src/components/modal/index.ts +10 -0
  119. package/src/components/modal/modal.stories.tsx +153 -0
  120. package/src/components/modal/modal.test.tsx +93 -0
  121. package/src/components/modal/modal.tsx +149 -0
  122. package/src/components/multi-select/index.ts +2 -0
  123. package/src/components/multi-select/multi-select.stories.tsx +211 -0
  124. package/src/components/multi-select/multi-select.test.tsx +151 -0
  125. package/src/components/multi-select/multi-select.tsx +339 -0
  126. package/src/components/nav/index.ts +7 -0
  127. package/src/components/nav/nav.stories.tsx +139 -0
  128. package/src/components/nav/nav.tsx +246 -0
  129. package/src/components/popover/index.ts +8 -0
  130. package/src/components/popover/popover.stories.tsx +92 -0
  131. package/src/components/popover/popover.tsx +135 -0
  132. package/src/components/progress-bar/index.ts +2 -0
  133. package/src/components/progress-bar/progress-bar.stories.tsx +75 -0
  134. package/src/components/progress-bar/progress-bar.test.tsx +29 -0
  135. package/src/components/progress-bar/progress-bar.tsx +57 -0
  136. package/src/components/radio-group/index.ts +2 -0
  137. package/src/components/radio-group/radio-group.stories.tsx +113 -0
  138. package/src/components/radio-group/radio-group.test.tsx +87 -0
  139. package/src/components/radio-group/radio-group.tsx +104 -0
  140. package/src/components/record-filter/attribute-picker.tsx +74 -0
  141. package/src/components/record-filter/chip-value.tsx +171 -0
  142. package/src/components/record-filter/filter-row.tsx +262 -0
  143. package/src/components/record-filter/filter-value-input.tsx +351 -0
  144. package/src/components/record-filter/group-add-controls.tsx +77 -0
  145. package/src/components/record-filter/group-body.tsx +173 -0
  146. package/src/components/record-filter/group-mutations.test.ts +104 -0
  147. package/src/components/record-filter/group-mutations.ts +116 -0
  148. package/src/components/record-filter/index.ts +58 -0
  149. package/src/components/record-filter/nested-group.tsx +138 -0
  150. package/src/components/record-filter/operator-picker.tsx +62 -0
  151. package/src/components/record-filter/operators.test.ts +105 -0
  152. package/src/components/record-filter/operators.ts +97 -0
  153. package/src/components/record-filter/record-filter-builder-panel.tsx +236 -0
  154. package/src/components/record-filter/record-filter-button.tsx +126 -0
  155. package/src/components/record-filter/record-filter-chips.tsx +527 -0
  156. package/src/components/record-filter/record-filter-editor.tsx +91 -0
  157. package/src/components/record-filter/record-filter.stories.tsx +196 -0
  158. package/src/components/record-filter/record-filter.test.tsx +437 -0
  159. package/src/components/record-filter/show-details-store.ts +72 -0
  160. package/src/components/record-filter/types.ts +150 -0
  161. package/src/components/segmented-control/index.ts +6 -0
  162. package/src/components/segmented-control/segmented-control.stories.tsx +67 -0
  163. package/src/components/segmented-control/segmented-control.test.tsx +47 -0
  164. package/src/components/segmented-control/segmented-control.tsx +105 -0
  165. package/src/components/select/index.ts +2 -0
  166. package/src/components/select/select.stories.tsx +76 -0
  167. package/src/components/select/select.test.tsx +56 -0
  168. package/src/components/select/select.tsx +159 -0
  169. package/src/components/separator/index.ts +2 -0
  170. package/src/components/separator/separator.stories.tsx +73 -0
  171. package/src/components/separator/separator.test.tsx +29 -0
  172. package/src/components/separator/separator.tsx +51 -0
  173. package/src/components/skeleton/index.ts +2 -0
  174. package/src/components/skeleton/skeleton.stories.tsx +68 -0
  175. package/src/components/skeleton/skeleton.test.tsx +38 -0
  176. package/src/components/skeleton/skeleton.tsx +43 -0
  177. package/src/components/success-animation/index.ts +2 -0
  178. package/src/components/success-animation/success-animation.stories.tsx +50 -0
  179. package/src/components/success-animation/success-animation.test.tsx +22 -0
  180. package/src/components/success-animation/success-animation.tsx +26 -0
  181. package/src/components/switch/index.ts +2 -0
  182. package/src/components/switch/switch.stories.tsx +60 -0
  183. package/src/components/switch/switch.test.tsx +50 -0
  184. package/src/components/switch/switch.tsx +49 -0
  185. package/src/components/table/index.ts +25 -0
  186. package/src/components/table/table.stories.tsx +306 -0
  187. package/src/components/table/table.test.tsx +115 -0
  188. package/src/components/table/table.tsx +543 -0
  189. package/src/components/tabs/index.ts +8 -0
  190. package/src/components/tabs/tabs.tsx +141 -0
  191. package/src/components/tag-input/index.ts +2 -0
  192. package/src/components/tag-input/tag-input.stories.tsx +73 -0
  193. package/src/components/tag-input/tag-input.test.tsx +80 -0
  194. package/src/components/tag-input/tag-input.tsx +193 -0
  195. package/src/components/textarea/index.ts +2 -0
  196. package/src/components/textarea/textarea.stories.tsx +75 -0
  197. package/src/components/textarea/textarea.test.tsx +41 -0
  198. package/src/components/textarea/textarea.tsx +86 -0
  199. package/src/components/time-field/index.ts +2 -0
  200. package/src/components/time-field/time-field.stories.tsx +76 -0
  201. package/src/components/time-field/time-field.test.tsx +51 -0
  202. package/src/components/time-field/time-field.tsx +98 -0
  203. package/src/components/tooltip/index.ts +2 -0
  204. package/src/components/tooltip/tooltip.stories.tsx +176 -0
  205. package/src/components/tooltip/tooltip.test.tsx +52 -0
  206. package/src/components/tooltip/tooltip.tsx +56 -0
  207. package/src/index.ts +47 -0
  208. package/src/lib/cn.ts +6 -0
  209. package/src/lib/commands/discovery-store.test.tsx +76 -0
  210. package/src/lib/commands/discovery-store.ts +110 -0
  211. package/src/lib/commands/index.ts +9 -0
  212. package/src/lib/commands/match-event.test.ts +99 -0
  213. package/src/lib/commands/match-event.ts +31 -0
  214. package/src/lib/commands/parse-combo.test.ts +65 -0
  215. package/src/lib/commands/parse-combo.ts +48 -0
  216. package/src/lib/commands/platform.ts +27 -0
  217. package/src/lib/commands/protected-combos.test.ts +18 -0
  218. package/src/lib/commands/protected-combos.ts +38 -0
  219. package/src/lib/commands/registry.test.ts +97 -0
  220. package/src/lib/commands/registry.ts +59 -0
  221. package/src/lib/commands/use-command.test.tsx +91 -0
  222. package/src/lib/commands/use-command.ts +32 -0
  223. package/src/lib/commands/use-escape-key.test.tsx +97 -0
  224. package/src/lib/commands/use-escape-key.ts +62 -0
  225. package/src/styles.css +321 -0
  226. package/src/test/setup.ts +9 -0
@@ -0,0 +1,97 @@
1
+ import { render } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { _resetEscapeStack, useEscapeKey } from './use-escape-key'
5
+
6
+ function Bind({
7
+ handler,
8
+ enabled,
9
+ }: {
10
+ handler: (e: KeyboardEvent) => void
11
+ enabled?: boolean
12
+ }) {
13
+ useEscapeKey(handler, enabled !== undefined ? { enabled } : undefined)
14
+ return null
15
+ }
16
+
17
+ function dispatchEscape(target?: EventTarget) {
18
+ const ev = new KeyboardEvent('keydown', {
19
+ bubbles: true,
20
+ cancelable: true,
21
+ key: 'Escape',
22
+ })
23
+ ;(target ?? window).dispatchEvent(ev)
24
+ }
25
+
26
+ describe('useEscapeKey', () => {
27
+ beforeEach(() => {
28
+ _resetEscapeStack()
29
+ })
30
+ afterEach(() => {
31
+ _resetEscapeStack()
32
+ })
33
+
34
+ it('fires the handler on Escape', () => {
35
+ const fn = vi.fn()
36
+ render(<Bind handler={fn} />)
37
+ dispatchEscape()
38
+ expect(fn).toHaveBeenCalledTimes(1)
39
+ })
40
+
41
+ it('skips editable targets', () => {
42
+ const fn = vi.fn()
43
+ render(<Bind handler={fn} />)
44
+ const input = document.createElement('input')
45
+ document.body.appendChild(input)
46
+ dispatchEscape(input)
47
+ document.body.removeChild(input)
48
+ expect(fn).not.toHaveBeenCalled()
49
+ })
50
+
51
+ it('respects enabled=false', () => {
52
+ const fn = vi.fn()
53
+ render(<Bind handler={fn} enabled={false} />)
54
+ dispatchEscape()
55
+ expect(fn).not.toHaveBeenCalled()
56
+ })
57
+
58
+ it('newest mount wins; only one handler fires per event', () => {
59
+ const outer = vi.fn()
60
+ const inner = vi.fn()
61
+ render(
62
+ <>
63
+ <Bind handler={outer} />
64
+ <Bind handler={inner} />
65
+ </>,
66
+ )
67
+ dispatchEscape()
68
+ expect(inner).toHaveBeenCalledTimes(1)
69
+ expect(outer).not.toHaveBeenCalled()
70
+ })
71
+
72
+ it('cleans up on unmount; the previous handler becomes top', () => {
73
+ const outer = vi.fn()
74
+ const inner = vi.fn()
75
+ function Outer() {
76
+ useEscapeKey(outer)
77
+ return null
78
+ }
79
+ function Inner() {
80
+ useEscapeKey(inner)
81
+ return null
82
+ }
83
+ function Wrap({ showInner }: { showInner: boolean }) {
84
+ return (
85
+ <>
86
+ <Outer />
87
+ {showInner ? <Inner /> : null}
88
+ </>
89
+ )
90
+ }
91
+ const { rerender } = render(<Wrap showInner={true} />)
92
+ rerender(<Wrap showInner={false} />)
93
+ dispatchEscape()
94
+ expect(outer).toHaveBeenCalledTimes(1)
95
+ expect(inner).not.toHaveBeenCalled()
96
+ })
97
+ })
@@ -0,0 +1,62 @@
1
+ /* Bare-Escape handler. Doesn't go through the `mod+<key>` registry because
2
+ Escape has no modifier. Same editable-target guard. Multiple subscribers
3
+ stack — only the newest fires per event, matching the "newest wins"
4
+ semantics of the command registry so nested overlays (peek over a
5
+ record stack, say) close one layer at a time. */
6
+
7
+ import { useEffect, useRef } from 'react'
8
+
9
+ import { isEditableTarget } from './match-event'
10
+
11
+ export type UseEscapeKeyOptions = {
12
+ /** Defaults to `true`. Set to `false` to temporarily disable without unmounting. */
13
+ enabled?: boolean
14
+ }
15
+
16
+ type Entry = { handler: (event: KeyboardEvent) => void }
17
+
18
+ const stack: Entry[] = []
19
+ let listenerInstalled = false
20
+
21
+ function onWindowKeyDown(event: KeyboardEvent): void {
22
+ if (event.key !== 'Escape') return
23
+ if (isEditableTarget(event.target)) return
24
+ const top = stack[stack.length - 1]
25
+ if (!top) return
26
+ top.handler(event)
27
+ }
28
+
29
+ function ensureListener(): void {
30
+ if (listenerInstalled) return
31
+ if (typeof window === 'undefined') return
32
+ window.addEventListener('keydown', onWindowKeyDown, { capture: true })
33
+ listenerInstalled = true
34
+ }
35
+
36
+ export function useEscapeKey(
37
+ handler: (event: KeyboardEvent) => void,
38
+ options?: UseEscapeKeyOptions,
39
+ ): void {
40
+ const enabled = options?.enabled ?? true
41
+ const handlerRef = useRef(handler)
42
+ useEffect(() => {
43
+ handlerRef.current = handler
44
+ })
45
+ useEffect(() => {
46
+ if (!enabled) return undefined
47
+ ensureListener()
48
+ const entry: Entry = {
49
+ handler: (event) => handlerRef.current(event),
50
+ }
51
+ stack.push(entry)
52
+ return () => {
53
+ const idx = stack.lastIndexOf(entry)
54
+ if (idx >= 0) stack.splice(idx, 1)
55
+ }
56
+ }, [enabled])
57
+ }
58
+
59
+ // Test-only.
60
+ export function _resetEscapeStack(): void {
61
+ stack.length = 0
62
+ }
package/src/styles.css ADDED
@@ -0,0 +1,321 @@
1
+ /* Google Fonts @import MUST come before `@import "tailwindcss"` — Tailwind's
2
+ directive expands inline into many rules, and any @import that follows them
3
+ violates CSS's "all @imports come first" rule. */
4
+ @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Inter:wght@300;400;450;500;600&display=swap");
5
+
6
+ @import "tailwindcss";
7
+
8
+ /* Make Tailwind's content scanner pick up classes used inside the lib's own
9
+ components when this stylesheet is consumed from another package (e.g.
10
+ `clients/web` imports it via `@import "@proteos/ui/styles.css"`). Without
11
+ this, utilities that only appear in lib source — `border-transparent`, the
12
+ indigo color-mix variants, `rounded-pill`, etc. — are tree-shaken from the
13
+ consumer's build and components render unstyled. Path is resolved relative
14
+ to *this* file. */
15
+ @source "./components/**/*.{ts,tsx}";
16
+
17
+ /* ==========================================================
18
+ PROTEOS · INDIGO — design tokens
19
+ Mirrors Proteos Design System v0.1 (spec: revision 2026.04.24).
20
+ Exposed to Tailwind v4 utilities via @theme, and preserved as
21
+ raw CSS custom properties so components can reference them.
22
+ ========================================================== */
23
+
24
+ @theme {
25
+ /* Base — the page/chrome background the app shell paints. The majority
26
+ surface: pages, panels, headers, toolbars, tables, ghost buttons.
27
+ Split from --color-surface so themes can lift raised blocks off it;
28
+ in paper the two are identical. */
29
+ --color-bg: #f2f0ea;
30
+
31
+ /* Surfaces — raised blocks that sit ON the base: cards, popovers, modals,
32
+ inputs. surface-2/-3 are the recessed tint steps (hovers, read-only
33
+ boxes, chips, table-header fill), stepped darker in paper. */
34
+ --color-surface: #f2f0ea;
35
+ --color-surface-2: #e7e4db;
36
+ --color-surface-3: #dcd8cc;
37
+
38
+ /* Ink — indigo-tinted slate */
39
+ --color-ink: #1a1d2b;
40
+ --color-ink-2: #353a4b;
41
+ --color-ink-3: #686c7a;
42
+ --color-ink-4: #9a9ca6;
43
+
44
+ /* Hairlines. rule-strong marks interactive-control borders (inputs,
45
+ ghost buttons, segmented tracks) — equal to rule in paper, darker in
46
+ the light/dark themes so touchable chrome pops. */
47
+ --color-rule: #cfcbbd;
48
+ --color-rule-2: #dbd7c9;
49
+ --color-rule-strong: #cfcbbd;
50
+
51
+ /* Accent — deep indigo */
52
+ --color-accent: #2f3a66;
53
+ --color-accent-2: #4a5580;
54
+ --color-accent-ink: #1a2042;
55
+
56
+ /* Semantic — muted, paper-compatible */
57
+ --color-success: #3d5c3a;
58
+ --color-warning: #8a6a1f;
59
+ --color-danger: #7a2a2a;
60
+ --color-info: #2f3a66;
61
+
62
+ /* Indigo ramp — for edge cases & future components */
63
+ --color-indigo-50: #e8e9f0;
64
+ --color-indigo-100: #c9ccdc;
65
+ --color-indigo-200: #9aa0bf;
66
+ --color-indigo-300: #6b73a0;
67
+ --color-indigo-400: #4a5580;
68
+ --color-indigo-500: #2f3a66;
69
+ --color-indigo-600: #1a2042;
70
+ --color-indigo-700: #121630;
71
+ --color-indigo-800: #0a0c1e;
72
+
73
+ /* App tile palette — coloured tiles in AppSwitcher and the canvas
74
+ element-type badge. `tile-fg` is the ink-on-tile colour, slightly
75
+ warmer than --color-surface so it reads as "printed on coloured paper". */
76
+ --color-tile-indigo: #2f3a66;
77
+ --color-tile-moss: #5b7052;
78
+ --color-tile-terra: #8a5a3f;
79
+ --color-tile-slate: #1a1d2b;
80
+ --color-tile-fg: #f0efe8;
81
+
82
+ /* Type */
83
+ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
84
+ --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
85
+
86
+ /* Radii */
87
+ --radius-sm: 6px;
88
+ --radius-md: 10px;
89
+ --radius-lg: 14px;
90
+ --radius-xl: 18px;
91
+ --radius-pill: 999px;
92
+
93
+ /* Shadows — warm, low-key */
94
+ --shadow-inset: 0 1px 0 rgba(255, 255, 255, 0.6) inset,
95
+ 0 1px 2px rgba(26, 29, 43, 0.05);
96
+ --shadow-rest: 0 1px 0 rgba(255, 255, 255, 0.6) inset,
97
+ 0 4px 10px -4px rgba(26, 29, 43, 0.1), 0 1px 2px rgba(26, 29, 43, 0.05);
98
+ --shadow-raised: 0 1px 0 rgba(255, 255, 255, 0.6) inset,
99
+ 0 14px 30px -14px rgba(26, 29, 43, 0.18),
100
+ 0 2px 4px rgba(26, 29, 43, 0.05);
101
+ --shadow-float: 0 1px 0 rgba(255, 255, 255, 0.6) inset,
102
+ 0 30px 60px -30px rgba(26, 29, 43, 0.28),
103
+ 0 2px 6px rgba(26, 29, 43, 0.06);
104
+ --shadow-focus: 0 0 0 4px color-mix(in oklab, var(--color-accent) 22%, transparent);
105
+ --shadow-focus-danger: 0 0 0 4px
106
+ color-mix(in oklab, var(--color-danger) 18%, transparent);
107
+
108
+ /* Motion — durations & easing. 150ms colour/border, 200ms transform,
109
+ 280ms layout. Default ease for layout transitions is the "out-soft"
110
+ curve; everything else uses CSS's `ease`. Nothing bounces. */
111
+ --ease-out-soft: cubic-bezier(0.2, 0.8, 0.2, 1);
112
+ --duration-fast: 150ms;
113
+ --duration-base: 200ms;
114
+ --duration-slow: 280ms;
115
+
116
+ /* Indeterminate progress — a 35%-wide segment that slides off the right
117
+ edge and re-enters from the left on a continuous loop. Paired with the
118
+ ProgressBar component's `w-[35%]` fill. */
119
+ --animate-indeterminate-progress: indeterminate-progress 1.5s ease-in-out infinite;
120
+
121
+ /* Status pop — one-shot scale+fade entrance for the large success/error
122
+ status icons. Soft ease, no overshoot (nothing bounces). */
123
+ --animate-status-pop: status-pop var(--duration-slow) var(--ease-out-soft) both;
124
+ }
125
+
126
+ @keyframes indeterminate-progress {
127
+ 0% {
128
+ left: -35%;
129
+ }
130
+ 60% {
131
+ left: 100%;
132
+ }
133
+ 100% {
134
+ left: 100%;
135
+ }
136
+ }
137
+
138
+ @keyframes status-pop {
139
+ from {
140
+ opacity: 0;
141
+ transform: scale(0.85);
142
+ }
143
+ to {
144
+ opacity: 1;
145
+ transform: scale(1);
146
+ }
147
+ }
148
+
149
+ /* Raw variables (non-color) for direct component consumption.
150
+ Tailwind v4 mirrors @theme into :root for colors/radii/shadows, but we
151
+ also expose spacing tokens here for components that need raw var() access. */
152
+ :root {
153
+ --s-1: 4px;
154
+ --s-2: 8px;
155
+ --s-3: 12px;
156
+ --s-4: 16px;
157
+ --s-5: 20px;
158
+ --s-6: 24px;
159
+ --s-7: 32px;
160
+ --s-8: 40px;
161
+ --s-9: 56px;
162
+ --s-10: 72px;
163
+ --s-11: 96px;
164
+ --s-12: 128px;
165
+
166
+ /* Table zebra-stripe + row-hover strength — the % of ink mixed into the
167
+ page bg per row (see Table). Per-theme so each mode tunes independently:
168
+ near-white bases show a given delta more, so light dials down. Paper 3%
169
+ matches the pre-theme stripe tone. */
170
+ --row-stripe: 3%;
171
+ --row-hover: 6%;
172
+
173
+ color-scheme: light;
174
+ }
175
+
176
+ /* ==========================================================
177
+ Theme modes — paper (default, tokens above) · light · dark.
178
+ Switched via data-theme on <html> (see clients/web lib/theme.ts).
179
+ Plain unlayered blocks: they beat Tailwind's @layer theme :root
180
+ declarations, and [data-theme] beats :root on specificity. Tailwind
181
+ utilities resolve var(--color-*) / var(--shadow-*) at use-time, so
182
+ redefining the custom properties re-skins every component.
183
+ ========================================================== */
184
+
185
+ /* Light — white surfaces lifted off a cool-gray base. */
186
+ :root[data-theme='light'] {
187
+ --color-bg: #fbfbfc;
188
+ --color-surface: #ffffff;
189
+ --color-surface-2: #f0f0f2;
190
+ --color-surface-3: #e5e5e9;
191
+
192
+ --color-ink: #1a1d2b;
193
+ --color-ink-2: #383d4e;
194
+ --color-ink-3: #63677a;
195
+ --color-ink-4: #7e818f;
196
+
197
+ --color-rule: #d7d7dc;
198
+ --color-rule-2: #e6e6ea;
199
+ --color-rule-strong: #b0b1ba;
200
+
201
+ /* Softer stripes — the near-white base makes any delta read stronger. */
202
+ --row-stripe: 2%;
203
+ --row-hover: 4%;
204
+
205
+ /* Accent, semantics, indigo ramp, tiles — unchanged from paper. */
206
+
207
+ /* Cooler, crisper shadows; the white top-inset highlight from paper is
208
+ dropped — invisible on white surfaces. */
209
+ --shadow-inset: 0 1px 2px rgba(23, 25, 45, 0.05);
210
+ --shadow-rest: 0 1px 2px rgba(23, 25, 45, 0.05),
211
+ 0 3px 10px -3px rgba(23, 25, 45, 0.07);
212
+ --shadow-raised: 0 2px 4px rgba(23, 25, 45, 0.05),
213
+ 0 12px 28px -10px rgba(23, 25, 45, 0.14);
214
+ --shadow-float: 0 4px 10px rgba(23, 25, 45, 0.06),
215
+ 0 28px 60px -20px rgba(23, 25, 45, 0.22);
216
+ }
217
+
218
+ /* Dark — parallel ramp, not an inversion. Accent lifts to a lighter
219
+ indigo so fills and focus halos hold contrast against dark surfaces. */
220
+ :root[data-theme='dark'] {
221
+ color-scheme: dark;
222
+
223
+ --color-bg: #111318;
224
+ --color-surface: #191c24;
225
+ --color-surface-2: #222633;
226
+ --color-surface-3: #2c3140;
227
+
228
+ --color-ink: #e9eaf0;
229
+ --color-ink-2: #c5c8d4;
230
+ --color-ink-3: #9297a8;
231
+ --color-ink-4: #6d7183;
232
+
233
+ --color-rule: #32384a;
234
+ --color-rule-2: #272c38;
235
+ --color-rule-strong: #4a5166;
236
+
237
+ /* Dark rows lighten as they stripe; a touch more to stay legible. */
238
+ --row-stripe: 4%;
239
+ --row-hover: 7%;
240
+
241
+ /* accent-2 is the hover fill on accent chrome under fixed light text —
242
+ in dark it must darken (rest accent is already light-ish), not lighten.
243
+ accent-ink flips to a light indigo: it's the text color on tinted
244
+ surfaces and the hover fill under text-surface (dark) — both want light. */
245
+ --color-accent: #6b78b4;
246
+ --color-accent-2: #5d6aa6;
247
+ --color-accent-ink: #ccd3f0;
248
+
249
+ --color-success: #7fae7c;
250
+ --color-warning: #d0a75a;
251
+ --color-danger: #d08a83;
252
+ --color-info: #6b78b4;
253
+
254
+ /* Indigo ramp inverted so a low index stays "subtle against bg". */
255
+ --color-indigo-50: #232945;
256
+ --color-indigo-100: #2c3354;
257
+ --color-indigo-200: #3a4370;
258
+ --color-indigo-300: #4d5890;
259
+ --color-indigo-400: #6b78b4;
260
+ --color-indigo-500: #8290c8;
261
+ --color-indigo-600: #a5b0d9;
262
+ --color-indigo-700: #c7cee9;
263
+ --color-indigo-800: #e4e7f5;
264
+
265
+ /* Tile fills stay saturated mid-tones; tile-fg unchanged. */
266
+
267
+ --shadow-inset: 0 1px 2px rgba(0, 0, 0, 0.35);
268
+ --shadow-rest: 0 1px 2px rgba(0, 0, 0, 0.35),
269
+ 0 4px 12px -4px rgba(0, 0, 0, 0.45);
270
+ --shadow-raised: 0 2px 4px rgba(0, 0, 0, 0.35),
271
+ 0 14px 30px -12px rgba(0, 0, 0, 0.55);
272
+ --shadow-float: 0 4px 10px rgba(0, 0, 0, 0.4),
273
+ 0 30px 60px -24px rgba(0, 0, 0, 0.65);
274
+ --shadow-focus: 0 0 0 4px color-mix(in oklab, var(--color-accent) 35%, transparent);
275
+ --shadow-focus-danger: 0 0 0 4px
276
+ color-mix(in oklab, var(--color-danger) 28%, transparent);
277
+ }
278
+
279
+ /* Paper grain — faint dotted multiply overlay, identical to the design system spec.
280
+ Applied as a fixed decorative layer above content but below interactive chrome. */
281
+ .proteos-grain::before {
282
+ content: "";
283
+ position: fixed;
284
+ inset: 0;
285
+ pointer-events: none;
286
+ z-index: 100;
287
+ opacity: 0.32;
288
+ mix-blend-mode: multiply;
289
+ background-image:
290
+ radial-gradient(rgba(40, 38, 55, 0.035) 1px, transparent 1px),
291
+ radial-gradient(rgba(40, 38, 55, 0.025) 1px, transparent 1px);
292
+ background-size: 3px 3px, 7px 7px;
293
+ background-position: 0 0, 1px 2px;
294
+ }
295
+
296
+ /* Grain is a paper-mode signature only. On white it reads as dirt; the
297
+ multiply dots vanish or muddy on dark. Absent attribute = paper.
298
+ `.proteos-grain-dots` gates inline-styled dot overlays (page-designer
299
+ canvas) the same way. */
300
+ :root[data-theme='light'] .proteos-grain::before,
301
+ :root[data-theme='dark'] .proteos-grain::before {
302
+ content: none;
303
+ }
304
+
305
+ :root[data-theme='light'] .proteos-grain-dots,
306
+ :root[data-theme='dark'] .proteos-grain-dots {
307
+ display: none;
308
+ }
309
+
310
+ /* DEPRECATED aliases for module components built before the surface rename
311
+ (2026-07). Already-deployed module bundles bake `var(--color-bg-2)` /
312
+ `var(--color-bg-3)` strings that no longer exist after the rename; these
313
+ keep them resolving (and they chain through the theme overrides above,
314
+ since custom properties resolve at use-time). Remove once every module is
315
+ redeployed against the surface-* names. Note: old `var(--color-bg)` now
316
+ resolves to the new base (was the surface color) — near-identical in every
317
+ theme, so no alias is provided for it. */
318
+ :root {
319
+ --color-bg-2: var(--color-surface-2);
320
+ --color-bg-3: var(--color-surface-3);
321
+ }
@@ -0,0 +1,9 @@
1
+ import * as matchers from '@testing-library/jest-dom/matchers'
2
+ import { cleanup } from '@testing-library/react'
3
+ import { afterEach, expect } from 'vitest'
4
+
5
+ expect.extend(matchers)
6
+
7
+ afterEach(() => {
8
+ cleanup()
9
+ })