describe-me 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "describe-me",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Living component documentation generated from your Vitest tests: viewer and CLI",
5
5
  "keywords": [
6
6
  "vitest",
@@ -43,13 +43,13 @@
43
43
  "dependencies": {
44
44
  "rrweb-snapshot": "^2.1.6",
45
45
  "vite": "^8.3.0",
46
- "@describe-me/core": "^0.1.1"
46
+ "@describe-me/core": "^0.3.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^22"
50
50
  },
51
51
  "scripts": {
52
52
  "build": "rimraf dist-cli && tsc -p tsconfig.cli.json",
53
- "dev": "node dist-cli/main.js dev --data ../../examples/react-basic/.describe-me"
53
+ "dev": "node dist-cli/main.js dev --data ../../examples/react-browser/.describe-me"
54
54
  }
55
55
  }
@@ -0,0 +1,51 @@
1
+ const SKIPPED_TAGS = new Set(['SCRIPT', 'NOSCRIPT', 'STYLE', 'LINK', 'META', 'TITLE'])
2
+
3
+ /**
4
+ * Whether an element draws something itself. Layout wrappers (the test
5
+ * container, a full-width flex row) stretch across the page and would make
6
+ * every measurement fall back to the whole viewport.
7
+ */
8
+ function isVisual(element: Element, view: Window): boolean {
9
+ if (element.children.length === 0) {
10
+ return true
11
+ }
12
+
13
+ const style = view.getComputedStyle(element)
14
+ const hasBackground =
15
+ style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor !== 'transparent'
16
+
17
+ const hasBorder = parseFloat(style.borderTopWidth) > 0 || parseFloat(style.borderLeftWidth) > 0
18
+ const hasShadow = style.boxShadow !== 'none'
19
+
20
+ return hasBackground || hasBorder || hasShadow
21
+ }
22
+
23
+ /** The box around everything that actually paints in a snapshot, in the iframe's own pixels. */
24
+ export function contentBox(body: HTMLElement, view: Window): DOMRect | null {
25
+ let box: DOMRect | null = null
26
+
27
+ for (const element of Array.from(body.querySelectorAll('*'))) {
28
+ if (SKIPPED_TAGS.has(element.tagName) || !isVisual(element, view)) {
29
+ continue
30
+ }
31
+
32
+ const rect = element.getBoundingClientRect()
33
+ if (rect.width === 0 || rect.height === 0) {
34
+ continue
35
+ }
36
+
37
+ if (!box) {
38
+ box = DOMRect.fromRect(rect)
39
+ continue
40
+ }
41
+
42
+ const right = Math.max(box.right, rect.right)
43
+ const bottom = Math.max(box.bottom, rect.bottom)
44
+ box.x = Math.min(box.x, rect.x)
45
+ box.y = Math.min(box.y, rect.y)
46
+ box.width = right - box.x
47
+ box.height = bottom - box.y
48
+ }
49
+
50
+ return box
51
+ }
@@ -0,0 +1,20 @@
1
+ import { contentBox } from './content-box.js'
2
+
3
+ const BOTTOM_MARGIN = 20
4
+ const MIN_HEIGHT = 120
5
+
6
+ /**
7
+ * Shrink the replay frame to what the snapshot actually paints. A snapshot is
8
+ * a whole document, so at its natural size the frame is a tall empty sheet
9
+ * with the component alone in the top corner.
10
+ */
11
+ export function fitStage(iframe: HTMLIFrameElement): void {
12
+ const body = iframe.contentDocument?.body
13
+ const view = iframe.contentWindow
14
+ const box = body && view ? contentBox(body, view) : null
15
+ if (!box) {
16
+ return
17
+ }
18
+
19
+ iframe.style.height = `${Math.max(MIN_HEIGHT, Math.round(box.bottom + BOTTOM_MARGIN))}px`
20
+ }
package/src/header.ts CHANGED
@@ -1,31 +1,31 @@
1
1
  import { el } from './el.js'
2
2
  import { allTests, state } from './state.js'
3
3
 
4
+ function stat(count: number, label: string, tone?: 'pass' | 'fail'): HTMLElement {
5
+ return el('span', { class: tone ? `stat ${tone}` : 'stat' }, el('b', {}, String(count)), label)
6
+ }
7
+
4
8
  /** Wordmark, pass/fail counts and the time of the last run. */
5
9
  export function renderHeader(): HTMLElement {
6
10
  const manifest = state.manifest
7
11
  const tests = manifest ? allTests(manifest) : []
8
12
  const passed = tests.filter((test) => test.state === 'passed').length
9
13
  const failed = tests.filter((test) => test.state === 'failed').length
10
- const summary = el('span', { class: 'summary' })
11
- summary.append(
12
- el('b', {}, String(tests.length)),
13
- ' tests · ',
14
- el('b', {}, String(passed)),
15
- ' passed',
16
- )
17
14
 
15
+ const summary = el('span', { class: 'summary' }, stat(tests.length, 'tests'))
16
+ summary.append(stat(passed, 'passed', 'pass'))
18
17
  if (failed) {
19
- summary.append(' · ', el('b', { class: 'fail' }, String(failed)), ' failed')
18
+ summary.append(stat(failed, 'failed', 'fail'))
20
19
  }
21
20
 
22
- const wm = el('span', { class: 'wordmark' }, 'describe', el('span', { class: 'me' }, '-me'))
21
+ const wordmark = el('span', { class: 'wordmark' }, 'describe', el('span', { class: 'me' }, '-me'))
23
22
  const when = manifest ? new Date(manifest.generatedAt).toLocaleTimeString() : '…'
23
+
24
24
  return el(
25
25
  'header',
26
26
  { class: 'header' },
27
- wm,
27
+ wordmark,
28
28
  summary,
29
- el('span', { class: 'right live' }, `updated ${when}`),
29
+ el('span', { class: 'right live' }, 'updated ', el('span', { class: 'mono' }, when)),
30
30
  )
31
31
  }
package/src/inspector.ts CHANGED
@@ -48,7 +48,7 @@ export function renderInspector(): HTMLElement {
48
48
  'section',
49
49
  {},
50
50
  el('h3', {}, `frame ${state.frame + 1} / ${test.frames.length}`),
51
- el('div', {}, frame.label),
51
+ el('div', { class: 'mono' }, frame.label),
52
52
  el('div', { class: 'hint' }, `${frame.kind} · ${frame.at}ms into the test`),
53
53
  ),
54
54
  )
package/src/main-panel.ts CHANGED
@@ -59,7 +59,7 @@ export function renderMain(): HTMLElement {
59
59
  },
60
60
  },
61
61
  el('span', { class: 'k' }, timelineFrame.kind),
62
- el('span', {}, timelineFrame.label),
62
+ el('span', { class: 'label' }, timelineFrame.label),
63
63
  el('span', { class: 't' }, `${timelineFrame.at}ms`),
64
64
  ),
65
65
  )
package/src/sidebar.ts CHANGED
@@ -1,13 +1,42 @@
1
+ import type { ManifestModule, ManifestTest } from '@describe-me/core/types'
1
2
  import { el } from './el.js'
2
3
  import { select, selectSuite, state } from './state.js'
3
4
  import { suiteKey } from './suite-key.js'
4
5
  import { buildTree, type SuiteNode } from './tree.js'
5
6
 
6
- function scopeButton(label: string, key: string, kind: 'module' | 'suite'): HTMLElement {
7
+ /**
8
+ * The one describe every test of a module sits under, if there is one. Its
9
+ * overview lists exactly the tests the module's overview would, so the two
10
+ * rows collapse into a single header instead of leading to near-identical
11
+ * pages.
12
+ */
13
+ function soleSuite(root: SuiteNode): SuiteNode | null {
14
+ if (root.tests.length > 0 || root.suites.size !== 1) {
15
+ return null
16
+ }
17
+
18
+ return [...root.suites.values()][0] ?? null
19
+ }
20
+
21
+ /** The header of a module block: the describe it holds, over the file it came from. */
22
+ function moduleButton(name: string | null, file: string, key: string): HTMLElement {
7
23
  return el(
8
24
  'button',
9
25
  {
10
- class: `${kind}-button${key === state.suiteKey ? ' active' : ''}`,
26
+ class: `module-button${key === state.suiteKey ? ' active' : ''}`,
27
+ click: () => selectSuite(key),
28
+ title: `overview of ${name ? `${name} — ${file}` : file}`,
29
+ },
30
+ name ? el('span', { class: 'module-name' }, name) : null,
31
+ el('span', { class: 'module-path' }, file),
32
+ )
33
+ }
34
+
35
+ function suiteButton(label: string, key: string): HTMLElement {
36
+ return el(
37
+ 'button',
38
+ {
39
+ class: `suite-button${key === state.suiteKey ? ' active' : ''}`,
11
40
  click: () => selectSuite(key),
12
41
  title: `overview of ${label}`,
13
42
  },
@@ -15,47 +44,61 @@ function scopeButton(label: string, key: string, kind: 'module' | 'suite'): HTML
15
44
  )
16
45
  }
17
46
 
18
- function renderSuite(node: SuiteNode, moduleId: string, path: string[]): HTMLElement {
19
- const wrap = el('div', { class: 'suite' })
20
- if (node.name) {
21
- wrap.append(scopeButton(node.name, suiteKey(moduleId, path), 'suite'))
22
- }
47
+ function testButton(test: ManifestTest): HTMLElement {
48
+ return el(
49
+ 'button',
50
+ {
51
+ class: `test${!state.suiteKey && test.id === state.testId ? ' active' : ''}`,
52
+ click: () => select(test.id),
53
+ title: test.fullName,
54
+ },
55
+ el('span', { class: `dot ${test.state}` }),
56
+ el('span', { class: 'test-name' }, test.name),
57
+ el('span', { class: 'n' }, String(test.frames.length)),
58
+ )
59
+ }
23
60
 
61
+ /** The rows under one suite: its own tests, then the suites nested in it. */
62
+ function renderRows(node: SuiteNode, moduleId: string, path: string[]): DocumentFragment {
63
+ const rows = document.createDocumentFragment()
24
64
  for (const test of node.tests) {
25
- wrap.append(
26
- el(
27
- 'button',
28
- {
29
- class: `test${!state.suiteKey && test.id === state.testId ? ' active' : ''}`,
30
- click: () => select(test.id),
31
- title: test.fullName,
32
- },
33
- el('span', { class: `dot ${test.state}` }),
34
- el('span', {}, test.name),
35
- el('span', { class: 'n' }, String(test.frames.length)),
36
- ),
37
- )
65
+ rows.append(testButton(test))
38
66
  }
39
67
 
40
68
  for (const child of node.suites.values()) {
41
- wrap.append(renderSuite(child, moduleId, [...path, child.name]))
69
+ rows.append(renderSuite(child, moduleId, [...path, child.name]))
42
70
  }
43
71
 
44
- return wrap
72
+ return rows
73
+ }
74
+
75
+ function renderSuite(node: SuiteNode, moduleId: string, path: string[]): HTMLElement {
76
+ return el(
77
+ 'div',
78
+ { class: 'suite' },
79
+ suiteButton(node.name, suiteKey(moduleId, path)),
80
+ renderRows(node, moduleId, path),
81
+ )
82
+ }
83
+
84
+ function renderModule(mod: ManifestModule): HTMLElement {
85
+ const root = buildTree(mod)
86
+ const only = soleSuite(root)
87
+ const path = only ? [only.name] : []
88
+
89
+ return el(
90
+ 'div',
91
+ { class: 'module' },
92
+ moduleButton(only?.name ?? null, mod.id, suiteKey(mod.id, path)),
93
+ el('div', { class: 'suite root' }, renderRows(only ?? root, mod.id, path)),
94
+ )
45
95
  }
46
96
 
47
97
  /** One block per test module, each holding its suite tree. */
48
98
  export function renderSidebar(): HTMLElement {
49
99
  const aside = el('aside', { class: 'sidebar' })
50
100
  for (const mod of state.manifest?.modules ?? []) {
51
- aside.append(
52
- el(
53
- 'div',
54
- { class: 'module' },
55
- scopeButton(mod.id, suiteKey(mod.id, []), 'module'),
56
- renderSuite(buildTree(mod), mod.id, []),
57
- ),
58
- )
101
+ aside.append(renderModule(mod))
59
102
  }
60
103
 
61
104
  return aside
package/src/stage.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createCache, createMirror, rebuildIntoSandboxedIframe } from 'rrweb-snapshot'
2
2
  import type { ManifestFrame } from '@describe-me/core/types'
3
3
  import { loadSnapshot } from './data.js'
4
+ import { fitStage } from './fit-stage.js'
4
5
  import { state } from './state.js'
5
6
 
6
7
  let paintToken = 0
@@ -25,4 +26,11 @@ export async function paintFrame(stage: HTMLElement, frame: ManifestFrame): Prom
25
26
  if (state.width !== 'auto') {
26
27
  iframe.style.width = `${state.width}px`
27
28
  }
29
+
30
+ // Styles are in place after a frame; only then do the boxes have their final size.
31
+ requestAnimationFrame(() => {
32
+ if (token === paintToken) {
33
+ fitStage(iframe)
34
+ }
35
+ })
28
36
  }
@@ -1,5 +1,6 @@
1
1
  import { createCache, createMirror, rebuildIntoSandboxedIframe } from 'rrweb-snapshot'
2
2
  import type { ManifestFrame, ManifestTest } from '@describe-me/core/types'
3
+ import { contentBox } from './content-box.js'
3
4
  import { loadSnapshot } from './data.js'
4
5
  import { el } from './el.js'
5
6
  import { select } from './state.js'
@@ -25,58 +26,6 @@ async function paintThumb(thumb: HTMLElement, frame: ManifestFrame, token: numbe
25
26
  requestAnimationFrame(() => fitThumb(thumb, iframe))
26
27
  }
27
28
 
28
- const SKIPPED_TAGS = new Set(['SCRIPT', 'NOSCRIPT', 'STYLE', 'LINK', 'META', 'TITLE'])
29
-
30
- /**
31
- * Whether an element draws something itself. Layout wrappers (the test
32
- * container, a full-width flex row) stretch across the page and would make
33
- * every thumbnail zoom out to the whole viewport.
34
- */
35
- function isVisual(element: Element, view: Window): boolean {
36
- if (element.children.length === 0) {
37
- return true
38
- }
39
-
40
- const style = view.getComputedStyle(element)
41
- const hasBackground =
42
- style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor !== 'transparent'
43
-
44
- const hasBorder = parseFloat(style.borderTopWidth) > 0 || parseFloat(style.borderLeftWidth) > 0
45
- const hasShadow = style.boxShadow !== 'none'
46
-
47
- return hasBackground || hasBorder || hasShadow
48
- }
49
-
50
- /** The box around everything that actually paints in the snapshot, in the iframe's own pixels. */
51
- function contentBox(body: HTMLElement, view: Window): DOMRect | null {
52
- let box: DOMRect | null = null
53
-
54
- for (const element of Array.from(body.querySelectorAll('*'))) {
55
- if (SKIPPED_TAGS.has(element.tagName) || !isVisual(element, view)) {
56
- continue
57
- }
58
-
59
- const rect = element.getBoundingClientRect()
60
- if (rect.width === 0 || rect.height === 0) {
61
- continue
62
- }
63
-
64
- if (!box) {
65
- box = DOMRect.fromRect(rect)
66
- continue
67
- }
68
-
69
- const right = Math.max(box.right, rect.right)
70
- const bottom = Math.max(box.bottom, rect.bottom)
71
- box.x = Math.min(box.x, rect.x)
72
- box.y = Math.min(box.y, rect.y)
73
- box.width = right - box.x
74
- box.height = bottom - box.y
75
- }
76
-
77
- return box
78
- }
79
-
80
29
  const THUMB_PADDING = 16
81
30
 
82
31
  /**
package/src/style.css CHANGED
@@ -1,29 +1,80 @@
1
+ /* --------------------------------------------------------------- tokens -- */
2
+
3
+ /*
4
+ * Chrome is set in the UI sans; anything that came out of the codebase —
5
+ * props, types, values, file paths, frame labels — is set in the mono face.
6
+ * That split is what keeps a documentation tool from reading like a terminal.
7
+ */
1
8
  :root {
2
- --bg: #fafafa;
9
+ --font-ui: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
10
+ --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
11
+ /*--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;*/
12
+ --bg: #f5f6f8;
3
13
  --panel: #ffffff;
4
- --ink: #141414;
5
- --muted: #7a7a7a;
6
- --line: #e6e6e6;
7
- --hover: #f0f0f0;
8
- --accent: #2b5cff;
9
- --pass: #1f9d55;
10
- --fail: #d43f3f;
11
- --skip: #b5b5b5;
14
+ --raised: #ffffff;
15
+ --stage-bg: #eef0f3;
12
16
  --stage: #ffffff;
13
- --mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
17
+
18
+ --ink: #1c2024;
19
+ --ink-2: #4f5762;
20
+ --ink-3: #6b7381;
21
+
22
+ --line: #e6e9ee;
23
+ --line-soft: #eff1f5;
24
+ --hover: #f1f3f6;
25
+
26
+ --accent: #4a63d8;
27
+ --pass: #2f9e5f;
28
+ --fail: #d04747;
29
+ --warn: #b0760c;
30
+ --skip: #a8b0bb;
31
+
32
+ --tint: color-mix(in srgb, var(--accent) 7%, transparent);
33
+ --tint-strong: color-mix(in srgb, var(--accent) 13%, transparent);
34
+ --ring: color-mix(in srgb, var(--accent) 45%, transparent);
35
+
36
+ --r-sm: 6px;
37
+ --r-md: 8px;
38
+ --r-lg: 12px;
39
+ --r-pill: 999px;
40
+
41
+ --shadow-sm: 0 1px 2px rgba(20, 24, 32, 0.05);
42
+ --shadow-md: 0 1px 2px rgba(20, 24, 32, 0.05), 0 6px 16px -6px rgba(20, 24, 32, 0.14);
43
+
14
44
  color-scheme: light dark;
15
45
  }
16
46
  @media (prefers-color-scheme: dark) {
17
47
  :root {
18
- --bg: #101010;
19
- --panel: #171717;
20
- --ink: #ececec;
21
- --muted: #8a8a8a;
22
- --line: #2a2a2a;
23
- --hover: #202020;
48
+ --bg: #111316;
49
+ --panel: #171a1f;
50
+ --raised: #1c2026;
51
+ --stage-bg: #0b0d10;
24
52
  --stage: #ffffff;
53
+
54
+ --ink: #e2e6ea;
55
+ --ink-2: #b0b9c5;
56
+ --ink-3: #8a939f;
57
+
58
+ --line: #252a31;
59
+ --line-soft: #1e2228;
60
+ --hover: #21262d;
61
+
62
+ --accent: #8ea2ff;
63
+ --pass: #4fbb7d;
64
+ --fail: #e8756f;
65
+ --warn: #d3a24a;
66
+ --skip: #5c646f;
67
+
68
+ --tint: color-mix(in srgb, var(--accent) 10%, transparent);
69
+ --tint-strong: color-mix(in srgb, var(--accent) 18%, transparent);
70
+
71
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
72
+ --shadow-md: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 20px -8px rgba(0, 0, 0, 0.6);
25
73
  }
26
74
  }
75
+
76
+ /* ----------------------------------------------------------------- base -- */
77
+
27
78
  * {
28
79
  box-sizing: border-box;
29
80
  }
@@ -35,7 +86,8 @@ body {
35
86
  body {
36
87
  background: var(--bg);
37
88
  color: var(--ink);
38
- font: 13px/1.45 var(--mono);
89
+ font: 400 13px/1.5 var(--font-ui);
90
+ -webkit-font-smoothing: antialiased;
39
91
  }
40
92
  button {
41
93
  font: inherit;
@@ -48,200 +100,326 @@ button {
48
100
  a {
49
101
  color: inherit;
50
102
  }
103
+ ::selection {
104
+ background: var(--tint-strong);
105
+ }
106
+ :focus-visible {
107
+ outline: 2px solid var(--ring);
108
+ outline-offset: 1px;
109
+ }
110
+
111
+ /* Panels scroll on their own; the thin bars keep the chrome quiet. */
112
+ .sidebar,
113
+ .inspector,
114
+ .overview,
115
+ .stage,
116
+ .timeline {
117
+ scrollbar-width: thin;
118
+ scrollbar-color: var(--line) transparent;
119
+ }
120
+
121
+ /** Anything quoted from the codebase. */
122
+ .mono {
123
+ font-family: var(--font-mono);
124
+ }
51
125
 
52
126
  #app {
53
127
  height: 100%;
54
128
  display: grid;
55
- grid-template-rows: 44px 1fr;
129
+ grid-template-rows: 52px 1fr;
56
130
  }
131
+ .body {
132
+ display: grid;
133
+ grid-template-columns: 296px 1fr 320px;
134
+ min-height: 0;
135
+ }
136
+
137
+ /* --------------------------------------------------------------- header -- */
138
+
57
139
  .header {
58
140
  display: flex;
59
141
  align-items: center;
60
- gap: 16px;
61
- padding: 0 16px;
142
+ gap: 18px;
143
+ padding: 0 18px;
62
144
  border-bottom: 1px solid var(--line);
63
145
  background: var(--panel);
64
146
  }
65
147
  .wordmark {
148
+ font-size: 14px;
66
149
  font-weight: 600;
67
- letter-spacing: -0.01em;
150
+ letter-spacing: -0.015em;
68
151
  }
69
152
  .wordmark .me {
70
153
  color: var(--accent);
71
154
  }
72
155
  .summary {
73
- color: var(--muted);
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 14px;
159
+ color: var(--ink-2);
160
+ font-size: 12px;
74
161
  }
75
- .summary b {
162
+ .stat {
163
+ display: inline-flex;
164
+ align-items: baseline;
165
+ gap: 5px;
166
+ }
167
+ .summary .stat + .stat::before {
168
+ content: '';
169
+ align-self: center;
170
+ width: 1px;
171
+ height: 11px;
172
+ margin-right: 14px;
173
+ background: var(--line);
174
+ }
175
+ .stat b {
76
176
  color: var(--ink);
77
- font-weight: 500;
177
+ font-weight: 600;
178
+ font-variant-numeric: tabular-nums;
78
179
  }
79
- .summary .fail {
180
+ .stat.pass b {
181
+ color: var(--pass);
182
+ }
183
+ .stat.fail {
184
+ padding: 2px 9px;
185
+ border-radius: var(--r-pill);
186
+ background: color-mix(in srgb, var(--fail) 12%, transparent);
80
187
  color: var(--fail);
81
188
  }
189
+ .stat.fail b {
190
+ color: inherit;
191
+ }
82
192
  .header .right {
83
193
  margin-left: auto;
84
- color: var(--muted);
194
+ color: var(--ink-3);
195
+ font-size: 12px;
85
196
  }
86
197
  .live {
87
198
  display: inline-flex;
88
199
  align-items: center;
89
- gap: 6px;
200
+ gap: 7px;
90
201
  }
91
202
  .live::before {
92
203
  content: '';
93
- width: 7px;
94
- height: 7px;
204
+ width: 6px;
205
+ height: 6px;
95
206
  border-radius: 50%;
96
207
  background: var(--pass);
208
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--pass) 18%, transparent);
97
209
  }
98
210
 
99
- .body {
100
- display: grid;
101
- grid-template-columns: 300px 1fr 320px;
102
- min-height: 0;
103
- }
211
+ /* -------------------------------------------------------------- sidebar -- */
212
+
104
213
  .sidebar {
105
214
  border-right: 1px solid var(--line);
106
215
  overflow: auto;
107
- padding: 12px 8px;
216
+ padding: 14px 10px 32px;
108
217
  background: var(--panel);
109
218
  }
110
- .module {
111
- margin-bottom: 14px;
219
+ .module + .module {
220
+ margin-top: 18px;
221
+ padding-top: 18px;
222
+ border-top: 1px solid var(--line-soft);
112
223
  }
113
224
  .module-button,
114
225
  .suite-button {
226
+ position: relative;
115
227
  display: block;
116
228
  width: 100%;
117
229
  text-align: left;
118
- border-radius: 6px;
230
+ border-radius: var(--r-sm);
119
231
  overflow: hidden;
120
232
  text-overflow: ellipsis;
121
233
  white-space: nowrap;
122
234
  }
123
235
  .module-button {
124
- color: var(--muted);
125
- padding: 4px 8px;
126
- font-size: 12px;
236
+ display: flex;
237
+ flex-direction: column;
238
+ align-items: flex-start;
239
+ gap: 1px;
240
+ padding: 5px 9px;
241
+ margin-bottom: 4px;
127
242
  }
128
- .suite {
129
- padding-left: 12px;
243
+ .module-name,
244
+ .module-path {
245
+ max-width: 100%;
246
+ overflow: hidden;
247
+ text-overflow: ellipsis;
248
+ white-space: nowrap;
130
249
  }
131
- .suite-button {
132
- padding: 3px 8px;
250
+ .module-name {
133
251
  color: var(--ink);
252
+ font-size: 13px;
253
+ font-weight: 600;
254
+ letter-spacing: -0.01em;
134
255
  }
135
- .module-button:hover,
136
- .suite-button:hover {
137
- background: var(--hover);
256
+ .module-path {
257
+ color: var(--ink-3);
258
+ font-family: var(--font-mono);
259
+ font-size: 10.5px;
138
260
  }
139
- .module-button.active,
140
- .suite-button.active {
141
- background: var(--ink);
142
- color: var(--bg);
261
+ .suite-button {
262
+ padding: 4px 9px;
263
+ color: var(--ink);
264
+ font-size: 12.5px;
265
+ font-weight: 500;
266
+ }
267
+
268
+ /* One hairline per nesting level: the tree stays readable without deep indents. */
269
+ .suite:not(.root) {
270
+ margin-left: 9px;
271
+ padding-left: 9px;
272
+ border-left: 1px solid var(--line-soft);
143
273
  }
144
274
  .test {
275
+ position: relative;
145
276
  display: flex;
146
277
  align-items: center;
147
278
  gap: 8px;
148
279
  width: 100%;
280
+ min-width: 0;
149
281
  text-align: left;
150
- padding: 4px 8px;
151
- border-radius: 6px;
152
- padding-left: 20px;
282
+ padding: 4px 9px;
283
+ border-radius: var(--r-sm);
284
+ color: var(--ink-2);
285
+ }
286
+ .test-name {
287
+ overflow: hidden;
288
+ text-overflow: ellipsis;
289
+ white-space: nowrap;
153
290
  }
291
+ .module-button:hover,
292
+ .suite-button:hover,
154
293
  .test:hover {
155
294
  background: var(--hover);
295
+ color: var(--ink);
156
296
  }
297
+
298
+ /* Selection is a tinted row with an accent edge — visible while scanning, quiet at rest. */
299
+ .module-button.active,
300
+ .suite-button.active,
157
301
  .test.active {
158
- background: var(--ink);
159
- color: var(--bg);
302
+ background: var(--tint);
303
+ color: var(--ink);
304
+ font-weight: 500;
305
+ }
306
+ .module-button.active::before,
307
+ .suite-button.active::before,
308
+ .test.active::before {
309
+ content: '';
310
+ position: absolute;
311
+ left: 0;
312
+ top: 3px;
313
+ bottom: 3px;
314
+ width: 2px;
315
+ border-radius: 0 2px 2px 0;
316
+ background: var(--accent);
160
317
  }
318
+ .test .n {
319
+ margin-left: auto;
320
+ color: var(--ink-3);
321
+ font-family: var(--font-mono);
322
+ font-size: 10.5px;
323
+ font-variant-numeric: tabular-nums;
324
+ }
325
+
326
+ /* Passing is the norm; only a failure earns full contrast. */
161
327
  .dot {
162
- width: 7px;
163
- height: 7px;
328
+ width: 6px;
329
+ height: 6px;
164
330
  border-radius: 50%;
165
331
  flex: none;
166
332
  }
167
333
  .dot.passed {
168
- background: var(--pass);
334
+ background: color-mix(in srgb, var(--pass) 55%, transparent);
169
335
  }
170
336
  .dot.failed {
171
337
  background: var(--fail);
338
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fail) 16%, transparent);
172
339
  }
173
340
  .dot.skipped,
174
341
  .dot.pending {
175
- background: var(--skip);
176
- }
177
- .test .n {
178
- color: var(--muted);
179
- margin-left: auto;
180
- font-size: 11px;
181
- }
182
- .test.active .n {
183
- color: inherit;
184
- opacity: 0.7;
342
+ background: transparent;
343
+ box-shadow: inset 0 0 0 1.5px var(--skip);
185
344
  }
186
345
 
346
+ /* ----------------------------------------------------------------- main -- */
347
+
187
348
  .main {
188
349
  display: grid;
189
- grid-template-rows: 40px 1fr auto;
350
+ grid-template-rows: 46px 1fr auto;
190
351
  min-width: 0;
191
352
  min-height: 0;
192
353
  }
193
354
  .crumbs {
194
355
  display: flex;
195
356
  align-items: center;
196
- gap: 8px;
357
+ gap: 7px;
197
358
  padding: 0 16px;
198
359
  border-bottom: 1px solid var(--line);
199
- color: var(--muted);
360
+ background: var(--panel);
361
+ color: var(--ink-3);
362
+ font-size: 12.5px;
200
363
  overflow: hidden;
201
364
  white-space: nowrap;
202
365
  }
203
366
  .crumbs .sep {
204
- opacity: 0.5;
367
+ color: var(--ink-3);
368
+ opacity: 0.55;
205
369
  }
206
370
  .crumbs .cur {
207
371
  color: var(--ink);
372
+ font-weight: 500;
208
373
  }
374
+
375
+ /* A segmented control, not three loose buttons. */
209
376
  .crumbs .tools {
210
377
  margin-left: auto;
211
378
  display: flex;
212
- gap: 4px;
379
+ gap: 2px;
380
+ padding: 2px;
381
+ border-radius: var(--r-md);
382
+ background: var(--hover);
213
383
  }
214
384
  .crumbs .tools button {
215
- padding: 2px 8px;
216
- border-radius: 5px;
217
- color: var(--muted);
385
+ padding: 3px 10px;
386
+ border-radius: var(--r-sm);
387
+ color: var(--ink-3);
388
+ font-size: 11.5px;
389
+ font-variant-numeric: tabular-nums;
390
+ }
391
+ .crumbs .tools button:hover {
392
+ color: var(--ink);
218
393
  }
219
394
  .crumbs .tools button.on {
220
- background: var(--hover);
395
+ background: var(--panel);
221
396
  color: var(--ink);
397
+ box-shadow: var(--shadow-sm);
222
398
  }
399
+
223
400
  .stage {
224
401
  position: relative;
225
402
  overflow: auto;
226
- padding: 24px;
403
+ padding: 28px;
227
404
  display: flex;
228
405
  justify-content: center;
229
406
  align-items: flex-start;
230
- background-image: radial-gradient(var(--line) 1px, transparent 1px);
231
- background-size: 16px 16px;
407
+ background: var(--stage-bg);
232
408
  }
233
409
  .stage iframe {
234
410
  border: 1px solid var(--line);
235
- border-radius: 8px;
411
+ border-radius: var(--r-lg);
236
412
  background: var(--stage);
237
413
  width: 100%;
238
- height: 100%;
239
- min-height: 320px;
240
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
241
- transition: width 0.15s ease;
414
+ height: 180px;
415
+ min-height: 120px;
416
+ box-shadow: var(--shadow-md);
417
+ transition:
418
+ width 0.2s ease,
419
+ height 0.2s ease;
242
420
  }
243
421
  .stage .empty {
244
- color: var(--muted);
422
+ color: var(--ink-3);
245
423
  align-self: center;
246
424
  }
247
425
 
@@ -256,88 +434,107 @@ a {
256
434
  .frame {
257
435
  display: inline-flex;
258
436
  align-items: center;
259
- gap: 8px;
260
- padding: 6px 10px;
261
- border: 1px solid var(--line);
262
- border-radius: 999px;
437
+ gap: 9px;
438
+ padding: 6px 12px;
439
+ border: 1px solid transparent;
440
+ border-radius: var(--r-md);
263
441
  white-space: nowrap;
264
- color: var(--muted);
442
+ color: var(--ink-2);
265
443
  }
266
444
  .frame:hover {
267
445
  background: var(--hover);
268
446
  }
269
447
  .frame.active {
270
- border-color: var(--ink);
448
+ background: var(--tint);
449
+ border-color: color-mix(in srgb, var(--accent) 30%, transparent);
271
450
  color: var(--ink);
272
451
  }
273
452
  .frame .k {
274
- font-size: 10px;
275
- text-transform: uppercase;
276
- letter-spacing: 0.06em;
277
- opacity: 0.7;
453
+ display: inline-flex;
454
+ align-items: center;
455
+ gap: 6px;
456
+ color: var(--ink-3);
457
+ font-size: 11px;
458
+ }
459
+ .frame .k::before {
460
+ content: '';
461
+ width: 6px;
462
+ height: 6px;
463
+ border-radius: 2px;
464
+ background: currentColor;
465
+ }
466
+ .frame .label {
467
+ font-family: var(--font-mono);
468
+ font-size: 12px;
278
469
  }
279
470
  .frame .t {
280
- opacity: 0.6;
281
- font-size: 11px;
471
+ color: var(--ink-3);
472
+ font-family: var(--font-mono);
473
+ font-size: 10.5px;
474
+ font-variant-numeric: tabular-nums;
282
475
  }
283
476
  .frame.render .k {
284
477
  color: var(--accent);
285
- opacity: 1;
286
478
  }
287
479
  .frame.action .k {
288
480
  color: var(--pass);
289
- opacity: 1;
290
481
  }
291
482
  .frame.step .k {
292
- color: #c77d00;
293
- opacity: 1;
294
- }
295
- .frame.end .k {
296
- color: var(--muted);
483
+ color: var(--warn);
297
484
  }
298
485
 
486
+ /* ------------------------------------------------------------ inspector -- */
487
+
299
488
  .inspector {
300
489
  border-left: 1px solid var(--line);
301
490
  overflow: auto;
302
- padding: 16px;
491
+ padding: 16px 18px 32px;
303
492
  background: var(--panel);
304
493
  }
305
494
  .inspector h3 {
306
- margin: 0 0 8px;
307
- font-size: 11px;
308
- text-transform: uppercase;
309
- letter-spacing: 0.08em;
310
- color: var(--muted);
311
- font-weight: 500;
495
+ margin: 0 0 9px;
496
+ font-size: 11.5px;
497
+ font-weight: 600;
498
+ color: var(--ink-3);
499
+ text-transform: capitalize;
312
500
  }
313
501
  .inspector section {
314
- margin-bottom: 20px;
502
+ margin-bottom: 22px;
503
+ }
504
+ .inspector section + section {
505
+ padding-top: 18px;
506
+ border-top: 1px solid var(--line-soft);
315
507
  }
316
508
  .status {
317
509
  display: inline-flex;
318
510
  align-items: center;
319
511
  gap: 8px;
320
- }
321
- .status .dot {
322
- width: 8px;
323
- height: 8px;
512
+ font-weight: 500;
513
+ text-transform: capitalize;
324
514
  }
325
515
  .kv {
326
516
  width: 100%;
327
517
  border-collapse: collapse;
328
518
  }
329
519
  .kv td {
330
- padding: 3px 0;
520
+ padding: 4px 0;
331
521
  vertical-align: top;
332
- border-bottom: 1px solid var(--line);
522
+ border-bottom: 1px solid var(--line-soft);
523
+ }
524
+ .kv tr:last-child td {
525
+ border-bottom: 0;
333
526
  }
334
527
  .kv td:first-child {
335
- color: var(--muted);
336
- padding-right: 12px;
528
+ color: var(--ink-3);
529
+ padding-right: 14px;
337
530
  white-space: nowrap;
531
+ font-family: var(--font-mono);
532
+ font-size: 12px;
338
533
  }
339
534
  .kv td:last-child {
340
535
  word-break: break-word;
536
+ font-family: var(--font-mono);
537
+ font-size: 12px;
341
538
  }
342
539
  .val-string {
343
540
  color: #b0501f;
@@ -347,99 +544,139 @@ a {
347
544
  color: var(--accent);
348
545
  }
349
546
  .val-fn {
350
- color: var(--muted);
547
+ color: var(--ink-3);
351
548
  font-style: italic;
352
549
  }
550
+ @media (prefers-color-scheme: dark) {
551
+ .val-string {
552
+ color: #d98a5f;
553
+ }
554
+ }
353
555
  .err {
556
+ margin: 0;
354
557
  color: var(--fail);
355
558
  white-space: pre-wrap;
559
+ font-family: var(--font-mono);
356
560
  font-size: 12px;
357
561
  }
358
562
  .hint {
359
- color: var(--muted);
563
+ color: var(--ink-3);
360
564
  font-size: 12px;
361
565
  }
362
566
  kbd {
567
+ display: inline-block;
568
+ min-width: 18px;
569
+ text-align: center;
363
570
  border: 1px solid var(--line);
364
- border-radius: 4px;
571
+ border-bottom-width: 2px;
572
+ border-radius: 5px;
365
573
  padding: 0 4px;
574
+ background: var(--bg);
366
575
  font: inherit;
367
576
  font-size: 11px;
577
+ color: var(--ink-2);
578
+ }
579
+ .missing-line {
580
+ color: var(--fail);
581
+ font-family: var(--font-mono);
582
+ font-size: 12px;
583
+ }
584
+ .component-line {
585
+ display: flex;
586
+ gap: 12px;
587
+ justify-content: space-between;
588
+ padding: 3px 0;
589
+ }
590
+ .component-line span:first-child {
591
+ font-family: var(--font-mono);
592
+ font-size: 12px;
593
+ }
594
+ .component-line .hint {
595
+ font-family: var(--font-mono);
596
+ font-size: 11px;
597
+ overflow: hidden;
598
+ text-overflow: ellipsis;
368
599
  }
369
600
 
601
+ /* ------------------------------------------------------------- overview -- */
602
+
370
603
  .overview {
371
604
  overflow: auto;
372
605
  min-width: 0;
373
606
  min-height: 0;
374
- padding: 20px 24px 48px;
607
+ padding: 24px 28px 56px;
375
608
  }
376
609
  .overview-head {
377
610
  display: flex;
378
611
  align-items: baseline;
379
612
  gap: 16px;
380
- padding-bottom: 10px;
613
+ padding-bottom: 14px;
381
614
  border-bottom: 1px solid var(--line);
382
615
  }
383
616
  .overview-head h1 {
384
617
  margin: 0;
385
- font-size: 17px;
618
+ font-size: 19px;
386
619
  font-weight: 600;
387
- letter-spacing: -0.01em;
620
+ letter-spacing: -0.02em;
388
621
  }
389
622
  .overview-meta {
390
623
  margin-left: auto;
391
- color: var(--muted);
624
+ color: var(--ink-3);
625
+ font-family: var(--font-mono);
626
+ font-size: 11px;
392
627
  }
393
628
  .overview-section {
394
- margin-top: 22px;
629
+ margin-top: 26px;
395
630
  }
396
631
  .overview-section h2 {
397
632
  margin: 0;
398
- font-size: 14px;
633
+ font-size: 15px;
399
634
  font-weight: 600;
400
635
  }
401
636
  .overview h3 {
402
- margin: 18px 0 8px;
403
- font-size: 11px;
404
- text-transform: uppercase;
405
- letter-spacing: 0.08em;
406
- color: var(--muted);
407
- font-weight: 500;
637
+ margin: 22px 0 10px;
638
+ font-size: 11.5px;
639
+ font-weight: 600;
640
+ color: var(--ink-3);
641
+ text-transform: capitalize;
408
642
  }
409
643
  .overview .empty {
410
- color: var(--muted);
411
- padding: 24px 0;
644
+ color: var(--ink-3);
645
+ padding: 28px 0;
412
646
  }
413
647
  .head-hint {
414
648
  margin-left: 10px;
415
- color: var(--muted);
649
+ color: var(--ink-3);
650
+ font-weight: 400;
416
651
  text-transform: none;
417
- letter-spacing: 0;
418
- opacity: 0.85;
419
652
  }
420
653
 
421
654
  .props th {
422
655
  text-align: left;
423
- padding: 0 12px 5px 0;
424
- font-size: 11px;
425
- font-weight: 500;
426
- text-transform: uppercase;
427
- letter-spacing: 0.06em;
428
- color: var(--muted);
656
+ padding: 0 14px 7px 0;
657
+ font-size: 11.5px;
658
+ font-weight: 600;
659
+ color: var(--ink-3);
660
+ text-transform: capitalize;
429
661
  border-bottom: 1px solid var(--line);
430
662
  }
431
663
  .props td {
432
- padding: 7px 12px 7px 0;
664
+ padding: 9px 14px 9px 0;
433
665
  }
434
666
  .props td.prop-name {
435
667
  color: var(--ink);
668
+ font-family: var(--font-mono);
669
+ font-size: 12px;
670
+ font-weight: 500;
436
671
  }
437
672
  .props .req {
438
673
  color: var(--fail);
439
674
  }
440
675
  .props .prop-type {
441
- color: var(--muted);
676
+ color: var(--ink-2);
442
677
  white-space: pre-wrap;
678
+ font-family: var(--font-mono);
679
+ font-size: 12px;
443
680
  }
444
681
  .props .prop-covered {
445
682
  width: 52%;
@@ -448,44 +685,43 @@ kbd {
448
685
  display: inline-flex;
449
686
  align-items: baseline;
450
687
  gap: 4px;
451
- margin: 0 6px 4px 0;
452
- padding: 1px 8px;
453
- border: 1px solid transparent;
454
- border-radius: 999px;
455
- font-size: 12px;
688
+ margin: 0 6px 5px 0;
689
+ padding: 2px 9px;
690
+ border-radius: var(--r-pill);
691
+ font-family: var(--font-mono);
692
+ font-size: 11.5px;
456
693
  white-space: nowrap;
457
694
  }
458
695
  .chip.covered {
459
696
  color: var(--pass);
460
- background: color-mix(in srgb, var(--pass) 14%, transparent);
461
- border-color: color-mix(in srgb, var(--pass) 32%, transparent);
697
+ background: color-mix(in srgb, var(--pass) 11%, transparent);
462
698
  }
463
699
  .chip.missing {
464
700
  color: var(--fail);
465
- background: color-mix(in srgb, var(--fail) 14%, transparent);
466
- border-color: color-mix(in srgb, var(--fail) 32%, transparent);
701
+ background: color-mix(in srgb, var(--fail) 11%, transparent);
467
702
  }
468
703
  .chip-default {
469
- color: var(--muted);
470
- font-size: 11px;
704
+ color: var(--ink-3);
705
+ font-size: 10.5px;
471
706
  }
472
707
 
473
708
  .gallery {
474
709
  display: grid;
475
710
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
476
- gap: 14px;
711
+ gap: 16px;
477
712
  }
478
713
  .tile {
479
714
  display: block;
480
715
  width: 100%;
481
716
  text-align: left;
482
717
  border: 1px solid var(--line);
483
- border-radius: 10px;
718
+ border-radius: var(--r-lg);
484
719
  overflow: hidden;
485
- background: var(--panel);
720
+ background: var(--raised);
721
+ transition: border-color 0.15s ease;
486
722
  }
487
723
  .tile:hover {
488
- border-color: var(--muted);
724
+ border-color: var(--accent);
489
725
  }
490
726
  .thumb {
491
727
  height: 150px;
@@ -502,26 +738,18 @@ kbd {
502
738
  pointer-events: none;
503
739
  }
504
740
  .thumb .hint {
505
- padding: 10px;
741
+ padding: 12px;
506
742
  }
507
743
  .cap {
508
744
  display: flex;
509
745
  align-items: center;
510
746
  gap: 8px;
511
- padding: 7px 10px;
747
+ padding: 9px 12px;
512
748
  min-width: 0;
749
+ font-size: 12.5px;
513
750
  }
514
751
  .cap-name {
515
752
  overflow: hidden;
516
753
  text-overflow: ellipsis;
517
754
  white-space: nowrap;
518
755
  }
519
-
520
- .missing-line {
521
- color: var(--fail);
522
- }
523
- .component-line {
524
- display: flex;
525
- gap: 12px;
526
- justify-content: space-between;
527
- }
package/vite.config.ts CHANGED
@@ -4,7 +4,7 @@ import { defineConfig, type Plugin } from 'vite'
4
4
 
5
5
  /** Where the manifest and snapshots live. The CLI sets this; the default is the example project. */
6
6
  function dataDir(): string {
7
- return resolve(process.env.DESCRIBE_ME_DIR ?? '../../examples/react-basic/.describe-me')
7
+ return resolve(process.env.DESCRIBE_ME_DIR ?? '../../examples/react-browser/.describe-me')
8
8
  }
9
9
 
10
10
  /**