Skip to content

feat(ui): add theme stories and support#2541

Merged
Davidknp merged 1 commit into
mainfrom
create-ui-package
Jun 16, 2026
Merged

feat(ui): add theme stories and support#2541
Davidknp merged 1 commit into
mainfrom
create-ui-package

Conversation

@Davidknp

Copy link
Copy Markdown
Collaborator

No description provided.

@Davidknp Davidknp merged commit bc73c37 into main Jun 16, 2026
1 check passed
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors the UI package's surface token system from an arbitrary naming scheme (app, secondary, tertiary, quaternary) to a semantically meaningful elevation hierarchy (sunken, base, raised, overlay, floating), wires up context-adaptive CSS variable indirection so components automatically inherit the correct surface colors from their ancestor scope class, and adds Storybook stories to visualise all five elevation families across both color modes.

  • Token rename + new values: tokens.json / tokens.generated.json and theme.css rename and recalibrate all surface tokens; dark-mode values now span a wider luminance range (0.067 → 0.2) so elevation is visually distinct at each level.
  • Component migration: button, combobox, dropdown-menu, popover, and select drop explicit bg-background-quaternary references in favour of surface-floating bg-surface, making them surface-context-aware.
  • Dev tooling: A new watch.mjs / theme:watch script runs an initial build and then hot-rebuilds theme.css on every tokens.json save, letting Storybook HMR pick up changes without a manual rebuild.

Confidence Score: 4/5

The changes are well-scoped to the UI package theme layer and Storybook; no runtime logic or data paths are affected.

The token system, CSS variable chain, and component migrations are internally consistent and the Storybook stories provide a good visual regression harness. The button.tsx secondary variant was not migrated alongside outline/ghost, which may cause a visual inconsistency on surfaces where background-2 differs from the surface context token; the watch script has a minor misleading log message at startup.

packages/ui/src/components/button.tsx — the secondary variant's bg-background-2 may need migrating; packages/ui/src/theme/watch.mjs — startup log message.

Important Files Changed

Filename Overview
packages/ui/src/theme/watch.mjs New dev-only watcher that rebuilds the theme on tokens.json changes; the initial-build log message says token change detected which is misleading on startup.
packages/ui/src/components/button.tsx Migrates outline and ghost variants to surface-aware tokens; secondary variant still uses old bg-background-2 token.
packages/ui/src/stories/surfaces.stories.tsx New story file with Palette, ComponentsOnAllSurfaces, and BothModes stories to visually validate all surface families.
packages/ui/src/theme/surfaces.css Renames surface scope classes to the new elevation vocabulary and adds a :root default binding to surface-base.
packages/ui/src/theme/tailwind.css Switches --color-surface/hover/selected to indirect CSS variable references (context-adaptive), and adds direct elevation aliases for all five levels.
packages/ui/src/theme/theme.css Defines new semantic elevation token values for both light and dark modes across all three color-mode blocks.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    TJ["tokens.json"]
    TC["theme.css"]
    SC["surfaces.css"]
    TW["tailwind.css"]
    COMP["Components"]
    WATCH["watch.mjs"]
    TJ -->|"terrazzo build"| TC
    TC --> SC
    SC --> TW
    TW --> COMP
    WATCH -.->|"triggers rebuild on save"| TJ
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    TJ["tokens.json"]
    TC["theme.css"]
    SC["surfaces.css"]
    TW["tailwind.css"]
    COMP["Components"]
    WATCH["watch.mjs"]
    TJ -->|"terrazzo build"| TC
    TC --> SC
    SC --> TW
    TW --> COMP
    WATCH -.->|"triggers rebuild on save"| TJ
Loading

Comments Outside Diff (1)

  1. packages/ui/src/components/button.tsx, line 37 (link)

    P2 Secondary variant not migrated to surface tokens

    The secondary variant still references bg-background-2 while outline and ghost were both updated to bg-surface / bg-surface-hover. If this was intentional (e.g. secondary intentionally stays outside the surface context), it's worth a comment explaining why; otherwise it may be an overlooked migration.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/ui/src/components/button.tsx
    Line: 37
    
    Comment:
    **Secondary variant not migrated to surface tokens**
    
    The `secondary` variant still references `bg-background-2` while `outline` and `ghost` were both updated to `bg-surface` / `bg-surface-hover`. If this was intentional (e.g. secondary intentionally stays outside the surface context), it's worth a comment explaining why; otherwise it may be an overlooked migration.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
packages/ui/src/theme/watch.mjs:25-26
The `runBuild()` function always logs "token change detected" even when called at startup before any file has changed. A developer running `theme:watch` for the first time will see a misleading message.

```suggestion
function runBuild(reason = 'token change detected') {
  console.log(`\n${reason} — rebuilding theme…`);
```

### Issue 2 of 5
packages/ui/src/theme/watch.mjs:16-17
Calling `runBuild()` without an argument at startup will still print "token change detected" with the default. Pass a descriptive reason so the initial run is clearly distinguished from watch-triggered rebuilds.

```suggestion
// Run once on start so the generated files are always fresh.
runBuild('initial startup');
```

### Issue 3 of 5
packages/ui/src/theme/watch.mjs:1-9
The `execSync` import is placed before the JSDoc comment that describes the module, splitting the import block. Moving it with the other imports keeps the file header readable and consistent.

```suggestion
/**
 * Watches tokens.json and re-runs theme:build whenever it changes.
 * Run via: pnpm run theme:watch
 * Storybook HMR then picks up the updated theme.css automatically.
 */
import { execSync } from 'node:child_process';
import { watch } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
```

### Issue 4 of 5
packages/ui/src/components/button.tsx:37
**Secondary variant not migrated to surface tokens**

The `secondary` variant still references `bg-background-2` while `outline` and `ghost` were both updated to `bg-surface` / `bg-surface-hover`. If this was intentional (e.g. secondary intentionally stays outside the surface context), it's worth a comment explaining why; otherwise it may be an overlooked migration.

### Issue 5 of 5
packages/ui/src/stories/surfaces.stories.tsx:104-119
With 5 families and `grid-cols-2`, the last card renders as a lone item in its own row in both the light and dark panes. Using `grid-cols-3` would keep the layout balanced (3+2) while still fitting in a half-screen column.

```suggestion
      <div className="emlight flex-1 space-y-6 bg-background p-8">
        <p className="text-sm font-medium text-foreground">Light mode</p>
        <div className="grid grid-cols-3 gap-3">
          {FAMILIES.map((f) => (
            <SurfaceCard key={f} family={f} />
          ))}
        </div>
      </div>
      <div className="emdark flex-1 space-y-6 bg-background p-8">
        <p className="text-sm font-medium text-foreground">Dark mode</p>
        <div className="grid grid-cols-3 gap-3">
          {FAMILIES.map((f) => (
            <SurfaceCard key={f} family={f} />
          ))}
        </div>
      </div>
```

Reviews (1): Last reviewed commit: "feat: add theme stories and support" | Re-trigger Greptile

Comment on lines +25 to +26
function runBuild() {
console.log('\ntoken change detected — rebuilding theme…');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The runBuild() function always logs "token change detected" even when called at startup before any file has changed. A developer running theme:watch for the first time will see a misleading message.

Suggested change
function runBuild() {
console.log('\ntoken change detected — rebuilding theme…');
function runBuild(reason = 'token change detected') {
console.log(`\n${reason} — rebuilding theme…`);
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/theme/watch.mjs
Line: 25-26

Comment:
The `runBuild()` function always logs "token change detected" even when called at startup before any file has changed. A developer running `theme:watch` for the first time will see a misleading message.

```suggestion
function runBuild(reason = 'token change detected') {
  console.log(`\n${reason} — rebuilding theme…`);
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +16 to +17
// Run once on start so the generated files are always fresh.
runBuild();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Calling runBuild() without an argument at startup will still print "token change detected" with the default. Pass a descriptive reason so the initial run is clearly distinguished from watch-triggered rebuilds.

Suggested change
// Run once on start so the generated files are always fresh.
runBuild();
// Run once on start so the generated files are always fresh.
runBuild('initial startup');
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/theme/watch.mjs
Line: 16-17

Comment:
Calling `runBuild()` without an argument at startup will still print "token change detected" with the default. Pass a descriptive reason so the initial run is clearly distinguished from watch-triggered rebuilds.

```suggestion
// Run once on start so the generated files are always fresh.
runBuild('initial startup');
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +1 to +9
import { execSync } from 'node:child_process';
/**
* Watches tokens.json and re-runs theme:build whenever it changes.
* Run via: pnpm run theme:watch
* Storybook HMR then picks up the updated theme.css automatically.
*/
import { watch } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The execSync import is placed before the JSDoc comment that describes the module, splitting the import block. Moving it with the other imports keeps the file header readable and consistent.

Suggested change
import { execSync } from 'node:child_process';
/**
* Watches tokens.json and re-runs theme:build whenever it changes.
* Run via: pnpm run theme:watch
* Storybook HMR then picks up the updated theme.css automatically.
*/
import { watch } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
/**
* Watches tokens.json and re-runs theme:build whenever it changes.
* Run via: pnpm run theme:watch
* Storybook HMR then picks up the updated theme.css automatically.
*/
import { execSync } from 'node:child_process';
import { watch } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/theme/watch.mjs
Line: 1-9

Comment:
The `execSync` import is placed before the JSDoc comment that describes the module, splitting the import block. Moving it with the other imports keeps the file header readable and consistent.

```suggestion
/**
 * Watches tokens.json and re-runs theme:build whenever it changes.
 * Run via: pnpm run theme:watch
 * Storybook HMR then picks up the updated theme.css automatically.
 */
import { execSync } from 'node:child_process';
import { watch } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +104 to +119
<div className="emlight flex-1 space-y-6 bg-background p-8">
<p className="text-sm font-medium text-foreground">Light mode</p>
<div className="grid grid-cols-2 gap-3">
{FAMILIES.map((f) => (
<SurfaceCard key={f} family={f} />
))}
</div>
</div>
<div className="emdark flex-1 space-y-6 bg-background p-8">
<p className="text-sm font-medium text-foreground">Dark mode</p>
<div className="grid grid-cols-2 gap-3">
{FAMILIES.map((f) => (
<SurfaceCard key={f} family={f} />
))}
</div>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 With 5 families and grid-cols-2, the last card renders as a lone item in its own row in both the light and dark panes. Using grid-cols-3 would keep the layout balanced (3+2) while still fitting in a half-screen column.

Suggested change
<div className="emlight flex-1 space-y-6 bg-background p-8">
<p className="text-sm font-medium text-foreground">Light mode</p>
<div className="grid grid-cols-2 gap-3">
{FAMILIES.map((f) => (
<SurfaceCard key={f} family={f} />
))}
</div>
</div>
<div className="emdark flex-1 space-y-6 bg-background p-8">
<p className="text-sm font-medium text-foreground">Dark mode</p>
<div className="grid grid-cols-2 gap-3">
{FAMILIES.map((f) => (
<SurfaceCard key={f} family={f} />
))}
</div>
</div>
<div className="emlight flex-1 space-y-6 bg-background p-8">
<p className="text-sm font-medium text-foreground">Light mode</p>
<div className="grid grid-cols-3 gap-3">
{FAMILIES.map((f) => (
<SurfaceCard key={f} family={f} />
))}
</div>
</div>
<div className="emdark flex-1 space-y-6 bg-background p-8">
<p className="text-sm font-medium text-foreground">Dark mode</p>
<div className="grid grid-cols-3 gap-3">
{FAMILIES.map((f) => (
<SurfaceCard key={f} family={f} />
))}
</div>
</div>
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/stories/surfaces.stories.tsx
Line: 104-119

Comment:
With 5 families and `grid-cols-2`, the last card renders as a lone item in its own row in both the light and dark panes. Using `grid-cols-3` would keep the layout balanced (3+2) while still fitting in a half-screen column.

```suggestion
      <div className="emlight flex-1 space-y-6 bg-background p-8">
        <p className="text-sm font-medium text-foreground">Light mode</p>
        <div className="grid grid-cols-3 gap-3">
          {FAMILIES.map((f) => (
            <SurfaceCard key={f} family={f} />
          ))}
        </div>
      </div>
      <div className="emdark flex-1 space-y-6 bg-background p-8">
        <p className="text-sm font-medium text-foreground">Dark mode</p>
        <div className="grid grid-cols-3 gap-3">
          {FAMILIES.map((f) => (
            <SurfaceCard key={f} family={f} />
          ))}
        </div>
      </div>
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant