@motion-proto/live-tokens 0.39.0 → 0.40.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.
@@ -11,10 +11,18 @@ For composing a page once you've picked components, see [[live-tokens-build-page
11
11
 
12
12
  ## Catalogue
13
13
 
14
- Action: `Button`. Input: `Input`. Selection: `SegmentedControl`, `TabBar`, `RadioButton`, `MenuSelect`, `Toggle`. Containers: `Card`, `CollapsibleSection`, `Dialog`. Messaging: `Callout`, `Notification`, `Tooltip`, `Badge`, `CornerBadge`. Display: `Table`, `Image`, `ImageLightbox`, `ProgressBar`, `SectionDivider`, `SideNavigation`, `CodeSnippet`.
14
+ Action: `Button`, `IconButton`. Input: `Input`. Selection: `SegmentedControl`, `TabBar`, `RadioButton`, `MenuSelect`, `Toggle`. Containers: `Card`, `CollapsibleSection`, `Dialog`. Messaging: `Callout`, `Notification`, `Tooltip`, `Badge`, `CornerBadge`. Display: `Table`, `Image`, `ImageLightbox`, `ProgressBar`, `SectionDivider`, `SideNavigation`, `CodeSnippet`.
15
15
 
16
16
  `CodeSnippet` is for a single-line command or value the user is meant to copy and paste back into a terminal (install commands, generated keys, ids). Click-to-copy with a brief "Copied" popover. Use it whenever your page asks the reader to *run* something, rather than just *read* it.
17
17
 
18
+ ## Action family: Button vs IconButton
19
+
20
+ Both trigger an action and share the same six variants (primary, secondary, outline, success, danger, warning), three states (default, hover, disabled) and two sizes (default, small). They differ only in content.
21
+
22
+ - `Button` carries a text label, optionally with a leading or trailing icon. Use it whenever the action needs a word to be unambiguous.
23
+ - `IconButton` is icon-only and square. Use it for compact, space-constrained actions whose meaning is obvious from the glyph alone (toolbar controls, close/edit/delete affordances, card overflow menus). It has no text slot, so an `ariaLabel` is required for accessibility.
24
+ - **Don't reach for `IconButton` when the icon's meaning isn't self-evident.** A labelled `Button` (or a `Button` with an icon) avoids the guessing game.
25
+
18
26
  ## Single-selection family: SegmentedControl vs TabBar vs RadioButton vs MenuSelect
19
27
 
20
28
  All four pick one option from a set. The right one depends on **option count**, **whether the selection changes what's rendered below**, and **how much visual weight** you want.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.40.0 — New IconButton component
4
+
5
+ ### Added
6
+
7
+ - **`IconButton`, an icon-only sibling of `Button`.** It shares Button's six
8
+ variants (primary, secondary, outline, success, danger, warning), three states
9
+ (default, hover, disabled), and two sizes (default, small), but renders a
10
+ single icon with no text. It is square (symmetric padding plus `aspect-ratio`),
11
+ exposes the icon colour as a first-class per-variant, per-state token, and
12
+ drops Button's text-typography properties. Its tokens live in their own
13
+ `--iconbutton-*` namespace, so styling it never affects Button. Because the
14
+ control has no visible text, `ariaLabel` is required. Editable in the editor
15
+ under Components, with the same linked base block (padding, radius, border
16
+ width, icon size) that links across variants.
17
+
18
+ ### Notes
19
+
20
+ - Additive only. No token renames or `tokens.css` migration, so existing
21
+ consumers are unaffected; the new component ships its defaults in its
22
+ `:global(:root)` block like every other.
23
+
3
24
  ## 0.39.0 — One unified palette model (no gray "mode")
4
25
 
5
26
  ### Changed (breaking)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -0,0 +1,175 @@
1
+ <script module lang="ts">
2
+ import { buildSiblings } from './scaffolding/siblings';
3
+ import type { Token } from './scaffolding/types';
4
+
5
+ export const component = 'iconbutton';
6
+
7
+ const variants = ['primary', 'secondary', 'outline', 'success', 'danger', 'warning'] as const;
8
+ type Variant = typeof variants[number];
9
+ const stateNames = ['default', 'hover', 'disabled'] as const;
10
+ type StateName = typeof stateNames[number];
11
+ function statePrefix(v: Variant, s: StateName): string {
12
+ return s === 'default' ? `--iconbutton-${v}` : `--iconbutton-${v}-${s}`;
13
+ }
14
+
15
+ // Shape. Icon-only, so unlike Button there is no text typography here — the
16
+ // base part carries only frame geometry, which links across variants.
17
+ function variantBaseTokens(v: Variant): Token[] {
18
+ return [
19
+ { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--iconbutton-${v}-padding` },
20
+ { label: 'padding-top', canBeLinked: true, groupKey: 'padding-top', variable: `--iconbutton-${v}-padding-top`, hidden: true },
21
+ { label: 'padding-right', canBeLinked: true, groupKey: 'padding-right', variable: `--iconbutton-${v}-padding-right`, hidden: true },
22
+ { label: 'padding-bottom', canBeLinked: true, groupKey: 'padding-bottom', variable: `--iconbutton-${v}-padding-bottom`, hidden: true },
23
+ { label: 'padding-left', canBeLinked: true, groupKey: 'padding-left', variable: `--iconbutton-${v}-padding-left`, hidden: true },
24
+ { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--iconbutton-${v}-radius` },
25
+ { label: 'border width', canBeLinked: true, groupKey: 'border-width', variable: `--iconbutton-${v}-border-width` },
26
+ { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: `--iconbutton-${v}-icon-size` },
27
+ ];
28
+ }
29
+
30
+ function variantStateTokens(v: Variant, s: StateName): Token[] {
31
+ const iconVar = s === 'default' ? `--iconbutton-${v}-icon` : `--iconbutton-${v}-${s}-icon`;
32
+ return [
33
+ { label: 'surface color', groupKey: 'surface', variable: `${statePrefix(v, s)}-surface` },
34
+ { label: 'border color', groupKey: 'border', variable: `${statePrefix(v, s)}-border` },
35
+ { label: 'icon color', groupKey: 'icon', variable: iconVar },
36
+ ];
37
+ }
38
+
39
+ // Outline is the only variant that paints a surface tint on :active.
40
+ const outlineActiveTokens: Token[] = [
41
+ { label: 'surface color', groupKey: 'surface', variable: '--iconbutton-outline-active-surface' },
42
+ ];
43
+
44
+ function variantStates(v: Variant): Record<string, Token[]> {
45
+ const out: Record<string, Token[]> = {};
46
+ out.base = variantBaseTokens(v);
47
+ out.default = variantStateTokens(v, 'default');
48
+ out.hover = variantStateTokens(v, 'hover');
49
+ if (v === 'outline') out.active = outlineActiveTokens;
50
+ out.disabled = variantStateTokens(v, 'disabled');
51
+ return out;
52
+ }
53
+
54
+ // Small-size schema. One shared spec across all variants (matches the runtime
55
+ // `.small` rule, which is variant-agnostic). Per-side padding rows are hidden
56
+ // and surface only when the editor splits the padding control.
57
+ const smallStates: Record<string, Token[]> = {
58
+ small: [
59
+ { label: 'padding', groupKey: 'small-padding', variable: '--iconbutton-small-padding' },
60
+ { label: 'padding-top', groupKey: 'small-padding-top', variable: '--iconbutton-small-padding-top', hidden: true },
61
+ { label: 'padding-right', groupKey: 'small-padding-right', variable: '--iconbutton-small-padding-right', hidden: true },
62
+ { label: 'padding-bottom', groupKey: 'small-padding-bottom', variable: '--iconbutton-small-padding-bottom', hidden: true },
63
+ { label: 'padding-left', groupKey: 'small-padding-left', variable: '--iconbutton-small-padding-left', hidden: true },
64
+ { label: 'icon size', groupKey: 'small-icon-size', variable: '--iconbutton-small-icon-size' },
65
+ ],
66
+ };
67
+ const smallTokensFlat: Token[] = Object.values(smallStates).flat();
68
+
69
+ export const allTokens: Token[] = [
70
+ ...variants.flatMap((v) => Object.values(variantStates(v)).flat()),
71
+ ...smallTokensFlat,
72
+ ];
73
+
74
+ // Frame geometry lives under each variant's "base" part and links across
75
+ // variants from there. Small tokens stay in their own namespace.
76
+ const linkableContexts = new Map<string, string>(
77
+ variants.flatMap((v) => [
78
+ [`--iconbutton-${v}-padding`, `${v} base`] as const,
79
+ [`--iconbutton-${v}-radius`, `${v} base`] as const,
80
+ [`--iconbutton-${v}-border-width`, `${v} base`] as const,
81
+ [`--iconbutton-${v}-icon-size`, `${v} base`] as const,
82
+ ]),
83
+ );
84
+
85
+ const variantOptions = variants.map((v) => ({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }));
86
+
87
+ const previewIcons: Record<Variant, string> = {
88
+ primary: 'fas fa-star',
89
+ secondary: 'fas fa-gear',
90
+ outline: 'fas fa-pen',
91
+ success: 'fas fa-check',
92
+ danger: 'fas fa-trash',
93
+ warning: 'fas fa-triangle-exclamation',
94
+ };
95
+ </script>
96
+
97
+ <script lang="ts">
98
+ import IconButton from '../../system/components/IconButton.svelte';
99
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
100
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
101
+ import { editorState } from '../core/store/editorStore';
102
+ import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
103
+
104
+ let previewSize = $state<'default' | 'small'>('default');
105
+
106
+ let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
107
+
108
+ let visibleVariantStates = $derived((v: Variant) => Object.fromEntries(
109
+ Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
110
+ ) as Record<string, Token[]>);
111
+
112
+ let visibleSmallStates = $derived(Object.fromEntries(
113
+ Object.entries(smallStates).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
114
+ ) as Record<string, Token[]>);
115
+
116
+ // At small size, no variant strip — the small spec is shared across all
117
+ // variants, so the strip would have nothing to navigate between.
118
+ let baseVariantOptions = $derived(previewSize === 'small' ? [] : variantOptions);
119
+ </script>
120
+
121
+ {#snippet sizeAction()}
122
+ <label>
123
+ <span>Size</span>
124
+ <select bind:value={previewSize}>
125
+ <option value="default">Default</option>
126
+ <option value="small">Small</option>
127
+ </select>
128
+ </label>
129
+ {/snippet}
130
+
131
+ <ComponentEditorBase {component} title="Icon Button" description="Icon-only button with the same variants, states and sizes as Button." tokens={allTokens} {linked} variants={baseVariantOptions}>
132
+ {#if previewSize === 'default'}
133
+ {#each variants as v}
134
+ <VariantGroup
135
+ name={v}
136
+ title={v.charAt(0).toUpperCase() + v.slice(1)}
137
+ states={visibleVariantStates(v)}
138
+ {component}
139
+ siblings={buildSiblings(variants, v, variantStates)}
140
+ previewActions={sizeAction}
141
+ >
142
+ {#snippet children({ activeState })}
143
+ {@const forceClass = activeState === 'hover' ? 'force-hover' : ''}
144
+ {@const isDisabled = activeState === 'disabled'}
145
+ <IconButton variant={v} icon={previewIcons[v]} ariaLabel={`${v} action`} disabled={isDisabled} class={forceClass} />
146
+ {/snippet}
147
+ </VariantGroup>
148
+ {/each}
149
+ {:else}
150
+ <VariantGroup
151
+ name="small"
152
+ title="Small"
153
+ states={visibleSmallStates}
154
+ {component}
155
+ previewActions={sizeAction}
156
+ >
157
+ {#snippet children()}
158
+ <div class="small-preview">
159
+ {#each variants as v}
160
+ <IconButton variant={v} size="small" icon={previewIcons[v]} ariaLabel={`${v} action`} />
161
+ {/each}
162
+ </div>
163
+ {/snippet}
164
+ </VariantGroup>
165
+ {/if}
166
+ </ComponentEditorBase>
167
+
168
+ <style>
169
+ .small-preview {
170
+ display: flex;
171
+ flex-wrap: wrap;
172
+ gap: var(--space-12);
173
+ align-items: center;
174
+ }
175
+ </style>
@@ -6,6 +6,7 @@ import BadgeEditor, { allTokens as badgeTokens } from './BadgeEditor.svelte';
6
6
  import CalloutEditor, { allTokens as calloutTokens } from './CalloutEditor.svelte';
7
7
  import CornerBadgeEditor, { allTokens as cornerBadgeTokens } from './CornerBadgeEditor.svelte';
8
8
  import ButtonEditor, { allTokens as buttonTokens } from './ButtonEditor.svelte';
9
+ import IconButtonEditor, { allTokens as iconButtonTokens } from './IconButtonEditor.svelte';
9
10
  import CardEditor, { allTokens as cardTokens, intrinsics as cardIntrinsics } from './CardEditor.svelte';
10
11
  import CodeSnippetEditor, { allTokens as codeSnippetTokens } from './CodeSnippetEditor.svelte';
11
12
  import CollapsibleSectionEditor, { allTokens as collapsibleSectionTokens } from './CollapsibleSectionEditor.svelte';
@@ -31,6 +32,7 @@ import TooltipEditor, { allTokens as tooltipTokens } from './TooltipEditor.svelt
31
32
  type BuiltInComponentId =
32
33
  | 'segmentedcontrol'
33
34
  | 'button'
35
+ | 'iconbutton'
34
36
  | 'notification'
35
37
  | 'dialog'
36
38
  | 'radiobutton'
@@ -108,6 +110,15 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
108
110
  schema: buttonTokens,
109
111
  origin: 'system',
110
112
  },
113
+ iconbutton: {
114
+ id: 'iconbutton',
115
+ label: 'Icon Button',
116
+ icon: 'fas fa-square-plus',
117
+ sourceFile: 'src/system/components/IconButton.svelte',
118
+ editorComponent: IconButtonEditor,
119
+ schema: iconButtonTokens,
120
+ origin: 'system',
121
+ },
111
122
  notification: {
112
123
  id: 'notification',
113
124
  label: 'Notification',
@@ -18,8 +18,9 @@ style your site by editing tokens and components in a live editor. When it looks
18
18
 
19
19
  - **Tokens**: the design-system primitives, colour palettes, type, spacing,
20
20
  radius, shadow, and gradients, that apply across your whole site.
21
- - **Components**: the package ships about 25 editable components (Button, Card,
22
- Dialog, Table, and more).You style components by changing the tokens assigned to each property.
21
+ - **Components**: the package ships about 25 editable components (Button,
22
+ IconButton, Card, Dialog, Table, and more). You style components by changing
23
+ the tokens assigned to each property.
23
24
 
24
25
  ## Where to go next
25
26
 
@@ -2,7 +2,7 @@
2
2
  // Source of truth: src/editor/docs/content/*.md · regenerate: npm run sync:docs
3
3
 
4
4
  export const docContent: Record<string, string> = {
5
- "01-overview": "# Overview\n\nLive Tokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the manifest and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button, Card,\n Dialog, Table, and more).You style components by changing the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
5
+ "01-overview": "# Overview\n\nLive Tokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the manifest and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button,\n IconButton, Card, Dialog, Table, and more). You style components by changing\n the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
6
6
  "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
7
7
  "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has two views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Two curves shape how lightness and saturation fall off across the\n ramp. Drag the handles to bias it darker, lighter, or more saturated.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Overlays and gradients\n\n- **Overlays** are translucent tints layered over surfaces, like the subtle\n tint a card gets on hover. Set a colour and opacity per state.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. **Save** is a separate step: it writes a named theme file under\n`src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`) and a file\nmenu for New, Save, Save as, Switch, and Delete. You can keep many themes side\nby side; one is active at a time. See [Themes](themes-workflow.md) for the full\nlifecycle.\n",
8
8
  "getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. Open the file menu and choose **Save as**. A theme appears as JSON under\n `src/live-tokens/data/themes/`.\n5. Reload. Your saved theme is the active theme, so the page returns as you\n left it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, promote a theme to production in the editor. That bakes the theme's\nvariables into `src/live-tokens/data/tokens.generated.css`, which your build\nbundles alongside `tokens.css`. The editor itself never reaches production.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
@@ -0,0 +1,322 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ disabled?: boolean;
4
+ type?: 'button' | 'submit' | 'reset';
5
+ variant?: 'primary' | 'secondary' | 'outline' | 'success' | 'danger' | 'warning';
6
+ size?: 'default' | 'small';
7
+ icon: string;
8
+ /** Required: the button has no visible text, so it needs an accessible name. */
9
+ ariaLabel: string;
10
+ tooltip?: string | undefined;
11
+ buttonRef?: HTMLButtonElement | undefined;
12
+ class?: string;
13
+ onclick?: (event: MouseEvent) => void;
14
+ }
15
+
16
+ let {
17
+ disabled = false,
18
+ type = 'button',
19
+ variant = 'primary',
20
+ size = 'default',
21
+ icon,
22
+ ariaLabel,
23
+ tooltip = undefined,
24
+ buttonRef = $bindable(undefined),
25
+ class: className = '',
26
+ onclick
27
+ }: Props = $props();
28
+
29
+ function handleClick(event: MouseEvent) {
30
+ if (!disabled) onclick?.(event);
31
+ }
32
+ </script>
33
+
34
+ <button
35
+ bind:this={buttonRef}
36
+ {type}
37
+ class="icon-button {variant} {className}"
38
+ class:small={size === 'small'}
39
+ {disabled}
40
+ aria-label={ariaLabel}
41
+ data-tooltip={tooltip}
42
+ onclick={handleClick}
43
+ >
44
+ <i class={icon}></i>
45
+ </button>
46
+
47
+ <style lang="scss">
48
+ @use '../styles/padding' as *;
49
+
50
+ :global(:root) {
51
+ /* Primary */
52
+ --iconbutton-primary-surface: var(--surface-brand-high);
53
+ --iconbutton-primary-icon: var(--text-primary);
54
+ --iconbutton-primary-border: var(--border-brand);
55
+ --iconbutton-primary-border-width: var(--border-width-1);
56
+ --iconbutton-primary-radius: var(--radius-xl);
57
+ --iconbutton-primary-padding: var(--space-8);
58
+ --iconbutton-primary-icon-size: var(--icon-size-md);
59
+ --iconbutton-primary-hover-surface: var(--surface-brand-higher);
60
+ --iconbutton-primary-hover-icon: var(--text-primary);
61
+ --iconbutton-primary-hover-border: var(--border-brand-strong);
62
+ --iconbutton-primary-disabled-surface: var(--color-neutral-700);
63
+ --iconbutton-primary-disabled-icon: var(--text-tertiary);
64
+ --iconbutton-primary-disabled-border: var(--border-neutral-faint);
65
+
66
+ /* Secondary */
67
+ --iconbutton-secondary-surface: var(--surface-neutral-high);
68
+ --iconbutton-secondary-icon: var(--text-primary);
69
+ --iconbutton-secondary-border: var(--border-neutral);
70
+ --iconbutton-secondary-border-width: var(--border-width-1);
71
+ --iconbutton-secondary-radius: var(--radius-xl);
72
+ --iconbutton-secondary-padding: var(--space-8);
73
+ --iconbutton-secondary-icon-size: var(--icon-size-md);
74
+ --iconbutton-secondary-hover-surface: var(--surface-neutral-higher);
75
+ --iconbutton-secondary-hover-icon: var(--text-primary);
76
+ --iconbutton-secondary-hover-border: var(--border-neutral-strong);
77
+ --iconbutton-secondary-disabled-surface: var(--color-neutral-700);
78
+ --iconbutton-secondary-disabled-icon: var(--text-tertiary);
79
+ --iconbutton-secondary-disabled-border: var(--border-neutral-faint);
80
+
81
+ /* Outline */
82
+ --iconbutton-outline-surface: var(--color-transparent);
83
+ --iconbutton-outline-icon: var(--text-primary);
84
+ --iconbutton-outline-border: var(--border-neutral);
85
+ --iconbutton-outline-border-width: var(--border-width-1);
86
+ --iconbutton-outline-radius: var(--radius-xl);
87
+ --iconbutton-outline-padding: var(--space-8);
88
+ --iconbutton-outline-icon-size: var(--icon-size-md);
89
+ --iconbutton-outline-hover-surface: var(--surface-neutral-lower);
90
+ --iconbutton-outline-hover-icon: var(--text-primary);
91
+ --iconbutton-outline-hover-border: var(--border-neutral-strong);
92
+ --iconbutton-outline-active-surface: var(--hover);
93
+ --iconbutton-outline-disabled-surface: var(--color-transparent);
94
+ --iconbutton-outline-disabled-icon: var(--text-tertiary);
95
+ --iconbutton-outline-disabled-border: var(--border-neutral-faint);
96
+
97
+ /* Success */
98
+ --iconbutton-success-surface: var(--surface-success-low);
99
+ --iconbutton-success-icon: var(--text-success);
100
+ --iconbutton-success-border: var(--border-success);
101
+ --iconbutton-success-border-width: var(--border-width-1);
102
+ --iconbutton-success-radius: var(--radius-xl);
103
+ --iconbutton-success-padding: var(--space-8);
104
+ --iconbutton-success-icon-size: var(--icon-size-md);
105
+ --iconbutton-success-hover-surface: var(--surface-success-higher);
106
+ --iconbutton-success-hover-icon: var(--text-primary);
107
+ --iconbutton-success-hover-border: var(--border-success-strong);
108
+ --iconbutton-success-disabled-surface: var(--color-neutral-700);
109
+ --iconbutton-success-disabled-icon: var(--text-tertiary);
110
+ --iconbutton-success-disabled-border: var(--border-neutral-faint);
111
+
112
+ /* Danger */
113
+ --iconbutton-danger-surface: var(--surface-danger-low);
114
+ --iconbutton-danger-icon: var(--text-danger);
115
+ --iconbutton-danger-border: var(--border-danger);
116
+ --iconbutton-danger-border-width: var(--border-width-1);
117
+ --iconbutton-danger-radius: var(--radius-xl);
118
+ --iconbutton-danger-padding: var(--space-8);
119
+ --iconbutton-danger-icon-size: var(--icon-size-md);
120
+ --iconbutton-danger-hover-surface: var(--surface-danger-high);
121
+ --iconbutton-danger-hover-icon: var(--text-primary);
122
+ --iconbutton-danger-hover-border: var(--border-danger-medium);
123
+ --iconbutton-danger-disabled-surface: var(--color-neutral-700);
124
+ --iconbutton-danger-disabled-icon: var(--text-tertiary);
125
+ --iconbutton-danger-disabled-border: var(--border-neutral-faint);
126
+
127
+ /* Warning */
128
+ --iconbutton-warning-surface: var(--surface-warning-low);
129
+ --iconbutton-warning-icon: var(--text-warning);
130
+ --iconbutton-warning-border: var(--border-warning);
131
+ --iconbutton-warning-border-width: var(--border-width-1);
132
+ --iconbutton-warning-radius: var(--radius-xl);
133
+ --iconbutton-warning-padding: var(--space-8);
134
+ --iconbutton-warning-icon-size: var(--icon-size-md);
135
+ --iconbutton-warning-hover-surface: var(--surface-warning-high);
136
+ --iconbutton-warning-hover-icon: var(--text-primary);
137
+ --iconbutton-warning-hover-border: var(--border-warning-medium);
138
+ --iconbutton-warning-disabled-surface: var(--color-neutral-700);
139
+ --iconbutton-warning-disabled-icon: var(--text-tertiary);
140
+ --iconbutton-warning-disabled-border: var(--border-neutral-faint);
141
+
142
+ /* Small size — shared across all variants, mirroring Button. */
143
+ --iconbutton-small-padding: var(--space-6);
144
+ --iconbutton-small-icon-size: var(--icon-size-sm);
145
+ }
146
+
147
+ .icon-button {
148
+ cursor: pointer;
149
+ display: inline-flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ aspect-ratio: 1;
153
+ transition: all var(--duration-150);
154
+
155
+ &:hover:not(:disabled),
156
+ &.force-hover:not(:disabled) {
157
+ transform: translateY(-0.0625rem);
158
+ box-shadow: var(--shadow-md);
159
+ }
160
+
161
+ &:active:not(:disabled) {
162
+ transform: translateY(0);
163
+ }
164
+
165
+ &:disabled {
166
+ cursor: not-allowed;
167
+ }
168
+
169
+ &.primary {
170
+ --iconbutton-icon-size: var(--iconbutton-primary-icon-size);
171
+ background: var(--iconbutton-primary-surface);
172
+ color: var(--iconbutton-primary-icon);
173
+ border: var(--iconbutton-primary-border-width) solid var(--iconbutton-primary-border);
174
+ border-radius: var(--iconbutton-primary-radius);
175
+ @include themed-padding(--iconbutton-primary-padding);
176
+
177
+ &:hover:not(:disabled),
178
+ &.force-hover:not(:disabled) {
179
+ background: var(--iconbutton-primary-hover-surface);
180
+ border-color: var(--iconbutton-primary-hover-border);
181
+ color: var(--iconbutton-primary-hover-icon);
182
+ }
183
+
184
+ &:active:not(:disabled) {
185
+ box-shadow: var(--shadow-sm);
186
+ }
187
+
188
+ &:disabled {
189
+ background: var(--iconbutton-primary-disabled-surface);
190
+ border-color: var(--iconbutton-primary-disabled-border);
191
+ color: var(--iconbutton-primary-disabled-icon);
192
+ }
193
+ }
194
+
195
+ &.secondary {
196
+ --iconbutton-icon-size: var(--iconbutton-secondary-icon-size);
197
+ background: var(--iconbutton-secondary-surface);
198
+ color: var(--iconbutton-secondary-icon);
199
+ border: var(--iconbutton-secondary-border-width) solid var(--iconbutton-secondary-border);
200
+ border-radius: var(--iconbutton-secondary-radius);
201
+ @include themed-padding(--iconbutton-secondary-padding);
202
+
203
+ &:hover:not(:disabled),
204
+ &.force-hover:not(:disabled) {
205
+ background: var(--iconbutton-secondary-hover-surface);
206
+ border-color: var(--iconbutton-secondary-hover-border);
207
+ color: var(--iconbutton-secondary-hover-icon);
208
+ }
209
+
210
+ &:disabled {
211
+ background: var(--iconbutton-secondary-disabled-surface);
212
+ border-color: var(--iconbutton-secondary-disabled-border);
213
+ color: var(--iconbutton-secondary-disabled-icon);
214
+ }
215
+ }
216
+
217
+ &.outline {
218
+ --iconbutton-icon-size: var(--iconbutton-outline-icon-size);
219
+ background: var(--iconbutton-outline-surface);
220
+ color: var(--iconbutton-outline-icon);
221
+ border: var(--iconbutton-outline-border-width) solid var(--iconbutton-outline-border);
222
+ border-radius: var(--iconbutton-outline-radius);
223
+ @include themed-padding(--iconbutton-outline-padding);
224
+
225
+ &:hover:not(:disabled),
226
+ &.force-hover:not(:disabled) {
227
+ background: var(--iconbutton-outline-hover-surface);
228
+ border-color: var(--iconbutton-outline-hover-border);
229
+ color: var(--iconbutton-outline-hover-icon);
230
+ }
231
+
232
+ &:active:not(:disabled) {
233
+ background: var(--iconbutton-outline-active-surface);
234
+ }
235
+
236
+ &:disabled {
237
+ background: var(--iconbutton-outline-disabled-surface);
238
+ border-color: var(--iconbutton-outline-disabled-border);
239
+ color: var(--iconbutton-outline-disabled-icon);
240
+ }
241
+ }
242
+
243
+ &.success {
244
+ --iconbutton-icon-size: var(--iconbutton-success-icon-size);
245
+ background: var(--iconbutton-success-surface);
246
+ color: var(--iconbutton-success-icon);
247
+ border: var(--iconbutton-success-border-width) solid var(--iconbutton-success-border);
248
+ border-radius: var(--iconbutton-success-radius);
249
+ @include themed-padding(--iconbutton-success-padding);
250
+
251
+ &:hover:not(:disabled),
252
+ &.force-hover:not(:disabled) {
253
+ background: var(--iconbutton-success-hover-surface);
254
+ border-color: var(--iconbutton-success-hover-border);
255
+ color: var(--iconbutton-success-hover-icon);
256
+ }
257
+
258
+ &:disabled {
259
+ background: var(--iconbutton-success-disabled-surface);
260
+ border-color: var(--iconbutton-success-disabled-border);
261
+ color: var(--iconbutton-success-disabled-icon);
262
+ }
263
+ }
264
+
265
+ &.danger {
266
+ --iconbutton-icon-size: var(--iconbutton-danger-icon-size);
267
+ background: var(--iconbutton-danger-surface);
268
+ color: var(--iconbutton-danger-icon);
269
+ border: var(--iconbutton-danger-border-width) solid var(--iconbutton-danger-border);
270
+ border-radius: var(--iconbutton-danger-radius);
271
+ @include themed-padding(--iconbutton-danger-padding);
272
+
273
+ &:hover:not(:disabled),
274
+ &.force-hover:not(:disabled) {
275
+ background: var(--iconbutton-danger-hover-surface);
276
+ border-color: var(--iconbutton-danger-hover-border);
277
+ color: var(--iconbutton-danger-hover-icon);
278
+ }
279
+
280
+ &:disabled {
281
+ background: var(--iconbutton-danger-disabled-surface);
282
+ border-color: var(--iconbutton-danger-disabled-border);
283
+ color: var(--iconbutton-danger-disabled-icon);
284
+ }
285
+ }
286
+
287
+ &.warning {
288
+ --iconbutton-icon-size: var(--iconbutton-warning-icon-size);
289
+ background: var(--iconbutton-warning-surface);
290
+ color: var(--iconbutton-warning-icon);
291
+ border: var(--iconbutton-warning-border-width) solid var(--iconbutton-warning-border);
292
+ border-radius: var(--iconbutton-warning-radius);
293
+ @include themed-padding(--iconbutton-warning-padding);
294
+
295
+ &:hover:not(:disabled),
296
+ &.force-hover:not(:disabled) {
297
+ background: var(--iconbutton-warning-hover-surface);
298
+ border-color: var(--iconbutton-warning-hover-border);
299
+ color: var(--iconbutton-warning-hover-icon);
300
+ }
301
+
302
+ &:disabled {
303
+ background: var(--iconbutton-warning-disabled-surface);
304
+ border-color: var(--iconbutton-warning-disabled-border);
305
+ color: var(--iconbutton-warning-disabled-icon);
306
+ }
307
+ }
308
+
309
+ &.small {
310
+ @include themed-padding(--iconbutton-small-padding);
311
+ }
312
+
313
+ &.small :global(i) {
314
+ font-size: var(--iconbutton-small-icon-size);
315
+ }
316
+
317
+ :global(i) {
318
+ font-size: var(--iconbutton-icon-size, var(--icon-size-md));
319
+ line-height: 1;
320
+ }
321
+ }
322
+ </style>