diff --git a/packages/base/color.gts b/packages/base/color.gts index 3d1977d5004..9229cc4bf99 100644 --- a/packages/base/color.gts +++ b/packages/base/color.gts @@ -1,9 +1,10 @@ -import { Component } from '@cardstack/base/card-api'; -import StringField from '@cardstack/base/string'; import { Swatch } from '@cardstack/boxel-ui/components'; import { markdownEscape } from '@cardstack/boxel-ui/helpers'; import PaletteIcon from '@cardstack/boxel-icons/palette'; + +import { Component } from './card-api'; import ColorPickerField from './color-field/components/color-picker-field'; +import StringField from './string'; class View extends Component { diff --git a/packages/base/number/components/gauge.gts b/packages/base/number/components/gauge.gts index f1374d06b51..1795bec6c48 100644 --- a/packages/base/number/components/gauge.gts +++ b/packages/base/number/components/gauge.gts @@ -51,12 +51,12 @@ export class GaugeAtom extends GlimmerComponent { const warningThreshold = this.options.warningThreshold; if (dangerThreshold !== undefined && numericValue >= dangerThreshold) { - return 'var(--destructive, #ef4444)'; + return 'var(--destructive)'; } if (warningThreshold !== undefined && numericValue >= warningThreshold) { - return 'var(--warning, #f59e0b)'; + return 'var(--warning)'; } - return 'var(--primary, #3b82f6)'; + return 'var(--primary)'; } get needleRotation() { @@ -86,7 +86,7 @@ export class GaugeAtom extends GlimmerComponent { @@ -105,13 +105,13 @@ export class GaugeAtom extends GlimmerComponent { ) }} /> - + { const warningThreshold = this.options.warningThreshold; if (dangerThreshold !== undefined && numericValue >= dangerThreshold) { - return 'var(--destructive, #ef4444)'; + return 'var(--destructive)'; } if (warningThreshold !== undefined && numericValue >= warningThreshold) { - return 'var(--warning, #f59e0b)'; + return 'var(--warning)'; } - return 'var(--primary, #3b82f6)'; + return 'var(--primary)'; } get needleRotation() { @@ -209,7 +209,7 @@ export class GaugeEmbedded extends GlimmerComponent { @@ -233,7 +233,7 @@ export class GaugeEmbedded extends GlimmerComponent { y='115' text-anchor='start' font-size='10' - fill='var(--muted-foreground, #94a3b8)' + fill='var(--muted-foreground)' > {{this.minValue}} @@ -242,17 +242,17 @@ export class GaugeEmbedded extends GlimmerComponent { y='115' text-anchor='end' font-size='10' - fill='var(--muted-foreground, #94a3b8)' + fill='var(--muted-foreground)' > {{this.maxValue}} - + { get fillColor() { if (this.options.useGradient === false) { - return 'var(--primary, #3b82f6)'; + return 'var(--primary)'; } - const p = this.percentage; - if (p <= 25) { - return 'var(--destructive, #ef4444)'; - } - if (p <= 50) { - return 'var(--warning, #f59e0b)'; - } - if (p <= 75) { - return 'var(--accent, #eab308)'; - } - return 'var(--success, #22c55e)'; + return statusRampColor(this.percentage); } get fillStyle() { @@ -83,7 +74,11 @@ export class ProgressBarAtom extends GlimmerComponent { diff --git a/packages/base/number/components/score.gts b/packages/base/number/components/score.gts index b3835675d21..fa18955f03d 100644 --- a/packages/base/number/components/score.gts +++ b/packages/base/number/components/score.gts @@ -5,6 +5,7 @@ import { getFormattedDisplayValue, getNumericValue, calculatePercentage, + statusRampColor, } from '../util/index'; export interface ScoreOptions { @@ -56,11 +57,7 @@ export class ScoreAtom extends GlimmerComponent { } get iconColor() { - const p = this.percentage; - if (p >= 75) return 'var(--success, #22c55e)'; - if (p >= 50) return 'var(--accent, #eab308)'; - if (p >= 25) return 'var(--warning, #f59e0b)'; - return 'var(--destructive, #ef4444)'; + return statusRampColor(this.percentage); } get displayValue() { @@ -89,9 +86,9 @@ export class ScoreAtom extends GlimmerComponent { align-items: center; gap: 0.375rem; padding: 0.25rem 0.625rem; - background: var(--muted, #f1f5f9); + background: var(--muted); border-radius: 999px; - border: 1px solid var(--border, #e2e8f0); + border: 1px solid var(--border); } .trophy-icon { width: 1rem; @@ -141,11 +138,7 @@ export class ScoreEmbedded extends GlimmerComponent { } get scoreColor() { - const p = this.percentage; - if (p >= 75) return 'var(--success, #22c55e)'; - if (p >= 50) return 'var(--accent, #eab308)'; - if (p >= 25) return 'var(--warning, #f59e0b)'; - return 'var(--destructive, #ef4444)'; + return statusRampColor(this.percentage); } get tierLabel() { @@ -175,6 +168,7 @@ export class ScoreEmbedded extends GlimmerComponent {
{ gap: 1rem; padding: 1.5rem; border-radius: 1rem; - background: linear-gradient( - 135deg, - var(--card, #ffffff) 0%, - var(--muted, #f8fafc) 100% - ); - border: 2px solid var(--border, #e2e8f0); + background: linear-gradient(135deg, var(--card) 0%, var(--muted) 100%); + border: 2px solid var(--border); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -1px rgb(0 0 0 / 0.03); @@ -230,7 +220,7 @@ export class ScoreEmbedded extends GlimmerComponent { font-weight: 600; letter-spacing: 0.025em; text-transform: uppercase; - color: var(--muted-foreground, #64748b); + color: var(--muted-foreground); } .score-badge { display: inline-flex; @@ -272,15 +262,15 @@ export class ScoreEmbedded extends GlimmerComponent { .score-max { font-size: 1.5rem; font-weight: 600; - color: var(--muted-foreground, #94a3b8); + color: var(--muted-foreground); } .score-percentile { font-size: 0.875rem; font-weight: 600; padding: 0.375rem 0.75rem; - background: var(--muted, #f1f5f9); + background: var(--muted); border-radius: 0.5rem; - color: var(--foreground, #0f172a); + color: var(--foreground); white-space: nowrap; } diff --git a/packages/base/number/components/stat.gts b/packages/base/number/components/stat.gts index 629e5d91dad..17f8af22d99 100644 --- a/packages/base/number/components/stat.gts +++ b/packages/base/number/components/stat.gts @@ -55,9 +55,9 @@ export class StatAtom extends GlimmerComponent { align-items: center; gap: 0.375rem; padding: 0.25rem 0.625rem; - background: var(--muted, #f1f5f9); + background: var(--muted); border-radius: 999px; - border: 1px solid var(--border, #e2e8f0); + border: 1px solid var(--border); } .stat-icon-pill { display: inline-flex; @@ -66,14 +66,14 @@ export class StatAtom extends GlimmerComponent { width: 1.25rem; height: 1.25rem; border-radius: 0.375rem; - background: var(--primary, #3b82f6); - color: var(--primary-foreground, #ffffff); + background: var(--primary); + color: var(--primary-foreground); flex-shrink: 0; } .stat-value { font-size: 0.875rem; font-weight: 700; - color: var(--foreground, var(--boxel-dark)); + color: var(--foreground); line-height: 1; } @@ -147,12 +147,8 @@ export class StatEmbedded extends GlimmerComponent { gap: 0.75rem; padding: 1.25rem; border-radius: 0.75rem; - background: linear-gradient( - 135deg, - var(--card, #ffffff) 0%, - var(--muted, #f8fafc) 100% - ); - border: 1px solid var(--border, #e2e8f0); + background: linear-gradient(135deg, var(--card) 0%, var(--muted) 100%); + border: 1px solid var(--border); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -1px rgb(0 0 0 / 0.03); @@ -162,7 +158,7 @@ export class StatEmbedded extends GlimmerComponent { box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05); - border-color: var(--ring, #cbd5e1); + border-color: var(--ring); } .stat-header { display: flex; @@ -175,7 +171,7 @@ export class StatEmbedded extends GlimmerComponent { font-weight: 600; letter-spacing: 0.025em; text-transform: uppercase; - color: var(--muted-foreground, #64748b); + color: var(--muted-foreground); } .stat-icon-container { display: inline-flex; @@ -184,8 +180,8 @@ export class StatEmbedded extends GlimmerComponent { width: 2.25rem; height: 2.25rem; border-radius: 0.5rem; - background: var(--primary, #3b82f6); - color: var(--primary-foreground, #ffffff); + background: var(--primary); + color: var(--primary-foreground); box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); } .stat-main { @@ -197,13 +193,13 @@ export class StatEmbedded extends GlimmerComponent { font-size: 2.5rem; font-weight: 800; line-height: 1; - color: var(--foreground, #0f172a); + color: var(--foreground); letter-spacing: -0.025em; } .stat-subtitle { font-size: 0.875rem; font-weight: 600; - color: var(--success, #22c55e); + color: var(--success); display: flex; align-items: center; gap: 0.25rem; @@ -213,17 +209,17 @@ export class StatEmbedded extends GlimmerComponent { align-items: center; gap: 0.5rem; padding-top: 0.5rem; - border-top: 1px solid var(--border, #e2e8f0); + border-top: 1px solid var(--border); } .stat-range-label { font-size: 0.75rem; font-weight: 600; - color: var(--muted-foreground, #94a3b8); + color: var(--muted-foreground); } .stat-range-value { font-size: 0.75rem; font-weight: 500; - color: var(--foreground, #0f172a); + color: var(--foreground); } diff --git a/packages/base/number/util/index.gts b/packages/base/number/util/index.gts index d06804b6986..f73c1c99485 100644 --- a/packages/base/number/util/index.gts +++ b/packages/base/number/util/index.gts @@ -61,3 +61,23 @@ export function calculatePercentage( export function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } + +/** + * Status color for a 0–100 progress value, in four bands from destructive up + * to success. The third band has no status token of its own, so it mixes its + * two neighbours: that keeps the ramp ordered under any theme, whatever hues + * the theme gives warning and success. Thresholds are inclusive, so 75 reads + * as success. + */ +export function statusRampColor(percentage: number): string { + if (percentage >= 75) { + return 'var(--success)'; + } + if (percentage >= 50) { + return 'color-mix(in oklch, var(--warning), var(--success))'; + } + if (percentage >= 25) { + return 'var(--warning)'; + } + return 'var(--destructive)'; +} diff --git a/packages/base/package.json b/packages/base/package.json index fce09db8dec..511e068d163 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -7,6 +7,7 @@ "@babel/core": "catalog:", "@cardstack/boxel-icons": "workspace:*", "@cardstack/boxel-ui": "workspace:*", + "@cardstack/local-types": "workspace:*", "@cardstack/runtime-common": "workspace:*", "@glimmer/component": "catalog:", "@glimmer/tracking": "^1.0.4", diff --git a/packages/base/structured-theme-variables.gts b/packages/base/structured-theme-variables.gts index 7629b0462c3..643c41f55bc 100644 --- a/packages/base/structured-theme-variables.gts +++ b/packages/base/structured-theme-variables.gts @@ -540,7 +540,12 @@ export default class ThemeVarField extends FieldDef { }); @field success = contains(ColorField, { description: describeColor( - 'Success/positive feedback color. Not standard shadcn: consumers fall back to primary when unset. Do not use as foreground color.', + 'Success/positive feedback color. Not standard shadcn: falls back to the fixed Boxel status palette green (--boxel-success) when unset.', + ), + }); + @field warning = contains(ColorField, { + description: describeColor( + 'Warning/caution feedback color. Not standard shadcn: falls back to the fixed Boxel status palette yellow (--boxel-warning) when unset.', ), }); @field border = contains(ColorField, { @@ -632,6 +637,30 @@ export default class ThemeVarField extends FieldDef { @field trackingNormal = contains(CSSValueField, { description: 'Specifies letter-spacing base value.', }); + // box-shadow primitives, stored so tweakcn themes round-trip losslessly. + // The composed shadow scale below does not derive from them; editing these + // has no effect on rendered shadows. + @field shadowX = contains(CSSValueField, { + description: 'Horizontal shadow offset (e.g. 0, 3px).', + }); + @field shadowY = contains(CSSValueField, { + description: 'Vertical shadow offset (e.g. 1px, 3px).', + }); + @field shadowBlur = contains(CSSValueField, { + description: 'Shadow blur radius (e.g. 3px, 0px).', + }); + @field shadowSpread = contains(CSSValueField, { + description: 'Shadow spread radius (e.g. 0px, -1px).', + }); + @field shadowOpacity = contains(CSSValueField, { + description: 'Shadow opacity as a 0-1 number (e.g. 0.1).', + }); + @field shadowColor = contains(ColorField, { + description: describeColor( + 'Shadow base color from tweakcn; not applied to the shadow scale.', + ), + }); + // box-shadow variables @field shadow2xs = contains(CSSValueField, { description: 'Smallest shadow depth.', @@ -709,6 +738,7 @@ export default class ThemeVarField extends FieldDef { 'destructive', 'destructiveForeground', 'success', + 'warning', ]; private chartColors = ['chart1', 'chart2', 'chart3', 'chart4', 'chart5']; private sidebarColors = [ @@ -721,7 +751,17 @@ export default class ThemeVarField extends FieldDef { 'sidebarBorder', 'sidebarRing', ]; - private boxShadows = ['shadowSm', 'shadowMd', 'shadowLg', 'shadowXl']; + private boxShadows = [ + 'shadowColor', + 'shadow2xs', + 'shadowXs', + 'shadowSm', + 'shadow', + 'shadowMd', + 'shadowLg', + 'shadowXl', + 'shadow2xl', + ]; get fieldGroups() { return [ { @@ -1015,6 +1055,13 @@ export default class ThemeVarField extends FieldDef { > <@fields.success /> + + <@fields.warning /> +
@@ -1272,6 +1319,60 @@ export default class ThemeVarField extends FieldDef { <@fields.shadow2xl /> +
Imported shadow primitives
+

+ Kept so a tweakcn export round-trips. The shadow scale above is not + derived from them, so editing these has no effect on rendered + shadows. +

+
+ + <@fields.shadowX /> + + + <@fields.shadowY /> + +
+
+ + <@fields.shadowBlur /> + + + <@fields.shadowSpread /> + +
+
+ + <@fields.shadowOpacity /> + + + <@fields.shadowColor /> + +
@@ -1331,6 +1432,14 @@ export default class ThemeVarField extends FieldDef { padding-bottom: var(--boxel-sp-xs); border-bottom: 1px solid var(--border, var(--boxel-border-color)); } + .theme-var-edit-subheading { + margin: var(--boxel-sp-sm) 0 0; + font-size: var(--boxel-font-size-xs); + font-weight: 600; + color: var(--muted-foreground, var(--boxel-400)); + text-transform: uppercase; + letter-spacing: 0.04em; + } .theme-var-edit-row { display: flex; flex-direction: column; diff --git a/packages/base/tsconfig.json b/packages/base/tsconfig.json index d71e0728fdf..4a2aa5d06cb 100644 --- a/packages/base/tsconfig.json +++ b/packages/base/tsconfig.json @@ -21,6 +21,7 @@ "baseUrl": ".", "strict": true, "experimentalDecorators": true, + "types": ["@cardstack/local-types"], "paths": { "@cardstack/boxel-ui": ["../boxel-ui/declarations"], "@cardstack/boxel-ui/*": ["../boxel-ui/declarations/*"], diff --git a/packages/boxel-ui/src/components/switch/index.gts b/packages/boxel-ui/src/components/switch/index.gts index 5556c1eb115..5363efe62d6 100644 --- a/packages/boxel-ui/src/components/switch/index.gts +++ b/packages/boxel-ui/src/components/switch/index.gts @@ -112,7 +112,7 @@ const Switch: TemplateOnlyComponent =