AgentSkillsCN

runwhen-ui

在用户页面中创建或修改任何 UI 组件、页面或样式时使用。严格遵循 RunWhen 的 MUI++ 设计体系标准。

SKILL.md
--- frontmatter
name: runwhen-ui
description: Use when creating or modifying any UI component, page, or styling in user-pages. Enforces RunWhen's MUI++ design system standards.

RunWhen UI Design System (MUI++)

This skill enforces RunWhen's UI standards when working on frontend code in user-pages/.

ALWAYS apply these standards when:

  • Creating new pages or components
  • Modifying existing UI code
  • Reviewing UI-related pull requests
  • Adding styles, colors, or typography

Core Philosophy

RunWhen UI follows "Stripe structure + Linear lightness":

  • Structured: Clear visual hierarchy, visible borders, organized layouts
  • Light: Font-weight 400, softer colors, generous whitespace
  • Consistent: All values from design tokens, no magic numbers

Brand Color Usage

Primary blue (#0570de) is reserved for interactive elements only:

Use Blue ForDON'T Use Blue For
Buttons (CTAs)Page titles
LinksSection headers
Active tab indicatorLabels
Active filter chipsDecorative elements
Focus rings

Page titles and headers use neutral dark gray (text.primary) - this maintains visual hierarchy where blue draws attention to actions, not static text.


1. Design Tokens

Typography

TokenValueUsage
fontSize.xs12pxCaptions, badges, metadata
fontSize.sm13pxSecondary text, table cells
fontSize.base14pxBody text, form inputs
fontSize.lg15pxSubheadings
fontSize.xl16pxPage titles
fontSize.2xl18pxHero text (rare)
TokenValueUsage
fontWeight.normal400Body text (DEFAULT - keeps it light)
fontWeight.medium500Emphasis, selected states
fontWeight.semibold600Buttons, headings
TokenValueUsage
lineHeight.tight1.3Headings
lineHeight.normal1.5Body text (DEFAULT)
lineHeight.relaxed1.6Long-form content

NEVER use:

  • Font sizes below 12px (accessibility)
  • Font sizes like 11px, 8px, 18px for body text
  • fontFamily: 'Inconsolata' for inputs (use Inter)
  • !important on font properties

Colors

Text Colors (softer, not harsh black):

TokenValueUsage
colors.text.primary#374151Main text
colors.text.secondary#6b7280Secondary/muted text
colors.text.tertiary#9ca3afPlaceholders, disabled
colors.text.muted#d1d5dbVery subtle text

Brand Colors:

TokenValueUsage
colors.primary#0570dePrimary actions, links
colors.primary.hover#0559b3Hover state
colors.primary.light#e0f2feActive chip background

Background Colors:

TokenValueUsage
colors.bg.page#ffffffPage background
colors.bg.subtle#f9fafbCards, sidebar, group rows
colors.bg.hover#f3f4f6Hover states
colors.bg.active#e5e7ebActive/pressed states

Border Colors:

TokenValueUsage
colors.border.light#f3f4f6Subtle separators
colors.border.default#e5e7ebStandard borders
colors.border.strong#d1d5dbEmphasized borders

NEVER use:

  • Hardcoded hex colors like #D6D6D6, #797979
  • rgba(0, 0, 0, 0.04) - use token equivalents
  • Pure black #000000 for text
  • MUI palette references like neutral.darker (migrate to tokens)

Spacing

TokenValue
spacing.14px
spacing.28px
spacing.312px
spacing.416px
spacing.520px
spacing.624px
spacing.832px
spacing.1040px

Component Sizes

TokenValueNotes
sizes.rowHeight44pxStandard list row
sizes.rowHeightCompact40pxTask rows, dense lists
sizes.chipHeight26pxFilter chips, badges
sizes.searchHeight38pxSearch input
sizes.buttonHeight36pxStandard buttons

NEVER use:

  • Row heights of 32px or 36px (too cramped)
  • Chip heights of 16px (too small, accessibility issue)
  • Chip font sizes of 8px (unreadable)

Border Radius

TokenValueUsage
radius.sm4pxSmall elements, badges
radius.md6pxButtons, inputs, chips (DEFAULT)
radius.lg8pxCards, modals
radius.full9999pxAvatars only

NEVER use:

  • borderRadius: 100px for search bars (use radius.md)
  • Inconsistent radius values across similar components

2. Page Structure Template

All pages MUST follow this structure (from concept diagram):

code
┌─────────────────────────────────────────────────────────────┐
│ Page Title | Workspace Name (dropdown)          [Row 1]     │
├─────────────────────────────────────────────────────────────┤
│ Tab1   Tab2   Tab3   Tab4                       [Row 2]     │
├─────────────────────────────────────────────────────────────┤
│ [🔍 Search bar]                    [Hero Button] [Row 3]    │
├─────────────────────────────────────────────────────────────┤
│ [Filter] [Filter] [Filter] [+Filters]           [Row 4]     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                    Content Area                  [Row 5]    │
│                                                             │
│              Formats: List | Form | Other                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Use PageLayout component:

tsx
<PageLayout
  title="Tasks"
  tabs={[
    { label: 'Tasks', value: 'tasks' },
    { label: 'Workflows', value: 'workflows' },
  ]}
  activeTab={activeTab}
  onTabChange={handleTabChange}
  searchPlaceholder="Search tasks..."
  onSearch={handleSearch}
  heroButton={{ label: 'Add Task', onClick: handleAdd }}
  filters={<FilterChips items={filters} />}
>
  {/* Content Area */}
  <DataList rows={rows} />
</PageLayout>

3. Component Standards

PageHeader

tsx
// CORRECT - neutral color, medium weight
<Typography
  sx={{
    fontWeight: tokens.fontWeight.medium,  // 500
    fontSize: tokens.fontSize.xl,          // 16px
    color: tokens.colors.text.primary,     // Dark gray
    letterSpacing: tokens.letterSpacing.tight,
  }}
>
  {pageTitle}
</Typography>

// WRONG - brand color for titles
<Typography
  sx={{
    fontWeight: 300,           // Too light
    fontSize: '18px',          // Hardcoded
    color: 'primary.main',     // Blue - reserve for CTAs
  }}
>
  {pageTitle}
</Typography>

PageHeader specs:

  • Font size: 16px (fontSize.xl)
  • Font weight: 500 (medium) - provides hierarchy
  • Color: text.primary (dark gray) - NOT blue
  • Letter spacing: tight (-0.01em)
  • Border bottom: 1px solid border.light

Tabs (MinimalTabs)

tsx
// Tab label styling
<Typography
  sx={{
    fontSize: tokens.fontSize.sm,           // 13px
    fontWeight: isActive
      ? tokens.fontWeight.medium            // 500 when active
      : tokens.fontWeight.normal,           // 400 when inactive
    color: isActive
      ? tokens.colors.text.primary          // Dark when active
      : tokens.colors.text.secondary,       // Muted when inactive
  }}
>
  {tab.label}
</Typography>

Tab specs:

  • Font size: 13px
  • Inactive: weight 400, text.secondary color
  • Active: weight 500, text.primary color
  • Active indicator: 2px, primary.main color
  • Container border: 1px solid border.light

SearchBar

tsx
// CORRECT
<SearchBar
  placeholder="Search tasks..."
  onSearch={handleSearch}
/>

// WRONG - don't build custom search inputs
<TextField
  sx={{
    '& .MuiInputBase-input': {
      fontFamily: 'Inconsolata',  // NO
      fontSize: '18px !important', // NO
    },
    borderRadius: '100px', // NO - not pill shape
  }}
/>

SearchBar specs:

  • Height: 38px
  • Border radius: 6px (not pill)
  • Font: Inter, 14px, weight 400
  • Border: 1px solid colors.border.default
  • Focus: blue ring with colors.primary

FilterChips

tsx
// CORRECT
<FilterChip
  label="Kubernetes"
  icon={<KubernetesIcon />}
  active={isActive}
  onClick={handleClick}
/>

// WRONG
<Chip
  sx={{
    height: '16px',    // NO - too small
    fontSize: '8px',   // NO - unreadable
  }}
/>

FilterChip specs:

  • Height: 26px
  • Font: 13px, weight 400 (500 when active)
  • Padding: 0 12px
  • Border radius: 6px
  • Active: colors.primary.light background, colors.primary text

Buttons

tsx
// CORRECT - Hero button (primary action)
<ActionButton variant="primary" onClick={handleAdd}>
  Add Task
</ActionButton>

// CORRECT - Secondary button
<ActionButton variant="secondary" onClick={handleCancel}>
  Cancel
</ActionButton>

Button specs:

  • Height: 36px
  • Font: 14px, weight 500
  • Border radius: 6px
  • No uppercase text (textTransform: 'none')
  • Primary: colors.primary background, white text
  • Secondary: colors.primary border, colors.primary text, transparent background
    • Hover: colors.primary.light background, colors.primary.hover border/text

When to use each variant:

  • Primary: Main action on the page (Add Task, Save, Submit)
  • Secondary: Supporting actions (Run, Cancel, Export) - uses blue outline to maintain brand consistency while being visually lighter than primary

DataList / Tree Rows

Row heights:

  • Group rows: 40px, background colors.bg.subtle
  • SLX rows: 44px
  • Task rows: 40px

Row borders:

  • Use 1px solid colors.border.light
  • NOT 0.05px or 0.5px (inconsistent rendering)

Hover actions:

  • Show on row hover
  • Icon buttons: 28px × 28px
  • Icon size: 16px

Badges/Labels

tsx
// CORRECT
<Badge variant="subtle">SLX</Badge>

// WRONG
<Chip
  sx={{
    height: '16px',
    fontSize: '8px',
    borderWidth: '0.5px', // NO
  }}
/>

Badge specs:

  • Height: 20px
  • Font: 12px, weight 400
  • Padding: 0 8px
  • Border radius: 4px
  • Background: colors.bg.subtle
  • Border: 1px solid colors.border.light

4. Verification Workflow

CRITICAL: Code written ≠ task complete. Visual verification is required.

After Creating/Modifying Each Component

  1. Ask user to verify rendered output

    code
    "I've implemented the SearchBar component. Can you check localhost:3002
    and confirm it matches the mockup? Specifically verify:
    - Border is visible (not just an underline)
    - Height is 38px
    - Border radius is 6px (not pill-shaped)"
    
  2. Wait for user confirmation before proceeding

    • Do NOT assume code is correct without visual verification
    • If user reports differences, fix before moving on
  3. Request screenshot if needed

    • "Can you share a screenshot so I can compare to the mockup?"

Before Starting Component Work

  1. Check theme.ts for conflicting defaults

    tsx
    // theme.ts may have defaults that override your component props
    // Example: MuiTextField defaultProps variant: 'standard'
    // This will override variant="outlined" unless you use themeOverrides
    
  2. Always set explicit props, don't rely on defaults

    tsx
    // CORRECT - explicit variant
    <TextField variant="outlined" ... />
    
    // WRONG - relies on default which may be overridden
    <TextField ... />
    
  3. Check themeOverrides.ts for existing overrides

    • Understand what's already configured
    • Add new overrides there, not inline in components

Comparison Checklist

When comparing component to mockup, verify:

  • Overall dimensions match (height, width)
  • Border style matches (color, width, radius)
  • Background color matches
  • Typography matches (size, weight, color)
  • Spacing matches (padding, margin)
  • States match (hover, focus, active, disabled)

5. DataGrid / Tree Row Styling

Row Type Styling

Row TypeHeightBackgroundLabel Style
Group row40pxcolors.bg.subtle12px, weight 500, text.secondary, UPPERCASE, letter-spacing wide
SLX row44pxtransparent13px, weight 400, text.primary
Task row40pxtransparent12px, weight 400, text.secondary

DataGrid Container

tsx
sx={{
  border: `1px solid ${tokens.colors.border.light}`,
  borderRadius: `${tokens.radius.lg}px`,
  overflow: 'hidden',
}}

Row Borders

tsx
// CORRECT - use design tokens
borderBottom: `1px solid ${tokens.colors.border.light}`

// WRONG - inconsistent rendering, not from tokens
borderBottom: '0.05px solid rgba(0, 0, 0, 0.04)'

Group Row Label

tsx
<Typography
  sx={{
    fontSize: tokens.fontSize.xs,        // 12px
    fontWeight: tokens.fontWeight.medium, // 500
    color: tokens.colors.text.secondary,  // #6b7280
    textTransform: 'uppercase',
    letterSpacing: tokens.letterSpacing.wide,
  }}
>
  {groupLabel}
</Typography>

SLX Row Label

tsx
<Typography
  sx={{
    fontSize: tokens.fontSize.sm,        // 13px
    fontWeight: tokens.fontWeight.normal, // 400
    color: tokens.colors.text.primary,    // #374151
  }}
>
  {slxName}
</Typography>

6. Enforcement Checklist

When reviewing or writing UI code, verify:

Typography

  • No font sizes below 12px
  • No hardcoded font sizes (use tokens)
  • Font weight 400 for body text, 500 for emphasis
  • No !important on font properties
  • No custom font families in inputs

Colors

  • No hardcoded hex colors
  • No rgba() colors (use tokens)
  • No palette colors (neutral.dark, etc.) - use tokens
  • Text uses colors.text.* tokens
  • Borders use colors.border.* tokens
  • No pure black (#000000) for text
  • Page titles use text.primary, NOT primary.main (blue)
  • Brand color (blue) reserved for CTAs and interactive elements only

Spacing & Sizing

  • Row heights ≥ 40px
  • Chip heights ≥ 24px
  • Border radius from tokens (4px, 6px, 8px)
  • No pill-shaped inputs (no borderRadius: 100px)
  • Consistent padding using spacing tokens

Components

  • New pages use PageLayout
  • Search uses SearchBar component
  • Filters use FilterChip component
  • No custom implementations of standard components

Borders

  • Border width is 1px (not 0.5px or 0.05px)
  • Border colors from tokens
  • Consistent border-radius

7. Migration Guide

When updating existing code:

Before (Current Style)

tsx
<Typography sx={{ fontSize: 11, color: 'text.secondary' }}>
  {taskName}
</Typography>

<Chip
  sx={{
    height: '16px',
    fontSize: '8px',
    borderWidth: '0.5px',
  }}
/>

<TextField
  sx={{
    '& .MuiInputBase-input': {
      fontFamily: 'Inconsolata',
      fontSize: '18px !important',
    },
    borderRadius: '100px',
  }}
/>

After (MUI++ Style)

tsx
<Typography
  sx={{
    fontSize: tokens.fontSize.sm,
    color: tokens.colors.text.secondary
  }}
>
  {taskName}
</Typography>

<FilterChip label="SLX" />

<SearchBar placeholder="Search tasks..." />

8. File Locations

code
user-pages/
├── styles/
│   ├── theme.ts              # Base MUI theme
│   ├── themeOverrides.ts     # MUI++ component overrides
│   └── designTokens.ts       # Token definitions
│
├── components/
│   ├── layouts/
│   │   └── PageLayout.tsx    # Standard page template
│   │
│   └── ui/                   # Standardized primitives
│       ├── SearchBar.tsx
│       ├── FilterChip.tsx
│       ├── ActionButton.tsx
│       ├── Badge.tsx
│       └── DataList.tsx

9. Component Reuse Pattern

Components are created ONCE and imported everywhere. Pages do NOT rewrite search bars, tabs, or layouts.

How Pages Use Components

tsx
// pages/workspace/[workspace]/tasks/[[...tab]].tsx

import { PageLayout } from '@/components/layouts/PageLayout'
import { SearchBar, FilterChips, ActionButton } from '@/components/ui'

function TasksPage() {
  const [searchQuery, setSearchQuery] = useState('')
  const [activeFilters, setActiveFilters] = useState<string[]>([])

  return (
    <PageLayout
      title="Tasks"
      tabs={TABS}
      activeTab={activeTab}
      onTabChange={handleTabChange}
    >
      {/* Row 3: Search + Hero Button */}
      <PageLayout.ActionBar>
        <SearchBar
          placeholder="Search tasks..."
          value={searchQuery}
          onChange={setSearchQuery}
        />
        <ActionButton onClick={handleAddTask}>
          Add Task
        </ActionButton>
      </PageLayout.ActionBar>

      {/* Row 4: Filters */}
      <PageLayout.Filters>
        <FilterChips
          items={platformFilters}
          active={activeFilters}
          onToggle={handleFilterToggle}
        />
      </PageLayout.Filters>

      {/* Row 5: Content - this is page-specific */}
      <PageLayout.Content>
        <DataGridPro {...dataGridProps} />
      </PageLayout.Content>
    </PageLayout>
  )
}

What's Reusable vs Page-Specific

Reusable (import from ui/)Page-Specific (write in page)
PageLayoutData fetching logic
SearchBarDataGrid columns/rows
FilterChipsBusiness logic handlers
ActionButtonPage-specific dialogs
BadgeCustom visualizations
DataList (basic)Complex data transformations
MinimalTabs

Component API Consistency

All UI components follow consistent patterns:

tsx
// All have similar prop structure
<SearchBar
  value={string}           // Controlled value
  onChange={(val) => void} // Change handler
  placeholder={string}     // Optional placeholder
  disabled={boolean}       // Optional disabled state
/>

<FilterChips
  items={Array}            // Data
  active={Array}           // Selected items
  onToggle={(id) => void}  // Toggle handler
/>

<ActionButton
  variant="primary" | "secondary"
  onClick={() => void}
  loading={boolean}
  disabled={boolean}
>
  {children}
</ActionButton>

10. Common Pitfalls

1. Theme defaults override component props

tsx
// theme.ts has: MuiTextField: { defaultProps: { variant: 'standard' } }

// This renders as 'standard' (underline only), NOT outlined:
<TextField variant="outlined" />  // prop is ignored!

// FIX: Override in themeOverrides.ts
MuiTextField: {
  defaultProps: { variant: 'outlined' }
}

2. Badge variants don't match mockup

tsx
// Mockup shows: background + border together
// Original Badge had:
//   - subtle: background only
//   - outlined: border only, transparent bg

// FIX: Both variants should have background + border
...(variant === 'subtle' && {
  backgroundColor: colors.bg,
  border: `1px solid ${colors.border}`,  // Added
  color: colors.text,
}),

3. Not checking rendered output

code
// WRONG workflow:
1. Write component code
2. Assume it matches mockup
3. Move on  <-- BUG INTRODUCED HERE

// CORRECT workflow:
1. Write component code
2. Ask user to verify in browser
3. User confirms OR reports differences
4. Fix differences
5. Get confirmation
6. Move on

4. Using rgba() instead of tokens

tsx
// WRONG
borderBottom: '0.05px solid rgba(0, 0, 0, 0.04)'

// CORRECT
borderBottom: `1px solid ${tokens.colors.border.light}`

5. Inconsistent row typography

tsx
// Group rows, SLX rows, and task rows need DIFFERENT styles
// Don't copy-paste the same Typography sx across row types
// Refer to Section 5 for correct styles per row type

6. Using brand color for page titles

tsx
// WRONG - blue title dilutes visual hierarchy
<Typography sx={{ color: 'primary.main' }}>
  Tasks
</Typography>

// CORRECT - neutral dark, reserve blue for CTAs
<Typography sx={{ color: tokens.colors.text.primary }}>
  Tasks
</Typography>

Rule: Blue = interactive. Titles = structural. Keep them separate.

7. Using palette colors instead of tokens

tsx
// WRONG - palette colors not from design system
color: 'neutral.dark'
color: 'neutral.darkest'
borderColor: 'rgba(0, 0, 0, 0.08)'

// CORRECT - design tokens
color: tokens.colors.text.secondary
color: tokens.colors.text.primary
borderColor: tokens.colors.border.light

11. Reference: Mockup

Interactive mockup showing the design system: user-pages/mockups/tasks-page-stripe-style.html

Open in browser to compare "Stripe Style (New)" vs "Current Style" and view token values.


12. Scrollable Tables

DO NOT use DataGridPro for simple scrollable tables

DataGridPro has complex internal DOM with multiple nested scrollable containers (.MuiDataGrid-main, .MuiDataGrid-virtualScroller, etc.) that create double scrollbar issues that are extremely difficult to fix with CSS. Only use DataGridPro when you need its advanced features (tree data, checkbox selection, column sorting, virtual row rendering for 1000+ rows).

Use Box-based tables for simple lists

For tables that just need a header row, clickable rows, and scroll — use the UsersPanel pattern: a plain Box with overflowY: 'auto' and maxHeight. This is simpler, more predictable, and guaranteed to produce a single scrollbar.

tsx
// CORRECT — simple scrollable table (UsersPanel pattern)
<Box
  sx={{
    border: `1px solid ${tokens.colors.border.light}`,
    borderRadius: `${tokens.radius.lg}px`,
    overflow: 'hidden',
    display: 'flex',
    flexDirection: 'column',
  }}
>
  {/* Sticky header */}
  <Box
    sx={{
      display: 'flex',
      alignItems: 'center',
      px: 2,
      py: 1.5,
      backgroundColor: tokens.colors.bg.subtle,
      borderBottom: `1px solid ${tokens.colors.border.light}`,
      flexShrink: 0,
    }}
  >
    <Typography sx={{ flex: 3, fontSize: tokens.fontSize.xs, fontWeight: tokens.fontWeight.medium, color: tokens.colors.text.secondary, textTransform: 'uppercase', letterSpacing: tokens.letterSpacing.wide }}>
      Column 1
    </Typography>
    <Typography sx={{ flex: 2, /* same header styles */ }}>
      Column 2
    </Typography>
  </Box>

  {/* Scrollable body — single native scrollbar */}
  <Box
    sx={{
      maxHeight: 340,
      overflowY: 'auto',
      '&::-webkit-scrollbar': { width: '6px' },
      '&::-webkit-scrollbar-track': { background: 'transparent' },
      '&::-webkit-scrollbar-thumb': { background: '#c1c1c1', borderRadius: '3px' },
      '&::-webkit-scrollbar-thumb:hover': { background: '#a8a8a8' },
      scrollbarWidth: 'thin',
      scrollbarColor: '#c1c1c1 transparent',
    }}
  >
    {rows.map((row, idx) => (
      <Box
        key={row.id}
        onClick={() => onSelect(row)}
        sx={{
          display: 'flex',
          alignItems: 'center',
          px: 2,
          py: 1,
          minHeight: `${tokens.sizes.rowHeight}px`,
          borderBottom: idx < rows.length - 1 ? `1px solid ${tokens.colors.border.light}` : 'none',
          cursor: 'pointer',
          transition: `background-color ${tokens.transitions.fast}`,
          '&:hover': { backgroundColor: tokens.colors.bg.subtle },
        }}
      >
        <Typography sx={{ flex: 3 }}>{row.col1}</Typography>
        <Typography sx={{ flex: 2 }}>{row.col2}</Typography>
      </Box>
    ))}
  </Box>
</Box>

// WRONG — DataGridPro for a simple list (scrollbar nightmares)
<DataGridPro rows={rows} columns={columns} />

When to use what

ApproachUse When
Box-based tableSimple list with < 500 rows, clickable rows, no sorting/filtering by column
MUI <TableContainer> + <Table stickyHeader>Need standard HTML table semantics, accessibility, or column alignment
DataGridProTree data, checkbox selection, column sorting/resizing, virtual rendering for 1000+ rows

Scrollbar styling (for any scrollable container)

Always use this pattern to remove the grey scrollbar gutter:

tsx
sx={{
  overflowY: 'auto',
  '&::-webkit-scrollbar': { width: '6px' },
  '&::-webkit-scrollbar-track': { background: 'transparent' },
  '&::-webkit-scrollbar-thumb': { background: '#c1c1c1', borderRadius: '3px' },
  '&::-webkit-scrollbar-thumb:hover': { background: '#a8a8a8' },
  scrollbarWidth: 'thin',
  scrollbarColor: '#c1c1c1 transparent',
}}

DataGridPro scrollbar (when you must use it)

If DataGridPro is required, use the '& *' wildcard pattern to style all internal scrollbars uniformly. Do NOT try to hide/show scrollbars on specific internal containers — DataGridPro manages its own overflow via inline styles that override CSS.

tsx
// CORRECT — style all scrollbars uniformly
<DataGridPro
  sx={{
    overflow: 'hidden',
    '& *': {
      '&::-webkit-scrollbar': { width: '6px' },
      '&::-webkit-scrollbar-track': { background: 'transparent' },
      '&::-webkit-scrollbar-thumb': { background: '#c1c1c1', borderRadius: '3px' },
      scrollbarWidth: 'thin',
      scrollbarColor: '#c1c1c1 transparent',
    },
  }}
/>

// WRONG — targeting individual internal containers
<DataGridPro
  sx={{
    '& .MuiDataGrid-main': { overflow: 'hidden !important' },           // Breaks scroll
    '& .MuiDataGrid-virtualScroller': { scrollbarWidth: 'thin' },       // Creates double scrollbar
  }}
/>

Reference implementations

  • Box-based table: components/settings/UsersPanel.tsx, components/dialogs/add-slx-wizard/CodeBundleBrowser.tsx
  • DataGridPro with scrollbar: pages/workspace/[workspace]/studio/[[...tab]].tsx (Tasks tree)

Last updated: 2026-02-09 Version: 1.3 - Added scrollable table guidelines, Box-based table pattern, scrollbar styling rules