Skip to content

Commit 113c90c

Browse files
committed
refactor: UI primitives, widgets split, brand sources/, SCSS theming
Post-Part-A refinement pass. Seven coherent changes that tighten naming discipline, deduplicate view SCSS, isolate web sources from shared HUD vocabulary, and fix the silently-broken brand theming chain. 23 files staged. 1. Three new shared/components/ui/ primitives data-point.vue <UiDataPoint> label/value tile (sm | lg) chip.vue <UiChip> lowercase pill (solid | overflow) badge.vue <UiBadge> uppercase status pill (active | dim) Replaced ~12 inline duplicates across stats.vue, card.vue, and detail.vue. ~40 lines of duplicated SCSS removed from consumers. All props-in-DOM-out — correctly placed in ui/ (no side-effects). 2. shared/widgets/ split by kind widgets/audio-meter.vue to widgets/hud/audio-meter.vue widgets/live-readout.vue to widgets/ui/live-readout.vue Mirrors the shared/components/hud|ui split: audio-meter is OBS-coupled (HUD-domain), live-readout is domain-agnostic. Consumers import via @widgets/hud/* and @widgets/ui/*. 3. Filenames describe purpose, never repeat the alias kind (Rule G) sections/meta.vue to sections/stats.vue ui/metric.vue to ui/data-point.vue ui/status.vue to ui/status-dot.vue views/utils/parse-emphasis.js to views/utils/markup.js "meta" was ambiguous (metadata/HTML-meta/meta-programming). "metric of what?" / "status shown how?" tests failed — multi- word names answer the question. Modals stay single-word (base/preview/detail.vue) because @modals/ already supplies "modal" — base-modal.vue would be redundant. Bindings keep the kind: <BaseModal>, <UiDataPoint>, <StatsSection>. 4. Utils as topic-based libraries (Rule J) markup.js holds parseEmphasis as a named export. Future markup parsers (parseLinks, parseCodeSpans) live in the same file. Never one-function-per-file. Composables exempt — one useX per file because hooks own state and lifecycle. 5. Brand folder restructured to mirror src/shared/ @brand/hud/cam-log.vue to @brand/sources/hud/cam-log.vue @brand/animation/ to @brand/sources/animation/ @brand/scene/ to @brand/sources/scene/ Eliminates the clash where @brand/hud/ (web sources) and @hud/ (shared HUD primitives) both used "hud". brand-loader.js globs updated; resolveComponent() key pattern now /<brand>/sources/<type>/<id>.vue. Reserved slots documented for brand-private components/{hud,ui}/, composables/, widgets/{hud,ui}/ (created only when 2+ files exist, Rule F). 6. <UiIcon> multi-pool SVG discovery Previously globbed only @shared/assets/svg/*.svg. Now also globs /@*/assets/svg/*.svg — any brand can drop SVGs in its own assets/svg/ folder and reference them by filename. Brand SVGs override shared SVGs on filename collision (useful for brand-flavored overrides of shared primitives). 7. Brand theming — SCSS single source of truth Fixed three silently-broken things: - @<brand>/styles/_theme.scss was orphaned (never imported). - brand.js colors duplicated the SCSS values. - brand.js held only a subset of palette ranges. Fix: src/main.js eagerly globs /@*/styles/_theme.scss so Vite bundles every brand theme into global CSS. brand.js lost its colors field entirely. App.vue dropped brand_theme_vars and only applies .brand-<handle> on route change — the CSS cascade does the rest. brand-loader test flipped from "has colors object" to "does NOT carry colors field" (enforcement). Verified: grep brand-kyonax-on-tech dist/assets/*.css matches in the production bundle. Validation: 27/27 tests pass, 0 lint errors (7 pre-existing warnings in use-audio-analyzer.js), production build clean in 1.25s. Modified-by: Cristian D. Moreno (Kyonax) <kyonax25@gmail.com>
1 parent 0619e67 commit 113c90c

23 files changed

Lines changed: 477 additions & 384 deletions

File tree

@kyonax_on_tech/brand.js

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
* License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/
55
*
66
* Brand metadata for @kyonax_on_tech.
7-
* Single source of truth for identity, styling, and links.
7+
*
8+
* Identity + links + description live here. Colors and tokens live
9+
* in `styles/_theme.scss` — that file is the single source of truth
10+
* for the brand's visual palette. `brand.js` never duplicates it.
811
*/
912

1013
export default {
@@ -23,22 +26,4 @@ export default {
2326
youtube: 'https://www.youtube.com/channel/UCrNDk3dbrDQL9CWd1_fjyvA',
2427
github: 'https://github.com/kyonax',
2528
},
26-
27-
colors: {
28-
primary_50: 'hsl(48, 95%, 78%)',
29-
primary_100: 'hsl(47, 95%, 56%)',
30-
primary_200: 'hsl(49, 100%, 39%)',
31-
primary_300: 'hsl(48, 100%, 29%)',
32-
primary_400: 'hsl(45, 100%, 20%)',
33-
primary_500: 'hsl(35, 100%, 13%)',
34-
secondary_50: 'hsl(224, 95%, 78%)',
35-
secondary_100: 'hsl(224, 95%, 56%)',
36-
secondary_200: 'hsl(224, 74%, 45%)',
37-
neutral_50: 'hsl(0, 0%, 95%)',
38-
neutral_100: 'hsl(0, 0%, 85%)',
39-
neutral_200: 'hsl(0, 0%, 65%)',
40-
neutral_300: 'hsl(0, 0%, 45%)',
41-
neutral_400: 'hsl(0, 0%, 25%)',
42-
neutral_500: 'hsl(0, 0%, 0%)',
43-
},
4429
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ import HudFrame from '@hud/frame.vue';
7575
import HudTimer from '@hud/timer.vue';
7676
import { getBrand } from '@shared/brand-loader.js';
7777
import { VERSION_TAG } from '@shared/version.js';
78-
import AudioMeter from '@widgets/audio-meter.vue';
79-
import LiveReadout from '@widgets/live-readout.vue';
78+
import AudioMeter from '@widgets/hud/audio-meter.vue';
79+
import LiveReadout from '@widgets/ui/live-readout.vue';
8080
import { computed, ref } from 'vue';
8181
8282
const brand = getBrand('@kyonax_on_tech');

@kyonax_on_tech/styles/_theme.scss

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,46 @@
1-
/*
2-
* Copyright (c) 2026 Cristian D. Moreno — @Kyonax
3-
* This Source Code Form is subject to the terms of the Mozilla Public
4-
* License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/
5-
*
6-
* Brand theme for @kyonax_on_tech.
7-
* Gold + black cyberpunk palette. Overrides CSS custom properties
8-
* when this brand's web sources are rendered.
9-
*
10-
* Colors here match the global defaults intentionally — they exist
11-
* so this brand is fully containerized. A new brand can copy this
12-
* file and change the values without touching the global theme.
13-
*/
1+
// Copyright (c) 2026 Cristian D. Moreno — @Kyonax
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/
4+
//
5+
// Brand theme for @kyonax_on_tech — gold + black cyberpunk palette.
6+
//
7+
// Single source of truth for this brand's visual palette. Auto-loaded
8+
// by src/main.js (eager glob over every brand's theme SCSS file).
9+
// Active whenever App.vue applies `class="brand-kyonax-on-tech"` —
10+
// which happens on any route whose meta.brand resolves to this brand
11+
// (see src/router.js + src/App.vue).
12+
//
13+
// Override any CSS custom property declared by src/app/scss/abstracts
14+
// by redeclaring it inside this class block. Unlisted variables fall
15+
// back to the global default from the project's :root scope.
16+
//
17+
// A new brand can copy this file, rename the class selector to
18+
// `.brand-<handle>` (with the @-sign stripped and underscores turned
19+
// into dashes), and tune any subset of variables it wants to override.
1420

1521
.brand-kyonax-on-tech {
22+
// ── Primary (gold) ──────────────────────────────────────────
1623
--clr-primary-50: hsl(48, 95%, 78%);
1724
--clr-primary-100: hsl(47, 95%, 56%);
1825
--clr-primary-200: hsl(49, 100%, 39%);
1926
--clr-primary-300: hsl(48, 100%, 29%);
2027
--clr-primary-400: hsl(45, 100%, 20%);
2128
--clr-primary-500: hsl(35, 100%, 13%);
2229

30+
// ── Secondary (blue accent) ─────────────────────────────────
2331
--clr-secondary-50: hsl(224, 95%, 78%);
2432
--clr-secondary-100: hsl(224, 95%, 56%);
2533
--clr-secondary-200: hsl(224, 74%, 45%);
2634

35+
// ── Neutrals (black → white) ────────────────────────────────
2736
--clr-neutral-50: hsl(0, 0%, 95%);
2837
--clr-neutral-100: hsl(0, 0%, 85%);
2938
--clr-neutral-200: hsl(0, 0%, 65%);
3039
--clr-neutral-300: hsl(0, 0%, 45%);
3140
--clr-neutral-400: hsl(0, 0%, 25%);
3241
--clr-neutral-500: hsl(0, 0%, 0%);
3342

43+
// ── Status colors ────────────────────────────────────────────
3444
--clr-error-100: hsl(352, 69%, 48%);
3545
--clr-success-100: hsl(91, 62%, 44%);
3646
}

src/App.vue

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@
55
-->
66

77
<template>
8-
<div
9-
:class="brand_class"
10-
:style="brand_theme_vars"
11-
>
8+
<div :class="brand_class">
129
<router-view />
1310
</div>
1411
</template>
1512

1613
<script setup>
17-
import { BRANDS } from '@shared/brand-loader.js';
1814
import { computed } from 'vue';
1915
import { useRoute } from 'vue-router';
2016
2117
const route = useRoute();
2218
19+
// Brand theme is applied via a class selector — the actual CSS
20+
// custom property overrides live in `@<brand>/styles/_theme.scss`,
21+
// auto-loaded by src/main.js. This component only announces the
22+
// brand context; the CSS cascade does the rest.
2323
const brand_class = computed(() => {
2424
const handle = route.meta?.brand;
2525
@@ -29,28 +29,6 @@ const brand_class = computed(() => {
2929
3030
return `brand-${handle.replace('@', '').replace(/_/g, '-')}`;
3131
});
32-
33-
const brand_theme_vars = computed(() => {
34-
const handle = route.meta?.brand;
35-
36-
if (!handle) {
37-
return {};
38-
}
39-
40-
const brand = BRANDS.find((b) => b.handle === handle);
41-
42-
if (!brand?.colors || !Object.keys(brand.colors).length) {
43-
return {};
44-
}
45-
46-
const vars = {};
47-
48-
for (const [key, value] of Object.entries(brand.colors)) {
49-
vars[`--clr-${key.replace(/_/g, '-')}`] = value;
50-
}
51-
52-
return vars;
53-
});
5432
</script>
5533
5634
<style lang="scss">

src/main.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import { createApp } from 'vue';
99
import App from './App.vue';
1010
import { router } from './router.js';
1111

12+
// Auto-load every brand's theme SCSS so the global CSS bundle
13+
// carries each brand's `.brand-<handle>` selector with its CSS
14+
// custom property overrides. Vite processes every matching file
15+
// eagerly at build time — the brand theme is the single source
16+
// of truth for colors/tokens per brand.
17+
import.meta.glob(
18+
'/@*/styles/_theme.scss',
19+
{ eager: true },
20+
);
21+
1222
const app = createApp(App);
1323

1424
app.use(router);

src/shared/brand-loader.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ const source_modules = import.meta.glob(
1919
{ eager: true, import: 'default' },
2020
);
2121

22-
const hud_components = import.meta.glob('/@*/hud/*.vue');
23-
const animation_components = import.meta.glob('/@*/animation/*.vue');
24-
const scene_components = import.meta.glob('/@*/scene/*.vue');
22+
const hud_components = import.meta.glob('/@*/sources/hud/*.vue');
23+
const animation_components = import.meta.glob(
24+
'/@*/sources/animation/*.vue',
25+
);
26+
const scene_components = import.meta.glob('/@*/sources/scene/*.vue');
2527

2628
export const BRANDS = Object.values(brand_modules);
2729
export const SOURCES = Object.values(source_modules).flat();
@@ -51,7 +53,7 @@ export function resolveComponent(source) {
5153
return null;
5254
}
5355

54-
const key = `/${source.brand}/${source.type}/${source.id}.vue`;
56+
const key = `/${source.brand}/sources/${source.type}/${source.id}.vue`;
5557

5658
return components[key] || null;
5759
}

src/shared/brand-loader.test.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,17 @@ describe('Brand loader — BRANDS', () => {
5757
);
5858

5959
it.each(BRANDS)(
60-
'brand $handle has colors object',
60+
'brand $handle has links object',
6161
(brand) => {
62-
expect(brand).toHaveProperty('colors');
63-
expect(typeof brand.colors).toBe('object');
62+
expect(brand).toHaveProperty('links');
63+
expect(typeof brand.links).toBe('object');
64+
},
65+
);
66+
67+
it.each(BRANDS)(
68+
'brand $handle does NOT carry a colors field (theme lives in styles/_theme.scss)',
69+
(brand) => {
70+
expect(brand.colors).toBeUndefined();
6471
},
6572
);
6673

src/shared/components/hud/timer.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
class="recording-timer"
1010
:class="{ recording: is_recording }"
1111
>
12-
<UiStatus :active="is_recording" />
12+
<UiStatusDot :active="is_recording" />
1313
<span class="rec" :class="{ active: is_recording }">REC</span>
1414
<span class="mode">MODE:</span>
1515
<span class="time">{{ elapsed_time }}</span>
1616
</div>
1717
</template>
1818

1919
<script setup>
20-
import UiStatus from '@ui/status.vue';
20+
import UiStatusDot from '@ui/status-dot.vue';
2121
2222
defineProps({
2323
is_recording: {

src/shared/components/ui/badge.vue

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<!--
2+
Copyright (c) 2026 Cristian D. Moreno — @Kyonax
3+
This Source Code Form is subject to the terms of the Mozilla Public
4+
License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/
5+
6+
ui-badge — HUD status pill with primary/neutral variant. Used for
7+
overlay status indicators (ready, planned), metadata flags, and
8+
any small standalone label that needs a bordered box.
9+
10+
Props:
11+
variant — "active" (default, gold on gold border)
12+
"dim" (muted text on muted border, for planned/inactive)
13+
-->
14+
15+
<template>
16+
<span class="ui-badge" :class="variant_class">
17+
<slot />
18+
</span>
19+
</template>
20+
21+
<script setup>
22+
import { computed } from 'vue';
23+
24+
const props = defineProps({
25+
variant: {
26+
type: String,
27+
default: 'active',
28+
validator: (v) => ['active', 'dim'].includes(v),
29+
},
30+
});
31+
32+
const variant_class = computed(() => `ui-badge--${props.variant}`);
33+
</script>
34+
35+
<style scoped lang="scss">
36+
@use "@app/scss/abstracts/mixins" as *;
37+
38+
.ui-badge {
39+
@include hud-label-base;
40+
position: static;
41+
display: inline-block;
42+
padding: 0.25em 0.65em;
43+
font-size: var(--fs-150);
44+
letter-spacing: 0.18em;
45+
white-space: nowrap;
46+
border: 1px solid var(--clr-primary-100);
47+
color: var(--clr-primary-100);
48+
}
49+
50+
.ui-badge--dim {
51+
border-color: var(--clr-neutral-200);
52+
color: var(--clr-neutral-200);
53+
}
54+
</style>

src/shared/components/ui/chip.vue

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<!--
2+
Copyright (c) 2026 Cristian D. Moreno — @Kyonax
3+
This Source Code Form is subject to the terms of the Mozilla Public
4+
License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/
5+
6+
ui-chip — lowercase pill chip with gold text on translucent gold
7+
background. Used for category / keyword / tag-like metadata in a
8+
flex row. Naming follows Material Design / PrimeVue / Quasar —
9+
"chip" is the standard web-UI term for this primitive (distinct
10+
from HTML tags, distinct from <UiBadge> which is uppercase status).
11+
12+
Renders as a span so callers control the surrounding layout
13+
(typically a flex container).
14+
15+
Props:
16+
variant — "solid" (default) | "overflow" (dashed border +
17+
dimmed — used for "+N more" indicators at the end
18+
of a list).
19+
-->
20+
21+
<template>
22+
<span class="ui-chip" :class="variant_class">
23+
<slot />
24+
</span>
25+
</template>
26+
27+
<script setup>
28+
import { computed } from 'vue';
29+
30+
const props = defineProps({
31+
variant: {
32+
type: String,
33+
default: 'solid',
34+
validator: (v) => ['solid', 'overflow'].includes(v),
35+
},
36+
});
37+
38+
const variant_class = computed(() => `ui-chip--${props.variant}`);
39+
</script>
40+
41+
<style scoped lang="scss">
42+
.ui-chip {
43+
display: inline-block;
44+
padding: 0.25em 0.6em;
45+
font-family: var(--font-mono);
46+
font-size: var(--fs-175);
47+
letter-spacing: 0.1em;
48+
color: var(--clr-primary-100);
49+
background: rgba(255, 215, 0, 0.06);
50+
border: 1px solid var(--clr-border-100);
51+
text-transform: lowercase;
52+
white-space: nowrap;
53+
}
54+
55+
.ui-chip--overflow {
56+
opacity: 0.5;
57+
border-style: dashed;
58+
}
59+
</style>

0 commit comments

Comments
 (0)