Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5e5ed5b75 | ||
|
|
572c09039f | ||
|
|
21c380595f |
@@ -30,7 +30,7 @@ Everything here is MIT licensed, see [LICENSE](LICENSE).
|
||||
## Install
|
||||
|
||||
The packages arent on pub.dev. Git-depend on this repo, pick the package with
|
||||
`path:`, and pin a tag:
|
||||
`path:`, and pin the commit of a release tag:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
@@ -38,20 +38,26 @@ dependencies:
|
||||
git:
|
||||
url: https://git.imbenji.dev/IMBENJI.NET/Garage-SDKs.git
|
||||
path: garage_auth
|
||||
ref: v0.1.0
|
||||
ref: b2692019197e60d44ddd3ecab4917caf71ff45c3 # v0.1.0
|
||||
garage_entitlements:
|
||||
git:
|
||||
url: https://git.imbenji.dev/IMBENJI.NET/Garage-SDKs.git
|
||||
path: garage_entitlements
|
||||
ref: v0.1.0
|
||||
ref: b2692019197e60d44ddd3ecab4917caf71ff45c3 # v0.1.0
|
||||
```
|
||||
|
||||
Pin a tag, dont float `main`. `garage_entitlements` decides who gets the paid
|
||||
Pin, dont float `main`. `garage_entitlements` decides who gets the paid
|
||||
features, and "it changed under us" is not a fun thing to debug.
|
||||
|
||||
`garage_entitlements` and `garage_iap` layer on `garage_auth`. You get it
|
||||
transitively, but declare it too — you'll be constructing a `GarageAuth`
|
||||
yourself anyway, and keep all of them on the same `ref`.
|
||||
Use the tag's **full commit hash**, not the tag name. `garage_entitlements` and
|
||||
`garage_iap` pull in `garage_auth` by relative path, which pub turns into a git
|
||||
dep at the resolved commit hash — so if your app asks for `garage_auth` at
|
||||
`v0.1.0`, pub sees two different sources for the same package and refuses to
|
||||
resolve. Same hash on every garage package and it just works. Keep the tag name
|
||||
in a comment so you can tell which release it is.
|
||||
|
||||
You'll be constructing a `GarageAuth` yourself, so declare `garage_auth`
|
||||
directly even when you only really want entitlements or iap.
|
||||
|
||||
**Working on the SDKs at the same time as an app?** Point the app at a local
|
||||
checkout with a gitignored `pubspec_overrides.yaml`, so edits hot reload with
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Garage design system
|
||||
|
||||
The look of every Garage app — the hub, the payment portal, Arcs & Angles — as
|
||||
tokens, CSS and preview cards, for designing new Garage screens.
|
||||
|
||||
It's generated from the Flutter package, not drawn separately:
|
||||
`tokens.css` and `tokens.json` come out of
|
||||
`garage_ui/tool/export_design_tokens_test.dart`, and `components.css` only
|
||||
uses those tokens. Don't edit the token files by hand — change garage_ui and
|
||||
rerun:
|
||||
|
||||
```sh
|
||||
cd garage_ui && flutter test tool/export_design_tokens_test.dart
|
||||
```
|
||||
|
||||
```
|
||||
design_system/
|
||||
tokens.css, tokens.json colours, density, type, shape — generated
|
||||
components.css the components, built on the tokens
|
||||
foundations/ colour, type, spacing, shape, density
|
||||
components/ buttons, fields, selects, lists, dialogs, …
|
||||
patterns/ whole screens from the hub and Arcs & Angles
|
||||
```
|
||||
|
||||
|
||||
## The one rule: let the theme speak
|
||||
|
||||
The colour scheme, the density and the component's variant decide every
|
||||
colour, font size, weight, icon size, padding, radius and gap. Designing a
|
||||
Garage screen is choosing **which component** and **which variant**. Then
|
||||
leave it alone.
|
||||
|
||||
- No custom colours, font sizes or weights on a component or its contents.
|
||||
- No icon sizes or colours — an icon takes them from where it sits.
|
||||
- No hand-picked padding, gaps or radii. Use the `--gap-*` and `--radius-*`
|
||||
tokens, and let components that already carry spacing keep it.
|
||||
- No home-made badges, pills, warning boxes or popovers built from a box with
|
||||
a fill and a border. If a component doesnt exist, that's a gap in the system,
|
||||
not an invitation.
|
||||
- No shadows. Everything is flat — surfaces are separated by a step in
|
||||
lightness and a hairline border.
|
||||
|
||||
If something looks wrong, the answer is a different variant or a different
|
||||
component.
|
||||
|
||||
|
||||
## Setting up a screen
|
||||
|
||||
```html
|
||||
<html data-scheme="carbon" data-density="product">
|
||||
<link rel="stylesheet" href="components.css">
|
||||
```
|
||||
|
||||
- **Scheme:** `carbon` (default — true black, no accent hue; what the Garage
|
||||
apps run), `dark`, `light`.
|
||||
- **Density:** `product` (default — web and product apps, 12px text, 33px
|
||||
controls), `compact` (dense desktop tools like Arcs & Angles, 10px text, 23px
|
||||
controls), `normal` (compact but roomier). Pick one per app. Don't mix.
|
||||
|
||||
Type is Geist, and Geist Mono for numbers and readouts. Body text is weight
|
||||
300, control labels 400, primary buttons 600.
|
||||
|
||||
|
||||
## Layout
|
||||
|
||||
Every Garage screen is the same shape: a `chrome` ground, `panel`s standing on
|
||||
it, and `panel-gap` gutters between them.
|
||||
|
||||
```html
|
||||
<div class="shell">
|
||||
<nav class="panel sunken">…rail…</nav>
|
||||
<main class="panel">…workspace…</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
Headers and footers are `chrome-bar`s. Only one panel is highlighted
|
||||
(`panel active`) at a time.
|
||||
|
||||
A hub page is a centred column inside the workspace panel: a `page-heading`,
|
||||
then either a stack of `properties-section`s or `sub-heading` + `settings-list`
|
||||
forms. See `patterns/`.
|
||||
|
||||
|
||||
## Choosing components
|
||||
|
||||
**Buttons**
|
||||
|
||||
| Variant | For |
|
||||
|---|---|
|
||||
| `primary` | the one action on the view, or the thing that's on / chosen |
|
||||
| `secondary` | a neutral filled control; the selected nav item; a button welded to a field |
|
||||
| `outline` | a normal action that isn't the main one — Cancel, Retry |
|
||||
| `ghost` | chrome, toolbars, nav items, row actions |
|
||||
| `ghost-destructive` | remove / revoke / delete as a row action |
|
||||
| `destructive` | the confirm button of a destructive dialog |
|
||||
| `link`, `text` | inline in copy |
|
||||
|
||||
Icon-only buttons are `btn icon-only`. A nav item is ghost with a leading icon,
|
||||
left-aligned, secondary when selected — never primary. Toolbar toggles are
|
||||
primary when on, outline when off.
|
||||
|
||||
**Fields**
|
||||
|
||||
- Free text: the default outline `field`.
|
||||
- Values inside a properties section: `field secondary`. A read-only value is a
|
||||
read-only secondary field — **never bare text** in a value slot.
|
||||
- Selects: secondary in a properties section, `ghost` in a settings list,
|
||||
outline elsewhere.
|
||||
- Numbers in an editor: secondary, centred, unit as a muted suffix.
|
||||
|
||||
**Lists**
|
||||
|
||||
- `properties-section` — a boxed card of label/value rows. Inspectors,
|
||||
account pages, one card per record in a list. Controls inside are secondary.
|
||||
- `settings-list` — unboxed rows ruled between entries, for forms. Controls
|
||||
inside are ghost; text fields are outline.
|
||||
|
||||
**Subtitles** are short static descriptions of what something is. Never live
|
||||
data — no counts, dates, names, emails or statuses. Facts go in rows.
|
||||
|
||||
**Dialogs** are `dialog` with the actions in their band: outline Cancel, then
|
||||
a primary or destructive confirm. Max width is fixed; don't widen it.
|
||||
|
||||
**Loading** is a spinner. No skeletons. **Empty** is one muted line.
|
||||
**Error** is the message, muted, and an outline Retry.
|
||||
@@ -0,0 +1,538 @@
|
||||
/* garage_ui components, in plain css. Built on tokens.css - every value here
|
||||
is a token or a formula the flutter package uses too. If something needs a
|
||||
new number, it goes in garage_ui first and comes out through the export.
|
||||
The odd literal is only here because the dart source has the same literal,
|
||||
and its commented where it happens.
|
||||
|
||||
Set data-scheme / data-density on <html> (or any container) and everything
|
||||
below follows.
|
||||
|
||||
States: every :hover / :active has an .is-hovered / .is-active twin, so a
|
||||
card can show a state sat still without anyone pointing at it. */
|
||||
|
||||
@import url("tokens.css");
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
body, .gu {
|
||||
margin: 0;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-height);
|
||||
font-weight: var(--weight-body);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* a nested scheme / density container has to re-resolve the things body set,
|
||||
or it just inherits the outer values and only the components change */
|
||||
[data-scheme] { background: var(--background); color: var(--foreground); }
|
||||
[data-density] { font-size: var(--font-size); line-height: var(--line-height); }
|
||||
|
||||
/* ---- type ------------------------------------------------------------ */
|
||||
|
||||
.text-xxs { font-size: var(--text-xxs); }
|
||||
.text-xs { font-size: var(--text-xs); }
|
||||
.text-sm { font-size: var(--text-sm); }
|
||||
.text-lg { font-size: var(--text-lg); line-height: 1.3; }
|
||||
.medium { font-weight: var(--weight-medium); }
|
||||
.semibold { font-weight: var(--weight-semibold); }
|
||||
.bold { font-weight: 700; }
|
||||
.muted { color: var(--muted-foreground); }
|
||||
.mono { font-family: var(--font-mono); font-weight: 400; }
|
||||
|
||||
/* PageHeading - title a step up, muted line under it, gapXs in from the side
|
||||
(the margin a properties section carries). trailing sits gapMd off the
|
||||
heading, centred on the whole block not the title line */
|
||||
.page-heading { padding: 0 var(--gap-xs); display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--gap-md); align-items: center; }
|
||||
.page-heading > :not(.trailing) { grid-column: 1; }
|
||||
.page-heading .title { font-size: var(--text-lg); font-weight: var(--weight-control); line-height: 1.3; }
|
||||
.page-heading .subtitle { margin-top: var(--gap-xs); color: var(--muted-foreground); }
|
||||
/* the hub puts a gapXs between two asides when theres more then one */
|
||||
.page-heading > .trailing { grid-column: 2; grid-row: 1 / span 2; display: flex; align-items: center; gap: var(--gap-xs); }
|
||||
|
||||
/* subHeading - the same shape one size down, more room over it than under */
|
||||
.sub-heading { padding: var(--gap-xxl) var(--gap-xs) var(--gap-lg); display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--gap-md); align-items: center; }
|
||||
.sub-heading > :not(.action) { grid-column: 1; }
|
||||
.sub-heading .title { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
|
||||
.sub-heading .subtitle { margin-top: var(--gap-xxs); font-size: var(--text-xxs); color: var(--muted-foreground); }
|
||||
.sub-heading > .action { grid-column: 2; grid-row: 1 / span 2; display: flex; align-items: center; gap: var(--gap-xs); }
|
||||
|
||||
.caption { font-size: var(--text-xxs); font-weight: var(--weight-semibold); color: var(--muted-foreground); }
|
||||
|
||||
/* ---- icons ----------------------------------------------------------- */
|
||||
/* lucide, as inline svg. size comes from where it sits, never the call site */
|
||||
|
||||
.icon { width: var(--icon-sm); height: var(--icon-sm); flex: none; stroke: currentColor; fill: none;
|
||||
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.icon-md { width: var(--icon-md); height: var(--icon-md); }
|
||||
.icon-lg { width: var(--icon-lg); height: var(--icon-lg); }
|
||||
|
||||
/* ---- buttons --------------------------------------------------------- */
|
||||
/* every variant shares the same control padding - only the density moves it.
|
||||
link and text included, they just dont paint a box */
|
||||
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
gap: var(--control-gap);
|
||||
height: var(--control-height);
|
||||
padding: 0 var(--control-padding-x);
|
||||
border: var(--control-border-width) solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--foreground);
|
||||
font: inherit; font-size: var(--font-size); font-weight: var(--weight-control);
|
||||
white-space: nowrap; cursor: pointer; user-select: none;
|
||||
outline: none;
|
||||
}
|
||||
.btn:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent); }
|
||||
.btn:disabled, .btn.disabled { cursor: default; color: var(--muted-foreground); }
|
||||
.btn.icon-only { width: var(--control-height); padding: 0; }
|
||||
.btn.stretch { display: flex; width: 100%; }
|
||||
.btn.align-left { justify-content: flex-start; }
|
||||
|
||||
.btn.primary { background: var(--primary); border-color: var(--control-border); color: var(--primary-foreground); font-weight: var(--weight-semibold); }
|
||||
.btn.primary:hover:not(:disabled), .btn.primary.is-hovered { background: var(--primary-hovered); }
|
||||
|
||||
.btn.secondary { background: var(--secondary); border-color: var(--control-border); color: var(--secondary-foreground); }
|
||||
.btn.secondary:hover:not(:disabled), .btn.secondary.is-hovered { background: var(--secondary-hovered); }
|
||||
|
||||
/* outline shares the text field's fill + stroke on purpose - one family */
|
||||
.btn.outline { background: var(--control-fill); border-color: var(--control-border); }
|
||||
.btn.outline .icon { color: var(--muted-foreground); }
|
||||
.btn.outline:hover:not(:disabled), .btn.outline.is-hovered { background: var(--control-fill-hovered); }
|
||||
|
||||
/* ghost tints what its standing on instead of painting a colour */
|
||||
.btn.ghost:hover:not(:disabled), .btn.ghost.is-hovered { background: color-mix(in srgb, var(--foreground) 7%, transparent); }
|
||||
.btn.ghost:active:not(:disabled), .btn.ghost.is-active { background: color-mix(in srgb, var(--foreground) 12%, transparent); }
|
||||
|
||||
.btn.ghost-destructive { color: var(--destructive); }
|
||||
.btn.ghost-destructive:hover:not(:disabled), .btn.ghost-destructive.is-hovered { background: color-mix(in srgb, var(--destructive) 12%, transparent); }
|
||||
.btn.ghost-destructive:active:not(:disabled), .btn.ghost-destructive.is-active { background: color-mix(in srgb, var(--destructive) 20%, transparent); }
|
||||
|
||||
/* white is a literal in button.dart - "straight up white regardless of light or dark mode" */
|
||||
.btn.destructive { background: color-mix(in srgb, var(--destructive) 50%, transparent); color: #fff; border-color: transparent; }
|
||||
.btn.destructive:hover:not(:disabled), .btn.destructive.is-hovered { background: color-mix(in srgb, var(--destructive) 80%, transparent); }
|
||||
|
||||
.btn.link:hover:not(:disabled), .btn.link.is-hovered { text-decoration: underline; }
|
||||
|
||||
.btn.text { color: var(--muted-foreground); }
|
||||
.btn.text:hover:not(:disabled), .btn.text.is-hovered { color: var(--primary); }
|
||||
|
||||
/* disabled, per variant, straight off button.dart. primary goes to a muted
|
||||
foreground slab so its label still reads; secondary keeps its fill and
|
||||
only the label dims; destructive swaps to the primary foreground */
|
||||
.btn.primary:disabled, .btn.primary.disabled { background: var(--muted-foreground); color: var(--primary-foreground); }
|
||||
.btn.secondary:disabled, .btn.secondary.disabled { background: var(--secondary); color: var(--muted-foreground); }
|
||||
.btn.ghost-destructive:disabled, .btn.ghost-destructive.disabled { background: transparent; color: var(--muted-foreground); }
|
||||
.btn.destructive:disabled, .btn.destructive.disabled { background: var(--primary-foreground); color: var(--muted-foreground); }
|
||||
/* text has no disabled look in the source, it just stays muted */
|
||||
.btn.text:disabled, .btn.text.disabled { color: var(--muted-foreground); }
|
||||
|
||||
/* button group - joined, only the outer corners round */
|
||||
.btn-group { display: inline-flex; }
|
||||
.btn-group > * { border-radius: 0; }
|
||||
.btn-group > *:first-child { border-top-left-radius: var(--radius-md); border-bottom-left-radius: var(--radius-md); }
|
||||
.btn-group > *:last-child { border-top-right-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md); }
|
||||
.btn-group > * + * { margin-left: calc(var(--control-border-width) * -1); }
|
||||
.btn-group.fill { display: flex; }
|
||||
.btn-group.fill > * { flex: 1; }
|
||||
/* span: the group takes the width its given and ONE member (.grow) takes the
|
||||
slack - a field with its IconButton welded on, the button keeping its square */
|
||||
.btn-group.span { display: flex; width: 100%; }
|
||||
.btn-group > .grow { flex: 1; min-width: 0; }
|
||||
.btn-group.vertical { flex-direction: column; }
|
||||
.btn-group.vertical > * { border-radius: 0; }
|
||||
.btn-group.vertical > *:first-child { border-radius: var(--radius-md) var(--radius-md) 0 0; }
|
||||
.btn-group.vertical > *:last-child { border-radius: 0 0 var(--radius-md) var(--radius-md); }
|
||||
.btn-group.vertical > *:only-child { border-radius: var(--radius-md); }
|
||||
.btn-group.vertical > * + * { margin-left: 0; margin-top: calc(var(--control-border-width) * -1); }
|
||||
/* a group inside a group - a corner only rounds where BOTH groups say so
|
||||
(ButtonGroupCorners.merge, its a product of the two) */
|
||||
.btn-group:not(.vertical) > .btn-group:not(:first-child) > * { border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
||||
.btn-group:not(.vertical) > .btn-group:not(:last-child) > * { border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
||||
.btn-group.vertical > .btn-group:not(:first-child) > * { border-top-left-radius: 0; border-top-right-radius: 0; }
|
||||
.btn-group.vertical > .btn-group:not(:last-child) > * { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
||||
|
||||
/* ---- text fields ----------------------------------------------------- */
|
||||
|
||||
.field {
|
||||
display: inline-flex; align-items: center; gap: var(--control-gap);
|
||||
height: var(--control-height);
|
||||
padding: 0 calc(var(--control-padding-x) - var(--control-border-width));
|
||||
background: var(--control-fill);
|
||||
border: var(--control-border-width) solid var(--control-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--foreground);
|
||||
font-weight: var(--weight-control);
|
||||
min-width: 0;
|
||||
}
|
||||
/* only an outline field that can be typed in reacts - secondary is a flat
|
||||
secondary fill in every state, and a read only one is just looked at */
|
||||
.field:not(.secondary):not(.readonly):hover, .field:not(.secondary):not(.readonly).is-hovered { background: var(--control-fill-hovered); }
|
||||
.field:not(.secondary):not(.readonly):focus-within, .field:not(.secondary):not(.readonly).is-focused { background: var(--control-fill-focused); }
|
||||
.field input, .field textarea {
|
||||
all: unset; flex: 1; min-width: 0; width: 100%;
|
||||
font-family: var(--font-sans); font-size: var(--font-size); font-weight: var(--weight-control);
|
||||
color: inherit;
|
||||
}
|
||||
.field input::placeholder { color: var(--muted-foreground); }
|
||||
.field .icon, .field .unit { color: var(--muted-foreground); }
|
||||
.field.secondary { background: var(--secondary); color: var(--secondary-foreground); }
|
||||
.field.secondary .icon { color: var(--secondary-foreground); }
|
||||
/* one rule for "you cant change this" - the value AND the icons beside it */
|
||||
.field.readonly, .field.readonly .icon { color: var(--muted-foreground); }
|
||||
.field.center input { text-align: center; }
|
||||
/* ValueField(monospace:) - for ids and skus, read a character at a time */
|
||||
.field.mono input { font-family: var(--font-mono); }
|
||||
/* a rejected value: the outline goes full destructive, never the fill */
|
||||
.field.error { border-color: var(--destructive); }
|
||||
.field.stretch { display: flex; width: 100%; }
|
||||
|
||||
/* ---- select ---------------------------------------------------------- */
|
||||
/* the value sits in the middle of the TRIGGER - a chevron's worth of nothing
|
||||
on the left balances the chevron on the right */
|
||||
|
||||
.select { display: inline-flex; align-items: center; justify-content: space-between; cursor: pointer; gap: var(--control-gap);
|
||||
height: var(--control-height); padding: 0 calc(var(--control-padding-x) - var(--control-border-width));
|
||||
background: var(--control-fill); border: var(--control-border-width) solid var(--control-border);
|
||||
border-radius: var(--radius-md); color: var(--foreground); font-weight: var(--weight-control); white-space: nowrap; }
|
||||
.select::before { content: ""; flex: none; width: var(--icon-sm); }
|
||||
.select > span { flex: 1; min-width: 0; text-align: center; }
|
||||
.select .placeholder { color: var(--muted-foreground); }
|
||||
.select .chevron { color: color-mix(in srgb, currentColor 50%, transparent); }
|
||||
.select:hover, .select.is-hovered { background: var(--control-fill-hovered); }
|
||||
.select.secondary { background: var(--secondary); color: var(--secondary-foreground); }
|
||||
.select.secondary:hover, .select.secondary.is-hovered { background: var(--secondary-hovered); }
|
||||
.select.ghost { background: transparent; border-color: transparent; }
|
||||
.select.ghost:hover, .select.ghost.is-hovered { background: var(--popover-item-hovered); }
|
||||
.select.disabled { color: var(--muted-foreground); cursor: default; }
|
||||
.select.disabled:hover { background: var(--control-fill); }
|
||||
.select.secondary.disabled:hover { background: var(--secondary); }
|
||||
.select.ghost.disabled:hover { background: transparent; }
|
||||
.select.error { border-color: var(--destructive); }
|
||||
|
||||
/* the select popup - its own surface, not a menu. inset is half the container
|
||||
gap, the corner is concentric with the rows (less 2, a literal in
|
||||
select.dart - the full sum read too soft) */
|
||||
.select-popup {
|
||||
display: inline-flex; flex-direction: column; gap: var(--gap-xxs);
|
||||
padding: calc(var(--container-gap) * 0.5);
|
||||
background: var(--popover);
|
||||
border: var(--control-border-width) solid var(--popover-border);
|
||||
border-radius: calc(var(--radius-md) + var(--container-gap) * 0.5 + var(--control-border-width) - 2px);
|
||||
color: var(--foreground);
|
||||
}
|
||||
.select-popup .option {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: var(--control-gap);
|
||||
height: var(--popup-row-height);
|
||||
padding: 0 calc(var(--control-padding-x) - var(--control-border-width));
|
||||
border: var(--control-border-width) solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: var(--weight-control); white-space: nowrap; cursor: default;
|
||||
}
|
||||
.select-popup .option:hover, .select-popup .option.is-hovered { background: var(--popover-item-hovered); }
|
||||
.select-popup .option.disabled { color: var(--muted-foreground); }
|
||||
/* the selected row IS a secondary button, stroke and all, with the check */
|
||||
.select-popup .option.selected { background: var(--secondary); border-color: var(--control-border); color: var(--secondary-foreground); }
|
||||
.select-popup .option.selected:hover, .select-popup .option.selected.is-hovered { background: var(--secondary-hovered); }
|
||||
/* once anything is selected every row reserves the check column, mirrored on
|
||||
the left, so the labels stay centred on the row */
|
||||
.select-popup:has(.option.selected) .option::before,
|
||||
.select-popup:has(.option.selected) .option:not(.selected)::after { content: ""; flex: none; width: var(--icon-sm); }
|
||||
|
||||
/* ---- popovers, menus, tooltips --------------------------------------- */
|
||||
|
||||
/* MenuPopup - the 2px inset and 1px border are literals in menu.dart */
|
||||
.popover {
|
||||
display: inline-flex; flex-direction: column;
|
||||
background: var(--popover);
|
||||
border: 1px solid var(--popover-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 2px;
|
||||
font-weight: var(--weight-control);
|
||||
}
|
||||
/* a row: an 18px icon slot and a 4px gap after it, the icon nudged toward the
|
||||
edge by (4 - padding) / 2. 18, 4 and the 4px row radius are all literals in
|
||||
menu.dart */
|
||||
.menu-item {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
height: var(--menu-row-height);
|
||||
padding: 0 var(--control-padding-x);
|
||||
border-radius: 4px;
|
||||
color: var(--foreground); font-weight: var(--weight-control); white-space: nowrap; cursor: default;
|
||||
}
|
||||
.menu-item > .icon:first-child { margin: 0 calc((18px - var(--icon-sm)) / 2); position: relative; left: calc((4px - var(--control-padding-x)) / 2); }
|
||||
.menu-item .icon { color: var(--muted-foreground); }
|
||||
.menu-item .shortcut { margin-left: auto; padding-left: var(--container-gap); color: var(--muted-foreground); font-size: var(--font-size); }
|
||||
.menu-item:hover, .menu-item.is-hovered { background: var(--row-hovered); }
|
||||
.menu-item.disabled { color: color-mix(in srgb, var(--foreground) 50%, transparent); }
|
||||
.menu-item.disabled .icon { color: color-mix(in srgb, var(--muted-foreground) 50%, transparent); }
|
||||
.menu-item.disabled:hover { background: transparent; }
|
||||
/* MenuLabel - same padding as a row, so the same height, just semibold */
|
||||
.menu-label { display: flex; align-items: center; height: var(--menu-row-height); padding: 0 var(--control-padding-x); font-weight: var(--weight-semibold); white-space: nowrap; }
|
||||
/* 4px either side is a literal in menu.dart */
|
||||
.menu-divider { height: 1px; background: var(--divider); margin: 4px 0; }
|
||||
|
||||
/* TooltipContainer - three quarters of the container padding across, three
|
||||
quarters of the container gap down */
|
||||
.tooltip {
|
||||
display: inline-block;
|
||||
background: var(--tooltip-background);
|
||||
border: 1px solid var(--tooltip-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: calc(var(--container-gap) * 0.75) calc(var(--container-padding) * 0.75);
|
||||
color: var(--foreground);
|
||||
font-size: var(--font-size); font-weight: var(--weight-control);
|
||||
}
|
||||
|
||||
/* ---- selection controls ---------------------------------------------- */
|
||||
|
||||
/* 16px box, 1px border - literals in selection_controls.dart */
|
||||
.checkbox {
|
||||
display: inline-flex; align-items: center; justify-content: center; flex: none;
|
||||
width: 16px; height: 16px;
|
||||
background: var(--secondary);
|
||||
border: 1px solid var(--control-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.checkbox.checked { background: var(--primary); border-color: var(--primary); }
|
||||
/* the tick is drawn, not an icon: a 9 x 6.5 check on a 1px round stroke, same
|
||||
geometry as AnimatedCheckPainter. the mask box is half a pixel bigger each
|
||||
side so the round caps dont get clipped */
|
||||
.checkbox.checked::after {
|
||||
content: ""; width: 10px; height: 7.5px;
|
||||
background: var(--primary-foreground);
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-0.5 -0.5 10 7.5'%3E%3Cpolyline points='0,3.25 3.15,6.5 9,0' fill='none' stroke='black' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-0.5 -0.5 10 7.5'%3E%3Cpolyline points='0,3.25 3.15,6.5 9,0' fill='none' stroke='black' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
}
|
||||
.checkbox.indeterminate::after { content: ""; width: 8px; height: 8px; background: var(--primary); border-radius: var(--radius-xs); }
|
||||
.checkbox.disabled { border-color: var(--muted); }
|
||||
|
||||
/* (32 + 4) x (16 + 4) is literal in the source; the inset is a quarter of the
|
||||
container gap and the thumb is a square filling whats left */
|
||||
.switch {
|
||||
display: inline-flex; align-items: stretch; flex: none;
|
||||
width: 36px; height: 20px; padding: calc(var(--container-gap) * 0.25);
|
||||
background: var(--switch-track-inactive);
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
.switch::after { content: ""; height: 100%; aspect-ratio: 1; border-radius: var(--radius-lg); background: var(--foreground); }
|
||||
.switch.on { background: var(--primary); justify-content: flex-end; }
|
||||
.switch.on::after { background: var(--background); }
|
||||
.switch.disabled { background: var(--muted); }
|
||||
.switch.disabled::after { background: var(--muted-foreground); }
|
||||
|
||||
/* ---- surfaces & layout ----------------------------------------------- */
|
||||
|
||||
/* the whole look: chrome ground, panels standing on it, panel-gap gutters */
|
||||
.shell { background: var(--chrome); padding: var(--panel-gap); display: flex; gap: var(--panel-gap); }
|
||||
.panel {
|
||||
background: var(--background);
|
||||
border: var(--panel-border-width) solid var(--panel-border);
|
||||
border-radius: var(--panel-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel.active { border-color: var(--panel-border-highlighted); }
|
||||
.panel.sunken { background: var(--surface-sunken); }
|
||||
|
||||
.chrome-bar {
|
||||
display: flex; align-items: center; gap: var(--gap-sm);
|
||||
height: var(--chrome-bar-height);
|
||||
background: var(--chrome);
|
||||
padding: 0 var(--gap-lg);
|
||||
}
|
||||
.chrome-bar .vr { width: 1px; height: 12px; background: var(--divider); }
|
||||
|
||||
.panel-header { display: flex; align-items: center; gap: 2px; padding: 10px 10px 8px; }
|
||||
.panel-header .icon { width: 12px; height: 12px; margin: 0 3px; color: var(--foreground); }
|
||||
.panel-header .title { font-weight: var(--weight-medium); flex: 1; }
|
||||
|
||||
/* a nav rail's category, as the hub draws it - gapMd round the run, a
|
||||
semibold caption over it, gapXxs between the buttons only */
|
||||
.nav-group { padding: var(--gap-md); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
.nav-group > .caption { padding: 0 var(--gap-sm) calc(var(--gap-xs) - var(--gap-xxs)); }
|
||||
|
||||
/* explorer tree rows (A&A). an 18px slot for the chevron and one for the
|
||||
icon, 2px between, 12px glyphs - all literals in explorer_panel.dart. the
|
||||
hover is inset 2px with a 3px corner, same file */
|
||||
.explorer-row {
|
||||
--depth: 0;
|
||||
position: relative; isolation: isolate;
|
||||
display: flex; align-items: center; gap: 2px;
|
||||
height: var(--explorer-row-height);
|
||||
padding: 0 var(--explorer-row-end-padding) 0 calc(var(--explorer-row-base-padding) + var(--depth) * 18px);
|
||||
color: var(--row-text); white-space: nowrap;
|
||||
}
|
||||
.explorer-row.depth-1 { --depth: 1; }
|
||||
.explorer-row.depth-2 { --depth: 2; }
|
||||
.explorer-row.depth-3 { --depth: 3; }
|
||||
.explorer-row > .slot { flex: none; width: 18px; display: flex; align-items: center; justify-content: center; }
|
||||
.explorer-row .icon { width: 12px; height: 12px; }
|
||||
.explorer-row .chevron { color: var(--muted-foreground); }
|
||||
.explorer-row > .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.explorer-row:hover::before, .explorer-row.is-hovered::before {
|
||||
content: ""; position: absolute; inset: 2px; z-index: -1;
|
||||
background: var(--row-hovered); border-radius: 3px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--properties-section-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--container-padding);
|
||||
}
|
||||
.outlined {
|
||||
background: var(--background);
|
||||
border: 1px solid var(--control-border);
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
.divider { height: 1px; background: var(--divider); border: 0; margin: 0; }
|
||||
|
||||
/* ---- properties section --------------------------------------------- */
|
||||
|
||||
.properties-section {
|
||||
margin: 0 var(--gap-xs) var(--properties-section-gap);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--properties-section-border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
.properties-section > .header {
|
||||
display: flex; align-items: center; gap: var(--gap-sm);
|
||||
padding: var(--control-padding-y) var(--control-padding-x);
|
||||
color: var(--row-text); font-weight: var(--weight-semibold);
|
||||
}
|
||||
.properties-section > .header .chevron { color: var(--muted-foreground); }
|
||||
/* the header's own subtitle - captionStyle, so the ambient weight not the title's */
|
||||
.properties-section > .header .subtitle { display: block; font-size: var(--text-xxs); color: var(--muted-foreground); font-weight: var(--weight-body); }
|
||||
.properties-section > .header .trailing { margin-left: auto; font-weight: var(--weight-control); }
|
||||
/* the last row gets more room under it than the step between rows */
|
||||
.properties-section > .rows { padding: 0 var(--control-padding-x) var(--gap-md); display: flex; flex-direction: column; gap: var(--gap-xs); }
|
||||
.properties-section > .actions {
|
||||
display: flex; justify-content: flex-end; gap: var(--gap-sm);
|
||||
border-top: 1px solid var(--properties-section-border);
|
||||
background: color-mix(in srgb, var(--card) 55%, var(--muted));
|
||||
padding: var(--gap-sm) var(--control-padding-x);
|
||||
}
|
||||
|
||||
/* PropertyRow - the label up to the split, the control from it. the gap comes
|
||||
out of the label's padding, so the control's edge lands right on the split.
|
||||
it wraps, so an .error or .description can take a line of its own under it */
|
||||
.property-row { display: flex; flex-wrap: wrap; align-items: center; }
|
||||
.property-row > .label { flex: 0 0 calc(var(--property-split) * 100%); padding-right: var(--property-label-gap); min-width: 0; color: var(--row-text); text-align: right; }
|
||||
.property-row > .label .subtitle { display: block; font-size: var(--text-xxs); color: var(--muted-foreground); }
|
||||
.property-row > .label .tag { font-size: var(--text-xxs); color: var(--muted-foreground); margin-left: var(--gap-xs); }
|
||||
.property-row > .value { flex: 1 1 0; min-width: 0; min-height: var(--property-row-height); display: flex; align-items: center; gap: var(--control-gap); }
|
||||
.property-row > .value > .field, .property-row > .value > .select, .property-row > .value > .btn-group.span { flex: 1; }
|
||||
/* why the value was refused - under the row, centred, destructive */
|
||||
.property-row > .error { flex: 0 0 100%; margin-top: var(--gap-xs); text-align: center; font-size: var(--text-xxs); color: var(--destructive); }
|
||||
/* copy about the setting, in a box that matches the outline controls */
|
||||
.property-row > .description {
|
||||
flex: 0 0 100%; margin-top: var(--gap-xs);
|
||||
padding: var(--gap-xs) var(--control-padding-x);
|
||||
background: var(--control-fill);
|
||||
border: var(--control-border-width) solid var(--control-border);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center; font-size: var(--text-xxs); color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* ---- settings list --------------------------------------------------- */
|
||||
|
||||
/* ruled BETWEEN rows, and the rule overshoots the text by the row inset so it
|
||||
reads as a divider rather than the edge of a box */
|
||||
.settings-list { display: flex; flex-direction: column; }
|
||||
.settings-list > .settings-row + .settings-row { border-top: 1px solid var(--divider); }
|
||||
.settings-row { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--gap-md); padding: var(--gap-md) var(--gap-sm); }
|
||||
.settings-row > .label { flex: 1 1 0; min-width: 0; font-size: var(--text-xs); font-weight: var(--weight-control); color: var(--foreground); }
|
||||
.settings-row > .label .subtitle { display: block; margin-top: var(--gap-xxs); font-size: var(--text-xxs); font-weight: var(--weight-body); color: var(--muted-foreground); }
|
||||
.settings-row > .label .tag { font-size: var(--text-xxs); font-weight: var(--weight-control); color: var(--muted-foreground); margin-left: var(--gap-xs); }
|
||||
/* a loose Flexible: at most what the label leaves, field pinned right. a
|
||||
select or a button keeps its own width, a text field fills the slot.
|
||||
the minimum lives here rather than on the label so the label can stay a
|
||||
plain block - its the height of a row WITH a subtitle, so a list mixing the
|
||||
two doesnt come out ragged */
|
||||
.settings-row > .field-slot {
|
||||
flex: 1 1 0; min-width: 0;
|
||||
min-height: calc(var(--label-column-height) + var(--gap-xxs));
|
||||
display: flex; align-items: center; justify-content: flex-end;
|
||||
}
|
||||
.settings-row > .field-slot > .field { flex: 1; }
|
||||
.settings-row > .error { flex: 0 0 100%; margin-top: var(--gap-sm); font-size: var(--text-xxs); color: var(--destructive); }
|
||||
.settings-row > .description { flex: 0 0 100%; margin-top: var(--gap-sm); font-size: var(--text-xxs); color: var(--muted-foreground); }
|
||||
.settings-row > .error + .description { margin-top: var(--gap-xxs); }
|
||||
|
||||
/* ---- dialogs, toasts ------------------------------------------------- */
|
||||
|
||||
/* AlertDialog's barrier - black at 0.8, no blur */
|
||||
.modal-backdrop { background: rgb(0 0 0 / 0.8); }
|
||||
/* pane overlays keep the lighter frosted scrim */
|
||||
.scrim { background: color-mix(in srgb, var(--background) 35%, transparent); backdrop-filter: blur(3px); }
|
||||
|
||||
.dialog {
|
||||
width: 100%; max-width: var(--dialog-max-width);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--properties-section-border);
|
||||
border-radius: var(--radius-xxl);
|
||||
overflow: hidden;
|
||||
}
|
||||
.dialog > .body { padding: calc(var(--container-padding) * 1.2) calc(var(--container-padding) * 1.4) calc(var(--container-padding) * 0.6); display: flex; flex-direction: column; gap: var(--container-gap); }
|
||||
.dialog > .body .title { font-size: var(--text-xs); font-weight: var(--weight-semibold); }
|
||||
.dialog > .body .content { font-size: var(--text-xs); color: var(--muted-foreground); }
|
||||
.dialog > .actions {
|
||||
display: flex; justify-content: flex-end; gap: var(--container-gap);
|
||||
border-top: 1px solid var(--properties-section-border);
|
||||
background: color-mix(in srgb, var(--card) 55%, var(--muted));
|
||||
padding: var(--gap-sm) var(--control-padding-x);
|
||||
}
|
||||
|
||||
/* the desktop toast. 300 wide, the 32px badge, the 18px glyph, the white on
|
||||
it and the 3px bar are all literals in toast.dart */
|
||||
.toast {
|
||||
--toast-accent: var(--primary);
|
||||
display: flex; flex-direction: column;
|
||||
width: 300px;
|
||||
background: var(--popover);
|
||||
border: 1px solid var(--popover-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
.toast.success { --toast-accent: var(--chart-2); }
|
||||
.toast.error { --toast-accent: var(--destructive); }
|
||||
.toast > .body { display: flex; align-items: flex-start; gap: var(--container-gap); padding: var(--container-gap); }
|
||||
.toast .badge {
|
||||
flex: none; width: 32px; height: 32px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: var(--control-border-width) solid var(--control-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: #fff;
|
||||
}
|
||||
.toast .badge .icon { width: 18px; height: 18px; }
|
||||
.toast .text { flex: 1; min-width: 0; }
|
||||
/* title and subtitle are the same text, told apart by colour only */
|
||||
.toast .title { font-weight: var(--weight-control); color: var(--foreground); }
|
||||
.toast .subtitle { margin-top: calc(var(--container-gap) * 0.375); font-weight: var(--weight-control); color: var(--muted-foreground); }
|
||||
/* the countdown - the accent drains off the right, over the border colour at 0.4 */
|
||||
.toast > .drain { display: flex; height: 3px; background: color-mix(in srgb, var(--popover-border) 40%, transparent); }
|
||||
.toast > .drain > span { background: var(--toast-accent); }
|
||||
|
||||
/* CircularProgressIndicator - no track, a 1.35pi arc (~243deg) on a 4px
|
||||
round stroke, 18px, primary, once round every 900ms. all literal in
|
||||
misc.dart. the two dots are the round caps */
|
||||
.spinner {
|
||||
position: relative; flex: none;
|
||||
width: 18px; height: 18px; border-radius: 50%;
|
||||
background: conic-gradient(var(--primary) 0 243deg, transparent 0);
|
||||
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
|
||||
mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
|
||||
animation: gu-spin 0.9s linear infinite;
|
||||
}
|
||||
.spinner::before, .spinner::after {
|
||||
content: ""; position: absolute; left: 7px; top: 0;
|
||||
width: 4px; height: 4px; border-radius: 50%;
|
||||
background: var(--primary);
|
||||
transform-origin: 2px 9px;
|
||||
}
|
||||
.spinner::after { transform: rotate(243deg); }
|
||||
@keyframes gu-spin { to { transform: rotate(360deg); } }
|
||||
@@ -0,0 +1,94 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Button groups</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-block { padding: 0 var(--gap-xs); }
|
||||
.ds-half + .ds-half { margin-top: var(--gap-lg); }
|
||||
.ds-block > .caption { padding-bottom: var(--gap-xs); }
|
||||
.ds-half { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-xl); }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Button groups</div>
|
||||
<div class="subtitle">Controls joined into one. Only the outer corners round.</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-half">
|
||||
<div class="ds-block">
|
||||
<div class="caption">Button welded to a field</div>
|
||||
<div class="btn-group span">
|
||||
<label class="field grow"><input value="gk_live_4f8a2c91" aria-label="Client ID"></label>
|
||||
<button class="btn secondary icon-only" aria-label="Copy"><svg class="icon" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-block">
|
||||
<div class="caption">Segmented choice</div>
|
||||
<div class="btn-group fill">
|
||||
<button class="btn secondary">Off</button>
|
||||
<button class="btn primary">Optional</button>
|
||||
<button class="btn secondary">Required</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-half">
|
||||
<div class="ds-block">
|
||||
<div class="caption">Vertical, with a group nested in it</div>
|
||||
<div class="btn-group vertical span">
|
||||
<label class="field"><input placeholder="Email" aria-label="Email"></label>
|
||||
<div class="btn-group span">
|
||||
<label class="field grow"><input placeholder="Password" type="password" aria-label="Password"></label>
|
||||
<button class="btn outline icon-only" aria-label="Show password"><svg class="icon" viewBox="0 0 24 24"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" /><circle cx="12" cy="12" r="3" /></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-block">
|
||||
<div class="caption">Toolbar toggles - primary on, outline off</div>
|
||||
<div class="chrome-bar">
|
||||
<button class="btn primary icon-only" aria-label="Bold"><svg class="icon" viewBox="0 0 24 24"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" /></svg></button>
|
||||
<button class="btn outline icon-only" aria-label="Italic"><svg class="icon" viewBox="0 0 24 24"><line x1="19" x2="10" y1="4" y2="4" /><line x1="14" x2="5" y1="20" y2="20" /><line x1="15" x2="9" y1="4" y2="20" /></svg></button>
|
||||
<button class="btn outline icon-only" aria-label="Underline"><svg class="icon" viewBox="0 0 24 24"><path d="M6 4v6a6 6 0 0 0 12 0V4" /><line x1="4" x2="20" y1="20" y2="20" /></svg></button>
|
||||
<span class="vr"></span>
|
||||
<div class="btn-group">
|
||||
<button class="btn primary icon-only" aria-label="Align left"><svg class="icon" viewBox="0 0 24 24"><path d="M21 5H3" /><path d="M15 12H3" /><path d="M17 19H3" /></svg></button>
|
||||
<button class="btn outline icon-only" aria-label="Align centre"><svg class="icon" viewBox="0 0 24 24"><path d="M21 5H3" /><path d="M17 12H7" /><path d="M19 19H5" /></svg></button>
|
||||
<button class="btn outline icon-only" aria-label="Align right"><svg class="icon" viewBox="0 0 24 24"><path d="M21 5H3" /><path d="M21 12H9" /><path d="M21 19H7" /></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>A field with its one action welded on: a spanning group, the field grows, then a secondary IconButton.</li>
|
||||
<li>Groups nest. A corner only rounds where both groups agree it's an outer one.</li>
|
||||
<li>A segmented choice: fill the group, chosen one primary, the rest secondary, labels centred.</li>
|
||||
<li>Editor toolbar toggles: primary when on, outline when off.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Leave a segmented control shrunk to its labels. It should span its field.</li>
|
||||
<li>Mix the primary-on toggle pattern and Toggle on the same surface.</li>
|
||||
<li>Use a secondary off state unless its a master switch whose off state matters.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,130 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Buttons</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gap-sm); padding: 0 var(--gap-xs); }
|
||||
.ds-block + .ds-block { margin-top: var(--gap-lg); }
|
||||
.ds-block > .caption { padding: 0 var(--gap-xs) var(--gap-xs); }
|
||||
|
||||
.ds-table { width: 100%; border-collapse: collapse; }
|
||||
.ds-table td { padding: var(--gap-xs) var(--gap-xs); border-top: var(--control-border-width) solid var(--divider); vertical-align: middle; }
|
||||
.ds-table tr:first-child td { border-top: 0; }
|
||||
.ds-table td:first-child { width: 30%; }
|
||||
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Buttons</div>
|
||||
<div class="subtitle">Pick the variant, then stop. The theme does the rest.</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-block">
|
||||
<div class="caption">Variants</div>
|
||||
<div class="ds-row">
|
||||
<button class="btn primary">Save</button>
|
||||
<button class="btn secondary">Duplicate</button>
|
||||
<button class="btn outline">Cancel</button>
|
||||
<button class="btn ghost">Settings</button>
|
||||
<button class="btn ghost-destructive">Remove</button>
|
||||
<button class="btn destructive">Delete</button>
|
||||
<button class="btn link">Learn more</button>
|
||||
<button class="btn text">Forgot password?</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-block">
|
||||
<div class="caption">With a leading icon</div>
|
||||
<div class="ds-row">
|
||||
<button class="btn primary"><svg class="icon" viewBox="0 0 24 24"><path d="M5 12h14" /><path d="M12 5v14" /></svg>New product</button>
|
||||
<button class="btn outline"><svg class="icon" viewBox="0 0 24 24"><path d="M12 15V3" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="m7 10 5 5 5-5" /></svg>Export</button>
|
||||
<button class="btn ghost"><svg class="icon" viewBox="0 0 24 24"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>Settings</button>
|
||||
<button class="btn ghost-destructive"><svg class="icon" viewBox="0 0 24 24"><path d="M10 11v6" /><path d="M14 11v6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg>Revoke</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-block">
|
||||
<div class="caption">Icon only - IconButton, never a Button with just an icon in it</div>
|
||||
<div class="ds-row">
|
||||
<button class="btn primary icon-only" aria-label="Add"><svg class="icon" viewBox="0 0 24 24"><path d="M5 12h14" /><path d="M12 5v14" /></svg></button>
|
||||
<button class="btn secondary icon-only" aria-label="Copy"><svg class="icon" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg></button>
|
||||
<button class="btn outline icon-only" aria-label="Refresh"><svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /><path d="M8 16H3v5" /></svg></button>
|
||||
<button class="btn ghost icon-only" aria-label="Edit"><svg class="icon" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /><path d="m15 5 4 4" /></svg></button>
|
||||
<button class="btn ghost-destructive icon-only" aria-label="Delete"><svg class="icon" viewBox="0 0 24 24"><path d="M10 11v6" /><path d="M14 11v6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-block">
|
||||
<div class="caption">Disabled</div>
|
||||
<div class="ds-row">
|
||||
<button class="btn primary" disabled>Save</button>
|
||||
<button class="btn secondary" disabled>Duplicate</button>
|
||||
<button class="btn outline" disabled>Cancel</button>
|
||||
<button class="btn ghost" disabled>Settings</button>
|
||||
<button class="btn ghost-destructive" disabled>Remove</button>
|
||||
<button class="btn destructive" disabled>Delete</button>
|
||||
<button class="btn link" disabled>Learn more</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-block">
|
||||
<div class="caption">Hovered</div>
|
||||
<div class="ds-row">
|
||||
<button class="btn primary is-hovered">Save</button>
|
||||
<button class="btn secondary is-hovered">Duplicate</button>
|
||||
<button class="btn outline is-hovered">Cancel</button>
|
||||
<button class="btn ghost is-hovered">Settings</button>
|
||||
<button class="btn ghost-destructive is-hovered">Remove</button>
|
||||
<button class="btn destructive is-hovered">Delete</button>
|
||||
<button class="btn link is-hovered">Learn more</button>
|
||||
<button class="btn text is-hovered">Forgot password?</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Variants mean something</div>
|
||||
</div>
|
||||
|
||||
<table class="ds-table">
|
||||
<tr><td><button class="btn primary">Primary</button></td><td>The one action, or the thing thats on or chosen. One per view, ideally.</td></tr>
|
||||
<tr><td><button class="btn secondary">Secondary</button></td><td>A neutral filled control. A resting toggle, a button welded to a field, the selected nav item.</td></tr>
|
||||
<tr><td><button class="btn outline">Outline</button></td><td>A normal action that isnt the main one. Cancel, Retry, Previous and Next.</td></tr>
|
||||
<tr><td><button class="btn ghost">Ghost</button></td><td>Chrome, toolbars, nav items, row actions. Anything that shouldnt compete.</td></tr>
|
||||
<tr><td><button class="btn destructive">Destructive</button></td><td>The confirm button of a destructive dialog.</td></tr>
|
||||
<tr><td><button class="btn ghost-destructive">Ghost destructive</button></td><td>A destructive trigger that isnt a filled slab. Remove, Revoke, Delete in a row.</td></tr>
|
||||
<tr><td><button class="btn link">Link</button> <button class="btn text">Text</button></td><td>Inline, in running copy.</td></tr>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Dialogs: Cancel is outline, confirm is primary or destructive.</li>
|
||||
<li>Retry after an error, and an empty state's call to action: outline.</li>
|
||||
<li>Panel header actions: a ghost IconButton, each in a Tooltip.</li>
|
||||
<li>An action in a settings row: ghost. In a property row: secondary.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Bold a button label, or colour its text or icon by hand.</li>
|
||||
<li>Hand-colour a ghost button red. Thats what ghost destructive is for.</li>
|
||||
<li>Set an alignment unless the button is stretched wider than its label.</li>
|
||||
<li>Force a compact density on a button to make it fit.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Dialogs</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-stage { display: flex; flex-direction: column; align-items: center; gap: var(--gap-xl); padding: var(--gap-xxl) var(--container-padding); border-radius: var(--radius-xl); }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dialogs</div>
|
||||
<div class="subtitle">A small confirm, over a dimmed page.</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop ds-stage">
|
||||
<div class="dialog" role="alertdialog" aria-labelledby="d1-title">
|
||||
<div class="body">
|
||||
<div class="title" id="d1-title">Delete this product?</div>
|
||||
<div class="content">People who already bought it keep their licence. It cant be sold again.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn outline">Cancel</button>
|
||||
<button class="btn destructive">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog" role="alertdialog" aria-labelledby="d2-title">
|
||||
<div class="body">
|
||||
<div class="title" id="d2-title">Publish changes?</div>
|
||||
<div class="content">The new price goes live at checkout straight away.</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn outline">Cancel</button>
|
||||
<button class="btn primary">Publish</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>showDialog with an AlertDialog: a title, a line of content and its actions.</li>
|
||||
<li>Cancel is outline. Confirm is primary, or destructive when it removes something.</li>
|
||||
<li>Bigger jobs, like settings or export, go in a pane overlay instead.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Constrain the dialog's width. It caps itself at the dialog max width.</li>
|
||||
<li>Build the action row by hand. The dialog draws its own band.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,110 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Feedback</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-row { display: flex; flex-wrap: wrap; gap: var(--gap-md); padding: 0 var(--gap-xs); align-items: flex-start; }
|
||||
.ds-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--gap-xl); padding: 0 var(--gap-xs); }
|
||||
.ds-cell { display: flex; flex-direction: column; gap: var(--gap-xs); }
|
||||
.ds-state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--gap-sm); padding: var(--gap-xxl) var(--gap-md); }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Feedback</div>
|
||||
<div class="subtitle">Toasts, and the three states a page can be in.</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-row">
|
||||
<div class="toast" role="status">
|
||||
<div class="body">
|
||||
<div class="badge"><svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg></div>
|
||||
<div class="text">
|
||||
<div class="title">Link copied</div>
|
||||
<div class="subtitle">Paste it anywhere a buyer can reach.</div>
|
||||
</div>
|
||||
<button class="btn outline icon-only" aria-label="Close"><svg class="icon" viewBox="0 0 24 24"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg></button>
|
||||
</div>
|
||||
<div class="drain"><span style="width: 80%"></span></div>
|
||||
</div>
|
||||
<div class="toast success" role="status">
|
||||
<div class="body">
|
||||
<div class="badge"><svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></svg></div>
|
||||
<div class="text">
|
||||
<div class="title">Saved</div>
|
||||
<div class="subtitle">Your changes are live.</div>
|
||||
</div>
|
||||
<button class="btn outline icon-only" aria-label="Close"><svg class="icon" viewBox="0 0 24 24"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg></button>
|
||||
</div>
|
||||
<div class="drain"><span style="width: 45%"></span></div>
|
||||
</div>
|
||||
<div class="toast error" role="alert">
|
||||
<div class="body">
|
||||
<div class="badge"><svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></svg></div>
|
||||
<div class="text">
|
||||
<div class="title">Couldnt save</div>
|
||||
<div class="subtitle">Check your connection and try again.</div>
|
||||
</div>
|
||||
<button class="btn outline icon-only" aria-label="Close"><svg class="icon" viewBox="0 0 24 24"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg></button>
|
||||
</div>
|
||||
<div class="drain"><span style="width: 20%"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Loading, empty, error</div>
|
||||
<div class="subtitle">The plainest thing that works.</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-grid">
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Loading</div>
|
||||
<div class="ds-state outlined">
|
||||
<div class="spinner" role="progressbar" aria-label="Loading"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Empty</div>
|
||||
<div class="ds-state outlined">
|
||||
<div class="text-xs muted">No coupons yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Error</div>
|
||||
<div class="ds-state outlined">
|
||||
<div class="text-xs muted">Couldnt load orders.</div>
|
||||
<button class="btn outline">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>showAppToast with a title, a subtitle and a severity: info, success or error. The severity colours the drain bar, nothing else.</li>
|
||||
<li>Loading is a spinner. Empty is one muted line. Error is the message muted and an outline Retry.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Use skeletons or shimmer.</li>
|
||||
<li>Hand-build a toast out of a card and some padding.</li>
|
||||
<li>Hand-build a warning box. A page that wants more is asking for a new garage_ui component.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Menus and tooltips</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--gap-xl); padding: 0 var(--gap-xs); align-items: start; }
|
||||
.ds-cell { display: flex; flex-direction: column; align-items: flex-start; gap: var(--gap-xs); }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Menus and tooltips</div>
|
||||
<div class="subtitle">Floating surfaces. Flat, no shadow.</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-grid">
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Menu</div>
|
||||
<div class="popover" role="menu">
|
||||
<div class="menu-label">Edit</div>
|
||||
<div class="menu-item" role="menuitem"><svg class="icon" viewBox="0 0 24 24"><circle cx="6" cy="6" r="3" /><path d="M8.12 8.12 12 12" /><path d="M20 4 8.12 15.88" /><circle cx="6" cy="18" r="3" /><path d="M14.8 14.8 20 20" /></svg>Cut<span class="shortcut">⌘ + X</span></div>
|
||||
<div class="menu-item is-hovered" role="menuitem"><svg class="icon" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg>Copy<span class="shortcut">⌘ + C</span></div>
|
||||
<div class="menu-item" role="menuitem"><svg class="icon" viewBox="0 0 24 24"><path d="M11 14h10" /><path d="M16 4h2a2 2 0 0 1 2 2v1.344" /><path d="m17 18 4-4-4-4" /><path d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113" /><rect x="8" y="2" width="8" height="4" rx="1" /></svg>Paste<span class="shortcut">⌘ + V</span></div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-item" role="menuitem"><svg class="icon" viewBox="0 0 24 24"><path d="M10 11v6" /><path d="M14 11v6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg>Delete</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Context menu</div>
|
||||
<div class="popover" role="menu">
|
||||
<div class="menu-item" role="menuitem"><svg class="icon" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" /></svg>Reset to Default</div>
|
||||
<div class="menu-item" role="menuitem"><svg class="icon" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg>Copy</div>
|
||||
<div class="menu-item disabled" aria-disabled="true" role="menuitem"><svg class="icon" viewBox="0 0 24 24"><path d="M11 14h10" /><path d="M16 4h2a2 2 0 0 1 2 2v1.344" /><path d="m17 18 4-4-4-4" /><path d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113" /><rect x="8" y="2" width="8" height="4" rx="1" /></svg>Paste</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Tooltip</div>
|
||||
<button class="btn ghost icon-only" aria-label="Copy"><svg class="icon" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg></button>
|
||||
<span class="tooltip" role="tooltip">Copy client ID</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>MenuButton rows, with MenuLabel and MenuDivider to group them.</li>
|
||||
<li>showContextMenu for right-click. Property rows get Reset, Copy and Paste, greyed when theres nothing to do.</li>
|
||||
<li>A tooltip on every icon-only button, with plain text in it.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Hand-build a popover out of a container with a fill, border and radius.</li>
|
||||
<li>Colour tooltip text, or pad the tooltip yourself.</li>
|
||||
<li>Add a shadow to a menu. Surfaces are separated by their border and fill.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Properties section</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Properties section</div>
|
||||
<div class="subtitle">A boxed card of label and value rows.</div>
|
||||
</div>
|
||||
|
||||
<div class="properties-section">
|
||||
<div class="header" role="button" aria-expanded="true">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<span>Subscription</span>
|
||||
<span class="trailing muted">Active</span>
|
||||
</div>
|
||||
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Plan</div>
|
||||
<div class="value"><label class="field secondary readonly"><input value="Pro, monthly" readonly disabled aria-label="Plan"></label></div>
|
||||
</div>
|
||||
|
||||
<div class="property-row">
|
||||
<div class="label">Seats<span class="subtitle">People who can sign in</span></div>
|
||||
<div class="value"><label class="field secondary error"><input value="0" aria-label="Seats"></label></div>
|
||||
<div class="error">Needs at least one seat</div>
|
||||
</div>
|
||||
|
||||
<div class="property-row">
|
||||
<div class="label">Billing</div>
|
||||
<div class="value">
|
||||
<div class="select secondary" role="button">
|
||||
<span>Monthly</span>
|
||||
<svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">Switching takes effect from the next invoice, not this one.</div>
|
||||
</div>
|
||||
|
||||
<div class="property-row">
|
||||
<div class="label">Invoices</div>
|
||||
<div class="value"><button class="btn secondary">Open billing portal</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn ghost">Reset</button>
|
||||
<button class="btn ghost-destructive">Cancel plan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">The value slot is never bare text</div>
|
||||
<div class="subtitle">A read-only value is a secondary field, read only and disabled.</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Beside real fields, bare text reads as a caption and the column goes ragged.</li>
|
||||
<li>A button in the value slot sits left and keeps its own width. It doesnt stretch.</li>
|
||||
<li>A refused value says why in the row's error, under it. Copy about the setting goes in the description box.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Inspectors, account and security pages, and lists of records, one section per record.</li>
|
||||
<li>A status or count in the header's trailing slot. It stays visible when collapsed.</li>
|
||||
<li>What the section can do, in the actions band under the rows.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Use outline fields or ghost selects in here. Controls in a section are secondary.</li>
|
||||
<li>Put live data in the subtitle. No counts, dates, names or statuses.</li>
|
||||
<li>Add gaps between sections. Each one carries its own margin.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,84 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Select</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--gap-xl); padding: 0 var(--gap-xs); align-items: start; }
|
||||
.ds-cell { display: flex; flex-direction: column; align-items: flex-start; gap: var(--gap-xs); min-width: 0; }
|
||||
.ds-open { display: flex; flex-direction: column; gap: var(--gap-xs); align-items: flex-start; padding: var(--gap-lg) var(--gap-xs) 0; }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Select</div>
|
||||
<div class="subtitle">One value from a list, in a popup.</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-grid">
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Outline - the default</div>
|
||||
<div class="select" role="button">
|
||||
<span>Engineering</span>
|
||||
<svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Secondary - in a property row</div>
|
||||
<div class="select secondary" role="button">
|
||||
<span>Monthly</span>
|
||||
<svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Ghost - in a settings row</div>
|
||||
<div class="select ghost" role="button">
|
||||
<span>All regions</span>
|
||||
<svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-open">
|
||||
<div class="caption">Open</div>
|
||||
<div class="select" role="button">
|
||||
<span>Engineering</span>
|
||||
<svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg>
|
||||
</div>
|
||||
<div class="select-popup" role="listbox">
|
||||
<div class="option" role="option">Design</div>
|
||||
<div class="option selected" role="option" aria-selected="true">Engineering<svg class="icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></svg></div>
|
||||
<div class="option is-hovered" role="option">Finance</div>
|
||||
<div class="option" role="option">Support</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Outline by default, beside other outline fields.</li>
|
||||
<li>Secondary in a property row. The row forces it anyway.</li>
|
||||
<li>Ghost in a settings row, or where it reports a value rather than offering a control.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Give a toolbar select a literal width.</li>
|
||||
<li>Put a ghost select inside a properties section.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Selection controls</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-xl); padding: 0 var(--gap-xs); }
|
||||
.ds-cell { display: flex; flex-direction: column; gap: var(--gap-md); }
|
||||
.ds-cell > .caption { margin-bottom: calc(var(--gap-xs) - var(--gap-md)); }
|
||||
.ds-pair { display: inline-flex; align-items: center; gap: var(--gap-sm); }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Selection controls</div>
|
||||
<div class="subtitle">On or off. Checkbox or Switch, one per surface.</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-grid">
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Checkbox</div>
|
||||
<label class="ds-pair"><span class="checkbox" role="checkbox" aria-checked="false"></span>Snap to grid</label>
|
||||
<label class="ds-pair"><span class="checkbox checked" role="checkbox" aria-checked="true"></span>Show labels</label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Switch</div>
|
||||
<label class="ds-pair"><span class="switch" role="switch" aria-checked="false"></span>Email receipts</label>
|
||||
<label class="ds-pair"><span class="switch on" role="switch" aria-checked="true"></span>Active</label>
|
||||
<label class="ds-pair muted"><span class="switch disabled" role="switch" aria-checked="false" aria-disabled="true"></span>Managed by your organisation</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Checkbox in dense editor panels. Arcs and Angles uses it throughout.</li>
|
||||
<li>Switch in rows on a product page. The hub uses it in settings rows.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Mix checkboxes and switches on one surface.</li>
|
||||
<li>Recolour the track, thumb or tick. The accent is primary.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Settings list</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Product</div>
|
||||
<div class="subtitle">What people see at checkout</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-list">
|
||||
<div class="settings-row">
|
||||
<div class="label">Name<span class="tag">Required</span><span class="subtitle">Shown on receipts</span></div>
|
||||
<div class="field-slot"><label class="field"><input value="Pro plan" aria-label="Name"></label></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="label">Billing<span class="subtitle">How often it charges</span></div>
|
||||
<div class="field-slot">
|
||||
<div class="select ghost" role="button">
|
||||
<span>Monthly</span>
|
||||
<svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="label">Checkout link</div>
|
||||
<div class="field-slot">
|
||||
<button class="btn ghost"><svg class="icon" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg>Copy link</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="label">Price<span class="tag">Required</span></div>
|
||||
<div class="field-slot"><label class="field error"><input value="0.00" aria-label="Price"><span class="unit">GBP</span></label></div>
|
||||
<div class="error">A paid product needs a price above zero</div>
|
||||
<div class="description">Buyers see this converted to their own currency at checkout.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Forms on the page ground: products, coupons, clients.</li>
|
||||
<li>Selects, buttons and icon buttons are ghost. Text fields are outline, read-only ones too.</li>
|
||||
<li>"Required" goes in the row's action slot, beside the label.</li>
|
||||
<li>Why a value was refused goes in the row's error slot, under the row.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Use secondary fields here. With no card behind them, filled controls float.</li>
|
||||
<li>Stretch a select or a button to a column. They keep their own width - only a text field fills its slot.</li>
|
||||
<li>Write a sentence in the subtitle. That goes in the description.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,98 @@
|
||||
<!-- @dsCard group="Components" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Text fields</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
.ds { padding: 0 var(--container-padding) var(--container-padding); }
|
||||
.ds-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--gap-lg) var(--gap-xl); padding: 0 var(--gap-xs); }
|
||||
.ds-cell { display: flex; flex-direction: column; gap: var(--gap-xs); min-width: 0; }
|
||||
.ds-rules { margin: 0; padding: 0 var(--gap-xs) 0 var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="ds">
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Text fields</div>
|
||||
<div class="subtitle">Outline for free text, secondary for values.</div>
|
||||
</div>
|
||||
|
||||
<div class="ds-grid">
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Outline - the default</div>
|
||||
<label class="field stretch"><input placeholder="Product name" aria-label="Product name"></label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Outline, filled in</div>
|
||||
<label class="field stretch"><input value="Pro plan" aria-label="Product name"></label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">With a leading icon</div>
|
||||
<label class="field stretch">
|
||||
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" /></svg>
|
||||
<input placeholder="Search" aria-label="Search">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Secondary</div>
|
||||
<label class="field secondary stretch"><input value="1280" aria-label="Width"></label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Read only</div>
|
||||
<label class="field secondary readonly stretch"><input value="prod_8f2k1c" readonly disabled aria-label="Product ID"></label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Numeric, centred, with a unit</div>
|
||||
<label class="field secondary center stretch">
|
||||
<input value="24" aria-label="Stroke width">
|
||||
<span class="unit">px</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Error - the outline reddens, never the fill</div>
|
||||
<label class="field error stretch"><input value="pro plan!" aria-label="Slug"></label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Outline, hovered</div>
|
||||
<label class="field stretch is-hovered"><input value="Pro plan" aria-label="Product name"></label>
|
||||
</div>
|
||||
|
||||
<div class="ds-cell">
|
||||
<div class="caption">Read only, monospace - ids and skus</div>
|
||||
<label class="field secondary readonly mono stretch"><input value="usr_8f2c41d9a7" readonly disabled aria-label="User ID"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Use it for</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Outline for free text: names, search, a composer, a form.</li>
|
||||
<li>Secondary for numeric property values and read-only values.</li>
|
||||
<li>Inside a property row the field is forced to secondary. You dont pick.</li>
|
||||
<li>A read-only value is a read-only, disabled field. Never bare text.</li>
|
||||
</ul>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Dont</div>
|
||||
</div>
|
||||
<ul class="ds-rules">
|
||||
<li>Hand-roll a red line under a field. Use the row's error slot.</li>
|
||||
<li>Put a secondary field in a settings list, or an outline one in a properties section.</li>
|
||||
<li>Colour or size the icon. The field does it.</li>
|
||||
</ul>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,140 @@
|
||||
<!-- @dsCard group="Foundations" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Colour</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
body { padding: var(--container-padding); }
|
||||
.group { margin-top: var(--gap-lg); }
|
||||
.group > .caption { margin-bottom: var(--gap-sm); padding: 0 var(--gap-xxs); }
|
||||
.swatches { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap-md); }
|
||||
.sw { display: flex; gap: var(--gap-sm); align-items: flex-start; min-width: 0; }
|
||||
.sw .chip { flex: none; width: 32px; height: 32px; border-radius: var(--radius-sm); border: var(--control-border-width) solid var(--divider); }
|
||||
.sw .meta { min-width: 0; display: flex; flex-direction: column; gap: var(--gap-xxs); }
|
||||
.sw .for { font-size: var(--text-xxs); }
|
||||
.schemes { margin-top: var(--gap-xl); display: flex; flex-direction: column; gap: var(--gap-sm); }
|
||||
.strip { display: flex; align-items: center; gap: var(--gap-md); padding: var(--gap-sm) var(--gap-md); border-radius: var(--radius-md); border: var(--control-border-width) solid var(--panel-border); }
|
||||
.strip .name { width: calc(var(--control-height) * 1.6); flex: none; }
|
||||
.strip .chips { display: flex; flex-wrap: wrap; gap: var(--gap-sm); }
|
||||
.strip .set { display: flex; gap: var(--gap-xxs); }
|
||||
.strip .mini { width: 12px; height: 12px; border-radius: var(--radius-xs); border: var(--control-border-width) solid var(--divider); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-heading">
|
||||
<div class="title">Colour</div>
|
||||
<div class="subtitle">37 slots per scheme. Carbon is the default. Pick the slot by what the thing is.</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="caption">Ground & surfaces</div>
|
||||
<div class="swatches">
|
||||
<div class="sw"><div class="chip" style="background: var(--background)"></div><div class="meta"><span class="mono">--background</span><span class="mono muted">#0d0d0d</span><span class="for muted">Page ground, and a panel's fill</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--chrome)"></div><div class="meta"><span class="mono">--chrome</span><span class="mono muted">#010101</span><span class="for muted">Header and footer bars, the gutter between panels</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--surface-sunken)"></div><div class="meta"><span class="mono">--surface-sunken</span><span class="mono muted">#0a0a0a</span><span class="for muted">One step below the ground: rails, panel bodies</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--card)"></div><div class="meta"><span class="mono">--card</span><span class="mono muted">#1b1b1b</span><span class="for muted">Raised surface: cards, properties sections, dialogs</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--muted)"></div><div class="meta"><span class="mono">--muted</span><span class="mono muted">#0f0f0f</span><span class="for muted">De-emphasised fill</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="caption">Text</div>
|
||||
<div class="swatches">
|
||||
<div class="sw"><div class="chip" style="background: var(--foreground)"></div><div class="meta"><span class="mono">--foreground</span><span class="mono muted">#f2f2f2</span><span class="for muted">Default text</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--muted-foreground)"></div><div class="meta"><span class="mono">--muted-foreground</span><span class="mono muted">#8f8f8f</span><span class="for muted">Secondary text, hints, units</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--row-text)"></div><div class="meta"><span class="mono">--row-text</span><span class="mono muted">#c2c2c2</span><span class="for muted">A row's resting label</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--row-hovered)"></div><div class="meta"><span class="mono">--row-hovered</span><span class="mono muted">#222222</span><span class="for muted">A hovered row in lists and menus</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="caption">Controls</div>
|
||||
<div class="swatches">
|
||||
<div class="sw"><div class="chip" style="background: var(--control-fill)"></div><div class="meta"><span class="mono">--control-fill</span><span class="mono muted">#080808</span><span class="for muted">Shared fill of every control</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--control-fill-hovered)"></div><div class="meta"><span class="mono">--control-fill-hovered</span><span class="mono muted">#070707</span><span class="for muted">Control fill, hovered</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--control-fill-focused)"></div><div class="meta"><span class="mono">--control-fill-focused</span><span class="mono muted">#000000</span><span class="for muted">Control fill, focused</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--control-border)"></div><div class="meta"><span class="mono">--control-border</span><span class="mono muted">#2a2a2a</span><span class="for muted">Shared stroke of every control</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--secondary)"></div><div class="meta"><span class="mono">--secondary</span><span class="mono muted">#303030</span><span class="for muted">A neutral filled control</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--secondary-hovered)"></div><div class="meta"><span class="mono">--secondary-hovered</span><span class="mono muted">#424242</span><span class="for muted">Secondary, hovered</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--secondary-foreground)"></div><div class="meta"><span class="mono">--secondary-foreground</span><span class="mono muted">#f2f2f2</span><span class="for muted">Text on secondary</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--switch-track-inactive)"></div><div class="meta"><span class="mono">--switch-track-inactive</span><span class="mono muted">#0f0f0f</span><span class="for muted">A switch while off</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="caption">Floating</div>
|
||||
<div class="swatches">
|
||||
<div class="sw"><div class="chip" style="background: var(--popover)"></div><div class="meta"><span class="mono">--popover</span><span class="mono muted">#000000</span><span class="for muted">Menus, select popups, anything floating</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--popover-border)"></div><div class="meta"><span class="mono">--popover-border</span><span class="mono muted">#1c1c1c</span><span class="for muted">A popover's edge</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--popover-item-hovered)"></div><div class="meta"><span class="mono">--popover-item-hovered</span><span class="mono muted">#1f1f1f</span><span class="for muted">Hover inside select and date popups</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--tooltip-background)"></div><div class="meta"><span class="mono">--tooltip-background</span><span class="mono muted">#080808</span><span class="for muted">Tooltips, above everything</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--tooltip-border)"></div><div class="meta"><span class="mono">--tooltip-border</span><span class="mono muted">#1f1f1f</span><span class="for muted">A tooltip's edge</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="caption">Accent & state</div>
|
||||
<div class="swatches">
|
||||
<div class="sw"><div class="chip" style="background: var(--primary)"></div><div class="meta"><span class="mono">--primary</span><span class="mono muted">#e8e8e8</span><span class="for muted">The accent. The one thing that's on or chosen</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--primary-hovered)"></div><div class="meta"><span class="mono">--primary-hovered</span><span class="mono muted">#fefefe</span><span class="for muted">Primary, hovered</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--primary-foreground)"></div><div class="meta"><span class="mono">--primary-foreground</span><span class="mono muted">#000000</span><span class="for muted">Text on primary</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--destructive)"></div><div class="meta"><span class="mono">--destructive</span><span class="mono muted">#c43333</span><span class="for muted">Delete, revoke, discard</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--ring)"></div><div class="meta"><span class="mono">--ring</span><span class="mono muted">#ffffff</span><span class="for muted">Keyboard focus</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="caption">Lines & panels</div>
|
||||
<div class="swatches">
|
||||
<div class="sw"><div class="chip" style="background: var(--border)"></div><div class="meta"><span class="mono">--border</span><span class="mono muted">#010101</span><span class="for muted">General outline</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--divider)"></div><div class="meta"><span class="mono">--divider</span><span class="mono muted">#202020</span><span class="for muted">A seam between things</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--panel-border)"></div><div class="meta"><span class="mono">--panel-border</span><span class="mono muted">#222222</span><span class="for muted">A panel's edge, resting</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--panel-border-highlighted)"></div><div class="meta"><span class="mono">--panel-border-highlighted</span><span class="mono muted">#3a3a3a</span><span class="for muted">The lit panel's edge</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--properties-section-border)"></div><div class="meta"><span class="mono">--properties-section-border</span><span class="mono muted">#272727</span><span class="for muted">A properties section's edge</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="caption">Charts</div>
|
||||
<div class="swatches">
|
||||
<div class="sw"><div class="chip" style="background: var(--chart-1)"></div><div class="meta"><span class="mono">--chart-1</span><span class="mono muted">#f45b69</span><span class="for muted">Data visualisation</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--chart-2)"></div><div class="meta"><span class="mono">--chart-2</span><span class="mono muted">#86d957</span><span class="for muted">Data visualisation</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--chart-3)"></div><div class="meta"><span class="mono">--chart-3</span><span class="mono muted">#62a7ff</span><span class="for muted">Data visualisation</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--chart-4)"></div><div class="meta"><span class="mono">--chart-4</span><span class="mono muted">#ffc857</span><span class="for muted">Data visualisation</span></div></div>
|
||||
<div class="sw"><div class="chip" style="background: var(--chart-5)"></div><div class="meta"><span class="mono">--chart-5</span><span class="mono muted">#ff8c42</span><span class="for muted">Data visualisation</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schemes">
|
||||
<div class="caption">Other schemes, same slots in the same order</div>
|
||||
<div class="gu strip" data-scheme="light">
|
||||
<span class="name medium">Light</span>
|
||||
<div class="chips">
|
||||
<div class="set"><div class="mini" style="background: var(--background)" title="--background"></div><div class="mini" style="background: var(--chrome)" title="--chrome"></div><div class="mini" style="background: var(--surface-sunken)" title="--surface-sunken"></div><div class="mini" style="background: var(--card)" title="--card"></div><div class="mini" style="background: var(--muted)" title="--muted"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--foreground)" title="--foreground"></div><div class="mini" style="background: var(--muted-foreground)" title="--muted-foreground"></div><div class="mini" style="background: var(--row-text)" title="--row-text"></div><div class="mini" style="background: var(--row-hovered)" title="--row-hovered"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--control-fill)" title="--control-fill"></div><div class="mini" style="background: var(--control-fill-hovered)" title="--control-fill-hovered"></div><div class="mini" style="background: var(--control-fill-focused)" title="--control-fill-focused"></div><div class="mini" style="background: var(--control-border)" title="--control-border"></div><div class="mini" style="background: var(--secondary)" title="--secondary"></div><div class="mini" style="background: var(--secondary-hovered)" title="--secondary-hovered"></div><div class="mini" style="background: var(--secondary-foreground)" title="--secondary-foreground"></div><div class="mini" style="background: var(--switch-track-inactive)" title="--switch-track-inactive"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--popover)" title="--popover"></div><div class="mini" style="background: var(--popover-border)" title="--popover-border"></div><div class="mini" style="background: var(--popover-item-hovered)" title="--popover-item-hovered"></div><div class="mini" style="background: var(--tooltip-background)" title="--tooltip-background"></div><div class="mini" style="background: var(--tooltip-border)" title="--tooltip-border"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--primary)" title="--primary"></div><div class="mini" style="background: var(--primary-hovered)" title="--primary-hovered"></div><div class="mini" style="background: var(--primary-foreground)" title="--primary-foreground"></div><div class="mini" style="background: var(--destructive)" title="--destructive"></div><div class="mini" style="background: var(--ring)" title="--ring"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--border)" title="--border"></div><div class="mini" style="background: var(--divider)" title="--divider"></div><div class="mini" style="background: var(--panel-border)" title="--panel-border"></div><div class="mini" style="background: var(--panel-border-highlighted)" title="--panel-border-highlighted"></div><div class="mini" style="background: var(--properties-section-border)" title="--properties-section-border"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--chart-1)" title="--chart-1"></div><div class="mini" style="background: var(--chart-2)" title="--chart-2"></div><div class="mini" style="background: var(--chart-3)" title="--chart-3"></div><div class="mini" style="background: var(--chart-4)" title="--chart-4"></div><div class="mini" style="background: var(--chart-5)" title="--chart-5"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gu strip" data-scheme="dark">
|
||||
<span class="name medium">Dark</span>
|
||||
<div class="chips">
|
||||
<div class="set"><div class="mini" style="background: var(--background)" title="--background"></div><div class="mini" style="background: var(--chrome)" title="--chrome"></div><div class="mini" style="background: var(--surface-sunken)" title="--surface-sunken"></div><div class="mini" style="background: var(--card)" title="--card"></div><div class="mini" style="background: var(--muted)" title="--muted"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--foreground)" title="--foreground"></div><div class="mini" style="background: var(--muted-foreground)" title="--muted-foreground"></div><div class="mini" style="background: var(--row-text)" title="--row-text"></div><div class="mini" style="background: var(--row-hovered)" title="--row-hovered"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--control-fill)" title="--control-fill"></div><div class="mini" style="background: var(--control-fill-hovered)" title="--control-fill-hovered"></div><div class="mini" style="background: var(--control-fill-focused)" title="--control-fill-focused"></div><div class="mini" style="background: var(--control-border)" title="--control-border"></div><div class="mini" style="background: var(--secondary)" title="--secondary"></div><div class="mini" style="background: var(--secondary-hovered)" title="--secondary-hovered"></div><div class="mini" style="background: var(--secondary-foreground)" title="--secondary-foreground"></div><div class="mini" style="background: var(--switch-track-inactive)" title="--switch-track-inactive"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--popover)" title="--popover"></div><div class="mini" style="background: var(--popover-border)" title="--popover-border"></div><div class="mini" style="background: var(--popover-item-hovered)" title="--popover-item-hovered"></div><div class="mini" style="background: var(--tooltip-background)" title="--tooltip-background"></div><div class="mini" style="background: var(--tooltip-border)" title="--tooltip-border"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--primary)" title="--primary"></div><div class="mini" style="background: var(--primary-hovered)" title="--primary-hovered"></div><div class="mini" style="background: var(--primary-foreground)" title="--primary-foreground"></div><div class="mini" style="background: var(--destructive)" title="--destructive"></div><div class="mini" style="background: var(--ring)" title="--ring"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--border)" title="--border"></div><div class="mini" style="background: var(--divider)" title="--divider"></div><div class="mini" style="background: var(--panel-border)" title="--panel-border"></div><div class="mini" style="background: var(--panel-border-highlighted)" title="--panel-border-highlighted"></div><div class="mini" style="background: var(--properties-section-border)" title="--properties-section-border"></div></div>
|
||||
<div class="set"><div class="mini" style="background: var(--chart-1)" title="--chart-1"></div><div class="mini" style="background: var(--chart-2)" title="--chart-2"></div><div class="mini" style="background: var(--chart-3)" title="--chart-3"></div><div class="mini" style="background: var(--chart-4)" title="--chart-4"></div><div class="mini" style="background: var(--chart-5)" title="--chart-5"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,89 @@
|
||||
<!-- @dsCard group="Foundations" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Density</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
body { padding: var(--container-padding); }
|
||||
.block { margin-top: var(--gap-xl); }
|
||||
.block > .caption { margin-bottom: var(--gap-sm); padding: 0 var(--gap-xxs); }
|
||||
|
||||
.tiers { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap-md); align-items: start; }
|
||||
.tier { border: var(--control-border-width) solid var(--panel-border); border-radius: var(--radius-lg); padding: var(--gap-md) 0 0; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-sm); }
|
||||
.tier > .caption, .tier > .ctl { padding: 0 var(--gap-md); }
|
||||
.tier > .ctl { display: flex; }
|
||||
.tier > .ctl > .field, .tier > .ctl > .select { flex: 1; }
|
||||
.tier .properties-section { margin-top: var(--gap-xxs); }
|
||||
|
||||
.rows { display: flex; flex-direction: column; }
|
||||
.rows .r { display: grid; grid-template-columns: 1.1fr 0.9fr 0.9fr 0.9fr; gap: var(--gap-md); align-items: baseline; padding: var(--gap-sm) var(--gap-xs); }
|
||||
.rows .r + .r { border-top: var(--control-border-width) solid var(--divider); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-heading">
|
||||
<div class="title">Density</div>
|
||||
<div class="subtitle">Three tiers. Pick one for the app, don't tune it.</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Same controls, three tiers</div>
|
||||
<div class="tiers">
|
||||
|
||||
<div class="gu tier" data-density="compact">
|
||||
<div class="caption">Compact</div>
|
||||
<div class="ctl"><button class="btn primary">Save</button></div>
|
||||
<div class="ctl"><label class="field"><input placeholder="Name"></label></div>
|
||||
<div class="ctl"><div class="select"><span>Monthly</span><svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
<div class="properties-section">
|
||||
<div class="rows">
|
||||
<div class="property-row"><span class="label">Width</span><div class="value"><label class="field secondary center"><input value="120"><span class="unit">px</span></label></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gu tier" data-density="normal">
|
||||
<div class="caption">Normal</div>
|
||||
<div class="ctl"><button class="btn primary">Save</button></div>
|
||||
<div class="ctl"><label class="field"><input placeholder="Name"></label></div>
|
||||
<div class="ctl"><div class="select"><span>Monthly</span><svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
<div class="properties-section">
|
||||
<div class="rows">
|
||||
<div class="property-row"><span class="label">Width</span><div class="value"><label class="field secondary center"><input value="120"><span class="unit">px</span></label></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gu tier" data-density="product">
|
||||
<div class="caption">Product</div>
|
||||
<div class="ctl"><button class="btn primary">Save</button></div>
|
||||
<div class="ctl"><label class="field"><input placeholder="Name"></label></div>
|
||||
<div class="ctl"><div class="select"><span>Monthly</span><svg class="icon chevron" viewBox="0 0 24 24"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
<div class="properties-section">
|
||||
<div class="rows">
|
||||
<div class="property-row"><span class="label">Width</span><div class="value"><label class="field secondary center"><input value="120"><span class="unit">px</span></label></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">The numbers</div>
|
||||
<div class="rows">
|
||||
<div class="r"><span></span><span class="medium">Compact</span><span class="medium">Normal</span><span class="medium">Product</span></div>
|
||||
<div class="r"><span class="mono muted">font-size</span><span class="mono">10</span><span class="mono">10</span><span class="mono">12</span></div>
|
||||
<div class="r"><span class="mono muted">control-height</span><span class="mono">23</span><span class="mono">27</span><span class="mono">33</span></div>
|
||||
<div class="r"><span class="mono muted">chrome-bar-height</span><span class="mono">31</span><span class="mono">37</span><span class="mono">43</span></div>
|
||||
<div class="r"><span class="muted">For</span><span>Dense desktop tools</span><span>The same, roomier</span><span>Web and product apps</span></div>
|
||||
<div class="r"><span class="muted">Used by</span><span>Arcs & Angles</span><span>A&A "Comfortable"</span><span>The hub</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- @dsCard group="Foundations" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Shape</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
body { padding: var(--container-padding); }
|
||||
.block { margin-top: var(--gap-xl); }
|
||||
.block > .caption { margin-bottom: var(--gap-sm); padding: 0 var(--gap-xxs); }
|
||||
|
||||
.radii { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--gap-md); }
|
||||
.radius { display: flex; flex-direction: column; gap: var(--gap-xs); }
|
||||
.radius .box { height: calc(var(--control-height) * 1.6); background: var(--card); border: var(--control-border-width) solid var(--control-border); }
|
||||
|
||||
.facts { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gap-md); }
|
||||
.facts .card { display: flex; flex-direction: column; gap: var(--gap-xxs); padding: var(--gap-md) var(--gap-lg); }
|
||||
|
||||
.demo { border-radius: var(--radius-lg); overflow: hidden; border: var(--control-border-width) solid var(--panel-border); }
|
||||
.demo .shell { height: calc(var(--control-height) * 5); }
|
||||
.demo .rail { width: 30%; }
|
||||
.demo .main { flex: 1; }
|
||||
.demo .panel { display: flex; flex-direction: column; }
|
||||
.demo .panel .body { flex: 1; padding: 0 var(--gap-md) var(--gap-md); }
|
||||
.demo .foot { padding: var(--gap-sm) var(--gap-lg); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-heading">
|
||||
<div class="title">Shape</div>
|
||||
<div class="subtitle">Small radii, flat surfaces. Borders and surface steps do the separating.</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Radii</div>
|
||||
<div class="radii">
|
||||
<div class="radius"><div class="box" style="border-radius: var(--radius-xs)"></div><span class="mono">radius-xs</span><span class="mono muted">2</span></div>
|
||||
<div class="radius"><div class="box" style="border-radius: var(--radius-sm)"></div><span class="mono">radius-sm</span><span class="mono muted">4</span></div>
|
||||
<div class="radius"><div class="box" style="border-radius: var(--radius-md)"></div><span class="mono">radius-md</span><span class="mono muted">6 · controls</span></div>
|
||||
<div class="radius"><div class="box" style="border-radius: var(--radius-lg)"></div><span class="mono">radius-lg</span><span class="mono muted">8</span></div>
|
||||
<div class="radius"><div class="box" style="border-radius: var(--radius-xl)"></div><span class="mono">radius-xl</span><span class="mono muted">10 · cards</span></div>
|
||||
<div class="radius"><div class="box" style="border-radius: var(--radius-xxl)"></div><span class="mono">radius-xxl</span><span class="mono muted">12 · dialogs</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Panels</div>
|
||||
<div class="facts">
|
||||
<div class="card"><span class="mono">panel-radius</span><span class="mono muted">10</span></div>
|
||||
<div class="card"><span class="mono">panel-gap</span><span class="mono muted">5</span></div>
|
||||
<div class="card"><span class="mono">panel-border-width</span><span class="mono muted">1.15</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Flat, no shadows</div>
|
||||
<div class="demo">
|
||||
<div class="chrome-bar"><span class="medium">Chrome</span></div>
|
||||
<div class="shell">
|
||||
<div class="panel sunken rail">
|
||||
<div class="panel-header"><span class="title">Rail</span></div>
|
||||
<div class="body muted">Sunken panel</div>
|
||||
</div>
|
||||
<div class="panel active main">
|
||||
<div class="panel-header"><span class="title">Workspace</span></div>
|
||||
<div class="body muted">The lit panel. Only one at a time.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot muted">Chrome ground, panels standing on it, panel-gap gutters between.</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!-- @dsCard group="Foundations" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Spacing</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
body { padding: var(--container-padding); }
|
||||
.block { margin-top: var(--gap-xl); }
|
||||
.block > .caption { margin-bottom: var(--gap-sm); padding: 0 var(--gap-xxs); }
|
||||
|
||||
.tiers { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-md); }
|
||||
.tier { border: var(--control-border-width) solid var(--panel-border); border-radius: var(--radius-lg); padding: var(--gap-md) var(--gap-lg); }
|
||||
.tier > .caption { margin-bottom: var(--gap-sm); }
|
||||
|
||||
.bar-row { display: flex; align-items: center; gap: var(--gap-md); min-height: var(--line-box); padding: var(--gap-xxs) 0; }
|
||||
.bar-row .tok { flex: 0 0 48%; white-space: nowrap; }
|
||||
.bar-row .bar { height: var(--gap-sm); background: var(--primary); border-radius: var(--radius-xs); flex: none; }
|
||||
.bar-row .px { font-size: var(--text-xxs); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-heading">
|
||||
<div class="title">Spacing</div>
|
||||
<div class="subtitle">Gaps come from the density tier. Never a literal.</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Gap ladder</div>
|
||||
<div class="tiers">
|
||||
<div class="gu tier" data-density="product">
|
||||
<div class="caption">Product (and normal)</div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-xxs</span><div class="bar" style="width: var(--gap-xxs)"></div><span class="px mono muted">3</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-xs</span><div class="bar" style="width: var(--gap-xs)"></div><span class="px mono muted">5</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-sm</span><div class="bar" style="width: var(--gap-sm)"></div><span class="px mono muted">8</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-md</span><div class="bar" style="width: var(--gap-md)"></div><span class="px mono muted">10</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-lg</span><div class="bar" style="width: var(--gap-lg)"></div><span class="px mono muted">15</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-xl</span><div class="bar" style="width: var(--gap-xl)"></div><span class="px mono muted">20</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-xxl</span><div class="bar" style="width: var(--gap-xxl)"></div><span class="px mono muted">30</span></div>
|
||||
</div>
|
||||
<div class="gu tier" data-density="compact">
|
||||
<div class="caption">Compact</div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-xxs</span><div class="bar" style="width: var(--gap-xxs)"></div><span class="px mono muted">2</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-xs</span><div class="bar" style="width: var(--gap-xs)"></div><span class="px mono muted">4</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-sm</span><div class="bar" style="width: var(--gap-sm)"></div><span class="px mono muted">6</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-md</span><div class="bar" style="width: var(--gap-md)"></div><span class="px mono muted">8</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-lg</span><div class="bar" style="width: var(--gap-lg)"></div><span class="px mono muted">12</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-xl</span><div class="bar" style="width: var(--gap-xl)"></div><span class="px mono muted">16</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">gap-xxl</span><div class="bar" style="width: var(--gap-xxl)"></div><span class="px mono muted">24</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Heights and containers</div>
|
||||
<div class="tiers">
|
||||
<div class="gu tier" data-density="product">
|
||||
<div class="caption">Product</div>
|
||||
<div class="bar-row"><span class="tok mono muted">control-height</span><div class="bar" style="width: var(--control-height)"></div><span class="px mono muted">33</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">property-row-height</span><div class="bar" style="width: var(--property-row-height)"></div><span class="px mono muted">36</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">chrome-bar-height</span><div class="bar" style="width: var(--chrome-bar-height)"></div><span class="px mono muted">43</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">container-gap</span><div class="bar" style="width: var(--container-gap)"></div><span class="px mono muted">10</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">container-padding</span><div class="bar" style="width: var(--container-padding)"></div><span class="px mono muted">20</span></div>
|
||||
</div>
|
||||
<div class="gu tier" data-density="compact">
|
||||
<div class="caption">Compact</div>
|
||||
<div class="bar-row"><span class="tok mono muted">control-height</span><div class="bar" style="width: var(--control-height)"></div><span class="px mono muted">23</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">property-row-height</span><div class="bar" style="width: var(--property-row-height)"></div><span class="px mono muted">28</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">chrome-bar-height</span><div class="bar" style="width: var(--chrome-bar-height)"></div><span class="px mono muted">31</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">container-gap</span><div class="bar" style="width: var(--container-gap)"></div><span class="px mono muted">8</span></div>
|
||||
<div class="bar-row"><span class="tok mono muted">container-padding</span><div class="bar" style="width: var(--container-padding)"></div><span class="px mono muted">16</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!-- @dsCard group="Foundations" -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Type</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
body { padding: var(--container-padding); }
|
||||
.block { margin-top: var(--gap-xl); }
|
||||
.block > .caption { margin-bottom: var(--gap-sm); padding: 0 var(--gap-xxs); }
|
||||
|
||||
.faces { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-md); }
|
||||
.faces .card { display: flex; flex-direction: column; gap: var(--gap-xs); }
|
||||
|
||||
.tiers { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-md); }
|
||||
.tier { border: var(--control-border-width) solid var(--panel-border); border-radius: var(--radius-lg); padding: var(--gap-md) var(--gap-lg); }
|
||||
.tier > .caption { margin-bottom: var(--gap-sm); }
|
||||
.step { display: flex; align-items: baseline; gap: var(--gap-md); padding: var(--gap-xs) 0; }
|
||||
.step + .step { border-top: var(--control-border-width) solid var(--divider); }
|
||||
.step .tok { flex: none; width: calc(var(--control-height) * 3.6); white-space: nowrap; font-size: var(--font-size); }
|
||||
|
||||
.weights { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap-md); }
|
||||
.weights .card { display: flex; flex-direction: column; gap: var(--gap-xs); }
|
||||
|
||||
.rows { display: flex; flex-direction: column; }
|
||||
.rows .r { display: grid; grid-template-columns: 1fr 1.3fr 1fr; gap: var(--gap-md); align-items: baseline; padding: var(--gap-sm) var(--gap-xs); }
|
||||
.rows .r + .r { border-top: var(--control-border-width) solid var(--divider); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-heading">
|
||||
<div class="title">Type</div>
|
||||
<div class="subtitle">Geist for everything, Geist Mono for numbers and readouts.</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Families</div>
|
||||
<div class="faces">
|
||||
<div class="card">
|
||||
<span class="text-lg">Geist</span>
|
||||
<span class="muted">--font-sans. Body, controls, headings.</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="text-lg mono">Geist Mono 0123</span>
|
||||
<span class="muted">--font-mono. Readouts, ids, stats.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Size ladder</div>
|
||||
<div class="tiers">
|
||||
<div class="gu tier" data-density="product">
|
||||
<div class="caption">Product</div>
|
||||
<div class="step"><span class="tok mono muted">text-lg 22</span><span class="text-lg">Page title</span></div>
|
||||
<div class="step"><span class="tok mono muted">text-sm 17</span><span class="text-sm">Section heading</span></div>
|
||||
<div class="step"><span class="tok mono muted">text-xs 13</span><span class="text-xs">List text</span></div>
|
||||
<div class="step"><span class="tok mono muted">font-size 12</span><span>Body and controls</span></div>
|
||||
<div class="step"><span class="tok mono muted">text-xxs 11</span><span class="text-xxs">Captions</span></div>
|
||||
</div>
|
||||
<div class="gu tier" data-density="compact">
|
||||
<div class="caption">Compact</div>
|
||||
<div class="step"><span class="tok mono muted">text-lg 18</span><span class="text-lg">Page title</span></div>
|
||||
<div class="step"><span class="tok mono muted">text-sm 14</span><span class="text-sm">Section heading</span></div>
|
||||
<div class="step"><span class="tok mono muted">text-xs 11</span><span class="text-xs">List text</span></div>
|
||||
<div class="step"><span class="tok mono muted">font-size 10</span><span>Body and controls</span></div>
|
||||
<div class="step"><span class="tok mono muted">text-xxs 9</span><span class="text-xxs">Captions</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Weights</div>
|
||||
<div class="weights">
|
||||
<div class="card"><span class="text-sm">Aa</span><span class="muted">Body 300</span></div>
|
||||
<div class="card"><span class="text-sm" style="font-weight: var(--weight-control)">Aa</span><span class="muted">Control 400</span></div>
|
||||
<div class="card"><span class="text-sm medium">Aa</span><span class="muted">Medium 500</span></div>
|
||||
<div class="card"><span class="text-sm semibold">Aa</span><span class="muted">Semibold 600</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<div class="caption">Job to style</div>
|
||||
<div class="rows">
|
||||
<div class="r"><span class="muted">Page title</span><span class="text-lg">Products</span><span class="mono muted">text-lg</span></div>
|
||||
<div class="r"><span class="muted">Subtitle</span><span class="muted">What this page is for</span><span class="mono muted">muted</span></div>
|
||||
<div class="r"><span class="muted">Section heading</span><span class="text-sm semibold">Billing</span><span class="mono muted">text-sm, 600</span></div>
|
||||
<div class="r"><span class="muted">Nav group caption</span><span class="caption">Account</span><span class="mono muted">text-xxs, 600, muted</span></div>
|
||||
<div class="r"><span class="muted">Hint, unit, empty line</span><span class="muted">Nothing here yet</span><span class="mono muted">muted</span></div>
|
||||
<div class="r"><span class="muted">Mono readout</span><span class="mono muted">60 fps · 12.4 ms</span><span class="mono muted">mono, muted</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,173 @@
|
||||
<!-- @dsCard group="Patterns" -->
|
||||
<!doctype html>
|
||||
<html lang="en" data-scheme="carbon" data-density="compact">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Editor shell</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
/* scaffolding - GarageShell: header, a chrome workspace with the canvas Panel and a
|
||||
sidebar split top/bottom, footer. panelGap gutters, no gutter above or below */
|
||||
html, body { background: var(--chrome); }
|
||||
.screen { width: 1200px; height: 760px; display: flex; flex-direction: column; background: var(--chrome); }
|
||||
.workspace { flex: 1; min-height: 0; display: flex; gap: var(--panel-gap); padding: 0 var(--panel-gap); }
|
||||
.canvas { flex: 1; min-width: 0; }
|
||||
.sidebar { width: 320px; flex: none; display: flex; flex-direction: column; gap: var(--panel-gap); }
|
||||
.sidebar > .panel { flex: 1; min-height: 0; display: flex; }
|
||||
|
||||
/* header: menu words left, perf + file name right, toggles on the true centre */
|
||||
.header { position: relative; }
|
||||
.header .menus { display: flex; }
|
||||
.header .spacer { flex: 1; }
|
||||
.header .file { width: 150px; text-align: center; }
|
||||
.header .centre { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: flex; gap: var(--gap-sm); }
|
||||
|
||||
/* a panel page: sunken body, PanelHeader, then the list */
|
||||
.page { flex: 1; min-width: 0; background: var(--surface-sunken); display: flex; flex-direction: column; }
|
||||
.page .body { flex: 1; min-height: 0; overflow: hidden; padding-top: var(--gap-md); }
|
||||
|
||||
/* properties tab strip - hand-rolled in the app too (see "Dont copy these") */
|
||||
.tabs { width: 40px; flex: none; background: var(--chrome); border-right: 1px solid var(--divider);
|
||||
padding: var(--gap-xs) 0 var(--gap-xs) var(--gap-xs); display: flex; flex-direction: column; gap: var(--gap-xs); }
|
||||
.tabs .tab { display: flex; justify-content: center; padding-right: var(--gap-xs); border: 1px solid transparent; }
|
||||
.tabs .tab.on { background: var(--surface-sunken); border-color: var(--divider); border-right-color: transparent;
|
||||
border-radius: var(--radius-sm) 0 0 var(--radius-sm); margin-right: -1px; }
|
||||
.tabs .split { height: var(--gap-xl); }
|
||||
|
||||
/* footer: three blocks, muted mono */
|
||||
.footer > div { flex: 1; display: flex; align-items: center; gap: var(--gap-md); }
|
||||
.footer .mid { justify-content: center; }
|
||||
.footer .end { justify-content: flex-end; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen">
|
||||
|
||||
<!-- header ChromeBar -->
|
||||
<header class="chrome-bar header">
|
||||
<div class="menus">
|
||||
<button class="btn ghost">File</button>
|
||||
<button class="btn ghost">Edit</button>
|
||||
<button class="btn ghost">Tools</button>
|
||||
<button class="btn ghost">Insert</button>
|
||||
<button class="btn ghost">View</button>
|
||||
<button class="btn ghost">Info</button>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<span class="mono muted">60 fps</span>
|
||||
<span class="vr"></span>
|
||||
<span class="file muted">Northern loop.ana</span>
|
||||
|
||||
<div class="centre">
|
||||
<div class="btn-group">
|
||||
<button class="btn primary icon-only" aria-label="Snapping, on"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m12 15 4 4" /> <path d="M2.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.029-6.029a1 1 0 1 1 3 3l-6.029 6.029a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.365-6.367A1 1 0 0 0 8.716 4.282z" /> <path d="m5 8 4 4" /></svg></button>
|
||||
<button class="btn secondary icon-only" aria-label="Snap options"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg></button>
|
||||
</div>
|
||||
<button class="btn outline icon-only" aria-label="Continuous arcs, off"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="19" cy="5" r="2" /> <circle cx="5" cy="19" r="2" /> <path d="M5 17A12 12 0 0 1 17 5" /></svg></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workspace">
|
||||
<!-- the canvas: an empty Panel, the app paints the map in it -->
|
||||
<main class="panel canvas"></main>
|
||||
|
||||
<aside class="sidebar">
|
||||
<!-- top: explorer -->
|
||||
<section class="panel">
|
||||
<div class="page">
|
||||
<div class="panel-header"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z" /> <path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12" /> <path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17" /></svg><span class="title">Explorer</span></div>
|
||||
<div class="body">
|
||||
<div class="explorer-row"><span class="slot"><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" /></svg></span><span class="name">Northern loop</span></div>
|
||||
<div class="explorer-row depth-1 is-hovered"><span class="slot"></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="19" r="3" /> <path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15" /> <circle cx="18" cy="5" r="3" /></svg></span><span class="name">Loop line</span></div>
|
||||
<div class="explorer-row depth-1"><span class="slot"></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="19" r="3" /> <path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15" /> <circle cx="18" cy="5" r="3" /></svg></span><span class="name">Harbour branch</span></div>
|
||||
<div class="explorer-row depth-1"><span class="slot"></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" /> <circle cx="12" cy="10" r="3" /></svg></span><span class="name">Market Street</span></div>
|
||||
<div class="explorer-row depth-1"><span class="slot"></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" /> <circle cx="12" cy="10" r="3" /></svg></span><span class="name">Quayside</span></div>
|
||||
<div class="explorer-row depth-1"><span class="slot"></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" /> <circle cx="12" cy="10" r="3" /></svg></span><span class="name">Mill Lane</span></div>
|
||||
<div class="explorer-row depth-1"><span class="slot"></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" /> <circle cx="12" cy="10" r="3" /></svg></span><span class="name">Northgate</span></div>
|
||||
<div class="explorer-row"><span class="slot"><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" /></svg></span><span class="name">Labels</span></div>
|
||||
<div class="explorer-row"><span class="slot"><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg></span><span class="slot"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 12.5 8 15l2 2.5" /> <path d="m14 12.5 2 2.5-2 2.5" /></svg></span><span class="name">Scripts</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- bottom: properties tab strip + the selected panel page -->
|
||||
<section class="panel active">
|
||||
<div class="tabs">
|
||||
<div class="tab on"><button class="btn ghost icon-only" aria-label="Canvas"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 15h4" /> <path d="m14.817 10.995-.971-1.45 1.034-1.232a2 2 0 0 0-2.025-3.238l-1.82.364L9.91 3.885a2 2 0 0 0-3.625.748L6.141 6.55l-1.725.426a2 2 0 0 0-.19 3.756l.657.27" /> <path d="m18.822 10.995 2.26-5.38a1 1 0 0 0-.557-1.318L16.954 2.9a1 1 0 0 0-1.281.533l-.924 2.122" /> <path d="M4 12.006A1 1 0 0 1 4.994 11H19a1 1 0 0 1 1 1v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" /></svg></button></div>
|
||||
<div class="tab"><button class="btn ghost icon-only" aria-label="Lines"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2" /> <path d="M9 8h7" /> <path d="M8 12h6" /> <path d="M11 16h5" /></svg></button></div>
|
||||
<div class="split"></div>
|
||||
<div class="tab"><button class="btn ghost icon-only" aria-label="Object Properties"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 5H3" /> <path d="M12 19H3" /> <path d="M14 3v4" /> <path d="M16 17v4" /> <path d="M21 12h-9" /> <path d="M21 19h-5" /> <path d="M21 5h-7" /> <path d="M8 10v4" /> <path d="M8 12H3" /></svg></button></div>
|
||||
<div class="tab"><button class="btn ghost icon-only" aria-label="Script"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 12.5 8 15l2 2.5" /> <path d="m14 12.5 2 2.5-2 2.5" /></svg></button></div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<div class="panel-header"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 15h4" /> <path d="m14.817 10.995-.971-1.45 1.034-1.232a2 2 0 0 0-2.025-3.238l-1.82.364L9.91 3.885a2 2 0 0 0-3.625.748L6.141 6.55l-1.725.426a2 2 0 0 0-.19 3.756l.657.27" /> <path d="m18.822 10.995 2.26-5.38a1 1 0 0 0-.557-1.318L16.954 2.9a1 1 0 0 0-1.281.533l-.924 2.122" /> <path d="M4 12.006A1 1 0 0 1 4.994 11H19a1 1 0 0 1 1 1v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" /></svg><span class="title">Canvas</span></div>
|
||||
<div class="body">
|
||||
<section class="properties-section">
|
||||
<div class="header"><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>Grid</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Show grid</div>
|
||||
<div class="value"><span class="checkbox checked"></span></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Grid Z-level</div>
|
||||
<div class="value"><div class="field secondary center"><input value="0"></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Watermark</div>
|
||||
<div class="value"><span class="checkbox"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="properties-section">
|
||||
<div class="header"><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>Bounds</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Unbounded</div>
|
||||
<div class="value"><span class="checkbox"></span></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Size</div>
|
||||
<div class="value">
|
||||
<div class="btn-group vertical span">
|
||||
<div class="field secondary center"><span class="unit">W</span><input value="120"></div>
|
||||
<div class="field secondary center"><span class="unit">H</span><input value="80"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="properties-section">
|
||||
<div class="header"><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>Stations</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Label size</div>
|
||||
<div class="value"><div class="field secondary center"><input value="14"><span class="unit">px</span></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Size subtitle by scale</div>
|
||||
<div class="value"><span class="checkbox checked"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- footer ChromeBar: muted mono stats -->
|
||||
<footer class="chrome-bar footer">
|
||||
<div><span class="mono muted">© 2026 IMBENJI.NET LTD - ARCS & ANGLES</span></div>
|
||||
<div class="mid"><span class="mono muted">X: 0 · Y: 0</span><span class="vr"></span><span class="mono muted">Zoom: 100%</span></div>
|
||||
<div class="end">
|
||||
<span class="mono muted">Segments: 48</span><span class="vr"></span>
|
||||
<span class="mono muted">Nodes: 112</span><span class="vr"></span>
|
||||
<span class="mono muted">Stations: 36</span><span class="vr"></span>
|
||||
<span class="mono muted">v2.4.0</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,135 @@
|
||||
<!-- @dsCard group="Patterns" -->
|
||||
<!doctype html>
|
||||
<html lang="en" data-scheme="carbon" data-density="product">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hub form</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
/* scaffolding only - the mock screen and the hub's Measure (railWidth = fontSize*18,
|
||||
contentMax = fontSize*56, pageGutter = fontSize*3). every value is a token */
|
||||
html, body { background: var(--chrome); }
|
||||
.screen { width: 1100px; height: 720px; }
|
||||
.shell { height: 100%; align-items: stretch; }
|
||||
|
||||
.rail { width: calc(var(--font-size) * 18); flex: none; display: flex; flex-direction: column; }
|
||||
.rail .wordmark { height: calc(var(--font-size) * 9); display: flex; align-items: center; justify-content: center; }
|
||||
.rail .groups { flex: 1; min-height: 0; overflow: hidden; }
|
||||
.rail .foot { padding: var(--gap-md) var(--gap-md) var(--gap-lg); }
|
||||
|
||||
.workspace { flex: 1; min-width: 0; }
|
||||
/* contentMax plus a pageGutter either side, centred in the pane */
|
||||
.column { width: 100%; max-width: calc(var(--font-size) * (56 + 6)); margin: 0 auto; align-self: center;
|
||||
padding: var(--gap-xxl) calc(var(--font-size) * 3); }
|
||||
.workspace { display: flex; flex-direction: column; }
|
||||
.pane-back { padding: var(--gap-sm) var(--gap-sm) 0; }
|
||||
.column { padding-top: var(--gap-lg); }
|
||||
.column > .page-heading { margin-bottom: var(--gap-md); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen">
|
||||
<div class="shell">
|
||||
|
||||
<!-- nav rail: a sunken Panel -->
|
||||
<nav class="panel sunken rail">
|
||||
<div class="wordmark"><span class="text-lg semibold">Garage</span></div>
|
||||
<hr class="divider">
|
||||
<div class="groups">
|
||||
<div class="nav-group">
|
||||
<div class="caption">Settings</div>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /> <circle cx="12" cy="7" r="4" /></svg>Account</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /> <path d="m9 12 2 2 4-4" /></svg>Security</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 17V7" /> <path d="M16 8h-6a2 2 0 0 0 0 4h4a2 2 0 0 1 0 4H8" /> <path d="M4 3a1 1 0 0 1 1-1 1.3 1.3 0 0 1 .7.2l.933.6a1.3 1.3 0 0 0 1.4 0l.934-.6a1.3 1.3 0 0 1 1.4 0l.933.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6A1.3 1.3 0 0 1 19 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1 1.3 1.3 0 0 1-.7-.2l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.934.6a1.3 1.3 0 0 1-1.4 0l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-1.4 0l-.934-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-.7.2 1 1 0 0 1-1-1z" /></svg>Payments</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2" /> <rect x="14" y="2" width="8" height="8" rx="1" /></svg>Authorised apps</button>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="nav-group">
|
||||
<div class="caption">Developer</div>
|
||||
<button class="btn secondary stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z" /> <path d="M8 10v4" /> <path d="M12 10v2" /> <path d="M16 10v6" /></svg>Projects</button>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="nav-group">
|
||||
<div class="caption">Garage Platform</div>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" /> <path d="M20 2v4" /> <path d="M22 4h-4" /> <circle cx="4" cy="20" r="2" /></svg>Assistant</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 15H6a4 4 0 0 0-4 4v2" /> <path d="m14.305 16.53.923-.382" /> <path d="m15.228 13.852-.923-.383" /> <path d="m16.852 12.228-.383-.923" /> <path d="m16.852 17.772-.383.924" /> <path d="m19.148 12.228.383-.923" /> <path d="m19.53 18.696-.382-.924" /> <path d="m20.772 13.852.924-.383" /> <path d="m20.772 16.148.924.383" /> <circle cx="18" cy="15" r="3" /> <circle cx="9" cy="7" r="4" /></svg>Users</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" /> <circle cx="16.5" cy="7.5" r=".5" fill="currentColor" /></svg>Roles</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 12h-5" /> <path d="M15 8h-5" /> <path d="M19 17V5a2 2 0 0 0-2-2H4" /> <path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" /></svg>Audit log</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" /></svg>Status</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="foot">
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m16 17 5-5-5-5" /> <path d="M21 12H9" /> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></svg>Sign out</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- workspace: a routed detail, so the pane carries the way back and the page wears its own heading -->
|
||||
<main class="panel workspace">
|
||||
<div class="pane-back"><button class="btn ghost"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m12 19-7-7 7-7" /> <path d="M19 12H5" /></svg>Back to project</button></div>
|
||||
<div class="column">
|
||||
<div class="page-heading">
|
||||
<div class="title">Pro licence</div>
|
||||
<div class="subtitle">Something you sell, and what it goes for.</div>
|
||||
<div class="trailing">
|
||||
<button class="btn ghost-destructive"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg>Delete</button>
|
||||
<button class="btn ghost"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>Preview checkout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Details</div>
|
||||
<div class="subtitle">What it is and how its sold</div>
|
||||
</div>
|
||||
<div class="settings-list">
|
||||
<div class="settings-row">
|
||||
<div class="label">Name<span class="tag">Required</span><span class="subtitle">What a buyer sees</span></div>
|
||||
<div class="field-slot"><div class="field"><input value="Pro licence"></div></div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="label">SKU<span class="subtitle">How an app asks for it. Fixed once created</span></div>
|
||||
<div class="field-slot"><div class="field readonly mono"><input value="pro_licence" disabled></div></div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="label">Use case<span class="subtitle">What's being sold, which sets the tax</span></div>
|
||||
<div class="field-slot"><div class="select ghost"><span>Downloadable software</span><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="label">Checkout<span class="subtitle">Who takes the card details</span></div>
|
||||
<div class="field-slot"><div class="select ghost"><span>Stripe Checkout</span><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="label">Key lifetime<span class="subtitle">Seconds. Empty inherits the project's</span></div>
|
||||
<div class="field-slot"><div class="field error"><input value="a day"></div></div>
|
||||
<div class="error">Has to be a whole number of seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-heading">
|
||||
<div class="title">Pricing</div>
|
||||
<div class="subtitle">What it costs, and how often</div>
|
||||
<div class="action"><button class="btn ghost"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14" /> <path d="M12 5v14" /></svg>Add price</button></div>
|
||||
</div>
|
||||
<section class="properties-section">
|
||||
<div class="header">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<div><div>Default price</div><span class="subtitle">What it goes on sale at</span></div>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Price</div>
|
||||
<div class="value"><div class="field secondary"><input value="12.00"><span class="unit">GBP</span></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Billing</div>
|
||||
<div class="value"><div class="select secondary"><span>Monthly</span><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,151 @@
|
||||
<!-- @dsCard group="Patterns" -->
|
||||
<!doctype html>
|
||||
<html lang="en" data-scheme="carbon" data-density="product">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hub page</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
/* scaffolding only - the mock screen and the hub's Measure (railWidth = fontSize*18,
|
||||
contentMax = fontSize*56, pageGutter = fontSize*3). every value is a token */
|
||||
html, body { background: var(--chrome); }
|
||||
.screen { width: 1100px; height: 720px; }
|
||||
.shell { height: 100%; align-items: stretch; }
|
||||
|
||||
.rail { width: calc(var(--font-size) * 18); flex: none; display: flex; flex-direction: column; }
|
||||
.rail .wordmark { height: calc(var(--font-size) * 9); display: flex; align-items: center; justify-content: center; }
|
||||
.rail .groups { flex: 1; min-height: 0; overflow: hidden; }
|
||||
.rail .foot { padding: var(--gap-md) var(--gap-md) var(--gap-lg); }
|
||||
|
||||
.workspace { flex: 1; min-width: 0; }
|
||||
/* contentMax plus a pageGutter either side, centred in the pane */
|
||||
.column { width: 100%; max-width: calc(var(--font-size) * (56 + 6)); margin: 0 auto; align-self: center;
|
||||
padding: var(--gap-xxl) calc(var(--font-size) * 3); }
|
||||
.column > .page-heading { margin-bottom: var(--gap-xxl); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen">
|
||||
<div class="shell">
|
||||
|
||||
<!-- nav rail: a sunken Panel -->
|
||||
<nav class="panel sunken rail">
|
||||
<div class="wordmark"><span class="text-lg semibold">Garage</span></div>
|
||||
<hr class="divider">
|
||||
<div class="groups">
|
||||
<div class="nav-group">
|
||||
<div class="caption">Settings</div>
|
||||
<button class="btn secondary stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /> <circle cx="12" cy="7" r="4" /></svg>Account</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /> <path d="m9 12 2 2 4-4" /></svg>Security</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 17V7" /> <path d="M16 8h-6a2 2 0 0 0 0 4h4a2 2 0 0 1 0 4H8" /> <path d="M4 3a1 1 0 0 1 1-1 1.3 1.3 0 0 1 .7.2l.933.6a1.3 1.3 0 0 0 1.4 0l.934-.6a1.3 1.3 0 0 1 1.4 0l.933.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6A1.3 1.3 0 0 1 19 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1 1.3 1.3 0 0 1-.7-.2l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.934.6a1.3 1.3 0 0 1-1.4 0l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-1.4 0l-.934-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-.7.2 1 1 0 0 1-1-1z" /></svg>Payments</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2" /> <rect x="14" y="2" width="8" height="8" rx="1" /></svg>Authorised apps</button>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="nav-group">
|
||||
<div class="caption">Developer</div>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z" /> <path d="M8 10v4" /> <path d="M12 10v2" /> <path d="M16 10v6" /></svg>Projects</button>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="nav-group">
|
||||
<div class="caption">Garage Platform</div>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" /> <path d="M20 2v4" /> <path d="M22 4h-4" /> <circle cx="4" cy="20" r="2" /></svg>Assistant</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 15H6a4 4 0 0 0-4 4v2" /> <path d="m14.305 16.53.923-.382" /> <path d="m15.228 13.852-.923-.383" /> <path d="m16.852 12.228-.383-.923" /> <path d="m16.852 17.772-.383.924" /> <path d="m19.148 12.228.383-.923" /> <path d="m19.53 18.696-.382-.924" /> <path d="m20.772 13.852.924-.383" /> <path d="m20.772 16.148.924.383" /> <circle cx="18" cy="15" r="3" /> <circle cx="9" cy="7" r="4" /></svg>Users</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" /> <circle cx="16.5" cy="7.5" r=".5" fill="currentColor" /></svg>Roles</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 12h-5" /> <path d="M15 8h-5" /> <path d="M19 17V5a2 2 0 0 0-2-2H4" /> <path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" /></svg>Audit log</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" /></svg>Status</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="foot">
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m16 17 5-5-5-5" /> <path d="M21 12H9" /> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></svg>Sign out</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- workspace: a Panel with the capped, centred reading column -->
|
||||
<main class="panel workspace">
|
||||
<div class="column">
|
||||
<div class="page-heading">
|
||||
<div class="title">Account</div>
|
||||
<div class="subtitle">Your personal details and how you appear on Garage.</div>
|
||||
</div>
|
||||
|
||||
<section class="properties-section">
|
||||
<div class="header">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<div><div>Account</div><span class="subtitle">Your names, sign-in email and account identifiers</span></div>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Username</div>
|
||||
<div class="value">
|
||||
<div class="btn-group span">
|
||||
<div class="field secondary readonly grow"><input value="ellis" disabled></div>
|
||||
<button class="btn secondary">Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Display name</div>
|
||||
<div class="value"><div class="field secondary"><input value="Ellis Hart"></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Email</div>
|
||||
<div class="value"><div class="field secondary readonly"><input value="ellis@hartworks.dev" disabled></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Member since</div>
|
||||
<div class="value"><div class="field secondary readonly"><input value="6 Sep 2026" disabled></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">User ID</div>
|
||||
<div class="value">
|
||||
<div class="btn-group span">
|
||||
<div class="field secondary readonly mono grow"><input value="usr_8f2c41d9a7" disabled></div>
|
||||
<button class="btn secondary icon-only" aria-label="Copy"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /> <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="properties-section">
|
||||
<div class="header">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<div><div>Personal information</div><span class="subtitle">The name and details on your account</span></div>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Legal name</div>
|
||||
<div class="value"><div class="field secondary readonly"><input value="Ellis Jordan Hart" disabled></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Date of birth</div>
|
||||
<div class="value"><div class="field secondary readonly"><input value="14 Mar 1994" disabled></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="properties-section">
|
||||
<div class="header">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<div><div>Password</div><span class="subtitle">Used to sign in when a passkey isn't available</span></div>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Password</div>
|
||||
<div class="value">
|
||||
<div class="btn-group span">
|
||||
<div class="field secondary readonly grow"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg><input value="Hidden" disabled></div>
|
||||
<button class="btn secondary icon-only" aria-label="Change password"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,112 @@
|
||||
<!-- @dsCard group="Patterns" -->
|
||||
<!doctype html>
|
||||
<html lang="en" data-scheme="carbon" data-density="compact">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Settings pane</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
/* scaffolding - the editor's chrome at dialog size: 580 x 420 at compact, a 176 rail
|
||||
and a content Panel on the chrome ground */
|
||||
html, body { background: var(--background); }
|
||||
.stage { width: 700px; height: 520px; display: flex; align-items: center; justify-content: center; }
|
||||
.pane { width: 580px; height: 420px; border-radius: var(--panel-radius); overflow: hidden; }
|
||||
.shell { height: 100%; align-items: stretch; }
|
||||
|
||||
.rail { width: 176px; flex: none; display: flex; flex-direction: column; }
|
||||
.rail .search { padding: var(--gap-sm); }
|
||||
/* the editor's rail is narrower than the hub's, so its categories inset gapSm not gapMd */
|
||||
.rail .nav-group { padding-inline: var(--gap-sm); }
|
||||
|
||||
.content { flex: 1; min-width: 0; }
|
||||
.content .scroll { padding: var(--gap-xl) var(--gap-sm) var(--gap-xxl); }
|
||||
.content .page-heading { margin-bottom: var(--gap-lg); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div class="pane">
|
||||
<div class="shell">
|
||||
|
||||
<nav class="panel sunken rail">
|
||||
<div class="search"><label class="field stretch"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></svg><input placeholder="Search"></label></div>
|
||||
<hr class="divider">
|
||||
<div class="nav-group">
|
||||
<div class="caption">Settings</div>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /> <circle cx="12" cy="7" r="4" /></svg>Account</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect width="20" height="14" x="2" y="5" rx="2" /> <line x1="2" x2="22" y1="10" y2="10" /> <path d="M6 14h2" /></svg>Billing</button>
|
||||
<button class="btn secondary stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" /> <circle cx="13.5" cy="6.5" r=".5" fill="currentColor" /> <circle cx="17.5" cy="10.5" r=".5" fill="currentColor" /> <circle cx="6.5" cy="12.5" r=".5" fill="currentColor" /> <circle cx="8.5" cy="7.5" r=".5" fill="currentColor" /></svg>Appearance</button>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 22v-5" /> <path d="M15 8V2" /> <path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z" /> <path d="M9 8V2" /></svg>Integrations</button>
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<div class="nav-group">
|
||||
<div class="caption">Editor</div>
|
||||
<button class="btn ghost stretch align-left"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8h.01" /> <path d="M12 12h.01" /> <path d="M14 8h.01" /> <path d="M16 12h.01" /> <path d="M18 8h.01" /> <path d="M6 8h.01" /> <path d="M7 16h10" /> <path d="M8 12h.01" /> <rect width="20" height="16" x="2" y="4" rx="2" /></svg>Keymap</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="panel content">
|
||||
<div class="scroll">
|
||||
<div class="page-heading">
|
||||
<div class="title">Appearance</div>
|
||||
<div class="subtitle">How the editor and the map are drawn.</div>
|
||||
</div>
|
||||
|
||||
<section class="properties-section">
|
||||
<div class="header">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<div><div>Editor</div><span class="subtitle">Colours the interface itself is drawn in</span></div>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Colour scheme</div>
|
||||
<div class="value"><div class="select secondary"><span>Carbon</span><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Accent</div>
|
||||
<div class="value"><div class="select secondary"><span>Default</span><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Dark mode</div>
|
||||
<div class="value"><span class="checkbox checked"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="properties-section">
|
||||
<div class="header">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<div><div>Interface</div><span class="subtitle">How much room a control takes up</span></div>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Density</div>
|
||||
<div class="value"><div class="select secondary"><span>Compact</span><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="properties-section">
|
||||
<div class="header">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<div><div>Canvas</div><span class="subtitle">The map's own colours, separate from the editor's</span></div>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<div class="property-row">
|
||||
<div class="label">Colour scheme</div>
|
||||
<div class="value"><div class="select secondary"><span>Follow editor</span><svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg></div></div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<div class="label">Dark mode</div>
|
||||
<div class="value"><span class="checkbox checked"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!-- @dsCard group="Patterns" -->
|
||||
<!doctype html>
|
||||
<html lang="en" data-scheme="carbon" data-density="product">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Page states</title>
|
||||
<link rel="stylesheet" href="../components.css">
|
||||
<style>
|
||||
/* scaffolding - three workspace Panels on the chrome ground, one state each:
|
||||
loading (a spinner), empty (one muted line), error (muted message + outline retry) */
|
||||
html, body { background: var(--chrome); }
|
||||
.screen { width: 1100px; height: 440px; }
|
||||
.shell { height: 100%; align-items: stretch; }
|
||||
.shell > .panel { flex: 1; min-width: 0; display: flex; flex-direction: column;
|
||||
padding: var(--gap-xxl) var(--gap-xl); }
|
||||
.shell > .panel > .page-heading { margin-bottom: var(--gap-xxl); }
|
||||
.fill { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--gap-md); }
|
||||
.note { padding: var(--gap-md) var(--container-padding); display: flex; flex-direction: column; align-items: flex-start; gap: var(--gap-md); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen">
|
||||
<div class="shell">
|
||||
|
||||
<!-- loading -->
|
||||
<section class="panel">
|
||||
<div class="page-heading">
|
||||
<div class="title">Payments</div>
|
||||
<div class="subtitle">What you've bought, and what you're still paying for.</div>
|
||||
</div>
|
||||
<div class="fill"><div class="spinner" role="status" aria-label="Loading"></div></div>
|
||||
</section>
|
||||
|
||||
<!-- empty -->
|
||||
<section class="panel">
|
||||
<div class="page-heading">
|
||||
<div class="title">Projects</div>
|
||||
<div class="subtitle">Applications you build and sell on Garage.</div>
|
||||
</div>
|
||||
<div class="fill">
|
||||
<div class="muted">No projects yet</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- error: still a section, with nothing in it -->
|
||||
<section class="panel">
|
||||
<div class="page-heading">
|
||||
<div class="title">Security</div>
|
||||
<div class="subtitle">Your password, passkeys and signed-in devices.</div>
|
||||
</div>
|
||||
<section class="properties-section">
|
||||
<div class="header">
|
||||
<svg class="icon chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
<div><div>Passkeys</div><span class="subtitle">Sign in with your device instead of a password</span></div>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<div class="note">
|
||||
<div class="muted">Couldn't load your passkeys.</div>
|
||||
<button class="btn outline">Try again</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,259 @@
|
||||
/* garage_ui design tokens. Generated by garage_ui/tool/export_design_tokens_test.dart
|
||||
- dont edit by hand, rerun the export.
|
||||
Scheme: data-scheme="carbon" (default) | "dark" | "light"
|
||||
Density: data-density="product" (default) | "normal" | "compact" */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500&display=swap");
|
||||
|
||||
:root {
|
||||
--font-sans: "Geist", system-ui, sans-serif;
|
||||
--font-mono: "Geist Mono", ui-monospace, monospace;
|
||||
--weight-body: 300;
|
||||
--weight-control: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-semibold: 600;
|
||||
--radius-xs: 2px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 10px;
|
||||
--radius-xxl: 12px;
|
||||
--panel-radius: 10px;
|
||||
--panel-gap: 5px;
|
||||
--panel-border-width: 1.15px;
|
||||
--property-split: 0.4;
|
||||
--property-label-gap: 10px;
|
||||
--dialog-max-width: 350px;
|
||||
}
|
||||
|
||||
:root, [data-scheme="carbon"] {
|
||||
--background: #0d0d0d;
|
||||
--foreground: #f2f2f2;
|
||||
--chrome: #010101;
|
||||
--surface-sunken: #0a0a0a;
|
||||
--card: #1b1b1b;
|
||||
--muted: #0f0f0f;
|
||||
--muted-foreground: #8f8f8f;
|
||||
--row-text: #c2c2c2;
|
||||
--row-hovered: #222222;
|
||||
--popover: #000000;
|
||||
--popover-border: #1c1c1c;
|
||||
--popover-item-hovered: #1f1f1f;
|
||||
--tooltip-background: #080808;
|
||||
--tooltip-border: #1f1f1f;
|
||||
--primary: #e8e8e8;
|
||||
--primary-hovered: #fefefe;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #303030;
|
||||
--secondary-hovered: #424242;
|
||||
--secondary-foreground: #f2f2f2;
|
||||
--destructive: #c43333;
|
||||
--control-fill: #080808;
|
||||
--control-fill-hovered: #070707;
|
||||
--control-fill-focused: #000000;
|
||||
--control-border: #2a2a2a;
|
||||
--switch-track-inactive: #0f0f0f;
|
||||
--border: #010101;
|
||||
--divider: #202020;
|
||||
--panel-border: #222222;
|
||||
--panel-border-highlighted: #3a3a3a;
|
||||
--properties-section-border: #272727;
|
||||
--ring: #ffffff;
|
||||
--chart-1: #f45b69;
|
||||
--chart-2: #86d957;
|
||||
--chart-3: #62a7ff;
|
||||
--chart-4: #ffc857;
|
||||
--chart-5: #ff8c42;
|
||||
}
|
||||
|
||||
[data-scheme="dark"] {
|
||||
--background: #303030;
|
||||
--foreground: #e6e6e6;
|
||||
--chrome: #171717;
|
||||
--surface-sunken: #272727;
|
||||
--card: #3d3d3d;
|
||||
--muted: #1d1d1d;
|
||||
--muted-foreground: #999999;
|
||||
--row-text: #c1c1c1;
|
||||
--row-hovered: #474747;
|
||||
--popover: #191919;
|
||||
--popover-border: #2d2d2d;
|
||||
--popover-item-hovered: #424242;
|
||||
--tooltip-background: #1d1d1d;
|
||||
--tooltip-border: #303030;
|
||||
--primary: #4772b3;
|
||||
--primary-hovered: #6b8fc4;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #565656;
|
||||
--secondary-hovered: #676767;
|
||||
--secondary-foreground: #e6e6e6;
|
||||
--destructive: #a9575f;
|
||||
--control-fill: #1d1d1d;
|
||||
--control-fill-hovered: #222222;
|
||||
--control-fill-focused: #1a1a1a;
|
||||
--control-border: #353535;
|
||||
--switch-track-inactive: #1d1d1d;
|
||||
--border: #171717;
|
||||
--divider: #454545;
|
||||
--panel-border: #464646;
|
||||
--panel-border-highlighted: #606060;
|
||||
--properties-section-border: #4c4c4c;
|
||||
--ring: #6592d7;
|
||||
--chart-1: #ff3352;
|
||||
--chart-2: #8bdc00;
|
||||
--chart-3: #2890ff;
|
||||
--chart-4: #edba18;
|
||||
--chart-5: #ed5700;
|
||||
}
|
||||
|
||||
[data-scheme="light"] {
|
||||
--background: #f2f2f4;
|
||||
--foreground: #1a1a1c;
|
||||
--chrome: #cdcdd5;
|
||||
--surface-sunken: #e7e7ea;
|
||||
--card: #fefeff;
|
||||
--muted: #d7d7de;
|
||||
--muted-foreground: #65656d;
|
||||
--row-text: #3b3b40;
|
||||
--row-hovered: #fefeff;
|
||||
--popover: #d1d1d8;
|
||||
--popover-border: #ededf0;
|
||||
--popover-item-hovered: #fefeff;
|
||||
--tooltip-background: #d7d7de;
|
||||
--tooltip-border: #f2f2f4;
|
||||
--primary: #3a63a1;
|
||||
--primary-hovered: #537fc1;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #fefeff;
|
||||
--secondary-hovered: #fefeff;
|
||||
--secondary-foreground: #1a1a1c;
|
||||
--destructive: #b3474f;
|
||||
--control-fill: #d7d7de;
|
||||
--control-fill-hovered: #e0e0e4;
|
||||
--control-fill-focused: #d3d3d9;
|
||||
--control-border: #f9f9fa;
|
||||
--switch-track-inactive: #d7d7de;
|
||||
--border: #cdcdd5;
|
||||
--divider: #d8d8de;
|
||||
--panel-border: #d7d7dd;
|
||||
--panel-border-highlighted: #fefeff;
|
||||
--properties-section-border: #eaeaff;
|
||||
--ring: #4880d5;
|
||||
--chart-1: #ff3352;
|
||||
--chart-2: #8bdc00;
|
||||
--chart-3: #2890ff;
|
||||
--chart-4: #edba18;
|
||||
--chart-5: #ed5700;
|
||||
}
|
||||
|
||||
:root, [data-density="product"] {
|
||||
--font-size: 12px;
|
||||
--line-height: 1.25;
|
||||
--line-box: 15px;
|
||||
--control-height: 33px;
|
||||
--control-padding-x: 10px;
|
||||
--control-padding-y: 9px;
|
||||
--control-gap: 5px;
|
||||
--control-border-width: 1px;
|
||||
--menu-row-height: 33px;
|
||||
--popup-row-height: 33px;
|
||||
--explorer-row-height: 29px;
|
||||
--chrome-bar-height: 43px;
|
||||
--property-row-height: 36px;
|
||||
--label-column-height: 31px;
|
||||
--properties-section-gap: 15px;
|
||||
--explorer-row-base-padding: 8px;
|
||||
--explorer-row-end-padding: 12px;
|
||||
--list-row-indent: 10px;
|
||||
--container-gap: 10px;
|
||||
--container-padding: 20px;
|
||||
--gap-xxs: 3px;
|
||||
--gap-xs: 5px;
|
||||
--gap-sm: 8px;
|
||||
--gap-md: 10px;
|
||||
--gap-lg: 15px;
|
||||
--gap-xl: 20px;
|
||||
--gap-xxl: 30px;
|
||||
--text-xxs: 11px;
|
||||
--text-xs: 13px;
|
||||
--text-sm: 17px;
|
||||
--text-lg: 22px;
|
||||
--icon-sm: 12px;
|
||||
--icon-md: 20px;
|
||||
--icon-lg: 24px;
|
||||
}
|
||||
|
||||
[data-density="compact"] {
|
||||
--font-size: 10px;
|
||||
--line-height: 1.1;
|
||||
--line-box: 11px;
|
||||
--control-height: 23px;
|
||||
--control-padding-x: 10px;
|
||||
--control-padding-y: 6px;
|
||||
--control-gap: 4px;
|
||||
--control-border-width: 1px;
|
||||
--menu-row-height: 20px;
|
||||
--popup-row-height: 20px;
|
||||
--explorer-row-height: 22px;
|
||||
--chrome-bar-height: 31px;
|
||||
--property-row-height: 28px;
|
||||
--label-column-height: 26px;
|
||||
--properties-section-gap: 6px;
|
||||
--explorer-row-base-padding: 4px;
|
||||
--explorer-row-end-padding: 10px;
|
||||
--list-row-indent: 6px;
|
||||
--container-gap: 8px;
|
||||
--container-padding: 16px;
|
||||
--gap-xxs: 2px;
|
||||
--gap-xs: 4px;
|
||||
--gap-sm: 6px;
|
||||
--gap-md: 8px;
|
||||
--gap-lg: 12px;
|
||||
--gap-xl: 16px;
|
||||
--gap-xxl: 24px;
|
||||
--text-xxs: 9px;
|
||||
--text-xs: 11px;
|
||||
--text-sm: 14px;
|
||||
--text-lg: 18px;
|
||||
--icon-sm: 10px;
|
||||
--icon-md: 20px;
|
||||
--icon-lg: 24px;
|
||||
}
|
||||
|
||||
[data-density="normal"] {
|
||||
--font-size: 10px;
|
||||
--line-height: 1.1;
|
||||
--line-box: 11px;
|
||||
--control-height: 27px;
|
||||
--control-padding-x: 12px;
|
||||
--control-padding-y: 8px;
|
||||
--control-gap: 5px;
|
||||
--control-border-width: 1px;
|
||||
--menu-row-height: 26px;
|
||||
--popup-row-height: 26px;
|
||||
--explorer-row-height: 30px;
|
||||
--chrome-bar-height: 37px;
|
||||
--property-row-height: 30px;
|
||||
--label-column-height: 26px;
|
||||
--properties-section-gap: 15px;
|
||||
--explorer-row-base-padding: 8px;
|
||||
--explorer-row-end-padding: 14px;
|
||||
--list-row-indent: 12px;
|
||||
--container-gap: 10px;
|
||||
--container-padding: 20px;
|
||||
--gap-xxs: 3px;
|
||||
--gap-xs: 5px;
|
||||
--gap-sm: 8px;
|
||||
--gap-md: 10px;
|
||||
--gap-lg: 15px;
|
||||
--gap-xl: 20px;
|
||||
--gap-xxl: 30px;
|
||||
--text-xxs: 9px;
|
||||
--text-xs: 11px;
|
||||
--text-sm: 14px;
|
||||
--text-lg: 18px;
|
||||
--icon-sm: 10px;
|
||||
--icon-md: 20px;
|
||||
--icon-lg: 24px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"_generated": "by garage_ui/tool/export_design_tokens_test.dart - dont edit by hand",
|
||||
"fonts": {
|
||||
"sans": "Geist",
|
||||
"mono": "Geist Mono"
|
||||
},
|
||||
"weights": {
|
||||
"body": 300,
|
||||
"control": 400,
|
||||
"medium": 500,
|
||||
"semibold": 600
|
||||
},
|
||||
"schemes": {
|
||||
"carbon": {
|
||||
"background": "#0d0d0d",
|
||||
"foreground": "#f2f2f2",
|
||||
"chrome": "#010101",
|
||||
"surface-sunken": "#0a0a0a",
|
||||
"card": "#1b1b1b",
|
||||
"muted": "#0f0f0f",
|
||||
"muted-foreground": "#8f8f8f",
|
||||
"row-text": "#c2c2c2",
|
||||
"row-hovered": "#222222",
|
||||
"popover": "#000000",
|
||||
"popover-border": "#1c1c1c",
|
||||
"popover-item-hovered": "#1f1f1f",
|
||||
"tooltip-background": "#080808",
|
||||
"tooltip-border": "#1f1f1f",
|
||||
"primary": "#e8e8e8",
|
||||
"primary-hovered": "#fefefe",
|
||||
"primary-foreground": "#000000",
|
||||
"secondary": "#303030",
|
||||
"secondary-hovered": "#424242",
|
||||
"secondary-foreground": "#f2f2f2",
|
||||
"destructive": "#c43333",
|
||||
"control-fill": "#080808",
|
||||
"control-fill-hovered": "#070707",
|
||||
"control-fill-focused": "#000000",
|
||||
"control-border": "#2a2a2a",
|
||||
"switch-track-inactive": "#0f0f0f",
|
||||
"border": "#010101",
|
||||
"divider": "#202020",
|
||||
"panel-border": "#222222",
|
||||
"panel-border-highlighted": "#3a3a3a",
|
||||
"properties-section-border": "#272727",
|
||||
"ring": "#ffffff",
|
||||
"chart-1": "#f45b69",
|
||||
"chart-2": "#86d957",
|
||||
"chart-3": "#62a7ff",
|
||||
"chart-4": "#ffc857",
|
||||
"chart-5": "#ff8c42"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#303030",
|
||||
"foreground": "#e6e6e6",
|
||||
"chrome": "#171717",
|
||||
"surface-sunken": "#272727",
|
||||
"card": "#3d3d3d",
|
||||
"muted": "#1d1d1d",
|
||||
"muted-foreground": "#999999",
|
||||
"row-text": "#c1c1c1",
|
||||
"row-hovered": "#474747",
|
||||
"popover": "#191919",
|
||||
"popover-border": "#2d2d2d",
|
||||
"popover-item-hovered": "#424242",
|
||||
"tooltip-background": "#1d1d1d",
|
||||
"tooltip-border": "#303030",
|
||||
"primary": "#4772b3",
|
||||
"primary-hovered": "#6b8fc4",
|
||||
"primary-foreground": "#ffffff",
|
||||
"secondary": "#565656",
|
||||
"secondary-hovered": "#676767",
|
||||
"secondary-foreground": "#e6e6e6",
|
||||
"destructive": "#a9575f",
|
||||
"control-fill": "#1d1d1d",
|
||||
"control-fill-hovered": "#222222",
|
||||
"control-fill-focused": "#1a1a1a",
|
||||
"control-border": "#353535",
|
||||
"switch-track-inactive": "#1d1d1d",
|
||||
"border": "#171717",
|
||||
"divider": "#454545",
|
||||
"panel-border": "#464646",
|
||||
"panel-border-highlighted": "#606060",
|
||||
"properties-section-border": "#4c4c4c",
|
||||
"ring": "#6592d7",
|
||||
"chart-1": "#ff3352",
|
||||
"chart-2": "#8bdc00",
|
||||
"chart-3": "#2890ff",
|
||||
"chart-4": "#edba18",
|
||||
"chart-5": "#ed5700"
|
||||
},
|
||||
"light": {
|
||||
"background": "#f2f2f4",
|
||||
"foreground": "#1a1a1c",
|
||||
"chrome": "#cdcdd5",
|
||||
"surface-sunken": "#e7e7ea",
|
||||
"card": "#fefeff",
|
||||
"muted": "#d7d7de",
|
||||
"muted-foreground": "#65656d",
|
||||
"row-text": "#3b3b40",
|
||||
"row-hovered": "#fefeff",
|
||||
"popover": "#d1d1d8",
|
||||
"popover-border": "#ededf0",
|
||||
"popover-item-hovered": "#fefeff",
|
||||
"tooltip-background": "#d7d7de",
|
||||
"tooltip-border": "#f2f2f4",
|
||||
"primary": "#3a63a1",
|
||||
"primary-hovered": "#537fc1",
|
||||
"primary-foreground": "#ffffff",
|
||||
"secondary": "#fefeff",
|
||||
"secondary-hovered": "#fefeff",
|
||||
"secondary-foreground": "#1a1a1c",
|
||||
"destructive": "#b3474f",
|
||||
"control-fill": "#d7d7de",
|
||||
"control-fill-hovered": "#e0e0e4",
|
||||
"control-fill-focused": "#d3d3d9",
|
||||
"control-border": "#f9f9fa",
|
||||
"switch-track-inactive": "#d7d7de",
|
||||
"border": "#cdcdd5",
|
||||
"divider": "#d8d8de",
|
||||
"panel-border": "#d7d7dd",
|
||||
"panel-border-highlighted": "#fefeff",
|
||||
"properties-section-border": "#eaeaff",
|
||||
"ring": "#4880d5",
|
||||
"chart-1": "#ff3352",
|
||||
"chart-2": "#8bdc00",
|
||||
"chart-3": "#2890ff",
|
||||
"chart-4": "#edba18",
|
||||
"chart-5": "#ed5700"
|
||||
}
|
||||
},
|
||||
"densities": {
|
||||
"compact": {
|
||||
"font-size": 10.0,
|
||||
"line-height": 1.1,
|
||||
"line-box": 11.0,
|
||||
"control-height": 23.0,
|
||||
"control-padding-x": 10.0,
|
||||
"control-padding-y": 6.0,
|
||||
"control-gap": 4.0,
|
||||
"control-border-width": 1.0,
|
||||
"menu-row-height": 20.0,
|
||||
"popup-row-height": 20.0,
|
||||
"explorer-row-height": 22.0,
|
||||
"chrome-bar-height": 31.0,
|
||||
"property-row-height": 28.0,
|
||||
"label-column-height": 26.0,
|
||||
"properties-section-gap": 6.0,
|
||||
"explorer-row-base-padding": 4.0,
|
||||
"explorer-row-end-padding": 10.0,
|
||||
"list-row-indent": 6.0,
|
||||
"container-gap": 8.0,
|
||||
"container-padding": 16.0,
|
||||
"gap-xxs": 2.0,
|
||||
"gap-xs": 4.0,
|
||||
"gap-sm": 6.0,
|
||||
"gap-md": 8.0,
|
||||
"gap-lg": 12.0,
|
||||
"gap-xl": 16.0,
|
||||
"gap-xxl": 24.0,
|
||||
"text-xxs": 9.0,
|
||||
"text-xs": 11.0,
|
||||
"text-sm": 14.0,
|
||||
"text-lg": 18.0,
|
||||
"icon-sm": 10.0,
|
||||
"icon-md": 20.0,
|
||||
"icon-lg": 24.0
|
||||
},
|
||||
"normal": {
|
||||
"font-size": 10.0,
|
||||
"line-height": 1.1,
|
||||
"line-box": 11.0,
|
||||
"control-height": 27.0,
|
||||
"control-padding-x": 12.0,
|
||||
"control-padding-y": 8.0,
|
||||
"control-gap": 5.0,
|
||||
"control-border-width": 1.0,
|
||||
"menu-row-height": 26.0,
|
||||
"popup-row-height": 26.0,
|
||||
"explorer-row-height": 30.0,
|
||||
"chrome-bar-height": 37.0,
|
||||
"property-row-height": 30.0,
|
||||
"label-column-height": 26.0,
|
||||
"properties-section-gap": 15.0,
|
||||
"explorer-row-base-padding": 8.0,
|
||||
"explorer-row-end-padding": 14.0,
|
||||
"list-row-indent": 12.0,
|
||||
"container-gap": 10.0,
|
||||
"container-padding": 20.0,
|
||||
"gap-xxs": 3.0,
|
||||
"gap-xs": 5.0,
|
||||
"gap-sm": 8.0,
|
||||
"gap-md": 10.0,
|
||||
"gap-lg": 15.0,
|
||||
"gap-xl": 20.0,
|
||||
"gap-xxl": 30.0,
|
||||
"text-xxs": 9.0,
|
||||
"text-xs": 11.0,
|
||||
"text-sm": 14.0,
|
||||
"text-lg": 18.0,
|
||||
"icon-sm": 10.0,
|
||||
"icon-md": 20.0,
|
||||
"icon-lg": 24.0
|
||||
},
|
||||
"product": {
|
||||
"font-size": 12.0,
|
||||
"line-height": 1.25,
|
||||
"line-box": 15.0,
|
||||
"control-height": 33.0,
|
||||
"control-padding-x": 10.0,
|
||||
"control-padding-y": 9.0,
|
||||
"control-gap": 5.0,
|
||||
"control-border-width": 1.0,
|
||||
"menu-row-height": 33.0,
|
||||
"popup-row-height": 33.0,
|
||||
"explorer-row-height": 29.0,
|
||||
"chrome-bar-height": 43.0,
|
||||
"property-row-height": 36.0,
|
||||
"label-column-height": 31.0,
|
||||
"properties-section-gap": 15.0,
|
||||
"explorer-row-base-padding": 8.0,
|
||||
"explorer-row-end-padding": 12.0,
|
||||
"list-row-indent": 10.0,
|
||||
"container-gap": 10.0,
|
||||
"container-padding": 20.0,
|
||||
"gap-xxs": 3.0,
|
||||
"gap-xs": 5.0,
|
||||
"gap-sm": 8.0,
|
||||
"gap-md": 10.0,
|
||||
"gap-lg": 15.0,
|
||||
"gap-xl": 20.0,
|
||||
"gap-xxl": 30.0,
|
||||
"text-xxs": 11.0,
|
||||
"text-xs": 13.0,
|
||||
"text-sm": 17.0,
|
||||
"text-lg": 22.0,
|
||||
"icon-sm": 12.0,
|
||||
"icon-md": 20.0,
|
||||
"icon-lg": 24.0
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"radius-xs": 2.0,
|
||||
"radius-sm": 4.0,
|
||||
"radius-md": 6.0,
|
||||
"radius-lg": 8.0,
|
||||
"radius-xl": 10.0,
|
||||
"radius-xxl": 12.0,
|
||||
"panel-radius": 10.0,
|
||||
"panel-gap": 5.0,
|
||||
"panel-border-width": 1.15,
|
||||
"property-split": 0.4,
|
||||
"property-label-gap": 10,
|
||||
"dialog-max-width": 350
|
||||
}
|
||||
}
|
||||
+387
-460
@@ -1,503 +1,430 @@
|
||||
# Garage UI style guide
|
||||
# garage_ui style guide
|
||||
|
||||
This is not the widget API reference (read the source for that — every file in
|
||||
`lib/` is short and commented). This is the *other* half: which widget, which
|
||||
variant, which colour token, in which situation. Getting a Garage app to use
|
||||
`Button` and `Panel` is easy. Getting it to actually look like a Garage app —
|
||||
right variant for the right emphasis, right token for the right surface, right
|
||||
gap for the right kind of space — is the part that doesn't fall out of the
|
||||
API. That's what this document is for.
|
||||
How to build a screen out of garage_ui so it actually looks like a Garage app.
|
||||
The API is in the source. This is the other half: which widget, which variant,
|
||||
where — and, mostly, what to leave alone.
|
||||
|
||||
It's written from **observed usage** in Arcs & Angles (metro_map_maker) and
|
||||
Music Maker, the two real apps built on this package. Every rule below is
|
||||
backed by a real call site, not a guess at what "should" be idiomatic. Where
|
||||
a pattern only has 1-2 examples, that's said explicitly — treat it as a lead,
|
||||
not a law.
|
||||
It's written from the two apps that are the reference:
|
||||
|
||||
## Where this comes from
|
||||
- **Arcs & Angles, blender mode** (`metro_map_maker`, `lib/pages/blender/`) — a
|
||||
dense desktop editor on the `compact` density.
|
||||
- **The Garage hub** (`garage_dashboard_portal`) — a web product on the
|
||||
`product` density.
|
||||
|
||||
Two influences, and they are not the same kind of influence — conflating them
|
||||
is the usual misreading:
|
||||
Where those two do something the guide says not to, it's listed at the end
|
||||
under [Dont copy these](#dont-copy-these). An existing call site is not
|
||||
precedent.
|
||||
|
||||
- **shadcn (via `shadcn_flutter`) — the API surface.** Variant names
|
||||
(`.primary` / `.secondary` / `.outline` / `.ghost` / `.destructive`), the
|
||||
`ColourScheme` slot vocabulary, the dot-constructor shape. This is why a
|
||||
shadcn snippet usually *compiles*.
|
||||
- **Blender — the rendering.** Flat chrome, bordered panels, dense controls,
|
||||
no elevation, a properties pane with a hard split down it. This is why the
|
||||
same snippet doesn't *look* like shadcn once it runs.
|
||||
|
||||
So: **not a pixel-for-pixel restyle of shadcn.** The names carried over; the
|
||||
geometry, the density and the entire chrome layer did not. `shadcn_flutter`
|
||||
was dropped as a dependency once the port finished — nothing here defers to
|
||||
it, and where the two disagree on how something should look, this package
|
||||
wins. Expect ported code to compile and then need its spacing and emphasis
|
||||
re-picked against the tables below.
|
||||
## The one rule: let the theme speak
|
||||
|
||||
## Mental model
|
||||
garage_ui already knows what everything should look like. The colour scheme,
|
||||
the density and the variant between them decide every colour, font size,
|
||||
weight, icon size, padding, radius and gap. Your job is to pick the **widget**
|
||||
and the **variant**. Then stop.
|
||||
|
||||
A Garage app has three layers, outside-in:
|
||||
So, inside or around a garage_ui component, don't:
|
||||
|
||||
1. **Chrome** — the app's own furniture: headers, footers, the menu bar, the
|
||||
shell that docks everything else. Flat, dark, no elevation. Reads as part
|
||||
of the window, not as content sitting on the window.
|
||||
2. **Panels** — bordered, rounded, elevated-feeling regions that hold actual
|
||||
tool content (explorer trees, property inspectors, docked windows). This
|
||||
is where most of the UI actually lives.
|
||||
3. **Controls** — buttons, fields, selects, menus. Live inside panels or
|
||||
chrome, never bare against the app background.
|
||||
- pass a `TextStyle` with a `fontSize`, `fontWeight` or `color`
|
||||
- give an `Icon` a `size:` or `color:`
|
||||
- write a literal `EdgeInsets`, `SizedBox`, `Gap(8)` or `BorderRadius.circular(8)`
|
||||
- pass `alignment:` to a button that doesnt need it
|
||||
- force `density: ControlDensity.compact` on a control to make it "fit"
|
||||
- wrap a dialog's content in a `ConstrainedBox(maxWidth: 360)`
|
||||
- build a `Container` with a fill, border and radius to make a badge, a pill, a
|
||||
warning box or a popover
|
||||
|
||||
Nothing in this system uses Flutter's Material widgets. There is no
|
||||
`ThemeData.dark()`, no `Colors.black`, no `Scaffold`. Every colour comes from
|
||||
`ColourScheme`, every size comes from `Density`, every font comes from
|
||||
`Typography`. If a widget you're building reaches for `package:flutter/material.dart`
|
||||
or a literal `Color(0x...)` outside `theme/*.dart`, that's the tell you've
|
||||
stepped outside the system.
|
||||
Every one of those is a number or a colour that was chosen once, per call site,
|
||||
and stops agreeing with the rest of the app the moment the density, scheme or
|
||||
accent changes. The components are built as families — outline buttons share
|
||||
text fields' fill, stroke and radius on purpose so they "read as one family
|
||||
instead of each getting styled by hand per call site". One override breaks the
|
||||
family.
|
||||
|
||||
## Getting the theme
|
||||
When something looks wrong, the fix is a different variant, a different
|
||||
widget, or a change **in garage_ui** so every app gets it. Not a local
|
||||
override. That's how `ButtonStyle.ghostDestructive` came about: apps kept
|
||||
hand-colouring ghost buttons red, so it became a variant.
|
||||
|
||||
What you *can* reach for, when a component doesnt cover it:
|
||||
|
||||
- `Gap.xs()` … `Gap.xxl()` and `density.gapXs` … for space between things
|
||||
- `density.textXxs` … `density.textLg`, or the text extensions, for text size
|
||||
- `theme.typography.medium` / `.semiBold` for weight
|
||||
- `scheme.<slot>` for colour, when you're drawing something garage_ui doesnt
|
||||
have a widget for
|
||||
|
||||
These are tokens — they move with the theme. A literal never does.
|
||||
|
||||
|
||||
## Setting up
|
||||
|
||||
```dart
|
||||
final theme = GarageTheme.of(context);
|
||||
final cs = theme.colorScheme;
|
||||
final theme = ThemeData(
|
||||
colorScheme: GarageSchemes.carbon, // or your own, see below
|
||||
density: const Density.product(), // or .compact() / .normal()
|
||||
);
|
||||
|
||||
GarageApp.router(routerConfig: router, theme: theme);
|
||||
```
|
||||
|
||||
Everything hangs off `theme`: `theme.colorScheme`, `theme.density`,
|
||||
`theme.typography`, `theme.iconTheme`, `theme.radiusMd` / `.borderRadiusMd`
|
||||
(and `Sm`/`Lg`/`Xl`/`Xxl`), `theme.panelRadius`, `theme.panelGap`.
|
||||
`GarageApp` is built on `WidgetsApp`, not `MaterialApp`, and installs
|
||||
`GarageTheme` plus the scroll behaviour and scrollbar. Nothing in garage_ui
|
||||
touches Material. If a file reaches for `package:flutter/material.dart`, it has
|
||||
left the system.
|
||||
|
||||
One exception: an app's *unaccented* scheme — the one nobody's accent-colour
|
||||
override has touched — is read through the app's settings provider, not
|
||||
`GarageTheme.of(context)`. In Arcs & Angles that's
|
||||
`context.watch<SettingsProvider>().anaScheme`. Reach for this specifically
|
||||
when you need `chrome` on something that must stay neutral even if the user
|
||||
picked a wild accent colour (this is rare — most code never needs to do this,
|
||||
because `ChromeBar`/`Panel`/`GarageShell` already read `chrome`/`panel`
|
||||
internally). See "Colour tokens" below for why `chrome` gets this treatment.
|
||||
Leave the rest of `ThemeData` on its defaults — `scaling 1.0`, `radius 0.5`
|
||||
(so `radiusSm 4`, `radiusMd 6`), `panelRadius 10`, `panelGap 5`. Both
|
||||
reference apps do.
|
||||
|
||||
## Colour tokens
|
||||
Read the theme with `GarageTheme.of(context)`: `.colorScheme`, `.density`,
|
||||
`.typography`, `.iconTheme`, `.radiusMd` and friends.
|
||||
|
||||
`ColourScheme` (in `theme/colour_scheme.dart`) is one flat list of named
|
||||
colours — no light/dark split, no derived roles computed at paint time. Every
|
||||
field is authored by hand per scheme (Arcs & Angles ships eleven: zinc,
|
||||
crimson, slate, forest, stone, teal, indigo, amber, carbon, fuchsia, plus each
|
||||
one's light twin). When you add a new UI surface, you're choosing which of
|
||||
these *existing* tokens it belongs to — you're not inventing a new colour.
|
||||
### Colour schemes
|
||||
|
||||
### Base semantic slots (shadcn-shaped, still the backbone)
|
||||
|
||||
| Token | What it's for |
|
||||
|---|---|
|
||||
| `background` / `foreground` | The app's base surface and default text colour. |
|
||||
| `card` / `cardForeground` | `Card`/`SurfaceCard` fill and the text colour merged inside them. |
|
||||
| `popover` / `popoverForeground` / `popoverBorder` | Dropdowns, select popups, context menus — anything that floats over content in an `OverlayPortal`. |
|
||||
| `primary` / `primaryHovered` / `primaryForeground` | The accent. Active/engaged control state, the one CTA in a dialog. Swappable per-user via `withAccent()`. |
|
||||
| `secondary` / `secondaryHovered` / `secondaryForeground` | Neutral filled control — a resting toggle, an attached-to-a-field icon button. Not an accent colour, just "has a fill." |
|
||||
| `muted` / `mutedForeground` | De-emphasised text/backgrounds — labels, hints, disabled-adjacent copy. `.muted()` text extension reads `mutedForeground`. |
|
||||
| `destructive` | Delete/discard/record actions. Used sparingly — 3 call sites total in Arcs & Angles. |
|
||||
| `border` | Generic 1px outline — text field default border colour, general dividing lines. |
|
||||
| `divider` | A line *between* things in a layout (menu separators, section dividers) — conceptually different from `border` even though schemes often set them equal. |
|
||||
| `ring` | Keyboard-focus outline. Not the canvas selection ring — see `canvasSelectionRing`. |
|
||||
| `chart1`-`chart5` | Reserved for data visualisation, unused by the UI kit itself. |
|
||||
|
||||
### App-chrome slots (the part shadcn never had)
|
||||
|
||||
| Token | What it's for |
|
||||
|---|---|
|
||||
| `chrome` | Header/footer/menu-bar background. Deliberately nudged off `background` so it reads as *app furniture*, not content. Read via the unaccented scheme (see above) — chrome shouldn't shift when the user picks an accent. `ChromeBar` and `GarageShell` apply this for you; you'd only reach for it by hand building a header from scratch (see `blender_header.dart`, `scene_stats_hud.dart`). |
|
||||
| `panel` / `panelBorder` / `panelBorderHighlighted` | `Panel`'s fill and border — resting vs. "this is the active/hovered one" (see `Panel(active: ...)`). This is the surface docked tool content lives in. |
|
||||
| `input` / `inputBackground` / `inputBackgroundHovered` / `inputBackgroundFocused` / `inputBorder` | Text field fill (three interaction states) and border. |
|
||||
| `explorerRowEven` / `explorerRowOdd` / `explorerRowHovered` / `explorerRowText` | Zebra-striped tree/list rows (explorer panel, layer lists). |
|
||||
| `menuItemText` / `menuItemHovered` | Dropdown/menu row text and hover fill. |
|
||||
| `popoverItemHovered` | Hover fill for popover rows that aren't menu items (kept distinct from `menuItemHovered` while the two are still being evaluated — may merge later). |
|
||||
| `tooltipBackground` / `tooltipBorder` | Tooltips get their own pair rather than reusing `popover*` — they sit on top of *everything* and want more contrast than a panel-level surface. |
|
||||
| `propertiesSectionBackground` / `propertiesSectionBorder` / `propertiesSectionLabel` | The boxed sub-sections inside an object-properties panel. |
|
||||
| `canvasBackdrop` / `canvasPaper` / `canvasGridMinor` / `canvasGridMajor` / `canvasGridMajorDot` / `canvasBoundary` | Authored canvas colours — not derived at paint time, these are picked by hand per scheme like everything else. App-specific (Arcs & Angles' map canvas); a non-canvas app can mostly ignore this group. |
|
||||
| `canvasAlignmentGuide` | Smart-guide lines while dragging/resizing. Deliberately its own slot, not `ring` — `ring` is keyboard focus, a different job. |
|
||||
| `canvasSelectionRing` | Selection outline/handles. Tracks `primary` for most schemes but exists as its own slot so an achromatic scheme (carbon: near-black/near-white primary) can still give selection actual hue. If you add `withAccent()` support anywhere, remember `canvasSelectionRing` needs the same brightened-for-visibility treatment `_brightenForSelectionRing` gives it, or an accent override leaves the selection ring the one thing on screen still showing the old colour. |
|
||||
|
||||
### Authoring a new scheme
|
||||
|
||||
Look at `settings_state.dart` in the host app, not the package — that's where
|
||||
the actual eleven-scheme palette lives (the package only defines the *type*
|
||||
and the neutral fallback used in tests/demos). Each scheme is grouped under
|
||||
`// ── Accounted for ──` vs `// ── not reviewed yet ──` comments — that's a
|
||||
live audit trail, not decoration; keep using it as new tokens get added so
|
||||
it's visible which colours were deliberately chosen vs. still riding an old
|
||||
default.
|
||||
|
||||
## Density & spacing
|
||||
|
||||
`Density` (`theme/theme_data.dart`) is the **only** place pixel heights get
|
||||
hand-set. Everything else — icon size, padding, line-box height — is a
|
||||
derived getter. The class doc lays out the chain in full; the short version:
|
||||
|
||||
- You set `controlHeight` (23 compact / 27 normal), `fontSize` (10, both
|
||||
densities), `lineHeight` (1.1, both densities).
|
||||
- `lineBox = (fontSize * lineHeight).roundToDouble()` falls out of those.
|
||||
- `iconSize = lineBox` — a control icon always matches the text beside it.
|
||||
- `controlPaddingY = (controlHeight - lineBox) / 2` — padding is *derived
|
||||
from* the height target, never the other way round.
|
||||
|
||||
**Never hand-type a control height or a vertical padding.** If a number needs
|
||||
tuning, it belongs in `Density`, not at the call site — that's exactly the
|
||||
mistake the class doc says produced four different control heights and a
|
||||
stray `Transform.translate` before this system existed.
|
||||
|
||||
Two densities are in active use: `ButtonDensity.compact` (54 call sites) is
|
||||
the default for tool chrome — toolbars, menus, panels. `ButtonDensity.normal`
|
||||
(5 call sites) shows up for things meant to feel less cramped — e.g. the
|
||||
`Button.primary` "confirm this row" pattern in `blender_properties_section.dart`
|
||||
via `ButtonDensity.fromTheme(theme)`. Default to compact unless you have a
|
||||
specific reason not to.
|
||||
|
||||
Two spacing scales, don't mix them up:
|
||||
- `controlGap` — INSIDE a control (icon-to-label gap). Tighter, on purpose.
|
||||
It's also the base unit the gap scale below is derived from.
|
||||
- `containerGap` / `containerPadding` — between controls in a panel/popover/
|
||||
dialog, and the padding inside one.
|
||||
|
||||
### The gap scale
|
||||
|
||||
Layout spacing between widgets comes off `theme.density`, not a literal:
|
||||
|
||||
| Step | compact | normal | Use it for |
|
||||
|---|---|---|---|
|
||||
| `gapXxs` | 2 | 3 | hairline — a label sat directly above its value |
|
||||
| `gapXs` | 4 | 5 | tight — icon-adjacent, or items reading as one unit |
|
||||
| `gapSm` | 6 | 8 | snug |
|
||||
| `gapMd` | 8 | 10 | **the default** — related but distinct |
|
||||
| `gapLg` | 12 | 15 | section-level, within a panel or form |
|
||||
| `gapXl` | 16 | 20 | between major blocks |
|
||||
| `gapXxl` | 24 | 30 | page-level |
|
||||
Don't hand-author 37 colours. `GarageSchemes` ships `dark`, `light` and
|
||||
`carbon` (the one the Garage apps run), and `ColourScheme.derive` builds a
|
||||
complete scheme from four:
|
||||
|
||||
```dart
|
||||
const Gap.md(), // <- this, not Gap(8)
|
||||
```
|
||||
|
||||
`Gap` has a named constructor per step. They resolve their extent from the
|
||||
theme at build time, so a call site stays `const` and still tracks the density
|
||||
- no `GarageTheme.of(context)` needed in a build method just because it
|
||||
contains a gap. `Gap(n)` with a literal still works and still wins, for the
|
||||
rare thing that genuinely isn't on the scale.
|
||||
|
||||
When in doubt, reach for `gapMd`. It's the same 8px `Gap(8)` was, so the old
|
||||
advice hasn't changed — it just has a name now, and it moves when the density
|
||||
does instead of staying 8 forever.
|
||||
|
||||
`gapMd` equals `containerGap` and `gapXl` equals `containerPadding` at both
|
||||
densities. That's not arranged, it's what those two were already set to, which
|
||||
is the evidence `controlGap` is the right base unit —
|
||||
`test/density_gap_scale_test.dart` holds it to that.
|
||||
|
||||
**Reaching for a literal is now the exception, not the rule.** Before the
|
||||
scale existed the two Garage web frontends had drifted to sixteen distinct gap
|
||||
values between them, including a 3, a 5 and ten 14s. If a step doesn't fit,
|
||||
that's worth a conversation about the scale rather than a one-off number.
|
||||
|
||||
## Buttons — variant semantics
|
||||
|
||||
`Button` and `IconButton` both expose five named constructors:
|
||||
`.primary` / `.secondary` / `.outline` / `.ghost` / `.destructive`. Real usage
|
||||
across both apps settles into a clear pattern — this is the single most
|
||||
useful thing in this document:
|
||||
|
||||
| Variant | Meaning | Evidence |
|
||||
|---|---|---|
|
||||
| **ghost** | Default, lowest-emphasis action. Toolbar icons, dialog close (X) buttons, settings-cog buttons. Most common `IconButton` variant by a wide margin (15 sites). | `pane_dialog.dart` close button, `blender_editor.dart` header icons |
|
||||
| **outline** | Second most common (`Button.outline`: 14 sites). The "resting/inactive" half of a toggle pair, AND the standard "Cancel"/dismissive action in a dialog action row. | `export_dialog_widgets.dart`: `Button.outline(onPressed: onCancel, child: Text("Cancel"))` |
|
||||
| **secondary** | Neutral filled control — NOT a toggle's resting state (that's outline), more like "has a job but isn't the emphasised one." An icon button glued onto a text field (browse/file-picker button in a `ButtonGroup`) — see *Properties* below; the field goes `TextFieldVariant.secondary` to match, and reaching for `outline` here is the usual slip. Also used as a toggle's resting state in a couple of places (Music Maker's transport controls) — outline and secondary are somewhat interchangeable for "not active right now," pick whichever reads better against the surrounding controls. | `export_dialog_widgets.dart` browse button, Music Maker transport |
|
||||
| **primary** | The accent colour. Two jobs: (1) the *engaged* half of a toggle-button pair — `condition ? primary : outline` is the standard toggle idiom, used repeatedly (`_SnapPopoverButton`, `draw_panel.dart` eyedropper, Music Maker play/pause); (2) the single confirm/CTA action in a dialog, almost always via the `PrimaryButton` shorthand rather than `Button.primary` directly. | `_open ? IconButton.primary(...) : IconButton.outline(...)` |
|
||||
| **destructive** | Reserved for genuinely dangerous/irreversible actions — delete, record. Rare on purpose (3 sites total). Don't reach for it just because something is "important." | Music Maker's record toggle: `recording ? IconButton.destructive(...) : IconButton.secondary(...)` |
|
||||
|
||||
**The toggle-button recipe** (this exact shape appears in every editor):
|
||||
|
||||
```dart
|
||||
active
|
||||
? IconButton.primary(
|
||||
density: ButtonDensity.compact,
|
||||
icon: Icon(LucideIcons.some_icon).iconSmall,
|
||||
onPressed: onToggle,
|
||||
)
|
||||
: IconButton.outline(
|
||||
density: ButtonDensity.compact,
|
||||
icon: Icon(LucideIcons.some_icon).iconSmall,
|
||||
onPressed: onToggle,
|
||||
)
|
||||
```
|
||||
|
||||
**The dialog action-row recipe** (`export_dialog_widgets.dart`, verbatim shape):
|
||||
|
||||
```dart
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Button.outline(onPressed: onCancel, child: const Text("Cancel")),
|
||||
const Gap(8),
|
||||
PrimaryButton(onPressed: enabled ? onConfirm : null, child: Text(confirmLabel)),
|
||||
],
|
||||
ColourScheme.derive(
|
||||
brightness: Brightness.dark,
|
||||
background: ...,
|
||||
foreground: ...,
|
||||
primary: ...,
|
||||
)
|
||||
```
|
||||
|
||||
There are also bare `PrimaryButton` / `SecondaryButton` / `OutlineButton` /
|
||||
`GhostButton` / `DestructiveButton` widgets (no `.constructor` dot-syntax) —
|
||||
lighter-weight wrappers around the same variants. `PrimaryButton` specifically
|
||||
is the idiomatic way to write a dialog's confirm button, over `Button.primary`.
|
||||
Every other slot is derived on a perceptual lightness ladder — surfaces step
|
||||
up or down from the background, strokes are measured off the surface they
|
||||
outline, text is a fraction of the background-to-foreground span. Any derived
|
||||
slot can be pinned by passing it. `schemes.dart` is the worked example.
|
||||
|
||||
A user-picked accent goes through `scheme.withAccent(colour)`, which swaps
|
||||
`primary`, `primaryHovered`, `ring` and a contrasting `primaryForeground`, and
|
||||
nothing else.
|
||||
|
||||
### Density: pick the tier, don't tune it
|
||||
|
||||
| | `compact` | `normal` | `product` |
|
||||
|---|---|---|---|
|
||||
| For | dense desktop tools | the same, roomier | web / product apps |
|
||||
| Used by | Arcs & Angles | A&A "Comfortable" | the hub |
|
||||
| `fontSize` | 10 | 10 | 12 |
|
||||
| `controlHeight` | 23 | 27 | 33 |
|
||||
| `chromeBarHeight` | 31 | 37 | 43 |
|
||||
|
||||
Use the named constructor. `Density()` is the same as `Density.compact()`,
|
||||
which is a trap for a product app.
|
||||
|
||||
`Density`'s fields are the only hand-set pixel values in the system; padding,
|
||||
icon size and line boxes are derived from them. If a tier feels wrong, that's
|
||||
a conversation about the tier, not a reason to set sizes at call sites.
|
||||
|
||||
Controls follow the theme's tier. A per-control `density:` override exists for
|
||||
exactly one job — a header bar that has to match a 23px menu bar — and even
|
||||
then it mixes tiers (compact geometry, the theme's font). Don't use it to make
|
||||
something smaller.
|
||||
|
||||
|
||||
## Colour: what the slots are for
|
||||
|
||||
You mostly wont touch these — the components read them. When you do draw
|
||||
something yourself, pick the slot by what the thing *is*.
|
||||
|
||||
| Slot | What it is |
|
||||
|---|---|
|
||||
| `background` / `foreground` | the page ground and default text. Also a `Panel`'s fill |
|
||||
| `chrome` | header and footer bars, and the gutter between panels |
|
||||
| `surfaceSunken` | one step below the ground — side rails, panel bodies, list backgrounds |
|
||||
| `card` | raised surface — cards, properties sections, dialogs |
|
||||
| `muted` / `mutedForeground` | de-emphasised fill / secondary text, hints, units |
|
||||
| `rowText` / `rowHovered` | a row's resting label colour / hovered row, in lists and menus |
|
||||
| `popover` / `popoverBorder` | menus, select popups, anything floating |
|
||||
| `tooltipBackground` / `tooltipBorder` | tooltips, which sit above everything |
|
||||
| `primary` (+`Hovered`, `Foreground`) | the accent. The one thing that's on or chosen |
|
||||
| `secondary` (+`Hovered`, `Foreground`) | a neutral filled control |
|
||||
| `destructive` | delete, revoke, discard |
|
||||
| `controlFill` (+`Hovered`, `Focused`), `controlBorder` | the shared fill and stroke of every control — fields, selects, outline buttons, checkboxes |
|
||||
| `switchTrackInactive` | a Switch while off |
|
||||
| `border` / `divider` | general outline / a seam between things |
|
||||
| `panelBorder` / `panelBorderHighlighted` | a Panel's edge, resting / the lit one |
|
||||
| `propertiesSectionBorder` | a properties section's edge |
|
||||
| `ring` | keyboard focus |
|
||||
| `popoverItemHovered` | hover inside select and date popups |
|
||||
| `chart1`–`chart5` | data visualisation |
|
||||
|
||||
There is no `panel`, `input*`, `explorerRow*`, `menuItem*` or `canvas*` slot any
|
||||
more. App-specific colours (A&A's canvas) belong in the app, derived with
|
||||
`shiftLstar` so they sit on the same ladder.
|
||||
|
||||
|
||||
## Space
|
||||
|
||||
- Between siblings: `Gap.xxs()` … `Gap.xxl()`. They resolve against the
|
||||
density at build time.
|
||||
- In padding: `density.gapXs` … `density.gapXxl`, `containerGap`,
|
||||
`containerPadding`.
|
||||
- Never a literal. `Gap(8)` is 8 in compact and wrong everywhere else.
|
||||
|
||||
| | `xxs` | `xs` | `sm` | `md` | `lg` | `xl` | `xxl` |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| compact | 2 | 4 | 6 | 8 | 12 | 16 | 24 |
|
||||
| normal, product | 3 | 5 | 8 | 10 | 15 | 20 | 30 |
|
||||
|
||||
A lot of the spacing is already in the components. `PropertiesSection` carries
|
||||
its own margin (`gapXs` either side, `gapLg` below), so a stack of sections
|
||||
needs no gaps between them. `SettingsList` insets its own rows. Don't add
|
||||
padding around things that already have it.
|
||||
|
||||
|
||||
## Type
|
||||
|
||||
- The ambient text is `fontSize` at w300. Plain `Text("...")` is already right
|
||||
for body copy.
|
||||
- Controls render their own labels at w400; primary buttons at w600 as optical
|
||||
compensation. Don't bold a button label.
|
||||
- Size steps are the extensions: `.xSmall()` (`textXs`), `.small()` (`textSm`),
|
||||
`.large()` (`textLg`). Weight: `.medium()`, `.semiBold()`, `.bold()`.
|
||||
Secondary text: `.muted()`.
|
||||
- The step below the control font, `textXxs`, has no extension; read
|
||||
`density.textXxs` when you need it (group captions).
|
||||
- Numbers and readouts: `theme.typography.monoStyle(...)`.
|
||||
|
||||
| Job | How |
|
||||
|---|---|
|
||||
| Page title | `Text(title).large()` |
|
||||
| Page / section subtitle | muted text under it |
|
||||
| Section heading inside a page | `.small().semiBold()` |
|
||||
| Nav group caption | `density.textXxs`, w600, `.muted()` |
|
||||
| Hint, unit, empty-state line | `.muted()` (hub list text: `.xSmall().muted()`) |
|
||||
| Footer stats, perf readouts | `monoStyle`, muted |
|
||||
|
||||
`.muted()` colours **text** only. It does nothing to an `Icon`.
|
||||
|
||||
|
||||
## Icons
|
||||
|
||||
- Source: `flutter_lucide`, re-exported through `garage_ui.dart` as
|
||||
`LucideIcons`. **Names are snake_case** (`LucideIcons.file_plus`,
|
||||
`LucideIcons.chevron_right`) — this is `flutter_lucide`'s native spelling,
|
||||
used directly. There is no camelCase shim in this package.
|
||||
- Three tiers: `.iconSmall` (`density.iconSize`), `.iconMedium` (20px),
|
||||
`.iconLarge` (24px). Set size via the extension, never a hardcoded `size:`.
|
||||
- **`Button` and `IconButton` already apply `small` to everything inside
|
||||
them** — `leading`, `trailing` and the child — so a bare `Icon` in a button
|
||||
is correctly sized and needs no extension. Reach for one only to *override*
|
||||
that. Outside a button the ambient default is `medium` (20px), which is
|
||||
usually too big for a control-adjacent icon; that's where a bare `Icon` does
|
||||
go wrong.
|
||||
- `small` is `1.1 × fontSize` — 13px at product, 11px at compact/normal — not
|
||||
the line box. It was the line box, which is tidy for layout (an icon-only
|
||||
control matches a text control's height for free) and wrong for the eye:
|
||||
lucide glyphs fill their box nearly edge to edge while a 12px font caps out
|
||||
around 8.5px, so a line-box icon read ~70% taller than the text next to it.
|
||||
Control height is unaffected either way — that comes from `controlHeight`.
|
||||
- Muted/de-emphasised icon: chain `.muted()` after the size extension —
|
||||
`Icon(LucideIcons.chevron_left).iconSmall.muted()`.
|
||||
- Lucide only: `LucideIcons.x`.
|
||||
- Inside a control's `leading` / `trailing` / `icon` slot: a bare
|
||||
`const Icon(LucideIcons.x)`. The control sizes and colours it — the variant's
|
||||
icon theme is merged over everything inside the button.
|
||||
- Outside a control, `.iconSmall` / `.iconMedium` / `.iconLarge` set size only
|
||||
(never colour). `iconSmall` equals `fontSize`: 10 compact, 12 product.
|
||||
- No literal `size: 13`.
|
||||
|
||||
## Typography
|
||||
|
||||
`theme.typography` exposes `normal` / `medium` (weight-only styles) and
|
||||
`small` (the shared control font — size and line-height come from `Density`,
|
||||
never set this by hand). Two font-family helpers:
|
||||
## Surfaces and layout
|
||||
|
||||
- `theme.typography.sansStyle(style)` — Geist, the UI's default face.
|
||||
- `theme.typography.monoStyle(style)` — Geist Mono. Reach for this
|
||||
specifically for **numeric readouts**: perf stats, transport time,
|
||||
coordinates — anywhere the content is a number that benefits from
|
||||
fixed-width digits. Not for general UI text.
|
||||
Both apps are the same shape: a `chrome` ground, `Panel`s standing on it, and
|
||||
`panelGap` gutters between them.
|
||||
|
||||
Text-widget shorthands (`Widget` extensions, merge over ambient
|
||||
`DefaultTextStyle`): `.xSmall()` / `.small()` / `.large()`, `.medium()` /
|
||||
`.semiBold()` / `.bold()`, `.muted()`.
|
||||
|
||||
### The text ladder
|
||||
|
||||
The three size shorthands come off `density`, not literals:
|
||||
|
||||
| Shorthand | Token | compact / normal | Use it for |
|
||||
|---|---|---|---|
|
||||
| `.xSmall()` | `textXs` | 11 | dialog copy, headings, hints — and the ambient body size |
|
||||
| `.small()` | `textSm` | 14 | body copy a step above the controls |
|
||||
| `.large()` | `textLg` | 18 | headings |
|
||||
|
||||
`textXs` sits one step above `density.fontSize` — 11 against a 10px control
|
||||
font — and is also what `GarageTheme` sets as the ambient body size, so page
|
||||
copy and dialog copy are the same size by construction.
|
||||
|
||||
It was `x1.2` (12) for a while, which put every dialog title and page heading a
|
||||
fifth above the controls beneath them. That read as oversized rather than as
|
||||
hierarchy — weight and colour carry the emphasis, not size.
|
||||
|
||||
What was wrong before is that the two were *unlinked*: these were hardcoded
|
||||
`12`/`14`/`18` multiplied by `scaling`, while the control font comes off
|
||||
`Density` and is deliberately not scaled. The intended 12-vs-10 held at scaling
|
||||
1.0 and drifted to 14.4-vs-10 at 1.2 — the ratio moved with scaling. Deriving
|
||||
them pins it, and makes `density.fontSize` the single knob that moves every
|
||||
piece of app text together.
|
||||
|
||||
`scaling` is deliberately **not** applied to these. It still moves the
|
||||
medium/large icon tiers, border widths and `GarageTheme`'s own
|
||||
`DefaultTextStyle` — but text that has to line up with a control cannot be on
|
||||
a different axis from the control.
|
||||
|
||||
## Structural widgets
|
||||
|
||||
- **`Panel`** — the bordered, rounded surface for docked tool content. Two
|
||||
modes: standalone (tracks its own hover) or controlled (`active: bool`,
|
||||
parent drives it — used when multiple panels need to be mutually exclusive,
|
||||
e.g. only one lit at a time in a Blender-style three-pane layout). Border
|
||||
goes from `panelBorder` to `panelBorderHighlighted` when active/hovered.
|
||||
- **`ChromeBar`** — the shared treatment for a header/footer strip: fixed
|
||||
height, `chrome` background, padding. Both the header and footer in an
|
||||
editor should be built from this rather than a raw `Container`, so they
|
||||
stay pixel-identical in height (`kChromeBarHeight`, shared constant).
|
||||
- **`EditorShell`** — header / center / optional left+right docks / footer,
|
||||
stacked as one frame. The outermost layout of a whole editor screen.
|
||||
- **`GarageShell`** — main content + two stacked sidebar panes with a
|
||||
draggable resize handle between main and sidebar, plus centralised hover
|
||||
(only one of the three panes lit at once). This is the Blender-style
|
||||
three-pane editor shell, factored out so it isn't hand-rolled per app.
|
||||
- **`ButtonGroup`** — lays controls out in a row (or column) and zeroes the
|
||||
corners where they touch, so they read as one connected control rather than
|
||||
two things that happen to be adjacent. A field with a button welded to its
|
||||
end (password + edit, path + browse, input + unit) is a `ButtonGroup`, not a
|
||||
`Row` with a `Gap` in it. Nested groups merge rather than shadow — an inner
|
||||
group can be told to drop both its top and its start edge, which is how a
|
||||
stacked field/eye/button cluster avoids a doubled stroke down the seam.
|
||||
|
||||
Watch `expands`. It defaults to `false`, which wraps the flex in an
|
||||
`IntrinsicHeight`; the group stretches its children on the cross axis, so
|
||||
without that wrapper it needs a bounded height from its parent and throws
|
||||
`BoxConstraints forces an infinite height` when it doesn't get one. Only
|
||||
pass `expands: true` when the parent already gives it a height.
|
||||
|
||||
- **`Card`** vs **`SurfaceCard`** — `Card` always draws its own
|
||||
`OutlinedContainer` (border + fill). `SurfaceCard` additionally understands
|
||||
sheet-overlay context: inside a sheet it collapses to just padding, because
|
||||
the sheet is already the surface and a nested card would double up the
|
||||
border. Default to `Card` unless the content might end up inside a sheet.
|
||||
- **`OutlinedContainer`** — the base primitive `Card`/`Panel` build on:
|
||||
border + radius + optional shadow. Reach for it directly for one-off
|
||||
floating chrome that isn't quite a card — e.g. a mobile slide-in panel:
|
||||
|
||||
```dart
|
||||
OutlinedContainer(
|
||||
borderColor: GarageTheme.of(context).colorScheme.border,
|
||||
boxShadow: [
|
||||
BoxShadow(color: const Color(0xff000000).withValues(alpha: 0.15), blurRadius: 4, spreadRadius: 2),
|
||||
],
|
||||
child: ...,
|
||||
)
|
||||
```
|
||||
|
||||
A bigger, "floating well above the app" shadow (splash/welcome overlay)
|
||||
goes heavier and uses negative spread to keep the blur from reading as a
|
||||
hard edge: `blurRadius: 40, spreadRadius: -8, offset: Offset(0, 20)`, alpha
|
||||
`0.4`. Scale shadow weight to how far off the page the thing is meant to
|
||||
read as floating — a docked panel border shadow and a modal-over-everything
|
||||
shadow should not look like the same intensity.
|
||||
|
||||
## Properties — the settings-row system
|
||||
|
||||
Any screen that is a list of *things you can change* is built from
|
||||
`PropertiesSection` + `PropertyRow`. This covers settings panes, inspectors,
|
||||
and account screens. Do not assemble one out of `Column` + `Text` + a
|
||||
divider — the split alignment, the collapse behaviour, the actions band and
|
||||
the row minimum height are all in here already.
|
||||
|
||||
- **`PropertiesSection`** — a titled, collapsible block: `title`, `subtitle`,
|
||||
`rows`, an optional `actions` band along the bottom for section-level
|
||||
buttons (Save / Reset / Refresh), and `collapsed` + `onToggle` driven by the
|
||||
parent so several sections can be remembered independently.
|
||||
- **`PropertyRow`** — one setting. `label` on the left of the split, `child`
|
||||
(the control) on the right. `split` is the fraction of the width sitting
|
||||
left of that line, so every row in a section lines its controls up at the
|
||||
same x. `labelless: true` for a row that has no name of its own.
|
||||
|
||||
### `subtitle` vs `description` — the one that gets got wrong
|
||||
|
||||
Both are muted second lines. They are not interchangeable, and picking the
|
||||
wrong one is the single most common mistake against this component:
|
||||
|
||||
| | Where it renders | What it's for |
|
||||
|---|---|---|
|
||||
| **`subtitle`** | Inside the **label column**, under the label, holding the same right-alignment against the split | *Naming the value.* "Last used 3d ago", "Never used", "2 permissions · last used 5m ago" — text that says **which** row this is |
|
||||
| **`description`** | **Full width** under the whole row, spanning label *and* control | *Explaining the setting.* Consequences, caveats, what changes when you change it — "Permanently remove this account… This cannot be undone." |
|
||||
|
||||
The test: does the sentence identify **this particular item** (subtitle), or
|
||||
does it explain **what the control does** (description)? A list of five
|
||||
passkeys wants five subtitles, not five full-width paragraphs.
|
||||
|
||||
**Never put explanatory copy in `child`.** It is the single failure mode this
|
||||
component has. A paragraph in the control column shares a cell with the
|
||||
control, so the copy wraps to three lines, the button gets squeezed against
|
||||
the right edge, and the section's split alignment stops meaning anything
|
||||
because every row's control now starts somewhere different. `child` is for
|
||||
the control. Copy goes in `description`.
|
||||
|
||||
```dart
|
||||
// WRONG - copy competing with the control for the same column
|
||||
PropertyRow(
|
||||
label: "Delete account",
|
||||
scheme: scheme,
|
||||
child: Row(children: [
|
||||
Expanded(child: Text("Permanently remove this account…").muted()),
|
||||
const Gap.md(),
|
||||
Button.destructive(onPressed: onDelete, child: const Text("Delete…")),
|
||||
]),
|
||||
)
|
||||
|
||||
// RIGHT - the slot that already exists for it
|
||||
PropertyRow(
|
||||
label: "Delete account",
|
||||
scheme: scheme,
|
||||
description: "Permanently remove this account, all sign-in methods, and "
|
||||
"any OAuth grants. This cannot be undone.",
|
||||
child: Button.destructive(onPressed: onDelete, child: const Text("Delete…")),
|
||||
)
|
||||
```
|
||||
ColoredBox(chrome)
|
||||
Padding(panelGap)
|
||||
Row[ Panel(rail), SizedBox(width: panelGap), Expanded(Panel(workspace)) ]
|
||||
```
|
||||
|
||||
A row that is *only* explanation and has no control at all is still a
|
||||
`PropertyRow` — give it the `description` and pass `const SizedBox.shrink()`
|
||||
as the child.
|
||||
A&A gets this from `GarageShell` (header, footer, main, a sidebar split top and
|
||||
bottom). The hub builds the same shape by hand because it needs a nav rail.
|
||||
Only one panel is lit at a time (`panelBorderHighlighted`).
|
||||
|
||||
### Fields inside a property row
|
||||
- **`ChromeBar`** for a header or footer — it's `chromeBarHeight` tall on
|
||||
`chrome`.
|
||||
- **`Panel`** for a docked region. Flat, `background` fill, 1.15 border.
|
||||
- **`PanelHeader(icon, title, scheme, trailing:)`** at the top of a panel body.
|
||||
A&A's panel pages are all `ColoredBox(surfaceSunken)` →
|
||||
`PanelHeader(bottomPadding: 0)` → `ScrollEdgeFade(ListView(...))`.
|
||||
- **`Card` / `SurfaceCard`** for a raised block (`card` fill, `radiusXl`).
|
||||
- **`OutlinedContainer`** for a bordered box that isn't a card.
|
||||
- **Flat.** No shadows on anything — the border and the surface step are what
|
||||
separate things. The package's sheet, menu and popup surfaces are all
|
||||
shadowless.
|
||||
|
||||
`TextField` takes a `variant`: `TextFieldVariant.outline` (default) or
|
||||
`.secondary`. Inside a properties pane, prefer `.secondary` — it matches the
|
||||
filled treatment the surrounding controls use, and it pairs with
|
||||
`ButtonStyle.secondary()` when a button is welded to the field in a
|
||||
`ButtonGroup`. An `outline` field next to a `secondary` button (or the
|
||||
reverse) reads as two controls from different screens.
|
||||
|
||||
A value the user is not allowed to edit — a password, a verified email — is
|
||||
still a field: `readOnly: true, enabled: false` with a stand-in value, not a
|
||||
bare `Text` floating in the column. It keeps the row's geometry and tells the
|
||||
reader "this is a value that lives here" rather than "this is a caption."
|
||||
## Buttons
|
||||
|
||||
## The menu system — one model, two renderers
|
||||
### Variants mean something
|
||||
|
||||
This is the pattern from the `AppMenuGroup` model in `app_menu.dart`, and
|
||||
it's worth calling out on its own because it's easy to accidentally
|
||||
reinvent per-app (it has been, twice):
|
||||
| Variant | Means |
|
||||
|---|---|
|
||||
| `primary` | the one action, or the thing that's on / chosen. One per view, ideally |
|
||||
| `secondary` | a neutral filled control — a resting toggle, a button welded to a field, the selected item in a nav |
|
||||
| `outline` | a normal action that isn't the main one — Cancel, Retry, Previous / Next |
|
||||
| `ghost` | chrome, toolbars, nav items, row actions — anything that shouldn't compete |
|
||||
| `destructive` | the confirm button of a destructive dialog |
|
||||
| `ghostDestructive` | a destructive *trigger* that isnt a filled slab — Remove, Revoke, Delete in a row |
|
||||
| `link` / `text` | inline, in running copy |
|
||||
|
||||
Define the menu once as data — `List<AppMenuGroup>` (`AppMenuGroup` →
|
||||
`AppMenuAction` / `AppMenuCheck` / `AppMenuSeparator`) — not as widgets. Then:
|
||||
`Button.primary(...)`, `Button.ghostDestructive(...)` etc. take a `style:`;
|
||||
the `PrimaryButton` / `OutlineButton` / … wrappers take `density:` directly.
|
||||
|
||||
1. **In-app render**: walk the model into `Menubar`/`MenuButton`/
|
||||
`MenuCheckbox`/`MenuDivider` widgets (see `_toMenuItem`/`_menuItemsFor` in
|
||||
either app's menu code for the translation).
|
||||
2. **Native macOS bar**: `AppMenuNativeRenderer.build(menus, appName: ...)`
|
||||
pushed into an `AppMenuNotifier` sitting above the app's `Router`, via
|
||||
`PlatformMenuHost`.
|
||||
### By context
|
||||
|
||||
Both renderers must consume the **same** computed `menus` value from the
|
||||
**same** build pass — not two independent calls to whatever builds the model.
|
||||
Two separate computations drift: they'll watch slightly different state,
|
||||
recompute at different times, and eventually disagree about what's checked or
|
||||
what a shortcut is (this exact bug shipped and had to be fixed). Compute
|
||||
once, hand the value to both. Gate the native push on
|
||||
`AppMenuNativeRenderer.signature(menus)`, not the list itself — building a
|
||||
fresh `AppMenuGroup` tree (and fresh `SingleActivator`s inside it) on every
|
||||
build is normal and fine, but pushing to the native bar on every build is not
|
||||
— the signature is what turns "recomputed every frame" into "pushed only
|
||||
when it actually changed."
|
||||
| Where | What |
|
||||
|---|---|
|
||||
| Icon only, anywhere | `IconButton.<variant>`, never a `Button` with just an icon in it |
|
||||
| Panel header actions | `IconButton.ghost`, each in a `Tooltip` |
|
||||
| Nav rail item | `ButtonStyle.ghost`, `secondary` when selected (never primary), `alignment: centerLeft`, `leading: Icon` |
|
||||
| Toolbar / header toggle | `IconButton.primary` when on, `outline` when off (`secondary` off for a master switch whose off state matters) |
|
||||
| Segmented choice | `ButtonGroup.horizontal`, `Expanded` children, chosen one `primary`, rest `secondary`, `alignment: center` |
|
||||
| Button welded to a field | `ButtonGroup.horizontal[field, IconButton.secondary]` |
|
||||
| Action inside a PropertyRow | `ButtonStyle.secondary` (the row doesnt force buttons, you pick it) |
|
||||
| Action in a SettingsRow | ghost |
|
||||
| Dialog | Cancel `outline`, confirm `Button.primary` or `Button.destructive` |
|
||||
| Retry after an error | `outline` |
|
||||
| Empty-state call to action | `outline`, centred |
|
||||
| Full-width stacked flow buttons (auth) | primary last, every one `alignment: Alignment.center` |
|
||||
|
||||
## Anti-patterns
|
||||
### Alignment
|
||||
|
||||
- **No `package:flutter/material.dart`.** Not even for `Colors.black`. Use
|
||||
`Color(0xff000000)` / `Color(0xffffffff)` — literal, not `Colors.*`. The UI
|
||||
kit is deliberately `WidgetsApp`-based, not `MaterialApp`-based, so an app
|
||||
built on it shouldn't reach for Material either.
|
||||
- **Don't hand-type a control height, icon size, or vertical padding.** It
|
||||
belongs in `Density` as a named field with everything else derived from it.
|
||||
- **Don't build `chrome`/`panel` treatment from raw `Container` + hardcoded
|
||||
colour.** Use `ChromeBar`/`Panel`/`EditorShell`/`GarageShell` — they read
|
||||
the right token (and in chrome's case, the right *unaccented* scheme) for
|
||||
you.
|
||||
- **Don't hand-roll a settings row.** `PropertiesSection` + `PropertyRow`
|
||||
exist, and a `Column` of `Text` + control + divider will silently lose the
|
||||
split alignment, the collapse state and the actions band. If you find
|
||||
yourself writing `Row(children: [Expanded(Text(...)), Gap, Button])` inside
|
||||
a settings pane, you want `description:` instead.
|
||||
- **Don't glue a button to a field with a `Gap`.** That's a `ButtonGroup` —
|
||||
it merges the touching corners so the pair reads as one control.
|
||||
- **Don't build a menu as widgets directly.** Model it as `AppMenuGroup` data
|
||||
first (see above), even if there's currently only one renderer consuming
|
||||
it — a native menu bar tends to get added later, and retrofitting a model
|
||||
under existing widget-only menu code is exactly the refactor that
|
||||
motivated this document.
|
||||
- **Don't reach for `destructive` for "this matters."** It means
|
||||
delete/discard/record — genuinely dangerous, genuinely irreversible.
|
||||
Leave `alignment:` off unless the button is stretched wider than its label.
|
||||
Then:
|
||||
|
||||
- `Alignment.center` for full-width buttons and segments.
|
||||
- `Alignment.centerLeft` only for nav and list rows, and only with a
|
||||
**leading** icon.
|
||||
|
||||
Never `centerLeft` with a trailing-only icon. When alignment is set and exactly
|
||||
one of leading / trailing is present, `Button` puts an invisible spacer on the
|
||||
empty side so the label lands on the true centre — with `centerLeft` that just
|
||||
indents the label behind a phantom icon.
|
||||
|
||||
### Toggles
|
||||
|
||||
`Toggle` is the package's on/off button: ghost while off, secondary while on.
|
||||
Editor toolbars use the primary-on pattern above instead. Pick one per surface.
|
||||
|
||||
|
||||
## Inputs
|
||||
|
||||
### Text fields
|
||||
|
||||
- **Default (`outline`)** for free text: names, search, a composer, a form.
|
||||
- **`secondary`** for numeric property values and read-only values.
|
||||
- Inside a `PropertyRow` you don't choose — the row wraps its child in a
|
||||
`PropertySlotScope` and `TextField`, `Select` and `DateInput` are forced to
|
||||
secondary. An explicit `variant:` loses. (Buttons aren't forced.)
|
||||
- Numeric editor fields (A&A): secondary, `textAlign: center`,
|
||||
`InputFeature.scrub(...)`, a unit as a trailing muted `Text`. Put trailing
|
||||
features before `scrub` — list order is render order.
|
||||
- A read-only value is a `TextField(readOnly: true, enabled: false)`, never a
|
||||
bare `Text` (see below).
|
||||
|
||||
### Select
|
||||
|
||||
`SelectVariant.secondary` in property rows (forced anyway), `ghost` in a
|
||||
`SettingsRow` or where the select reports a value rather than offering a
|
||||
control, default `outline` elsewhere.
|
||||
|
||||
### Booleans
|
||||
|
||||
A&A uses `Checkbox` throughout. The hub uses `Switch` in rows. Either is
|
||||
fine; don't mix them within one surface.
|
||||
|
||||
### Errors
|
||||
|
||||
Use the row's `error:` slot (`PropertyRow` and `SettingsRow` both have one).
|
||||
It reddens the outline, never the fill. Don't hand-roll a red `Text` under a
|
||||
field. "Required" goes in the row's `action:` slot.
|
||||
|
||||
|
||||
## Properties and settings
|
||||
|
||||
The two list shapes, and the hub's two page archetypes.
|
||||
|
||||
**`PropertiesSection` + `PropertyRow`** — a boxed card of label/value rows. For
|
||||
inspectors, account and security pages, and lists of records (one collapsible
|
||||
section per record, then the hub's `Pager`).
|
||||
|
||||
- The value slot is **always a field or a control**, never bare `Text`. A
|
||||
read-only value is a secondary `TextField(readOnly: true, enabled: false)` —
|
||||
the hub wraps that as `ValueField`. Beside real fields, bare text reads as a
|
||||
caption and the column goes ragged.
|
||||
- A button in the value slot goes in `Align(centerLeft)` so it doesn't stretch.
|
||||
- Controls here are secondary.
|
||||
- Editor rows carry `PropertyActions` (right-click reset / copy / paste).
|
||||
- A status badge or count goes in the section's `trailing`, which stays
|
||||
visible when collapsed.
|
||||
- Actions go in the section's `actions:` band.
|
||||
|
||||
**`SettingsList` + `SettingsRow`** — unboxed rows ruled between entries, on
|
||||
the page ground. For forms: products, coupons, clients.
|
||||
|
||||
- There's no card, so filled controls would float. Selects, buttons and icon
|
||||
buttons are **ghost**; text fields are the default **outline** — read-only
|
||||
ones too.
|
||||
|
||||
Converting one to the other means changing the variants, not just the
|
||||
container.
|
||||
|
||||
### Subtitle vs description
|
||||
|
||||
- `subtitle` is a short **static** line saying what the thing is. Never live
|
||||
data: no counts, dates, names, emails, statuses or "3 of 5". This holds
|
||||
everywhere — page headings, section headings, rows, and record cards in a
|
||||
list. A record's identifying data goes in its rows.
|
||||
- `description` is longer copy about a setting.
|
||||
|
||||
|
||||
## Dialogs, sheets, panes, overlays
|
||||
|
||||
- **Confirm / small dialog:** `showDialog` → `AlertDialog(title, content,
|
||||
actions: [cancel, confirm])`. It's `card`-filled, capped at
|
||||
`kDialogMaxWidth` (350) and renders the actions in its own band. Don't
|
||||
constrain its width yourself, and don't build the action row by hand.
|
||||
- **Bottom sheet:** `showSheet(...)` with `SheetRow`s.
|
||||
- **Big pane** (settings, export — A&A): `showPaneOverlay`, blurred scrim,
|
||||
inside the same chrome → Panel → gutter shape as the editor.
|
||||
- **Toasts:** `showAppToast(context: context, title: ..., subtitle: ..., severity: ...)`.
|
||||
- **Tooltips:** `Tooltip(tooltip: (_) => TooltipContainer(child: Text(msg)))`,
|
||||
unstyled.
|
||||
- **Menus:** `MenuButton`, `MenuDivider`, `MenuLabel`; `showContextMenu` for
|
||||
right-click; app menus as `AppMenuItem` data.
|
||||
|
||||
|
||||
## Loading, empty, error
|
||||
|
||||
- **Loading:** a `CircularProgressIndicator`. No skeletons, no shimmer.
|
||||
- **Empty:** one muted line.
|
||||
- **Error:** the message muted, and an outline Retry.
|
||||
|
||||
Plainest thing that works. If a page wants more, it's asking for a new garage_ui
|
||||
component, not a one-off.
|
||||
|
||||
|
||||
## Dont copy these
|
||||
|
||||
Known overrides in the reference apps. They're debt, not patterns.
|
||||
|
||||
**Arcs & Angles**
|
||||
|
||||
- `IconButton.ghost(icon: Icon(x, size: 13))` in panel headers, the tab strip
|
||||
and the agent composer — should be a bare `Icon`.
|
||||
- Tooltip text forced to `foreground` or `background`, custom
|
||||
`TooltipContainer` padding.
|
||||
- Literal font sizes in the agent panel and the download modal.
|
||||
- The hand-rolled snap popover, `PaneDialog` title bar and properties tab
|
||||
strip.
|
||||
- Plain `Text` values in settings and download `PropertyRow`s.
|
||||
- Literal `SizedBox` / `EdgeInsets` spacing outside the settings modal.
|
||||
|
||||
**The hub**
|
||||
|
||||
- Ghost and link buttons hand-coloured `destructive` ("Remove", "Revoke",
|
||||
"Refund", "Delete?") — use `ButtonStyle.ghostDestructive`.
|
||||
- `TextStyle(fontWeight: w600)` on primary labels in the older auth steps.
|
||||
- Four competing badges (disabled compact buttons, disabled coloured
|
||||
`IconButton`s, `SandboxTag`, `Pill`), and hand-built warning boxes
|
||||
(`FlowError`, the delete-account box, `_Banner`).
|
||||
- `ConstrainedBox(maxWidth: 360/380)` around dialog content.
|
||||
- Literal widths on toolbar selects.
|
||||
- Outline text fields and ghost selects inside `PropertiesSection`s;
|
||||
secondary fields inside `SettingsList`s.
|
||||
- Record-card and detail-page subtitles carrying data (sku · price, buyer ·
|
||||
date, counts, emails).
|
||||
- About 37 hand-written copies of `ValueField`, and a bare `Text` value in two
|
||||
role rows.
|
||||
- Hand-built toasts (`showToast` + `SurfaceCard` + literal padding) instead of
|
||||
`showAppToast`.
|
||||
- `ControlDensity.compact` on ordinary buttons outside a header.
|
||||
|
||||
@@ -15,7 +15,7 @@ dependencies:
|
||||
git:
|
||||
url: https://git.imbenji.dev/IMBENJI.NET/Garage-SDKs.git
|
||||
path: garage_ui
|
||||
ref: v0.1.0
|
||||
ref: b2692019197e60d44ddd3ecab4917caf71ff45c3 # v0.1.0
|
||||
```
|
||||
|
||||
Leave that alone. Clone this repo somewhere, then next to the app's
|
||||
@@ -109,7 +109,7 @@ Once the change works locally:
|
||||
|
||||
1. Commit and push it in this repo.
|
||||
2. Tag a new version, e.g. `v0.1.1`, and push the tag.
|
||||
3. In each app that should get it, bump `ref:` to the new tag. Do it on purpose,
|
||||
3. In each app that should get it, bump `ref:` to the new tag's full commit hash. Do it on purpose,
|
||||
per app — dont float `main`.
|
||||
4. Check the app builds **without** the override. Move it aside, resolve against
|
||||
the real tag, and analyze:
|
||||
|
||||
@@ -186,6 +186,17 @@ class ButtonVariance implements AbstractButtonStyle {
|
||||
margin: _buttonZeroMargin,
|
||||
);
|
||||
|
||||
// ghost, but for the remove/revoke/delete kind of action that doesnt deserve
|
||||
// a filled red slab. before this existed every call site hand coloured a
|
||||
// ghost button's text + icon, which is exactly the override we dont want
|
||||
static const AbstractButtonStyle ghostDestructive = ButtonVariance(
|
||||
decoration: _buttonGhostDestructiveDecoration,
|
||||
mouseCursor: _buttonMouseCursor,
|
||||
textStyle: _buttonGhostDestructiveTextStyle,
|
||||
iconTheme: _buttonGhostDestructiveIconTheme,
|
||||
margin: _buttonZeroMargin,
|
||||
);
|
||||
|
||||
@override
|
||||
final ButtonStateProperty<Decoration> decoration;
|
||||
@override
|
||||
@@ -307,6 +318,14 @@ class ButtonStyle implements AbstractButtonStyle {
|
||||
this.paddingOverride,
|
||||
}) : variance = ButtonVariance.destructive;
|
||||
|
||||
const ButtonStyle.ghostDestructive({
|
||||
this.size = ButtonSize.normal,
|
||||
this.density,
|
||||
this.shape = ButtonShape.rectangle,
|
||||
this.squarePadding = false,
|
||||
this.paddingOverride,
|
||||
}) : variance = ButtonVariance.ghostDestructive;
|
||||
|
||||
// icon flavours — same as above but default to icon density (square padding)
|
||||
const ButtonStyle.primaryIcon({
|
||||
this.size = ButtonSize.normal,
|
||||
@@ -348,6 +367,14 @@ class ButtonStyle implements AbstractButtonStyle {
|
||||
this.paddingOverride,
|
||||
}) : variance = ButtonVariance.destructive;
|
||||
|
||||
const ButtonStyle.ghostDestructiveIcon({
|
||||
this.size = ButtonSize.normal,
|
||||
this.density,
|
||||
this.shape = ButtonShape.rectangle,
|
||||
this.squarePadding = true,
|
||||
this.paddingOverride,
|
||||
}) : variance = ButtonVariance.ghostDestructive;
|
||||
|
||||
@override
|
||||
ButtonStateProperty<Decoration> get decoration {
|
||||
if (shape == ButtonShape.circle) {
|
||||
@@ -923,6 +950,56 @@ IconThemeData _buttonDestructiveIconTheme(
|
||||
);
|
||||
}
|
||||
|
||||
// GHOST DESTRUCTIVE
|
||||
/// Ghost with the destructive colour on the label and icon. Same wash as ghost
|
||||
/// on hover, just tinted with `destructive` instead of the foreground so the
|
||||
/// highlight agrees with the text.
|
||||
Decoration _buttonGhostDestructiveDecoration(
|
||||
BuildContext context,
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
final themeData = GarageTheme.of(context);
|
||||
final radius = BorderRadius.circular(themeData.radiusMd);
|
||||
final tint = themeData.colorScheme.destructive;
|
||||
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return BoxDecoration(color: const Color(0x00000000), borderRadius: radius);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return BoxDecoration(color: tint.withValues(alpha: 0.2), borderRadius: radius);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return BoxDecoration(color: tint.withValues(alpha: 0.12), borderRadius: radius);
|
||||
}
|
||||
return BoxDecoration(color: const Color(0x00000000), borderRadius: radius);
|
||||
}
|
||||
|
||||
|
||||
TextStyle _buttonGhostDestructiveTextStyle(
|
||||
BuildContext context,
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
var themeData = GarageTheme.of(context);
|
||||
return themeData.typography.small.copyWith(
|
||||
color: states.contains(WidgetState.disabled)
|
||||
? themeData.colorScheme.mutedForeground
|
||||
: themeData.colorScheme.destructive,
|
||||
);
|
||||
}
|
||||
|
||||
IconThemeData _buttonGhostDestructiveIconTheme(
|
||||
BuildContext context,
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
var themeData = GarageTheme.of(context);
|
||||
return IconThemeData(
|
||||
color: states.contains(WidgetState.disabled)
|
||||
? themeData.colorScheme.mutedForeground
|
||||
: themeData.colorScheme.destructive,
|
||||
size: themeData.iconTheme.small.size,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// button group border merging
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1327,6 +1404,46 @@ class Button extends StatefulWidget {
|
||||
this.disableFocusOutline = false,
|
||||
});
|
||||
|
||||
const Button.ghostDestructive({
|
||||
super.key,
|
||||
this.statesController,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.leadingGap,
|
||||
this.trailingGap,
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.focusNode,
|
||||
this.alignment,
|
||||
this.enabled,
|
||||
this.style = const ButtonStyle.ghostDestructive(),
|
||||
this.disableTransition = false,
|
||||
this.onFocus,
|
||||
this.onHover,
|
||||
this.disableHoverEffect = false,
|
||||
this.enableFeedback,
|
||||
this.onTapDown,
|
||||
this.onTapUp,
|
||||
this.onTapCancel,
|
||||
this.onSecondaryTapDown,
|
||||
this.onSecondaryTapUp,
|
||||
this.onSecondaryTapCancel,
|
||||
this.onTertiaryTapDown,
|
||||
this.onTertiaryTapUp,
|
||||
this.onTertiaryTapCancel,
|
||||
this.onLongPressStart,
|
||||
this.onLongPressUp,
|
||||
this.onLongPressMoveUpdate,
|
||||
this.onLongPressEnd,
|
||||
this.onSecondaryLongPress,
|
||||
this.onTertiaryLongPress,
|
||||
this.semanticLabel,
|
||||
this.semanticToggled,
|
||||
this.semanticSelected,
|
||||
this.marginAlignment,
|
||||
this.disableFocusOutline = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Button> createState() => _ButtonState();
|
||||
}
|
||||
@@ -2185,6 +2302,26 @@ class IconButton extends StatelessWidget {
|
||||
this.shape = ButtonShape.rectangle,
|
||||
});
|
||||
|
||||
const IconButton.ghostDestructive({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.enabled,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.alignment,
|
||||
this.size = ButtonSize.normal,
|
||||
this.focusNode,
|
||||
this.disableTransition = false,
|
||||
this.onHover,
|
||||
this.onFocus,
|
||||
this.enableFeedback,
|
||||
this.semanticLabel,
|
||||
this.variance = ButtonVariance.ghostDestructive,
|
||||
this.density,
|
||||
this.shape = ButtonShape.rectangle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Button(
|
||||
|
||||
@@ -585,7 +585,9 @@ class SelectPopup<T> extends StatefulWidget {
|
||||
/// whether the search field is shown (only in builder mode).
|
||||
final bool enableSearch;
|
||||
|
||||
/// whether the list shrink-wraps (static lists do, builder lists scroll).
|
||||
/// whether the rows are a plain Column (static lists) or a lazy ListView
|
||||
/// (builder lists, which can be long). Both only take the height they
|
||||
/// need - see [_listView].
|
||||
final bool shrinkWrap;
|
||||
|
||||
const SelectPopup({
|
||||
@@ -657,9 +659,9 @@ class _SelectPopupState<T> extends State<SelectPopup<T>> {
|
||||
// nothing to measure - asking a column how wide it wants to be and then
|
||||
// ignoring the answer is just a layout pass nobody reads.
|
||||
//
|
||||
// [shrinkWrap] still decides the VERTICAL: a static list is a scrolling
|
||||
// Column that takes only the height it needs, the builder path is a lazy
|
||||
// ListView.
|
||||
// [shrinkWrap] picks which list: a static one is a scrolling Column, the
|
||||
// builder path is a lazy ListView. Either way the popup ends up as tall
|
||||
// as its rows and no taller.
|
||||
return _popupSurface(theme, column);
|
||||
}
|
||||
|
||||
@@ -729,10 +731,19 @@ class _SelectPopupState<T> extends State<SelectPopup<T>> {
|
||||
);
|
||||
}
|
||||
|
||||
// shrinkWrap regardless of [widget.shrinkWrap] - that flag is about which
|
||||
// list we build, not how tall it ends up. A ListView left to expand eats
|
||||
// every pixel of popupMaxHeight its handed, so a one-row "Search lines"
|
||||
// popup opened a 312px wall of empty surface under its single row.
|
||||
//
|
||||
// This stays lazy: the incoming maxHeight is bounded (the overlay's
|
||||
// ConstrainedBox, then Flexible), so the shrink wrapping viewport still
|
||||
// only lays out as far as that extent and stops. Its the *unbounded* case
|
||||
// that forces a full layout pass, and we never hand it one.
|
||||
return ListView.separated(
|
||||
controller: _scrollController,
|
||||
padding: EdgeInsets.all(inset),
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
shrinkWrap: true,
|
||||
itemCount: count,
|
||||
separatorBuilder: (context, _) => Gap(_popupRowGap(theme)),
|
||||
itemBuilder: (context, index) =>
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// Dumps the real theme values out to design_system/, so the design system
|
||||
// never has to be typed out by hand (and never drifts from the package).
|
||||
//
|
||||
// cd garage_ui && flutter test tool/export_design_tokens_test.dart
|
||||
//
|
||||
// its a "test" only because thats the easiest way to get a flutter binding
|
||||
// without an app. it writes files, it doesnt assert anything interesting.
|
||||
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:garage_ui/theme.dart";
|
||||
|
||||
String _hex(Color c) {
|
||||
final argb = c.toARGB32();
|
||||
final a = (argb >> 24) & 0xff;
|
||||
final rgb = (argb & 0xffffff).toRadixString(16).padLeft(6, "0");
|
||||
if (a == 0xff) return "#$rgb";
|
||||
return "#$rgb${a.toRadixString(16).padLeft(2, "0")}";
|
||||
}
|
||||
|
||||
Map<String, String> _colours(ColourScheme s) => {
|
||||
"background": _hex(s.background),
|
||||
"foreground": _hex(s.foreground),
|
||||
"chrome": _hex(s.chrome),
|
||||
"surface-sunken": _hex(s.surfaceSunken),
|
||||
"card": _hex(s.card),
|
||||
"muted": _hex(s.muted),
|
||||
"muted-foreground": _hex(s.mutedForeground),
|
||||
"row-text": _hex(s.rowText),
|
||||
"row-hovered": _hex(s.rowHovered),
|
||||
"popover": _hex(s.popover),
|
||||
"popover-border": _hex(s.popoverBorder),
|
||||
"popover-item-hovered": _hex(s.popoverItemHovered),
|
||||
"tooltip-background": _hex(s.tooltipBackground),
|
||||
"tooltip-border": _hex(s.tooltipBorder),
|
||||
"primary": _hex(s.primary),
|
||||
"primary-hovered": _hex(s.primaryHovered),
|
||||
"primary-foreground": _hex(s.primaryForeground),
|
||||
"secondary": _hex(s.secondary),
|
||||
"secondary-hovered": _hex(s.secondaryHovered),
|
||||
"secondary-foreground": _hex(s.secondaryForeground),
|
||||
"destructive": _hex(s.destructive),
|
||||
"control-fill": _hex(s.controlFill),
|
||||
"control-fill-hovered": _hex(s.controlFillHovered),
|
||||
"control-fill-focused": _hex(s.controlFillFocused),
|
||||
"control-border": _hex(s.controlBorder),
|
||||
"switch-track-inactive": _hex(s.switchTrackInactive),
|
||||
"border": _hex(s.border),
|
||||
"divider": _hex(s.divider),
|
||||
"panel-border": _hex(s.panelBorder),
|
||||
"panel-border-highlighted": _hex(s.panelBorderHighlighted),
|
||||
"properties-section-border": _hex(s.propertiesSectionBorder),
|
||||
"ring": _hex(s.ring),
|
||||
"chart-1": _hex(s.chart1),
|
||||
"chart-2": _hex(s.chart2),
|
||||
"chart-3": _hex(s.chart3),
|
||||
"chart-4": _hex(s.chart4),
|
||||
"chart-5": _hex(s.chart5),
|
||||
};
|
||||
|
||||
Map<String, num> _density(Density d) {
|
||||
final t = ThemeData(colorScheme: GarageSchemes.carbon, density: d);
|
||||
return {
|
||||
"font-size": d.fontSize,
|
||||
"line-height": d.lineHeight,
|
||||
"line-box": d.lineBox,
|
||||
"control-height": d.controlHeight,
|
||||
"control-padding-x": d.buttonPaddingX,
|
||||
"control-padding-y": d.controlPaddingY,
|
||||
"control-gap": d.controlGap,
|
||||
"control-border-width": d.controlBorderWidth,
|
||||
"menu-row-height": d.menuRowHeight,
|
||||
"popup-row-height": d.popupRowHeight,
|
||||
"explorer-row-height": d.explorerRowHeight,
|
||||
"chrome-bar-height": d.chromeBarHeight,
|
||||
"property-row-height": d.propertyRowHeight,
|
||||
// the label + subtitle stack a SettingsRow is sized off
|
||||
"label-column-height": d.labelColumnHeight,
|
||||
"properties-section-gap": d.propertiesSectionGap,
|
||||
"explorer-row-base-padding": d.explorerRowBasePadding,
|
||||
"explorer-row-end-padding": d.explorerRowEndPadding,
|
||||
"list-row-indent": d.listRowIndent,
|
||||
"container-gap": d.containerGap,
|
||||
"container-padding": d.containerPadding,
|
||||
"gap-xxs": d.gapXxs,
|
||||
"gap-xs": d.gapXs,
|
||||
"gap-sm": d.gapSm,
|
||||
"gap-md": d.gapMd,
|
||||
"gap-lg": d.gapLg,
|
||||
"gap-xl": d.gapXl,
|
||||
"gap-xxl": d.gapXxl,
|
||||
"text-xxs": d.textXxs,
|
||||
"text-xs": d.textXs,
|
||||
"text-sm": d.textSm,
|
||||
"text-lg": d.textLg,
|
||||
"icon-sm": t.iconTheme.small.size ?? d.iconSize,
|
||||
"icon-md": t.iconTheme.medium.size ?? 20,
|
||||
"icon-lg": t.iconTheme.large.size ?? 24,
|
||||
};
|
||||
}
|
||||
|
||||
String _num(num n) => n == n.roundToDouble() ? n.round().toString() : n.toString();
|
||||
|
||||
void main() {
|
||||
test("export design tokens", () {
|
||||
final out = Directory("../design_system");
|
||||
out.createSync(recursive: true);
|
||||
|
||||
final t = ThemeData(colorScheme: GarageSchemes.carbon);
|
||||
|
||||
final schemes = {
|
||||
"carbon": _colours(GarageSchemes.carbon),
|
||||
"dark": _colours(GarageSchemes.dark),
|
||||
"light": _colours(GarageSchemes.light),
|
||||
};
|
||||
final densities = {
|
||||
"compact": _density(const Density.compact()),
|
||||
"normal": _density(const Density.normal()),
|
||||
"product": _density(const Density.product()),
|
||||
};
|
||||
final shape = {
|
||||
"radius-xs": t.radiusXs,
|
||||
"radius-sm": t.radiusSm,
|
||||
"radius-md": t.radiusMd,
|
||||
"radius-lg": t.radiusLg,
|
||||
"radius-xl": t.radiusXl,
|
||||
"radius-xxl": t.radiusXxl,
|
||||
"panel-radius": t.panelRadius,
|
||||
"panel-gap": t.panelGap,
|
||||
"panel-border-width": 1.15,
|
||||
"property-split": 0.4,
|
||||
"property-label-gap": 10,
|
||||
"dialog-max-width": 350,
|
||||
};
|
||||
|
||||
File("${out.path}/tokens.json").writeAsStringSync(
|
||||
const JsonEncoder.withIndent(" ").convert({
|
||||
"_generated": "by garage_ui/tool/export_design_tokens_test.dart - dont edit by hand",
|
||||
"fonts": {"sans": "Geist", "mono": "Geist Mono"},
|
||||
"weights": {"body": 300, "control": 400, "medium": 500, "semibold": 600},
|
||||
"schemes": schemes,
|
||||
"densities": densities,
|
||||
"shape": shape,
|
||||
}),
|
||||
);
|
||||
|
||||
final css = StringBuffer()
|
||||
..writeln("/* garage_ui design tokens. Generated by garage_ui/tool/export_design_tokens_test.dart")
|
||||
..writeln(" - dont edit by hand, rerun the export.")
|
||||
..writeln(" Scheme: data-scheme=\"carbon\" (default) | \"dark\" | \"light\"")
|
||||
..writeln(" Density: data-density=\"product\" (default) | \"normal\" | \"compact\" */")
|
||||
..writeln()
|
||||
..writeln("@import url(\"https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500&display=swap\");")
|
||||
..writeln();
|
||||
|
||||
void block(String selector, Map<String, String> vars) {
|
||||
css.writeln("$selector {");
|
||||
vars.forEach((k, v) => css.writeln(" --$k: $v;"));
|
||||
css.writeln("}");
|
||||
css.writeln();
|
||||
}
|
||||
|
||||
block(":root", {
|
||||
"font-sans": "\"Geist\", system-ui, sans-serif",
|
||||
"font-mono": "\"Geist Mono\", ui-monospace, monospace",
|
||||
"weight-body": "300",
|
||||
"weight-control": "400",
|
||||
"weight-medium": "500",
|
||||
"weight-semibold": "600",
|
||||
for (final e in shape.entries)
|
||||
e.key: (e.key == "property-split") ? _num(e.value) : "${_num(e.value)}px",
|
||||
});
|
||||
|
||||
schemes.forEach((name, vars) {
|
||||
final sel = name == "carbon" ? ":root, [data-scheme=\"carbon\"]" : "[data-scheme=\"$name\"]";
|
||||
block(sel, vars);
|
||||
});
|
||||
|
||||
// the default block has to come FIRST. `:root` and `[data-density=x]` are
|
||||
// the same specificity, so whichever is later wins - with product last,
|
||||
// <html data-density="compact"> quietly rendered product
|
||||
final densityOrder = ["product", "compact", "normal"];
|
||||
for (final name in densityOrder) {
|
||||
final vars = densities[name]!;
|
||||
final sel = name == "product" ? ":root, [data-density=\"product\"]" : "[data-density=\"$name\"]";
|
||||
block(sel, {
|
||||
for (final e in vars.entries)
|
||||
e.key: e.key == "line-height" ? _num(e.value) : "${_num(e.value)}px",
|
||||
});
|
||||
}
|
||||
|
||||
File("${out.path}/tokens.css").writeAsStringSync(css.toString());
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user