16 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build/Test Commands
npm run dev # Start dev server
npm run build # Production build (static site via adapter-static)
npm run test # Run all tests once
npm run test:unit # Run tests in watch mode
npx vitest run src/lib/ling/test/ling.test.ts # Run single test file
npx vitest run -t "describe name" # Run tests matching pattern
npm run check # Type check with svelte-check
npm run lint # Check formatting with Prettier
npm run format # Auto-format with Prettier
Code Style
- Tabs for indentation, single quotes, no trailing commas, 100 char print width (Prettier)
- Spanish comments in code are acceptable
- Svelte 5 runes mode enforced globally (
svelte.config.jsdynamicCompileOptions) - DOM event handlers: lowercase (
onclick,onfocus). User callbacks: camelCase (onComplete,onInteractOutside) - State fields:
$state()without#. Derived:readonly x = $derived.by(...) - Boolean attrs: always use helpers (
boolToTrueOrUndef,boolToEmptyStrOrUndef,boolToStr) — never ternaries
Path Aliases
Aliases must be synced in BOTH svelte.config.js AND vite.config.ts:
| Alias | Target |
|---|---|
@/ |
src/ |
@/ling |
src/lib/ling (i18n) |
@/logr |
src/lib/logr (logging) |
@/glob |
src/lib/glob (legacy; do not use for new code) |
@/actx |
src/lib/actx (app context) |
@/uiux |
src/lib/uiux |
$uix |
src/uix (UI layers) |
$terra |
src/uix/terra (headless primitives) |
$terra/utils |
src/uix/terra/utils/sound.svelte.ts |
$terra/system |
src/uix/terra/system/exports.ts |
$terra/external/dates |
src/uix/terra/external/dates/sound.svelte.ts |
$lib |
src/lib |
$lib/util/dias |
src/lib/util/dias (date/time library — canonical) |
Vitest Two-Project Structure
vite.config.ts defines two test projects:
- client: Browser tests via Playwright for
*.svelte.{test,spec}.{js,ts} - server: Node environment for
*.{test,spec}.{js,ts}(excludes svelte tests)
Architecture: Three-Layer UI System
terra → headless primitives: behavior, accessibility, data-* contracts, context
air → visual layer on top of terra: tokens, themes, recipes, styled components
glob → domain logic: dates, numbers, currency, units, locale
The dependency flows one way: air → terra. Terra never imports from air.
terra (src/uix/terra/)
Headless primitive library. Each component follows Element.SubElement pattern (Dialog.Root, Dialog.Trigger, Dialog.Content).
Key patterns:
- Thin
.sveltewrappers + logic in*.svelte.tsState classes - State classes use
static create()for context registration,createComposed()for internal composition without context - Reactive system:
Active<T>,State<T>,readableActive(),writableActive()— values travel as boxes with.current - Context via
createTerraContext()(wraps runed'sContext) - Props merging:
mergeProps(restProps, state.props)in every wrapper data-*attrs are formal public API, validated viaassertDataContract()againstutils/contracts.tschildsnippet replaces the rendered node;childrenfills the interior
Structure:
[component]/components/— Svelte wrappers[component]/types.ts— public props[component]/*.svelte.ts— State classes[component]/exports.ts+sound.svelte.ts— barrel exportslayers/— shared infra (portal, floating, presence, focus-scope, dismissible, escape, scroll)dom/— browser-dependent utils (focus, roving focus, arrow navigation)utils/— pure helpers (state system, attrs, merge-props, events, keyboard, css, datetime)system/—TerraConfigfor cross-cutting services (translator, formatters, logger, dir, portalTo)external/dates/— adapter boundary for date library
Import rules within terra: use $terra/utils or relative paths internally, never $uix/terra.
air (src/uix/air/)
Visual layer built on terra. Consumes terra's public anatomy and data-* attrs — never internal classes or incidental DOM structure.
Three-level token system:
- Primitive tokens (
tokens/primitive.css) — absolute scale values, never used directly by components - Semantic tokens (
tokens/semantic.css) — theme-aware, switch via[data-theme] - Component tokens (
tokens/components/*.css) — scoped per component (--air-button-*), the only level component CSS writes
Token naming:
- Public:
--air-{component}-*(stable, overridable) - Private:
--_air-{component}-*(implementation detail, may change)
Contracts (contracts/):
contract.css— minimum semantic tokens any theme must implementcontract-categorical.css— optional categorical color familiescontract-primitives.css— optional advanced 12-step scalescontracts/components/*.css— per-component public token API
CSS rules:
- Tokens and
data-*selectors must be global CSS (imported.cssfiles), not<style scoped> - Svelte scoped styles only for properties that don't depend on
data-*or terra internals - Components respond to terra states purely via
data-*selectors:[data-disabled],[data-state='open'], etc. - Theme activation via
data-themeattribute on any DOM container (supports nesting) - Variant dimensions are orthogonal:
size→ dimensions,variant→ appearance,state→ transversal overrides
Structure:
components/— styled wrappers over terra primitivestokens/— token implementation (primitive, semantic, per-component)contracts/— public token interfaces for theme authorsthemes/— theme implementations (base light/dark)icons/— icon system (Icon+ per-icon components inicons/lib/)
Internal Libraries (src/lib/)
ling— i18n system (factory:createLing)logr— logging (factory:createLogr, depends onling)ext/— App-layer reactive services:app,dates,lang,money,nums,presentation,unitsutil/dias— canonical date/time library. Single public door at$lib/util/dias._vendor/holds the internal implementation (Adobe Apache 2.0 vendored code) and MUST NOT be imported by consumersglob— legacy (pre-App-layer); do not use for new codeactx— app contextvice— app-level utilities
Dates — canonical layout:
- Value types (
CalendarDate,CalendarDateTime,Time,ZonedDateTime), 9 calendars, queries, operations, parsing: all re-exported from$lib/util/dias - All date-related
Intl(DateFormatter, cache,resolveDateOrder,resolveHourCycle, placeholders, defaults) lives in$lib/util/dias/format.ts ext/datesis a thin reactive wrapper for App-level preferences (dateOrder, hourCycle) that delegates formatting todiasHourCycleis canonically numeric —12 | 24— matchingIntl.DateTimeFormat'shour12resolved option. Never'12h' | '24h'- Soma consumes via
$soma/external/dates; terra via$terra/external/dates. Both re-export fromdias. Components never importdiasdirectly
Number formatting: use createNumr from the App layer, NEVER raw Intl.NumberFormat.
soma (src/uix/soma/)
Headless component library. Follows terra's architecture exactly — same patterns, same conventions. Uses $soma alias.
Key patterns (must match terra):
- Child providers reference parent as
provider, NEVERroot - The root export is always
Provider, neverRoot. Consumer writes<Dialog.Provider>,<Calendar.Provider>.createAttrsstill uses the part name'root'internally — that is the DOM attribute part, not the export - Data-attr naming is canonical:
data-{component}(root) anddata-{component}-{part}(sub-parts), emitted bycreateAttrs. Neverdata-soma-*. QuerySelectors, CSS selectors, README tables, and inline strings must all match exactly whatcreateAttrswrites on the DOM — the contract validator does NOT check this (only enum values) onChangecallbacks go inwritableActivesetter in the.sveltewrapper, NOT in provider Opts- Translations: idlangref constants (
#?components.xxx.yyy|Fallback) defined in the component'slangs.ts. Root-level entries live insrc/uix/soma/core/langs.tsundercomponents.{kebab-name}. Providers callsoma?.langs.ts(IDLANGREF)— noresolveSomaTranslationPathwrapper - Props defined ONCE in
types.ts(consumer API). Provider Opts only has reactive wrappers + internal fields Soma.get()returns the Soma config from context.Soma.require()throws when missing- Test pages use shared layout at
src/routes/test/soma/+layout.sveltewith realcreateLangs()+createPresentation()+App.create()
Structure: mirrors terra exactly:
[component]/components/— Svelte wrappers[component]/types.ts— public props with JSDoc[component]/*-provider.svelte.ts— Provider classes[component]/exports.ts+index.ts— barrel exportscore/— Soma class, translator adapter, prop-resolvers, formattersprovider/— base Provider class, context utilitylayers/— shared infra (floating, presence, focus-scope, dismissal, scroll-lock)
Import rules within soma: use $soma/ alias paths, never relative paths across component boundaries.
Test Pages
Component demos live in src/routes/test/. Each terra/air/soma component should have a test page demonstrating its states and variants.
Soma test pages share a layout at src/routes/test/soma/+layout.svelte that provides:
- Real ling instance with soma translations via
extendSomaTranslationModule createSomaTranslatoradapter connecting to ling- Locale/dir switcher in a sticky bar
<Soma>wrapper for all children
Test pages should NOT create their own <Soma> or translator — use the layout's.
TerraConfig / Soma Integration
<!-- Terra -->
<TerraConfig {translator} {dateTimeFormatter}>
<Dialog.Root />
</TerraConfig>
<!-- Soma -->
<Soma {translator} {presentation}>
<Dialog.Provider />
</Soma>
Priority chain for resolved props: explicit component prop → config context value → internal fallback. Both support nesting (child overrides parent partially).
Key Conventions
- When creating terra components: follow the checklist in
src/uix/terra/README.md§21 - When creating soma components: follow the full checklist in
src/uix/soma/COMPONENT_GUIDE.md(rules A1–A37, 40 checklist items). Walk every item explicitly before reporting done — this is mandatory, not optional. Compare features against ark-ui, bits-ui, radix-ui, react-aria and document the gap table in the component's README. Runnpm run perm:check(permutation runner) before shipping; instrument the demo withdata-perm-step="N"annotations for every distinct state transition. - When reading existing modules: start with
exports.ts→types.ts→ root.svelte→*-provider.svelte.ts→ child wrappers → test page → README $bindable()without fallback when parent might passundefined; apply defaults via coalescing (seesrc/uix/terra/README.md§13.1)- Components that use Portal: consumer must manage
z-indexexplicitly (layers don't impose z-indices) - Every soma component ships with its own
README.mdfollowing the format ofdialog/README.md— anatomy, parts, props, ARIA, data-attrs, keyboard, comparison table, example
Critical Rules
- NEVER modify terra source code. Terra is the reference architecture. Soma copies its patterns.
- NEVER delete files without explicit instruction ("delete", "remove", "borra", "elimina"). If ambiguous, ASK first. Reading costs nothing, deleting can be irreversible.
- NEVER create fake translators in test pages. Use real ling instances with
createSomaTranslator+extendSomaTranslationModule. - READ before acting. When told to read a file, read it. Don't interpret "léete" as "eléte".
- Verify before reporting done. Run
npm run check, test the UI in the browser.npm run smokeandnpx tsx scripts/morfo-check.tsmust both be green — smoke proves 200 OK, morfo-check proves the DOM matches the morfo contract and catcheseffect_update_depth_exceeded(it shows up as "Execution context was destroyed" on the affected route — see COMPONENT_GUIDE A35). - Soma audits: the two most recent are
src/uix/soma/soma-audit-2026-04-20.mdandsoma-audit-2026-04-21.md. All their findings are resolved. Older audits (AUDIT_1.md,codex_audit.md) are historical. Read the latest audit before making architectural changes. - Morfo layer: part naming is
kebab: 'provider'for the root part (never'root').createAttrsstill emitsdata-{component}for it. Seesrc/uix/morfo/README.md.
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
1. Think Before Coding
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
2. Simplicity First
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
3. Surgical Changes
Touch only what you must. Clean up only your own mess.
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
4. Goal-Driven Execution
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.