diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f588c15..6542a63 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,4 +13,4 @@ jobs: test: uses: saucebase-dev/saucebase/.github/workflows/test-module.yml@main with: - module: Themes + module: themes diff --git a/CLAUDE.md b/CLAUDE.md index b36739c..422945c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -112,9 +112,9 @@ documentElement inline styles ← set by applyThemeVars() when a theme is activ |------|------| | `resources/themes/*.json` | Theme definitions — committed source of truth | | `resources/css/theme.css` | (in `resources/css/`, not inside module) — baked CSS output | -| `app/Console/Commands/ApplyThemeCommand.php` | Patches theme.css from JSON; writes `:root` and `.dark` blocks | -| `app/Providers/ThemesServiceProvider.php` | Discovers themes, parses JSON, shares via Inertia | -| `app/Http/Controllers/ThemesController.php` | REST API for save/update/delete of user themes | +| `src/Console/Commands/ApplyThemeCommand.php` | Patches theme.css from JSON; writes `:root` and `.dark` blocks | +| `src/Providers/ThemesServiceProvider.php` | Discovers themes, parses JSON, shares via Inertia | +| `src/Http/Controllers/ThemesController.php` | REST API for save/update/delete of user themes | | `resources/js/fields.ts` | Canonical list of all editable fields with type, vars, constraints | | `resources/js/utils/theme.ts` | Core utilities: `applyThemeVars`, `computeShadows`, `computeRadiusScale`, `computeTrackingScale`, font loading | | `resources/js/components/ThemePanel.vue` | Full visual editor — field rendering, per-field mode sync, save dropdown | @@ -242,7 +242,7 @@ ls modules/Themes/resources/themes/ php -d memory_limit=2048M artisan test --compact modules/Themes/tests/ # E2E -npx playwright test --project="@Themes*" +npx playwright test --project="@themes*" ``` Key test files: diff --git a/LICENSE b/LICENSE index f9a509b..cf78a69 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Saucebase +Copyright (c) 2026 Saucebase Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Taskfile.yml b/Taskfile.yml index 9df1742..d1d61d2 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -9,11 +9,17 @@ tasks: test:e2e: desc: Run E2E tests for Themes module - cmd: npx playwright test --project="@Themes*" {{.CLI_ARGS}} + cmd: npx playwright test --project="@themes*" {{.CLI_ARGS}} interactive: true + # ── Database ────────────────────────────────────────────────── + + db:seed: + desc: Seed the Themes module database + cmd: php artisan modules:seed --module=themes + # ── Code Generation ──────────────────────────────────────────── types:generate: desc: Generate TypeScript types from PHP DTOs and enums - cmd: php artisan module:generate-types Themes + cmd: php artisan module:generate-types themes diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php deleted file mode 100644 index 165cb09..0000000 --- a/app/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,15 +0,0 @@ -group(module_path('Themes', '/routes/web.php')); - } -} diff --git a/composer.json b/composer.json index 2ce03c1..108ebd8 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,9 @@ { "name": "saucebase/themes", - "description": "", + "description": "Themes module", "type": "saucebase-module", + "license": "proprietary", + "version": "1.2.2", "authors": [ { "name": "Saucebase", @@ -10,20 +12,24 @@ ], "extra": { "laravel": { - "providers": [], + "providers": [ + "Modules\\Themes\\Providers\\ThemesServiceProvider" + ], "aliases": {} } }, "autoload": { "psr-4": { - "Modules\\Themes\\": "app/", + "Modules\\Themes\\": "src/", "Modules\\Themes\\Database\\Factories\\": "database/factories/", - "Modules\\Themes\\Database\\Seeders\\": "database/seeders/" + "Modules\\Themes\\Database\\Seeders\\": "database/seeders/", + "Modules\\Themes\\Tests\\Support\\": "tests/Support/" } }, "autoload-dev": { "psr-4": { "Modules\\Themes\\Tests\\": "tests/" } - } + }, + "minimum-stability": "stable" } diff --git a/module.json b/module.json deleted file mode 100644 index 938ea17..0000000 --- a/module.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Themes", - "alias": "themes", - "description": "Themes module", - "author": "Saucebase", - "version": "v1.2.2", - "keywords": [ - "themes" - ], - "priority": 0, - "providers": [ - "Modules\\Themes\\Providers\\ThemesServiceProvider" - ], - "files": [] -} diff --git a/resources/js/app.ts b/resources/js/app.ts index 13179b5..3043f15 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,9 +1 @@ -import { registerGlobalComponent } from '@/lib/globalComponents'; -import '../css/app.css'; -import ThemePanel from './components/ThemePanel.vue'; - -export function setup() { - registerGlobalComponent('top', ThemePanel); -} - -export function afterMount() {} +export * from './vue/app'; diff --git a/resources/js/vue/app.ts b/resources/js/vue/app.ts new file mode 100644 index 0000000..4d2ca36 --- /dev/null +++ b/resources/js/vue/app.ts @@ -0,0 +1,9 @@ +import { registerGlobalComponent } from '@/lib/globalComponents'; +import '@modules/themes/resources/css/app.css'; +import ThemePanel from './components/ThemePanel.vue'; + +export function setup() { + registerGlobalComponent('top', ThemePanel); +} + +export function afterMount() {} diff --git a/resources/js/components/ColorInput.vue b/resources/js/vue/components/ColorInput.vue similarity index 100% rename from resources/js/components/ColorInput.vue rename to resources/js/vue/components/ColorInput.vue diff --git a/resources/js/components/ColorPickerPopover.vue b/resources/js/vue/components/ColorPickerPopover.vue similarity index 100% rename from resources/js/components/ColorPickerPopover.vue rename to resources/js/vue/components/ColorPickerPopover.vue diff --git a/resources/js/components/DialogCommand.vue b/resources/js/vue/components/DialogCommand.vue similarity index 100% rename from resources/js/components/DialogCommand.vue rename to resources/js/vue/components/DialogCommand.vue diff --git a/resources/js/components/DialogSave.vue b/resources/js/vue/components/DialogSave.vue similarity index 100% rename from resources/js/components/DialogSave.vue rename to resources/js/vue/components/DialogSave.vue diff --git a/resources/js/components/FontPicker.vue b/resources/js/vue/components/FontPicker.vue similarity index 100% rename from resources/js/components/FontPicker.vue rename to resources/js/vue/components/FontPicker.vue diff --git a/resources/js/components/LinkToggle.vue b/resources/js/vue/components/LinkToggle.vue similarity index 100% rename from resources/js/components/LinkToggle.vue rename to resources/js/vue/components/LinkToggle.vue diff --git a/resources/js/components/SearchInput.vue b/resources/js/vue/components/SearchInput.vue similarity index 100% rename from resources/js/components/SearchInput.vue rename to resources/js/vue/components/SearchInput.vue diff --git a/resources/js/components/SliderInput.vue b/resources/js/vue/components/SliderInput.vue similarity index 100% rename from resources/js/components/SliderInput.vue rename to resources/js/vue/components/SliderInput.vue diff --git a/resources/js/components/TailwindColorPicker.vue b/resources/js/vue/components/TailwindColorPicker.vue similarity index 100% rename from resources/js/components/TailwindColorPicker.vue rename to resources/js/vue/components/TailwindColorPicker.vue diff --git a/resources/js/components/ThemeColorSwatch.vue b/resources/js/vue/components/ThemeColorSwatch.vue similarity index 100% rename from resources/js/components/ThemeColorSwatch.vue rename to resources/js/vue/components/ThemeColorSwatch.vue diff --git a/resources/js/components/ThemePanel.vue b/resources/js/vue/components/ThemePanel.vue similarity index 97% rename from resources/js/components/ThemePanel.vue rename to resources/js/vue/components/ThemePanel.vue index 377160d..dd6b046 100644 --- a/resources/js/components/ThemePanel.vue +++ b/resources/js/vue/components/ThemePanel.vue @@ -37,7 +37,7 @@ import type { FieldState, Font, Theme } from '../types'; import { useDialog } from '@/composables/useDialog'; import { useHttp, usePage } from '@inertiajs/vue3'; -import { useDark } from '@vueuse/core'; +import { useColorMode } from '@vueuse/core'; import { trans } from 'laravel-vue-i18n'; import { toast } from 'vue-sonner'; import ColorInput from './ColorInput.vue'; @@ -75,7 +75,8 @@ const http = useHttp({ dark: {} as Record, }, }); -const isDark = useDark(); +const colorMode = useColorMode({ storageKey: 'appearance' }); +const isDark = computed(() => colorMode.value === 'dark'); const { confirm } = useDialog(); const themesEnabled = computed(() => page.props?.themes != null); @@ -92,7 +93,9 @@ const fontOptions = computed>(() => ({ const defaultThemeId = computed(() => themes.value[0]?.id ?? 'default'); const selectedThemeId = ref( - (typeof localStorage !== 'undefined' ? localStorage.getItem(THEME_STORAGE_KEY) : null) ?? defaultThemeId.value, + (typeof localStorage !== 'undefined' + ? localStorage.getItem(THEME_STORAGE_KEY) + : null) ?? defaultThemeId.value, ); const currentTheme = computed( @@ -175,7 +178,8 @@ const fields = reactive( themeFields().map((f) => ({ ...f, value: '' })), ); -const uniqueGroups = themeFields().filter((f) => f.group) +const uniqueGroups = themeFields() + .filter((f) => f.group) .map((f) => f.group!) .filter((g, i, arr) => arr.findIndex((x) => x.name === g.name) === i); @@ -230,14 +234,20 @@ function populateFieldsFromTheme(theme: Theme): void { field.value = raw; } } - // Apply sidebar sync now so originalValues captures the synced state, - // preventing a false "live edits" indicator on fresh load. - if (sidebarSynced.value) { - applySidebarSync(); - for (const f of fields) { - if (f.key.startsWith('sidebar') && f.type === 'color') - fieldSynced[f.key] = true; - } + // Detect actual sidebar sync state from loaded values — never force-apply sync. + // This prevents sidebar values from being overwritten with surface values on load. + const allSidebarSynced = SIDEBAR_SYNC_MAP.every( + ([sidebarKey, sourceKey]) => { + const s = fields.find((f) => f.key === sidebarKey); + const src = fields.find((f) => f.key === sourceKey); + return ( + s?.value !== '' && src?.value !== '' && s?.value === src?.value + ); + }, + ); + for (const f of fields) { + if (f.key.startsWith('sidebar') && f.type === 'color') + fieldSynced[f.key] = allSidebarSynced; } originalValues.value = Object.fromEntries( fields.map((f) => [f.key, f.value]), @@ -649,7 +659,7 @@ const SIDEBAR_SYNC_MAP: [string, string][] = [ ['sidebar-ring', 'ring'], ]; -const sidebarSynced = ref(true); +const sidebarSynced = ref(false); function applySidebarSync(): void { for (const [sidebarKey, sourceKey] of SIDEBAR_SYNC_MAP) { @@ -842,10 +852,14 @@ const dialogCommandOpen = ref(false); {{ $t('Add your own flavor') }}

- +