claude-dev-env 1.83.0 → 1.84.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/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +0 -4
- package/hooks/blocking/code_rules_constants_config.py +5 -0
- package/hooks/blocking/code_rules_docstrings.py +116 -6
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +11 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/hooks_constants/CLAUDE.md +0 -3
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -1
- package/rules/plain-illustrative-docstrings.md +35 -1
- package/skills/CLAUDE.md +6 -1
- package/skills/autoconverge/SKILL.md +38 -57
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +70 -73
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
package/skills/doc-gist/SKILL.md
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: doc-gist
|
|
3
|
-
description: >-
|
|
4
|
-
Use when the user asks to share, publish, preview, or open as a webpage any HTML doc,
|
|
5
|
-
writeup, report, plan, runbook, or interactive artifact. Triggers on /doc-gist,
|
|
6
|
-
"publish this", "share as a gist", "open this as a webpage", "make me a writeup", or
|
|
7
|
-
any request ending in a shareable HTML preview URL. Provides the gist_upload transport
|
|
8
|
-
script, the <!-- @publish-as-gist --> auto-publish hook, and an HTML pattern gallery.
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# doc-gist
|
|
12
|
-
|
|
13
|
-
Design fresh HTML for the artifact at hand, mark it for publishing, write it. The rest is automatic — a hook spots the marker, uploads to a secret gist, and prints the htmlpreview URL into your output for the user to click.
|
|
14
|
-
|
|
15
|
-
## Principle (and what this skill deliberately does not ship)
|
|
16
|
-
|
|
17
|
-
This skill ships **transport, not shape**. There are no document templates here. There is no markdown-to-HTML converter. There is no rebase-report mode. The shape of every artifact is your fresh design per request, drawing on the gallery in [`references/examples/`](references/examples/) for inspiration.
|
|
18
|
-
|
|
19
|
-
Per Thariq's html-effectiveness thesis: *"twenty self-contained .html files an agent produced — each one trades a document you'd skim for one you'd actually read."* Every doc-gist invocation produces a fresh design appropriate to the work, not a template fill.
|
|
20
|
-
|
|
21
|
-
## How auto-publish works
|
|
22
|
-
|
|
23
|
-
1. You write HTML to any path (no directory rule, no naming rule).
|
|
24
|
-
2. The HTML contains the marker comment `<!-- @publish-as-gist -->` — typically as the first child of `<head>` or just inside `<body>`.
|
|
25
|
-
3. The PostToolUse hook ([`workflow/doc_gist_auto_publish.py`](../../hooks/workflow/doc_gist_auto_publish.py)) fires after the Write/Edit completes, sees the marker, and runs [`skills/doc-gist/scripts/gist_upload.py`](scripts/gist_upload.py) against the file.
|
|
26
|
-
4. The upload script's gist + preview URLs print into your tool output. Quote them back to the user.
|
|
27
|
-
|
|
28
|
-
The hook is a no-op for any HTML that lacks the marker — React components, test fixtures, scraped pages, partial fragments. The marker is the *intent signal*; absent marker means "this HTML isn't for sharing."
|
|
29
|
-
|
|
30
|
-
## Gotchas
|
|
31
|
-
|
|
32
|
-
- **`gh` must be authenticated.** The upload runs `gh gist create`. If `gh auth status` is failing, the hook surfaces the error to stderr and exits 0 (does not block the write). Run `gh auth login` and re-trigger by editing the HTML once more.
|
|
33
|
-
- **The marker is a literal HTML comment, not a meta tag.** `<!-- @publish-as-gist -->` exactly. `<meta name="publish-as-gist">` does not match. Whitespace inside the marker breaks it.
|
|
34
|
-
- **htmlpreview render delay.** First load of the preview URL takes 5–10 seconds while htmlpreview.github.io fetches the raw gist content. A blank page on first visit means refresh once.
|
|
35
|
-
- **Filenames carry into the gist.** The gist filename is the same as the source file's name. Name your files for the artifact, not for filesystem convenience — `auth-migration-plan.html` reads better in the gist UI than `tmp_plan_v3_final.html`.
|
|
36
|
-
- **Markers in code samples need escaping.** If you embed an example HTML snippet inside `<pre><code>` and that snippet contains the literal marker text, the hook will publish on first save. Either escape the comment angle brackets in the embedded sample, or write the marker as `<!- - @publish-as-gist - ->` in the embedded version.
|
|
37
|
-
- **Self-contained HTML only.** The upload sends a single file. External CSS/JS via `<link href="./style.css">` or `<script src="./app.js">` will fail to load in the htmlpreview view. Inline everything — `<style>`, `<script>`, base64 images, SVG.
|
|
38
|
-
- **Secret gist, not private.** `gh gist create` defaults to "secret" (anyone with the URL can view; not indexed; not on your public profile). Treat the preview URL like a shareable Google Doc — share with intent.
|
|
39
|
-
|
|
40
|
-
## When to include the marker
|
|
41
|
-
|
|
42
|
-
Include `<!-- @publish-as-gist -->` when **the artifact is for sharing or reading**: writeups, plans, reports, explainers, decision records, runbooks, status updates, prototypes the user will look at. Skip the marker for: HTML that's part of a code change (React components, test fixtures), HTML you're authoring as a one-off scratch file you'll delete, embedded HTML samples inside other artifacts.
|
|
43
|
-
|
|
44
|
-
The user's prompt is the strongest signal. *"Make me a writeup of this PR"* → publish. *"Add this React component"* → don't publish.
|
|
45
|
-
|
|
46
|
-
## The transport script — `skills/doc-gist/scripts/gist_upload.py`
|
|
47
|
-
|
|
48
|
-
For manual invocation when the marker route doesn't apply (an existing file you want to publish, HTML piped from another tool, a one-off):
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
python3 skills/doc-gist/scripts/gist_upload.py --input <path-or-stdin>
|
|
52
|
-
[--filename gist-file.html]
|
|
53
|
-
[--description "short label"]
|
|
54
|
-
[--no-open]
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Reads HTML from `--input <path>` or stdin (`--input -`), runs `gh gist create`, prints `Gist:` and `Preview:` URLs to stderr, prints the preview URL to stdout (so callers can pipe), opens the preview in the default browser unless `--no-open`.
|
|
58
|
-
|
|
59
|
-
## Designing fresh — the example gallery
|
|
60
|
-
|
|
61
|
-
The skill ships [`references/examples/`](references/examples/) with 21 html-effectiveness examples: Thariq's 20 prototypes verbatim from [thariqs.github.io/html-effectiveness](https://thariqs.github.io/html-effectiveness/) (`01`–`20`), plus one original addition (`21-decision-signoff.html`). They are *examples to learn from, not templates to fill.*
|
|
62
|
-
|
|
63
|
-
When the user requests an artifact, decide the shape that fits. Use the gallery for grounding:
|
|
64
|
-
|
|
65
|
-
| User wants | Gallery entries to study |
|
|
66
|
-
|---|---|
|
|
67
|
-
| PR writeup with file-by-file tour | `17-pr-writeup.html` |
|
|
68
|
-
| Annotated diff or code review | `03-code-review-pr.html` |
|
|
69
|
-
| Code-explainer with module map | `04-code-understanding.html` |
|
|
70
|
-
| Implementation plan with timeline + risks | `16-implementation-plan.html` |
|
|
71
|
-
| Side-by-side approach exploration | `01-exploration-code-approaches.html` |
|
|
72
|
-
| Visual design comparison | `02-exploration-visual-designs.html` |
|
|
73
|
-
| Design system swatches | `05-design-system.html` |
|
|
74
|
-
| Component variants matrix | `06-component-variants.html` |
|
|
75
|
-
| Animation tuning sandbox with sliders | `07-prototype-animation.html` |
|
|
76
|
-
| Multi-screen interaction mockup | `08-prototype-interaction.html` |
|
|
77
|
-
| Slide deck (keyboard-navigable) | `09-slide-deck.html` |
|
|
78
|
-
| SVG illustration | `10-svg-illustrations.html` |
|
|
79
|
-
| Status report (visual) | `11-status-report.html` |
|
|
80
|
-
| Incident timeline / post-mortem | `12-incident-report.html` |
|
|
81
|
-
| Flowchart / pipeline diagram | `13-flowchart-diagram.html` |
|
|
82
|
-
| Feature explainer with collapsibles | `14-research-feature-explainer.html` |
|
|
83
|
-
| Concept explainer (interactive learning) | `15-research-concept-explainer.html` |
|
|
84
|
-
| Triage / kanban board (drag-drop) | `18-editor-triage-board.html` |
|
|
85
|
-
| Feature flag toggles with deps | `19-editor-feature-flags.html` |
|
|
86
|
-
| Live-updating template editor | `20-editor-prompt-tuner.html` |
|
|
87
|
-
| Decision / sign-off doc (accept-or-change each call, export digest) | `21-decision-signoff.html` |
|
|
88
|
-
|
|
89
|
-
Read the matching example for the artifact you're designing. Crib palette, typography, spatial idioms, component patterns. **Adapt — do not copy.** A PR writeup for a hooks PR shouldn't look identical to one for a notification-queue PR. The gallery teaches what shapes work; the request decides which shape fits.
|
|
90
|
-
|
|
91
|
-
## Folder map
|
|
92
|
-
|
|
93
|
-
- `SKILL.md` — this file.
|
|
94
|
-
- `skills/doc-gist/scripts/gist_upload.py` — transport: HTML in, gist + preview URLs out.
|
|
95
|
-
- `skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py` — the URL prefixes and template strings.
|
|
96
|
-
- `references/examples/` — Thariq's 20 html-effectiveness prototypes (`01`–`20`) plus one original addition (`21-decision-signoff.html`).
|
|
97
|
-
- (PostToolUse hook lives in `packages/claude-dev-env/hooks/workflow/doc_gist_auto_publish.py` — wired into the plugin's `hooks.json`.)
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# doc-gist/references
|
|
2
|
-
|
|
3
|
-
Reference material for the `doc-gist` skill.
|
|
4
|
-
|
|
5
|
-
## Subdirectories
|
|
6
|
-
|
|
7
|
-
| Directory | Role |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `examples/` | A gallery of 21 self-contained HTML artifact examples that show distinct shapes — annotated PR diff, design system swatches, slide deck, incident timeline, prompt tuner, decision sign-off, and more. Study these when designing a fresh artifact; do not copy them verbatim. |
|
|
@@ -1,453 +0,0 @@
|
|
|
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.0">
|
|
6
|
-
<title>Debounced search — three approaches</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root {
|
|
9
|
-
--ivory: #FAF9F5;
|
|
10
|
-
--slate: #141413;
|
|
11
|
-
--clay: #D97757;
|
|
12
|
-
--oat: #E3DACC;
|
|
13
|
-
--olive: #788C5D;
|
|
14
|
-
--gray-150: #F0EEE6;
|
|
15
|
-
--gray-300: #D1CFC5;
|
|
16
|
-
--gray-500: #87867F;
|
|
17
|
-
--gray-700: #3D3D3A;
|
|
18
|
-
--white: #FFFFFF;
|
|
19
|
-
|
|
20
|
-
--serif: ui-serif, Georgia, 'Times New Roman', serif;
|
|
21
|
-
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
22
|
-
--mono: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
26
|
-
|
|
27
|
-
body {
|
|
28
|
-
font-family: var(--sans);
|
|
29
|
-
background: var(--ivory);
|
|
30
|
-
color: var(--gray-700);
|
|
31
|
-
line-height: 1.55;
|
|
32
|
-
padding: 56px 32px 96px;
|
|
33
|
-
-webkit-font-smoothing: antialiased;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.page {
|
|
37
|
-
max-width: 1360px;
|
|
38
|
-
margin: 0 auto;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/* ---------- header ---------- */
|
|
42
|
-
|
|
43
|
-
header.page-head {
|
|
44
|
-
margin-bottom: 48px;
|
|
45
|
-
max-width: 760px;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.eyebrow {
|
|
49
|
-
font-size: 12px;
|
|
50
|
-
letter-spacing: 0.08em;
|
|
51
|
-
text-transform: uppercase;
|
|
52
|
-
color: var(--gray-500);
|
|
53
|
-
margin-bottom: 12px;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
h1 {
|
|
57
|
-
font-family: var(--serif);
|
|
58
|
-
font-weight: 500;
|
|
59
|
-
font-size: 38px;
|
|
60
|
-
line-height: 1.15;
|
|
61
|
-
color: var(--slate);
|
|
62
|
-
margin-bottom: 18px;
|
|
63
|
-
letter-spacing: -0.01em;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.prompt-box {
|
|
67
|
-
background: var(--gray-150);
|
|
68
|
-
border: 1.5px solid var(--gray-300);
|
|
69
|
-
border-radius: 12px;
|
|
70
|
-
padding: 16px 20px;
|
|
71
|
-
font-size: 14.5px;
|
|
72
|
-
color: var(--gray-700);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.prompt-box .label {
|
|
76
|
-
font-family: var(--mono);
|
|
77
|
-
font-size: 11px;
|
|
78
|
-
text-transform: uppercase;
|
|
79
|
-
letter-spacing: 0.06em;
|
|
80
|
-
color: var(--gray-500);
|
|
81
|
-
display: block;
|
|
82
|
-
margin-bottom: 6px;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/* ---------- approach grid ---------- */
|
|
86
|
-
|
|
87
|
-
.approaches {
|
|
88
|
-
display: grid;
|
|
89
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
90
|
-
gap: 28px;
|
|
91
|
-
margin-bottom: 56px;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
@media (max-width: 1100px) {
|
|
95
|
-
.approaches { grid-template-columns: 1fr; }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.approach {
|
|
99
|
-
background: var(--white);
|
|
100
|
-
border: 1.5px solid var(--gray-300);
|
|
101
|
-
border-radius: 12px;
|
|
102
|
-
padding: 24px;
|
|
103
|
-
display: flex;
|
|
104
|
-
flex-direction: column;
|
|
105
|
-
gap: 20px;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.approach-head h2 {
|
|
109
|
-
font-family: var(--serif);
|
|
110
|
-
font-weight: 500;
|
|
111
|
-
font-size: 21px;
|
|
112
|
-
color: var(--slate);
|
|
113
|
-
margin-bottom: 6px;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.approach-head .num {
|
|
117
|
-
display: inline-block;
|
|
118
|
-
font-family: var(--mono);
|
|
119
|
-
font-size: 12px;
|
|
120
|
-
background: var(--oat);
|
|
121
|
-
color: var(--slate);
|
|
122
|
-
padding: 2px 8px;
|
|
123
|
-
border-radius: 8px;
|
|
124
|
-
margin-right: 8px;
|
|
125
|
-
vertical-align: 3px;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.approach-head p {
|
|
129
|
-
font-size: 14px;
|
|
130
|
-
color: var(--gray-500);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/* ---------- code panel ---------- */
|
|
134
|
-
|
|
135
|
-
.code {
|
|
136
|
-
background: var(--slate);
|
|
137
|
-
border-radius: 12px;
|
|
138
|
-
padding: 18px 20px;
|
|
139
|
-
overflow-x: auto;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.code pre {
|
|
143
|
-
font-family: var(--mono);
|
|
144
|
-
font-size: 12.5px;
|
|
145
|
-
line-height: 1.65;
|
|
146
|
-
color: #E8E6DE;
|
|
147
|
-
white-space: pre;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.code .kw { color: var(--clay); } /* keywords */
|
|
151
|
-
.code .str { color: var(--olive); } /* strings */
|
|
152
|
-
.code .cm { color: var(--gray-500); } /* comments */
|
|
153
|
-
.code .fn { color: #C9B98A; } /* identifiers, subtle warm */
|
|
154
|
-
|
|
155
|
-
/* ---------- tradeoffs table ---------- */
|
|
156
|
-
|
|
157
|
-
.tradeoffs {
|
|
158
|
-
border: 1.5px solid var(--gray-300);
|
|
159
|
-
border-radius: 8px;
|
|
160
|
-
overflow: hidden;
|
|
161
|
-
font-size: 13px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.tradeoffs .row {
|
|
165
|
-
display: grid;
|
|
166
|
-
grid-template-columns: 1fr 1fr;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.tradeoffs .row + .row {
|
|
170
|
-
border-top: 1.5px solid var(--gray-300);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.tradeoffs .cell {
|
|
174
|
-
padding: 10px 14px;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.tradeoffs .cell:first-child {
|
|
178
|
-
border-right: 1.5px solid var(--gray-300);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.tradeoffs .head {
|
|
182
|
-
background: var(--gray-150);
|
|
183
|
-
font-weight: 600;
|
|
184
|
-
color: var(--slate);
|
|
185
|
-
font-size: 12px;
|
|
186
|
-
text-transform: uppercase;
|
|
187
|
-
letter-spacing: 0.04em;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.tradeoffs .pro::before,
|
|
191
|
-
.tradeoffs .con::before {
|
|
192
|
-
content: '';
|
|
193
|
-
display: inline-block;
|
|
194
|
-
width: 6px;
|
|
195
|
-
height: 6px;
|
|
196
|
-
border-radius: 50%;
|
|
197
|
-
margin-right: 8px;
|
|
198
|
-
vertical-align: 2px;
|
|
199
|
-
}
|
|
200
|
-
.tradeoffs .pro::before { background: var(--olive); }
|
|
201
|
-
.tradeoffs .con::before { background: var(--clay); }
|
|
202
|
-
|
|
203
|
-
/* ---------- chip footer ---------- */
|
|
204
|
-
|
|
205
|
-
.chips {
|
|
206
|
-
display: flex;
|
|
207
|
-
flex-wrap: wrap;
|
|
208
|
-
gap: 8px;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.chip {
|
|
212
|
-
font-family: var(--mono);
|
|
213
|
-
font-size: 11.5px;
|
|
214
|
-
background: var(--gray-150);
|
|
215
|
-
border: 1.5px solid var(--gray-300);
|
|
216
|
-
color: var(--gray-700);
|
|
217
|
-
padding: 5px 10px;
|
|
218
|
-
border-radius: 8px;
|
|
219
|
-
white-space: nowrap;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
.chip strong { color: var(--slate); font-weight: 600; }
|
|
223
|
-
|
|
224
|
-
/* ---------- recommendation ---------- */
|
|
225
|
-
|
|
226
|
-
.reco {
|
|
227
|
-
border-left: 4px solid var(--clay);
|
|
228
|
-
background: var(--white);
|
|
229
|
-
border-radius: 0 12px 12px 0;
|
|
230
|
-
padding: 24px 28px;
|
|
231
|
-
max-width: 860px;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
.reco h2 {
|
|
235
|
-
font-family: var(--serif);
|
|
236
|
-
font-weight: 500;
|
|
237
|
-
font-size: 22px;
|
|
238
|
-
color: var(--slate);
|
|
239
|
-
margin-bottom: 10px;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.reco p {
|
|
243
|
-
font-size: 15px;
|
|
244
|
-
margin-bottom: 8px;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.reco code {
|
|
248
|
-
font-family: var(--mono);
|
|
249
|
-
font-size: 0.92em;
|
|
250
|
-
background: var(--gray-150);
|
|
251
|
-
padding: 1px 6px;
|
|
252
|
-
border-radius: 4px;
|
|
253
|
-
}
|
|
254
|
-
</style>
|
|
255
|
-
</head>
|
|
256
|
-
<body>
|
|
257
|
-
<div class="page">
|
|
258
|
-
|
|
259
|
-
<header class="page-head">
|
|
260
|
-
<div class="eyebrow">Exploration · Birchline web client</div>
|
|
261
|
-
<h1>Three ways to implement debounced search</h1>
|
|
262
|
-
<div class="prompt-box">
|
|
263
|
-
<span class="label">Prompt</span>
|
|
264
|
-
Show me three different ways to implement debounced search for the task
|
|
265
|
-
filter input in our React codebase, with tradeoffs for each.
|
|
266
|
-
</div>
|
|
267
|
-
</header>
|
|
268
|
-
|
|
269
|
-
<!-- ============================================================= -->
|
|
270
|
-
|
|
271
|
-
<section class="approaches">
|
|
272
|
-
|
|
273
|
-
<!-- ---------- Approach 1 ---------- -->
|
|
274
|
-
<article class="approach">
|
|
275
|
-
<header class="approach-head">
|
|
276
|
-
<h2><span class="num">01</span>Inline useEffect + setTimeout</h2>
|
|
277
|
-
<p>Debounce logic lives directly inside the component that owns the input.</p>
|
|
278
|
-
</header>
|
|
279
|
-
|
|
280
|
-
<div class="code"><pre><span class="kw">export function</span> <span class="fn">TaskSearch</span>() {
|
|
281
|
-
<span class="kw">const</span> [draft, setDraft] = <span class="fn">useState</span>(<span class="str">''</span>);
|
|
282
|
-
<span class="kw">const</span> [query, setQuery] = <span class="fn">useState</span>(<span class="str">''</span>);
|
|
283
|
-
|
|
284
|
-
<span class="fn">useEffect</span>(() <span class="kw">=></span> {
|
|
285
|
-
<span class="kw">const</span> id = <span class="fn">setTimeout</span>(() <span class="kw">=></span> setQuery(draft), <span class="str">300</span>);
|
|
286
|
-
<span class="kw">return</span> () <span class="kw">=></span> <span class="fn">clearTimeout</span>(id);
|
|
287
|
-
}, [draft]);
|
|
288
|
-
|
|
289
|
-
<span class="kw">const</span> { data } = <span class="fn">useTasks</span>({ search: query });
|
|
290
|
-
|
|
291
|
-
<span class="kw">return</span> (
|
|
292
|
-
<<span class="fn">input</span>
|
|
293
|
-
value={draft}
|
|
294
|
-
onChange={(e) <span class="kw">=></span> setDraft(e.target.value)}
|
|
295
|
-
placeholder=<span class="str">"Filter tasks…"</span>
|
|
296
|
-
/>
|
|
297
|
-
);
|
|
298
|
-
}</pre></div>
|
|
299
|
-
|
|
300
|
-
<div class="tradeoffs">
|
|
301
|
-
<div class="row head">
|
|
302
|
-
<div class="cell">Pro</div>
|
|
303
|
-
<div class="cell">Con</div>
|
|
304
|
-
</div>
|
|
305
|
-
<div class="row">
|
|
306
|
-
<div class="cell pro">Zero new abstractions to learn</div>
|
|
307
|
-
<div class="cell con">Logic duplicated everywhere search exists</div>
|
|
308
|
-
</div>
|
|
309
|
-
<div class="row">
|
|
310
|
-
<div class="cell pro">Easy to step through in devtools</div>
|
|
311
|
-
<div class="cell con">Two pieces of state for one conceptual value</div>
|
|
312
|
-
</div>
|
|
313
|
-
<div class="row">
|
|
314
|
-
<div class="cell pro">No dependency or bundle change</div>
|
|
315
|
-
<div class="cell con">Delay constant is buried in component body</div>
|
|
316
|
-
</div>
|
|
317
|
-
</div>
|
|
318
|
-
|
|
319
|
-
<div class="chips">
|
|
320
|
-
<span class="chip">Bundle impact: <strong>+0 kb</strong></span>
|
|
321
|
-
<span class="chip">Testability: <strong>medium</strong></span>
|
|
322
|
-
<span class="chip">Reuse: <strong>low</strong></span>
|
|
323
|
-
<span class="chip">SSR safe: <strong>yes</strong></span>
|
|
324
|
-
</div>
|
|
325
|
-
</article>
|
|
326
|
-
|
|
327
|
-
<!-- ---------- Approach 2 ---------- -->
|
|
328
|
-
<article class="approach">
|
|
329
|
-
<header class="approach-head">
|
|
330
|
-
<h2><span class="num">02</span>Custom useDebounce hook</h2>
|
|
331
|
-
<p>Extract the timer into a shared hook under <code style="font-family:var(--mono)">src/hooks/</code>.</p>
|
|
332
|
-
</header>
|
|
333
|
-
|
|
334
|
-
<div class="code"><pre><span class="cm">// src/hooks/useDebounce.ts</span>
|
|
335
|
-
<span class="kw">export function</span> <span class="fn">useDebounce</span><T>(value: T, ms = <span class="str">300</span>): T {
|
|
336
|
-
<span class="kw">const</span> [debounced, setDebounced] = <span class="fn">useState</span>(value);
|
|
337
|
-
<span class="fn">useEffect</span>(() <span class="kw">=></span> {
|
|
338
|
-
<span class="kw">const</span> id = <span class="fn">setTimeout</span>(() <span class="kw">=></span> setDebounced(value), ms);
|
|
339
|
-
<span class="kw">return</span> () <span class="kw">=></span> <span class="fn">clearTimeout</span>(id);
|
|
340
|
-
}, [value, ms]);
|
|
341
|
-
<span class="kw">return</span> debounced;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
<span class="cm">// TaskSearch.tsx</span>
|
|
345
|
-
<span class="kw">const</span> [draft, setDraft] = <span class="fn">useState</span>(<span class="str">''</span>);
|
|
346
|
-
<span class="kw">const</span> query = <span class="fn">useDebounce</span>(draft, <span class="str">300</span>);
|
|
347
|
-
<span class="kw">const</span> { data } = <span class="fn">useTasks</span>({ search: query });</pre></div>
|
|
348
|
-
|
|
349
|
-
<div class="tradeoffs">
|
|
350
|
-
<div class="row head">
|
|
351
|
-
<div class="cell">Pro</div>
|
|
352
|
-
<div class="cell">Con</div>
|
|
353
|
-
</div>
|
|
354
|
-
<div class="row">
|
|
355
|
-
<div class="cell pro">Single import reused across filter, command bar, board search</div>
|
|
356
|
-
<div class="cell con">One more file to maintain and document</div>
|
|
357
|
-
</div>
|
|
358
|
-
<div class="row">
|
|
359
|
-
<div class="cell pro">Trivial to unit test with fake timers</div>
|
|
360
|
-
<div class="cell con">Generic <code style="font-family:var(--mono)">T</code> hides intent slightly</div>
|
|
361
|
-
</div>
|
|
362
|
-
<div class="row">
|
|
363
|
-
<div class="cell pro">Delay is a visible, tunable argument</div>
|
|
364
|
-
<div class="cell con">Still re-renders on every keystroke</div>
|
|
365
|
-
</div>
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
<div class="chips">
|
|
369
|
-
<span class="chip">Bundle impact: <strong>+0.2 kb</strong></span>
|
|
370
|
-
<span class="chip">Testability: <strong>high</strong></span>
|
|
371
|
-
<span class="chip">Reuse: <strong>high</strong></span>
|
|
372
|
-
<span class="chip">SSR safe: <strong>yes</strong></span>
|
|
373
|
-
</div>
|
|
374
|
-
</article>
|
|
375
|
-
|
|
376
|
-
<!-- ---------- Approach 3 ---------- -->
|
|
377
|
-
<article class="approach">
|
|
378
|
-
<header class="approach-head">
|
|
379
|
-
<h2><span class="num">03</span>Tiny external library</h2>
|
|
380
|
-
<p>Adopt <code style="font-family:var(--mono)">use-debounce</code> for both values and callbacks.</p>
|
|
381
|
-
</header>
|
|
382
|
-
|
|
383
|
-
<div class="code"><pre><span class="kw">import</span> { useDebouncedCallback }
|
|
384
|
-
<span class="kw">from</span> <span class="str">'use-debounce'</span>;
|
|
385
|
-
|
|
386
|
-
<span class="kw">export function</span> <span class="fn">TaskSearch</span>() {
|
|
387
|
-
<span class="kw">const</span> [query, setQuery] = <span class="fn">useState</span>(<span class="str">''</span>);
|
|
388
|
-
|
|
389
|
-
<span class="kw">const</span> onChange = <span class="fn">useDebouncedCallback</span>(
|
|
390
|
-
(next: <span class="kw">string</span>) <span class="kw">=></span> setQuery(next),
|
|
391
|
-
<span class="str">300</span>,
|
|
392
|
-
{ leading: <span class="kw">false</span>, maxWait: <span class="str">1000</span> },
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
<span class="kw">const</span> { data } = <span class="fn">useTasks</span>({ search: query });
|
|
396
|
-
|
|
397
|
-
<span class="kw">return</span> (
|
|
398
|
-
<<span class="fn">input</span>
|
|
399
|
-
defaultValue=<span class="str">""</span>
|
|
400
|
-
onChange={(e) <span class="kw">=></span> onChange(e.target.value)}
|
|
401
|
-
/>
|
|
402
|
-
);
|
|
403
|
-
}</pre></div>
|
|
404
|
-
|
|
405
|
-
<div class="tradeoffs">
|
|
406
|
-
<div class="row head">
|
|
407
|
-
<div class="cell">Pro</div>
|
|
408
|
-
<div class="cell">Con</div>
|
|
409
|
-
</div>
|
|
410
|
-
<div class="row">
|
|
411
|
-
<div class="cell pro">leading / trailing / maxWait handled for us</div>
|
|
412
|
-
<div class="cell con">New runtime dependency to audit and update</div>
|
|
413
|
-
</div>
|
|
414
|
-
<div class="row">
|
|
415
|
-
<div class="cell pro">Callback form skips intermediate re-renders</div>
|
|
416
|
-
<div class="cell con">Uncontrolled input diverges from Birchline form patterns</div>
|
|
417
|
-
</div>
|
|
418
|
-
<div class="row">
|
|
419
|
-
<div class="cell pro">Well-tested edge cases (unmount, flush, cancel)</div>
|
|
420
|
-
<div class="cell con">~1.4 kb gzipped for something we could own</div>
|
|
421
|
-
</div>
|
|
422
|
-
</div>
|
|
423
|
-
|
|
424
|
-
<div class="chips">
|
|
425
|
-
<span class="chip">Bundle impact: <strong>+1.4 kb</strong></span>
|
|
426
|
-
<span class="chip">Testability: <strong>high</strong></span>
|
|
427
|
-
<span class="chip">Reuse: <strong>high</strong></span>
|
|
428
|
-
<span class="chip">SSR safe: <strong>yes</strong></span>
|
|
429
|
-
</div>
|
|
430
|
-
</article>
|
|
431
|
-
|
|
432
|
-
</section>
|
|
433
|
-
|
|
434
|
-
<!-- ============================================================= -->
|
|
435
|
-
|
|
436
|
-
<aside class="reco">
|
|
437
|
-
<h2>Recommendation</h2>
|
|
438
|
-
<p>
|
|
439
|
-
Go with <strong>approach 02, the custom <code>useDebounce</code> hook</strong>.
|
|
440
|
-
Birchline already has three places that hand-roll the inline pattern
|
|
441
|
-
(task filter, command palette, member picker), so extracting one
|
|
442
|
-
shared hook removes duplication without taking on a new dependency.
|
|
443
|
-
</p>
|
|
444
|
-
<p>
|
|
445
|
-
Revisit approach 03 only if we later need <code>maxWait</code> or
|
|
446
|
-
<code>flush()</code> semantics — the library earns its bundle cost
|
|
447
|
-
once the requirements outgrow a ten-line hook.
|
|
448
|
-
</p>
|
|
449
|
-
</aside>
|
|
450
|
-
|
|
451
|
-
</div>
|
|
452
|
-
</body>
|
|
453
|
-
</html>
|