@erclx/canon 5.25.0 → 5.27.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 +4 -3
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +2 -0
- package/claude/skills/auto-ship/SKILL.md +3 -11
- package/claude/skills/auto-ship/references/review-findings.md +20 -0
- package/claude/skills/backlog-triage/SKILL.md +2 -0
- package/claude/skills/bash-cli-script/SKILL.md +2 -0
- package/claude/skills/build-in-slices/SKILL.md +2 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/canon-feedback/SKILL.md +2 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
- package/claude/skills/canon-operator/SKILL.md +2 -0
- package/claude/skills/canon-rollout/SKILL.md +3 -11
- package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
- package/claude/skills/canvas/SKILL.md +14 -1
- package/claude/skills/ci-workflow/SKILL.md +2 -0
- package/claude/skills/code-craft/SKILL.md +2 -0
- package/claude/skills/codebase-layout/SKILL.md +2 -0
- package/claude/skills/context-fold/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +9 -21
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- package/claude/skills/context-fold/references/completion-report.md +26 -0
- package/claude/skills/create-rule/SKILL.md +2 -0
- package/claude/skills/create-skill/SKILL.md +2 -0
- package/claude/skills/create-standard/SKILL.md +2 -0
- package/claude/skills/decision-escalate/SKILL.md +2 -0
- package/claude/skills/deploy-app/SKILL.md +2 -0
- package/claude/skills/deprecation-migration/SKILL.md +2 -0
- package/claude/skills/design-extract/SKILL.md +2 -0
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +6 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/docs-sync/SKILL.md +2 -0
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +23 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +3 -1
- package/claude/skills/draft-doc/SKILL.md +2 -0
- package/claude/skills/draft-doc/references/wiki.md +1 -1
- package/claude/skills/draft-figure/SKILL.md +2 -0
- package/claude/skills/draft-identity/SKILL.md +3 -1
- package/claude/skills/draft-ready/SKILL.md +2 -0
- package/claude/skills/draft-screencast/SKILL.md +11 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +33 -20
- package/claude/skills/git-branch/SKILL.md +2 -0
- package/claude/skills/git-commit/SKILL.md +2 -0
- package/claude/skills/git-followup/SKILL.md +2 -0
- package/claude/skills/git-issue/SKILL.md +2 -0
- package/claude/skills/git-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/evidence.md +14 -6
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-ship/SKILL.md +2 -0
- package/claude/skills/git-split/SKILL.md +21 -8
- package/claude/skills/git-stage/SKILL.md +2 -0
- package/claude/skills/git-worktree/SKILL.md +2 -0
- package/claude/skills/index-lookup/SKILL.md +2 -0
- package/claude/skills/markdown-propose/SKILL.md +2 -0
- package/claude/skills/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-capture/SKILL.md +2 -0
- package/claude/skills/memory-review/SKILL.md +2 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -0
- package/claude/skills/plan-feature/SKILL.md +2 -0
- package/claude/skills/plan-groundwork/SKILL.md +2 -0
- package/claude/skills/plan-intake/SKILL.md +2 -0
- package/claude/skills/plan-intake-answer/SKILL.md +2 -0
- package/claude/skills/project-commands/SKILL.md +2 -0
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +2 -0
- package/claude/skills/repo-metadata/SKILL.md +2 -0
- package/claude/skills/restate-plainly/SKILL.md +2 -0
- package/claude/skills/review-address/SKILL.md +3 -7
- package/claude/skills/review-address/references/post-review-findings.md +16 -0
- package/claude/skills/review-branch/SKILL.md +2 -0
- package/claude/skills/review-craft/SKILL.md +2 -0
- package/claude/skills/review-craft/references/ui.md +2 -2
- package/claude/skills/review-pr/SKILL.md +4 -8
- package/claude/skills/review-pr/references/body-variants.md +52 -0
- package/claude/skills/review-pr/references/key-changes.md +16 -0
- package/claude/skills/review-pr/references/post.md +15 -43
- package/claude/skills/review-ui/SKILL.md +3 -1
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/SKILL.md +5 -14
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +5 -33
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -21
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
- package/claude/skills/role-planner/SKILL.md +2 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +2 -0
- package/claude/skills/role-worker/SKILL.md +2 -0
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +5 -1
- package/claude/skills/search-craft/references/adopted.md +30 -0
- package/claude/skills/seed-sync/SKILL.md +2 -0
- package/claude/skills/session-compact/SKILL.md +2 -0
- package/claude/skills/session-map/SKILL.md +2 -0
- package/claude/skills/session-relay/SKILL.md +2 -0
- package/claude/skills/session-resume/SKILL.md +2 -0
- package/claude/skills/session-worktree/SKILL.md +3 -11
- package/claude/skills/session-worktree/references/install-command.md +20 -0
- package/claude/skills/sketch-design/SKILL.md +2 -0
- package/claude/skills/systematic-debugging/SKILL.md +2 -0
- package/claude/skills/target-check/SKILL.md +2 -0
- package/claude/skills/target-setup/SKILL.md +2 -0
- package/claude/skills/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -0
- package/claude/skills/task-board/SKILL.md +2 -0
- package/claude/skills/teach-workspace/SKILL.md +5 -15
- package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
- package/claude/skills/teach-workspace/references/revisit.md +22 -0
- package/claude/skills/test-craft/SKILL.md +2 -0
- package/claude/skills/test-first/SKILL.md +4 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/ui-checklist/SKILL.md +2 -0
- package/claude/skills/ux-audit/SKILL.md +2 -0
- package/claude/skills/ux-measure/SKILL.md +2 -0
- package/claude/skills/ux-walkthrough/SKILL.md +2 -0
- package/claude/skills/video-craft/SKILL.md +2 -0
- package/claude/skills/write-human/SKILL.md +3 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/claude/skills/youtube-transcripts/SKILL.md +2 -0
- package/docs/agents/canvas.md +24 -6
- package/docs/agents/capture.md +6 -6
- package/docs/agents/commands.md +5 -5
- package/docs/agents/index.md +5 -5
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/pr-evidence.md +38 -15
- package/docs/agents/pr-local.md +19 -13
- package/docs/agents/pr-preview.md +44 -21
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +29 -5
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +11 -3
- package/docs/agents/slides.md +15 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/operating-model.md +5 -6
- package/docs/workflow/parallel-features.md +23 -2
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +3 -34
- package/docs/workflow/zshrc-aliases.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.ts +1 -1
- package/scripts/lib/sandbox-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +6 -162
- package/scripts/sandbox-hook.sh +99 -0
- package/src/audits/run.ts +1 -1
- package/src/canvas/address.ts +12 -3
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/align-grid.tsx +162 -0
- package/src/canvas/client/inspector/color-field.tsx +398 -0
- package/src/canvas/client/inspector/color-inputs.tsx +139 -0
- package/src/canvas/client/inspector/color-picker.tsx +668 -0
- package/src/canvas/client/inspector/color-space.ts +240 -0
- package/src/canvas/client/inspector/field.tsx +5 -1
- package/src/canvas/client/inspector/fields.css +547 -17
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/values.ts +166 -1
- package/src/canvas/client/inspector.tsx +236 -73
- package/src/canvas/client/selection.tsx +197 -0
- package/src/canvas/client/shell.css +104 -12
- package/src/canvas/client/state.ts +69 -9
- package/src/canvas/client/surface.tsx +147 -22
- package/src/canvas/content.ts +50 -1
- package/src/canvas/edit.ts +37 -4
- package/src/canvas/editing.ts +203 -0
- package/src/canvas/global.d.ts +15 -0
- package/src/canvas/server.ts +149 -12
- package/src/canvas/tokens.ts +59 -17
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +2 -2
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/seeds.ts +2 -2
- package/src/claude/skills-audit.ts +96 -14
- package/src/claude/skills-drift.ts +1 -1
- package/src/claude/skills-families.ts +36 -0
- package/src/claude/skills-list.ts +76 -15
- package/src/commands/audits.ts +2 -2
- package/src/commands/canvas.ts +179 -2
- package/src/commands/claude.ts +94 -11
- package/src/commands/design.ts +142 -2
- package/src/commands/feedback.ts +3 -3
- package/src/commands/gov.ts +1 -1
- package/src/commands/hooks.ts +1 -1
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +1 -1
- package/src/commands/pr.ts +105 -58
- package/src/commands/records.ts +89 -7
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +103 -11
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +31 -77
- package/src/commands/sync.ts +1 -1
- package/src/commands/tasks.ts +2 -2
- package/src/commands/teach.ts +1 -1
- package/src/commands/transcripts.ts +1 -1
- package/src/commands/worktrees.ts +1 -1
- package/src/comments/scan.ts +1 -1
- package/src/comments/trend.ts +1 -1
- package/src/context/audit.ts +1 -1
- package/src/context/citations.ts +1 -1
- package/src/context/classify/extract.ts +2 -2
- package/src/design/project-fonts.ts +392 -0
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +2 -2
- package/src/{client-commands.ts → gate/client-commands.ts} +1 -1
- package/src/gate/measures.ts +53 -13
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +9 -0
- package/src/{git-files.ts → git/files.ts} +2 -2
- package/src/{git-ignore.ts → git/ignore.ts} +1 -1
- package/src/{worktree.ts → git/worktree.ts} +1 -1
- package/src/gov/citations.ts +2 -2
- package/src/gov/install.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +3 -3
- package/src/gov/test-order.ts +2 -2
- package/src/help.ts +1 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/indexes/walk.ts +1 -1
- package/src/labels/audit.ts +1 -1
- package/src/markdown/ceiling.ts +1 -1
- package/src/markdown/files.ts +1 -1
- package/src/migrate/apply.ts +1 -1
- package/src/pr/bijection.ts +1 -1
- package/src/pr/evidence.ts +96 -43
- package/src/pr/local.ts +16 -10
- package/src/pr/preview.ts +78 -2
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +2 -1
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- package/src/sandbox/headless/dispatch.ts +264 -0
- package/src/sandbox/headless/run.ts +653 -0
- package/src/sandbox/headless/watch.ts +145 -0
- package/src/sandbox/hooks.ts +128 -0
- package/src/sandbox/provision.ts +607 -0
- package/src/sandbox/tree.ts +99 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +2 -2
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- package/src/slides/package/fonts.ts +6 -2
- package/src/standards/audit.ts +2 -2
- package/src/standards/read.ts +1 -1
- package/src/sync/check.ts +2 -2
- package/src/sync/engine.ts +2 -2
- package/src/sync/git.ts +1 -1
- package/src/sync/history.ts +1 -1
- package/src/sync/reverse.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/sync/target.ts +1 -1
- package/src/sync/workflow.ts +1 -1
- package/src/targets/pulls.ts +2 -2
- package/src/targets/sweep.ts +2 -2
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/trunk.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/subfolder.ts +1 -1
- package/src/web/readme-citations.ts +2 -2
- package/src/worktrees/bare-flag.ts +1 -1
- package/src/worktrees/reclaim.ts +2 -2
- package/src/worktrees/remove.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +3 -2
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +2 -0
- package/standards/wiki.md +12 -3
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
- package/scripts/lib/sandbox-dispatch.sh +0 -190
- package/scripts/manage-sandbox.sh +0 -472
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
- /package/src/{git-base.ts → git/base.ts} +0 -0
- /package/src/{git-env.ts → git/env.ts} +0 -0
- /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{copy.ts → sync/copy.ts} +0 -0
- /package/src/{target.ts → targets/validate.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from 'preact'
|
|
3
|
+
import { useLayoutEffect, useRef, useState } from 'preact/hooks'
|
|
4
|
+
import type { ScrubSession } from '@/canvas/client/inspector/field'
|
|
5
|
+
import {
|
|
6
|
+
ColorInputs,
|
|
7
|
+
currentOpacity,
|
|
8
|
+
} from '@/canvas/client/inspector/color-inputs'
|
|
9
|
+
import {
|
|
10
|
+
ColorPicker,
|
|
11
|
+
isPickerHeld,
|
|
12
|
+
releasePicker,
|
|
13
|
+
} from '@/canvas/client/inspector/color-picker'
|
|
14
|
+
import {
|
|
15
|
+
composeColor,
|
|
16
|
+
type ParsedColor,
|
|
17
|
+
readHex,
|
|
18
|
+
} from '@/canvas/client/inspector/values'
|
|
19
|
+
|
|
20
|
+
interface ColorFieldProps {
|
|
21
|
+
/** The full word, which names the hex input for assistive technology. */
|
|
22
|
+
readonly label: string
|
|
23
|
+
/** What the element holds, or undefined for a form the picker cannot read. */
|
|
24
|
+
readonly color: ParsedColor | undefined
|
|
25
|
+
/** The value as written, shown when `color` is undefined. */
|
|
26
|
+
readonly text: string
|
|
27
|
+
/** The element's own inline value, empty where none stands. */
|
|
28
|
+
readonly inline: string
|
|
29
|
+
/** The color the browser paints, which fills the swatch. */
|
|
30
|
+
readonly painted: string
|
|
31
|
+
/** The color tokens in the order the token sheet sorts them. */
|
|
32
|
+
readonly tokens: readonly string[]
|
|
33
|
+
/** The color a token paints in the frame, read when the list opens. */
|
|
34
|
+
readonly paint: (name: string) => string
|
|
35
|
+
/** The frame, element, and property, so a reload reopens the right picker. */
|
|
36
|
+
readonly owner: string
|
|
37
|
+
readonly isRaw: boolean
|
|
38
|
+
readonly isBusy: boolean
|
|
39
|
+
/** Opens a session previewing into the frame, where the element takes one. */
|
|
40
|
+
readonly preview?: () => ScrubSession
|
|
41
|
+
readonly onCommit: (value: string) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The accessible name of the row control that last wrote a value, so it takes
|
|
46
|
+
* focus back each time the frame reloads. A saved edit remounts the inspector,
|
|
47
|
+
* once for the edit and again when the file watcher reports the write, and each
|
|
48
|
+
* remount replaces the control that held focus and drops it to the page. It
|
|
49
|
+
* holds until focus lands anywhere else.
|
|
50
|
+
*/
|
|
51
|
+
let refocus: string | undefined
|
|
52
|
+
|
|
53
|
+
function swatchLabel(label: string): string {
|
|
54
|
+
return `${label} picker`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tokensLabel(label: string): string {
|
|
58
|
+
return `${label} tokens`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function eyedropperLabel(label: string): string {
|
|
62
|
+
return `${label} eyedropper`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof document !== 'undefined') {
|
|
66
|
+
document.addEventListener('focusin', (event) => {
|
|
67
|
+
if (refocus === undefined) return
|
|
68
|
+
const target = event.target
|
|
69
|
+
const name =
|
|
70
|
+
target instanceof Element ? target.getAttribute('aria-label') : null
|
|
71
|
+
if (name !== refocus) refocus = undefined
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The screen sampler, where the browser ships one. */
|
|
76
|
+
function eyeDropper(): (new () => EyeDropperSampler) | undefined {
|
|
77
|
+
return typeof window === 'undefined' ? undefined : window.EyeDropper
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Open = 'picker' | 'tokens' | undefined
|
|
81
|
+
|
|
82
|
+
const GAP = 8
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The left edge of the scrolling panel holding the row, so a popover clears
|
|
86
|
+
* the panel's padding rather than overlapping it. The row's own edge stands in
|
|
87
|
+
* where no ancestor scrolls.
|
|
88
|
+
*/
|
|
89
|
+
function panelLeft(row: HTMLElement): number {
|
|
90
|
+
for (let node = row.parentElement; node; node = node.parentElement) {
|
|
91
|
+
const overflow = getComputedStyle(node).overflowY
|
|
92
|
+
if (overflow === 'auto' || overflow === 'scroll') {
|
|
93
|
+
return node.getBoundingClientRect().left
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return row.getBoundingClientRect().left
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Places a popover beside the row, to the left of the panel, and holds it
|
|
101
|
+
* inside the window. The panel scrolls and clips, so the popover sits in the
|
|
102
|
+
* top layer where the browser has one and at fixed coordinates either way.
|
|
103
|
+
*/
|
|
104
|
+
function place(popover: HTMLElement, row: HTMLElement): void {
|
|
105
|
+
if (
|
|
106
|
+
typeof popover.showPopover === 'function' &&
|
|
107
|
+
!popover.matches(':popover-open')
|
|
108
|
+
) {
|
|
109
|
+
popover.showPopover()
|
|
110
|
+
}
|
|
111
|
+
const box = popover.getBoundingClientRect()
|
|
112
|
+
const anchor = row.getBoundingClientRect()
|
|
113
|
+
const width = document.documentElement.clientWidth || window.innerWidth
|
|
114
|
+
const height = document.documentElement.clientHeight || window.innerHeight
|
|
115
|
+
const before = panelLeft(row) - GAP - box.width
|
|
116
|
+
const left =
|
|
117
|
+
before >= GAP
|
|
118
|
+
? before
|
|
119
|
+
: Math.max(GAP, Math.min(anchor.right + GAP, width - box.width - GAP))
|
|
120
|
+
const top = Math.max(GAP, Math.min(anchor.top, height - box.height - GAP))
|
|
121
|
+
popover.style.left = `${left}px`
|
|
122
|
+
popover.style.top = `${top}px`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* One color property: a swatch opening the picker, the hex, the opacity, a
|
|
127
|
+
* tokens icon opening the project's color tokens, and an eyedropper where the
|
|
128
|
+
* browser has one. A token keeps following the theme and a literal does not,
|
|
129
|
+
* so the tokens sit one press from the row.
|
|
130
|
+
*/
|
|
131
|
+
export function ColorField({
|
|
132
|
+
label,
|
|
133
|
+
color,
|
|
134
|
+
text,
|
|
135
|
+
inline,
|
|
136
|
+
painted,
|
|
137
|
+
tokens,
|
|
138
|
+
paint,
|
|
139
|
+
owner,
|
|
140
|
+
isRaw,
|
|
141
|
+
isBusy,
|
|
142
|
+
preview,
|
|
143
|
+
onCommit,
|
|
144
|
+
}: ColorFieldProps): JSX.Element {
|
|
145
|
+
const [open, setOpen] = useState<Open>(() =>
|
|
146
|
+
isPickerHeld(owner) ? 'picker' : undefined,
|
|
147
|
+
)
|
|
148
|
+
const root = useRef<HTMLDivElement>(null)
|
|
149
|
+
const row = useRef<HTMLDivElement>(null)
|
|
150
|
+
const swatch = useRef<HTMLButtonElement>(null)
|
|
151
|
+
const tokenButton = useRef<HTMLButtonElement>(null)
|
|
152
|
+
const list = useRef<HTMLUListElement>(null)
|
|
153
|
+
const dialog = useRef<HTMLDivElement>(null)
|
|
154
|
+
const picked = color?.kind === 'token' ? tokens.indexOf(color.name) : -1
|
|
155
|
+
const [active, setActive] = useState(Math.max(0, picked))
|
|
156
|
+
const isNone = color?.kind === 'none'
|
|
157
|
+
|
|
158
|
+
/*
|
|
159
|
+
* Focus moves in the commit rather than after paint, so a key typed straight
|
|
160
|
+
* after the one that opened the list reaches it, not the button.
|
|
161
|
+
*/
|
|
162
|
+
useLayoutEffect(() => {
|
|
163
|
+
if (!open || !dialog.current || !row.current) return
|
|
164
|
+
place(dialog.current, row.current)
|
|
165
|
+
if (open === 'tokens') (list.current ?? dialog.current).focus()
|
|
166
|
+
}, [open])
|
|
167
|
+
|
|
168
|
+
useLayoutEffect(() => {
|
|
169
|
+
if (refocus === undefined) return
|
|
170
|
+
root.current
|
|
171
|
+
?.querySelector<HTMLElement>(`[aria-label="${refocus}"]`)
|
|
172
|
+
?.focus()
|
|
173
|
+
}, [label])
|
|
174
|
+
|
|
175
|
+
const close = () => {
|
|
176
|
+
const trigger = open === 'tokens' ? tokenButton : swatch
|
|
177
|
+
releasePicker()
|
|
178
|
+
setOpen(undefined)
|
|
179
|
+
trigger.current?.focus()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const toggle = (next: Exclude<Open, undefined>) => {
|
|
183
|
+
releasePicker()
|
|
184
|
+
setActive(Math.max(0, picked))
|
|
185
|
+
setOpen(open === next ? undefined : next)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const pick = (name: string) => {
|
|
189
|
+
if (color?.kind === 'token' && color.name === name) {
|
|
190
|
+
close()
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
close()
|
|
194
|
+
refocus = tokensLabel(label)
|
|
195
|
+
onCommit(
|
|
196
|
+
composeColor({ kind: 'token', name, opacity: currentOpacity(color) }),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const Sampler = eyeDropper()
|
|
201
|
+
const sample = async (sampler: new () => EyeDropperSampler) => {
|
|
202
|
+
let picked: string
|
|
203
|
+
try {
|
|
204
|
+
picked = (await new sampler().open()).sRGBHex
|
|
205
|
+
} catch {
|
|
206
|
+
/* The operator dismissed the sampler, which writes nothing. */
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
const hex = readHex(picked)
|
|
210
|
+
if (!hex) return
|
|
211
|
+
refocus = eyedropperLabel(label)
|
|
212
|
+
onCommit(
|
|
213
|
+
composeColor({
|
|
214
|
+
kind: 'hex',
|
|
215
|
+
hex: hex.hex,
|
|
216
|
+
opacity: currentOpacity(color),
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const id = `color-${label.replace(/\W+/g, '-')}`
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div
|
|
225
|
+
ref={root}
|
|
226
|
+
class="color-field is-wide"
|
|
227
|
+
onFocusOut={(event) => {
|
|
228
|
+
const field = event.currentTarget
|
|
229
|
+
/*
|
|
230
|
+
* Read once focus has settled. A reload removes the field while it
|
|
231
|
+
* holds focus, and a picker held across that reload must stay open.
|
|
232
|
+
*/
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
if (!field.isConnected) return
|
|
235
|
+
if (field.contains(document.activeElement)) return
|
|
236
|
+
releasePicker()
|
|
237
|
+
setOpen(undefined)
|
|
238
|
+
}, 0)
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<div ref={row} class="fill-row">
|
|
242
|
+
<div class="glyph-field">
|
|
243
|
+
<button
|
|
244
|
+
ref={swatch}
|
|
245
|
+
class={isNone ? 'swatch is-none' : 'swatch'}
|
|
246
|
+
type="button"
|
|
247
|
+
aria-label={swatchLabel(label)}
|
|
248
|
+
aria-haspopup="dialog"
|
|
249
|
+
aria-expanded={open === 'picker'}
|
|
250
|
+
title={label}
|
|
251
|
+
disabled={isBusy}
|
|
252
|
+
onClick={() => toggle('picker')}
|
|
253
|
+
>
|
|
254
|
+
<span
|
|
255
|
+
class="swatch-paint"
|
|
256
|
+
style={isNone ? undefined : { '--paint': painted }}
|
|
257
|
+
/>
|
|
258
|
+
</button>
|
|
259
|
+
<ColorInputs
|
|
260
|
+
hexLabel={label}
|
|
261
|
+
opacityLabel={`${label} opacity`}
|
|
262
|
+
color={color}
|
|
263
|
+
text={text}
|
|
264
|
+
isBusy={isBusy}
|
|
265
|
+
onCommit={onCommit}
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
<button
|
|
269
|
+
ref={tokenButton}
|
|
270
|
+
class="color-icon"
|
|
271
|
+
type="button"
|
|
272
|
+
aria-label={tokensLabel(label)}
|
|
273
|
+
aria-haspopup="dialog"
|
|
274
|
+
aria-expanded={open === 'tokens'}
|
|
275
|
+
title="Pick a color token"
|
|
276
|
+
disabled={isBusy}
|
|
277
|
+
onClick={() => toggle('tokens')}
|
|
278
|
+
>
|
|
279
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
280
|
+
<circle cx="5" cy="5" r="2.25" />
|
|
281
|
+
<circle cx="11" cy="5" r="2.25" />
|
|
282
|
+
<circle cx="5" cy="11" r="2.25" />
|
|
283
|
+
<circle cx="11" cy="11" r="2.25" />
|
|
284
|
+
</svg>
|
|
285
|
+
</button>
|
|
286
|
+
{Sampler ? (
|
|
287
|
+
<button
|
|
288
|
+
class="color-icon"
|
|
289
|
+
type="button"
|
|
290
|
+
aria-label={eyedropperLabel(label)}
|
|
291
|
+
title="Pick a color from the screen"
|
|
292
|
+
disabled={isBusy}
|
|
293
|
+
onClick={() => void sample(Sampler)}
|
|
294
|
+
>
|
|
295
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
296
|
+
<path d="M10.5 2.5a1.4 1.4 0 0 1 2 0l1 1a1.4 1.4 0 0 1 0 2L12 7l.5.5-1 1-4-4 1-1L9 4zM7.5 5.5l3 3-5 5H2.5v-3z" />
|
|
297
|
+
</svg>
|
|
298
|
+
</button>
|
|
299
|
+
) : null}
|
|
300
|
+
{isRaw ? (
|
|
301
|
+
<span
|
|
302
|
+
class="raw"
|
|
303
|
+
title="Set as a raw value, so it will not follow the theme. Pick a token to fix it"
|
|
304
|
+
>
|
|
305
|
+
raw
|
|
306
|
+
</span>
|
|
307
|
+
) : null}
|
|
308
|
+
</div>
|
|
309
|
+
{open ? (
|
|
310
|
+
<div
|
|
311
|
+
ref={dialog}
|
|
312
|
+
class={
|
|
313
|
+
open === 'picker' ? 'color-popover' : 'color-popover is-tokens'
|
|
314
|
+
}
|
|
315
|
+
popover="manual"
|
|
316
|
+
role="dialog"
|
|
317
|
+
aria-label={
|
|
318
|
+
open === 'picker' ? `${label} color` : `${label} token list`
|
|
319
|
+
}
|
|
320
|
+
tabIndex={-1}
|
|
321
|
+
onKeyDown={(event) => {
|
|
322
|
+
if (event.key !== 'Escape') return
|
|
323
|
+
event.preventDefault()
|
|
324
|
+
/* A blur would commit what was typed, so put it back first. */
|
|
325
|
+
if (event.target instanceof HTMLInputElement) {
|
|
326
|
+
event.target.value = event.target.defaultValue
|
|
327
|
+
}
|
|
328
|
+
close()
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
{open === 'picker' ? (
|
|
332
|
+
<ColorPicker
|
|
333
|
+
owner={owner}
|
|
334
|
+
color={color}
|
|
335
|
+
text={text}
|
|
336
|
+
inline={inline}
|
|
337
|
+
painted={painted}
|
|
338
|
+
begin={preview}
|
|
339
|
+
isBusy={isBusy}
|
|
340
|
+
onCommit={onCommit}
|
|
341
|
+
/>
|
|
342
|
+
) : tokens.length > 0 ? (
|
|
343
|
+
<ul
|
|
344
|
+
ref={list}
|
|
345
|
+
class="token-list"
|
|
346
|
+
role="listbox"
|
|
347
|
+
aria-label="Tokens"
|
|
348
|
+
tabIndex={0}
|
|
349
|
+
aria-activedescendant={`${id}-${active}`}
|
|
350
|
+
onKeyDown={(event) => {
|
|
351
|
+
const last = tokens.length - 1
|
|
352
|
+
const moves: Record<string, number> = {
|
|
353
|
+
ArrowDown: Math.min(last, active + 1),
|
|
354
|
+
ArrowUp: Math.max(0, active - 1),
|
|
355
|
+
Home: 0,
|
|
356
|
+
End: last,
|
|
357
|
+
}
|
|
358
|
+
if (event.key in moves) {
|
|
359
|
+
event.preventDefault()
|
|
360
|
+
setActive(moves[event.key] ?? active)
|
|
361
|
+
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
362
|
+
event.preventDefault()
|
|
363
|
+
const name = tokens[active]
|
|
364
|
+
if (name) pick(name)
|
|
365
|
+
}
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{tokens.map((name, index) => (
|
|
369
|
+
<li
|
|
370
|
+
key={name}
|
|
371
|
+
id={`${id}-${index}`}
|
|
372
|
+
class={
|
|
373
|
+
index === active ? 'token-option is-active' : 'token-option'
|
|
374
|
+
}
|
|
375
|
+
role="option"
|
|
376
|
+
aria-selected={index === picked}
|
|
377
|
+
title={name}
|
|
378
|
+
onClick={() => pick(name)}
|
|
379
|
+
>
|
|
380
|
+
<span
|
|
381
|
+
class="swatch-paint"
|
|
382
|
+
aria-hidden="true"
|
|
383
|
+
style={{ '--paint': paint(name) }}
|
|
384
|
+
/>
|
|
385
|
+
<span class="token-name">{name}</span>
|
|
386
|
+
</li>
|
|
387
|
+
))}
|
|
388
|
+
</ul>
|
|
389
|
+
) : (
|
|
390
|
+
<p class="empty">
|
|
391
|
+
No color tokens resolve. Add them to the token sheet
|
|
392
|
+
</p>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
) : null}
|
|
396
|
+
</div>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from 'preact'
|
|
3
|
+
import {
|
|
4
|
+
composeColor,
|
|
5
|
+
type ParsedColor,
|
|
6
|
+
readHex,
|
|
7
|
+
readOpacity,
|
|
8
|
+
} from '@/canvas/client/inspector/values'
|
|
9
|
+
|
|
10
|
+
function shownHex(color: ParsedColor | undefined, text: string): string {
|
|
11
|
+
if (!color) return text
|
|
12
|
+
if (color.kind === 'none') return ''
|
|
13
|
+
return color.kind === 'token' ? color.name : color.hex
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shownOpacity(color: ParsedColor | undefined): string {
|
|
17
|
+
return color && color.kind !== 'none' ? String(color.opacity) : ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function currentOpacity(color: ParsedColor | undefined): number {
|
|
21
|
+
return color && color.kind !== 'none' ? color.opacity : 100
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What a typed hex writes. A token name typed whole picks the token, and any
|
|
26
|
+
* other text goes through as written, which the panel then marks raw.
|
|
27
|
+
*/
|
|
28
|
+
function fromTyped(typed: string, opacity: number): string {
|
|
29
|
+
const hex = readHex(typed)
|
|
30
|
+
if (hex) {
|
|
31
|
+
return composeColor({
|
|
32
|
+
kind: 'hex',
|
|
33
|
+
hex: hex.hex,
|
|
34
|
+
opacity: hex.opacity ?? opacity,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
if (/^--[\w-]+$/.test(typed)) {
|
|
38
|
+
return composeColor({ kind: 'token', name: typed, opacity })
|
|
39
|
+
}
|
|
40
|
+
return typed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ColorInputsProps {
|
|
44
|
+
readonly hexLabel: string
|
|
45
|
+
readonly opacityLabel: string
|
|
46
|
+
readonly color: ParsedColor | undefined
|
|
47
|
+
readonly text: string
|
|
48
|
+
readonly isBusy: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Holds the inputs read-only rather than disabled while busy, for a
|
|
51
|
+
* popover that closes once focus leaves it and a disabled input drops focus.
|
|
52
|
+
*/
|
|
53
|
+
readonly keepsFocus?: boolean
|
|
54
|
+
readonly onCommit: (value: string) => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The hex and the opacity. Each commits on change and posts nothing when the
|
|
59
|
+
* value is what it started at, or an opacity outside 0 to 100.
|
|
60
|
+
*/
|
|
61
|
+
export function ColorInputs({
|
|
62
|
+
hexLabel,
|
|
63
|
+
opacityLabel,
|
|
64
|
+
color,
|
|
65
|
+
text,
|
|
66
|
+
isBusy,
|
|
67
|
+
keepsFocus = false,
|
|
68
|
+
onCommit,
|
|
69
|
+
}: ColorInputsProps): JSX.Element {
|
|
70
|
+
const hex = shownHex(color, text)
|
|
71
|
+
const opacity = shownOpacity(color)
|
|
72
|
+
const isDisabled = isBusy && !keepsFocus
|
|
73
|
+
const isReadOnly = isBusy && keepsFocus
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<input
|
|
77
|
+
class={
|
|
78
|
+
color === undefined
|
|
79
|
+
? 'glyph-field-input is-text'
|
|
80
|
+
: color.kind === 'token'
|
|
81
|
+
? 'glyph-field-input is-token'
|
|
82
|
+
: 'glyph-field-input'
|
|
83
|
+
}
|
|
84
|
+
type="text"
|
|
85
|
+
aria-label={hexLabel}
|
|
86
|
+
title={hex}
|
|
87
|
+
defaultValue={hex}
|
|
88
|
+
placeholder="None"
|
|
89
|
+
disabled={isDisabled}
|
|
90
|
+
readOnly={isReadOnly}
|
|
91
|
+
spellcheck={false}
|
|
92
|
+
onChange={(event) => {
|
|
93
|
+
const typed = event.currentTarget.value.trim()
|
|
94
|
+
if (isBusy) {
|
|
95
|
+
event.currentTarget.value = hex
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
if (typed === '' || typed === hex) return
|
|
99
|
+
onCommit(fromTyped(typed, currentOpacity(color)))
|
|
100
|
+
}}
|
|
101
|
+
onKeyDown={(event) => {
|
|
102
|
+
if (event.key === 'Escape') event.currentTarget.value = hex
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
<input
|
|
106
|
+
class="color-opacity"
|
|
107
|
+
type="text"
|
|
108
|
+
inputMode="numeric"
|
|
109
|
+
aria-label={opacityLabel}
|
|
110
|
+
defaultValue={opacity}
|
|
111
|
+
disabled={isDisabled || !color || color.kind === 'none'}
|
|
112
|
+
readOnly={isReadOnly}
|
|
113
|
+
spellcheck={false}
|
|
114
|
+
onChange={(event) => {
|
|
115
|
+
const percent = readOpacity(event.currentTarget.value)
|
|
116
|
+
if (
|
|
117
|
+
isBusy ||
|
|
118
|
+
percent === undefined ||
|
|
119
|
+
!color ||
|
|
120
|
+
color.kind === 'none' ||
|
|
121
|
+
percent === color.opacity
|
|
122
|
+
) {
|
|
123
|
+
event.currentTarget.value = opacity
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
onCommit(composeColor({ ...color, opacity: percent }))
|
|
127
|
+
}}
|
|
128
|
+
onKeyDown={(event) => {
|
|
129
|
+
if (event.key === 'Escape') event.currentTarget.value = opacity
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
{opacity ? (
|
|
133
|
+
<span class="color-percent" aria-hidden="true">
|
|
134
|
+
%
|
|
135
|
+
</span>
|
|
136
|
+
) : null}
|
|
137
|
+
</>
|
|
138
|
+
)
|
|
139
|
+
}
|