@zeix/le-truc 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai-context.md +16 -15
- package/.github/copilot-instructions.md +3 -2
- package/.github/workflows/ci-cd.yml +101 -0
- package/.github/workflows/publish.yml +59 -0
- package/.github/workflows/release.yml +98 -0
- package/.zed/settings.json +3 -0
- package/ARCHITECTURE.md +272 -0
- package/CHANGELOG.md +44 -0
- package/CLAUDE.md +15 -11
- package/LICENSE +1 -1
- package/README.md +63 -19
- package/TASKS.md +41 -0
- package/docs/about.html +12 -9
- package/docs/assets/main.css +1 -1
- package/docs/assets/main.js +9 -9
- package/docs/assets/main.js.map +47 -43
- package/docs/components.html +77 -33
- package/docs/data-flow.html +17 -14
- package/docs/examples/basic-number.html +4 -4
- package/docs/examples/basic-pluralize.html +6 -2
- package/docs/examples/card-mediaqueries.html +1 -1
- package/docs/examples/form-checkbox.html +2 -2
- package/docs/examples/form-combobox.html +9 -6
- package/docs/examples/form-listbox.html +19 -12
- package/docs/examples/form-radiogroup.html +12 -9
- package/docs/examples/form-spinbutton.html +7 -7
- package/docs/examples/form-textbox.html +2 -2
- package/docs/examples/module-carousel.html +145 -198
- package/docs/examples/module-catalog.html +6 -6
- package/docs/examples/module-dialog.html +3 -1
- package/docs/examples/module-lazyload.html +11 -11
- package/docs/examples/module-list.html +12 -4
- package/docs/examples/module-tabgroup.html +11 -9
- package/docs/examples/module-todo.html +12 -11
- package/docs/favicon.ico +0 -0
- package/docs/getting-started.html +12 -9
- package/docs/index.html +15 -12
- package/docs/sitemap.xml +6 -6
- package/docs/styling.html +13 -9
- package/docs/sw.js +4 -4
- package/docs-src/api/README.md +369 -325
- package/docs-src/api/_media/LICENSE +1 -1
- package/docs-src/api/classes/CircularDependencyError.md +11 -17
- package/docs-src/api/classes/ContextRequestEvent.md +15 -15
- package/docs-src/api/classes/DependencyTimeoutError.md +6 -6
- package/docs-src/api/classes/InvalidCallbackError.md +14 -18
- package/docs-src/api/classes/InvalidComponentNameError.md +6 -6
- package/docs-src/api/classes/InvalidCustomElementError.md +6 -6
- package/docs-src/api/classes/InvalidEffectsError.md +6 -6
- package/docs-src/api/classes/InvalidPropertyNameError.md +6 -6
- package/docs-src/api/classes/InvalidReactivesError.md +6 -6
- package/docs-src/api/classes/InvalidSignalValueError.md +14 -18
- package/docs-src/api/classes/MissingElementError.md +6 -6
- package/docs-src/api/classes/NullishSignalValueError.md +11 -17
- package/docs-src/api/functions/asBoolean.md +3 -3
- package/docs-src/api/functions/asEnum.md +3 -3
- package/docs-src/api/functions/asInteger.md +3 -3
- package/docs-src/api/functions/asJSON.md +3 -3
- package/docs-src/api/functions/asNumber.md +3 -3
- package/docs-src/api/functions/asString.md +3 -3
- package/docs-src/api/functions/batch.md +41 -0
- package/docs-src/api/functions/createCollection.md +22 -60
- package/docs-src/api/functions/createComputed.md +87 -0
- package/docs-src/api/functions/createEffect.md +53 -0
- package/docs-src/api/functions/createElementsMemo.md +87 -0
- package/docs-src/api/functions/createEventsSensor.md +71 -0
- package/docs-src/api/functions/createList.md +43 -0
- package/docs-src/api/functions/createMemo.md +129 -0
- package/docs-src/api/functions/createMutableSignal.md +127 -0
- package/docs-src/api/functions/createScope.md +45 -0
- package/docs-src/api/functions/createSensor.md +41 -41
- package/docs-src/api/functions/createSlot.md +50 -0
- package/docs-src/api/functions/createState.md +60 -0
- package/docs-src/api/functions/createStore.md +53 -0
- package/docs-src/api/functions/createTask.md +139 -0
- package/docs-src/api/functions/dangerouslySetInnerHTML.md +5 -5
- package/docs-src/api/functions/defineComponent.md +7 -7
- package/docs-src/api/{variables → functions}/isAsyncFunction.md +9 -13
- package/docs-src/api/functions/isCollection.md +11 -11
- package/docs-src/api/functions/isComputed.md +37 -0
- package/docs-src/api/{variables → functions}/isEqual.md +5 -5
- package/docs-src/api/{variables → functions}/isFunction.md +9 -13
- package/docs-src/api/functions/isList.md +37 -0
- package/docs-src/api/functions/isMemo.md +37 -0
- package/docs-src/api/functions/isMutableSignal.md +31 -0
- package/docs-src/api/functions/isParser.md +3 -3
- package/docs-src/api/{variables → functions}/isRecord.md +9 -13
- package/docs-src/api/functions/isSensor.md +37 -0
- package/docs-src/api/{variables → functions}/isSignal.md +7 -7
- package/docs-src/api/functions/isSlot.md +37 -0
- package/docs-src/api/functions/isState.md +46 -0
- package/docs-src/api/functions/isStore.md +37 -0
- package/docs-src/api/functions/isTask.md +37 -0
- package/docs-src/api/functions/match.md +16 -21
- package/docs-src/api/functions/on.md +5 -5
- package/docs-src/api/functions/pass.md +8 -8
- package/docs-src/api/functions/provideContexts.md +3 -3
- package/docs-src/api/functions/read.md +3 -3
- package/docs-src/api/functions/requestContext.md +5 -5
- package/docs-src/api/functions/runEffects.md +3 -3
- package/docs-src/api/functions/runElementEffects.md +3 -3
- package/docs-src/api/functions/schedule.md +3 -3
- package/docs-src/api/functions/setAttribute.md +5 -5
- package/docs-src/api/functions/setProperty.md +5 -5
- package/docs-src/api/functions/setStyle.md +5 -5
- package/docs-src/api/functions/setText.md +3 -3
- package/docs-src/api/functions/show.md +3 -3
- package/docs-src/api/functions/toggleAttribute.md +5 -5
- package/docs-src/api/functions/toggleClass.md +5 -5
- package/docs-src/api/functions/updateElement.md +3 -3
- package/docs-src/api/functions/valueString.md +29 -0
- package/docs-src/api/globals.md +60 -37
- package/docs-src/api/type-aliases/AllElements.md +59 -0
- package/docs-src/api/type-aliases/Cleanup.md +5 -15
- package/docs-src/api/type-aliases/Collection.md +141 -27
- package/docs-src/api/type-aliases/CollectionChanges.md +49 -0
- package/docs-src/api/type-aliases/CollectionOptions.md +59 -0
- package/docs-src/api/type-aliases/Component.md +3 -3
- package/docs-src/api/type-aliases/ComponentProp.md +3 -3
- package/docs-src/api/type-aliases/ComponentProps.md +3 -3
- package/docs-src/api/type-aliases/ComponentSetup.md +3 -3
- package/docs-src/api/type-aliases/ComponentUI.md +3 -3
- package/docs-src/api/type-aliases/ComputedOptions.md +56 -0
- package/docs-src/api/type-aliases/Context.md +3 -3
- package/docs-src/api/type-aliases/ContextCallback.md +34 -0
- package/docs-src/api/type-aliases/ContextType.md +3 -3
- package/docs-src/api/type-aliases/DangerouslySetInnerHTMLOptions.md +5 -5
- package/docs-src/api/type-aliases/Effect.md +3 -3
- package/docs-src/api/type-aliases/EffectCallback.md +9 -13
- package/docs-src/api/type-aliases/Effects.md +3 -3
- package/docs-src/api/type-aliases/ElementEffects.md +3 -3
- package/docs-src/api/type-aliases/ElementFromKey.md +4 -4
- package/docs-src/api/type-aliases/ElementFromSelector.md +17 -0
- package/docs-src/api/type-aliases/ElementFromSingleSelector.md +17 -0
- package/docs-src/api/type-aliases/ElementQueries.md +7 -7
- package/docs-src/api/type-aliases/ElementUpdater.md +11 -11
- package/docs-src/api/type-aliases/ElementsFromSelectorArray.md +17 -0
- package/docs-src/api/type-aliases/EventHandler.md +3 -3
- package/docs-src/api/type-aliases/EventHandlers.md +25 -0
- package/docs-src/api/type-aliases/EventType.md +3 -3
- package/docs-src/api/type-aliases/ExtractRightmostSelector.md +17 -0
- package/docs-src/api/type-aliases/ExtractTag.md +17 -0
- package/docs-src/api/type-aliases/ExtractTagFromSimpleSelector.md +17 -0
- package/docs-src/api/type-aliases/Fallback.md +3 -3
- package/docs-src/api/type-aliases/FirstElement.md +99 -0
- package/docs-src/api/type-aliases/Initializers.md +3 -3
- package/docs-src/api/type-aliases/KnownTag.md +17 -0
- package/docs-src/api/type-aliases/List.md +321 -0
- package/docs-src/api/type-aliases/ListOptions.md +45 -0
- package/docs-src/api/type-aliases/LooseReader.md +3 -3
- package/docs-src/api/type-aliases/MatchHandlers.md +18 -22
- package/docs-src/api/type-aliases/MaybeCleanup.md +4 -8
- package/docs-src/api/type-aliases/MaybeSignal.md +4 -4
- package/docs-src/api/type-aliases/Memo.md +52 -0
- package/docs-src/api/type-aliases/MemoCallback.md +35 -0
- package/docs-src/api/type-aliases/MethodProducer.md +31 -0
- package/docs-src/api/type-aliases/Parser.md +3 -3
- package/docs-src/api/type-aliases/ParserOrFallback.md +3 -3
- package/docs-src/api/type-aliases/PassedProp.md +3 -3
- package/docs-src/api/type-aliases/PassedProps.md +3 -3
- package/docs-src/api/type-aliases/Reactive.md +3 -3
- package/docs-src/api/type-aliases/Reader.md +3 -3
- package/docs-src/api/type-aliases/ReservedWords.md +3 -3
- package/docs-src/api/type-aliases/Sensor.md +50 -0
- package/docs-src/api/type-aliases/SensorEventHandler.md +53 -0
- package/docs-src/api/type-aliases/SensorOptions.md +38 -0
- package/docs-src/api/type-aliases/Signal.md +5 -9
- package/docs-src/api/type-aliases/SignalOptions.md +58 -0
- package/docs-src/api/type-aliases/Slot.md +129 -0
- package/docs-src/api/type-aliases/SplitByComma.md +17 -0
- package/docs-src/api/type-aliases/State.md +31 -23
- package/docs-src/api/type-aliases/Store.md +9 -13
- package/docs-src/api/type-aliases/StoreOptions.md +31 -0
- package/docs-src/api/type-aliases/Task.md +84 -0
- package/docs-src/api/type-aliases/TaskCallback.md +41 -0
- package/docs-src/api/type-aliases/TrimWhitespace.md +17 -0
- package/docs-src/api/type-aliases/UI.md +4 -4
- package/docs-src/api/type-aliases/UnknownContext.md +3 -3
- package/docs-src/api/type-aliases/UpdateOperation.md +11 -0
- package/docs-src/api/variables/CONTEXT_REQUEST.md +3 -3
- package/{server → docs-src}/layouts/api.html +1 -1
- package/{server → docs-src}/layouts/blog.html +1 -1
- package/{server → docs-src}/layouts/example.html +1 -1
- package/{server → docs-src}/layouts/overview.html +1 -1
- package/docs-src/layouts/page.html +50 -0
- package/docs-src/layouts/test.html +29 -0
- package/docs-src/pages/about.md +1 -1
- package/docs-src/pages/components.md +123 -27
- package/docs-src/pages/data-flow.md +6 -6
- package/docs-src/pages/getting-started.md +1 -1
- package/docs-src/pages/index.md +9 -9
- package/docs-src/pages/styling.md +16 -0
- package/eslint.config.js +0 -3
- package/examples/_common/clear.ts +8 -1
- package/examples/_common/focus.ts +15 -12
- package/examples/_global.css +6 -6
- package/examples/basic-button/basic-button.spec.ts +1 -1
- package/examples/basic-counter/basic-counter.spec.ts +1 -1
- package/examples/basic-hello/basic-hello.spec.ts +1 -1
- package/examples/basic-number/basic-number.spec.ts +3 -3
- package/examples/basic-number/basic-number.ts +4 -4
- package/examples/basic-pluralize/basic-pluralize.spec.ts +1 -1
- package/examples/basic-pluralize/basic-pluralize.ts +6 -2
- package/examples/card-mediaqueries/card-mediaqueries.spec.ts +1 -1
- package/examples/card-mediaqueries/card-mediaqueries.ts +1 -1
- package/examples/form-checkbox/form-checkbox.spec.ts +1 -1
- package/examples/form-checkbox/form-checkbox.ts +2 -2
- package/examples/form-combobox/form-combobox.html +4 -1
- package/examples/form-combobox/form-combobox.spec.ts +21 -18
- package/examples/form-combobox/form-combobox.ts +5 -5
- package/examples/form-listbox/form-listbox.html +1 -1
- package/examples/form-listbox/form-listbox.spec.ts +21 -23
- package/examples/form-listbox/form-listbox.ts +18 -11
- package/examples/form-listbox/mocks/simple-options.json +5 -0
- package/examples/form-radiogroup/form-radiogroup.spec.ts +1 -1
- package/examples/form-radiogroup/form-radiogroup.ts +12 -9
- package/examples/form-spinbutton/form-spinbutton.spec.ts +1 -1
- package/examples/form-spinbutton/form-spinbutton.ts +7 -7
- package/examples/form-textbox/form-textbox.spec.ts +1 -1
- package/examples/form-textbox/form-textbox.ts +2 -2
- package/examples/main.ts +3 -0
- package/examples/module-carousel/module-carousel.css +68 -4
- package/examples/module-carousel/module-carousel.html +3 -154
- package/examples/module-carousel/module-carousel.spec.ts +107 -83
- package/examples/module-carousel/module-carousel.ts +73 -39
- package/examples/module-catalog/module-catalog.spec.ts +1 -1
- package/examples/module-catalog/module-catalog.ts +6 -6
- package/examples/module-dialog/module-dialog.spec.ts +1 -1
- package/examples/module-dialog/module-dialog.ts +3 -1
- package/examples/module-lazyload/mocks/nested-components.html +1 -3
- package/examples/module-lazyload/mocks/recursive.html +1 -1
- package/examples/module-lazyload/module-lazyload.html +8 -8
- package/examples/module-lazyload/module-lazyload.spec.ts +35 -17
- package/examples/module-lazyload/module-lazyload.ts +3 -3
- package/examples/module-list/module-list.spec.ts +1 -1
- package/examples/module-list/module-list.ts +12 -4
- package/examples/module-pagination/module-pagination.spec.ts +1 -1
- package/examples/module-scrollarea/module-scrollarea.spec.ts +105 -24
- package/examples/module-tabgroup/module-tabgroup.spec.ts +1 -1
- package/examples/module-tabgroup/module-tabgroup.ts +11 -9
- package/examples/module-todo/module-todo.spec.ts +1 -1
- package/examples/module-todo/module-todo.ts +12 -11
- package/examples/test-setup.md +1 -1
- package/index.dev.js +1662 -895
- package/index.js +2 -2
- package/index.js.map +30 -28
- package/index.ts +57 -27
- package/package.json +18 -16
- package/scripts/test-component.ts +176 -0
- package/server/BUILD_SYSTEM.md +244 -90
- package/server/SERVER.md +409 -242
- package/server/build.ts +74 -7
- package/server/config.ts +41 -48
- package/server/dev.ts +116 -0
- package/server/effects/api.ts +2 -2
- package/server/effects/css.ts +17 -23
- package/server/effects/examples.ts +19 -11
- package/server/effects/js.ts +2 -6
- package/server/effects/menu.ts +3 -3
- package/server/effects/pages.ts +50 -55
- package/server/effects/service-worker.ts +8 -9
- package/server/effects/sitemap.ts +13 -18
- package/server/file-signals.ts +42 -28
- package/server/file-watcher.ts +62 -46
- package/server/schema/carousel.markdoc.ts +4 -5
- package/server/serve.ts +321 -554
- package/server/templates/sitemap.ts +1 -1
- package/server/templates/utils.ts +4 -1
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/src/component.ts +38 -23
- package/src/context.ts +7 -7
- package/src/effects/event.ts +2 -5
- package/src/effects/html.ts +23 -6
- package/src/effects/pass.ts +38 -40
- package/src/effects/property.ts +2 -3
- package/src/effects.ts +25 -110
- package/src/errors.ts +0 -19
- package/src/events.ts +117 -0
- package/src/internal.ts +18 -0
- package/src/parsers.ts +2 -2
- package/src/ui.ts +135 -41
- package/src/util.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -6
- package/types/index.d.ts +11 -10
- package/types/index.dev.d.ts +5 -6
- package/types/src/component.d.ts +3 -3
- package/types/src/context.d.ts +4 -4
- package/types/src/effects/event.d.ts +2 -2
- package/types/src/effects/pass.d.ts +6 -3
- package/types/src/effects.d.ts +1 -87
- package/types/src/errors.d.ts +1 -13
- package/types/src/events.d.ts +12 -12
- package/types/src/internal.d.ts +4 -0
- package/types/src/ui.d.ts +18 -6
- package/bun.lock +0 -239
- package/docs-src/api/classes/CircularMutationError.md +0 -301
- package/docs-src/api/classes/StoreKeyExistsError.md +0 -303
- package/docs-src/api/classes/StoreKeyRangeError.md +0 -299
- package/docs-src/api/classes/StoreKeyReadonlyError.md +0 -303
- package/docs-src/api/functions/resolve.md +0 -40
- package/docs-src/api/functions/toSignal.md +0 -37
- package/docs-src/api/type-aliases/CollectionListener.md +0 -27
- package/docs-src/api/type-aliases/Computed.md +0 -49
- package/docs-src/api/type-aliases/ComputedCallback.md +0 -29
- package/docs-src/api/type-aliases/DiffResult.md +0 -61
- package/docs-src/api/type-aliases/ResolveResult.md +0 -29
- package/docs-src/api/type-aliases/SensorEvents.md +0 -25
- package/docs-src/api/variables/UNSET.md +0 -23
- package/docs-src/api/variables/batch.md +0 -25
- package/docs-src/api/variables/createComputed.md +0 -41
- package/docs-src/api/variables/createEffect.md +0 -35
- package/docs-src/api/variables/createState.md +0 -37
- package/docs-src/api/variables/createStore.md +0 -42
- package/docs-src/api/variables/diff.md +0 -43
- package/docs-src/api/variables/isAbortError.md +0 -33
- package/docs-src/api/variables/isComputed.md +0 -37
- package/docs-src/api/variables/isMutableSignal.md +0 -37
- package/docs-src/api/variables/isNumber.md +0 -33
- package/docs-src/api/variables/isRecordOrArray.md +0 -39
- package/docs-src/api/variables/isState.md +0 -37
- package/docs-src/api/variables/isStore.md +0 -37
- package/docs-src/api/variables/isString.md +0 -33
- package/docs-src/api/variables/isSymbol.md +0 -33
- package/docs-src/api/variables/toError.md +0 -33
- package/docs-src/api/variables/valueString.md +0 -33
- package/examples/form-checkbox/vanilla-checkbox.ts +0 -101
- package/examples/server.ts +0 -95
- package/index.dev.ts +0 -127
- package/server/layout-engine.ts +0 -470
- package/server/layout-utils.ts +0 -615
- package/server/layouts/base.html +0 -37
- package/server/layouts/page.html +0 -36
- package/server/layouts/test.html +0 -24
- package/src/effects/method.ts +0 -57
- package/src/signals/collection.ts +0 -253
- package/src/signals/sensor.ts +0 -131
- package/types/examples/basic-button/basic-button.d.ts +0 -16
- package/types/examples/basic-hello/basic-hello.d.ts +0 -18
- package/types/src/collection.d.ts +0 -27
- package/types/src/sensor.d.ts +0 -27
- package/types/src/signals/collection.d.ts +0 -32
- package/types/src/signals/sensor.d.ts +0 -27
package/.ai-context.md
CHANGED
|
@@ -36,12 +36,12 @@ The selector function provides type-safe DOM queries:
|
|
|
36
36
|
({ first, all }) => ({
|
|
37
37
|
button?: first('button'), // HTMLButtonElement | undefined
|
|
38
38
|
input: first('input', 'required'), // HTMLInputElement (throws if missing)
|
|
39
|
-
items: all('.item'), //
|
|
39
|
+
items: all('.item'), // Memo<HTMLElement[]>
|
|
40
40
|
custom?: first<MyElement>('my-el') // Custom typing
|
|
41
41
|
})
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
`all()` returns `Memo<E[]>` backed by a lazy `MutationObserver` that tracks DOM mutations automatically.
|
|
45
45
|
|
|
46
46
|
### Effect System
|
|
47
47
|
Effects run reactively when dependencies change:
|
|
@@ -77,9 +77,10 @@ Effects run reactively when dependencies change:
|
|
|
77
77
|
- `setStyle(property, reactive)` - Update inline styles
|
|
78
78
|
|
|
79
79
|
### Advanced
|
|
80
|
-
- `pass(props)` - Pass
|
|
81
|
-
- `dangerouslySetInnerHTML(reactive)`
|
|
82
|
-
- `
|
|
80
|
+
- `pass(props)` - Pass reactive values to child Le Truc components via Slot signal replacement
|
|
81
|
+
- `dangerouslySetInnerHTML(reactive, opts?)` - Set innerHTML (use only for trusted sources)
|
|
82
|
+
- `callMethod(name, reactive, args?)` - Call a method when reactive is truthy
|
|
83
|
+
- `focus(reactive)` - Focus element when truthy
|
|
83
84
|
|
|
84
85
|
## Parsers
|
|
85
86
|
|
|
@@ -114,23 +115,23 @@ const countSignal = createState(0)
|
|
|
114
115
|
setText(() => countSignal.get())
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
##
|
|
118
|
+
## Element Memos
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
`all()` returns `Memo<E[]>` — a lazily observed collection of elements:
|
|
120
121
|
|
|
121
122
|
```typescript
|
|
122
123
|
// Creation
|
|
123
|
-
({ all }) => ({ items: all('.item') })
|
|
124
|
-
const items =
|
|
124
|
+
({ all }) => ({ items: all('.item') }) // In UI query, will be ui.items
|
|
125
|
+
const items = createElementsMemo(parent, '.item') // Elsewhere with arbitrary parent
|
|
125
126
|
|
|
126
127
|
// Access
|
|
127
|
-
items.get() // Get current elements
|
|
128
|
-
items.length
|
|
129
|
-
items[0]
|
|
130
|
-
items.on('add', fn) // Listen for additions
|
|
131
|
-
items.on('remove', fn) // Listen for removals
|
|
128
|
+
items.get() // Get current elements array
|
|
129
|
+
items.get().length // Current count
|
|
130
|
+
items.get()[0] // Index access
|
|
132
131
|
```
|
|
133
132
|
|
|
133
|
+
The `MutationObserver` backing the memo activates lazily — only when the memo is read from within a reactive effect. This avoids unnecessary observation overhead for memos that aren't actively consumed.
|
|
134
|
+
|
|
134
135
|
## Common Patterns
|
|
135
136
|
|
|
136
137
|
### Form Components
|
|
@@ -222,7 +223,7 @@ export default defineComponent<MyComponentProps, MyComponentUI>(...)
|
|
|
222
223
|
|
|
223
224
|
### Performance
|
|
224
225
|
- Effects automatically optimize re-runs when dependencies don't change
|
|
225
|
-
-
|
|
226
|
+
- Element memos efficiently track only actual DOM changes via lazy MutationObserver
|
|
226
227
|
- Use `schedule()` for non-critical updates in passive event handlers
|
|
227
228
|
- Proper cleanup prevents memory leaks
|
|
228
229
|
|
|
@@ -32,7 +32,8 @@ Files to consult for examples and authoritative patterns
|
|
|
32
32
|
- Selector helpers & mutation-observer logic: `src/ui.ts`
|
|
33
33
|
- Parser implementations: `src/parsers/*.ts` (e.g. `json.ts`, `number.ts`, `string.ts`)
|
|
34
34
|
- Effect implementations: `src/effects/*.ts` (exported from root `index.ts`)
|
|
35
|
-
-
|
|
35
|
+
- Event-driven sensors: `src/events.ts` (createEventsSensor)
|
|
36
|
+
- Element memos: `createElementsMemo` in `src/ui.ts`
|
|
36
37
|
- Examples demonstrating usage: `examples/*` (start from `basic-hello` and `basic-counter`)
|
|
37
38
|
|
|
38
39
|
Developer workflows (essential commands)
|
|
@@ -45,7 +46,7 @@ Developer workflows (essential commands)
|
|
|
45
46
|
What to change (and what to avoid)
|
|
46
47
|
- Change: small refactors that preserve exported API in `index.ts` and `.d.ts` files.
|
|
47
48
|
- Change: add or update examples in `examples/` to demonstrate new or changed behavior.
|
|
48
|
-
- Avoid: breaking changes to public exports without updating `index.ts`
|
|
49
|
+
- Avoid: breaking changes to public exports without updating `index.ts` and re-building (`bun run build`).
|
|
49
50
|
- Avoid: changing the custom element registration pattern (i.e., calling `customElements.define`) in a way that prevents `getHelpers` dependency detection.
|
|
50
51
|
|
|
51
52
|
PR guidance / descriptions
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
name: CI/CD Pipeline
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
tags: ['v*']
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main, develop]
|
|
9
|
+
release:
|
|
10
|
+
types: [published]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout code
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup Bun
|
|
20
|
+
uses: oven-sh/setup-bun@v2
|
|
21
|
+
with:
|
|
22
|
+
bun-version: latest
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: bun install --frozen-lockfile
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: bun run lint
|
|
29
|
+
|
|
30
|
+
- name: Build
|
|
31
|
+
run: bun run build
|
|
32
|
+
|
|
33
|
+
- name: Install Playwright browsers
|
|
34
|
+
run: bunx playwright install --with-deps
|
|
35
|
+
|
|
36
|
+
- name: Run tests
|
|
37
|
+
run: bun run test
|
|
38
|
+
|
|
39
|
+
- name: Upload test results
|
|
40
|
+
uses: actions/upload-artifact@v4
|
|
41
|
+
if: always()
|
|
42
|
+
with:
|
|
43
|
+
name: playwright-report
|
|
44
|
+
path: playwright-report/
|
|
45
|
+
retention-days: 30
|
|
46
|
+
|
|
47
|
+
publish:
|
|
48
|
+
needs: test
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
if: github.event_name == 'release' && github.event.action == 'published'
|
|
51
|
+
permissions:
|
|
52
|
+
contents: read
|
|
53
|
+
id-token: write # Required for npm provenance
|
|
54
|
+
|
|
55
|
+
steps:
|
|
56
|
+
- name: Checkout code
|
|
57
|
+
uses: actions/checkout@v4
|
|
58
|
+
|
|
59
|
+
- name: Setup Bun
|
|
60
|
+
uses: oven-sh/setup-bun@v2
|
|
61
|
+
with:
|
|
62
|
+
bun-version: latest
|
|
63
|
+
|
|
64
|
+
- name: Install dependencies
|
|
65
|
+
run: bun install --frozen-lockfile
|
|
66
|
+
|
|
67
|
+
- name: Build package
|
|
68
|
+
run: bun run build
|
|
69
|
+
|
|
70
|
+
- name: Setup Node.js for npm
|
|
71
|
+
uses: actions/setup-node@v4
|
|
72
|
+
with:
|
|
73
|
+
node-version: '20'
|
|
74
|
+
registry-url: 'https://registry.npmjs.org'
|
|
75
|
+
|
|
76
|
+
- name: Verify package version matches tag
|
|
77
|
+
run: |
|
|
78
|
+
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
79
|
+
TAG_VERSION=${GITHUB_REF#refs/tags/v}
|
|
80
|
+
if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
|
|
81
|
+
echo "Package version ($PACKAGE_VERSION) doesn't match tag version ($TAG_VERSION)"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
- name: Publish to npm
|
|
86
|
+
run: npm publish --access public --provenance
|
|
87
|
+
env:
|
|
88
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
89
|
+
|
|
90
|
+
- name: Create deployment status
|
|
91
|
+
uses: actions/github-script@v7
|
|
92
|
+
with:
|
|
93
|
+
script: |
|
|
94
|
+
github.rest.repos.createDeploymentStatus({
|
|
95
|
+
owner: context.repo.owner,
|
|
96
|
+
repo: context.repo.repo,
|
|
97
|
+
deployment_id: context.payload.deployment?.id || 0,
|
|
98
|
+
state: 'success',
|
|
99
|
+
environment: 'npm',
|
|
100
|
+
target_url: 'https://www.npmjs.com/package/@zeix/le-truc'
|
|
101
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
version:
|
|
9
|
+
description: 'Version to publish (leave empty to use package.json version)'
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
id-token: write # Required for npm provenance
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout code
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Setup Bun
|
|
25
|
+
uses: oven-sh/setup-bun@v2
|
|
26
|
+
with:
|
|
27
|
+
bun-version: latest
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: bun install --frozen-lockfile
|
|
31
|
+
|
|
32
|
+
- name: Build package
|
|
33
|
+
run: bun run build
|
|
34
|
+
|
|
35
|
+
- name: Setup Node.js for npm
|
|
36
|
+
uses: actions/setup-node@v4
|
|
37
|
+
with:
|
|
38
|
+
node-version: '20'
|
|
39
|
+
registry-url: 'https://registry.npmjs.org'
|
|
40
|
+
|
|
41
|
+
- name: Update version (if specified)
|
|
42
|
+
if: github.event.inputs.version != ''
|
|
43
|
+
run: npm version ${{ github.event.inputs.version }} --no-git-tag-version
|
|
44
|
+
|
|
45
|
+
- name: Publish to npm
|
|
46
|
+
run: npm publish --access public --provenance
|
|
47
|
+
env:
|
|
48
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
49
|
+
|
|
50
|
+
- name: Create GitHub release (if manual dispatch)
|
|
51
|
+
if: github.event_name == 'workflow_dispatch'
|
|
52
|
+
uses: actions/create-release@v1
|
|
53
|
+
env:
|
|
54
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
55
|
+
with:
|
|
56
|
+
tag_name: v${{ steps.version.outputs.version || fromJson(steps.package.outputs.json).version }}
|
|
57
|
+
release_name: Release v${{ steps.version.outputs.version || fromJson(steps.package.outputs.json).version }}
|
|
58
|
+
draft: false
|
|
59
|
+
prerelease: false
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version_type:
|
|
7
|
+
description: 'Version bump type'
|
|
8
|
+
required: true
|
|
9
|
+
type: choice
|
|
10
|
+
options:
|
|
11
|
+
- patch
|
|
12
|
+
- minor
|
|
13
|
+
- major
|
|
14
|
+
default: 'patch'
|
|
15
|
+
prerelease:
|
|
16
|
+
description: 'Create prerelease'
|
|
17
|
+
required: false
|
|
18
|
+
type: boolean
|
|
19
|
+
default: false
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
release:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
permissions:
|
|
25
|
+
contents: write
|
|
26
|
+
id-token: write
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout code
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
+
fetch-depth: 0
|
|
34
|
+
|
|
35
|
+
- name: Setup Bun
|
|
36
|
+
uses: oven-sh/setup-bun@v2
|
|
37
|
+
with:
|
|
38
|
+
bun-version: latest
|
|
39
|
+
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
run: bun install --frozen-lockfile
|
|
42
|
+
|
|
43
|
+
- name: Run tests and build
|
|
44
|
+
run: bun run build
|
|
45
|
+
|
|
46
|
+
- name: Configure Git
|
|
47
|
+
run: |
|
|
48
|
+
git config user.name "github-actions[bot]"
|
|
49
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
50
|
+
|
|
51
|
+
- name: Bump version
|
|
52
|
+
id: version
|
|
53
|
+
run: |
|
|
54
|
+
if [ "${{ github.event.inputs.prerelease }}" = "true" ]; then
|
|
55
|
+
NEW_VERSION=$(npm version pre${{ github.event.inputs.version_type }} --preid=beta --no-git-tag-version)
|
|
56
|
+
else
|
|
57
|
+
NEW_VERSION=$(npm version ${{ github.event.inputs.version_type }} --no-git-tag-version)
|
|
58
|
+
fi
|
|
59
|
+
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
60
|
+
echo "version_number=${NEW_VERSION#v}" >> $GITHUB_OUTPUT
|
|
61
|
+
|
|
62
|
+
- name: Update changelog
|
|
63
|
+
run: |
|
|
64
|
+
echo "## ${{ steps.version.outputs.new_version }} - $(date +%Y-%m-%d)" >> CHANGELOG_NEW.md
|
|
65
|
+
echo "" >> CHANGELOG_NEW.md
|
|
66
|
+
echo "- Version bump: ${{ github.event.inputs.version_type }}" >> CHANGELOG_NEW.md
|
|
67
|
+
echo "" >> CHANGELOG_NEW.md
|
|
68
|
+
if [ -f CHANGELOG.md ]; then
|
|
69
|
+
cat CHANGELOG.md >> CHANGELOG_NEW.md
|
|
70
|
+
fi
|
|
71
|
+
mv CHANGELOG_NEW.md CHANGELOG.md
|
|
72
|
+
|
|
73
|
+
- name: Commit changes
|
|
74
|
+
run: |
|
|
75
|
+
git add package.json CHANGELOG.md
|
|
76
|
+
git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }}"
|
|
77
|
+
git tag ${{ steps.version.outputs.new_version }}
|
|
78
|
+
git push origin main
|
|
79
|
+
git push origin ${{ steps.version.outputs.new_version }}
|
|
80
|
+
|
|
81
|
+
- name: Create GitHub Release
|
|
82
|
+
uses: actions/create-release@v1
|
|
83
|
+
env:
|
|
84
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
85
|
+
with:
|
|
86
|
+
tag_name: ${{ steps.version.outputs.new_version }}
|
|
87
|
+
release_name: Release ${{ steps.version.outputs.new_version }}
|
|
88
|
+
draft: false
|
|
89
|
+
prerelease: ${{ github.event.inputs.prerelease }}
|
|
90
|
+
body: |
|
|
91
|
+
## Changes in ${{ steps.version.outputs.new_version }}
|
|
92
|
+
|
|
93
|
+
This release was created automatically via GitHub Actions.
|
|
94
|
+
|
|
95
|
+
**Version bump:** ${{ github.event.inputs.version_type }}
|
|
96
|
+
**Prerelease:** ${{ github.event.inputs.prerelease }}
|
|
97
|
+
|
|
98
|
+
View the full changelog at [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)
|
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Le Truc is a reactive custom elements library. This document describes how the pieces in `src/` fit together.
|
|
4
|
+
|
|
5
|
+
## File Map
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
component.ts The heart: defineComponent() and the Truc class
|
|
10
|
+
effects.ts Effect orchestration: runEffects, updateElement
|
|
11
|
+
ui.ts DOM queries (first/all), dependency resolution, selector type inference
|
|
12
|
+
parsers.ts Parser/Reader type system and composition
|
|
13
|
+
events.ts Event-driven sensor factory (createEventsSensor)
|
|
14
|
+
context.ts Context protocol (provide/request) for dependency injection
|
|
15
|
+
scheduler.ts rAF-based task deduplication
|
|
16
|
+
errors.ts Domain-specific error classes
|
|
17
|
+
internal.ts Internal signal map (getSignals) — shared by component.ts and pass.ts
|
|
18
|
+
util.ts Logging, element introspection, property validation
|
|
19
|
+
|
|
20
|
+
effects/
|
|
21
|
+
attribute.ts setAttribute, toggleAttribute
|
|
22
|
+
class.ts toggleClass
|
|
23
|
+
event.ts on() — event listener effect
|
|
24
|
+
html.ts dangerouslySetInnerHTML
|
|
25
|
+
pass.ts pass() — inter-component reactive property binding
|
|
26
|
+
property.ts setProperty, show
|
|
27
|
+
style.ts setStyle
|
|
28
|
+
text.ts setText
|
|
29
|
+
|
|
30
|
+
parsers/
|
|
31
|
+
boolean.ts asBoolean
|
|
32
|
+
json.ts asJSON
|
|
33
|
+
number.ts asInteger, asNumber
|
|
34
|
+
string.ts asString, asEnum
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Dependency Graph
|
|
38
|
+
|
|
39
|
+
Arrows mean "imports from". The graph flows bottom-up from leaf utilities to `component.ts`.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
util.ts ─────────────────────────────────────────────┐
|
|
43
|
+
errors.ts ──────── util.ts │
|
|
44
|
+
scheduler.ts ──── (leaf, no internal imports) │
|
|
45
|
+
parsers.ts ─────── ui.ts (types only) │
|
|
46
|
+
parsers/* ──────── parsers.ts, ui.ts (types) │
|
|
47
|
+
│
|
|
48
|
+
internal.ts ────── (leaf, signal storage) │
|
|
49
|
+
ui.ts ──────────── errors.ts, util.ts │
|
|
50
|
+
│
|
|
51
|
+
effects.ts ─────── component.ts (types), errors.ts, │
|
|
52
|
+
ui.ts, util.ts │
|
|
53
|
+
│
|
|
54
|
+
effects/* ──────── component.ts (types), effects.ts │
|
|
55
|
+
+ scheduler.ts, util.ts, etc. │
|
|
56
|
+
│
|
|
57
|
+
events.ts ──────── component.ts (types), parsers.ts, │
|
|
58
|
+
scheduler.ts, ui.ts │
|
|
59
|
+
│
|
|
60
|
+
context.ts ─────── component.ts (types), parsers.ts, │
|
|
61
|
+
ui.ts │
|
|
62
|
+
│
|
|
63
|
+
component.ts ───── effects.ts, errors.ts, parsers.ts,│
|
|
64
|
+
ui.ts, util.ts │
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The single external dependency is `@zeix/cause-effect`, which provides the reactive primitives: `createState`, `createComputed`, `createEffect`, `createMemo`, `createSensor`, `Signal`, `Memo`, `Sensor`, `batch`, and various type guards.
|
|
68
|
+
|
|
69
|
+
## The Component Lifecycle
|
|
70
|
+
|
|
71
|
+
`defineComponent(name, props, select, setup)` is the main entry point. It creates a class `Truc extends HTMLElement`, registers it via `customElements.define()`, and returns the class.
|
|
72
|
+
|
|
73
|
+
### connectedCallback — initialization
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
connectedCallback()
|
|
77
|
+
│
|
|
78
|
+
├─ 1. getHelpers(this) → [{ first, all }, resolveDependencies]
|
|
79
|
+
│ Determines query root (shadowRoot ?? this).
|
|
80
|
+
│ Tracks custom element dependencies found during queries.
|
|
81
|
+
│
|
|
82
|
+
├─ 2. ui = { ...select({ first, all }), host: this }
|
|
83
|
+
│ User-provided select function queries DOM elements.
|
|
84
|
+
│ Object is frozen — immutable after creation.
|
|
85
|
+
│
|
|
86
|
+
├─ 3. Initialize signals for each property:
|
|
87
|
+
│ ├─ Parser (≥2 args)? → parser(ui, this.getAttribute(key))
|
|
88
|
+
│ ├─ Function (1 arg)? → reader(ui) or methodProducer(ui)
|
|
89
|
+
│ └─ Otherwise → use value directly (static or Signal)
|
|
90
|
+
│ Each result is passed to #setAccessor(key, value).
|
|
91
|
+
│
|
|
92
|
+
└─ 4. resolveDependencies(() => {
|
|
93
|
+
this.#cleanup = runEffects(ui, setup(ui))
|
|
94
|
+
})
|
|
95
|
+
Waits for child custom elements to be defined (50ms timeout),
|
|
96
|
+
then runs the setup function and activates effects.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### #setAccessor — signal creation
|
|
100
|
+
|
|
101
|
+
Takes a key and a value and creates the appropriate signal:
|
|
102
|
+
|
|
103
|
+
- Already a `Signal` → use directly
|
|
104
|
+
- A function → `createComputed(fn)` (read-only)
|
|
105
|
+
- Anything else → `createState(value)` (read-write)
|
|
106
|
+
|
|
107
|
+
For mutable signals, the value is wrapped in a `createSlot(signal)` — a Slot from `@zeix/cause-effect` that acts as an indirection layer. The Slot's `get`/`set` are used as the property descriptor on the component instance, which is what makes `host.count` reactive. Reading calls `signal.get()` inside effects, registering the dependency automatically.
|
|
108
|
+
|
|
109
|
+
The Slot enables signal swapping: if `#setAccessor` is called again for an existing key (e.g., via `attributeChangedCallback`), it calls `slot.replace(newSignal)` instead of redefining the property. This is also the mechanism used by `pass()` to inject parent signals into a child component.
|
|
110
|
+
|
|
111
|
+
### attributeChangedCallback — attribute sync
|
|
112
|
+
|
|
113
|
+
Only fires for properties whose initializer `isParser` (function with ≥2 parameters). These are collected into `static observedAttributes` at class creation time.
|
|
114
|
+
|
|
115
|
+
When an attribute changes: parse the new value through the parser, then assign it to the component property (which triggers `signal.set()`). Computed (read-only) signals are skipped.
|
|
116
|
+
|
|
117
|
+
### disconnectedCallback — cleanup
|
|
118
|
+
|
|
119
|
+
Calls the cleanup function returned by `runEffects()`, which tears down all effects and event listeners.
|
|
120
|
+
|
|
121
|
+
## The Effect System
|
|
122
|
+
|
|
123
|
+
### Three layers
|
|
124
|
+
|
|
125
|
+
1. **`runEffects(ui, effects)`** — top-level orchestrator. Iterates the keys of the effects record. For each key, checks whether `ui[key]` is a `Memo` (from `all()`) or a single `Element` (from `first()`), and delegates accordingly.
|
|
126
|
+
|
|
127
|
+
2. **`runElementsEffects(host, elements, effects)`** — handles dynamic collections. Creates a `createEffect()` that watches `Memo<E[]>`, computes added/removed elements by diffing against currently attached cleanups, and attaches/detaches per-element effects.
|
|
128
|
+
|
|
129
|
+
3. **`runElementEffects(host, target, effects)`** — runs one or many effect functions against a single target element, collecting their cleanup functions.
|
|
130
|
+
|
|
131
|
+
### updateElement — the shared abstraction
|
|
132
|
+
|
|
133
|
+
Every built-in effect (`setAttribute`, `toggleClass`, `setText`, `setProperty`, `setStyle`, `toggleAttribute`, `dangerouslySetInnerHTML`, `callMethod`, `focus`, `show`) follows the same pattern via `updateElement(reactive, updater)`:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
updateElement(reactive, { op, name, read, update, delete? })
|
|
137
|
+
│
|
|
138
|
+
├─ Captures fallback = read(target) ← current DOM value
|
|
139
|
+
│
|
|
140
|
+
└─ createEffect(() => {
|
|
141
|
+
value = resolveReactive(reactive) ← auto-tracks signal deps
|
|
142
|
+
if value === RESET → use fallback
|
|
143
|
+
if value === null → delete(target) if available, else use fallback
|
|
144
|
+
if value !== current → update(target, value)
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The `Reactive<T>` type is a union of three forms:
|
|
149
|
+
- `keyof P` — a string property name on the host (reads `host[name]`)
|
|
150
|
+
- `Signal<T>` — a signal (calls `.get()`)
|
|
151
|
+
- `(target: E) => T` — a reader function
|
|
152
|
+
|
|
153
|
+
`resolveReactive()` handles all three and returns the concrete value. Because it calls `.get()` inside a `createEffect`, signal dependencies are automatically tracked.
|
|
154
|
+
|
|
155
|
+
### The RESET sentinel
|
|
156
|
+
|
|
157
|
+
`RESET` is a `Symbol('RESET')` typed as `any`. When a reactive resolves to `RESET` (e.g., the reader function threw an error), the effect restores the original DOM value captured at setup time.
|
|
158
|
+
|
|
159
|
+
### Built-in effects at a glance
|
|
160
|
+
|
|
161
|
+
| Effect | Op | What it does |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `setAttribute(name, reactive?)` | `a` | Sets an attribute with URL safety validation |
|
|
164
|
+
| `toggleAttribute(name, reactive?)` | `a` | Boolean attribute: present when truthy |
|
|
165
|
+
| `toggleClass(token, reactive?)` | `c` | Adds/removes a CSS class |
|
|
166
|
+
| `setText(reactive)` | `t` | Replaces non-comment child nodes with a text node |
|
|
167
|
+
| `setProperty(key, reactive?)` | `p` | Sets a DOM property directly |
|
|
168
|
+
| `show(reactive)` | `p` | Controls `el.hidden` |
|
|
169
|
+
| `setStyle(prop, reactive?)` | `s` | Sets/removes an inline style |
|
|
170
|
+
| `dangerouslySetInnerHTML(reactive, opts?)` | `h` | Sets innerHTML, optionally in a shadow root |
|
|
171
|
+
|
|
172
|
+
All default their `reactive` parameter to the effect name (e.g., `setAttribute('href')` reads `host.href`).
|
|
173
|
+
|
|
174
|
+
### on() — event listener effect
|
|
175
|
+
|
|
176
|
+
`on(type, handler, options?)` is different from `updateElement`-based effects. It directly attaches an event listener to the target element. The handler receives the event and may return a partial property update object like `{ count: host.count + 1 }`. If it does, the updates are applied to the host in a `batch()`. For passive events (scroll, resize, touch, wheel), execution is deferred via `schedule()`.
|
|
177
|
+
|
|
178
|
+
### pass() — inter-component binding
|
|
179
|
+
|
|
180
|
+
`pass(props)` replaces the backing signal of a child Le Truc component's Slot properties. It uses `getSignals(target)` to access the child's internal signal map, then for each passed prop calls `slot.replace(signal)` with a new signal derived from the parent's reactive value. This creates a live reactive binding between parent and child without the child needing to know about the parent. No cleanup/restore is needed: when the parent unmounts, the child is torn down as well.
|
|
181
|
+
|
|
182
|
+
## The UI Query System
|
|
183
|
+
|
|
184
|
+
`getHelpers(host)` returns `[{ first, all }, resolveDependencies]`.
|
|
185
|
+
|
|
186
|
+
### first(selector, required?)
|
|
187
|
+
|
|
188
|
+
Calls `root.querySelector()`. If the matched element is an undefined custom element, its tag name is added to the dependency set. Returns the element or `undefined` (throws `MissingElementError` if `required` is provided and element is missing).
|
|
189
|
+
|
|
190
|
+
### all(selector, required?)
|
|
191
|
+
|
|
192
|
+
Returns a `Memo<E[]>` created by `createElementsMemo()`. This sets up a `MutationObserver` (lazily, via the `watched` option on `createMemo`) that watches for `childList`, `subtree`, and relevant attribute changes. The memo always contains the current matching elements; added/removed diffs are derived downstream where needed (for example in `runElementsEffects`).
|
|
193
|
+
|
|
194
|
+
The `MutationObserver` config is smart about which attributes to watch: `extractAttributes(selector)` parses the CSS selector to find attribute names implied by `.class`, `#id`, and `[attr]` patterns.
|
|
195
|
+
|
|
196
|
+
### Dependency resolution
|
|
197
|
+
|
|
198
|
+
During `first()` and `all()` calls, any matched custom element that isn't yet defined (matches `:not(:defined)`) is collected. `resolveDependencies(callback)` then awaits `customElements.whenDefined()` for all of them with a 50ms timeout. On timeout, it logs a `DependencyTimeoutError` but still runs the callback — effects proceed even if dependencies aren't ready.
|
|
199
|
+
|
|
200
|
+
### Compile-time selector type inference
|
|
201
|
+
|
|
202
|
+
The file contains a type-level CSS selector parser that infers the correct `HTMLElement` subtype from selector strings at compile time. `first('button')` returns `HTMLButtonElement`, `first('input[type="text"]')` returns `HTMLInputElement`, `first('.foo')` returns `HTMLElement`. This works through template literal types that split combinators, extract tag names, and look them up in `HTMLElementTagNameMap` / `SVGElementTagNameMap` / `MathMLElementTagNameMap`.
|
|
203
|
+
|
|
204
|
+
## The Parser System
|
|
205
|
+
|
|
206
|
+
Parsers transform HTML attribute strings into typed JavaScript values. The key design choice: **a Parser is a function with ≥2 parameters** (`(ui, value, old?) => T`), while a **Reader is any function with 1 parameter** (`(ui) => T`). This distinction is checked at runtime via `value.length >= 2` in `isParser()`.
|
|
207
|
+
|
|
208
|
+
Parsers serve dual duty:
|
|
209
|
+
1. As property initializers — `{ config: asJSON({ theme: 'light' }) }` — called during `connectedCallback` with the attribute's initial value
|
|
210
|
+
2. As attribute watchers — automatically added to `observedAttributes` and called in `attributeChangedCallback`
|
|
211
|
+
|
|
212
|
+
The `read(reader, fallback)` function composes a `LooseReader` (which may return `string | null | undefined`) with a parser/fallback into a clean `Reader<T>`. This is useful for reading DOM state and parsing it: `read(ui => ui.input.value, asInteger())`.
|
|
213
|
+
|
|
214
|
+
## Event-Driven Sensors
|
|
215
|
+
|
|
216
|
+
`createEventsSensor(init, key, events)` returns a Reader that creates a `Sensor<T>` — a signal driven by DOM events. It uses event delegation: all listeners are attached to the host, and when an event fires, the sensor finds the matching target element via `Node.contains()`.
|
|
217
|
+
|
|
218
|
+
This is more declarative than `on()`: instead of imperatively updating host properties, the sensor produces a single reactive value from multiple event types. Use case: combining `input`, `change`, `focus`, `blur` into a single state value.
|
|
219
|
+
|
|
220
|
+
The sensor is created via `createSensor(set => ...)` from `@zeix/cause-effect`, which manages the lifecycle (activate when read, deactivate when unwatched).
|
|
221
|
+
|
|
222
|
+
## The Context Protocol
|
|
223
|
+
|
|
224
|
+
Implements the [W3C Community Protocol for Context](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md) for dependency injection between components.
|
|
225
|
+
|
|
226
|
+
### Provider side
|
|
227
|
+
|
|
228
|
+
`provideContexts(['theme', 'user'])` returns a function `(host) => Cleanup` that adds a `context-request` event listener. When a matching request arrives, it stops propagation and provides a getter `() => host[context]` to the callback.
|
|
229
|
+
|
|
230
|
+
This is used as a `MethodProducer` — a property initializer that returns `void` and exists only for its side effects (setting up the listener).
|
|
231
|
+
|
|
232
|
+
### Consumer side
|
|
233
|
+
|
|
234
|
+
`requestContext('theme', 'light')` returns a `Reader<Memo<T>>` used as a property initializer. During `connectedCallback`, it dispatches a `ContextRequestEvent` that bubbles up the DOM. If an ancestor provider intercepts it, the consumer receives a getter and wraps it in a `createMemo()`, creating a live reactive binding. If no provider responds, it falls back to the provided default value.
|
|
235
|
+
|
|
236
|
+
## The Scheduler
|
|
237
|
+
|
|
238
|
+
`schedule(element, task)` deduplicates high-frequency DOM updates using `requestAnimationFrame`. A `WeakMap<Element, () => void>` stores the latest task per element. If the same element schedules multiple tasks before the next frame, only the last one runs. This is used by `on()` for passive events and by `dangerouslySetInnerHTML`.
|
|
239
|
+
|
|
240
|
+
## Security
|
|
241
|
+
|
|
242
|
+
`setAttribute()` includes security validation:
|
|
243
|
+
- Blocks `on*` event handler attributes (prevents XSS via attribute injection)
|
|
244
|
+
- Validates URLs against an allowlist of safe protocols (`http:`, `https:`, `ftp:`, `mailto:`, `tel:`) — blocks `javascript:`, `data:`, etc.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Open Questions
|
|
249
|
+
|
|
250
|
+
### Parser/Reader distinction via `function.length`
|
|
251
|
+
|
|
252
|
+
The distinction between Parser (≥2 params) and Reader (1 param) is detected at runtime via `value.length >= 2`. This is fragile — default parameters, rest parameters, and destructuring all affect `function.length` in non-obvious ways. A function `(ui, value = '') => ...` has `length === 1` and would be misclassified as a Reader. This is a potential source of subtle bugs. Would a branded type, a wrapper function, or a static property be a more robust marker?
|
|
253
|
+
|
|
254
|
+
### MethodProducer is invisible in the type system
|
|
255
|
+
|
|
256
|
+
`MethodProducer<P, U>` is defined as `(ui) => void`, but `isReaderOrMethodProducer` just checks `isFunction`. There's no way to distinguish a `Reader` from a `MethodProducer` at runtime — the only difference is that a MethodProducer returns `void` and relies on side effects (like `provideContexts`). Since `#setAccessor` is only called when the result is non-null, this works by convention, but the flow is non-obvious: the MethodProducer's return value (`undefined`) causes `#setAccessor` to be silently skipped, which is the desired behavior but isn't explicitly documented in the code.
|
|
257
|
+
|
|
258
|
+
### Dependency timeout of 50ms
|
|
259
|
+
|
|
260
|
+
`DEPENDENCY_TIMEOUT` is hardcoded at 50ms. This seems very short — on slower devices or with lazy-loaded component definitions, this could fire frequently. The error is logged but effects still run, so it's non-fatal, but it could cause effects to run against not-yet-upgraded elements. Is this timeout well-calibrated? Should it be configurable?
|
|
261
|
+
|
|
262
|
+
### `resolveDependencies` uses Promise.race with error swallowing
|
|
263
|
+
|
|
264
|
+
The dependency resolution catches all errors and runs the callback anyway. The `.catch(() => { callback() })` pattern means even unexpected errors (not just timeouts) are silently swallowed. The `DependencyTimeoutError` is constructed and passed to `reject`, but the actual logging happens... nowhere visible. The error is created inside a `new Promise((_, reject) => { reject(new DependencyTimeoutError(...)) })`, which rejects the race, but the `.catch` just calls `callback()` without logging the error.
|
|
265
|
+
|
|
266
|
+
### `createEventsSensor` captures `targets` once
|
|
267
|
+
|
|
268
|
+
In `createEventsSensor`, the `targets` array is computed once at sensor creation time from the current state of the `Memo`. If the collection changes later (elements added/removed), the sensor won't pick up new targets. For `Memo`-based collections that are specifically designed to be dynamic, this seems like a gap.
|
|
269
|
+
|
|
270
|
+
### `dangerouslySetInnerHTML` script handling
|
|
271
|
+
|
|
272
|
+
The script re-execution logic clones scripts by copying only `textContent` and `type`. This drops `src`, `async`, `defer`, `crossorigin`, `integrity`, `nomodule`, and other attributes. External scripts (`<script src="...">`) will silently become empty inline scripts. Is this intentional (security boundary) or an oversight?
|