@erclx/canon 5.52.0 → 5.54.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/CHANGELOG.md +17 -0
- package/README.md +11 -13
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -8
- package/claude/skills/context-fold/REQUIREMENT.md +0 -1
- package/claude/skills/design-identity/REQUIREMENT.md +2 -2
- package/claude/skills/design-identity/SKILL.md +6 -6
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +4 -4
- package/claude/skills/design-record/references/folder.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/references/kinds.md +0 -1
- package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
- package/claude/skills/draw-figure/SKILL.md +5 -6
- package/claude/skills/draw-figure/references/figures.md +0 -1
- package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
- package/claude/skills/pick-by-eye/SKILL.md +34 -41
- package/claude/skills/pick-by-eye/references/board.md +42 -0
- package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
- package/claude/skills/teach-craft/SKILL.md +70 -0
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
- package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
- package/docs/agents/audits.md +0 -1
- package/docs/agents/install-and-sync.md +0 -8
- package/docs/target/migrations.md +16 -2
- package/docs/target/projects.md +1 -1
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +10 -22
- package/docs/workflow/skill-map.md +11 -14
- package/docs/workflow/visual-design-workflow.md +1 -4
- package/governance/detect.toml +1 -6
- package/governance/renames.toml +0 -1
- package/package.json +2 -8
- package/scripts/core/regen-web-previews.ts +2 -67
- package/src/audits/catalog.ts +0 -1
- package/src/browser/engine.ts +3 -6
- package/src/capture/render.ts +1 -1
- package/src/claude/cases/authoring.ts +0 -12
- package/src/claude/cases/misc.ts +0 -22
- package/src/claude/cases/workflow.ts +2 -38
- package/src/claude/skills-audit.ts +1 -1
- package/src/cli/help.ts +0 -10
- package/src/cli.ts +0 -8
- package/src/commands/design.ts +16 -315
- package/src/commands/records.ts +0 -1
- package/src/commands/serve.ts +2 -2
- package/src/commands/sync.ts +4 -5
- package/src/design/components.ts +8 -1514
- package/src/design/css.ts +5 -6
- package/src/design/fonts.ts +3 -5
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/driver/drive.ts +3 -4
- package/src/driver/steps.ts +2 -5
- package/src/gate/stages.ts +4 -10
- package/src/gov/audit.ts +1 -6
- package/src/inventory/walk.ts +1 -1
- package/src/markdown/scope.ts +6 -35
- package/src/records/validate.ts +4 -176
- package/src/sandbox/equivalence.ts +0 -5
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +1 -1
- package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
- package/claude/skills/canvas-board/SKILL.md +0 -143
- package/claude/skills/canvas-board/references/canvas.md +0 -170
- package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
- package/claude/skills/draft-and-pick/SKILL.md +0 -11
- package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
- package/claude/skills/draft-doc/SKILL.md +0 -11
- package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
- package/claude/skills/draft-figure/SKILL.md +0 -11
- package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
- package/claude/skills/draft-identity/SKILL.md +0 -11
- package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
- package/claude/skills/draft-ready/SKILL.md +0 -11
- package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
- package/claude/skills/draft-screencast/SKILL.md +0 -11
- package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
- package/claude/skills/draft-slides/SKILL.md +0 -11
- package/claude/skills/make-deck/REQUIREMENT.md +0 -36
- package/claude/skills/make-deck/SKILL.md +0 -80
- package/claude/skills/make-deck/references/deck.md +0 -183
- package/claude/skills/read-frames/REQUIREMENT.md +0 -30
- package/claude/skills/read-frames/SKILL.md +0 -55
- package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
- package/claude/skills/record-screencast/SKILL.md +0 -99
- package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
- package/claude/skills/script-screencast/SKILL.md +0 -70
- package/claude/skills/script-screencast/references/beats-template.md +0 -119
- package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
- package/claude/skills/teach-workspace/SKILL.md +0 -196
- package/claude/skills/teach-workspace/references/glossary.md +0 -90
- package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
- package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
- package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
- package/claude/skills/teach-workspace/references/promotion.md +0 -116
- package/claude/skills/teach-workspace/references/revisit.md +0 -28
- package/claude/skills/teach-workspace/references/teach.md +0 -211
- package/claude/skills/video-craft/REQUIREMENT.md +0 -35
- package/claude/skills/video-craft/SKILL.md +0 -96
- package/claude/skills/video-craft/references/adopted.md +0 -110
- package/governance/rules/ui/canvas.md +0 -13
- package/src/canvas/address.ts +0 -227
- package/src/canvas/arrange.ts +0 -66
- package/src/canvas/capture.ts +0 -391
- package/src/canvas/client/app.tsx +0 -188
- package/src/canvas/client/inspector/align-grid.tsx +0 -162
- package/src/canvas/client/inspector/color-field.tsx +0 -398
- package/src/canvas/client/inspector/color-inputs.tsx +0 -139
- package/src/canvas/client/inspector/color-picker.tsx +0 -668
- package/src/canvas/client/inspector/color-space.ts +0 -240
- package/src/canvas/client/inspector/field.tsx +0 -263
- package/src/canvas/client/inspector/fields.css +0 -763
- package/src/canvas/client/inspector/section.tsx +0 -27
- package/src/canvas/client/inspector/size-field.tsx +0 -69
- package/src/canvas/client/inspector/token-select.tsx +0 -109
- package/src/canvas/client/inspector/values.ts +0 -297
- package/src/canvas/client/inspector.tsx +0 -692
- package/src/canvas/client/keys.ts +0 -71
- package/src/canvas/client/layers.tsx +0 -118
- package/src/canvas/client/main.tsx +0 -71
- package/src/canvas/client/pages-panel.tsx +0 -212
- package/src/canvas/client/panel-handle.tsx +0 -90
- package/src/canvas/client/selection.tsx +0 -199
- package/src/canvas/client/shell.css +0 -762
- package/src/canvas/client/state.ts +0 -792
- package/src/canvas/client/surface.tsx +0 -803
- package/src/canvas/client/theme-panel.tsx +0 -55
- package/src/canvas/client/tools.tsx +0 -153
- package/src/canvas/content.ts +0 -781
- package/src/canvas/edit.ts +0 -575
- package/src/canvas/editing.ts +0 -203
- package/src/canvas/fit.ts +0 -238
- package/src/canvas/global.d.ts +0 -15
- package/src/canvas/history.ts +0 -116
- package/src/canvas/index.html +0 -20
- package/src/canvas/server.ts +0 -766
- package/src/canvas/tokens.ts +0 -221
- package/src/commands/canvas.ts +0 -1097
- package/src/commands/demo.ts +0 -542
- package/src/commands/slides.ts +0 -184
- package/src/commands/teach.ts +0 -1401
- package/src/demo/beats.ts +0 -135
- package/src/demo/compile.ts +0 -350
- package/src/demo/container.ts +0 -219
- package/src/demo/cursors.ts +0 -55
- package/src/demo/drive.ts +0 -442
- package/src/demo/pointer.ts +0 -178
- package/src/demo/theme.ts +0 -112
- package/src/demo/timeline.ts +0 -64
- package/src/design/adapter.ts +0 -59
- package/src/design/base.css +0 -203
- package/src/design/brand-favicon.ts +0 -18
- package/src/design/neutral.ts +0 -127
- package/src/design/project-fonts.ts +0 -392
- package/src/slides/convert/chart.ts +0 -165
- package/src/slides/convert/deck.ts +0 -244
- package/src/slides/convert/export.ts +0 -567
- package/src/slides/convert/master.ts +0 -198
- package/src/slides/convert/shapes.ts +0 -804
- package/src/slides/convert/svg.ts +0 -38
- package/src/slides/convert/walk.ts +0 -671
- package/src/slides/decks.ts +0 -110
- package/src/slides/open.ts +0 -18
- package/src/slides/package/fonts.ts +0 -301
- package/src/slides/package/motion.ts +0 -316
- package/src/slides/package/test-face.ts +0 -113
- package/src/teach/browser/compile.ts +0 -44
- package/src/teach/browser/dismiss.ts +0 -76
- package/src/teach/browser/focus-line.ts +0 -19
- package/src/teach/browser/glossary-filter.ts +0 -44
- package/src/teach/browser/head.ts +0 -30
- package/src/teach/browser/quiz.ts +0 -19
- package/src/teach/browser/reload.ts +0 -9
- package/src/teach/browser/sidebar.ts +0 -233
- package/src/teach/browser/theme.ts +0 -18
- package/src/teach/components/code.tsx +0 -24
- package/src/teach/components/heading.tsx +0 -11
- package/src/teach/components/list.tsx +0 -17
- package/src/teach/components/paragraph.tsx +0 -29
- package/src/teach/components/refs.tsx +0 -28
- package/src/teach/cross-references.ts +0 -78
- package/src/teach/fonts.ts +0 -16
- package/src/teach/highlight.ts +0 -71
- package/src/teach/html/jsx-dev-runtime.ts +0 -13
- package/src/teach/html/jsx-runtime.ts +0 -113
- package/src/teach/html/raw.ts +0 -28
- package/src/teach/lesson.ts +0 -178
- package/src/teach/nav.ts +0 -1337
- package/src/teach/render.ts +0 -240
- package/src/teach/workspace.ts +0 -1164
- package/src/teach/write-if-changed.ts +0 -17
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource preact */
|
|
2
|
-
import { effect } from '@preact/signals'
|
|
3
|
-
import type { JSX } from 'preact'
|
|
4
|
-
import { useLayoutEffect, useRef } from 'preact/hooks'
|
|
5
|
-
import { Inspector } from '@/canvas/client/inspector'
|
|
6
|
-
import { PagesPanel } from '@/canvas/client/pages-panel'
|
|
7
|
-
import { PanelHandle } from '@/canvas/client/panel-handle'
|
|
8
|
-
import {
|
|
9
|
-
clampPanelWidth,
|
|
10
|
-
currentPage,
|
|
11
|
-
DEFAULT_PANEL_WIDTHS,
|
|
12
|
-
focusFrame,
|
|
13
|
-
type PanelWidths,
|
|
14
|
-
panelsHidden,
|
|
15
|
-
panelWidths,
|
|
16
|
-
tokens,
|
|
17
|
-
} from '@/canvas/client/state'
|
|
18
|
-
import { Surface } from '@/canvas/client/surface'
|
|
19
|
-
|
|
20
|
-
const SOURCE_LABEL = {
|
|
21
|
-
toolkit: 'This toolkit',
|
|
22
|
-
installed: 'Installed design',
|
|
23
|
-
none: 'None',
|
|
24
|
-
} as const
|
|
25
|
-
|
|
26
|
-
function TokenDetails(): JSX.Element | null {
|
|
27
|
-
const value = tokens.value
|
|
28
|
-
if (!value) return null
|
|
29
|
-
return (
|
|
30
|
-
<section aria-labelledby="tokens-heading">
|
|
31
|
-
<h2 id="tokens-heading" class="section-label">
|
|
32
|
-
Tokens
|
|
33
|
-
</h2>
|
|
34
|
-
<p class="detail">{SOURCE_LABEL[value.source]}</p>
|
|
35
|
-
{value.files && value.files.length > 0 ? (
|
|
36
|
-
<ul class="files">
|
|
37
|
-
{value.files.map((file) => (
|
|
38
|
-
<li key={file} title={file}>
|
|
39
|
-
<code>{file}</code>
|
|
40
|
-
</li>
|
|
41
|
-
))}
|
|
42
|
-
</ul>
|
|
43
|
-
) : null}
|
|
44
|
-
{value.notice ? <p class="notice">{value.notice}</p> : null}
|
|
45
|
-
</section>
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function PageDetails(): JSX.Element | null {
|
|
50
|
-
const page = currentPage.value
|
|
51
|
-
if (!page) return null
|
|
52
|
-
const unplaced = page.frames.filter((frame) => !frame.placed).length
|
|
53
|
-
return (
|
|
54
|
-
<section aria-labelledby="page-heading">
|
|
55
|
-
<h2 id="page-heading" class="section-label">
|
|
56
|
-
Page
|
|
57
|
-
</h2>
|
|
58
|
-
<p class="detail" title={page.name}>
|
|
59
|
-
{page.name}
|
|
60
|
-
</p>
|
|
61
|
-
<p class="detail-meta">
|
|
62
|
-
{page.frames.length} {page.frames.length === 1 ? 'frame' : 'frames'}
|
|
63
|
-
</p>
|
|
64
|
-
{unplaced > 0 ? (
|
|
65
|
-
<p class="notice">
|
|
66
|
-
{unplaced} {unplaced === 1 ? 'frame has' : 'frames have'} no box in
|
|
67
|
-
layout.json and sit in a default row.
|
|
68
|
-
</p>
|
|
69
|
-
) : null}
|
|
70
|
-
{page.layoutIssue === 'malformed' ? (
|
|
71
|
-
<p class="notice" role="alert">
|
|
72
|
-
layout.json does not parse, so every frame sits in a default row. Fix
|
|
73
|
-
the file to restore their places.
|
|
74
|
-
</p>
|
|
75
|
-
) : null}
|
|
76
|
-
</section>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const PANELS_KEY = 'canon-canvas-panels'
|
|
81
|
-
|
|
82
|
-
interface StoredPanels extends PanelWidths {
|
|
83
|
-
readonly hidden: boolean
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function storedWidth(value: unknown, fallback: number): number {
|
|
87
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** What the last visit left, with anything unreadable read as the default. */
|
|
91
|
-
function readStoredPanels(): StoredPanels {
|
|
92
|
-
const fallback = { hidden: false, ...DEFAULT_PANEL_WIDTHS }
|
|
93
|
-
try {
|
|
94
|
-
const parsed: unknown = JSON.parse(localStorage.getItem(PANELS_KEY) ?? '{}')
|
|
95
|
-
if (typeof parsed !== 'object' || parsed === null) return fallback
|
|
96
|
-
const record = parsed as Record<string, unknown>
|
|
97
|
-
return {
|
|
98
|
-
hidden: record.hidden === true,
|
|
99
|
-
left: storedWidth(record.left, fallback.left),
|
|
100
|
-
right: storedWidth(record.right, fallback.right),
|
|
101
|
-
}
|
|
102
|
-
} catch {
|
|
103
|
-
return fallback
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function storePanels(panels: StoredPanels): void {
|
|
108
|
-
try {
|
|
109
|
-
localStorage.setItem(PANELS_KEY, JSON.stringify(panels))
|
|
110
|
-
} catch {
|
|
111
|
-
/* A blocked store costs the remembered layout, nothing else. */
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Holds both widths inside this window, the details panel giving way first,
|
|
117
|
-
* since long layer names are what the pages panel widens for.
|
|
118
|
-
*/
|
|
119
|
-
function fitWidths(widths: PanelWidths): PanelWidths {
|
|
120
|
-
const right = clampPanelWidth(
|
|
121
|
-
'right',
|
|
122
|
-
widths.right,
|
|
123
|
-
window.innerWidth,
|
|
124
|
-
widths,
|
|
125
|
-
)
|
|
126
|
-
const left = clampPanelWidth('left', widths.left, window.innerWidth, {
|
|
127
|
-
...widths,
|
|
128
|
-
right,
|
|
129
|
-
})
|
|
130
|
-
return { left, right }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Restores the panels the last visit left and stores each change, the way the
|
|
135
|
-
* theme pick is kept. The read lands before the first paint, so a hidden
|
|
136
|
-
* layout never flashes open. The widths are clamped on that read and on every
|
|
137
|
-
* window resize, since a width kept from a wider window could squeeze the
|
|
138
|
-
* surface out.
|
|
139
|
-
*/
|
|
140
|
-
function usePanelPreference(): void {
|
|
141
|
-
useLayoutEffect(() => {
|
|
142
|
-
const stored = readStoredPanels()
|
|
143
|
-
panelsHidden.value = stored.hidden
|
|
144
|
-
panelWidths.value = fitWidths(stored)
|
|
145
|
-
const handleResize = () => {
|
|
146
|
-
const fitted = fitWidths(panelWidths.value)
|
|
147
|
-
const { left, right } = panelWidths.value
|
|
148
|
-
if (fitted.left !== left || fitted.right !== right) {
|
|
149
|
-
panelWidths.value = fitted
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
window.addEventListener('resize', handleResize)
|
|
153
|
-
const stopStoring = effect(() =>
|
|
154
|
-
storePanels({ hidden: panelsHidden.value, ...panelWidths.value }),
|
|
155
|
-
)
|
|
156
|
-
return () => {
|
|
157
|
-
window.removeEventListener('resize', handleResize)
|
|
158
|
-
stopStoring()
|
|
159
|
-
}
|
|
160
|
-
}, [])
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function App(): JSX.Element {
|
|
164
|
-
const viewportRef = useRef<HTMLDivElement>(null)
|
|
165
|
-
usePanelPreference()
|
|
166
|
-
const { left, right } = panelWidths.value
|
|
167
|
-
return (
|
|
168
|
-
<div
|
|
169
|
-
class={panelsHidden.value ? 'shell panels-hidden' : 'shell'}
|
|
170
|
-
style={{ '--panel-left': `${left}px`, '--panel-right': `${right}px` }}
|
|
171
|
-
>
|
|
172
|
-
<PagesPanel
|
|
173
|
-
onFocusFrame={(frame) => {
|
|
174
|
-
const rect = viewportRef.current?.getBoundingClientRect()
|
|
175
|
-
focusFrame(frame, rect?.width ?? 0, rect?.height ?? 0)
|
|
176
|
-
}}
|
|
177
|
-
/>
|
|
178
|
-
<PanelHandle side="left" controls="pages-panel" />
|
|
179
|
-
<Surface viewportRef={viewportRef} />
|
|
180
|
-
<aside id="details-panel" class="panel panel-right" aria-label="Details">
|
|
181
|
-
<Inspector />
|
|
182
|
-
<PageDetails />
|
|
183
|
-
<TokenDetails />
|
|
184
|
-
</aside>
|
|
185
|
-
<PanelHandle side="right" controls="details-panel" />
|
|
186
|
-
</div>
|
|
187
|
-
)
|
|
188
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource preact */
|
|
2
|
-
import type { JSX } from 'preact'
|
|
3
|
-
import { useRef, useState } from 'preact/hooks'
|
|
4
|
-
|
|
5
|
-
/** The value each third of an axis writes, start to end as the axis runs. */
|
|
6
|
-
const POSITIONS = ['flex-start', 'center', 'flex-end'] as const
|
|
7
|
-
|
|
8
|
-
const ROWS = ['top', 'middle', 'bottom'] as const
|
|
9
|
-
const COLUMNS = ['left', 'center', 'right'] as const
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Computed values that read as one of the three positions. A container left
|
|
13
|
-
* at its defaults packs from the start, and stretched children do too, so
|
|
14
|
-
* both show the start cell without stating it.
|
|
15
|
-
*/
|
|
16
|
-
const READS: Readonly<Record<string, number>> = {
|
|
17
|
-
normal: 0,
|
|
18
|
-
stretch: 0,
|
|
19
|
-
'flex-start': 0,
|
|
20
|
-
start: 0,
|
|
21
|
-
'self-start': 0,
|
|
22
|
-
left: 0,
|
|
23
|
-
center: 1,
|
|
24
|
-
'flex-end': 2,
|
|
25
|
-
end: 2,
|
|
26
|
-
'self-end': 2,
|
|
27
|
-
right: 2,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Values a container takes without stating a position, which a pick pins. */
|
|
31
|
-
const IMPLIED = new Set(['normal', 'stretch'])
|
|
32
|
-
|
|
33
|
-
export interface Alignment {
|
|
34
|
-
readonly justifyContent: string
|
|
35
|
-
readonly alignItems: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface AlignGridProps {
|
|
39
|
-
/** The container's computed `flex-direction`, which decides the axes. */
|
|
40
|
-
readonly direction: string
|
|
41
|
-
readonly justifyContent: string
|
|
42
|
-
readonly alignItems: string
|
|
43
|
-
readonly isBusy: boolean
|
|
44
|
-
readonly onPick: (alignment: Alignment) => void
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isRow(direction: string): boolean {
|
|
48
|
-
return !direction.startsWith('column')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The cell, as row then column on screen, that a container's alignment puts
|
|
53
|
-
* its children in. A reversed main axis runs from the far edge, so its start
|
|
54
|
-
* is the right or the bottom.
|
|
55
|
-
*/
|
|
56
|
-
function cellOf(
|
|
57
|
-
direction: string,
|
|
58
|
-
justifyContent: string,
|
|
59
|
-
alignItems: string,
|
|
60
|
-
): readonly [number, number] | undefined {
|
|
61
|
-
const main = READS[justifyContent.trim()]
|
|
62
|
-
const cross = READS[alignItems.trim()]
|
|
63
|
-
if (main === undefined || cross === undefined) return undefined
|
|
64
|
-
const along = direction.endsWith('reverse') ? 2 - main : main
|
|
65
|
-
return isRow(direction) ? [cross, along] : [along, cross]
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function alignmentOf(
|
|
69
|
-
direction: string,
|
|
70
|
-
row: number,
|
|
71
|
-
column: number,
|
|
72
|
-
): Alignment {
|
|
73
|
-
const [main, cross] = isRow(direction) ? [column, row] : [row, column]
|
|
74
|
-
const along = direction.endsWith('reverse') ? 2 - main : main
|
|
75
|
-
return {
|
|
76
|
-
justifyContent: POSITIONS[along],
|
|
77
|
-
alignItems: POSITIONS[cross],
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const KEY_STEPS: Readonly<Record<string, readonly [number, number]>> = {
|
|
82
|
-
ArrowUp: [-1, 0],
|
|
83
|
-
ArrowDown: [1, 0],
|
|
84
|
-
ArrowLeft: [0, -1],
|
|
85
|
-
ArrowRight: [0, 1],
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Nine cells placing a flex container's children, one radio group. The
|
|
90
|
-
* arrow keys move focus inside the grid and stop at its edges, and a press
|
|
91
|
-
* or Enter or Space picks, since each pick writes the frame twice.
|
|
92
|
-
*/
|
|
93
|
-
export function AlignGrid({
|
|
94
|
-
direction,
|
|
95
|
-
justifyContent,
|
|
96
|
-
alignItems,
|
|
97
|
-
isBusy,
|
|
98
|
-
onPick,
|
|
99
|
-
}: AlignGridProps): JSX.Element {
|
|
100
|
-
const checked = cellOf(direction, justifyContent, alignItems)
|
|
101
|
-
const isImplied =
|
|
102
|
-
IMPLIED.has(justifyContent.trim()) || IMPLIED.has(alignItems.trim())
|
|
103
|
-
const [focus, setFocus] = useState<readonly [number, number]>(
|
|
104
|
-
checked ?? [0, 0],
|
|
105
|
-
)
|
|
106
|
-
const cells = useRef<(HTMLButtonElement | null)[]>([])
|
|
107
|
-
|
|
108
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
109
|
-
const step = KEY_STEPS[event.key]
|
|
110
|
-
if (!step) return
|
|
111
|
-
event.preventDefault()
|
|
112
|
-
const next = [
|
|
113
|
-
Math.min(2, Math.max(0, focus[0] + step[0])),
|
|
114
|
-
Math.min(2, Math.max(0, focus[1] + step[1])),
|
|
115
|
-
] as const
|
|
116
|
-
setFocus(next)
|
|
117
|
-
cells.current[next[0] * 3 + next[1]]?.focus()
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<div
|
|
122
|
-
class="align-grid"
|
|
123
|
-
role="radiogroup"
|
|
124
|
-
aria-label="Alignment"
|
|
125
|
-
onKeyDown={handleKeyDown}
|
|
126
|
-
>
|
|
127
|
-
{ROWS.map((rowName, row) =>
|
|
128
|
-
COLUMNS.map((columnName, column) => {
|
|
129
|
-
const isChecked = checked?.[0] === row && checked[1] === column
|
|
130
|
-
const isFocusable = focus[0] === row && focus[1] === column
|
|
131
|
-
const name =
|
|
132
|
-
rowName === 'middle' && columnName === 'center'
|
|
133
|
-
? 'center'
|
|
134
|
-
: `${rowName} ${columnName}`
|
|
135
|
-
return (
|
|
136
|
-
<button
|
|
137
|
-
key={name}
|
|
138
|
-
ref={(node) => {
|
|
139
|
-
cells.current[row * 3 + column] = node
|
|
140
|
-
}}
|
|
141
|
-
type="button"
|
|
142
|
-
class="align-cell"
|
|
143
|
-
role="radio"
|
|
144
|
-
aria-label={name}
|
|
145
|
-
aria-checked={isChecked}
|
|
146
|
-
tabIndex={isFocusable ? 0 : -1}
|
|
147
|
-
disabled={isBusy}
|
|
148
|
-
onFocus={() => setFocus([row, column])}
|
|
149
|
-
onClick={() => {
|
|
150
|
-
if (!isChecked || isImplied) {
|
|
151
|
-
onPick(alignmentOf(direction, row, column))
|
|
152
|
-
}
|
|
153
|
-
}}
|
|
154
|
-
>
|
|
155
|
-
<span class="align-dot" aria-hidden="true" />
|
|
156
|
-
</button>
|
|
157
|
-
)
|
|
158
|
-
}),
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
161
|
-
)
|
|
162
|
-
}
|