mirror of
https://github.com/we-promise/sure.git
synced 2026-07-20 00:35:22 +00:00
feat(mobile): generate shadow scale + focus-ring/bg-inverse tokens (#2349)
Extend mobile/tool/generate_sure_tokens.mjs to emit, from the canonical design/tokens/sure.tokens.json: a mode-aware box-shadow scale (shadowXs..shadowXl, parsed from shadow.xs..xl into List<BoxShadow>), plus the focus-ring (color.focus-ring) and bg-inverse (utility.bg-inverse) colors on SureTokenPalette. Regenerates mobile/lib/theme/sure_tokens.dart (import dart:ui -> package:flutter/painting.dart for BoxShadow/Offset); keeps the --check parity gate; adds token tests for the new values incl. the negative xl spread. Radii are already complete (the source only defines md/lg); a type-scale is not in the token source, so both are intentionally untouched. The shadow-border composites are deferred (derivable in-widget as a shadow + a 1px border from existing border tokens). This unblocks SureCard/elevated surfaces. Part of #2235.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// Source: design/tokens/sure.tokens.json
|
||||
// Build: node mobile/tool/generate_sure_tokens.mjs
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
class SureTokens {
|
||||
const SureTokens._();
|
||||
@@ -38,6 +38,8 @@ class SureTokens {
|
||||
info: Color(0xFF1570EF),
|
||||
link: Color(0xFF1570EF),
|
||||
shadow: Color(0x0F0B0B0B),
|
||||
focusRing: Color(0x800B0B0B),
|
||||
bgInverse: Color(0xFF242424),
|
||||
textPrimary: Color(0xFF171717),
|
||||
textInverse: Color(0xFFFFFFFF),
|
||||
textSecondary: Color(0xFF737373),
|
||||
@@ -49,6 +51,11 @@ class SureTokens {
|
||||
buttonPrimaryHover: Color(0xFF242424),
|
||||
buttonDestructive: Color(0xFFEC2222),
|
||||
buttonDestructiveHover: Color(0xFFC91313),
|
||||
shadowXs: [BoxShadow(color: Color(0x0F0B0B0B), offset: Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)],
|
||||
shadowSm: [BoxShadow(color: Color(0x0F0B0B0B), offset: Offset(0.0, 1.0), blurRadius: 6.0, spreadRadius: 0.0)],
|
||||
shadowMd: [BoxShadow(color: Color(0x0F0B0B0B), offset: Offset(0.0, 4.0), blurRadius: 8.0, spreadRadius: -2.0)],
|
||||
shadowLg: [BoxShadow(color: Color(0x0F0B0B0B), offset: Offset(0.0, 12.0), blurRadius: 16.0, spreadRadius: -4.0)],
|
||||
shadowXl: [BoxShadow(color: Color(0x0F0B0B0B), offset: Offset(0.0, 20.0), blurRadius: 24.0, spreadRadius: -4.0)],
|
||||
);
|
||||
|
||||
static const dark = SureTokenPalette(
|
||||
@@ -67,6 +74,8 @@ class SureTokens {
|
||||
info: Color(0xFF2E90FA),
|
||||
link: Color(0xFF2E90FA),
|
||||
shadow: Color(0x14FFFFFF),
|
||||
focusRing: Color(0x80FFFFFF),
|
||||
bgInverse: Color(0xFFFFFFFF),
|
||||
textPrimary: Color(0xFFFFFFFF),
|
||||
textInverse: Color(0xFF171717),
|
||||
textSecondary: Color(0xFFCFCFCF),
|
||||
@@ -78,6 +87,11 @@ class SureTokens {
|
||||
buttonPrimaryHover: Color(0xFFF7F7F7),
|
||||
buttonDestructive: Color(0xFFED4E4E),
|
||||
buttonDestructiveHover: Color(0xFFF13636),
|
||||
shadowXs: [BoxShadow(color: Color(0x14FFFFFF), offset: Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)],
|
||||
shadowSm: [BoxShadow(color: Color(0x14FFFFFF), offset: Offset(0.0, 1.0), blurRadius: 6.0, spreadRadius: 0.0)],
|
||||
shadowMd: [BoxShadow(color: Color(0x14FFFFFF), offset: Offset(0.0, 4.0), blurRadius: 8.0, spreadRadius: -2.0)],
|
||||
shadowLg: [BoxShadow(color: Color(0x14FFFFFF), offset: Offset(0.0, 12.0), blurRadius: 16.0, spreadRadius: -4.0)],
|
||||
shadowXl: [BoxShadow(color: Color(0x14FFFFFF), offset: Offset(0.0, 20.0), blurRadius: 24.0, spreadRadius: -4.0)],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,6 +112,8 @@ class SureTokenPalette {
|
||||
required this.info,
|
||||
required this.link,
|
||||
required this.shadow,
|
||||
required this.focusRing,
|
||||
required this.bgInverse,
|
||||
required this.textPrimary,
|
||||
required this.textInverse,
|
||||
required this.textSecondary,
|
||||
@@ -109,6 +125,11 @@ class SureTokenPalette {
|
||||
required this.buttonPrimaryHover,
|
||||
required this.buttonDestructive,
|
||||
required this.buttonDestructiveHover,
|
||||
required this.shadowXs,
|
||||
required this.shadowSm,
|
||||
required this.shadowMd,
|
||||
required this.shadowLg,
|
||||
required this.shadowXl,
|
||||
});
|
||||
|
||||
final Color surface;
|
||||
@@ -126,6 +147,8 @@ class SureTokenPalette {
|
||||
final Color info;
|
||||
final Color link;
|
||||
final Color shadow;
|
||||
final Color focusRing;
|
||||
final Color bgInverse;
|
||||
final Color textPrimary;
|
||||
final Color textInverse;
|
||||
final Color textSecondary;
|
||||
@@ -137,4 +160,9 @@ class SureTokenPalette {
|
||||
final Color buttonPrimaryHover;
|
||||
final Color buttonDestructive;
|
||||
final Color buttonDestructiveHover;
|
||||
final List<BoxShadow> shadowXs;
|
||||
final List<BoxShadow> shadowSm;
|
||||
final List<BoxShadow> shadowMd;
|
||||
final List<BoxShadow> shadowLg;
|
||||
final List<BoxShadow> shadowXl;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,42 @@ void main() {
|
||||
expect(SureTokens.radiusMd, _resolveDimension(tokens, 'border.radius.md'));
|
||||
expect(SureTokens.radiusLg, _resolveDimension(tokens, 'border.radius.lg'));
|
||||
});
|
||||
|
||||
test('generated focus-ring and bg-inverse match canonical tokens', () {
|
||||
expect(
|
||||
SureTokens.light.focusRing.value,
|
||||
_resolveColor(tokens, 'color.focus-ring', dark: false),
|
||||
);
|
||||
expect(
|
||||
SureTokens.dark.focusRing.value,
|
||||
_resolveColor(tokens, 'color.focus-ring', dark: true),
|
||||
);
|
||||
expect(
|
||||
SureTokens.light.bgInverse.value,
|
||||
_resolveColor(tokens, 'utility.bg-inverse', dark: false),
|
||||
);
|
||||
expect(
|
||||
SureTokens.dark.bgInverse.value,
|
||||
_resolveColor(tokens, 'utility.bg-inverse', dark: true),
|
||||
);
|
||||
});
|
||||
|
||||
test('generated shadow scale carries mode-aware color and geometry', () {
|
||||
final lightSm = SureTokens.light.shadowSm.single;
|
||||
expect(lightSm.offset, const Offset(0, 1));
|
||||
expect(lightSm.blurRadius, 6.0);
|
||||
expect(lightSm.spreadRadius, 0.0);
|
||||
expect(
|
||||
lightSm.color.value,
|
||||
_resolveColorValue(tokens, '{color.black|6%}', dark: false),
|
||||
);
|
||||
expect(
|
||||
SureTokens.dark.shadowSm.single.color.value,
|
||||
_resolveColorValue(tokens, '{color.white|8%}', dark: true),
|
||||
);
|
||||
// The xl step keeps the canonical negative spread (0px 20px 24px -4px).
|
||||
expect(SureTokens.light.shadowXl.single.spreadRadius, -4.0);
|
||||
});
|
||||
}
|
||||
|
||||
int _resolveColor(
|
||||
|
||||
@@ -23,6 +23,8 @@ const COLOR_TOKENS = [
|
||||
["info", "color.info"],
|
||||
["link", "color.link"],
|
||||
["shadow", "color.shadow"],
|
||||
["focusRing", "color.focus-ring"],
|
||||
["bgInverse", "utility.bg-inverse"],
|
||||
["textPrimary", "utility.text-primary"],
|
||||
["textInverse", "utility.text-inverse"],
|
||||
["textSecondary", "utility.text-secondary"],
|
||||
@@ -41,6 +43,16 @@ const RADIUS_TOKENS = [
|
||||
["radiusLg", "border.radius.lg"],
|
||||
];
|
||||
|
||||
// Single-layer elevation scale (shadow/*). Mode-aware: each token carries a
|
||||
// `sure.dark` extension, so light/dark emit different shadow colors.
|
||||
const SHADOW_TOKENS = [
|
||||
["shadowXs", "shadow.xs"],
|
||||
["shadowSm", "shadow.sm"],
|
||||
["shadowMd", "shadow.md"],
|
||||
["shadowLg", "shadow.lg"],
|
||||
["shadowXl", "shadow.xl"],
|
||||
];
|
||||
|
||||
function readTokens() {
|
||||
return JSON.parse(readFileSync(TOKENS_PATH, "utf8"));
|
||||
}
|
||||
@@ -105,6 +117,31 @@ function resolveDimension(tokens, path) {
|
||||
return Number(match[1]).toFixed(1);
|
||||
}
|
||||
|
||||
// Parse a single-layer CSS box-shadow ("<x>px <y>px <blur>px <spread>px {color}")
|
||||
// into a Dart `BoxShadow(...)` literal. Offsets and spread may be negative
|
||||
// (e.g. -4px); blur must be >= 0. A strict number pattern rejects malformed
|
||||
// dimensions (e.g. "1.2.3px") instead of emitting NaN into the generated Dart.
|
||||
function resolveShadow(tokens, path, mode) {
|
||||
const node = nodeAt(tokens, path);
|
||||
const value = valueForMode(node, mode);
|
||||
const num = "-?[0-9]+(?:\\.[0-9]+)?";
|
||||
const nonNeg = "[0-9]+(?:\\.[0-9]+)?";
|
||||
const match = String(value).match(
|
||||
new RegExp(`^(${num})px (${num})px (${nonNeg})px (${num})px (\\{[^}]+\\})$`),
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(`[mobile-tokens] ${path} must be a single-layer box shadow: ${value}`);
|
||||
}
|
||||
const [, offsetX, offsetY, blur, spread, colorRef] = match;
|
||||
const color = resolveColorValue(tokens, colorRef, mode, path);
|
||||
const px = (raw) => Number(raw).toFixed(1);
|
||||
return (
|
||||
`BoxShadow(color: Color(${color}), ` +
|
||||
`offset: Offset(${px(offsetX)}, ${px(offsetY)}), ` +
|
||||
`blurRadius: ${px(blur)}, spreadRadius: ${px(spread)})`
|
||||
);
|
||||
}
|
||||
|
||||
function firstFontFamily(stack) {
|
||||
for (const rawFamily of stack.split(",")) {
|
||||
const family = rawFamily.trim();
|
||||
@@ -117,11 +154,14 @@ function firstFontFamily(stack) {
|
||||
}
|
||||
|
||||
function emitPalette(tokens, mode) {
|
||||
const lines = COLOR_TOKENS.map(
|
||||
const colorLines = COLOR_TOKENS.map(
|
||||
([name, path]) => ` ${name}: Color(${resolveColor(tokens, path, mode)}),`,
|
||||
);
|
||||
const shadowLines = SHADOW_TOKENS.map(
|
||||
([name, path]) => ` ${name}: [${resolveShadow(tokens, path, mode)}],`,
|
||||
);
|
||||
|
||||
return ` static const ${mode} = SureTokenPalette(\n${lines.join("\n")}\n );`;
|
||||
return ` static const ${mode} = SureTokenPalette(\n${colorLines.join("\n")}\n${shadowLines.join("\n")}\n );`;
|
||||
}
|
||||
|
||||
function buildDart(tokens) {
|
||||
@@ -135,7 +175,7 @@ function buildDart(tokens) {
|
||||
// Source: design/tokens/sure.tokens.json
|
||||
// Build: node mobile/tool/generate_sure_tokens.mjs
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
class SureTokens {
|
||||
const SureTokens._();
|
||||
@@ -162,9 +202,11 @@ ${emitPalette(tokens, "dark")}
|
||||
class SureTokenPalette {
|
||||
const SureTokenPalette({
|
||||
${COLOR_TOKENS.map(([name]) => ` required this.${name},`).join("\n")}
|
||||
${SHADOW_TOKENS.map(([name]) => ` required this.${name},`).join("\n")}
|
||||
});
|
||||
|
||||
${COLOR_TOKENS.map(([name]) => ` final Color ${name};`).join("\n")}
|
||||
${SHADOW_TOKENS.map(([name]) => ` final List<BoxShadow> ${name};`).join("\n")}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user