describe-me 0.1.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/LICENSE +21 -0
- package/README.md +34 -0
- package/bin/describe-me.js +4 -0
- package/dist-cli/main.d.ts +1 -0
- package/dist-cli/main.js +25 -0
- package/dist-cli/parse-args.d.ts +12 -0
- package/dist-cli/parse-args.js +29 -0
- package/dist-cli/run-build.d.ts +2 -0
- package/dist-cli/run-build.js +13 -0
- package/dist-cli/run-dev.d.ts +2 -0
- package/dist-cli/run-dev.js +15 -0
- package/dist-cli/viewer-root.d.ts +2 -0
- package/dist-cli/viewer-root.js +5 -0
- package/index.html +13 -0
- package/package.json +54 -0
- package/src/coverage.ts +75 -0
- package/src/data.ts +54 -0
- package/src/el.ts +25 -0
- package/src/header.ts +31 -0
- package/src/inspector.ts +86 -0
- package/src/keyboard.ts +58 -0
- package/src/main-panel.ts +69 -0
- package/src/main.ts +41 -0
- package/src/overview-inspector.ts +101 -0
- package/src/overview.ts +92 -0
- package/src/props-table.ts +72 -0
- package/src/render-all.ts +17 -0
- package/src/rerender.ts +15 -0
- package/src/scope-components.ts +14 -0
- package/src/scope.ts +17 -0
- package/src/sidebar.ts +62 -0
- package/src/stage.ts +28 -0
- package/src/state.ts +71 -0
- package/src/states-gallery.ts +191 -0
- package/src/style.css +527 -0
- package/src/suite-key.ts +19 -0
- package/src/tree.ts +27 -0
- package/src/value-cell.ts +22 -0
- package/vite.config.ts +85 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createCache, createMirror, rebuildIntoSandboxedIframe } from 'rrweb-snapshot'
|
|
2
|
+
import type { ManifestFrame, ManifestTest } from '@describe-me/core/types'
|
|
3
|
+
import { loadSnapshot } from './data.js'
|
|
4
|
+
import { el } from './el.js'
|
|
5
|
+
import { select } from './state.js'
|
|
6
|
+
|
|
7
|
+
/** Bumped on every gallery render, so snapshots that arrive late are dropped. */
|
|
8
|
+
let galleryToken = 0
|
|
9
|
+
|
|
10
|
+
async function paintThumb(thumb: HTMLElement, frame: ManifestFrame, token: number): Promise<void> {
|
|
11
|
+
const node = await loadSnapshot(frame.snapshot)
|
|
12
|
+
if (token !== galleryToken || !thumb.isConnected) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { iframe } = rebuildIntoSandboxedIframe(node, {
|
|
17
|
+
root: thumb,
|
|
18
|
+
iframeAttributes: { title: frame.label },
|
|
19
|
+
cache: createCache(),
|
|
20
|
+
mirror: createMirror(),
|
|
21
|
+
hackCss: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Styles are in place after a frame; only then do the boxes have their final size.
|
|
25
|
+
requestAnimationFrame(() => fitThumb(thumb, iframe))
|
|
26
|
+
}
|
|
27
|
+
|
|
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
|
+
const THUMB_PADDING = 16
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Zoom the snapshot so the component fills the tile instead of sitting in the
|
|
84
|
+
* corner of a full-size page. Never enlarges beyond natural size.
|
|
85
|
+
*/
|
|
86
|
+
function fitThumb(thumb: HTMLElement, iframe: HTMLIFrameElement): void {
|
|
87
|
+
const body = iframe.contentDocument?.body
|
|
88
|
+
const view = iframe.contentWindow
|
|
89
|
+
const box = body && view ? contentBox(body, view) : null
|
|
90
|
+
if (!box) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const width = thumb.clientWidth - THUMB_PADDING * 2
|
|
95
|
+
const height = thumb.clientHeight - THUMB_PADDING * 2
|
|
96
|
+
const scale = Math.min(width / box.width, height / box.height, 1)
|
|
97
|
+
const offsetX = THUMB_PADDING + (width - box.width * scale) / 2 - box.x * scale
|
|
98
|
+
const offsetY = THUMB_PADDING + (height - box.height * scale) / 2 - box.y * scale
|
|
99
|
+
|
|
100
|
+
iframe.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The overview column scrolls, not the page, and an observer rooted at the
|
|
105
|
+
* viewport would never see the tiles below its fold.
|
|
106
|
+
*/
|
|
107
|
+
function scrollRoot(node: HTMLElement): HTMLElement | null {
|
|
108
|
+
let parent = node.parentElement
|
|
109
|
+
while (parent) {
|
|
110
|
+
const overflow = getComputedStyle(parent).overflowY
|
|
111
|
+
if (overflow === 'auto' || overflow === 'scroll') {
|
|
112
|
+
return parent
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
parent = parent.parentElement
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Build each thumbnail once its tile comes near the visible part of the column. */
|
|
122
|
+
function paintWhenVisible(gallery: HTMLElement, pending: Map<Element, ManifestFrame>): void {
|
|
123
|
+
const token = galleryToken
|
|
124
|
+
const observer = new IntersectionObserver(
|
|
125
|
+
(entries) => {
|
|
126
|
+
if (token !== galleryToken) {
|
|
127
|
+
observer.disconnect()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const frame = pending.get(entry.target)
|
|
133
|
+
if (entry.isIntersecting && frame) {
|
|
134
|
+
pending.delete(entry.target)
|
|
135
|
+
observer.unobserve(entry.target)
|
|
136
|
+
void paintThumb(entry.target as HTMLElement, frame, token)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{ root: scrollRoot(gallery), rootMargin: '400px' },
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
for (const thumb of pending.keys()) {
|
|
144
|
+
observer.observe(thumb)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One scaled-down tile per test, showing its last frame and leading to it. */
|
|
149
|
+
export function renderStatesGallery(tests: ManifestTest[]): HTMLElement {
|
|
150
|
+
const token = ++galleryToken
|
|
151
|
+
const gallery = el('div', { class: 'gallery' })
|
|
152
|
+
const pending = new Map<Element, ManifestFrame>()
|
|
153
|
+
|
|
154
|
+
for (const test of tests) {
|
|
155
|
+
const lastIndex = Math.max(0, test.frames.length - 1)
|
|
156
|
+
const last = test.frames[lastIndex]
|
|
157
|
+
const thumb = el('div', { class: 'thumb' })
|
|
158
|
+
if (last) {
|
|
159
|
+
pending.set(thumb, last)
|
|
160
|
+
} else {
|
|
161
|
+
thumb.append(el('div', { class: 'hint' }, 'no frames'))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
gallery.append(
|
|
165
|
+
el(
|
|
166
|
+
'button',
|
|
167
|
+
{
|
|
168
|
+
class: 'tile',
|
|
169
|
+
title: test.fullName,
|
|
170
|
+
click: () => select(test.id, lastIndex),
|
|
171
|
+
},
|
|
172
|
+
thumb,
|
|
173
|
+
el(
|
|
174
|
+
'span',
|
|
175
|
+
{ class: 'cap' },
|
|
176
|
+
el('span', { class: `dot ${test.state}` }),
|
|
177
|
+
el('span', { class: 'cap-name' }, test.name),
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The scroll container only exists once the caller has put the gallery on screen.
|
|
184
|
+
requestAnimationFrame(() => {
|
|
185
|
+
if (token === galleryToken && gallery.isConnected) {
|
|
186
|
+
paintWhenVisible(gallery, pending)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return gallery
|
|
191
|
+
}
|
package/src/style.css
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #fafafa;
|
|
3
|
+
--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;
|
|
12
|
+
--stage: #ffffff;
|
|
13
|
+
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
|
14
|
+
color-scheme: light dark;
|
|
15
|
+
}
|
|
16
|
+
@media (prefers-color-scheme: dark) {
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #101010;
|
|
19
|
+
--panel: #171717;
|
|
20
|
+
--ink: #ececec;
|
|
21
|
+
--muted: #8a8a8a;
|
|
22
|
+
--line: #2a2a2a;
|
|
23
|
+
--hover: #202020;
|
|
24
|
+
--stage: #ffffff;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
* {
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
}
|
|
30
|
+
html,
|
|
31
|
+
body {
|
|
32
|
+
height: 100%;
|
|
33
|
+
margin: 0;
|
|
34
|
+
}
|
|
35
|
+
body {
|
|
36
|
+
background: var(--bg);
|
|
37
|
+
color: var(--ink);
|
|
38
|
+
font: 13px/1.45 var(--mono);
|
|
39
|
+
}
|
|
40
|
+
button {
|
|
41
|
+
font: inherit;
|
|
42
|
+
color: inherit;
|
|
43
|
+
background: none;
|
|
44
|
+
border: 0;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
padding: 0;
|
|
47
|
+
}
|
|
48
|
+
a {
|
|
49
|
+
color: inherit;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#app {
|
|
53
|
+
height: 100%;
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-rows: 44px 1fr;
|
|
56
|
+
}
|
|
57
|
+
.header {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 16px;
|
|
61
|
+
padding: 0 16px;
|
|
62
|
+
border-bottom: 1px solid var(--line);
|
|
63
|
+
background: var(--panel);
|
|
64
|
+
}
|
|
65
|
+
.wordmark {
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
letter-spacing: -0.01em;
|
|
68
|
+
}
|
|
69
|
+
.wordmark .me {
|
|
70
|
+
color: var(--accent);
|
|
71
|
+
}
|
|
72
|
+
.summary {
|
|
73
|
+
color: var(--muted);
|
|
74
|
+
}
|
|
75
|
+
.summary b {
|
|
76
|
+
color: var(--ink);
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
}
|
|
79
|
+
.summary .fail {
|
|
80
|
+
color: var(--fail);
|
|
81
|
+
}
|
|
82
|
+
.header .right {
|
|
83
|
+
margin-left: auto;
|
|
84
|
+
color: var(--muted);
|
|
85
|
+
}
|
|
86
|
+
.live {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 6px;
|
|
90
|
+
}
|
|
91
|
+
.live::before {
|
|
92
|
+
content: '';
|
|
93
|
+
width: 7px;
|
|
94
|
+
height: 7px;
|
|
95
|
+
border-radius: 50%;
|
|
96
|
+
background: var(--pass);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.body {
|
|
100
|
+
display: grid;
|
|
101
|
+
grid-template-columns: 300px 1fr 320px;
|
|
102
|
+
min-height: 0;
|
|
103
|
+
}
|
|
104
|
+
.sidebar {
|
|
105
|
+
border-right: 1px solid var(--line);
|
|
106
|
+
overflow: auto;
|
|
107
|
+
padding: 12px 8px;
|
|
108
|
+
background: var(--panel);
|
|
109
|
+
}
|
|
110
|
+
.module {
|
|
111
|
+
margin-bottom: 14px;
|
|
112
|
+
}
|
|
113
|
+
.module-button,
|
|
114
|
+
.suite-button {
|
|
115
|
+
display: block;
|
|
116
|
+
width: 100%;
|
|
117
|
+
text-align: left;
|
|
118
|
+
border-radius: 6px;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
text-overflow: ellipsis;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
.module-button {
|
|
124
|
+
color: var(--muted);
|
|
125
|
+
padding: 4px 8px;
|
|
126
|
+
font-size: 12px;
|
|
127
|
+
}
|
|
128
|
+
.suite {
|
|
129
|
+
padding-left: 12px;
|
|
130
|
+
}
|
|
131
|
+
.suite-button {
|
|
132
|
+
padding: 3px 8px;
|
|
133
|
+
color: var(--ink);
|
|
134
|
+
}
|
|
135
|
+
.module-button:hover,
|
|
136
|
+
.suite-button:hover {
|
|
137
|
+
background: var(--hover);
|
|
138
|
+
}
|
|
139
|
+
.module-button.active,
|
|
140
|
+
.suite-button.active {
|
|
141
|
+
background: var(--ink);
|
|
142
|
+
color: var(--bg);
|
|
143
|
+
}
|
|
144
|
+
.test {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 8px;
|
|
148
|
+
width: 100%;
|
|
149
|
+
text-align: left;
|
|
150
|
+
padding: 4px 8px;
|
|
151
|
+
border-radius: 6px;
|
|
152
|
+
padding-left: 20px;
|
|
153
|
+
}
|
|
154
|
+
.test:hover {
|
|
155
|
+
background: var(--hover);
|
|
156
|
+
}
|
|
157
|
+
.test.active {
|
|
158
|
+
background: var(--ink);
|
|
159
|
+
color: var(--bg);
|
|
160
|
+
}
|
|
161
|
+
.dot {
|
|
162
|
+
width: 7px;
|
|
163
|
+
height: 7px;
|
|
164
|
+
border-radius: 50%;
|
|
165
|
+
flex: none;
|
|
166
|
+
}
|
|
167
|
+
.dot.passed {
|
|
168
|
+
background: var(--pass);
|
|
169
|
+
}
|
|
170
|
+
.dot.failed {
|
|
171
|
+
background: var(--fail);
|
|
172
|
+
}
|
|
173
|
+
.dot.skipped,
|
|
174
|
+
.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;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.main {
|
|
188
|
+
display: grid;
|
|
189
|
+
grid-template-rows: 40px 1fr auto;
|
|
190
|
+
min-width: 0;
|
|
191
|
+
min-height: 0;
|
|
192
|
+
}
|
|
193
|
+
.crumbs {
|
|
194
|
+
display: flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
gap: 8px;
|
|
197
|
+
padding: 0 16px;
|
|
198
|
+
border-bottom: 1px solid var(--line);
|
|
199
|
+
color: var(--muted);
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
white-space: nowrap;
|
|
202
|
+
}
|
|
203
|
+
.crumbs .sep {
|
|
204
|
+
opacity: 0.5;
|
|
205
|
+
}
|
|
206
|
+
.crumbs .cur {
|
|
207
|
+
color: var(--ink);
|
|
208
|
+
}
|
|
209
|
+
.crumbs .tools {
|
|
210
|
+
margin-left: auto;
|
|
211
|
+
display: flex;
|
|
212
|
+
gap: 4px;
|
|
213
|
+
}
|
|
214
|
+
.crumbs .tools button {
|
|
215
|
+
padding: 2px 8px;
|
|
216
|
+
border-radius: 5px;
|
|
217
|
+
color: var(--muted);
|
|
218
|
+
}
|
|
219
|
+
.crumbs .tools button.on {
|
|
220
|
+
background: var(--hover);
|
|
221
|
+
color: var(--ink);
|
|
222
|
+
}
|
|
223
|
+
.stage {
|
|
224
|
+
position: relative;
|
|
225
|
+
overflow: auto;
|
|
226
|
+
padding: 24px;
|
|
227
|
+
display: flex;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
align-items: flex-start;
|
|
230
|
+
background-image: radial-gradient(var(--line) 1px, transparent 1px);
|
|
231
|
+
background-size: 16px 16px;
|
|
232
|
+
}
|
|
233
|
+
.stage iframe {
|
|
234
|
+
border: 1px solid var(--line);
|
|
235
|
+
border-radius: 8px;
|
|
236
|
+
background: var(--stage);
|
|
237
|
+
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;
|
|
242
|
+
}
|
|
243
|
+
.stage .empty {
|
|
244
|
+
color: var(--muted);
|
|
245
|
+
align-self: center;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.timeline {
|
|
249
|
+
border-top: 1px solid var(--line);
|
|
250
|
+
padding: 10px 16px;
|
|
251
|
+
display: flex;
|
|
252
|
+
gap: 6px;
|
|
253
|
+
overflow-x: auto;
|
|
254
|
+
background: var(--panel);
|
|
255
|
+
}
|
|
256
|
+
.frame {
|
|
257
|
+
display: inline-flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: 8px;
|
|
260
|
+
padding: 6px 10px;
|
|
261
|
+
border: 1px solid var(--line);
|
|
262
|
+
border-radius: 999px;
|
|
263
|
+
white-space: nowrap;
|
|
264
|
+
color: var(--muted);
|
|
265
|
+
}
|
|
266
|
+
.frame:hover {
|
|
267
|
+
background: var(--hover);
|
|
268
|
+
}
|
|
269
|
+
.frame.active {
|
|
270
|
+
border-color: var(--ink);
|
|
271
|
+
color: var(--ink);
|
|
272
|
+
}
|
|
273
|
+
.frame .k {
|
|
274
|
+
font-size: 10px;
|
|
275
|
+
text-transform: uppercase;
|
|
276
|
+
letter-spacing: 0.06em;
|
|
277
|
+
opacity: 0.7;
|
|
278
|
+
}
|
|
279
|
+
.frame .t {
|
|
280
|
+
opacity: 0.6;
|
|
281
|
+
font-size: 11px;
|
|
282
|
+
}
|
|
283
|
+
.frame.render .k {
|
|
284
|
+
color: var(--accent);
|
|
285
|
+
opacity: 1;
|
|
286
|
+
}
|
|
287
|
+
.frame.action .k {
|
|
288
|
+
color: var(--pass);
|
|
289
|
+
opacity: 1;
|
|
290
|
+
}
|
|
291
|
+
.frame.step .k {
|
|
292
|
+
color: #c77d00;
|
|
293
|
+
opacity: 1;
|
|
294
|
+
}
|
|
295
|
+
.frame.end .k {
|
|
296
|
+
color: var(--muted);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.inspector {
|
|
300
|
+
border-left: 1px solid var(--line);
|
|
301
|
+
overflow: auto;
|
|
302
|
+
padding: 16px;
|
|
303
|
+
background: var(--panel);
|
|
304
|
+
}
|
|
305
|
+
.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;
|
|
312
|
+
}
|
|
313
|
+
.inspector section {
|
|
314
|
+
margin-bottom: 20px;
|
|
315
|
+
}
|
|
316
|
+
.status {
|
|
317
|
+
display: inline-flex;
|
|
318
|
+
align-items: center;
|
|
319
|
+
gap: 8px;
|
|
320
|
+
}
|
|
321
|
+
.status .dot {
|
|
322
|
+
width: 8px;
|
|
323
|
+
height: 8px;
|
|
324
|
+
}
|
|
325
|
+
.kv {
|
|
326
|
+
width: 100%;
|
|
327
|
+
border-collapse: collapse;
|
|
328
|
+
}
|
|
329
|
+
.kv td {
|
|
330
|
+
padding: 3px 0;
|
|
331
|
+
vertical-align: top;
|
|
332
|
+
border-bottom: 1px solid var(--line);
|
|
333
|
+
}
|
|
334
|
+
.kv td:first-child {
|
|
335
|
+
color: var(--muted);
|
|
336
|
+
padding-right: 12px;
|
|
337
|
+
white-space: nowrap;
|
|
338
|
+
}
|
|
339
|
+
.kv td:last-child {
|
|
340
|
+
word-break: break-word;
|
|
341
|
+
}
|
|
342
|
+
.val-string {
|
|
343
|
+
color: #b0501f;
|
|
344
|
+
}
|
|
345
|
+
.val-number,
|
|
346
|
+
.val-boolean {
|
|
347
|
+
color: var(--accent);
|
|
348
|
+
}
|
|
349
|
+
.val-fn {
|
|
350
|
+
color: var(--muted);
|
|
351
|
+
font-style: italic;
|
|
352
|
+
}
|
|
353
|
+
.err {
|
|
354
|
+
color: var(--fail);
|
|
355
|
+
white-space: pre-wrap;
|
|
356
|
+
font-size: 12px;
|
|
357
|
+
}
|
|
358
|
+
.hint {
|
|
359
|
+
color: var(--muted);
|
|
360
|
+
font-size: 12px;
|
|
361
|
+
}
|
|
362
|
+
kbd {
|
|
363
|
+
border: 1px solid var(--line);
|
|
364
|
+
border-radius: 4px;
|
|
365
|
+
padding: 0 4px;
|
|
366
|
+
font: inherit;
|
|
367
|
+
font-size: 11px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.overview {
|
|
371
|
+
overflow: auto;
|
|
372
|
+
min-width: 0;
|
|
373
|
+
min-height: 0;
|
|
374
|
+
padding: 20px 24px 48px;
|
|
375
|
+
}
|
|
376
|
+
.overview-head {
|
|
377
|
+
display: flex;
|
|
378
|
+
align-items: baseline;
|
|
379
|
+
gap: 16px;
|
|
380
|
+
padding-bottom: 10px;
|
|
381
|
+
border-bottom: 1px solid var(--line);
|
|
382
|
+
}
|
|
383
|
+
.overview-head h1 {
|
|
384
|
+
margin: 0;
|
|
385
|
+
font-size: 17px;
|
|
386
|
+
font-weight: 600;
|
|
387
|
+
letter-spacing: -0.01em;
|
|
388
|
+
}
|
|
389
|
+
.overview-meta {
|
|
390
|
+
margin-left: auto;
|
|
391
|
+
color: var(--muted);
|
|
392
|
+
}
|
|
393
|
+
.overview-section {
|
|
394
|
+
margin-top: 22px;
|
|
395
|
+
}
|
|
396
|
+
.overview-section h2 {
|
|
397
|
+
margin: 0;
|
|
398
|
+
font-size: 14px;
|
|
399
|
+
font-weight: 600;
|
|
400
|
+
}
|
|
401
|
+
.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;
|
|
408
|
+
}
|
|
409
|
+
.overview .empty {
|
|
410
|
+
color: var(--muted);
|
|
411
|
+
padding: 24px 0;
|
|
412
|
+
}
|
|
413
|
+
.head-hint {
|
|
414
|
+
margin-left: 10px;
|
|
415
|
+
color: var(--muted);
|
|
416
|
+
text-transform: none;
|
|
417
|
+
letter-spacing: 0;
|
|
418
|
+
opacity: 0.85;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.props th {
|
|
422
|
+
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);
|
|
429
|
+
border-bottom: 1px solid var(--line);
|
|
430
|
+
}
|
|
431
|
+
.props td {
|
|
432
|
+
padding: 7px 12px 7px 0;
|
|
433
|
+
}
|
|
434
|
+
.props td.prop-name {
|
|
435
|
+
color: var(--ink);
|
|
436
|
+
}
|
|
437
|
+
.props .req {
|
|
438
|
+
color: var(--fail);
|
|
439
|
+
}
|
|
440
|
+
.props .prop-type {
|
|
441
|
+
color: var(--muted);
|
|
442
|
+
white-space: pre-wrap;
|
|
443
|
+
}
|
|
444
|
+
.props .prop-covered {
|
|
445
|
+
width: 52%;
|
|
446
|
+
}
|
|
447
|
+
.chip {
|
|
448
|
+
display: inline-flex;
|
|
449
|
+
align-items: baseline;
|
|
450
|
+
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;
|
|
456
|
+
white-space: nowrap;
|
|
457
|
+
}
|
|
458
|
+
.chip.covered {
|
|
459
|
+
color: var(--pass);
|
|
460
|
+
background: color-mix(in srgb, var(--pass) 14%, transparent);
|
|
461
|
+
border-color: color-mix(in srgb, var(--pass) 32%, transparent);
|
|
462
|
+
}
|
|
463
|
+
.chip.missing {
|
|
464
|
+
color: var(--fail);
|
|
465
|
+
background: color-mix(in srgb, var(--fail) 14%, transparent);
|
|
466
|
+
border-color: color-mix(in srgb, var(--fail) 32%, transparent);
|
|
467
|
+
}
|
|
468
|
+
.chip-default {
|
|
469
|
+
color: var(--muted);
|
|
470
|
+
font-size: 11px;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.gallery {
|
|
474
|
+
display: grid;
|
|
475
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
476
|
+
gap: 14px;
|
|
477
|
+
}
|
|
478
|
+
.tile {
|
|
479
|
+
display: block;
|
|
480
|
+
width: 100%;
|
|
481
|
+
text-align: left;
|
|
482
|
+
border: 1px solid var(--line);
|
|
483
|
+
border-radius: 10px;
|
|
484
|
+
overflow: hidden;
|
|
485
|
+
background: var(--panel);
|
|
486
|
+
}
|
|
487
|
+
.tile:hover {
|
|
488
|
+
border-color: var(--muted);
|
|
489
|
+
}
|
|
490
|
+
.thumb {
|
|
491
|
+
height: 150px;
|
|
492
|
+
overflow: hidden;
|
|
493
|
+
background: var(--stage);
|
|
494
|
+
border-bottom: 1px solid var(--line);
|
|
495
|
+
}
|
|
496
|
+
.thumb iframe {
|
|
497
|
+
width: 800px;
|
|
498
|
+
height: 500px;
|
|
499
|
+
border: 0;
|
|
500
|
+
transform: scale(0.3);
|
|
501
|
+
transform-origin: 0 0;
|
|
502
|
+
pointer-events: none;
|
|
503
|
+
}
|
|
504
|
+
.thumb .hint {
|
|
505
|
+
padding: 10px;
|
|
506
|
+
}
|
|
507
|
+
.cap {
|
|
508
|
+
display: flex;
|
|
509
|
+
align-items: center;
|
|
510
|
+
gap: 8px;
|
|
511
|
+
padding: 7px 10px;
|
|
512
|
+
min-width: 0;
|
|
513
|
+
}
|
|
514
|
+
.cap-name {
|
|
515
|
+
overflow: hidden;
|
|
516
|
+
text-overflow: ellipsis;
|
|
517
|
+
white-space: nowrap;
|
|
518
|
+
}
|
|
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/src/suite-key.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity of a sidebar selection: a module plus a suite path inside it.
|
|
3
|
+
* A module-level selection has an empty path, so its key ends with `::`.
|
|
4
|
+
*/
|
|
5
|
+
export function suiteKey(moduleId: string, path: string[]): string {
|
|
6
|
+
return `${moduleId}::${path.join(' > ')}`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The inverse of `suiteKey`. A key without a separator is read as a whole module. */
|
|
10
|
+
export function parseSuiteKey(key: string): { moduleId: string; path: string[] } {
|
|
11
|
+
const cut = key.indexOf('::')
|
|
12
|
+
if (cut < 0) {
|
|
13
|
+
return { moduleId: key, path: [] }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const tail = key.slice(cut + 2)
|
|
17
|
+
|
|
18
|
+
return { moduleId: key.slice(0, cut), path: tail ? tail.split(' > ') : [] }
|
|
19
|
+
}
|