@litfamily/litclaude 1.0.30 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +8 -8
- package/README_ko-KR.md +8 -8
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +16 -9
- package/package.json +1 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/hooks/lit-mod.mjs +16 -10
- package/plugins/litclaude/lib/mod/native-card.mjs +133 -0
- package/plugins/litclaude/lib/mod/svg-card.mjs +17 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.31 - 2026-10-17
|
|
4
|
+
|
|
5
|
+
- In the Claude Code desktop app, the mod card used to blink: every real change, such as a new step, tool or skill, swapped in a whole new page, so the card went blank for a moment and its animations started over. The desktop card is now built from the app's own boxes and text, and a change updates only what moved. It looks the same but no longer animates. The terminal and phone cards are unchanged.
|
|
6
|
+
|
|
3
7
|
## 1.0.30 - 2026-10-16
|
|
4
8
|
|
|
5
9
|
- The pre-write guard now refuses more of the lines that tell a reader what a document is not for, such as `This table is not a ranking.` or `이 문서는 제품을 비교하려는 것이 아닙니다.` These forms came from real sessions. Bold-label callouts such as `> **주의:** ...` are now read too, while ordinary quoted text is still skipped. Lines from manuals and lab reports that only look similar stay allowed, and a large flood of matches can no longer push the scan past its limit into an allowed write.
|
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitClaude robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
|
|
2
2
|
|
|
3
3
|
<h1 align="center">LitClaude</h1>
|
|
4
4
|
<p align="center"><strong>Keep the work lit.</strong></p>
|
|
5
5
|
<p align="center">A Claude Code plugin that helps every session plan the work, build it, check it, and leave a note the next session can pick up.</p>
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
8
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
7
|
+
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/readme/badge-version.svg" alt="1.0.31" />
|
|
8
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/litclaude#readme">Full guide and skills gallery on GitHub</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/litclaude#readme">Full guide and skills gallery on GitHub</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/README_ko-KR.md">한국어</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
Add `lit` to a Claude Code prompt and Claude pins the goal, writes a failing test first,
|
|
@@ -73,7 +73,7 @@ the `/plugin` menu.
|
|
|
73
73
|
## More
|
|
74
74
|
|
|
75
75
|
- [Full guide and skills gallery on GitHub](https://github.com/wjgoarxiv/litclaude#readme)
|
|
76
|
-
- [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
77
|
-
- [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
78
|
-
- [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
79
|
-
- [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
76
|
+
- [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/privacy.md)
|
|
77
|
+
- [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/migration.md)
|
|
78
|
+
- [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/CHANGELOG.md)
|
|
79
|
+
- [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/LICENSE)
|
package/README_ko-KR.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitClaude 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
|
|
2
2
|
|
|
3
3
|
<h1 align="center">LitClaude</h1>
|
|
4
4
|
<p align="center"><strong>Keep the work lit.</strong></p>
|
|
5
5
|
<p align="center">Claude Code에서 세션마다 계획하고, 만들고, 확인한 뒤 다음 세션이 이어받을 기록을 남기게 돕는 플러그인입니다.</p>
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
8
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
7
|
+
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/readme/badge-version.svg" alt="1.0.31" />
|
|
8
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md">전체 안내와 스킬 갤러리는 GitHub에서</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md">전체 안내와 스킬 갤러리는 GitHub에서</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/README.md">English</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
Claude Code 프롬프트에 `lit`을 붙이면 Claude가 목표를 고정하고, 실패하는 테스트를 먼저 쓰고,
|
|
@@ -71,7 +71,7 @@ LitClaude는 프로젝트 상태를 `.litclaude/`에 두고, 그 안의 `.gitign
|
|
|
71
71
|
## 더 보기
|
|
72
72
|
|
|
73
73
|
- [전체 안내와 스킬 갤러리 (GitHub)](https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md)
|
|
74
|
-
- [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
75
|
-
- [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
76
|
-
- [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
77
|
-
- [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
74
|
+
- [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/privacy.md)
|
|
75
|
+
- [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/docs/migration.md)
|
|
76
|
+
- [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/CHANGELOG.md)
|
|
77
|
+
- [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.31/LICENSE)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.31"><title>release: 1.0.31</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.31</text></g></svg>
|
package/docs/hooks.md
CHANGED
|
@@ -462,15 +462,22 @@ step's spinner (`- \ | /`) and clock are ASCII, and a glyph swaps only together
|
|
|
462
462
|
|
|
463
463
|
The desktop app's Code tab, and the other surfaces that are not a terminal, draw a mod's `Text` in
|
|
464
464
|
a proportional font and cut each span to the cells it was laid out for, so the card above comes
|
|
465
|
-
apart there.
|
|
466
|
-
(`lib/mod/
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
465
|
+
apart there. In the desktop app the mod builds the same five rows from the app's own boxes and
|
|
466
|
+
text instead (`lib/mod/native-card.mjs`): a dark card with a rounded border, the LIT pill, the
|
|
467
|
+
stage pills, the running tool, the skill chips and the usage figures, each row laid out by the app
|
|
468
|
+
in its monospaced face. The app updates that card in place when something changes, so a new stage,
|
|
469
|
+
tool or skill no longer blanks it. It holds still: the app's boxes and text have no animation, and
|
|
470
|
+
the card shows the settled state with no entrances or crossfades.
|
|
471
|
+
|
|
472
|
+
On a phone the card is one `Svg` drawing (`lib/mod/svg-card.mjs`): monospace text placed by
|
|
473
|
+
counted cells, real pills and a rounded border. While LitClaude works it moves with SMIL animation
|
|
474
|
+
inside the drawing: the pill and the border breathe and the step's spinner turns, and on `full` a
|
|
475
|
+
light crosses the current stage and the current chips, a head runs the rail leaving it, the count
|
|
476
|
+
bar's head shimmers and a comet runs around the border. An idle card is a still image.
|
|
477
|
+
|
|
478
|
+
Neither the desktop card nor the phone drawing has the step clock or the loop countdown: a new
|
|
479
|
+
drawing restarts the phone's animation, and the desktop card would only show a frozen clock. Both
|
|
480
|
+
name the running tool and show the loop round, and are redrawn only when something you would
|
|
474
481
|
notice changes, such as the stage, the tool, a skill, a usage figure or the goal. While the card
|
|
475
482
|
shows in a terminal and the app at once, the terminal's loop countdown and lane times wait for such
|
|
476
483
|
a change too; its step clock keeps counting on its own.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@litfamily/litclaude",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"description": "Claude Code plugin: add lit to a prompt and Claude plans, tests, checks and reviews the work, then leaves a handoff for the next session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { composeBand, composeEvidence } from "../lib/mod/frame.mjs";
|
|
16
16
|
import { TIMINGS } from "../lib/mod/motion.mjs";
|
|
17
|
+
import { composeNativeCard } from "../lib/mod/native-card.mjs";
|
|
17
18
|
import { renderBand, renderRows } from "../lib/mod/render.mjs";
|
|
18
19
|
import { accentFromSettings, normalizeModSettings, resolveGlyphs } from "../lib/mod/settings.mjs";
|
|
19
20
|
import { composeSvgCard } from "../lib/mod/svg-card.mjs";
|
|
@@ -101,17 +102,18 @@ const stillModel = (model) => model && {
|
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
// What the card would show on each surface still attached. Invalidation redraws every surface at
|
|
104
|
-
// once
|
|
105
|
-
//
|
|
106
|
-
// the
|
|
107
|
-
// terminal
|
|
105
|
+
// once. A phone's Svg is loaded afresh with each new source, restarting its animations, and the
|
|
106
|
+
// desktop's own-element card shows no clock; so while either is attached, time alone never
|
|
107
|
+
// redraws: the Svg is compared by its drawing, the desktop and the terminal by the still model (the
|
|
108
|
+
// terminal's surface module counts the step clock on by itself). A terminal on its own is compared
|
|
109
|
+
// by the whole model, so its countdown and lane times move too.
|
|
108
110
|
function drawnKey(live, model) {
|
|
109
111
|
const kinds = new Set(Object.values(live.drawnOn));
|
|
110
112
|
if (kinds.size === 0) return JSON.stringify(model);
|
|
111
|
-
const
|
|
113
|
+
const still = kinds.has("svg") || kinds.has("native");
|
|
112
114
|
return [...kinds].map((kind) => (kind === "svg"
|
|
113
115
|
? (model?.visible ? composeSvgCard(model, { motion: live.motion, accent: live.accent }).source : "hidden")
|
|
114
|
-
: JSON.stringify(
|
|
116
|
+
: JSON.stringify(still ? stillModel(model) : model))).join("\n");
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
async function rebuild($, live) {
|
|
@@ -373,10 +375,14 @@ export const register = (on, options) => {
|
|
|
373
375
|
const elements = $.ui.resolve(e);
|
|
374
376
|
const columns = Math.max(20, e.props.bodyColumns ?? e.viewport?.columns ?? 80);
|
|
375
377
|
let drawn = null;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
//
|
|
378
|
+
const native = e.surface === "desktop";
|
|
379
|
+
live.drawnOn[e.surface] = native ? "native" : e.surface !== "terminal" && elements.Svg ? "svg" : "cells";
|
|
380
|
+
if (native) {
|
|
381
|
+
// The desktop draws Text in a proportional font, so the cell-built card comes apart there, and
|
|
382
|
+
// it loads each new Svg into a fresh frame, blanking it; it gets a card of flex rows instead.
|
|
383
|
+
if (model.visible) drawn = composeNativeCard(elements, model, { accent: live.accent });
|
|
384
|
+
} else if (e.surface !== "terminal" && elements.Svg) {
|
|
385
|
+
// A phone or editor draws Text in a proportional font too; it gets the card as one drawing.
|
|
380
386
|
if (model.visible) {
|
|
381
387
|
const { source, alt, animated } = composeSvgCard(model, { motion: settings.motion, accent: live.accent });
|
|
382
388
|
drawn = elements.Svg({ source, alt, ...(animated ? { isInteractive: true } : {}) });
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The LIT card in the desktop app's own elements: a bordered Box of flex rows and monospaced Text
|
|
2
|
+
// spans, laid out by the app rather than by counted cells. The app draws a Box as a flex div and a
|
|
3
|
+
// Text as a styled span (the host's StyledElement), so a new tree updates the page in place. The Svg
|
|
4
|
+
// card it replaces was a whole document the app loaded into a fresh sandboxed frame on every real
|
|
5
|
+
// change (the stage, the tool, a skill), which blanked the card and restarted its animations.
|
|
6
|
+
//
|
|
7
|
+
// It shows the same five rows and colours as the Svg card (svg-card.mjs): the brand pill, the
|
|
8
|
+
// discipline and the stage indicator; the workflow map or the phase track with the lanes and
|
|
9
|
+
// progress; the detail line with the running tool; the skill chips, the notice or the finish; the
|
|
10
|
+
// usage figures. Box and Text carry no animation props, so this card is still; like the Svg card
|
|
11
|
+
// it carries no step clock and no loop countdown, so time alone never changes the tree.
|
|
12
|
+
|
|
13
|
+
import { barCells } from "./motion.mjs";
|
|
14
|
+
import { C, LABELS, PHASES, TONES, VERDICT_FILL, borderColor, indicatorOf, levelColor, mapColor, rightOfPhases } from "./svg-card.mjs";
|
|
15
|
+
|
|
16
|
+
const MAX_CHIPS = 6;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The card as one element tree from a surface's `{ Box, Text }`. `accent` is the HUD accent.
|
|
20
|
+
*/
|
|
21
|
+
export const composeNativeCard = ({ Box, Text }, model, { accent = "#5fd7ff" } = {}) => {
|
|
22
|
+
const span = (body, { fill = C.white, bold = false, bg = null } = {}) => Text({
|
|
23
|
+
children: body,
|
|
24
|
+
color: fill,
|
|
25
|
+
mono: true,
|
|
26
|
+
wrap: "truncate-end",
|
|
27
|
+
...(bg ? { backgroundColor: bg } : {}),
|
|
28
|
+
...(bold ? { weight: "semibold" } : {}),
|
|
29
|
+
});
|
|
30
|
+
const pill = (label, fill, ink = C.ink, bold = true) => span(` ${label} `, { fill: ink, bg: fill, bold });
|
|
31
|
+
const pieces = (list) => list.map(([body, style]) => span(body, style));
|
|
32
|
+
const group = (children, gap = "xs") => Box({ flexDirection: "row", alignItems: "center", gap, flexShrink: 1, overflow: "hidden", children });
|
|
33
|
+
const joined = (groups) => groups.flatMap((list, index) => (index > 0 ? [span("·", { fill: C.faint }), ...pieces(list)] : pieces(list)));
|
|
34
|
+
const row = (left, right = []) => Box({
|
|
35
|
+
flexDirection: "row",
|
|
36
|
+
justifyContent: "space-between",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: "md",
|
|
39
|
+
children: [group(left.length ? left : [span(" ")]), ...(right.length ? [group(right)] : [])],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const indicator = indicatorOf(model, accent);
|
|
43
|
+
const top = row(
|
|
44
|
+
[pill("🔥 LIT", C.pink), span("·", { fill: C.dim }), span(model.title ?? "ready", model.idle ? { fill: C.dim } : { fill: accent, bold: true })],
|
|
45
|
+
[span(indicator.label, { fill: indicator.fill, bold: !indicator.plain })],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const nowFill = (isWork = true) => (model.idle ? C.muted : model.ctxWarning && isWork ? C.amber : accent);
|
|
49
|
+
const nowInk = model.idle ? C.mutedText : C.ink;
|
|
50
|
+
let track;
|
|
51
|
+
if (model.workflow) {
|
|
52
|
+
const { stages } = model.workflow;
|
|
53
|
+
const last = Math.max(1, stages.length - 1);
|
|
54
|
+
const left = [];
|
|
55
|
+
stages.forEach((stage, index) => {
|
|
56
|
+
if (index > 0) left.push(span("──", { fill: stage.state === "todo" ? C.rail : mapColor(accent, (index - 0.5) / last) }));
|
|
57
|
+
if (stage.state === "now") left.push(pill(stage.label, nowFill(), nowInk));
|
|
58
|
+
else left.push(span(`${stage.label} ${stage.state === "done" ? "✓" : "·"}`, { fill: stage.state === "done" ? mapColor(accent, index / last) : C.dim }));
|
|
59
|
+
});
|
|
60
|
+
track = row(left, joined(rightOfPhases(model, false)));
|
|
61
|
+
} else {
|
|
62
|
+
const left = [];
|
|
63
|
+
PHASES.forEach((id, index) => {
|
|
64
|
+
const state = model.phases[id];
|
|
65
|
+
if (index > 0) left.push(span("──", { fill: model.phases[PHASES[index - 1]] === "done" ? accent : C.rail }));
|
|
66
|
+
if (state === "now") left.push(pill(LABELS[id], nowFill(id === "work"), nowInk));
|
|
67
|
+
else left.push(span(`${state === "done" ? "●" : "○"} ${LABELS[id]}`, { fill: state === "done" ? accent : C.dim }));
|
|
68
|
+
});
|
|
69
|
+
track = row(left, joined(rightOfPhases(model)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const flow = model.workflow;
|
|
73
|
+
const detailLeft = [];
|
|
74
|
+
if (flow) {
|
|
75
|
+
detailLeft.push(span("└", { fill: C.dim }));
|
|
76
|
+
const parts = [];
|
|
77
|
+
if (flow.count) {
|
|
78
|
+
const { done, total, unit } = flow.count;
|
|
79
|
+
const bar = barCells(total > 0 ? done / total : 0, 8).map(({ glyph, color }) => span("▮", { fill: glyph === "█" ? color : C.empty }));
|
|
80
|
+
parts.push([span(unit, { fill: C.dim }), Box({ flexDirection: "row", children: [span(`${done}`, { bold: true }), span(`/${total}`, { fill: C.dim })] }), Box({ flexDirection: "row", children: bar })]);
|
|
81
|
+
}
|
|
82
|
+
if (flow.verdicts) parts.push(flow.verdicts.map(({ name, state }) => span(`${name} ${VERDICT_FILL[state][0]}`, { fill: VERDICT_FILL[state][1] })));
|
|
83
|
+
for (const item of flow.items) parts.push([span(item)]);
|
|
84
|
+
if (flow.next) parts.push([span("next", { fill: C.dim }), span("▸", { fill: accent }), span(flow.next)]);
|
|
85
|
+
parts.forEach((part, index) => detailLeft.push(...(index > 0 ? [span("·", { fill: C.faint })] : []), ...part));
|
|
86
|
+
}
|
|
87
|
+
const detail = row(detailLeft, model.step ? [span("◌", { fill: accent }), span(model.step.name)] : []);
|
|
88
|
+
|
|
89
|
+
let middle;
|
|
90
|
+
if (model.completion) {
|
|
91
|
+
middle = row([span("✦", { fill: C.pink }), span("COMPLETE", { bold: true }), span(`· ${model.completion.detail ?? ""}`, { fill: C.green })]);
|
|
92
|
+
} else if (model.notice) {
|
|
93
|
+
const [bar, title, icon] = TONES[model.notice.tone] ?? TONES.pink;
|
|
94
|
+
middle = row([span("▌", { fill: bar }), span(`${icon} ${model.notice.title}`, { fill: title, bold: true }), ...(model.notice.detail ? [span(`· ${model.notice.detail}`, { fill: C.dim })] : [])]);
|
|
95
|
+
} else if (model.skills.length === 0) {
|
|
96
|
+
if (!model.idle && model.next && !model.workflow?.next) middle = row([span("▸", { fill: accent }), span(model.next)]);
|
|
97
|
+
else if (!model.idle) middle = row([span("no LitFamily skill yet in this request", { fill: C.dim })]);
|
|
98
|
+
else middle = row([span("no LitFamily skill yet · type", { fill: C.dim }), span("lit <task>", { fill: accent }), span("to start one", { fill: C.dim })]);
|
|
99
|
+
} else {
|
|
100
|
+
const shown = model.skills.slice(0, MAX_CHIPS).map((chip) => (chip.state === "current"
|
|
101
|
+
? pill(`⚡ ${chip.id}`, accent)
|
|
102
|
+
: pill(`↯ ${chip.id}`, C.muted, C.mutedText, false)));
|
|
103
|
+
const rest = model.skills.length - MAX_CHIPS;
|
|
104
|
+
middle = row([span("skills", { fill: C.dim }), group(rest > 0 ? [...shown, pill(`+${rest}`, C.muted, C.dim, false)] : shown, "sm")]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const bottomGroups = [];
|
|
108
|
+
const warning = model.warnings[0];
|
|
109
|
+
if (warning) {
|
|
110
|
+
const color = warning.tone === "red" ? C.red : C.amber;
|
|
111
|
+
bottomGroups.push([span(`${warning.tone === "red" ? "✗" : "⚠"} ${warning.subject}`, { fill: color, bold: true }), span(warning.text, { fill: color })]);
|
|
112
|
+
}
|
|
113
|
+
const { context, rates, git } = model.figures;
|
|
114
|
+
if (context !== null) {
|
|
115
|
+
const filled = Math.max(0, Math.min(5, Math.round(context / 20)));
|
|
116
|
+
const bar = Array.from({ length: 5 }, (_, i) => span("▮", { fill: i < filled ? accent : C.empty }));
|
|
117
|
+
bottomGroups.push([span("ctx", { fill: C.dim }), span(`${context}%`, { fill: accent }), Box({ flexDirection: "row", children: bar })]);
|
|
118
|
+
}
|
|
119
|
+
for (const rate of rates) bottomGroups.push([span(rate.label, { fill: C.dim }), span(`${rate.percent}%`, { fill: levelColor(rate.percent) })]);
|
|
120
|
+
if (git) bottomGroups.push([span("git", { fill: C.dim }), span(git, { fill: accent })]);
|
|
121
|
+
const bottom = row(bottomGroups.flatMap((list, index) => (index > 0 ? [span("·", { fill: C.faint }), ...list] : list)));
|
|
122
|
+
|
|
123
|
+
return Box({
|
|
124
|
+
flexDirection: "column",
|
|
125
|
+
gap: "xs",
|
|
126
|
+
paddingX: "md",
|
|
127
|
+
paddingY: "sm",
|
|
128
|
+
borderStyle: "round",
|
|
129
|
+
borderColor: borderColor(model, accent),
|
|
130
|
+
backgroundColor: C.bg,
|
|
131
|
+
children: [top, track, detail, middle, bottom],
|
|
132
|
+
});
|
|
133
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// The LIT card as one SVG drawing, for the surfaces that are
|
|
2
|
-
// app
|
|
3
|
-
// terminal card's text border and its padded pills come apart
|
|
4
|
-
// element instead, and this module draws the same card into
|
|
5
|
-
// the stage indicator on top, the workflow map (or the phase
|
|
6
|
-
// places the session), the detail line with the live step, the
|
|
7
|
-
// finish), and the live figures at the bottom.
|
|
1
|
+
// The LIT card as one SVG drawing, for the surfaces that are neither a terminal grid nor the desktop
|
|
2
|
+
// app (which gets native-card.mjs). They draw a mod's Text in a proportional font and cut each span
|
|
3
|
+
// to the cells it was given, so the terminal card's text border and its padded pills come apart
|
|
4
|
+
// there. Those surfaces carry an `Svg` element instead, and this module draws the same card into
|
|
5
|
+
// it: the brand pill, the discipline and the stage indicator on top, the workflow map (or the phase
|
|
6
|
+
// track and progress when no family places the session), the detail line with the live step, the
|
|
7
|
+
// skill chips (or the notice or the finish), and the live figures at the bottom.
|
|
8
8
|
//
|
|
9
9
|
// It draws the settled state of the model; the terminal's entrances and crossfades stay terminal
|
|
10
10
|
// only. While LitClaude works and motion allows, SMIL animation in the drawing keeps it moving: the
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { textWidth, truncateToWidth } from "./layout.mjs";
|
|
22
22
|
import { TIMINGS, barCells, gradientAt } from "./motion.mjs";
|
|
23
23
|
|
|
24
|
-
const C = Object.freeze({
|
|
24
|
+
export const C = Object.freeze({
|
|
25
25
|
bg: "#0d1117",
|
|
26
26
|
pink: "#ff2d95",
|
|
27
27
|
orange: "#ff6337",
|
|
@@ -41,8 +41,8 @@ const C = Object.freeze({
|
|
|
41
41
|
hudYellow: "#d7af00",
|
|
42
42
|
hudRed: "#d75f5f",
|
|
43
43
|
});
|
|
44
|
-
const LABELS = Object.freeze({ plan: "PLAN", work: "WORK", review: "REVIEW" });
|
|
45
|
-
const PHASES = Object.freeze(["plan", "work", "review"]);
|
|
44
|
+
export const LABELS = Object.freeze({ plan: "PLAN", work: "WORK", review: "REVIEW" });
|
|
45
|
+
export const PHASES = Object.freeze(["plan", "work", "review"]);
|
|
46
46
|
|
|
47
47
|
// Drawing units: CSS pixels at the card's natural size; the surface draws it at that width, up to its slot.
|
|
48
48
|
export const SVG_CARD = Object.freeze({ width: 880, height: 160, font: 13, cell: 7.8, pad: 18 });
|
|
@@ -52,7 +52,7 @@ const FONT = "ui-monospace, 'SF Mono', SFMono-Regular, Menlo, 'JetBrains Mono',
|
|
|
52
52
|
|
|
53
53
|
const esc = (text) => String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
54
54
|
const widthOf = (text) => textWidth(text) * SVG_CARD.cell;
|
|
55
|
-
const levelColor = (percent) => (percent >= 90 ? C.hudRed : percent >= 70 ? C.hudYellow : C.hudGreen);
|
|
55
|
+
export const levelColor = (percent) => (percent >= 90 ? C.hudRed : percent >= 70 ? C.hudYellow : C.hudGreen);
|
|
56
56
|
|
|
57
57
|
// Edge spaces become offsets: an SVG text element drops leading and trailing whitespace.
|
|
58
58
|
const text = (x, y, body, { fill = C.white, bold = false, anchor = "start" } = {}) => {
|
|
@@ -85,7 +85,7 @@ const pill = (x, y, label, { fill, ink, bold = true, sweep = false, breath = nul
|
|
|
85
85
|
return { markup: parts.join(""), end: x + w };
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
const indicatorOf = (model, accent) => {
|
|
88
|
+
export const indicatorOf = (model, accent) => {
|
|
89
89
|
if (model.completion) return { label: "DONE ✓", fill: C.green };
|
|
90
90
|
if (model.idle) return { label: "idle", fill: C.dim, plain: true };
|
|
91
91
|
const flow = model.workflow;
|
|
@@ -109,7 +109,7 @@ const topRow = (model, accent, animate) => {
|
|
|
109
109
|
return brand.markup + title.markup + right;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
const rightOfPhases = (model, withProgress = true) => {
|
|
112
|
+
export const rightOfPhases = (model, withProgress = true) => {
|
|
113
113
|
const pieces = [];
|
|
114
114
|
if (model.ctxWarning) pieces.push([[`ctx ${model.ctxWarning.percent}% → ${model.ctxWarning.threshold}%`, { fill: C.amber, bold: true }]]);
|
|
115
115
|
if (model.lanes.count > 0) pieces.push([[`● ${model.lanes.count} ${model.lanes.count === 1 ? "lane" : "lanes"}`, { fill: C.green }]]);
|
|
@@ -158,7 +158,7 @@ const phaseRow = (model, accent, animate) => {
|
|
|
158
158
|
return out.join("");
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
const mapColor = (accent, position) => gradientAt([C.orange, C.pink, accent], position);
|
|
161
|
+
export const mapColor = (accent, position) => gradientAt([C.orange, C.pink, accent], position);
|
|
162
162
|
const shortLabel = (label) => label.replace(/^[A-Za-z]+/u, (word) => word.slice(0, 3));
|
|
163
163
|
|
|
164
164
|
// Right-aligned groups after `x`, the leftmost dropped first when the row is short of room.
|
|
@@ -218,7 +218,7 @@ const mapRowSvg = (model, accent, animate) => {
|
|
|
218
218
|
return out.join("");
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
-
const VERDICT_FILL = Object.freeze({ pass: ["✓", C.green], fail: ["✗", C.red], pending: ["·", C.dim], running: ["●", C.amber] });
|
|
221
|
+
export const VERDICT_FILL = Object.freeze({ pass: ["✓", C.green], fail: ["✗", C.red], pending: ["·", C.dim], running: ["●", C.amber] });
|
|
222
222
|
|
|
223
223
|
// └ task 3/7 ▮▮▮▯▯▯▯▯ · next ▸ <task> on the left, the spinner and the tool on the right.
|
|
224
224
|
const detailRowSvg = (model, accent, animate) => {
|
|
@@ -276,7 +276,7 @@ const detailRowSvg = (model, accent, animate) => {
|
|
|
276
276
|
return out.join("");
|
|
277
277
|
};
|
|
278
278
|
|
|
279
|
-
const TONES = Object.freeze({ pink: [C.pink, "#ff6fae", "🔥"], amber: [C.amber, C.amber, "⚠"], red: [C.red, C.red, "✗"] });
|
|
279
|
+
export const TONES = Object.freeze({ pink: [C.pink, "#ff6fae", "🔥"], amber: [C.amber, C.amber, "⚠"], red: [C.red, C.red, "✗"] });
|
|
280
280
|
|
|
281
281
|
// Skill chips, or the notice or the finish while one is up, or the hint while nothing has fired.
|
|
282
282
|
const middleRow = (model, accent, animate) => {
|
|
@@ -351,7 +351,7 @@ const bottomRow = (model, accent) => {
|
|
|
351
351
|
return out.join("");
|
|
352
352
|
};
|
|
353
353
|
|
|
354
|
-
const borderColor = (model, accent) => {
|
|
354
|
+
export const borderColor = (model, accent) => {
|
|
355
355
|
if (model.warnings.some(({ tone }) => tone === "red")) return C.red;
|
|
356
356
|
if (model.warnings.length) return C.amber;
|
|
357
357
|
return model.idle ? C.faint : accent;
|