The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme, docs under docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
This commit is contained in:
@@ -0,0 +1,573 @@
|
||||
import "package:flutter/foundation.dart"
|
||||
show TargetPlatform, defaultTargetPlatform;
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
import "package:garage_ui/theme/colour_scheme.dart";
|
||||
import "package:garage_ui/theme/typography.dart";
|
||||
|
||||
/// The two control tiers. One type for both levels: hand it to [ThemeData] to
|
||||
/// set the app-wide density, or to any control to override that control.
|
||||
///
|
||||
/// Replaces the pair this used to be - a `DensityMode` enum on the theme and a
|
||||
/// separate `ControlDensity` class on widgets - which modelled the same two
|
||||
/// values twice, in two shapes, and named the widget-level one after a single
|
||||
/// widget despite Select, Menubar, MenuButton, MenuPopup, TextField and
|
||||
/// TabView all taking it.
|
||||
enum ControlDensity {
|
||||
compact,
|
||||
normal;
|
||||
|
||||
bool get isCompact => this == ControlDensity.compact;
|
||||
|
||||
/// The canonical token set for this tier, ignoring any theme tuning.
|
||||
Density get canonical =>
|
||||
isCompact ? const Density.compact() : const Density.normal();
|
||||
|
||||
/// The tier the theme is currently set to.
|
||||
static ControlDensity of(ThemeData theme) => theme.density.control;
|
||||
|
||||
/// The token set a control on this tier should read.
|
||||
///
|
||||
/// When the tier matches the theme's, the theme's own [Density] is used, so
|
||||
/// a tuned one survives. When a control asks for the other tier, it falls
|
||||
/// back to that tier's canonical tokens.
|
||||
Density tokens(ThemeData theme) =>
|
||||
this == theme.density.control ? theme.density : canonical;
|
||||
|
||||
EdgeInsets resolve(ThemeData theme) => tokens(theme).buttonPadding;
|
||||
|
||||
/// Padding for an icon-only control, collapsed to its smallest side so the
|
||||
/// control comes out square rather than inheriting the asymmetric
|
||||
/// horizontal padding.
|
||||
EdgeInsets resolveIcon(ThemeData theme) => _squarePadding(resolve(theme));
|
||||
}
|
||||
|
||||
EdgeInsets _squarePadding(EdgeInsets p) {
|
||||
final side = p.vertical < p.horizontal ? p.vertical / 2 : p.horizontal / 2;
|
||||
return EdgeInsets.all(side);
|
||||
}
|
||||
|
||||
/// The app-wide UI density.
|
||||
///
|
||||
/// ONE RULE: the fields on this class are the only hand-set numbers in the
|
||||
/// control system. Everything else - line box, icon size, every vertical
|
||||
/// padding, every control height - is a getter derived from them. If you find
|
||||
/// yourself typing a pixel height anywhere else, it belongs here instead.
|
||||
///
|
||||
/// The heights are the configurable part. You say how tall a control should be
|
||||
/// and the padding falls out of it:
|
||||
///
|
||||
/// lineBox = fontSize * lineHeight
|
||||
/// controlPaddingY = (controlHeight - lineBox) / 2
|
||||
///
|
||||
/// NOT the other way round. Tuning padding until a height came out right is
|
||||
/// what produced four different control heights, a `Transform.translate` in two
|
||||
/// widgets, and 23/27 re-typed as literals in the explorer.
|
||||
///
|
||||
/// `lineBox` needs no font metrics: when a TextStyle carries an explicit
|
||||
/// `height`, Flutter sizes the line box to exactly `fontSize * height` and does
|
||||
/// not consult the font. That is what makes this whole chain deterministic and
|
||||
/// testable - a style with a null `height` measures 1.0x in the test host and
|
||||
/// ~1.25x in the app, which is how a ~26px TextField once passed a suite of
|
||||
/// tests that all asserted 23.
|
||||
class Density {
|
||||
const Density({
|
||||
this.control = ControlDensity.compact,
|
||||
|
||||
// ---- type ----
|
||||
this.fontSize = 10.0,
|
||||
this.lineHeight = 1.1,
|
||||
|
||||
// ---- height targets (configure these; paddings derive from them) ----
|
||||
this.controlHeight = 23.0,
|
||||
this.menuRowHeight = 20.0,
|
||||
this.popupRowHeight = 20.0,
|
||||
this.popupMaxRows = 12,
|
||||
this.explorerRowHeight = 22.0,
|
||||
|
||||
// ---- horizontal + misc primitives ----
|
||||
this.buttonPaddingX = 10.0,
|
||||
this.controlGap = 4.0,
|
||||
this.controlBorderWidth = 1.0,
|
||||
this.explorerRowBasePadding = 4.0,
|
||||
this.explorerRowEndPadding = 10.0,
|
||||
this.listRowIndent = 6.0,
|
||||
|
||||
// ---- container-level spacing (panels, popovers, dialogs) ----
|
||||
this.containerGap = 8.0,
|
||||
this.containerPadding = 16.0,
|
||||
|
||||
// ---- optical ----
|
||||
this.controlTextOffset = 0.0,
|
||||
});
|
||||
|
||||
/// The compact tier, said out loud. Same as the unnamed constructor's
|
||||
/// defaults - which is exactly why it exists, because `Density()` silently
|
||||
/// meaning "compact" was a trap.
|
||||
const Density.compact() : this();
|
||||
|
||||
const Density.normal()
|
||||
: control = ControlDensity.normal,
|
||||
fontSize = 10.0,
|
||||
lineHeight = 1.1,
|
||||
controlHeight = 27.0,
|
||||
menuRowHeight = 26.0,
|
||||
popupRowHeight = 26.0,
|
||||
popupMaxRows = 12,
|
||||
explorerRowHeight = 30.0,
|
||||
buttonPaddingX = 12.0,
|
||||
controlGap = 5.0,
|
||||
controlBorderWidth = 1.0,
|
||||
explorerRowBasePadding = 8.0,
|
||||
explorerRowEndPadding = 14.0,
|
||||
listRowIndent = 12.0,
|
||||
containerGap = 10.0,
|
||||
containerPadding = 20.0,
|
||||
controlTextOffset = 0.0;
|
||||
|
||||
/// The product tier - for the Garage web apps rather than for A&A.
|
||||
///
|
||||
/// compact and normal are both answers to "a properties inspector has to sit
|
||||
/// beside a viewport without eating it", which is why they share a 10px font
|
||||
/// and differ only in control geometry. An app with no canvas inherits that
|
||||
/// economy for nothing, so this tier scales the TYPE as well: 12px control
|
||||
/// font, 33px controls, and a gap ladder off a base of 5.
|
||||
///
|
||||
/// `control` stays [ControlDensity.normal] deliberately. That enum is the
|
||||
/// two-way geometry switch widgets branch on (`isCompact`), not a name for
|
||||
/// the tier, and A&A switches over it exhaustively in its settings UI.
|
||||
///
|
||||
/// 12 * 1.25 = 15 line box, (33 - 15) / 2 = 9 padding. both whole, so the
|
||||
/// control geometry stays exact - see [lineBox].
|
||||
const Density.product()
|
||||
: control = ControlDensity.normal,
|
||||
fontSize = 12.0,
|
||||
lineHeight = 1.25,
|
||||
controlHeight = 33.0,
|
||||
menuRowHeight = 33.0,
|
||||
popupRowHeight = 33.0,
|
||||
popupMaxRows = 10,
|
||||
explorerRowHeight = 29.0,
|
||||
buttonPaddingX = 10.0,
|
||||
controlGap = 5.0,
|
||||
controlBorderWidth = 1.0,
|
||||
explorerRowBasePadding = 8.0,
|
||||
explorerRowEndPadding = 12.0,
|
||||
listRowIndent = 10.0,
|
||||
containerGap = 10.0,
|
||||
containerPadding = 20.0,
|
||||
controlTextOffset = 0.0;
|
||||
|
||||
/// Which tier this token set represents.
|
||||
final ControlDensity control;
|
||||
|
||||
// ---- type ----
|
||||
|
||||
/// Control font size. The app is a fixed compact desktop tool, so these are
|
||||
/// final pixel values and are deliberately NOT multiplied by `scaling`.
|
||||
final double fontSize;
|
||||
|
||||
/// Line box as a multiple of [fontSize]. Pinning this is what makes control
|
||||
/// geometry font-independent - see the class doc.
|
||||
final double lineHeight;
|
||||
|
||||
// ---- height targets ----
|
||||
|
||||
/// Button / text field / select trigger / icon button.
|
||||
final double controlHeight;
|
||||
|
||||
/// Menu rows (MenuButton and friends).
|
||||
///
|
||||
/// Happens to equal [popupRowHeight] in both densities today - menus and
|
||||
/// select popups are the same kind of surface. Kept as two tokens anyway so
|
||||
/// one can move without dragging the other along.
|
||||
final double menuRowHeight;
|
||||
|
||||
/// Rows inside a select popup.
|
||||
final double popupRowHeight;
|
||||
|
||||
/// How many rows a select popup shows before it starts scrolling. A count,
|
||||
/// not a pixel value - the height falls out of it via [popupMaxHeight], so a
|
||||
/// denser popup gets shorter rather than showing more of them.
|
||||
final int popupMaxRows;
|
||||
|
||||
/// Explorer tree rows.
|
||||
final double explorerRowHeight;
|
||||
|
||||
// ---- horizontal + misc ----
|
||||
|
||||
final double buttonPaddingX;
|
||||
|
||||
/// Gap between a control's leading/trailing icon and its label. Sits INSIDE
|
||||
/// the control, so it is tighter than the outer padding on purpose.
|
||||
final double controlGap;
|
||||
|
||||
final double controlBorderWidth;
|
||||
|
||||
/// Leading indent applied per depth level in the explorer tree.
|
||||
final double explorerRowBasePadding;
|
||||
|
||||
/// Trailing inset on an explorer row, so the hide toggle isnt sat right
|
||||
/// under the scrollbar thumb.
|
||||
final double explorerRowEndPadding;
|
||||
|
||||
/// Leading inset on a flat list row, before its icon. Distinct from
|
||||
/// [explorerRowBasePadding], which is a per-depth indent in a tree.
|
||||
///
|
||||
/// Authored from the values the Lines slots list was already branching on by
|
||||
/// hand. It and the explorer disagree about this inset (6/12 vs 4/8) and
|
||||
/// always have - reconciling them is a visual decision, not a refactor.
|
||||
final double listRowIndent;
|
||||
|
||||
// ---- container-level ----
|
||||
|
||||
/// Spacing between elements in a panel / popover / dialog. This is layout
|
||||
/// spacing, NOT control-internal spacing - reach for [controlGap] inside a
|
||||
/// control. (Replaces the old shadcn-inherited `baseGap`.)
|
||||
final double containerGap;
|
||||
|
||||
/// Padding inside a panel / popover / dialog. (Replaces `baseContentPadding`
|
||||
/// and `baseContainerPadding`, which always held the same value.)
|
||||
final double containerPadding;
|
||||
|
||||
// ---- optical ----
|
||||
|
||||
/// Downward nudge applied to control content so it sits on its optical
|
||||
/// centre rather than its geometric one. Negative moves it up.
|
||||
///
|
||||
/// This is the one value the maths cannot settle on its own - the line box is
|
||||
/// exact, but where the ink sits inside it depends on the font's
|
||||
/// ascent/descent split. So it is a judgement made by eye, once, here. It
|
||||
/// used to be `Offset(0, 1)` hardcoded in Button and `Offset(0, 2)` in
|
||||
/// Select, which is two judgements that disagreed.
|
||||
///
|
||||
/// Currently 0 - i.e. the geometric centre is what looks right in Geist at
|
||||
/// this size. Keep the token even so: it is the knob, and a zero here costs
|
||||
/// nothing because the controls skip the transform entirely when it is 0.
|
||||
final double controlTextOffset;
|
||||
|
||||
// =========================================================================
|
||||
// derived - do not hand-set any of these, and do not re-derive them at a
|
||||
// call site
|
||||
// =========================================================================
|
||||
|
||||
/// Height of one line of control text: `fontSize * lineHeight`, rounded to a
|
||||
/// whole pixel.
|
||||
///
|
||||
/// Measured, not assumed. Against Georgia and Andale Mono (both natural ratio
|
||||
/// 1.10) and the test host's fallback (1.00): when the product is a whole
|
||||
/// number the engine lays the line box out at exactly that in all three, so
|
||||
/// the font's own ascent/descent genuinely do not participate. That is what
|
||||
/// makes control geometry font-independent and testable.
|
||||
///
|
||||
/// When the product is fractional the engine rounds it, and at a .5 tie the
|
||||
/// direction is FONT-DEPENDENT (13.5 came out 14 in Andale, 13 in Georgia).
|
||||
/// So keep `fontSize * lineHeight` on a whole number - 10 * 1.1 = 11 does -
|
||||
/// and every height below is exact. The rounding here keeps the derivation
|
||||
/// honest for other configs rather than quietly missing the target by a
|
||||
/// fraction of a pixel.
|
||||
double get lineBox => (fontSize * lineHeight).roundToDouble();
|
||||
|
||||
/// Control icons, sized against the TEXT rather than the line box.
|
||||
///
|
||||
/// This used to be `lineBox` - 15px at product - on the reasoning that an
|
||||
/// icon-only control and a text control then come out the same height for
|
||||
/// free. That holds, but it isnt what the eye measures: lucide glyphs fill
|
||||
/// their box nearly edge to edge while a 12px font has a cap height around
|
||||
/// 8.5px, so a line-box icon reads about 70% taller than the letters beside
|
||||
/// it and every button with an icon in it looked slightly wrong.
|
||||
///
|
||||
/// Level with the font. 1.1x was the first attempt at the optical match and
|
||||
/// still read a shade heavy next to the label beside it. Control HEIGHT is
|
||||
/// unaffected either way: that comes from controlHeight, not from whats
|
||||
/// inside.
|
||||
double get iconSize => fontSize;
|
||||
|
||||
double get controlPaddingY => (controlHeight - lineBox) / 2;
|
||||
double get menuRowPaddingY => (menuRowHeight - lineBox) / 2;
|
||||
double get popupRowPaddingY => (popupRowHeight - lineBox) / 2;
|
||||
double get explorerRowPaddingY => (explorerRowHeight - lineBox) / 2;
|
||||
|
||||
/// Height of a chrome bar - an editor's header or footer.
|
||||
///
|
||||
/// One [controlGap] above the control and one below, which is [gapMd] all
|
||||
/// told. A bar sized any tighter than that isnt giving its contents a
|
||||
/// margin so much as a haircut: the old hardcoded 30 left 1.5px over a
|
||||
/// normal-tier control and would have been SHORTER than a product-tier one,
|
||||
/// so a button in the header didnt fit the header.
|
||||
///
|
||||
/// Matching the vertical margin to the horizontal gap is the whole point -
|
||||
/// a row of buttons then sits in an even field instead of one thats
|
||||
/// generous side to side and tight top to bottom.
|
||||
///
|
||||
/// compact 23 + 8 = 31, normal 27 + 10 = 37, product 33 + 10 = 43
|
||||
double get chromeBarHeight => controlHeight + gapMd;
|
||||
|
||||
/// Cap on a select popup's list. Counts rows only - the list's own padding
|
||||
/// (and a search field, when there is one) sits on top, so the last row
|
||||
/// clips slightly rather than landing flush. Thats deliberate: a half row
|
||||
/// showing is the cheapest "theres more below" hint there is.
|
||||
///
|
||||
/// Was `kDefaultSelectMaxHeight = 240.0` in select.dart, hand-typed and then
|
||||
/// multiplied by `scaling`, so it never moved with density at all.
|
||||
double get popupMaxHeight => popupRowHeight * popupMaxRows;
|
||||
|
||||
/// Padding for a borderless control (ghost/primary button, etc).
|
||||
EdgeInsets get buttonPadding => EdgeInsets.symmetric(
|
||||
horizontal: buttonPaddingX,
|
||||
vertical: controlPaddingY,
|
||||
);
|
||||
|
||||
/// Padding for a bordered control. A BoxDecoration border adds its width to
|
||||
/// the Container's layout, so the stroke comes out of the padding and the
|
||||
/// outer height stays [controlHeight] either way.
|
||||
EdgeInsets get borderedControlPadding => EdgeInsets.symmetric(
|
||||
horizontal: buttonPaddingX - controlBorderWidth,
|
||||
vertical: controlPaddingY - controlBorderWidth,
|
||||
);
|
||||
|
||||
/// Text fields are bordered controls.
|
||||
EdgeInsets get textFieldPadding => borderedControlPadding;
|
||||
|
||||
// ---- text ----
|
||||
//
|
||||
// The body text ladder. [fontSize] is the control font - what a button
|
||||
// label, a field's text and a select trigger render at - and these are the
|
||||
// sizes for text that ISN'T inside a control: dialog copy, headings, hints.
|
||||
//
|
||||
// Dialog copy reading a step larger than the button labels beneath it is
|
||||
// deliberate - content and controls are different things. The bug was never
|
||||
// that they differed, it's that they were UNLINKED: these were hardcoded
|
||||
// 12/14/18 times `scaling`, while the control font comes off this class and
|
||||
// is deliberately not scaled. So 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, which is
|
||||
// the actual defect.
|
||||
//
|
||||
// Deriving them from [fontSize] pins the ratio. At both canonical densities
|
||||
// fontSize is 10, so these come out 12/14/18 - exactly the numbers they
|
||||
// replaced, so nothing shifts at scaling 1.0.
|
||||
|
||||
/// The one step BELOW the control font - a caption sat under something,
|
||||
/// not beside it. A property row's subtitle and description, and the same
|
||||
/// tier anything else that explains a control rather than labelling it
|
||||
/// should reach for.
|
||||
///
|
||||
/// This rung didn't exist. The ladder only ever went UP from [fontSize],
|
||||
/// because in A&A the control font IS the smallest thing on screen - so a
|
||||
/// page needing a caption had nowhere to go and hardcoded one. x0.875 lands
|
||||
/// on 9 against a 10px control font, and 14 against a 16px one.
|
||||
double get textXxs => (fontSize * 0.875).roundToDouble();
|
||||
|
||||
/// Content that sits near controls without being one - dialog copy, page
|
||||
/// headings, hints.
|
||||
///
|
||||
/// x1.1, which lands on the ambient body size: 11 against a 10px control
|
||||
/// font. It was x1.2 (12), matching the literal the shorthands used to
|
||||
/// hardcode - but that put dialog copy and every page heading a fifth above
|
||||
/// the controls beneath them and a pixel above ordinary body text, which
|
||||
/// read as oversized rather than as hierarchy. Weight and colour carry the
|
||||
/// emphasis instead.
|
||||
double get textXs => (fontSize * 1.1).roundToDouble();
|
||||
|
||||
/// Geist's own line box, as a multiple of font size - hhea ascender 1005,
|
||||
/// descender -295, lineGap 0 over a 1000 upem. The package hardcodes Geist
|
||||
/// (see GarageTheme), and a label sets no explicit `height`, so this is the
|
||||
/// ratio its line actually lays out at. Flutter rounds each line to a whole
|
||||
/// pixel, which is why the derivations below round rather than ceil.
|
||||
static const double geistLineRatio = 1.3;
|
||||
|
||||
/// Height of a labelled row's label column - the label on [textXs] with a
|
||||
/// subtitle under it on [textXxs], both on the font's own line box.
|
||||
///
|
||||
/// Worth having as a token because it OUTGROWS [controlHeight] at the small
|
||||
/// tiers: 26 against a 23px compact control. A row sized on controlHeight
|
||||
/// alone therefore got its height from the label rather than from the
|
||||
/// control, which is backwards and moves with the font.
|
||||
double get labelColumnHeight =>
|
||||
(textXs * geistLineRatio).roundToDouble() +
|
||||
(textXxs * geistLineRatio).roundToDouble();
|
||||
|
||||
/// Minimum height of a [PropertyRow]. Clears whichever of the control and
|
||||
/// the label column is taller, plus a step, so neither one is the thing
|
||||
/// setting the row height and a row is the same height with or without a
|
||||
/// subtitle.
|
||||
double get propertyRowHeight =>
|
||||
(labelColumnHeight > controlHeight ? labelColumnHeight : controlHeight) +
|
||||
gapXxs;
|
||||
|
||||
/// Body copy that wants to read a step above the controls.
|
||||
double get textSm => (fontSize * 1.4).roundToDouble();
|
||||
|
||||
/// Headings.
|
||||
double get textLg => (fontSize * 1.8).roundToDouble();
|
||||
|
||||
// ---- layout spacing ----
|
||||
//
|
||||
// The gap scale. Layout spacing BETWEEN widgets - what a call site reaches
|
||||
// for when it puts a `Gap` between two things. Distinct from [controlGap],
|
||||
// which is spacing INSIDE a control, and which is the base unit here.
|
||||
//
|
||||
// Before this existed, call sites picked `Gap(n)` literals by eye. That was
|
||||
// documented rather than derived, and the apps drifted off it - the two
|
||||
// Garage web frontends between them had sixteen distinct gap values,
|
||||
// including a 3, a 5 and ten 14s that no scale would have produced.
|
||||
//
|
||||
// The steps are multiples of [controlGap], which makes [gapMd] equal to
|
||||
// [containerGap] and [gapXl] equal to [containerPadding] at both densities.
|
||||
// That agreement isn't arranged, it's what those two tokens already were -
|
||||
// which is the evidence the base unit is right.
|
||||
//
|
||||
// Rounded because the normal density's base is 5, and a 1.5x step off it
|
||||
// lands on 7.5. A fractional gap isn't wrong the way a fractional line box
|
||||
// is (nothing derives from it), but a whole pixel won't seam on a fractional
|
||||
// device ratio, so it's free to keep them whole.
|
||||
|
||||
/// Hairline separation - a label sat directly above its value.
|
||||
double get gapXxs => (controlGap * 0.5).roundToDouble();
|
||||
|
||||
/// Tight. Icon-adjacent, or items that read as one unit.
|
||||
double get gapXs => controlGap;
|
||||
|
||||
/// Snug, between [gapXs] and the default.
|
||||
double get gapSm => (controlGap * 1.5).roundToDouble();
|
||||
|
||||
/// The default. "These two things are related but distinct." When a call
|
||||
/// site has no particular reason to pick another step, it wants this one.
|
||||
double get gapMd => controlGap * 2;
|
||||
|
||||
/// Section-level: separates groups within a panel or form.
|
||||
double get gapLg => controlGap * 3;
|
||||
|
||||
/// Between major blocks of a layout.
|
||||
double get gapXl => controlGap * 4;
|
||||
|
||||
/// The largest step - page-level separation, above the panel scale.
|
||||
double get gapXxl => controlGap * 6;
|
||||
}
|
||||
|
||||
/// Icon sizes. `small` is the control icon and is derived from the density's
|
||||
/// line box so it can never drift from the text beside it.
|
||||
class IconThemeTokens {
|
||||
const IconThemeTokens({
|
||||
required this.small,
|
||||
required this.medium,
|
||||
required this.large,
|
||||
});
|
||||
|
||||
final IconThemeData small;
|
||||
final IconThemeData medium;
|
||||
final IconThemeData large;
|
||||
|
||||
static IconThemeTokens forDensity(
|
||||
Density density,
|
||||
double scaling,
|
||||
Color color,
|
||||
) => IconThemeTokens(
|
||||
small: IconThemeData(size: density.iconSize, color: color),
|
||||
medium: IconThemeData(size: 20 * scaling, color: color),
|
||||
large: IconThemeData(size: 24 * scaling, color: color),
|
||||
);
|
||||
}
|
||||
|
||||
// The apps first class theme data. Everything the widgets used to pull off
|
||||
// shadcns ThemeData (colours, the global scaling, radius tokens, icon sizes,
|
||||
// the optional surface glass) now lives here as our own thing.
|
||||
class ThemeData {
|
||||
ThemeData({
|
||||
required this.colorScheme,
|
||||
this.scaling = 1.0,
|
||||
this.radius = 0.5,
|
||||
this.surfaceOpacity,
|
||||
this.surfaceBlur,
|
||||
this.enableFeedback,
|
||||
this.panelRadius = 10,
|
||||
this.panelGap = 5,
|
||||
Density density = const Density(),
|
||||
Typography? typography,
|
||||
IconThemeTokens? iconTheme,
|
||||
}) : density = density,
|
||||
// typography and icon sizes are derived from the density so the control
|
||||
// type, the control icon and the control height all move together.
|
||||
typography = typography ?? Typography.forDensity(density),
|
||||
iconTheme =
|
||||
iconTheme ??
|
||||
IconThemeTokens.forDensity(density, scaling, colorScheme.foreground);
|
||||
|
||||
final ColourScheme colorScheme;
|
||||
|
||||
// chrome panel layout - corner radius of the docked panels, and the gap
|
||||
// around + between them in the shell.
|
||||
final double panelRadius;
|
||||
final double panelGap;
|
||||
|
||||
final Typography typography;
|
||||
|
||||
final Density density;
|
||||
|
||||
// haptic/click feedback toggle. null = decide by platform (mobile on).
|
||||
final bool? enableFeedback;
|
||||
|
||||
// used by controls that behave differently on touch platforms.
|
||||
TargetPlatform get platform => defaultTargetPlatform;
|
||||
|
||||
// global ui scale. was shadcns AdaptiveScaling(0.75). widgets multiply their
|
||||
// paddings/sizes by this to stay the size they always were.
|
||||
final double scaling;
|
||||
|
||||
// base radius (rem-ish). the tokens below are derived from it exactly the way
|
||||
// shadcn derived theirs (radius * step).
|
||||
final double radius;
|
||||
|
||||
final IconThemeTokens iconTheme;
|
||||
|
||||
// optional surface glassmorphism - null means opaque / no blur, same defaults
|
||||
// shadcn shipped.
|
||||
final double? surfaceOpacity;
|
||||
final double? surfaceBlur;
|
||||
|
||||
double get radiusXs => radius * 4;
|
||||
double get radiusSm => radius * 8;
|
||||
double get radiusMd => radius * 12;
|
||||
double get radiusLg => radius * 16;
|
||||
double get radiusXl => radius * 20;
|
||||
double get radiusXxl => radius * 24;
|
||||
|
||||
BorderRadius get borderRadiusXs => BorderRadius.circular(radiusXs);
|
||||
BorderRadius get borderRadiusSm => BorderRadius.circular(radiusSm);
|
||||
BorderRadius get borderRadiusMd => BorderRadius.circular(radiusMd);
|
||||
BorderRadius get borderRadiusLg => BorderRadius.circular(radiusLg);
|
||||
BorderRadius get borderRadiusXl => BorderRadius.circular(radiusXl);
|
||||
BorderRadius get borderRadiusXxl => BorderRadius.circular(radiusXxl);
|
||||
|
||||
Radius get radiusMdRadius => Radius.circular(radiusMd);
|
||||
Radius get radiusLgRadius => Radius.circular(radiusLg);
|
||||
Radius get radiusXlRadius => Radius.circular(radiusXl);
|
||||
|
||||
ThemeData copyWith({
|
||||
ColourScheme? colorScheme,
|
||||
double? scaling,
|
||||
double? radius,
|
||||
double? surfaceOpacity,
|
||||
double? surfaceBlur,
|
||||
Typography? typography,
|
||||
Density? density,
|
||||
bool? enableFeedback,
|
||||
double? panelRadius,
|
||||
double? panelGap,
|
||||
IconThemeTokens? iconTheme,
|
||||
}) {
|
||||
return ThemeData(
|
||||
colorScheme: colorScheme ?? this.colorScheme,
|
||||
scaling: scaling ?? this.scaling,
|
||||
radius: radius ?? this.radius,
|
||||
surfaceOpacity: surfaceOpacity ?? this.surfaceOpacity,
|
||||
surfaceBlur: surfaceBlur ?? this.surfaceBlur,
|
||||
typography: typography ?? this.typography,
|
||||
density: density ?? this.density,
|
||||
enableFeedback: enableFeedback ?? this.enableFeedback,
|
||||
panelRadius: panelRadius ?? this.panelRadius,
|
||||
panelGap: panelGap ?? this.panelGap,
|
||||
iconTheme: iconTheme ?? this.iconTheme,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user