@panphora/clayjs 1.11.3 → 1.12.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.
@@ -0,0 +1,178 @@
1
+ import { isDraftMode } from "../../core/is-edit-mode.js";
2
+ import { BOTTOM_UI_CHANGED, announceBottomUi, bottomBarHeight, promptPanel } from "./bottom-ui.js";
3
+
4
+ export const COPY = {
5
+ draft: "You're seeing a private draft, like a visitor.",
6
+ draftAction: "Edit the original",
7
+ edit: "You're editing the original. Visitors get these changes.",
8
+ editAction: "Back to my draft",
9
+ close: "Hide this note",
10
+ };
11
+
12
+ const CHIP_CSS = `.clay-owner {
13
+ --hcp-ground: #FDF8F0;
14
+ --hcp-surface: #FFFCF6;
15
+ --hcp-sunk: #F4ECDF;
16
+ --hcp-ink: #2B241B;
17
+ --hcp-ink-2: #4A4033;
18
+ --hcp-muted: #6B5942;
19
+ --hcp-line: #E8DCC6;
20
+ --hcp-line-2: #D8C8AF;
21
+ --hcp-brass: #8F6310;
22
+ --hcp-shadow: 0 1px 2px rgba(23, 28, 36, .06), 0 14px 34px -14px rgba(43, 36, 27, .32);
23
+ --hcp-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
24
+ --hcp-lift: 0px;
25
+ color-scheme: light;
26
+ }
27
+ @media (prefers-color-scheme: dark) {
28
+ .clay-owner {
29
+ --hcp-surface: #11131E; --hcp-sunk: #1A1D2C;
30
+ --hcp-ink: #ECEAF2; --hcp-ink-2: #C9CDE0; --hcp-muted: #8A90AB;
31
+ --hcp-line: #262B42; --hcp-line-2: #353B52;
32
+ --hcp-brass: #D9A445;
33
+ --hcp-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 38px -14px rgba(0, 0, 0, .7);
34
+ color-scheme: dark;
35
+ }
36
+ }
37
+ .clay-owner, .clay-owner *, .clay-owner *::before, .clay-owner *::after {
38
+ box-sizing: border-box; margin: 0; padding: 0; border: 0;
39
+ font: inherit; letter-spacing: normal; text-transform: none; text-shadow: none;
40
+ line-height: inherit; color: inherit; background: none; float: none; max-width: none;
41
+ }
42
+ .clay-owner {
43
+ position: fixed; left: 12px; bottom: calc(12px + var(--hcp-lift)); z-index: 2147482000;
44
+ display: flex; align-items: center; gap: 10px;
45
+ max-width: min(460px, calc(100vw - 24px));
46
+ padding: 8px 8px 8px 12px;
47
+ font-family: var(--hcp-sans); font-size: 13px; font-weight: 400; line-height: 1.45;
48
+ color: var(--hcp-ink-2); text-align: left; background: var(--hcp-surface);
49
+ border: 1px solid var(--hcp-line-2); border-left: 3px solid var(--hcp-brass);
50
+ box-shadow: var(--hcp-shadow);
51
+ -webkit-font-smoothing: antialiased;
52
+ }
53
+ .clay-owner__action {
54
+ flex: none; min-height: 0; font-weight: 600; color: var(--hcp-brass); cursor: pointer;
55
+ text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--hcp-line-2);
56
+ white-space: nowrap;
57
+ }
58
+ .clay-owner .clay-owner__action:hover { color: var(--hcp-ink); background: none; }
59
+ .clay-owner__x {
60
+ flex: none; display: inline-flex; align-items: center; justify-content: center;
61
+ width: 24px; height: 24px; min-height: 0; font-size: 15px; color: var(--hcp-muted); cursor: pointer;
62
+ }
63
+ .clay-owner .clay-owner__x:hover { color: var(--hcp-ink); background: none; }
64
+ .clay-owner__action:focus-visible, .clay-owner__x:focus-visible { outline: 2px solid var(--hcp-brass); outline-offset: 2px; }
65
+
66
+ .clay-owner svg {
67
+ display: block; flex: none; width: 1em; height: 1em; line-height: 1;
68
+ fill: currentColor; stroke: none; stroke-width: 0; stroke-linecap: butt; stroke-linejoin: miter;
69
+ }
70
+ .clay-owner button, .clay-owner a { border-radius: 0; line-height: 1.45; }
71
+ @media (pointer: coarse) {
72
+ .clay-owner__action, .clay-owner__x { min-height: 44px; }
73
+ .clay-owner__x { width: 44px; }
74
+ }
75
+ @media (prefers-reduced-motion: reduce) { .clay-owner { transition: none; } }
76
+ @media (forced-colors: active) { .clay-owner { border: 1px solid CanvasText; } }
77
+ @media print { .clay-owner { display: none; } }
78
+ `;
79
+
80
+ function stylesOnce() {
81
+ if (document.getElementById("clay-owner-style")) return;
82
+ const style = document.createElement("style");
83
+ style.id = "clay-owner-style";
84
+ style.setAttribute("clay", "editor-ui");
85
+ style.textContent = CHIP_CSS;
86
+ document.head.appendChild(style);
87
+ }
88
+
89
+ export function mountOwnerChip() {
90
+ if (document.getElementById("clay-owner")) return null;
91
+ stylesOnce();
92
+
93
+ const draft = isDraftMode;
94
+ const chip = document.createElement("div");
95
+ chip.id = "clay-owner";
96
+ chip.className = "clay-owner";
97
+ chip.setAttribute("clay", "editor-ui");
98
+ chip.setAttribute("sap-ignore", "");
99
+ chip.setAttribute("role", "status");
100
+
101
+ const lead = document.createElement("span");
102
+ lead.className = "clay-owner__lead";
103
+ lead.textContent = draft ? COPY.draft : COPY.edit;
104
+
105
+ const action = document.createElement("button");
106
+ action.type = "button";
107
+ action.className = "clay-owner__action";
108
+ action.textContent = draft ? COPY.draftAction : COPY.editAction;
109
+ action.addEventListener("click", () => {
110
+ const api = window.clay && window.clay.visitorDrafts;
111
+ if (!api) return;
112
+ if (draft) api.editOriginal();
113
+ else api.backToDraft();
114
+ });
115
+
116
+ const close = document.createElement("button");
117
+ close.type = "button";
118
+ close.className = "clay-owner__x";
119
+ close.setAttribute("aria-label", COPY.close);
120
+ close.textContent = "\u00d7";
121
+ close.addEventListener("click", () => { chip.remove(); liftOff(); release(); });
122
+
123
+ chip.append(lead, action, close);
124
+ document.body.appendChild(chip);
125
+
126
+ const vars = document.createElement("style");
127
+ vars.id = "clay-owner-vars";
128
+ vars.setAttribute("clay", "editor-ui");
129
+ document.head.appendChild(vars);
130
+
131
+ let published = "";
132
+ function publish(lift) {
133
+ const next = `:root { --clay-owner-room: ${Math.ceil(chip.getBoundingClientRect().height) + 16}px; }`;
134
+ if (vars.textContent !== next) vars.textContent = next;
135
+ if (published === next + lift) return;
136
+ published = next + lift;
137
+ announceBottomUi();
138
+ }
139
+
140
+ function release() {
141
+ vars.remove();
142
+ announceBottomUi();
143
+ }
144
+
145
+ let frame = 0;
146
+ function lift() {
147
+ if (frame) return;
148
+ frame = requestAnimationFrame(() => {
149
+ frame = 0;
150
+ let lift = bottomBarHeight(24, (el) => chip.contains(el) || el.closest("#hcp"));
151
+ const panel = promptPanel(chip.getBoundingClientRect());
152
+ const covered = panel >= window.innerHeight / 3;
153
+ if (panel && !covered) lift = Math.max(lift, panel - 4);
154
+ chip.style.visibility = covered ? "hidden" : "";
155
+ chip.style.setProperty("--hcp-lift", lift + "px");
156
+ publish(covered ? -1 : lift);
157
+ });
158
+ }
159
+
160
+ function liftOff() {
161
+ removeEventListener("scroll", lift);
162
+ removeEventListener("resize", lift);
163
+ document.removeEventListener(BOTTOM_UI_CHANGED, lift);
164
+ document.removeEventListener("transitionend", lift, true);
165
+ document.removeEventListener("pointerup", lift, true);
166
+ window.removeEventListener("hashchange", lift);
167
+ }
168
+
169
+ addEventListener("scroll", lift, { passive: true });
170
+ addEventListener("resize", lift);
171
+ document.addEventListener(BOTTOM_UI_CHANGED, lift);
172
+ document.addEventListener("transitionend", lift, true);
173
+ document.addEventListener("pointerup", lift, true);
174
+ window.addEventListener("hashchange", lift);
175
+ lift();
176
+
177
+ return chip;
178
+ }
@@ -0,0 +1,198 @@
1
+ export const PROMPT_CSS = `.hcp {
2
+ --hcp-ground: #FDF8F0;
3
+ --hcp-surface: #FFFCF6;
4
+ --hcp-sunk: #F4ECDF;
5
+ --hcp-ink: #2B241B;
6
+ --hcp-ink-2: #4A4033;
7
+ --hcp-muted: #6B5942;
8
+ --hcp-line: #E8DCC6;
9
+ --hcp-line-2: #D8C8AF;
10
+ --hcp-brass: #8F6310;
11
+ --hcp-brass-soft: #F0E4CC;
12
+ --hcp-face: #F1E7D4;
13
+ --hcp-edge-hi: #FFF9ED;
14
+ --hcp-edge-lo: #D6C3A5;
15
+ --hcp-shadow: 0 1px 2px rgba(23, 28, 36, .06), 0 14px 34px -14px rgba(43, 36, 27, .32);
16
+ --hcp-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
17
+ --hcp-serif: Newsreader, Georgia, "Times New Roman", serif;
18
+ --hcp-mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
19
+ --hcp-lift: 0px;
20
+ color-scheme: light;
21
+ }
22
+ @media (prefers-color-scheme: dark) {
23
+ .hcp:not([data-hcp-theme="light"]) {
24
+ --hcp-ground: #0B0C13; --hcp-surface: #11131E; --hcp-sunk: #1A1D2C;
25
+ --hcp-ink: #ECEAF2; --hcp-ink-2: #C9CDE0; --hcp-muted: #8A90AB;
26
+ --hcp-line: #262B42; --hcp-line-2: #353B52;
27
+ --hcp-brass: #D9A445; --hcp-brass-soft: #3A2F17;
28
+ --hcp-face: #22273E; --hcp-edge-hi: #3C4260; --hcp-edge-lo: #1B2033;
29
+ --hcp-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 38px -14px rgba(0, 0, 0, .7);
30
+ color-scheme: dark;
31
+ }
32
+ }
33
+ .hcp[data-hcp-theme="dark"] {
34
+ --hcp-ground: #0B0C13; --hcp-surface: #11131E; --hcp-sunk: #1A1D2C;
35
+ --hcp-ink: #ECEAF2; --hcp-ink-2: #C9CDE0; --hcp-muted: #8A90AB;
36
+ --hcp-line: #262B42; --hcp-line-2: #353B52;
37
+ --hcp-brass: #D9A445; --hcp-brass-soft: #3A2F17;
38
+ --hcp-face: #22273E; --hcp-edge-hi: #3C4260; --hcp-edge-lo: #1B2033;
39
+ --hcp-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 38px -14px rgba(0, 0, 0, .7);
40
+ color-scheme: dark;
41
+ }
42
+
43
+ .hcp, .hcp * , .hcp *::before, .hcp *::after {
44
+ box-sizing: border-box; margin: 0; padding: 0; border: 0;
45
+ font: inherit; letter-spacing: normal; text-transform: none; text-shadow: none;
46
+ line-height: inherit; color: inherit; background: none; float: none; max-width: none;
47
+ }
48
+ .hcp {
49
+ position: fixed; right: 16px; bottom: calc(16px + var(--hcp-lift)); z-index: 2147482000;
50
+ display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
51
+ font-family: var(--hcp-sans); font-size: 14px; font-weight: 400; line-height: 1.5;
52
+ color: var(--hcp-ink); text-align: left;
53
+ -webkit-font-smoothing: antialiased;
54
+ transition: bottom .25s ease, opacity .2s ease;
55
+ pointer-events: none;
56
+ }
57
+ .hcp[hidden], .hcp[data-hcp-state="dismissed"], .hcp[data-hcp-state="off"] { display: none; }
58
+ .hcp[data-hcp-typing] { opacity: 0; visibility: hidden; }
59
+ .hcp > * { pointer-events: auto; }
60
+ .hcp svg {
61
+ display: block; flex: none; width: 16px; height: 16px; line-height: 1;
62
+ fill: none; stroke: none; stroke-width: 0; stroke-linecap: butt; stroke-linejoin: miter;
63
+ }
64
+ .hcp .hcp-x svg, .hcp .hcp-actions .hcp-btn svg, .hcp-tab svg, .hcp-file svg { fill: currentColor; }
65
+ .hcp .hcp-file svg { width: 20px; height: 20px; }
66
+ .hcp .hcp-tab svg { width: 16px; height: 18px; }
67
+ .hcp .hcp-dock .hcp-x svg { width: 12px; height: 12px; }
68
+ .hcp button, .hcp a { border-radius: 0; line-height: 1.5; }
69
+
70
+ .hcp-btn {
71
+ display: inline-flex; align-items: center; justify-content: center; gap: 7px;
72
+ height: 34px; min-height: 0; padding: 0 14px;
73
+ font-family: var(--hcp-sans); font-size: 14px; font-weight: 500; line-height: 1.5;
74
+ white-space: nowrap; text-decoration: none; cursor: pointer; user-select: none;
75
+ color: var(--hcp-ink); background: var(--hcp-face);
76
+ border: 2px solid; border-radius: 0;
77
+ border-color: var(--hcp-edge-hi) var(--hcp-edge-lo) var(--hcp-edge-lo) var(--hcp-edge-hi);
78
+ outline: none;
79
+ }
80
+ .hcp .hcp-btn:hover { background: color-mix(in srgb, var(--hcp-face), var(--hcp-edge-hi) 45%); color: var(--hcp-ink); }
81
+ .hcp-btn:active { border-color: var(--hcp-edge-lo) var(--hcp-edge-hi) var(--hcp-edge-hi) var(--hcp-edge-lo); }
82
+ .hcp-btn:active > span { translate: 1px 1px; }
83
+ .hcp-btn > span { display: inline-flex; align-items: center; gap: 7px; }
84
+ .hcp-btn:focus-visible, .hcp-x:focus-visible, .hcp-quiet:focus-visible { outline: 2px solid var(--hcp-brass); outline-offset: 2px; }
85
+ .hcp-btn--primary {
86
+ --hcp-face: var(--hcp-ink);
87
+ --hcp-edge-hi: color-mix(in srgb, var(--hcp-ink), white 24%);
88
+ --hcp-edge-lo: color-mix(in srgb, var(--hcp-ink), black 32%);
89
+ color: var(--hcp-ground);
90
+ }
91
+ .hcp .hcp-btn--primary:hover { background: color-mix(in srgb, var(--hcp-ink), var(--hcp-ground) 12%); color: var(--hcp-ground); }
92
+
93
+ .hcp-dock { display: flex; align-items: stretch; box-shadow: 0 6px 16px -8px rgba(43, 36, 27, .45); }
94
+ .hcp-tab { font-size: 13.5px; padding: 0 12px 0 10px; }
95
+ .hcp-tab svg { color: var(--hcp-brass); }
96
+ .hcp-dock .hcp-x {
97
+ width: 26px; height: 34px; min-height: 0; border-radius: 0; display: inline-flex; align-items: center; justify-content: center;
98
+ background: var(--hcp-face); color: var(--hcp-muted); cursor: pointer;
99
+ border: 2px solid; border-left: 0;
100
+ border-color: var(--hcp-edge-hi) var(--hcp-edge-lo) var(--hcp-edge-lo) var(--hcp-edge-hi);
101
+ }
102
+ .hcp .hcp-dock .hcp-x:hover { color: var(--hcp-ink); background: var(--hcp-face); }
103
+ .hcp-tab__label { display: inline; }
104
+ .hcp[data-hcp-state="mini"] .hcp-tab__label,
105
+ .hcp[data-hcp-state="mini"] .hcp-dock .hcp-x { display: none; }
106
+ .hcp[data-hcp-state="mini"] .hcp-tab { width: 34px; padding: 0; }
107
+ .hcp[data-hcp-state="card"] .hcp-dock { display: none; }
108
+
109
+ .hcp-card {
110
+ display: none; position: relative;
111
+ width: min(360px, calc(100vw - 32px)); max-height: calc(100dvh - 32px - var(--hcp-lift)); overflow: auto;
112
+ background: var(--hcp-surface); border: 1px solid var(--hcp-line-2); border-top: 3px solid var(--hcp-brass);
113
+ box-shadow: var(--hcp-shadow);
114
+ padding: 18px 20px 16px;
115
+ }
116
+ .hcp[data-hcp-state="card"] .hcp-card { display: block; animation: hcp-rise .32s cubic-bezier(.2, .8, .3, 1) both; }
117
+ @keyframes hcp-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
118
+
119
+ .hcp-card .hcp-x {
120
+ position: absolute; top: 12px; right: 12px; width: 24px; height: 24px; min-height: 0;
121
+ display: inline-flex; align-items: center; justify-content: center;
122
+ border: 1px solid var(--hcp-line-2); border-radius: 3px; background: var(--hcp-surface);
123
+ color: var(--hcp-muted); cursor: pointer;
124
+ }
125
+ .hcp .hcp-card .hcp-x:hover { color: var(--hcp-ink); border-color: var(--hcp-muted); background: var(--hcp-surface); }
126
+ .hcp .hcp-card .hcp-x svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2; }
127
+ .hcp-kicker {
128
+ display: flex; align-items: center; gap: 8px;
129
+ font-family: var(--hcp-mono); font-size: 11px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase;
130
+ color: var(--hcp-brass); margin-bottom: 8px;
131
+ }
132
+ .hcp-title {
133
+ font-family: var(--hcp-serif); font-size: 23px; font-weight: 500; line-height: 1.18;
134
+ color: var(--hcp-ink); padding-right: 26px; margin-bottom: 10px;
135
+ font-variation-settings: "opsz" 36;
136
+ }
137
+ .hcp-title [data-hcp-for] { display: none; }
138
+ .hcp[data-hcp-reason="use"] .hcp-title [data-hcp-for="use"],
139
+ .hcp[data-hcp-reason="open"] .hcp-title [data-hcp-for="open"],
140
+ .hcp[data-hcp-reason="return"] .hcp-title [data-hcp-for="return"],
141
+ .hcp[data-hcp-reason="error"] .hcp-title [data-hcp-for="error"] { display: inline; }
142
+ .hcp-body [data-hcp-for] { display: none; }
143
+ .hcp[data-hcp-reason="use"] .hcp-body [data-hcp-for="use"], .hcp[data-hcp-reason="open"] .hcp-body [data-hcp-for="open"], .hcp[data-hcp-reason="return"] .hcp-body [data-hcp-for="return"], .hcp[data-hcp-reason="error"] .hcp-body [data-hcp-for="error"] { display: inline; }
144
+ .hcp-body { font-size: 14px; line-height: 1.55; color: var(--hcp-ink-2); }
145
+ .hcp-file {
146
+ display: flex; align-items: center; gap: 10px; margin: 12px 0 14px;
147
+ padding: 9px 11px; background: var(--hcp-sunk); border: 1px solid var(--hcp-line);
148
+ }
149
+ .hcp-file svg { color: var(--hcp-brass); }
150
+ .hcp-file b { display: block; font-family: var(--hcp-sans); font-size: 14px; font-weight: 600; color: var(--hcp-ink); overflow-wrap: anywhere; }
151
+ .hcp-file small { display: block; font-size: 12px; color: var(--hcp-muted); line-height: 1.4; }
152
+ .hcp-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
153
+ .hcp-actions .hcp-btn--primary { flex: 1 1 auto; }
154
+ .hcp-signup { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hcp-line); }
155
+ .hcp-signup label { display: block; font-size: 13px; line-height: 1.45; color: var(--hcp-ink-2); margin-bottom: 8px; }
156
+ .hcp-signup-row { display: flex; gap: 8px; }
157
+ .hcp-signup-row[hidden] { display: none; }
158
+ .hcp-signup input[type="email"] {
159
+ flex: 1 1 auto; min-width: 0; height: 34px; padding: 0 10px; margin: 0;
160
+ font: 14px var(--hcp-sans); color: var(--hcp-ink); background: var(--hcp-surface);
161
+ border: 1px solid var(--hcp-line-2); border-radius: 0; box-shadow: none;
162
+ }
163
+ .hcp-signup input[type="email"]:focus-visible { outline: 2px solid var(--hcp-brass); outline-offset: 1px; }
164
+ .hcp-hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
165
+ .hcp-msg { margin-top: 8px; font-size: 13px; color: var(--hcp-ink-2); }
166
+ .hcp[data-hcp-reason="error"] .hcp-signup { display: none; }
167
+ @media (max-width: 360px) { .hcp-signup-row { flex-direction: column; } }
168
+ .hcp-foot { margin-top: 12px; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
169
+ .hcp-foot small { font-size: 12px; color: var(--hcp-muted); }
170
+ .hcp-quiet {
171
+ min-height: 0; border-radius: 0;
172
+ font-size: 12px; color: var(--hcp-muted); cursor: pointer;
173
+ text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--hcp-line-2);
174
+ white-space: nowrap;
175
+ }
176
+ .hcp .hcp-quiet:hover { color: var(--hcp-ink); background: none; }
177
+
178
+ @media (max-width: 480px) {
179
+ .hcp { right: 12px; bottom: calc(12px + var(--hcp-lift)); }
180
+ .hcp-card { width: calc(100vw - 24px); padding: 16px 16px 14px; max-height: min(70dvh, calc(100dvh - 24px - var(--hcp-lift))); }
181
+ .hcp-title { font-size: 21px; }
182
+ }
183
+ @media (pointer: coarse) {
184
+ .hcp-btn, .hcp-dock .hcp-x, .hcp-card .hcp-x { min-height: 44px; }
185
+ .hcp-card .hcp-x { width: 44px; height: 44px; top: 6px; right: 6px; }
186
+ .hcp-quiet { display: inline-flex; align-items: center; min-height: 44px; }
187
+ }
188
+ @media (prefers-reduced-motion: reduce) {
189
+ .hcp { transition: none; }
190
+ .hcp[data-hcp-state="card"] .hcp-card { animation: none; }
191
+ .hcp-btn:active > span { translate: none; }
192
+ }
193
+ @media (forced-colors: active) {
194
+ .hcp-btn, .hcp-dock .hcp-x { border: 1px solid ButtonText; }
195
+ .hcp-card { border: 1px solid CanvasText; }
196
+ }
197
+ @media print { .hcp { display: none; } }
198
+ `;
@@ -0,0 +1,203 @@
1
+ import { PROMPT_CSS } from "./prompt-style.js";
2
+ import { announceBottomUi, bottomBarHeight } from "./bottom-ui.js";
3
+
4
+ export const SIGNUP_COPY = {
5
+ thanks: "Thanks. Check your email for a link to confirm.",
6
+ failed: "That didn't go through. Check the address and try again.",
7
+ };
8
+
9
+ const FONT_HREF = "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500&family=IBM+Plex+Sans:wght@400;500;600&family=Newsreader:opsz,wght@6..72,500&display=swap";
10
+
11
+ const ASIDE = `<aside class="hcp" id="hcp" hidden aria-label="Download this app" data-hcp-state="tab" data-hcp-reason="open" sap-ignore clay="editor-ui">
12
+ <section class="hcp-card" id="hcp-card" role="dialog" aria-modal="false" aria-labelledby="hcp-title">
13
+ <button type="button" class="hcp-x" data-hcp="minimize" aria-label="Minimize"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" aria-hidden="true"><path d="M5 12h14"/></svg></button>
14
+ <div class="hcp-kicker">Free download</div>
15
+ <h2 class="hcp-title" id="hcp-title"><span data-hcp-for="use">Your work is saved in this browser only.</span><span data-hcp-for="open">This app is free to keep.</span><span data-hcp-for="return">Your work is saved in this browser only.</span><span data-hcp-for="error">Your work isn't being saved in this browser.</span></h2>
16
+ <p class="hcp-body"><span data-hcp-for="open">Download it as one file and use it on your computer, no account needed.</span><span data-hcp-for="use">Download the file to keep it on your computer for good.</span><span data-hcp-for="return">Download the file to keep it on your computer for good.</span><span data-hcp-for="error">Storage is full or turned off here. Download the file to keep what you've done.</span></p>
17
+ <div class="hcp-file">
18
+ <svg width="20" height="20" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"/></svg>
19
+ <div><b class="hcp-name" data-hcp-name>This app</b><small>One file with the whole app and your work in it</small></div>
20
+ </div>
21
+ <p class="hcp-body">Open it with HTML Clay, a free app for Mac, Windows and Linux. It opens the file in a window and saves every change into it, so your work stays on your computer.</p>
22
+ <div class="hcp-actions">
23
+ <a class="hcp-btn hcp-btn--primary" data-hcp-link href="/_/download?as=htmlclay"><span><svg width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"/></svg>Download free</span></a>
24
+ <button type="button" class="hcp-btn" data-hcp="minimize"><span>Not now</span></button>
25
+ </div>
26
+ <form class="hcp-signup" data-hcp-signup novalidate>
27
+ <label for="hcp-email">I make one of these about every two weeks. Get the next one by email.</label>
28
+ <div class="hcp-signup-row"><input id="hcp-email" type="email" autocomplete="email" required placeholder="you@example.com"><button type="submit" class="hcp-btn"><span>Send me the next one</span></button></div>
29
+ <input class="hcp-hp" name="website" tabindex="-1" autocomplete="off" aria-hidden="true">
30
+ <p class="hcp-msg" role="status" hidden></p>
31
+ </form>
32
+ <div class="hcp-foot"><small>Free, and the file is yours to keep.</small><button type="button" class="hcp-quiet" data-hcp="dismiss">Don&rsquo;t show again</button></div>
33
+ </section>
34
+ <div class="hcp-dock">
35
+ <button type="button" class="hcp-btn hcp-tab" data-hcp="open" aria-controls="hcp-card" aria-expanded="false" aria-label="This app is free to keep"><span><svg width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"/></svg><span class="hcp-tab__label">This app is free to keep</span></span></button>
36
+ <button type="button" class="hcp-x" data-hcp="dismiss" aria-label="Hide this for good"><svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true"><path d="M1.4 0 6 4.6 10.6 0 12 1.4 7.4 6 12 10.6 10.6 12 6 7.4 1.4 12 0 10.6 4.6 6 0 1.4z"/></svg></button>
37
+ </div>
38
+ </aside>`;
39
+
40
+ export function mountPrompt({ memory, appName, link, tag, source }) {
41
+ if (!document.getElementById("hcp-style")) {
42
+ document.head.insertAdjacentHTML("beforeend", `<link rel="stylesheet" clay="editor-ui" href="${FONT_HREF}">\n<style clay="editor-ui" id="hcp-style">${PROMPT_CSS}</style>`);
43
+ }
44
+ if (!document.getElementById("hcp")) document.body.insertAdjacentHTML("beforeend", ASIDE);
45
+
46
+ const root = document.getElementById("hcp");
47
+ const ui = memory.get();
48
+ if (!ui.prompt || typeof ui.prompt !== "object") ui.prompt = {};
49
+ const mem = ui.prompt;
50
+ const USES_TO_SHOW = 3;
51
+ const IDLE_MS = 2500;
52
+ const TEXTY = /^(INPUT|TEXTAREA|SELECT)$/;
53
+ const OUR_UI = "#hcp, .clay-intro, .clay-owner";
54
+
55
+ const linkEl = root.querySelector("[data-hcp-link]");
56
+ linkEl.href = link;
57
+
58
+ const signup = root.querySelector("[data-hcp-signup]");
59
+ if (signup) {
60
+ const email = signup.querySelector("#hcp-email");
61
+ const honey = signup.querySelector(".hcp-hp");
62
+ const row = signup.querySelector(".hcp-signup-row");
63
+ const message = signup.querySelector(".hcp-msg");
64
+ signup.addEventListener("submit", (event) => {
65
+ event.preventDefault();
66
+ fetch("/_/newsletter/subscribe", {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ credentials: "same-origin",
70
+ body: JSON.stringify({ email: email.value.trim(), tags: [tag], source, website: honey.value }),
71
+ }).then((response) => {
72
+ if (!response.ok) throw new Error("signup failed");
73
+ row.hidden = true;
74
+ message.textContent = SIGNUP_COPY.thanks;
75
+ message.hidden = false;
76
+ }).catch(() => {
77
+ message.textContent = SIGNUP_COPY.failed;
78
+ message.hidden = false;
79
+ });
80
+ });
81
+ }
82
+
83
+ if (appName) root.querySelector("[data-hcp-name]").textContent = appName;
84
+
85
+ const tab = root.querySelector("[data-hcp=open]");
86
+ function set(state, reason) {
87
+ root.hidden = false;
88
+ root.setAttribute("data-hcp-state", state);
89
+ if (reason) root.setAttribute("data-hcp-reason", reason);
90
+ tab.setAttribute("aria-expanded", state === "card" ? "true" : "false");
91
+ if (state === "card") { mem.shown = true; memory.save(); }
92
+ lift();
93
+ }
94
+
95
+ function isHostField(el) {
96
+ return el && !root.contains(el) && !el.closest(OUR_UI) && (TEXTY.test(el.tagName) || el.isContentEditable);
97
+ }
98
+
99
+ let timer = null;
100
+ function maybeShow(reason) {
101
+ clearTimeout(timer);
102
+ timer = setTimeout(() => {
103
+ if (mem.shown || mem.dismissed || root.getAttribute("data-hcp-state") !== "tab") return;
104
+ const a = document.activeElement;
105
+ if (isHostField(a)) {
106
+ const r = a.getBoundingClientRect();
107
+ const w = Math.min(360, innerWidth - 32);
108
+ if (innerWidth < 700 || (r.right > innerWidth - w - 32 && r.bottom > innerHeight - 380)) return;
109
+ }
110
+ set("card", reason);
111
+ }, IDLE_MS);
112
+ }
113
+
114
+ const touched = [];
115
+ function onUse(e) {
116
+ if (root.contains(e.target) || !isHostField(e.target)) return;
117
+ if (touched.indexOf(e.target) < 0) touched.push(e.target);
118
+ mem.uses = Math.max(mem.uses || 0, touched.length);
119
+ if (!mem.shown) memory.save();
120
+ if (touched.length >= USES_TO_SHOW) maybeShow("use");
121
+ }
122
+
123
+ function onFocusIn(e) {
124
+ if (!isHostField(e.target)) return;
125
+ if (root.getAttribute("data-hcp-state") === "card") set(mem.shown ? "mini" : "tab");
126
+ if (innerWidth < 700) root.setAttribute("data-hcp-typing", "");
127
+ }
128
+ function onFocusOut() {
129
+ setTimeout(() => { if (!isHostField(document.activeElement)) root.removeAttribute("data-hcp-typing"); }, 50);
130
+ }
131
+
132
+ let liftFrame = 0;
133
+ function lift() {
134
+ if (liftFrame) return;
135
+ liftFrame = requestAnimationFrame(() => {
136
+ liftFrame = 0;
137
+ const raise = bottomBarHeight(innerWidth - 24, (el) => root.contains(el) || el.closest("[data-hcp-preview], .clay-owner"));
138
+ const next = raise + "px";
139
+ if (root.style.getPropertyValue("--hcp-lift") !== next) root.style.setProperty("--hcp-lift", next);
140
+ announceBottomUi();
141
+ });
142
+ }
143
+
144
+ function downloadCurrent() {
145
+ const getHTML = window.clay && window.clay.getHTML;
146
+ if (typeof getHTML !== "function" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return false;
147
+ const segment = location.pathname.split("/").filter(Boolean).pop() || "index.html";
148
+ const objectUrl = URL.createObjectURL(new Blob([getHTML()], { type: "text/html" }));
149
+ const a = document.createElement("a");
150
+ a.href = objectUrl;
151
+ a.download = segment.replace(/\.html$/, ".htmlclay");
152
+ document.body.appendChild(a);
153
+ a.click();
154
+ a.remove();
155
+ setTimeout(() => URL.revokeObjectURL?.(objectUrl), 1000);
156
+ return true;
157
+ }
158
+
159
+ root.addEventListener("click", (e) => {
160
+ const t = e.target.closest("[data-hcp]");
161
+ if (t) {
162
+ const act = t.getAttribute("data-hcp");
163
+ if (act === "open") { set("card", "open"); linkEl.focus(); }
164
+ else if (act === "minimize") { set("mini"); tab.focus(); }
165
+ else if (act === "dismiss") {
166
+ if (root.getAttribute("data-hcp-reason") === "error") { set("mini"); tab.focus(); }
167
+ else { mem.dismissed = true; memory.save(); set("dismissed"); }
168
+ }
169
+ return;
170
+ }
171
+ if (e.target.closest("[data-hcp-link]")) {
172
+ if (root.getAttribute("data-hcp-reason") === "error") {
173
+ e.preventDefault();
174
+ downloadCurrent();
175
+ }
176
+ mem.downloaded = true; mem.shown = true; memory.save();
177
+ }
178
+ });
179
+ root.addEventListener("keydown", (e) => {
180
+ if (e.key === "Escape" && root.getAttribute("data-hcp-state") === "card") { set("mini"); tab.focus(); }
181
+ });
182
+
183
+ function showError() { set("card", "error"); }
184
+ function clearError() {
185
+ if (root.getAttribute("data-hcp-reason") === "error") root.setAttribute("data-hcp-reason", "use");
186
+ }
187
+ const api = { set, root, showError, clearError };
188
+
189
+ if (mem.dismissed) { root.setAttribute("data-hcp-state", "dismissed"); return api; }
190
+
191
+ document.addEventListener("input", onUse, true);
192
+ document.addEventListener("change", onUse, true);
193
+ document.addEventListener("focusin", onFocusIn);
194
+ document.addEventListener("focusout", onFocusOut);
195
+ addEventListener("scroll", lift, { passive: true });
196
+ addEventListener("resize", lift);
197
+ document.addEventListener("transitionend", lift, true);
198
+
199
+ set(mem.shown ? "mini" : "tab");
200
+ if (!mem.shown && (mem.uses || 0) >= USES_TO_SHOW) maybeShow("return");
201
+
202
+ return api;
203
+ }
@@ -0,0 +1,21 @@
1
+ const SRC = /^[a-z0-9][a-z0-9-]{0,39}$/;
2
+
3
+ export function rememberSource(memory, search = window.location.search) {
4
+ const mem = memory.get();
5
+ let incoming = null;
6
+ try { incoming = new URLSearchParams(search).get("src"); } catch {}
7
+ if (!mem.src && incoming && SRC.test(incoming)) { mem.src = incoming; memory.save(); }
8
+ return mem.src;
9
+ }
10
+
11
+ export function downloadUrl(src, loc = window.location) {
12
+ let path = loc.pathname.replace(/\/+$/, "");
13
+ if (path && !/\.(html|htmlclay)$/.test(path)) path += ".html";
14
+ return `/_/download${path}?as=htmlclay${src ? `&src=${encodeURIComponent(src)}` : ""}`;
15
+ }
16
+
17
+ export function signupTag(slot, template) {
18
+ const explicit = template?.getAttribute("signup-tag");
19
+ const name = explicit || slot.replace(/-v\d+$/, "");
20
+ return `app-${name}`.slice(0, 50);
21
+ }