diff --git a/ACTIONS_API.md b/ACTIONS_API.md new file mode 100644 index 0000000..0cbb4ba --- /dev/null +++ b/ACTIONS_API.md @@ -0,0 +1,585 @@ +# Action Methods API Reference + +Simplified action methods for Datastar HTTP actions with smart event detection. These methods reduce verbosity while maintaining full access to Datastar's capabilities. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Smart Event Detection](#smart-event-detection) +- [CSRF-Protected Actions](#csrf-protected-actions) +- [Standard Datastar Actions](#standard-datastar-actions) +- [Navigate Action](#navigate-action) +- [Dispatch Action](#dispatch-action) +- [Event Modifiers](#event-modifiers) +- [Options Reference](#options-reference) +- [Common Patterns](#common-patterns) + +## Quick Start + +```php +use Dancycodes\Hyper\Html\Html; + +// Before: verbose dataOn() syntax +Html::button()->dataOn('click', '@postx("/save")'); + +// After: simplified action method with smart event detection +Html::button()->postx('/save'); + +// Forms automatically use 'submit__prevent' +Html::form()->postx('/submit'); // Uses submit__prevent event + +// Inputs automatically use 'input' event +Html::input()->get('/search'); // Uses input event +``` + +## Smart Event Detection + +Action methods automatically select the appropriate event based on element type and attributes: + +| Element Type | Default Event | Reasoning | +|-------------|---------------|-----------| +| `form` | `submit__prevent` | Prevents default form submission | +| `button`, `a` | `click` | Standard click interaction | +| `input[type=checkbox]` | `change` | Fires when checked/unchecked | +| `input[type=radio]` | `change` | Fires when selection changes | +| `input[type=file]` | `change` | Fires when file selected | +| `input[type=color]` | `change` | Fires when color selected | +| `input` (text-like) | `input` | Fires on every keystroke/change | +| `select` | `change` | Fires when option selected | +| `textarea` | `input` | Fires on every keystroke | +| All others | `click` | Safe default for interactions | + +### Input Type Detection + +For `` elements, the smart detection checks the `type` attribute: + +**Change Event Types** (state-based interactions): +- `checkbox` - Toggle checked state +- `radio` - Select from radio group +- `file` - Select file from file system +- `color` - Pick color from color picker + +**Input Event Types** (continuous feedback): +- `text`, `email`, `password` - Text entry +- `number`, `date`, `time` - Numeric/date entry +- `search`, `url`, `tel` - Specialized text entry +- `range` - Slider movement +- All other input types default to `input` + +### Smart Detection Examples + +```php +// Text input - automatically uses 'input' event +Html::input()->type('text')->get('/autosave'); +// Generates: data-on:input="@get('/autosave')" + +// Checkbox - automatically uses 'change' event +Html::input()->type('checkbox')->patchx('/toggle-active'); +// Generates: data-on:change="@patchx('/toggle-active')" + +// Radio button - automatically uses 'change' event +Html::input()->type('radio')->patchx('/select-option'); +// Generates: data-on:change="@patchx('/select-option')" + +// File input - automatically uses 'change' event +Html::input()->type('file')->postx('/upload'); +// Generates: data-on:change="@postx('/upload')" + +// Select dropdown - automatically uses 'change' event +Html::select()->get('/filter'); +// Generates: data-on:change="@get('/filter')" + +// Form - automatically uses 'submit__prevent' event +Html::form()->postx('/submit'); +// Generates: data-on:submit__prevent="@postx('/submit')" +``` + +### Override Smart Defaults + +Explicitly pass an event to override the smart default: + +```php +// Override form's default submit__prevent +Html::form()->postx('/save', 'change'); + +// Override checkbox's default change event +Html::input()->type('checkbox')->patchx('/toggle', 'click'); + +// Add event modifiers to text input +Html::input()->type('text')->get('/search', 'input__debounce.300ms'); + +// Use window event modifier +Html::div()->get('/refresh', 'click__window'); +``` + +## CSRF-Protected Actions + +Laravel Hyper extends Datastar with CSRF-protected variants of mutating actions. **Always use these for Laravel applications.** + +### postx() + +POST request with automatic CSRF token inclusion. + +```php +public function postx(string $url, ?string $event = null, array $options = []): static +``` + +**Usage:** + +```php +// Simple POST (uses smart event: 'click' for button) +Html::button()->postx('/create'); + +// POST on form (uses smart event: 'submit__prevent') +Html::form()->postx('/submit'); + +// Explicit event with modifiers +Html::button()->postx('/save', 'dblclick__once'); + +// With options +Html::button()->postx('/save', null, [ + 'headers' => ['X-Custom' => 'value'], + 'retry' => 3 +]); +``` + +### putx() + +PUT request with automatic CSRF token inclusion. + +```php +public function putx(string $url, ?string $event = null, array $options = []): static +``` + +**Usage:** + +```php +// Full resource update +Html::button()->putx('/users/123'); + +// With explicit event +Html::form()->putx('/update', 'submit__prevent'); +``` + +### patchx() + +PATCH request with automatic CSRF token inclusion. + +```php +public function patchx(string $url, ?string $event = null, array $options = []): static +``` + +**Usage:** + +```php +// Partial resource update +Html::button()->patchx('/users/123'); + +// Toggle status on checkbox change +Html::input() + ->type('checkbox') + ->patchx('/toggle-status', 'change'); +``` + +### deletex() + +DELETE request with automatic CSRF token inclusion. + +```php +public function deletex(string $url, ?string $event = null, array $options = []): static +``` + +**Usage:** + +```php +// Simple delete +Html::button()->deletex('/items/123'); + +// With confirmation (use window modifier for global event) +Html::button()->deletex('/items/123', 'click__window'); +``` + +## Standard Datastar Actions + +These are the standard Datastar actions **without CSRF protection**. Use these only when: +- Interfacing with external APIs +- Working with read-only GET operations +- You've handled CSRF another way + +### get() + +GET request to fetch data or HTML. + +```php +public function get(string $url, ?string $event = null, array $options = []): static +``` + +**Usage:** + +```php +// Simple GET +Html::button()->get('/fetch-data'); + +// Search on input with debounce +Html::input()->get('/search', 'input__debounce.300ms'); + +// Infinite scroll on intersection +Html::div()->get('/load-more', 'intersect__once'); + +// With options +Html::button()->get('/api/data', null, [ + 'headers' => ['Accept' => 'application/json'] +]); +``` + +### post(), put(), patch(), delete() + +Standard Datastar actions **without CSRF protection**. + +```php +public function post(string $url, ?string $event = null, array $options = []): static +public function put(string $url, ?string $event = null, array $options = []): static +public function patch(string $url, ?string $event = null, array $options = []): static +public function delete(string $url, ?string $event = null, array $options = []): static +``` + +**⚠️ Warning:** Use the `*x` variants (postx, putx, patchx, deletex) for Laravel applications instead. + +## Navigate Action + +Client-side navigation with optional targeted updates. Updates browser URL and fetches new content without full page reload. + +```php +public function navigate( + string $url, + ?string $event = null, + ?string $key = null, + array $options = [] +): static +``` + +**Parameters:** +- `$url`: Target URL to navigate to +- `$event`: Event trigger (null = smart default) +- `$key`: Optional navigation key for targeted updates +- `$options`: Datastar fetch options + +**Usage:** + +```php +// Simple navigation +Html::a() + ->href('/dashboard') + ->navigate('/dashboard'); + +// Targeted navigation (only updates specific fragment) +Html::a()->navigate('/sidebar', null, 'sidebar-content'); + +// With explicit event +Html::div()->navigate('/page', 'dblclick'); + +// With options +Html::a()->navigate('/page', null, 'main', [ + 'headers' => ['X-Partial' => 'true'] +]); + +// Navigation with event modifiers +Html::a()->navigate('/content', 'click__prevent', 'main'); +``` + +## Dispatch Action + +Dispatches custom JavaScript events with optional detail data. Useful for component communication and decoupled architectures. + +```php +public function dispatch( + string $eventName, + ?string $event = null, + array $detail = [] +): static +``` + +**Parameters:** +- `$eventName`: Custom event name to dispatch +- `$event`: DOM event that triggers dispatch (null = smart default) +- `$detail`: Optional event detail data (sent as `event.detail`) + +**Usage:** + +```php +// Simple event dispatch +Html::button()->dispatch('modal-open'); + +// With event detail +Html::button()->dispatch('user-selected', null, [ + 'userId' => 123, + 'username' => 'john' +]); + +// Explicit trigger event +Html::div()->dispatch('item-clicked', 'dblclick'); + +// Complex detail data +Html::button()->dispatch('form-ready', null, [ + 'formId' => 'user-form', + 'fields' => ['name', 'email'], + 'timestamp' => time() +]); +``` + +## Event Modifiers + +Event modifiers control event behavior. Use Datastar's double underscore (`__`) syntax: + +| Modifier | Description | Example | +|---------|-------------|---------| +| `prevent` | Calls `preventDefault()` | `click__prevent` | +| `stop` | Calls `stopPropagation()` | `click__stop` | +| `once` | Fire only once | `click__once` | +| `passive` | Passive event listener | `scroll__passive` | +| `capture` | Use capture phase | `click__capture` | +| `debounce.{time}ms` | Debounce execution | `input__debounce.300ms` | +| `throttle.{time}ms` | Throttle execution | `scroll__throttle.250ms` | +| `delay.{time}ms` | Simple delay | `click__delay.500ms` | +| `window` | Attach to window | `resize__window` | +| `outside` | Trigger when click outside | `click__outside` | +| `document` | Attach to document | `keydown__document` | +| `viewtransition` | Use View Transition API | `click__viewtransition` | + +### Modifier Examples + +```php +// Prevent default form submission +Html::form()->postx('/save', 'submit__prevent'); + +// Debounced search input +Html::input()->get('/search', 'input__debounce.300ms'); + +// Fire only once +Html::button()->postx('/init', 'click__once'); + +// Window resize event +Html::div()->get('/layout', 'resize__window'); + +// Combine multiple modifiers +Html::button()->postx('/save', 'click__prevent__stop__once'); + +// Throttle scroll event +Html::div()->get('/load-items', 'scroll__throttle.250ms'); +``` + +## Options Reference + +All action methods accept an optional `$options` array for Datastar fetch configuration: + +```php +Html::button()->postx('/save', null, [ + // Custom headers + 'headers' => [ + 'X-Custom-Header' => 'value', + 'Accept' => 'application/json' + ], + + // Retry configuration + 'retry' => 3, // Number of retries + 'retryDelay' => 1000, // Delay between retries (ms) + + // Credentials handling + 'credentials' => 'include', // 'omit', 'same-origin', 'include' + + // Filter signals sent to server + 'filterSignals' => ['userId', 'formData'], + + // Content-Type override + 'contentType' => 'application/json', + + // Timeout + 'timeout' => 5000, // Request timeout (ms) +]); +``` + +### Common Options Patterns + +```php +// API endpoint with JSON +Html::button()->postx('/api/save', null, [ + 'headers' => ['Accept' => 'application/json'], + 'contentType' => 'application/json' +]); + +// Retry on failure +Html::button()->postx('/unreliable-endpoint', null, [ + 'retry' => 5, + 'retryDelay' => 2000 +]); + +// Filter sensitive signals +Html::form()->postx('/submit', null, [ + 'filterSignals' => ['email', 'formData'] +]); +``` + +## Common Patterns + +### Search with Debounce + +```php +Html::input() + ->type('search') + ->name('q') + ->placeholder('Search...') + ->dataBind('searchQuery') + ->get('/search', 'input__debounce.300ms') + ->dataIndicator('searching'); +``` + +### Save Button with Loading State + +```php +Html::button() + ->type('submit') + ->class('btn btn-primary') + ->dataIndicator('saving') + ->postx('/save') + ->dataAttr('disabled', '$saving') + ->content( + Html::span()->dataShow('!$saving')->text('Save'), + Html::span()->dataShow('$saving')->text('Saving...') + ); +``` + +### Infinite Scroll + +```php +Html::div() + ->id('scroll-trigger') + ->get('/load-more', 'intersect__once') + ->dataIndicator('loading') + ->content( + Html::div()->dataShow('$loading')->text('Loading...') + ); +``` + +### Delete with Confirmation + +```php +Html::button() + ->class('btn btn-danger') + ->dataIndicator('deleting') + ->deletex('/items/123', 'click__window') // window modifier for global confirmation + ->dataAttr('disabled', '$deleting') + ->text('Delete'); +``` + +### Form with Multiple Submit Actions + +```php +Html::form() + ->dataSignals(['status' => 'draft']) + ->dataIndicator('submitting') + ->content( + Html::input()->type('text')->name('title')->dataBind('title'), + + // Save as draft + Html::button() + ->type('submit') + ->postx('/save?status=draft') + ->dataAttr('disabled', '$submitting') + ->text('Save Draft'), + + // Publish + Html::button() + ->type('submit') + ->postx('/save?status=published') + ->dataAttr('disabled', '$submitting') + ->text('Publish') + ); +``` + +### Navigation with Targeted Updates + +```php +// Main navigation link +Html::a() + ->href('/dashboard') + ->class('nav-link') + ->navigate('/dashboard', null, 'main-content', [ + 'headers' => ['X-Partial' => 'main-content'] + ]) + ->text('Dashboard'); + +// Sidebar navigation (updates sidebar only) +Html::a() + ->href('/sidebar/users') + ->navigate('/sidebar/users', null, 'sidebar', [ + 'headers' => ['X-Partial' => 'sidebar'] + ]) + ->text('Users'); +``` + +### Auto-save on Input + +```php +Html::input() + ->type('text') + ->name('title') + ->dataBind('title') + ->patchx('/autosave', 'input__debounce.500ms') + ->dataIndicator('autoSaving') + ->dataAttr('title', "$autoSaving ? 'Saving...' : 'Auto-save enabled'"); +``` + +### Toggle on Checkbox + +```php +// Smart detection automatically uses 'change' event +Html::input() + ->type('checkbox') + ->name('active') + ->dataBind('isActive') + ->patchx('/toggle-active') // No need to specify 'change'! + ->dataIndicator('toggling') + ->dataAttr('disabled', '$toggling'); +``` + +### File Upload Preview + +```php +// Smart detection automatically uses 'change' event for file inputs +Html::input() + ->type('file') + ->name('avatar') + ->dataBind('avatarFile') + ->postx('/upload') // No need to specify 'change'! + ->dataIndicator('uploading'); +``` + +## Migration Guide + +Migrating from verbose `dataOn()` syntax to simplified action methods: + +```php +// Before +Html::button()->dataOn('click', '@postx("/save")'); + +// After +Html::button()->postx('/save'); + +// Before (with event modifiers) +Html::input()->dataOn('input__debounce.300ms', '@get("/search")'); + +// After +Html::input()->get('/search', 'input__debounce.300ms'); + +// Before (with options) +Html::button()->dataOn('click', '@postx("/save", {retry: 3})'); + +// After +Html::button()->postx('/save', null, ['retry' => 3]); +``` + +## See Also + +- [Loading States Documentation](./LOADING_STATES.md) +- [Datastar Backend Actions](https://data-star.dev/reference/plugins/backend) +- [Datastar Event Modifiers](https://data-star.dev/reference/plugins/backend#event-modifiers) diff --git a/resources/js/hyper.js b/resources/js/hyper.js index 946bd46..bfb8027 100644 --- a/resources/js/hyper.js +++ b/resources/js/hyper.js @@ -2593,7 +2593,6 @@ action({ const opts = options || {}; const fallback = opts.fallback || ""; const defaultMime = opts.defaultMime || "application/octet-stream"; - const mimeSignal = opts.mimeSignal; if (fileSource == null) { return fallback; } @@ -2607,21 +2606,10 @@ action({ if (!contents || typeof contents !== "string") { return fallback; } - const mimeType2 = mime || defaultMime; - return `data:${mimeType2};base64,${contents}`; + const mimeType = mime || defaultMime; + return `data:${mimeType};base64,${contents}`; } - const base64Content = firstItem; - if (!base64Content || typeof base64Content !== "string") { - return fallback; - } - let mimeType = defaultMime; - if (mimeSignal && typeof window !== "undefined") { - const mimeArray = (window.$ || {})[mimeSignal]; - if (Array.isArray(mimeArray) && mimeArray.length > 0) { - mimeType = mimeArray[0] || defaultMime; - } - } - return `data:${mimeType};base64,${base64Content}`; + return fallback; } if (typeof fileSource === "string") { const trimmed = fileSource.trim(); @@ -2759,11 +2747,12 @@ attribute({ const keyExpression = keyMod && keyMod.size > 0 ? Array.from(keyMod)[0] : null; const template = el; template.style.display = "none"; - const templateContent = template.content.cloneNode(true); + const templateContent = template.content.cloneNode( + true + ); const sourceSignalPath = parsed.items; const sourceData = getPath(sourceSignalPath); const isNormalized = shouldNormalizeData(sourceData); - const iterationId = `__for_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const state = { prevKeys: [], lookup: /* @__PURE__ */ new Map(), @@ -2774,41 +2763,46 @@ attribute({ effectCleanup: null, sourceSignalPath, isNormalized, - iterationId + isUpdating: false, + justCleaned: false }; iterationStates.set(el, state); const effectCleanup = effect(() => { - const sourceData2 = getPath(sourceSignalPath); - if (Array.isArray(sourceData2)) { - const reactiveArray = root[sourceSignalPath]; - if (reactiveArray && reactiveArray.length > 0) { - const firstItem = reactiveArray[0]; - if (firstItem && typeof firstItem === "object" && !Array.isArray(firstItem)) { - for (const key in reactiveArray) { - const item = reactiveArray[key]; - if (item && typeof item === "object") { - for (const prop in item) { - void item[prop]; - } - } - } + if (state.isUpdating) { + return; + } + state.isUpdating = true; + try { + let sourceData2 = getPath(sourceSignalPath); + if (Array.isArray(sourceData2)) { + sourceData2 = sourceData2.slice(); + } + if (state.justCleaned && Array.isArray(sourceData2) && state.prevKeys.length > 0) { + const currentLength = sourceData2.length; + const expectedLength = state.prevKeys.length; + if (currentLength > expectedLength) { + state.justCleaned = false; + return; } } + state.justCleaned = false; + const items = normalizeData(sourceData2); + const newArrayWithKeys = items.map((item, index) => ({ + item, + index, + key: evaluateKey(keyExpression, item, index) + })); + peek(() => { + beginBatch(); + try { + diffAndUpdate(newArrayWithKeys, state, ctx); + } finally { + endBatch(); + } + }); + } finally { + state.isUpdating = false; } - const items = normalizeData(sourceData2); - const newArrayWithKeys = items.map((item, index) => ({ - item, - index, - key: evaluateKey(keyExpression, item, index) - })); - peek(() => { - beginBatch(); - try { - diffAndUpdate(newArrayWithKeys, state, ctx); - } finally { - endBatch(); - } - }); }); state.effectCleanup = effectCleanup; return () => { @@ -2858,29 +2852,30 @@ function diffAndUpdate(newArrayWithKeys, state, ctx) { const prevKeys = state.prevKeys; const changeType = detectChange(prevKeys, newKeys); if (changeType === "no-change") { - if (!state.isNormalized && finalArray.length > 0) { - const firstItem = finalArray[0].item; - if (firstItem && typeof firstItem === "object" && !Array.isArray(firstItem)) { - for (const itemData of finalArray) { - const loopEl = state.lookup.get(itemData.key); - if (loopEl) { - const sanitizedKey = sanitizeKey(itemData.key); - mergePatch({ - [state.iterationId]: { - [sanitizedKey]: itemData.item - } - }); - } - } - } - } return; } else if (changeType === "simple-add") { handleAdd(finalArray, state); } else if (changeType === "simple-remove") { handleRemove(prevKeys, newKeys, state, ctx, finalArray); } else { - handleReorder(finalArray, state); + handleReorder(finalArray, state, ctx); + } + const oldLength = prevKeys.length; + const newLength = newKeys.length; + if (newLength < oldLength) { + const arrayProxy = root[state.sourceSignalPath]; + if (arrayProxy) { + startPeeking(); + try { + arrayProxy.length = newLength; + for (let i = newLength; i < oldLength; i++) { + delete arrayProxy[i]; + } + } finally { + stopPeeking(); + } + state.justCleaned = true; + } } state.prevKeys = [...newKeys]; } @@ -2939,164 +2934,201 @@ function handleRemove(prevKeys, newKeys, state, _ctx, data) { for (const itemData of data) { const loopEl = state.lookup.get(itemData.key); if (loopEl && loopEl.index !== itemData.index) { + const oldIndex = loopEl.index; loopEl.index = itemData.index; if (!state.isNormalized) { - const sanitizedKey = sanitizeKey(itemData.key); - const currentData = root[state.iterationId]?.[sanitizedKey]; - if (currentData !== void 0) { - mergePatch({ - [state.iterationId]: { - [`${sanitizedKey}__index`]: itemData.index - } - }); - } + retransformElement(loopEl.el, state, oldIndex, itemData.index); } } } } function handleReorder(data, state, ctx) { const newSet = new Set(data.map((d) => d.key)); - for (const pk of state.prevKeys) { - if (!newSet.has(pk)) { - const loopEl = state.lookup.get(pk); - if (loopEl) { - loopEl.el.remove(); - state.lookup.delete(pk); + beginBatch(); + startPeeking(); + try { + for (const pk of state.prevKeys) { + if (!newSet.has(pk)) { + const loopEl = state.lookup.get(pk); + if (loopEl) { + loopEl.el.remove(); + state.lookup.delete(pk); + } + } + } + let prevEl = state.template; + for (const item of data) { + const existing = state.lookup.get(item.key); + if (existing) { + if (existing.index !== item.index) { + const oldIndex = existing.index; + existing.index = item.index; + if (!state.isNormalized) { + retransformElement( + existing.el, + state, + oldIndex, + item.index + ); + } + } + if (existing.el.previousElementSibling !== prevEl) { + prevEl.after(existing.el); + } + prevEl = existing.el; + } else { + const loopEl = createElement(item, state, ctx); + state.lookup.set(item.key, loopEl); + prevEl.after(loopEl.el); + prevEl = loopEl.el; } } - } - let prevEl = state.template; - for (const item of data) { - const existing = state.lookup.get(item.key); - if (existing) { - if (existing.index !== item.index) { - existing.index = item.index; - if (!state.isNormalized) { - const sanitizedKey = sanitizeKey(item.key); - const currentData = root[state.iterationId]?.[sanitizedKey]; - if (currentData !== void 0) { - mergePatch({ - [state.iterationId]: { - [`${sanitizedKey}__index`]: item.index - } - }); + } finally { + stopPeeking(); + endBatch(); + queueMicrotask(() => { + startPeeking(); + try { + for (const item of data) { + const loopEl = state.lookup.get(item.key); + if (loopEl && !loopEl.el.dataset.__hyperForInitialized) { + apply(loopEl.el); + loopEl.el.dataset.__hyperForInitialized = "true"; } } + } finally { + stopPeeking(); } - if (existing.el.previousElementSibling !== prevEl) { - prevEl.after(existing.el); - } - prevEl = existing.el; - } else { - const loopEl = createElement(item, state); - state.lookup.set(item.key, loopEl); - prevEl.after(loopEl.el); - prevEl = loopEl.el; - queueMicrotask(() => apply(loopEl.el)); - } + }); } } function createElement(itemData, state, _ctx) { - const { templateContent, sourceSignalPath, iteratorNames, isNormalized, iterationId } = state; + const { templateContent, sourceSignalPath, iteratorNames, isNormalized } = state; const clone = templateContent.cloneNode(true); const el = clone.firstElementChild; - let signalPath = sourceSignalPath; - let indexSignalPath = null; - if (!isNormalized) { - const sanitizedKey = sanitizeKey(itemData.key); - signalPath = `${iterationId}.${sanitizedKey}`; - indexSignalPath = `${iterationId}.${sanitizedKey}__index`; - mergePatch({ - [iterationId]: { - [sanitizedKey]: itemData.item, - [`${sanitizedKey}__index`]: itemData.index - } - }); - } - transformElement(el, iteratorNames, itemData.index, signalPath, indexSignalPath, isNormalized, itemData.item); + transformElement( + el, + iteratorNames, + itemData.index, + sourceSignalPath, + isNormalized, + itemData.item + ); return { el, key: itemData.key, index: itemData.index }; } -function sanitizeKey(key) { - return String(key).replace(/[^a-zA-Z0-9_]/g, "_"); -} -function transformElement(el, iterators, index, signalPath, indexSignalPath, isNormalized, value) { +function transformElement(el, iterators, index, sourceArrayPath, isNormalized, value) { const process = (elem) => { Array.from(elem.attributes).forEach((attr) => { if (attr.name.startsWith("data-") && attr.value) { + const originalAttrName = `data-hyper-original-${attr.name.slice(5)}`; + if (!elem.hasAttribute(originalAttrName)) { + elem.setAttribute(originalAttrName, attr.value); + } attr.value = transformExpression( attr.value, attr.name, iterators, index, - signalPath, - indexSignalPath, + sourceArrayPath, isNormalized, value ); } }); if (elem instanceof HTMLTemplateElement && elem.content) { - Array.from(elem.content.children).forEach((child) => process(child)); + Array.from(elem.content.children).forEach( + (child) => process(child) + ); } else { Array.from(elem.children).forEach((child) => process(child)); } }; process(el); } -function transformExpression(expr, attrName, iterators, index, signalPath, indexSignalPath, isNormalized, value) { +function transformExpression(expr, attrName, iterators, index, sourceArrayPath, isNormalized, value) { let result = expr; - const isSignalName = /^(bind|ref|indicator|signals|computed)/.test(attrName.replace(/^data-/, "")); + const isSignalName = /^(bind|ref|indicator|signals|computed|error)/.test( + attrName.replace(/^data-/, "") + ); const literals = []; - result = result.replace(/'(?:[^'\\]|\\.)*'/g, (m) => (literals.push(m), `__LIT${literals.length - 1}__`)).replace(/"(?:[^"\\]|\\.)*"/g, (m) => (literals.push(m), `__LIT${literals.length - 1}__`)).replace(/`(?:[^`\\]|\\.)*`/g, (m) => (literals.push(m), `__LIT${literals.length - 1}__`)); + result = result.replace( + /'(?:[^'\\]|\\.)*'/g, + (m) => (literals.push(m), `__LIT${literals.length - 1}__`) + ).replace( + /"(?:[^"\\]|\\.)*"/g, + (m) => (literals.push(m), `__LIT${literals.length - 1}__`) + ).replace( + /`(?:[^`\\]|\\.)*`/g, + (m) => (literals.push(m), `__LIT${literals.length - 1}__`) + ); if (iterators.index) { - if (indexSignalPath && !isNormalized) { - const prefix = isSignalName ? "" : "$"; - result = result.replace(new RegExp(`\\b${esc(iterators.index)}\\b`, "g"), prefix + indexSignalPath); - } else { - result = result.replace(new RegExp(`\\b${esc(iterators.index)}\\b`, "g"), index.toString()); - } + result = result.replace( + new RegExp(`\\b${esc(iterators.index)}\\b`, "g"), + index.toString() + ); } if (iterators.collection) { const prefix = isSignalName ? "" : "$"; - result = result.replace(new RegExp(`\\b${esc(iterators.collection)}\\b`, "g"), prefix + signalPath); + result = result.replace( + new RegExp(`\\b${esc(iterators.collection)}\\b`, "g"), + prefix + sourceArrayPath + ); } if (isDestructured(iterators.item)) { const vars = extractVars(iterators.item); const isArrayDestructuring = iterators.item.trim().startsWith("["); vars.forEach((v, idx) => { - let varSignalPath; + let varPath; let varValue; if (isArrayDestructuring) { - varSignalPath = `${signalPath}.${idx}`; + varPath = isNormalized ? `${sourceArrayPath}.${idx}` : `${sourceArrayPath}.${index}.${idx}`; varValue = Array.isArray(value) ? value[idx] : value; } else { - varSignalPath = `${signalPath}.${v}`; + varPath = isNormalized ? `${sourceArrayPath}.${v}` : `${sourceArrayPath}.${index}.${v}`; varValue = value && typeof value === "object" ? value[v] : value; } - result = transformVar(result, v, varSignalPath, isSignalName, isNormalized, varValue); + result = transformVar( + result, + v, + varPath, + isSignalName, + isNormalized, + varValue + ); }); } else { - result = transformVar(result, iterators.item, signalPath, isSignalName, isNormalized, value); + const itemPath = isNormalized ? sourceArrayPath : `${sourceArrayPath}.${index}`; + result = transformVar( + result, + iterators.item, + itemPath, + isSignalName, + isNormalized, + value + ); } result = result.replace(/__LIT(\d+)__/g, (_, i) => literals[parseInt(i)]); return result; } -function transformVar(expr, varName, signalPath, isSignalName, isNormalized, value, _iterators) { +function transformVar(expr, varName, arrayPath, isSignalName, isNormalized, value) { if (isNormalized) { - return expr.replace(new RegExp(`\\b${esc(varName)}\\b(?!\\.)`, "g"), JSON.stringify(value)); + return expr.replace( + new RegExp(`\\b${esc(varName)}\\b(?!\\.)`, "g"), + JSON.stringify(value) + ); } const prefix = isSignalName ? "" : "$"; expr = expr.replace( new RegExp(`\\b${esc(varName)}\\.([a-zA-Z_][a-zA-Z0-9_.]*)\\b`, "g"), - `${prefix}${signalPath}.$1` + `${prefix}${arrayPath}.$1` ); expr = expr.replace( new RegExp(`\\b${esc(varName)}\\b(?!\\.)`, "g"), - prefix + signalPath + prefix + arrayPath ); return expr; } @@ -3106,7 +3138,8 @@ function shouldNormalizeData(data) { function normalizeData(data) { if (data == null) return []; if (Array.isArray(data)) return data; - if (typeof data === "number") return Array.from({ length: data }, (_, i) => i + 1); + if (typeof data === "number") + return Array.from({ length: data }, (_, i) => i + 1); if (typeof data === "object") return Object.entries(data); return [data]; } @@ -3147,6 +3180,51 @@ function extractVars(item) { function esc(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +function retransformElement(el, state, oldIndex, newIndex) { + if (oldIndex === newIndex) return; + const { sourceSignalPath, iteratorNames, isNormalized } = state; + const sourceData = getPath(sourceSignalPath); + const items = normalizeData(sourceData); + const itemValue = items[newIndex]; + const processElement = (elem) => { + Array.from(elem.attributes).forEach((attr) => { + if (attr.name.startsWith("data-") && !attr.name.startsWith("data-hyper-original-")) { + const originalAttrName = `data-hyper-original-${attr.name.slice(5)}`; + const originalValue = elem.getAttribute(originalAttrName); + if (originalValue) { + attr.value = transformExpression( + originalValue, + attr.name, + iteratorNames, + newIndex, + sourceSignalPath, + isNormalized, + itemValue + ); + } + } + }); + if (elem instanceof HTMLTemplateElement && elem.content) { + Array.from(elem.content.children).forEach((child) => { + if (!child.hasAttribute("data-for")) { + processElement(child); + } + }); + } else { + Array.from(elem.children).forEach((child) => { + if (!child.hasAttribute("data-for")) { + processElement(child); + } + }); + } + }; + processElement(el); + queueMicrotask(() => { + peek(() => { + apply(el); + }); + }); +} function cleanup(state) { if (state.effectCleanup) { state.effectCleanup(); diff --git a/resources/js/hyper.js.map b/resources/js/hyper.js.map index 4f98194..c3642ee 100644 --- a/resources/js/hyper.js.map +++ b/resources/js/hyper.js.map @@ -1 +1 @@ -{"version":3,"file":"hyper.js","sources":["../src/datastar/engine/consts.ts","../src/datastar/utils/text.ts","../src/datastar/utils/polyfills.ts","../src/datastar/utils/paths.ts","../src/datastar/engine/signals.ts","../src/datastar/utils/dom.ts","../src/datastar/engine/engine.ts","../src/datastar/plugins/actions/peek.ts","../src/datastar/plugins/actions/setAll.ts","../src/datastar/plugins/actions/toggleAll.ts","../src/datastar/plugins/actions/fetch.ts","../src/datastar/plugins/attributes/attr.ts","../src/datastar/plugins/attributes/bind.ts","../src/datastar/plugins/attributes/class.ts","../src/datastar/plugins/attributes/computed.ts","../src/datastar/plugins/attributes/effect.ts","../src/datastar/plugins/attributes/indicator.ts","../src/datastar/plugins/attributes/jsonSignals.ts","../src/datastar/utils/tags.ts","../src/datastar/utils/timing.ts","../src/datastar/utils/view-transitions.ts","../src/datastar/plugins/attributes/on.ts","../src/datastar/plugins/attributes/onIntersect.ts","../src/datastar/plugins/attributes/onInterval.ts","../src/datastar/plugins/attributes/init.ts","../src/datastar/plugins/attributes/onSignalPatch.ts","../src/datastar/plugins/attributes/ref.ts","../src/datastar/plugins/attributes/show.ts","../src/datastar/plugins/attributes/signals.ts","../src/datastar/plugins/attributes/style.ts","../src/datastar/plugins/attributes/text.ts","../src/datastar/engine/morph.ts","../src/datastar/plugins/watchers/patchElements.ts","../src/datastar/plugins/watchers/patchSignals.ts","../src/hyper-ext/plugins/actions/dispatch.ts","../src/hyper-ext/plugins/actions/fileUrl.ts","../src/hyper-ext/plugins/actions/back.ts","../src/hyper-ext/plugins/actions/refresh.ts","../src/hyper-ext/plugins/actions/reload.ts","../src/hyper-ext/plugins/attributes/error.ts","../src/hyper-ext/plugins/attributes/for.ts","../src/hyper-ext/plugins/attributes/if.ts","../src/hyper-ext/plugins/attributes/navigate.ts","../src/hyper-ext/utils/csrf.ts","../src/hyper-ext/plugins/backend/actions/fetch-laravel.ts","../src/hyper-ext/plugins/backend/actions/postx.ts","../src/hyper-ext/plugins/backend/actions/putx.ts","../src/hyper-ext/plugins/backend/actions/patchx.ts","../src/hyper-ext/plugins/backend/actions/deletex.ts","../src/hyper-ext/plugins/backend/actions/navigate.ts","../src/hyper-ext/plugins/backend/watchers/responseInterceptor.ts","../src/hyper-ext/plugins/backend/watchers/globalNavigate.ts","../src/hyper-ext/bundles/hyper.ts"],"sourcesContent":["const lol = /🖕JS_DS🚀/.source\nexport const DSP = lol.slice(0, 5)\nexport const DSS = lol.slice(4)\nexport const DATASTAR_FETCH_EVENT = 'datastar-fetch'\nexport const DATASTAR_SIGNAL_PATCH_EVENT = 'datastar-signal-patch'\n","import type { Modifiers } from '@engine/types'\n\nexport const kebab = (str: string): string =>\n str\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([a-z])([0-9]+)/gi, '$1-$2')\n .replace(/([0-9]+)([a-z])/gi, '$1-$2')\n .toLowerCase()\n\nexport const camel = (str: string): string =>\n kebab(str).replace(/-./g, (x) => x[1].toUpperCase())\n\nexport const snake = (str: string): string => kebab(str).replace(/-/g, '_')\n\nexport const pascal = (str: string): string =>\n camel(str).replace(/(^.|(?<=\\.).)/g, (x) => x[0].toUpperCase())\n\nexport const jsStrToObject = (raw: string) => {\n try {\n return JSON.parse(raw)\n } catch {\n // If JSON parsing fails, try to evaluate as a JavaScript object\n // This is less safe and should be used with caution\n return Function(`return (${raw})`)()\n }\n}\n\nconst caseFns: Record string> = {\n camel: (str) => str.replace(/-[a-z]/g, (x) => x[1].toUpperCase()),\n snake: (str) => str.replace(/-/g, '_'),\n pascal: (str) => str[0].toUpperCase() + caseFns.camel(str.slice(1)),\n}\n\nexport const modifyCasing = (\n str: string,\n mods: Modifiers,\n defaultCase = 'camel',\n): string => {\n for (const c of mods.get('case') || [defaultCase]) {\n str = caseFns[c]?.(str) || str\n }\n return str\n}\n\nexport const aliasify = (name: string) =>\n ALIAS ? `data-${ALIAS}-${name}` : `data-${name}`\n","export const hasOwn: (obj: object, prop: PropertyKey) => boolean =\n // @ts-expect-error\n Object.hasOwn ?? Object.prototype.hasOwnProperty.call\n","import type { Paths } from '@engine/types'\nimport { hasOwn } from '@utils/polyfills'\n\nexport const isPojo = (obj: any): obj is Record =>\n obj !== null &&\n typeof obj === 'object' &&\n (Object.getPrototypeOf(obj) === Object.prototype ||\n Object.getPrototypeOf(obj) === null)\n\nexport const isEmpty = (obj: Record): boolean => {\n for (const prop in obj) {\n if (hasOwn(obj, prop)) {\n return false\n }\n }\n return true\n}\n\nexport const updateLeaves = (\n obj: Record,\n fn: (oldValue: any) => any,\n) => {\n for (const key in obj) {\n const val = obj[key]\n if (isPojo(val) || Array.isArray(val)) {\n updateLeaves(val, fn)\n } else {\n obj[key] = fn(val)\n }\n }\n}\n\nexport const pathToObj = (paths: Paths): Record => {\n const result: Record = {}\n for (const [path, value] of paths) {\n const keys = path.split('.')\n const lastKey = keys.pop()!\n const obj = keys.reduce((acc, key) => (acc[key] ??= {}), result)\n obj[lastKey] = value\n }\n return result\n}\n","import { DATASTAR_SIGNAL_PATCH_EVENT } from '@engine/consts'\nimport type {\n Computed,\n Effect,\n JSONPatch,\n MergePatchArgs,\n Paths,\n Signal,\n SignalFilterOptions,\n} from '@engine/types'\nimport { isPojo, pathToObj } from '@utils/paths'\nimport { hasOwn } from '@utils/polyfills'\n\ninterface ReactiveNode {\n deps_?: Link\n depsTail_?: Link\n subs_?: Link\n subsTail_?: Link\n flags_: ReactiveFlags\n}\n\ninterface Link {\n version_: number\n dep_: ReactiveNode\n sub_: ReactiveNode\n prevSub_?: Link\n nextSub_?: Link\n prevDep_?: Link\n nextDep_?: Link\n}\n\ninterface Stack {\n value_: T\n prev_?: Stack\n}\n\nenum ReactiveFlags {\n None = 0,\n Mutable = 1 << 0,\n Watching = 1 << 1,\n RecursedCheck = 1 << 2,\n Recursed = 1 << 3,\n Dirty = 1 << 4,\n Pending = 1 << 5,\n}\n\nenum EffectFlags {\n Queued = 1 << 6,\n}\n\ninterface AlienEffect extends ReactiveNode {\n fn_(): void\n}\n\ninterface AlienComputed extends ReactiveNode {\n value_?: T\n getter(previousValue?: T): T\n}\n\ninterface AlienSignal extends ReactiveNode {\n previousValue: T\n value_: T\n}\n\nconst currentPatch: Paths = []\nconst queuedEffects: (AlienEffect | undefined)[] = []\nlet batchDepth = 0\nlet notifyIndex = 0\nlet queuedEffectsLength = 0\nlet prevSub: ReactiveNode | undefined\nlet activeSub: ReactiveNode | undefined\nlet version = 0\n\nexport const beginBatch = (): void => {\n batchDepth++\n}\n\nexport const endBatch = (): void => {\n if (!--batchDepth) {\n flush()\n dispatch()\n }\n}\n\nexport const startPeeking = (sub?: ReactiveNode): void => {\n prevSub = activeSub\n activeSub = sub\n}\n\nexport const stopPeeking = (): void => {\n activeSub = prevSub\n prevSub = undefined\n}\n\nexport const signal = (initialValue?: T): Signal => {\n return signalOper.bind(0, {\n previousValue: initialValue,\n value_: initialValue,\n flags_: 1 satisfies ReactiveFlags.Mutable,\n }) as Signal\n}\n\nconst computedSymbol = Symbol('computed')\nexport const computed = (getter: (previousValue?: T) => T): Computed => {\n const c = computedOper.bind(0, {\n flags_: 17 as ReactiveFlags.Mutable | ReactiveFlags.Dirty,\n getter,\n }) as Computed\n // @ts-expect-error\n c[computedSymbol] = 1\n return c\n}\n\nexport const effect = (fn: () => void): Effect => {\n const e: AlienEffect = {\n fn_: fn,\n flags_: 2 satisfies ReactiveFlags.Watching,\n }\n if (activeSub) {\n link(e, activeSub)\n }\n startPeeking(e)\n beginBatch()\n try {\n e.fn_()\n } finally {\n endBatch()\n stopPeeking()\n }\n return effectOper.bind(0, e)\n}\n\nconst flush = () => {\n while (notifyIndex < queuedEffectsLength) {\n const effect = queuedEffects[notifyIndex]!\n queuedEffects[notifyIndex++] = undefined\n run(effect, (effect.flags_ &= ~EffectFlags.Queued))\n }\n notifyIndex = 0\n queuedEffectsLength = 0\n}\n\nconst update = (signal: AlienSignal | AlienComputed): boolean => {\n if ('getter' in signal) {\n return updateComputed(signal)\n }\n return updateSignal(signal, signal.value_)\n}\n\nconst updateComputed = (c: AlienComputed): boolean => {\n startPeeking(c)\n startTracking(c)\n try {\n const oldValue = c.value_\n return oldValue !== (c.value_ = c.getter(oldValue))\n } finally {\n stopPeeking()\n endTracking(c)\n }\n}\n\nconst updateSignal = (s: AlienSignal, value: T): boolean => {\n s.flags_ = 1 satisfies ReactiveFlags.Mutable\n return s.previousValue !== (s.previousValue = value)\n}\n\nconst notify = (e: AlienEffect): void => {\n const flags = e.flags_\n if (!(flags & EffectFlags.Queued)) {\n e.flags_ = flags | EffectFlags.Queued\n const subs = e.subs_\n if (subs) {\n notify(subs.sub_ as AlienEffect)\n } else {\n queuedEffects[queuedEffectsLength++] = e\n }\n }\n}\n\nconst run = (e: AlienEffect, flags: ReactiveFlags): void => {\n if (\n flags & (16 satisfies ReactiveFlags.Dirty) ||\n (flags & (32 satisfies ReactiveFlags.Pending) && checkDirty(e.deps_!, e))\n ) {\n startPeeking(e)\n startTracking(e)\n beginBatch()\n try {\n e.fn_()\n } finally {\n endBatch()\n stopPeeking()\n endTracking(e)\n }\n return\n }\n if (flags & (32 satisfies ReactiveFlags.Pending)) {\n e.flags_ = flags & ~(32 satisfies ReactiveFlags.Pending)\n }\n let link = e.deps_\n while (link) {\n const dep = link.dep_\n const depFlags = dep.flags_\n if (depFlags & EffectFlags.Queued) {\n run(dep as AlienEffect, (dep.flags_ = depFlags & ~EffectFlags.Queued))\n }\n link = link.nextDep_\n }\n}\n\nconst signalOper = (s: AlienSignal, ...value: [T]): T | boolean => {\n if (value.length) {\n if (s.value_ !== (s.value_ = value[0])) {\n s.flags_ = 17 as ReactiveFlags.Mutable | ReactiveFlags.Dirty\n const subs = s.subs_\n if (subs) {\n propagate(subs)\n if (!batchDepth) {\n flush()\n }\n }\n return true\n }\n return false\n }\n const currentValue = s.value_\n if (s.flags_ & (16 satisfies ReactiveFlags.Dirty)) {\n if (updateSignal(s, currentValue)) {\n const subs_ = s.subs_\n if (subs_) {\n shallowPropagate(subs_)\n }\n }\n }\n if (activeSub) {\n link(s, activeSub)\n }\n return currentValue\n}\n\nconst computedOper = (c: AlienComputed): T => {\n const flags = c.flags_\n if (\n flags & (16 satisfies ReactiveFlags.Dirty) ||\n (flags & (32 satisfies ReactiveFlags.Pending) && checkDirty(c.deps_!, c))\n ) {\n if (updateComputed(c)) {\n const subs = c.subs_\n if (subs) {\n shallowPropagate(subs)\n }\n }\n } else if (flags & (32 satisfies ReactiveFlags.Pending)) {\n c.flags_ = flags & ~(32 satisfies ReactiveFlags.Pending)\n }\n if (activeSub) {\n link(c, activeSub)\n }\n return c.value_!\n}\n\nconst effectOper = (e: AlienEffect): void => {\n let dep = e.deps_\n while (dep) {\n dep = unlink(dep, e)\n }\n const sub = e.subs_\n if (sub) {\n unlink(sub)\n }\n e.flags_ = 0 satisfies ReactiveFlags.None\n}\n\nconst link = (dep: ReactiveNode, sub: ReactiveNode): void => {\n const prevDep = sub.depsTail_\n if (prevDep && prevDep.dep_ === dep) {\n return\n }\n const nextDep = prevDep ? prevDep.nextDep_ : sub.deps_\n if (nextDep && nextDep.dep_ === dep) {\n nextDep.version_ = version\n sub.depsTail_ = nextDep\n return\n }\n const prevSub = dep.subsTail_\n if (prevSub && prevSub.version_ === version && prevSub.sub_ === sub) {\n return\n }\n const newLink =\n (sub.depsTail_ =\n dep.subsTail_ =\n {\n version_: version,\n dep_: dep,\n sub_: sub,\n prevDep_: prevDep,\n nextDep_: nextDep,\n prevSub_: prevSub,\n })\n if (nextDep) {\n nextDep.prevDep_ = newLink\n }\n if (prevDep) {\n prevDep.nextDep_ = newLink\n } else {\n sub.deps_ = newLink\n }\n if (prevSub) {\n prevSub.nextSub_ = newLink\n } else {\n dep.subs_ = newLink\n }\n}\n\nconst unlink = (link: Link, sub = link.sub_): Link | undefined => {\n const dep_ = link.dep_\n const prevDep_ = link.prevDep_\n const nextDep_ = link.nextDep_\n const nextSub_ = link.nextSub_\n const prevSub_ = link.prevSub_\n if (nextDep_) {\n nextDep_.prevDep_ = prevDep_\n } else {\n sub.depsTail_ = prevDep_\n }\n if (prevDep_) {\n prevDep_.nextDep_ = nextDep_\n } else {\n sub.deps_ = nextDep_\n }\n if (nextSub_) {\n nextSub_.prevSub_ = prevSub_\n } else {\n dep_.subsTail_ = prevSub_\n }\n if (prevSub_) {\n prevSub_.nextSub_ = nextSub_\n } else if (!(dep_.subs_ = nextSub_)) {\n if ('getter' in dep_) {\n let toRemove = dep_.deps_\n if (toRemove) {\n dep_.flags_ = 17 as ReactiveFlags.Mutable | ReactiveFlags.Dirty\n do {\n toRemove = unlink(toRemove, dep_)\n } while (toRemove)\n }\n } else if (!('previousValue' in dep_)) {\n effectOper(dep_ as AlienEffect)\n }\n }\n return nextDep_\n}\n\nconst propagate = (link: Link): void => {\n let next = link.nextSub_\n let stack: Stack | undefined\n\n top: while (true) {\n const sub = link.sub_\n\n let flags = sub.flags_\n\n if (\n !(\n flags &\n (60 as\n | ReactiveFlags.RecursedCheck\n | ReactiveFlags.Recursed\n | ReactiveFlags.Dirty\n | ReactiveFlags.Pending)\n )\n ) {\n sub.flags_ = flags | (32 satisfies ReactiveFlags.Pending)\n } else if (\n !(flags & (12 as ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))\n ) {\n flags = 0 satisfies ReactiveFlags.None\n } else if (!(flags & (4 satisfies ReactiveFlags.RecursedCheck))) {\n sub.flags_ =\n (flags & ~(8 satisfies ReactiveFlags.Recursed)) |\n (32 satisfies ReactiveFlags.Pending)\n } else if (\n !(flags & (48 as ReactiveFlags.Dirty | ReactiveFlags.Pending)) &&\n isValidLink(link, sub)\n ) {\n sub.flags_ =\n flags | (40 as ReactiveFlags.Recursed | ReactiveFlags.Pending)\n flags &= 1 satisfies ReactiveFlags.Mutable\n } else {\n flags = 0 satisfies ReactiveFlags.None\n }\n\n if (flags & (2 satisfies ReactiveFlags.Watching)) {\n notify(sub as AlienEffect)\n }\n\n if (flags & (1 satisfies ReactiveFlags.Mutable)) {\n const subSubs = sub.subs_\n if (subSubs) {\n const nextSub = (link = subSubs).nextSub_\n if (nextSub) {\n stack = { value_: next, prev_: stack }\n next = nextSub\n }\n continue\n }\n }\n\n if ((link = next!)) {\n next = link.nextSub_\n continue\n }\n\n while (stack) {\n link = stack.value_!\n stack = stack.prev_\n if (link) {\n next = link.nextSub_\n continue top\n }\n }\n\n break\n }\n}\n\nconst startTracking = (sub: ReactiveNode): void => {\n version++\n sub.depsTail_ = undefined\n sub.flags_ =\n (sub.flags_ &\n ~(56 as\n | ReactiveFlags.Recursed\n | ReactiveFlags.Dirty\n | ReactiveFlags.Pending)) |\n (4 satisfies ReactiveFlags.RecursedCheck)\n}\n\nconst endTracking = (sub: ReactiveNode): void => {\n const depsTail_ = sub.depsTail_\n let toRemove = depsTail_ ? depsTail_.nextDep_ : sub.deps_\n while (toRemove) {\n toRemove = unlink(toRemove, sub)\n }\n sub.flags_ &= ~(4 satisfies ReactiveFlags.RecursedCheck)\n}\n\nconst checkDirty = (link: Link, sub: ReactiveNode): boolean => {\n let stack: Stack | undefined\n let checkDepth = 0\n let dirty = false\n\n top: while (true) {\n const dep = link.dep_\n const flags = dep.flags_\n\n if (sub.flags_ & (16 satisfies ReactiveFlags.Dirty)) {\n dirty = true\n } else if (\n (flags & (17 as ReactiveFlags.Mutable | ReactiveFlags.Dirty)) ===\n (17 as ReactiveFlags.Mutable | ReactiveFlags.Dirty)\n ) {\n if (update(dep as AlienSignal | AlienComputed)) {\n const subs = dep.subs_!\n if (subs.nextSub_) {\n shallowPropagate(subs)\n }\n dirty = true\n }\n } else if (\n (flags & (33 as ReactiveFlags.Mutable | ReactiveFlags.Pending)) ===\n (33 as ReactiveFlags.Mutable | ReactiveFlags.Pending)\n ) {\n if (link.nextSub_ || link.prevSub_) {\n stack = { value_: link, prev_: stack }\n }\n link = dep.deps_!\n sub = dep\n ++checkDepth\n continue\n }\n\n if (!dirty) {\n const nextDep = link.nextDep_\n if (nextDep) {\n link = nextDep\n continue\n }\n }\n\n while (checkDepth--) {\n const firstSub = sub.subs_!\n const hasMultipleSubs = firstSub.nextSub_\n if (hasMultipleSubs) {\n link = stack!.value_\n stack = stack!.prev_\n } else {\n link = firstSub\n }\n if (dirty) {\n if (update(sub as AlienSignal | AlienComputed)) {\n if (hasMultipleSubs) {\n shallowPropagate(firstSub)\n }\n sub = link.sub_\n continue\n }\n dirty = false\n } else {\n sub.flags_ &= ~(32 satisfies ReactiveFlags.Pending)\n }\n sub = link.sub_\n if (link.nextDep_) {\n link = link.nextDep_\n continue top\n }\n }\n\n return dirty\n }\n}\n\nconst shallowPropagate = (link: Link): void => {\n do {\n const sub = link.sub_\n const flags = sub.flags_\n if (\n (flags & (48 as ReactiveFlags.Pending | ReactiveFlags.Dirty)) ===\n (32 satisfies ReactiveFlags.Pending)\n ) {\n sub.flags_ = flags | (16 satisfies ReactiveFlags.Dirty)\n if (flags & (2 satisfies ReactiveFlags.Watching)) {\n notify(sub as AlienEffect)\n }\n }\n } while ((link = link.nextSub_!))\n}\n\nconst isValidLink = (checkLink: Link, sub: ReactiveNode): boolean => {\n let link = sub.depsTail_\n while (link) {\n if (link === checkLink) {\n return true\n }\n link = link.prevDep_\n }\n return false\n}\n\nexport const getPath = (path: string): T | undefined => {\n let result = root\n const split = path.split('.')\n for (const path of split) {\n if (result == null || !hasOwn(result, path)) {\n return\n }\n result = result[path]\n }\n return result as T\n}\n\nconst deep = (value: any, prefix = ''): any => {\n const isArr = Array.isArray(value)\n if (isArr || isPojo(value)) {\n const deepObj = (isArr ? [] : {}) as Record>\n for (const key in value) {\n deepObj[key] = signal(\n deep((value as Record>)[key], `${prefix + key}.`),\n )\n }\n const keys = signal(0)\n return new Proxy(deepObj, {\n get(_, prop: string) {\n // JSON.stringify calls `.toJSON()` directly instead of checking if it exists on the object\n // so we have to check if `toJSON` is being called and prevent a signal from automatically\n // being made so JSON.stringify can fallback to the default stringify\n if (!(prop === 'toJSON' && !hasOwn(deepObj, prop))) {\n // special case for when prop is an array function because every array function needs to\n // be reactive to when the keys change\n if (isArr && prop in Array.prototype) {\n keys()\n return deepObj[prop]\n }\n // if prop is a symbol just return the symbol because we don't want to make up that theres\n // an iterator symbol on an object or not\n if (typeof prop === 'symbol') {\n return deepObj[prop]\n }\n // if obj doesnt have prop OR prop is null or undefined then create a signal and default\n // to an empty string\n if (!hasOwn(deepObj, prop) || deepObj[prop]() == null) {\n deepObj[prop] = signal('')\n dispatch(prefix + prop, '')\n keys(keys() + 1)\n }\n return deepObj[prop]()\n }\n },\n set(_, prop: string, newValue) {\n const path = prefix + prop\n // special case for when setting length so we can make a diff patch\n if (isArr && prop === 'length') {\n const diff = (deepObj[prop] as unknown as number) - newValue\n deepObj[prop] = newValue\n // manually make a diff patch for now, shouldnt have to do this when object diffing is\n // implemented. see https://github.com/starfederation/datastar-dev/issues/274\n if (diff > 0) {\n const patch: Record = {}\n for (let i = newValue; i < deepObj[prop]; i++) {\n patch[i] = null\n }\n dispatch(prefix.slice(0, -1), patch)\n keys(keys() + 1)\n }\n } else if (hasOwn(deepObj, prop)) {\n if (newValue == null) {\n delete deepObj[prop]\n // if newValue is a computed set the computed directly instead of wrapping in signal\n } else if (hasOwn(newValue, computedSymbol)) {\n deepObj[prop] = newValue\n dispatch(path, '')\n // if prop changed after setting it then dispatch\n } else if (deepObj[prop](deep(newValue, `${path}.`))) {\n dispatch(path, newValue)\n }\n // if newValue is null or undefined then noop\n } else if (newValue != null) {\n // if newValue is a computed set the computed directly instead of wrapping in signal\n if (hasOwn(newValue, computedSymbol)) {\n deepObj[prop] = newValue\n dispatch(path, '')\n } else {\n deepObj[prop] = signal(deep(newValue, `${path}.`))\n dispatch(path, newValue)\n }\n keys(keys() + 1)\n }\n\n return true\n },\n deleteProperty(_, prop: string) {\n delete deepObj[prop]\n keys(keys() + 1)\n return true\n },\n ownKeys() {\n keys()\n return Reflect.ownKeys(deepObj)\n },\n has(_, prop) {\n keys()\n return prop in deepObj\n },\n })\n }\n return value\n}\n\nconst dispatch = (path?: string, value?: any) => {\n if (path !== undefined && value !== undefined) {\n currentPatch.push([path, value])\n }\n if (!batchDepth && currentPatch.length) {\n const detail = pathToObj(currentPatch)\n currentPatch.length = 0\n document.dispatchEvent(\n new CustomEvent(DATASTAR_SIGNAL_PATCH_EVENT, {\n detail,\n }),\n )\n }\n}\n\nexport const mergePatch = (\n patch: JSONPatch,\n { ifMissing }: MergePatchArgs = {},\n): void => {\n beginBatch()\n for (const key in patch) {\n if (patch[key] == null) {\n if (!ifMissing) {\n delete root[key]\n }\n } else {\n mergeInner(patch[key], key, root, '', ifMissing)\n }\n }\n endBatch()\n}\n\nexport const mergePaths = (paths: Paths, options?: MergePatchArgs): void =>\n mergePatch(pathToObj(paths), options)\n\nconst mergeInner = (\n patch: any,\n target: string,\n targetParent: Record,\n prefix: string,\n ifMissing: boolean | undefined,\n): void => {\n if (isPojo(patch)) {\n if (\n !(\n hasOwn(targetParent, target) &&\n (isPojo(targetParent[target]) || Array.isArray(targetParent[target]))\n )\n ) {\n targetParent[target] = {}\n }\n\n for (const key in patch) {\n if (patch[key] == null) {\n if (!ifMissing) {\n delete targetParent[target][key]\n }\n } else {\n mergeInner(\n patch[key],\n key,\n targetParent[target],\n `${prefix + target}.`,\n ifMissing,\n )\n }\n }\n } else if (!(ifMissing && hasOwn(targetParent, target))) {\n targetParent[target] = patch\n }\n}\n\nconst toRegExp = (val: string | RegExp): RegExp =>\n typeof val === 'string' ? RegExp(val.replace(/^\\/|\\/$/g, '')) : val\n\n/**\n * Filters the root store based on an include and exclude RegExp\n *\n * @returns The filtered object\n */\nexport const filtered = (\n { include = /.*/, exclude = /(?!)/ }: SignalFilterOptions = {},\n obj: JSONPatch = root,\n): Record => {\n const includeRe = toRegExp(include)\n const excludeRe = toRegExp(exclude)\n const paths: Paths = []\n const stack: [any, string][] = [[obj, '']]\n\n while (stack.length) {\n const [node, prefix] = stack.pop()!\n\n for (const key in node) {\n const path = prefix + key\n if (isPojo(node[key])) {\n stack.push([node[key], `${path}.`])\n } else if (includeRe.test(path) && !excludeRe.test(path)) {\n paths.push([path, getPath(path)])\n }\n }\n }\n\n return pathToObj(paths)\n}\n\nexport const root: Record = deep({})\n","import type { HTMLOrSVG } from '@engine/types'\n\nexport const isHTMLOrSVG = (el: Node): el is HTMLOrSVG =>\n el instanceof HTMLElement ||\n el instanceof SVGElement ||\n el instanceof MathMLElement\n","import { DATASTAR_FETCH_EVENT, DSP, DSS } from '@engine/consts'\nimport { snake } from '@utils/text'\nimport { root } from '@engine/signals'\nimport type {\n ActionPlugin,\n ActionContext,\n AttributeContext,\n AttributePlugin,\n DatastarFetchEvent,\n HTMLOrSVG,\n Requirement,\n WatcherPlugin,\n} from '@engine/types'\nimport { isHTMLOrSVG } from '@utils/dom'\nimport { aliasify } from '@utils/text'\n\nconst url = 'https://data-star.dev/errors'\n\nconst error = (\n ctx: Record,\n reason: string,\n metadata: Record = {},\n) => {\n Object.assign(metadata, ctx)\n const e = new Error()\n const r = snake(reason)\n const q = new URLSearchParams({\n metadata: JSON.stringify(metadata),\n }).toString()\n const c = JSON.stringify(metadata, null, 2)\n e.message = `${reason}\\nMore info: ${url}/${r}?${q}\\nContext: ${c}`\n return e\n}\n\nconst actionPlugins: Map = new Map()\nconst attributePlugins: Map = new Map()\nconst watcherPlugins: Map = new Map()\n\nexport const actions: Record<\n string,\n (ctx: ActionContext, ...args: any[]) => any\n> = new Proxy(\n {},\n {\n get: (_, prop: string) => actionPlugins.get(prop)?.apply,\n has: (_, prop: string) => actionPlugins.has(prop),\n ownKeys: () => Reflect.ownKeys(actionPlugins),\n set: () => false,\n deleteProperty: () => false,\n },\n)\n\n// Map of cleanups keyed by element and attribute name\nconst removals = new Map void>>()\n\nconst queuedAttributes: AttributePlugin[] = []\nconst queuedAttributeNames = new Set()\nexport const attribute = (\n plugin: AttributePlugin,\n): void => {\n queuedAttributes.push(plugin as unknown as AttributePlugin)\n\n if (queuedAttributes.length === 1) {\n setTimeout(() => {\n for (const attribute of queuedAttributes) {\n queuedAttributeNames.add(attribute.name)\n attributePlugins.set(attribute.name, attribute)\n }\n queuedAttributes.length = 0\n apply()\n queuedAttributeNames.clear()\n })\n }\n}\n\nexport const action = (plugin: ActionPlugin): void => {\n actionPlugins.set(plugin.name, plugin)\n}\n\ndocument.addEventListener(DATASTAR_FETCH_EVENT, ((\n evt: CustomEvent,\n) => {\n const plugin = watcherPlugins.get(evt.detail.type)\n if (plugin) {\n plugin.apply(\n {\n error: error.bind(0, {\n plugin: { type: 'watcher', name: plugin.name },\n element: {\n id: (evt.target as Element).id,\n tag: (evt.target as Element).tagName,\n },\n }),\n },\n evt.detail.argsRaw,\n )\n }\n}) as EventListener)\n\nexport const watcher = (plugin: WatcherPlugin): void => {\n watcherPlugins.set(plugin.name, plugin)\n}\n\nconst cleanupEls = (els: Iterable): void => {\n for (const el of els) {\n const cleanups = removals.get(el)\n // If removals has el, delete it and run all cleanup functions\n if (removals.delete(el)) {\n for (const cleanup of cleanups!.values()) {\n cleanup()\n }\n cleanups!.clear()\n }\n }\n}\n\nconst aliasedIgnore = aliasify('ignore')\nconst aliasedIgnoreAttr = `[${aliasedIgnore}]`\nconst shouldIgnore = (el: HTMLOrSVG) =>\n el.hasAttribute(`${aliasedIgnore}__self`) || !!el.closest(aliasedIgnoreAttr)\n\nconst applyEls = (els: Iterable, onlyNew?: boolean): void => {\n for (const el of els) {\n if (!shouldIgnore(el)) {\n for (const key in el.dataset) {\n applyAttributePlugin(\n el,\n key.replace(/[A-Z]/g, '-$&').toLowerCase(),\n el.dataset[key]!,\n onlyNew,\n )\n }\n }\n }\n}\n\nconst observe = (mutations: MutationRecord[]) => {\n for (const {\n target,\n type,\n attributeName,\n addedNodes,\n removedNodes,\n } of mutations) {\n if (type === 'childList') {\n for (const node of removedNodes) {\n if (isHTMLOrSVG(node)) {\n cleanupEls([node])\n cleanupEls(node.querySelectorAll('*'))\n }\n }\n\n for (const node of addedNodes) {\n if (isHTMLOrSVG(node)) {\n applyEls([node])\n applyEls(node.querySelectorAll('*'))\n }\n }\n } else if (\n type === 'attributes' &&\n attributeName!.startsWith('data-') &&\n isHTMLOrSVG(target) &&\n !shouldIgnore(target)\n ) {\n // skip over 'data-'\n const key = attributeName!.slice(5)\n const value = target.getAttribute(attributeName!)\n if (value === null) {\n const cleanups = removals.get(target)\n if (cleanups) {\n cleanups.get(key)?.()\n cleanups.delete(key)\n }\n } else {\n applyAttributePlugin(target, key, value)\n }\n }\n }\n}\n\n// TODO: mutation observer per root so applying to web component doesnt overwrite main observer\nconst mutationObserver = new MutationObserver(observe)\n\nexport const apply = (\n root: HTMLOrSVG | ShadowRoot = document.documentElement,\n): void => {\n if (isHTMLOrSVG(root)) {\n applyEls([root], true)\n }\n applyEls(root.querySelectorAll('*'), true)\n\n mutationObserver.observe(root, {\n subtree: true,\n childList: true,\n attributes: true,\n })\n}\n\nconst applyAttributePlugin = (\n el: HTMLOrSVG,\n attrKey: string,\n value: string,\n onlyNew?: boolean,\n): void => {\n if (!ALIAS || attrKey.startsWith(`${ALIAS}-`)) {\n const rawKey = ALIAS ? attrKey.slice(ALIAS.length + 1) : attrKey\n const [namePart, ...rawModifiers] = rawKey.split('__')\n const [pluginName, key] = namePart.split(/:(.+)/)\n const plugin = attributePlugins.get(pluginName)\n if ((!onlyNew || queuedAttributeNames.has(pluginName)) && plugin) {\n const ctx = {\n el,\n rawKey,\n mods: new Map(),\n error: error.bind(0, {\n plugin: { type: 'attribute', name: plugin.name },\n element: { id: el.id, tag: el.tagName },\n expression: { rawKey, key, value },\n }),\n key,\n value,\n rx: undefined,\n } as AttributeContext\n\n const keyReq =\n (plugin.requirement &&\n (typeof plugin.requirement === 'string'\n ? plugin.requirement\n : plugin.requirement.key)) ||\n 'allowed'\n const valueReq =\n (plugin.requirement &&\n (typeof plugin.requirement === 'string'\n ? plugin.requirement\n : plugin.requirement.value)) ||\n 'allowed'\n\n if (key) {\n if (keyReq === 'denied') {\n throw ctx.error('KeyNotAllowed')\n }\n } else if (keyReq === 'must') {\n throw ctx.error('KeyRequired')\n }\n\n if (value) {\n if (valueReq === 'denied') {\n throw ctx.error('ValueNotAllowed')\n }\n } else if (valueReq === 'must') {\n throw ctx.error('ValueRequired')\n }\n\n if (keyReq === 'exclusive' || valueReq === 'exclusive') {\n if (key && value) {\n throw ctx.error('KeyAndValueProvided')\n }\n if (!key && !value) {\n throw ctx.error('KeyOrValueRequired')\n }\n }\n\n if (value) {\n let cachedRx: GenRxFn\n ctx.rx = (...args: any[]) => {\n if (!cachedRx) {\n cachedRx = genRx(value, {\n returnsValue: plugin.returnsValue,\n argNames: plugin.argNames,\n })\n }\n return cachedRx(el, ...args)\n }\n }\n\n for (const rawMod of rawModifiers) {\n const [label, ...mod] = rawMod.split('.')\n ctx.mods.set(label, new Set(mod))\n }\n\n const cleanup = plugin.apply(ctx)\n if (cleanup) {\n let cleanups = removals.get(el)\n if (cleanups) {\n cleanups.get(rawKey)?.()\n } else {\n cleanups = new Map()\n removals.set(el, cleanups)\n }\n cleanups.set(rawKey, cleanup)\n }\n }\n }\n}\n\ntype GenRxOptions = {\n returnsValue?: boolean\n argNames?: string[]\n}\n\ntype GenRxFn = (el: HTMLOrSVG, ...args: any[]) => T\n\nconst genRx = (\n value: string,\n { returnsValue = false, argNames = [] }: GenRxOptions = {},\n): GenRxFn => {\n let expr = ''\n if (returnsValue) {\n // This regex allows Datastar expressions to support nested\n // regex and strings that contain ; without breaking.\n //\n // Each of these regex defines a block type we want to match\n // (importantly we ignore the content within these blocks):\n //\n // regex \\/(\\\\\\/|[^\\/])*\\/\n // double quotes \"(\\\\\"|[^\\\"])*\"\n // single quotes '(\\\\'|[^'])*'\n // ticks `(\\\\`|[^`])*`\n // iife \\(\\s*((function)\\s*\\(\\s*\\)|(\\(\\s*\\))\\s*=>)\\s*(?:\\{[\\s\\S]*?\\}|[^;)\\{]*)\\s*\\)\\s*\\(\\s*\\)\n //\n // The iife support is (intentionally) limited. It only supports\n // function and arrow syntax with no arguments, and no nested IIFEs.\n //\n // We also want to match the non delimiter part of statements\n // note we only support ; statement delimiters:\n //\n // [^;]\n //\n const statementRe =\n /(\\/(\\\\\\/|[^/])*\\/|\"(\\\\\"|[^\"])*\"|'(\\\\'|[^'])*'|`(\\\\`|[^`])*`|\\(\\s*((function)\\s*\\(\\s*\\)|(\\(\\s*\\))\\s*=>)\\s*(?:\\{[\\s\\S]*?\\}|[^;){]*)\\s*\\)\\s*\\(\\s*\\)|[^;])+/gm\n const statements = value.trim().match(statementRe)\n if (statements) {\n const lastIdx = statements.length - 1\n const last = statements[lastIdx].trim()\n if (!last.startsWith('return')) {\n statements[lastIdx] = `return (${last});`\n }\n expr = statements.join(';\\n')\n }\n } else {\n expr = value.trim()\n }\n\n // Ignore any escaped values\n const escaped = new Map()\n const escapeRe = RegExp(`(?:${DSP})(.*?)(?:${DSS})`, 'gm')\n let counter = 0\n for (const match of expr.matchAll(escapeRe)) {\n const k = match[1]\n const v = `__escaped${counter++}`\n escaped.set(v, k)\n expr = expr.replace(DSP + k + DSS, v)\n }\n\n // Replace signal references with bracket notation\n // Examples:\n // $count -> $['count']\n // $count-- -> $['count']--\n // $foo.bar -> $['foo']['bar']\n // $foo-bar -> $['foo-bar']\n // $foo.bar-baz -> $['foo']['bar-baz']\n // $foo-$bar -> $['foo']-$['bar']\n // $arr[$index] -> $['arr'][$['index']]\n // $['foo'] -> $['foo']\n // $foo[obj.bar] -> $['foo'][obj.bar]\n // $foo['bar.baz'] -> $['foo']['bar.baz']\n // $123 -> $['123']\n // $foo.0.name -> $['foo']['0']['name']\n\n expr = expr\n // $['x'] -> $x (normalize existing bracket notation)\n .replace(/\\$\\['([a-zA-Z_$\\d][\\w$]*)'\\]/g, '$$$1')\n // $x -> $['x'] (including dots and hyphens)\n .replace(/\\$([a-zA-Z_\\d]\\w*(?:[.-]\\w+)*)/g, (_, signalName) =>\n signalName\n .split('.')\n .reduce((acc: string, part: string) => `${acc}['${part}']`, '$'),\n )\n // [$x] -> [$['x']] ($ inside brackets)\n .replace(\n /\\[(\\$[a-zA-Z_\\d]\\w*)\\]/g,\n (_, varName) => `[$['${varName.slice(1)}']]`,\n )\n\n expr = expr.replaceAll(/@(\\w+)\\(/g, '__action(\"$1\",evt,')\n\n // Replace any escaped values\n for (const [k, v] of escaped) {\n expr = expr.replace(k, v)\n }\n\n try {\n const fn = Function('el', '$', '__action', 'evt', ...argNames, expr)\n return (el: HTMLOrSVG, ...args: any[]) => {\n const action = (name: string, evt: Event | undefined, ...args: any[]) => {\n const err = error.bind(0, {\n plugin: { type: 'action', name },\n element: { id: el.id, tag: el.tagName },\n expression: {\n fnContent: expr,\n value,\n },\n })\n const fn = actions[name]\n if (fn) {\n return fn(\n {\n el,\n evt,\n error: err,\n },\n ...args,\n )\n }\n throw err('UndefinedAction')\n }\n try {\n return fn(el, root, action, undefined, ...args)\n } catch (e: any) {\n console.error(e)\n throw error(\n {\n element: { id: el.id, tag: el.tagName },\n expression: {\n fnContent: expr,\n value,\n },\n error: e.message,\n },\n 'ExecuteExpression',\n )\n }\n }\n } catch (e: any) {\n console.error(e)\n throw error(\n {\n expression: {\n fnContent: expr,\n value,\n },\n error: e.message,\n },\n 'GenerateExpression',\n )\n }\n}\n","// Icon: ion:eye\n// Slug: Access signals without subscribing to changes.\n// Description: Allows accessing signals without subscribing to their changes in expressions.\n\nimport { action } from '@engine'\nimport { startPeeking, stopPeeking } from '@engine/signals'\n\naction({\n name: 'peek',\n apply(_, fn: () => any) {\n startPeeking()\n try {\n return fn()\n } finally {\n stopPeeking()\n }\n },\n})\n","// Icon: ion:checkmark-round\n// Slug: Sets the value of all matching signals.\n// Description: Sets the value of all matching signals (or all signals if no filter is used) to the expression provided in the first argument.\n\nimport { action } from '@engine'\nimport {\n filtered,\n mergePatch,\n startPeeking,\n stopPeeking,\n} from '@engine/signals'\nimport type { SignalFilterOptions } from '@engine/types'\nimport { updateLeaves } from '@utils/paths'\n\naction({\n name: 'setAll',\n apply(_, value: any, filter: SignalFilterOptions) {\n // peek because in an effect you would be subscribing to signals and then setting them which\n // would cause an infinite loop and why would you want to infinite loop on purpose\n startPeeking()\n const masked = filtered(filter)\n updateLeaves(masked, () => value)\n mergePatch(masked)\n stopPeeking()\n },\n})\n","// Icon: material-symbols:toggle-off\n// Slug: Toggles the value of all matching signals.\n// Description: Toggles the boolean value of all matching signals (or all signals if no filter is used).\n\nimport { action } from '@engine'\nimport {\n filtered,\n mergePatch,\n startPeeking,\n stopPeeking,\n} from '@engine/signals'\nimport type { SignalFilterOptions } from '@engine/types'\nimport { updateLeaves } from '@utils/paths'\n\naction({\n name: 'toggleAll',\n apply(_, filter: SignalFilterOptions) {\n // peek because in an effect you would be subscribing to signals and then setting them which\n // would cause an infinite loop and why would you want to infinite loop on purpose\n startPeeking()\n const masked = filtered(filter)\n updateLeaves(masked, (oldValue: any) => !oldValue)\n mergePatch(masked)\n stopPeeking()\n },\n})\n","// Icon: ion:eye\n// Slug: Access signals without subscribing to changes.\n// Description: Allows accessing signals without subscribing to their changes in expressions.\n\nimport { action } from '@engine'\nimport { DATASTAR_FETCH_EVENT } from '@engine/consts'\nimport { filtered } from '@engine/signals'\nimport type {\n DatastarFetchEvent,\n HTMLOrSVG,\n SignalFilterOptions,\n} from '@engine/types'\nimport { kebab } from '@utils/text'\n\nconst fetchAbortControllers = new WeakMap()\n\nconst createHttpMethod = (name: string, method: string): void =>\n action({\n name,\n apply: async (\n { el, evt, error },\n url: string,\n {\n selector,\n headers: userHeaders,\n contentType = 'json',\n filterSignals: { include = /.*/, exclude = /(^|\\.)_/ } = {},\n openWhenHidden = false,\n retryInterval = 1000,\n retryScaler = 2,\n retryMaxWaitMs = 30_000,\n retryMaxCount = 10,\n requestCancellation = 'auto',\n }: FetchArgs = {},\n ) => {\n const controller =\n requestCancellation instanceof AbortController\n ? requestCancellation\n : new AbortController()\n const isDisabled = requestCancellation === 'disabled'\n if (!isDisabled) {\n const oldController = fetchAbortControllers.get(el)\n if (oldController) {\n oldController.abort()\n // wait one tick for FINISHED to fire\n await Promise.resolve()\n }\n }\n\n if (!isDisabled && !(requestCancellation instanceof AbortController)) {\n fetchAbortControllers.set(el, controller)\n }\n\n try {\n const observer = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n for (const removed of mutation.removedNodes) {\n if (removed === el) {\n controller.abort()\n cleanupFn()\n }\n }\n }\n })\n if (el.parentNode) {\n observer.observe(el.parentNode, { childList: true })\n }\n\n let cleanupFn = () => {\n observer.disconnect()\n }\n\n try {\n if (!url?.length) {\n throw error('FetchNoUrlProvided', { action })\n }\n\n const initialHeaders: Record = {\n Accept: 'text/event-stream, text/html, application/json',\n 'Datastar-Request': true,\n }\n if (contentType === 'json') {\n initialHeaders['Content-Type'] = 'application/json'\n }\n const headers = Object.assign({}, initialHeaders, userHeaders)\n\n // We ignore the content-type header if using form data\n // if missing the boundary will be set automatically\n\n const req: FetchEventSourceInit = {\n method,\n headers,\n openWhenHidden,\n retryInterval,\n retryScaler,\n retryMaxWaitMs,\n retryMaxCount,\n signal: controller.signal,\n onopen: async (response: Response) => {\n if (response.status >= 400)\n dispatchFetch(ERROR, el, { status: response.status.toString() })\n },\n onmessage: (evt) => {\n if (!evt.event.startsWith('datastar')) return\n const type = evt.event\n const argsRawLines: Record = {}\n\n for (const line of evt.data.split('\\n')) {\n const i = line.indexOf(' ')\n const k = line.slice(0, i)\n const v = line.slice(i + 1)\n ;(argsRawLines[k] ||= []).push(v)\n }\n\n const argsRaw = Object.fromEntries(\n Object.entries(argsRawLines).map(([k, v]) => [k, v.join('\\n')]),\n )\n\n dispatchFetch(type, el, argsRaw)\n },\n onerror: (error) => {\n if (isWrongContent(error)) {\n // don't retry if the content-type is wrong\n throw error('FetchExpectedTextEventStream', { url })\n }\n // do nothing and it will retry\n if (error) {\n console.error(error.message)\n dispatchFetch(RETRYING, el, { message: error.message })\n }\n },\n }\n\n const urlInstance = new URL(url, document.baseURI)\n const queryParams = new URLSearchParams(urlInstance.search)\n\n if (contentType === 'json') {\n const res = JSON.stringify(filtered({ include, exclude }))\n if (method === 'GET') {\n queryParams.set('datastar', res)\n } else {\n req.body = res\n }\n } else if (contentType === 'form') {\n const formEl = (\n selector ? document.querySelector(selector) : el.closest('form')\n ) as HTMLFormElement\n if (!formEl) {\n throw error('FetchFormNotFound', { action, selector })\n }\n\n // Validate the form\n if (!formEl.checkValidity()) {\n formEl.reportValidity()\n cleanupFn()\n return\n }\n\n // Collect the form data\n\n const formData = new FormData(formEl)\n let submitter = el as HTMLElement | null\n\n if (el === formEl && evt instanceof SubmitEvent) {\n // Get the submitter from the event\n submitter = evt.submitter\n } else {\n // Prevent the form being submitted\n const preventDefault = (evt: Event) => evt.preventDefault()\n formEl.addEventListener('submit', preventDefault)\n cleanupFn = () => {\n formEl.removeEventListener('submit', preventDefault)\n observer.disconnect()\n }\n }\n\n // Append the value of the form submitter if it is a button with a name\n if (submitter instanceof HTMLButtonElement) {\n const name = submitter.getAttribute('name')\n if (name) formData.append(name, submitter.value)\n }\n\n const multipart =\n formEl.getAttribute('enctype') === 'multipart/form-data'\n // Leave the `Content-Type` header empty for multipart encoding so the browser can set it automatically with the correct boundary\n if (!multipart) {\n headers['Content-Type'] = 'application/x-www-form-urlencoded'\n }\n\n const formParams = new URLSearchParams(formData as any)\n if (method === 'GET') {\n for (const [key, value] of formParams) {\n queryParams.append(key, value)\n }\n } else if (multipart) {\n req.body = formData\n } else {\n req.body = formParams\n }\n } else {\n throw error('FetchInvalidContentType', { action, contentType })\n }\n\n dispatchFetch(STARTED, el, {})\n urlInstance.search = queryParams.toString()\n\n try {\n await fetchEventSource(urlInstance.toString(), el, req)\n } catch (e: any) {\n if (!isWrongContent(e)) {\n throw error('FetchFailed', { method, url, error: e.message })\n }\n // exit gracefully and do nothing if the content-type is wrong\n // this can happen if the client is sending a request\n // where no response is expected, and they haven’t\n // set the content-type to text/event-stream\n }\n } finally {\n dispatchFetch(FINISHED, el, {})\n cleanupFn()\n }\n } finally {\n if (fetchAbortControllers.get(el) === controller) {\n fetchAbortControllers.delete(el)\n }\n }\n },\n })\n\ncreateHttpMethod('delete', 'DELETE')\ncreateHttpMethod('get', 'GET')\ncreateHttpMethod('patch', 'PATCH')\ncreateHttpMethod('post', 'POST')\ncreateHttpMethod('put', 'PUT')\n\nexport const STARTED = 'started'\nexport const FINISHED = 'finished'\nexport const ERROR = 'error'\nexport const RETRYING = 'retrying'\nexport const RETRIES_FAILED = 'retries-failed'\n\nconst dispatchFetch = (\n type: string,\n el: HTMLOrSVG,\n argsRaw: Record,\n) =>\n document.dispatchEvent(\n new CustomEvent(DATASTAR_FETCH_EVENT, {\n detail: { type, el, argsRaw },\n }),\n )\n\nconst isWrongContent = (err: any) => `${err}`.includes('text/event-stream')\n\ntype ResponseOverrides =\n | {\n selector?: string\n mode?: string\n useViewTransition?: boolean\n }\n | {\n onlyIfMissing?: boolean\n }\n\nexport type FetchArgs = {\n headers?: Record\n credentials?: RequestCredentials\n openWhenHidden?: boolean\n retryInterval?: number\n retryScaler?: number\n retryMaxWaitMs?: number\n retryMaxCount?: number\n responseOverrides?: ResponseOverrides\n contentType?: 'json' | 'form'\n filterSignals?: SignalFilterOptions\n selector?: string\n requestCancellation?: 'auto' | 'disabled' | AbortController\n}\n\n// Below originally from https://github.com/Azure/fetch-event-source/blob/main/LICENSE\n\n/**\n * Represents a message sent in an event stream\n * https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format\n */\n\ninterface EventSourceMessage {\n id: string\n event: string\n data: string\n retry?: number\n}\n\n/**\n * Converts a ReadableStream into a callback pattern.\n * @param stream The input ReadableStream.\n * @param onChunk A function that will be called on each new byte chunk in the stream.\n * @returns {Promise} A promise that will be resolved when the stream closes.\n */\nconst getBytes = async (\n stream: ReadableStream,\n onChunk: (arr: Uint8Array) => void,\n): Promise => {\n const reader = stream.getReader()\n let result = await reader.read()\n while (!result.done) {\n onChunk(result.value)\n result = await reader.read()\n }\n}\n\nconst getLines = (onLine: (line: Uint8Array, fieldLength: number) => void) => {\n let buffer: Uint8Array | undefined\n let position: number // current read position\n let fieldLength: number // length of the `field` portion of the line\n let discardTrailingNewline = false\n\n // return a function that can process each incoming byte chunk:\n return (arr: Uint8Array) => {\n if (!buffer) {\n buffer = arr\n position = 0\n fieldLength = -1\n } else {\n // we're still parsing the old line. Append the new bytes into buffer:\n buffer = concat(buffer, arr)\n }\n\n const bufLength = buffer.length\n let lineStart = 0 // index where the current line starts\n while (position < bufLength) {\n if (discardTrailingNewline) {\n if (buffer[position] === 10) lineStart = ++position // skip to next char\n discardTrailingNewline = false\n }\n\n // start looking forward till the end of line:\n let lineEnd = -1 // index of the \\r or \\n char\n for (; position < bufLength && lineEnd === -1; ++position) {\n switch (buffer[position]) {\n case 58: // :\n if (fieldLength === -1) {\n // first colon in line\n fieldLength = position - lineStart\n }\n break\n // @ts-expect-error:7029 \\r case below should fallthrough to \\n:\n // biome-ignore lint/suspicious/noFallthroughSwitchClause: intentional fallthrough for CR to LF\n case 13: // \\r\n discardTrailingNewline = true\n case 10: // \\n\n lineEnd = position\n break\n }\n }\n\n if (lineEnd === -1) break // Wait for the next arr and then continue parsing\n\n // we've reached the line end, send it out:\n onLine(buffer.subarray(lineStart, lineEnd), fieldLength)\n lineStart = position // we're now on the next line\n fieldLength = -1\n }\n\n if (lineStart === bufLength)\n buffer = undefined // we've finished reading it\n else if (lineStart) {\n // Create a new view into buffer beginning at lineStart so we don't\n // need to copy over the previous lines when we get the new arr:\n buffer = buffer.subarray(lineStart)\n position -= lineStart\n }\n }\n}\n\nconst getMessages = (\n onId: (id: string) => void,\n onRetry: (retry: number) => void,\n onMessage?: (msg: EventSourceMessage) => void,\n): ((line: Uint8Array, fieldLength: number) => void) => {\n let message = newMessage()\n const decoder = new TextDecoder()\n\n // return a function that can process each incoming line buffer:\n return (line, fieldLength) => {\n if (!line.length) {\n // empty line denotes end of message. Trigger the callback and start a new message:\n onMessage?.(message)\n message = newMessage()\n } else if (fieldLength > 0) {\n // exclude comments and lines with no values\n // line is of format \":\" or \": \"\n // https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation\n const field = decoder.decode(line.subarray(0, fieldLength))\n const valueOffset = fieldLength + (line[fieldLength + 1] === 32 ? 2 : 1)\n const value = decoder.decode(line.subarray(valueOffset))\n\n switch (field) {\n case 'data':\n message.data = message.data ? `${message.data}\\n${value}` : value\n break\n case 'event':\n message.event = value\n break\n case 'id':\n onId((message.id = value))\n break\n case 'retry': {\n const retry = +value\n if (!Number.isNaN(retry)) {\n // per spec, ignore non-integers\n onRetry((message.retry = retry))\n }\n break\n }\n }\n }\n }\n}\n\nconst concat = (a: Uint8Array, b: Uint8Array) => {\n const res = new Uint8Array(a.length + b.length)\n res.set(a)\n res.set(b, a.length)\n return res\n}\n\nconst newMessage = (): EventSourceMessage => ({\n // data, event, and id must be initialized to empty strings:\n // https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation\n // retry should be initialized to undefined so we return a consistent shape\n // to the js engine all the time: https://mathiasbynens.be/notes/shapes-ics#takeaways\n data: '',\n event: '',\n id: '',\n retry: undefined,\n})\n\ntype FetchEventSourceInit = RequestInit & {\n headers?: Record\n onopen?: (response: Response) => Promise\n onmessage?: (ev: EventSourceMessage) => void\n onclose?: () => void\n onerror?: (err: any) => number | null | undefined | void\n openWhenHidden?: boolean\n fetch?: typeof fetch\n retryInterval?: number\n retryScaler?: number\n retryMaxWaitMs?: number\n retryMaxCount?: number\n overrides?: ResponseOverrides\n}\n\nconst fetchEventSource = (\n input: RequestInfo,\n el: HTMLOrSVG,\n {\n signal: inputSignal,\n headers: inputHeaders,\n onopen: inputOnOpen,\n onmessage,\n onclose,\n onerror,\n openWhenHidden,\n fetch: inputFetch,\n retryInterval = 1_000,\n retryScaler = 2,\n retryMaxWaitMs = 30_000,\n retryMaxCount = 10,\n overrides,\n ...rest\n }: FetchEventSourceInit,\n): Promise => {\n return new Promise((resolve, reject) => {\n // make a copy of the input headers since we may modify it below:\n const headers: Record = {\n ...inputHeaders,\n }\n\n let curRequestController: AbortController\n const onVisibilityChange = () => {\n curRequestController.abort() // close existing request on every visibility change\n if (!document.hidden) create() // page is now visible again, recreate request.\n }\n\n if (!openWhenHidden) {\n document.addEventListener('visibilitychange', onVisibilityChange)\n }\n\n let retryTimer = 0\n const dispose = () => {\n document.removeEventListener('visibilitychange', onVisibilityChange)\n clearTimeout(retryTimer)\n curRequestController.abort()\n }\n\n // if the incoming signal aborts, dispose resources and resolve:\n inputSignal?.addEventListener('abort', () => {\n dispose()\n resolve() // don't waste time constructing/logging errors\n })\n\n const fetch = inputFetch || window.fetch\n const onopen = inputOnOpen || (() => {})\n\n let retries = 0\n let baseRetryInterval = retryInterval\n const create = async () => {\n curRequestController = new AbortController()\n try {\n const response = await fetch(input, {\n ...rest,\n headers,\n signal: curRequestController.signal,\n })\n\n // on successful connection, reset the retry logic\n retries = 0\n retryInterval = baseRetryInterval\n\n await onopen(response)\n\n const dispatchNonSSE = async (\n dispatchType: string,\n response: Response,\n name: string,\n overrides?: ResponseOverrides,\n ...argNames: string[]\n ) => {\n const argsRaw: Record = {\n [name]: await response.text(),\n }\n for (const n of argNames) {\n let v = response.headers.get(`datastar-${kebab(n)}`)\n if (overrides) {\n const o = (overrides as any)[n]\n if (o) v = typeof o === 'string' ? o : JSON.stringify(o)\n }\n if (v) argsRaw[n] = v\n }\n\n dispatchFetch(dispatchType, el, argsRaw)\n dispose()\n resolve()\n }\n\n const ct = response.headers.get('Content-Type')\n if (ct?.includes('text/html')) {\n return await dispatchNonSSE(\n 'datastar-patch-elements',\n response,\n 'elements',\n overrides,\n 'selector',\n 'mode',\n 'useViewTransition',\n )\n }\n\n if (ct?.includes('application/json')) {\n return await dispatchNonSSE(\n 'datastar-patch-signals',\n response,\n 'signals',\n overrides,\n 'onlyIfMissing',\n )\n }\n\n if (ct?.includes('text/javascript')) {\n const script = document.createElement('script')\n const scriptAttributesHeader = response.headers.get(\n 'datastar-script-attributes',\n )\n\n if (scriptAttributesHeader) {\n for (const [name, value] of Object.entries(\n JSON.parse(scriptAttributesHeader),\n )) {\n script.setAttribute(name, value as string)\n }\n }\n script.textContent = await response.text()\n document.head.appendChild(script)\n dispose()\n return\n }\n\n await getBytes(\n response.body!,\n getLines(\n getMessages(\n (id) => {\n if (id) {\n // signals the id and send it back on the next retry:\n headers['last-event-id'] = id\n } else {\n // don't send the last-event-id header anymore:\n delete headers['last-event-id']\n }\n },\n (retry) => {\n baseRetryInterval = retryInterval = retry\n },\n onmessage,\n ),\n ),\n )\n\n onclose?.()\n dispose()\n resolve()\n } catch (err) {\n if (!curRequestController.signal.aborted) {\n // if we haven’t aborted the request ourselves:\n try {\n // check if we need to retry:\n const interval: any = onerror?.(err) || retryInterval\n clearTimeout(retryTimer)\n retryTimer = setTimeout(create, interval)\n retryInterval = Math.min(\n retryInterval * retryScaler,\n retryMaxWaitMs,\n ) // exponential backoff\n if (++retries >= retryMaxCount) {\n dispatchFetch(RETRIES_FAILED, el, {})\n // we should not retry anymore:\n dispose()\n reject('Max retries reached.') // Max retries reached, check your server or network connection\n } else {\n console.error(\n `Datastar failed to reach ${input.toString()} retrying in ${interval}ms.`,\n )\n }\n } catch (innerErr) {\n // we should not retry anymore:\n dispose()\n reject(innerErr)\n }\n }\n }\n }\n\n create()\n })\n}\n","// Icon: material-symbols:edit-attributes-outline\n// Slug: Syncs the value of an attribute with an expression.\n// Description: Sets the value of any HTML attribute to an expression, and keeps it in sync.\n\nimport { attribute } from '@engine'\nimport { effect } from '@engine/signals'\n\nattribute({\n name: 'attr',\n requirement: { value: 'must' },\n returnsValue: true,\n apply({ el, key, rx }) {\n const syncAttr = (key: string, val: any) => {\n if (val === '' || val === true) {\n el.setAttribute(key, '')\n } else if (val === false || val == null) {\n el.removeAttribute(key)\n } else if (typeof val === 'string') {\n el.setAttribute(key, val)\n } else {\n el.setAttribute(key, JSON.stringify(val))\n }\n }\n\n const update = key\n ? () => {\n observer.disconnect()\n const val = rx() as string\n syncAttr(key, val)\n observer.observe(el, {\n attributeFilter: [key],\n })\n }\n : () => {\n observer.disconnect()\n const obj = rx() as Record\n const attributeFilter = Object.keys(obj)\n for (const key of attributeFilter) {\n syncAttr(key, obj[key])\n }\n observer.observe(el, {\n attributeFilter,\n })\n }\n\n const observer = new MutationObserver(update)\n const cleanup = effect(update)\n\n return () => {\n observer.disconnect()\n cleanup()\n }\n },\n})\n","// Icon: akar-icons:link-chain\n// Slug: Creates a signal with two-way data binding.\n// Description: Creates a signal (if one doesn’t already exist) and sets up two-way data binding between it and an element’s value.\n\nimport { attribute } from '@engine'\nimport { effect, getPath, mergePaths } from '@engine/signals'\nimport type { Paths } from '@engine/types'\nimport { aliasify, modifyCasing } from '@utils/text'\n\ntype SignalFile = {\n name: string\n contents: string\n mime: string\n}\n\nconst dataURIRegex = /^data:(?[^;]+);base64,(?.*)$/\nconst empty = Symbol('empty')\n\nconst aliasedBind = aliasify('bind')\n\nattribute({\n name: 'bind',\n requirement: 'exclusive',\n apply({ el, key, mods, value, error }) {\n const signalName = key != null ? modifyCasing(key, mods) : value\n\n let get = (el: any, type: string) =>\n type === 'number' ? +el.value : el.value\n\n let set = (value: any) => {\n ;(el as HTMLInputElement).value = `${value}`\n }\n\n if (el instanceof HTMLInputElement) {\n switch (el.type) {\n case 'range':\n case 'number':\n get = (el: any, type: string) =>\n type === 'string' ? el.value : +el.value\n break\n\n case 'checkbox':\n get = (el: HTMLInputElement, type: string) => {\n if (el.value !== 'on') {\n if (type === 'boolean') {\n return el.checked\n } else {\n return el.checked ? el.value : ''\n }\n } else {\n if (type === 'string') {\n return el.checked ? el.value : ''\n } else {\n return el.checked\n }\n }\n }\n set = (value: string | boolean) => {\n el.checked = typeof value === 'string' ? value === el.value : value\n }\n break\n\n case 'radio':\n // Set up radio button name attribute if not present\n if (!el.getAttribute('name')?.length) {\n el.setAttribute('name', signalName)\n }\n\n get = (el: HTMLInputElement, type: string) =>\n el.checked ? (type === 'number' ? +el.value : el.value) : empty\n set = (value: string | number) => {\n el.checked =\n value === (typeof value === 'number' ? +el.value : el.value)\n }\n break\n case 'file': {\n const syncSignal = () => {\n const files = [...(el.files || [])]\n const signalFiles: SignalFile[] = []\n Promise.all(\n files.map(\n (f) =>\n new Promise((resolve) => {\n const reader = new FileReader()\n reader.onload = () => {\n if (typeof reader.result !== 'string') {\n throw error('InvalidFileResultType', {\n resultType: typeof reader.result,\n })\n }\n const match = reader.result.match(dataURIRegex)\n if (!match?.groups) {\n throw error('InvalidDataUri', {\n result: reader.result,\n })\n }\n signalFiles.push({\n name: f.name,\n contents: match.groups.contents,\n mime: match.groups.mime,\n })\n }\n reader.onloadend = () => resolve()\n reader.readAsDataURL(f)\n }),\n ),\n ).then(() => {\n mergePaths([[signalName, signalFiles]])\n })\n }\n\n el.addEventListener('change', syncSignal)\n el.addEventListener('input', syncSignal)\n\n return () => {\n el.removeEventListener('change', syncSignal)\n el.removeEventListener('input', syncSignal)\n }\n }\n }\n } else if (el instanceof HTMLSelectElement) {\n if (el.multiple) {\n const typeMap = new Map()\n get = (el: HTMLSelectElement) =>\n [...el.selectedOptions].map((option) => {\n const type = typeMap.get(option.value)\n return type === 'string' || type == null\n ? option.value\n : +option.value\n })\n\n set = (value: (string | number)[]) => {\n for (const option of el.options) {\n if (value.includes(option.value)) {\n typeMap.set(option.value, 'string')\n option.selected = true\n } else if (value.includes(+option.value)) {\n typeMap.set(option.value, 'number')\n option.selected = true\n } else {\n option.selected = false\n }\n }\n }\n }\n } else if (el instanceof HTMLTextAreaElement) {\n // default case\n } else {\n // web component\n get = (el: Element) =>\n 'value' in el ? el.value : el.getAttribute('value')\n set = (value: any) => {\n if ('value' in el) {\n el.value = value\n } else {\n el.setAttribute('value', value)\n }\n }\n }\n\n const initialValue = getPath(signalName)\n const type = typeof initialValue\n\n let path = signalName\n if (\n Array.isArray(initialValue) &&\n !(el instanceof HTMLSelectElement && el.multiple)\n ) {\n const signalNameKebab = key ? key : value!\n const inputs = document.querySelectorAll(\n `[${aliasedBind}\\\\:${CSS.escape(signalNameKebab)}],[${aliasedBind}=\"${CSS.escape(signalNameKebab)}\"]`,\n ) as NodeListOf\n\n const paths: Paths = []\n let i = 0\n for (const input of inputs) {\n paths.push([`${path}.${i}`, get(input, 'none')])\n\n if (el === input) {\n break\n }\n i++\n }\n mergePaths(paths, { ifMissing: true })\n path = `${path}.${i}`\n } else {\n mergePaths([[path, get(el, type)]], {\n ifMissing: true,\n })\n }\n\n const syncSignal = () => {\n const signalValue = getPath(path)\n if (signalValue != null) {\n const value = get(el, typeof signalValue)\n if (value !== empty) {\n mergePaths([[path, value]])\n }\n }\n }\n\n el.addEventListener('input', syncSignal)\n el.addEventListener('change', syncSignal)\n const cleanup = effect(() => {\n set(getPath(path))\n })\n\n return () => {\n cleanup()\n el.removeEventListener('input', syncSignal)\n el.removeEventListener('change', syncSignal)\n }\n },\n})\n","// Icon: ic:baseline-format-paint\n// Slug: Adds or removes a class based on an expression.\n// Description: Adds or removes a class to or from an element based on an expression.\n\nimport { attribute } from '@engine'\nimport { effect } from '@engine/signals'\nimport { modifyCasing } from '@utils/text'\n\nattribute({\n name: 'class',\n requirement: {\n value: 'must',\n },\n returnsValue: true,\n apply({ key, el, mods, rx }) {\n if (key) {\n key = modifyCasing(key, mods, 'kebab')\n }\n\n const callback = () => {\n observer.disconnect()\n\n const classes = key\n ? { [key]: rx() as boolean }\n : (rx() as Record)\n\n for (const k in classes) {\n const classNames = k.split(/\\s+/).filter((cn) => cn.length > 0)\n if (classes[k]) {\n for (const name of classNames) {\n if (!el.classList.contains(name)) {\n el.classList.add(name)\n }\n }\n } else {\n for (const name of classNames) {\n if (el.classList.contains(name)) {\n el.classList.remove(name)\n }\n }\n }\n }\n\n observer.observe(el, { attributeFilter: ['class'] })\n }\n\n const observer = new MutationObserver(callback)\n const cleanup = effect(callback)\n\n return () => {\n observer.disconnect()\n cleanup()\n\n const classes = key\n ? { [key]: rx() as boolean }\n : (rx() as Record)\n\n for (const k in classes) {\n const classNames = k.split(/\\s+/).filter((cn) => cn.length > 0)\n for (const name of classNames) {\n el.classList.remove(name)\n }\n }\n }\n },\n})\n","// Icon: streamline-ultimate:wifi-signal-2\n// Slug: Creates a computed signal.\n// Description: Creates a signal that is computed based on an expression.\n\nimport { attribute } from '@engine'\nimport { computed, mergePaths, mergePatch } from '@engine/signals'\nimport { modifyCasing } from '@utils/text'\nimport { updateLeaves } from '@utils/paths'\n\nattribute({\n name: 'computed',\n requirement: {\n value: 'must',\n },\n returnsValue: true,\n apply({ key, mods, rx, error }) {\n if (key) {\n mergePaths([[modifyCasing(key, mods), computed(rx)]])\n } else {\n const patch = Object.assign({}, rx() as Record any>)\n updateLeaves(patch, (old) => {\n if (typeof old === 'function') {\n return computed(old)\n } else {\n throw error('ComputedExpectedFunction')\n }\n })\n mergePatch(patch)\n }\n },\n})\n","// Icon: oui:security-signal\n// Slug: Executes an expression when signals change.\n// Description: Executes an expression on page load and whenever any signals in the expression change.\n\nimport { attribute } from '@engine'\nimport { effect } from '@engine/signals'\n\nattribute({\n name: 'effect',\n requirement: {\n key: 'denied',\n value: 'must',\n },\n apply: ({ rx }) => effect(rx),\n})\n","// Icon: streamline:signal-loading-remix\n// Slug: Creates an indicator for whether an SSE request is in flight.\n// Description: Creates a signal and sets its value to `true` while an SSE request request is in flight, otherwise `false`.\n\nimport { attribute } from '@engine'\nimport { DATASTAR_FETCH_EVENT } from '@engine/consts'\nimport { mergePaths } from '@engine/signals'\nimport type { DatastarFetchEvent } from '@engine/types'\nimport { FINISHED, STARTED } from '@plugins/actions/fetch'\nimport { modifyCasing } from '@utils/text'\n\nattribute({\n name: 'indicator',\n requirement: 'exclusive',\n apply({ el, key, mods, value }) {\n const signalName = key != null ? modifyCasing(key, mods) : value\n\n mergePaths([[signalName, false]])\n\n const watcher = ((event: CustomEvent) => {\n const { type, el: elt } = event.detail\n if (elt !== el) {\n return\n }\n switch (type) {\n case STARTED:\n mergePaths([[signalName, true]])\n break\n case FINISHED:\n mergePaths([[signalName, false]])\n break\n }\n }) as EventListener\n document.addEventListener(DATASTAR_FETCH_EVENT, watcher)\n return () => {\n mergePaths([[signalName, false]])\n document.removeEventListener(DATASTAR_FETCH_EVENT, watcher)\n }\n },\n})\n","// Icon: si:json-fill\n// Slug: Outputs a JSON stringified version of signals.\n// Description: Sets the text content of an element to a reactive JSON stringified version of signals.\n\nimport { attribute } from '@engine'\nimport { effect, filtered } from '@engine/signals'\nimport type { SignalFilterOptions } from '@engine/types'\nimport { jsStrToObject } from '@utils/text'\n\nattribute({\n name: 'json-signals',\n requirement: {\n key: 'denied',\n },\n apply({ el, value, mods }) {\n const spaces = mods.has('terse') ? 0 : 2\n let filters: SignalFilterOptions = {}\n if (value) {\n filters = jsStrToObject(value)\n }\n\n const callback = () => {\n observer.disconnect()\n el.textContent = JSON.stringify(filtered(filters), null, spaces)\n observer.observe(el, {\n childList: true,\n characterData: true,\n subtree: true,\n })\n }\n const observer = new MutationObserver(callback)\n const cleanup = effect(callback)\n\n return () => {\n observer.disconnect()\n cleanup()\n }\n },\n})\n","export const tagToMs = (args: Set) => {\n if (!args || args.size <= 0) return 0\n for (const arg of args) {\n if (arg.endsWith('ms')) {\n return +arg.replace('ms', '')\n }\n if (arg.endsWith('s')) {\n return +arg.replace('s', '') * 1000\n }\n try {\n return Number.parseFloat(arg)\n } catch (_) {}\n }\n return 0\n}\n\nexport const tagHas = (\n tags: Set,\n tag: string,\n defaultValue = false,\n) => {\n if (!tags) return defaultValue\n return tags.has(tag.toLowerCase())\n}\n\nexport const tagFirst = (tags?: Set, defaultValue = ''): string => {\n if (tags && tags.size > 0) {\n for (const tag of tags) {\n return tag\n }\n }\n return defaultValue\n}\n","import type { EventCallbackHandler, Modifiers } from '@engine/types'\nimport { tagHas, tagToMs } from '@utils/tags'\n\nexport const delay = (\n callback: EventCallbackHandler,\n wait: number,\n): EventCallbackHandler => {\n return (...args: any[]) => {\n setTimeout(() => {\n callback(...args)\n }, wait)\n }\n}\n\nexport const debounce = (\n callback: EventCallbackHandler,\n wait: number,\n leading = false,\n trailing = true,\n): EventCallbackHandler => {\n let timer = 0\n return (...args: any[]) => {\n timer && clearTimeout(timer)\n\n if (leading && !timer) {\n callback(...args)\n }\n\n timer = setTimeout(() => {\n if (trailing) {\n callback(...args)\n }\n timer && clearTimeout(timer)\n timer = 0\n }, wait)\n }\n}\n\nexport const throttle = (\n callback: EventCallbackHandler,\n wait: number,\n leading = true,\n trailing = false,\n): EventCallbackHandler => {\n let waiting = false\n\n return (...args: any[]) => {\n if (waiting) return\n\n if (leading) {\n callback(...args)\n }\n\n waiting = true\n setTimeout(() => {\n if (trailing) {\n callback(...args)\n }\n waiting = false\n }, wait)\n }\n}\n\nexport const modifyTiming = (\n callback: EventCallbackHandler,\n mods: Modifiers,\n): EventCallbackHandler => {\n const delayArgs = mods.get('delay')\n if (delayArgs) {\n const wait = tagToMs(delayArgs)\n callback = delay(callback, wait)\n }\n\n const debounceArgs = mods.get('debounce')\n if (debounceArgs) {\n const wait = tagToMs(debounceArgs)\n const leading = tagHas(debounceArgs, 'leading', false)\n const trailing = !tagHas(debounceArgs, 'notrailing', false)\n callback = debounce(callback, wait, leading, trailing)\n }\n\n const throttleArgs = mods.get('throttle')\n if (throttleArgs) {\n const wait = tagToMs(throttleArgs)\n const leading = !tagHas(throttleArgs, 'noleading', false)\n const trailing = tagHas(throttleArgs, 'trailing', false)\n callback = throttle(callback, wait, leading, trailing)\n }\n\n return callback\n}\n","import type { EventCallbackHandler, Modifiers } from '@engine/types'\n\nexport const supportsViewTransitions = !!document.startViewTransition\n\nexport const modifyViewTransition = (\n callback: EventCallbackHandler,\n mods: Modifiers,\n): EventCallbackHandler => {\n if (mods.has('viewtransition') && supportsViewTransitions) {\n const cb = callback // I hate javascript\n callback = (...args: any[]) =>\n document.startViewTransition(() => cb(...args))\n }\n\n return callback\n}\n","// Icon: material-symbols:mail\n// Slug: Attaches an event listener to an element.\n// Description: Attaches an event listener to an element, executing an expression whenever the event is triggered.\n\nimport { attribute } from '@engine'\nimport {\n DATASTAR_FETCH_EVENT,\n DATASTAR_SIGNAL_PATCH_EVENT,\n} from '@engine/consts'\nimport { beginBatch, endBatch } from '@engine/signals'\nimport { modifyCasing } from '@utils/text'\nimport { modifyTiming } from '@utils/timing'\nimport { modifyViewTransition } from '@utils/view-transitions'\n\nattribute({\n name: 'on',\n requirement: 'must',\n argNames: ['evt'],\n apply({ el, key, mods, rx }) {\n let target: Element | Window | Document = el\n if (mods.has('window')) target = window\n let callback = (evt?: Event) => {\n if (evt) {\n if (mods.has('prevent')) {\n evt.preventDefault()\n }\n if (mods.has('stop')) {\n evt.stopPropagation()\n }\n }\n beginBatch()\n rx(evt)\n endBatch()\n }\n callback = modifyViewTransition(callback, mods)\n callback = modifyTiming(callback, mods)\n const evtListOpts: AddEventListenerOptions = {\n capture: mods.has('capture'),\n passive: mods.has('passive'),\n once: mods.has('once'),\n }\n if (mods.has('outside')) {\n target = document\n const cb = callback\n callback = (evt?: Event) => {\n if (!el.contains(evt?.target as HTMLElement)) {\n cb(evt)\n }\n }\n }\n const eventName = modifyCasing(key, mods, 'kebab')\n // Listen for Datastar events on the document\n if (\n eventName === DATASTAR_FETCH_EVENT ||\n eventName === DATASTAR_SIGNAL_PATCH_EVENT\n ) {\n target = document\n }\n // Prevent default on form submit events\n if (el instanceof HTMLFormElement && eventName === 'submit') {\n const cb = callback\n callback = (evt?: Event) => {\n evt?.preventDefault()\n cb(evt)\n }\n }\n target.addEventListener(eventName, callback, evtListOpts)\n return () => {\n target.removeEventListener(eventName, callback)\n }\n },\n})\n","// Icon: mdi-light:vector-intersection\n// Slug: Runs an expression on intersection.\n// Description: Runs an expression when the element intersects with the viewport.\n\nimport { attribute } from '@engine'\nimport { beginBatch, endBatch } from '@engine/signals'\nimport type { HTMLOrSVG } from '@engine/types'\nimport { modifyTiming } from '@utils/timing'\nimport { modifyViewTransition } from '@utils/view-transitions'\n\nconst once = new WeakSet()\n\nattribute({\n name: 'on-intersect',\n requirement: {\n key: 'denied',\n value: 'must',\n },\n apply({ el, mods, rx }) {\n let callback = () => {\n beginBatch()\n rx()\n endBatch()\n }\n callback = modifyViewTransition(callback, mods)\n callback = modifyTiming(callback, mods)\n const options = { threshold: 0 }\n if (mods.has('full')) {\n options.threshold = 1\n } else if (mods.has('half')) {\n options.threshold = 0.5\n }\n let observer: IntersectionObserver | null = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n callback()\n if (observer && once.has(el)) {\n observer.disconnect()\n }\n }\n }\n },\n options,\n )\n observer.observe(el)\n if (mods.has('once')) {\n once.add(el)\n }\n return () => {\n if (!mods.has('once')) {\n once.delete(el)\n }\n if (observer) {\n observer.disconnect()\n observer = null\n }\n }\n },\n})\n","// Icon: material-symbols:timer-outline\n// Slug: Runs an expression at a regular interval.\n// Description: Runs an expression at a regular interval.\n\nimport { attribute } from '@engine'\nimport { beginBatch, endBatch } from '@engine/signals'\nimport { tagHas, tagToMs } from '@utils/tags'\nimport { modifyViewTransition } from '@utils/view-transitions'\n\nattribute({\n name: 'on-interval',\n requirement: {\n key: 'denied',\n value: 'must',\n },\n apply({ mods, rx }) {\n let callback = () => {\n beginBatch()\n rx()\n endBatch()\n }\n callback = modifyViewTransition(callback, mods)\n let duration = 1000\n const durationArgs = mods.get('duration')\n if (durationArgs) {\n duration = tagToMs(durationArgs)\n const leading = tagHas(durationArgs, 'leading', false)\n if (leading) {\n callback()\n }\n }\n const intervalId = setInterval(callback, duration)\n return () => {\n clearInterval(intervalId)\n }\n },\n})\n","// Icon: material-symbols:timer-play-outline\n// Slug: Runs an expression when loaded into the DOM.\n// Description: Runs an expression when the element is loaded into the DOM.\n\nimport { attribute } from '@engine'\nimport { beginBatch, endBatch } from '@engine/signals'\nimport { tagToMs } from '@utils/tags'\nimport { delay } from '@utils/timing'\nimport { modifyViewTransition } from '@utils/view-transitions'\n\nattribute({\n name: 'init',\n requirement: {\n key: 'denied',\n value: 'must',\n },\n apply({ rx, mods }) {\n let callback = () => {\n beginBatch()\n rx()\n endBatch()\n }\n callback = modifyViewTransition(callback, mods)\n let wait = 0\n const delayArgs = mods.get('delay')\n if (delayArgs) {\n wait = tagToMs(delayArgs)\n if (wait > 0) {\n callback = delay(callback, wait)\n }\n }\n callback()\n },\n})\n","// Icon: material-symbols:change-circle-outline\n// Slug: Runs an expression when signals are patched.\n// Description: Runs an expression whenever one or more signals are patched.\n\nimport { attribute } from '@engine'\nimport { DATASTAR_SIGNAL_PATCH_EVENT } from '@engine/consts'\nimport { beginBatch, endBatch, filtered } from '@engine/signals'\nimport type { JSONPatch, SignalFilterOptions } from '@engine/types'\nimport { isEmpty } from '@utils/paths'\nimport { jsStrToObject } from '@utils/text'\nimport { modifyTiming } from '@utils/timing'\n\nattribute({\n name: 'on-signal-patch',\n requirement: {\n value: 'must',\n },\n argNames: ['patch'],\n returnsValue: true,\n apply({ el, key, mods, rx, error }) {\n if (!!key && key !== 'filter') {\n throw error('KeyNotAllowed')\n }\n\n // Look for data-on-signal-patch-filter data attribute\n const filtersRaw = el.getAttribute('data-on-signal-patch-filter')\n let filters: SignalFilterOptions = {}\n if (filtersRaw) {\n filters = jsStrToObject(filtersRaw)\n }\n\n const callback: EventListener = modifyTiming(\n (evt: CustomEvent) => {\n const watched = filtered(filters, evt.detail)\n if (!isEmpty(watched)) {\n beginBatch()\n rx(watched)\n endBatch()\n }\n },\n mods,\n )\n\n document.addEventListener(DATASTAR_SIGNAL_PATCH_EVENT, callback)\n return () => {\n document.removeEventListener(DATASTAR_SIGNAL_PATCH_EVENT, callback)\n }\n },\n})\n","// Icon: mdi:cursor-pointer\n// Slug: Creates a reference to an element.\n// Description: Creates a new signal that is a reference to the element on which the data attribute is placed.\n\nimport { attribute } from '@engine'\nimport { mergePaths } from '@engine/signals'\nimport { modifyCasing } from '@utils/text'\n\nattribute({\n name: 'ref',\n requirement: 'exclusive',\n apply({ el, key, mods, value }) {\n const signalName = key != null ? modifyCasing(key, mods) : value\n mergePaths([[signalName, el]])\n },\n})\n","// Icon: streamline:interface-edit-view-eye-eyeball-open-view\n// Slug: Shows or hides an element.\n// Description: Shows or hides an element based on whether an expression evaluates to `true` or `false`.\n\nimport { attribute } from '@engine'\nimport { effect } from '@engine/signals'\n\nconst NONE = 'none'\nconst DISPLAY = 'display'\n\nattribute({\n name: 'show',\n requirement: {\n key: 'denied',\n value: 'must',\n },\n returnsValue: true,\n apply({ el, rx }) {\n const update = () => {\n observer.disconnect()\n const shouldShow = rx()\n if (shouldShow) {\n if (el.style.display === NONE) el.style.removeProperty(DISPLAY)\n } else {\n el.style.setProperty(DISPLAY, NONE)\n }\n observer.observe(el, { attributeFilter: ['style'] })\n }\n const observer = new MutationObserver(update)\n const cleanup = effect(update)\n\n return () => {\n observer.disconnect()\n cleanup()\n }\n },\n})\n","// Icon: streamline:wifi-signal-full-remix\n// Slug: Patches signals into the existing signals.\n// Description: Patches (adds, updates or removes) one or more signals into the existing signals.\n\nimport { attribute } from '@engine'\nimport { mergePatch, mergePaths } from '@engine/signals'\nimport { modifyCasing } from '@utils/text'\n\nattribute({\n name: 'signals',\n returnsValue: true,\n apply({ key, mods, rx }) {\n const ifMissing = mods.has('ifmissing')\n\n if (key) {\n key = modifyCasing(key, mods)\n mergePaths([[key, rx?.()]], { ifMissing })\n } else {\n const patch = Object.assign({}, rx?.() as Record)\n mergePatch(patch, { ifMissing })\n }\n },\n})\n","// Icon: material-symbols:format-paint-outline\n// Slug: Sets inline styles on an element based on an expression.\n// Description: Sets CSS styles on an element using either key-based or object syntax, and keeps them in sync with reactive signals.\n\nimport { attribute } from '@engine'\nimport { effect } from '@engine/signals'\nimport { kebab } from '@utils/text'\n\nattribute({\n name: 'style',\n requirement: {\n value: 'must',\n },\n returnsValue: true,\n apply({ key, el, rx }) {\n const { style } = el\n const initialStyles = new Map()\n\n const apply = (prop: string, value: any) => {\n const initial = initialStyles.get(prop)\n if (!value && value !== 0) {\n initial !== undefined &&\n (initial\n ? style.setProperty(prop, initial)\n : style.removeProperty(prop))\n } else {\n initial === undefined &&\n initialStyles.set(prop, style.getPropertyValue(prop))\n style.setProperty(prop, String(value))\n }\n }\n\n const update = () => {\n observer.disconnect()\n\n if (key) {\n apply(key, rx())\n } else {\n const styles = rx() as Record\n\n for (const [prop, initial] of initialStyles) {\n prop in styles ||\n (initial\n ? style.setProperty(prop, initial)\n : style.removeProperty(prop))\n }\n\n for (const prop in styles) {\n apply(kebab(prop), styles[prop])\n }\n }\n\n observer.observe(el, { attributeFilter: ['style'] })\n }\n\n const observer = new MutationObserver(update)\n const cleanup = effect(update)\n\n return () => {\n observer.disconnect()\n cleanup()\n for (const [prop, initial] of initialStyles) {\n initial ? style.setProperty(prop, initial) : style.removeProperty(prop)\n }\n }\n },\n})\n","// Icon: icon-park-outline:text\n// Slug: Binds the text content of an element.\n// Description: Binds the text content of an element to an expression.\n\nimport { attribute } from '@engine'\nimport { effect } from '@engine/signals'\n\nattribute({\n name: 'text',\n requirement: {\n key: 'denied',\n value: 'must',\n },\n returnsValue: true,\n apply({ el, rx }) {\n const update = () => {\n observer.disconnect()\n el.textContent = `${rx()}`\n observer.observe(el, {\n childList: true,\n characterData: true,\n subtree: true,\n })\n }\n\n const observer = new MutationObserver(update)\n const cleanup = effect(update)\n\n return () => {\n observer.disconnect()\n cleanup()\n }\n },\n})\n","import { isHTMLOrSVG } from '@utils/dom'\nimport { aliasify } from '@utils/text'\n\nconst ctxIdMap = new Map>()\nconst ctxPersistentIds = new Set()\nconst oldIdTagNameMap = new Map()\nconst duplicateIds = new Set()\nconst ctxPantry = document.createElement('div')\nctxPantry.hidden = true\n\nconst aliasedIgnoreMorph = aliasify('ignore-morph')\nconst aliasedIgnoreMorphAttr = `[${aliasedIgnoreMorph}]`\nexport const morph = (\n oldElt: Element | ShadowRoot,\n newContent: DocumentFragment | Element,\n mode: 'outer' | 'inner' = 'outer',\n): void => {\n if (\n (isHTMLOrSVG(oldElt) &&\n isHTMLOrSVG(newContent) &&\n oldElt.hasAttribute(aliasedIgnoreMorph) &&\n newContent.hasAttribute(aliasedIgnoreMorph)) ||\n oldElt.parentElement?.closest(aliasedIgnoreMorphAttr)\n ) {\n return\n }\n\n const normalizedElt = document.createElement('div')\n normalizedElt.append(newContent)\n document.body.insertAdjacentElement('afterend', ctxPantry)\n\n // Computes the set of IDs that persist between the two contents excluding duplicates\n const oldIdElements = oldElt.querySelectorAll('[id]')\n for (const { id, tagName } of oldIdElements) {\n if (oldIdTagNameMap.has(id)) {\n duplicateIds.add(id)\n } else {\n oldIdTagNameMap.set(id, tagName)\n }\n }\n if (oldElt instanceof Element && oldElt.id) {\n if (oldIdTagNameMap.has(oldElt.id)) {\n duplicateIds.add(oldElt.id)\n } else {\n oldIdTagNameMap.set(oldElt.id, oldElt.tagName)\n }\n }\n\n ctxPersistentIds.clear()\n const newIdElements = normalizedElt.querySelectorAll('[id]')\n for (const { id, tagName } of newIdElements) {\n if (ctxPersistentIds.has(id)) {\n duplicateIds.add(id)\n } else if (oldIdTagNameMap.get(id) === tagName) {\n ctxPersistentIds.add(id)\n }\n }\n\n for (const id of duplicateIds) {\n ctxPersistentIds.delete(id)\n }\n\n oldIdTagNameMap.clear()\n duplicateIds.clear()\n ctxIdMap.clear()\n\n const parent = mode === 'outer' ? oldElt.parentElement! : oldElt\n populateIdMapWithTree(parent, oldIdElements)\n populateIdMapWithTree(normalizedElt, newIdElements)\n\n morphChildren(\n parent,\n normalizedElt,\n mode === 'outer' ? oldElt : null,\n oldElt.nextSibling,\n )\n\n ctxPantry.remove()\n}\n\n// This is the core algorithm for matching up children.\n// The idea is to use ID sets to try to match up nodes as faithfully as possible.\n// We greedily match, which allows us to keep the algorithm fast,\n// but by using ID sets, we are able to better match up with content deeper in the DOM.\nconst morphChildren = (\n oldParent: Element | ShadowRoot, // the old content that we are merging the new content into\n newParent: Element, // the parent element of the new content\n insertionPoint: Node | null = null, // the point in the DOM we start morphing at (defaults to first child)\n endPoint: Node | null = null, // the point in the DOM we stop morphing at (defaults to after last child)\n): void => {\n // normalize\n if (\n oldParent instanceof HTMLTemplateElement &&\n newParent instanceof HTMLTemplateElement\n ) {\n // we can pretend the DocumentElement is an Element\n oldParent = oldParent.content as unknown as Element\n newParent = newParent.content as unknown as Element\n }\n insertionPoint ??= oldParent.firstChild\n\n // run through all the new content\n for (const newChild of newParent.childNodes) {\n // once we reach the end of the old parent content skip to the end and insert the rest\n if (insertionPoint && insertionPoint !== endPoint) {\n const bestMatch = findBestMatch(newChild, insertionPoint, endPoint)\n if (bestMatch) {\n // if the node to morph is not at the insertion point then remove/move up to it\n if (bestMatch !== insertionPoint) {\n let cursor: Node | null = insertionPoint\n // Remove nodes between the start and end nodes\n while (cursor && cursor !== bestMatch) {\n const tempNode = cursor\n cursor = cursor.nextSibling\n removeNode(tempNode)\n }\n }\n morphNode(bestMatch, newChild)\n insertionPoint = bestMatch.nextSibling\n continue\n }\n }\n\n // if the matching node is elsewhere in the original content\n if (newChild instanceof Element && ctxPersistentIds.has(newChild.id)) {\n // move it and all its children here and morph, will always be found\n // Search for an element by ID within the document and pantry, and move it using moveBefore.\n const movedChild = document.getElementById(newChild.id) as Element\n\n // Removes an element from its ancestors' ID maps.\n // This is needed when an element is moved from the \"future\" via `moveBeforeId`.\n // Otherwise, its erstwhile ancestors could be mistakenly moved to the pantry rather than being deleted,\n // preventing their removal hooks from being called.\n let current = movedChild\n while ((current = current.parentNode as Element)) {\n const idSet = ctxIdMap.get(current)\n if (idSet) {\n idSet.delete(newChild.id)\n if (!idSet.size) {\n ctxIdMap.delete(current)\n }\n }\n }\n\n moveBefore(oldParent, movedChild, insertionPoint)\n morphNode(movedChild, newChild)\n insertionPoint = movedChild.nextSibling\n continue\n }\n\n // This performs the action of inserting a new node while handling situations where the node contains\n // elements with persistent IDs and possible state info we can still preserve by moving in and then morphing\n if (ctxIdMap.has(newChild)) {\n // node has children with IDs with possible state so create a dummy elt of same type and apply full morph algorithm\n const newEmptyChild = document.createElement(\n (newChild as Element).tagName,\n )\n oldParent.insertBefore(newEmptyChild, insertionPoint)\n morphNode(newEmptyChild, newChild)\n insertionPoint = newEmptyChild.nextSibling\n } else {\n // optimization: no id state to preserve so we can just insert a clone of the newChild and its descendants\n const newClonedChild = document.importNode(newChild, true) // importNode to not mutate newParent\n oldParent.insertBefore(newClonedChild, insertionPoint)\n insertionPoint = newClonedChild.nextSibling\n }\n }\n\n // remove any remaining old nodes that didn't match up with new content\n while (insertionPoint && insertionPoint !== endPoint) {\n const tempNode = insertionPoint\n insertionPoint = insertionPoint.nextSibling\n removeNode(tempNode)\n }\n}\n\n// Scans forward from the startPoint to the endPoint looking for a match for the node.\n// It looks for an id set match first, then a soft match.\n// We abort soft matching if we find two future soft matches, to reduce churn.\nconst findBestMatch = (\n node: Node,\n startPoint: Node | null,\n endPoint: Node | null,\n): Node | null => {\n let bestMatch: Node | null | undefined = null\n let nextSibling = node.nextSibling\n let siblingSoftMatchCount = 0\n let displaceMatchCount = 0\n\n // Max ID matches we are willing to displace in our search\n const nodeMatchCount = ctxIdMap.get(node)?.size || 0\n\n let cursor = startPoint\n while (cursor && cursor !== endPoint) {\n // soft matching is a prerequisite for id set matching\n if (isSoftMatch(cursor, node)) {\n let isIdSetMatch = false\n const oldSet = ctxIdMap.get(cursor)\n const newSet = ctxIdMap.get(node)\n\n if (newSet && oldSet) {\n for (const id of oldSet) {\n // a potential match is an id in the new and old nodes that\n // has not already been merged into the DOM\n // But the newNode content we call this on has not been\n // merged yet and we don't allow duplicate IDs so it is simple\n if (newSet.has(id)) {\n isIdSetMatch = true\n break\n }\n }\n }\n\n if (isIdSetMatch) {\n return cursor // found an id set match, we're done!\n }\n\n // we haven’t yet saved a soft match fallback\n // the current soft match will hard match something else in the future, leave it\n if (!bestMatch && !ctxIdMap.has(cursor)) {\n // optimization: if node can't id set match, we can just return the soft match immediately\n if (!nodeMatchCount) {\n return cursor\n }\n // save this as the fallback if we get through the loop without finding a hard match\n bestMatch = cursor\n }\n }\n\n // check for IDs we may be displaced when matching\n displaceMatchCount += ctxIdMap.get(cursor)?.size || 0\n if (displaceMatchCount > nodeMatchCount) {\n // if we are going to displace more IDs than the node contains then\n // we do not have a good candidate for an ID match, so return\n break\n }\n\n if (bestMatch === null && nextSibling && isSoftMatch(cursor, nextSibling)) {\n // The next new node has a soft match with this node, so\n // increment the count of future soft matches\n siblingSoftMatchCount++\n nextSibling = nextSibling.nextSibling\n\n // If there are two future soft matches, block soft matching for this node to allow\n // future siblings to soft match. This is to reduce churn in the DOM when an element\n // is prepended.\n if (siblingSoftMatchCount >= 2) {\n bestMatch = undefined\n }\n }\n\n cursor = cursor.nextSibling\n }\n\n return bestMatch || null\n}\n\n// ok to cast: if one is not element, `id` and `tagName` will be null and we'll just compare that.\nconst isSoftMatch = (oldNode: Node, newNode: Node): boolean =>\n oldNode.nodeType === newNode.nodeType &&\n (oldNode as Element).tagName === (newNode as Element).tagName &&\n // If oldElt has an `id` with possible state and it doesn’t match newElt.id then avoid morphing.\n // We'll still match an anonymous node with an IDed newElt, though, because if it got this far,\n // its not persistent, and new nodes can't have any hidden state.\n (!(oldNode as Element).id ||\n (oldNode as Element).id === (newNode as Element).id)\n\n// Gets rid of an unwanted DOM node; strategy depends on nature of its reuse:\n// - Persistent nodes will be moved to the pantry for later reuse\n// - Other nodes will have their hooks called, and then are removed\nconst removeNode = (node: Node): void => {\n // are we going to id set match this later?\n ctxIdMap.has(node)\n ? // skip callbacks and move to pantry\n moveBefore(ctxPantry, node, null)\n : // remove for realsies\n node.parentNode?.removeChild(node)\n}\n\n// Moves an element before another element within the same parent.\n// Uses the proposed `moveBefore` API if available (and working), otherwise falls back to `insertBefore`.\n// This is essentially a forward-compat wrapper.\nconst moveBefore: (parentNode: Node, node: Node, after: Node | null) => void =\n // @ts-expect-error\n removeNode.call.bind(ctxPantry.moveBefore ?? ctxPantry.insertBefore)\n\nconst aliasedPreserveAttr = aliasify('preserve-attr')\n\n// syncs the oldNode to the newNode, copying over all attributes and\n// inner element state from the newNode to the oldNode\nconst morphNode = (\n oldNode: Node, // root node to merge content into\n newNode: Node, // new content to merge\n): Node => {\n const type = newNode.nodeType\n\n // if is an element type, sync the attributes from the\n // new node into the new node\n if (type === 1 /* element type */) {\n const oldElt = oldNode as Element\n const newElt = newNode as Element\n if (\n oldElt.hasAttribute(aliasedIgnoreMorph) &&\n newElt.hasAttribute(aliasedIgnoreMorph)\n ) {\n return oldNode\n }\n\n // many bothans died to bring us this information:\n // https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js\n // https://github.com/choojs/nanomorph/blob/master/lib/morph.js#L113\n if (\n oldElt instanceof HTMLInputElement &&\n newElt instanceof HTMLInputElement &&\n newElt.type !== 'file'\n ) {\n // https://github.com/bigskysoftware/idiomorph/issues/27\n // | old input value | new input value | behaviour |\n // | --------------- | ---------------- | -------------------------------------- |\n // | `null` | `null` | preserve old input value |\n // | some value | the same value | preserve old input value |\n // | some value | `null` | set old input value to `\"\"` |\n // | `null` | some value | set old input value to new input value |\n // | some value | some other value | set old input value to new input value |\n if (newElt.getAttribute('value') !== oldElt.getAttribute('value')) {\n oldElt.value = newElt.getAttribute('value') ?? ''\n }\n } else if (\n oldElt instanceof HTMLTextAreaElement &&\n newElt instanceof HTMLTextAreaElement\n ) {\n if (newElt.value !== oldElt.value) {\n oldElt.value = newElt.value\n }\n if (oldElt.firstChild && oldElt.firstChild.nodeValue !== newElt.value) {\n oldElt.firstChild.nodeValue = newElt.value\n }\n }\n\n const preserveAttrs = (\n (newNode as HTMLElement).getAttribute(aliasedPreserveAttr) ?? ''\n ).split(' ')\n\n for (const { name, value } of newElt.attributes) {\n if (\n oldElt.getAttribute(name) !== value &&\n !preserveAttrs.includes(name)\n ) {\n oldElt.setAttribute(name, value)\n }\n }\n\n for (let i = oldElt.attributes.length - 1; i >= 0; i--) {\n const { name } = oldElt.attributes[i]!\n if (!newElt.hasAttribute(name) && !preserveAttrs.includes(name)) {\n oldElt.removeAttribute(name)\n }\n }\n\n if (!oldElt.isEqualNode(newElt)) {\n morphChildren(oldElt, newElt)\n }\n }\n\n if (type === 8 /* comment */ || type === 3 /* text */) {\n if (oldNode.nodeValue !== newNode.nodeValue) {\n oldNode.nodeValue = newNode.nodeValue\n }\n }\n\n return oldNode\n}\n\n// A bottom-up algorithm that populates a map of Element -> IdSet.\n// The ID set for a given element is the set of all IDs contained within its subtree.\n// As an optimization, we filter these IDs through the given list of persistent IDs,\n// because we don't need to bother considering IDed elements that won't be in the new content.\nconst populateIdMapWithTree = (\n root: Element | ShadowRoot | null,\n elements: Iterable,\n): void => {\n for (const elt of elements) {\n if (ctxPersistentIds.has(elt.id)) {\n let current: Element | null = elt\n // walk up the parent hierarchy of that element, adding the ID of element to the parent's ID set\n while (current && current !== root) {\n let idSet = ctxIdMap.get(current)\n // if the ID set doesn’t exist, create it and insert it in the map\n if (!idSet) {\n idSet = new Set()\n ctxIdMap.set(current, idSet)\n }\n idSet.add(elt.id)\n current = current.parentElement\n }\n }\n }\n}\n","// Icon: material-symbols:cloud-download\n// Slug: Patches elements into the DOM.\n// Description: Patches elements into the DOM.\n\nimport { watcher } from '@engine'\nimport type { WatcherContext } from '@engine/types'\nimport { morph } from '@engine/morph'\nimport { supportsViewTransitions } from '@utils/view-transitions'\n\ntype PatchElementsMode =\n | 'remove'\n | 'outer'\n | 'inner'\n | 'replace'\n | 'prepend'\n | 'append'\n | 'before'\n | 'after'\n\ntype PatchElementsArgs = {\n elements: string\n mode: PatchElementsMode\n selector: string\n useViewTransition: boolean\n}\n\nwatcher({\n name: 'datastar-patch-elements',\n apply(\n ctx,\n { elements = '', selector = '', mode = 'outer', useViewTransition },\n ) {\n switch (mode) {\n case 'remove':\n case 'outer':\n case 'inner':\n case 'replace':\n case 'prepend':\n case 'append':\n case 'before':\n case 'after':\n break\n default:\n throw ctx.error('PatchElementsInvalidMode', { mode })\n }\n\n if (!selector && mode !== 'outer' && mode !== 'replace') {\n throw ctx.error('PatchElementsExpectedSelector')\n }\n\n const args2: PatchElementsArgs = {\n mode,\n selector,\n elements,\n useViewTransition: useViewTransition?.trim() === 'true',\n }\n\n if (supportsViewTransitions && useViewTransition) {\n document.startViewTransition(() => onPatchElements(ctx, args2))\n } else {\n onPatchElements(ctx, args2)\n }\n },\n})\n\nconst onPatchElements = (\n { error }: WatcherContext,\n { elements, selector, mode }: PatchElementsArgs,\n) => {\n const elementsWithSvgsRemoved = elements.replace(\n /]*>|>)([\\s\\S]*?)<\\/svg>/gim,\n '',\n )\n const hasHtml = /<\\/html>/.test(elementsWithSvgsRemoved)\n const hasHead = /<\\/head>/.test(elementsWithSvgsRemoved)\n const hasBody = /<\\/body>/.test(elementsWithSvgsRemoved)\n\n const newDocument = new DOMParser().parseFromString(\n hasHtml || hasHead || hasBody\n ? elements\n : ``,\n 'text/html',\n )\n\n let newContent = document.createDocumentFragment()\n if (hasHtml) {\n newContent.appendChild(newDocument.documentElement)\n } else if (hasHead && hasBody) {\n newContent.appendChild(newDocument.head)\n newContent.appendChild(newDocument.body)\n } else if (hasHead) {\n newContent.appendChild(newDocument.head)\n } else if (hasBody) {\n newContent.appendChild(newDocument.body)\n } else {\n newContent = newDocument.querySelector('template')!.content\n }\n\n if (!selector && (mode === 'outer' || mode === 'replace')) {\n for (const child of newContent.children) {\n let target: Element\n if (child instanceof HTMLHtmlElement) {\n target = document.documentElement\n } else if (child instanceof HTMLBodyElement) {\n target = document.body\n } else if (child instanceof HTMLHeadElement) {\n target = document.head\n } else {\n target = document.getElementById(child.id)!\n if (!target) {\n console.warn(error('PatchElementsNoTargetsFound'), {\n element: { id: child.id },\n })\n continue\n }\n }\n\n applyToTargets(mode as PatchElementsMode, child, [target])\n }\n } else {\n const targets = document.querySelectorAll(selector)\n if (!targets.length) {\n console.warn(error('PatchElementsNoTargetsFound'), { selector })\n return\n }\n\n applyToTargets(mode as PatchElementsMode, newContent, targets)\n }\n}\n\nconst scripts = new WeakSet()\nfor (const script of document.querySelectorAll('script')) {\n scripts.add(script)\n}\n\nconst execute = (target: Element): void => {\n const elScripts =\n target instanceof HTMLScriptElement\n ? [target]\n : target.querySelectorAll('script')\n for (const old of elScripts) {\n if (!scripts.has(old)) {\n const script = document.createElement('script')\n for (const { name, value } of old.attributes) {\n script.setAttribute(name, value)\n }\n script.text = old.text\n old.replaceWith(script)\n scripts.add(script)\n }\n }\n}\n\nconst applyPatchMode = (\n targets: Iterable,\n element: DocumentFragment | Element,\n action: string,\n) => {\n for (const target of targets) {\n const cloned = element.cloneNode(true) as Element\n execute(cloned)\n // @ts-expect-error\n target[action](cloned)\n }\n}\n\nconst applyToTargets = (\n mode: PatchElementsMode,\n element: DocumentFragment | Element,\n targets: Iterable,\n) => {\n switch (mode) {\n case 'remove':\n for (const target of targets) {\n target.remove()\n }\n break\n case 'outer':\n case 'inner':\n for (const target of targets) {\n morph(target, element.cloneNode(true) as Element, mode)\n execute(target)\n }\n break\n case 'replace':\n applyPatchMode(targets, element, 'replaceWith')\n break\n case 'prepend':\n case 'append':\n case 'before':\n case 'after':\n applyPatchMode(targets, element, mode)\n }\n}\n","// Icon: material-symbols:settings-input-antenna\n// Slug: Patches signals.\n// Description: Patches signals.\n\nimport { watcher } from '@engine'\nimport { mergePatch } from '@engine/signals'\nimport { jsStrToObject } from '@utils/text'\n\nwatcher({\n name: 'datastar-patch-signals',\n apply({ error }, { signals, onlyIfMissing }) {\n if (signals) {\n const ifMissing = onlyIfMissing?.trim() === 'true'\n mergePatch(jsStrToObject(signals), { ifMissing })\n } else {\n throw error('PatchSignalsExpectedSignals')\n }\n },\n})\n","// Icon: material-symbols:send\n// Slug: Dispatches custom browser events for component communication.\n// Description: Dispatches CustomEvents globally or to specific elements, enabling event-driven reactive patterns like Livewire.\n\nimport { action } from '@engine'\n\n/**\n * Dispatch Action - Event Communication System\n *\n * Mirrors Livewire's dispatch system, enabling both frontend and backend\n * to trigger custom events for component communication.\n *\n * Usage:\n * @dispatch('event-name') // Global dispatch\n * @dispatch('event-name', {data: 'value'}) // With data\n * @dispatch('event-name', {id: 123}, {selector: '#target'}) // Targeted\n *\n * Options:\n * - selector: CSS selector for targeted dispatch (multiple elements supported)\n * - window: Force window dispatch (default: true if no selector)\n * - bubbles: Event bubbles (default: true)\n * - cancelable: Event can be canceled (default: true)\n * - composed: Event composes through shadow DOM (default: true)\n */\naction({\n name: 'dispatch',\n apply: (\n { el },\n eventName: string,\n data?: any,\n options?: {\n selector?: string\n window?: boolean\n bubbles?: boolean\n cancelable?: boolean\n composed?: boolean\n }\n ) => {\n // Validate event name\n if (!eventName || typeof eventName !== 'string') {\n console.error(\n '[Hyper Dispatch] Invalid event name. Must be a non-empty string.',\n { eventName }\n )\n return\n }\n\n // Default options\n const opts = {\n selector: options?.selector,\n window: options?.window ?? (!options?.selector), // Default to window if no selector\n bubbles: options?.bubbles ?? true,\n cancelable: options?.cancelable ?? true,\n composed: options?.composed ?? true,\n }\n\n // Create CustomEvent\n const event = new CustomEvent(eventName, {\n detail: data || {},\n bubbles: opts.bubbles,\n cancelable: opts.cancelable,\n composed: opts.composed,\n })\n\n // Targeted dispatch\n if (opts.selector) {\n const targets = document.querySelectorAll(opts.selector)\n\n if (targets.length === 0) {\n console.warn(\n `[Hyper Dispatch] No elements found for selector: ${opts.selector}`,\n { eventName, selector: opts.selector }\n )\n return\n }\n\n targets.forEach((target) => {\n target.dispatchEvent(new CustomEvent(eventName, {\n detail: data || {},\n bubbles: opts.bubbles,\n cancelable: opts.cancelable,\n composed: opts.composed,\n }))\n })\n\n return\n }\n\n // Global dispatch (window)\n if (opts.window) {\n window.dispatchEvent(event)\n return\n }\n\n // Element dispatch (fallback to current element)\n el.dispatchEvent(event)\n },\n})\n","// Icon: material-symbols:attachment\n// Slug: Converts file sources to valid HTML src/href URLs with smart fallbacks.\n// Description: Handles base64 arrays from file binding and provides fallbacks. File paths are auto-converted by Laravel backend.\n\nimport { action } from '@engine'\nimport type { ActionContext } from '@engine/types'\n\n/**\n * FileUrl Action - Simplified and Reliable\n *\n * Now much simpler since Laravel handles path-to-URL conversion.\n * Frontend only needs to handle:\n * - Base64 arrays from Datastar file inputs\n * - URLs (already converted by Laravel)\n * - Fallbacks for missing/empty data\n *\n * Usage Examples:\n * @fileUrl($profilePicture) // Auto-detect with fallback\n * @fileUrl($document, {fallback: '/default.pdf'}) // Custom fallback\n * @fileUrl('/storage/file.jpg') // URL passthrough\n * @fileUrl($avatar, {defaultMime: 'image/png'}) // Custom MIME for base64\n */\naction({\n name: 'fileUrl',\n apply: (\n _ctx: ActionContext,\n fileSource: any,\n options?: {\n fallback?: string\n defaultMime?: string\n mimeSignal?: string // Name of companion MIME signal\n }\n ): string => {\n const opts = options || {}\n const fallback = opts.fallback || ''\n const defaultMime = opts.defaultMime || 'application/octet-stream'\n const mimeSignal = opts.mimeSignal\n\n // Handle null, undefined, or empty values\n if (fileSource == null) {\n return fallback\n }\n\n // Handle arrays (from Datastar file binding)\n if (Array.isArray(fileSource)) {\n // Empty array means no file selected\n if (fileSource.length === 0) {\n return fallback\n }\n\n const firstItem = fileSource[0]\n\n // RC6 Format: Array of {name, contents, mime} objects\n if (typeof firstItem === 'object' && firstItem !== null && 'contents' in firstItem) {\n const { contents, mime } = firstItem as { contents: string; mime: string; name?: string }\n\n // Validate contents\n if (!contents || typeof contents !== 'string') {\n return fallback\n }\n\n // Use MIME from object (RC6 provides it automatically)\n const mimeType = mime || defaultMime\n\n // Construct data URL\n return `data:${mimeType};base64,${contents}`\n }\n\n // Legacy Format: Array of base64 strings (for backward compatibility)\n const base64Content = firstItem\n if (!base64Content || typeof base64Content !== 'string') {\n return fallback\n }\n\n // Determine MIME type\n let mimeType = defaultMime\n\n // Try to get MIME from companion signal if specified\n if (mimeSignal && typeof window !== 'undefined') {\n const mimeArray = ((window as any).$ || {})[mimeSignal]\n if (Array.isArray(mimeArray) && mimeArray.length > 0) {\n mimeType = mimeArray[0] || defaultMime\n }\n }\n\n // Construct data URL\n return `data:${mimeType};base64,${base64Content}`\n }\n\n // Handle strings (URLs, paths, data URLs)\n if (typeof fileSource === 'string') {\n const trimmed = fileSource.trim()\n\n // Empty string\n if (!trimmed) {\n return fallback\n }\n\n // Laravel should have converted file paths to proper URLs\n // We just return whatever string we got\n return trimmed\n }\n\n // Handle unexpected types gracefully\n if (typeof fileSource === 'object') {\n console.warn(\n 'FileUrl action received unexpected object:',\n fileSource\n )\n }\n\n return fallback\n },\n})\n","// Icon: material-symbols:arrow-back\n// Slug: Navigate back with fallback support\n// Description: Convenience action for going back to previous page or fallback URL\n\nimport { action, actions } from '@engine'\nimport type { ActionContext } from '@engine/types'\n\n/**\n * Back Action - Navigate to Previous Page\n *\n * Convenience action for navigating back in browser history.\n * Falls back to a specified URL if no referrer available.\n *\n * USAGE EXAMPLES:\n *\n * @back() // Go back or to '/'\n * @back('/dashboard') // Go back or to '/dashboard'\n * @back('/home', 'back') // With custom navigation key\n * @back('/home', 'back', {merge: true}) // With options\n */\naction({\n name: 'back',\n apply: async (\n ctx: ActionContext,\n fallback: string = '/',\n key: string = 'back',\n options: Record = {}\n ): Promise => {\n // Determine the back URL\n let backUrl = fallback\n\n // Try to get the previous URL from browser history\n if (\n document.referrer &&\n document.referrer !== window.location.href\n ) {\n try {\n const referrerUrl = new URL(document.referrer)\n const currentUrl = new URL(window.location.href)\n\n // Only use referrer if it's from the same origin\n if (referrerUrl.origin === currentUrl.origin) {\n backUrl = document.referrer\n }\n } catch (error) {\n console.warn('Error parsing referrer URL:', error)\n // Use fallback\n }\n }\n\n // Call @navigate action\n const navigateAction = actions.navigate\n\n if (navigateAction) {\n await navigateAction(ctx, backUrl, key, {\n merge: true, // Default to merge for back navigation\n ...options,\n })\n } else {\n console.error('Navigate action not found. Falling back to location.href')\n window.location.href = backUrl\n }\n },\n})\n\n/**\n * EXAMPLES FOR DOCUMENTATION:\n *\n * // Basic usage - go back or to home\n * \n *\n * // With custom fallback\n * \n *\n * // With navigation key (for targeted updates)\n * \n *\n * // With options\n * \n */\n","// Icon: material-symbols:refresh\n// Slug: Refresh current page reactively\n// Description: Convenience action for refreshing the current page using Datastar navigation\n\nimport { action, actions } from '@engine'\nimport type { ActionContext } from '@engine/types'\n\n/**\n * Refresh Action - Reactive Page Refresh\n *\n * Convenience action for refreshing the current page using Datastar navigation\n * (not a full page reload). Preserves reactive state and uses SSE updates.\n *\n * USAGE EXAMPLES:\n *\n * @refresh() // Refresh current page (merge by default)\n * @refresh('refresh') // With custom navigation key\n * @refresh('refresh', {merge: false}) // Refresh without merging query params\n */\naction({\n name: 'refresh',\n apply: async (\n ctx: ActionContext,\n key: string = 'refresh',\n options: Record = {}\n ): Promise => {\n // Get current URL\n const currentUrl = window.location.href\n\n // Call @navigate action with current URL\n const navigateAction = actions.navigate\n\n if (navigateAction) {\n await navigateAction(ctx, currentUrl, key, {\n merge: true, // Default to merge for refresh\n replace: true, // Replace history entry (don't add new)\n ...options,\n })\n } else {\n console.error('Navigate action not found. Falling back to location.reload')\n window.location.reload()\n }\n },\n})\n\n/**\n * EXAMPLES FOR DOCUMENTATION:\n *\n * // Basic refresh - updates current page reactively\n * \n *\n * // With custom navigation key (for targeted updates)\n * \n *\n * // Clean refresh (no query param merging)\n * \n *\n * // Polling pattern\n *
\n * Auto-refreshing content\n *
\n */\n","// Icon: material-symbols:restart-alt\n// Slug: Force full page reload\n// Description: Hard refresh that discards all client state and reloads the page completely\n\nimport { action } from '@engine'\nimport type { ActionContext } from '@engine/types'\n\n/**\n * Reload Action - Hard Page Refresh\n *\n * Forces a complete page reload, discarding all client-side state.\n * This breaks out of the reactive mode and performs a traditional full page load.\n *\n * Use when you need to completely reset the application state or when\n * reactive updates are insufficient (e.g., after major configuration changes).\n *\n * USAGE EXAMPLES:\n *\n * @reload() // Force full page reload\n */\naction({\n name: 'reload',\n apply: (_ctx: ActionContext): void => {\n // Perform hard refresh\n // This is equivalent to the browser's refresh button (F5)\n window.location.reload()\n },\n})\n\n/**\n * EXAMPLES FOR DOCUMENTATION:\n *\n * // Full page reload (discards all state)\n * \n *\n * // Use after settings change\n * \n *\n * // Emergency reset\n * \n */\n","// Icon: material-symbols:error-outline\n// Slug: Shows validation error for a field.\n// Description: Displays the first validation error message for a specified field from the errors signal.\n\nimport { attribute } from '@engine'\nimport { effect, computed, getPath, mergePatch } from '@engine/signals'\n\nattribute({\n name: 'error',\n requirement: {\n key: 'denied',\n value: 'must',\n },\n apply({ el, value }) {\n const fieldName = value.trim()\n\n // Ensure the 'errors' signal exists and is initialized as an empty object\n // This works like the signals plugin - creates the signal if missing\n mergePatch({ errors: {} }, { ifMissing: true })\n\n // Create a computed that reactively reads the error for this field\n const errorComputed = computed(() => {\n const errors = getPath('errors')\n if (errors && errors[fieldName]) {\n const fieldErrors = errors[fieldName]\n if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {\n return fieldErrors[0]\n } else if (typeof fieldErrors === 'string') {\n return fieldErrors\n }\n }\n return null\n })\n\n // Effect that updates the DOM when the error changes\n const cleanup = effect(() => {\n const errorMessage = errorComputed()\n\n if (errorMessage) {\n el.style.removeProperty('display')\n el.textContent = errorMessage\n } else {\n el.style.setProperty('display', 'none')\n el.textContent = ''\n }\n })\n\n return cleanup\n },\n})\n","// Icon: material-symbols:repeat\n// Slug: Iterates over an array or object with efficient diffing.\n// Description: Production-ready loop directive with keyed diffing, zero signal pollution, and full Datastar reactivity integration.\n\nimport { attribute, apply } from '@engine'\nimport { effect, getPath, mergePatch, root, beginBatch, endBatch, startPeeking, stopPeeking } from '@engine/signals'\nimport type { HTMLOrSVG } from '@engine/types'\n\n/**\n * ARCHITECTURE: Clean Loop Implementation\n *\n * PRINCIPLES:\n * 1. NO SIGNAL POLLUTION - Only source signals exist (e.g., $users)\n * 2. Direct mapping: iterator \"user\" → source path \"users.0\", \"users.1\", etc.\n * 3. Datastar's Proxy-based reactivity handles all property mutations\n * 4. Keyed diffing reuses DOM elements, retransforming indices on position changes\n * 5. Efficient change detection: no-change, simple-add, simple-remove, complex\n */\n\ninterface IterationState {\n prevKeys: any[];\n lookup: Map;\n template: HTMLTemplateElement;\n templateContent: DocumentFragment;\n iteratorNames: ParsedExpression;\n keyExpression: string | null;\n effectCleanup: (() => void) | null;\n sourceSignalPath: string;\n isNormalized: boolean;\n iterationId: string;\n}\n\ninterface LoopElement {\n el: HTMLOrSVG;\n key: any;\n index: number;\n}\n\ninterface ParsedExpression {\n item: string;\n index?: string;\n collection?: string;\n items: string;\n}\n\nconst iterationStates = new WeakMap()\n\n// Helper function to peek without tracking\nconst peek = (fn: () => void) => {\n startPeeking()\n try {\n fn()\n } finally {\n stopPeeking()\n }\n}\n\nattribute({\n name: 'for',\n requirement: {\n key: 'denied',\n value: 'must',\n },\n apply(ctx) {\n const { el, value, mods, error } = ctx\n\n // Validate template element\n if (!(el instanceof HTMLTemplateElement)) {\n throw error('ForMustBeOnTemplate', {\n message: 'data-for must be used on