@8ux-co/eelzap 0.10.0 → 0.10.1
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/CHANGELOG.md +63 -1
- package/README.md +68 -8
- package/dist/{chunk-QN53G5P7.js → chunk-6XHCA4U4.js} +25 -9
- package/dist/chunk-6XHCA4U4.js.map +1 -0
- package/dist/{chunk-VW4EH7NY.js → chunk-7CXQK2PR.js} +148 -80
- package/dist/chunk-7CXQK2PR.js.map +1 -0
- package/dist/{chunk-2IUU53QA.js → chunk-7ZPXZZUT.js} +171 -2
- package/dist/chunk-7ZPXZZUT.js.map +1 -0
- package/dist/index.cjs +94 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +119 -12
- package/dist/index.d.ts +119 -12
- package/dist/index.js +94 -1
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +6 -0
- package/dist/next.js.map +1 -1
- package/dist/preview.d.ts +55 -7
- package/dist/preview.js +95 -39
- package/dist/preview.js.map +1 -1
- package/dist/react.js +3 -3
- package/dist/react.js.map +1 -1
- package/dist/{signin-2QOSWZFK.js → signin-QT7YANBN.js} +14 -111
- package/dist/signin-QT7YANBN.js.map +1 -0
- package/dist/{suggest-5RHURHED.js → suggest-JZ63L3WP.js} +1001 -66
- package/dist/suggest-JZ63L3WP.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-2IUU53QA.js.map +0 -1
- package/dist/chunk-QN53G5P7.js.map +0 -1
- package/dist/chunk-VW4EH7NY.js.map +0 -1
- package/dist/signin-2QOSWZFK.js.map +0 -1
- package/dist/suggest-5RHURHED.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,69 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.10.
|
|
8
|
+
## [0.10.1] - Unreleased
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- The site client's on-page read returns the viewer's own `email` and an `editorUrl` for the record the page resolves to («Abrir en Zap»), plus `records` naming the other records the page shows.
|
|
13
|
+
- **`webhookChanges` covers SEO, collection, schema and site events**, so a
|
|
14
|
+
site that revalidates on webhooks also refreshes when they change:
|
|
15
|
+
- `zap.seo.updated` gives the same `item` or `document` change an edit
|
|
16
|
+
does (action `updated`), one per entry or document;
|
|
17
|
+
- `zap.collection.created`, `updated` and `deleted` give a `collection`
|
|
18
|
+
change carrying `collectionKey`;
|
|
19
|
+
- `zap.schema.field_changed` (action `field_changed`) gives one
|
|
20
|
+
`collection` change per collection whose fields changed, with
|
|
21
|
+
`resourceKey` and `collectionKey` set to the collection's key, and one
|
|
22
|
+
`document` change per document whose fields changed, named by the
|
|
23
|
+
document's key. Zap now sends `collection_key` and `document_key` on
|
|
24
|
+
every schema change. An older payload without them names a collection
|
|
25
|
+
by its id (no `collectionKey`) and widens a document's fields to one
|
|
26
|
+
`site` change;
|
|
27
|
+
- `zap.site.updated` gives a `site` change.
|
|
28
|
+
- `WebhookEventType` adds `'collection'` and `'site'`, and `WebhookAction`
|
|
29
|
+
adds `'field_changed'`. The item, document and media changes keep their
|
|
30
|
+
shape.
|
|
31
|
+
- Typed `data` for these events: `WebhookSeoEventData`,
|
|
32
|
+
`WebhookCollectionEventData`, `WebhookSchemaEventData` (with optional
|
|
33
|
+
`collection_key` and `document_key`) and `WebhookSiteEventData`.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **`canonicalUrl` takes a path or a full URL** (`SeoInput`, docs only; the
|
|
38
|
+
type is still `string | null`). A path such as `/blog/original-post` is
|
|
39
|
+
stored as sent and resolved against the site URL on delivery, so it
|
|
40
|
+
follows a change of address; a full https URL (http only on localhost) is
|
|
41
|
+
kept as sent, for content first published on another domain. Leave it
|
|
42
|
+
unset and delivery uses the record's own URL. Zap refuses `//host`,
|
|
43
|
+
`javascript:` and other schemes, whitespace and credentials with a 400
|
|
44
|
+
whose `details[].message` says why.
|
|
45
|
+
- `Seo.canonicalUrl` on delivery is always an absolute URL or null, as
|
|
46
|
+
before; a stored path arrives resolved. `VersionSeo.canonicalUrl` and the
|
|
47
|
+
SEO routes return the value as stored.
|
|
48
|
+
- The canonical is an SEO field only: it no longer changes the page Zap's
|
|
49
|
+
preview opens. Where a record lives is its preview path.
|
|
50
|
+
- **A document without a preview path is site-wide** (a header and footer
|
|
51
|
+
document, say): `Seo.canonicalUrl` and `ogUrl` are null for it unless an
|
|
52
|
+
editor set a canonical, instead of `/{documentKey}`. A document that is a
|
|
53
|
+
page keeps its preview path's URL.
|
|
54
|
+
- **Zap keeps every version.** The per-site version limit is gone, so
|
|
55
|
+
`WebhookSiteEventData.changed` on `zap.site.updated` never lists
|
|
56
|
+
`maxVersionsPerEntry`, and `itemVersions.list()` and `documentVersions.list()`
|
|
57
|
+
return the whole history. No SDK type carried the setting, so no code changes.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- `webhookChanges` returns an empty list for `zap.item.draft_updated` and
|
|
62
|
+
`zap.document.draft_updated`: saving a draft changes nothing the live site
|
|
63
|
+
serves.
|
|
64
|
+
- **The preview client pauses instead of running away.** When the tagged
|
|
65
|
+
count passes 5,000, or grows on three passes in a row that nothing on the
|
|
66
|
+
page explains, the client stops observing the page and writing values,
|
|
67
|
+
logs one warning and tells Zap's editor (`zap:paused`), which offers a
|
|
68
|
+
reload. Values carrying stega markers no longer add markers on each pass.
|
|
69
|
+
|
|
70
|
+
## [0.10.0] - 2026-10-06
|
|
9
71
|
|
|
10
72
|
The first release as **`@8ux-co/eelzap`**, which replaces
|
|
11
73
|
`@8ux-co/eelzap-api-sdk-ts` (its last release, 0.9.1, points here). One
|
package/README.md
CHANGED
|
@@ -761,20 +761,77 @@ never include comment text. Their types are `WebhookCommentCreatedData`,
|
|
|
761
761
|
|
|
762
762
|
### Cache invalidation
|
|
763
763
|
|
|
764
|
-
`webhookChanges(payload)` flattens an
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
764
|
+
`webhookChanges(payload)` flattens an event into the changes a site
|
|
765
|
+
revalidates by, `{ type, action, id, resourceKey, collectionKey?, siteKey }`:
|
|
766
|
+
|
|
767
|
+
| `type` | From | `resourceKey` | `collectionKey` |
|
|
768
|
+
| ------------ | ----------------------------------------------------------------------- | -------------------- | --------------- |
|
|
769
|
+
| `item` | `zap.item.*`, and `zap.seo.updated` on entries (action `updated`) | the entry's slug | its collection |
|
|
770
|
+
| `document` | `zap.document.*`, and `zap.seo.updated` on documents (action `updated`) | the document's key | |
|
|
771
|
+
| `document` | `zap.schema.field_changed` on a document's fields (`field_changed`) | the document's key | |
|
|
772
|
+
| `media` | `zap.media.*` | the file's id | |
|
|
773
|
+
| `collection` | `zap.collection.*` | the collection's key | the same key |
|
|
774
|
+
| `collection` | `zap.schema.field_changed` on a collection's fields (`field_changed`) | the collection's key | the same key |
|
|
775
|
+
| `site` | `zap.site.updated` | the site's key | |
|
|
776
|
+
|
|
777
|
+
One event can name several entries, documents or files: each is its own
|
|
778
|
+
change. A `collection` change means everything that shows that collection; a
|
|
779
|
+
`site` change means the whole site. Drafts (`draft_updated`), assignments,
|
|
780
|
+
comments, API keys, `ping`, and sites created or deleted give an empty list.
|
|
781
|
+
|
|
782
|
+
A `zap.schema.field_changed` sent before the event carried `collection_key`
|
|
783
|
+
and `document_key` names its owners by id only: a collection's fields give a
|
|
784
|
+
`collection` change whose `resourceKey` is the collection's id, with no
|
|
785
|
+
`collectionKey`, and a document's fields give one `site` change.
|
|
786
|
+
|
|
787
|
+
#### Subscribing a live site
|
|
788
|
+
|
|
789
|
+
Subscribe the site's webhook, narrowed to the site with `site_ids`, to:
|
|
790
|
+
|
|
791
|
+
- the `zap.item.*`, `zap.document.*` and `zap.media.*` events, except
|
|
792
|
+
`zap.item.draft_updated`, `zap.document.draft_updated`, `zap.item.assigned`
|
|
793
|
+
and `zap.document.assigned`;
|
|
794
|
+
- `zap.collection.created`, `zap.collection.updated` and
|
|
795
|
+
`zap.collection.deleted`;
|
|
796
|
+
- `zap.seo.updated`, `zap.schema.field_changed` and `zap.site.updated`.
|
|
797
|
+
|
|
798
|
+
Do not subscribe to `draft_updated` or `assigned`: a saved draft and a new
|
|
799
|
+
Responsable change nothing the site serves, and drafts save often.
|
|
800
|
+
|
|
801
|
+
#### Revalidating
|
|
802
|
+
|
|
803
|
+
A Next.js route handler for a site that serves documents at `/<key>` and
|
|
804
|
+
entries at `/<collection>/<slug>`:
|
|
768
805
|
|
|
769
806
|
```ts
|
|
807
|
+
// app/api/zap-webhook/route.ts
|
|
808
|
+
import { revalidatePath } from 'next/cache'
|
|
770
809
|
import {
|
|
771
|
-
MemoryCacheAdapter,
|
|
772
810
|
verifyWebhookSignature,
|
|
773
811
|
webhookChanges,
|
|
812
|
+
type WebhookChange,
|
|
774
813
|
type WebhookPayload,
|
|
775
814
|
} from '@8ux-co/eelzap'
|
|
776
815
|
|
|
777
|
-
|
|
816
|
+
function revalidate(change: WebhookChange) {
|
|
817
|
+
switch (change.type) {
|
|
818
|
+
case 'item':
|
|
819
|
+
revalidatePath(`/${change.collectionKey}/${change.resourceKey}`)
|
|
820
|
+
revalidatePath(`/${change.collectionKey}`) // the listing
|
|
821
|
+
return
|
|
822
|
+
case 'document':
|
|
823
|
+
revalidatePath(`/${change.resourceKey}`)
|
|
824
|
+
return
|
|
825
|
+
case 'collection':
|
|
826
|
+
// An older field change without the key names the collection by id: refresh the whole site.
|
|
827
|
+
revalidatePath(change.collectionKey ? `/${change.collectionKey}` : '/', 'layout')
|
|
828
|
+
return
|
|
829
|
+
case 'media':
|
|
830
|
+
case 'site':
|
|
831
|
+
revalidatePath('/', 'layout')
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
}
|
|
778
835
|
|
|
779
836
|
export async function POST(request: Request) {
|
|
780
837
|
const payload = await request.text()
|
|
@@ -785,13 +842,16 @@ export async function POST(request: Request) {
|
|
|
785
842
|
}
|
|
786
843
|
|
|
787
844
|
for (const change of webhookChanges(JSON.parse(payload) as WebhookPayload)) {
|
|
788
|
-
|
|
789
|
-
if (change.type === 'item') cache.delete(`${change.collectionKey}:${change.resourceKey}`)
|
|
845
|
+
revalidate(change)
|
|
790
846
|
}
|
|
791
847
|
return new Response('ok')
|
|
792
848
|
}
|
|
793
849
|
```
|
|
794
850
|
|
|
851
|
+
With `cachedFetch` and a `MemoryCacheAdapter`, do the same with
|
|
852
|
+
`cache.delete(key)` for an item or a document, and `cache.clear()` for a
|
|
853
|
+
`collection`, `media` or `site` change.
|
|
854
|
+
|
|
795
855
|
`WEBHOOK_SIGNATURE_HEADER`, `WEBHOOK_TIMESTAMP_HEADER` and
|
|
796
856
|
`WEBHOOK_TOLERANCE_SECONDS` export the header names and the default window.
|
|
797
857
|
|
|
@@ -15,6 +15,7 @@ function siteClientId(zapOrigin, siteId) {
|
|
|
15
15
|
|
|
16
16
|
// src/preview/site/token.ts
|
|
17
17
|
var EXPIRY_MARGIN_MS = 3e4;
|
|
18
|
+
var REFRESH_LIFETIME_MS = 8 * 60 * 60 * 1e3;
|
|
18
19
|
function store(win) {
|
|
19
20
|
try {
|
|
20
21
|
return win.sessionStorage;
|
|
@@ -22,15 +23,26 @@ function store(win) {
|
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
function
|
|
26
|
+
function parse(raw, siteId) {
|
|
27
|
+
if (!raw) return null;
|
|
28
|
+
const value = JSON.parse(raw);
|
|
29
|
+
if (typeof value.token !== "string" || !value.token.startsWith("eel_at_") || typeof value.exp !== "number" || value.site !== siteId) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const refresh = typeof value.refresh === "string" && value.refresh.startsWith("eel_rt_") && typeof value.rexp === "number" ? { refresh: value.refresh, rexp: value.rexp } : {};
|
|
33
|
+
return { token: value.token, exp: value.exp, site: value.site, ...refresh };
|
|
34
|
+
}
|
|
35
|
+
function accessLive(value, now = Date.now()) {
|
|
36
|
+
return value.exp - EXPIRY_MARGIN_MS > now;
|
|
37
|
+
}
|
|
38
|
+
function refreshLive(value, now = Date.now()) {
|
|
39
|
+
return !!value.refresh && typeof value.rexp === "number" && value.rexp > now;
|
|
40
|
+
}
|
|
41
|
+
function readSession(win, siteId, now = Date.now()) {
|
|
26
42
|
const storage = store(win);
|
|
27
43
|
try {
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
const value = JSON.parse(raw);
|
|
31
|
-
if (typeof value.token === "string" && value.token.startsWith("eel_at_") && typeof value.exp === "number" && value.exp - EXPIRY_MARGIN_MS > now && value.site === siteId) {
|
|
32
|
-
return { token: value.token, exp: value.exp, site: value.site };
|
|
33
|
-
}
|
|
44
|
+
const value = parse(storage?.getItem(TOKEN_STORAGE_KEY), siteId);
|
|
45
|
+
if (value && (accessLive(value, now) || refreshLive(value, now))) return value;
|
|
34
46
|
storage?.removeItem(TOKEN_STORAGE_KEY);
|
|
35
47
|
} catch {
|
|
36
48
|
}
|
|
@@ -57,8 +69,12 @@ export {
|
|
|
57
69
|
STATE_PREFIX,
|
|
58
70
|
PENDING_STORAGE_KEY,
|
|
59
71
|
siteClientId,
|
|
60
|
-
|
|
72
|
+
EXPIRY_MARGIN_MS,
|
|
73
|
+
REFRESH_LIFETIME_MS,
|
|
74
|
+
accessLive,
|
|
75
|
+
refreshLive,
|
|
76
|
+
readSession,
|
|
61
77
|
writeToken,
|
|
62
78
|
clearToken
|
|
63
79
|
};
|
|
64
|
-
//# sourceMappingURL=chunk-
|
|
80
|
+
//# sourceMappingURL=chunk-6XHCA4U4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/preview/site/config.ts","../src/preview/site/token.ts"],"sourcesContent":["/**\n * «Editar» and «Comentar» on the customer's site (zap-cms-v2 §3.4, ADR 041):\n * the constants and the context the core hands its two lazy chunks.\n *\n * Three pieces, so an ordinary visitor downloads nothing beyond the core:\n *\n * - the core (`boot.ts`, in the `./preview` budget): the opt-in trigger\n * (`?zap` in the URL, or Shift Z), the OAuth callback relay on `{origin}/`,\n * and the loader;\n * - `signin` (loaded only on the trigger): the launcher, «Entrar con Eel», PKCE,\n * the popup or full-page redirect and the code exchange at Nest;\n * - `suggest` (loaded only after sign-in; the name is the file's, kept): the\n * toolbar, Navegar, Editar and Comentar, the composer, the numbered pins\n * and the calls to Zap.\n *\n * No directive, no imports: pure values both chunks and the core share.\n */\n\n/** Nest, the suite's OAuth issuer (ADR 024). Compiled in; `authOrigin` is the dev opt-in. */\nexport const NEST_PRODUCTION_ORIGIN = 'https://auth.eel.software'\n\n/**\n * The scope a site client asks for, by name (ADR 041 §2). `zap:preview:read`\n * is not asked for until a «Ver borradores» feature needs it (Manager,\n * 2026-10-05); Nest would drop it anyway.\n */\nexport const SITE_CLIENT_SCOPE = 'zap:suggest:write'\n\n/** The popup hands the code to the opener tab over this channel (same origin). */\nexport const SIGNIN_CHANNEL = 'eel-zap'\n\n/** Every `state` this client mints starts with it, so a site's own `?code=` is never ours. */\nexport const STATE_PREFIX = 'zap1.'\n\n/** sessionStorage only (ADR 041 §7): the token, and a pending full-page sign-in. */\nexport const TOKEN_STORAGE_KEY = 'eelzap:site-token'\nexport const PENDING_STORAGE_KEY = 'eelzap:signin'\n\nexport type ChunkName = 'signin' | 'suggest'\n\n/** What a callback page carries back from Nest (`{origin}/?code&state&iss`). */\nexport interface CallbackParams {\n code: string\n state: string\n iss: string\n}\n\nexport interface StartOptions {\n /**\n * signin: open the panel at once, and why (`expired` after a 401 or the\n * hour). suggest: `renewed` after a sign-in that followed an expiry, which\n * says «Sesión renovada» (board SitioRenovacion).\n */\n reason?: 'expired' | 'trigger' | 'renewed'\n /** signin: finish a full-page sign-in that landed on this page. */\n callback?: CallbackParams\n /**\n * signin: start signing in at once, as if «Entrar con Eel» was pressed.\n * Passed by «Volver a entrar» (the bar's expired state), inside its click, so\n * the popup is still allowed.\n */\n signIn?: boolean\n}\n\nexport interface ChunkHandle {\n destroy(): void\n}\n\nexport interface ChunkModule {\n start(ctx: SiteContext, options?: StartOptions): ChunkHandle\n}\n\n/** Everything a chunk needs, decided once by the core. */\nexport interface SiteContext {\n win: Window\n doc: Document\n /** The Zap site's id (`data-site-id`); names its OAuth client. */\n siteId: string\n /** Where Zap's API and the client's CIMD document live. */\n zapOrigin: string\n /** Nest, the issuer: `/oauth/authorize`, `/api/oauth/token`, and the `iss` to expect. */\n authOrigin: string\n /** `{zapOrigin}/oauth/clients/{siteId}.json` (ADR 041 §1). */\n clientId: string\n /** `{page origin}/`, exactly as the CIMD document lists it. */\n redirectUri: string\n locale: 'es' | 'en'\n /** The letter pressed with Shift to open the tools, for the launcher's hint; false when off. */\n shortcut: string | false\n /**\n * The site's draft-mode route (`/api/zap-preview` by default), as the page\n * announced it: «Editar» enters draft mode through it on an untagged page\n * (`draft-session.ts`). Null or absent when the site has none.\n */\n draftRoute?: string | null\n /** Replace whatever chunk is showing with `name`. */\n open(name: ChunkName, options?: StartOptions): Promise<void>\n /** Close the showing chunk; the trigger works again. */\n close(): void\n}\n\n/** The client id of one site, the same string `@eel/api-keys` `zapSiteClientId` builds. */\nexport function siteClientId(zapOrigin: string, siteId: string): string {\n return `${zapOrigin}/oauth/clients/${siteId.toLowerCase()}.json`\n}\n","import { TOKEN_STORAGE_KEY } from './config'\n\n/**\n * The site client's tokens (ADR 041 §7): in memory and in `sessionStorage`,\n * never in `localStorage` or a cookie. The access token lives one hour. Since\n * ADR 041's amendment of 2026-10-06 it comes with a short refresh token (eight\n * hours, rotating, redeemable only inside the person's remembered consent and\n * from this origin), which `session.ts` spends to renew the hour without a\n * sign-in; when that fails the person signs in again.\n *\n * Stored with the expiries and the site it was issued for: a token for another\n * site, a malformed entry, or one whose access AND refresh are both spent reads\n * as nothing and is removed.\n */\n\nexport interface SiteToken {\n /** The `eel_at_` access token. */\n token: string\n /** Epoch ms when it stops working. */\n exp: number\n /** The site it was issued for. */\n site: string\n /** The `eel_rt_` refresh token, when Nest issued one. */\n refresh?: string\n /** Epoch ms after which the refresh token is not worth trying. */\n rexp?: number\n}\n\n/** How long before the real expiry we stop using a token, so a call never races it. */\nexport const EXPIRY_MARGIN_MS = 30_000\n\n/** A refresh token lives eight hours at Nest (ADR 041 amendment 2026-10-06). */\nexport const REFRESH_LIFETIME_MS = 8 * 60 * 60 * 1000\n\nfunction store(win: Window): Storage | null {\n try {\n return win.sessionStorage\n } catch {\n return null\n }\n}\n\nfunction parse(raw: string | null | undefined, siteId: string): SiteToken | null {\n if (!raw) return null\n const value = JSON.parse(raw) as Partial<SiteToken>\n if (\n typeof value.token !== 'string' ||\n !value.token.startsWith('eel_at_') ||\n typeof value.exp !== 'number' ||\n value.site !== siteId\n ) {\n return null\n }\n const refresh =\n typeof value.refresh === 'string' &&\n value.refresh.startsWith('eel_rt_') &&\n typeof value.rexp === 'number'\n ? { refresh: value.refresh, rexp: value.rexp }\n : {}\n return { token: value.token, exp: value.exp, site: value.site, ...refresh }\n}\n\n/** True while the access token is still worth sending. */\nexport function accessLive(value: SiteToken, now = Date.now()): boolean {\n return value.exp - EXPIRY_MARGIN_MS > now\n}\n\n/** True while the refresh token is still worth trying. */\nexport function refreshLive(value: SiteToken, now = Date.now()): boolean {\n return !!value.refresh && typeof value.rexp === 'number' && value.rexp > now\n}\n\n/**\n * The stored session while it can still act: a live access token, or a\n * refresh token that may renew it (`session.ts` does). Anything else is\n * removed and reads as null.\n */\nexport function readSession(win: Window, siteId: string, now = Date.now()): SiteToken | null {\n const storage = store(win)\n try {\n const value = parse(storage?.getItem(TOKEN_STORAGE_KEY), siteId)\n if (value && (accessLive(value, now) || refreshLive(value, now))) return value\n storage?.removeItem(TOKEN_STORAGE_KEY)\n } catch {\n // Unreadable entry or blocked storage: no token.\n }\n return null\n}\n\n/** The stored session only while its ACCESS token is live (no renewal needed). */\nexport function readToken(win: Window, siteId: string, now = Date.now()): SiteToken | null {\n const value = readSession(win, siteId, now)\n return value && accessLive(value, now) ? value : null\n}\n\nexport function writeToken(win: Window, value: SiteToken): void {\n try {\n store(win)?.setItem(TOKEN_STORAGE_KEY, JSON.stringify(value))\n } catch {\n // Blocked storage: the token lives in memory for this page only.\n }\n}\n\nexport function clearToken(win: Window): void {\n try {\n store(win)?.removeItem(TOKEN_STORAGE_KEY)\n } catch {\n // Nothing to clear.\n }\n}\n"],"mappings":";;;;;AAmBO,IAAM,yBAAyB;AAO/B,IAAM,oBAAoB;AAG1B,IAAM,iBAAiB;AAGvB,IAAM,eAAe;AAGrB,IAAM,oBAAoB;AAC1B,IAAM,sBAAsB;AAkE5B,SAAS,aAAa,WAAmB,QAAwB;AACtE,SAAO,GAAG,SAAS,kBAAkB,OAAO,YAAY,CAAC;AAC3D;;;AC3EO,IAAM,mBAAmB;AAGzB,IAAM,sBAAsB,IAAI,KAAK,KAAK;AAEjD,SAAS,MAAM,KAA6B;AAC1C,MAAI;AACF,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,MAAM,KAAgC,QAAkC;AAC/E,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,MACE,OAAO,MAAM,UAAU,YACvB,CAAC,MAAM,MAAM,WAAW,SAAS,KACjC,OAAO,MAAM,QAAQ,YACrB,MAAM,SAAS,QACf;AACA,WAAO;AAAA,EACT;AACA,QAAM,UACJ,OAAO,MAAM,YAAY,YACzB,MAAM,QAAQ,WAAW,SAAS,KAClC,OAAO,MAAM,SAAS,WAClB,EAAE,SAAS,MAAM,SAAS,MAAM,MAAM,KAAK,IAC3C,CAAC;AACP,SAAO,EAAE,OAAO,MAAM,OAAO,KAAK,MAAM,KAAK,MAAM,MAAM,MAAM,GAAG,QAAQ;AAC5E;AAGO,SAAS,WAAW,OAAkB,MAAM,KAAK,IAAI,GAAY;AACtE,SAAO,MAAM,MAAM,mBAAmB;AACxC;AAGO,SAAS,YAAY,OAAkB,MAAM,KAAK,IAAI,GAAY;AACvE,SAAO,CAAC,CAAC,MAAM,WAAW,OAAO,MAAM,SAAS,YAAY,MAAM,OAAO;AAC3E;AAOO,SAAS,YAAY,KAAa,QAAgB,MAAM,KAAK,IAAI,GAAqB;AAC3F,QAAM,UAAU,MAAM,GAAG;AACzB,MAAI;AACF,UAAM,QAAQ,MAAM,SAAS,QAAQ,iBAAiB,GAAG,MAAM;AAC/D,QAAI,UAAU,WAAW,OAAO,GAAG,KAAK,YAAY,OAAO,GAAG,GAAI,QAAO;AACzE,aAAS,WAAW,iBAAiB;AAAA,EACvC,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAQO,SAAS,WAAW,KAAa,OAAwB;AAC9D,MAAI;AACF,UAAM,GAAG,GAAG,QAAQ,mBAAmB,KAAK,UAAU,KAAK,CAAC;AAAA,EAC9D,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,WAAW,KAAmB;AAC5C,MAAI;AACF,UAAM,GAAG,GAAG,WAAW,iBAAiB;AAAA,EAC1C,QAAQ;AAAA,EAER;AACF;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__publicField
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-6XHCA4U4.js";
|
|
4
4
|
|
|
5
5
|
// src/refs.ts
|
|
6
6
|
var FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/;
|
|
@@ -63,7 +63,14 @@ var MAX_TAG_REPORT = 2e3;
|
|
|
63
63
|
var MAX_LABELS = 500;
|
|
64
64
|
var MAX_PINS = 50;
|
|
65
65
|
var OVERLAY_MODES = ["inspect", "select", "spot", "off"];
|
|
66
|
-
var CAPABILITIES = [
|
|
66
|
+
var CAPABILITIES = [
|
|
67
|
+
"overlay",
|
|
68
|
+
"values",
|
|
69
|
+
"stega",
|
|
70
|
+
"pins",
|
|
71
|
+
"links",
|
|
72
|
+
"refresh"
|
|
73
|
+
];
|
|
67
74
|
var MAX_EMAIL_LENGTH = 320;
|
|
68
75
|
function emptyDropStats() {
|
|
69
76
|
return {
|
|
@@ -200,12 +207,13 @@ var PAGE_VALIDATORS = {
|
|
|
200
207
|
"zap:tags": (p) => Array.isArray(p) && p.length <= MAX_TAG_REPORT && p.every(
|
|
201
208
|
(t) => isObject(t) && typeof t.tag === "string" && isRecordRef(t.recordRef) && isFieldKey(t.fieldKey) && t.tag === `${t.recordRef}#${t.fieldKey}` && Number.isInteger(t.count) && t.count > 0 && (t.source === void 0 || t.source === "stega" || t.source === "attr")
|
|
202
209
|
),
|
|
203
|
-
"zap:click": isFieldRef,
|
|
210
|
+
"zap:click": (p) => isFieldRef(p) && (p.rect === void 0 || isObject(p.rect) && isFiniteNumber(p.rect.x, -1e7, 1e7) && isFiniteNumber(p.rect.y, -1e7, 1e7) && isFiniteNumber(p.rect.w, 0, 1e7) && isFiniteNumber(p.rect.h, 0, 1e7)),
|
|
204
211
|
"zap:select": (p) => isObject(p) && isAnchorList(p.anchors, isPageAnchor),
|
|
205
212
|
"zap:navigate": (p) => isObject(p) && isHttpUrl(p.url),
|
|
206
213
|
"zap:error": (p) => isObject(p) && typeof p.code === "string" && ERROR_CODE_RE.test(p.code) && isOptional(p.detail, (d) => isString(d, 500)),
|
|
207
214
|
"zap:spot": (p) => isObject(p) && isSpotPoint(p.spot) && isObject(p.viewport) && isFiniteNumber(p.viewport.w, 1, 1e5) && isFiniteNumber(p.viewport.h, 1, 1e7) && isHttpUrl(p.pageUrl),
|
|
208
|
-
"zap:pin": (p) => isObject(p) && isPinId(p.id)
|
|
215
|
+
"zap:pin": (p) => isObject(p) && isPinId(p.id),
|
|
216
|
+
"zap:paused": (p) => isObject(p) && (p.reason === "runaway" || p.reason === "cap") && Number.isInteger(p.count) && p.count >= 0
|
|
209
217
|
};
|
|
210
218
|
var EDITOR_VALIDATORS = {
|
|
211
219
|
"zap:hello": (p) => {
|
|
@@ -214,10 +222,11 @@ var EDITOR_VALIDATORS = {
|
|
|
214
222
|
if (p.theme !== void 0 && !isTheme(p.theme)) return false;
|
|
215
223
|
if (p.zoom !== void 0 && !isZoom(p.zoom)) return false;
|
|
216
224
|
if (p.previewToken !== void 0 && !isPreviewTokenShape(p.previewToken)) return false;
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
const within = (map, ok) => isObject(map) && Object.keys(map).length <= MAX_LABELS && Object.entries(map).every(([key, value]) => ok(key, value));
|
|
226
|
+
return within(p.labels, (key, label) => isFieldKey(key) && isString(label, 200)) && within(
|
|
227
|
+
p.foreign ?? {},
|
|
228
|
+
(tag, pair) => !!parseTag(tag) && Array.isArray(pair) && pair.length === 2 && pair.every((part) => isString(part, 200))
|
|
229
|
+
);
|
|
221
230
|
},
|
|
222
231
|
"zap:values": (p) => {
|
|
223
232
|
if (!isObject(p) || !isRecordRef(p.recordRef) || !isLocale(p.locale)) return false;
|
|
@@ -228,6 +237,7 @@ var EDITOR_VALIDATORS = {
|
|
|
228
237
|
"zap:focus-field": isFieldRef,
|
|
229
238
|
"zap:mode": isMode,
|
|
230
239
|
"zap:zoom": (p) => isObject(p) && isZoom(p.zoom),
|
|
240
|
+
"zap:refresh": isObject,
|
|
231
241
|
"zap:highlight": (p) => isObject(p) && isAnchorList(p.anchors, isDomAnchor),
|
|
232
242
|
"zap:pins": (p) => isObject(p) && Array.isArray(p.pins) && p.pins.length <= MAX_PINS && p.pins.every(isPin)
|
|
233
243
|
};
|
|
@@ -272,6 +282,53 @@ function envelope(type, session, payload) {
|
|
|
272
282
|
return { source: PROTOCOL_SOURCE, v: PROTOCOL_VERSION, session, type, payload };
|
|
273
283
|
}
|
|
274
284
|
|
|
285
|
+
// src/stega.ts
|
|
286
|
+
var DIGITS = ["\u200B", "\u200C", "\u200D", "\u2060"];
|
|
287
|
+
var PREFIX = "\u2060\u200D\u200C\u200C\u200D\u200D\u200C\u2060";
|
|
288
|
+
var PAYLOAD_RE = /^(doc:[a-z0-9]+(?:-[a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*)#([a-z][a-z0-9_]{0,127})(?:@([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}))?$/;
|
|
289
|
+
var MARKER_RE = /(?:[][]{3})*/g;
|
|
290
|
+
function hasStega(value) {
|
|
291
|
+
return typeof value === "string" && value.includes(PREFIX);
|
|
292
|
+
}
|
|
293
|
+
function stegaDecode(text) {
|
|
294
|
+
if (!text.includes(PREFIX)) return [];
|
|
295
|
+
const found = [];
|
|
296
|
+
for (const match of text.match(MARKER_RE) ?? []) {
|
|
297
|
+
let payload = "";
|
|
298
|
+
for (let i = PREFIX.length; i + 4 <= match.length; i += 4) {
|
|
299
|
+
let byte = 0;
|
|
300
|
+
for (let j = 0; j < 4; j++) byte = byte * 4 + DIGITS.indexOf(match[i + j]);
|
|
301
|
+
payload += String.fromCharCode(byte);
|
|
302
|
+
}
|
|
303
|
+
const parsed = PAYLOAD_RE.exec(payload);
|
|
304
|
+
if (!parsed) continue;
|
|
305
|
+
found.push({
|
|
306
|
+
recordRef: parsed[1],
|
|
307
|
+
fieldKey: parsed[2],
|
|
308
|
+
locale: parsed[3] ?? null,
|
|
309
|
+
tag: `${parsed[1]}#${parsed[2]}`
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return found;
|
|
313
|
+
}
|
|
314
|
+
function splitStega(text) {
|
|
315
|
+
if (!text.includes(PREFIX)) return { text, markers: "" };
|
|
316
|
+
return { text: text.replace(MARKER_RE, ""), markers: (text.match(MARKER_RE) ?? []).join("") };
|
|
317
|
+
}
|
|
318
|
+
function cleanString(text) {
|
|
319
|
+
return text.includes(PREFIX) ? text.replace(MARKER_RE, "") : text;
|
|
320
|
+
}
|
|
321
|
+
function cleanStega(value) {
|
|
322
|
+
if (typeof value === "string") return cleanString(value);
|
|
323
|
+
if (Array.isArray(value)) return value.map((item) => cleanStega(item));
|
|
324
|
+
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
325
|
+
const out = {};
|
|
326
|
+
for (const [key, item] of Object.entries(value)) out[key] = cleanStega(item);
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
|
|
275
332
|
// src/preview/anchor.ts
|
|
276
333
|
function cssEscape(value) {
|
|
277
334
|
const native = globalThis.CSS?.escape;
|
|
@@ -352,7 +409,7 @@ function fit(selector) {
|
|
|
352
409
|
return utf8Length(selector) <= MAX_SELECTOR_BYTES ? selector : null;
|
|
353
410
|
}
|
|
354
411
|
function normalizeText(value) {
|
|
355
|
-
return value.replace(/\s+/g, " ").trim();
|
|
412
|
+
return cleanStega(value).replace(/\s+/g, " ").trim();
|
|
356
413
|
}
|
|
357
414
|
function textQuoteFor(element, doc) {
|
|
358
415
|
const exact = normalizeText(element.textContent ?? "").slice(0, MAX_QUOTE_EXACT);
|
|
@@ -480,6 +537,7 @@ var Overlay = class {
|
|
|
480
537
|
__publicField(this, "zoom", 1);
|
|
481
538
|
__publicField(this, "theme", { ...DEFAULT_THEME });
|
|
482
539
|
__publicField(this, "labels", {});
|
|
540
|
+
__publicField(this, "foreign", {});
|
|
483
541
|
__publicField(this, "hover", null);
|
|
484
542
|
__publicField(this, "focused", []);
|
|
485
543
|
__publicField(this, "highlighted", []);
|
|
@@ -538,6 +596,7 @@ var Overlay = class {
|
|
|
538
596
|
if (!event.relatedTarget) this.setHover(null);
|
|
539
597
|
});
|
|
540
598
|
this.listen(win, "click", (event) => this.onClick(event));
|
|
599
|
+
this.listen(win, "submit", (event) => this.mode !== "off" && this.consume(event));
|
|
541
600
|
this.listen(win, "keydown", (event) => this.onKey(event));
|
|
542
601
|
this.listen(win, "scroll", () => this.schedule());
|
|
543
602
|
this.listen(win, "resize", () => this.schedule());
|
|
@@ -584,17 +643,17 @@ var Overlay = class {
|
|
|
584
643
|
this.theme = { ...DEFAULT_THEME, ...theme ?? {} };
|
|
585
644
|
this.schedule();
|
|
586
645
|
}
|
|
587
|
-
|
|
646
|
+
/** Labels by field key; `foreign`, by full tag, the fields of other records (`HelloPayload.foreign`). */
|
|
647
|
+
setLabels(labels, foreign = {}) {
|
|
588
648
|
this.labels = { ...labels };
|
|
649
|
+
this.foreign = { ...foreign };
|
|
589
650
|
this.schedule();
|
|
590
651
|
}
|
|
591
652
|
/** Outline every element of a field and scroll the first into view. */
|
|
592
653
|
focusField(elements) {
|
|
593
654
|
this.focused = elements.slice();
|
|
594
655
|
const first = elements[0];
|
|
595
|
-
if (first
|
|
596
|
-
first.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
597
|
-
}
|
|
656
|
+
if (first) reveal(this.options.win, first);
|
|
598
657
|
this.schedule();
|
|
599
658
|
return elements.length;
|
|
600
659
|
}
|
|
@@ -602,9 +661,7 @@ var Overlay = class {
|
|
|
602
661
|
highlight(elements) {
|
|
603
662
|
this.highlighted = elements.slice(0, MAX_ANCHORS);
|
|
604
663
|
const first = this.highlighted[0];
|
|
605
|
-
if (first
|
|
606
|
-
first.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
607
|
-
}
|
|
664
|
+
if (first) reveal(this.options.win, first);
|
|
608
665
|
this.schedule();
|
|
609
666
|
}
|
|
610
667
|
/** Draw these pins (≤ 50); drops the pending spot pin. */
|
|
@@ -672,6 +729,7 @@ var Overlay = class {
|
|
|
672
729
|
}
|
|
673
730
|
onClick(event) {
|
|
674
731
|
if (this.mode === "off" || this.isOwn(event.target)) return;
|
|
732
|
+
if (event.target.closest?.("a[href],area[href]")) this.consume(event);
|
|
675
733
|
if (this.mode === "spot") {
|
|
676
734
|
this.consume(event);
|
|
677
735
|
return this.spotAt(event);
|
|
@@ -758,7 +816,8 @@ var Overlay = class {
|
|
|
758
816
|
if (seen.has(element) || !element.isConnected) return;
|
|
759
817
|
seen.add(element);
|
|
760
818
|
const tagged = this.options.index.get(element);
|
|
761
|
-
|
|
819
|
+
const [label, suffix] = tagged ? this.labelFor(tagged) : [null];
|
|
820
|
+
boxes.push({ element, kind, label, suffix });
|
|
762
821
|
};
|
|
763
822
|
if (this.mode !== "off") {
|
|
764
823
|
for (const element of this.selected) add(element, "selected");
|
|
@@ -788,6 +847,12 @@ var Overlay = class {
|
|
|
788
847
|
}
|
|
789
848
|
}
|
|
790
849
|
layer.replaceChildren(...nodes);
|
|
850
|
+
const inset = EDGE_PX / this.zoom;
|
|
851
|
+
for (const chip of layer.querySelectorAll(".chip")) {
|
|
852
|
+
const { left, right } = chip.getBoundingClientRect();
|
|
853
|
+
const dx = Math.min(Math.max(0, inset - left), this.options.win.innerWidth - inset - right);
|
|
854
|
+
chip.style.transform = `translateX(${dx}px)`;
|
|
855
|
+
}
|
|
791
856
|
}
|
|
792
857
|
/** A gold teardrop whose point (bottom-left) sits on `[x, y]`; without a pin, the pending one. */
|
|
793
858
|
drawPin([x, y], pin) {
|
|
@@ -795,9 +860,12 @@ var Overlay = class {
|
|
|
795
860
|
node.className = "pin";
|
|
796
861
|
const z = this.zoom;
|
|
797
862
|
const size = PIN_PX / z;
|
|
863
|
+
const vw = this.options.win.innerWidth;
|
|
864
|
+
const below = y >= 0 && y - size < EDGE_PX / z;
|
|
865
|
+
if (below) node.style.borderRadius = "0 999px 999px";
|
|
798
866
|
Object.assign(node.style, {
|
|
799
|
-
left: `${x}px`,
|
|
800
|
-
top: `${y - size}px`,
|
|
867
|
+
left: `${x <= vw ? Math.min(x, vw - size - EDGE_PX / z) : x}px`,
|
|
868
|
+
top: `${below ? y : y - size}px`,
|
|
801
869
|
width: `${size}px`,
|
|
802
870
|
height: `${size}px`,
|
|
803
871
|
fontSize: `${11 / z}px`,
|
|
@@ -814,8 +882,8 @@ var Overlay = class {
|
|
|
814
882
|
return node;
|
|
815
883
|
}
|
|
816
884
|
labelFor(tagged) {
|
|
817
|
-
const label = this.labels[tagged.fieldKey];
|
|
818
|
-
return typeof label === "string" && label.trim() ? label : tagged.fieldKey;
|
|
885
|
+
const [label, suffix] = this.foreign[tagged.tag] ?? [this.labels[tagged.fieldKey]];
|
|
886
|
+
return [typeof label === "string" && label.trim() ? label : tagged.fieldKey, suffix];
|
|
819
887
|
}
|
|
820
888
|
drawBox(box) {
|
|
821
889
|
const doc = this.options.doc;
|
|
@@ -839,9 +907,11 @@ var Overlay = class {
|
|
|
839
907
|
if (box.label && box.kind !== "highlight" && !tooSmall) {
|
|
840
908
|
const chip = doc.createElement("span");
|
|
841
909
|
chip.className = "chip";
|
|
910
|
+
const lift = 27 / z;
|
|
911
|
+
const above = rect.top - offset - lift >= EDGE_PX / z || rect.bottom + lift > this.options.win.innerHeight;
|
|
842
912
|
Object.assign(chip.style, {
|
|
843
913
|
right: px(-5),
|
|
844
|
-
top: px(-27),
|
|
914
|
+
[above ? "top" : "bottom"]: px(-27),
|
|
845
915
|
height: px(CHIP_HEIGHT_PX),
|
|
846
916
|
padding: `0 ${px(7)}`,
|
|
847
917
|
gap: px(4),
|
|
@@ -859,13 +929,26 @@ var Overlay = class {
|
|
|
859
929
|
chip.appendChild(icon);
|
|
860
930
|
}
|
|
861
931
|
chip.appendChild(doc.createTextNode(box.label));
|
|
932
|
+
if (box.suffix) {
|
|
933
|
+
const muted = doc.createElement("span");
|
|
934
|
+
muted.className = "of";
|
|
935
|
+
muted.textContent = box.suffix;
|
|
936
|
+
chip.appendChild(muted);
|
|
937
|
+
}
|
|
862
938
|
node.appendChild(chip);
|
|
863
939
|
}
|
|
864
940
|
return node;
|
|
865
941
|
}
|
|
866
942
|
};
|
|
867
943
|
var LARGE_SHARE = 0.5;
|
|
944
|
+
function reveal(win, element) {
|
|
945
|
+
const { top, bottom, height } = element.getBoundingClientRect();
|
|
946
|
+
const h = win.innerHeight;
|
|
947
|
+
if (top < 0 || bottom > h)
|
|
948
|
+
win.scrollTo?.({ top: win.scrollY + top - Math.max(0, (h - height) / 2), behavior: "smooth" });
|
|
949
|
+
}
|
|
868
950
|
var CHIP_HEIGHT_PX = 20;
|
|
951
|
+
var EDGE_PX = 4;
|
|
869
952
|
var PIN_PX = 22;
|
|
870
953
|
var STYLES = `
|
|
871
954
|
.layer { position: fixed; inset: 0; pointer-events: none; }
|
|
@@ -875,6 +958,7 @@ var STYLES = `
|
|
|
875
958
|
font-family: Poppins, ui-sans-serif, system-ui, sans-serif; font-weight: 600; line-height: 1;
|
|
876
959
|
letter-spacing: 0; box-sizing: border-box;
|
|
877
960
|
}
|
|
961
|
+
.of { font-weight: 400; opacity: .72; }
|
|
878
962
|
.capture { position: fixed; inset: 0; pointer-events: auto; cursor: crosshair; display: none; }
|
|
879
963
|
.pin {
|
|
880
964
|
position: fixed; box-sizing: border-box; pointer-events: auto; cursor: pointer; text-align: center;
|
|
@@ -885,53 +969,6 @@ var STYLES = `
|
|
|
885
969
|
.icon svg { width: 100%; height: 100%; display: block; }
|
|
886
970
|
`;
|
|
887
971
|
|
|
888
|
-
// src/stega.ts
|
|
889
|
-
var DIGITS = ["\u200B", "\u200C", "\u200D", "\u2060"];
|
|
890
|
-
var PREFIX = "\u2060\u200D\u200C\u200C\u200D\u200D\u200C\u2060";
|
|
891
|
-
var PAYLOAD_RE = /^(doc:[a-z0-9]+(?:-[a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*)#([a-z][a-z0-9_]{0,127})(?:@([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}))?$/;
|
|
892
|
-
var MARKER_RE = /(?:[][]{3})*/g;
|
|
893
|
-
function hasStega(value) {
|
|
894
|
-
return typeof value === "string" && value.includes(PREFIX);
|
|
895
|
-
}
|
|
896
|
-
function stegaDecode(text) {
|
|
897
|
-
if (!text.includes(PREFIX)) return [];
|
|
898
|
-
const found = [];
|
|
899
|
-
for (const match of text.match(MARKER_RE) ?? []) {
|
|
900
|
-
let payload = "";
|
|
901
|
-
for (let i = PREFIX.length; i + 4 <= match.length; i += 4) {
|
|
902
|
-
let byte = 0;
|
|
903
|
-
for (let j = 0; j < 4; j++) byte = byte * 4 + DIGITS.indexOf(match[i + j]);
|
|
904
|
-
payload += String.fromCharCode(byte);
|
|
905
|
-
}
|
|
906
|
-
const parsed = PAYLOAD_RE.exec(payload);
|
|
907
|
-
if (!parsed) continue;
|
|
908
|
-
found.push({
|
|
909
|
-
recordRef: parsed[1],
|
|
910
|
-
fieldKey: parsed[2],
|
|
911
|
-
locale: parsed[3] ?? null,
|
|
912
|
-
tag: `${parsed[1]}#${parsed[2]}`
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
return found;
|
|
916
|
-
}
|
|
917
|
-
function splitStega(text) {
|
|
918
|
-
if (!text.includes(PREFIX)) return { text, markers: "" };
|
|
919
|
-
return { text: text.replace(MARKER_RE, ""), markers: (text.match(MARKER_RE) ?? []).join("") };
|
|
920
|
-
}
|
|
921
|
-
function cleanString(text) {
|
|
922
|
-
return text.includes(PREFIX) ? text.replace(MARKER_RE, "") : text;
|
|
923
|
-
}
|
|
924
|
-
function cleanStega(value) {
|
|
925
|
-
if (typeof value === "string") return cleanString(value);
|
|
926
|
-
if (Array.isArray(value)) return value.map((item) => cleanStega(item));
|
|
927
|
-
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
928
|
-
const out = {};
|
|
929
|
-
for (const [key, item] of Object.entries(value)) out[key] = cleanStega(item);
|
|
930
|
-
return out;
|
|
931
|
-
}
|
|
932
|
-
return value;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
972
|
// src/preview/tags.ts
|
|
936
973
|
var TAG_SELECTOR = "[data-zap],[data-zap-field]";
|
|
937
974
|
function readElementTag(element) {
|
|
@@ -1065,6 +1102,17 @@ var TagIndex = class {
|
|
|
1065
1102
|
})).sort((a, b) => a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0);
|
|
1066
1103
|
}
|
|
1067
1104
|
};
|
|
1105
|
+
var MAX_TAGGED = 5e3;
|
|
1106
|
+
var RUNAWAY_PASSES = 3;
|
|
1107
|
+
function tagGuard() {
|
|
1108
|
+
let last = 0;
|
|
1109
|
+
let grew = 0;
|
|
1110
|
+
return (count, page) => {
|
|
1111
|
+
grew = count > last && !page ? grew + 1 : 0;
|
|
1112
|
+
last = count;
|
|
1113
|
+
return count > MAX_TAGGED ? "cap" : grew >= RUNAWAY_PASSES ? "runaway" : null;
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1068
1116
|
function observeTags(target, onChange, options = {}) {
|
|
1069
1117
|
const throttleMs = options.throttleMs ?? 250;
|
|
1070
1118
|
const set = options.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
@@ -1075,17 +1123,24 @@ function observeTags(target, onChange, options = {}) {
|
|
|
1075
1123
|
};
|
|
1076
1124
|
let timer = null;
|
|
1077
1125
|
let last = -Infinity;
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
last = Date.now();
|
|
1081
|
-
onChange();
|
|
1082
|
-
};
|
|
1083
|
-
const observer = new Observer((records) => {
|
|
1126
|
+
let page = false;
|
|
1127
|
+
const queue = (records, fromPage) => {
|
|
1084
1128
|
if (options.ignore && records.every((record) => options.ignore(record.target))) return;
|
|
1129
|
+
page || (page = fromPage);
|
|
1085
1130
|
if (timer !== null) return;
|
|
1086
1131
|
const wait = Math.max(0, last + throttleMs - Date.now());
|
|
1087
1132
|
timer = set(fire, Math.max(wait, 16));
|
|
1088
|
-
}
|
|
1133
|
+
};
|
|
1134
|
+
const fire = () => {
|
|
1135
|
+
timer = null;
|
|
1136
|
+
last = Date.now();
|
|
1137
|
+
const changed = page;
|
|
1138
|
+
page = false;
|
|
1139
|
+
onChange(changed);
|
|
1140
|
+
const own = observer.takeRecords();
|
|
1141
|
+
if (own.length) queue(own, false);
|
|
1142
|
+
};
|
|
1143
|
+
const observer = new Observer((records) => queue(records, true));
|
|
1089
1144
|
observer.observe(target, {
|
|
1090
1145
|
subtree: true,
|
|
1091
1146
|
childList: true,
|
|
@@ -1099,6 +1154,15 @@ function observeTags(target, onChange, options = {}) {
|
|
|
1099
1154
|
};
|
|
1100
1155
|
}
|
|
1101
1156
|
|
|
1157
|
+
// src/preview/site/shortcut.ts
|
|
1158
|
+
var DEFAULT_SHORTCUT = "Z";
|
|
1159
|
+
var TYPING = 'input,textarea,select,[contenteditable]:not([contenteditable="false"])';
|
|
1160
|
+
function isShortcut(event, key) {
|
|
1161
|
+
const target = event.composedPath?.()[0] ?? event.target;
|
|
1162
|
+
return event.shiftKey && !(event.ctrlKey || event.metaKey || event.altKey || event.repeat || event.isComposing) && // Some browsers report a key inside an IME composition only as keyCode 229.
|
|
1163
|
+
event.keyCode !== 229 && event.key?.toLowerCase() === key.toLowerCase() && !target?.closest?.(TYPING);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1102
1166
|
export {
|
|
1103
1167
|
FIELD_KEY_RE,
|
|
1104
1168
|
parseRecordRef,
|
|
@@ -1141,6 +1205,8 @@ export {
|
|
|
1141
1205
|
parseEditorMessage,
|
|
1142
1206
|
parsePageMessage,
|
|
1143
1207
|
envelope,
|
|
1208
|
+
splitStega,
|
|
1209
|
+
cleanStega,
|
|
1144
1210
|
cssEscape,
|
|
1145
1211
|
safeQueryAll,
|
|
1146
1212
|
buildSelector,
|
|
@@ -1152,11 +1218,13 @@ export {
|
|
|
1152
1218
|
findAnchorElement,
|
|
1153
1219
|
OVERLAY_HOST_TAG,
|
|
1154
1220
|
Overlay,
|
|
1155
|
-
|
|
1156
|
-
cleanStega,
|
|
1221
|
+
reveal,
|
|
1157
1222
|
TAG_SELECTOR,
|
|
1158
1223
|
readElementTag,
|
|
1159
1224
|
TagIndex,
|
|
1160
|
-
|
|
1225
|
+
tagGuard,
|
|
1226
|
+
observeTags,
|
|
1227
|
+
DEFAULT_SHORTCUT,
|
|
1228
|
+
isShortcut
|
|
1161
1229
|
};
|
|
1162
|
-
//# sourceMappingURL=chunk-
|
|
1230
|
+
//# sourceMappingURL=chunk-7CXQK2PR.js.map
|