@erclx/aitk 1.3.0 → 1.4.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/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents/output-shape.md +13 -0
- package/docs/visual-design-workflow.md +2 -0
- package/package.json +1 -1
- package/src/cli.ts +4 -4
- package/src/commands/claude.ts +4 -4
- package/src/commands/design.ts +2 -6
- package/src/commands/feedback.ts +2 -4
- package/src/commands/gov.ts +3 -3
- package/src/commands/init.ts +3 -5
- package/src/commands/slides.ts +4 -6
- package/src/commands/snippets.ts +2 -3
- package/src/commands/standards.ts +2 -4
- package/src/commands/sync.ts +3 -4
- package/src/commands/tooling.ts +2 -3
- package/src/commands/transcripts.ts +2 -6
- package/src/commands/wiki.ts +2 -3
- package/src/design/parse.ts +42 -5
- package/src/design/render.ts +106 -27
- package/src/sync/engine.ts +3 -4
- package/src/sync/workflow.ts +2 -3
- package/src/ui.ts +66 -6
- package/standards/design.md +10 -0
|
@@ -46,6 +46,19 @@ Help skips the banner. The `Usage:` line sits directly on `├`. Help writes to
|
|
|
46
46
|
|
|
47
47
|
`--json` and `--names` keep stdout clean and machine-readable. The frame still renders on stderr (open, banner, close) so the stream discipline is consistent across modes. Consumers that only read stdout see pure data.
|
|
48
48
|
|
|
49
|
+
## Color
|
|
50
|
+
|
|
51
|
+
Escape sequences reach a destination that renders them and nowhere else. The question is asked per stream rather than once for the process, so a run piping its data while keeping a terminal on stderr still gets color on the frame.
|
|
52
|
+
|
|
53
|
+
Either condition alone turns color off:
|
|
54
|
+
|
|
55
|
+
- `NO_COLOR` is set to any non-empty value, whatever that value says
|
|
56
|
+
- The destination is not a terminal, which covers a pipe, a file, and a captured session
|
|
57
|
+
|
|
58
|
+
The frame survives both. `┌`, `│`, `├`, `└`, and the `✓ ! + - ✗` marks are structure rather than color, and they are what lets a captured run still read as one block. A caller wanting neither the frame nor the color reads `--json` instead.
|
|
59
|
+
|
|
60
|
+
Terminal control is a separate question this section does not cover. The cursor and key sequences an interactive prompt writes run only where a terminal already exists.
|
|
61
|
+
|
|
49
62
|
## Process exit
|
|
50
63
|
|
|
51
64
|
A command action sets `process.exitCode` and returns. Calling `process.exit()` there ends the process before a stdout write drains, which truncates piped output at the 64K pipe buffer while still reporting the right exit code. Redirecting to a file hides the truncation, so it surfaces only through a pipe, which is what a check has to use to catch it.
|
|
@@ -24,6 +24,8 @@ The toolkit seed in `tooling/claude/seeds/.claude/DESIGN.md` ships a token-table
|
|
|
24
24
|
|
|
25
25
|
The `aitk:claude-design-extract` skill drafts the file, sourcing tokens from a project's existing prose and CLI UI surfaces, or proposing them from `.claude/REQUIREMENTS.md` and a `## Personality` paragraph when no UI code exists yet. `aitk design render` writes an HTML plus CSS preview to `.claude/review/design/` for eyeballing the current system without leaving Claude Code. See `.claude/context/design.md`.
|
|
26
26
|
|
|
27
|
+
A cell no source anchors ends in `? verify`, and the preview shows that marker beside the value rather than folding it in, so a swatch and a font sample stay built from the value alone. A confidence line above the sections names how many cells are anchored against how many are tagged, which is what tells a reader whether they are looking at a record of the code or a proposal about it. It reads the columns a source could anchor and leaves out the row names, so the ratio is not diluted by cells no tag could ever reach. The proposal path tags nearly all of them, so that count reads low on day one by design.
|
|
28
|
+
|
|
27
29
|
### Tools
|
|
28
30
|
|
|
29
31
|
- None beyond Claude Code itself
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -28,12 +28,12 @@ import { register as sessions } from '@/commands/sessions'
|
|
|
28
28
|
import { register as audits } from '@/commands/audits'
|
|
29
29
|
import { register as upgrade } from '@/commands/upgrade'
|
|
30
30
|
import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
|
|
31
|
-
|
|
32
|
-
const GREY = '\x1b[0;90m'
|
|
33
|
-
const WHITE = '\x1b[1;37m'
|
|
34
|
-
const NC = '\x1b[0m'
|
|
31
|
+
import { palette } from '@/ui'
|
|
35
32
|
|
|
36
33
|
function showHelp(): void {
|
|
34
|
+
// The help text is the one framed surface written to stdout, so it asks
|
|
35
|
+
// about that stream rather than the stderr every other writer here uses.
|
|
36
|
+
const { GREY, NC, WHITE } = palette(process.stdout)
|
|
37
37
|
const lines = [
|
|
38
38
|
`${GREY}┌${NC}`,
|
|
39
39
|
`${GREY}├${NC} ${WHITE}Usage:${NC} aitk [command]`,
|
package/src/commands/claude.ts
CHANGED
|
@@ -44,16 +44,13 @@ import {
|
|
|
44
44
|
logStep,
|
|
45
45
|
logWarn,
|
|
46
46
|
outro,
|
|
47
|
+
palette,
|
|
47
48
|
pipeOutput,
|
|
48
49
|
plural,
|
|
49
50
|
select,
|
|
50
51
|
} from '@/ui'
|
|
51
52
|
import { describeSkew, readSkew, type SkewReport } from '@/version/skew'
|
|
52
53
|
|
|
53
|
-
const GREEN = '\x1b[0;32m'
|
|
54
|
-
const GREY = '\x1b[0;90m'
|
|
55
|
-
const NC = '\x1b[0m'
|
|
56
|
-
|
|
57
54
|
interface SeedsListOptions {
|
|
58
55
|
readonly json?: boolean
|
|
59
56
|
readonly names?: boolean
|
|
@@ -266,6 +263,7 @@ export function register(program: Command): void {
|
|
|
266
263
|
}
|
|
267
264
|
|
|
268
265
|
function succeed(message: string): number {
|
|
266
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
269
267
|
outro()
|
|
270
268
|
process.stderr.write(`${GREEN}✓ ${message}${NC}\n`)
|
|
271
269
|
return 0
|
|
@@ -472,6 +470,7 @@ async function runSeedsList(opts: SeedsListOptions): Promise<number> {
|
|
|
472
470
|
return 0
|
|
473
471
|
}
|
|
474
472
|
|
|
473
|
+
const { GREY, NC } = palette(process.stderr)
|
|
475
474
|
intro('aitk claude')
|
|
476
475
|
logStep('Seed docs')
|
|
477
476
|
for (const listing of listings) {
|
|
@@ -531,6 +530,7 @@ async function runSkillsDrift(
|
|
|
531
530
|
// is the moment a skew warning is worth most, since an old binary is one
|
|
532
531
|
// reason the cache and the CLI disagree in the first place.
|
|
533
532
|
if (skew.state === 'behind') {
|
|
533
|
+
const { GREY, NC } = palette(process.stderr)
|
|
534
534
|
process.stderr.write(`${GREY}${describeSkew(skew)}${NC}\n`)
|
|
535
535
|
}
|
|
536
536
|
}
|
package/src/commands/design.ts
CHANGED
|
@@ -2,12 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { renderDesignDoc } from '@/design/render'
|
|
5
|
-
|
|
6
|
-
const GREY = '\x1b[0;90m'
|
|
7
|
-
const WHITE = '\x1b[1;37m'
|
|
8
|
-
const RED = '\x1b[0;31m'
|
|
9
|
-
const GREEN = '\x1b[0;32m'
|
|
10
|
-
const NC = '\x1b[0m'
|
|
5
|
+
import { palette } from '@/ui'
|
|
11
6
|
|
|
12
7
|
export function register(program: Command): void {
|
|
13
8
|
const design = program
|
|
@@ -22,6 +17,7 @@ export function register(program: Command): void {
|
|
|
22
17
|
.action((opts: { source: string; out: string }) => {
|
|
23
18
|
const sourcePath = resolve(process.cwd(), opts.source)
|
|
24
19
|
const outDir = resolve(process.cwd(), opts.out)
|
|
20
|
+
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
25
21
|
if (!existsSync(sourcePath)) {
|
|
26
22
|
process.stderr.write(
|
|
27
23
|
`${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${opts.source} not found\n${GREY}└${NC}\n`,
|
package/src/commands/feedback.ts
CHANGED
|
@@ -4,10 +4,7 @@ import type { Command } from 'commander'
|
|
|
4
4
|
import { deriveSlug, deriveTitle } from '@/commands/feedback-format'
|
|
5
5
|
import { PROJECT_ROOT } from '@/project-root'
|
|
6
6
|
import { createGithubIssue } from '@/github'
|
|
7
|
-
import { frameError, frameSuccess } from '@/ui'
|
|
8
|
-
|
|
9
|
-
const YELLOW = '\x1b[0;33m'
|
|
10
|
-
const NC = '\x1b[0m'
|
|
7
|
+
import { frameError, frameSuccess, palette } from '@/ui'
|
|
11
8
|
|
|
12
9
|
function readStdin(): Promise<string> {
|
|
13
10
|
return new Promise((resolveStream, rejectStream) => {
|
|
@@ -88,6 +85,7 @@ export function register(program: Command): void {
|
|
|
88
85
|
process.exitCode = 1
|
|
89
86
|
return
|
|
90
87
|
}
|
|
88
|
+
const { NC, YELLOW } = palette(process.stderr)
|
|
91
89
|
process.stderr.write(
|
|
92
90
|
`${YELLOW}! gh unavailable, wrote local scratch instead${NC}\n`,
|
|
93
91
|
)
|
package/src/commands/gov.ts
CHANGED
|
@@ -30,12 +30,10 @@ import {
|
|
|
30
30
|
logStep,
|
|
31
31
|
logWarn,
|
|
32
32
|
outro,
|
|
33
|
+
palette,
|
|
33
34
|
select,
|
|
34
35
|
} from '@/ui'
|
|
35
36
|
|
|
36
|
-
const GREEN = '\x1b[0;32m'
|
|
37
|
-
const NC = '\x1b[0m'
|
|
38
|
-
|
|
39
37
|
const PAYLOAD_REL = join('.claude', '.tmp', 'gov', 'rules.md')
|
|
40
38
|
const RULES_REL = join('.claude', 'rules')
|
|
41
39
|
|
|
@@ -461,6 +459,7 @@ async function runInstall(
|
|
|
461
459
|
)
|
|
462
460
|
}
|
|
463
461
|
|
|
462
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
464
463
|
outro()
|
|
465
464
|
process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
|
|
466
465
|
return 0
|
|
@@ -510,6 +509,7 @@ async function runBuild(target: string): Promise<number> {
|
|
|
510
509
|
await writeFile(output, buildRulesPayload(files))
|
|
511
510
|
logAdd(PAYLOAD_REL)
|
|
512
511
|
|
|
512
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
513
513
|
outro()
|
|
514
514
|
process.stderr.write(
|
|
515
515
|
`${GREEN}✓ Rules built (${files.length} rules → ${PAYLOAD_REL})${NC}\n`,
|
package/src/commands/init.ts
CHANGED
|
@@ -6,11 +6,7 @@ import { type InitFlags, parseSkip, planInit } from '@/init/plan'
|
|
|
6
6
|
import { runDomains } from '@/init/run'
|
|
7
7
|
import { buildSteps } from '@/init/steps'
|
|
8
8
|
import { resolveTarget } from '@/target'
|
|
9
|
-
import { intro, logInfo, logStep, logWarn, outro, select } from '@/ui'
|
|
10
|
-
|
|
11
|
-
const GREEN = '\x1b[0;32m'
|
|
12
|
-
const YELLOW = '\x1b[0;33m'
|
|
13
|
-
const NC = '\x1b[0m'
|
|
9
|
+
import { intro, logInfo, logStep, logWarn, outro, palette, select } from '@/ui'
|
|
14
10
|
|
|
15
11
|
interface InitOptions {
|
|
16
12
|
/** Always present: the option falls back to `DEFAULT_STACK`. */
|
|
@@ -107,6 +103,8 @@ async function runInit(
|
|
|
107
103
|
outro()
|
|
108
104
|
process.stderr.write('\n')
|
|
109
105
|
|
|
106
|
+
const { GREEN, NC, YELLOW } = palette(process.stderr)
|
|
107
|
+
|
|
110
108
|
if (failed.length === 0) {
|
|
111
109
|
process.stderr.write(
|
|
112
110
|
`${GREEN}✓ Project initialized (${plan.total} domains)${NC}\n`,
|
package/src/commands/slides.ts
CHANGED
|
@@ -5,12 +5,7 @@ import { LAYOUTS } from '@/slides/layouts'
|
|
|
5
5
|
import { openDeck } from '@/slides/open'
|
|
6
6
|
import { renderSlidesDoc } from '@/slides/render'
|
|
7
7
|
import type { Variant } from '@/slides/styles'
|
|
8
|
-
import { intro, outro } from '@/ui'
|
|
9
|
-
|
|
10
|
-
const GREY = '\x1b[0;90m'
|
|
11
|
-
const GREEN = '\x1b[0;32m'
|
|
12
|
-
const RED = '\x1b[0;31m'
|
|
13
|
-
const NC = '\x1b[0m'
|
|
8
|
+
import { intro, outro, palette } from '@/ui'
|
|
14
9
|
|
|
15
10
|
export function register(program: Command): void {
|
|
16
11
|
const slides = program
|
|
@@ -44,6 +39,7 @@ export function register(program: Command): void {
|
|
|
44
39
|
}
|
|
45
40
|
const variant = parseVariant(opts.variant)
|
|
46
41
|
const mirror = resolveMirror(opts.mirror)
|
|
42
|
+
const { GREEN, GREY, NC, RED } = palette(process.stderr)
|
|
47
43
|
intro('Render slides')
|
|
48
44
|
const result = await renderSlidesDoc(sourcePath, outDir, {
|
|
49
45
|
variant,
|
|
@@ -81,6 +77,7 @@ export function register(program: Command): void {
|
|
|
81
77
|
process.stdout.write(`${JSON.stringify(LAYOUTS)}\n`)
|
|
82
78
|
return
|
|
83
79
|
}
|
|
80
|
+
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
84
81
|
intro('Slide layouts')
|
|
85
82
|
for (const layout of LAYOUTS) {
|
|
86
83
|
process.stderr.write(
|
|
@@ -121,6 +118,7 @@ function fail(message: string): never {
|
|
|
121
118
|
*/
|
|
122
119
|
function reportFailure(error: unknown): void {
|
|
123
120
|
if (!(error instanceof SlidesError)) throw error
|
|
121
|
+
const { GREY, NC, RED } = palette(process.stderr)
|
|
124
122
|
process.stderr.write(
|
|
125
123
|
`${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${error.message}\n${GREY}└${NC}\n`,
|
|
126
124
|
)
|
package/src/commands/snippets.ts
CHANGED
|
@@ -21,12 +21,10 @@ import {
|
|
|
21
21
|
logStep,
|
|
22
22
|
logWarn,
|
|
23
23
|
outro,
|
|
24
|
+
palette,
|
|
24
25
|
select,
|
|
25
26
|
} from '@/ui'
|
|
26
27
|
|
|
27
|
-
const GREEN = '\x1b[0;32m'
|
|
28
|
-
const NC = '\x1b[0m'
|
|
29
|
-
|
|
30
28
|
const SNIPPETS_REL = '.claude/snippets'
|
|
31
29
|
|
|
32
30
|
const PASS_THROUGH_VERBS = ['create'] as const
|
|
@@ -180,6 +178,7 @@ async function runInstall(
|
|
|
180
178
|
}
|
|
181
179
|
await recordStamp(createSnippetsAdapter(PROJECT_ROOT), resolved, new Date())
|
|
182
180
|
|
|
181
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
183
182
|
outro()
|
|
184
183
|
process.stderr.write(`${GREEN}✓ Snippets installed${NC}\n`)
|
|
185
184
|
return 0
|
|
@@ -21,13 +21,10 @@ import {
|
|
|
21
21
|
logStep,
|
|
22
22
|
logWarn,
|
|
23
23
|
outro,
|
|
24
|
+
palette,
|
|
24
25
|
select,
|
|
25
26
|
} from '@/ui'
|
|
26
27
|
|
|
27
|
-
const GREEN = '\x1b[0;32m'
|
|
28
|
-
const GREY = '\x1b[0;90m'
|
|
29
|
-
const NC = '\x1b[0m'
|
|
30
|
-
|
|
31
28
|
interface InstallOptions {
|
|
32
29
|
/** Always present: the option falls back to `ALL_SELECTION`. */
|
|
33
30
|
readonly only: string
|
|
@@ -196,6 +193,7 @@ async function runInstall(target: string, selection: string): Promise<number> {
|
|
|
196
193
|
await recordStamp(createStandardsAdapter(PROJECT_ROOT), resolved, new Date())
|
|
197
194
|
|
|
198
195
|
outro()
|
|
196
|
+
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
199
197
|
process.stderr.write(
|
|
200
198
|
`\n${GREEN}✓ Standards installed${NC} ${GREY}(${files.length} files)${NC}\n`,
|
|
201
199
|
)
|
package/src/commands/sync.ts
CHANGED
|
@@ -29,13 +29,10 @@ import {
|
|
|
29
29
|
logStep,
|
|
30
30
|
logWarn,
|
|
31
31
|
outro,
|
|
32
|
+
palette,
|
|
32
33
|
} from '@/ui'
|
|
33
34
|
import { describeSkew } from '@/version/skew'
|
|
34
35
|
|
|
35
|
-
const GREY = '\x1b[0;90m'
|
|
36
|
-
const YELLOW = '\x1b[0;33m'
|
|
37
|
-
const NC = '\x1b[0m'
|
|
38
|
-
|
|
39
36
|
const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
|
|
40
37
|
standards: ['standards', 'sync'],
|
|
41
38
|
snippets: ['snippets', 'sync'],
|
|
@@ -180,6 +177,7 @@ function renderCheck(report: CheckReport): void {
|
|
|
180
177
|
)
|
|
181
178
|
if (uncovered.length === 0) return
|
|
182
179
|
|
|
180
|
+
const { GREY, NC } = palette(process.stderr)
|
|
183
181
|
process.stderr.write(
|
|
184
182
|
`${GREY}Unstamped: ${uncovered.join(', ')}. Run the matching sync to record one.${NC}\n`,
|
|
185
183
|
)
|
|
@@ -338,6 +336,7 @@ async function runSync(target: string): Promise<number> {
|
|
|
338
336
|
if (typeof resolved === 'number') return resolved
|
|
339
337
|
|
|
340
338
|
const git = createGitRunner(resolved)
|
|
339
|
+
const { GREY, NC, YELLOW } = palette(process.stderr)
|
|
341
340
|
|
|
342
341
|
logStep('Checking working tree')
|
|
343
342
|
if (!isTreeClean(await git.status([]))) {
|
package/src/commands/tooling.ts
CHANGED
|
@@ -28,12 +28,10 @@ import {
|
|
|
28
28
|
logStep,
|
|
29
29
|
logWarn,
|
|
30
30
|
outro,
|
|
31
|
+
palette,
|
|
31
32
|
select,
|
|
32
33
|
} from '@/ui'
|
|
33
34
|
|
|
34
|
-
const GREEN = '\x1b[0;32m'
|
|
35
|
-
const NC = '\x1b[0m'
|
|
36
|
-
|
|
37
35
|
const PASS_THROUGH_VERBS = ['ref', 'create', 'verify'] as const
|
|
38
36
|
|
|
39
37
|
interface SyncOptions {
|
|
@@ -256,6 +254,7 @@ async function runSync(
|
|
|
256
254
|
report(result, includeReferences)
|
|
257
255
|
|
|
258
256
|
const mode = resolveWriteMode(opts)
|
|
257
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
259
258
|
|
|
260
259
|
if (result.totalChanges === 0) {
|
|
261
260
|
// The stamp is a write like any other, so a run with no authority to write
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import { ensureYtDlp, fetchOne } from '@/transcripts/fetch'
|
|
4
|
-
|
|
5
|
-
const GREY = '\x1b[0;90m'
|
|
6
|
-
const WHITE = '\x1b[1;37m'
|
|
7
|
-
const RED = '\x1b[0;31m'
|
|
8
|
-
const GREEN = '\x1b[0;32m'
|
|
9
|
-
const NC = '\x1b[0m'
|
|
4
|
+
import { palette } from '@/ui'
|
|
10
5
|
|
|
11
6
|
interface TranscriptOptions {
|
|
12
7
|
out: string
|
|
@@ -24,6 +19,7 @@ export function register(program: Command): void {
|
|
|
24
19
|
)
|
|
25
20
|
.action(async (url: string, opts: TranscriptOptions) => {
|
|
26
21
|
const outDir = resolve(process.cwd(), opts.out)
|
|
22
|
+
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
27
23
|
process.stderr.write(
|
|
28
24
|
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}aitk transcripts${NC}\n`,
|
|
29
25
|
)
|
package/src/commands/wiki.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
logStep,
|
|
10
10
|
logWarn,
|
|
11
11
|
outro,
|
|
12
|
+
palette,
|
|
12
13
|
select,
|
|
13
14
|
} from '@/ui'
|
|
14
15
|
import {
|
|
@@ -20,9 +21,6 @@ import {
|
|
|
20
21
|
WIKI_INDEX_REL,
|
|
21
22
|
} from '@/wiki/init'
|
|
22
23
|
|
|
23
|
-
const GREEN = '\x1b[0;32m'
|
|
24
|
-
const NC = '\x1b[0m'
|
|
25
|
-
|
|
26
24
|
export function register(program: Command): void {
|
|
27
25
|
const wiki = program
|
|
28
26
|
.command('wiki')
|
|
@@ -59,6 +57,7 @@ async function runInit(target: string): Promise<number> {
|
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
const plan = planWikiInit(resolved)
|
|
60
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
62
61
|
|
|
63
62
|
logStep(`Scanning ${WIKI_DIR_REL}`)
|
|
64
63
|
if (plan.changes.includes('dir')) logAdd(WIKI_DIR_REL)
|
package/src/design/parse.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* One table cell, split into the value a renderer emits and whether the record
|
|
5
|
+
* marked it as unsourced.
|
|
6
|
+
*
|
|
7
|
+
* The marker sits inside the cell rather than in a trailing column, because a
|
|
8
|
+
* trailing marker breaks the table parse. Splitting it out here is what keeps a
|
|
9
|
+
* swatch or a font sample built from the value alone.
|
|
10
|
+
*/
|
|
11
|
+
export interface Cell {
|
|
12
|
+
value: string
|
|
13
|
+
tagged: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Row = Record<string, Cell>
|
|
4
17
|
|
|
5
18
|
export interface DesignDoc {
|
|
6
19
|
personality: string
|
|
@@ -12,6 +25,11 @@ export interface DesignDoc {
|
|
|
12
25
|
iconography: string
|
|
13
26
|
}
|
|
14
27
|
|
|
28
|
+
const VERIFY_TAG = /\s*\?\s*verify\s*$/
|
|
29
|
+
|
|
30
|
+
/** A cell whose whole content is one balanced code span and nothing else. */
|
|
31
|
+
const CODE_SPAN = /^`([^`]*)`$/
|
|
32
|
+
|
|
15
33
|
export function parseDesignDoc(path: string): DesignDoc {
|
|
16
34
|
const raw = readFileSync(path, 'utf8')
|
|
17
35
|
const sections = splitSections(raw)
|
|
@@ -58,22 +76,41 @@ function table(body: string | undefined): Row[] {
|
|
|
58
76
|
if (!body) return []
|
|
59
77
|
const rows = body.split('\n').filter((l) => l.trim().startsWith('|'))
|
|
60
78
|
if (rows.length < 2) return []
|
|
61
|
-
const headers = splitRow(rows[0])
|
|
79
|
+
const headers = splitRow(rows[0]).map((c) => c.value)
|
|
62
80
|
const data = rows.slice(2)
|
|
63
81
|
return data.map((line) => {
|
|
64
82
|
const cells = splitRow(line)
|
|
65
83
|
const row: Row = {}
|
|
66
84
|
headers.forEach((h, i) => {
|
|
67
|
-
row[h] =
|
|
85
|
+
row[h] = cells[i] ?? emptyCell()
|
|
68
86
|
})
|
|
69
87
|
return row
|
|
70
88
|
})
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
function
|
|
91
|
+
function emptyCell(): Cell {
|
|
92
|
+
return { value: '', tagged: false }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function splitRow(line: string): Cell[] {
|
|
74
96
|
return line
|
|
75
97
|
.replace(/^\s*\|/, '')
|
|
76
98
|
.replace(/\|\s*$/, '')
|
|
77
99
|
.split('|')
|
|
78
|
-
.map(
|
|
100
|
+
.map(parseCell)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The tag is tested against the cell with any surrounding code span removed,
|
|
105
|
+
* since a value wrapping itself in backticks puts one after the tag and an
|
|
106
|
+
* end-anchored test misses it there. The span is restored around the clean
|
|
107
|
+
* value so an untagged cell and a tagged one carry the same formatting.
|
|
108
|
+
*/
|
|
109
|
+
function parseCell(raw: string): Cell {
|
|
110
|
+
const trimmed = raw.trim()
|
|
111
|
+
const span = trimmed.match(CODE_SPAN)
|
|
112
|
+
const inner = span ? span[1] : trimmed
|
|
113
|
+
if (!VERIFY_TAG.test(inner)) return { value: trimmed, tagged: false }
|
|
114
|
+
const value = inner.replace(VERIFY_TAG, '')
|
|
115
|
+
return { value: span ? `\`${value}\`` : value, tagged: true }
|
|
79
116
|
}
|
package/src/design/render.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import type { DesignDoc, Row } from '@/design/parse'
|
|
3
|
+
import type { Cell, DesignDoc, Row } from '@/design/parse'
|
|
4
4
|
import { parseDesignDoc } from '@/design/parse'
|
|
5
5
|
|
|
6
6
|
export interface RenderResult {
|
|
@@ -28,29 +28,99 @@ function slug(s: string): string {
|
|
|
28
28
|
.replace(/(^-|-$)/g, '')
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function cell(row: Row, key: string): Cell {
|
|
32
|
+
return row[key] ?? { value: '', tagged: false }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The value alone. Every swatch, sample, and custom property is built from it. */
|
|
36
|
+
function val(row: Row, key: string): string {
|
|
37
|
+
return cell(row, key).value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The marker, rendered beside a value rather than inside it. */
|
|
41
|
+
function mark(row: Row, key: string): string {
|
|
42
|
+
return cell(row, key).tagged
|
|
43
|
+
? ' <span class="verify" title="No source anchors this value">? verify</span>'
|
|
44
|
+
: ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A displayed cell: its escaped text, then its marker when it carries one. */
|
|
48
|
+
function cellText(row: Row, key: string): string {
|
|
49
|
+
return escape(val(row, key)) + mark(row, key)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Confidence {
|
|
53
|
+
tagged: number
|
|
54
|
+
total: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The columns the confidence ratio reads, fixed by `standards/design.md`. The
|
|
59
|
+
* first column of each table names its row, and `Multiplier` and `When used`
|
|
60
|
+
* restate what the row already carries, so none of them is something a source
|
|
61
|
+
* could anchor and none belongs in the denominator.
|
|
62
|
+
*/
|
|
63
|
+
const ANCHORABLE = {
|
|
64
|
+
borders: ['Radius', 'Width'],
|
|
65
|
+
color: ['Intent', 'Value'],
|
|
66
|
+
spacing: ['Value'],
|
|
67
|
+
typography: ['Family', 'Weight', 'Size', 'Line height'],
|
|
68
|
+
} as const
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A cell counts when it carries a tag, or when it holds a value in a column a
|
|
72
|
+
* source could anchor. A blank the record left unfilled is neither, and so is a
|
|
73
|
+
* row name. Counting a tagged cell whichever column it sits in is what keeps a
|
|
74
|
+
* marker the preview draws from sitting outside the ratio printed beside it.
|
|
75
|
+
*/
|
|
76
|
+
function confidence(doc: DesignDoc): Confidence {
|
|
77
|
+
const tables: ReadonlyArray<readonly [Row[], readonly string[]]> = [
|
|
78
|
+
[doc.color, ANCHORABLE.color],
|
|
79
|
+
[doc.typography, ANCHORABLE.typography],
|
|
80
|
+
[doc.spacing, ANCHORABLE.spacing],
|
|
81
|
+
[doc.borders, ANCHORABLE.borders],
|
|
82
|
+
]
|
|
83
|
+
let tagged = 0
|
|
84
|
+
let total = 0
|
|
85
|
+
for (const [rows, columns] of tables) {
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
for (const [key, c] of Object.entries(row)) {
|
|
88
|
+
if (!c.tagged && (!c.value || !columns.includes(key))) continue
|
|
89
|
+
total += 1
|
|
90
|
+
if (c.tagged) tagged += 1
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { tagged, total }
|
|
95
|
+
}
|
|
96
|
+
|
|
31
97
|
function buildCss(doc: DesignDoc): string {
|
|
32
98
|
const lines: string[] = [':root {']
|
|
33
99
|
for (const row of doc.color) {
|
|
34
|
-
if (row
|
|
35
|
-
lines.push(` --color-${slug(row
|
|
100
|
+
if (val(row, 'Value')) {
|
|
101
|
+
lines.push(` --color-${slug(val(row, 'Role'))}: ${val(row, 'Value')};`)
|
|
36
102
|
}
|
|
37
103
|
}
|
|
38
104
|
for (const row of doc.spacing) {
|
|
39
|
-
if (row
|
|
40
|
-
lines.push(` --space-${slug(row
|
|
105
|
+
if (val(row, 'Value')) {
|
|
106
|
+
lines.push(` --space-${slug(val(row, 'Step'))}: ${val(row, 'Value')};`)
|
|
41
107
|
}
|
|
42
108
|
}
|
|
43
109
|
for (const row of doc.typography) {
|
|
44
|
-
if (row
|
|
45
|
-
lines.push(
|
|
110
|
+
if (val(row, 'Size')) {
|
|
111
|
+
lines.push(
|
|
112
|
+
` --type-${slug(val(row, 'Role'))}-size: ${val(row, 'Size')};`,
|
|
113
|
+
)
|
|
46
114
|
}
|
|
47
|
-
if (row
|
|
48
|
-
lines.push(
|
|
115
|
+
if (val(row, 'Line height')) {
|
|
116
|
+
lines.push(
|
|
117
|
+
` --type-${slug(val(row, 'Role'))}-lh: ${val(row, 'Line height')};`,
|
|
118
|
+
)
|
|
49
119
|
}
|
|
50
120
|
}
|
|
51
121
|
for (const row of doc.borders) {
|
|
52
|
-
if (row
|
|
53
|
-
lines.push(` --radius-${slug(row
|
|
122
|
+
if (val(row, 'Radius')) {
|
|
123
|
+
lines.push(` --radius-${slug(val(row, 'Role'))}: ${val(row, 'Radius')};`)
|
|
54
124
|
}
|
|
55
125
|
}
|
|
56
126
|
lines.push('}')
|
|
@@ -75,6 +145,14 @@ function buildHtml(doc: DesignDoc): string {
|
|
|
75
145
|
sectionLine('Motion', doc.motion),
|
|
76
146
|
sectionLine('Iconography', doc.iconography),
|
|
77
147
|
]
|
|
148
|
+
const { tagged, total } = confidence(doc)
|
|
149
|
+
const verifyStyle = tagged
|
|
150
|
+
? '\n .verify { color: #a4471c; font-size: 12px; font-weight: 600; margin-left: 0.35rem; white-space: nowrap; }'
|
|
151
|
+
: ''
|
|
152
|
+
const verb = tagged === 1 ? 'carries' : 'carry'
|
|
153
|
+
const summary = tagged
|
|
154
|
+
? `\n<p class="note">${total - tagged} of ${total} cells are anchored to a source. The other ${tagged} ${verb} <code>? verify</code>, so nothing anchors them yet.</p>`
|
|
155
|
+
: ''
|
|
78
156
|
return `<!doctype html>
|
|
79
157
|
<html lang="en">
|
|
80
158
|
<head>
|
|
@@ -91,12 +169,12 @@ function buildHtml(doc: DesignDoc): string {
|
|
|
91
169
|
.swatch { display: inline-block; width: 1.5rem; height: 1.5rem; border-radius: 4px; border: 1px solid #ddd; vertical-align: middle; margin-right: 0.5rem; }
|
|
92
170
|
.bar { display: inline-block; height: 1rem; background: #888; border-radius: 2px; vertical-align: middle; }
|
|
93
171
|
.note { color: #666; font-size: 13px; margin-top: 0.5rem; }
|
|
94
|
-
.empty { color: #999; font-style: italic; }
|
|
172
|
+
.empty { color: #999; font-style: italic; }${verifyStyle}
|
|
95
173
|
</style>
|
|
96
174
|
</head>
|
|
97
175
|
<body>
|
|
98
176
|
<h1>Design tokens</h1>
|
|
99
|
-
<p class="note">Generated from <code>.claude/DESIGN.md</code> by <code>aitk design render</code>. Token preview only, not a screen mock.</p
|
|
177
|
+
<p class="note">Generated from <code>.claude/DESIGN.md</code> by <code>aitk design render</code>. Token preview only, not a screen mock.</p>${summary}
|
|
100
178
|
${sections.join('\n')}
|
|
101
179
|
</body>
|
|
102
180
|
</html>
|
|
@@ -112,11 +190,12 @@ function sectionColor(rows: Row[]): string {
|
|
|
112
190
|
if (!rows.length) return ''
|
|
113
191
|
const body = rows
|
|
114
192
|
.map((r) => {
|
|
115
|
-
const
|
|
116
|
-
|
|
193
|
+
const value = val(r, 'Value')
|
|
194
|
+
const swatch = value
|
|
195
|
+
? `<span class="swatch" style="background:${escape(value)}"></span>`
|
|
117
196
|
: '<span class="swatch"></span>'
|
|
118
|
-
const
|
|
119
|
-
return `<tr><td>${swatch}${
|
|
197
|
+
const shown = value ? escape(value) : '<span class="empty">unset</span>'
|
|
198
|
+
return `<tr><td>${swatch}${cellText(r, 'Role')}</td><td>${cellText(r, 'Intent')}</td><td><code>${shown}</code>${mark(r, 'Value')}</td></tr>`
|
|
120
199
|
})
|
|
121
200
|
.join('\n')
|
|
122
201
|
return `<h2>Color</h2>\n<table><thead><tr><th>Role</th><th>Intent</th><th>Value</th></tr></thead><tbody>${body}</tbody></table>`
|
|
@@ -126,12 +205,12 @@ function sectionTypography(rows: Row[]): string {
|
|
|
126
205
|
if (!rows.length) return ''
|
|
127
206
|
const body = rows
|
|
128
207
|
.map((r) => {
|
|
129
|
-
const family = r
|
|
130
|
-
const weight = r
|
|
131
|
-
const size = r
|
|
132
|
-
const lh = r
|
|
208
|
+
const family = val(r, 'Family') || 'system-ui'
|
|
209
|
+
const weight = val(r, 'Weight') || '400'
|
|
210
|
+
const size = val(r, 'Size') || '16px'
|
|
211
|
+
const lh = val(r, 'Line height') || '1.4'
|
|
133
212
|
const sample = `<span style="font-family:${escape(family)};font-weight:${escape(weight)};font-size:${escape(size)};line-height:${escape(lh)}">The quick brown fox</span>`
|
|
134
|
-
return `<tr><td>${
|
|
213
|
+
return `<tr><td>${cellText(r, 'Role')}</td><td>${escape(family)}${mark(r, 'Family')}</td><td>${escape(weight)}${mark(r, 'Weight')}</td><td>${escape(size)}${mark(r, 'Size')}</td><td>${escape(lh)}${mark(r, 'Line height')}</td><td>${sample}</td></tr>`
|
|
135
214
|
})
|
|
136
215
|
.join('\n')
|
|
137
216
|
return `<h2>Typography</h2>\n<table><thead><tr><th>Role</th><th>Family</th><th>Weight</th><th>Size</th><th>Line height</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
|
|
@@ -141,11 +220,11 @@ function sectionSpacing(rows: Row[]): string {
|
|
|
141
220
|
if (!rows.length) return ''
|
|
142
221
|
const body = rows
|
|
143
222
|
.map((r) => {
|
|
144
|
-
const value = r
|
|
223
|
+
const value = val(r, 'Value')
|
|
145
224
|
const bar = value
|
|
146
225
|
? `<span class="bar" style="width:${escape(value)}"></span>`
|
|
147
226
|
: '<span class="empty">unset</span>'
|
|
148
|
-
return `<tr><td>${
|
|
227
|
+
return `<tr><td>${cellText(r, 'Step')}</td><td>${cellText(r, 'Multiplier')}</td><td><code>${escape(value || 'unset')}</code>${mark(r, 'Value')}</td><td>${bar}</td></tr>`
|
|
149
228
|
})
|
|
150
229
|
.join('\n')
|
|
151
230
|
return `<h2>Spacing</h2>\n<table><thead><tr><th>Step</th><th>Multiplier</th><th>Value</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
|
|
@@ -155,10 +234,10 @@ function sectionBorders(rows: Row[]): string {
|
|
|
155
234
|
if (!rows.length) return ''
|
|
156
235
|
const body = rows
|
|
157
236
|
.map((r) => {
|
|
158
|
-
const radius = r
|
|
159
|
-
const width = r
|
|
237
|
+
const radius = val(r, 'Radius') || '0'
|
|
238
|
+
const width = val(r, 'Width') || '1px'
|
|
160
239
|
const sample = `<span style="display:inline-block;width:2rem;height:1.5rem;background:#eee;border:${escape(width)} solid #888;border-radius:${escape(radius)};vertical-align:middle"></span>`
|
|
161
|
-
return `<tr><td>${
|
|
240
|
+
return `<tr><td>${cellText(r, 'Role')}</td><td><code>${escape(radius)}</code>${mark(r, 'Radius')}</td><td><code>${escape(width)}</code>${mark(r, 'Width')}</td><td>${cellText(r, 'When used')}</td><td>${sample}</td></tr>`
|
|
162
241
|
})
|
|
163
242
|
.join('\n')
|
|
164
243
|
return `<h2>Borders</h2>\n<table><thead><tr><th>Role</th><th>Radius</th><th>Width</th><th>When used</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
|
package/src/sync/engine.ts
CHANGED
|
@@ -22,12 +22,10 @@ import {
|
|
|
22
22
|
logStep,
|
|
23
23
|
logWarn,
|
|
24
24
|
outro,
|
|
25
|
+
palette,
|
|
25
26
|
select,
|
|
26
27
|
} from '@/ui'
|
|
27
28
|
|
|
28
|
-
const GREEN = '\x1b[0;32m'
|
|
29
|
-
const NC = '\x1b[0m'
|
|
30
|
-
|
|
31
29
|
/**
|
|
32
30
|
* One installed file in three path flavours: absolute, relative to the
|
|
33
31
|
* domain's installed root, and relative to the target. Adapters match on
|
|
@@ -304,6 +302,7 @@ export async function runDomainSync(
|
|
|
304
302
|
}
|
|
305
303
|
|
|
306
304
|
const plan = planSync(adapter, resolved)
|
|
305
|
+
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
307
306
|
|
|
308
307
|
if (
|
|
309
308
|
!existsSync(adapter.installedRoot(resolved)) &&
|
|
@@ -358,7 +357,7 @@ export async function runDomainSync(
|
|
|
358
357
|
|
|
359
358
|
outro()
|
|
360
359
|
process.stderr.write(
|
|
361
|
-
`${GREEN}✓ Sync complete${NC}
|
|
360
|
+
`${GREEN}✓ Sync complete${NC} ${GREY}(${count} ${adapter.unit})${NC}\n`,
|
|
362
361
|
)
|
|
363
362
|
return 0
|
|
364
363
|
}
|
package/src/sync/workflow.ts
CHANGED
|
@@ -19,13 +19,11 @@ import {
|
|
|
19
19
|
logStep,
|
|
20
20
|
logWarn,
|
|
21
21
|
outro,
|
|
22
|
+
palette,
|
|
22
23
|
pipeOutput,
|
|
23
24
|
select,
|
|
24
25
|
} from '@/ui'
|
|
25
26
|
|
|
26
|
-
const GREEN = '\x1b[0;32m'
|
|
27
|
-
const NC = '\x1b[0m'
|
|
28
|
-
|
|
29
27
|
const PROTECTED_BRANCHES: readonly string[] = ['main', 'master']
|
|
30
28
|
|
|
31
29
|
export type WorkflowChoice = 'pr' | 'commit' | 'cancel'
|
|
@@ -195,6 +193,7 @@ function promptChoice(canOpenPullRequest: boolean): Promise<WorkflowChoice> {
|
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
function succeed(message: string): number {
|
|
196
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
198
197
|
outro()
|
|
199
198
|
process.stderr.write(`\n${GREEN}✓ ${message}${NC}\n`)
|
|
200
199
|
return 0
|
package/src/ui.ts
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export interface Palette {
|
|
2
|
+
readonly GREEN: string
|
|
3
|
+
readonly RED: string
|
|
4
|
+
readonly YELLOW: string
|
|
5
|
+
readonly WHITE: string
|
|
6
|
+
readonly GREY: string
|
|
7
|
+
readonly NC: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const COLOR: Palette = {
|
|
11
|
+
GREEN: '\x1b[0;32m',
|
|
12
|
+
RED: '\x1b[0;31m',
|
|
13
|
+
YELLOW: '\x1b[0;33m',
|
|
14
|
+
WHITE: '\x1b[1;37m',
|
|
15
|
+
GREY: '\x1b[0;90m',
|
|
16
|
+
NC: '\x1b[0m',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The blank palette keeps every frame character and drops only the escapes, so
|
|
21
|
+
* a captured run still reads as one block.
|
|
22
|
+
*/
|
|
23
|
+
const PLAIN: Palette = {
|
|
24
|
+
GREEN: '',
|
|
25
|
+
RED: '',
|
|
26
|
+
YELLOW: '',
|
|
27
|
+
WHITE: '',
|
|
28
|
+
GREY: '',
|
|
29
|
+
NC: '',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `NO_COLOR` follows the published convention, where any non-empty value turns
|
|
34
|
+
* color off whatever the value says.
|
|
35
|
+
*/
|
|
36
|
+
export function supportsColor(stream: { isTTY?: boolean }): boolean {
|
|
37
|
+
const optOut = process.env.NO_COLOR
|
|
38
|
+
if (optOut !== undefined && optOut !== '') return false
|
|
39
|
+
return stream.isTTY === true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The question is asked per stream rather than once for the process. The framed
|
|
44
|
+
* output goes to stderr and a structured record to stdout, so a run piping only
|
|
45
|
+
* its data keeps a terminal on stderr and keeps its color there. This is a
|
|
46
|
+
* third question again from `isNonInteractive`, which answers whether a caller
|
|
47
|
+
* can be prompted rather than whether a destination renders escapes.
|
|
48
|
+
*
|
|
49
|
+
* Read at write time rather than at import, so nothing freezes an answer taken
|
|
50
|
+
* before the caller's environment was in place.
|
|
51
|
+
*/
|
|
52
|
+
export function palette(stream: { isTTY?: boolean }): Palette {
|
|
53
|
+
return supportsColor(stream) ? COLOR : PLAIN
|
|
54
|
+
}
|
|
7
55
|
|
|
8
56
|
export function intro(title: string): void {
|
|
57
|
+
const { GREY, NC, WHITE } = palette(process.stderr)
|
|
9
58
|
process.stderr.write(`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}${title}${NC}\n`)
|
|
10
59
|
}
|
|
11
60
|
|
|
@@ -15,18 +64,22 @@ export function intro(title: string): void {
|
|
|
15
64
|
* leaving stdout clean for JSON and lists.
|
|
16
65
|
*/
|
|
17
66
|
export function logInfo(message: string): void {
|
|
67
|
+
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
18
68
|
process.stderr.write(`${GREY}│${NC} ${GREEN}✓${NC} ${message}\n`)
|
|
19
69
|
}
|
|
20
70
|
|
|
21
71
|
export function logWarn(message: string): void {
|
|
72
|
+
const { GREY, NC, YELLOW } = palette(process.stderr)
|
|
22
73
|
process.stderr.write(`${GREY}│${NC} ${YELLOW}!${NC} ${message}\n`)
|
|
23
74
|
}
|
|
24
75
|
|
|
25
76
|
export function logAdd(message: string): void {
|
|
77
|
+
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
26
78
|
process.stderr.write(`${GREY}│${NC} ${GREEN}+${NC} ${message}\n`)
|
|
27
79
|
}
|
|
28
80
|
|
|
29
81
|
export function logRemove(message: string): void {
|
|
82
|
+
const { GREY, NC, RED } = palette(process.stderr)
|
|
30
83
|
process.stderr.write(`${GREY}│${NC} ${RED}-${NC} ${message}\n`)
|
|
31
84
|
}
|
|
32
85
|
|
|
@@ -36,14 +89,17 @@ export function logRemove(message: string): void {
|
|
|
36
89
|
* exit code rather than terminating mid-write.
|
|
37
90
|
*/
|
|
38
91
|
export function logError(message: string): void {
|
|
92
|
+
const { GREY, NC, RED } = palette(process.stderr)
|
|
39
93
|
process.stderr.write(`${GREY}│${NC} ${RED}✗${NC} ${message}\n`)
|
|
40
94
|
}
|
|
41
95
|
|
|
42
96
|
export function logStep(message: string): void {
|
|
97
|
+
const { GREY, NC, WHITE } = palette(process.stderr)
|
|
43
98
|
process.stderr.write(`${GREY}│${NC}\n${GREY}├${NC} ${WHITE}${message}${NC}\n`)
|
|
44
99
|
}
|
|
45
100
|
|
|
46
101
|
export function outro(): void {
|
|
102
|
+
const { GREY, NC } = palette(process.stderr)
|
|
47
103
|
process.stderr.write(`${GREY}└${NC}\n`)
|
|
48
104
|
}
|
|
49
105
|
|
|
@@ -53,6 +109,7 @@ export function outro(): void {
|
|
|
53
109
|
* such as a pull request body or the output of a git mutation.
|
|
54
110
|
*/
|
|
55
111
|
export function pipeOutput(text: string): void {
|
|
112
|
+
const { GREY, NC } = palette(process.stderr)
|
|
56
113
|
const lines = text.replace(/\n$/, '').split('\n')
|
|
57
114
|
process.stderr.write(
|
|
58
115
|
`${lines.map((line) => `${GREY}│${NC} ${line}`).join('\n')}\n`,
|
|
@@ -65,12 +122,14 @@ export function plural(count: number, noun: string): string {
|
|
|
65
122
|
}
|
|
66
123
|
|
|
67
124
|
export function frameError(message: string): void {
|
|
125
|
+
const { GREY, NC, RED } = palette(process.stderr)
|
|
68
126
|
process.stderr.write(
|
|
69
127
|
`${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${message}\n${GREY}└${NC}\n`,
|
|
70
128
|
)
|
|
71
129
|
}
|
|
72
130
|
|
|
73
131
|
export function frameSuccess(command: string, target: string): void {
|
|
132
|
+
const { GREEN, GREY, NC, WHITE } = palette(process.stderr)
|
|
74
133
|
process.stderr.write(
|
|
75
134
|
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}${command}${NC}\n${GREY}│${NC}\n${GREY}│${NC} ${GREEN}✓${NC} ${target}\n${GREY}└${NC}\n`,
|
|
76
135
|
)
|
|
@@ -92,6 +151,7 @@ export async function select<Value>(opts: {
|
|
|
92
151
|
nonInteractiveDefault?: boolean
|
|
93
152
|
}): Promise<Value> {
|
|
94
153
|
const { message, options } = opts
|
|
154
|
+
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
95
155
|
const count = options.length
|
|
96
156
|
let cursor = 0
|
|
97
157
|
|
package/standards/design.md
CHANGED
|
@@ -34,6 +34,16 @@ Does not govern:
|
|
|
34
34
|
- Plain English over technical notation. If a section could be removed and the developer would still build correctly from wireframes and code alone, remove it.
|
|
35
35
|
- Keep table headers and role names intact so the render tooling can parse the token tables.
|
|
36
36
|
|
|
37
|
+
## The uncertainty tag
|
|
38
|
+
|
|
39
|
+
A cell no source anchors ends in ` ? verify`, written inside the cell rather than as a trailing column, since a trailing marker breaks the table parse. A cell wrapping itself in a code span carries the tag inside the span, as in `` `#ffffff ? verify` ``. Both spellings parse.
|
|
40
|
+
|
|
41
|
+
The renderer splits the tag off the value, so a swatch and a font sample are built from the value alone and the marker shows beside it. The preview also reports how many cells are anchored against how many are tagged, which is the reading a reviewer takes the record's overall confidence from.
|
|
42
|
+
|
|
43
|
+
Which columns that ratio reads is fixed by the table rather than by the record. The first column of each table names its row, and `Multiplier` and `When used` restate what the row already carries, so none of them is something a source could anchor and none is counted. That leaves `Intent` and `Value` in Color, `Family`, `Weight`, `Size`, and `Line height` in Typography, `Value` in Spacing, and `Radius` and `Width` in Borders. A cell tagged outside that set counts anyway, so a marker the preview draws is never missing from the ratio beside it.
|
|
44
|
+
|
|
45
|
+
A prose section takes its uncertainty inline instead, in a sentence saying what is proposed and what has yet to confirm it. A tag appended to a paragraph renders verbatim.
|
|
46
|
+
|
|
37
47
|
## Sections
|
|
38
48
|
|
|
39
49
|
Use `## Personality`, `## Color`, `## Typography`, `## Spacing`, `## Borders`, `## Motion`, and `## Iconography`. The token tables carry fixed headers the renderer reads.
|