@asrunjs/asrun 0.2.0 → 0.5.3

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.
Files changed (103) hide show
  1. package/.claude/commands/explore-case.md +18 -15
  2. package/.claude/commands/gen-spec.md +16 -13
  3. package/.claude/commands/heal-spec.md +26 -23
  4. package/.claude/commands/make-case.md +11 -8
  5. package/.claude/skills/exploring-cases/SKILL.md +146 -132
  6. package/.claude/skills/exploring-cases/references/open-questions.md +116 -112
  7. package/.claude/skills/exploring-cases/references/readiness.md +146 -137
  8. package/.claude/skills/exploring-cases/references/user-handoff.md +157 -147
  9. package/.claude/skills/generating-specs/SKILL.md +219 -207
  10. package/.claude/skills/generating-specs/references/conventions.md +148 -148
  11. package/.claude/skills/making-cases/SKILL.md +72 -64
  12. package/.claude/skills/making-cases/references/normalizing.md +39 -38
  13. package/.claude/skills/making-cases/references/writes-and-email.md +37 -36
  14. package/AGENTS.md +169 -154
  15. package/CLAUDE.md +2 -1
  16. package/README.md +221 -410
  17. package/dist/lib/build-artifacts.d.ts +8 -0
  18. package/dist/lib/build-artifacts.js +10 -0
  19. package/dist/lib/case-doc.d.ts +311 -0
  20. package/dist/lib/case-doc.js +1117 -0
  21. package/dist/lib/cases.d.ts +2 -15
  22. package/dist/lib/cases.js +5 -78
  23. package/dist/lib/command.d.ts +24 -0
  24. package/dist/lib/command.js +90 -0
  25. package/dist/lib/fixtures.js +2 -2
  26. package/dist/lib/login-card.d.ts +22 -0
  27. package/dist/lib/login-card.js +183 -0
  28. package/dist/lib/node-cli.d.ts +6 -2
  29. package/dist/lib/node-cli.js +16 -6
  30. package/dist/lib/profiles.d.ts +25 -1
  31. package/dist/lib/profiles.js +54 -1
  32. package/dist/lib/run-filters.js +10 -2
  33. package/dist/lib/session.d.ts +13 -1
  34. package/dist/lib/session.js +73 -7
  35. package/dist/lib/targets.d.ts +5 -13
  36. package/dist/lib/targets.js +4 -29
  37. package/dist/lib/values.js +2 -2
  38. package/dist/scripts/build.js +2 -1
  39. package/dist/scripts/check-cases.js +74 -6
  40. package/dist/scripts/doctor.js +115 -4
  41. package/dist/scripts/explore.js +22 -1
  42. package/dist/scripts/go.mjs +188 -188
  43. package/dist/scripts/init.d.ts +7 -1
  44. package/dist/scripts/init.js +62 -14
  45. package/dist/scripts/lint-specs.js +4 -1
  46. package/dist/scripts/login.js +30 -2
  47. package/dist/ui/server/actions.d.ts +4 -0
  48. package/dist/ui/server/actions.js +25 -12
  49. package/dist/ui/server/ai-models.d.ts +3 -0
  50. package/dist/ui/server/ai-models.js +347 -0
  51. package/dist/ui/server/ai-stream.d.ts +34 -9
  52. package/dist/ui/server/ai-stream.js +283 -77
  53. package/dist/ui/server/app.js +74 -12
  54. package/dist/ui/server/jobs.d.ts +7 -1
  55. package/dist/ui/server/jobs.js +69 -15
  56. package/dist/ui/server/profiles.d.ts +20 -1
  57. package/dist/ui/server/profiles.js +214 -38
  58. package/dist/ui/shared/ai-commands.d.ts +16 -6
  59. package/dist/ui/shared/ai-commands.js +35 -12
  60. package/dist/ui/shared/types.d.ts +114 -0
  61. package/docs/agents/case-format.md +193 -0
  62. package/docs/agents/framework-dev.md +167 -0
  63. package/docs/ai-workflow.md +235 -0
  64. package/docs/cases.md +189 -0
  65. package/docs/commands.md +175 -0
  66. package/docs/install.md +174 -0
  67. package/docs/profiles.md +139 -0
  68. package/docs/theme-guide.html +922 -692
  69. package/docs/troubleshooting.md +101 -0
  70. package/docs/web-ui.md +139 -0
  71. package/package.json +20 -1
  72. package/templates/.claude/commands/explore-case.md +22 -21
  73. package/templates/.claude/commands/gen-spec.md +21 -20
  74. package/templates/.claude/commands/heal-spec.md +31 -30
  75. package/templates/.claude/commands/make-case.md +13 -12
  76. package/templates/.mcp.json +8 -8
  77. package/templates/AGENTS.md +66 -45
  78. package/templates/cases/_template.md +57 -63
  79. package/templates/opencode.jsonc +16 -11
  80. package/templates/profiles.yaml +42 -29
  81. package/ui/dist/assets/{dist-D_I45xgJ.js → dist-BudNAjOv.js} +1 -1
  82. package/ui/dist/assets/{dist-CBV7_vDN.js → dist-C-eCSFja.js} +1 -1
  83. package/ui/dist/assets/{dist-gF2bFn-5.js → dist-CO4heSzh.js} +1 -1
  84. package/ui/dist/assets/{dist-DX8knLVr.js → dist-CPLhRzom.js} +1 -1
  85. package/ui/dist/assets/{dist-BqAROxzI.js → dist-DEPJZ3Me.js} +1 -1
  86. package/ui/dist/assets/{dist-BwYRbe5s.js → dist-DZ7RiqmA.js} +1 -1
  87. package/ui/dist/assets/{dist-BHaIOZdW.js → dist-LixzGniF.js} +1 -1
  88. package/ui/dist/assets/{dist-DHCY9KpV.js → dist-O_RVi4tP.js} +1 -1
  89. package/ui/dist/assets/{dist-BiE4suo9.js → dist-jPS6HCJ5.js} +1 -1
  90. package/ui/dist/assets/{dist-BSJMM_jp.js → dist-vmf3fnkM.js} +1 -1
  91. package/ui/dist/assets/index-BIGt8Hfz.js +338 -0
  92. package/ui/dist/assets/index-CTmnF0Wp.css +2 -0
  93. package/ui/dist/help/ai-tools.png +0 -0
  94. package/ui/dist/help/case-explore.png +0 -0
  95. package/ui/dist/help/case-generate.png +0 -0
  96. package/ui/dist/help/cases-memo.png +0 -0
  97. package/ui/dist/help/profile-form.png +0 -0
  98. package/ui/dist/help/profiles-login.png +0 -0
  99. package/ui/dist/help/reports.png +0 -0
  100. package/ui/dist/help/run-tests.png +0 -0
  101. package/ui/dist/index.html +22 -16
  102. package/ui/dist/assets/index-DmasQ6Rq.css +0 -2
  103. package/ui/dist/assets/index-I7uU93kk.js +0 -294
@@ -1,692 +1,922 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Inkstone theme guide · AsRun UI</title>
7
- <style>
8
- /* Inkstone theme, designed and created by Zoro Chen. */
9
- /* Tokens copied from ui/web/src/index.css. Keep both in sync. */
10
- :root {
11
- --paper: #f6f2e9;
12
- --card: #ffffff;
13
- --warm: #fcf9f3;
14
- --ink: #1f1f1f;
15
- --ink-2: #6b6b6b;
16
- --ink-3: #9c9c9c;
17
- --line: #e5e1d8;
18
- --line-mid: #dcd7cb;
19
-
20
- --seal: #b5443a;
21
- --seal-deep: #8a2f28;
22
- --seal-bg: #f1dad7;
23
- --seal-soft: #e8a79f;
24
- --seal-glow: #ff7b6e;
25
-
26
- --gold: #c9a54a;
27
- --gold-deep: #7a5a1e;
28
- --gold-bg: #f4e9c9;
29
- --gold-glow: #ffd23f;
30
- --warn-ink: #6e5b24;
31
-
32
- --qing: #5b8aa6;
33
- --qing-deep: #33506b;
34
- --qing-bg: #e2eaf0;
35
- --qing-glow: #4fc3ff;
36
-
37
- --jade: #4da791;
38
- --jade-deep: #3a8572;
39
- --jade-btn: #2e6b5a;
40
- --jade-bg: #e0e8d5;
41
- --jade-glow: #3de0b0;
42
-
43
- --jiang: #a65998;
44
-
45
- --cheng: #de8435;
46
-
47
- --dai: #2e4a5c;
48
- --dai-deep: #1e3340;
49
- --dai-bg: #e2ecef;
50
-
51
- --serif: 'Iowan Old Style', Georgia, 'Songti SC', 'Noto Serif SC', 'SimSun', serif;
52
- --sans: 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', ui-sans-serif, system-ui, sans-serif;
53
- --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
54
-
55
- --theme: var(--jade);
56
- --theme-ink: #2e6b5a;
57
- --theme-btn: #2e6b5a;
58
- --theme-bg: #e0e8d5;
59
- }
60
-
61
- * { box-sizing: border-box; }
62
- html { scroll-behavior: smooth; }
63
- body {
64
- margin: 0;
65
- background: var(--paper);
66
- color: var(--ink);
67
- font-family: var(--sans);
68
- line-height: 1.6;
69
- -webkit-font-smoothing: antialiased;
70
- }
71
- a { color: var(--qing-deep); }
72
- code, .mono { font-family: var(--mono); font-size: 0.85em; }
73
- code { background: var(--warm); border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; }
74
-
75
- .layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); max-width: 1100px; margin: 0 auto; }
76
- nav.toc {
77
- position: sticky; top: 0; align-self: start; height: 100vh; overflow: auto;
78
- padding: 32px 20px; border-right: 1px solid var(--line-mid);
79
- }
80
- nav.toc .brand { font-family: var(--serif); font-weight: 700; letter-spacing: 0.1em; margin-bottom: 16px; }
81
- nav.toc a {
82
- display: block; padding: 4px 8px; margin: 1px 0; border-radius: 4px;
83
- color: var(--ink-2); text-decoration: none; font-size: 14px;
84
- }
85
- nav.toc a:hover { background: var(--warm); color: var(--ink); }
86
- main { padding: 32px 40px 80px; min-width: 0; }
87
- @media (max-width: 800px) {
88
- .layout { display: block; }
89
- nav.toc { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--line-mid); }
90
- main { padding: 24px 20px 60px; }
91
- }
92
-
93
- h1, h2, h3 { font-family: var(--serif); }
94
- h1 { font-size: 30px; margin: 0 0 4px; border-left: 4px solid var(--jade); padding-left: 12px; }
95
- h2 { font-size: 22px; margin: 48px 0 8px; padding-bottom: 6px; border-bottom: 1px solid var(--line-mid); }
96
- h3 { font-size: 16px; margin: 24px 0 8px; }
97
- p { margin: 8px 0; }
98
- .author { margin: 0 0 12px; padding-left: 16px; }
99
- .lead { color: var(--ink-2); margin-bottom: 24px; }
100
- .muted { color: var(--ink-2); }
101
- .small { font-size: 13px; }
102
-
103
- .card {
104
- background:
105
- radial-gradient(120% 100% at 16% -8%, color-mix(in srgb, var(--theme) 9%, white) 0%, transparent 58%),
106
- var(--card);
107
- border: 1px solid var(--line);
108
- border-radius: 4px;
109
- box-shadow:
110
- 0 1px 2px color-mix(in srgb, var(--theme) 16%, transparent),
111
- 0 8px 20px -10px color-mix(in srgb, var(--theme) 34%, transparent);
112
- padding: 16px;
113
- }
114
-
115
- table { width: 100%; border-collapse: collapse; font-size: 14px; background: var(--card); border: 1px solid var(--line); }
116
- th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
117
- th { background: var(--warm); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-2); }
118
-
119
- .grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
120
- .swatch { border: 1px solid var(--line-mid); border-radius: 4px; overflow: hidden; background: var(--card); font-size: 12px; }
121
- .swatch .chip { height: 52px; }
122
- .swatch .meta { padding: 6px 8px; }
123
- .swatch .meta b { display: block; font-size: 13px; }
124
- .swatch .meta span { color: var(--ink-2); font-family: var(--mono); }
125
-
126
- .row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin: 12px 0; }
127
- .demo { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 16px; background: var(--paper); border: 1px dashed var(--line-mid); border-radius: 4px; }
128
- .label { font-size: 12px; color: var(--ink-2); font-family: var(--mono); }
129
-
130
- /* Buttons (mirror buttonClass in ui/web/src/components/status.tsx). */
131
- .btn {
132
- display: inline-flex; align-items: center; justify-content: center; gap: 6px;
133
- border: 0; border-radius: 4px; padding: 6px 12px; font: 500 14px var(--sans); cursor: pointer;
134
- transition: transform .15s, filter .15s, box-shadow .15s, background .15s;
135
- }
136
- .btn:hover { transform: translateY(-1px); }
137
- .btn[disabled] { opacity: .5; cursor: not-allowed; box-shadow: none; transform: none; }
138
- .btn svg { flex: none; }
139
- .btn-primary, .btn-success {
140
- color: #fff;
141
- background: var(--theme-btn);
142
- box-shadow: inset 0 1px 0 rgb(255 255 255 / .18), 0 3px 8px -2px color-mix(in srgb, var(--theme-btn) 50%, transparent);
143
- }
144
- .btn-success { background: var(--jade-btn); box-shadow: inset 0 1px 0 rgb(255 255 255 / .18), 0 3px 8px -2px color-mix(in srgb, var(--jade-btn) 50%, transparent); }
145
- .btn-primary:hover, .btn-success:hover { filter: brightness(1.1); }
146
- .btn-stop { color: #fff; background: var(--seal); box-shadow: inset 0 0 0 0 transparent, 0 3px 8px -2px color-mix(in srgb, var(--seal) 50%, transparent); }
147
- .btn-stop:hover { filter: brightness(1.1); }
148
- .btn-secondary { background: var(--card); color: var(--ink); box-shadow: 0 0 0 1px var(--line-mid); }
149
- .btn-secondary:hover { background: var(--warm); }
150
- .btn-info { background: var(--qing-bg); color: var(--qing-deep); box-shadow: 0 0 0 1px color-mix(in srgb, var(--qing) 50%, transparent); }
151
- .btn-info:hover { filter: brightness(.95); }
152
- .btn-danger { background: var(--card); color: var(--seal-deep); box-shadow: 0 0 0 1px color-mix(in srgb, var(--seal) 40%, transparent); }
153
- .btn-danger:hover { background: var(--seal-bg); }
154
-
155
- /* Badges (mirror TONES in status.tsx). */
156
- .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 2px; font-size: 12px; font-weight: 500; }
157
- .badge.green { background: var(--jade-bg); color: var(--jade-btn); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--jade-deep) 30%, transparent); }
158
- .badge.red { background: var(--seal-bg); color: var(--seal-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--seal) 35%, transparent); }
159
- .badge.amber { background: var(--gold-bg); color: var(--gold-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold-deep) 30%, transparent); }
160
- .badge.blue { background: var(--qing-bg); color: var(--qing-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--qing) 40%, transparent); }
161
- .badge.gray { background: var(--warm); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line-mid); }
162
- /* Environment markers (mirror envFillClass in status.tsx): solid fill, white text. */
163
- .env { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 2px; font-size: 12px; font-weight: 700; letter-spacing: 0.05em; color: #fff; }
164
- .env.production { background: var(--jiang); }
165
- .env.staging { background: var(--cheng); }
166
- .env.other { background: var(--gold-deep); }
167
-
168
- /* AI command select (mirror AI_COMMAND_STYLE in ui/web/src/pages/cases.tsx). */
169
- .cmd { position: relative; display: inline-block; }
170
- .cmd .dot { position: absolute; left: 12px; top: 50%; width: 8px; height: 8px; border-radius: 50%; transform: translateY(-50%); }
171
- .cmd .fake {
172
- display: inline-flex; align-items: center; height: 32px; padding: 0 12px 0 28px; border: 1px solid;
173
- border-radius: 4px; font-size: 14px; font-weight: 500;
174
- }
175
- .cmd.review .fake { border-color: color-mix(in srgb, var(--qing) 50%, transparent); background: var(--qing-bg); color: var(--qing-deep); }
176
- .cmd.review .dot { background: var(--qing); }
177
- .cmd.gen .fake { border-color: color-mix(in srgb, var(--gold-deep) 40%, transparent); background: var(--gold-bg); color: var(--gold-deep); }
178
- .cmd.gen .dot { background: var(--gold); }
179
- .cmd.heal .fake { border-color: color-mix(in srgb, var(--seal) 40%, transparent); background: var(--seal-bg); color: var(--seal-deep); }
180
- .cmd.heal .dot { background: var(--seal); }
181
-
182
- /* Dropdown demo (mirror the Select component in ui/web/src/components/status.tsx). */
183
- .select { position: relative; display: inline-block; }
184
- .select .fake {
185
- display: inline-flex; align-items: center; height: 32px; min-width: 120px; padding: 0 32px 0 8px; border: 1px solid var(--line-mid);
186
- border-radius: 4px; background: #fff; color: var(--ink); font-size: 14px;
187
- }
188
- .select .chev { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); color: var(--ink-2); pointer-events: none; }
189
- .select.focus .fake { border-color: var(--theme-demo, #5b8aa6); box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-demo, #5b8aa6) 25%, transparent); }
190
- .select.invalid .fake { border-color: var(--seal); background: var(--seal-bg); color: var(--seal-deep); }
191
- .select.invalid .chev { color: var(--seal-deep); }
192
- .select.dotted .fake { padding-left: 28px; font-weight: 500; }
193
- .select .dot { position: absolute; left: 12px; top: 50%; width: 8px; height: 8px; border-radius: 50%; transform: translateY(-50%); background: var(--gold); }
194
- .select.dotted .fake { border-color: color-mix(in srgb, var(--gold-deep) 40%, transparent); background: var(--gold-bg); color: var(--gold-deep); }
195
- .select-label { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; color: var(--ink-2); }
196
- .select-label.invalid { color: var(--seal-deep); }
197
-
198
- /* Sidebar sections (mirror SectionHeader, CaseSearch and the case list in ui/web/src/pages/cases.tsx). */
199
- .side { width: 300px; background: var(--card); border: 1px solid var(--line-mid); }
200
- .side .sec {
201
- display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--theme-btn); color: #fff; font-family: var(--serif);
202
- font-size: 14px; font-weight: 700; letter-spacing: 0.12em; border-top: 1px solid var(--theme-btn); border-bottom: 1px solid var(--theme-btn);
203
- }
204
- .side .sec svg { flex: none; }
205
- .side .sec .aside { margin-left: auto; font-family: var(--sans); font-size: 12px; font-weight: 400; letter-spacing: 0; color: rgb(255 255 255 / .75); }
206
- .side .sec .toggle { margin-left: auto; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; background: rgb(255 255 255 / .15); flex: none; }
207
- .side .sec .aside + .toggle { margin-left: 0; }
208
- .side .search { position: relative; padding: 8px; border-bottom: 1px solid var(--line-mid); }
209
- .side .search svg { position: absolute; left: 20px; top: 50%; transform: translateY(-50%); color: var(--ink-3); }
210
- .side .search .fake {
211
- display: block; padding: 6px 8px 6px 32px; border: 1px solid var(--line-mid); border-radius: 4px; background: #fff;
212
- color: var(--ink-3); font-size: 14px;
213
- }
214
- .side .folder { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: var(--warm); border-bottom: 1px solid var(--line); font-size: 12px; font-weight: 500; color: var(--ink-2); }
215
- .side .item { padding: 8px 16px 8px 12px; border-bottom: 1px solid var(--line); border-left: 4px solid transparent; font-family: var(--mono); font-size: 14px; }
216
- .side .item.selected { background: var(--theme-bg); border-left-color: var(--theme); }
217
-
218
- .seal-stamp {
219
- display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 3px;
220
- background: var(--seal); color: var(--paper); font-family: var(--serif); font-weight: 700; font-size: 16px; line-height: 1;
221
- box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--paper) 32%, transparent), 0 3px 8px -2px color-mix(in srgb, var(--seal) 55%, transparent);
222
- transform: rotate(-3deg);
223
- }
224
- .theme-demo { padding: 12px 16px; border-radius: 4px; }
225
- .nav-pill { display: inline-block; padding: 6px 12px; border-radius: 4px; font-size: 14px; font-weight: 500; background: var(--theme-bg); color: var(--theme-ink); position: relative; }
226
- .nav-pill::after { content: ''; position: absolute; left: 12px; right: 12px; bottom: -7px; height: 2px; background: var(--theme); }
227
-
228
- /* Markdown editor sample (mirror the .inkstone-editor rules in ui/web/src/index.css). */
229
- .editor-demo { background: var(--warm); border: 1px solid var(--line-mid); border-radius: 4px; padding: 12px 16px; font: 13px/1.7 var(--mono); }
230
- .editor-demo .ed-h { color: var(--seal-deep); font-weight: 700; }
231
- .editor-demo .ed-mark { color: var(--ink-3); }
232
- .editor-demo .ed-list { color: var(--gold-deep); font-weight: 700; }
233
- .editor-demo .ed-prop { color: var(--qing-deep); }
234
- .editor-demo .ed-punct { color: var(--ink-2); }
235
- .editor-demo .ed-code { color: var(--jade-btn); background: color-mix(in srgb, var(--jade-bg) 70%, transparent); border-radius: 2px; }
236
- .editor-demo .ed-url { color: var(--ink-3); text-decoration: underline; }
237
- .editor-demo .ed-link { color: var(--qing-deep); }
238
- .editor-demo .ed-quote { color: var(--ink-2); font-style: italic; }
239
- .editor-demo .ed-active { background: color-mix(in srgb, var(--theme-bg) 40%, transparent); margin: 0 -16px; padding: 0 16px; }
240
-
241
- .rule { border-left: 3px solid var(--gold); background: var(--gold-bg); color: var(--warn-ink); padding: 8px 12px; border-radius: 0 4px 4px 0; font-size: 14px; }
242
- ul.tight { margin: 6px 0; padding-left: 20px; }
243
- ul.tight li { margin: 2px 0; }
244
- footer { margin-top: 64px; color: var(--ink-3); font-size: 12px; }
245
- footer .creator { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; margin-top: 8px; }
246
- footer .creator span, footer .creator a { display: inline-flex; align-items: center; gap: 4px; color: inherit; text-decoration: none; }
247
- footer .creator a:hover { color: var(--ink); }
248
- </style>
249
- </head>
250
- <body>
251
- <div class="layout">
252
- <nav class="toc" aria-label="Contents">
253
- <div class="brand">Inkstone</div>
254
- <div class="small muted" style="margin:-10px 0 16px">AsRun UI theme</div>
255
- <a href="#principles">Principles</a>
256
- <a href="#palette">Color palette</a>
257
- <a href="#page-themes">Page themes</a>
258
- <a href="#typography">Typography</a>
259
- <a href="#buttons">Buttons</a>
260
- <a href="#badges">Badges</a>
261
- <a href="#action-colors">Action colors</a>
262
- <a href="#dropdowns">Dropdowns</a>
263
- <a href="#icons">Icons</a>
264
- <a href="#tooltips">Tooltips</a>
265
- <a href="#editor">Markdown editor</a>
266
- <a href="#surfaces">Surfaces</a>
267
- <a href="#sections">Sidebar sections</a>
268
- <a href="#checklist">Checklist</a>
269
- </nav>
270
-
271
- <main>
272
- <h1>Inkstone theme guide</h1>
273
- <p class="author small muted"><b>Inkstone</b> theme, designed and created by <b>Zoro Chen</b>.</p>
274
- <p class="lead">
275
- Inkstone is the look of the local web UI (<code>pnpm ui</code>): a rice-paper base, ink text and mineral pigments as accents.
276
- Source of truth: <code>ui/web/src/index.css</code> (tokens) and <code>ui/web/src/components/status.tsx</code>
277
- (buttons, badges). This page is static and has no dependencies; open it in any browser.
278
- </p>
279
-
280
- <h2 id="principles">Principles</h2>
281
- <ul class="tight">
282
- <li><b>Only the tokens below.</b> The default Tailwind palette is removed on purpose (<code>--color-*: initial</code>), so an unknown color class simply does nothing.</li>
283
- <li><b>Color means impact.</b> Green = go / safe, blue = read-only or informational, amber = writes or needs attention, red = destructive, failed or rewrites existing data.</li>
284
- <li><b>One accent per page.</b> Each top-level page sets <code>data-theme</code>; decorations, shadows and primary buttons follow it.</li>
285
- <li><b>Text-safe pairs.</b> Use <code>-deep</code> / <code>-ink</code> for text, <code>-bg</code> for tinted backgrounds, <code>-btn</code> for fills that carry white text.</li>
286
- </ul>
287
-
288
- <h2 id="palette">Color palette</h2>
289
- <h3>Base</h3>
290
- <div class="grid">
291
- <div class="swatch"><div class="chip" style="background:#f6f2e9"></div><div class="meta"><b>paper</b><span>#f6f2e9</span><br />page background</div></div>
292
- <div class="swatch"><div class="chip" style="background:#ffffff"></div><div class="meta"><b>card</b><span>#ffffff</span><br />cards, inputs</div></div>
293
- <div class="swatch"><div class="chip" style="background:#fcf9f3"></div><div class="meta"><b>warm</b><span>#fcf9f3</span><br />table heads, hover</div></div>
294
- <div class="swatch"><div class="chip" style="background:#1f1f1f"></div><div class="meta"><b>ink</b><span>#1f1f1f</span><br />primary text</div></div>
295
- <div class="swatch"><div class="chip" style="background:#6b6b6b"></div><div class="meta"><b>ink-2</b><span>#6b6b6b</span><br />secondary text</div></div>
296
- <div class="swatch"><div class="chip" style="background:#9c9c9c"></div><div class="meta"><b>ink-3</b><span>#9c9c9c</span><br />placeholders</div></div>
297
- <div class="swatch"><div class="chip" style="background:#e5e1d8"></div><div class="meta"><b>line</b><span>#e5e1d8</span><br />default borders</div></div>
298
- <div class="swatch"><div class="chip" style="background:#dcd7cb"></div><div class="meta"><b>line-mid</b><span>#dcd7cb</span><br />stronger borders</div></div>
299
- </div>
300
-
301
- <h3>Pigments (semantic)</h3>
302
- <table>
303
- <thead><tr><th>Family</th><th>Tokens</th><th>Meaning</th></tr></thead>
304
- <tbody>
305
- <tr>
306
- <td><b>jade</b> (green)</td>
307
- <td>
308
- <span class="badge green">jade-bg #e0e8d5</span>
309
- <code>jade #4da791</code> <code>jade-deep #3a8572</code> <code>jade-btn #2e6b5a</code> <code>jade-glow #3de0b0</code>
310
- </td>
311
- <td>Success, logged in, Run buttons</td>
312
- </tr>
313
- <tr>
314
- <td><b>qing</b> (blue)</td>
315
- <td>
316
- <span class="badge blue">qing-bg #e2eaf0</span>
317
- <code>qing #5b8aa6</code> <code>qing-deep #33506b</code> <code>qing-glow #4fc3ff</code>
318
- </td>
319
- <td>Info, running, read-only actions</td>
320
- </tr>
321
- <tr>
322
- <td><b>gold</b> (amber)</td>
323
- <td>
324
- <span class="badge amber">gold-bg #f4e9c9</span>
325
- <code>gold #c9a54a</code> <code>gold-deep #7a5a1e</code> <code>gold-glow #ffd23f</code> <code>warn-ink #6e5b24</code>
326
- </td>
327
- <td>Warnings, unverified, actions that write files</td>
328
- </tr>
329
- <tr>
330
- <td><b>seal</b> (red)</td>
331
- <td>
332
- <span class="badge red">seal-bg #f1dad7</span>
333
- <code>seal #b5443a</code> <code>seal-deep #8a2f28</code> <code>seal-soft #e8a79f</code> <code>seal-glow #ff7b6e</code>
334
- </td>
335
- <td>Errors, expired sessions, cancel, actions that rewrite files</td>
336
- </tr>
337
- <tr>
338
- <td><b>jiang</b> (purple)</td>
339
- <td>
340
- <code>jiang #a65998</code>
341
- </td>
342
- <td>The <b>production</b> environment marker only (white text, 4.7:1 contrast). Not red on purpose: production must not look like an error.</td>
343
- </tr>
344
- <tr>
345
- <td><b>cheng</b> (orange)</td>
346
- <td>
347
- <code>cheng #de8435</code>
348
- </td>
349
- <td>The <b>staging</b> environment marker only (white text). Not amber (gold) on purpose: gold means warnings and writes.</td>
350
- </tr>
351
- <tr>
352
- <td><b>dai</b> (deep teal)</td>
353
- <td>
354
- <span class="badge gray" style="background:#e2ecef;color:#1e3340">dai-bg #e2ecef</span>
355
- <code>dai #2e4a5c</code> <code>dai-deep #1e3340</code>
356
- </td>
357
- <td>Reports page accent, code blocks, modal scrims</td>
358
- </tr>
359
- </tbody>
360
- </table>
361
-
362
- <h2 id="page-themes">Page themes</h2>
363
- <p>
364
- <code>Layout</code> picks <code>data-theme</code> from the route. The theme drives
365
- <code>--theme</code>, <code>--theme-ink</code>, <code>--theme-btn</code>, <code>--theme-bg</code>
366
- (and a neighbouring hue <code>--theme-adj</code> for the card gradient). Use the Tailwind tokens
367
- <code>text-theme-ink</code>, <code>bg-theme-btn</code>, <code>bg-theme-bg</code>, <code>border-theme</code>.
368
- </p>
369
- <table>
370
- <thead><tr><th>Page</th><th><code>data-theme</code></th><th>Accent</th><th>Preview</th></tr></thead>
371
- <tbody>
372
- <tr><td>Profiles</td><td><code>jade</code></td><td>#4da791</td>
373
- <td><span class="nav-pill" style="--theme:#4da791;--theme-ink:#2e6b5a;--theme-bg:#e0e8d5">Profiles</span></td></tr>
374
- <tr><td>Cases</td><td><code>qing</code></td><td>#5b8aa6</td>
375
- <td><span class="nav-pill" style="--theme:#5b8aa6;--theme-ink:#33506b;--theme-bg:#e2eaf0">Cases</span></td></tr>
376
- <tr><td>Run tests</td><td><code>seal</code></td><td>#b5443a</td>
377
- <td><span class="nav-pill" style="--theme:#b5443a;--theme-ink:#8a2f28;--theme-bg:#f1dad7">Run tests</span></td></tr>
378
- <tr><td>Reports</td><td><code>dai</code></td><td>#2e4a5c</td>
379
- <td><span class="nav-pill" style="--theme:#2e4a5c;--theme-ink:#1e3340;--theme-bg:#e2ecef">Reports</span></td></tr>
380
- <tr><td>AI guide</td><td><code>gold</code></td><td>#c9a54a</td>
381
- <td><span class="nav-pill" style="--theme:#c9a54a;--theme-ink:#7a5a1e;--theme-bg:#f4e9c9">AI guide</span></td></tr>
382
- <tr><td>Help (<code>?</code> icon, no label)</td><td><code>qing</code></td><td>#5b8aa6</td>
383
- <td><span class="nav-pill" style="--theme:#5b8aa6;--theme-ink:#33506b;--theme-bg:#e2eaf0">?</span></td></tr>
384
- </tbody>
385
- </table>
386
- <p class="small muted">
387
- The page theme only decorates. It must not carry meaning: a green Run button stays green on the red "Run tests" page.
388
- To add a page: add a nav entry with a theme in <code>layout.tsx</code>; for a new accent also add a
389
- <code>[data-theme='name']</code> block in <code>index.css</code>.
390
- </p>
391
-
392
- <h2 id="typography">Typography</h2>
393
- <div class="card">
394
- <p style="font-family:var(--serif);font-size:24px;font-weight:700;margin:0">Serif: headings, brand name, eyebrows</p>
395
- <p class="mono small muted" style="margin:2px 0 12px">Iowan Old Style, Georgia, Songti SC, Noto Serif SC, SimSun</p>
396
- <p style="margin:0">Sans: body text, buttons, tables, forms. The quick brown fox jumps over the lazy dog.</p>
397
- <p class="mono small muted" style="margin:2px 0 12px">PingFang SC, Noto Sans SC, Microsoft YaHei, system-ui</p>
398
- <p class="mono" style="margin:0">Mono: commands, ids, paths, job output (pnpm asrun --profile example-production)</p>
399
- </div>
400
- <p class="small muted">Page titles: serif, bold, <code>text-2xl</code>, with a 4px accent bar on the left (<code>border-l-4 border-theme pl-3</code>).</p>
401
-
402
- <h2 id="buttons">Buttons</h2>
403
- <p>Use <code>buttonClass(variant)</code> from <code>status.tsx</code>. Never hand-style a button.</p>
404
- <div class="demo">
405
- <button class="btn btn-success">
406
- <!-- Play icon from Feather (https://feathericons.com/) -->
407
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
408
- Run tests
409
- </button>
410
- <button class="btn btn-success">
411
- <!-- Send icon from Feather (https://feathericons.com/) -->
412
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
413
- Run AI
414
- </button>
415
- <button class="btn btn-stop">
416
- <!-- XCircle icon from Feather (https://feathericons.com/) -->
417
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
418
- Close browser
419
- </button>
420
- <button class="btn btn-primary">Log in</button>
421
- <button class="btn btn-secondary">Verify</button>
422
- <button class="btn btn-info">Full screen</button>
423
- <button class="btn btn-danger">Cancel</button>
424
- <button class="btn btn-secondary" disabled>Disabled</button>
425
- </div>
426
- <table>
427
- <thead><tr><th>Variant</th><th>Use for</th></tr></thead>
428
- <tbody>
429
- <tr><td><code>success</code> (green)</td><td>Starting a run. Starting tests ("Run tests" on the Run page) always has the Feather <code>Play</code> icon. Sending a command to an AI tool or a shell job ("Run AI" in the case toolbar, "Make cases") always has the Feather <code>Send</code> icon. The icon goes on the left; never use <code>Play</code> for an AI command.</td></tr>
430
- <tr><td><code>stop</code> (solid red)</td><td>Forcibly ends something that is running, shown in place of the button that started it: "Close browser" on the Profiles page. Always with the Feather <code>XCircle</code> icon.</td></tr>
431
- <tr><td><code>primary</code> (page accent)</td><td>The main call to action of a page that is not a run: Log in, Save.</td></tr>
432
- <tr><td><code>info</code> (light blue)</td><td>A view toggle that must be easy to find, not an action on data: "Full screen" / "Restore" in the Jobs drawer bar. Blue = read-only.</td></tr>
433
- <tr><td><code>secondary</code> (white)</td><td>Everything else: Verify, Visit, Discard, Copy, navigation.</td></tr>
434
- <tr><td><code>danger</code> (white, red text)</td><td>Cancel a running job, destructive confirmations.</td></tr>
435
- </tbody>
436
- </table>
437
- <p class="small muted">Hover lifts the button 1px; disabled is 50% opacity with no shadow. With an icon, add <code>gap-1.5</code> to the button.</p>
438
-
439
- <h2 id="badges">Badges</h2>
440
- <div class="demo">
441
- <span class="badge green">Logged in</span>
442
- <span class="badge red">Session expired</span>
443
- <span class="badge amber">Saved, not verified</span>
444
- <span class="badge blue">Running</span>
445
- <span class="badge amber">Needs your input</span>
446
- <span class="badge gray">Cancelled</span>
447
- </div>
448
- <table>
449
- <thead><tr><th>Tone</th><th>Profile status</th><th>Job status</th></tr></thead>
450
- <tbody>
451
- <tr><td><span class="badge green">green</span></td><td>ok: Logged in</td><td>Succeeded</td></tr>
452
- <tr><td><span class="badge red">red</span></td><td>expired, missing</td><td>Failed</td></tr>
453
- <tr><td><span class="badge amber">amber</span></td><td>unverified, error</td><td>Needs your input (an AI run that ended with <code>RESULT: needs-input</code>)</td></tr>
454
- <tr><td><span class="badge blue">blue</span></td><td>-</td><td>Running</td></tr>
455
- <tr><td><span class="badge gray">gray</span></td><td>-</td><td>Cancelled</td></tr>
456
- </tbody>
457
- </table>
458
-
459
- <h3>Environment markers</h3>
460
- <div class="demo">
461
- <span class="env production">PRODUCTION</span>
462
- <span class="env staging">STAGING</span>
463
- <span class="env other">OTHER</span>
464
- </div>
465
- <p class="small muted">
466
- The environment must never be missed, so it is a solid block with white text, from <code>envFillClass()</code> in <code>status.tsx</code>
467
- (used by <code>EnvBadge</code> and the banner on top of the Reports page). Production is <code>jiang</code> (purple), staging <code>cheng</code> (orange),
468
- any other environment <code>gold-deep</code>. Never red: red means errors, failures and writes, and production must not look like an error.
469
- </p>
470
-
471
- <h2 id="action-colors">Action colors (AI commands)</h2>
472
- <p>
473
- Each AI action has one color that tells how much it changes. The select, its dot and its options share the color;
474
- a tooltip spells the impact out. Defined in <code>AI_COMMAND_STYLE</code> in <code>pages/cases.tsx</code>.
475
- </p>
476
- <div class="demo">
477
- <span class="cmd review"><span class="dot"></span><span class="fake">Explore case</span></span>
478
- <span class="cmd gen"><span class="dot"></span><span class="fake">Generate spec</span></span>
479
- <span class="cmd heal"><span class="dot"></span><span class="fake">Heal spec</span></span>
480
- </div>
481
- <table>
482
- <thead><tr><th>Action</th><th>Color</th><th>Impact</th></tr></thead>
483
- <tbody>
484
- <tr><td><code>explore-case</code></td><td>blue (qing)</td><td>Edits only the case file (wording, observations, open questions, inputs). The agent only reads pages; you do the steps that write data.</td></tr>
485
- <tr><td><code>gen-spec</code></td><td>amber (gold)</td><td>Writes: creates the spec and fills the case's <code>## Values</code>. Does not run the tests.</td></tr>
486
- <tr><td><code>heal-spec</code></td><td>red (seal)</td><td>Rewrites an existing spec and runs the tests.</td></tr>
487
- </tbody>
488
- </table>
489
- <p class="small muted">
490
- A new action must pick the color by impact, not by taste: read-only or touches only the case file is blue, adds files (spec, case values) is amber, rewrites or deletes is red. Two actions must never share a color.
491
- The select itself is the <a href="#dropdowns">Dropdown</a> component with <code>tone</code> and <code>dot</code>. Native <code>&lt;option&gt;</code> backgrounds are honored by Chrome and Edge but ignored by Safari; the closed select is always colored.
492
- </p>
493
-
494
- <h2 id="dropdowns">Dropdowns</h2>
495
- <p>
496
- Every dropdown is the <code>Select</code> component from <code>ui/web/src/components/status.tsx</code>.
497
- <b>Never write a bare <code>&lt;select&gt;</code></b> and never copy its classes: a native select draws the
498
- browser's own arrow and a different height, so it looks wrong next to the others (this has gone wrong twice).
499
- </p>
500
- <div class="demo">
501
- <span class="select"><span class="fake">staging</span><svg class="chev" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
502
- <span class="select focus"><span class="fake">staging</span><svg class="chev" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
503
- <span class="select-label invalid">Explore in
504
- <span class="select invalid"><span class="fake">Choose...</span><svg class="chev" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
505
- </span>
506
- <span class="select dotted"><span class="dot"></span><span class="fake">Generate spec</span><svg class="chev" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
507
- </div>
508
- <p class="small muted">Left to right: normal, focused, required and empty (invalid), and colored with a dot (the AI command select).</p>
509
- <pre class="card mono" style="overflow:auto">import {Select} from '../components/status';
510
-
511
- &lt;Select value={env} onChange={(e) =&gt; setEnv(e.target.value)} invalid={env === ''} aria-label="Environment"&gt;
512
- &lt;option value=""&gt;Choose...&lt;/option&gt;
513
- ...
514
- &lt;/Select&gt;</pre>
515
- <table>
516
- <thead><tr><th>Part</th><th>Rule</th></tr></thead>
517
- <tbody>
518
- <tr><td>Size</td><td>Height 32px (<code>h-8</code>), text <code>text-sm</code>, corners <code>rounded</code>. Same height as <code>buttonClass()</code> buttons, so they line up in a toolbar.</td></tr>
519
- <tr><td>Arrow</td><td>One Feather <code>ChevronDown</code>, size 14, <code>text-ink-2</code>, at the right. The native arrow is removed with <code>appearance-none</code>. The component does both; do not add your own.</td></tr>
520
- <tr><td>Normal</td><td>White background, <code>border-line-mid</code>, <code>text-ink</code>. Focus: <code>theme</code> border and a 2px ring at 25%.</td></tr>
521
- <tr><td>Required and empty</td><td>Pass <code>invalid</code>: <code>seal</code> border, <code>seal-bg</code> background, <code>seal-deep</code> text and arrow. Also color the label <code>text-seal-deep</code> and set <code>required</code>. Do not add a separate error message when the placeholder option (<code>Choose...</code>) already says what to do.</td></tr>
522
- <tr><td>Colored by choice</td><td>Pass <code>tone</code> (border, background, text and ring classes) and <code>dot</code> (dot background) when each option has a meaning color, as in <a href="#action-colors">Action colors</a>.</td></tr>
523
- <tr><td>Code values</td><td>Pass <code>mono</code> for ids and file names.</td></tr>
524
- <tr><td>Label</td><td>Put a visible label in front (<code>text-sm text-ink-2</code>) or an <code>aria-label</code>.</td></tr>
525
- <tr><td>Remembering a choice</td><td>Use <code>usePersistedState(key, default, guard)</code> from <code>lib/persisted.ts</code> with the key prefix <code>asrun-ui.</code>. Treat a stored value that is no longer in the options as empty.</td></tr>
526
- </tbody>
527
- </table>
528
-
529
- <h2 id="icons">Icons</h2>
530
- <p>
531
- Icons come from <a href="https://feathericons.com/" target="_blank" rel="noopener">Feather</a> through the
532
- <code>react-feather</code> package. <b>Do not draw or paste custom SVGs in the app.</b>
533
- </p>
534
- <div class="demo">
535
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
536
- <span class="label">Play (run tests)</span>
537
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
538
- <span class="label">Send (run AI / shell commands)</span>
539
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
540
- <span class="label">ChevronDown</span>
541
- </div>
542
- <pre class="card mono" style="overflow:auto">import {Play} from 'react-feather';
543
-
544
- &lt;button className={`${buttonClass('success')} gap-1.5`}&gt;
545
- &lt;Play size={14} aria-hidden="true" /&gt;
546
- Run tests
547
- &lt;/button&gt;</pre>
548
- <ul class="tight">
549
- <li>Size 14 inside buttons and selects, 16-20 elsewhere. Stroke width stays at Feather's default (2).</li>
550
- <li>Icons inherit <code>currentColor</code>; color them with the text color classes, not fills.</li>
551
- <li>Decorative icons get <code>aria-hidden="true"</code>; icon-only buttons need an <code>aria-label</code>.</li>
552
- <li>A button that is just an icon in a dense list has no background or border: <code>text-jade-deep hover:text-jade-btn</code> for "run" (the run button in the Cases sidebar), <code>text-ink-3 hover:text-ink</code> for neutral ones (clear search).</li>
553
- <li>Find names at <a href="https://feathericons.com/" target="_blank" rel="noopener">feathericons.com</a> and import them in PascalCase (<code>arrow-right</code> becomes <code>ArrowRight</code>).</li>
554
- <li>Exception, brand logos: Feather has no logos of other products, so the Claude and OpenCode logos of the AI agent switch are static SVG files in <code>ui/web/public/logos/</code> (single path, <code>viewBox="0 0 24 24"</code>). Show them with the <code>Logo</code> mask in <code>components/tool-switch.tsx</code> so they take the text color. Never paste logo SVGs into components.</li>
555
- </ul>
556
-
557
- <h2 id="tooltips">Tooltips</h2>
558
- <p>
559
- Write a plain <code>title="..."</code> on the element. <code>lib/tooltip.ts</code> (tippy.js) turns every <code>title</code> into
560
- a dark tooltip with an arrow on hover, so the browser's native tooltip never shows. Do not set up tippy per component and do not
561
- use a <code>title</code> on an <code>iframe</code>. Each <code>\n</code> in the text starts a new paragraph, and a line that starts with <code># </code> is a bold heading. Keep it short: a heading, then one or two sentences. Never put a command line or other long raw text in a tooltip. Disabled buttons and inputs get the
562
- tooltip too, so a <code>title</code> that says why something is disabled works. The box is styled by <code>.tippy-box[data-theme~='tip']</code>
563
- in <code>index.css</code>; the keyword help of the case editor has its own <code>inkstone</code> theme.
564
- </p>
565
-
566
- <h2 id="editor">Markdown editor</h2>
567
- <p class="muted">
568
- The case editor (CodeMirror) is the "draft" next to the white preview "sheet". Layout and colors live in the
569
- <code>.inkstone-editor</code> rules of <code>index.css</code>; <code>ui/web/src/lib/editor-theme.ts</code> maps syntax tags to
570
- the <code>tok-*</code> classes. Use both for any new editor, and never the built-in CodeMirror themes.
571
- The editor is monospace only: no serif or sans font, not even for headings.
572
- The preview renders the front matter as a key/value table (keys in <code>warm</code> cells, list values as blue badges).
573
- </p>
574
- <div class="editor-demo">
575
- <div class="ed-active"><span class="ed-mark">---</span></div>
576
- <div><span class="ed-prop">suites</span><span class="ed-punct">:</span> <span class="ed-punct">[</span>smoke<span class="ed-punct">]</span></div>
577
- <div><span class="ed-mark">---</span></div>
578
- <div><span class="ed-mark">##</span> <span class="ed-h">Steps</span></div>
579
- <div><span class="ed-list">1.</span> Open <span class="ed-mark">`</span><span class="ed-code">/app/documents</span><span class="ed-mark">`</span>.</div>
580
- <div><span class="ed-list">-</span> See <span class="ed-mark">[</span><span class="ed-link">docs</span><span class="ed-mark">](</span><span class="ed-url">https://example.com</span><span class="ed-mark">)</span></div>
581
- <div><span class="ed-mark">&gt;</span> <span class="ed-quote">A quoted note</span></div>
582
- </div>
583
- <table style="margin-top:12px">
584
- <tr><th>Element</th><th>Token</th></tr>
585
- <tr><td>Editor background</td><td><code>warm</code> over the paper grain; gutter <code>paper</code>, separated by a <code>line-mid</code> border</td></tr>
586
- <tr><td>Cursor, active line, active line number</td><td><code>theme-btn</code>, <code>theme-bg</code> at 40%, <code>theme-ink</code>; they follow the page theme</td></tr>
587
- <tr><td>Selection</td><td><code>theme</code> at 30%</td></tr>
588
- <tr><td>Bracket match</td><td><code>gold-bg</code> with <code>gold-deep</code> text</td></tr>
589
- <tr><td>Headings</td><td><code>seal-deep</code>, bold, monospace (<code>theme-ink</code> on the red <code>seal</code> page)</td></tr>
590
- <tr><td>Marks (<code>#</code> <code>&gt;</code> backticks, <code>---</code>)</td><td><code>ink-3</code></td></tr>
591
- <tr><td>List markers</td><td><code>gold-deep</code>, bold</td></tr>
592
- <tr><td>Bold, italic</td><td><code>ink</code>, bold or italic</td></tr>
593
- <tr><td>Quote</td><td><code>ink-2</code>, italic</td></tr>
594
- <tr><td>Link text, URL</td><td><code>qing-deep</code>; <code>ink-3</code> underlined</td></tr>
595
- <tr><td>Inline code</td><td><code>jade-btn</code> on <code>jade-bg</code></td></tr>
596
- <tr><td>Front matter keys, types, tags</td><td><code>qing-deep</code></td></tr>
597
- <tr><td>Code blocks</td><td>keywords <code>seal-deep</code>, strings <code>jade-btn</code>, numbers <code>gold-deep</code>, functions <code>dai</code>, comments <code>ink-3</code>, punctuation <code>ink-2</code></td></tr>
598
- </table>
599
-
600
- <h3>Keyword highlights and their tooltip</h3>
601
- <p class="muted">
602
- Words that need a human are marked in the editor and in the preview (<code>&lt;mark&gt;</code>) with the same
603
- <code>kw-*</code> classes: <code>TODO [blocking]</code> <code>seal</code>, other <code>TODO</code> <code>gold</code>,
604
- "to be confirmed by QA" / "not observed" <code>qing</code>, <code>Answer:</code> <code>jade</code>; the emoji in front
605
- of a marker is part of it and takes the same color. Hovering one shows a
606
- small dark card with an arrow (tippy.js, theme <code>inkstone</code>: <code>dai-deep</code> background like the preview code
607
- blocks, <code>paper</code> text, <code>shadow-lift</code>) so that it stands out on both the white preview and the beige
608
- editor. A 6px left bar and the title, both in the bright <code>*-glow</code> color of the kind (<code>seal-glow</code>,
609
- <code>gold-glow</code>, <code>qing-glow</code>, <code>jade-glow</code>; they exist only for dark surfaces),
610
- show the kind; below come one line of meaning (<code>line-mid</code>) and a numbered "You can:" list. The texts live in
611
- <code>keywordHelp</code> in <code>ui/web/src/lib/highlight-keywords.ts</code>; keep every line short and direct.
612
- Do not add another tooltip library: use <code>tippy.js</code> and the <code>inkstone</code> theme.
613
- </p>
614
- <p class="muted">
615
- After every marker comes a small remove button (<code>kw-remove</code>, <code>kw-remove-&lt;kind&gt;</code>): the
616
- Feather <code>X</code> at 14px in the same text and background colors as the marker, 20px square, 2px radius,
617
- <code>theme</code> focus outline. It is a plain button (a CodeMirror widget in the editor, an element in the preview),
618
- never a new icon. It deletes the marker, or the whole question block of a <code>TODO</code> (the sentence above stays),
619
- or what is typed after an <code>Answer:</code>; the logic is in <code>ui/web/src/lib/keyword-removal.ts</code>.
620
- </p>
621
- <p class="muted">
622
- The preview block that holds the editor caret is shaded with <code>theme-bg</code> at 60% and a 2px <code>theme</code> left
623
- border (an overlay, so it follows the page theme). The editor and the preview scroll together and a click in the preview
624
- moves the caret; the logic is in <code>ui/web/src/lib/preview-sync.ts</code>.
625
- </p>
626
-
627
- <h2 id="surfaces">Surfaces and shadows</h2>
628
- <div class="row">
629
- <div class="card" style="width:240px">
630
- <b>card-paper</b>
631
- <p class="small muted" style="margin:4px 0 0">White sheet, 4px radius, a faint accent tint at the top left, colored shadow that follows the page theme.</p>
632
- </div>
633
- <div style="display:flex;align-items:center;gap:10px">
634
- <span class="seal-stamp">A</span>
635
- <span class="small muted"><code>.seal</code>: the stamp. The only slightly rotated element; use it once, in the header.</span>
636
- </div>
637
- </div>
638
- <ul class="tight">
639
- <li>Radius: 4px for cards, buttons and inputs; 2px for badges; no pills.</li>
640
- <li>Shadows are tinted with the page accent (<code>shadow-paper</code>, <code>shadow-lift</code>, <code>shadow-btn</code>, <code>shadow-btn-lift</code>), never plain gray.</li>
641
- <li>The page background carries a subtle paper grain (<code>--grain</code>).</li>
642
- <li>The Jobs drawer, header and top-level separators use a 2px <code>ink</code> border; everything inside uses <code>line</code> / <code>line-mid</code>.</li>
643
- <li>The Jobs drawer's title bar is a solid <code>dai-deep</code> bar with white text, so it reads as a handle. It ends with the same small chevron block as a sidebar section header and brightens on hover.</li>
644
- <li><b>Every expand/collapse animates.</b> Use <code>Collapse</code> from <code>components/collapse.tsx</code> (height and fade, 200ms, <code>ease-out</code>) for content that appears and disappears, and rotate the chevron with <code>transition-transform duration-200</code> instead of swapping two icons. Always add <code>motion-reduce:transition-none</code>. Turn transitions off while the user drags a resize handle.</li>
645
- </ul>
646
-
647
- <h2 id="sections">Sidebar sections and selection</h2>
648
- <div class="row" style="align-items:flex-start">
649
- <div class="side">
650
- <div class="sec"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>New cases<span class="toggle"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg></span></div>
651
- <div class="sec"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>Case list<span class="aside">3 / 12</span></div>
652
- <div class="search">
653
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
654
- <span class="fake">Search name, title, suite...</span>
655
- </div>
656
- <div class="folder">documents <span style="margin-left:auto;color:var(--ink-3)">2</span></div>
657
- <div class="item selected">hub-landing</div>
658
- <div class="item">header-logo</div>
659
- </div>
660
- <div style="flex:1;min-width:260px">
661
- <ul class="tight">
662
- <li>Sections of a sidebar (<code>SectionHeader</code> in <code>pages/cases.tsx</code>) are all on one level: a solid page-accent bar (<code>bg-theme-btn</code>, white text and icon, serif bold label), so a section header is never mistaken for a folder row (gray). A collapsible one ends with a small button block (<code>bg-white/15</code>, Feather <code>ChevronDown</code> when collapsed, <code>ChevronUp</code> when open, size 16) that brightens on hover; a static one has no block, only an optional count. The block is what says "this can be expanded".</li>
663
- <li><code>bg-theme-bg</code> means <b>selected</b> and nothing else. Never use it for a header, so a header cannot be mistaken for a selected row. A selected row also gets <code>border-l-4 border-l-theme</code>.</li>
664
- <li>Levels inside a list: section header, then folder (<code>bg-warm</code>, small, regular weight, no accent), then item.</li>
665
- <li>A search box is a <code>fieldClass</code> input with a Feather <code>Search</code> icon at the left and a clear button (<code>X</code>, <code>aria-label</code>) at the right. It filters while typing, with no submit; <code>Esc</code> clears it.</li>
666
- </ul>
667
- <p class="small muted">The icons in the demo are inline only because this page has no build step; in the app they always come from <code>react-feather</code>.</p>
668
- </div>
669
- </div>
670
-
671
- <h2 id="checklist">Checklist for a new UI element</h2>
672
- <ol class="tight">
673
- <li>Pick colors only from the tokens above (<code>text-ink-2</code>, <code>bg-warm</code>, <code>bg-seal-bg</code>, ...).</li>
674
- <li>Decide the impact: safe/go = green, read-only/info = blue, writes/attention = amber, destructive/rewrite/error = red.</li>
675
- <li>Use <code>buttonClass()</code> and <code>Badge</code> instead of new styles.</li>
676
- <li>Dropdowns: use <code>Select</code>, never a bare <code>&lt;select&gt;</code> (see <a href="#dropdowns">Dropdowns</a>).</li>
677
- <li>Icons: Feather via <code>react-feather</code> only.</li>
678
- <li>If you add or change a token, update <code>index.css</code> and this page together.</li>
679
- </ol>
680
- <div class="rule" style="margin-top:16px">
681
- Exception: if a color's meaning has to differ from this guide (for example a destructive action that is not red), say why in a code comment.
682
- </div>
683
-
684
- <footer>Inkstone theme guide for the AsRun UI. Static file: <code>docs/theme-guide.html</code>. Inkstone theme, designed and created by Zoro Chen.
685
- <div class="creator">
686
- <span>Created by Zoro Chen</span>
687
- </div>
688
- </footer>
689
- </main>
690
- </div>
691
- </body>
692
- </html>
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Inkstone theme guide · AsRun UI</title>
7
+ <!-- Same Google Fonts as ui/web/index.html: Calistoga, Inter, Ubuntu Mono. -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link
11
+ rel="stylesheet"
12
+ href="https://fonts.googleapis.com/css2?family=Calistoga&family=Inter:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Ubuntu+Mono:wght@400;700&display=swap"
13
+ />
14
+ <style>
15
+ /* Inkstone theme, designed and created by Zoro Chen. */
16
+ /* Tokens copied from ui/web/src/index.css. Keep both in sync. */
17
+ :root {
18
+ --paper: #f6f2e9;
19
+ --card: #ffffff;
20
+ --warm: #fcf9f3;
21
+ --ink: #1f1f1f;
22
+ --ink-2: #6b6b6b;
23
+ --ink-3: #9c9c9c;
24
+ --line: #e5e1d8;
25
+ --line-mid: #dcd7cb;
26
+
27
+ --seal: #b5443a;
28
+ --seal-deep: #8a2f28;
29
+ --seal-bg: #f1dad7;
30
+ --seal-soft: #e8a79f;
31
+ --seal-glow: #ff7b6e;
32
+
33
+ --gold: #c9a54a;
34
+ --gold-deep: #7a5a1e;
35
+ --gold-bg: #f4e9c9;
36
+ --gold-glow: #ffd23f;
37
+ --warn-ink: #6e5b24;
38
+
39
+ --qing: #5b8aa6;
40
+ --qing-deep: #33506b;
41
+ --qing-bg: #e2eaf0;
42
+ --qing-glow: #4fc3ff;
43
+
44
+ --jade: #4da791;
45
+ --jade-deep: #3a8572;
46
+ --jade-btn: #2e6b5a;
47
+ --jade-bg: #e0e8d5;
48
+ --jade-glow: #3de0b0;
49
+
50
+ --jiang: #a65998;
51
+
52
+ --cheng: #de8435;
53
+ --cheng-deep: #a05a1c;
54
+ --cheng-bg: #fbe9d7;
55
+
56
+ --dai: #2e4a5c;
57
+ --dai-deep: #1e3340;
58
+ --dai-bg: #e2ecef;
59
+
60
+ --serif: 'Calistoga', 'Iowan Old Style', Georgia, 'Songti SC', 'Noto Serif SC', 'SimSun', serif;
61
+ --sans: 'Inter', 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', ui-sans-serif, system-ui, sans-serif;
62
+ --mono: 'Ubuntu Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
63
+
64
+ --theme: var(--jade);
65
+ --theme-ink: #2e6b5a;
66
+ --theme-btn: #2e6b5a;
67
+ --theme-bg: #e0e8d5;
68
+ }
69
+
70
+ * { box-sizing: border-box; }
71
+ html { scroll-behavior: smooth; }
72
+ body {
73
+ margin: 0;
74
+ background: var(--paper);
75
+ color: var(--ink);
76
+ font-family: var(--sans);
77
+ line-height: 1.6;
78
+ -webkit-font-smoothing: antialiased;
79
+ }
80
+ a { color: var(--qing-deep); }
81
+ code, .mono { font-family: var(--mono); font-size: 0.85em; }
82
+ code { background: var(--warm); border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; }
83
+
84
+ .layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); max-width: 1100px; margin: 0 auto; }
85
+ nav.toc {
86
+ position: sticky; top: 0; align-self: start; height: 100vh; overflow: auto;
87
+ padding: 32px 20px; border-right: 1px solid var(--line-mid);
88
+ }
89
+ nav.toc .brand { font-family: var(--serif); font-weight: 400; letter-spacing: 0.1em; margin-bottom: 16px; }
90
+ nav.toc a {
91
+ display: block; padding: 4px 8px; margin: 1px 0; border-radius: 4px;
92
+ color: var(--ink-2); text-decoration: none; font-size: 14px;
93
+ }
94
+ nav.toc a:hover { background: var(--warm); color: var(--ink); }
95
+ main { padding: 32px 40px 80px; min-width: 0; }
96
+ @media (max-width: 800px) {
97
+ .layout { display: block; }
98
+ nav.toc { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--line-mid); }
99
+ main { padding: 24px 20px 60px; }
100
+ }
101
+
102
+ h1, h2, h3 { font-family: var(--serif); font-weight: 400; }
103
+ h1 { font-size: 30px; margin: 0 0 4px; border-left: 4px solid var(--jade); padding-left: 12px; }
104
+ h2 { font-size: 22px; margin: 48px 0 8px; padding-bottom: 6px; border-bottom: 1px solid var(--line-mid); }
105
+ h3 { font-size: 16px; margin: 24px 0 8px; }
106
+ p { margin: 8px 0; }
107
+ .author { margin: 0 0 12px; padding-left: 16px; }
108
+ .lead { color: var(--ink-2); margin-bottom: 24px; }
109
+ .muted { color: var(--ink-2); }
110
+ .small { font-size: 13px; }
111
+
112
+ .card {
113
+ background:
114
+ radial-gradient(120% 100% at 16% -8%, color-mix(in srgb, var(--theme) 9%, white) 0%, transparent 58%),
115
+ var(--card);
116
+ border: 1px solid var(--line);
117
+ border-radius: 4px;
118
+ box-shadow:
119
+ 0 1px 2px color-mix(in srgb, var(--theme) 16%, transparent),
120
+ 0 8px 20px -10px color-mix(in srgb, var(--theme) 34%, transparent);
121
+ padding: 16px;
122
+ }
123
+
124
+ table { width: 100%; border-collapse: collapse; font-size: 14px; background: var(--card); border: 1px solid var(--line); }
125
+ th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
126
+ th { background: var(--warm); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-2); }
127
+
128
+ .grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
129
+ .swatch { border: 1px solid var(--line-mid); border-radius: 4px; overflow: hidden; background: var(--card); font-size: 12px; }
130
+ .swatch .chip { height: 52px; }
131
+ .swatch .meta { padding: 6px 8px; }
132
+ .swatch .meta b { display: block; font-size: 13px; }
133
+ .swatch .meta span { color: var(--ink-2); font-family: var(--mono); }
134
+
135
+ .row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin: 12px 0; }
136
+ .demo { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 16px; background: var(--paper); border: 1px dashed var(--line-mid); border-radius: 4px; }
137
+ .label { font-size: 12px; color: var(--ink-2); font-family: var(--mono); }
138
+
139
+ /* Buttons (mirror buttonClass in ui/web/src/components/status.tsx). */
140
+ .btn {
141
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
142
+ border: 0; border-radius: 4px; padding: 6px 12px; font: 500 14px var(--sans); cursor: pointer;
143
+ transition: transform .15s, filter .15s, box-shadow .15s, background .15s;
144
+ }
145
+ .btn:hover { transform: translateY(-1px); }
146
+ .btn[disabled] { opacity: .5; cursor: not-allowed; box-shadow: none; transform: none; }
147
+ .btn svg { flex: none; }
148
+ .btn-primary, .btn-success {
149
+ color: #fff;
150
+ background: var(--theme-btn);
151
+ box-shadow: inset 0 1px 0 rgb(255 255 255 / .18), 0 3px 8px -2px color-mix(in srgb, var(--theme-btn) 50%, transparent);
152
+ }
153
+ .btn-success { background: var(--jade-btn); box-shadow: inset 0 1px 0 rgb(255 255 255 / .18), 0 3px 8px -2px color-mix(in srgb, var(--jade-btn) 50%, transparent); }
154
+ .btn-primary:hover, .btn-success:hover { filter: brightness(1.1); }
155
+ .btn-stop { color: #fff; background: var(--seal); box-shadow: inset 0 0 0 0 transparent, 0 3px 8px -2px color-mix(in srgb, var(--seal) 50%, transparent); }
156
+ .btn-stop:hover { filter: brightness(1.1); }
157
+ .btn-secondary { background: var(--card); color: var(--ink); box-shadow: 0 0 0 1px var(--line-mid); }
158
+ .btn-secondary:hover { background: var(--warm); }
159
+ .btn-info { background: var(--qing-bg); color: var(--qing-deep); box-shadow: 0 0 0 1px color-mix(in srgb, var(--qing) 50%, transparent); }
160
+ .btn-info:hover { filter: brightness(.95); }
161
+ .btn-danger { background: var(--card); color: var(--seal-deep); box-shadow: 0 0 0 1px color-mix(in srgb, var(--seal) 40%, transparent); }
162
+ .btn-danger:hover { background: var(--seal-bg); }
163
+
164
+ /* Badges (mirror TONES in status.tsx). */
165
+ .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 2px; font-size: 12px; font-weight: 500; }
166
+ .badge.green { background: var(--jade-bg); color: var(--jade-btn); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--jade-deep) 30%, transparent); }
167
+ .badge.red { background: var(--seal-bg); color: var(--seal-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--seal) 35%, transparent); }
168
+ .badge.amber { background: var(--gold-bg); color: var(--gold-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold-deep) 30%, transparent); }
169
+ .badge.blue { background: var(--qing-bg); color: var(--qing-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--qing) 40%, transparent); }
170
+ .badge.gray { background: var(--warm); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line-mid); }
171
+ /* WRITES (mirror WritesBadge in status.tsx): red text in a red outline, never a red fill, so it does not read as an error. */
172
+ .badge.writes { background: var(--card); color: var(--seal-deep); font-weight: 700; letter-spacing: 0.05em; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--seal) 50%, transparent); }
173
+ /* Environment markers (mirror envFillClass in status.tsx): solid fill, white text. */
174
+ .env { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 2px; font-size: 12px; font-weight: 700; letter-spacing: 0.05em; color: #fff; }
175
+ .env.production { background: var(--jiang); }
176
+ .env.staging { background: var(--cheng); }
177
+ .env.other { background: var(--gold-deep); }
178
+
179
+ /* Target reference chips (mirror target-chip.tsx): an orange stamp that opens a card when clicked. */
180
+ .target-chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 6px 2px 4px; border-radius: 2px; font-size: 12px; font-weight: 500; background: var(--card); color: var(--ink); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cheng) 45%, transparent); }
181
+ .target-chip .stamp { padding: 2px 6px; border-radius: 2px; background: var(--cheng); color: #fff; font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 0.04em; }
182
+ .target-chip.warn { background: var(--gold-bg); color: var(--gold-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold-deep) 30%, transparent); }
183
+ .target-chip.missing { background: var(--seal-bg); color: var(--seal-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--seal) 35%, transparent); }
184
+
185
+ /* AI command select (mirror AI_COMMAND_STYLE in ui/web/src/pages/cases.tsx). */
186
+ .cmd { position: relative; display: inline-block; }
187
+ .cmd .dot { position: absolute; left: 12px; top: 50%; width: 8px; height: 8px; border-radius: 50%; transform: translateY(-50%); }
188
+ .cmd .fake {
189
+ display: inline-flex; align-items: center; height: 32px; padding: 0 12px 0 28px; border: 1px solid;
190
+ border-radius: 4px; font-size: 14px; font-weight: 500;
191
+ }
192
+ .cmd.review .fake { border-color: color-mix(in srgb, var(--qing) 50%, transparent); background: var(--qing-bg); color: var(--qing-deep); }
193
+ .cmd.review .dot { background: var(--qing); }
194
+ .cmd.gen .fake { border-color: color-mix(in srgb, var(--gold-deep) 40%, transparent); background: var(--gold-bg); color: var(--gold-deep); }
195
+ .cmd.gen .dot { background: var(--gold); }
196
+ .cmd.heal .fake { border-color: color-mix(in srgb, var(--seal) 40%, transparent); background: var(--seal-bg); color: var(--seal-deep); }
197
+ .cmd.heal .dot { background: var(--seal); }
198
+
199
+ /* Dropdown demo (mirror the Select component in ui/web/src/components/status.tsx). */
200
+ .select { position: relative; display: inline-block; }
201
+ .select .fake {
202
+ display: inline-flex; align-items: center; height: 32px; min-width: 120px; padding: 0 32px 0 8px; border: 1px solid var(--line-mid);
203
+ border-radius: 4px; background: #fff; color: var(--ink); font-size: 14px;
204
+ }
205
+ .select .chev { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); color: var(--ink-2); pointer-events: none; }
206
+ .select.focus .fake { border-color: var(--theme-demo, #5b8aa6); box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-demo, #5b8aa6) 25%, transparent); }
207
+ .select.invalid .fake { border-color: var(--seal); background: var(--seal-bg); color: var(--seal-deep); }
208
+ .select.invalid .chev { color: var(--seal-deep); }
209
+ .select.dotted .fake { padding-left: 28px; font-weight: 500; }
210
+ .select .dot { position: absolute; left: 12px; top: 50%; width: 8px; height: 8px; border-radius: 50%; transform: translateY(-50%); background: var(--gold); }
211
+ .select.dotted .fake { border-color: color-mix(in srgb, var(--gold-deep) 40%, transparent); background: var(--gold-bg); color: var(--gold-deep); }
212
+ .select-label { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; color: var(--ink-2); }
213
+ .select-label.invalid { color: var(--seal-deep); }
214
+
215
+ /* Tag field demo (mirror components/case-panel/tag-field.tsx). */
216
+ .tags { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; min-height: 32px; min-width: 300px; padding: 4px 6px; border: 1px solid var(--line-mid); border-radius: 4px; background: #fff; }
217
+ .tags.focus { border-color: var(--theme-demo, #5b8aa6); box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-demo, #5b8aa6) 25%, transparent); }
218
+ .tags .tag { display: inline-flex; align-items: center; gap: 2px; height: 24px; padding: 0 2px 0 6px; border-radius: 2px; background: var(--warm); color: var(--ink-2); font-family: var(--mono); font-size: 12px; box-shadow: inset 0 0 0 1px var(--line-mid); }
219
+ .tags .tag svg { color: var(--ink-3); }
220
+ .tags .entry { font-family: var(--mono); font-size: 12px; color: var(--ink-3); }
221
+ .tags .entry .caret { display: inline-block; width: 1px; height: 14px; margin-left: 2px; background: var(--theme-btn); vertical-align: -3px; }
222
+ .suggest { display: inline-block; min-width: 224px; margin-top: 4px; padding: 4px; border: 1px solid var(--line); border-radius: 4px; background: var(--card); box-shadow: 0 10px 22px -12px color-mix(in srgb, var(--ink) 24%, transparent); }
223
+ .suggest .opt { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: 2px; font-family: var(--mono); font-size: 12px; color: var(--ink); }
224
+ .suggest .opt.on { background: var(--theme-bg, #e2eaf0); color: var(--theme-ink, #33506b); }
225
+ .suggest .opt .new { margin-left: auto; font-family: var(--sans); font-size: 11px; color: var(--ink-3); }
226
+
227
+ /* Environment toggle chips (mirror components/case-panel/env-chips.tsx). */
228
+ .env-chip { display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 6px; border-radius: 2px; font-size: 12px; font-weight: 700; letter-spacing: 0.05em; }
229
+ .env-chip.on { color: #fff; }
230
+ .env-chip.on.production { background: var(--jiang); }
231
+ .env-chip.on.staging { background: var(--cheng); }
232
+ .env-chip.on.other { background: var(--gold-deep); }
233
+ .env-chip.off { background: var(--card); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line-mid); }
234
+ .env-chip.blocked { background: var(--seal-bg); color: var(--seal-deep); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--seal) 35%, transparent); }
235
+
236
+ /* Field-level help (mirror FieldHelp in components/case-panel/section.tsx). */
237
+ .field-label { display: inline-flex; align-items: flex-start; gap: 4px; font-family: var(--mono); font-size: 12px; line-height: 1.35; color: var(--ink-3); }
238
+ .field-label .help { margin: -2px 0; padding: 2px; border-radius: 4px; color: var(--ink-3); }
239
+
240
+ /* Sidebar sections (mirror SectionHeader, CaseSearch and the case list in ui/web/src/pages/cases.tsx). */
241
+ .side { width: 300px; background: var(--card); border: 1px solid var(--line-mid); }
242
+ .side .sec {
243
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--theme-btn); color: #fff; font-family: var(--serif);
244
+ font-size: 14px; font-weight: 400; letter-spacing: 0.12em; border-top: 1px solid var(--theme-btn); border-bottom: 1px solid var(--theme-btn);
245
+ }
246
+ .side .sec svg { flex: none; }
247
+ .side .sec .aside { margin-left: auto; font-family: var(--sans); font-size: 12px; font-weight: 400; letter-spacing: 0; color: rgb(255 255 255 / .75); }
248
+ .side .sec .toggle { margin-left: auto; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; background: rgb(255 255 255 / .15); flex: none; }
249
+ .side .sec .aside + .toggle { margin-left: 0; }
250
+ .side .search { position: relative; padding: 8px; border-bottom: 1px solid var(--line-mid); }
251
+ .side .search svg { position: absolute; left: 20px; top: 50%; transform: translateY(-50%); color: var(--ink-3); }
252
+ .side .search .fake {
253
+ display: block; padding: 6px 8px 6px 32px; border: 1px solid var(--line-mid); border-radius: 4px; background: #fff;
254
+ color: var(--ink-3); font-size: 14px;
255
+ }
256
+ .side .folder { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: var(--warm); border-bottom: 1px solid var(--line); font-size: 12px; font-weight: 500; color: var(--ink-2); }
257
+ .side .item { padding: 8px 16px 8px 12px; border-bottom: 1px solid var(--line); border-left: 4px solid transparent; font-family: var(--mono); font-size: 14px; }
258
+ .side .item.selected { background: var(--theme-bg); border-left-color: var(--theme); }
259
+
260
+ .seal-stamp {
261
+ display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 3px;
262
+ background: var(--seal); color: var(--paper); font-family: var(--serif); font-weight: 400; font-size: 16px; line-height: 1;
263
+ box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--paper) 32%, transparent), 0 3px 8px -2px color-mix(in srgb, var(--seal) 55%, transparent);
264
+ transform: rotate(-3deg);
265
+ }
266
+ .theme-demo { padding: 12px 16px; border-radius: 4px; }
267
+ .nav-pill { display: inline-block; padding: 6px 12px; border-radius: 4px; font-size: 14px; font-weight: 500; background: var(--theme-bg); color: var(--theme-ink); position: relative; }
268
+ .nav-pill::after { content: ''; position: absolute; left: 12px; right: 12px; bottom: -7px; height: 2px; background: var(--theme); }
269
+
270
+ /* Markdown editor sample (mirror the .inkstone-editor rules in ui/web/src/index.css). */
271
+ .editor-demo { background: var(--warm); border: 1px solid var(--line-mid); border-radius: 4px; padding: 12px 16px; font: 13px/1.7 var(--mono); }
272
+ .editor-demo .ed-h { color: var(--seal-deep); font-weight: 700; }
273
+ .editor-demo .ed-mark { color: var(--ink-3); }
274
+ .editor-demo .ed-list { color: var(--gold-deep); font-weight: 700; }
275
+ .editor-demo .ed-prop { color: var(--qing-deep); }
276
+ .editor-demo .ed-punct { color: var(--ink-2); }
277
+ .editor-demo .ed-code { color: var(--jade-btn); background: color-mix(in srgb, var(--jade-bg) 70%, transparent); border-radius: 2px; }
278
+ .editor-demo .ed-url { color: var(--ink-3); text-decoration: underline; }
279
+ .editor-demo .ed-link { color: var(--qing-deep); }
280
+ .editor-demo .ed-quote { color: var(--ink-2); font-style: italic; }
281
+ .editor-demo .ed-active { background: color-mix(in srgb, var(--theme-bg) 40%, transparent); margin: 0 -16px; padding: 0 16px; }
282
+
283
+ .rule { border-left: 3px solid var(--gold); background: var(--gold-bg); color: var(--warn-ink); padding: 8px 12px; border-radius: 0 4px 4px 0; font-size: 14px; }
284
+ ul.tight { margin: 6px 0; padding-left: 20px; }
285
+ ul.tight li { margin: 2px 0; }
286
+ footer { margin-top: 64px; color: var(--ink-3); font-size: 12px; }
287
+ footer .creator { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; margin-top: 8px; }
288
+ footer .creator span, footer .creator a { display: inline-flex; align-items: center; gap: 4px; color: inherit; text-decoration: none; }
289
+ footer .creator a:hover { color: var(--ink); }
290
+ </style>
291
+ </head>
292
+ <body>
293
+ <div class="layout">
294
+ <nav class="toc" aria-label="Contents">
295
+ <div class="brand">Inkstone</div>
296
+ <div class="small muted" style="margin:-10px 0 16px">A standalone theme system</div>
297
+ <a href="#principles">Principles</a>
298
+ <a href="#palette">Color palette</a>
299
+ <a href="#page-themes">Page themes</a>
300
+ <a href="#typography">Typography</a>
301
+ <a href="#buttons">Buttons</a>
302
+ <a href="#badges">Badges</a>
303
+ <a href="#action-colors">Action colors</a>
304
+ <a href="#dropdowns">Dropdowns</a>
305
+ <a href="#multi-value">Multi-value fields</a>
306
+ <a href="#icons">Icons</a>
307
+ <a href="#tooltips">Tooltips</a>
308
+ <a href="#editor">Markdown editor</a>
309
+ <a href="#surfaces">Surfaces</a>
310
+ <a href="#sections">Sidebar sections</a>
311
+ <a href="#checklist">Checklist</a>
312
+ </nav>
313
+
314
+ <main>
315
+ <h1>Inkstone theme guide</h1>
316
+ <p class="author small muted"><b>Inkstone</b> theme, designed and created by <b>Zoro Chen</b>.</p>
317
+ <!-- Where this lives, for agents and maintainers (deliberately not page text: a reader does not need it).
318
+ - Inkstone is the theme of the local web UI, started with `pnpm ui` in the workspace root. That server is
319
+ `ui/server/app.ts`; it serves this file at /docs/theme-guide.html, linked from the top bar of
320
+ `ui/web/src/components/layout.tsx`.
321
+ - Source of truth: `ui/web/src/index.css` (tokens) and `ui/web/src/components/status.tsx` (buttons, badges).
322
+ A new or changed token means editing that CSS and this page together.
323
+ - Stack: React 19 + Vite 8, Tailwind CSS v4 (`@tailwindcss/vite`), react-feather icons, CodeMirror 6 for the
324
+ markdown/YAML editors, React Router, Hono for the server side; fonts Calistoga, Inter and Ubuntu Mono
325
+ (Google Fonts, the same three as `ui/web/index.html`).
326
+ - This page itself is static HTML with one inline <style>: no build step and no local dependencies, it only
327
+ loads the three fonts. Open it in any browser to compare a real element against the guide. -->
328
+ <p class="lead">
329
+ Inkstone comes from Chinese ink painting and its mineral pigments. The ground is rice paper, the text and the
330
+ structure are ink, and the colors are the ones painters grind and layer:
331
+ <span style="white-space:nowrap">石青 azurite</span>, <span style="white-space:nowrap">石绿 malachite</span>,
332
+ <span style="white-space:nowrap">朱砂 cinnabar</span>, <span style="white-space:nowrap">藤黄 gamboge</span>,
333
+ <span style="white-space:nowrap">赭石 ochre</span>. Like a painting, it separates form from material: paper and
334
+ ink carry the page, and a pigment appears only where it means something, never as decoration. A restrained
335
+ surface also keeps the eye on what matters, which for a test tool is the difference between safe and
336
+ destructive.
337
+ </p>
338
+ <p class="lead">
339
+ Inkstone is a standalone theme system: the tokens, the type scale, the button and badge recipes and the icon rules
340
+ on this page are the whole of it, and every value is defined once, so nothing is inherited from a component
341
+ library's defaults. It is plain CSS custom properties over a thin Tailwind CSS v4 layer whose default palette is
342
+ removed on purpose, plus three font families and Feather icons. A new surface adopts it by taking those tokens,
343
+ not by copying a component.
344
+ </p>
345
+
346
+ <h2 id="principles">Principles</h2>
347
+ <ul class="tight">
348
+ <li><b>Only the tokens below.</b> The default Tailwind palette is removed on purpose (<code>--color-*: initial</code>), so an unknown color class simply does nothing.</li>
349
+ <li><b>Color means impact.</b> Green = go / safe, blue = read-only or informational, amber = writes or needs attention, red = destructive, failed or rewrites existing data.</li>
350
+ <li><b>One accent per page.</b> Each top-level page sets <code>data-theme</code>; decorations, shadows and primary buttons follow it.</li>
351
+ <li><b>Text-safe pairs.</b> Use <code>-deep</code> / <code>-ink</code> for text, <code>-bg</code> for tinted backgrounds, <code>-btn</code> for fills that carry white text.</li>
352
+ </ul>
353
+
354
+ <h2 id="palette">Color palette</h2>
355
+ <h3>Base</h3>
356
+ <div class="grid">
357
+ <div class="swatch"><div class="chip" style="background:#f6f2e9"></div><div class="meta"><b>paper</b><span>#f6f2e9</span><br />page background</div></div>
358
+ <div class="swatch"><div class="chip" style="background:#ffffff"></div><div class="meta"><b>card</b><span>#ffffff</span><br />cards, inputs</div></div>
359
+ <div class="swatch"><div class="chip" style="background:#fcf9f3"></div><div class="meta"><b>warm</b><span>#fcf9f3</span><br />table heads, hover</div></div>
360
+ <div class="swatch"><div class="chip" style="background:#1f1f1f"></div><div class="meta"><b>ink</b><span>#1f1f1f</span><br />primary text</div></div>
361
+ <div class="swatch"><div class="chip" style="background:#6b6b6b"></div><div class="meta"><b>ink-2</b><span>#6b6b6b</span><br />secondary text</div></div>
362
+ <div class="swatch"><div class="chip" style="background:#9c9c9c"></div><div class="meta"><b>ink-3</b><span>#9c9c9c</span><br />placeholders</div></div>
363
+ <div class="swatch"><div class="chip" style="background:#e5e1d8"></div><div class="meta"><b>line</b><span>#e5e1d8</span><br />default borders</div></div>
364
+ <div class="swatch"><div class="chip" style="background:#dcd7cb"></div><div class="meta"><b>line-mid</b><span>#dcd7cb</span><br />stronger borders</div></div>
365
+ </div>
366
+
367
+ <h3>Pigments (semantic)</h3>
368
+ <table>
369
+ <thead><tr><th>Family</th><th>Tokens</th><th>Meaning</th></tr></thead>
370
+ <tbody>
371
+ <tr>
372
+ <td><b>jade</b> (green)</td>
373
+ <td>
374
+ <span class="badge green">jade-bg #e0e8d5</span>
375
+ <code>jade #4da791</code> <code>jade-deep #3a8572</code> <code>jade-btn #2e6b5a</code> <code>jade-glow #3de0b0</code>
376
+ </td>
377
+ <td>Success, logged in, Run buttons</td>
378
+ </tr>
379
+ <tr>
380
+ <td><b>qing</b> (blue)</td>
381
+ <td>
382
+ <span class="badge blue">qing-bg #e2eaf0</span>
383
+ <code>qing #5b8aa6</code> <code>qing-deep #33506b</code> <code>qing-glow #4fc3ff</code>
384
+ </td>
385
+ <td>Info, running, read-only actions</td>
386
+ </tr>
387
+ <tr>
388
+ <td><b>gold</b> (amber)</td>
389
+ <td>
390
+ <span class="badge amber">gold-bg #f4e9c9</span>
391
+ <code>gold #c9a54a</code> <code>gold-deep #7a5a1e</code> <code>gold-glow #ffd23f</code> <code>warn-ink #6e5b24</code>
392
+ </td>
393
+ <td>Warnings, unverified, actions that write files</td>
394
+ </tr>
395
+ <tr>
396
+ <td><b>seal</b> (red)</td>
397
+ <td>
398
+ <span class="badge red">seal-bg #f1dad7</span>
399
+ <code>seal #b5443a</code> <code>seal-deep #8a2f28</code> <code>seal-soft #e8a79f</code> <code>seal-glow #ff7b6e</code>
400
+ </td>
401
+ <td>Errors, expired sessions, cancel, actions that rewrite files</td>
402
+ </tr>
403
+ <tr>
404
+ <td><b>jiang</b> (purple)</td>
405
+ <td>
406
+ <code>jiang #a65998</code>
407
+ </td>
408
+ <td>The <b>production</b> environment marker only (white text, 4.7:1 contrast). Not red on purpose: production must not look like an error.</td>
409
+ </tr>
410
+ <tr>
411
+ <td><b>cheng</b> (orange)</td>
412
+ <td>
413
+ <code>cheng #de8435</code> <code>cheng-deep #a05a1c</code> <code>cheng-bg #fbe9d7</code>
414
+ </td>
415
+ <td>
416
+ A <b>reference</b> to something you can open: the <code>T1</code> stamp of a target card, its chips in the case panel
417
+ and the ring of a Targets row. Also the <b>staging</b> environment marker (white text) in the run and report
418
+ pages. Not amber (gold) on purpose: gold means warnings and writes.
419
+ </td>
420
+ </tr>
421
+ <tr>
422
+ <td><b>dai</b> (deep teal)</td>
423
+ <td>
424
+ <span class="badge gray" style="background:#e2ecef;color:#1e3340">dai-bg #e2ecef</span>
425
+ <code>dai #2e4a5c</code> <code>dai-deep #1e3340</code>
426
+ </td>
427
+ <td>Reports page accent, code blocks, modal scrims</td>
428
+ </tr>
429
+ </tbody>
430
+ </table>
431
+
432
+ <h2 id="page-themes">Page themes</h2>
433
+ <p>
434
+ <code>Layout</code> picks <code>data-theme</code> from the route. The theme drives
435
+ <code>--theme</code>, <code>--theme-ink</code>, <code>--theme-btn</code>, <code>--theme-bg</code>
436
+ (and a neighbouring hue <code>--theme-adj</code> for the card gradient). Use the Tailwind tokens
437
+ <code>text-theme-ink</code>, <code>bg-theme-btn</code>, <code>bg-theme-bg</code>, <code>border-theme</code>.
438
+ </p>
439
+ <table>
440
+ <thead><tr><th>Page</th><th><code>data-theme</code></th><th>Accent</th><th>Preview</th></tr></thead>
441
+ <tbody>
442
+ <tr><td>Profiles</td><td><code>jade</code></td><td>#4da791</td>
443
+ <td><span class="nav-pill" style="--theme:#4da791;--theme-ink:#2e6b5a;--theme-bg:#e0e8d5">Profiles</span></td></tr>
444
+ <tr><td>Cases</td><td><code>qing</code></td><td>#5b8aa6</td>
445
+ <td><span class="nav-pill" style="--theme:#5b8aa6;--theme-ink:#33506b;--theme-bg:#e2eaf0">Cases</span></td></tr>
446
+ <tr><td>Run tests</td><td><code>seal</code></td><td>#b5443a</td>
447
+ <td><span class="nav-pill" style="--theme:#b5443a;--theme-ink:#8a2f28;--theme-bg:#f1dad7">Run tests</span></td></tr>
448
+ <tr><td>Reports</td><td><code>dai</code></td><td>#2e4a5c</td>
449
+ <td><span class="nav-pill" style="--theme:#2e4a5c;--theme-ink:#1e3340;--theme-bg:#e2ecef">Reports</span></td></tr>
450
+ <tr><td>AI guide</td><td><code>gold</code></td><td>#c9a54a</td>
451
+ <td><span class="nav-pill" style="--theme:#c9a54a;--theme-ink:#7a5a1e;--theme-bg:#f4e9c9">AI guide</span></td></tr>
452
+ <tr><td>Help (<code>?</code> icon, no label)</td><td><code>qing</code></td><td>#5b8aa6</td>
453
+ <td><span class="nav-pill" style="--theme:#5b8aa6;--theme-ink:#33506b;--theme-bg:#e2eaf0">?</span></td></tr>
454
+ </tbody>
455
+ </table>
456
+ <p class="small muted">
457
+ The page theme only decorates. It must not carry meaning: a green Run button stays green on the red "Run tests" page.
458
+ To add a page: add a nav entry with a theme in <code>layout.tsx</code>; for a new accent also add a
459
+ <code>[data-theme='name']</code> block in <code>index.css</code>.
460
+ </p>
461
+
462
+ <h2 id="typography">Typography</h2>
463
+ <div class="card">
464
+ <p style="font-family:var(--serif);font-size:24px;font-weight:400;margin:0">Serif: headings, brand name, eyebrows</p>
465
+ <p class="mono small muted" style="margin:2px 0 12px">Calistoga (Google Fonts) → Iowan Old Style, Georgia, Songti SC, Noto Serif SC, SimSun</p>
466
+ <p style="margin:0">Sans: body text, buttons, tables, forms. The quick brown fox jumps over the lazy dog.</p>
467
+ <p class="mono small muted" style="margin:2px 0 12px">Inter (Google Fonts) → PingFang SC, Noto Sans SC, Microsoft YaHei, system-ui</p>
468
+ <p class="mono" style="margin:0">Mono: commands, ids, paths, job output (pnpm asrun --profile example-production)</p>
469
+ <p class="mono small muted" style="margin:2px 0 0">Ubuntu Mono (Google Fonts) → ui-monospace, SFMono-Regular, Menlo, Consolas</p>
470
+ </div>
471
+ <p class="small muted">Page titles: serif, regular weight, <code>text-2xl</code>, with a 4px accent bar on the left (<code>border-l-4 border-theme pl-3</code>).</p>
472
+
473
+ <h2 id="buttons">Buttons</h2>
474
+ <p>Use <code>buttonClass(variant)</code> from <code>status.tsx</code>. Never hand-style a button.</p>
475
+ <div class="demo">
476
+ <button class="btn btn-success">
477
+ <!-- Play icon from Feather (https://feathericons.com/) -->
478
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
479
+ Run tests
480
+ </button>
481
+ <button class="btn btn-success">
482
+ <!-- Send icon from Feather (https://feathericons.com/) -->
483
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
484
+ Run AI
485
+ </button>
486
+ <button class="btn btn-stop">
487
+ <!-- XCircle icon from Feather (https://feathericons.com/) -->
488
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
489
+ Close browser
490
+ </button>
491
+ <button class="btn btn-primary">Log in</button>
492
+ <button class="btn btn-secondary">Verify</button>
493
+ <button class="btn btn-info">Full screen</button>
494
+ <button class="btn btn-danger">Cancel</button>
495
+ <button class="btn btn-secondary" disabled>Disabled</button>
496
+ </div>
497
+ <table>
498
+ <thead><tr><th>Variant</th><th>Use for</th></tr></thead>
499
+ <tbody>
500
+ <tr><td><code>success</code> (green)</td><td>Starting a run. Starting tests ("Run tests" on the Run page) always has the Feather <code>Play</code> icon. Sending a command to an AI tool or a shell job ("Run AI" in the case toolbar, "Make cases") always has the Feather <code>Send</code> icon. The icon goes on the left; never use <code>Play</code> for an AI command.</td></tr>
501
+ <tr><td><code>stop</code> (solid red)</td><td>Forcibly ends something that is running, shown in place of the button that started it: "Close browser" on the Profiles page. Always with the Feather <code>XCircle</code> icon.</td></tr>
502
+ <tr><td><code>primary</code> (page accent)</td><td>The main call to action of a page that is not a run: Log in, Save.</td></tr>
503
+ <tr><td><code>info</code> (light blue)</td><td>A view toggle that must be easy to find, not an action on data: "Full screen" / "Restore" in the Jobs drawer bar. Blue = read-only.</td></tr>
504
+ <tr><td><code>secondary</code> (white)</td><td>Everything else: Verify, Visit, Discard, Copy, navigation.</td></tr>
505
+ <tr><td><code>danger</code> (white, red text)</td><td>Cancel a running job, destructive confirmations.</td></tr>
506
+ </tbody>
507
+ </table>
508
+ <p class="small muted">Hover lifts the button 1px; disabled is 50% opacity with no shadow. With an icon, add <code>gap-1.5</code> to the button.</p>
509
+
510
+ <h2 id="badges">Badges</h2>
511
+ <div class="demo">
512
+ <span class="badge green">Logged in</span>
513
+ <span class="badge red">Session expired</span>
514
+ <span class="badge amber">Saved, not verified</span>
515
+ <span class="badge blue">Running</span>
516
+ <span class="badge amber">Needs your input</span>
517
+ <span class="badge gray">Cancelled</span>
518
+ <span class="badge writes">WRITES</span>
519
+ </div>
520
+ <table>
521
+ <thead><tr><th>Tone</th><th>Profile status</th><th>Job status</th></tr></thead>
522
+ <tbody>
523
+ <tr><td><span class="badge green">green</span></td><td>ok: Logged in</td><td>Succeeded</td></tr>
524
+ <tr><td><span class="badge red">red</span></td><td>expired, missing</td><td>Failed</td></tr>
525
+ <tr><td><span class="badge amber">amber</span></td><td>unverified, error</td><td>Needs your input (an AI run that ended with <code>RESULT: needs-input</code>)</td></tr>
526
+ <tr><td><span class="badge blue">blue</span></td><td>-</td><td>Running</td></tr>
527
+ <tr><td><span class="badge gray">gray</span></td><td>-</td><td>Cancelled</td></tr>
528
+ </tbody>
529
+ </table>
530
+ <p class="small muted">
531
+ <code>WRITES</code> is the one badge that is not a tone: red text in a red outline on the card color
532
+ (<code>WritesBadge</code>). A red fill reads as a failure of the case itself, and it must still be noticed wherever
533
+ cases are listed, so the outline carries it without the alarm.
534
+ </p>
535
+
536
+ <h3>Target reference chips</h3>
537
+ <div class="demo">
538
+ <span class="target-chip"><span class="stamp">T1</span> Conduit logo</span>
539
+ <span class="target-chip warn"><span class="stamp">T2</span> Top-bar username link</span>
540
+ <span class="target-chip missing"><span class="stamp">T9</span> no target card</span>
541
+ </div>
542
+ <p class="small muted">
543
+ A <code>[T1]</code> in a step, a ready signal or a check becomes this chip in the case panel (the Targets rows use
544
+ the same orange stamp): clicking it opens the card. Orange is the colour of a reference; the fill says whether the
545
+ card is ready (white), not ready (amber) or missing (red).
546
+ </p>
547
+
548
+ <h3>Environment markers</h3>
549
+ <div class="demo">
550
+ <span class="env production">PRODUCTION</span>
551
+ <span class="env staging">STAGING</span>
552
+ <span class="env other">OTHER</span>
553
+ </div>
554
+ <p class="small muted">
555
+ The environment must never be missed, so it is a solid block with white text, from <code>envFillClass()</code> in <code>status.tsx</code>
556
+ (used by <code>EnvBadge</code> and the banner on top of the Reports page). Production is <code>jiang</code> (purple), staging <code>cheng</code> (orange),
557
+ any other environment <code>gold-deep</code>. Never red: red means errors, failures and writes, and production must not look like an error.
558
+ </p>
559
+
560
+ <h3>Target ids (T1, T2)</h3>
561
+ <div class="demo">
562
+ <span class="badge blue" style="padding-left:4px;gap:6px"><span style="background:var(--dai);color:var(--paper);font-family:var(--mono);font-weight:600;padding:2px 6px;border-radius:2px;font-size:11px">T1</span> Conduit logo</span>
563
+ <span class="badge gray" style="padding-left:4px;gap:6px"><span style="background:var(--dai);color:var(--paper);font-family:var(--mono);font-weight:600;padding:2px 6px;border-radius:2px;font-size:11px">T2</span> Top-bar username link</span>
564
+ </div>
565
+ <p class="small muted">
566
+ A target id is always a solid <code>dai</code> stamp with <code>paper</code> mono text, from <code>TargetKey</code> in
567
+ <code>components/case-panel/target-chip.tsx</code>. It sits inside the chip, as the row marker of a Targets card and at the
568
+ start of the card window's title, so <code>T1</code> looks the same in all three places and never reads as part of the name.
569
+ The chip around it keeps the state colors: red no card, amber not ready, blue ready.
570
+ </p>
571
+
572
+ <h2 id="action-colors">Action colors (AI commands)</h2>
573
+ <p>
574
+ Each AI action has one color that tells how much it changes. The select, its dot and its options share the color;
575
+ a tooltip spells the impact out. Defined in <code>AI_COMMAND_STYLE</code> in <code>pages/cases.tsx</code>.
576
+ </p>
577
+ <div class="demo">
578
+ <span class="cmd review"><span class="dot"></span><span class="fake">Explore case</span></span>
579
+ <span class="cmd gen"><span class="dot"></span><span class="fake">Generate spec</span></span>
580
+ <span class="cmd heal"><span class="dot"></span><span class="fake">Heal spec</span></span>
581
+ </div>
582
+ <table>
583
+ <thead><tr><th>Action</th><th>Color</th><th>Impact</th></tr></thead>
584
+ <tbody>
585
+ <tr><td><code>explore-case</code></td><td>blue (qing)</td><td>Edits only the case file (wording, observations, open questions, inputs). The agent only reads pages; you do the steps that write data.</td></tr>
586
+ <tr><td><code>gen-spec</code></td><td>amber (gold)</td><td>Writes: creates the spec and fills the case's <code>## Values</code>. Does not run the tests.</td></tr>
587
+ <tr><td><code>heal-spec</code></td><td>red (seal)</td><td>Rewrites an existing spec and runs the tests.</td></tr>
588
+ </tbody>
589
+ </table>
590
+ <p class="small muted">
591
+ A new action must pick the color by impact, not by taste: read-only or touches only the case file is blue, adds files (spec, case values) is amber, rewrites or deletes is red. Two actions must never share a color.
592
+ The select itself is the <a href="#dropdowns">Dropdown</a> component with <code>tone</code> and <code>dot</code>; its list shows each choice with the dot of that action.
593
+ </p>
594
+
595
+ <h2 id="dropdowns">Dropdowns</h2>
596
+ <p>
597
+ Every dropdown is the <code>Select</code> component from <code>ui/web/src/components/status.tsx</code>.
598
+ <b>Never write a bare <code>&lt;select&gt;</code></b> and never copy its classes: a native popup is drawn by the
599
+ browser, with its own arrow, height and no shadow, so it looks wrong next to the others (this has gone wrong twice).
600
+ <code>Select</code> is a button that draws its own list under it, in a <code>card-popup</code> panel, which is what
601
+ gives every dropdown the same raised look.
602
+ </p>
603
+ <div class="demo">
604
+ <span class="select"><span class="fake">staging</span><svg class="chev" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
605
+ <span class="select focus"><span class="fake">staging</span><svg class="chev" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
606
+ <span class="select-label invalid">Explore in
607
+ <span class="select invalid"><span class="fake">Choose...</span><svg class="chev" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
608
+ </span>
609
+ <span class="select dotted"><span class="dot"></span><span class="fake">Generate spec</span><svg class="chev" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
610
+ </div>
611
+ <p class="small muted">Left to right: normal, focused, required and empty (invalid), and colored with a dot (the AI command select).</p>
612
+ <pre class="card mono" style="overflow:auto">import {Select} from '../components/status';
613
+
614
+ &lt;Select value={env} onChange={setEnv} invalid={env === ''} aria-label="Environment"&gt;
615
+ &lt;option value=""&gt;Choose...&lt;/option&gt;
616
+ ...
617
+ &lt;/Select&gt;</pre>
618
+ <table>
619
+ <thead><tr><th>Part</th><th>Rule</th></tr></thead>
620
+ <tbody>
621
+ <tr><td>Size</td><td>Height 32px (<code>h-8</code>), text <code>text-sm</code>, corners <code>rounded</code>. Same height as <code>buttonClass()</code> buttons, so they line up in a toolbar.</td></tr>
622
+ <tr><td>Arrow</td><td>One Feather <code>ChevronDown</code>, size 14, <code>text-ink-2</code>, at the right. The component draws it; do not add your own.</td></tr>
623
+ <tr><td>Normal</td><td>White background, <code>border-line-mid</code>, <code>text-ink</code>. Focus: <code>theme</code> border and a 2px ring at 25%.</td></tr>
624
+ <tr><td>Required and empty</td><td>Pass <code>invalid</code>: <code>seal</code> border, <code>seal-bg</code> background, <code>seal-deep</code> text and arrow. Also color the label <code>text-seal-deep</code>. Do not add a separate error message when the placeholder option (<code>Choose...</code>) already says what to do.</td></tr>
625
+ <tr><td>Colored by choice</td><td>Pass <code>tone</code> (border, background, text and ring classes) and <code>dot</code> (dot background) when each option has a meaning color, as in <a href="#action-colors">Action colors</a>. Give each <code>&lt;option&gt;</code> the same dot as <code>data-dot</code>, so the open list shows the color of every choice.</td></tr>
626
+ <tr><td>Code values</td><td>Pass <code>mono</code> for ids and file names.</td></tr>
627
+ <tr><td>Label</td><td>Put a visible label in front (<code>text-sm text-ink-2</code>) or an <code>aria-label</code>. Never wrap the control in a <code>&lt;label&gt;</code>: a click on a row of the open list would be forwarded to the button and reopen it.</td></tr>
628
+ <tr><td>Open list</td><td>A <code>card-popup</code> panel under the control, at most <code>max-h-64</code>, one row per option, the current value with a <code>Check</code>. The row under the pointer or the arrow keys is <code>theme-bg</code>. Arrow keys move, Enter picks, Escape closes, a click outside closes, and the focus stays on the button (a <code>listbox</code> driven by <code>aria-activedescendant</code>).</td></tr>
629
+ <tr><td>Change</td><td><code>onChange(value: string)</code> receives the chosen value, not an event. The options are still written as <code>&lt;option value=...&gt;</code> children.</td></tr>
630
+ <tr><td>Remembering a choice</td><td>Use <code>usePersistedState(key, default, guard)</code> from <code>lib/persisted.ts</code> with the key prefix <code>asrun-ui.</code>. Treat a stored value that is no longer in the options as empty.</td></tr>
631
+ </tbody>
632
+ </table>
633
+
634
+ <h3 id="searchable">Searchable lists</h3>
635
+ <p>
636
+ A list the user has to search in is <code>SearchSelect</code> from
637
+ <code>ui/web/src/components/search-select.tsx</code>, never an <code>input</code> with a <code>&lt;datalist&gt;</code>:
638
+ a native suggestion list cannot be styled, and it has become a dropdown that looks like nothing else. It is a
639
+ text field with the filtered list under it; a native <code>&lt;select&gt;</code> takes no text, which is the only
640
+ reason it is not the <a href="#dropdowns">Dropdown</a>.
641
+ </p>
642
+ <pre class="card mono" style="overflow:auto">import {SearchSelect} from '../components/search-select';
643
+
644
+ &lt;SearchSelect aria-label="Case to run" value={grep} onChange={setGrep} mono
645
+ placeholder="e.g. hub-landing" options={cases.map((c) =&gt; ({value: c.id, hint: c.title}))} /&gt;</pre>
646
+ <table>
647
+ <thead><tr><th>Part</th><th>Rule</th></tr></thead>
648
+ <tbody>
649
+ <tr><td>Shape</td><td>Same 32px height, <code>text-sm</code>, border and focus ring as the Dropdown, so a form that uses both still looks like one family.</td></tr>
650
+ <tr><td>Icons</td><td>Feather <code>Search</code> size 14 at the left; <code>ChevronDown</code> at the right while the field is empty, and a clear <code>X</code> once it holds text.</td></tr>
651
+ <tr><td>List</td><td><code>card-popup</code> panel under the field, at most <code>max-h-64</code>, one row per option with the optional <code>hint</code> as a second muted line. The row under the pointer or the arrow keys is <code>theme-bg</code>.</td></tr>
652
+ <tr><td>Picking</td><td>A picked row fills the field with its value, and focusing a picked value shows the whole list again. The <code>X</code> clears the field.</td></tr>
653
+ <tr><td>Keyboard</td><td>Arrow keys move, Enter picks, Escape closes, and the focus stays in the field. The list is a <code>listbox</code> driven by <code>aria-activedescendant</code>, not a menu.</td></tr>
654
+ </tbody>
655
+ </table>
656
+
657
+ <h2 id="multi-value">Multi-value fields</h2>
658
+ <p>
659
+ A field whose value is a list is never a <a href="#dropdowns">Dropdown</a> plus an "Add" text input: that mixes a
660
+ 32px <code>text-sm</code> control with 12px tags, and the two ways to add a value overlap. The case settings panel
661
+ uses two components from <code>ui/web/src/components/case-panel/</code>, one per kind of list.
662
+ </p>
663
+
664
+ <h3>Tag field</h3>
665
+ <div class="demo">
666
+ <div>
667
+ <span class="tags focus">
668
+ <span class="tag">smoke
669
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
670
+ </span>
671
+ <span class="tag">regression
672
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
673
+ </span>
674
+ <span class="entry">hea<span class="caret"></span></span>
675
+ </span>
676
+ <div class="suggest">
677
+ <div class="opt on"><span>header-navigation</span></div>
678
+ <div class="opt"><span>header-links</span></div>
679
+ <div class="opt"><span>header-menu</span><span class="new">new</span></div>
680
+ </div>
681
+ </div>
682
+ </div>
683
+ <p class="small muted">Left: two committed tags and the entry with the caret. Under it, the open list: a matching name, another one, and a name no case uses yet ("new").</p>
684
+ <table>
685
+ <thead><tr><th>Part</th><th>Rule</th></tr></thead>
686
+ <tbody>
687
+ <tr><td>Box</td><td>Min height 32px (the height of a <code>Select</code> and a button), white, <code>border-line-mid</code>, <code>rounded</code>, <code>px-1.5 py-1</code>. Focus: <code>border-theme</code> and a 2px <code>theme</code> ring at 25%. The box is the input; the entry inside it has no border of its own.</td></tr>
688
+ <tr><td>Tag</td><td>24px high, <code>bg-warm</code>, <code>font-mono text-xs</code>, <code>ring-line-mid</code>. The X is a Feather <code>X</code>, size 12, with <code>aria-label="Remove &lt;value&gt;"</code>.</td></tr>
689
+ <tr><td>Entry</td><td><code>font-mono text-xs</code>, the same size as a tag, so the two never look like different controls. Placeholder says what to type ("Add a suite…").</td></tr>
690
+ <tr><td>Committing</td><td>Comma, Enter and leaving the field add the value. A comma in the middle of the text commits what is in front of it and keeps the rest in the entry, so <code>smoke, regression</code> becomes two tags. Backspace on an empty entry removes the last tag.</td></tr>
691
+ <tr><td>Suggestion list</td><td>At most 8 rows, <code>card-paper</code>, marked with <code>role="listbox"</code>/<code>"option"</code>. It is drawn on the body with fixed coordinates, because the case panel scrolls and would cut it off; any scroll, resize or outside click closes it (<code>row-menu.tsx</code> does the same). Arrow keys move, Enter picks, Escape closes.</td></tr>
692
+ <tr><td>New values</td><td>A typed name that no case uses yet is the last row, marked "new". It must match <code>^[a-z0-9]+(-[a-z0-9]+)*$</code>; a name that does not match turns the box red and never reaches the front matter.</td></tr>
693
+ <tr><td>Required</td><td><code>keepOne</code>: the X of the last tag is disabled and the tag's tooltip says why. Never leave a required list empty and let validation complain afterwards.</td></tr>
694
+ </tbody>
695
+ </table>
696
+
697
+ <h3>Environment toggle chips</h3>
698
+ <div class="demo">
699
+ <span class="env-chip on production"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>PRODUCTION</span>
700
+ <span class="env-chip off">STAGING</span>
701
+ <span class="env-chip blocked">SANDBOX</span>
702
+ </div>
703
+ <p class="small muted">Left to right: pressed, not pressed, and a value this form cannot offer (red; a click removes it).</p>
704
+ <table>
705
+ <thead><tr><th>Part</th><th>Rule</th></tr></thead>
706
+ <tbody>
707
+ <tr><td>Shape</td><td>One tag per value, 24px high, <code>text-xs font-bold tracking-wider</code>. The whole tag is the button (<code>aria-pressed</code>): no box around the group, so no part of a border looks clickable and is not.</td></tr>
708
+ <tr><td>On</td><td><code>envFillClass(env)</code> (see <a href="#badges">Environment markers</a>) plus a Feather <code>Check</code>, size 12. Color is never the only signal.</td></tr>
709
+ <tr><td>Off</td><td>White, <code>ring-line-mid</code>, <code>text-ink-2</code>; hover <code>bg-warm</code>.</td></tr>
710
+ <tr><td>No X</td><td>Never a removable tag for an on/off choice: pressing it again turns it off. An X on "only these" asks the opposite question ("not these?"), which is exactly the confusion to avoid.</td></tr>
711
+ <tr><td>Blocked value</td><td>A selected value the form cannot offer (a write environment the case does not run in) is <code>seal-bg</code> / <code>seal-deep</code>; its tooltip says what to do, and a click removes it.</td></tr>
712
+ <tr><td>Required</td><td><code>keepOne</code>: the last pressed tag stays on; its tooltip says why. The group is what lets the case run anywhere.</td></tr>
713
+ </tbody>
714
+ </table>
715
+
716
+ <h2 id="icons">Icons</h2>
717
+ <p>
718
+ Icons come from <a href="https://feathericons.com/" target="_blank" rel="noopener">Feather</a> through the
719
+ <code>react-feather</code> package. <b>Do not draw or paste custom SVGs in the app.</b>
720
+ </p>
721
+ <div class="demo">
722
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
723
+ <span class="label">Play (run tests)</span>
724
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
725
+ <span class="label">Send (run AI / shell commands)</span>
726
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
727
+ <span class="label">ChevronDown</span>
728
+ </div>
729
+ <pre class="card mono" style="overflow:auto">import {Play} from 'react-feather';
730
+
731
+ &lt;button className={`${buttonClass('success')} gap-1.5`}&gt;
732
+ &lt;Play size={14} aria-hidden="true" /&gt;
733
+ Run tests
734
+ &lt;/button&gt;</pre>
735
+ <ul class="tight">
736
+ <li>Size 14 inside buttons and selects, 16-20 elsewhere. Stroke width stays at Feather's default (2).</li>
737
+ <li>Icons inherit <code>currentColor</code>; color them with the text color classes, not fills.</li>
738
+ <li>Decorative icons get <code>aria-hidden="true"</code>; icon-only buttons need an <code>aria-label</code>.</li>
739
+ <li>A button that is just an icon in a dense list has no background or border: <code>text-jade-deep hover:text-jade-btn</code> for "run", <code>text-ink-3 hover:text-ink</code> for neutral ones (clear search). The one exception is the run mark at the left of a Cases row (see <a href="#sections">Sidebar sections</a>): a solid green disc with a white icon, because the state of the row must be read before the row itself.</li>
740
+ <li>Find names at <a href="https://feathericons.com/" target="_blank" rel="noopener">feathericons.com</a> and import them in PascalCase (<code>arrow-right</code> becomes <code>ArrowRight</code>).</li>
741
+ <li>Exception, brand logos: Feather has no logos of other products, so the Claude and OpenCode logos of the AI agent switch are static SVG files in <code>ui/web/public/logos/</code> (single path, <code>viewBox="0 0 24 24"</code>). Show them with the <code>Logo</code> mask in <code>components/tool-switch.tsx</code> so they take the text color. Never paste logo SVGs into components.</li>
742
+ </ul>
743
+
744
+ <h2 id="tooltips">Tooltips</h2>
745
+ <p>
746
+ Write a plain <code>title="..."</code> on the element. <code>lib/tooltip.ts</code> (tippy.js) turns every <code>title</code> into
747
+ a dark tooltip with an arrow on hover, so the browser's native tooltip never shows. Do not set up tippy per component and do not
748
+ use a <code>title</code> on an <code>iframe</code>. Each <code>\n</code> in the text starts a new paragraph, and a line that starts with <code># </code> is a bold heading. Keep it short: a heading, then one or two sentences. Never put a command line or other long raw text in a tooltip. Disabled buttons and inputs get the
749
+ tooltip too, so a <code>title</code> that says why something is disabled works. The box is styled by <code>.tippy-box[data-theme~='tip']</code>
750
+ in <code>index.css</code>; the keyword help of the case editor and the ? help of the case panel have their own
751
+ <code>inkstone</code> theme, and both open on click (see <code>ui/web/src/lib/section-help.ts</code>).
752
+ A target card is too long for a tooltip: it opens as a window (<code>components/case-panel/target-card-dialog.tsx</code>).
753
+ </p>
754
+
755
+ <h3>Field help (the <code>?</code> after a label)</h3>
756
+ <div class="demo">
757
+ <span class="field-label">writes data in
758
+ <svg class="help" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
759
+ </span>
760
+ <span class="field-label">suites
761
+ <svg class="help" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
762
+ </span>
763
+ </div>
764
+ <p class="small muted">
765
+ A form row keeps its label short; the <code>?</code> right after it opens a card with that one field's meaning
766
+ (<code>FieldHelp</code> in <code>components/case-panel/section.tsx</code>, text in <code>FIELD_HELP</code> of
767
+ <code>lib/section-help.ts</code>). It is the same delegated tippy instance as the section help: click to open,
768
+ Escape closes. The icon is 12, not the usual 14, because it sits next to a 12px row label; it is an icon-only
769
+ button, so it needs <code>aria-label="About &lt;field&gt;"</code>.
770
+ </p>
771
+
772
+ <h2 id="editor">Markdown editor</h2>
773
+ <p class="muted">
774
+ The case editor (CodeMirror) is the "draft" next to the white preview "sheet". Layout and colors live in the
775
+ <code>.inkstone-editor</code> rules of <code>index.css</code>; <code>ui/web/src/lib/editor-theme.ts</code> maps syntax tags to
776
+ the <code>tok-*</code> classes. Use both for any new editor, and never the built-in CodeMirror themes.
777
+ The editor is monospace only: no serif or sans font, not even for headings.
778
+ The preview renders the front matter as a key/value table (keys in <code>warm</code> cells, list values as blue badges).
779
+ The small Markdown fields of the case panel (steps, bullets, answers, the writes-note) reuse the same theme in a
780
+ bordered, auto-growing box (<code>.inkstone-field</code>): same font and highlight colors, scrolling past a height
781
+ limit; a line revealed from the panel blinks three times (<code>.cm-flash</code>, <code>flashField</code>).
782
+ </p>
783
+ <div class="editor-demo">
784
+ <div class="ed-active"><span class="ed-mark">---</span></div>
785
+ <div><span class="ed-prop">suites</span><span class="ed-punct">:</span> <span class="ed-punct">[</span>smoke<span class="ed-punct">]</span></div>
786
+ <div><span class="ed-mark">---</span></div>
787
+ <div><span class="ed-mark">##</span> <span class="ed-h">Steps</span></div>
788
+ <div><span class="ed-list">1.</span> Open <span class="ed-mark">`</span><span class="ed-code">/app/documents</span><span class="ed-mark">`</span>.</div>
789
+ <div><span class="ed-list">-</span> See <span class="ed-mark">[</span><span class="ed-link">docs</span><span class="ed-mark">](</span><span class="ed-url">https://example.com</span><span class="ed-mark">)</span></div>
790
+ <div><span class="ed-mark">&gt;</span> <span class="ed-quote">A quoted note</span></div>
791
+ </div>
792
+ <table style="margin-top:12px">
793
+ <tr><th>Element</th><th>Token</th></tr>
794
+ <tr><td>Editor background</td><td><code>warm</code> over the paper grain; gutter <code>paper</code>, separated by a <code>line-mid</code> border</td></tr>
795
+ <tr><td>Cursor, active line, active line number</td><td><code>theme-btn</code>, <code>theme-bg</code> at 40%, <code>theme-ink</code>; they follow the page theme</td></tr>
796
+ <tr><td>Selection</td><td><code>theme</code> at 30%</td></tr>
797
+ <tr><td>Bracket match</td><td><code>gold-bg</code> with <code>gold-deep</code> text</td></tr>
798
+ <tr><td>Headings</td><td><code>seal-deep</code>, bold, monospace (<code>theme-ink</code> on the red <code>seal</code> page)</td></tr>
799
+ <tr><td>Marks (<code>#</code> <code>&gt;</code> backticks, <code>---</code>)</td><td><code>ink-3</code></td></tr>
800
+ <tr><td>List markers</td><td><code>gold-deep</code>, bold</td></tr>
801
+ <tr><td>Bold, italic</td><td><code>ink</code>, bold or italic</td></tr>
802
+ <tr><td>Quote</td><td><code>ink-2</code>, italic</td></tr>
803
+ <tr><td>Link text, URL</td><td><code>qing-deep</code>; <code>ink-3</code> underlined</td></tr>
804
+ <tr><td>Inline code</td><td><code>jade-btn</code> on <code>jade-bg</code></td></tr>
805
+ <tr><td>Front matter keys, types, tags</td><td><code>qing-deep</code></td></tr>
806
+ <tr><td>Code blocks</td><td>keywords <code>seal-deep</code>, strings <code>jade-btn</code>, numbers <code>gold-deep</code>, functions <code>dai</code>, comments <code>ink-3</code>, punctuation <code>ink-2</code></td></tr>
807
+ </table>
808
+
809
+ <h3>Keyword highlights and their tooltip</h3>
810
+ <p class="muted">
811
+ Words that need a human are marked in the editor and in the preview (<code>&lt;mark&gt;</code>) with the same
812
+ <code>kw-*</code> classes: <code>TODO [blocking]</code> <code>seal</code>, other <code>TODO</code> <code>gold</code>,
813
+ "to be confirmed by QA" / "not observed" <code>qing</code>, <code>Answer:</code> <code>jade</code>; the emoji in front
814
+ of a marker is part of it and takes the same color. Hovering one shows a
815
+ small dark card with an arrow (tippy.js, theme <code>inkstone</code>: <code>dai-deep</code> background like the preview code
816
+ blocks, <code>paper</code> text, <code>shadow-lift</code>) so that it stands out on both the white preview and the beige
817
+ editor. A 6px left bar and the title, both in the bright <code>*-glow</code> color of the kind (<code>seal-glow</code>,
818
+ <code>gold-glow</code>, <code>qing-glow</code>, <code>jade-glow</code>; they exist only for dark surfaces),
819
+ show the kind; below come one line of meaning (<code>line-mid</code>) and a numbered "You can:" list. The texts live in
820
+ <code>keywordHelp</code> in <code>ui/web/src/lib/highlight-keywords.ts</code>; keep every line short and direct.
821
+ Do not add another tooltip library: use <code>tippy.js</code> and the <code>inkstone</code> theme.
822
+ </p>
823
+ <p class="muted">
824
+ After every marker comes a small remove button (<code>kw-remove</code>, <code>kw-remove-&lt;kind&gt;</code>): the
825
+ Feather <code>X</code> at 14px in the same text and background colors as the marker, 20px square, 2px radius,
826
+ <code>theme</code> focus outline. It is a plain button (a CodeMirror widget in the editor, an element in the preview),
827
+ never a new icon. It deletes the marker, or the whole question block of a <code>TODO</code> (the sentence above stays),
828
+ or what is typed after an <code>Answer:</code>; the logic is in <code>ui/web/src/lib/keyword-removal.ts</code>.
829
+ </p>
830
+ <p class="muted">
831
+ The preview block that holds the editor caret is shaded with <code>theme-bg</code> at 60% and a 2px <code>theme</code> left
832
+ border (an overlay, so it follows the page theme). The editor and the preview scroll together and a click in the preview
833
+ moves the caret; the logic is in <code>ui/web/src/lib/preview-sync.ts</code>.
834
+ </p>
835
+ <p class="muted">
836
+ The previous / next buttons of the case toolbar move the caret to the next marker and blink that marker for a moment in
837
+ both panes (<code>cm-kw-flash</code> in the editor, <code>kw-flash</code> over the <code>&lt;mark&gt;</code> of the
838
+ document): <code>gold-glow</code> at 65% behind it and a 2px <code>dai-deep</code> ring with a wider
839
+ <code>gold-glow</code> glow around it, the ring blinking on and off three times (0.45&nbsp;s each), then it goes away.
840
+ The ring is what makes a marker findable whatever its own color is (a <code>TODO</code> is gold, a blocking one red);
841
+ the caret and the shaded block alone are not enough. Without motion (reduced motion) the highlight is shown for the
842
+ same 1.4&nbsp;s, steady.
843
+ </p>
844
+
845
+ <h2 id="surfaces">Surfaces and shadows</h2>
846
+ <div class="row">
847
+ <div class="card" style="width:240px">
848
+ <b>card-paper</b>
849
+ <p class="small muted" style="margin:4px 0 0">White sheet, 4px radius, a faint accent tint at the top left, colored shadow that follows the page theme.</p>
850
+ </div>
851
+ <div style="display:flex;align-items:center;gap:10px">
852
+ <span class="seal-stamp">A</span>
853
+ <span class="small muted"><code>.seal</code>: the stamp. The only slightly rotated element; use it once, in the header.</span>
854
+ </div>
855
+ </div>
856
+ <ul class="tight">
857
+ <li>Radius: 4px for cards, buttons and inputs; 2px for badges; no pills.</li>
858
+ <li>Shadows are tinted with the page accent (<code>shadow-paper</code>, <code>shadow-lift</code>, <code>shadow-btn</code>, <code>shadow-btn-lift</code>), never plain gray.</li>
859
+ <li><code>card-panel</code> (the case panel's sections): the white sheet without the accent gradients, with a neutral ink-tinted elevation, because the panel is dense with semantic colors (green/gold/red badges).</li>
860
+ <li><code>card-popup</code>: a flat white panel that floats over the page — every dropdown list, suggestion list and row menu — with a heavier elevation than the cards (a dark near shadow for definition plus a wide accent-tinted one), so it reads as above the content it covers rather than as another sheet on it. Its accent is the one of its own page: the shadow is declared in <code>index.css</code>, not taken from a <code>shadow-*</code> utility.</li>
861
+ <li>The page background carries a subtle paper grain (<code>--grain</code>).</li>
862
+ <li>The Jobs drawer, header and top-level separators use a 2px <code>ink</code> border; everything inside uses <code>line</code> / <code>line-mid</code>.</li>
863
+ <li>The Jobs drawer's title bar is a solid <code>dai-deep</code> bar with white text, so it reads as a handle. It ends with the same small chevron block as a sidebar section header and brightens on hover.</li>
864
+ <li>The job log is a <code>dai-deep</code> pane of monospace lines. An AI run's blocks use <code>paper</code> at
865
+ different weights: the timestamp and the time a call took at 30%, a tool's name in <code>qing-glow</code>, its
866
+ detail at 55%, its output at 60% behind a <code>paper/15</code> left border, and the agent's thinking at 45%
867
+ italic, because it is not a result. What the agent says is Markdown rendered inside the pane
868
+ (<code>prose prose-log</code> in <code>index.css</code>): dark <code>--tw-prose-*</code> tokens, sans body, the
869
+ page's serif headings. A failure is <code>seal-glow</code>, a hint <code>gold-glow</code>. Never color a block any
870
+ other way: the four weights are what makes the log scannable.</li>
871
+ <li><b>Every expand/collapse animates.</b> Use <code>Collapse</code> from <code>components/collapse.tsx</code> (height and fade, 200ms, <code>ease-out</code>) for content that appears and disappears, and rotate the chevron with <code>transition-transform duration-200</code> instead of swapping two icons. Always add <code>motion-reduce:transition-none</code>. Turn transitions off while the user drags a resize handle.</li>
872
+ </ul>
873
+
874
+ <h2 id="sections">Sidebar sections and selection</h2>
875
+ <div class="row" style="align-items:flex-start">
876
+ <div class="side">
877
+ <div class="sec"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>New cases<span class="toggle"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg></span></div>
878
+ <div class="sec"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>Case list<span class="aside">3 / 12</span></div>
879
+ <div class="search">
880
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
881
+ <span class="fake">Search name, title, suite...</span>
882
+ </div>
883
+ <div class="folder">documents <span style="margin-left:auto;color:var(--ink-3)">2</span></div>
884
+ <div class="item selected">hub-landing</div>
885
+ <div class="item">header-logo</div>
886
+ </div>
887
+ <div style="flex:1;min-width:260px">
888
+ <ul class="tight">
889
+ <li>Sections of a sidebar (<code>SectionHeader</code> in <code>pages/cases.tsx</code>) are all on one level: a solid page-accent bar (<code>bg-theme-btn</code>, white text and icon, serif label), so a section header is never mistaken for a folder row (gray). A collapsible one ends with a small button block (<code>bg-white/15</code>, Feather <code>ChevronDown</code> when collapsed, <code>ChevronUp</code> when open, size 16) that brightens on hover; a static one has no block, only an optional count. The block is what says "this can be expanded".</li>
890
+ <li><code>bg-theme-bg</code> means <b>selected</b> and nothing else. Never use it for a header, so a header cannot be mistaken for a selected row. A selected row also gets <code>border-l-4 border-l-theme</code>.</li>
891
+ <li>Levels inside a list: section header, then folder (<code>bg-warm</code>, small, regular weight, no accent), then item.</li>
892
+ <li>A search box is a <code>fieldClass</code> input with a Feather <code>Search</code> icon at the left and a clear button (<code>X</code>, <code>aria-label</code>) at the right. It filters while typing, with no submit; <code>Esc</code> clears it.</li>
893
+ <li>An item in the Cases list shows its two states at the left edge, never as badges: a solid green round button (<code>bg-jade</code>, white Feather <code>Play</code> 12, <code>hover:bg-jade-btn</code>) when its spec is generated, which opens the Run tests page for that case, and an amber <code>AlertTriangle</code> 15 in <code>text-gold</code> alone, with no disc or halo, when it has none: not a button, only a tooltip ("No spec yet"), because there is nothing to do yet. Both sit in the same 20px slot, so a row keeps its shape; the tooltip states the state in words, which keeps the row's badges free for suites, login and <code>WRITES</code>.</li>
894
+ </ul>
895
+ <p class="small muted">The icons in the demo are inline only because this page has no build step; in the app they always come from <code>react-feather</code>.</p>
896
+ </div>
897
+ </div>
898
+
899
+ <h2 id="checklist">Checklist for a new UI element</h2>
900
+ <ol class="tight">
901
+ <li>Pick colors only from the tokens above (<code>text-ink-2</code>, <code>bg-warm</code>, <code>bg-seal-bg</code>, ...).</li>
902
+ <li>Decide the impact: safe/go = green, read-only/info = blue, writes/attention = amber, destructive/rewrite/error = red.</li>
903
+ <li>Use <code>buttonClass()</code> and <code>Badge</code> instead of new styles.</li>
904
+ <li>Dropdowns: use <code>Select</code>, never a bare <code>&lt;select&gt;</code> (see <a href="#dropdowns">Dropdowns</a>).</li>
905
+ <li>Lists of values: use <code>TagField</code> (open list of names) or <code>EnvChips</code> (on/off environments), never <code>Select</code> + "Add" (see <a href="#multi-value">Multi-value fields</a>).</li>
906
+ <li>Fonts: only the three families in <code>--font-serif</code>, <code>--font-sans</code> and <code>--font-mono</code> (Calistoga, Inter, Ubuntu Mono). Never add a fourth.</li>
907
+ <li>Icons: Feather via <code>react-feather</code> only.</li>
908
+ <li>If you add or change a token, update <code>index.css</code> and this page together.</li>
909
+ </ol>
910
+ <div class="rule" style="margin-top:16px">
911
+ Exception: if a color's meaning has to differ from this guide (for example a destructive action that is not red), say why in a code comment.
912
+ </div>
913
+
914
+ <footer>Inkstone theme guide for the AsRun UI. Static file: <code>docs/theme-guide.html</code>. Inkstone theme, designed and created by Zoro Chen.
915
+ <div class="creator">
916
+ <span>Created by Zoro Chen</span>
917
+ </div>
918
+ </footer>
919
+ </main>
920
+ </div>
921
+ </body>
922
+ </html>