From d6c18a623be4886caaa1d55590bb9db59d55c451 Mon Sep 17 00:00:00 2001 From: Norman Niati Date: Wed, 8 Jul 2026 13:38:22 +0200 Subject: [PATCH 1/2] feat(quote): theme-aware breakdown donut Replace the hard-coded hex palette (TAB_TYPES, SUB_PALETTE, drill fallback) with Vuetify semantic theme tokens so the donut follows the active theme. Colors now go through style bindings because SVG presentation attributes do not resolve var(). Modernize the ring (thinner annulus, centred KPI, hairline slice separation) and align the legend on tabular figures. --- ui/src/__tests__/quoteFormatters.test.js | 4 +- ui/src/quoteFormatters.js | 20 +++-- ui/src/views/QuoteBreakdown.vue | 108 +++++++++++++++++------ 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/ui/src/__tests__/quoteFormatters.test.js b/ui/src/__tests__/quoteFormatters.test.js index 0d02ce94..95005c87 100644 --- a/ui/src/__tests__/quoteFormatters.test.js +++ b/ui/src/__tests__/quoteFormatters.test.js @@ -319,7 +319,9 @@ describe('quoteFormatters', () => { for (const t of TAB_TYPES) { expect(t.icon).toMatch(/^mdi-/) expect(t.listField).toMatch(/s$/) - expect(t.color).toMatch(/^#[0-9A-F]{6}$/i) + // Theme-token expression (no hard-coded hex) so the donut and + // legend follow the active Vuetify theme. + expect(t.color).toMatch(/^rgb\(var\(--v-theme-[a-z-]+\)\)$/) } }) }) diff --git a/ui/src/quoteFormatters.js b/ui/src/quoteFormatters.js index 429a289d..fb1f8bda 100644 --- a/ui/src/quoteFormatters.js +++ b/ui/src/quoteFormatters.js @@ -92,17 +92,19 @@ export function scaleCost(cost, period) { /** * Tab metadata — drives the v-tabs/v-window structure of QuoteView AND - * the cost-breakdown donut. The colour is a fixed hex picked from - * Vuetify's default palette so plugin-prov stays theme-agnostic (the - * donut renders inside its own SVG, outside Vuetify's CSS chrome). + * the cost-breakdown donut. Colours are Vuetify semantic theme tokens + * (CSS variable expressions) so the donut follows the active theme — + * they must be applied through inline `style` bindings (`fill` / + * `background-color`), not SVG presentation attributes, because + * attribute values don't resolve `var()`. */ export const TAB_TYPES = [ - { key: 'instance', icon: 'mdi-server', listField: 'instances', color: '#1976D2' }, - { key: 'database', icon: 'mdi-database', listField: 'databases', color: '#388E3C' }, - { key: 'container', icon: 'mdi-docker', listField: 'containers', color: '#00ACC1' }, - { key: 'function', icon: 'mdi-lambda', listField: 'functions', color: '#7B1FA2' }, - { key: 'storage', icon: 'mdi-harddisk', listField: 'storages', color: '#F57C00' }, - { key: 'support', icon: 'mdi-lifebuoy', listField: 'supports', color: '#D32F2F' }, + { key: 'instance', icon: 'mdi-server', listField: 'instances', color: 'rgb(var(--v-theme-primary))' }, + { key: 'database', icon: 'mdi-database', listField: 'databases', color: 'rgb(var(--v-theme-success))' }, + { key: 'container', icon: 'mdi-docker', listField: 'containers', color: 'rgb(var(--v-theme-info))' }, + { key: 'function', icon: 'mdi-lambda', listField: 'functions', color: 'rgb(var(--v-theme-secondary))' }, + { key: 'storage', icon: 'mdi-harddisk', listField: 'storages', color: 'rgb(var(--v-theme-warning))' }, + { key: 'support', icon: 'mdi-lifebuoy', listField: 'supports', color: 'rgb(var(--v-theme-error))' }, ] /** diff --git a/ui/src/views/QuoteBreakdown.vue b/ui/src/views/QuoteBreakdown.vue index 39dda834..31ec7706 100644 --- a/ui/src/views/QuoteBreakdown.vue +++ b/ui/src/views/QuoteBreakdown.vue @@ -12,14 +12,17 @@
+ @@ -71,9 +74,11 @@ const props = defineProps({ const { t } = useI18nStore() -const size = 140 -const r = 60 -const ri = 36 +/* Slightly larger donut with a thinner ring — reads as a modern + * "annulus + centred KPI" rather than the legacy pie. */ +const size = 168 +const r = 76 +const ri = 57 const cx = size / 2 const cy = size / 2 @@ -99,13 +104,17 @@ const DRILL_DIMENSIONS = { storage: { field: 'type', getter: (r) => r.price?.type?.name || r.price?.type?.code }, } -/* Sub-segment palette. Picked to be distinct against any of the - * top-level colors so the eye can tell drilled-down from root. */ -const SUB_PALETTE = [ - '#1976D2', '#0288D1', '#00897B', '#43A047', '#7CB342', - '#C0CA33', '#FDD835', '#FFB300', '#FB8C00', '#F4511E', - '#E53935', '#D81B60', '#8E24AA', '#5E35B1', '#3949AB', -] +/* Sub-segment palette, derived from the theme's semantic tokens so the + * drilled view follows the active theme like everything else. The first + * pass cycles the 6 semantic colours pure, later passes soften them + * towards the surface — enough contrast between neighbours without any + * hard-coded colour. */ +const SUB_BASES = ['primary', 'info', 'success', 'warning', 'error', 'secondary'] +const SUB_PALETTE = [100, 70, 45].flatMap((mix) => + SUB_BASES.map((base) => mix === 100 + ? `rgb(var(--v-theme-${base}))` + : `color-mix(in srgb, rgb(var(--v-theme-${base})) ${mix}%, rgb(var(--v-theme-surface)))`), +) function isDrillable(key) { return !!DRILL_DIMENSIONS[key] @@ -126,7 +135,7 @@ watch(() => props.config, () => { drill.value = null }) const parentColor = computed(() => { const tab = TAB_TYPES.find((t) => t.key === drill.value) - return tab ? tab.color : '#888' + return tab ? tab.color : 'rgba(var(--v-theme-on-surface), .5)' }) const drillTitle = computed(() => { @@ -235,36 +244,62 @@ defineExpose({ drill, isDrillable, onSliceClick, breakdown, subBreakdown, active From 7be2aade319bd878edfe1b202d8d5004499567b9 Mon Sep 17 00:00:00 2001 From: Norman Niati Date: Wed, 8 Jul 2026 13:38:37 +0200 Subject: [PATCH 2/2] feat(quote): redesign header, cost card, tabs and table Visual/layout pass only, all actions and data flows preserved: - header: provider tile, quote name + description pill, total cost as a key figure, period/refresh/refresh-prices/export grouped in a pill toolbar - cost card: modern ring + a row of stat tiles (instances, total vCPU, total RAM) next to the existing cost/CO2 toggle - tabs: count badges, primary underline, no uppercase transform - per-tab toolbar: pill search field, filtered/total counter pill, discreet danger delete-all - table: comfortable density, tabular numerals, mono type chip, location pill, row actions revealed on hover (pointer devices only) All colors come from theme tokens (Vuetify semantic variables and the shared .lj-surface design variables) so the 11 presets render without any hard-coded hex. --- ui/src/views/QuoteView.vue | 556 +++++++++++++++++++++++++++++++------ 1 file changed, 466 insertions(+), 90 deletions(-) diff --git a/ui/src/views/QuoteView.vue b/ui/src/views/QuoteView.vue index aeef1f9b..122b14da 100644 --- a/ui/src/views/QuoteView.vue +++ b/ui/src/views/QuoteView.vue @@ -1,81 +1,94 @@
+
{{ error }}