Add a design system for Claude Design, generated from garage_ui
Tokens come out of garage_ui/tool/export_design_tokens_test.dart, the components are CSS on top of them, and the cards cover foundations, components and whole hub and Arcs & Angles screens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
572c09039f
commit
b5e5ed5b75
@@ -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); } }
|
||||
Reference in New Issue
Block a user