|
|
|
|
<script lang="ts">
|
|
|
|
|
import ThemeToggle from './ThemeToggle.svelte';
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<header class="topbar">
|
|
|
|
|
<div class="inner">
|
|
|
|
|
<a class="brand" href="/active" aria-label="Active home">
|
|
|
|
|
<span class="brand-mark" aria-hidden="true"></span>
|
|
|
|
|
<span class="brand-name">active</span>
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
<span class="brand-version" aria-label="version">1.0</span>
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<nav class="primary" aria-label="Primary">
|
|
|
|
|
<a href="/active">Docs</a>
|
|
|
|
|
<a href="/test">Playground</a>
|
|
|
|
|
<a href="https://github.com/" target="_blank" rel="noreferrer noopener">GitHub</a>
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<div class="search" role="search">
|
|
|
|
|
<svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true">
|
|
|
|
|
<path
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="1.6"
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round"
|
|
|
|
|
d="M9 3.5a5.5 5.5 0 1 0 3.4 9.85l3.62 3.62 1.06-1.06-3.62-3.62A5.5 5.5 0 0 0 9 3.5Z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
<input type="search" placeholder="Search docs..." aria-label="Search docs" />
|
|
|
|
|
<kbd>⌘K</kbd>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="actions">
|
|
|
|
|
<ThemeToggle />
|
|
|
|
|
<a class="icon-btn" href="https://github.com/" target="_blank" rel="noreferrer noopener" aria-label="GitHub">
|
|
|
|
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
|
|
|
|
<path
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.topbar {
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0;
|
|
|
|
|
z-index: 50;
|
|
|
|
|
height: var(--size-topbar);
|
|
|
|
|
background: var(--c-bg-overlay);
|
|
|
|
|
backdrop-filter: saturate(180%) blur(10px);
|
|
|
|
|
-webkit-backdrop-filter: saturate(180%) blur(10px);
|
|
|
|
|
border-bottom: 1px solid var(--c-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.inner {
|
|
|
|
|
height: 100%;
|
|
|
|
|
max-width: 1440px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: 0 1.5rem;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: auto 1fr minmax(220px, 360px) auto;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
color: var(--c-fg);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
letter-spacing: -0.02em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand:hover {
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand-mark {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
border-radius: 7px;
|
|
|
|
|
background: linear-gradient(135deg, var(--c-accent) 0%, var(--c-accent-emphasized) 100%);
|
|
|
|
|
box-shadow:
|
|
|
|
|
inset 0 0 0 1px hsl(0 0% 100% / 0.18),
|
|
|
|
|
0 1px 2px rgb(0 0 0 / 0.15),
|
|
|
|
|
0 0 0 1px color-mix(in oklab, var(--c-accent) 25%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand-mark::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 4px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
background:
|
|
|
|
|
linear-gradient(135deg, transparent 40%, hsl(0 0% 100% / 0.35)),
|
|
|
|
|
repeating-linear-gradient(
|
|
|
|
|
45deg,
|
|
|
|
|
hsl(0 0% 100% / 0.18) 0 1px,
|
|
|
|
|
transparent 1px 4px
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand-name {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand-version {
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--c-fg-subtle);
|
|
|
|
|
background: var(--c-bg-muted);
|
|
|
|
|
padding: 0.1rem 0.4rem;
|
|
|
|
|
border-radius: var(--r-sm);
|
|
|
|
|
border: 1px solid var(--c-border);
|
|
|
|
|
margin-left: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.primary {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.primary a {
|
|
|
|
|
color: var(--c-fg-muted);
|
|
|
|
|
padding: 0.4rem 0.7rem;
|
|
|
|
|
border-radius: var(--r-md);
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
transition: background-color 120ms var(--ease), color 120ms var(--ease);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.primary a:hover {
|
|
|
|
|
color: var(--c-fg);
|
|
|
|
|
background: var(--c-bg-muted);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
background: var(--c-bg-subtle);
|
|
|
|
|
border: 1px solid var(--c-border);
|
|
|
|
|
border-radius: var(--r-md);
|
|
|
|
|
padding: 0 0.6rem;
|
|
|
|
|
height: 34px;
|
|
|
|
|
color: var(--c-fg-subtle);
|
|
|
|
|
transition:
|
|
|
|
|
border-color 120ms var(--ease),
|
|
|
|
|
box-shadow 120ms var(--ease);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search:focus-within {
|
|
|
|
|
border-color: var(--c-accent);
|
|
|
|
|
box-shadow: 0 0 0 3px var(--c-accent-subtle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search input {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
border: none;
|
|
|
|
|
background: transparent;
|
|
|
|
|
outline: none;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
color: var(--c-fg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search input::placeholder {
|
|
|
|
|
color: var(--c-fg-subtle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search kbd {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
color: var(--c-fg-subtle);
|
|
|
|
|
background: var(--c-bg-muted);
|
|
|
|
|
padding: 0.1rem 0.35rem;
|
|
|
|
|
border-radius: var(--r-sm);
|
|
|
|
|
border: 1px solid var(--c-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-btn {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 34px;
|
|
|
|
|
height: 34px;
|
|
|
|
|
border-radius: var(--r-md);
|
|
|
|
|
color: var(--c-fg-muted);
|
|
|
|
|
transition: background-color 120ms var(--ease), color 120ms var(--ease);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-btn:hover {
|
|
|
|
|
background: var(--c-bg-muted);
|
|
|
|
|
color: var(--c-fg);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 980px) {
|
|
|
|
|
.inner {
|
|
|
|
|
grid-template-columns: auto 1fr auto;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
}
|
|
|
|
|
.search {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
.primary {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
.brand-version {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|