synorch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/application/doctor-service.d.ts +24 -0
- package/dist/application/doctor-service.d.ts.map +1 -0
- package/dist/application/doctor-service.js +508 -0
- package/dist/application/doctor-service.js.map +1 -0
- package/dist/application/project-discovery.d.ts +23 -0
- package/dist/application/project-discovery.d.ts.map +1 -0
- package/dist/application/project-discovery.js +741 -0
- package/dist/application/project-discovery.js.map +1 -0
- package/dist/application/skill-resolver.d.ts +25 -0
- package/dist/application/skill-resolver.d.ts.map +1 -0
- package/dist/application/skill-resolver.js +90 -0
- package/dist/application/skill-resolver.js.map +1 -0
- package/dist/application/structure-service.d.ts +11 -0
- package/dist/application/structure-service.d.ts.map +1 -0
- package/dist/application/structure-service.js +116 -0
- package/dist/application/structure-service.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +170 -0
- package/dist/cli.js.map +1 -0
- package/dist/domain/config.d.ts +210 -0
- package/dist/domain/config.d.ts.map +1 -0
- package/dist/domain/config.js +108 -0
- package/dist/domain/config.js.map +1 -0
- package/dist/domain/errors.d.ts +5 -0
- package/dist/domain/errors.d.ts.map +1 -0
- package/dist/domain/errors.js +9 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/generation.d.ts +21 -0
- package/dist/domain/generation.d.ts.map +1 -0
- package/dist/domain/generation.js +2 -0
- package/dist/domain/generation.js.map +1 -0
- package/dist/domain/product.d.ts +3 -0
- package/dist/domain/product.d.ts.map +1 -0
- package/dist/domain/product.js +3 -0
- package/dist/domain/product.js.map +1 -0
- package/dist/domain/skill-packs.d.ts +39 -0
- package/dist/domain/skill-packs.d.ts.map +1 -0
- package/dist/domain/skill-packs.js +98 -0
- package/dist/domain/skill-packs.js.map +1 -0
- package/dist/domain/skill-sources.d.ts +20 -0
- package/dist/domain/skill-sources.d.ts.map +1 -0
- package/dist/domain/skill-sources.js +95 -0
- package/dist/domain/skill-sources.js.map +1 -0
- package/dist/infrastructure/bundled-skill-library.d.ts +10 -0
- package/dist/infrastructure/bundled-skill-library.d.ts.map +1 -0
- package/dist/infrastructure/bundled-skill-library.js +108 -0
- package/dist/infrastructure/bundled-skill-library.js.map +1 -0
- package/dist/infrastructure/file-system.d.ts +21 -0
- package/dist/infrastructure/file-system.d.ts.map +1 -0
- package/dist/infrastructure/file-system.js +72 -0
- package/dist/infrastructure/file-system.js.map +1 -0
- package/dist/infrastructure/serialization.d.ts +3 -0
- package/dist/infrastructure/serialization.d.ts.map +1 -0
- package/dist/infrastructure/serialization.js +11 -0
- package/dist/infrastructure/serialization.js.map +1 -0
- package/dist/templates/structure-templates.d.ts +4 -0
- package/dist/templates/structure-templates.d.ts.map +1 -0
- package/dist/templates/structure-templates.js +471 -0
- package/dist/templates/structure-templates.js.map +1 -0
- package/dist/templates/technology-skill-templates.d.ts +3 -0
- package/dist/templates/technology-skill-templates.d.ts.map +1 -0
- package/dist/templates/technology-skill-templates.js +73 -0
- package/dist/templates/technology-skill-templates.js.map +1 -0
- package/package.json +59 -0
- package/skill-sources/ingenium/NOTICE.md +11 -0
- package/skill-sources/ingenium/skills/db-schema-craft/SKILL.md +127 -0
- package/skill-sources/ingenium/skills/debug-detective/SKILL.md +67 -0
- package/skill-sources/ingenium/skills/design-system/SKILL.md +57 -0
- package/skill-sources/ingenium/skills/docs-sync/SKILL.md +68 -0
- package/skill-sources/ingenium/skills/dotnet-backend/SKILL.md +110 -0
- package/skill-sources/ingenium/skills/frontend-craft/SKILL.md +69 -0
- package/skill-sources/ingenium/skills/game-audio/SKILL.md +73 -0
- package/skill-sources/ingenium/skills/game-design/SKILL.md +92 -0
- package/skill-sources/ingenium/skills/godot-dev/SKILL.md +78 -0
- package/skill-sources/ingenium/skills/human-made-design/SKILL.md +73 -0
- package/skill-sources/ingenium/skills/java-backend/SKILL.md +98 -0
- package/skill-sources/ingenium/skills/jev/SKILL.md +150 -0
- package/skill-sources/ingenium/skills/motion-craft/SKILL.md +66 -0
- package/skill-sources/ingenium/skills/multiplayer-netcode/SKILL.md +63 -0
- package/skill-sources/ingenium/skills/node-backend/SKILL.md +113 -0
- package/skill-sources/ingenium/skills/node-backend/reference.md +144 -0
- package/skill-sources/ingenium/skills/perf-audit/SKILL.md +70 -0
- package/skill-sources/ingenium/skills/pixel-art-assets/SKILL.md +91 -0
- package/skill-sources/ingenium/skills/pixel-art-assets/scripts/px.py +169 -0
- package/skill-sources/ingenium/skills/pixel-game-dev/SKILL.md +79 -0
- package/skill-sources/ingenium/skills/project-onboard/SKILL.md +78 -0
- package/skill-sources/ingenium/skills/pwa-offline/SKILL.md +70 -0
- package/skill-sources/ingenium/skills/query-tuning/SKILL.md +137 -0
- package/skill-sources/ingenium/skills/react-modern/SKILL.md +92 -0
- package/skill-sources/ingenium/skills/refactor-safe/SKILL.md +63 -0
- package/skill-sources/ingenium/skills/release-prep/SKILL.md +57 -0
- package/skill-sources/ingenium/skills/safe-merge/SKILL.md +91 -0
- package/skill-sources/ingenium/skills/session-recap/SKILL.md +95 -0
- package/skill-sources/ingenium/skills/session-recap/scripts/extract_session.py +409 -0
- package/skill-sources/ingenium/skills/shader-vfx/SKILL.md +70 -0
- package/skill-sources/ingenium/skills/tailwind-v4-tokens/SKILL.md +165 -0
- package/skill-sources/ingenium/skills/task-conductor/SKILL.md +158 -0
- package/skill-sources/ingenium/skills/tauri-game-dev/SKILL.md +81 -0
- package/skill-sources/ingenium/skills/ui-ux-design/SKILL.md +96 -0
- package/skill-sources/ingenium/skills/vue-modern/SKILL.md +88 -0
- package/skill-sources/ingenium/skills/web-kickoff/SKILL.md +63 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Distill Claude Code session transcripts into a resumable digest.
|
|
3
|
+
|
|
4
|
+
Reads ~/.claude/projects/<encoded-cwd>/*.jsonl without loading transcripts into
|
|
5
|
+
the model's context. Stdlib only.
|
|
6
|
+
|
|
7
|
+
python extract_session.py --list
|
|
8
|
+
python extract_session.py --session latest
|
|
9
|
+
python extract_session.py --session <session-id> --json
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from collections import Counter, OrderedDict
|
|
18
|
+
|
|
19
|
+
EDIT_TOOLS = ("Edit", "Write", "NotebookEdit", "MultiEdit")
|
|
20
|
+
SHELL_TOOLS = ("Bash", "PowerShell")
|
|
21
|
+
RESEARCH_TOOLS = ("WebSearch", "WebFetch")
|
|
22
|
+
INTERRUPT_MARK = "[Request interrupted by user"
|
|
23
|
+
NOISE_PREFIXES = ("<local-command", "<command-name>", "<command-message>")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def projects_root():
|
|
27
|
+
return os.path.join(os.path.expanduser("~"), ".claude", "projects")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def encode_cwd(path):
|
|
31
|
+
return re.sub(r"[^a-zA-Z0-9]", "-", path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_project_dir(cwd):
|
|
35
|
+
root = projects_root()
|
|
36
|
+
direct = os.path.join(root, encode_cwd(os.path.abspath(cwd)))
|
|
37
|
+
if os.path.isdir(direct):
|
|
38
|
+
return direct
|
|
39
|
+
target = os.path.abspath(cwd).rstrip("\\/").lower()
|
|
40
|
+
if not os.path.isdir(root):
|
|
41
|
+
return None
|
|
42
|
+
for name in os.listdir(root):
|
|
43
|
+
cand = os.path.join(root, name)
|
|
44
|
+
if not os.path.isdir(cand):
|
|
45
|
+
continue
|
|
46
|
+
for f in transcript_files(cand)[:1]:
|
|
47
|
+
for rec in read_records(f, limit=40):
|
|
48
|
+
rc = rec.get("cwd")
|
|
49
|
+
if rc and rc.rstrip("\\/").lower() == target:
|
|
50
|
+
return cand
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def transcript_files(project_dir):
|
|
55
|
+
"""All transcripts, newest first."""
|
|
56
|
+
files = [
|
|
57
|
+
os.path.join(project_dir, n)
|
|
58
|
+
for n in os.listdir(project_dir)
|
|
59
|
+
if n.endswith(".jsonl")
|
|
60
|
+
]
|
|
61
|
+
return sorted(files, key=os.path.getmtime, reverse=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def seconds_idle(path):
|
|
65
|
+
import time
|
|
66
|
+
|
|
67
|
+
return time.time() - os.path.getmtime(path)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def read_records(path, limit=None):
|
|
71
|
+
out = []
|
|
72
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
73
|
+
for line in fh:
|
|
74
|
+
line = line.strip()
|
|
75
|
+
if not line:
|
|
76
|
+
continue
|
|
77
|
+
try:
|
|
78
|
+
out.append(json.loads(line))
|
|
79
|
+
except ValueError:
|
|
80
|
+
continue
|
|
81
|
+
if limit and len(out) >= limit:
|
|
82
|
+
break
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def blocks(rec):
|
|
87
|
+
msg = rec.get("message")
|
|
88
|
+
if not isinstance(msg, dict):
|
|
89
|
+
return []
|
|
90
|
+
content = msg.get("content")
|
|
91
|
+
if isinstance(content, list):
|
|
92
|
+
return [b for b in content if isinstance(b, dict)]
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def text_of(rec):
|
|
97
|
+
msg = rec.get("message")
|
|
98
|
+
if not isinstance(msg, dict):
|
|
99
|
+
return ""
|
|
100
|
+
content = msg.get("content")
|
|
101
|
+
if isinstance(content, str):
|
|
102
|
+
return content
|
|
103
|
+
return " ".join(b.get("text", "") for b in blocks(rec) if b.get("type") == "text")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_real_prompt(rec):
|
|
107
|
+
if rec.get("type") != "user" or rec.get("isMeta") or rec.get("isSidechain"):
|
|
108
|
+
return False
|
|
109
|
+
if rec.get("promptSource") not in ("typed", "queued", None):
|
|
110
|
+
return False
|
|
111
|
+
txt = text_of(rec).strip()
|
|
112
|
+
if not txt or txt.startswith(NOISE_PREFIXES):
|
|
113
|
+
return False
|
|
114
|
+
return rec.get("promptSource") in ("typed", "queued")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def squeeze(text, limit):
|
|
118
|
+
text = " ".join(text.split())
|
|
119
|
+
return text if len(text) <= limit else text[: limit - 1] + "…"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def rel(path, cwd):
|
|
123
|
+
if not path:
|
|
124
|
+
return path
|
|
125
|
+
norm = path.replace("/", os.sep).replace("\\", os.sep)
|
|
126
|
+
base = os.path.abspath(cwd).replace("/", os.sep).replace("\\", os.sep)
|
|
127
|
+
if norm.lower().startswith(base.lower()):
|
|
128
|
+
norm = norm[len(base) :].lstrip("\\/")
|
|
129
|
+
return norm.replace("\\", "/")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def summarize(path, cwd, prompt_chars=400, cap=25):
|
|
133
|
+
recs = read_records(path)
|
|
134
|
+
stamps = [r["timestamp"] for r in recs if r.get("timestamp")]
|
|
135
|
+
title = next(
|
|
136
|
+
(r["aiTitle"] for r in reversed(recs) if r.get("type") == "ai-title" and r.get("aiTitle")),
|
|
137
|
+
None,
|
|
138
|
+
)
|
|
139
|
+
session_cwd = next((r["cwd"] for r in recs if r.get("cwd")), cwd)
|
|
140
|
+
branches = OrderedDict(
|
|
141
|
+
(r["gitBranch"], None) for r in recs if r.get("gitBranch")
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
prompts, edits, shell, research, skills, agents = [], Counter(), [], [], [], []
|
|
145
|
+
errors, denials, interrupts = [], [], 0
|
|
146
|
+
last_assistant = ""
|
|
147
|
+
tool_names = {}
|
|
148
|
+
|
|
149
|
+
def tool_of(rec):
|
|
150
|
+
"""Resolve which tool a tool_result block belongs to."""
|
|
151
|
+
for blk in blocks(rec):
|
|
152
|
+
tid = blk.get("tool_use_id")
|
|
153
|
+
if tid and tid in tool_names:
|
|
154
|
+
return tool_names[tid]
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
for rec in recs:
|
|
158
|
+
if is_real_prompt(rec):
|
|
159
|
+
txt = text_of(rec)
|
|
160
|
+
if INTERRUPT_MARK in txt:
|
|
161
|
+
interrupts += 1
|
|
162
|
+
prompts.append(squeeze(txt, prompt_chars))
|
|
163
|
+
if rec.get("toolDenialKind"):
|
|
164
|
+
denials.append(
|
|
165
|
+
"%s (%s)" % (tool_of(rec) or "unknown tool", rec["toolDenialKind"])
|
|
166
|
+
)
|
|
167
|
+
if rec.get("interruptedMessageId"):
|
|
168
|
+
interrupts += 1
|
|
169
|
+
if rec.get("type") == "file-history-delta" and rec.get("trackingPath"):
|
|
170
|
+
edits[rel(rec["trackingPath"], session_cwd)] += 0
|
|
171
|
+
|
|
172
|
+
msg = rec.get("message") or {}
|
|
173
|
+
role = msg.get("role") if isinstance(msg, dict) else None
|
|
174
|
+
if role == "assistant" and not rec.get("isSidechain"):
|
|
175
|
+
t = text_of(rec).strip()
|
|
176
|
+
if t:
|
|
177
|
+
last_assistant = t
|
|
178
|
+
for blk in blocks(rec):
|
|
179
|
+
kind = blk.get("type")
|
|
180
|
+
if kind == "tool_result" and blk.get("is_error"):
|
|
181
|
+
body = blk.get("content")
|
|
182
|
+
if isinstance(body, list):
|
|
183
|
+
body = " ".join(
|
|
184
|
+
b.get("text", "") for b in body if isinstance(b, dict)
|
|
185
|
+
)
|
|
186
|
+
who = tool_names.get(blk.get("tool_use_id")) or "tool"
|
|
187
|
+
errors.append("%s: %s" % (who, squeeze(str(body), 180)))
|
|
188
|
+
if kind != "tool_use":
|
|
189
|
+
continue
|
|
190
|
+
name, inp = blk.get("name"), blk.get("input") or {}
|
|
191
|
+
if blk.get("id"):
|
|
192
|
+
tool_names[blk["id"]] = name
|
|
193
|
+
if name in EDIT_TOOLS:
|
|
194
|
+
fp = inp.get("file_path") or inp.get("notebook_path")
|
|
195
|
+
if fp:
|
|
196
|
+
edits[rel(fp, session_cwd)] += 1
|
|
197
|
+
elif name in SHELL_TOOLS:
|
|
198
|
+
cmd = squeeze(str(inp.get("command", "")), 160)
|
|
199
|
+
if cmd:
|
|
200
|
+
shell.append(cmd)
|
|
201
|
+
elif name in RESEARCH_TOOLS:
|
|
202
|
+
research.append(
|
|
203
|
+
"%s: %s" % (name, squeeze(str(inp.get("query") or inp.get("url") or ""), 160))
|
|
204
|
+
)
|
|
205
|
+
elif name == "Skill":
|
|
206
|
+
if inp.get("skill"):
|
|
207
|
+
skills.append(inp["skill"])
|
|
208
|
+
elif name == "Agent":
|
|
209
|
+
agents.append(squeeze(str(inp.get("description") or ""), 90))
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
"session_id": os.path.basename(path)[:-6],
|
|
213
|
+
"file": path,
|
|
214
|
+
"title": title,
|
|
215
|
+
"cwd": session_cwd,
|
|
216
|
+
"started": stamps[0] if stamps else None,
|
|
217
|
+
"ended": stamps[-1] if stamps else None,
|
|
218
|
+
"branches": list(branches),
|
|
219
|
+
"prompts": prompts,
|
|
220
|
+
"files": [
|
|
221
|
+
{"path": p, "edits": n} for p, n in sorted(edits.items(), key=lambda kv: -kv[1])
|
|
222
|
+
],
|
|
223
|
+
"commands": dedupe(shell)[:cap],
|
|
224
|
+
"research": dedupe(research)[:cap],
|
|
225
|
+
"skills": dedupe(skills),
|
|
226
|
+
"agents": dedupe(agents)[:cap],
|
|
227
|
+
"errors": dedupe(errors)[:cap],
|
|
228
|
+
"denials": denials,
|
|
229
|
+
"interrupts": interrupts,
|
|
230
|
+
"last_assistant": squeeze(last_assistant, 700),
|
|
231
|
+
"record_count": len(recs),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def dedupe(items):
|
|
236
|
+
return list(OrderedDict((i, None) for i in items))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def brief(path, cwd):
|
|
240
|
+
"""Cheap listing entry."""
|
|
241
|
+
d = summarize(path, cwd, prompt_chars=110, cap=1)
|
|
242
|
+
return {
|
|
243
|
+
"session_id": d["session_id"],
|
|
244
|
+
"title": d["title"],
|
|
245
|
+
"started": d["started"],
|
|
246
|
+
"ended": d["ended"],
|
|
247
|
+
"prompts": len(d["prompts"]),
|
|
248
|
+
"files": len(d["files"]),
|
|
249
|
+
"first_prompt": d["prompts"][0] if d["prompts"] else "",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def render(d):
|
|
254
|
+
L = ["# Session digest — %s" % (d["title"] or d["session_id"])]
|
|
255
|
+
L.append("")
|
|
256
|
+
L.append("- session: `%s`" % d["session_id"])
|
|
257
|
+
L.append("- window: %s → %s" % (d["started"], d["ended"]))
|
|
258
|
+
L.append("- cwd: `%s`" % d["cwd"])
|
|
259
|
+
if d["branches"]:
|
|
260
|
+
L.append("- branch(es): %s" % ", ".join("`%s`" % b for b in d["branches"]))
|
|
261
|
+
L.append("- records: %d" % d["record_count"])
|
|
262
|
+
|
|
263
|
+
def block(head, items, fmt=lambda x: "- %s" % x):
|
|
264
|
+
if not items:
|
|
265
|
+
return
|
|
266
|
+
L.append("")
|
|
267
|
+
L.append("## %s" % head)
|
|
268
|
+
L.extend(fmt(i) for i in items)
|
|
269
|
+
|
|
270
|
+
block("User prompts (verbatim intent, in order)", list(enumerate(d["prompts"], 1)),
|
|
271
|
+
lambda t: "%d. %s" % t)
|
|
272
|
+
block("Files touched", d["files"],
|
|
273
|
+
lambda f: "- `%s`%s" % (f["path"], "" if not f["edits"] else " (%d edit calls)" % f["edits"]))
|
|
274
|
+
block("Research", d["research"])
|
|
275
|
+
block("Skills loaded", d["skills"])
|
|
276
|
+
block("Subagents", d["agents"])
|
|
277
|
+
block("Commands run", d["commands"])
|
|
278
|
+
block("Tool errors (dead-end candidates)", d["errors"])
|
|
279
|
+
if d["denials"] or d["interrupts"]:
|
|
280
|
+
L.append("")
|
|
281
|
+
L.append("## Friction signals")
|
|
282
|
+
if d["denials"]:
|
|
283
|
+
L.append("- denied tool calls: %s" % ", ".join(d["denials"]))
|
|
284
|
+
if d["interrupts"]:
|
|
285
|
+
L.append("- user interruptions: %d" % d["interrupts"])
|
|
286
|
+
if d["last_assistant"]:
|
|
287
|
+
L.append("")
|
|
288
|
+
L.append("## Final assistant state")
|
|
289
|
+
L.append(d["last_assistant"])
|
|
290
|
+
return "\n".join(L)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def force_utf8():
|
|
294
|
+
for stream in (sys.stdout, sys.stderr):
|
|
295
|
+
try:
|
|
296
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
297
|
+
except (AttributeError, ValueError):
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def main():
|
|
302
|
+
force_utf8()
|
|
303
|
+
ap = argparse.ArgumentParser()
|
|
304
|
+
ap.add_argument("--cwd", default=os.getcwd())
|
|
305
|
+
ap.add_argument("--list", action="store_true")
|
|
306
|
+
ap.add_argument("--session", help="'latest' or a session id")
|
|
307
|
+
ap.add_argument("--limit", type=int, default=10)
|
|
308
|
+
ap.add_argument("--prompt-chars", type=int, default=400)
|
|
309
|
+
ap.add_argument("--json", action="store_true")
|
|
310
|
+
ap.add_argument(
|
|
311
|
+
"--exclude-session",
|
|
312
|
+
action="append",
|
|
313
|
+
default=[],
|
|
314
|
+
help="session id to leave out of 'latest' selection (repeatable) — use for the live session",
|
|
315
|
+
)
|
|
316
|
+
ap.add_argument(
|
|
317
|
+
"--active-window",
|
|
318
|
+
type=int,
|
|
319
|
+
default=60,
|
|
320
|
+
help="seconds of idleness below which a transcript is flagged as possibly live (default 60)",
|
|
321
|
+
)
|
|
322
|
+
ap.add_argument(
|
|
323
|
+
"--all",
|
|
324
|
+
action="store_true",
|
|
325
|
+
help="keep sessions with no typed prompt (headless -p runs, aborted starts)",
|
|
326
|
+
)
|
|
327
|
+
args = ap.parse_args()
|
|
328
|
+
|
|
329
|
+
pdir = resolve_project_dir(args.cwd)
|
|
330
|
+
if not pdir:
|
|
331
|
+
print("No transcript directory for cwd: %s" % args.cwd, file=sys.stderr)
|
|
332
|
+
print("Looked under: %s" % projects_root(), file=sys.stderr)
|
|
333
|
+
return 2
|
|
334
|
+
files = transcript_files(pdir)
|
|
335
|
+
if not files:
|
|
336
|
+
print("No transcripts in %s" % pdir, file=sys.stderr)
|
|
337
|
+
return 2
|
|
338
|
+
|
|
339
|
+
# An explicit id always wins: no emptiness, liveness or limit filtering.
|
|
340
|
+
if args.session and args.session != "latest":
|
|
341
|
+
target = next(
|
|
342
|
+
(f for f in files if os.path.basename(f).startswith(args.session)), None
|
|
343
|
+
)
|
|
344
|
+
if not target:
|
|
345
|
+
print("Session not found: %s" % args.session, file=sys.stderr)
|
|
346
|
+
print("Known ids: %s" % ", ".join(os.path.basename(f)[:8] for f in files[:10]), file=sys.stderr)
|
|
347
|
+
return 2
|
|
348
|
+
d = summarize(target, args.cwd, prompt_chars=args.prompt_chars)
|
|
349
|
+
print(json.dumps(d, indent=2, ensure_ascii=False) if args.json else render(d))
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
rows, kept, skipped = [], [], 0
|
|
353
|
+
for f in files:
|
|
354
|
+
r = brief(f, args.cwd)
|
|
355
|
+
if not args.all and r["prompts"] == 0:
|
|
356
|
+
skipped += 1
|
|
357
|
+
continue
|
|
358
|
+
idle = seconds_idle(f)
|
|
359
|
+
r["idle_seconds"] = int(idle)
|
|
360
|
+
r["maybe_live"] = idle < args.active_window
|
|
361
|
+
r["excluded"] = any(r["session_id"].startswith(x) for x in args.exclude_session)
|
|
362
|
+
rows.append(r)
|
|
363
|
+
kept.append(f)
|
|
364
|
+
if len(rows) >= args.limit:
|
|
365
|
+
break
|
|
366
|
+
if not kept:
|
|
367
|
+
print("No session with a typed prompt in %s (--all to inspect them)" % pdir, file=sys.stderr)
|
|
368
|
+
return 2
|
|
369
|
+
|
|
370
|
+
eligible = [
|
|
371
|
+
(r, f) for r, f in zip(rows, kept) if not r["excluded"] and not r["maybe_live"]
|
|
372
|
+
] or [(r, f) for r, f in zip(rows, kept) if not r["excluded"]]
|
|
373
|
+
|
|
374
|
+
if args.list or not args.session:
|
|
375
|
+
if args.json:
|
|
376
|
+
print(json.dumps(
|
|
377
|
+
{"project_dir": pdir, "sessions": rows, "skipped_empty": skipped,
|
|
378
|
+
"suggested": eligible[0][0]["session_id"] if eligible else None},
|
|
379
|
+
indent=2, ensure_ascii=False))
|
|
380
|
+
else:
|
|
381
|
+
print("# Recent sessions — %s\n" % pdir)
|
|
382
|
+
for i, r in enumerate(rows, 1):
|
|
383
|
+
flags = []
|
|
384
|
+
if r["maybe_live"]:
|
|
385
|
+
flags.append("POSSIBLY THE SESSION YOU ARE IN — written %ds ago" % r["idle_seconds"])
|
|
386
|
+
if r["excluded"]:
|
|
387
|
+
flags.append("excluded")
|
|
388
|
+
print("%d. **%s**%s" % (i, r["title"] or "(untitled)",
|
|
389
|
+
" ⟨%s⟩" % "; ".join(flags) if flags else ""))
|
|
390
|
+
print(" id `%s` | %s → %s" % (r["session_id"], r["started"], r["ended"]))
|
|
391
|
+
print(" %d prompts, %d files | opened with: %s" % (r["prompts"], r["files"], r["first_prompt"]))
|
|
392
|
+
if skipped:
|
|
393
|
+
print("\n_(%d session(s) with no typed prompt hidden — headless or aborted runs; --all to show)_" % skipped)
|
|
394
|
+
if eligible:
|
|
395
|
+
print("\nSuggested: `%s`. Confirm it is not this conversation before summarizing." % eligible[0][0]["session_id"])
|
|
396
|
+
return 0
|
|
397
|
+
|
|
398
|
+
if not eligible:
|
|
399
|
+
print("Every candidate is excluded or possibly live; pass --session <id> explicitly.", file=sys.stderr)
|
|
400
|
+
return 2
|
|
401
|
+
target = eligible[0][1]
|
|
402
|
+
d = summarize(target, args.cwd, prompt_chars=args.prompt_chars)
|
|
403
|
+
print(json.dumps(d, indent=2, ensure_ascii=False) if args.json else render(d))
|
|
404
|
+
return 0
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
sys.exit(main())
|
|
409
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shader-vfx
|
|
3
|
+
description: Shaders and visual effects for web and games - fragment/vertex shader mental model, GLSL toolkit (SDFs, smoothstep, noise, fbm), ready recipes for game VFX (hit flash, dissolve, outline, water, shockwave, palette swap, CRT), pixel-art-safe effects, post-processing chains, per-environment usage (three.js, PixiJS filters, Phaser pipelines, Godot shading language) and shader debugging/performance. Use when writing or fixing shaders, adding visual effects like glow, dissolve, distortion or outlines, doing post-processing, or when an effect needs GPU work. Türkçe tetikleyiciler - "shader yaz", "efekt ekle", "glow efekti", "dissolve efekti", "outline shader", "su efekti", "ekran efekti", "crt efekti", "godot shader", "glsl".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Shader VFX
|
|
7
|
+
|
|
8
|
+
You write shaders that serve the game's look and run fast. Shaders are per-pixel programs running massively parallel: think "what color is *this* pixel, given its coordinates, time and textures" — not loops over the image.
|
|
9
|
+
|
|
10
|
+
Always communicate with the user in their own language.
|
|
11
|
+
|
|
12
|
+
## Mental model (2 minutes, saves hours)
|
|
13
|
+
|
|
14
|
+
- **Vertex shader** positions geometry; **fragment shader** colors each pixel. 95% of 2D game VFX is fragment work.
|
|
15
|
+
- Coordinates: work in **UV space** (0–1 across the texture/quad). Center-origin trick: `vec2 p = uv - 0.5;` (aspect-correct with `p.x *= aspect;`).
|
|
16
|
+
- Everything is a gradient: you compute values (distances, noise, masks) and map them to color with `mix` and `smoothstep`. There is no "if pixel is inside shape" — there is "how far is this pixel from the shape edge".
|
|
17
|
+
- Inputs come as **uniforms** (time, resolution, effect strength, textures); per-frame animation = a `time` uniform, not new shaders.
|
|
18
|
+
|
|
19
|
+
## The core toolkit (memorize these five)
|
|
20
|
+
|
|
21
|
+
1. `smoothstep(a, b, x)` — THE shaping function: soft thresholds, anti-aliased edges (`smoothstep(0.0, 2.0/resolution.y, d)`).
|
|
22
|
+
2. `mix(a, b, t)` — blend anything: colors, positions, whole effects.
|
|
23
|
+
3. **SDF shapes**: `length(p) - r` (circle), rect/segment SDFs; combine with `min` (union) / `max` (intersect); distance drives glow, outlines, soft shadows.
|
|
24
|
+
4. `fract`/`mod` — repetition: tiles, stripes, scanlines (`fract(uv.y * 100.0)`).
|
|
25
|
+
5. **Noise**: cheap hash → value noise → **fbm** (4–5 octaves of noise at doubling frequency, halving amplitude) for anything organic — fire, water, smoke, dissolve. Include a known-good hash/noise snippet rather than inventing one.
|
|
26
|
+
|
|
27
|
+
## Recipe book (game VFX that ship)
|
|
28
|
+
|
|
29
|
+
- **Hit flash**: `color = mix(color, vec3(1.0), u_flash)` — drive `u_flash` from gameplay (spike to 1, decay over ~80ms).
|
|
30
|
+
- **Dissolve**: `if (noise(uv * scale) < u_threshold) discard;` + emissive edge where `noise - threshold < 0.05`. Animate threshold 0→1 to disintegrate.
|
|
31
|
+
- **Outline (sprite)**: sample alpha at 4–8 neighbor offsets; outside pixels with an opaque neighbor get outline color. Pixel-art: 4 offsets of exactly one texel.
|
|
32
|
+
- **Water**: scroll two noise layers at different speeds/scales, use them to distort sample UVs of the scene below + tint + specular band via smoothstep.
|
|
33
|
+
- **Shockwave**: radial UV displacement — `uv += dir * sin((dist - u_t) * freq) * falloff(dist, u_t)` expanding ring.
|
|
34
|
+
- **Heat haze / refraction**: small animated-noise UV offsets on the background sample.
|
|
35
|
+
- **Palette swap / LUT grading**: map luminance (or index) through a lookup texture — day/night, damage states, retro palettes for free.
|
|
36
|
+
- **CRT/retro post**: scanlines (sine on screen y), slight barrel distortion, chromatic aberration (offset R/B samples), vignette. Subtlety: each at 10–20% of what first looks cool.
|
|
37
|
+
- **Glow/bloom**: threshold bright pixels → downscale-blur passes → additive composite. Use the engine's bloom before hand-rolling.
|
|
38
|
+
|
|
39
|
+
## Pixel-art-safe VFX
|
|
40
|
+
|
|
41
|
+
- Snap effect UVs to the texel grid (`floor(uv * texSize) / texSize`) or effects smear across the chunky pixels and break the look.
|
|
42
|
+
- Render effects at the game's native low resolution, then integer-upscale — post applied after upscale reads as "HD filter on retro game".
|
|
43
|
+
- Prefer **dither patterns** over smooth gradients (Bayer-matrix threshold) to stay on-palette; palette-swap via LUT beats hue-shifting.
|
|
44
|
+
|
|
45
|
+
## Where shaders live per environment
|
|
46
|
+
|
|
47
|
+
- **three.js**: `ShaderMaterial`/`onBeforeCompile`; post via `EffectComposer`.
|
|
48
|
+
- **PixiJS**: `Filter` (fragment + uniforms) per-sprite or per-container.
|
|
49
|
+
- **Phaser**: PostFX/PreFX pipelines (built-ins: bloom, glow, blur) or custom `PostFXPipeline` — check built-ins first.
|
|
50
|
+
- **Godot**: `.gdshader`, GLSL-like with `shader_type canvas_item;` for 2D; `hint_range` uniforms editable in Inspector; screen-reading via `SCREEN_TEXTURE`/`hint_screen_texture`. Same recipes translate almost 1:1.
|
|
51
|
+
- **WebGPU/WGSL**: different syntax, same mental model; compute shaders unlock particles/sim — port there when the target supports it (see tauri-game-dev webview notes for desktop caveats).
|
|
52
|
+
|
|
53
|
+
## Post-processing chains
|
|
54
|
+
|
|
55
|
+
Render scene → render target A → effect pass A→B → effect pass B→A (**ping-pong**) → screen. Order matters and is a look decision: grade → bloom → grain reads different from bloom → grade. Keep a debug toggle per pass.
|
|
56
|
+
|
|
57
|
+
## Performance
|
|
58
|
+
|
|
59
|
+
- Cost = pixels covered × work per pixel. Fullscreen passes are the budget hogs — count them; half-resolution for blurry effects (blur/bloom/haze) is visually free, 4× cheaper.
|
|
60
|
+
- Texture fetches dominate: 8 neighbor samples per pixel is fine; 64 is a blur done wrong (separable blur: two 1D passes beat one 2D pass).
|
|
61
|
+
- `mediump` default precision on mobile; branches on *uniforms* are fine, per-pixel divergent branches less so — but measure before contorting code.
|
|
62
|
+
- Overdraw from stacked transparent quads (particles) kills mobile — fewer, bigger, smarter particles.
|
|
63
|
+
- Profile with SpectorJS (WebGL frame inspector) or the engine's GPU timers; never optimize a shader you haven't measured.
|
|
64
|
+
|
|
65
|
+
## Debugging (there is no console.log)
|
|
66
|
+
|
|
67
|
+
- **Output the value as color**: `gl_FragColor = vec4(vec3(suspectValue), 1.0);` — the image *is* the debugger. Expect NaN to render black/weird: check divisions and `normalize(vec2(0))`.
|
|
68
|
+
- Isolate: comment passes back to a solid color, re-add until it breaks.
|
|
69
|
+
- Prototype on Shadertoy/The Book of Shaders playgrounds (instant iteration), then port — mind their uniform naming (`iTime` → your `u_time`).
|
|
70
|
+
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tailwind-v4-tokens
|
|
3
|
+
description: Tailwind CSS v4 token architecture and its silent failure modes - the three-layer setup (raw palette to theme-scoped semantic tokens to an @theme inline handoff), why @theme inline is mandatory for values that change at runtime, why hand-written CSS classes never get hover/focus variants and fail without a build error, @utility and @variant for real custom utilities, custom breakpoints via --breakpoint-*, opacity modifiers on custom tokens, and CSS-first config differences from v3. Carries executable diagnostics - grep patterns that find variant-on-custom-class bugs and escaped-selector verification in the compiled CSS. Use when setting up or migrating Tailwind 4 theming, when hover/focus/dark styles silently do nothing, when a class sits in the markup but has no effect, or when wiring CSS variables into Tailwind. Türkçe tetikleyiciler - "tailwind 4'e geç", "hover çalışmıyor", "sınıf uygulanmıyor", "tema token sistemi kur", "dark mode altyapısı", "css değişkenini tailwind'e bağla", "stil neden gitmiyor".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tailwind v4 Tokens
|
|
7
|
+
|
|
8
|
+
Tailwind v4 fails **silently**. A class it does not own produces no CSS, no warning and no non-zero exit — the build is green and the pixels are missing. This skill owns that mechanic: which classes Tailwind can generate, how to hand CSS variables over so theming survives, and how to prove a class actually compiled. Token *taste* (which semantic roles exist, contrast in both themes) belongs to the design-system skill.
|
|
9
|
+
|
|
10
|
+
Always communicate with the user in their own language.
|
|
11
|
+
|
|
12
|
+
## Phase 1 — Diagnose an existing project
|
|
13
|
+
|
|
14
|
+
The failure has one shape: a class is **hand-written CSS**, but used **with a variant**. Tailwind generates variants only for utilities it owns — its own, `@theme`-derived, or `@utility`-registered. `hover:bg-theme-card` where `.bg-theme-card` is your own rule compiles to nothing.
|
|
15
|
+
|
|
16
|
+
**Why no error:** the compiler scans source files for candidate strings and emits CSS for recognized ones. An unrecognized candidate is indistinguishable from any ordinary class name (`card`, `swiper-slide`, a third-party class) — erroring would break every project on earth. Class attributes are opaque strings; nothing type-checks them. Missing pixels are the only signal.
|
|
17
|
+
|
|
18
|
+
**Find variant-on-custom-class bugs** — every class defined in your CSS, cross-checked against variant usage in source:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
rg -oNI -g '*.css' '^\.([a-z0-9-]+)' src | sed 's/^\.//' | sort -u | while read -r c; do
|
|
22
|
+
rg -qI "(hover|focus|focus-within|focus-visible|active|disabled|group-hover|peer-focus|dark|xs|sm|md|lg|xl|2xl):$c\b" src \
|
|
23
|
+
&& echo "DEAD: variant used on hand-written class -> $c"
|
|
24
|
+
done
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It flags *variant* usage only — a hand-written class used bare (`class="bg-theme-card"`) still works as ordinary CSS, so that one is a naming problem, not a broken-pixel one.
|
|
28
|
+
|
|
29
|
+
**Find invented breakpoints** — v4 ships `sm md lg xl 2xl` only; anything else must be a `--breakpoint-*` token, or the chain silently loses a step and two branches of a `hidden xs:inline sm:hidden` sequence show at once:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
rg -oI '\b(xs|sm|md|lg|xl|2xl|3xl):' src | sort -u # variants in use
|
|
33
|
+
rg -n -g '*.css' -- '--breakpoint-' src # variants actually defined
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Prove a class compiled** — build, then find the escaped selector in the output CSS. Ground truth, not a guess. The output selector is `.hover\:bg-hover:hover`; match the backslash with a wildcard `.` instead of escaping it — a backslash eaten by a shell quoting layer turns a working class into a false zero, and a lying verifier is worse than none:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run build
|
|
40
|
+
rg -c 'hover.:bg-hover|focus-within.:border-accent' dist/assets/*.css # no output = not generated
|
|
41
|
+
rg -o -- '--color-[a-z-]+' dist/assets/*.css | sort -u # tokens Tailwind knows
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Calibrate it once against a class you know exists and once against an invented name that must return nothing.
|
|
45
|
+
|
|
46
|
+
## Phase 2 — `@theme` vs `@theme inline`
|
|
47
|
+
|
|
48
|
+
The single most consequential decision. `@theme` emits the token as a variable in `:root` and points utilities at *that* variable; `@theme inline` substitutes the referenced value straight into the utility. Both compile, so the difference only shows up as a theme toggle that does nothing.
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
/* WRONG — one indirection too many */
|
|
52
|
+
@theme {
|
|
53
|
+
--color-surface: var(--bg-surface);
|
|
54
|
+
}
|
|
55
|
+
/* emits :root { --color-surface: var(--bg-surface) }
|
|
56
|
+
.bg-surface { background-color: var(--color-surface) }
|
|
57
|
+
--bg-surface is resolved on the root element and inherited as a finished value. */
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```css
|
|
61
|
+
/* RIGHT */
|
|
62
|
+
@theme inline {
|
|
63
|
+
--color-surface: var(--bg-surface);
|
|
64
|
+
}
|
|
65
|
+
/* emits .bg-surface { background-color: var(--bg-surface) }
|
|
66
|
+
resolved on the element itself, so the nearest .light/.dark ancestor wins. */
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Be precise about when the wrong version bites, or you will "disprove" it by accident: with `.dark` on `<html>` (the root element) a global toggle appears to work, because root is where the resolution happens. It breaks when the theme class sits anywhere else (`<body>`, an app wrapper) and it breaks for **nested** theme scopes — a light-themed panel inside a dark page — even when the class is on `<html>`. The inline version is correct in all three cases, which is why it is the default here.
|
|
70
|
+
|
|
71
|
+
**Rule:** literal value → `@theme`. A `var()` whose value is redefined in any scope (`.dark`, `[data-theme]`, a subtree) → `@theme inline`.
|
|
72
|
+
|
|
73
|
+
## Phase 3 — Three-layer token architecture (copyable skeleton)
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
@import "tailwindcss";
|
|
77
|
+
|
|
78
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
79
|
+
|
|
80
|
+
/* Layer 1 — raw palette. Theme-independent, never referenced by components. */
|
|
81
|
+
:root {
|
|
82
|
+
--gray-50: oklch(0.985 0 0); --gray-200: oklch(0.92 0.004 260);
|
|
83
|
+
--gray-400: oklch(0.71 0.012 260); --gray-800: oklch(0.28 0.014 260);
|
|
84
|
+
--gray-950: oklch(0.15 0.012 260);
|
|
85
|
+
--brand-400: oklch(0.70 0.16 255); --brand-500: oklch(0.62 0.19 255);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Layer 2 — semantic roles, redefined per theme. The only layer that knows themes. */
|
|
89
|
+
.light {
|
|
90
|
+
--bg-base: var(--gray-50); --bg-surface: #fff; --bg-hover: var(--gray-200);
|
|
91
|
+
--fg-base: var(--gray-950); --fg-muted: var(--gray-400);
|
|
92
|
+
--line: var(--gray-200); --accent: var(--brand-500);
|
|
93
|
+
}
|
|
94
|
+
.dark {
|
|
95
|
+
--bg-base: var(--gray-950); --bg-surface: var(--gray-800);
|
|
96
|
+
--bg-hover: oklch(0.33 0.014 260);
|
|
97
|
+
--fg-base: var(--gray-50); --fg-muted: var(--gray-400);
|
|
98
|
+
--line: var(--gray-800); --accent: var(--brand-400);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Layer 3 — handoff. Runtime-swapped values MUST be inline. */
|
|
102
|
+
@theme inline {
|
|
103
|
+
--color-base: var(--bg-base);
|
|
104
|
+
--color-surface: var(--bg-surface);
|
|
105
|
+
--color-hover: var(--bg-hover);
|
|
106
|
+
--color-fg: var(--fg-base);
|
|
107
|
+
--color-muted: var(--fg-muted);
|
|
108
|
+
--color-line: var(--line);
|
|
109
|
+
--color-accent: var(--accent);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Static scales — no runtime swap, so plain @theme. */
|
|
113
|
+
@theme {
|
|
114
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
115
|
+
--radius-card: 0.75rem;
|
|
116
|
+
--text-caption: 0.8125rem; --text-caption--line-height: 1.4;
|
|
117
|
+
--shadow-card-lifted: 0 12px 32px oklch(0 0 0 / 0.16);
|
|
118
|
+
--breakpoint-xs: 30rem;
|
|
119
|
+
--animate-fade-in: fade-in 200ms ease-out;
|
|
120
|
+
@keyframes fade-in { from { opacity: 0 } to { opacity: 1 } }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
What this buys: `bg-surface`, `text-muted`, `border-line`, `hover:bg-hover`, `focus-within:border-accent`, `hover:shadow-card-lifted`, `xs:inline`, `animate-fade-in` all compile natively, with every variant, because Tailwind owns them. Note that `dark:` variants become largely unnecessary for color — the swap happens one layer below. Keep `@custom-variant dark` for the exceptions only (an inverted image filter, a different shadow strategy).
|
|
125
|
+
|
|
126
|
+
## Phase 4 — v4 mechanics worth knowing
|
|
127
|
+
|
|
128
|
+
- **Own utilities via `@utility`** — the fix for `transition-smooth` / `interactive-scale` style helpers. Registered utilities get variants; `.class {}` rules do not. `@variant` applies a Tailwind variant inside custom CSS:
|
|
129
|
+
|
|
130
|
+
```css
|
|
131
|
+
@utility transition-smooth {
|
|
132
|
+
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
133
|
+
}
|
|
134
|
+
@utility interactive-scale {
|
|
135
|
+
transition: transform 150ms ease-out;
|
|
136
|
+
@variant hover { transform: scale(1.02); }
|
|
137
|
+
@variant active { transform: scale(0.98); }
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
Verify with the Phase 1 escaped-selector grep — `rg -c 'md.:interactive-scale' dist/assets/*.css` must be non-zero.
|
|
141
|
+
|
|
142
|
+
- **Opacity modifiers work on custom tokens**: `bg-surface/60` compiles to `color-mix(in oklab, var(--bg-surface) 60%, transparent)`, behind a `@supports` guard with the flat color as fallback. It requires the token to resolve to a *color* — a var holding a gradient or a shadow yields nothing usable.
|
|
143
|
+
|
|
144
|
+
- **Custom breakpoints** are tokens: `--breakpoint-xs: 30rem` in `@theme` creates the `xs:` variant; without the token the prefix is simply dropped.
|
|
145
|
+
|
|
146
|
+
| v3 | v4 |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `tailwind.config.js` | CSS-first `@theme` (legacy opt-in: `@config "./tailwind.config.js"`) |
|
|
149
|
+
| `@tailwind base/components/utilities` | `@import "tailwindcss"` |
|
|
150
|
+
| `content: [...]` globs | automatic source detection |
|
|
151
|
+
| `theme.extend.colors` | `--color-*` in `@theme` / `@theme inline` |
|
|
152
|
+
| `@layer utilities { .foo {} }` | `@utility foo { }` |
|
|
153
|
+
| `darkMode: 'class'` | `@custom-variant dark (&:where(.dark, .dark *))` |
|
|
154
|
+
|
|
155
|
+
## Phase 5 — Migration order (each step has a done-check)
|
|
156
|
+
|
|
157
|
+
1. **Tokens first.** Build the three layers; keep the old classes alive alongside so nothing breaks mid-flight. *Done-check:* toggling `.dark` on the root element changes computed colors in DevTools, and `--color-*` names appear in the built CSS.
|
|
158
|
+
2. **Variant audit.** Replace every variant-prefixed custom class with a token-derived utility; register genuine helpers with `@utility`; define missing breakpoints. *Done-check:* the Phase 1 loop prints nothing, and every variant class in the diff appears as an escaped selector in `dist`.
|
|
159
|
+
3. **Dead class sweep.** Delete the hand-written color classes and any never-defined utilities. *Done-check:* `rg -w '<class-name>' src` returns zero hits per removed class; CSS bundle size drops.
|
|
160
|
+
4. **Contrast verification** in both themes — hand off to the design-system skill (4.5:1 body, 3:1 large text and UI); a token swap that compiles can still be unreadable.
|
|
161
|
+
|
|
162
|
+
## Anti-patterns
|
|
163
|
+
|
|
164
|
+
Hand-writing `.my-color { color: var(--x) }` for anything Tailwind could generate from a token; defining the same token in both `@theme` and `:root` (two sources of truth, whichever loses is the confusing one); `@theme` for a value redefined under `.dark`; using variants on classes you wrote yourself; relying on a utility that was never defined anywhere; inventing a breakpoint prefix without a `--breakpoint-*` token; hoarding tokens no component consumes; patching a cascade fight with `!important` instead of finding which layer owns the property.
|
|
165
|
+
|