LCARdS Pack System
The definitive reference for how packs are structured, loaded, distributed, and consumed. Follow this document to add new packs, new component types, and new registries. Deviating from it creates the kind of inconsistency this document exists to prevent.
Overview
A pack is a self-contained bundle of data that extends LCARdS. Packs contain style presets, component definitions, themes, rules, animations, or assets. They are loaded at startup by PackManager, which distributes each key to the appropriate singleton registry.
Cardinal rule: Cards and editors never import pack data files directly. They always query a singleton registry (stylePresetManager, componentManager, themeManager, etc.). Packs are the source of truth; registries are the query interface.
Architecture Diagram
Pack Format
Every pack is a plain JavaScript object. All keys are optional — PackManager ignores keys it doesn't recognise and skips empty ones silently.
{
// ── Required ──────────────────────────────────────────────────────────────
id: 'my_pack', // Unique identifier (snake_case)
version: '1.0.0', // Semver string
name: 'My Pack', // Human-readable name (shown in Pack Explorer)
description: 'What this adds', // One-line description
// ── Style Presets → StylePresetManager ───────────────────────────────────
// Named style bundles applied to overlay types.
// Key is the overlay type; value is { presetName: presetObject, ... }
style_presets: {
button: { lozenge: { ... }, barrel: { ... } },
slider: { pills: { ... }, gauge: { ... } }
},
// ── Components → ComponentManager ────────────────────────────────────────
// Structural component definitions (SVG shells, path generators, zone metadata).
// Each component MUST have metadata.type declared for type-based lookup.
components: {
'header-left': {
orientation: 'header-left',
features: ['simple', 'segmented'],
pathGenerator: (config) => '...',
metadata: { type: 'elbow', name: 'Header Left', version: '1.0' }
}
},
// ── Themes → ThemeManager ─────────────────────────────────────────────────
themes: {
'my-theme': { id: 'my-theme', name: 'My Theme', tokens: { ... } }
},
// ── Rules → RulesEngine ───────────────────────────────────────────────────
rules: [
{ id: 'my_rule', conditions: [...], patches: [...] }
],
// ── Sound Schemes → SoundManager ─────────────────────────────────────────
// Maps event types to asset keys within this pack.
sound_schemes: {
'my_scheme': {
card_tap: 'my_tap_sound',
alert_red: 'my_alert_sound',
alert_clear: null // null = silence this event
}
},
// ── Audio Assets → AssetManager (+ override picker) ──────────────────────
audio_assets: {
'my_tap_sound': { url: '/hacsfiles/my_pack/tap.mp3', description: 'Tap beep' }
},
// ── SVG Assets → AssetManager ─────────────────────────────────────────────
svg_assets: {
'my_msd': { url: '/hacsfiles/my_pack/msd.svg', metadata: { ... } }
},
// ── Font Assets → AssetManager ────────────────────────────────────────────
font_assets: {
'my_font': { url: '/hacsfiles/my_pack/font.woff2', displayName: 'My Font', ... }
},
// ── Image Assets → AssetManager (URL-only registry, see Asset Manager) ────
image_assets: {
'my_background': { url: '/hacsfiles/my_pack/bg.jpg', label: 'My Background', category: 'backgrounds' }
}
}Builtin Pack Inventory
| Pack ID | File | Key(s) | Registry |
|---|---|---|---|
core | core-pack.js | (none — version anchor only) | Pack Explorer |
lcards_buttons | lcards-buttons-pack.js | style_presets.button | StylePresetManager |
lcards_sliders | lcards-sliders-pack.js | style_presets.slider | StylePresetManager |
lcards_elbows | lcards-elbows-pack.js | components (type: elbow) | ComponentManager |
builtin_themes | builtin-themes.js | themes | ThemeManager |
lcards_sounds_default | lcards-default-sound-scheme.js | audio_assets, sound_schemes | AssetManager, SoundManager |
lcards_beeps | lcards-beeps-pack.js | audio_assets | AssetManager |
lcards_alerts | lcards-alerts-pack.js | audio_assets | AssetManager |
builtin_msd_backgrounds | builtin-msd.js | svg_assets | AssetManager |
core_fonts | core-fonts-pack.js | font_assets | AssetManager |
lcards_textures | lcards-textures-pack.js | (metadata only — SHAPE_TEXTURE_PRESETS consumed directly by render methods) | Pack Explorer |
lcards_images | lcards-images-pack.js | image_assets | AssetManager |
lcards_power | lcards-power-pack.js | audio_assets | AssetManager |
Always-loaded packs (regardless of requested parameter): builtin_themes, builtin_msd_backgrounds, core_fonts, lcards_sounds_default, lcards_beeps, lcards_alerts, lcards_elbows, lcards_textures, lcards_images, lcards_power.
File Structure Convention
All pack wrapper files live directly under src/core/packs/. Domain subfolders contain only raw data (presets, components, tokens) that pack wrappers import.
src/core/packs/
loadBuiltinPacks.js ← manifest + loader ONLY (no pack data)
externalPackLoader.js
mergePacks.js
core_fonts.json
── Pack wrappers (all visible at one level) ──────────────────────
core-pack.js ← version anchor only, no data keys
core-fonts-pack.js ← font_assets (from core_fonts.json)
lcards-buttons-pack.js ← style_presets.button
lcards-sliders-pack.js ← style_presets.slider
lcards-elbows-pack.js ← components (type: elbow)
builtin-themes.js ← themes
builtin-msd.js ← svg_assets
lcards-default-sound-scheme.js ← audio_assets + sound_schemes
lcards-beeps-pack.js ← audio_assets
lcards-alerts-pack.js ← audio_assets
lcards-images-pack.js ← image_assets
lcards-power-pack.js ← audio_assets
── Raw data (imported by pack wrappers above) ────────────────────
style-presets/
buttons/index.js ← BUTTON_PRESETS
sliders/index.js ← SLIDER_PRESETS
components/
elbows/index.js ← elbowComponents
sliders/index.js ← sliderComponents
dpad/index.js ← dpadComponents
alert/index.js ← alertComponents
buttons/index.js ← BUTTON_COMPONENTS (metadata only)
index.js ← unified re-export for ComponentManager bootstrap
themes/
tokens/lcardsDefaultTokens.js ← theme token tree
index.js ← re-exports for convenience
animations/
index.js ← registerBuiltinAnimationPresets()
example/
lcards-example-pack.js ← full-feature template for third-party authorsRule: loadBuiltinPacks.js imports packs; it does not define them — even CORE_PACK and CORE_FONTS_PACK live in their own wrapper files (core-pack.js, core-fonts-pack.js), not inline.
Rule: Pack wrapper files live at the packs/ root. Never put a pack wrapper inside a domain subfolder.
Registration Flow
lcards-core.js _performInitialization():
1. new ComponentManager() + initialize() ← loads components/index.js directly
2. new PackManager(this)
3. packManager.loadBuiltinPacks([...])
→ loadBuiltinPacks() from loadBuiltinPacks.js
→ returns array of pack objects
→ for each pack: PackManager.registerPack(pack)
step 1: pack.themes → ThemeManager.registerThemesFromPack()
step 2: pack.style_presets → StylePresetManager.registerPresetsFromPack()
step 3: pack.rules → RulesEngine (direct push)
step 4: pack.audio/svg/font/image → AssetManager.preloadFromPack()
step 5: pack.sound_schemes → SoundManager.registerSchemes()
step 6: (animations — cache-on-demand, no registration)
step 7: pack.components → ComponentManager.registerComponentsFromPack()Note: ComponentManager initializes from components/index.js at step 1 before PackManager runs. When a pack with components is registered at step 7, it re-registers those components with added pack provenance (overwrites, which is intentional). The components/index.js load ensures component types are available immediately even if packs haven't loaded yet; the pack registration at step 7 enriches them with provenance metadata.
Consumer Rules
| Consumer | Correct pattern | Anti-pattern (do not do) |
|---|---|---|
| Card rendering | window.lcards.core.componentManager.getComponent('header-left') | import { getElbowComponent } from '…/elbows/index.js' |
| Card editor dropdown | window.lcards.core.stylePresetManager.getAvailablePresets('button') | import { BUTTON_COMPONENTS } from '…/buttons/index.js' |
| Theme token | window.lcards.core.themeManager.getToken('colors.ui.primary', fallback) | Direct token file import |
| Sound asset | window.lcards.core.soundManager.play('card_tap') | Direct audio URL reference |
Why this matters: Direct imports couple cards to specific files, bypass the pack system, and prevent external packs from overriding or extending definitions. Singleton queries allow any pack to augment the system without touching card code.
How to Add a New Pack
Create the pack wrapper file at the
packs/root (never in a domain subfolder — see the file-structure rule above):js// src/core/packs/lcards-my-pack.js export const MY_PACK = { id: 'my_pack', name: 'My Pack', version: '1.0.0', description: '...', audio_assets: { ... } };Import and register in
loadBuiltinPacks.js:jsimport { MY_PACK } from './lcards-my-pack.js'; // Add to BUILTIN_REGISTRY: my_pack: MY_PACK, // Add to alwaysLoad if it should always be present, or leave it // as requestable-onlyThat's it. PackManager distributes the pack keys automatically.
How to Add a New Component Type
Component types are just a components pack under the hood — follow How to Add a New Pack above for the wrapper file and loadBuiltinPacks.js registration. Two things are specific to components:
metadata.typeis required on every component definition —ComponentManageruses it to group components forgetComponentsByType('mytype'):js// src/core/packs/components/mytype/index.js export const myTypeComponents = { 'my-variant': { orientation: '…', features: ['…'], metadata: { type: 'mytype', name: 'My Variant', version: '1.0' }, svg: `<svg>…</svg>` } };The pack wrapper (
src/core/packs/components/lcards-mytype-pack.js) just setscomponents: myTypeComponents, same shape as theMY_PACKexample above.Also add the new type to the
components/index.jsspread, soComponentManager's synchronous bootstrap load includes it immediately — before packs have finished loading asynchronously:jsimport { myTypeComponents } from './mytype/index.js'; export const components = { ...existingComponents, ...myTypeComponents };
Consumers then query the new type via the singleton, same as any other component:
window.lcards.core.componentManager.getComponentsByType('mytype')
window.lcards.core.componentManager.getComponent('my-variant')Public API
| Method | Returns | Description |
|---|---|---|
getLoadedPacks() | Object[] | All loaded packs with content-count summaries (used by Pack Explorer) |
getPack(id) | Object|undefined | Raw pack definition by ID string |
getLoadedPackIds() | string[] | IDs of all loaded packs in load order |
registerPack(pack) | void | Register a pack definition object (called at init time) |
Console Access
window.lcards.debug.singleton('packManager')
// → { loadedPacks: [ { id: 'core', ... }, { id: 'lcards_buttons', ... }, ... ], packCount: 12 }const pm = window.lcards.core.packManager
pm.getLoadedPackIds() // ['core', 'lcards_buttons', ...]
pm.getLoadedPacks() // array of all loaded pack objects (with content-count summaries)
pm.getPack('lcards_buttons') // specific pack by id