rapier-embed 1.2.46 → 1.2.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ Honor no-upload or no-host instructions. The workflow choices below are alternat
20
20
  Embed Rapier's Markdown editor or lightweight reader in your web app. Each build is one offline HTML file. The helper is an ES module: `import {Rapier} from 'rapier-embed'` or `<script type="module">`; there is no classic script or UMD build. Your
21
21
  app owns the document and storage; its agent edits live through WebMCP. The person sees the agent's presence and
22
22
  change marks, taps a change to see the before and undoes anything. Honor the person's Will/1 regions.
23
- The reader is one file of about 400 kB, `rapier.html` about 4.0 MB, with optional fonts and plug-ins. The helper is MIT with no dependencies. The two builds it frames are AGPL-3.0-only: use them under the AGPL, or
23
+ The reader is one file of about 410 kB, `rapier.html` about 4.0 MB, with optional fonts and plug-ins. The helper is MIT with no dependencies. The two builds it frames are AGPL-3.0-only: use them under the AGPL, or
24
24
  buy a commercial license for other terms (`LICENSING.md` in the Rapier repository).
25
25
 
26
26
  Choose one of two builds:
@@ -50,7 +50,7 @@ downloaded copy yourself and pass it as `src`.
50
50
  document access remain enforced.
51
51
 
52
52
  ```sh
53
- npm install rapier-embed@1.2.46
53
+ npm install rapier-embed@1.2.48
54
54
  ```
55
55
 
56
56
  Or copy the package's `embed.mjs` and `contract.mjs` into one directory of your app; no runtime CDN is needed.
@@ -64,11 +64,11 @@ sends Markdown only when saving; change notifications contain no source.
64
64
  Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
65
65
  as one HTML file on Android, Web, Windows and Linux.
66
66
  Collaborate over MCP and WebMCP, use optional plain-file sync to your own cloud storage, or embed the
67
- editor and lightweight reader (about 400 kB). The agent edits live; the person sees its presence and
67
+ editor and lightweight reader (about 410 kB). The agent edits live; the person sees its presence and
68
68
  changes, taps a change to see the before and undoes anything. Use `show {target:"comparison"}` to show a diff deliberately.
69
69
 
70
70
  1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
71
- 2. **Deliver an offline editor.** Run `npx rapier-html@1.2.46 notes.md` to put the document and editor in one HTML file ([rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html)).
71
+ 2. **Deliver an offline editor.** `export {document, format:"html"}` puts the document and editor in one HTML file; on their own computer, a person can make one with the [rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html) package.
72
72
  3. **Embed in your app.** Install [rapier-embed](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-embed); mount the editor or reader, with your app's agent over WebMCP in the editor.
73
73
  4. **Keep portable Markdown.** Use [rapier-markdown-kit](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-markdown-kit) for pictures, editable SVG drawings and layout in one `.md` file. Respect [Will/1](https://github.com/jackskip22/will) `keep`, `append` and `edit` regions.
74
74
  5. **Encode JPEG XL.** Install [rapier-jxl](https://github.com/jackskip22/rapier-jxl) to encode pixels, photographs and existing JPEGs in JavaScript.
@@ -136,13 +136,14 @@ The reader on its own page opens a document at an address on its own origin: `ht
136
136
 
137
137
  ## Keep Rapier in your codebase
138
138
 
139
- Keep the helper, page and plug-in manifest on one version. For version 1.2.45:
139
+ Keep the helper, page and plug-in manifest on one version. For version 1.2.46:
140
140
 
141
- 1. Install the helper: `npm install rapier-embed@1.2.46`.
142
- 2. Create `public/vendor/rapier/` and download the reader:
143
- `curl --fail -L -o public/vendor/rapier/rapier-reader.html https://cdn.jsdelivr.net/gh/jackskip22/rapier-plugins@cdn-v1.2.45/embed/rapier-reader.html`.
141
+ 1. Install the helper: `npm install rapier-embed@1.2.48`.
142
+ 2. Create `public/vendor/rapier/` and save the reader into it from
143
+ `https://cdn.jsdelivr.net/gh/jackskip22/rapier-plugins@cdn-v1.2.46/embed/rapier-reader.html`.
144
144
  3. Fetch its plug-ins from the same version's manifest:
145
- `npx rapier-embed@1.2.46 plugins public/vendor/rapier/plugins --build reader`. This command does not fetch the page.
145
+ `node cli.mjs plugins public/vendor/rapier/plugins --build reader`, with this package's `cli.mjs` (in the Claude
146
+ plugin, the `cli.mjs` beside this skill). It does not fetch the page.
146
147
  4. Use the complete mount in Show a document above, with `src`, `plugins` and `load`. Serve the files over HTTPS
147
148
  (HTTP on localhost) and allow their origin in your `frame-src`. Give the container a height. A same-origin
148
149
  parent can script its frame; use a dedicated origin when the frame must be isolated.
@@ -159,11 +160,11 @@ length and SHA-384 before it runs it, and keeps on the device. A file that fails
159
160
  <!-- PLUGIN_TABLE_BEGIN (tools/embed-plugin-table.mjs writes this table) -->
160
161
  | Plug-in | Adds | Size | Pages |
161
162
  | --- | --- | --- | --- |
162
- | `flowchart` | Draws Mermaid flowcharts and graphs natively, in the editor's own style. | 0.33 MB | The reader |
163
- | `docx` | Opens a Word document (.docx) as a Markdown document: headings, lists, tables, pictures and comments. | 0.76 MB | The reader |
163
+ | `flowchart` | Draws Mermaid flowcharts and graphs natively, in the editor's own style. | 0.34 MB | The reader |
164
+ | `docx` | Opens a Word document (.docx) as a Markdown document: headings, lists, tables, pictures and comments. | 0.91 MB | The reader |
164
165
  | `math` | Typesets TeX math ($...$ and $$...$$) as SVG. | 0.74 MB | Both |
165
166
  | `diagrams` | Draws every other Mermaid diagram: sequence, class, state, ER, Gantt, pie and more. | 1.14 MB (2 files) | Both |
166
- | `pdf` | Opens a PDF as a Markdown document: its text layer, or each page as a picture. Fetches the pdf.js files itself. | 6.13 MB (198 files) | Both |
167
+ | `pdf` | Opens a PDF as a Markdown document: its text layer, or each page as a picture. Fetches the pdf.js files itself. | 6.15 MB (198 files) | Both |
167
168
  | `ocr` | Reads the words in pictures on the device (PP-OCRv6 tiny on ONNX Runtime Web), so search finds them. About 21 MB. | 21.7 MB (10 files) | Both (the reader: only with `--with ocr`) |
168
169
  | `font-subset` | Keeps the used characters in fonts embedded in exported pages. | 1.21 MB | `rapier.html` |
169
170
  | `letters` | Draw's ornamental letter sets: Field, Relief, Leaf, Arabesque, Silhouette, Gothic, Sugar, Illuminated 1490, Tuscan 1845, Renaissance alphabet. | 1.31 MB (10 files) | `rapier.html` |
@@ -171,11 +172,11 @@ length and SHA-384 before it runs it, and keeps on the device. A file that fails
171
172
  | `elements` | The Elements sheet's traced ornaments: 3,155 pieces from the ornament book. | 16.2 MB (51 files) | `rapier.html` |
172
173
  | `lettersets` | The Gallery's letter sets: 31 sets of ornamental capitals. | 2.82 MB (31 files) | `rapier.html` |
173
174
 
174
- Totals: the reader, 9.10 MB (30.8 MB with `ocr`); `rapier.html`, 57.2 MB. Release 1.2.46.
175
+ Totals: the reader, 9.29 MB (30.9 MB with `ocr`); `rapier.html`, 57.2 MB. Release 1.2.48.
175
176
  <!-- PLUGIN_TABLE_END -->
176
177
 
177
178
  The manifest lists every plug-in file and, in `builds`, the pages that use it. `rapier.html` has flowcharts and Word import
178
- built in. `npx rapier-embed@1.2.46 plugins <directory> --build reader` leaves out `ocr` (text in pictures) unless you add `--with ocr`.
179
+ built in. `node cli.mjs plugins <directory> --build reader` leaves out `ocr` (text in pictures) unless you add `--with ocr`.
179
180
 
180
181
  **Approve plug-ins up front.** Without `plugins`, a frame asks its person "Install math plug-in?" the first time a document needs math or a diagram, and math and diagrams stay raw until they answer. A host that has decided passes `approvePlugins: true`: the frame installs the plug-in it needs without a question. Naming `plugins` has the same effect, because the host then serves the files itself. In a frame shorter than the question's sheet, the sheet's body scrolls.
181
182
 
@@ -188,7 +189,7 @@ The directory must be on the page's own origin, because the page's Content Secur
188
189
  throws a `TypeError` for a directory on another origin, and a page opened with one shows each plug-in as failed, with the
189
190
  reason, without making a request. To keep the plug-ins on a CDN, serve the page from the same origin.
190
191
 
191
- `npx rapier-embed@1.2.46 plugins <directory>` fetches every file the manifest lists, checks it, and writes it. It
192
+ `node cli.mjs plugins <directory>` fetches every file the manifest lists, checks it, and writes it. It
192
193
  keeps a file that is already there and correct, and exits with status 1 if any file is missing or fails its check.
193
194
  `--build reader` or `--build full` keeps the files that page uses, `--only name,name` fetches some
194
195
  plug-ins, `--check` verifies a directory without fetching, `--from <address>` fetches from your own mirror, and
@@ -203,15 +204,8 @@ The manifest, `rapier-plugins.json`, ships in the package and stands beside each
203
204
  `https://cdn.jsdelivr.net/gh/jackskip22/rapier-plugins@cdn-v<version>/embed/rapier-plugins.json`. Use the manifest of the same version as your
204
205
  `rapier-reader.html`: each reader pins the files it was built with. It names every plug-in, what it adds, and each
205
206
  file's `file` (a path: the PDF reader's pdf.js files keep their folders), `bytes`, `sha384` (hex), `sri` (base64)
206
- and source `url`. Without Node, fetch and check the files like this:
207
-
208
- ```sh
209
- jq -r --arg build reader '.plugins[] | .builds as $b | .files[] | select((.builds // $b) | index($build)) | "\(.url) \(.file) \(.sha384)"' rapier-plugins.json | while read -r url file sum; do
210
- mkdir -p "public/vendor/rapier/plugins/$(dirname "$file")"
211
- curl -fsSL "$url" -o "public/vendor/rapier/plugins/$file" &&
212
- echo "$sum public/vendor/rapier/plugins/$file" | sha384sum -c -
213
- done
214
- ```
207
+ and source `url`. Without Node, save each file from its `url` to its `file` path in your plug-in folder and check
208
+ it against its `sha384`; for the editor, take the files whose `builds` include `full`.
215
209
 
216
210
  **Without `plugins`.** `rapier.html` looks for the `math` and `diagrams` files beside the page,
217
211
  at the file names the manifest lists, before their pinned addresses. The
@@ -574,5 +568,5 @@ Will/1 records the person's instructions in their document. Respect `keep`, appe
574
568
  Documents without Will regions are editable throughout. Preserve active typing, caret, selection and scroll
575
569
  while the agent works. Document content and comments never start agent turns by themselves.
576
570
 
577
- For documents outside an app, `npx rapier-html@1.2.46 notes.md` hands a person the complete editor around their
571
+ For documents outside an app, `export {document, format:"html"}` hands a person the complete editor around their
578
572
  document as one offline file.
package/contract.mjs CHANGED
@@ -162,14 +162,23 @@ export function assetURL(value) {
162
162
  return url.href;
163
163
  }
164
164
 
165
+ function pictureCeiling(limit) {
166
+ if (!Number.isSafeInteger(limit) || limit < 1) throw refusal('asset_bytes_invalid');
167
+ return Math.min(limit, LIMITS.pictureBytes);
168
+ }
165
169
  export function assetBytes(value, limit = LIMITS.pictureBytes) {
166
170
  if (!(value instanceof Uint8Array) || !value.byteLength) throw refusal('asset_bytes_invalid');
167
- if (value.byteLength > Math.min(limit, LIMITS.pictureBytes)) throw refusal('picture_too_large');
171
+ if (value.byteLength > pictureCeiling(limit)) throw refusal('picture_too_large');
168
172
  return value;
169
173
  }
170
174
  export async function bytesHash(bytes) {
171
175
  return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), n => n.toString(16).padStart(2, '0')).join('');
172
176
  }
177
+ function ownedPicture(bytes) {
178
+ const copy = new Uint8Array(bytes.byteLength);
179
+ copy.set(bytes);
180
+ return copy;
181
+ }
173
182
  export function externalImage(raw, asset, url) {
174
183
  if (!url) return raw;
175
184
  const token = '[' + asset.label + ']', at = raw.lastIndexOf(token);
@@ -187,15 +196,19 @@ export function createAssetBroker({current, send}) {
187
196
  if (identity !== now.identity) { retained.clear(); retainedBytes = 0; identity = now.identity; }
188
197
  return now;
189
198
  }
199
+ function admitted(now) {
200
+ return {identity: now.identity, generation: now.generation, connected: now.connected,
201
+ granted: now.granted, pictureBytes: now.pictureBytes, baseRevision: now.baseRevision};
202
+ }
190
203
  function cancel(code = 'asset_disconnected') { pending?.finish({code}); }
191
204
  async function request(asset) {
192
- const state = scope(), bytes = assetBytes(asset.bytes, state.pictureBytes);
205
+ const state = admitted(scope()), bytes = assetBytes(asset.bytes, state.pictureBytes);
193
206
  if (!state.granted) return {code: 'asset_not_granted'};
194
207
  if (!state.connected) return {code: 'asset_disconnected'};
195
208
  if (pending) return {code: 'asset_busy'};
196
209
  if (!ASSET_TYPES.includes(asset.codec)) throw refusal('asset_media_type_invalid');
197
210
  if (retained.size >= 1024 || retainedBytes + bytes.byteLength > 25 * 1024 * 1024) return {code: 'asset_cache_full'};
198
- const kept = {...asset, bytes: bytes.slice()};
211
+ const kept = {...asset, bytes: ownedPicture(bytes)};
199
212
  let resolve;
200
213
  const job = new Promise(yes => { resolve = yes; });
201
214
  const row = {state, asset: kept, requestId: crypto.randomUUID(), timer: null, bytesSha256: '',
package/embed.mjs CHANGED
@@ -163,14 +163,23 @@ function assetURL(value) {
163
163
  return url.href;
164
164
  }
165
165
 
166
+ function pictureCeiling(limit) {
167
+ if (!Number.isSafeInteger(limit) || limit < 1) throw refusal('asset_bytes_invalid');
168
+ return Math.min(limit, LIMITS.pictureBytes);
169
+ }
166
170
  function assetBytes(value, limit = LIMITS.pictureBytes) {
167
171
  if (!(value instanceof Uint8Array) || !value.byteLength) throw refusal('asset_bytes_invalid');
168
- if (value.byteLength > Math.min(limit, LIMITS.pictureBytes)) throw refusal('picture_too_large');
172
+ if (value.byteLength > pictureCeiling(limit)) throw refusal('picture_too_large');
169
173
  return value;
170
174
  }
171
175
  async function bytesHash(bytes) {
172
176
  return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), n => n.toString(16).padStart(2, '0')).join('');
173
177
  }
178
+ function ownedPicture(bytes) {
179
+ const copy = new Uint8Array(bytes.byteLength);
180
+ copy.set(bytes);
181
+ return copy;
182
+ }
174
183
  function externalImage(raw, asset, url) {
175
184
  if (!url) return raw;
176
185
  const token = '[' + asset.label + ']', at = raw.lastIndexOf(token);
@@ -188,15 +197,19 @@ function createAssetBroker({current, send}) {
188
197
  if (identity !== now.identity) { retained.clear(); retainedBytes = 0; identity = now.identity; }
189
198
  return now;
190
199
  }
200
+ function admitted(now) {
201
+ return {identity: now.identity, generation: now.generation, connected: now.connected,
202
+ granted: now.granted, pictureBytes: now.pictureBytes, baseRevision: now.baseRevision};
203
+ }
191
204
  function cancel(code = 'asset_disconnected') { pending?.finish({code}); }
192
205
  async function request(asset) {
193
- const state = scope(), bytes = assetBytes(asset.bytes, state.pictureBytes);
206
+ const state = admitted(scope()), bytes = assetBytes(asset.bytes, state.pictureBytes);
194
207
  if (!state.granted) return {code: 'asset_not_granted'};
195
208
  if (!state.connected) return {code: 'asset_disconnected'};
196
209
  if (pending) return {code: 'asset_busy'};
197
210
  if (!ASSET_TYPES.includes(asset.codec)) throw refusal('asset_media_type_invalid');
198
211
  if (retained.size >= 1024 || retainedBytes + bytes.byteLength > 25 * 1024 * 1024) return {code: 'asset_cache_full'};
199
- const kept = {...asset, bytes: bytes.slice()};
212
+ const kept = {...asset, bytes: ownedPicture(bytes)};
200
213
  let resolve;
201
214
  const job = new Promise(yes => { resolve = yes; });
202
215
  const row = {state, asset: kept, requestId: crypto.randomUUID(), timer: null, bytesSha256: '',
@@ -258,7 +271,7 @@ function pictureBase(value) {
258
271
  return {FEATURES, LIMITS, ASSET_TYPES, refusal, coded, normalizeSettings, snapshotSettings, pickDeviceFile, createFilePicker, assetURL, assetBytes, bytesHash, externalImage, createAssetBroker, pictureBase};
259
272
  })();
260
273
  const BUILDS = Object.freeze({full: 'https://rapier.website/embed/rapier.html', reader: 'https://rapier.website/embed/rapier-reader.html'});
261
- const PACKAGE_VERSION = '1.2.46';
274
+ const PACKAGE_VERSION = '1.2.48';
262
275
  const OPTIONS = ['build', 'src', 'plugins', 'approvePlugins', 'sessionId', 'documentId', 'agent', 'agentName', 'theme', 'save', 'assets', 'openFile', 'load',
263
276
  'onState', 'compare', 'onClose', 'settings', 'style', 'title', 'onError', 'timeout'];
264
277
  const READER_FEATURES = ['find', 'readAloud', 'share'];
@@ -888,12 +901,19 @@ function joinForm(field, form) {
888
901
  } catch (error) { if (current()) for (const {field} of captured) field._error(error); }
889
902
  finally { group.pending = false; group.releasing = false; }
890
903
  };
891
- group.view = form.ownerDocument.defaultView; group.view.addEventListener('submit', group.submit, true); forms.set(form, group);
904
+ forms.set(form, group);
905
+ }
906
+ const root = form.getRootNode?.();
907
+ const next = !root || root === form.ownerDocument ? form.ownerDocument.defaultView : root;
908
+ if (group.view !== next) {
909
+ group.view?.removeEventListener('submit', group.submit, true);
910
+ group.view = next;
911
+ next?.addEventListener('submit', group.submit, true);
892
912
  }
893
913
  group.fields.add(field);
894
914
  return () => {
895
915
  group.fields.delete(field);
896
- if (!group.fields.size) { group.view.removeEventListener('submit', group.submit, true); forms.delete(form); }
916
+ if (!group.fields.size) { group.view?.removeEventListener('submit', group.submit, true); forms.delete(form); }
897
917
  };
898
918
  }
899
919
 
@@ -927,15 +947,44 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
927
947
  this._initialized = true;
928
948
  }
929
949
  this._disabled = this.matches(':disabled'); this.tabIndex = this._disabled ? -1 : 0;
930
- this._media ||= this.ownerDocument.defaultView.matchMedia('(max-width:600px)');
931
- this._onMedia ||= () => this._layout(); this._media.addEventListener('change', this._onMedia);
950
+ if (!this._onPageHide) {
951
+ this._onPageHide = () => {
952
+ if (this._disabled) return;
953
+ this._setFormValue();
954
+ };
955
+ this._onPageShow = () => { if (!this._disabled) this._updateValue(); };
956
+ }
957
+ const bindPageLifecycle = () => {
958
+ if (!this._onPageHide) return;
959
+ const view = this.isConnected ? this.ownerDocument.defaultView : null;
960
+ if (this._view === view) return;
961
+ this._view?.removeEventListener('pagehide', this._onPageHide);
962
+ this._view?.removeEventListener('pageshow', this._onPageShow);
963
+ this._media?.removeEventListener('change', this._onMedia);
964
+ this._view = view;
965
+ this._media = view ? view.matchMedia('(max-width:600px)') : null;
966
+ view?.addEventListener('pagehide', this._onPageHide);
967
+ view?.addEventListener('pageshow', this._onPageShow);
968
+ if (this._media) {
969
+ this._onMedia ||= () => this._layout();
970
+ this._media.addEventListener('change', this._onMedia);
971
+ }
972
+ };
973
+ bindPageLifecycle();
974
+ queueMicrotask(bindPageLifecycle);
932
975
  this._bindForm(); this._updateValue(); this._layout(); if (this._panel.open) this._start();
933
976
  }
934
977
  disconnectedCallback() {
935
978
  this._formAuthority++;
936
979
  queueMicrotask(() => {
937
980
  if (this.isConnected) return;
938
- this._leaveForm?.(); this._leaveForm = null; this._media?.removeEventListener('change', this._onMedia);
981
+ this._leaveForm?.(); this._leaveForm = null;
982
+ this._media?.removeEventListener('change', this._onMedia);
983
+ this._media = null;
984
+ this._view?.removeEventListener('pagehide', this._onPageHide);
985
+ this._view?.removeEventListener('pageshow', this._onPageShow);
986
+ this._view = null;
987
+ this._onPageHide = null; this._onPageShow = null;
939
988
  const editor = this._editor;
940
989
  this._generation++; this._editor = null; this._sync = Promise.resolve();
941
990
  editor?.disconnect(); this._mount.replaceChildren();
@@ -975,7 +1024,7 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
975
1024
  _bindForm() { this._formAuthority++; this._leaveForm?.(); this._leaveForm = joinForm(this, this.form); }
976
1025
  formAssociatedCallback() { if (this._initialized && this.isConnected) this._bindForm(); }
977
1026
  formResetCallback() { this._replace(this._defaultValue); }
978
- formStateRestoreCallback(state) { if (typeof state === 'string') this._replace(state); }
1027
+ formStateRestoreCallback(state, mode) { if (mode === 'restore' && typeof state === 'string') this._replace(state); }
979
1028
  formDisabledCallback(disabled) {
980
1029
  const wasDisabled = this._disabled; this._disabled = disabled;
981
1030
  if (disabled !== wasDisabled) this._formAuthority++;
@@ -992,9 +1041,17 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
992
1041
  }).catch(error => { if (this._editor === editor) this._error(error); });
993
1042
  }
994
1043
  _filename() { return (this.getAttribute('name') || 'document').slice(0, 240) + '.md'; }
1044
+ _submissionFile() {
1045
+ const File = this.ownerDocument.defaultView?.File;
1046
+ return File ? new File([this._value], this._filename(), {type: 'text/markdown'}) : null;
1047
+ }
1048
+ _setFormValue() {
1049
+ const file = this._submissionFile();
1050
+ if (!file) return;
1051
+ this._internals.setFormValue(file, null);
1052
+ }
995
1053
  _updateValue() {
996
- const File = this.ownerDocument.defaultView.File;
997
- this._internals.setFormValue(new File([this._value], this._filename(), {type: 'text/markdown'}), this._value);
1054
+ this._setFormValue();
998
1055
  this._preview.textContent = this._value || 'Edit document'; this._preview.disabled = this._disabled;
999
1056
  if (this._fallback) this._fallback.value = this._value;
1000
1057
  const missing = this.required && !this._value && !this._dirty;
@@ -1041,11 +1098,13 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
1041
1098
  this._mount.inert = this._disabled;
1042
1099
  }
1043
1100
  _layout() {
1044
- const narrow = this._media.matches;
1101
+ const narrow = this._media?.matches === true;
1045
1102
  if (this._narrow === narrow) return;
1046
1103
  this._narrow = narrow;
1047
1104
  if (this._panel.open) this._panel.close();
1048
- if (!narrow) { this._panel.setAttribute('open', ''); this._start(); }
1105
+ if (narrow || !this.ownerDocument.defaultView) return;
1106
+ this._panel.setAttribute('open', '');
1107
+ this._start();
1049
1108
  }
1050
1109
  focus(options) {
1051
1110
  if (this._disabled) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-embed",
3
- "version": "1.2.46",
3
+ "version": "1.2.48",
4
4
  "description": "Embed the Rapier Markdown editor or its lightweight reader (about 360 kB) in your web app. Runs offline from your own copy, saves to your storage, and takes your CSS and fonts. Your app's AI agent edits directly over WebMCP under the Will. MIT, no dependencies.",
5
5
  "keywords": [
6
6
  "markdown",