Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ Imperative helpers for dynamic code paths: `dmSet(...)`, `dmSub(...)`, `dmScan(.

CSS custom property binding: any `style.*` target that doesn't match a known CSS prop falls through to `style.setProperty('--<kebab>', val)`.

For explicit custom property names, write the leading `--` in the dKey:

```html
<div data-m-ex:.style.--accent-color@accent></div>
<div data-m-ex:.style.--gap-2@theme.gap="val * 2"></div>
```

The leading `--` is preserved verbatim and not mangled by camel/kebab conversion.

## Read modifiers (pipeline)

Read mods compose left-to-right. The first selector picks a value source; transforms map it; `^` extracts a sub-path or array index.
Expand Down Expand Up @@ -189,6 +198,47 @@ Drive host props with normal `data-m-ex`:
<mx-uplot data-m-ex:chart-last^merge@.point="{ from: 'chart-1', ...val }"></mx-uplot>
```

### Shadow DOM and per-host state

Opt a custom element into a shadow root with a `^dom` mod on
`dmWc`. `^dom` attaches an open shadow root, `^dom.closed` attaches
a closed one. In both cases the host's `data-m-*` attrs are still
wired normally.

```js
dmWc('my-card', '<style>:host{display:block}</style><article><slot></slot></article>',
undefined, [{ root: 'dom', path: ['open'] }])
```

Slots project automatically: in light DOM, child elements fall into
matching `<slot>` elements; in shadow mode, light-DOM children of
the host are projected into the shadow slots by the browser.

Use `:_wc` as a signal root to keep state on the host instead of
the page-level store. Each host gets its own `_wc`; siblings do not
share it.

```html
<my-counter data-m-ex:.text-content@:_wc.count>
<button data-m-ex:_wc.count^dec@.click>-1</button>
<button data-m-ex:_wc.count^inc@.click>+1</button>
</my-counter>
```

The helpers `dmGetHost(host, path)` and `dmSetHost(host, path, val)`
read and write per-host state. `dmSet` refuses `:_wc` targets to
keep page-level and per-host signals distinct.

CSS custom property dKeys support an explicit `--` prefix:

```html
<div data-m-ex:.style.--accent-color@accent></div>
<div data-m-ex:.style.--gap-2@theme.gap="val * 2"></div>
```

The leading `--` is preserved verbatim and not mangled by
camel/kebab conversion.

## Actions and SSE

### Actions
Expand Down
7 changes: 6 additions & 1 deletion dm-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,17 @@
title: opts.title,
note: opts.note,
})
// Shadow-mode panel CSS — selectors are naturally scoped to the shadow root,
// so the legacy `${name}` interpolations are no longer needed. ~600 chars saved.
const panelShadowCss = () => `details{position:relative;display:block}button{all:unset;box-sizing:border-box}summary{display:grid;list-style:none;cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation}summary::-webkit-details-marker{display:none}.fab{display:grid;place-items:center;inline-size:34px;block-size:34px;border-radius:999px;background:#0f172acc;color:#fff;box-shadow:0 8px 24px #0003;backdrop-filter:blur(10px)}details[open] .fab{background:#2563ebd9}.panel{position:absolute;top:42px;right:0;inline-size:min(22rem,calc(100vw - 24px));max-block-size:min(78vh,48rem);overflow:auto;padding:10px;border:1px solid #cbd5e1;border-radius:14px;background:#fffc;color:#0f172a;box-shadow:0 16px 40px #00000026;backdrop-filter:blur(14px)}h2,h3,p{margin:0}h2,h3{font-size:12px}p{margin-top:4px;color:#475569}.tools{display:flex;gap:6px;margin-top:8px}.tools button{padding:5px 7px;border-radius:8px;background:#e2e8f0;color:#0f172a;cursor:pointer}.group{margin-top:10px;padding-top:2px}.group h3{color:#475569;text-transform:uppercase;letter-spacing:.04em}.defs{display:grid;gap:6px;margin:6px 0 0}div.r{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:6px 8px;align-items:center;padding-top:6px;border-top:1px solid #e2e8f0}dt,dd{min-inline-size:0;margin:0}code{color:#475569}.tone{display:grid;gap:6px}.tone-row{display:grid;grid-template-columns:2.25rem minmax(0,1fr);gap:6px;align-items:center}input{inline-size:100%;font:inherit}input[type='color']{inline-size:2.25rem;block-size:2rem;padding:0;border:1px solid #cbd5e1;border-radius:8px;background:#fff}input[type='text']{padding:6px 7px;border:1px solid #cbd5e1;border-radius:8px;background:#fff}input[type='range']{accent-color:#2563eb}input:focus-visible,.tools button:focus-visible,summary:focus-visible{outline:2px solid #2563eb;outline-offset:2px}@media(max-width:40rem){details{display:grid;justify-items:end}.panel{position:fixed;right:8px;bottom:50px;left:8px;top:auto;inline-size:auto;max-block-size:min(70dvh,32rem)}}`
const panel = (name = defaults.panelName, defs = dmStyle.defs, opts = {}) => {
const signal = opts.signal || defaults.signal, open = opts.open || defaults.open, help = opts.help || defaults.help, sigJs = pathJs(signal), helpAttr = help ? ` data-m-ex:.title@${help}` : '', parts = partitionDefs(defs)
dmStyle.panels[name] = defs
const range = (d, path = signal + '.' + kebab(d.key)) => `<div class="r"><dt><code title="${d.label}">${d.css}</code></dt><dd><input type="range" min="${d.min}" max="${d.max}" step="${d.step}" aria-label="${d.label}" data-m-ex:.value@${path} data-m-ex:${path}@.input^num data-m-ex:.title@${path}="'${d.label}: ' + val"></dd></div>`
const tone = (d, path = signal + '.' + kebab(d.key), colorLabel = d.label + ' color') => `<div class="r"><dt><code title="${d.label}">${d.css}</code></dt><dd><div class="tone"><div class="tone-row"><input type="color" aria-label="${colorLabel}" data-m-ex:.value@${path}="dmStyle.oklchToHex(val) || '#ffffff'" data-m-ex:${path}@.input="dmStyle.hexToOklch(val)" data-m-ex:.style.background-color@${path}="dmStyle.oklchToHex(val) || '#ffffff'" data-m-ex:.style.border-color@${path}="dmStyle.oklchToHex(val) || '#cbd5e1'"${helpAttr}><input type="text" aria-label="${d.label}" data-m-ex:.value@${path} data-m-ex:${path}@.input${helpAttr}></div></div></dd></div>`
return dmWc(name, `<style>${panelCss(name)}</style><details data-m-ex@.^rw@${open}><summary class="fab" aria-controls="${name}-panel">◐</summary><section class="panel" id="${name}-panel"><h2>${opts.title || 'Style props'}</h2><p>${opts.note || 'Compact live tokens for this page. Copy styles writes the current values as a :root block.'}</p><div class="tools"><button type="button" data-m-ex:${signal}@.click="(dmStyle.copy(${sigJs}, dmStyle.panels['${name}']), ${sigJs})">copy styles</button><button type="button" data-m-ex:${signal}@.click="dmStyle.promptImport(${sigJs}, dmStyle.panels['${name}'])">import</button><button type="button" data-m-ex:${signal}@.click="dmStyle.initVals(dmStyle.panels['${name}'])">reset</button></div><section class="group"><h3>Layout</h3><div class="defs">${parts.range.map((d) => range(d)).join('')}</div></section><section class="group"><h3>Tones</h3><div class="defs">${parts.tone.map((d) => tone(d)).join('')}</div></section></section></details>`)
// ^dom.open isolates the panel's CSS via shadow root, and :_wc keeps the
// open state on the host instead of polluting the page-level signal store.
return dmWc(name, `<style>${panelShadowCss()}</style><details data-m-ex@.^rw@${open}><summary class="fab" aria-controls="${name}-panel">◐</summary><section class="panel" id="${name}-panel"><h2>${opts.title || 'Style props'}</h2><p>${opts.note || 'Compact live tokens for this page. Copy styles writes the current values as a :root block.'}</p><div class="tools"><button type="button" data-m-ex:${signal}@.click="(dmStyle.copy(${sigJs}, dmStyle.panels['${name}']), ${sigJs})">copy styles</button><button type="button" data-m-ex:${signal}@.click="dmStyle.promptImport(${sigJs}, dmStyle.panels['${name}'])">import</button><button type="button" data-m-ex:${signal}@.click="dmStyle.initVals(dmStyle.panels['${name}'])">reset</button></div><section class="group"><h3>Layout</h3><div class="defs">${parts.range.map((d) => range(d)).join('')}</div></section><section class="group"><h3>Tones</h3><div class="defs">${parts.tone.map((d) => tone(d)).join('')}</div></section></section></details>`, undefined, [{ root: 'dom', path: ['open'] }])
}
const findPanelEl = (root, name) => root && root.querySelector(name)
const pinExpr = (panelName) => `dmStyle.applyVars(el._dmStyleRoot,dmStyle.reconcileVars(el._dmStyleRoot,val,dmStyle.panels['${panelName}']))`
Expand Down
Loading
Loading