Releases: stamat/code-preview-element
Release list
v3.0.4
Fixed
-
A preview no longer shakes when the sample is about as tall as the height cap. The
wrapper scrolls whatever is pastmax-height: 70vh, and a scrollbar arriving is a width
change, which is what the element refits on. A sample whose height follows its width —
anything with anaspect-ratio, a<video>above all — shortened by the scrollbar's
width, dropped back under the cap, lost the scrollbar, widened, grew past the cap again,
and went round for as long as it was on screen. Easiest to hit zoomed in or on a narrow
window, where a fixed-ratio sample sits nearest the cap..code-preview-viewportnow
carriesscrollbar-width: none: it still scrolls, and the scrollbar no longer costs it
any width to appear.The cost is stated rather than hidden. A sample past the cap now shows no scrollbar to
say there is more — the cut of its own content is the only sign — though wheel,
trackpad, keyboard and find-in-page all still reach it. Reserving a stable gutter was the
other way to hold the width still, and it charges an empty strip down the right of every
preview on the platforms that have scrollbars, including the great majority that never
reach the cap. On an engine withoutscrollbar-widththe scrollbar returns and so does
the shake, which is where this started rather than anywhere worse. Put it back for
yourself withcode-preview .code-preview-viewport { scrollbar-width: auto; }.
Full Changelog: v3.0.3...v3.0.4
v3.0.3
v3.0.2
Fixed
- A long sample line no longer reflows the block at upgrade. CodeJar writes
white-space: pre-wrapandoverflow-wrap: break-wordinline on the block it takes
over, so a sample whose longest line overflowed the column scrolled sideways before
upgrade and wrapped after it — one extra visual line per overflowing line, a layout
shift the height reservation could not see coming. Editable blocks now wear both from
the start, the same treatment 3.0.1 gave the editor's padding. Same honest edge too: a
pane read-only by fence word or pane type wraps for the wait and unwraps when the
editor declines it. The sibling-pane hide crosses the same line now — keyed off
.is-tabbedinstead of:defined, so the later fences cannot stand on the page for
the one frame between upgrade and the tab strip's collapse taking over. A stray
non-fence<pre>inside the element stays hidden for good where it used to reappear
at upgrade; the panes contract never included one.
Full Changelog: v3.0.1...v3.0.2
v3.0.1
Fixed
-
The console no longer opens a seam under the code block. The strip took
0.25rem
of padding on all four sides, and the top quarter of that landed exactly on the join
the 3.0.0 layout works to hide — no top border, shared corners, two boxes meant to read
as one — so a gap appeared right where the block was supposed to run into the strip. It
ispadding: 0 0 0.25remnow; the space below the last line stays.CSS an author may be targeting:
code-preview .code-preview-console. A sheet that
restores symmetric padding gets the seam back. The asymmetry is now stated in the
comment above the rule so it does not read as an oversight worth tidying. -
Upgrading no longer shifts the page twice while the manifest loads. The pre-upgrade
reservation counted the code strip and the options panel, then upgrade dropped it whole
— but withmanifestboth boxes wait on a fetch, so the page rose by their height and
came back down when they landed. Measured on a consumer's docs: upgrade at 224ms,
arrival at 274ms, a layout shift each way, on every load. Three CSS rules now carry the
reservation across the gap: the strip's row is held as top margin on the first pane
until the tablist exists, thetab="options"panel's box is held by an::afteruntil
the panel exists, and an editable block wears--code-preview-hint-spacefrom the
start instead of growing by it when the editor attaches. The costs run the honest way:
a page that names amanifestand never loads the options bundle keeps the held strip
row for good, and a pane read-only by fence word wears the editor padding for the wait
and gives it back.
Full Changelog: v3.0.0...v3.0.1
v3.0.0
Changed
-
The console strip moved from under the preview to under the code block, and the error
banner has folded into it. Two boxes said the same kind of thing in two places, and
neither was where the reader was looking: the lines are logged by the js in the pane, so
they belong against the pane, the way a devtools console sits under the source above it.
The block gives up its bottom corners to the strip and the two read as one box.An uncaught throw is now a line in that strip rather than a banner below it — in sequence
with everything the sample logged on the way there, which is what a broken sample is read
from. It keeps everything the banner had: tinted from--code-preview-danger, a ⚠ glyph,
androle="alert", so it is still announced assertively out of a region (role="log")
that is otherwise polite. A loggedconsole.erroris red but is not an alert — the sample
asked for that one.no-consoleno longer silences errors. It is the answer for a demo that logs on every
frame, and no sample is asked to swallow the error that stopped it; the strip is built for
that one line if it has to be.DOM the element produces:
div.code-preview-console[role="log"]is now the last child
of the host, under every pane.p.code-preview-erroranddata-erroron the host are
gone — an uncaught throw is ap.code-preview-console-line.is-error[role="alert"]in the
strip.--code-preview-tailis new and written by the element: the strip's measured
height, which the Edit and Run buttons in the block's corner are lifted clear by.CSS an author may be targeting:
code-preview .code-preview-errorand
code-preview[data-error]style nothing now. The corner radii are keyed off
code-preview:has(> .code-preview-console:not([hidden]))instead.Contents of the preview iframe: a second inline
<script>in head, beside the console
hook, listening forerrorand forwarding it as acode-preview-errorCustomEvent on the
iframe. It is written whether or notno-consoleis set.
Fixed
-
A sample that threw during its first parse reported nothing. The js pane is inlined as
atype="module", so a top-level throw is over before the frame'sloadevent — which is
where the host attached itserrorlistener, so the one error the reader who just broke
their edit needed to see was the one error never shown. The capture is now an inline
script first in the frame's head, armed before anything can run. A sample that brings its
own whole document owns its head and gets no hook, so that case is still heard on the
frame's window as before. -
Safari: a preview could vibrate by one pixel, forever. WebKit lays an iframe's
innards out a hair differently against each integral height it is given, so the height
measurewrote could move the next measurement across theMath.ceilboundary — and
the resize report that write fired triggered the next measure, so the two heights took
turns indefinitely. Two defenses, either sufficient: a height-only resize report from
the wrapper is now recognized as our own write coming back and does not re-measure
(the wrapper is watched for its width, which is what an emulated viewport scales
against), and the frame height write carries one pixel of dead-band — a real change is
bigger, and a preview one pixel short is invisible where a vibrating one is not.
Chrome and Firefox quantized the two measurements identically and never flapped;
nothing changes there.
Full Changelog: v2.0.0...v3.0.0
v2.0.0
Added
-
A sample can be several fences — markup, its css, its js — and each becomes a tab.
Until now the element took one<pre><code>, so a demo that needed a stylesheet or a
script had to bury both inside the html as<style>and<script>: unreadable as a
sample, uneditable as css, and impossible to copy the interesting half out of. Write
them as the separate blocks they are and the element pairs them up:<code-preview css="dist/lib.css" js="dist/lib.js"> <pre><code class="language-html"><aside class="drawer">…</aside></code></pre> <pre><code class="language-css">.drawer { transition: transform 0.2s; }</code></pre> <pre><code class="language-js">document.querySelector(".drawer");</code></pre> </code-preview>
The languages are read off the
language-*class a site generator already writes, so
there is no new markup vocabulary — three fences in the markdown, three tabs on the page.
Anything the frame cannot run (ascssblock beside the css it compiles to) still gets a
tab, read-only. So does any fence beside a sample that is a whole document — it owns its
head and body, so there is nowhere in it to write the pane — and a second fence in a
language that already has one, under a numbered tab (CSS2), since the frame is built
from the first.The js pane is inlined as
<script type="module">, and that is not about scoping. A
classic inline script runs while the parser is still going, before the deferred bundles
injshave defined anything — so a sample that writes a property on a custom element
gets one that has not upgraded, and the write installs an own property that shadows the
accessor the class is about to bring. It fails silently and for good. A module is
deferred, and deferred scripts run in document order, so the pane runs after every url in
js.A css edit no longer reloads the frame. The pane is one
<style>in a head this
element built, so an edit is a write to its text: nothing reparses, and the sample keeps
the state a rebuild would cost it — a script's variables, an open menu, the control the
reader had focused. Markup edits patch or reload exactly as before, and a js pane always
reloads, for the reasonjsurls always have.DOM it produces: each pane's box gets
role="tabpanel",data-pane="<name>"and
hidden="until-found"while it is not showing; the strip is arole="tablist"of
.code-preview-tabbuttons in the existing.code-preview-bar. The markup pane is named
code, nothtml— sotab="code"still means the sample, and every page already using
this element is untouched. One fence still produces exactly what it did: no strip, no
role, nohidden, byte for byte.CSS an author may be targeting: the rule that collapsed the hidden pane was
code-preview.is-tabbed[tab="options"] > :is(pre, .code-wrap)and is now keyed off
[data-pane][hidden], which is one rule for two panes or for five. The editor's keyboard
hint is hidden bycode-preview.is-tabbed:not(.is-code-pane)rather than by naming the
options tab. -
no-editcan lock some panes and not others. It was all-or-nothing, which for a
three-fence sample meant choosing between an editable stylesheet you did not want touched
and no editing at all. Two ways to say it, and they add up:<code-preview no-edit="css js">…</code-preview>
<pre no-edit><code class="language-css">.drawer { transition: transform 0.2s; }</code></pre>
Panes are named by what their tab says (
html,css,js) or by the pane's own name
(codefor the markup one). In markdown the per-fence form needs no new vocabulary if
your generator turns a bare word in the info string into a class on the block —```css no-edit— since that class is what the element reads. Bareno-editis unchanged:
the whole sample stays read-only.CSS an author may be targeting:
code-preview.is-code-panenow means the pane
showing has an editor in it, not merely code — a pane locked by either form no longer
gets the class, so the buttons and the keyboard hint are not left on a block nobody can
type into.
Unchanged for a sample that locks nothing. -
Editing is opt-in: a block takes no keystrokes until you open it. A block that can be
edited is not editable at rest — nocontenteditable, nothing announced as a text field.
An Edit button in its bottom-left corner opens it, Enter on the focused
block does the same, and Esc or a second press on the button closes it again.The reason is Tab. Tab has to indent inside a code editor, so it cannot also be the way
out — which makes an always-editable block a keyboard trap sitting in a docs page, hit by
every reader tabbing past a sample they never meant to type into, with the way out being
a key they are told about only once they are already stuck. Opting in removes the trap
rather than signposting it, and the Esc advice is then owed only to someone who
asked to be there. The block keeps a tab stop at rest so that Enter has
somewhere to be pressed: a keyboard user is offered the editor where they already are.Closing the editor is also a second way to apply a js edit, alongside Run.
DOM the element produces: a
div.code-preview-actions(role="group") as a child of
the host — not of the strip, and not of the code block — holding
button.code-preview-action:.code-preview-edit(witharia-pressed) and
.code-preview-run. Each holds anaria-hiddenglyph and a<span>with the word for it
— Edit, Run — and that word is the accessible name, so neither carries an
aria-labelor atitle. The<pre>of an editable pane now carriestabindex="0"and an
aria-describedbypointing atp.code-preview-hint;role="textbox",
aria-multiline,aria-keyshortcutsandcontenteditableare written on the<code>
only while the editor is open, and removed when it closes.Both buttons are on by default and neither is built on a sample with no editor in it.
no-actionstakes them away, spelled the wayno-editis: bare for both, or naming the
one to drop (no-actions="run"). Dropping Run from a js sample leaves
Ctrl/Cmd + Enter and closing the editor as the ways to
apply an edit — both are keyed to what the sample is, not to whether the button exists.The Esc hint is not drawn under
(hover: none) and (pointer: coarse): it names
a key a touch device does not have, and the trap it warns about is a keyboard trap. Its
aria-describedbyis unaffected.CSS an author may be targeting:
code-preview.is-editingis new and says the editor
is open..is-editablestill says the element has one to open. The--code-preview-hint-space
bottom padding on an editable block is now the room the buttons sit in as well as the hint. -
No copy button of the element's own, and a docs theme's is left alone. Copying a code
block is something a docs theme already does, on every block on the page rather than only
on the samples. The element's own copy button is gone and so is the rule that hid the
theme's.CSS an author may be targeting:
code-preview :is(pre, .code-wrap) > button { display: none }
no longer ships. A theme that was relying on the element to hide its button gets it back;
thedisplay: revertoverride some pages added for exactly that is now a no-op and can go.
.code-preview-copy,.code-preview-note,.code-preview-icon-copyand
.code-preview-icon-checkno longer exist, andno-actions="copy"names nothing. -
The sample's console, under the preview.
console.log,info,warn,errorand
debugfrom inside the frame land in a strip directly under the preview — on screen
while the reader types the js that causes them, which a tab of its own could not be.
The strip appears with the first line and costs nothing before it: no box, no reserved
height. It holds the last hundred lines, follows the tail unless the reader has
scrolled up to read, and starts over when the frame rebuilds — a new document is a new
run, the same bargain the event counts make. A patched frame keeps its document and so
keeps its log. Lines still reach the browser's own console.The capture is an inline script written first into the frame's head, ahead of every
deferredjsurl and of the sample's own module — so a top-levelconsole.logon the
first run is caught, which wrapping the console from the host on the frame's load event
would miss: load fires after the sample has already said the interesting thing. Each
call is forwarded to the host as acode-preview-logCustomEvent on the iframe.
Values are formatted withoutinstanceof— the frame is another realm, where its
ElementandErrorare different classes — so an element prints as<tag>, an
error asname: message, the rest as JSON where JSON can say it.no-consoleon the element turns it off, hook and all, for a sample that logs on
every frame. A whole-document sample owns its head and gets no hook. Uncaught errors
stay the error banner's job.DOM the element produces:
div.code-preview-console[role="log"]between the
viewport and whatever sits below, once something has logged;p.code-preview-console-line
per line, with.is-warn/.is-errorby level.--code-preview-console-height
(default10rem) caps the strip.Contents of the preview iframe: one inline
<script>first in head, rewiring the
console. A sample asserting on its document's first script will see this one.
Changed
- The text that is code waits for a Run button; everything else applies as you type.
The 600ms reload debounce is gone — it was never the right tool. Markup and css are
inert an...
v1.0.0
After a detailed polish releasing the first stable version
Fixed
-
A sample's own
<script>stopped running after the first edit. A js demo keeps its
script inside the sample, because the element takes one fence — and an edit was applied
to the loaded frame withinnerHTML, which never executes a script it inserts. The
first paint went throughsrcdocand worked, so the demo only died from the first
keystroke on: still rendered, still correctly marked up, nothing in the console. Only
jsurls and thereloadattribute forced the rebuild; the sample's own script was
not looked at.A sample containing a
<script>now rebuilds the frame rather than patching it, on the
same longer debounce ajsurl already used. Nothing to change in a page: samples with
no script in them still patch, and keep their scroll position and stylesheets as before. -
A sample's own elements never came alive when
jspointed at a custom element
bundle — which is most of what this element is for. The scripts went into<head>
undeferred, socustomElements.defineran before the body was parsed and the parser
then upgraded each element the instant it opened its tag, with none of its light-DOM
children there yet. Every element that reads its own children on connect found nothing
and bailed.The failure was silent and total: the sample rendered, the markup was right, the
stylesheet applied, nothing appeared in the console — and not one element was wired.
Native behaviour inside the sample (a<details>toggling) still worked, which is
exactly what made it read as "the preview is a bit unresponsive" rather than as a bug.
It also made the options panel look broken from the outside: its knobs were writing
correctly the whole time, into elements that were not listening.jsscripts now carrydefer, which is what these libraries already document as their
requirement, and which keeps execution order across several urls. An inline<script>
in the sample is untouched — that one is the author's, and it is in body where they
wrote it. -
The editable code block was a keyboard trap. Tab indents in there, which left a
keyboard user who tabbed in with nothing to press — WCAG 2.1.2 Level
A, and the one
failure here with no workaround from the outside. Esc now hands Tab back: the next
Tab moves focus, and leaving the block re-arms it, so tab-to-indent is unchanged for
anyone who does not need to leave by keyboard. -
The editor says what it is. CodeJar leaves a block that is editable and nothing
else, so the element now addsrole="textbox",aria-multiline="true",
aria-keyshortcuts="Escape"and anaria-labelnaming the language. Anaria-label
oraria-labelledbyalready on the block is left alone. -
The focus ring follows focus. It hung off
code-preview:focus-within, so clicking
a width button or a tab lit the code block up instead. It is now on the block. -
Switching tab no longer drops focus on the floor. Setting
tabfrom a script or
from markup while the reader was inside the pane being hidden left focus on an element
that was about to disappear, which the browser answers by moving it to the body — the
next Tab starts again at the top of the page, with the whole document between a screen
reader and the widget it was just in. Focus now moves to the tab being switched to,
which is where clicking or arrowing to that tab had already left it. Focus outside the
pane is not touched, so the frame's own load — which calls the same code — cannot yank a
reader into the tab strip. -
A keystroke that changed nothing reloaded the preview. CodeJar reports an update on
every keyup, not only the ones that edited the text — the arrows, Tab, every modifier,
and now the Esc this element asks people to press — and each one rebuilt the frame a
quarter-second later for a sample that had not moved. That reload throws away everything
live inside the preview: a script's state, and the focus a keyboard user had put on a
control in there. So an accessible component could not be demonstrated in its own
preview — Tab into the frame, focus a control, and it vanished under you a moment later.
The frame is now rendered only when the source it would render has actually changed.Editing the sample still rebuilds, and still costs whatever was live in there. That one
is the sample changing, which is the point. -
An Escape pressed outside the editor released its Tab. The listener sits on the
element, so an Escape in an options-panel field or on a width button also flipped the
editor's tab-to-indent off and rewrote the keyboard hint — about an editor the reader
was not in. Leaving the editor re-armed it, so no trap could result, but the hint could
claim a state that was no longer true. Only an Escape from inside the editor counts now. -
A failed manifest fetch was cached for the life of the page. One transient network
error cost every preview sharing that url its options panel until a reload. A rejected
fetch is now evicted from the cache, so a preview mounting later tries again. -
An attribute
<select>now says its default. Its empty option reads
default (quiet)when the manifest documents one, the same way a custom property's
already did — it used to say onlydefault, with the manifest's answer dropped. -
A duplicate width in
viewport-widthsno longer renders a duplicate button. -
The color swatch treats an alpha it cannot parse as unknown — the swatch stays
where it was, like every other value it cannot be sure about, rather than showing the
color as opaque. -
An attribute name containing a
.is matched literally when the options panel
reads or rewrites the sample, rather than as a regex wildcard. -
Publishing runs the tests. CI runs on branches and pull requests, not on tags, so
the publish workflow ran none at all — a tag cut from a broken commit would have
published untested code.npm testnow runs beforenpm publish.
Added
-
Every color and font the stylesheet reads now has a
--code-preview-name.
--code-preview-bg,--code-preview-fg,--code-preview-fg-muted,
--code-preview-border,--code-preview-accent,--code-preview-danger,
--code-preview-radiusand--code-preview-font-monojoin the four
--code-preview-*sizing properties that were already there, so nothing about the
element's look is reachable except through its own namespace.Nothing to change in a page. Each one falls back to the unprefixed name it used to
read before its default —var(--code-preview-bg, var(--bg, #fff))— so a host page
themed through--border,--bg,--accent,--fg,--fg-muted,--danger,
--radiusor--font-monolooks exactly as it did. The prefixed name is only the
first lookup, which is what makes it possible to move this element alone without
moving the page around it:code-preview { --code-preview-bg: #161b22; }
dist/code-preview-hljs.cssreads--code-preview-fg-mutedthe same way, so the
optional syntax theme moves with the element rather than with the page. -
The options panel lists what the sample fires. A third group,
Events, built from
the manifest'sevents[]— every documented event is listed whether or not it has fired,
with a count and the lastdetailbeside it once it has. An element whose whole API is a
CustomEventwas otherwise a preview that appears to do nothing when you click it.The rows are
<div class="code-preview-event">with a
<span class="code-preview-event-value">readout, and the<fieldset>around them
carriesaria-live="polite". Nothing here is a control, so nothing writes to the sample
or to the frame's stylesheet.The listeners go on the frame's document, in the capture phase: capture is what hears
an event that does not bubble — most of them, dispatched on the element itself — and the
document is what survives theinnerHTMLpatch a keystroke does. A rebuilt frame is a
new document with a new sample in it, so its counts start again from—.They are also attached whichever tab is open, which is a behaviour change inside the
panel: the controls used to be re-read only when the Options tab was activated, and an
event fired while the reader is looking at the code still has to be counted. -
The event readout is highlighted, and says when it changed. A
detailis now written
as spans carrying highlight.js's own token classes —hljs-attrfor a key,hljs-string,
hljs-number,hljs-literal,hljs-tagfor a node — so a docs page that already ships a
syntax theme colors it with no extra css.dist/code-preview-hljs.cssscopes its rules to
:is(pre code, .code-preview-event-value)for the same reason; a theme of your own that
targeted thepre codeform still wins on any real code block. The readout's text is
unchanged, so anything readingtextContentreads what it read before.A
detailis one line and stays one line: a string over 42 characters is clipped, a
function isƒ, anything nested is{…}and an array is its length. The sample's own
console is where a full payload is read.The readout is now two cells —
<span class="code-preview-event-count">and
<span class="code-preview-event-detail">inside the same
.code-preview-event-value— and the row no longer borrows the knobs' column grid. A
knob's second column is a field wide, which put a two-character count an inch from the
name it belongs to; the name takes what it needs and the count follows it, with the
details lined up in a column of their ...
v0.2.0
Added
-
An options panel, as a third bundle you opt into —
dist/code-preview-options.min.js, 7KB, carrying no copy of the element. Amanifest
attribute pointing at a
custom-elements.jsonturns on
a second tab beside the code, with controls generated from it:attributes[]become
attribute knobs,cssProperties[]become custom-property knobs, and each control's kind
comes from the type or Houdini syntax the manifest already declares.<script src="dist/code-preview-options.min.js"></script> <code-preview manifest="dist/custom-elements.json" tab="options"> … </code-preview>
The two halves of the panel write to two different places, which is the one real design
decision in it. An attribute belongs to an element in the sample, so its knob rewrites
the code block — spliced into the opening tag with a regex rather than parsed and
re-serialized, because on a documentation page the markup is the documentation and
reformatting it on the first knob turn is not acceptable. Edit it back by hand and the
controls re-read the source next time the tab is opened. A custom property is not part
of the sample at all: it goes into one<style>appended last in the frame's head, whose
selector is the element's own tag and never:root— and that rule is printed at the
bottom of the panel to be copied, which is worth more than the knobs are.An untouched knob writes nothing at all. Defaults are placeholders, not values, so
emptying a control is how you reset it.No manifest, no tabs — a page that does not use one renders byte-identically to before,
and a page that never loads the bundle pays nothing for the attribute existing.
Changed
- The DOM of the bar above the preview.
viewport-widthsused to putrole="group"on
.code-preview-baritself; the buttons now sit in a.code-preview-widthsgroup inside it,
and the bar is a plain strip that the options panel's tab list shares. One bar means one
border, one set of top corners and one height to reserve however many things end up in it.
Only affects CSS or scripts that targeted.code-preview-bar[role="group"]directly. code-preview:not(:defined)[viewport-widths]::beforeis now
code-preview:not(:defined):is([viewport-widths], [manifest])::before, sincemanifest
puts a bar there too.--code-preview-options-height(default12rem) joins
--code-preview-heightand--code-preview-bar-height, and matters only with
tab="options"— the one case where upgrading hides something, so the code block is hidden
from the start and the panel's space held instead.
Full Changelog: v0.1.0...v0.2.0
v0.1.0
Initial release
Full Changelog: https://github.com/stamat/code-preview-element/commits/v0.1.0