You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
5.5 KiB
161 lines
5.5 KiB
<script lang="ts">
|
|
/**
|
|
* Estudio — the Eidos theme builder. A route-tool for theme authors that
|
|
* dogfoods the framework and previews REAL components under the drafted theme,
|
|
* applied GLOBALLY at :root (so real components re-skin) while the control
|
|
* chrome (literal colours) stays put.
|
|
*/
|
|
import { onDestroy } from 'svelte'
|
|
import { createActiveUix, setActiveUix } from '$active-uix'
|
|
import { Soma } from '$soma/core/soma.svelte'
|
|
import { siumLangs } from '$sium/langs/langs'
|
|
import { secsLangs } from '$libs/secs'
|
|
import { ActiveEidos } from '$uix/eidos'
|
|
import type { ScalingKey } from '$uix/eidos/lib/config-types'
|
|
import '@/uix/eidos/index.css'
|
|
import '@/uix/eidos/generated/palette.css'
|
|
import './_lib/estudio.css'
|
|
|
|
import { ThemeDraftStore, createDraftStore } from './_lib/store.svelte'
|
|
import { applyDraft, draftToCode, draftToJson } from './_lib/apply'
|
|
import ColorPanel from './panels/ColorPanel.svelte'
|
|
import TypographyPanel from './panels/TypographyPanel.svelte'
|
|
import ShapePanel from './panels/ShapePanel.svelte'
|
|
import SurfacesPanel from './panels/SurfacesPanel.svelte'
|
|
import SpacePanel from './panels/SpacePanel.svelte'
|
|
import GradientsPanel from './panels/GradientsPanel.svelte'
|
|
import AdvancedPanel from './panels/AdvancedPanel.svelte'
|
|
import Preview from './panels/Preview.svelte'
|
|
|
|
let store = $state(createDraftStore())
|
|
let themeName = $state('mi-tema')
|
|
|
|
// ── Runtime boot (uix + soma + eidos), mode/density/scaling from the store ──
|
|
const uix = createActiveUix({
|
|
langs: { schema: { sium: siumLangs, secs: secsLangs }, defaultLocale: 'es' as const },
|
|
events: { sound: false, haptic: false }
|
|
})
|
|
setActiveUix(uix)
|
|
Soma.create()
|
|
|
|
// One shared registry; the structural sources all re-read from the store on
|
|
// notify, so the value arg is ignored (eidos re-reads `get()`).
|
|
const listeners = new Set<() => void>()
|
|
function onChange<T>(handler: (value: T) => void): () => void {
|
|
const fn = () => handler(undefined as T)
|
|
listeners.add(fn)
|
|
return () => listeners.delete(fn)
|
|
}
|
|
const eidos = ActiveEidos.create({
|
|
applyDom: true,
|
|
modeSource: { get: () => store.mode, onChange },
|
|
densitySource: { get: () => store.density, onChange },
|
|
scalingSource: { get: () => String(store.scaling) as ScalingKey, onChange }
|
|
})
|
|
// Notify eidos sources when the structural axes change.
|
|
$effect(() => {
|
|
void store.mode
|
|
void store.density
|
|
void store.scaling
|
|
for (const l of listeners) l()
|
|
})
|
|
|
|
onDestroy(() => {
|
|
eidos.dispose()
|
|
uix.dispose()
|
|
})
|
|
|
|
// ── The single reactive application ──
|
|
$effect(() => {
|
|
applyDraft(uix, eidos, store)
|
|
})
|
|
|
|
// ── Tabs ──
|
|
type Tab = { id: string; label: string }
|
|
const TABS: Tab[] = [
|
|
{ id: 'color', label: 'Color' },
|
|
{ id: 'type', label: 'Tipografía' },
|
|
{ id: 'shape', label: 'Forma' },
|
|
{ id: 'surfaces', label: 'Superficies' },
|
|
{ id: 'space', label: 'Espacio' },
|
|
{ id: 'gradients', label: 'Gradientes' },
|
|
{ id: 'advanced', label: 'Avanzado' }
|
|
]
|
|
let tab = $state('color')
|
|
|
|
let showExport = $state(false)
|
|
let exportKind = $state<'code' | 'json'>('code')
|
|
const exportText = $derived(exportKind === 'code' ? draftToCode(store) : draftToJson(store))
|
|
|
|
function reset(): void {
|
|
store = createDraftStore()
|
|
}
|
|
</script>
|
|
|
|
<svelte:head><title>Estudio · Eidos theme builder</title></svelte:head>
|
|
|
|
<div class="studio">
|
|
<header class="topbar">
|
|
<span class="brand">Estudio <small>eidos theme builder</small></span>
|
|
|
|
<div class="top-group">
|
|
<span class="top-label">tema</span>
|
|
<input class="field-select" style="inline-size: 8rem;" bind:value={themeName} spellcheck="false" />
|
|
</div>
|
|
|
|
<div class="top-group">
|
|
<span class="top-label">apariencia</span>
|
|
<div class="seg">
|
|
<button class="seg-btn" data-active={store.mode === 'light'} onclick={() => (store.mode = 'light')}>◐ claro</button>
|
|
<button class="seg-btn" data-active={store.mode === 'dark'} onclick={() => (store.mode = 'dark')}>◑ oscuro</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn-ghost" onclick={reset}>restablecer</button>
|
|
<button class="btn-primary" onclick={() => (showExport = !showExport)}>Exportar</button>
|
|
</header>
|
|
|
|
<div class="lab">
|
|
<aside class="controls">
|
|
<nav class="tabbar">
|
|
{#each TABS as t}
|
|
<button class="tab" data-active={tab === t.id} onclick={() => (tab = t.id)}>{t.label}</button>
|
|
{/each}
|
|
</nav>
|
|
|
|
{#if tab === 'color'}
|
|
<ColorPanel {store} />
|
|
{:else if tab === 'type'}
|
|
<TypographyPanel {store} />
|
|
{:else if tab === 'shape'}
|
|
<ShapePanel {store} />
|
|
{:else if tab === 'surfaces'}
|
|
<SurfacesPanel {store} />
|
|
{:else if tab === 'space'}
|
|
<SpacePanel {store} />
|
|
{:else if tab === 'gradients'}
|
|
<GradientsPanel {store} />
|
|
{:else if tab === 'advanced'}
|
|
<AdvancedPanel {store} />
|
|
{/if}
|
|
</aside>
|
|
|
|
<main>
|
|
{#if showExport}
|
|
<div class="panel" style="background:#14141f;border:1px solid #26263a;border-radius:16px;margin-block-end:1.25rem;">
|
|
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
<div class="seg">
|
|
<button class="seg-btn" data-active={exportKind === 'code'} onclick={() => (exportKind = 'code')}>código</button>
|
|
<button class="seg-btn" data-active={exportKind === 'json'} onclick={() => (exportKind = 'json')}>JSON</button>
|
|
</div>
|
|
<button class="btn-ghost" style="margin-inline-start:auto;" onclick={() => navigator.clipboard?.writeText(exportText)}>copiar</button>
|
|
<button class="btn-ghost" onclick={() => (showExport = false)}>cerrar</button>
|
|
</div>
|
|
<code class="export-code">{exportText}</code>
|
|
</div>
|
|
{/if}
|
|
<Preview {store} />
|
|
</main>
|
|
</div>
|
|
</div>
|