@gradeui/ui 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,15 +2,16 @@
2
2
  name: Button
3
3
  import: "@gradeui/ui"
4
4
  variants: [default, destructive, outline, secondary, ghost, link, raised]
5
- sizes: [sm, md, lg, icon]
5
+ sizes: [2xs, xs, sm, md, lg]
6
6
  props:
7
7
  - variant? (default | destructive | outline | secondary | ghost | link | raised) — `raised` here is a back-compat alias (the raised TRAIT on a neutral key surface); prefer the `raised` prop
8
8
  - raised?: boolean — presence TRAIT: tactile elevation (bevel + drop + hover glow + pressed sink) layered onto ANY variant — raised primary, raised outline, etc. Glow tone reads --btn-glow → --accent-glow → --selected-glow; override per-button via style={{ "--btn-glow": "var(--warning)" }}
9
- - size? (sm | md | lg | icon) — t-shirt scale aligned with Tabs/ToggleGroup heights (sm=h-7, md=h-8, lg=h-10). `default` still works as an alias for `md`.
9
+ - size? (2xs | xs | sm | md | lg) — t-shirt scale aligned with Tabs/ToggleGroup heights (2xs=h-5, xs=h-6, sm=h-7, md=h-8, lg=h-10). 2xs/xs are the dense tool-panel sizes (match Figma Button size=2xs/xs). `default` still works as an alias for `md`.
10
+ - iconOnly?: boolean — squares the button at the current `size` height (w = h, no horizontal padding) for icon-only buttons; the icon child is centered. This is THE way to make a square icon button at any density (sm→28², 2xs→20²).
10
11
  - asChild?: boolean — renders as the child element (use to wrap <a>/<Link>)
11
12
  - disabled?: boolean
12
13
  - All native button HTML attrs (onClick, type, etc.)
13
- when_to_use: Any clickable action. Use size="icon" for square icon-only buttons, variant="link" for inline links that should look like Button, the `raised` prop for high-commitment / weighty actions where the chrome can afford a tactile "physical key" treatment (composes with any variant; variant="raised" remains the neutral-key alias). A Button placed next to a TabsList of the same size lines up edge-to-edge without per-call overrides.
14
+ when_to_use: Any clickable action. Use `iconOnly` for square icon-only buttons (at any size), variant="link" for inline links that should look like Button, the `raised` prop for high-commitment / weighty actions where the chrome can afford a tactile "physical key" treatment (composes with any variant; variant="raised" remains the neutral-key alias). A Button placed next to a TabsList of the same size lines up edge-to-edge without per-call overrides.
14
15
  composes_with: [Dialog, DropdownMenu, Tooltip, Card (in CardFooter), Row, Form controls]
15
16
  aliases: [button, push button, plain button, bordered button, destructive button, capsule button, link button, action button, cta, raised button, pill button, key button]
16
17
  ---
@@ -18,7 +19,8 @@ aliases: [button, push button, plain button, bordered button, destructive button
18
19
  ```jsx
19
20
  <Button>Save</Button>
20
21
  <Button variant="outline" size="sm">Cancel</Button>
21
- <Button size="icon" variant="ghost"><Mail /></Button>
22
+ <Button iconOnly variant="ghost"><Mail /></Button>
23
+ <Button size="sm" iconOnly variant="outline"><Plus /></Button>
22
24
  ```
23
25
 
24
26
  ```jsx
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: ColorPicker
3
+ import: "@gradeui/ui"
4
+ props:
5
+ - value?: string | null — a Grade colour token NAME ("action/primary"), the literal "transparent", or null when nothing is picked
6
+ - onValueChange?: (value: string | null) => void — fired with the next value (token name, "transparent", or null)
7
+ - tokens?: { group, tokens }[] — token families offered in the list; defaults to the Grade semantic set (surface / action / status)
8
+ - searchable?: boolean — show the search input (default true)
9
+ - triggerVariant? (default | inline) — default = form-control surface (swatch + name); inline = just a clickable swatch for inspector / fill-row use
10
+ - placeholder?: string — trigger text when nothing is selected
11
+ - searchPlaceholder?: string — search-input placeholder
12
+ - emptyMessage?: string — shown when search returns no rows
13
+ - allowTransparent?: boolean — include a Transparent option at the top (default true)
14
+ - align? (start | center | end) — popover alignment (default start)
15
+ - disabled?: boolean — lock to a read-only display of the current value
16
+ when_to_use: The token-led single-select colour picker — the focused "pick one colour token" sibling of FillPicker's solid tab. Use it anywhere a value is ONE Grade colour token (a fill colour, a border colour, an accent override) rather than a full paint. Composes Popover + Command exactly like Combobox, but each row is a Swatch + the token's short name, grouped by family and searchable. triggerVariant="inline" reduces the trigger to a single clickable swatch — reach for that inside inspectors and the FillSection fill rows. For a full paint (gradient / image / shader) use FillPicker; for a list of fills use FillSection; for a multi-stop gradient use GradientEditor.
17
+ composes_with: [Popover, Command, Swatch, FillSection, GradientEditor, Field, PropertyList]
18
+ aliases: [color picker, colour picker, token picker, colour token picker, color token picker, swatch picker, paint colour, fill colour picker, accent picker, colour dropdown]
19
+ ---
20
+
21
+ ```jsx
22
+ // Token-led colour field.
23
+ <ColorPicker value={color} onValueChange={setColor} />
24
+ ```
25
+
26
+ ```jsx
27
+ // Inline swatch trigger — the inspector / fill-row affordance.
28
+ <ColorPicker
29
+ triggerVariant="inline"
30
+ value={stopColor}
31
+ onValueChange={setStopColor}
32
+ aria-label="Stop colour"
33
+ />
34
+ ```
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: Combobox
3
+ import: "@gradeui/ui"
4
+ props:
5
+ - options: { value, label, icon?, keywords?, disabled? }[] — the selectable pool
6
+ - value?: string | null — controlled selection (wire onValueChange)
7
+ - defaultValue?: string | null — uncontrolled initial selection
8
+ - onValueChange?: (next: string | null) => void — fired with the next value, or null when cleared
9
+ - placeholder?: string — trigger text when nothing is selected
10
+ - searchPlaceholder?: string — search-input placeholder
11
+ - emptyMessage?: string — shown when search returns no rows
12
+ - searchable?: boolean — show the search input (default true)
13
+ - clearable?: boolean — add a Clear row so the value can return to unset
14
+ - triggerVariant?: "default" | "inline" — default = form-control surface (like Select); inline = chrome-free token trigger
15
+ - renderValue?: (option) => ReactNode — render the selected value yourself (e.g. a Badge); falls back to icon + label
16
+ - hideChevron?: boolean — drop the trailing chevron (inline token look)
17
+ - disabled?: boolean — lock to a read-only display of the current value
18
+ - align?: "start" | "center" | "end" — popover alignment
19
+ when_to_use: Single-pick searchable picker — the single-select sibling of MultiSelect and the Linear "selectable badge" pattern (status / priority / assignee). Use triggerVariant="inline" with renderValue returning a Badge to make a value read as a clickable token that opens a command menu. For multiple selection use MultiSelect; for a small fixed list with no search use Select; for free-form command palettes use Command directly. Pass disabled (driven by a permission check) to show the value without letting the user edit it.
20
+ composes_with: [Popover, Command, Badge, Avatar, PropertyList, Table, Field]
21
+ aliases: [combobox, single select, searchable select, picker, status picker, priority picker, assignee picker, command select, autocomplete, dropdown select, selectable badge, inline select, token select, linear combobox]
22
+ ---
23
+
24
+ ```jsx
25
+ <Combobox
26
+ options={[
27
+ { value: "low", label: "Low" },
28
+ { value: "medium", label: "Medium" },
29
+ { value: "high", label: "High" },
30
+ ]}
31
+ defaultValue="low"
32
+ placeholder="Set priority"
33
+ />
34
+ ```
35
+
36
+ ```jsx
37
+ // Linear-style: the value IS the trigger.
38
+ <Combobox
39
+ triggerVariant="inline"
40
+ hideChevron
41
+ options={priorityOptions}
42
+ value={priority}
43
+ onValueChange={setPriority}
44
+ renderValue={(opt) => <Badge variant="warning-soft">{opt.label}</Badge>}
45
+ />
46
+ ```
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: DataView
3
+ import: "@gradeui/ui"
4
+ props:
5
+ - data: T[] — the rows
6
+ - columns: { key, header, type?, options?, cell?, role?, sortable?, pinned?, width?, align?, hideable?, defaultHidden? }[] — the schema; one list drives table, cards, and grid
7
+ - getRowId?: (row, i) => string — defaults to row.id
8
+ - view? / defaultView? / onViewChange?: "table" | "cards" | "grid" — controlled or uncontrolled view
9
+ - views?: ("table" | "cards" | "grid")[] — allowed views; one entry = single view, no toggle
10
+ - activeId? / defaultActiveId? / onActiveChange?: string | null — the selected row; click emits it
11
+ - sorting? / defaultSorting? / onSortingChange? — TanStack SortingState
12
+ - columnVisibility? / defaultColumnVisibility? / onColumnVisibilityChange? — which fields show
13
+ - stickyHeader?: boolean — freeze the header row on scroll
14
+ - toolbar?: boolean — render the built-in columns menu + view toggle above the view
15
+ - renderCard?: (row, { active }) => ReactNode — override card / grid tiles
16
+ - emptyMessage?: ReactNode
17
+ when_to_use: One dataset, drawn as a table, a list of cards, or a grid — without re-typing the TanStack boilerplate (sortable headers, flexRender, selection, view switch) on every page. Hand it data + a columns schema; columns declare a `type` (badge/tags/number/currency/percent/date/boolean/url/text) that DataView renders, with a `cell` override for bespoke cells (avatars, relations). The view toggle can live anywhere — `useDataView()` holds the state so a `<DataViewToggle>` or `<DataViewColumns>` in a page header drives a `<DataView>` lower down. Mark a column `pinned="left"` (with a `width`) for a fixed column and `stickyHeader` to freeze the header. For a single record's fields use PropertyList; for the raw table primitive use Table.
18
+ composes_with: [Table, Card, Badge, Avatar, ToggleGroup, DropdownMenu, PropertyList, Combobox]
19
+ aliases: [data view, data table, datatable, data grid, dataview, table view, card view, grid view, list view, gallery, records list, master list, tanstack table, sortable table, column visibility, pinned column, frozen column, sticky header, view switcher]
20
+ ---
21
+
22
+ ```jsx
23
+ const dv = useDataView({ defaultView: "table", defaultActiveId: rows[0].id });
24
+
25
+ // The toggle / columns menu can live anywhere — they just read dv.
26
+ <Row justify="between">
27
+ <h1>Alerts</h1>
28
+ <Row gap="sm">
29
+ <DataView.Columns columns={columns} visibility={dv.columnVisibility} onVisibilityChange={dv.setColumnVisibility} />
30
+ <DataView.Toggle value={dv.view} onChange={dv.setView} views={dv.views} />
31
+ </Row>
32
+ </Row>
33
+
34
+ <DataView
35
+ data={rows}
36
+ columns={columns}
37
+ view={dv.view}
38
+ activeId={dv.activeId}
39
+ onActiveChange={dv.setActiveId}
40
+ sorting={dv.sorting}
41
+ onSortingChange={dv.setSorting}
42
+ columnVisibility={dv.columnVisibility}
43
+ onColumnVisibilityChange={dv.setColumnVisibility}
44
+ stickyHeader
45
+ />
46
+ ```
47
+
48
+ ```jsx
49
+ // Self-contained: built-in toolbar, single column pinned, table only.
50
+ <DataView
51
+ data={rows}
52
+ toolbar
53
+ columns={[
54
+ { key: "name", header: "Name", role: "title", pinned: "left", width: 220 },
55
+ { key: "status", header: "Status", type: "badge", options: statusOptions, sortable: true },
56
+ { key: "arr", header: "ARR", type: "currency", align: "end", sortable: true },
57
+ ]}
58
+ />
59
+ ```
@@ -1,12 +1,16 @@
1
1
  ---
2
2
  name: FillPicker
3
3
  import: "@gradeui/ui"
4
+ subcomponents: [FillSection]
4
5
  props:
5
6
  - value: FillValue — current paint ({ type, color?, gradient?, src?, fit?, repeat?, tileSize?, preset?, palette?, postPreset?, opacity? }) (required)
6
7
  - onChange: (value: FillValue) => void — called on any change (required)
7
- when_to_use: Grade's paint picker the control for choosing a frame's background fill, modelled on Figma's fill popover. A fill-type icon row (solid · gradient · image · pattern · video · shader) switches the panel below; a global opacity sits at the foot. Emits a FillValue that maps 1:1 onto BackgroundFill props. This is a Studio/inspector chrome control pair it with BackgroundFill, which renders the chosen paint. Not for app content.
8
- composes_with: [BackgroundFill (renders the FillValue), Popover (host it in a popover), ShaderPresetPicker (the shader tab), the inspector Fill section]
9
- aliases: [fill picker, paint picker, background picker, fill chooser, fill popover]
8
+ - FillSection: value — FillValue[]the ordered list of fills to stack as rows
9
+ - FillSection: onChange (value: FillValue[]) => void fired with the next list on add / edit / remove / visibility toggle
10
+ - FillSection: title?: string section heading (default "Fills")
11
+ when_to_use: Grade's paint picker — the control for choosing a frame's background fill, modelled on Figma's fill popover. A fill-type icon row (solid · gradient · image · pattern · video · shader) switches the panel below; a global opacity sits at the foot. Emits a FillValue that maps 1:1 onto BackgroundFill props. This is a Studio/inspector chrome control — pair it with BackgroundFill, which renders the chosen paint. Not for app content. Use the FillSection subcomponent to edit a LIST of fills (the Figma "Fill" inspector section): each row is a Solid/Gradient/Image toggle, the matching value control (ColorPicker / GradientEditor popover / image URL), an opacity %, a visibility eye, and a remove button, with an add button in the header.
12
+ composes_with: [BackgroundFill (renders the FillValue), Popover (host it in a popover), ColorPicker (the solid value), GradientEditor (the gradient value), ShaderPresetPicker (the shader tab), the inspector Fill section]
13
+ aliases: [fill picker, paint picker, background picker, fill chooser, fill popover, fill section, fill list, fills inspector, paint section]
10
14
  notes: |
11
15
  Grade is token-led, so the solid + gradient tabs lead with theme-token
12
16
  swatches (`primary`, `accent`, `secondary`, `muted`, `card`,
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: GradientEditor
3
+ import: "@gradeui/ui"
4
+ props:
5
+ - value: { type, angle?, stops } — the structured gradient (type linear/radial/angular, optional angle in deg, ordered stops). NOT a CSS string — render the string via gradientToCss(value).
6
+ - onChange: (value) => void — fired with the next structured gradient on any edit
7
+ when_to_use: Edit a multi-stop CSS gradient with token-led stops. A type Select (Linear / Radial / Angular) with reverse + rotate actions, a live full-width preview bar (a Swatch type="gradient"), then a Stops list where each stop is a position %, a colour (ColorPicker token or raw), an opacity %, and a remove button; an add button appends a stop. Token stops resolve to oklch(var(--<token>)) so the preview re-voices with the theme. Emits the structured GradientValue (kept editable + serialisable); the caller turns it into CSS with the exported gradientToCss(value). Use inside a Popover from a FillSection gradient row, or standalone in a theme builder. For a single solid colour use ColorPicker; for a full paint (solid / gradient / image / shader) use FillPicker.
8
+ composes_with: [Select, Button, Input, ColorPicker, Swatch, Popover, FillSection]
9
+ aliases: [gradient editor, gradient picker, gradient builder, css gradient editor, stop editor, gradient stops, linear gradient editor, conic gradient editor]
10
+ ---
11
+
12
+ ```jsx
13
+ <GradientEditor
14
+ value={{
15
+ type: "linear",
16
+ angle: 90,
17
+ stops: [
18
+ { id: "a", position: 0, token: "action/primary", opacity: 1 },
19
+ { id: "b", position: 100, token: "action/accent", opacity: 1 },
20
+ ],
21
+ }}
22
+ onChange={setGradient}
23
+ />
24
+ ```
25
+
26
+ ```jsx
27
+ // Render the CSS string for a background.
28
+ import { gradientToCss } from "@gradeui/ui";
29
+ <div style={{ background: gradientToCss(gradient) }} />
30
+ ```
@@ -90,3 +90,9 @@ ANTI-PATTERNS — don't do these:
90
90
  - DO NOT render >500 markers without clustering. The component warns in dev. For larger datasets, drop to `.instance` and use the provider's clustering layer.
91
91
 
92
92
  Markers are DOM — children inherit `--gds-*` tokens. Drop a `<Card>`, `<Badge>`, `<Avatar>`, or anything else inside `<MapMarker>` and it themes correctly.
93
+
94
+ Stacking inside a marker follows normal DOM order on every provider — you do NOT need `z-index` hacks to layer content (e.g. a count label sitting on top of an inline pin-shield `<svg>`). The DS neutralizes Leaflet's vendor rule that sets `z-index: 200` on map `<svg>` elements (via `[data-gds-part="map-marker-content"] svg { z-index: auto }` in `globals.css`); without it, an inline SVG would paint above later siblings on Leaflet (the default provider) but not on Mapbox/MapLibre/Google, making the same marker markup look provider-dependent. If you nest an inline SVG behind text in a marker, just rely on source order.
95
+
96
+ For a floating text label over busy tiles, add the `gds-map-label` class — it applies a mode-aware halo (`--gds-map-label-halo`: white stroke on light tiles, near-black on dark) so the label never washes out. Don't hard-code a white `-webkit-text-stroke`.
97
+
98
+ Note: `<Map>` carries no border-radius or border of its own — it's an unopinionated primitive (the container clips with `overflow: hidden`). Round or frame it from the call site with `className` (e.g. `rounded-xl border`).
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: PropertyList
3
+ import: "@gradeui/ui"
4
+ props:
5
+ - layout?: "row" | "stack" — row (default): label column beside value; stack: label above value for narrow panels
6
+ - density?: "compact" | "default" | "relaxed" — row rhythm
7
+ - align?: "start" | "center" — default vertical alignment of label vs value; use start when values wrap (tag groups, multi-line)
8
+ - divider?: boolean — hairline rule between rows
9
+ - labelWidth?: string — override the label column width (any CSS length); sets --gds-property-list-label-width
10
+ - children: PropertyList.Row[]
11
+ when_to_use: Read-only display of the properties of a SINGLE item — detail panels, inspectors, "about this" cards, order/record summaries. It is a Table row transposed (schema vertical, one record). The value side is a polymorphic slot, so the same renderers that fill a Table cell (text, Badge, tag group, Avatar stack, date, link) drop straight into a row. For an EDITABLE field (label + control) use Field instead; a panel that flips between read and edit swaps a PropertyList for a stack of Fields.
12
+ composes_with: [Badge, Avatar, Table, Field, Separator, Card]
13
+ aliases: [property list, properties, property panel, description list, definition list, detail list, key value, key-value, data list, field list, attributes, metadata list, record summary, detail panel, inspector fields, spec list]
14
+ ---
15
+
16
+ ```jsx
17
+ <PropertyList>
18
+ <PropertyList.Row label="Status" icon={<Activity />}>
19
+ <Badge variant="warning-soft">Low</Badge>
20
+ </PropertyList.Row>
21
+ <PropertyList.Row label="Published">2026-06-18</PropertyList.Row>
22
+ <PropertyList.Row label="Owner">
23
+ <Avatar className="h-5 w-5"><AvatarFallback>EO</AvatarFallback></Avatar>
24
+ </PropertyList.Row>
25
+ </PropertyList>
26
+ ```
27
+
28
+ ```jsx
29
+ <PropertyList density="compact" divider align="start">
30
+ <PropertyList.Row label="Topics">
31
+ <Row gap="xs" wrap>
32
+ <Badge variant="secondary">Pricing</Badge>
33
+ <Badge variant="secondary">Onboarding</Badge>
34
+ </Row>
35
+ </PropertyList.Row>
36
+ <PropertyList.Row label="Business profiles">
37
+ <Row gap="xs" wrap>
38
+ <Badge variant="outline">Acme</Badge>
39
+ <Badge variant="outline">Kite</Badge>
40
+ </Row>
41
+ </PropertyList.Row>
42
+ </PropertyList>
43
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: Section
3
+ import: "@gradeui/ui"
4
+ subcomponents: [Container, SectionEyebrow, SectionTitle, SectionSubtitle, SectionDescription, SectionActions, SectionMedia]
5
+ props:
6
+ - Section: scope? (default | inverse | brand | accent | muted | card) — colour SUBTHEME; applies the `scope-*` class so the whole band re-tones (bg/fg/card/muted/border) while action colours stay vivid. Unset = the page surface. See STUDIO-COLOR.md.
7
+ - Section: background?: ReactNode — visual band background slot: image / video / gradient / shader (drop a <BackgroundFill> here). Renders BEHIND the content; Section owns the relative/overflow/z plumbing. Works with `scope` (which re-tones the content tokens so text stays legible over the media).
8
+ - Section: pad? (none | sm | md | lg | xl) — vertical rhythm (responsive py); default lg. Section is ALWAYS full width — it never sets a max width.
9
+ - Section: as? (section | header | footer | div) — semantic element; default section.
10
+ - Container: maxW? (sm | md | lg | xl | prose | full) — centred max width + gutters; default lg. The MEASURE.
11
+ - Container: grid?: boolean — snap children to a 12-column grid (use `col-span-*` on children); default false.
12
+ - Container: as? (div | section) — semantic element; default div.
13
+ when_to_use: THE page scaffold. A page is an ordered stack of Sections — every distinct band (hero, logos, features, pricing, testimonial, CTA, footer) gets its OWN Section so each is independently themeable. `Section` is the full-width band (scope + vertical rhythm); drop a `Container` inside it for a measure, or omit the Container for a full-bleed band. Reach for Section/Container instead of hand-rolling `<section className="py-20"><div className="max-w-7xl mx-auto px-6">`. The content inside is free — use the parts (SectionEyebrow/Title/Subtitle/Description/Actions/Media) for the common heading+copy+CTA+media shape, or drop any JSX. SectionMedia is a slot for any media (MediaSurface image, Carousel, VideoPlayer, embed, or a whole app UI). Don't use Section for app chrome — that's AppShell.
14
+ composes_with: [Container, MediaSurface, Carousel, VideoPlayer, Button, Badge, Card, Grid, Stack]
15
+ aliases: [section, band, hero section, page section, content section, marketing section, landing section, full bleed, container, max width wrapper, page band, section block]
16
+ ---
17
+
18
+ ```jsx
19
+ // A page is a stack of Sections. Each band picks a scope; a Container
20
+ // holds the measure (omit it to let the band bleed full-width).
21
+ <Section scope="inverse" pad="xl">
22
+ <Container maxW="lg">
23
+ <SectionEyebrow>New</SectionEyebrow>
24
+ <SectionTitle>Use the agent you prefer.</SectionTitle>
25
+ <SectionSubtitle>Own the components. Ship on your subscription.</SectionSubtitle>
26
+ <SectionActions>
27
+ <Button size="lg">Open Studio</Button>
28
+ <Button size="lg" variant="outline">Docs</Button>
29
+ </SectionActions>
30
+ </Container>
31
+ </Section>
32
+ ```
33
+
34
+ ```jsx
35
+ // Full-bleed media band — no Container, so the media spans edge to edge.
36
+ // The scope re-tones the band; the media frames itself.
37
+ <Section scope="card" pad="lg">
38
+ <SectionMedia>
39
+ <MediaSurface hint="Studio canvas" alt="A generated screen" className="aspect-[21/9] w-full" />
40
+ </SectionMedia>
41
+ </Section>
42
+ ```
43
+
44
+ ```jsx
45
+ // Contained content on a grid — children snap to the 12-col Container grid.
46
+ <Section pad="lg">
47
+ <Container grid>
48
+ <div className="col-span-12 md:col-span-7">{/* lead */}</div>
49
+ <div className="col-span-12 md:col-span-5">{/* aside */}</div>
50
+ </Container>
51
+ </Section>
52
+ ```
@@ -2,11 +2,14 @@
2
2
  name: Swatch
3
3
  import: "@gradeui/ui"
4
4
  subcomponents: [SwatchGroup]
5
- sizes: [xs, sm, md, lg, xl]
5
+ sizes: [2xs, xs, sm, md, lg, xl]
6
6
  props:
7
7
  - color?: string — any raw CSS colour (`#1f6feb`, `oklch(...)`, `rgb(...)`, or `var(--x)`). Takes precedence over `token`. Use for one-off or external colours.
8
8
  - token?: string — a Grade colour token NAME with no `--` and no `oklch()` wrap; resolved internally to `oklch(var(--<token>))`. THE design-system path — e.g. `token="brand-3"`, `token="primary"`, `token="chart-2"`. Re-voices live when the theme changes.
9
- - size? (xs | sm | md | lg | xl) t-shirt scale, 20px 56px; default md (32px). Prefer over h-*/w-* utilities.
9
+ - type? (solid | gradient | image) fill kind; default solid (or inferred from `image` / `gradient`). Determines what the chip renders in place.
10
+ - gradient?: string — CSS gradient for `type="gradient"`, e.g. `linear-gradient(135deg,#6366f1,#ec4899)`.
11
+ - image?: string — image URL for `type="image"`; rendered cover-fit behind the chip.
12
+ - size? (2xs | xs | sm | md | lg | xl) — t-shirt scale, 16px → 56px; default md (32px). 2xs (16px) suits dense colour lists. Prefer over h-*/w-* utilities.
10
13
  - shape? (square | rounded | circle) — default rounded (rides `--radius`); circle for dot pickers; square for a hard tile.
11
14
  - selected?: boolean — draws the shared selection ring (`--selected`). For palette / accent pickers.
12
15
  - onSelect?: () => void — makes the swatch a pickable <button> (adds aria-pressed, focus ring, hover lift). Omit for a static display chip.
@@ -2,15 +2,15 @@
2
2
  name: ToggleGroup
3
3
  import: "@gradeui/ui"
4
4
  subcomponents: [ToggleGroupItem]
5
- variants: [default, outline]
6
- sizes: [sm, md, lg]
5
+ variants: [default, outline, segmented]
6
+ sizes: [2xs, xs, sm, md, lg]
7
7
  props:
8
8
  - ToggleGroup: type: "single" | "multiple" — single picks one, multiple picks any number
9
9
  - ToggleGroup: value?: string | string[] — controlled; matches `type` (string for single, string[] for multiple)
10
10
  - ToggleGroup: defaultValue?: string | string[] — uncontrolled initial
11
11
  - ToggleGroup: onValueChange?: (value: string | string[]) => void
12
- - ToggleGroup: size? (sm | md | lg, default md) — cascades to every ToggleGroupItem via context, matches Tabs/Button heights
13
- - ToggleGroup: variant? (default | outline)
12
+ - ToggleGroup: size? (2xs | xs | sm | md | lg, default md) — cascades to every ToggleGroupItem via context, matches Tabs/Button heights; 2xs/xs are the dense tool-panel sizes (2xs also drops text to text-2xs and icons to size-3 so labelled items read at panel density)
13
+ - ToggleGroup: variant? (default | outline | segmented) — segmented sits the items in a muted track with the active item as a soft raised pill, so it reads like a TabsList; reach for it in dense property panels (e.g. a Row/Stack direction toggle)
14
14
  - ToggleGroupItem: value: string — what the group reports when this item is pressed
15
15
  - ToggleGroupItem: tooltip?: ReactNode — when set, wraps the item in a Tooltip; required for icon-only items where the visible chrome doesn't carry a label
16
16
  - ToggleGroupItem: tooltipSide? ("top" | "right" | "bottom" | "left", default "top") — side the tooltip renders on
@@ -41,3 +41,20 @@ aliases: [toggle group, segmented control, segmented buttons, button group, pill
41
41
  <ToggleGroupItem value="underline" aria-label="Underline"><Underline /></ToggleGroupItem>
42
42
  </ToggleGroup>
43
43
  ```
44
+
45
+ ```jsx
46
+ // Segmented variant + 2xs — a dense property-panel toggle. Reads like a
47
+ // tab strip (muted track, active pill) but emits a value, so it's a form
48
+ // control, not panel-switching. This is the Studio Row/Stack control.
49
+ <ToggleGroup
50
+ type="single"
51
+ variant="segmented"
52
+ size="2xs"
53
+ value={direction}
54
+ onValueChange={(v) => v && setDirection(v)}
55
+ className="w-full"
56
+ >
57
+ <ToggleGroupItem value="row" className="flex-1"><Columns3 /> Row</ToggleGroupItem>
58
+ <ToggleGroupItem value="col" className="flex-1"><Rows3 /> Stack</ToggleGroupItem>
59
+ </ToggleGroup>
60
+ ```