feat(web): pull the four new design-system primitives into the app (HT-93) - #103
Conversation
…T-93) The Claude Design "Helpthread" project gained four net-new primitives on 2026-07-19, authored in response to a request from app work. They were never pulled down. This adds them to web/src/components/ds/core/: - SplitButton — primary action with an attached caret menu - CommandMenu — searchable saved-replies inserter - SnoozePicker — presets plus a custom calendar and time - CredentialRow / PasskeyList — passkey management, rename and two-step revoke Shared icon glyphs, the focus-ring token, and the date formatters live in primitives-support.jsx rather than being duplicated per component — the design source carried one copy in a single file, and splitting that file must not turn one definition into four. Conversion from the design source was mechanical and every style value is preserved: the source is browser-rendered, so it used React.createElement, module.exports, and local duplicate copies of MenuItem/Button. Those become JSX, ESM exports, and real imports from ./MenuItem and ./Button. The Showcase, its layout scaffolding, and the specimen fixtures are dropped — they are design-project demo code, not app code. One intentional behavioral difference, commented at the site: the design source froze a reference clock (NOW = 2026-07-19 14:30) so specimen times would not drift between renders. The app uses real time via now(); a frozen clock would make SnoozePicker compute "tomorrow" from a past date. Five lint findings are suppressed with reasons rather than auto-fixed, because Biome's fixes would each be silent behavior drift from the design: two noAutofocus (both fields appear only in response to an explicit user action), two noArrayIndexKey (repeating weekday initials in a fixed row, and month-padding cells that have no identity), and one useExhaustiveDependencies (q is the effect's trigger, not a read). Verified: biome check exit 0, tsc exit 0, next build exit 0. None of the existing 16 ds components were modified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds shared design-system primitives and declarations alongside four interactive React components: a searchable command menu, passkey credential controls, a snooze picker, and a split action button. ChangesDesign system components
Estimated code review effort: 4 (Complex) | ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
web/src/components/ds/core/primitives-support.d.ts (1)
2-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPrefer
React.JSX.Elementover the deprecated globalJSX.Elementnamespace.React 19 removed the global
JSXnamespace in favor ofReact.JSX. While@types/react@19still ships a backwards-compatible global for now, it is deprecated and will be removed in a future release. Since this file doesn't importReact, add a type-only import and update the return types.♻️ Proposed refactor
+import type { JSX } from 'react' + export declare const RING: string -export declare function chevron(dir?: 'down' | 'up' | 'left' | 'right', sz?: number): JSX.Element -export declare function IconKey(sz?: number): JSX.Element -export declare function IconSearch(sz?: number): JSX.Element -export declare function IconReply(sz?: number): JSX.Element -export declare function IconClock(sz?: number): JSX.Element -export declare function IconPlus(sz?: number): JSX.Element -export declare function IconPencil(sz?: number): JSX.Element -export declare function IconTrash(sz?: number): JSX.Element +export declare function chevron(dir?: 'down' | 'up' | 'left' | 'right', sz?: number): JSX.Element +export declare function IconKey(sz?: number): JSX.Element +export declare function IconSearch(sz?: number): JSX.Element +export declare function IconReply(sz?: number): JSX.Element +export declare function IconClock(sz?: number): JSX.Element +export declare function IconPlus(sz?: number): JSX.Element +export declare function IconPencil(sz?: number): JSX.Element +export declare function IconTrash(sz?: number): JSX.ElementWith
import type { JSX } from 'react', theJSX.Elementreferences resolve toReact.JSX.Elementwithout needing the global namespace.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/ds/core/primitives-support.d.ts` around lines 2 - 9, Add a type-only React import in the declarations file and update the return types of chevron, IconKey, IconSearch, IconReply, IconClock, IconPlus, IconPencil, and IconTrash to use the imported React JSX element type instead of the deprecated global JSX.Element.web/src/components/ds/core/SplitButton.jsx (1)
99-122: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCaret toggle lacks
aria-expanded/aria-haspopup.The caret button controls the dropdown (
openstate) but doesn't exposearia-haspopup="menu"/aria-expanded={open}for assistive tech. Since ds/core files must remain verbatim copies of the design source, please confirm whether the prototype already includes these attributes; if not, this should be added upstream first rather than patched locally.As per coding guidelines, "Design-system files under
web/src/components/ds/must remain verbatim copies of the Claude Design prototype/design-system source; improvements must be made upstream in the design project first."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/ds/core/SplitButton.jsx` around lines 99 - 122, Verify whether the upstream design-system prototype’s caret button includes aria-haspopup="menu" and aria-expanded={open}; if present, synchronize this SplitButton caret button with the prototype. If absent, make no local change and instead flag the upstream design source for adding these attributes before updating the verbatim copy.Source: Coding guidelines
web/src/components/ds/core/CredentialRow.jsx (1)
290-290: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
key={c.name}assumes credential names are unique.
Credentialhas no id field, and renaming is user-controlled, so two credentials could end up sharing a name, causing React key collisions/incorrect reconciliation. Consider a stableidonCredentialfor the list key.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/ds/core/CredentialRow.jsx` at line 290, Update the credential list rendering around CredentialRow to use a stable, unique Credential identifier for the React key instead of c.name. Add or propagate an id field on Credential as needed, and pass that identifier to key while preserving the existing row props and ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/src/components/ds/core/CommandMenu.d.ts`:
- Line 15: Update the CommandMenu declaration to import the JSX type namespace
from React and retain JSX.Element as the return type of CommandMenu, removing
its reliance on the ambient global JSX namespace.
In `@web/src/components/ds/core/CommandMenu.jsx`:
- Around line 47-49: Update the ArrowDown handling in the CommandMenu highlight
state update to clamp the highlighted index at zero when filtered is empty,
while preserving the existing upper bound for non-empty results. Ensure hi
remains non-negative so later-arriving items can be highlighted and selected.
In `@web/src/components/ds/core/CredentialRow.jsx`:
- Around line 230-291: Update PasskeyList to accept onRename and onRevoke and
forward both callbacks to every rendered CredentialRow, preserving the
credential and new name arguments used by CredentialRow. Add the corresponding
optional callback declarations to PasskeyListProps in CredentialRow.d.ts.
In `@web/src/components/ds/core/SnoozePicker.jsx`:
- Around line 169-174: Update the resolved useMemo in SnoozePicker to validate
that time is non-empty and contains valid hour/minute values before calling
setHours; return a safe non-submittable state when invalid. Ensure the
confirmation display does not use an invalid Date, and disable or guard the
submission path so onSnooze cannot receive an invalid resolved value.
- Around line 5-8: Update laterToday() so it never returns a past timestamp:
after 5 PM, either hide the Later today preset or roll its timestamp forward to
the next valid slot, while preserving the existing 5 PM behavior before that
cutoff.
---
Nitpick comments:
In `@web/src/components/ds/core/CredentialRow.jsx`:
- Line 290: Update the credential list rendering around CredentialRow to use a
stable, unique Credential identifier for the React key instead of c.name. Add or
propagate an id field on Credential as needed, and pass that identifier to key
while preserving the existing row props and ordering.
In `@web/src/components/ds/core/primitives-support.d.ts`:
- Around line 2-9: Add a type-only React import in the declarations file and
update the return types of chevron, IconKey, IconSearch, IconReply, IconClock,
IconPlus, IconPencil, and IconTrash to use the imported React JSX element type
instead of the deprecated global JSX.Element.
In `@web/src/components/ds/core/SplitButton.jsx`:
- Around line 99-122: Verify whether the upstream design-system prototype’s
caret button includes aria-haspopup="menu" and aria-expanded={open}; if present,
synchronize this SplitButton caret button with the prototype. If absent, make no
local change and instead flag the upstream design source for adding these
attributes before updating the verbatim copy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b30c6b5f-14f6-40b1-a7be-a6a7afa78542
📒 Files selected for processing (10)
web/src/components/ds/core/CommandMenu.d.tsweb/src/components/ds/core/CommandMenu.jsxweb/src/components/ds/core/CredentialRow.d.tsweb/src/components/ds/core/CredentialRow.jsxweb/src/components/ds/core/SnoozePicker.d.tsweb/src/components/ds/core/SnoozePicker.jsxweb/src/components/ds/core/SplitButton.d.tsweb/src/components/ds/core/SplitButton.jsxweb/src/components/ds/core/primitives-support.d.tsweb/src/components/ds/core/primitives-support.jsx
| width?: number | ||
| initialQuery?: string | ||
| } | ||
| export declare function CommandMenu(props: CommandMenuProps): JSX.Element |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does @types/react19.2.4 expose a global JSX namespace, or should library declaration files import JSX fromreactand useJSX.Element?
💡 Result:
No, @types/react 19.2.4 does not expose a global JSX namespace [1][2]. The global JSX namespace was removed in @types/react version 19 to prevent type pollution and conflicts with other UI libraries that also utilize JSX [3][4][1]. For library declaration files and general TypeScript code, you should import the JSX namespace directly from React and use React.JSX.Element instead of the previously global JSX.Element [1][5]. You can access it by importing: import { JSX } from 'react'; // Use as const myElement: JSX.Element =
Citations:
- 1: @types/react - Cannot find namespace 'JSX' DefinitelyTyped/DefinitelyTyped#74625
- 2: https://github.com/facebook/react/blob/ee0855f427832e899767f7659c5289364218ab9e/CHANGELOG.md
- 3: [react] Types for React 19 DefinitelyTyped/DefinitelyTyped#69022
- 4: https://react.dev/blog/2024/04/25/react-19-upgrade-guide
- 5: [react] Add JSX namespace to React namespace DefinitelyTyped/DefinitelyTyped#64464
- 6: [@types/react] does not exist on type 'JSX.IntrinsicElements'.ts after upgrade to react 19 and @types/react 19.0.1 DefinitelyTyped/DefinitelyTyped#71395
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='web/src/components/ds/core/CommandMenu.d.ts'
echo '--- file outline ---'
wc -l "$file"
echo
cat -n "$file"
echo
echo '--- search for JSX imports/usages in nearby ds core declarations ---'
rg -n --hidden --glob 'web/src/components/ds/core/*.{d.ts,ts,tsx,js,jsx}' 'from '\''react'\''|namespace JSX|JSX\.Element|import type \{ JSX \}' .Repository: Helpthread/helpthread
Length of output: 4929
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- package.json ---'
if [ -f package.json ]; then
cat -n package.json | sed -n '1,240p'
fi
echo
echo '--- tsconfig files mentioning jsx or types ---'
for f in $(git ls-files '*tsconfig*.json' 'package.json' 'pnpm-lock.yaml' 'yarn.lock' 'package-lock.json' 2>/dev/null); do
case "$f" in
*tsconfig*.json|package.json)
echo "### $f"
cat -n "$f" | sed -n '1,240p'
echo
;;
esac
done
echo '--- any JSX namespace declarations in repo ---'
rg -n --hidden --glob '!**/node_modules/**' 'declare\s+namespace\s+JSX|namespace\s+JSX|interface\s+IntrinsicElements' .Repository: Helpthread/helpthread
Length of output: 4990
Reference React’s scoped JSX namespace.
web/src/components/ds/core/CommandMenu.d.ts:15 should not depend on the ambient global JSX namespace. Import type { JSX } from 'react' and keep the return type as JSX.Element so this public declaration stays compatible with React 19 consumers.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/components/ds/core/CommandMenu.d.ts` at line 15, Update the
CommandMenu declaration to import the JSX type namespace from React and retain
JSX.Element as the return type of CommandMenu, removing its reliance on the
ambient global JSX namespace.
| if (e.key === 'ArrowDown') { | ||
| e.preventDefault() | ||
| setHi((i) => Math.min(i + 1, filtered.length - 1)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the highlighted index non-negative for empty results.
Line 49 sets hi to -1 when filtered is empty. If items later arrive without changing q, the menu has no highlighted row and Enter cannot select one.
Proposed fix
- setHi((i) => Math.min(i + 1, filtered.length - 1))
+ setHi((i) => Math.max(0, Math.min(i + 1, filtered.length - 1)))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (e.key === 'ArrowDown') { | |
| e.preventDefault() | |
| setHi((i) => Math.min(i + 1, filtered.length - 1)) | |
| if (e.key === 'ArrowDown') { | |
| e.preventDefault() | |
| setHi((i) => Math.max(0, Math.min(i + 1, filtered.length - 1))) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/components/ds/core/CommandMenu.jsx` around lines 47 - 49, Update the
ArrowDown handling in the CommandMenu highlight state update to clamp the
highlighted index at zero when filtered is empty, while preserving the existing
upper bound for non-empty results. Ensure hi remains non-negative so
later-arriving items can be highlighted and selected.
| export function PasskeyList({ creds = [], empty, onAdd }) { | ||
| const addBtn = ( | ||
| <Button variant="outline" onClick={() => onAdd?.()}> | ||
| <span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span> | ||
| Add a passkey | ||
| </Button> | ||
| ) | ||
| return ( | ||
| <div | ||
| style={{ | ||
| border: '1px solid var(--ht-divider)', | ||
| borderRadius: 'var(--ht-radius-md)', | ||
| background: 'var(--ht-surface)', | ||
| overflow: 'hidden', | ||
| }} | ||
| > | ||
| {empty || creds.length === 0 ? ( | ||
| <div style={{ padding: '40px 24px 34px', textAlign: 'center' }}> | ||
| <div | ||
| style={{ | ||
| width: 44, | ||
| height: 44, | ||
| margin: '0 auto 14px', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| borderRadius: 'var(--ht-radius-md)', | ||
| background: 'var(--ht-surface-2)', | ||
| color: 'var(--ht-ink-dim)', | ||
| }} | ||
| > | ||
| {IconKey(24)} | ||
| </div> | ||
| <div | ||
| style={{ | ||
| fontFamily: 'var(--ht-display)', | ||
| fontSize: 18, | ||
| fontWeight: 600, | ||
| color: 'var(--ht-ink)', | ||
| }} | ||
| > | ||
| No passkeys yet | ||
| </div> | ||
| <div | ||
| style={{ | ||
| margin: '7px auto 18px', | ||
| maxWidth: 320, | ||
| fontSize: 13.5, | ||
| lineHeight: 1.6, | ||
| color: 'var(--ht-ink-muted)', | ||
| }} | ||
| > | ||
| Add a passkey to sign in with your fingerprint, face, or security key — no password to | ||
| remember. | ||
| </div> | ||
| {addBtn} | ||
| </div> | ||
| ) : ( | ||
| <> | ||
| {creds.map((c, i) => ( | ||
| <CredentialRow key={c.name} cred={c} first={i === 0} /> | ||
| ))} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
PasskeyList never forwards onRename/onRevoke to CredentialRow.
PasskeyList only destructures creds, empty, onAdd. Each rendered CredentialRow therefore always has onRename/onRevoke as undefined, so the rename/revoke UI is fully interactive but has no observable effect — onRename?.(cred, name) and onRevoke?.(cred) are silent no-ops for anyone using the composed PasskeyList (rather than CredentialRow directly). This defeats the primary purpose of the list.
🐛 Proposed fix
-export function PasskeyList({ creds = [], empty, onAdd }) {
+export function PasskeyList({ creds = [], empty, onAdd, onRename, onRevoke }) {
...
{creds.map((c, i) => (
- <CredentialRow key={c.name} cred={c} first={i === 0} />
+ <CredentialRow key={c.name} cred={c} first={i === 0} onRename={onRename} onRevoke={onRevoke} />
))}(Also add onRename?: (cred: Credential, name: string) => void and onRevoke?: (cred: Credential) => void to PasskeyListProps in CredentialRow.d.ts.)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function PasskeyList({ creds = [], empty, onAdd }) { | |
| const addBtn = ( | |
| <Button variant="outline" onClick={() => onAdd?.()}> | |
| <span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span> | |
| Add a passkey | |
| </Button> | |
| ) | |
| return ( | |
| <div | |
| style={{ | |
| border: '1px solid var(--ht-divider)', | |
| borderRadius: 'var(--ht-radius-md)', | |
| background: 'var(--ht-surface)', | |
| overflow: 'hidden', | |
| }} | |
| > | |
| {empty || creds.length === 0 ? ( | |
| <div style={{ padding: '40px 24px 34px', textAlign: 'center' }}> | |
| <div | |
| style={{ | |
| width: 44, | |
| height: 44, | |
| margin: '0 auto 14px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| borderRadius: 'var(--ht-radius-md)', | |
| background: 'var(--ht-surface-2)', | |
| color: 'var(--ht-ink-dim)', | |
| }} | |
| > | |
| {IconKey(24)} | |
| </div> | |
| <div | |
| style={{ | |
| fontFamily: 'var(--ht-display)', | |
| fontSize: 18, | |
| fontWeight: 600, | |
| color: 'var(--ht-ink)', | |
| }} | |
| > | |
| No passkeys yet | |
| </div> | |
| <div | |
| style={{ | |
| margin: '7px auto 18px', | |
| maxWidth: 320, | |
| fontSize: 13.5, | |
| lineHeight: 1.6, | |
| color: 'var(--ht-ink-muted)', | |
| }} | |
| > | |
| Add a passkey to sign in with your fingerprint, face, or security key — no password to | |
| remember. | |
| </div> | |
| {addBtn} | |
| </div> | |
| ) : ( | |
| <> | |
| {creds.map((c, i) => ( | |
| <CredentialRow key={c.name} cred={c} first={i === 0} /> | |
| ))} | |
| export function PasskeyList({ creds = [], empty, onAdd, onRename, onRevoke }) { | |
| const addBtn = ( | |
| <Button variant="outline" onClick={() => onAdd?.()}> | |
| <span style={{ display: 'inline-flex', marginRight: -2 }}>{IconPlus(14)}</span> | |
| Add a passkey | |
| </Button> | |
| ) | |
| return ( | |
| <div | |
| style={{ | |
| border: '1px solid var(--ht-divider)', | |
| borderRadius: 'var(--ht-radius-md)', | |
| background: 'var(--ht-surface)', | |
| overflow: 'hidden', | |
| }} | |
| > | |
| {empty || creds.length === 0 ? ( | |
| <div style={{ padding: '40px 24px 34px', textAlign: 'center' }}> | |
| <div | |
| style={{ | |
| width: 44, | |
| height: 44, | |
| margin: '0 auto 14px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| borderRadius: 'var(--ht-radius-md)', | |
| background: 'var(--ht-surface-2)', | |
| color: 'var(--ht-ink-dim)', | |
| }} | |
| > | |
| {IconKey(24)} | |
| </div> | |
| <div | |
| style={{ | |
| fontFamily: 'var(--ht-display)', | |
| fontSize: 18, | |
| fontWeight: 600, | |
| color: 'var(--ht-ink)', | |
| }} | |
| > | |
| No passkeys yet | |
| </div> | |
| <div | |
| style={{ | |
| margin: '7px auto 18px', | |
| maxWidth: 320, | |
| fontSize: 13.5, | |
| lineHeight: 1.6, | |
| color: 'var(--ht-ink-muted)', | |
| }} | |
| > | |
| Add a passkey to sign in with your fingerprint, face, or security key — no password to | |
| remember. | |
| </div> | |
| {addBtn} | |
| </div> | |
| ) : ( | |
| <> | |
| {creds.map((c, i) => ( | |
| <CredentialRow key={c.name} cred={c} first={i === 0} onRename={onRename} onRevoke={onRevoke} /> | |
| ))} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/components/ds/core/CredentialRow.jsx` around lines 230 - 291, Update
PasskeyList to accept onRename and onRevoke and forward both callbacks to every
rendered CredentialRow, preserving the credential and new name arguments used by
CredentialRow. Add the corresponding optional callback declarations to
PasskeyListProps in CredentialRow.d.ts.
| function laterToday() { | ||
| const d = now() | ||
| d.setHours(17, 0, 0, 0) | ||
| return d |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== file listing ==\n'
git ls-files 'web/src/components/ds/core/SnoozePicker.jsx'
printf '\n== line count ==\n'
wc -l web/src/components/ds/core/SnoozePicker.jsx
printf '\n== relevant excerpt ==\n'
cat -n web/src/components/ds/core/SnoozePicker.jsx | sed -n '1,220p'
printf '\n== search for now() / laterToday / time handling in nearby files ==\n'
rg -n "laterToday|now\(\)|type=\"time\"|setHours\(|SnoozePicker" web/src/components/ds/core web/src/components/ds -g '!**/node_modules/**'Repository: Helpthread/helpthread
Length of output: 12578
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,140p' web/src/components/ds/core/SnoozePicker.jsx | nl -baRepository: Helpthread/helpthread
Length of output: 199
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== SnoozePicker time section ==\n'
sed -n '240,300p' web/src/components/ds/core/SnoozePicker.jsx | cat -n
printf '\n== primitives-support clock commentary ==\n'
sed -n '80,120p' web/src/components/ds/core/primitives-support.jsx | cat -n
printf '\n== search for empty-time handling ==\n'
rg -n "time.*''|setTime\(|onChange=.*time|type=\"time\"" web/src/components/ds/core/SnoozePicker.jsxRepository: Helpthread/helpthread
Length of output: 4323
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== SnoozePicker around time input ==\n'
sed -n '248,292p' web/src/components/ds/core/SnoozePicker.jsx | cat -n
printf '\n== primitives-support around now() commentary ==\n'
sed -n '96,118p' web/src/components/ds/core/primitives-support.jsx | cat -nRepository: Helpthread/helpthread
Length of output: 2941
Make Later today future-safe (web/src/components/ds/core/SnoozePicker.jsx:5-8). After 5 PM, laterToday() resolves to a past timestamp; hide the preset once it expires or roll it forward to the next valid slot.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/components/ds/core/SnoozePicker.jsx` around lines 5 - 8, Update
laterToday() so it never returns a past timestamp: after 5 PM, either hide the
Later today preset or roll its timestamp forward to the next valid slot, while
preserving the existing 5 PM behavior before that cutoff.
Source: Coding guidelines
| const resolved = React.useMemo(() => { | ||
| const [h, mm] = time.split(':').map(Number) | ||
| const d = new Date(sel) | ||
| d.setHours(h, mm, 0, 0) | ||
| return d | ||
| }, [sel, time]) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Prevent an invalid Date when the time field is cleared.
A native time input can emit ''; destructuring then leaves mm undefined, so setHours() produces an invalid Date. The confirmation text becomes invalid and onSnooze can receive that invalid value. Validate the time before deriving resolved and disable or guard submission until it is valid.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/components/ds/core/SnoozePicker.jsx` around lines 169 - 174, Update
the resolved useMemo in SnoozePicker to validate that time is non-empty and
contains valid hour/minute values before calling setHours; return a safe
non-submittable state when invalid. Ensure the confirmation display does not use
an invalid Date, and disable or guard the submission path so onSnooze cannot
receive an invalid resolved value.
CLAUDE.md requires web/src/components/ds/ to be verbatim copies of the Claude Design project's components. They aren't: Biome reformats them on arrival — the repo's javascript.formatter settings (single quotes, semicolons asNeeded) rewrite the design source's double quotes and semicolons, and organizeImports reorders their imports. The code behaves identically; Button.jsx was confirmed semantically identical to the design project's copy, with the entire diff being quotes, semicolons and line wrapping. But that noise makes byte comparison useless as a drift detector — a real design change would be indistinguishable from formatter churn. An override for ds/** already existed, disabling several lint rules that don't suit design-source code. It left the formatter and the import assist enabled, which is what does the rewriting. This turns both off for that path. Verified by construction: a file written in design-source style (double quotes, semicolons) passes `biome check` with exit 0 inside ds/, and fails with a format error outside it. Whole-repo `biome check` stays exit 0. Note this prevents FUTURE mangling; it does not retroactively restore the 16 existing components, which are already reformatted. Making byte comparison actually work requires re-pulling those from the design project — tracked separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The four new primitives (plus their shared helpers) were promoted into the design project's components/core/ in this ticket. This re-pulls them back down verbatim, so ds/ no longer holds five files that were a conversion rather than a copy — every file under ds/ is now a byte-for-byte copy of its design-project counterpart, which is what CLAUDE.md claims. Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against the exact bytes uploaded, all identical. Two defects were caught by the normalized-diff pass and fixed upstream first, then re-pulled: - CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight quotes. That is rendered output, so it is a fidelity break, not formatting. - CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the null the component actually branches on. Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply and IconClock take a required `sz`, because unlike IconSearch and IconPlus they carry no default and render wrong without it. The biome.json override gains noAutofocus, useExhaustiveDependencies and noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments; those are app-lint artifacts and do not belong in verbatim design source, so the suppression moves to the override that already exists for exactly this purpose. The design rationale comments stay in the components. No rule is relaxed outside web/src/components/ds/**. Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0, npm run build = 0.
PR #103 stopped Biome from reformatting web/src/components/ds/ on arrival, but it could not un-mangle the 16 components already there. This does that: each file re-fetched from the Claude Design "Helpthread" project via DesignSync and written byte-for-byte — double quotes, semicolons, original import order and line wrapping restored. Equivalence was proven before overwriting, not assumed. Both the pre-change and post-change trees were formatted through one canonical Biome config and diffed; the diff is empty across all 32 files. So no style value, prop, or branch of logic changed — the whole historical drift really was formatting, and nothing had been hand-edited in the app or moved in the design project. Byte fidelity spot-checked against fresh get_file responses; all 32 files end with a newline and none carry CRLF. Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0, npm run build = 0. Stacked on feat/ht-93-ds-new-primitives — the biome.json override there is a precondition for the biome gate to pass on verbatim files.
The four new primitives (plus their shared helpers) were promoted into the design project's components/core/ in this ticket. This re-pulls them back down verbatim, so ds/ no longer holds five files that were a conversion rather than a copy — every file under ds/ is now a byte-for-byte copy of its design-project counterpart, which is what CLAUDE.md claims. Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against the exact bytes uploaded, all identical. Two defects were caught by the normalized-diff pass and fixed upstream first, then re-pulled: - CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight quotes. That is rendered output, so it is a fidelity break, not formatting. - CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the null the component actually branches on. Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply and IconClock take a required `sz`, because unlike IconSearch and IconPlus they carry no default and render wrong without it. The biome.json override gains noAutofocus, useExhaustiveDependencies and noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments; those are app-lint artifacts and do not belong in verbatim design source, so the suppression moves to the override that already exists for exactly this purpose. The design rationale comments stay in the components. No rule is relaxed outside web/src/components/ds/**. Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0, npm run build = 0.
* chore(web): re-pull the 16 design-system components verbatim (HT-94) PR #103 stopped Biome from reformatting web/src/components/ds/ on arrival, but it could not un-mangle the 16 components already there. This does that: each file re-fetched from the Claude Design "Helpthread" project via DesignSync and written byte-for-byte — double quotes, semicolons, original import order and line wrapping restored. Equivalence was proven before overwriting, not assumed. Both the pre-change and post-change trees were formatted through one canonical Biome config and diffed; the diff is empty across all 32 files. So no style value, prop, or branch of logic changed — the whole historical drift really was formatting, and nothing had been hand-edited in the app or moved in the design project. Byte fidelity spot-checked against fresh get_file responses; all 32 files end with a newline and none carry CRLF. Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0, npm run build = 0. Stacked on feat/ht-93-ds-new-primitives — the biome.json override there is a precondition for the biome gate to pass on verbatim files. * docs: UI fidelity is bidirectional, not design-first-only (HT-94) The section said improvements go upstream in the design project first. That has not been the working policy: HT-54's screens were built app-first and approved in the app, and TJ's call (2026-07-20) is that approval in the app is approval — the work flows back up. Documents both directions, why ds/ is excluded from Biome (a formatter pass breaks byte comparison as a drift detector), and that a semantic difference found during a re-pull is a finding to escalate rather than something to quietly resolve. * chore(web): close the loop on the five promoted primitives (HT-94) The four new primitives (plus their shared helpers) were promoted into the design project's components/core/ in this ticket. This re-pulls them back down verbatim, so ds/ no longer holds five files that were a conversion rather than a copy — every file under ds/ is now a byte-for-byte copy of its design-project counterpart, which is what CLAUDE.md claims. Round-trip proven byte-exact: the ten re-pulled files were `cmp`'d against the exact bytes uploaded, all identical. Two defects were caught by the normalized-diff pass and fixed upstream first, then re-pulled: - CommandMenu flattened the curly quotes in `Nothing matches “…”` to straight quotes. That is rendered output, so it is a fidelity break, not formatting. - CredentialRow.d.ts narrowed `lastUsed?: Date | null` to `Date`, losing the null the component actually branches on. Three .d.ts signatures deliberately differ from PR #103's: IconKey, IconReply and IconClock take a required `sz`, because unlike IconSearch and IconPlus they carry no default and render wrong without it. The biome.json override gains noAutofocus, useExhaustiveDependencies and noArrayIndexKey. PR #103 suppressed these with inline biome-ignore comments; those are app-lint artifacts and do not belong in verbatim design source, so the suppression moves to the override that already exists for exactly this purpose. The design rationale comments stay in the components. No rule is relaxed outside web/src/components/ds/**. Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0, npm run build = 0. * docs: record the design project's three-folder component taxonomy (HT-94) TJ approved components/app/ as the home for app-level surface upstream, so the rule it implies gets written down rather than living in one PR thread. core/ is primitives, inbox/ is inbox-specific composition, app/ is whole screens plus the chrome framing them; the test for app/ is that the thing owns a route or wraps all of them. Also records the one place the two sides are deliberately NOT byte-identical: the app/ screens are .tsx here and are converted to presentational .jsx on the way up, unlike ds/ which is a copy. The same taxonomy note is now in the design project's own readme.md.
…dules match the desk (HT-95) Modules are out-of-process and render their own UI, so nothing today makes a module look like the desk it installs into. web/src/components/ds/ and web/src/theme/tokens/ are the right raw material but are AGPL, and a born-proprietary paid module importing them links AGPL code in-process — the case the §7 Module API Exception covers, which is still DRAFT. Proposes publishing the pack under a permissive license as its own package: needs no §7 exception at all, and paid → free is the permitted direction under catalog.md §1. Components are not the moat. Also: theming resolves against the installed desk (white-labeling is a paid item), the pack is generated from ds/ rather than forked (same discipline as CLAUDE.md's UI-fidelity rule, one hop out), and conformance is a marketplace listing requirement since no runtime check can ever enforce it. Docs only. ds/ is owned by HT-93 (PR #103) and HT-94 — untouched here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dules match the desk (HT-95) Modules are out-of-process and render their own UI, so nothing today makes a module look like the desk it installs into. web/src/components/ds/ and web/src/theme/tokens/ are the right raw material but are AGPL, and a born-proprietary paid module importing them links AGPL code in-process — the case the §7 Module API Exception covers, which is still DRAFT. Proposes publishing the pack under a permissive license as its own package: needs no §7 exception at all, and paid → free is the permitted direction under catalog.md §1. Components are not the moat. Also: theming resolves against the installed desk (white-labeling is a paid item), the pack is generated from ds/ rather than forked (same discipline as CLAUDE.md's UI-fidelity rule, one hop out), and conformance is a marketplace listing requirement since no runtime check can ever enforce it. Docs only. ds/ is owned by HT-93 (PR #103) and HT-94 — untouched here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dules match the desk (HT-95) (#104) * docs(spec): module design pack — permissive components + tokens so modules match the desk (HT-95) Modules are out-of-process and render their own UI, so nothing today makes a module look like the desk it installs into. web/src/components/ds/ and web/src/theme/tokens/ are the right raw material but are AGPL, and a born-proprietary paid module importing them links AGPL code in-process — the case the §7 Module API Exception covers, which is still DRAFT. Proposes publishing the pack under a permissive license as its own package: needs no §7 exception at all, and paid → free is the permitted direction under catalog.md §1. Components are not the moat. Also: theming resolves against the installed desk (white-labeling is a paid item), the pack is generated from ds/ rather than forked (same discipline as CLAUDE.md's UI-fidelity rule, one hop out), and conformance is a marketplace listing requirement since no runtime check can ever enforce it. Docs only. ds/ is owned by HT-93 (PR #103) and HT-94 — untouched here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): settle the three open decisions — MIT, own repo, no token endpoint (HT-95) Apache-2.0 was considered and rejected: no patentable invention in a component library, so its patent grant covers a threat that does not exist, while NOTICE preservation is real overhead. MIT is the React component-library norm and readable without legal review. Adoption friction is the live constraint. Pack ships as its own repo (helpthread-design-pack, @helpthread/design-pack) — a permissive directory inside an AGPL tree gets misread by the audience that needs to trust it. Token transport: custom properties ship with the pack, desk values win. Embedded modules inherit the desk scope for free; a public endpoint waits until an operator has both re-skinned and installed a cross-origin module. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): pack sources from the design project as a sibling of ds/, not downstream (HT-95) Chaining pack <- ds/ <- design project would propagate any ds/ drift into every module — the same failure the spec exists to prevent, one layer down. As siblings, the desk and its modules cannot drift from each other without both drifting from a single source that byte-comparison catches. Also unblocks: 16 of 20 components are already promoted upstream, so the pack no longer waits on HT-93/HT-94. The four new primitives arrive when HT-94 part B promotes them out of templates/new-primitives/. Verified against the design project's file list rather than assumed — it already carries components/core/, components/inbox/, tokens/, theme/, and fonts/. Sync is documented as a process, not a pipe: DesignSync authenticates through a claude.ai login and its writes need interactive plan approval, so it cannot run unattended in CI. Options are the /design-sync skill or a scheduled agent that opens a PR — cadence automation, not a live connection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): address CodeRabbit — provenance gate, content-hash drift gate, MUST-level conformance (HT-95) Three Major findings, all real. Provenance (§2.1): relicensing requires owning the rights, and generation establishes origin, not grantability. Fonts verified clear — fonts.css @imports Source Serif 4 and Source Code Pro from Google Fonts, both OFL, no binaries bundled, so nothing is redistributed. Components still need an audit against the repo's provenance/AI policy. Noted the asymmetry that lowers the stakes: MIT grants whatever rights exist, so uncopyrightability means 'cannot enforce', not 'infringing' — near-harmless for a pack meant to be used. Drift gate (§5): a revision pin is not available — the design project is not a git repo and DesignSync exposes no version identifier. Baseline is a content-hash manifest instead, which separates 'upstream moved' from 'pack was hand-edited'; byte-compare alone conflates them. Conformance (§6): SHOULD-consume contradicted calling it a listing requirement. Now MUST, but on the outcome (matches the desk) rather than the mechanism (imports the pack) — otherwise a module with no operator-visible UI would be non-conformant for having nothing to style. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): address CodeRabbit round 2 — 7 findings (HT-95) Vocabulary (CLAUDE.md violation, mine): 'agent session' and 'scheduled agent' described AI automation. Agents are human support staff; Assistants are AI actors. Corrected, with the rule cited inline so the next editor does not repeat it. Inventory: §3 said 16 core, §5 said 12 core with 4 staged — both true at different times, neither said when. Now stated as 12 core at v1, 16 after HT-94 part B, with totals given once (16 -> 20) and a rule for reading every other count in the doc. Cross-origin conformance: §6's MUST required matching a white-labeled desk while §4 shipped no token transport, so a cross-origin module could not conform. Made the exemption explicit and time-bound — a conformance rule nobody can satisfy is worse than no rule. Drift gate: hashing only sourced files cannot distinguish expected generation from hand edits, since the pack also contains derived output. Now two closed path sets, sources and generated, with appearing/vanishing files failing the gate. Truth table extended to all four states. Fonts: excluded from the pack pending HT-99. Licensing was settled (OFL, no binaries vendored) but the @import means every module page would hit fonts.googleapis.com with the visitor's IP — inconsistent with shipping open-tracking-off as a free-core position, and a CSP/offline problem for self-hosters. Already live in the desk, so filed against core. Sync wording: 'no unattended sync' contradicted permitting scheduled sync. Reworded to bound it at the PR — an Assistant may prepare and open, never merge or release. Markdown: fenced the source-tree diagram as text (MD040). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): MIT ratified by TJ; exclude brand assets from the pack (HT-95) TJ ratified MIT explicitly ('yes MIT') after confirming the scope: the design pack only. Core stays AGPL-3.0, LICENSE untouched, ds/ stays part of the AGPL core. Recorded as his decision with his words, per the verdict protocol — the earlier PR body framed it as settled on the strength of 'MIT, shit, i don't care', which is not ratification of a one-way licensing door. Noted the nuance that makes it low-risk: this is not relicensing core code. Resonant IQ holds the copyright outright — ds/ has a single author — so the same components are published under a second licence in a separate package while the copies inside the core remain AGPL. Dual-licensing your own work costs the core nothing. Added the one boundary worth holding: brand assets are excluded permanently. The wordmark, logo, and any Helpthread-identifying mark stay out of the pack and its repo. MIT grants copyright and not trademark, so nobody could call their product Helpthread either way — but shipping the wordmark inside an MIT package invites the confusion legal/trademark-policy.md exists to prevent. The pack ships the system, never the identity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): address CodeRabbit round 3 — inventory, provenance gate, hash coverage (HT-95) Three findings, all real. Inventory: the spec counted 'CredentialRow/PasskeyList' as one item, leaving the post-HT-94 total ambiguous. Verified against the file — CredentialRow.jsx exports BOTH CredentialRow and PasskeyList, and there is no separate PasskeyList.jsx. So it is one file, two components. Counts are now given as an explicit files-vs- components table (20 files / 21 components after HT-94 part B) with a stated default so every other figure in the doc reads unambiguously. Provenance: the audit was described as 'due diligence, not a blocker to design around', on the reasoning that MIT grants only whatever rights exist so the downside is unenforceability rather than infringement. That reasoning is sound about the downside and wrong about the sequencing — MIT publication is a one-way door, and copies already taken cannot be recalled. It is now a blocking release gate with four named items: every published path cleared (tokens and theme, not just components), sole authorship verified and dated, AI-assisted generation reconciled against legal/provenance-policy.md, and the clearance written down in the pack repo. Hash coverage: the manifest covered sourced and generated files but not release-only paths — package.json, README, LICENSE, CI config. A hand-edit there would pass both existing checks. Added a third 'static' set and stated the invariant plainly: the manifest is exhaustive over the published package, not merely over its inputs, and any published path missing from the manifest fails the gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): fix 6 defects found by adversarial review (HT-95) CodeRabbit was rate limited, so an independent adversarial pass ran in its place. Six findings, five blocking. All were mine. 1. THE COPYRIGHT CLAIM CONTRADICTED THE CHARTER. The spec asserted 'Resonant IQ holds the copyright outright (ds/ has a single author)'. Checked: git log returns one name, TJ Baker — an individual, not the company — and CHARTER.md §3 says contributions arrive under DCO with no assignment and 'Contributors keep the copyright on their work'. The project holds an inbound AGPL licence, not title. Single authorship records who committed, not who owns. Publishing under MIT still works — a sole author may license their own work — but a public claim about which entity holds title, contradicted by the repo's own constitution, must not ship. Naming the grantor is now item 5 of the §2.1 gate. 2. WRONG AUTHORITY CITED. catalog.md's 'paid → free stays possible' is a monetization axis — whether a paid module may later join the free core. It says nothing about copyleft→permissive. Citing it made relicensing look pre-approved by an accepted spec. Withdrawn; the argument stands without it. 3. THE BLOCKING GATE COULD NOT BLOCK. It governed a repo that does not exist yet, with no owner, no artifact, and no mechanism — prose wearing the word 'blocking', the same shape as the instructions that failed in this repo's own audit. Now: a dated CLEARANCE.md in the pack repo, asserted by the publish workflow and failing npm publish if missing or stale, owned by TJ, with the pack repo not created until the grantor question is answered. 4. THE ANTI-DRIFT TABLE WAS ITSELF WRONG. It counted only .jsx and omitted 21 .d.ts files and primitives-support.jsx — a shared helper the four newest primitives import, which ships and would break the package if left out. Real counts, verified against the tree: 34 core + 8 inbox = 42 published files; 20 components across 21 .jsx. Files is now the stated default unit, which is also what the clearance gate and the manifest operate on. 5. .d.ts WERE CLASSIFIED AS GENERATED. They are fetched verbatim from the design project (HT-97 re-pulled 21), so they belong in 'sources'. As written, upstream .d.ts churn could never trip the 'upstream moved' row. 6. THE BRAND CARVE-OUT EXCLUDED ALMOST NOTHING. It pointed at theme/helpthread.css, which is a comment plus three @imports and holds no brand value. Meanwhile the token files that DO ship are brand-bearing: colors.css opens with a block commented '/* identity */' above --ht-accent, and typography.css sets --ht-serif to the wordmark face. Now states plainly that the palette and type scale ship and are meant to be overridden; only the marks are withheld. Also dropped a restatement of the Agent/Assistant vocabulary rule — that definition was revised 2026-07-31 and a copy here is one more place to drift. Rebased on current main; HT-93 and HT-94 have merged, so the counts reflect the tree as it stands rather than a pending promotion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): correct one stale count missed in the previous pass (HT-95) §2 still read '20 files / 21 components after HT-93' — a figure from before the counts were verified against the tree. It is 42 published files (§3). Caught on re-check after committing; the earlier replacement had matched an intermediate version of the line rather than the final one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(spec): name Resonant IQ, Inc. as MIT grantor; close clearance-gate item 5 (HT-95) Replaces the unsupported chain-of-title discussion with a plain licensing statement, and tags the three remaining provenance decisions (repo split, brand carve-out, marketplace conformance MUST) as maintainer-decided. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(spec): item 5 states a future requirement, not a present fact (HT-95) Adversarial review caught: the pack's LICENSE file can't "state" the grantor yet — the pack repo doesn't exist. Made it a MUST. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Closes HT-93.
Why
The Claude Design "Helpthread" project gained four net-new primitives on 2026-07-19, authored in response to a request from app work. They were never pulled down. They were sitting in
templates/new-primitives/Primitives.jsx, unused.SplitButtonCommandMenusaved_repliestable)SnoozePickersnooze-wakecron)CredentialRow/PasskeyListwebauthn_credentials, HT-75)The conversion
Mechanical, with every style value preserved. The design source is browser-rendered, so it used
React.createElement,module.exports, and local duplicate copies ofMenuItem/Button. Those become JSX, ESM exports, and real imports from./MenuItemand./Button.Dropped: the
Showcase, its layout scaffolding (Section/Panel/StateLabel/Stack/twoUp), and the specimen fixtures. Design-project demo code, not app code.Shared helpers (icon glyphs, the
RINGtoken, date formatters) go inprimitives-support.jsx— the source carried one copy in a single file, and splitting that file must not turn one definition into four.One intentional behavioral difference
The design source froze a reference clock (
NOW = 2026-07-19 14:30) so specimen times wouldn't drift between renders. Correct for a showcase, wrong for the app —SnoozePickerwould compute "tomorrow" from a date in the past. Replaced withnow(), commented at the site.Five lint findings suppressed, not fixed
Biome marks most of these FIXABLE, but each "fix" would be silent drift from the approved design:
noAutofocus×2 — both fields appear only in response to an explicit user action (opening the command menu; clicking Rename). Removing autofocus would strand keyboard users.noArrayIndexKey×2 — weekday initials repeat (two T, two S) in a fixed-length row that never reorders; month-padding cells have no identity of their own.useExhaustiveDependencies×1 —qis the effect's trigger, not a read. Dropping it would stop the highlight resetting when the query changes.Each carries a
biome-ignorewith the reason inline.Verification
biome check web/src/components/ds/core/tsc -p tsconfig.jsonnext buildNone of the existing 16
ds/components were modified —git statusshows only the 10 new files. The formatter pass was run with--linter-enabled=falsespecifically so it couldn't touch them or apply behavior-changing lint fixes.Not in this PR
components/core/upstream so the design project's own library matches. Right now they live intemplates/remotely, so they're staged on both sides.web/src/components/ds/is Biome-formatted, so it no longer byte-matches the design project despiteCLAUDE.mdrequiring verbatim copies.Button.jsxwas confirmed semantically identical — the entire diff is quotes, semicolons, and wrapping. This makes byte comparison useless as a drift detector and wants a deliberate call: excludeds/from Biome, or adopt a normalizing comparison. Filed in HT-93's description.Reviewer attention
The claim worth checking is pixel fidelity — that no style value drifted in the
createElement→ JSX conversion. Worth a spot-check againsttemplates/new-primitives/Primitives.jsxin the design project, particularlySplitButton's seam/border handling andSnoozePicker'sMiniCalendarcell states, which had the densest conditional styling.🤖 Generated with Claude Code
Summary by CodeRabbit