@erclx/canon 5.14.0 → 5.15.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/README.md +3 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/draft-screencast/REQUIREMENT.md +2 -1
- package/claude/skills/draft-screencast/SKILL.md +14 -9
- package/claude/skills/read-frames/SKILL.md +3 -2
- package/claude/skills/record-screencast/REQUIREMENT.md +3 -0
- package/claude/skills/record-screencast/SKILL.md +28 -5
- package/claude/skills/review-pr/SKILL.md +1 -1
- package/claude/skills/review-pr/references/close-out.md +1 -1
- package/claude/skills/review-pr/references/post.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +10 -10
- package/claude/skills/role-orchestrator/scripts/poll.ts +628 -0
- package/claude/skills/role-orchestrator/scripts/repo.ts +69 -0
- package/claude/skills/role-orchestrator/scripts/watch.ts +267 -0
- package/docs/agents/commands.md +3 -0
- package/docs/agents/demo.md +34 -7
- package/docs/agents/migrate.md +1 -1
- package/docs/agents/pr-reads.md +1 -1
- package/docs/agents/sandbox.md +28 -3
- package/docs/agents/state-scoped-risk.md +2 -2
- package/docs/target/migrations.md +1 -1
- package/docs/workflow/ai-workflow.md +2 -3
- package/docs/workflow/skill-map.md +1 -1
- package/governance/renames.toml +7 -5
- package/governance/rules/canon/630-requirements.md +1 -1
- package/package.json +8 -6
- package/scripts/core/check-color-source.sh +11 -1
- package/scripts/core/install-check.sh +13 -4
- package/scripts/lib/sandbox-dispatch.sh +1 -1
- package/scripts/lib/sandbox-fixtures.sh +2 -2
- package/scripts/lib/sandbox-path.sh +1 -1
- package/scripts/manage-sandbox.sh +2 -2
- package/src/canvas/client/app.tsx +85 -0
- package/src/canvas/client/main.tsx +62 -0
- package/src/canvas/client/pages-panel.tsx +123 -0
- package/src/canvas/client/shell.css +319 -0
- package/src/canvas/client/state.ts +186 -0
- package/src/canvas/client/surface.tsx +213 -0
- package/src/canvas/content.ts +326 -0
- package/src/canvas/index.html +14 -0
- package/src/canvas/server.ts +302 -0
- package/src/canvas/tokens.ts +71 -0
- package/src/claude/seeds.ts +0 -1
- package/src/cli.ts +2 -0
- package/src/commands/canvas.ts +325 -0
- package/src/commands/claude.ts +9 -14
- package/src/commands/demo.ts +6 -6
- package/src/commands/sandbox.ts +110 -2
- package/src/context/citations.ts +1 -1
- package/src/context/classify/prompts.ts +1 -1
- package/src/context/folders.ts +0 -1
- package/src/demo/compile.ts +8 -4
- package/src/demo/container.ts +1 -1
- package/src/gov/renames.ts +3 -2
- package/src/help.ts +1 -0
- package/src/record-root.ts +1 -0
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/coverage.ts +1 -1
- package/src/sandbox/equivalence.ts +847 -0
- package/src/sandbox/expect.ts +0 -1
- package/src/serve/static.ts +25 -11
- package/src/shipped/references.ts +8 -10
- package/src/surface-root.ts +0 -1
- package/src/targets/pulls.ts +2 -2
- package/standards/architecture.md +1 -1
- package/standards/context.md +1 -1
- package/standards/design.md +1 -1
- package/standards/index.md +0 -1
- package/standards/requirements.md +4 -6
- package/standards/wireframes.md +2 -2
- package/tooling/claude/reference.md +0 -1
- package/tooling/claude/seeds/canon/REQUIREMENTS.md +0 -2
- package/tsconfig.json +1 -1
- package/claude/skills/role-orchestrator/scripts/poll.sh +0 -504
- package/claude/skills/role-orchestrator/scripts/watch.sh +0 -181
- package/governance/rules/canon/645-decisions.md +0 -12
- package/standards/decisions.md +0 -105
- package/tooling/claude/seeds/canon/decisions/index.md +0 -8
|
@@ -22,7 +22,7 @@ Usage: scripts/core/install-check.sh [--keep]
|
|
|
22
22
|
|
|
23
23
|
Verifies the published install path end to end:
|
|
24
24
|
1. Packs this repo into a tarball at .canon/tmp/install-check/pack
|
|
25
|
-
2. Extracts it and asserts
|
|
25
|
+
2. Extracts it and asserts sandbox is absent from the tree
|
|
26
26
|
3. Runs bun install --production in the extracted tree
|
|
27
27
|
4. Runs the CLI with --help from the extracted tree to confirm it resolves
|
|
28
28
|
5. Scaffolds a fresh project in .canon/tmp/install-check/target
|
|
@@ -51,6 +51,15 @@ mkdir -p "$PACK_DIR" "$EXTRACT_DIR"
|
|
|
51
51
|
|
|
52
52
|
open_timeline "Install verification"
|
|
53
53
|
|
|
54
|
+
# An absence check over a path the source tree no longer holds passes on
|
|
55
|
+
# nothing, so the excluded tree has to exist here before its absence below can
|
|
56
|
+
# mean the publish left it out.
|
|
57
|
+
log_step "Assert excluded path exists in source"
|
|
58
|
+
if [ ! -f "$PROJECT_ROOT/sandbox/run.sh" ]; then
|
|
59
|
+
log_error "sandbox/run.sh is missing from the source tree, so the absence check below would pass on nothing"
|
|
60
|
+
fi
|
|
61
|
+
log_info "Present: sandbox/run.sh"
|
|
62
|
+
|
|
54
63
|
log_step "Pack"
|
|
55
64
|
TARBALL_PATH="$(cd "$PROJECT_ROOT" && bun pm pack --quiet --ignore-scripts --destination "$PACK_DIR" | tail -n 1)"
|
|
56
65
|
log_info "Packed to $TARBALL_PATH"
|
|
@@ -60,10 +69,10 @@ tar -xzf "$TARBALL_PATH" -C "$EXTRACT_DIR" --strip-components=1
|
|
|
60
69
|
log_info "Extracted to $EXTRACT_DIR"
|
|
61
70
|
|
|
62
71
|
log_step "Assert excluded path is absent"
|
|
63
|
-
if [ -e "$EXTRACT_DIR/
|
|
64
|
-
log_error "
|
|
72
|
+
if [ -e "$EXTRACT_DIR/sandbox" ]; then
|
|
73
|
+
log_error "sandbox shipped in the tarball, and the files field in package.json leaves it out"
|
|
65
74
|
fi
|
|
66
|
-
log_info "Absent:
|
|
75
|
+
log_info "Absent: sandbox"
|
|
67
76
|
|
|
68
77
|
log_step "Install dependencies (production)"
|
|
69
78
|
(cd "$EXTRACT_DIR" && bun install --production --ignore-scripts --silent 2>&1 | pipe_output) || log_error "bun install --production failed"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# them, so the escape watch reported clean throughout and was right to. The run
|
|
10
10
|
# spent real cost until a person found it, and `SIGTERM` alone did not end it.
|
|
11
11
|
#
|
|
12
|
-
# Three mechanisms sit here rather than in `
|
|
12
|
+
# Three mechanisms sit here rather than in `sandbox/run.sh`, which is the
|
|
13
13
|
# only caller. Each is reachable from a test that never launches a session:
|
|
14
14
|
# `src/sandbox-dispatch.test.ts` drives the shim against a stub binary and the
|
|
15
15
|
# reap against a `sleep`, where proving either through `run.sh` would mean
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
FIXTURE_SUFFIX=".fixture"
|
|
7
7
|
|
|
8
8
|
# The tree mirrors the scenario path, so `category` and `scenario` together match
|
|
9
|
-
# `
|
|
9
|
+
# `sandbox/<category>/<scenario>.sh`. Both segments are load-bearing:
|
|
10
10
|
# four scenario basenames repeat across categories (`claude`, `docs`, `review`,
|
|
11
11
|
# `sync`), and `docs` is also a category of its own.
|
|
12
12
|
fixture_stage_dir() {
|
|
@@ -14,7 +14,7 @@ fixture_stage_dir() {
|
|
|
14
14
|
local scenario="$2"
|
|
15
15
|
local arm="$3"
|
|
16
16
|
local stage="$4"
|
|
17
|
-
echo "$PROJECT_ROOT/
|
|
17
|
+
echo "$PROJECT_ROOT/sandbox/fixtures/$category/$scenario/$arm/$stage"
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
list_fixture_files() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
# The sandbox tree lives outside the toolkit worktree. `
|
|
3
|
+
# The sandbox tree lives outside the toolkit worktree. `sandbox/run.sh`
|
|
4
4
|
# sets cwd to it for `claude -p`, and every `CLAUDE.md` between that cwd and the
|
|
5
5
|
# filesystem root loads into the session. Under the repository the toolkit's own
|
|
6
6
|
# instructions join that chain beside the seeded copy the scenario installed,
|
|
@@ -40,7 +40,7 @@ show_help() {
|
|
|
40
40
|
exit 0
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
ANCHOR_FIXTURE_DIR="$PROJECT_ROOT/
|
|
43
|
+
ANCHOR_FIXTURE_DIR="$PROJECT_ROOT/sandbox/fixtures/anchor/create"
|
|
44
44
|
|
|
45
45
|
# Where an anchor scenario's tree comes from and where its remote points are two
|
|
46
46
|
# concerns, and only the remote needs a network. Taking the tree from a fixture
|
|
@@ -438,7 +438,7 @@ main() {
|
|
|
438
438
|
fi
|
|
439
439
|
|
|
440
440
|
SANDBOX="$(resolve_sandbox_dir)"
|
|
441
|
-
SANDBOX_DIR="$PROJECT_ROOT/
|
|
441
|
+
SANDBOX_DIR="$PROJECT_ROOT/sandbox"
|
|
442
442
|
|
|
443
443
|
local unsafe
|
|
444
444
|
if ! unsafe="$(assert_sandbox_dir_safe "$SANDBOX" "$PROJECT_ROOT")"; then
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from 'preact'
|
|
3
|
+
import { useRef } from 'preact/hooks'
|
|
4
|
+
import { PagesPanel } from '@/canvas/client/pages-panel'
|
|
5
|
+
import { currentPage, focusFrame, tokens } from '@/canvas/client/state'
|
|
6
|
+
import { Surface } from '@/canvas/client/surface'
|
|
7
|
+
|
|
8
|
+
const SOURCE_LABEL = {
|
|
9
|
+
toolkit: 'This toolkit',
|
|
10
|
+
installed: 'Installed design',
|
|
11
|
+
none: 'None',
|
|
12
|
+
} as const
|
|
13
|
+
|
|
14
|
+
function TokenDetails(): JSX.Element | null {
|
|
15
|
+
const value = tokens.value
|
|
16
|
+
if (!value) return null
|
|
17
|
+
return (
|
|
18
|
+
<section aria-labelledby="tokens-heading">
|
|
19
|
+
<h2 id="tokens-heading" class="section-label">
|
|
20
|
+
Tokens
|
|
21
|
+
</h2>
|
|
22
|
+
<p class="detail">{SOURCE_LABEL[value.source]}</p>
|
|
23
|
+
{value.files && value.files.length > 0 ? (
|
|
24
|
+
<ul class="files">
|
|
25
|
+
{value.files.map((file) => (
|
|
26
|
+
<li key={file} title={file}>
|
|
27
|
+
<code>{file}</code>
|
|
28
|
+
</li>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
) : null}
|
|
32
|
+
{value.notice ? <p class="notice">{value.notice}</p> : null}
|
|
33
|
+
</section>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function PageDetails(): JSX.Element | null {
|
|
38
|
+
const page = currentPage.value
|
|
39
|
+
if (!page) return null
|
|
40
|
+
const unplaced = page.frames.filter((frame) => !frame.placed).length
|
|
41
|
+
return (
|
|
42
|
+
<section aria-labelledby="page-heading">
|
|
43
|
+
<h2 id="page-heading" class="section-label">
|
|
44
|
+
Page
|
|
45
|
+
</h2>
|
|
46
|
+
<p class="detail" title={page.name}>
|
|
47
|
+
{page.name}
|
|
48
|
+
</p>
|
|
49
|
+
<p class="detail-meta">
|
|
50
|
+
{page.frames.length} {page.frames.length === 1 ? 'frame' : 'frames'}
|
|
51
|
+
</p>
|
|
52
|
+
{unplaced > 0 ? (
|
|
53
|
+
<p class="notice">
|
|
54
|
+
{unplaced} {unplaced === 1 ? 'frame has' : 'frames have'} no box in
|
|
55
|
+
layout.json and sit in a default row.
|
|
56
|
+
</p>
|
|
57
|
+
) : null}
|
|
58
|
+
{page.layoutIssue === 'malformed' ? (
|
|
59
|
+
<p class="notice" role="alert">
|
|
60
|
+
layout.json does not parse, so every frame sits in a default row. Fix
|
|
61
|
+
the file to restore their places.
|
|
62
|
+
</p>
|
|
63
|
+
) : null}
|
|
64
|
+
</section>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function App(): JSX.Element {
|
|
69
|
+
const viewportRef = useRef<HTMLDivElement>(null)
|
|
70
|
+
return (
|
|
71
|
+
<div class="shell">
|
|
72
|
+
<PagesPanel
|
|
73
|
+
onFocusFrame={(frame) => {
|
|
74
|
+
const rect = viewportRef.current?.getBoundingClientRect()
|
|
75
|
+
focusFrame(frame, rect?.width ?? 0, rect?.height ?? 0)
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
<Surface viewportRef={viewportRef} />
|
|
79
|
+
<aside class="panel panel-right" aria-label="Details">
|
|
80
|
+
<PageDetails />
|
|
81
|
+
<TokenDetails />
|
|
82
|
+
</aside>
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import { effect } from '@preact/signals'
|
|
3
|
+
import { render } from 'preact'
|
|
4
|
+
import { App } from '@/canvas/client/app'
|
|
5
|
+
import {
|
|
6
|
+
applyChange,
|
|
7
|
+
type ChangeEvent,
|
|
8
|
+
loadPages,
|
|
9
|
+
systemTheme,
|
|
10
|
+
theme,
|
|
11
|
+
themeChoice,
|
|
12
|
+
type Theme,
|
|
13
|
+
} from '@/canvas/client/state'
|
|
14
|
+
|
|
15
|
+
const THEME_KEY = 'canon-canvas-theme'
|
|
16
|
+
|
|
17
|
+
function readStoredTheme(): Theme | undefined {
|
|
18
|
+
try {
|
|
19
|
+
const stored = localStorage.getItem(THEME_KEY)
|
|
20
|
+
return stored === 'light' || stored === 'dark' ? stored : undefined
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function storeTheme(value: Theme | undefined): void {
|
|
27
|
+
try {
|
|
28
|
+
if (value === undefined) localStorage.removeItem(THEME_KEY)
|
|
29
|
+
else localStorage.setItem(THEME_KEY, value)
|
|
30
|
+
} catch {
|
|
31
|
+
/* A blocked store costs the remembered pick, nothing else. */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const darkQuery = matchMedia('(prefers-color-scheme: dark)')
|
|
36
|
+
systemTheme.value = darkQuery.matches ? 'dark' : 'light'
|
|
37
|
+
darkQuery.addEventListener('change', (event) => {
|
|
38
|
+
systemTheme.value = event.matches ? 'dark' : 'light'
|
|
39
|
+
})
|
|
40
|
+
themeChoice.value = readStoredTheme()
|
|
41
|
+
|
|
42
|
+
effect(() => {
|
|
43
|
+
document.documentElement.dataset.theme = theme.value
|
|
44
|
+
document.documentElement.style.colorScheme = theme.value
|
|
45
|
+
})
|
|
46
|
+
effect(() => storeTheme(themeChoice.value))
|
|
47
|
+
|
|
48
|
+
/* The chrome reads this toolkit's own tokens, served beside the shell. */
|
|
49
|
+
const chrome = document.createElement('link')
|
|
50
|
+
chrome.rel = 'stylesheet'
|
|
51
|
+
chrome.href = '/api/chrome.css'
|
|
52
|
+
document.head.prepend(chrome)
|
|
53
|
+
|
|
54
|
+
const events = new EventSource('/api/events')
|
|
55
|
+
events.addEventListener('message', (message) => {
|
|
56
|
+
void applyChange(JSON.parse(message.data) as ChangeEvent)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
void loadPages()
|
|
60
|
+
|
|
61
|
+
const mount = document.getElementById('app')
|
|
62
|
+
if (mount) render(<App />, mount)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from 'preact'
|
|
3
|
+
import type { Frame } from '@/canvas/content'
|
|
4
|
+
import {
|
|
5
|
+
currentPage,
|
|
6
|
+
isLoaded,
|
|
7
|
+
loadError,
|
|
8
|
+
pages,
|
|
9
|
+
selectedPage,
|
|
10
|
+
theme,
|
|
11
|
+
toggleTheme,
|
|
12
|
+
} from '@/canvas/client/state'
|
|
13
|
+
|
|
14
|
+
export interface PagesPanelProps {
|
|
15
|
+
/** Brings a frame into view on the surface. */
|
|
16
|
+
readonly onFocusFrame: (frame: Frame) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ThemeToggle(): JSX.Element {
|
|
20
|
+
const next = theme.value === 'dark' ? 'light' : 'dark'
|
|
21
|
+
return (
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="icon-button"
|
|
25
|
+
aria-label={`Switch to ${next} theme`}
|
|
26
|
+
title={`Switch to ${next} theme`}
|
|
27
|
+
onClick={toggleTheme}
|
|
28
|
+
>
|
|
29
|
+
<span aria-hidden="true">{theme.value === 'dark' ? '☾' : '☀'}</span>
|
|
30
|
+
</button>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function PageList(): JSX.Element {
|
|
35
|
+
if (pages.value.length === 0) {
|
|
36
|
+
return (
|
|
37
|
+
<p class="empty">
|
|
38
|
+
No pages yet. Run <code>canon canvas page add <name></code> to add
|
|
39
|
+
one.
|
|
40
|
+
</p>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
const current = currentPage.value?.name
|
|
44
|
+
return (
|
|
45
|
+
<ul class="list" aria-label="Pages">
|
|
46
|
+
{pages.value.map((page) => (
|
|
47
|
+
<li key={page.name}>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
class="row"
|
|
51
|
+
aria-current={page.name === current ? 'page' : undefined}
|
|
52
|
+
title={page.name}
|
|
53
|
+
onClick={() => {
|
|
54
|
+
selectedPage.value = page.name
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<span class="row-label">{page.name}</span>
|
|
58
|
+
<span class="row-meta">{page.frames.length}</span>
|
|
59
|
+
</button>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function FrameList({ onFocusFrame }: PagesPanelProps): JSX.Element | null {
|
|
67
|
+
const page = currentPage.value
|
|
68
|
+
if (!page) return null
|
|
69
|
+
if (page.frames.length === 0) {
|
|
70
|
+
return (
|
|
71
|
+
<p class="empty">
|
|
72
|
+
No frames on this page. Run{' '}
|
|
73
|
+
<code>canon canvas frame add {page.name} <name></code> to add one.
|
|
74
|
+
</p>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return (
|
|
78
|
+
<ul class="list" aria-label="Frames">
|
|
79
|
+
{page.frames.map((frame) => (
|
|
80
|
+
<li key={frame.name}>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="row"
|
|
84
|
+
title={frame.name}
|
|
85
|
+
onClick={() => onFocusFrame(frame)}
|
|
86
|
+
>
|
|
87
|
+
<span class="row-label">{frame.name}</span>
|
|
88
|
+
<span class="row-meta">{frame.width}</span>
|
|
89
|
+
</button>
|
|
90
|
+
</li>
|
|
91
|
+
))}
|
|
92
|
+
</ul>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function PagesPanel(props: PagesPanelProps): JSX.Element {
|
|
97
|
+
return (
|
|
98
|
+
<nav class="panel panel-left" aria-label="Pages and frames">
|
|
99
|
+
<header class="panel-head">
|
|
100
|
+
<span class="brand">Canvas</span>
|
|
101
|
+
<ThemeToggle />
|
|
102
|
+
</header>
|
|
103
|
+
{loadError.value ? (
|
|
104
|
+
<p class="empty" role="alert">
|
|
105
|
+
{loadError.value}
|
|
106
|
+
</p>
|
|
107
|
+
) : !isLoaded.value ? (
|
|
108
|
+
<p class="empty">Loading</p>
|
|
109
|
+
) : (
|
|
110
|
+
<>
|
|
111
|
+
<h2 class="section-label">Pages</h2>
|
|
112
|
+
<PageList />
|
|
113
|
+
{currentPage.value ? (
|
|
114
|
+
<>
|
|
115
|
+
<h2 class="section-label">Frames</h2>
|
|
116
|
+
<FrameList {...props} />
|
|
117
|
+
</>
|
|
118
|
+
) : null}
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
</nav>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The canvas chrome. Every value reads a custom property from this toolkit's
|
|
3
|
+
* own token module, served at /api/chrome.css, which maps dark onto :root and
|
|
4
|
+
* light onto [data-theme='light']. A frame reads the project's tokens inside
|
|
5
|
+
* its own iframe, so the two sets of names never meet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
html,
|
|
13
|
+
body {
|
|
14
|
+
height: 100%;
|
|
15
|
+
margin: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
background: var(--color-background);
|
|
20
|
+
color: var(--color-text);
|
|
21
|
+
font-family: var(--type-body-family);
|
|
22
|
+
font-size: var(--type-label-size);
|
|
23
|
+
line-height: var(--type-label-lh);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#app {
|
|
27
|
+
height: 100%;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:focus-visible {
|
|
31
|
+
outline: 2px solid var(--color-accent);
|
|
32
|
+
outline-offset: 2px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
code {
|
|
36
|
+
font-family: var(--type-code-family);
|
|
37
|
+
font-size: var(--type-code-size);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.shell {
|
|
41
|
+
display: grid;
|
|
42
|
+
grid-template-columns: 15rem minmax(0, 1fr) 13rem;
|
|
43
|
+
height: 100%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.panel {
|
|
47
|
+
overflow-y: auto;
|
|
48
|
+
padding: var(--space-md);
|
|
49
|
+
background: var(--color-chrome);
|
|
50
|
+
min-width: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.panel-left {
|
|
54
|
+
border-right: 1px solid var(--color-border);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.panel-right {
|
|
58
|
+
border-left: 1px solid var(--color-border);
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: var(--space-lg);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.panel-head {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: space-between;
|
|
68
|
+
margin-bottom: var(--space-md);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.brand {
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
color: var(--color-text);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.section-label {
|
|
77
|
+
margin: var(--space-md) 0 var(--space-xs);
|
|
78
|
+
font-size: var(--type-label-size);
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
color: var(--color-text-secondary);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.panel-right .section-label {
|
|
84
|
+
margin-top: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.list {
|
|
88
|
+
list-style: none;
|
|
89
|
+
margin: 0;
|
|
90
|
+
padding: 2px;
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: 2px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.row {
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: space-between;
|
|
100
|
+
gap: var(--space-sm);
|
|
101
|
+
width: 100%;
|
|
102
|
+
min-height: 28px;
|
|
103
|
+
padding: var(--space-xs) var(--space-sm);
|
|
104
|
+
border: 0;
|
|
105
|
+
border-radius: var(--radius-action);
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: var(--color-text-body);
|
|
108
|
+
font: inherit;
|
|
109
|
+
text-align: left;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.row:hover {
|
|
114
|
+
background: var(--color-surface);
|
|
115
|
+
color: var(--color-text);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.row:active {
|
|
119
|
+
background: var(--color-border);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.row[aria-current='page'] {
|
|
123
|
+
background: var(--color-surface);
|
|
124
|
+
color: var(--color-text);
|
|
125
|
+
box-shadow: inset 2px 0 0 var(--color-accent);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.row-label {
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
text-overflow: ellipsis;
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.row-meta {
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
color: var(--color-muted);
|
|
137
|
+
font-variant-numeric: tabular-nums;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.empty,
|
|
141
|
+
.notice {
|
|
142
|
+
margin: var(--space-xs) 0;
|
|
143
|
+
color: var(--color-text-secondary);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.notice {
|
|
147
|
+
border-left: 2px solid var(--color-accent);
|
|
148
|
+
padding-left: var(--space-sm);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.detail {
|
|
152
|
+
margin: 0;
|
|
153
|
+
color: var(--color-text);
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
text-overflow: ellipsis;
|
|
156
|
+
white-space: nowrap;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.detail-meta {
|
|
160
|
+
margin: 0;
|
|
161
|
+
color: var(--color-muted);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.files {
|
|
165
|
+
list-style: none;
|
|
166
|
+
margin: var(--space-xs) 0 0;
|
|
167
|
+
padding: 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.files li {
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
text-overflow: ellipsis;
|
|
173
|
+
white-space: nowrap;
|
|
174
|
+
color: var(--color-text-body);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.icon-button,
|
|
178
|
+
.text-button {
|
|
179
|
+
min-width: 28px;
|
|
180
|
+
min-height: 28px;
|
|
181
|
+
padding: 0 var(--space-sm);
|
|
182
|
+
border: 1px solid var(--color-border);
|
|
183
|
+
border-radius: var(--radius-action);
|
|
184
|
+
background: var(--color-surface);
|
|
185
|
+
color: var(--color-text);
|
|
186
|
+
font: inherit;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.icon-button:hover,
|
|
191
|
+
.text-button:hover {
|
|
192
|
+
border-color: var(--color-text-secondary);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.icon-button:active,
|
|
196
|
+
.text-button:active {
|
|
197
|
+
background: var(--color-border);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.surface {
|
|
201
|
+
position: relative;
|
|
202
|
+
min-width: 0;
|
|
203
|
+
overflow: hidden;
|
|
204
|
+
background: var(--color-background);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.viewport {
|
|
208
|
+
position: absolute;
|
|
209
|
+
inset: 0;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
cursor: grab;
|
|
212
|
+
touch-action: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.viewport:active {
|
|
216
|
+
cursor: grabbing;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.plane {
|
|
220
|
+
position: absolute;
|
|
221
|
+
top: 0;
|
|
222
|
+
left: 0;
|
|
223
|
+
transform-origin: 0 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.frame {
|
|
227
|
+
position: absolute;
|
|
228
|
+
margin: 0;
|
|
229
|
+
cursor: default;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.frame-label {
|
|
233
|
+
position: absolute;
|
|
234
|
+
bottom: 100%;
|
|
235
|
+
left: 0;
|
|
236
|
+
right: 0;
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: var(--space-sm);
|
|
240
|
+
padding-bottom: var(--space-sm);
|
|
241
|
+
font-size: 1.5rem;
|
|
242
|
+
color: var(--color-text-secondary);
|
|
243
|
+
white-space: nowrap;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.frame-name {
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
text-overflow: ellipsis;
|
|
249
|
+
color: var(--color-text);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.frame-size {
|
|
253
|
+
color: var(--color-muted);
|
|
254
|
+
font-variant-numeric: tabular-nums;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.frame-theme {
|
|
258
|
+
margin-left: auto;
|
|
259
|
+
padding: 2px var(--space-sm);
|
|
260
|
+
border: 1px solid var(--color-border);
|
|
261
|
+
border-radius: var(--radius-pill);
|
|
262
|
+
background: var(--color-surface);
|
|
263
|
+
color: var(--color-text-secondary);
|
|
264
|
+
font: inherit;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.frame-theme:hover {
|
|
269
|
+
color: var(--color-text);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.frame iframe {
|
|
273
|
+
display: block;
|
|
274
|
+
border: 1px solid var(--color-border);
|
|
275
|
+
border-radius: 2px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.toolbar {
|
|
279
|
+
position: absolute;
|
|
280
|
+
right: var(--space-md);
|
|
281
|
+
bottom: var(--space-md);
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: var(--space-xs);
|
|
285
|
+
padding: var(--space-xs);
|
|
286
|
+
border: 1px solid var(--color-border);
|
|
287
|
+
border-radius: var(--radius-panel);
|
|
288
|
+
background: var(--color-chrome);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.zoom {
|
|
292
|
+
min-width: 3.5rem;
|
|
293
|
+
text-align: center;
|
|
294
|
+
font-variant-numeric: tabular-nums;
|
|
295
|
+
color: var(--color-text-secondary);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@media (max-width: 900px) {
|
|
299
|
+
.shell {
|
|
300
|
+
grid-template-columns: 12rem minmax(0, 1fr);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.panel-right {
|
|
304
|
+
display: none;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@media (max-width: 600px) {
|
|
309
|
+
.shell {
|
|
310
|
+
grid-template-columns: minmax(0, 1fr);
|
|
311
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.panel-left {
|
|
315
|
+
max-height: 40vh;
|
|
316
|
+
border-right: 0;
|
|
317
|
+
border-bottom: 1px solid var(--color-border);
|
|
318
|
+
}
|
|
319
|
+
}
|