|
| 1 | +--- |
| 2 | +name: shadow-objects-basics |
| 3 | +description: Learn the fundamentals of the Shadow Objects Framework - a reactive library for decoupling business logic from UI. Use this skill when starting a new Shadow Objects project, understanding the architecture (Entities, Tokens, Shadow Objects, Kernel, Registry), or setting up Web Components like <shae-worker>, <shae-ent>, and <shae-prop>. |
| 4 | +--- |
| 5 | + |
| 6 | +# Shadow Objects Basics |
| 7 | + |
| 8 | +The Shadow Objects Framework separates your application logic from UI rendering. Think of it like a shadow theater: the audience sees the screen (UI), but the real action happens behind the scenes with the puppeteer (your logic). |
| 9 | + |
| 10 | +## When to Use This Skill |
| 11 | + |
| 12 | +Use this skill when: |
| 13 | + |
| 14 | +- Starting a new Shadow Objects project |
| 15 | +- Understanding the core architecture |
| 16 | +- Setting up Web Components (`<shae-worker>`, `<shae-ent>`, `<shae-prop>`) |
| 17 | +- Creating your first Shadow Object |
| 18 | +- Configuring the Registry |
| 19 | + |
| 20 | +## The Shadow Theater Model |
| 21 | + |
| 22 | +| Concept | Role | Description | |
| 23 | +|---------|------|-------------| |
| 24 | +| **Screen** | View | The visible UI (DOM, Canvas) | |
| 25 | +| **Puppets** | Entities | Abstract representations with hierarchical structure | |
| 26 | +| **Puppeteer** | Shadow Objects | The logic that react on the puppets | |
| 27 | + |
| 28 | +**Key insight**: "Don't script the screen directly. Script the Puppeteer, and the framework projects the state onto the screen automatically." |
| 29 | + |
| 30 | +## The Two Worlds |
| 31 | + |
| 32 | +### Light World (Browser/Main Thread) |
| 33 | + |
| 34 | +- The user-facing UI |
| 35 | +- Contains Web Components, DOM elements or other state structures created by the View Component API |
| 36 | +- Should hold minimal state |
| 37 | +- Sends data downstream, receives events upstream |
| 38 | + |
| 39 | +### Shadow World (Web Worker or Main Thread) |
| 40 | + |
| 41 | +- Where your application logic lives |
| 42 | +- The source of truth for state |
| 43 | +- Contains the Kernel, Entities, and Shadow Objects |
| 44 | +- Runs independently from the UI thread |
| 45 | + |
| 46 | +## Core Concepts |
| 47 | + |
| 48 | +### Token |
| 49 | + |
| 50 | +A string identifier linking View to Logic. Tokens describe *what* something is, not *which specific instance*. |
| 51 | + |
| 52 | +```html |
| 53 | +<shae-ent token="my-counter">...</shae-ent> |
| 54 | +``` |
| 55 | + |
| 56 | +### Entity |
| 57 | + |
| 58 | +The abstract representation of a component in the Shadow World. Entities: |
| 59 | + |
| 60 | +- Form a tree structure (parent/child relationships) |
| 61 | +- Hold Properties synced from the View |
| 62 | +- Participate in the Context system |
| 63 | +- Might have one or more Shadow Objects attached |
| 64 | + |
| 65 | +### Shadow Object |
| 66 | + |
| 67 | +A functional unit of logic attached to an Entity. Shadow Objects are: |
| 68 | + |
| 69 | +- **Reusable**: Domain-specific, works across different UI components |
| 70 | +- **Reactive**: Responds to property and context changes automatically |
| 71 | +- **Composable**: Multiple Shadow Objects can attach to one Entity |
| 72 | + |
| 73 | +```typescript |
| 74 | +function MyShadowObject({ useProperty, createEffect }: ShadowObjectCreationAPI) { |
| 75 | + const count = useProperty('count'); |
| 76 | + |
| 77 | + createEffect(() => { |
| 78 | + console.log('Count changed:', count()); |
| 79 | + }); |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +### Kernel |
| 84 | + |
| 85 | +The engine of the Shadow World: |
| 86 | + |
| 87 | +- Manages the Entity tree lifecycle |
| 88 | +- Orchestrates Shadow Objects |
| 89 | +- Schedules reactive update cycles |
| 90 | + |
| 91 | +### Registry |
| 92 | + |
| 93 | +The configuration that maps Tokens to Shadow Object constructors: |
| 94 | + |
| 95 | +```typescript |
| 96 | +export default { |
| 97 | + define: { |
| 98 | + 'counter': CounterLogic, |
| 99 | + 'analytics': AnalyticsTracker, |
| 100 | + }, |
| 101 | + routes: { |
| 102 | + // Composition: 'counter' also loads 'analytics' |
| 103 | + 'counter': ['analytics'], |
| 104 | + } |
| 105 | +}; |
| 106 | +``` |
| 107 | + |
| 108 | +## HTML Setup |
| 109 | + |
| 110 | +### Step 1: Import Web Components |
| 111 | + |
| 112 | +```html |
| 113 | +<script type="module"> |
| 114 | + import "@spearwolf/shadow-objects/elements.js"; |
| 115 | +</script> |
| 116 | +``` |
| 117 | + |
| 118 | +### Step 2: Create a Shadow Worker Environment |
| 119 | + |
| 120 | +```html |
| 121 | +<shae-worker src="./my-logic.js" ns="app"> |
| 122 | +</shae-worker> |
| 123 | +``` |
| 124 | + |
| 125 | +| Attribute | Description | |
| 126 | +|-----------|-------------| |
| 127 | +| `src` | Path to your logic module (aka Shadow Objects Module) | |
| 128 | +| `ns` | Optional Namespace (connects related components). If empty, the default namespace is used, which is fine when only one shadow objects environment (worker) is used. | |
| 129 | +| `local` | Run on main thread instead of worker | |
| 130 | +| `no-structered-clone` | (Only if `local`): Do NOT use `structeredClone()` helper to clone the property and event arguments. | |
| 131 | +| `auto-sync` | Sync frequency: `frame`, `30fps`, `100` (ms, interval), `no` | |
| 132 | + |
| 133 | +### Step 3: Create Entities |
| 134 | + |
| 135 | +```html |
| 136 | +<shae-ent token="counter" ns="app"> |
| 137 | + <shae-prop name="count" value="0" type="int"></shae-prop> |
| 138 | + <button>Click me</button> |
| 139 | +</shae-ent> |
| 140 | +``` |
| 141 | + |
| 142 | +| Attribute | Description | |
| 143 | +|-----------|-------------| |
| 144 | +| `token` | Maps to a Registry entry | |
| 145 | +| `ns` | Must match `<shae-worker>` namespace | |
| 146 | +| `forward-custom-events` | Forward events from Shadow Objects Environment as DOM CustomEvents | |
| 147 | + |
| 148 | +**Important**: `<shae-ent>` elements do NOT need to be DOM children of `<shae-worker>`. They connect via namespace. |
| 149 | + |
| 150 | +### Step 4: Bind Properties |
| 151 | + |
| 152 | +```html |
| 153 | +<shae-prop name="score" value="100" type="int"></shae-prop> |
| 154 | +<shae-prop name="active" value="true" type="boolean"></shae-prop> |
| 155 | +<shae-prop name="config" value='{"level":1}' type="json"></shae-prop> |
| 156 | +<shae-prop name="position" value="10 20 30" type="float32array"></shae-prop> |
| 157 | +``` |
| 158 | + |
| 159 | +Supported types: `string`, `number`, `int`, `boolean`, `json`, `number[]`, `float32array`, `uint8array` |
| 160 | + |
| 161 | +**Important**: `<shae-prop>` elements MUST BE be a DOM children of `<shae-ent>`. |
| 162 | + |
| 163 | +## Creating Your First Shadow Object |
| 164 | + |
| 165 | +See [references/basic-shadow-object.ts](references/basic-shadow-object.ts) for a complete example. |
| 166 | + |
| 167 | +```typescript |
| 168 | +export function Counter({ |
| 169 | + useProperty, |
| 170 | + createSignal, |
| 171 | + createEffect, |
| 172 | + onViewEvent, |
| 173 | + dispatchMessageToView |
| 174 | +}: ShadowObjectCreationAPI) { |
| 175 | + // Read property from View |
| 176 | + const initialCount = useProperty<number>('count'); |
| 177 | + |
| 178 | + // Create local reactive state |
| 179 | + const count = createSignal(initialCount() ?? 0); |
| 180 | + |
| 181 | + // React to changes |
| 182 | + createEffect(() => { |
| 183 | + dispatchMessageToView('count-changed', { value: count() }); |
| 184 | + }); |
| 185 | + |
| 186 | + // Handle View events |
| 187 | + onViewEvent((type, data) => { |
| 188 | + if (type === 'increment') { |
| 189 | + count.set(count.value + (data?.amount ?? 1)); |
| 190 | + } |
| 191 | + }); |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +## The ShadowObjectCreationAPI |
| 196 | + |
| 197 | +When a Shadow Object function is called, it receives these tools: |
| 198 | + |
| 199 | +| API | Purpose | |
| 200 | +|-----|---------| |
| 201 | +| `useProperty(name)` | Read a reactive property from View | |
| 202 | +| `useProperties(map)` | Read multiple properties at once | |
| 203 | +| `createSignal(initial)` | Create local reactive state | |
| 204 | +| `createEffect(fn)` | Run side effects on dependency changes | |
| 205 | +| `createMemo(fn)` | Create computed/derived values | |
| 206 | +| `createResource(factory, cleanup)` | Manage external resources | |
| 207 | +| `onViewEvent(handler)` | Listen to events from View | |
| 208 | +| `dispatchMessageToView(type, data)` | Send events to View | |
| 209 | +| `provideContext(name, value)` | Provide context for descendants | |
| 210 | +| `useContext(name)` | Consume context from ancestors | |
| 211 | +| `onDestroy(cleanup)` | Register cleanup on destruction | |
| 212 | +| `entity` | Reference to the Entity object | |
| 213 | + |
| 214 | +## Registry Configuration |
| 215 | + |
| 216 | +See [references/registry-config.ts](references/registry-config.ts) for advanced patterns. |
| 217 | + |
| 218 | +### Basic Definition |
| 219 | + |
| 220 | +```typescript |
| 221 | +import type { ShadowObjectsModule } from '@spearwolf/shadow-objects'; |
| 222 | + |
| 223 | +const mod: ShadowObjectsModule = { |
| 224 | + define: { |
| 225 | + 'counter': Counter, |
| 226 | + 'logger': Logger, |
| 227 | + }, |
| 228 | +}; |
| 229 | + |
| 230 | +export default mod; |
| 231 | +``` |
| 232 | + |
| 233 | +### Composition with Routes |
| 234 | + |
| 235 | +```typescript |
| 236 | +export default { |
| 237 | + define: { |
| 238 | + 'counter': Counter, |
| 239 | + 'logger': Logger, |
| 240 | + 'analytics': AnalyticsTracker, |
| 241 | + }, |
| 242 | + routes: { |
| 243 | + // Load 'logger' and 'analytics' whenever 'counter' is created |
| 244 | + 'counter': ['logger', 'analytics'], |
| 245 | + }, |
| 246 | +}; |
| 247 | +``` |
| 248 | + |
| 249 | +### Conditional Routes |
| 250 | + |
| 251 | +```typescript |
| 252 | +export default { |
| 253 | + define: { |
| 254 | + 'counter': Counter, |
| 255 | + 'debug-panel': DebugPanel, |
| 256 | + }, |
| 257 | + routes: { |
| 258 | + // 'counter' conditionally loads '@debug' route |
| 259 | + 'counter': ['@debug'], |
| 260 | + // '@debug' resolves to 'debug-panel' only if 'debug' property is truthy |
| 261 | + '@debug': ['debug-panel'], |
| 262 | + }, |
| 263 | +}; |
| 264 | +``` |
| 265 | + |
| 266 | +## Best Practices |
| 267 | + |
| 268 | +1. **Keep Shadow Objects focused**: One concern per Shadow Object |
| 269 | +2. **Use composition**: Multiple Shadow Objects per Entity for complex behavior |
| 270 | +3. **Let the Registry handle routing**: Don't instantiate Shadow Objects manually |
| 271 | +4. **Tokens describe types, not instances**: The framework assigns unique IDs internally |
| 272 | +5. **Namespace carefully**: Group related app domains with the same `ns` attribute |
| 273 | + |
| 274 | +## Common Pitfalls |
| 275 | + |
| 276 | +| Pitfall | Solution | |
| 277 | +|---------|----------| |
| 278 | +| Forgetting `ns` attribute | Ensure `<shae-ent>` and `<shae-worker>` share the same namespace | |
| 279 | +| Modifying shared state | Data is cloned across Worker boundary. Use events for updates. | |
| 280 | +| Direct DOM manipulation | Use `dispatchMessageToView` to send state to View | |
| 281 | +| Missing cleanup | Use `onDestroy` for external resources (see lifecycle skill) | |
| 282 | +| Synchronous expectations | The Shadow Objects Environment and the View generally run asynchronously to each other. Communication takes place exclusively from the View to the Shadow Objects Environment via property changes or events. Communication from the Shadow Object Environment to the View takes place only via events. | |
| 283 | + |
| 284 | +## Complete Example |
| 285 | + |
| 286 | +See [references/html-setup.html](references/html-setup.html) for a full working example. |
| 287 | + |
| 288 | +```html |
| 289 | +<!DOCTYPE html> |
| 290 | +<html> |
| 291 | +<head> |
| 292 | + <script type="module"> |
| 293 | + import "@spearwolf/shadow-objects/elements.js"; |
| 294 | + </script> |
| 295 | +</head> |
| 296 | +<body> |
| 297 | + <shae-worker src="./counter-logic.js" ns="app"></shae-worker> |
| 298 | + |
| 299 | + <shae-ent token="counter" ns="app" forward-custom-events="count-changed"> |
| 300 | + <shae-prop name="count" value="0" type="int"></shae-prop> |
| 301 | + <button id="inc">+1</button> |
| 302 | + <span id="display">0</span> |
| 303 | + </shae-ent> |
| 304 | + |
| 305 | + <script> |
| 306 | + const ent = document.querySelector('shae-ent'); |
| 307 | + const display = document.querySelector('#display'); |
| 308 | +
|
| 309 | + document.querySelector('#inc').addEventListener('click', () => { |
| 310 | + ent.viewComponent?.dispatchShadowObjectsEvent('increment', { amount: 1 }); |
| 311 | + }); |
| 312 | +
|
| 313 | + ent.addEventListener('count-changed', (e) => { |
| 314 | + display.textContent = e.detail.value; |
| 315 | + }); |
| 316 | + </script> |
| 317 | +</body> |
| 318 | +</html> |
| 319 | +``` |
| 320 | +
|
| 321 | +## Next Steps |
| 322 | +
|
| 323 | +After mastering the basics, explore these related skills: |
| 324 | +
|
| 325 | +- **shadow-objects-context**: Share state between Entities with Provider/Consumer patterns |
| 326 | +- **shadow-objects-signals**: Deep dive into reactive programming with Signals and Effects |
| 327 | +- **shadow-objects-lifecycle**: Manage resources and understand the Shadow Object lifecycle |
| 328 | +- **shadow-objects-events**: Advanced event communication patterns |
0 commit comments