diff --git a/.env.example b/.env.example index 5f41b08..4d785d1 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,26 @@ +# Copyright (c) 2026 Cristian D. Moreno — @Kyonax +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + +# __ __ ____ +# / /_/ / ___ _ _____ ___ __/ / /_ +# / __/ _ \/ -_) | |/ / _ `/ // / / __/ +# \__/_//_/\__/ |___/\_,_/\_,_/_/\__/ +# +# .env.example — Environment variable template +# 2026-04-17 +# +# Template for the runtime env vars consumed by the Vue app. +# Copy to .env and fill in your values, VITE_ prefix is required. +# +# VITE_OBS_WS_HOST WebSocket host +# VITE_OBS_WS_PORT WebSocket port +# VITE_OBS_WS_PASS WebSocket password +# VITE_OBS_WS_LAN LAN address +# +# Cristian D. Moreno (Kyonax) +# kyonax.corp@gmail.com + VITE_OBS_WS_HOST=127.0.0.1 VITE_OBS_WS_PORT=4455 VITE_OBS_WS_PASS= diff --git a/.gitattributes b/.gitattributes index e00d8c6..910769d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,25 @@ +# Copyright (c) 2026 Cristian D. Moreno — @Kyonax +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + +# __ __ __ __ +# / /_/ / ___ / /__ _/ / +# / __/ _ \/ -_) / / _ `/ _ \ +# \__/_//_/\__/ /_/\_,_/_.__/ +# +# .gitattributes — Git file handling rules +# 2026-04-17 +# +# Controls how Git handles specific files on checkout and commit. +# The logo.txt rule prevents Windows CRLF from corrupting the +# Unicode box-drawing glyphs in the ASCII logo. +# +# logo.txt: UTF-8 + LF enforcement +# Default: auto-normalize text to LF +# +# Cristian D. Moreno (Kyonax) +# kyonax.corp@gmail.com + # Preserve box-drawing characters and line endings in the brand logo. # Windows clones would otherwise apply CRLF conversion and potentially # corrupt the Unicode frame glyphs. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 93ea7bd..0b65670 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,23 @@ + + diff --git a/src/main.js b/src/main.js index 136557c..1c0efa6 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,16 @@ import { createApp } from 'vue'; import App from './App.vue'; import { router } from './router.js'; +// Auto-load every brand's theme SCSS so the global CSS bundle +// carries each brand's `.brand-` selector with its CSS +// custom property overrides. Vite processes every matching file +// eagerly at build time — the brand theme is the single source +// of truth for colors/tokens per brand. +import.meta.glob( + '/@*/styles/_theme.scss', + { eager: true }, +); + const app = createApp(App); app.use(router); diff --git a/src/router.js b/src/router.js index 8b742fc..b131f15 100644 --- a/src/router.js +++ b/src/router.js @@ -4,21 +4,37 @@ * License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ */ +import { resolveComponent, SOURCES } from '@shared/brand-loader.js'; import { createRouter, createWebHistory } from 'vue-router'; +const source_routes = SOURCES + .map((source) => { + const loader = resolveComponent(source); + + if (!loader) { + return null; + } + + return { + path: source.path, + name: `${source.brand.replace('@', '')}-${source.id}`, + component: loader, + meta: { + brand: source.brand, + source_id: source.id, + source_type: source.type, + }, + }; + }) + .filter(Boolean); + const routes = [ { path: '/', name: 'home', - component: () => import('./views/home.vue'), - }, - { - path: '/@kyonax_on_tech/cam-person', - name: 'kyonax-cam-person', - component: () => import( - './brands/kyonax-on-tech/cam-person.vue' - ), + component: () => import('@views/home.vue'), }, + ...source_routes, { path: '/:pathMatch(.*)*', name: 'blank', diff --git a/src/shared/assets/svg/corner-bracket.svg b/src/shared/assets/svg/corner-bracket.svg new file mode 100644 index 0000000..eec2a68 --- /dev/null +++ b/src/shared/assets/svg/corner-bracket.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/brand-loader.js b/src/shared/brand-loader.js new file mode 100644 index 0000000..da536ba --- /dev/null +++ b/src/shared/brand-loader.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2026 Cristian D. Moreno — @Kyonax + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + * + * brand-loader — auto-discovers @brand/ folders at the project + * root and merges their metadata + source registries into a + * single runtime object. Uses import.meta.glob for static + * analysis by Vite's bundler. + */ + +const brand_modules = import.meta.glob( + '/@*/brand.js', + { eager: true, import: 'default' }, +); + +const source_modules = import.meta.glob( + '/@*/sources.js', + { eager: true, import: 'default' }, +); + +const hud_components = import.meta.glob('/@*/sources/hud/*.vue'); +const animation_components = import.meta.glob( + '/@*/sources/animation/*.vue', +); +const scene_components = import.meta.glob('/@*/sources/scene/*.vue'); + +export const BRANDS = Object.values(brand_modules); +export const SOURCES = Object.values(source_modules).flat(); + +/** + * Get a brand's metadata by handle. + */ +export function getBrand(handle) { + return BRANDS.find((b) => b.handle === handle) || null; +} + +/** + * Resolve the lazy component loader for a given source entry. + * Matches source.type + source.brand + source.id to the glob + * results. + */ +export function resolveComponent(source) { + const type_map = { + hud: hud_components, + animation: animation_components, + scene: scene_components, + }; + + const components = type_map[source.type]; + + if (!components) { + return null; + } + + const key = `/${source.brand}/sources/${source.type}/${source.id}.vue`; + + return components[key] || null; +} diff --git a/src/shared/brand-loader.test.js b/src/shared/brand-loader.test.js new file mode 100644 index 0000000..137df52 --- /dev/null +++ b/src/shared/brand-loader.test.js @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2026 Cristian D. Moreno — @Kyonax + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + * + * Tests for the brand-loader auto-discovery engine. + * Validates brand metadata and web source schema. + */ + +import { BRANDS, SOURCES } from '@shared/brand-loader.js'; +import { describe, expect, it } from 'vitest'; + +const REQUIRED_SOURCE_FIELDS = [ + 'id', + 'type', + 'brand', + 'name', + 'description', + 'use_cases', + 'path', + 'width', + 'height', + 'fps', + 'requires', + 'triggers', + 'status', +]; + +const ALLOWED_STATUSES = ['ready', 'planned']; +const ALLOWED_TYPES = ['hud', 'animation', 'scene']; +const EM_DASH = '\u2014'; + +describe('Brand loader — BRANDS', () => { + it('discovers at least one brand', () => { + expect(BRANDS.length).toBeGreaterThan(0); + }); + + it.each(BRANDS)( + 'brand $handle has handle, name, description', + (brand) => { + expect(brand).toHaveProperty('handle'); + expect(brand).toHaveProperty('name'); + expect(brand).toHaveProperty('description'); + expect(typeof brand.handle).toBe('string'); + expect(typeof brand.name).toBe('string'); + expect(typeof brand.description).toBe('string'); + }, + ); + + it.each(BRANDS)( + 'brand $handle has identity with author and display_handle', + (brand) => { + expect(brand).toHaveProperty('identity'); + expect(brand.identity).toHaveProperty('author'); + expect(brand.identity).toHaveProperty('display_handle'); + }, + ); + + it.each(BRANDS)( + 'brand $handle has links object', + (brand) => { + expect(brand).toHaveProperty('links'); + expect(typeof brand.links).toBe('object'); + }, + ); + + it.each(BRANDS)( + 'brand $handle does NOT carry a colors field (theme lives in styles/_theme.scss)', + (brand) => { + expect(brand.colors).toBeUndefined(); + }, + ); + + it('every brand handle is unique', () => { + const handles = BRANDS.map((b) => b.handle); + expect(new Set(handles).size).toBe(handles.length); + }); +}); + +describe('Brand loader — SOURCES', () => { + it('is a non-empty array', () => { + expect(Array.isArray(SOURCES)).toBe(true); + expect(SOURCES.length).toBeGreaterThan(0); + }); + + it('every source composite key (brand/id) is unique', () => { + const keys = SOURCES.map((s) => `${s.brand}/${s.id}`); + expect(new Set(keys).size).toBe(keys.length); + }); + + it.each(SOURCES)( + 'source $id declares every required field', + (source) => { + for (const field of REQUIRED_SOURCE_FIELDS) { + expect(source).toHaveProperty(field); + } + }, + ); + + it.each(SOURCES)( + 'source $id has a valid type', + (source) => { + expect(ALLOWED_TYPES).toContain(source.type); + }, + ); + + it.each(SOURCES)( + 'source $id uses a valid status', + (source) => { + expect(ALLOWED_STATUSES).toContain(source.status); + }, + ); + + it.each(SOURCES)( + 'source $id description has no em dashes', + (source) => { + expect(source.description).not.toContain(EM_DASH); + }, + ); + + it.each(SOURCES)( + 'source $id use_cases is a string array', + (source) => { + expect(Array.isArray(source.use_cases)).toBe(true); + expect( + source.use_cases.every((k) => typeof k === 'string'), + ).toBe(true); + }, + ); + + it.each(SOURCES)( + 'source $id path matches /@brand/id', + (source) => { + expect(source.path).toBe(`/${source.brand}/${source.id}`); + }, + ); + + it.each(SOURCES)( + 'source $id canvas dimensions are positive integers', + (source) => { + expect(Number.isInteger(source.width)).toBe(true); + expect(Number.isInteger(source.height)).toBe(true); + expect(Number.isInteger(source.fps)).toBe(true); + expect(source.width).toBeGreaterThan(0); + expect(source.height).toBeGreaterThan(0); + expect(source.fps).toBeGreaterThan(0); + }, + ); + + it.each(SOURCES)( + 'source $id brand exists in BRANDS', + (source) => { + const handles = BRANDS.map((b) => b.handle); + expect(handles).toContain(source.brand); + }, + ); +}); diff --git a/src/shared/components/corner-bracket.vue b/src/shared/components/corner-bracket.vue deleted file mode 100644 index 9499eaa..0000000 --- a/src/shared/components/corner-bracket.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - diff --git a/src/shared/components/hud-frame.vue b/src/shared/components/hud/frame.vue similarity index 67% rename from src/shared/components/hud-frame.vue rename to src/shared/components/hud/frame.vue index 9d6f85e..865f0e3 100644 --- a/src/shared/components/hud-frame.vue +++ b/src/shared/components/hud/frame.vue @@ -12,10 +12,26 @@ height: `${height}px`, }" > - - - - + + + +
@@ -52,7 +68,7 @@ diff --git a/src/shared/components/ui/chip.vue b/src/shared/components/ui/chip.vue new file mode 100644 index 0000000..a46b061 --- /dev/null +++ b/src/shared/components/ui/chip.vue @@ -0,0 +1,59 @@ + + + + + + + diff --git a/src/shared/components/ui/data-point.vue b/src/shared/components/ui/data-point.vue new file mode 100644 index 0000000..05aa4d0 --- /dev/null +++ b/src/shared/components/ui/data-point.vue @@ -0,0 +1,99 @@ + + + + + + + diff --git a/src/shared/components/ui/icon.vue b/src/shared/components/ui/icon.vue new file mode 100644 index 0000000..3a9f996 --- /dev/null +++ b/src/shared/components/ui/icon.vue @@ -0,0 +1,104 @@ + + + + + + + diff --git a/src/shared/components/status-indicator.vue b/src/shared/components/ui/status-dot.vue similarity index 68% rename from src/shared/components/status-indicator.vue rename to src/shared/components/ui/status-dot.vue index afc5271..9014209 100644 --- a/src/shared/components/status-indicator.vue +++ b/src/shared/components/ui/status-dot.vue @@ -2,11 +2,18 @@ Copyright (c) 2026 Cristian D. Moreno — @Kyonax This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. See LICENSE or https://mozilla.org/MPL/2.0/ + + ui-status-dot — small 6x6 square that blinks red when active, + renders muted grey when inactive. Name answers "status of what, + shown how?": a status indicator shown as a dot. + + Props: + active — boolean. When true, dot turns red + blinks. --> @@ -21,7 +28,7 @@ defineProps({ diff --git a/src/views/components/modals/base.vue b/src/views/components/modals/base.vue new file mode 100644 index 0000000..12e9c80 --- /dev/null +++ b/src/views/components/modals/base.vue @@ -0,0 +1,163 @@ + + + + + + + diff --git a/src/views/components/modals/detail.vue b/src/views/components/modals/detail.vue new file mode 100644 index 0000000..adc20a7 --- /dev/null +++ b/src/views/components/modals/detail.vue @@ -0,0 +1,214 @@ + + + + + + + diff --git a/src/shared/components/preview-modal.vue b/src/views/components/modals/preview.vue similarity index 54% rename from src/shared/components/preview-modal.vue rename to src/views/components/modals/preview.vue index 02c1728..8615b98 100644 --- a/src/shared/components/preview-modal.vue +++ b/src/views/components/modals/preview.vue @@ -10,83 +10,69 @@ -->