-
Notifications
You must be signed in to change notification settings - Fork 0
chore(web): design-system reconciliation, both directions (HT-94) #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2b14c61
chore(web): re-pull the 16 design-system components verbatim (HT-94)
zaridan e0ef28c
docs: UI fidelity is bidirectional, not design-first-only (HT-94)
zaridan f297739
chore(web): close the loop on the five promoted primitives (HT-94)
zaridan 8abcaaf
docs: record the design project's three-folder component taxonomy (HT…
zaridan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,13 @@ | ||
| export interface AvatarProps { | ||
| /** initials + tone derive from this */ | ||
| email?: string | ||
| email?: string; | ||
| /** explicit initials override */ | ||
| initials?: string | ||
| size?: number | ||
| initials?: string; | ||
| size?: number; | ||
| /** the Agent avatar: accent fill, "S" */ | ||
| agent?: boolean | ||
| agent?: boolean; | ||
| /** white ring + shadow (threads, context panel) */ | ||
| ring?: boolean | ||
| style?: React.CSSProperties | ||
| ring?: boolean; | ||
| style?: React.CSSProperties; | ||
| } | ||
| export declare function Avatar(props: AvatarProps): JSX.Element | ||
| export declare function Avatar(props: AvatarProps): JSX.Element; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,46 +1,32 @@ | ||
| import React from 'react' | ||
| import React from "react"; | ||
|
|
||
| const PALETTE = [ | ||
| ['oklch(0.9 0.03 60)', 'oklch(0.42 0.07 60)'], | ||
| ['oklch(0.9 0.03 150)', 'oklch(0.4 0.06 150)'], | ||
| ['oklch(0.9 0.03 250)', 'oklch(0.42 0.06 250)'], | ||
| ['oklch(0.9 0.03 320)', 'oklch(0.42 0.06 320)'], | ||
| ['oklch(0.9 0.035 95)', 'oklch(0.42 0.06 95)'], | ||
| ] | ||
| ["oklch(0.9 0.03 60)", "oklch(0.42 0.07 60)"], | ||
| ["oklch(0.9 0.03 150)", "oklch(0.4 0.06 150)"], | ||
| ["oklch(0.9 0.03 250)", "oklch(0.42 0.06 250)"], | ||
| ["oklch(0.9 0.03 320)", "oklch(0.42 0.06 320)"], | ||
| ["oklch(0.9 0.035 95)", "oklch(0.42 0.06 95)"], | ||
| ]; | ||
| function initialsOf(email) { | ||
| const local = String(email || '').split('@')[0] | ||
| const parts = local.split(/[._-]+/).filter(Boolean) | ||
| if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase() | ||
| return local.slice(0, 2).toUpperCase() | ||
| const local = String(email || "").split("@")[0]; | ||
| const parts = local.split(/[._-]+/).filter(Boolean); | ||
| if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); | ||
| return local.slice(0, 2).toUpperCase(); | ||
| } | ||
|
|
||
| /** Initials avatar. Tone is a stable hash of the email; agent=true fills with accent. | ||
| * ring adds the white ring + shadow used in threads and the context panel. */ | ||
| export function Avatar({ email, initials, size = 32, agent = false, ring = true, style }) { | ||
| let h = 0 | ||
| for (const ch of String(email || initials || '?')) h = (h * 31 + ch.charCodeAt(0)) >>> 0 | ||
| const [bg, fg] = agent ? ['var(--ht-accent)', 'var(--ht-on-accent)'] : PALETTE[h % PALETTE.length] | ||
| let h = 0; | ||
| for (const ch of String(email || initials || "?")) h = (h * 31 + ch.charCodeAt(0)) >>> 0; | ||
| const [bg, fg] = agent ? ["var(--ht-accent)", "var(--ht-on-accent)"] : PALETTE[h % PALETTE.length]; | ||
| return ( | ||
| <div | ||
| style={{ | ||
| width: size, | ||
| height: size, | ||
| borderRadius: '50%', | ||
| flexShrink: 0, | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| fontWeight: 600, | ||
| letterSpacing: '0.02em', | ||
| fontSize: Math.round(size * 0.34), | ||
| background: bg, | ||
| color: fg, | ||
| border: ring ? (size >= 48 ? '3px' : '2px') + ' solid var(--ht-surface)' : 'none', | ||
| boxShadow: ring ? (size >= 48 ? 'var(--ht-shadow-md)' : 'var(--ht-shadow-sm)') : 'none', | ||
| ...style, | ||
| }} | ||
| > | ||
| {initials || (agent ? 'S' : initialsOf(email))} | ||
| <div style={{ width: size, height: size, borderRadius: "50%", flexShrink: 0, display: "flex", | ||
| alignItems: "center", justifyContent: "center", fontWeight: 600, letterSpacing: "0.02em", | ||
| fontSize: Math.round(size * 0.34), background: bg, color: fg, | ||
| border: ring ? (size >= 48 ? "3px" : "2px") + " solid var(--ht-surface)" : "none", | ||
| boxShadow: ring ? (size >= 48 ? "var(--ht-shadow-md)" : "var(--ht-shadow-sm)") : "none", ...style }}> | ||
| {initials || (agent ? "S" : initialsOf(email))} | ||
| </div> | ||
| ) | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,13 @@ | ||
| /** The brand's one button. Sentence case labels; destructive actions arm on first press instead of opening a modal. */ | ||
| export interface ButtonProps { | ||
| /** primary | outline | ghost | destructive */ | ||
| variant?: 'primary' | 'outline' | 'ghost' | 'destructive' | ||
| variant?: "primary" | "outline" | "ghost" | "destructive"; | ||
| /** destructive two-step arm state ("Confirm" fill) */ | ||
| armed?: boolean | ||
| disabled?: boolean | ||
| title?: string | ||
| onClick?: () => void | ||
| style?: React.CSSProperties | ||
| children: React.ReactNode | ||
| armed?: boolean; | ||
| disabled?: boolean; | ||
| title?: string; | ||
| onClick?: () => void; | ||
| style?: React.CSSProperties; | ||
| children: React.ReactNode; | ||
| } | ||
| export declare function Button(props: ButtonProps): JSX.Element | ||
| export declare function Button(props: ButtonProps): JSX.Element; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,72 +1,27 @@ | ||
| import React from 'react' | ||
| import React from "react"; | ||
|
|
||
| /** Primary action button. variant: primary | outline | ghost | destructive. | ||
| * Destructive supports the two-step arm pattern via the `armed` prop. */ | ||
| export function Button({ | ||
| variant = 'primary', | ||
| armed = false, | ||
| disabled = false, | ||
| onClick, | ||
| title, | ||
| style, | ||
| children, | ||
| }) { | ||
| export function Button({ variant = "primary", armed = false, disabled = false, onClick, title, style, children }) { | ||
| const base = { | ||
| display: 'inline-flex', | ||
| alignItems: 'center', | ||
| gap: 8, | ||
| whiteSpace: 'nowrap', | ||
| font: 'inherit', | ||
| fontSize: '13.5px', | ||
| fontWeight: 600, | ||
| borderRadius: 'var(--ht-radius-md)', | ||
| padding: '8px 18px', | ||
| cursor: disabled ? 'default' : 'pointer', | ||
| border: 'none', | ||
| } | ||
| display: "inline-flex", alignItems: "center", gap: 8, whiteSpace: "nowrap", | ||
| font: "inherit", fontSize: "13.5px", fontWeight: 600, borderRadius: "var(--ht-radius-md)", | ||
| padding: "8px 18px", cursor: disabled ? "default" : "pointer", border: "none", | ||
| }; | ||
| const variants = { | ||
| primary: { | ||
| color: 'var(--ht-on-accent)', | ||
| background: disabled | ||
| ? 'color-mix(in oklab, var(--ht-accent) 42%, var(--ht-bg))' | ||
| : 'var(--ht-accent)', | ||
| }, | ||
| outline: { | ||
| color: 'var(--ht-ink)', | ||
| background: 'var(--ht-surface)', | ||
| border: '1px solid var(--ht-border)', | ||
| padding: '7px 17px', | ||
| }, | ||
| ghost: { color: 'var(--ht-ink-muted)', background: 'none' }, | ||
| primary: { color: "var(--ht-on-accent)", background: disabled ? "color-mix(in oklab, var(--ht-accent) 42%, var(--ht-bg))" : "var(--ht-accent)" }, | ||
| outline: { color: "var(--ht-ink)", background: "var(--ht-surface)", border: "1px solid var(--ht-border)", padding: "7px 17px" }, | ||
| ghost: { color: "var(--ht-ink-muted)", background: "none" }, | ||
| destructive: armed | ||
| ? { | ||
| color: 'var(--ht-surface)', | ||
| background: 'var(--ht-critical)', | ||
| border: '1px solid color-mix(in oklab, var(--ht-critical) 40%, transparent)', | ||
| padding: '7px 17px', | ||
| } | ||
| : { | ||
| color: 'var(--ht-critical)', | ||
| background: 'transparent', | ||
| border: '1px solid color-mix(in oklab, var(--ht-critical) 40%, transparent)', | ||
| padding: '7px 17px', | ||
| }, | ||
| } | ||
| ? { color: "var(--ht-surface)", background: "var(--ht-critical)", border: "1px solid color-mix(in oklab, var(--ht-critical) 40%, transparent)", padding: "7px 17px" } | ||
| : { color: "var(--ht-critical)", background: "transparent", border: "1px solid color-mix(in oklab, var(--ht-critical) 40%, transparent)", padding: "7px 17px" }, | ||
| }; | ||
| return ( | ||
| <button | ||
| type="button" | ||
| title={title} | ||
| disabled={disabled} | ||
| onClick={onClick} | ||
| <button type="button" title={title} disabled={disabled} onClick={onClick} | ||
| style={{ ...base, ...variants[variant], ...style }} | ||
| onMouseEnter={(e) => { | ||
| if (!disabled) e.currentTarget.style.filter = 'brightness(0.95)' | ||
| }} | ||
| onMouseLeave={(e) => { | ||
| e.currentTarget.style.filter = '' | ||
| }} | ||
| > | ||
| onMouseEnter={(e) => { if (!disabled) e.currentTarget.style.filter = "brightness(0.95)"; }} | ||
| onMouseLeave={(e) => { e.currentTarget.style.filter = ""; }}> | ||
| {children} | ||
| </button> | ||
| ) | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,20 @@ | ||
| export interface CommandMenuItem { | ||
| label: string | ||
| snippet?: string | ||
| keywords?: string | ||
| shortcut?: string | ||
| label: string; | ||
| /** shown under the label, and matched against the query */ | ||
| snippet?: string; | ||
| /** extra text matched against the query but not shown in the row */ | ||
| keywords?: string; | ||
| shortcut?: React.ReactNode; | ||
| } | ||
|
|
||
| /** Searchable inserter for saved replies. */ | ||
| export interface CommandMenuProps { | ||
| items?: CommandMenuItem[] | ||
| placeholder?: string | ||
| onPick?: (item: CommandMenuItem) => void | ||
| inline?: boolean | ||
| width?: number | ||
| initialQuery?: string | ||
| items?: CommandMenuItem[]; | ||
| placeholder?: string; | ||
| onPick?: (item: CommandMenuItem) => void; | ||
| /** drops the rise-in animation and autofocus, for embedding inline */ | ||
| inline?: boolean; | ||
| width?: number; | ||
| initialQuery?: string; | ||
| } | ||
| export declare function CommandMenu(props: CommandMenuProps): JSX.Element | ||
| export declare function CommandMenu(props: CommandMenuProps): JSX.Element; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Helpthread/helpthread
Length of output: 3135
🏁 Script executed:
Repository: Helpthread/helpthread
Length of output: 1652
🏁 Script executed:
Repository: Helpthread/helpthread
Length of output: 294
🌐 Web query:
React 19 upgrade guide JSX.Element React.JSX global JSX namespace declaration files💡 Result:
In React 19, the global JSX namespace has been removed to prevent type pollution and conflicts with other UI libraries [1][2][3]. Developers must now use the scoped React.JSX namespace instead [1][4][3]. Key Changes and Migration Steps: 1. Update Code References: Replace all instances of JSX.Element (or other JSX types) with React.JSX.Element by importing the namespace from the react package [4][5]: import { JSX } from 'react'; // Use JSX.Element as before, now correctly scoped 2. Automated Migration: You can use the types-react-codemod tool to automatically update your codebase. The scoped-jsx preset is specifically designed to migrate deprecated global JSX references to React.JSX [1][6][2]: npx types-react-codemod scoped-jsx. 3. Module Augmentation: If your project relies on module augmentation to extend the JSX namespace (e.g., for custom elements), you must update your declaration files to augment the react module instead of the global scope [1][2][7]. Old way (Global): declare global { namespace JSX { interface IntrinsicElements { "my-element": { myProp: string }; } } } New way (Scoped): declare module "react" { namespace JSX { interface IntrinsicElements { "my-element": { myProp: string }; } } } The exact module specifier (e.g., "react", "react/jsx-runtime", or "react/jsx-dev-runtime") in your declare module block depends on the "jsx" compiler option set in your tsconfig.json [1][7]. Most standard configurations use "react" [1]. If you encounter "Cannot find namespace 'JSX'" errors in third-party dependencies after upgrading, those packages likely still reference the deprecated global namespace and may require updates or patch-package resolutions [8][9].
Citations:
JSXtype from React to prevent "Cannot find namespace 'JSX'"-error when type-checking with React 19 facebook/lexical#7080JSXglobal namespace withReact.JSXreduxjs/redux-toolkit#4381🏁 Script executed:
Repository: Helpthread/helpthread
Length of output: 50378
🏁 Script executed:
Repository: Helpthread/helpthread
Length of output: 2450
🏁 Script executed:
Repository: Helpthread/helpthread
Length of output: 16459
🏁 Script executed:
Repository: Helpthread/helpthread
Length of output: 854
Use
React.JSX.Elementin these declarationsThese declarations still use the global
JSX.Elementreturn type; switch them toReact.JSX.Elementfor the React 19 typings.DropdownMenualso needs| nullbecause it returnsnullwhen closed.web/src/components/ds/core/Avatar.d.tsweb/src/components/ds/core/DropdownMenu.d.tsweb/src/components/ds/core/EmptyState.d.tsweb/src/components/ds/core/MenuItem.d.tsweb/src/components/ds/core/Skeleton.d.ts📍 Affects 5 files
web/src/components/ds/core/Avatar.d.ts#L13-L13(this comment)web/src/components/ds/core/DropdownMenu.d.ts#L9-L9web/src/components/ds/core/EmptyState.d.ts#L7-L7web/src/components/ds/core/MenuItem.d.ts#L9-L9web/src/components/ds/core/Skeleton.d.ts#L7-L7🤖 Prompt for AI Agents