@panphora/clayjs 1.8.1 → 1.9.0
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/dist/clay.standalone.js +2477 -1549
- package/package.json +8 -4
- package/src/core/host-attrs.js +11 -0
- package/src/core/host-meta.js +47 -12
- package/src/core/is-edit-mode.js +5 -2
- package/src/events/onpagemutation.js +4 -2
- package/src/lib/root-attrs.js +6 -1
- package/src/loader-logic.js +4 -1
- package/src/loader.js +11 -3
- package/src/plugins/people.js +424 -0
- package/src/plugins/sortable.js +68 -0
- package/src/plugins/upload.js +6 -0
- package/src/ui/bevel-controls.js +70 -10
- package/src/ui/bevel-dialog.js +70 -21
- package/src/ui/dialogs.js +16 -8
- package/src/ui/index.js +3 -0
- package/src/ui/modal.js +18 -3
- package/src/ui/theme-parts.js +122 -0
- package/src/ui/theme.js +277 -0
- package/src/vendor/hyper-morph.vendor.js +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clayjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
|
|
6
6
|
"license": "MIT-0",
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
|
-
"test": "
|
|
49
|
+
"test": "node scripts/test-guard.mjs",
|
|
50
50
|
"test:conformance": "UPDATE_GOLDENS=0 web-test-runner --config conformance/wtr.config.mjs",
|
|
51
51
|
"conformance:update": "UPDATE_GOLDENS=1 web-test-runner --config conformance/wtr.config.mjs",
|
|
52
52
|
"build:standalone": "node build.js --standalone",
|
|
53
53
|
"prepack": "node build.js --standalone",
|
|
54
54
|
"test:standalone": "node build.js --standalone && web-test-runner --config tests/browser/wtr.config.mjs",
|
|
55
|
-
"prepublishOnly": "
|
|
55
|
+
"prepublishOnly": "testbox run",
|
|
56
56
|
"dev": "node tests/stub-server.mjs",
|
|
57
57
|
"build": "npm run build:bevel && npm run build:skins && node build.js",
|
|
58
58
|
"propagate:foundation": "node scripts/propagate-foundation.mjs",
|
|
@@ -73,9 +73,13 @@
|
|
|
73
73
|
"esbuild": "^0.28.2",
|
|
74
74
|
"jest": "^29.7.0",
|
|
75
75
|
"jest-environment-jsdom": "^29.7.0",
|
|
76
|
+
"jsdom": "^20.0.3",
|
|
77
|
+
"marked": "^18.0.14",
|
|
76
78
|
"parse5": "^7.3.0",
|
|
77
79
|
"playwright": "1.62.1",
|
|
78
|
-
"postcss": "^8.5.28"
|
|
80
|
+
"postcss": "^8.5.28",
|
|
81
|
+
"turndown": "^7.2.4",
|
|
82
|
+
"turndown-plugin-gfm": "^1.0.2"
|
|
79
83
|
},
|
|
80
84
|
"hyper": {
|
|
81
85
|
"kind": "lib",
|
package/src/core/host-attrs.js
CHANGED
|
@@ -23,6 +23,17 @@ export const servedDocumentEtag =
|
|
|
23
23
|
? null
|
|
24
24
|
: document.documentElement.getAttribute("documentetag") || null;
|
|
25
25
|
|
|
26
|
+
// Whether the host serves this site privately, captured ONCE, for the same reason as
|
|
27
|
+
// the etag above: it describes this response, and a morph may later rewrite the root.
|
|
28
|
+
// null when the host said nothing (HTML Clay, Hyperclay Local, a downloaded copy) or
|
|
29
|
+
// said something this library does not recognize.
|
|
30
|
+
const SITE_VISIBILITIES = new Set(["private", "public"]);
|
|
31
|
+
export const servedSiteVisibility = (() => {
|
|
32
|
+
if (typeof document === "undefined") return null;
|
|
33
|
+
const value = document.documentElement.getAttribute("sitevisibility");
|
|
34
|
+
return SITE_VISIBILITIES.has(value) ? value : null;
|
|
35
|
+
})();
|
|
36
|
+
|
|
26
37
|
let warnedAboutLegacyToken = false;
|
|
27
38
|
|
|
28
39
|
/**
|
package/src/core/host-meta.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* host-meta.js — what this host can do (spec §5).
|
|
3
3
|
*
|
|
4
|
-
* The first discovery client clayjs has had. Not fetched at boot: the first
|
|
5
|
-
* caller is the first file pick,
|
|
4
|
+
* The first discovery client clayjs has had. Not fetched at boot by core: the first
|
|
5
|
+
* caller is the first file pick, or the people plugin when a page asks for it, so a
|
|
6
|
+
* page that uses neither never asks.
|
|
6
7
|
*
|
|
7
8
|
* The spec's own rule about what counts as an answer is deliberately strict on
|
|
8
9
|
* the way in and forgiving on the way out. Only a 2xx carrying a JSON object with
|
|
@@ -17,6 +18,29 @@ import { saveToken } from "./host-attrs.js";
|
|
|
17
18
|
const META_PATH = "/_/meta";
|
|
18
19
|
const META_TIMEOUT_MS = 6000;
|
|
19
20
|
|
|
21
|
+
// How each answer came about, kept beside the answer rather than on it so its
|
|
22
|
+
// shape never changes. "ok": a capability document. "none": no discovery on offer
|
|
23
|
+
// (a 404, a file nobody is serving, a server that has never heard of the spec).
|
|
24
|
+
// "failed": a host that should have answered did not (a 5xx, a timeout, a dropped
|
|
25
|
+
// connection). Save and upload treat the last two alike; people must not, because
|
|
26
|
+
// a host that failed to say who you are has not said you are nobody.
|
|
27
|
+
const outcomes = new WeakMap();
|
|
28
|
+
|
|
29
|
+
function settle(outcome, meta = bareHost()) {
|
|
30
|
+
outcomes.set(meta, outcome);
|
|
31
|
+
return meta;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* How the discovery that produced `meta` ended: "ok", "none" or "failed".
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} meta - an answer from hostMeta()
|
|
38
|
+
* @returns {"ok"|"none"|"failed"}
|
|
39
|
+
*/
|
|
40
|
+
export function hostMetaOutcome(meta) {
|
|
41
|
+
return outcomes.get(meta) ?? "none";
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
// The bare-core-host answer, which is also every failure's answer.
|
|
21
45
|
function bareHost() {
|
|
22
46
|
return { spec: null, extensions: [], document: null };
|
|
@@ -34,8 +58,9 @@ let refreshing = null;
|
|
|
34
58
|
* `spec` and `extensions` describe the host and never change under a loaded
|
|
35
59
|
* document, which is why one answer serves the whole page. The `document` block
|
|
36
60
|
* does change: its `etag` ticks on every save, by anyone. A caller that needs a
|
|
37
|
-
* current one passes `fresh`, which asks again and replaces the memoized answer
|
|
38
|
-
*
|
|
61
|
+
* current one passes `fresh`, which asks again and replaces the memoized answer
|
|
62
|
+
* unless it failed where an earlier one succeeded, and concurrent fresh callers
|
|
63
|
+
* still share one request.
|
|
39
64
|
*
|
|
40
65
|
* @param {Object} [options]
|
|
41
66
|
* @param {boolean} [options.fresh]
|
|
@@ -47,8 +72,15 @@ export function hostMeta({ fresh = false } = {}) {
|
|
|
47
72
|
return inFlight;
|
|
48
73
|
}
|
|
49
74
|
if (!refreshing) {
|
|
75
|
+
const previous = inFlight;
|
|
50
76
|
refreshing = fetchMeta().catch(bareHost).finally(() => { refreshing = null; });
|
|
51
|
-
|
|
77
|
+
// The caller that asked for a fresh answer gets it, failure and all. The page keeps its
|
|
78
|
+
// last good answer: one 502 during a deploy must not turn uploads off for the session.
|
|
79
|
+
inFlight = refreshing.then(async (meta) => {
|
|
80
|
+
if (hostMetaOutcome(meta) === "ok" || !previous) return meta;
|
|
81
|
+
const last = await previous;
|
|
82
|
+
return hostMetaOutcome(last) === "ok" ? last : meta;
|
|
83
|
+
});
|
|
52
84
|
}
|
|
53
85
|
return refreshing;
|
|
54
86
|
}
|
|
@@ -74,6 +106,9 @@ export function resetHostMeta() {
|
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
async function fetchMeta() {
|
|
109
|
+
// A file opened from disk has no host to ask, and fetch would only throw.
|
|
110
|
+
if (!/^https?:$/.test(window.location.protocol)) return settle("none");
|
|
111
|
+
|
|
77
112
|
const token = saveToken();
|
|
78
113
|
// A host that mints tokens answers discovery per token, because on a sandboxed
|
|
79
114
|
// document the token is the only identity there is: the browser gives it an
|
|
@@ -99,25 +134,25 @@ async function fetchMeta() {
|
|
|
99
134
|
redirect: "manual",
|
|
100
135
|
signal: controller.signal
|
|
101
136
|
});
|
|
102
|
-
if (!res.ok) return
|
|
137
|
+
if (!res.ok) return settle(res.status >= 500 ? "failed" : "none");
|
|
103
138
|
const text = await res.text();
|
|
104
|
-
if (!text) return
|
|
139
|
+
if (!text) return settle("none");
|
|
105
140
|
|
|
106
141
|
let body;
|
|
107
142
|
try {
|
|
108
143
|
body = JSON.parse(text);
|
|
109
144
|
} catch (_) {
|
|
110
|
-
return
|
|
145
|
+
return settle("none");
|
|
111
146
|
}
|
|
112
|
-
if (!body || typeof body !== "object" || typeof body.spec !== "number") return
|
|
147
|
+
if (!body || typeof body !== "object" || typeof body.spec !== "number") return settle("none");
|
|
113
148
|
|
|
114
|
-
return {
|
|
149
|
+
return settle("ok", {
|
|
115
150
|
spec: body.spec,
|
|
116
151
|
extensions: Array.isArray(body.extensions) ? body.extensions : [],
|
|
117
152
|
document: body.document && typeof body.document === "object" ? body.document : null
|
|
118
|
-
};
|
|
153
|
+
});
|
|
119
154
|
} catch (_) {
|
|
120
|
-
return
|
|
155
|
+
return settle("failed");
|
|
121
156
|
} finally {
|
|
122
157
|
clearTimeout(timeoutId);
|
|
123
158
|
}
|
package/src/core/is-edit-mode.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cookie from "../lib/cookie.js";
|
|
2
2
|
import query from "../lib/query.js";
|
|
3
|
-
import { hasSaveToken, servedStaleToken } from "./host-attrs.js";
|
|
3
|
+
import { hasSaveToken, servedStaleToken, servedSiteVisibility } from "./host-attrs.js";
|
|
4
4
|
|
|
5
5
|
// Edit-mode precedence: an explicit ?editmode=true|false URL param wins, then an
|
|
6
6
|
// authored `viewonly` attribute on <html> (a tool page that must never save its
|
|
@@ -47,7 +47,10 @@ const isEditMode = query.editmode
|
|
|
47
47
|
|
|
48
48
|
const isOwner = Boolean(cookie.get("isAdminOfCurrentResource"));
|
|
49
49
|
|
|
50
|
+
const siteVisibility = servedSiteVisibility;
|
|
51
|
+
|
|
50
52
|
export {
|
|
51
53
|
isEditMode,
|
|
52
|
-
isOwner
|
|
54
|
+
isOwner,
|
|
55
|
+
siteVisibility
|
|
53
56
|
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
[onpagemutation] / [onglobalmutation] - Trigger code when ANY element on the page changes
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
|
-
<span onglobalmutation="
|
|
6
|
-
<span onpagemutation="
|
|
5
|
+
<span onglobalmutation="const n = String(All('li').length); if (this.textContent !== n) this.textContent = n">0</span>
|
|
6
|
+
<span onpagemutation="const n = String(All('li').length); if (this.textContent !== n) this.textContent = n">0</span>
|
|
7
|
+
|
|
8
|
+
Guard every write: writing into the page, even the same text, is a mutation and re-runs the handler.
|
|
7
9
|
|
|
8
10
|
Both attributes are equivalent - onglobalmutation is the preferred name for clarity.
|
|
9
11
|
*/
|
package/src/lib/root-attrs.js
CHANGED
|
@@ -74,7 +74,12 @@ export const HOST_IDENTITY_ATTRS = ["documentid", "htmlclayid"];
|
|
|
74
74
|
// Kept out of SAVE_TOKEN_ATTRS on purpose: host-attrs.js returns the first name it
|
|
75
75
|
// finds in that list straight into the save URL, and a version stamp is not a
|
|
76
76
|
// credential.
|
|
77
|
-
|
|
77
|
+
//
|
|
78
|
+
// `sitevisibility` is the host telling the owner's tab whether this site is served
|
|
79
|
+
// privately or publicly ("private" | "public"). It describes the response, not the
|
|
80
|
+
// file: the same bytes are private today and public tomorrow, so it must never be
|
|
81
|
+
// written to disk, and a peer tab's copy says nothing about what this tab was served.
|
|
82
|
+
export const HOST_RESPONSE_ATTRS = ["documentetag", "sitevisibility"];
|
|
78
83
|
|
|
79
84
|
// What a host may have injected, and therefore what has to be stripped before a save
|
|
80
85
|
// and kept out of an incoming morph. Wider than what is READ, on purpose: the old token
|
package/src/loader-logic.js
CHANGED
|
@@ -36,6 +36,8 @@ export const PLUGIN_PATHS = {
|
|
|
36
36
|
// injects its own editing toggle there and clayjs's edit-mode signal is a
|
|
37
37
|
// superset of the cms's, so the plugin is present exactly when it can be used.
|
|
38
38
|
upload: { path: "plugins/upload.js", editOnly: true, default: false },
|
|
39
|
+
// Who is editing and who wrote what. Not editOnly: a reader still sees names.
|
|
40
|
+
people: { path: "plugins/people.js", editOnly: false, default: false },
|
|
39
41
|
wire: { path: "plugins/wire.js", editOnly: false, default: false },
|
|
40
42
|
// Edit mode only: the AI comment box is an editing gesture. Opt in with
|
|
41
43
|
// `plugins=ai-edit`; it stays dormant on any host that does not list a ready
|
|
@@ -89,6 +91,7 @@ export const MODULES = {
|
|
|
89
91
|
"plugins/cms.js": () => import("./plugins/cms.js"),
|
|
90
92
|
"plugins/quickcrop.js": () => import("./plugins/quickcrop.js"),
|
|
91
93
|
"plugins/upload.js": () => import("./plugins/upload.js"),
|
|
94
|
+
"plugins/people.js": () => import("./plugins/people.js"),
|
|
92
95
|
"plugins/wire.js": () => import("./plugins/wire.js"),
|
|
93
96
|
"plugins/ai-edit.js": () => import("./plugins/ai-edit.js"),
|
|
94
97
|
"plugins/demo.js": () => import("./plugins/demo.js"),
|
|
@@ -97,7 +100,7 @@ export const MODULES = {
|
|
|
97
100
|
|
|
98
101
|
// `source` is last on purpose: its install captures a save clone, so it wants every
|
|
99
102
|
// plugin that registers a document transform to have registered it first.
|
|
100
|
-
const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "quickcrop", "upload", "cms", "sync", "wire", "ai-edit", "demo", "source"];
|
|
103
|
+
const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "quickcrop", "upload", "people", "cms", "sync", "wire", "ai-edit", "demo", "source"];
|
|
101
104
|
|
|
102
105
|
// A plugin that cannot do its whole job alone. hypercms reads the cropper through
|
|
103
106
|
// a capability lookup (`clay.quickcrop`) and silently uploads the raw file when it
|
package/src/loader.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveModules, MODULES } from "./loader-logic.js";
|
|
2
2
|
import onDomReady from "./lib/dom-ready.js";
|
|
3
|
+
import { initTheme } from "./ui/theme.js";
|
|
3
4
|
|
|
4
5
|
function domReady() {
|
|
5
6
|
return new Promise((resolve) => onDomReady(resolve));
|
|
@@ -11,12 +12,14 @@ function domReady() {
|
|
|
11
12
|
// previous release. A call shape that differs between them leaves the page with
|
|
12
13
|
// no clayjs at all.
|
|
13
14
|
export async function boot(base, params, readyResolve) {
|
|
15
|
+
initTheme();
|
|
16
|
+
|
|
14
17
|
await domReady(); // Mutation observes document.body
|
|
15
18
|
// unconditionally, and a <head> placement
|
|
16
19
|
// would otherwise observe null
|
|
17
20
|
|
|
18
21
|
const editMode = await MODULES["core/is-edit-mode.js"]();
|
|
19
|
-
const { isEditMode, isOwner } = editMode;
|
|
22
|
+
const { isEditMode, isOwner, siteVisibility } = editMode;
|
|
20
23
|
|
|
21
24
|
// richclay's vendor build detects edit mode via this legacy global; set it
|
|
22
25
|
// before any plugin import so its autoInit sees the right value. Overwrite
|
|
@@ -33,7 +36,7 @@ export async function boot(base, params, readyResolve) {
|
|
|
33
36
|
loaded[path] = await MODULES[path](); // sequential: order is load-bearing
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
assembleCore(loaded, { isEditMode, isOwner }, regionPolicy); // window.clay MUST be assembled
|
|
39
|
+
assembleCore(loaded, { isEditMode, isOwner, siteVisibility }, regionPolicy); // window.clay MUST be assembled
|
|
37
40
|
// before any plugin import
|
|
38
41
|
|
|
39
42
|
for (const path of plan.plugins) {
|
|
@@ -65,7 +68,7 @@ export async function boot(base, params, readyResolve) {
|
|
|
65
68
|
document.dispatchEvent(new CustomEvent("clay:ready", { detail: { clay: window.clay } }));
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
|
|
71
|
+
function assembleCore(loaded, { isEditMode, isOwner, siteVisibility }, regionPolicy) {
|
|
69
72
|
const clay = window.clay;
|
|
70
73
|
|
|
71
74
|
const mutation = loaded["lib/mutation.js"];
|
|
@@ -75,6 +78,7 @@ function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
|
|
|
75
78
|
toggleEditMode: editModeMod.toggleEditMode,
|
|
76
79
|
isEditMode,
|
|
77
80
|
isOwner,
|
|
81
|
+
siteVisibility,
|
|
78
82
|
Mutation: mutation.default,
|
|
79
83
|
region: regionPolicy.windowRegionShape,
|
|
80
84
|
});
|
|
@@ -124,6 +128,10 @@ function attachPluginMember(path, mod, loaded) {
|
|
|
124
128
|
clay.cms = mod.cms || mod.default;
|
|
125
129
|
} else if (path === "plugins/upload.js") {
|
|
126
130
|
clay.upload = mod.upload || mod.default;
|
|
131
|
+
} else if (path === "plugins/people.js") {
|
|
132
|
+
clay.people = mod.people;
|
|
133
|
+
clay.author = mod.author;
|
|
134
|
+
Object.defineProperty(clay, "me", { get: mod.currentMe, configurable: true, enumerable: true });
|
|
127
135
|
} else if (path === "plugins/wire.js") {
|
|
128
136
|
clay.wire = mod.wire || mod.default;
|
|
129
137
|
} else if (path === "plugins/ai-edit.js") {
|