@titan-design/react-ui 0.6.0 → 0.7.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 (248) hide show
  1. package/dist/bodymap.js +12 -7
  2. package/dist/bodymap.js.map +1 -1
  3. package/dist/bodymap.mjs +12 -7
  4. package/dist/bodymap.mjs.map +1 -1
  5. package/dist/index.d.mts +855 -124
  6. package/dist/index.d.ts +855 -124
  7. package/dist/index.js +2276 -801
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2230 -802
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{pages-MO0JCySL.d.mts → pages-8u_4WbL-.d.mts} +280 -57
  12. package/dist/{pages-D7Jlssvp.d.ts → pages-DaWiBOGa.d.ts} +280 -57
  13. package/dist/pages.d.mts +1 -1
  14. package/dist/pages.d.ts +1 -1
  15. package/dist/pages.js +1255 -798
  16. package/dist/pages.js.map +1 -1
  17. package/dist/pages.mjs +1255 -798
  18. package/dist/pages.mjs.map +1 -1
  19. package/dist/theme/index.d.mts +16 -82
  20. package/dist/theme/index.d.ts +16 -82
  21. package/dist/theme/index.js +13 -115
  22. package/dist/theme/index.js.map +1 -1
  23. package/dist/theme/index.mjs +13 -115
  24. package/dist/theme/index.mjs.map +1 -1
  25. package/dist/theme/tokens-css.js +14 -6
  26. package/dist/theme/tokens-css.js.map +1 -1
  27. package/dist/theme/tokens-css.mjs +14 -6
  28. package/dist/theme/tokens-css.mjs.map +1 -1
  29. package/package.json +1 -1
  30. package/src/arch/Architecture.stories.tsx +870 -0
  31. package/src/arch/annotations.json +48 -0
  32. package/src/arch/arch-graph.json +4301 -0
  33. package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
  34. package/src/components/custom/EmptyState/EmptyState.stories.tsx +1 -1
  35. package/src/components/custom/Gauge/Gauge.stories.tsx +1 -1
  36. package/src/components/custom/Metric/Metric.stories.tsx +1 -1
  37. package/src/components/custom/Scatter/Scatter.stories.tsx +1 -1
  38. package/src/components/custom/Sidebar/Sidebar.stories.tsx +1 -1
  39. package/src/components/custom/Table/Table.stories.tsx +1 -1
  40. package/src/components/custom/TimerReadout/TimerReadout.stories.tsx +62 -0
  41. package/src/components/custom/TimerReadout/TimerReadout.test.tsx +72 -0
  42. package/src/components/custom/TimerReadout/TimerReadout.tsx +75 -0
  43. package/src/components/custom/TimerReadout/index.ts +1 -0
  44. package/src/components/custom/Treemap/Treemap.stories.tsx +1 -1
  45. package/src/components/custom/Typography/Typography.stories.tsx +1 -1
  46. package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +21 -13
  47. package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +16 -13
  48. package/src/components/custom/Workout/ActiveWorkoutPage.tsx +9 -15
  49. package/src/components/custom/Workout/BaseBadge.stories.tsx +1 -1
  50. package/src/components/custom/Workout/BodyMap.stories.tsx +1 -1
  51. package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +1 -1
  52. package/src/components/custom/Workout/CapacityBandChart.stories.tsx +1 -1
  53. package/src/components/custom/Workout/DeviationBar.stories.tsx +1 -1
  54. package/src/components/custom/Workout/ExerciseCard.stories.tsx +51 -66
  55. package/src/components/custom/Workout/ExerciseCard.test.tsx +146 -143
  56. package/src/components/custom/Workout/ExerciseCard.tsx +176 -198
  57. package/src/components/custom/Workout/ExerciseCardHeading.stories.tsx +99 -0
  58. package/src/components/custom/Workout/ExerciseCardHeading.test.tsx +72 -0
  59. package/src/components/custom/Workout/ExerciseCardHeading.tsx +43 -0
  60. package/src/components/custom/Workout/ExerciseCardUnified.stories.tsx +151 -0
  61. package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +5 -3
  62. package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +4 -4
  63. package/src/components/custom/Workout/ExerciseDetailPage.tsx +4 -5
  64. package/src/components/custom/Workout/ExerciseHeading.stories.tsx +81 -0
  65. package/src/components/custom/Workout/ExerciseHeading.test.tsx +77 -0
  66. package/src/components/custom/Workout/ExerciseHeading.tsx +85 -0
  67. package/src/components/custom/Workout/ExerciseIndicator.stories.tsx +110 -0
  68. package/src/components/custom/Workout/ExerciseIndicator.test.tsx +93 -0
  69. package/src/components/custom/Workout/ExerciseIndicator.tsx +120 -0
  70. package/src/components/custom/Workout/FatigueMeter.stories.tsx +51 -0
  71. package/src/components/custom/Workout/FatigueMeter.test.tsx +73 -0
  72. package/src/components/custom/Workout/FatigueMeter.tsx +82 -0
  73. package/src/components/custom/Workout/InputBar.stories.tsx +1 -1
  74. package/src/components/custom/Workout/IntensityBar.stories.tsx +1 -1
  75. package/src/components/custom/Workout/LiveAuraFrame.stories.tsx +153 -0
  76. package/src/components/custom/Workout/LiveAuraFrame.test.tsx +74 -0
  77. package/src/components/custom/Workout/LiveAuraFrame.tsx +105 -0
  78. package/src/components/custom/Workout/MesoCard.stories.tsx +1 -1
  79. package/src/components/custom/Workout/MesoProgressBar.stories.tsx +1 -1
  80. package/src/components/custom/Workout/MesoStatusCard.stories.tsx +1 -1
  81. package/src/components/custom/Workout/MetricTiles.stories.tsx +60 -0
  82. package/src/components/custom/Workout/MetricTiles.test.tsx +58 -0
  83. package/src/components/custom/Workout/MetricTiles.tsx +48 -0
  84. package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +1 -1
  85. package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +1 -1
  86. package/src/components/custom/Workout/PrBadge.stories.tsx +1 -1
  87. package/src/components/custom/Workout/PrHistoryModal.stories.tsx +1 -1
  88. package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +4 -16
  89. package/src/components/custom/Workout/ProgramPlanningPage.tsx +8 -6
  90. package/src/components/custom/Workout/README.md +146 -4
  91. package/src/components/custom/Workout/ReadinessCheck.stories.tsx +1 -1
  92. package/src/components/custom/Workout/RestTimer.stories.tsx +1 -1
  93. package/src/components/custom/Workout/RestTimer.tsx +15 -10
  94. package/src/components/custom/Workout/RowInventory.stories.tsx +405 -0
  95. package/src/components/custom/Workout/S3FullRail.stories.tsx +34 -0
  96. package/src/components/custom/Workout/S3SessionPace.stories.tsx +95 -0
  97. package/src/components/custom/Workout/S3SessionRailHeading.stories.tsx +61 -0
  98. package/src/components/custom/Workout/S3SetTypes.stories.tsx +176 -0
  99. package/src/components/custom/Workout/S3WorkoutExpansion.stories.tsx +240 -0
  100. package/src/components/custom/Workout/ScheduleTiles.stories.tsx +40 -0
  101. package/src/components/custom/Workout/ScheduleTiles.test.tsx +34 -0
  102. package/src/components/custom/Workout/ScheduleTiles.tsx +70 -0
  103. package/src/components/custom/Workout/SegmentedBar.stories.tsx +99 -0
  104. package/src/components/custom/Workout/SegmentedBar.test.tsx +77 -0
  105. package/src/components/custom/Workout/SegmentedBar.tsx +172 -0
  106. package/src/components/custom/Workout/SegmentedProgressBar.stories.tsx +50 -0
  107. package/src/components/custom/Workout/SegmentedProgressBar.test.tsx +80 -0
  108. package/src/components/custom/Workout/SegmentedProgressBar.tsx +67 -0
  109. package/src/components/custom/Workout/SessionHeader.stories.tsx +121 -0
  110. package/src/components/custom/Workout/SessionHeader.test.tsx +122 -0
  111. package/src/components/custom/Workout/SessionHeader.tsx +152 -0
  112. package/src/components/custom/Workout/SessionRail.stories.tsx +153 -0
  113. package/src/components/custom/Workout/SessionRail.test.tsx +86 -0
  114. package/src/components/custom/Workout/SessionRail.tsx +126 -0
  115. package/src/components/custom/Workout/SetBar.stories.tsx +124 -0
  116. package/src/components/custom/Workout/SetBar.test.tsx +123 -0
  117. package/src/components/custom/Workout/SetBar.tsx +171 -0
  118. package/src/components/custom/Workout/SetRow.stories.tsx +50 -100
  119. package/src/components/custom/Workout/SetRow.test.tsx +94 -166
  120. package/src/components/custom/Workout/SetRow.tsx +169 -232
  121. package/src/components/custom/Workout/SetStrip.stories.tsx +154 -0
  122. package/src/components/custom/Workout/SetStrip.test.tsx +109 -0
  123. package/src/components/custom/Workout/SetStrip.tsx +71 -0
  124. package/src/components/custom/Workout/SetTableHeader.stories.tsx +66 -0
  125. package/src/components/custom/Workout/SetTableHeader.test.tsx +47 -0
  126. package/src/components/custom/Workout/SetTableHeader.tsx +88 -0
  127. package/src/components/custom/Workout/SetsRepsLoad.stories.tsx +47 -0
  128. package/src/components/custom/Workout/SetsRepsLoad.test.tsx +44 -0
  129. package/src/components/custom/Workout/SetsRepsLoad.tsx +52 -0
  130. package/src/components/custom/Workout/Sparkline.stories.tsx +1 -1
  131. package/src/components/custom/Workout/StatusDot.stories.tsx +1 -1
  132. package/src/components/custom/Workout/StatusPill.stories.tsx +59 -0
  133. package/src/components/custom/Workout/StatusPill.test.tsx +63 -0
  134. package/src/components/custom/Workout/StatusPill.tsx +88 -0
  135. package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +1 -1
  136. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +1 -1
  137. package/src/components/custom/Workout/TempoBar.stories.tsx +1 -1
  138. package/src/components/custom/Workout/TempoDisplay.stories.tsx +12 -1
  139. package/src/components/custom/Workout/TempoDisplay.tsx +23 -33
  140. package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +1 -1
  141. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +254 -0
  142. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +145 -0
  143. package/src/components/custom/Workout/VelocityStrip.stories.tsx +129 -66
  144. package/src/components/custom/Workout/VelocityStrip.test.tsx +376 -141
  145. package/src/components/custom/Workout/VelocityStrip.tsx +508 -120
  146. package/src/components/custom/Workout/VolumeLandmarkBar.stories.tsx +73 -0
  147. package/src/components/custom/Workout/VolumeLandmarkBar.test.tsx +119 -0
  148. package/src/components/custom/Workout/VolumeLandmarkBar.tsx +154 -0
  149. package/src/components/custom/Workout/WeekRow.stories.tsx +1 -1
  150. package/src/components/custom/Workout/WeightBadge.stories.tsx +1 -1
  151. package/src/components/custom/Workout/WorkoutCard.stories.tsx +2 -6
  152. package/src/components/custom/Workout/WorkoutCard.test.tsx +13 -46
  153. package/src/components/custom/Workout/WorkoutPill.stories.tsx +1 -1
  154. package/src/components/custom/Workout/ZoneTrack.stories.tsx +140 -0
  155. package/src/components/custom/Workout/ZoneTrack.test.tsx +200 -0
  156. package/src/components/custom/Workout/ZoneTrack.tsx +315 -0
  157. package/src/components/custom/Workout/index.ts +55 -2
  158. package/src/components/custom/Workout/metricText.stories.tsx +69 -0
  159. package/src/components/custom/Workout/metricText.tsx +34 -0
  160. package/src/components/custom/Workout/paceTone.test.ts +29 -0
  161. package/src/components/custom/Workout/paceTone.ts +27 -0
  162. package/src/components/custom/Workout/setHeadingKit.tsx +177 -0
  163. package/src/components/custom/index.ts +1 -0
  164. package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
  165. package/src/components/icons/icons.stories.tsx +67 -2
  166. package/src/components/icons/icons.test.tsx +32 -2
  167. package/src/components/icons/icons.tsx +108 -0
  168. package/src/components/shell/BrandLockup.stories.tsx +3 -3
  169. package/src/components/shell/DashboardShell.stories.tsx +65 -0
  170. package/src/components/shell/DashboardShell.test.tsx +42 -0
  171. package/src/components/shell/DashboardShell.tsx +74 -0
  172. package/src/components/shell/DeviceIndicator.stories.tsx +2 -2
  173. package/src/components/shell/DeviceMenu.stories.tsx +5 -5
  174. package/src/components/shell/DeviceRow.stories.tsx +3 -3
  175. package/src/components/shell/NavItem.stories.tsx +52 -0
  176. package/src/components/shell/NavItem.test.tsx +35 -0
  177. package/src/components/shell/NavItem.tsx +75 -0
  178. package/src/components/shell/README.md +30 -0
  179. package/src/components/shell/SessionRailSpecimen.stories.tsx +753 -0
  180. package/src/components/shell/SessionStatePill.stories.tsx +3 -3
  181. package/src/components/shell/SessionStatePill.tsx +1 -1
  182. package/src/components/shell/SideNav.stories.tsx +81 -0
  183. package/src/components/shell/SideNav.test.tsx +51 -0
  184. package/src/components/shell/SideNav.tsx +74 -0
  185. package/src/components/shell/TopBar.stories.tsx +5 -5
  186. package/src/components/shell/index.ts +5 -0
  187. package/src/components/ui/alert/Alert.stories.tsx +1 -1
  188. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
  189. package/src/components/ui/avatar/Avatar.stories.tsx +1 -1
  190. package/src/components/ui/badge/Badge.stories.tsx +1 -1
  191. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  192. package/src/components/ui/button/Button.stories.tsx +1 -1
  193. package/src/components/ui/card/Card.stories.tsx +1 -1
  194. package/src/components/ui/checkbox/Checkbox.stories.tsx +1 -1
  195. package/src/components/ui/chip/Chip.stories.tsx +1 -1
  196. package/src/components/ui/collapse/Collapse.stories.tsx +1 -1
  197. package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
  198. package/src/components/ui/divider/Divider.stories.tsx +1 -1
  199. package/src/components/ui/drawer/Drawer.stories.tsx +1 -1
  200. package/src/components/ui/form-field/FormField.stories.tsx +1 -1
  201. package/src/components/ui/help-tip/HelpTip.stories.tsx +1 -1
  202. package/src/components/ui/icon-box/IconBox.stories.tsx +1 -1
  203. package/src/components/ui/index.ts +1 -0
  204. package/src/components/ui/indicator/Indicator.stories.tsx +2 -2
  205. package/src/components/ui/indicator/Indicator.test.tsx +1 -0
  206. package/src/components/ui/indicator/Indicator.tsx +6 -1
  207. package/src/components/ui/input/Input.stories.tsx +1 -1
  208. package/src/components/ui/link/Link.stories.tsx +1 -1
  209. package/src/components/ui/list-item/ListItem.stories.tsx +1 -1
  210. package/src/components/ui/menu/Menu.stories.tsx +1 -1
  211. package/src/components/ui/modal/Modal.stories.tsx +1 -1
  212. package/src/components/ui/pill/Pill.stories.tsx +1 -1
  213. package/src/components/ui/popover/Popover.stories.tsx +1 -1
  214. package/src/components/ui/progress/Progress.stories.tsx +1 -1
  215. package/src/components/ui/radio/Radio.stories.tsx +1 -1
  216. package/src/components/ui/section/Section.stories.tsx +1 -1
  217. package/src/components/ui/select/Select.stories.tsx +1 -1
  218. package/src/components/ui/skeleton/Skeleton.stories.tsx +1 -1
  219. package/src/components/ui/spinner/Spinner.stories.tsx +1 -1
  220. package/src/components/ui/stack/Stack.stories.tsx +1 -1
  221. package/src/components/ui/surface/Surface.stories.tsx +1 -1
  222. package/src/components/ui/switch/Switch.stories.tsx +1 -1
  223. package/src/components/ui/tabs/Tabs.stories.tsx +1 -1
  224. package/src/components/ui/tile/Tile.stories.tsx +79 -0
  225. package/src/components/ui/tile/Tile.test.tsx +48 -0
  226. package/src/components/ui/tile/Tile.tsx +64 -0
  227. package/src/components/ui/tile/index.ts +2 -0
  228. package/src/components/ui/toast/Toast.stories.tsx +1 -1
  229. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
  230. package/src/components/ui/tooltip/Tooltip.stories.tsx +1 -1
  231. package/src/examples/react-hook-form/ReactHookForm.stories.tsx +1 -1
  232. package/src/hooks/index.ts +2 -0
  233. package/src/hooks/useTimer.test.ts +150 -0
  234. package/src/hooks/useTimer.ts +77 -0
  235. package/src/index.ts +3 -0
  236. package/src/pages.ts +1 -1
  237. package/src/stories/PresetShowcase.stories.tsx +1 -1
  238. package/src/stories/ThemePresets.stories.tsx +1 -1
  239. package/src/theme/ColorSystem.stories.tsx +1 -1
  240. package/src/theme/Colors.stories.tsx +5 -8
  241. package/src/theme/config.ts +4 -0
  242. package/src/theme/elevation.stories.tsx +1 -1
  243. package/src/theme/global.css +5 -1
  244. package/src/theme/shadows.stories.tsx +1 -1
  245. package/src/theme/tokens/primitives.ts +11 -107
  246. package/src/theme/tokens/semantic.ts +8 -0
  247. package/src/theme/workout-tokens.ts +11 -7
  248. package/tailwind.config.js +5 -0
@@ -0,0 +1,870 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+ import { getSemanticColors } from '../theme/tokens/semantic'
4
+ import graph from './arch-graph.json'
5
+
6
+ /**
7
+ * `Docs/Architecture` — a deterministic, regenerable map of the component library.
8
+ * Generated by `scripts/arch-graph.mjs` (codewatch symbol graph + a light import-parse
9
+ * of the consuming apps) → `arch-graph.json`. Two first-class sections: the internal
10
+ * dependency diagram (tiered by composition depth, click a node to pin it and inspect /
11
+ * render it) and the health audit (dead code, primitive-substitution leaks, extraction
12
+ * candidates). Regenerate with `node scripts/arch-graph.mjs --reindex`.
13
+ */
14
+
15
+ const C = getSemanticColors('dark')
16
+
17
+ type Comp = (typeof graph.components)[number]
18
+ type Tier = 'atom' | 'molecule' | 'organism' | 'page'
19
+ const TIERS: Tier[] = ['atom', 'molecule', 'organism', 'page']
20
+ const TIER_LABEL: Record<Tier, string> = {
21
+ atom: 'Atoms',
22
+ molecule: 'Molecules',
23
+ organism: 'Organisms',
24
+ page: 'Pages',
25
+ }
26
+
27
+ const VERDICT: Record<string, { label: string; color: string }> = {
28
+ 'keep-core': { label: 'Core (lib + app)', color: C['status-success'] },
29
+ 'keep-internal': { label: 'Internal only', color: C['status-info'] },
30
+ 'keep-app': { label: 'App-facing leaf', color: C['brand-secondary'] },
31
+ 'keep-page': { label: 'Shipped page', color: C['status-success'] },
32
+ 'demo-only': { label: 'Demo page (unshipped)', color: C['status-warning'] },
33
+ remove: { label: 'Throwaway → remove', color: C['text-tertiary'] },
34
+ dead: { label: 'Dead everywhere', color: C['status-error-vivid'] },
35
+ }
36
+ const verdictColor = (v: string) => VERDICT[v]?.color ?? C['text-tertiary']
37
+ const byName: Record<string, Comp> = Object.fromEntries(graph.components.map((c) => [c.name, c]))
38
+
39
+ // reverse edges: who composes each component
40
+ const usedByMap: Record<string, string[]> = {}
41
+ for (const [a, b] of graph.edges) (usedByMap[b] ||= []).push(a)
42
+
43
+ // ---- storybook story index (component file -> its default story id) --------
44
+ function useStoryIndex() {
45
+ const [map, setMap] = useState<Record<string, string>>({})
46
+ useEffect(() => {
47
+ let live = true
48
+ fetch('index.json')
49
+ .then((r) => r.json())
50
+ .then(
51
+ (idx: {
52
+ entries?: Record<
53
+ string,
54
+ { type?: string; id?: string; importPath?: string; componentPath?: string }
55
+ >
56
+ }) => {
57
+ if (!live) return
58
+ const m: Record<string, string> = {}
59
+ for (const e of Object.values(idx.entries ?? {})) {
60
+ if (e.type !== 'story' || !e.id) continue
61
+ const cp = (e.componentPath ?? '')
62
+ .split('/')
63
+ .pop()
64
+ ?.replace(/\.(t|j)sx?$/, '')
65
+ const ip = (e.importPath ?? '')
66
+ .split('/')
67
+ .pop()
68
+ ?.replace(/\.stories\.(t|j)sx?$/, '')
69
+ const name = cp || ip
70
+ if (name && !m[name]) m[name] = e.id // first story per component wins
71
+ }
72
+ setMap(m)
73
+ }
74
+ )
75
+ .catch(() => {})
76
+ return () => {
77
+ live = false
78
+ }
79
+ }, [])
80
+ return map
81
+ }
82
+
83
+ // ---- layout ---------------------------------------------------------------
84
+ const COL_W = 250
85
+ const NODE_H = 24
86
+ const NODE_GAP = 6
87
+ const ROW = NODE_H + NODE_GAP
88
+ const PAD_TOP = 40
89
+ const NODE_W = 196
90
+
91
+ function useLayout() {
92
+ return useMemo(() => {
93
+ const cols: Record<Tier, Comp[]> = { atom: [], molecule: [], organism: [], page: [] }
94
+ for (const c of graph.components) cols[c.tier as Tier].push(c)
95
+ for (const t of TIERS) cols[t].sort((a, b) => b.leak.score - a.leak.score)
96
+ const pos: Record<string, { x: number; y: number }> = {}
97
+ TIERS.forEach((t, ci) => {
98
+ cols[t].forEach((c, ri) => {
99
+ pos[c.name] = { x: ci * COL_W + 20, y: PAD_TOP + ri * ROW }
100
+ })
101
+ })
102
+ const height = PAD_TOP + Math.max(...TIERS.map((t) => cols[t].length)) * ROW + 20
103
+ return { cols, pos, height, width: TIERS.length * COL_W + 20 }
104
+ }, [])
105
+ }
106
+
107
+ function neighborsOf(active: string | null) {
108
+ if (!active) return null
109
+ const set = new Set<string>([active])
110
+ for (const [a, b] of graph.edges) {
111
+ if (a === active) set.add(b)
112
+ if (b === active) set.add(a)
113
+ }
114
+ return set
115
+ }
116
+
117
+ // ---- dependency diagram ---------------------------------------------------
118
+ function Diagram({
119
+ selected,
120
+ onSelect,
121
+ onOpenRender,
122
+ }: {
123
+ selected: string | null
124
+ onSelect: (name: string | null) => void
125
+ onOpenRender: (name: string) => void
126
+ }) {
127
+ const { cols, pos, height, width } = useLayout()
128
+ const [hover, setHover] = useState<string | null>(null)
129
+ const active = selected ?? hover
130
+ const neighbors = useMemo(() => neighborsOf(active), [active])
131
+
132
+ const edgePath = (a: string, b: string) => {
133
+ const s = pos[a]
134
+ const t = pos[b]
135
+ if (!s || !t) return null
136
+ const x1 = s.x
137
+ const y1 = s.y + NODE_H / 2
138
+ const x2 = t.x + NODE_W
139
+ const y2 = t.y + NODE_H / 2
140
+ const mx = (x1 + x2) / 2
141
+ return `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`
142
+ }
143
+
144
+ return (
145
+ <div
146
+ onClick={() => onSelect(null)}
147
+ style={{
148
+ overflow: 'auto',
149
+ border: `1px solid ${C['border-subtle']}`,
150
+ borderRadius: 8,
151
+ background: C['surface-base'],
152
+ }}
153
+ >
154
+ <svg width={width} height={height} style={{ display: 'block', minWidth: '100%' }}>
155
+ {TIERS.map((t, ci) => (
156
+ <text
157
+ key={t}
158
+ x={ci * COL_W + 20}
159
+ y={22}
160
+ fill={C['text-secondary']}
161
+ fontSize={12}
162
+ fontWeight={700}
163
+ fontFamily="Inter, sans-serif"
164
+ >
165
+ {TIER_LABEL[t]} · {cols[t].length}
166
+ </text>
167
+ ))}
168
+ {graph.edges.map(([a, b], i) => {
169
+ const d = edgePath(a, b)
170
+ if (!d) return null
171
+ const on = neighbors ? neighbors.has(a) && neighbors.has(b) : false
172
+ const dim = neighbors && !on
173
+ return (
174
+ <path
175
+ key={i}
176
+ d={d}
177
+ fill="none"
178
+ stroke={on ? C['brand-primary'] : C['border-prominent']}
179
+ strokeWidth={on ? 1.8 : 1}
180
+ opacity={dim ? 0.04 : on ? 0.95 : 0.45}
181
+ />
182
+ )
183
+ })}
184
+ {graph.components.map((c) => {
185
+ const p = pos[c.name]
186
+ const isSel = selected === c.name
187
+ const dim = neighbors ? !neighbors.has(c.name) : false
188
+ return (
189
+ <g
190
+ key={c.name}
191
+ transform={`translate(${p.x},${p.y})`}
192
+ opacity={dim ? 0.25 : 1}
193
+ style={{ cursor: 'pointer' }}
194
+ onMouseEnter={() => setHover(c.name)}
195
+ onMouseLeave={() => setHover(null)}
196
+ onClick={(e) => {
197
+ e.stopPropagation()
198
+ onSelect(isSel ? null : c.name)
199
+ }}
200
+ >
201
+ <rect
202
+ width={NODE_W}
203
+ height={NODE_H}
204
+ rx={5}
205
+ fill={isSel ? C['interactive-selected'] : C['surface-raised']}
206
+ stroke={isSel ? C['brand-primary'] : verdictColor(c.verdict)}
207
+ strokeWidth={isSel ? 2 : c.verdict === 'dead' ? 1.6 : 1}
208
+ />
209
+ <rect width={4} height={NODE_H} rx={2} fill={verdictColor(c.verdict)} />
210
+ {c.deadByAssociation && (
211
+ <rect
212
+ width={NODE_W}
213
+ height={NODE_H}
214
+ rx={5}
215
+ fill="none"
216
+ stroke={C['status-warning']}
217
+ strokeWidth={1}
218
+ strokeDasharray="3 3"
219
+ />
220
+ )}
221
+ {c.audited === 'audited' && (
222
+ <circle cx={NODE_W - 30} cy={NODE_H / 2} r={3} fill={C['status-success']} />
223
+ )}
224
+ <text
225
+ x={11}
226
+ y={NODE_H / 2 + 4}
227
+ fill={C['text-primary']}
228
+ fontSize={11.5}
229
+ fontFamily="Inter, sans-serif"
230
+ >
231
+ {c.name}
232
+ </text>
233
+ <text
234
+ x={NODE_W - 26}
235
+ y={NODE_H / 2 + 4}
236
+ textAnchor="end"
237
+ fill={C['text-tertiary']}
238
+ fontSize={9.5}
239
+ fontFamily="monospace"
240
+ >
241
+ {c.libDependents}·{c.xproj.total}
242
+ </text>
243
+ <g
244
+ onClick={(e) => {
245
+ e.stopPropagation()
246
+ onOpenRender(c.name)
247
+ }}
248
+ style={{ cursor: 'pointer' }}
249
+ >
250
+ <rect
251
+ x={NODE_W - 21}
252
+ y={4}
253
+ width={17}
254
+ height={16}
255
+ rx={3}
256
+ fill={C['surface-elevated']}
257
+ stroke={C['border-default']}
258
+ />
259
+ <text
260
+ x={NODE_W - 12.5}
261
+ y={15.5}
262
+ textAnchor="middle"
263
+ fill={C['text-secondary']}
264
+ fontSize={11}
265
+ fontFamily="Inter, sans-serif"
266
+ >
267
+
268
+ </text>
269
+ </g>
270
+ </g>
271
+ )
272
+ })}
273
+ </svg>
274
+ </div>
275
+ )
276
+ }
277
+
278
+ // ---- detail panel (pinned selection) --------------------------------------
279
+ function DepLine({ label, items }: { label: string; items: string[] }) {
280
+ return (
281
+ <div style={{ display: 'flex', gap: 8, alignItems: 'baseline', flexWrap: 'wrap' }}>
282
+ <span style={{ color: C['text-tertiary'], fontSize: 11, minWidth: 74 }}>{label}</span>
283
+ {items.length ? (
284
+ items.map((n) => (
285
+ <span
286
+ key={n}
287
+ style={{
288
+ fontSize: 12,
289
+ color: C['text-secondary'],
290
+ background: C['surface-raised'],
291
+ border: `1px solid ${C['border-subtle']}`,
292
+ borderRadius: 5,
293
+ padding: '1px 7px',
294
+ }}
295
+ >
296
+ {n}
297
+ </span>
298
+ ))
299
+ ) : (
300
+ <span style={{ fontSize: 12, color: C['text-tertiary'] }}>—</span>
301
+ )}
302
+ </div>
303
+ )
304
+ }
305
+
306
+ function DetailPanel({
307
+ comp,
308
+ onOpenRender,
309
+ onClose,
310
+ }: {
311
+ comp: Comp
312
+ onOpenRender: (name: string) => void
313
+ onClose: () => void
314
+ }) {
315
+ const usedBy = usedByMap[comp.name] ?? []
316
+ const v = VERDICT[comp.verdict]
317
+ return (
318
+ <div
319
+ style={{
320
+ margin: '10px 0',
321
+ padding: 14,
322
+ borderRadius: 8,
323
+ background: C['surface-elevated'],
324
+ border: `1px solid ${C['border-default']}`,
325
+ display: 'flex',
326
+ flexDirection: 'column',
327
+ gap: 8,
328
+ }}
329
+ >
330
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
331
+ <span
332
+ style={{
333
+ width: 10,
334
+ height: 10,
335
+ borderRadius: 999,
336
+ background: verdictColor(comp.verdict),
337
+ }}
338
+ />
339
+ <span style={{ fontSize: 16, fontWeight: 700, color: C['text-primary'] }}>{comp.name}</span>
340
+ <span style={{ fontSize: 12, color: C['text-secondary'] }}>
341
+ {comp.tier} · {v?.label ?? comp.verdict}
342
+ </span>
343
+ <span style={{ fontFamily: 'monospace', fontSize: 12, color: C['text-tertiary'] }}>
344
+ lib {comp.libDependents} · story {comp.storyRefs} · app {comp.xproj.total}
345
+ </span>
346
+ {comp.standard && <Chip color={C['brand-secondary']}>standard</Chip>}
347
+ <Chip color={comp.audited === 'audited' ? C['status-success'] : C['text-tertiary']}>
348
+ {comp.audited === 'audited'
349
+ ? 'audited'
350
+ : comp.audited === 'reviewing'
351
+ ? 'reviewing'
352
+ : 'unaudited'}
353
+ </Chip>
354
+ {comp.deadByAssociation && <Chip color={C['status-warning']}>⚠ only dead consumers</Chip>}
355
+ <div style={{ flex: 1 }} />
356
+ <button
357
+ onClick={() => onOpenRender(comp.name)}
358
+ style={btn(C['brand-primary'], C['on-brand-primary'])}
359
+ >
360
+ View rendering ↗
361
+ </button>
362
+ <button onClick={onClose} style={btn(C['surface-raised'], C['text-secondary'])}>
363
+ ✕ unpin
364
+ </button>
365
+ </div>
366
+ <DepLine label="composes ↓" items={comp.dependsOn} />
367
+ <DepLine label="used-by ↑" items={usedBy} />
368
+ </div>
369
+ )
370
+ }
371
+ const btn = (bg: string, fg: string): React.CSSProperties => ({
372
+ background: bg,
373
+ color: fg,
374
+ border: 'none',
375
+ borderRadius: 6,
376
+ padding: '5px 11px',
377
+ fontSize: 12,
378
+ fontWeight: 600,
379
+ cursor: 'pointer',
380
+ fontFamily: 'Inter, sans-serif',
381
+ })
382
+
383
+ // ---- render modal (embeds the component's Storybook story) -----------------
384
+ function RenderModal({
385
+ name,
386
+ storyId,
387
+ onClose,
388
+ }: {
389
+ name: string
390
+ storyId?: string
391
+ onClose: () => void
392
+ }) {
393
+ return (
394
+ <div
395
+ onClick={onClose}
396
+ style={{
397
+ position: 'fixed',
398
+ inset: 0,
399
+ background: 'rgba(0,0,0,0.66)',
400
+ display: 'flex',
401
+ alignItems: 'center',
402
+ justifyContent: 'center',
403
+ zIndex: 1000,
404
+ }}
405
+ >
406
+ <div
407
+ onClick={(e) => e.stopPropagation()}
408
+ style={{
409
+ width: 'min(940px, 92vw)',
410
+ height: 'min(620px, 88vh)',
411
+ background: C['surface-base'],
412
+ border: `1px solid ${C['border-default']}`,
413
+ borderRadius: 10,
414
+ display: 'flex',
415
+ flexDirection: 'column',
416
+ overflow: 'hidden',
417
+ }}
418
+ >
419
+ <div
420
+ style={{
421
+ display: 'flex',
422
+ alignItems: 'center',
423
+ gap: 10,
424
+ padding: '10px 14px',
425
+ borderBottom: `1px solid ${C['border-subtle']}`,
426
+ }}
427
+ >
428
+ <span style={{ fontSize: 15, fontWeight: 700, color: C['text-primary'] }}>{name}</span>
429
+ <span style={{ fontSize: 12, color: C['text-tertiary'] }}>live Storybook render</span>
430
+ <div style={{ flex: 1 }} />
431
+ {storyId && (
432
+ <a
433
+ href={`?path=/story/${storyId}`}
434
+ target="_blank"
435
+ rel="noreferrer"
436
+ style={{ ...btn(C['surface-raised'], C['text-secondary']), textDecoration: 'none' }}
437
+ >
438
+ open full story ↗
439
+ </a>
440
+ )}
441
+ <button onClick={onClose} style={btn(C['surface-raised'], C['text-secondary'])}>
442
+ ✕ close
443
+ </button>
444
+ </div>
445
+ {storyId ? (
446
+ <iframe
447
+ title={name}
448
+ src={`iframe.html?id=${storyId}&viewMode=story`}
449
+ style={{ flex: 1, width: '100%', border: 'none', background: C['background-base'] }}
450
+ />
451
+ ) : (
452
+ <div
453
+ style={{
454
+ flex: 1,
455
+ display: 'flex',
456
+ alignItems: 'center',
457
+ justifyContent: 'center',
458
+ color: C['text-tertiary'],
459
+ fontSize: 14,
460
+ textAlign: 'center',
461
+ padding: 24,
462
+ }}
463
+ >
464
+ No Storybook story found for{' '}
465
+ <span style={{ color: C['text-secondary'], margin: '0 5px' }}>{name}</span>{' '}
466
+ {byName[name]?.verdict === 'remove'
467
+ ? '(throwaway — slated for removal).'
468
+ : '(this component has no story to render).'}
469
+ </div>
470
+ )}
471
+ </div>
472
+ </div>
473
+ )
474
+ }
475
+
476
+ // ---- health tables --------------------------------------------------------
477
+ function Chip({ color, children }: { color: string; children: React.ReactNode }) {
478
+ return (
479
+ <span
480
+ style={{
481
+ display: 'inline-flex',
482
+ alignItems: 'center',
483
+ gap: 5,
484
+ padding: '3px 9px',
485
+ borderRadius: 999,
486
+ background: C['surface-raised'],
487
+ border: `1px solid ${C['border-subtle']}`,
488
+ fontSize: 12,
489
+ color: C['text-secondary'],
490
+ }}
491
+ >
492
+ <span style={{ width: 8, height: 8, borderRadius: 999, background: color }} />
493
+ {children}
494
+ </span>
495
+ )
496
+ }
497
+
498
+ function Th({ children, right }: { children: React.ReactNode; right?: boolean }) {
499
+ return (
500
+ <th
501
+ style={{
502
+ textAlign: right ? 'right' : 'left',
503
+ padding: '6px 12px',
504
+ color: C['text-tertiary'],
505
+ fontSize: 11,
506
+ fontWeight: 600,
507
+ textTransform: 'uppercase',
508
+ letterSpacing: 0.5,
509
+ borderBottom: `1px solid ${C['border-subtle']}`,
510
+ }}
511
+ >
512
+ {children}
513
+ </th>
514
+ )
515
+ }
516
+ function Td({
517
+ children,
518
+ right,
519
+ mono,
520
+ }: {
521
+ children: React.ReactNode
522
+ right?: boolean
523
+ mono?: boolean
524
+ }) {
525
+ return (
526
+ <td
527
+ style={{
528
+ textAlign: right ? 'right' : 'left',
529
+ padding: '6px 12px',
530
+ color: C['text-primary'],
531
+ fontSize: 13,
532
+ fontFamily: mono ? 'monospace' : 'inherit',
533
+ borderBottom: `1px solid ${C['border-subtle']}`,
534
+ }}
535
+ >
536
+ {children}
537
+ </td>
538
+ )
539
+ }
540
+
541
+ function DeadTable() {
542
+ const dead = graph.components.filter(
543
+ (c) =>
544
+ c.verdict === 'dead' ||
545
+ c.verdict === 'remove' ||
546
+ c.verdict === 'demo-only' ||
547
+ c.deadByAssociation
548
+ )
549
+ const label = (c: Comp) =>
550
+ c.deadByAssociation && c.verdict.startsWith('keep')
551
+ ? { text: 'Only dead consumers', color: C['status-warning'] }
552
+ : { text: VERDICT[c.verdict]?.label ?? c.verdict, color: verdictColor(c.verdict) }
553
+ return (
554
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
555
+ <thead>
556
+ <tr>
557
+ <Th>Component</Th>
558
+ <Th>Tier</Th>
559
+ <Th>Status</Th>
560
+ <Th right>lib</Th>
561
+ <Th right>story</Th>
562
+ <Th right>app</Th>
563
+ </tr>
564
+ </thead>
565
+ <tbody>
566
+ {dead.map((c) => {
567
+ const l = label(c)
568
+ return (
569
+ <tr key={c.name}>
570
+ <Td>{c.name}</Td>
571
+ <Td>{c.tier}</Td>
572
+ <Td>
573
+ <Chip color={l.color}>{l.text}</Chip>
574
+ </Td>
575
+ <Td right mono>
576
+ {c.libDependents}
577
+ </Td>
578
+ <Td right mono>
579
+ {c.storyRefs}
580
+ </Td>
581
+ <Td right mono>
582
+ {c.xproj.total}
583
+ </Td>
584
+ </tr>
585
+ )
586
+ })}
587
+ </tbody>
588
+ </table>
589
+ )
590
+ }
591
+
592
+ function StandardCoverageTable() {
593
+ const cov = graph.summary.standardCoverage
594
+ const cats = [...new Set(cov.map((s) => s.category))]
595
+ const statusChip: Record<string, { label: string; color: string }> = {
596
+ present: { label: 'present', color: C['status-success'] },
597
+ 'present-dead': { label: 'present but dead', color: C['status-warning'] },
598
+ missing: { label: 'missing', color: C['status-error-vivid'] },
599
+ }
600
+ return (
601
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
602
+ <thead>
603
+ <tr>
604
+ <Th>Category</Th>
605
+ <Th>Standard component</Th>
606
+ <Th>Status</Th>
607
+ <Th>Our component</Th>
608
+ </tr>
609
+ </thead>
610
+ <tbody>
611
+ {cats.flatMap((cat) =>
612
+ cov
613
+ .filter((s) => s.category === cat)
614
+ .map((s) => {
615
+ const sc = statusChip[s.status]
616
+ return (
617
+ <tr key={cat + s.name}>
618
+ <Td>{s.category}</Td>
619
+ <Td>{s.name}</Td>
620
+ <Td>
621
+ <Chip color={sc.color}>{sc.label}</Chip>
622
+ </Td>
623
+ <Td mono>{s.have.length ? s.have.join(', ') : '—'}</Td>
624
+ </tr>
625
+ )
626
+ })
627
+ )}
628
+ </tbody>
629
+ </table>
630
+ )
631
+ }
632
+
633
+ function SubstitutionTable() {
634
+ const sub = graph.summary.substitution
635
+ const prims = Object.keys(sub['library(titan)'] ?? {})
636
+ const scopes = Object.keys(sub)
637
+ return (
638
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
639
+ <thead>
640
+ <tr>
641
+ <Th>Scope</Th>
642
+ {prims.map((p) => (
643
+ <Th key={p} right>
644
+ {p}
645
+ </Th>
646
+ ))}
647
+ </tr>
648
+ </thead>
649
+ <tbody>
650
+ {scopes.map((s) => (
651
+ <tr key={s}>
652
+ <Td>{s}</Td>
653
+ {prims.map((p) => {
654
+ const val = (sub as Record<string, Record<string, number>>)[s][p] ?? 0
655
+ return (
656
+ <Td key={p} right mono>
657
+ {val > 0 ? val : '·'}
658
+ </Td>
659
+ )
660
+ })}
661
+ </tr>
662
+ ))}
663
+ </tbody>
664
+ </table>
665
+ )
666
+ }
667
+
668
+ function ExtractionTable() {
669
+ return (
670
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
671
+ <thead>
672
+ <tr>
673
+ <Th>Component</Th>
674
+ <Th>Tier</Th>
675
+ <Th right>{'<View>'}</Th>
676
+ <Th right>{'<Text>'}</Th>
677
+ <Th right>{'style={{'}</Th>
678
+ <Th right>rawHex</Th>
679
+ <Th right>loc</Th>
680
+ <Th right>score</Th>
681
+ </tr>
682
+ </thead>
683
+ <tbody>
684
+ {graph.summary.extractionTop.map((r) => (
685
+ <tr key={r.name}>
686
+ <Td>{r.name}</Td>
687
+ <Td>{r.tier}</Td>
688
+ <Td right mono>
689
+ {r.rawView}
690
+ </Td>
691
+ <Td right mono>
692
+ {r.rawText}
693
+ </Td>
694
+ <Td right mono>
695
+ {r.inlineStyle}
696
+ </Td>
697
+ <Td right mono>
698
+ {r.rawHex > 0 ? r.rawHex : '·'}
699
+ </Td>
700
+ <Td right mono>
701
+ {r.loc}
702
+ </Td>
703
+ <Td right mono>
704
+ {r.score}
705
+ </Td>
706
+ </tr>
707
+ ))}
708
+ </tbody>
709
+ </table>
710
+ )
711
+ }
712
+
713
+ function Section({
714
+ title,
715
+ subtitle,
716
+ children,
717
+ }: {
718
+ title: string
719
+ subtitle: string
720
+ children: React.ReactNode
721
+ }) {
722
+ return (
723
+ <section style={{ marginTop: 28 }}>
724
+ <h2
725
+ style={{
726
+ color: C['text-primary'],
727
+ fontSize: 18,
728
+ fontWeight: 700,
729
+ margin: 0,
730
+ fontFamily: 'Inter, sans-serif',
731
+ }}
732
+ >
733
+ {title}
734
+ </h2>
735
+ <p
736
+ style={{
737
+ color: C['text-tertiary'],
738
+ fontSize: 13,
739
+ margin: '4px 0 12px',
740
+ fontFamily: 'Inter, sans-serif',
741
+ }}
742
+ >
743
+ {subtitle}
744
+ </p>
745
+ {children}
746
+ </section>
747
+ )
748
+ }
749
+
750
+ function ArchitecturePage() {
751
+ const [selected, setSelected] = useState<string | null>(null)
752
+ const [modalFor, setModalFor] = useState<string | null>(null)
753
+ const storyIndex = useStoryIndex()
754
+ const dead = graph.components.filter((c) => c.verdict === 'dead').length
755
+ const libLeak = graph.summary.substitution['library(titan)']
756
+ const leakText = (libLeak?.Text ?? 0) + (libLeak?.View ?? 0)
757
+ const standardDead = graph.summary.standardCoverage.filter(
758
+ (s) => s.status === 'present-dead'
759
+ ).length
760
+ const standardMissing = graph.summary.standardCoverage.filter(
761
+ (s) => s.status === 'missing'
762
+ ).length
763
+ const sel = selected ? byName[selected] : null
764
+
765
+ return (
766
+ <div
767
+ style={{
768
+ padding: 28,
769
+ background: C['background-base'],
770
+ minHeight: '100vh',
771
+ fontFamily: 'Inter, sans-serif',
772
+ }}
773
+ >
774
+ <h1 style={{ color: C['text-primary'], fontSize: 26, fontWeight: 800, margin: 0 }}>
775
+ Component Architecture
776
+ </h1>
777
+ <p style={{ color: C['text-secondary'], fontSize: 14, margin: '6px 0 16px', maxWidth: 760 }}>
778
+ Deterministic dependency + health map from the codewatch symbol graph and the consuming
779
+ apps. Node label shows <span style={{ fontFamily: 'monospace' }}>lib·app</span> usage.
780
+ Regenerate:{' '}
781
+ <span style={{ fontFamily: 'monospace', color: C['text-primary'] }}>
782
+ node scripts/arch-graph.mjs --reindex
783
+ </span>
784
+ .
785
+ </p>
786
+ <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 8 }}>
787
+ <Chip color={C['text-secondary']}>{graph.components.length} components</Chip>
788
+ <Chip color={C['status-error-vivid']}>{dead} dead everywhere</Chip>
789
+ <Chip color={C['status-warning']}>
790
+ {graph.summary.deadByAssociation.length} dead-by-association
791
+ </Chip>
792
+ <Chip color={C['status-info']}>
793
+ {graph.summary.auditedCount}/{graph.components.length} audited
794
+ </Chip>
795
+ <Chip color={C['brand-secondary']}>
796
+ {standardDead} standard dead · {standardMissing} missing
797
+ </Chip>
798
+ <Chip color={C['status-warning']}>{leakText} raw Text/View leaks in-library</Chip>
799
+ {graph.summary.consumers.map((c) => (
800
+ <Chip key={c.name} color={c.present ? C['status-success'] : C['text-tertiary']}>
801
+ {c.name}
802
+ {c.present ? '' : ' (absent)'}
803
+ </Chip>
804
+ ))}
805
+ </div>
806
+
807
+ <Section
808
+ title="Dependency diagram"
809
+ subtitle="Columns by composition depth (leaves = atoms). Hover to trace, click a node to pin it, or press ↗ on any node to render the component live. Border color = verdict; dashed outline = only dead consumers."
810
+ >
811
+ <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 10 }}>
812
+ {Object.entries(VERDICT).map(([k, v]) => (
813
+ <Chip key={k} color={v.color}>
814
+ {v.label}
815
+ </Chip>
816
+ ))}
817
+ </div>
818
+ {sel && (
819
+ <DetailPanel comp={sel} onOpenRender={setModalFor} onClose={() => setSelected(null)} />
820
+ )}
821
+ <Diagram selected={selected} onSelect={setSelected} onOpenRender={setModalFor} />
822
+ </Section>
823
+
824
+ <Section
825
+ title="Standard component coverage"
826
+ subtitle="The canonical set a design system is expected to provide (curated in annotations.json), cross-referenced against what we ship. 'present but dead' = we built it but nothing uses it."
827
+ >
828
+ <StandardCoverageTable />
829
+ </Section>
830
+
831
+ <Section
832
+ title="Dead & unshipped"
833
+ subtitle="Zero production dependents in the library AND zero imports across all apps (dead everywhere), or kept alive only by other dead components (only dead consumers). Dashed warning outline in the diagram."
834
+ >
835
+ <DeadTable />
836
+ </Section>
837
+
838
+ <Section
839
+ title="Primitive-substitution leaks"
840
+ subtitle="Files importing a raw RN primitive that a titan atom already covers (Text→Typography, View→Surface/Stack, Pressable→Button…). The library bypassing its own primitives is the root cause of drift."
841
+ >
842
+ <SubstitutionTable />
843
+ </Section>
844
+
845
+ <Section
846
+ title="Extraction candidates"
847
+ subtitle="Highest inline-primitive density — repeated raw <View>/<Text>/style blocks and hardcoded hex that could be promoted into reusable primitives. rawHex flags token violations."
848
+ >
849
+ <ExtractionTable />
850
+ </Section>
851
+
852
+ {modalFor && (
853
+ <RenderModal
854
+ name={modalFor}
855
+ storyId={storyIndex[modalFor]}
856
+ onClose={() => setModalFor(null)}
857
+ />
858
+ )}
859
+ </div>
860
+ )
861
+ }
862
+
863
+ const meta: Meta = {
864
+ title: 'Docs/Architecture',
865
+ parameters: { layout: 'fullscreen' },
866
+ }
867
+ export default meta
868
+ type Story = StoryObj
869
+
870
+ export const Overview: Story = { render: () => <ArchitecturePage /> }