@jini-ai/ui 0.4.4 → 0.4.5

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +20 -0
  3. package/dist/core/theme/default.d.ts +4 -0
  4. package/dist/core/theme/default.js +11 -0
  5. package/dist/core/theme/stylesheet.d.ts +8 -0
  6. package/dist/core/theme/stylesheet.js +14 -0
  7. package/dist/core/theme/types.d.ts +39 -0
  8. package/dist/core/theme/types.js +2 -0
  9. package/dist/core/theme/validation.d.ts +15 -0
  10. package/dist/core/theme/validation.js +55 -0
  11. package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
  12. package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
  13. package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
  14. package/dist/features/admin-widgets/components/Select/Select.js +4 -3
  15. package/dist/features/appearance/constants.js +1 -1
  16. package/dist/features/connectors/rules.d.ts +1 -6
  17. package/dist/features/connectors/types.d.ts +1 -6
  18. package/dist/features/connectors/types.js +1 -6
  19. package/dist/features/execution/http-adapter.d.ts +111 -0
  20. package/dist/features/execution/http-adapter.js +160 -0
  21. package/dist/features/execution/index.d.ts +4 -0
  22. package/dist/features/execution/index.js +2 -0
  23. package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
  24. package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
  25. package/dist/features/html-editor/canvas-style.d.ts +1 -1
  26. package/dist/features/html-editor/canvas-style.js +2 -2
  27. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
  28. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
  29. package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
  30. package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
  31. package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
  32. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
  33. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
  34. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
  35. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
  36. package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
  37. package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
  38. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
  39. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
  40. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
  41. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
  42. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
  43. package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
  44. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
  45. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
  46. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
  47. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
  48. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
  49. package/dist/features/i18n/dictionary-translator.d.ts +5 -2
  50. package/dist/features/i18n/dictionary-translator.js +1 -1
  51. package/dist/features/i18n/index.d.ts +1 -1
  52. package/dist/features/i18n/index.js +1 -1
  53. package/dist/features/i18n/template-i18n.d.ts +11 -0
  54. package/dist/features/i18n/template-i18n.js +12 -0
  55. package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +1 -1
  56. package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +1 -1
  57. package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
  58. package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
  59. package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
  60. package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
  61. package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
  62. package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
  63. package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
  64. package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
  65. package/dist/features/lexical-rich-text-editor/types.js +3 -13
  66. package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
  67. package/dist/features/mcp-ui/early-message-buffer.js +1 -2
  68. package/dist/features/mcp-ui/escape.d.ts +4 -0
  69. package/dist/features/mcp-ui/escape.js +2 -9
  70. package/dist/features/mcp-ui/index.d.ts +1 -0
  71. package/dist/features/mcp-ui/index.js +1 -0
  72. package/dist/features/mcp-ui/protocol.d.ts +2 -5
  73. package/dist/features/mcp-ui/protocol.js +2 -5
  74. package/dist/features/mcp-ui/resource.d.ts +47 -7
  75. package/dist/features/mcp-ui/resource.js +5 -8
  76. package/dist/features/mcp-ui/secret-card.d.ts +115 -0
  77. package/dist/features/mcp-ui/secret-card.js +137 -0
  78. package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
  79. package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
  80. package/dist/features/mcp-ui/surfaces/fields.js +3 -1
  81. package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
  82. package/dist/features/mcp-ui/surfaces/form.js +65 -3
  83. package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
  84. package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
  85. package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
  86. package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
  87. package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
  88. package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
  89. package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
  90. package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
  91. package/dist/features/panel-kit/helpers/permissions.js +2 -0
  92. package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
  93. package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
  94. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
  95. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
  96. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
  97. package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
  98. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
  99. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
  100. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
  101. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
  102. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
  103. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
  104. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
  105. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
  106. package/dist/features/panel-kit/index.d.ts +7 -1
  107. package/dist/features/panel-kit/index.js +5 -1
  108. package/dist/features/resource-dashboard/types.d.ts +8 -41
  109. package/dist/features/resource-dashboard/types.js +3 -26
  110. package/dist/features/tab-strip/constants.d.ts +3 -4
  111. package/dist/features/tab-strip/constants.js +3 -4
  112. package/dist/features/tab-strip/dependencies.d.ts +1 -6
  113. package/dist/features/tab-strip/dependencies.js +1 -6
  114. package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
  115. package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
  116. package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
  117. package/dist/features/tab-strip/react/components/TabBar.js +12 -1
  118. package/dist/features/tab-strip/rules.d.ts +4 -17
  119. package/dist/features/tab-strip/rules.js +4 -17
  120. package/dist/features/tab-strip/types.d.ts +2 -13
  121. package/dist/icon-name.d.ts +1 -1
  122. package/dist/icon-name.js +1 -0
  123. package/dist/index.d.ts +42 -417
  124. package/dist/index.js +42 -430
  125. package/dist/react/components/CustomSelect.d.ts +6 -2
  126. package/dist/react/components/CustomSelect.js +20 -91
  127. package/dist/react/components/Icon.d.ts +6 -0
  128. package/dist/react/components/Icon.js +12 -0
  129. package/dist/react/components/select-popover.d.ts +106 -0
  130. package/dist/react/components/select-popover.js +210 -0
  131. package/dist/renderers/sandbox-scripts.d.ts +29 -0
  132. package/dist/renderers/sandbox-scripts.js +127 -0
  133. package/dist/renderers/sandboxed-document.js +3 -86
  134. package/dist/renderers/srcdoc/build.d.ts +10 -17
  135. package/dist/renderers/srcdoc/build.js +31 -234
  136. package/dist/styles/variables.css +2 -2
  137. package/dist/theme/apply.d.ts +1 -1
  138. package/dist/theme/apply.js +2 -2
  139. package/dist/theme/color-scheme.d.ts +1 -1
  140. package/dist/theme/default.d.ts +1 -3
  141. package/dist/theme/default.js +2 -10
  142. package/dist/theme/index.d.ts +3 -3
  143. package/dist/theme/index.js +2 -2
  144. package/dist/theme/stylesheet.d.ts +1 -7
  145. package/dist/theme/stylesheet.js +2 -13
  146. package/dist/theme/types.d.ts +1 -38
  147. package/dist/theme/types.js +2 -1
  148. package/dist/theme/validation.d.ts +1 -14
  149. package/dist/theme/validation.js +2 -54
  150. package/dist/utils/endpoint-policy.d.ts +21 -39
  151. package/dist/utils/endpoint-policy.js +19 -107
  152. package/dist/utils/file-system-errors.d.ts +1 -2
  153. package/dist/utils/file-system-errors.js +1 -2
  154. package/dist/utils/ime-composing.d.ts +0 -3
  155. package/dist/utils/ime-composing.js +0 -3
  156. package/dist/utils/platform.d.ts +1 -4
  157. package/dist/utils/platform.js +1 -4
  158. package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
  159. package/dist/utils/smooth-scroll-to-top.js +3 -13
  160. package/dist/utils/timezone.js +1 -5
  161. package/dist/utils/uuid.d.ts +0 -3
  162. package/dist/utils/uuid.js +0 -3
  163. package/dist/utils/visual-stability.d.ts +1 -3
  164. package/dist/utils/visual-stability.js +1 -3
  165. package/package.json +74 -15
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Script that shims `localStorage`/`sessionStorage` with an in-memory store
3
+ * when the real Storage API throws, and intercepts `<a href>` clicks so
4
+ * hash-only links scroll within the document (instead of attempting a
5
+ * sandboxed top-level navigation) and `target="_blank"` links open via
6
+ * `window.open` behind an `http:`/`https:`/`mailto:` scheme allow-list
7
+ * (instead of being silently blocked by the iframe sandbox).
8
+ */
9
+ // Sandboxed iframes (`sandbox="allow-scripts"`, deliberately without
10
+ // `allow-same-origin` — see `react/components/SrcDocSandbox.tsx`) raise a
11
+ // SecurityError on first `localStorage`/`sessionStorage` access. Many
12
+ // freeform-generated artifacts call `localStorage.getItem(...)` at the top
13
+ // of an IIFE with no try/catch — when it throws, the whole script aborts
14
+ // and the artifact renders blank. Installing a same-origin in-memory shim
15
+ // before any user script runs lets those artifacts degrade gracefully
16
+ // (position/state just doesn't persist across reloads) instead of crashing.
17
+ // The same script also intercepts same-document anchor navigation and
18
+ // `target="_blank"` links so they behave sanely inside a sandboxed iframe
19
+ // (`allow-popups`/`allow-popups-to-escape-sandbox` still required on the
20
+ // iframe itself for the latter to actually open).
21
+ export function buildStorageScript({ format }, _optional = {}) {
22
+ const modern = format === 'srcdoc';
23
+ return `<script data-jini-sandbox-shim>(function(){
24
+ function makeStore(){
25
+ var data = {};
26
+ var api = {
27
+ getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
28
+ setItem: function(k, v){ data[k] = String(v); },
29
+ removeItem: function(k){ delete data[k]; },
30
+ clear: function(){ data = {}; },
31
+ key: function(i){ return Object.keys(data)[i] || null; }
32
+ };
33
+ Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
34
+ return api;
35
+ }
36
+ function tryShim(name){
37
+ var works = false;
38
+ try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
39
+ catch (_) { works = false; }
40
+ if (works) return;
41
+ try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
42
+ catch (_) { try { window[name] = makeStore(); } catch (__) {} }
43
+ }
44
+ tryShim('localStorage');
45
+ tryShim('sessionStorage');
46
+ document.addEventListener('click', ${modern ? '(e) => {' : 'function(e){'}
47
+ if (!e.target || !(e.target instanceof Element)) return;
48
+ var link = e.target.closest('a[href]');
49
+ if (!link) return;
50
+ var href = link.getAttribute('href');
51
+ if (href === null) return;
52
+ var isAnchor = ${modern ? "href.startsWith('#')" : "href.indexOf('#') === 0"} || href === '';
53
+ if (isAnchor) {
54
+ e.preventDefault();
55
+ if (href === '' || href === '#') {
56
+ window.scrollTo({ top: 0 });
57
+ history.replaceState(null, '', ' ');
58
+ } else {
59
+ var targetId = href.slice(1);
60
+ var target = targetId ? document.getElementById(targetId) : null;
61
+ if (target) {
62
+ target.scrollIntoView();
63
+ location.hash === href && history.replaceState(null, '', ' ');
64
+ location.hash = href;
65
+ }
66
+ }
67
+ } else if (link.getAttribute('target') === '_blank') {
68
+ e.preventDefault();
69
+ ${modern ? 'let' : 'var'} safe = false;
70
+ try {
71
+ var url = new URL(href, location.href);
72
+ ${modern ? `safe =
73
+ url.protocol === 'http:' ||
74
+ url.protocol === 'https:' ||
75
+ url.protocol === 'mailto:';` : "safe = url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'mailto:';"}
76
+ } catch (_) {}
77
+ safe && window.open(href, '_blank', 'noopener,noreferrer');
78
+ }
79
+ });
80
+ })();</script>`;
81
+ }
82
+ /**
83
+ * Script that suppresses `focus()` calls (on `window` or any element) that
84
+ * aren't the direct result of a real pointer/keyboard event within the
85
+ * last second, so embedded content can't steal keyboard focus away from
86
+ * the host page around it.
87
+ */
88
+ export function buildFocusScript({ format }, _optional = {}) {
89
+ const preview = format === 'srcdoc';
90
+ return `<script ${preview ? 'data-jini-preview-focus-guard' : 'data-jini-focus-guard'}>(function(){
91
+ var lastTrustedInputAt = 0;
92
+ function userActivated(){
93
+ return Date.now() - lastTrustedInputAt < 1000;
94
+ }
95
+ function markTrustedInput(event){
96
+ if (event && event.isTrusted) lastTrustedInputAt = Date.now();
97
+ }
98
+ ${preview ? ` document.addEventListener('pointerdown', function(event){
99
+ markTrustedInput(event);
100
+ }, true);
101
+ document.addEventListener('keydown', function(event){
102
+ markTrustedInput(event);
103
+ }, true);` : ` document.addEventListener('pointerdown', markTrustedInput, true);
104
+ document.addEventListener('keydown', markTrustedInput, true);`}
105
+ try {
106
+ var nativeWindowFocus = window.focus && window.focus.bind(window);
107
+ Object.defineProperty(window, 'focus', {
108
+ configurable: true,
109
+ writable: true,
110
+ value: function(){
111
+ if (userActivated() && nativeWindowFocus) return nativeWindowFocus();
112
+ }
113
+ });
114
+ } catch (_) {}
115
+ try {
116
+ var nativeElementFocus = HTMLElement.prototype.focus;
117
+ Object.defineProperty(HTMLElement.prototype, 'focus', {
118
+ configurable: true,
119
+ writable: true,
120
+ value: function(options){
121
+ if (userActivated()) return nativeElementFocus.call(this, options);
122
+ }
123
+ });
124
+ } catch (_) {}
125
+ })();</script>`;
126
+ }
127
+ //# sourceMappingURL=sandbox-scripts.js.map
@@ -1,3 +1,4 @@
1
+ import { buildFocusScript, buildStorageScript } from './sandbox-scripts.js';
1
2
  import { escapeHtmlAttribute, injectAfterHeadOpen, injectBeforeHeadEnd } from './html-utils.js';
2
3
  /** Whether `html` already looks like a full document (starts with `<!doctype>` or `<html>`), vs. a bare fragment. */
3
4
  export function isFullHtmlDocument(html) {
@@ -35,61 +36,7 @@ export function injectBaseHref(doc, baseHref) {
35
36
  * (instead of being silently blocked by the iframe sandbox).
36
37
  */
37
38
  export function buildStorageShimScript() {
38
- return `<script data-jini-sandbox-shim>(function(){
39
- function makeStore(){
40
- var data = {};
41
- var api = {
42
- getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
43
- setItem: function(k, v){ data[k] = String(v); },
44
- removeItem: function(k){ delete data[k]; },
45
- clear: function(){ data = {}; },
46
- key: function(i){ return Object.keys(data)[i] || null; }
47
- };
48
- Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
49
- return api;
50
- }
51
- function tryShim(name){
52
- var works = false;
53
- try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
54
- catch (_) { works = false; }
55
- if (works) return;
56
- try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
57
- catch (_) { try { window[name] = makeStore(); } catch (__) {} }
58
- }
59
- tryShim('localStorage');
60
- tryShim('sessionStorage');
61
- document.addEventListener('click', function(e){
62
- if (!e.target || !(e.target instanceof Element)) return;
63
- var link = e.target.closest('a[href]');
64
- if (!link) return;
65
- var href = link.getAttribute('href');
66
- if (href === null) return;
67
- var isAnchor = href.indexOf('#') === 0 || href === '';
68
- if (isAnchor) {
69
- e.preventDefault();
70
- if (href === '' || href === '#') {
71
- window.scrollTo({ top: 0 });
72
- history.replaceState(null, '', ' ');
73
- } else {
74
- var targetId = href.slice(1);
75
- var target = targetId ? document.getElementById(targetId) : null;
76
- if (target) {
77
- target.scrollIntoView();
78
- location.hash === href && history.replaceState(null, '', ' ');
79
- location.hash = href;
80
- }
81
- }
82
- } else if (link.getAttribute('target') === '_blank') {
83
- e.preventDefault();
84
- var safe = false;
85
- try {
86
- var url = new URL(href, location.href);
87
- safe = url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'mailto:';
88
- } catch (_) {}
89
- safe && window.open(href, '_blank', 'noopener,noreferrer');
90
- }
91
- });
92
- })();</script>`;
39
+ return buildStorageScript({ format: 'sandbox' });
93
40
  }
94
41
  /**
95
42
  * Script that suppresses `focus()` calls (on `window` or any element) that
@@ -98,37 +45,7 @@ export function buildStorageShimScript() {
98
45
  * the host page around it.
99
46
  */
100
47
  export function buildFocusGuardScript() {
101
- return `<script data-jini-focus-guard>(function(){
102
- var lastTrustedInputAt = 0;
103
- function userActivated(){
104
- return Date.now() - lastTrustedInputAt < 1000;
105
- }
106
- function markTrustedInput(event){
107
- if (event && event.isTrusted) lastTrustedInputAt = Date.now();
108
- }
109
- document.addEventListener('pointerdown', markTrustedInput, true);
110
- document.addEventListener('keydown', markTrustedInput, true);
111
- try {
112
- var nativeWindowFocus = window.focus && window.focus.bind(window);
113
- Object.defineProperty(window, 'focus', {
114
- configurable: true,
115
- writable: true,
116
- value: function(){
117
- if (userActivated() && nativeWindowFocus) return nativeWindowFocus();
118
- }
119
- });
120
- } catch (_) {}
121
- try {
122
- var nativeElementFocus = HTMLElement.prototype.focus;
123
- Object.defineProperty(HTMLElement.prototype, 'focus', {
124
- configurable: true,
125
- writable: true,
126
- value: function(options){
127
- if (userActivated()) return nativeElementFocus.call(this, options);
128
- }
129
- });
130
- } catch (_) {}
131
- })();</script>`;
48
+ return buildFocusScript({ format: 'sandbox' });
132
49
  }
133
50
  /**
134
51
  * Build a sandboxed-iframe-ready HTML document from arbitrary artifact
@@ -1,16 +1,3 @@
1
- /**
2
- * Wrap artifact HTML for a sandboxed iframe (`srcDoc`).
3
- *
4
- * Origin: `apps/web/src/runtime/srcdoc.ts` in the origin project (2,689
5
- * lines). Ported the generic sandbox mechanics only — document wrapping,
6
- * title sanitization, the same-origin storage shim, the anti-focus-steal
7
- * guard, and the lazy-transport-shell perf optimization. NOT ported: every
8
- * postMessage bridge (deck navigation, comment/inspect element-selection,
9
- * CSS tweaks palette, manual-edit overlay, snapshot/export-capture) — those
10
- * are that product's own UI protocols. They become `SrcDocBridge` plugins a
11
- * host registers via `bridges` (see `bridge.ts`), not built-ins. See
12
- * `archived provenance ledger` for the full file-by-file breakdown.
13
- */
14
1
  import { type SrcDocBridge } from './bridge.js';
15
2
  export interface BuildSrcDocOptions {
16
3
  /** `<base href>` for the document, so relative asset/script URLs resolve as if served from this path. */
@@ -63,10 +50,16 @@ export declare function sanitizePreviewTitle(text: string): string;
63
50
  /**
64
51
  * Rewrite the `<title>` element in an HTML string so its text content is
65
52
  * filename-safe (see {@link sanitizePreviewTitle}). Only the real `<title>`
66
- * in the `<head>` region is changed — occurrences inside HTML comments,
67
- * `<script>`, or `<style>` blocks are left untouched. Pure string
68
- * operations — no `DOMParser` — so it behaves identically in Node test
69
- * environments and in the browser.
53
+ * in the parsed `<head>` is changed — comments, scripts, styles, templates,
54
+ * and body titles cannot supply it. parse5 owns HTML recovery and single-pass
55
+ * entity decoding, identically in Node and the browser. Source offsets let us
56
+ * splice only title content; serializing the document would change unrelated
57
+ * formatting. A title without an explicit closing tag is left untouched.
58
+ *
59
+ * @param html Original document source; never mutated or re-serialized.
60
+ * @returns Source with the first head title's decoded, filename-safe text.
61
+ * @complexity O(n) time and space in source length for parsing and splicing.
62
+ * @example sanitizeTitleInDoc('<title>Q3: Report</title>') // '<title>Q3- Report</title>'
70
63
  */
71
64
  export declare function sanitizeTitleInDoc(html: string): string;
72
65
  /** Splice `payload` right after the opening `<head …>` tag, prepending it when no `<head>` exists. */
@@ -11,6 +11,10 @@
11
11
  * host registers via `bridges` (see `bridge.ts`), not built-ins. See
12
12
  * `archived provenance ledger` for the full file-by-file breakdown.
13
13
  */
14
+ import { defaultTreeAdapter, parse } from 'parse5';
15
+ import { escapeHtmlAttribute } from '../html-utils.js';
16
+ import { injectBaseHref, wrapFragmentAsDocument } from '../sandboxed-document.js';
17
+ import { buildFocusScript, buildStorageScript } from '../sandbox-scripts.js';
14
18
  import { applySrcDocBridges } from './bridge.js';
15
19
  /**
16
20
  * Default Content-Security-Policy applied to every srcDoc document unless
@@ -24,7 +28,7 @@ import { applySrcDocBridges } from './bridge.js';
24
28
  */
25
29
  export const DEFAULT_SRC_DOC_CSP = "default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' blob: data:; style-src 'unsafe-inline'; img-src * data: blob:; font-src * data:; media-src * data: blob:; connect-src *; frame-src *; child-src *; object-src 'none'; base-uri 'none'; form-action 'none';";
26
30
  function injectContentSecurityPolicy(doc, csp) {
27
- const tag = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(csp)}">`;
31
+ const tag = `<meta http-equiv="Content-Security-Policy" content="${escapeHtmlAttribute(csp)}">`;
28
32
  return injectAfterHeadOpen(doc, tag);
29
33
  }
30
34
  /**
@@ -44,106 +48,35 @@ export function sanitizePreviewTitle(text) {
44
48
  result = result.replace(/[:#%&*{}\\<>?/+|"]+/g, '-');
45
49
  return result.trim();
46
50
  }
47
- const NAMED_ENTITY_MAP = {
48
- agrave: 'à', aacute: 'á', acirc: 'â', atilde: 'ã', auml: 'ä', aring: 'å',
49
- aelig: 'æ', ccedil: 'ç',
50
- egrave: 'è', eacute: 'é', ecirc: 'ê', euml: 'ë',
51
- igrave: 'ì', iacute: 'í', icirc: 'î', iuml: 'ï',
52
- eth: 'ð', ntilde: 'ñ',
53
- ograve: 'ò', oacute: 'ó', ocirc: 'ô', otilde: 'õ', ouml: 'ö', oslash: 'ø',
54
- ugrave: 'ù', uacute: 'ú', ucirc: 'û', uuml: 'ü',
55
- yacute: 'ý', thorn: 'þ', yuml: 'ÿ',
56
- Agrave: 'À', Aacute: 'Á', Acirc: 'Â', Atilde: 'Ã', Auml: 'Ä', Aring: 'Å',
57
- AElig: 'Æ', Ccedil: 'Ç',
58
- Egrave: 'È', Eacute: 'É', Ecirc: 'Ê', Euml: 'Ë',
59
- Igrave: 'Ì', Iacute: 'Í', Icirc: 'Î', Iuml: 'Ï',
60
- ETH: 'Ð', Ntilde: 'Ñ',
61
- Ograve: 'Ò', Oacute: 'Ó', Ocirc: 'Ô', Otilde: 'Õ', Ouml: 'Ö', Oslash: 'Ø',
62
- Ugrave: 'Ù', Uacute: 'Ú', Ucirc: 'Û', Uuml: 'Ü',
63
- Yacute: 'Ý', THORN: 'Þ',
64
- ndash: '–', mdash: '—', lsquo: '‘', rsquo: '’',
65
- ldquo: '“', rdquo: '”', hellip: '…', trade: '™', reg: '®',
66
- copy: '©', deg: '°', euro: '€', pound: '£', yen: '¥',
67
- };
68
- function safeFromCodePoint(cp) {
69
- if (cp < 0 || cp > 0x10ffff)
70
- return '�';
71
- return String.fromCodePoint(cp);
72
- }
73
- function decodeHtmlEntitiesForTitle(encoded) {
74
- return encoded
75
- .replace(/&([A-Za-z]+);/g, (match, name) => NAMED_ENTITY_MAP[name] ?? match)
76
- .replace(/&amp;/gi, '&')
77
- .replace(/&lt;/gi, '<')
78
- .replace(/&gt;/gi, '>')
79
- .replace(/&quot;/gi, '"')
80
- .replace(/&apos;/gi, "'")
81
- .replace(/&#(\d+);/g, (_, n) => safeFromCodePoint(Number(n)))
82
- .replace(/&#x([0-9a-f]+);/gi, (_, h) => safeFromCodePoint(parseInt(h, 16)));
83
- }
84
- function findRealTitleOffset(html, searchLimit) {
85
- let i = 0;
86
- const limit = Math.min(html.length, searchLimit);
87
- while (i < limit) {
88
- if (html.charCodeAt(i) === 60 /* < */ && html.slice(i, i + 4) === '<!--') {
89
- const end = html.indexOf('-->', i + 4);
90
- if (end < 0)
91
- return -1;
92
- i = end + 3;
93
- continue;
94
- }
95
- if (html.charCodeAt(i) === 60 /* < */) {
96
- const tagMatch = /^<(script|style)\b/i.exec(html.slice(i, i + 20));
97
- if (tagMatch) {
98
- const closingTag = `</${tagMatch[1]}`;
99
- const end = html.toLowerCase().indexOf(closingTag.toLowerCase(), i + tagMatch[0].length);
100
- if (end < 0)
101
- return -1;
102
- const closeEnd = html.indexOf('>', end);
103
- i = closeEnd >= 0 ? closeEnd + 1 : end + closingTag.length;
104
- continue;
105
- }
106
- }
107
- if (html.charCodeAt(i) === 60 /* < */) {
108
- if (/^<title[\s>]/i.test(html.slice(i, i + 8))) {
109
- return i;
110
- }
111
- }
112
- i++;
113
- }
114
- return -1;
51
+ /** Finds a direct element child; template contents must not supply the document title. */
52
+ function findElementChild({ parent, tagName }) {
53
+ return parent?.childNodes.find((node) => defaultTreeAdapter.isElementNode(node) && node.tagName === tagName);
115
54
  }
116
55
  /**
117
56
  * Rewrite the `<title>` element in an HTML string so its text content is
118
57
  * filename-safe (see {@link sanitizePreviewTitle}). Only the real `<title>`
119
- * in the `<head>` region is changed — occurrences inside HTML comments,
120
- * `<script>`, or `<style>` blocks are left untouched. Pure string
121
- * operations — no `DOMParser` — so it behaves identically in Node test
122
- * environments and in the browser.
58
+ * in the parsed `<head>` is changed — comments, scripts, styles, templates,
59
+ * and body titles cannot supply it. parse5 owns HTML recovery and single-pass
60
+ * entity decoding, identically in Node and the browser. Source offsets let us
61
+ * splice only title content; serializing the document would change unrelated
62
+ * formatting. A title without an explicit closing tag is left untouched.
63
+ *
64
+ * @param html Original document source; never mutated or re-serialized.
65
+ * @returns Source with the first head title's decoded, filename-safe text.
66
+ * @complexity O(n) time and space in source length for parsing and splicing.
67
+ * @example sanitizeTitleInDoc('<title>Q3: Report</title>') // '<title>Q3- Report</title>'
123
68
  */
124
69
  export function sanitizeTitleInDoc(html) {
125
- const lower = html.toLowerCase();
126
- const bodyStart = lower.indexOf('<body');
127
- const headEnd = lower.lastIndexOf('</head>', bodyStart >= 0 ? bodyStart - 1 : lower.length - 1);
128
- const searchLimit = headEnd >= 0 ? headEnd + 7 : bodyStart >= 0 ? bodyStart : html.length;
129
- const titleStart = findRealTitleOffset(html, searchLimit);
130
- if (titleStart < 0)
70
+ const document = parse(html, { sourceCodeLocationInfo: true });
71
+ const root = findElementChild({ parent: document, tagName: 'html' });
72
+ const head = findElementChild({ parent: root, tagName: 'head' });
73
+ const title = findElementChild({ parent: head, tagName: 'title' });
74
+ const location = title?.sourceCodeLocation;
75
+ if (!title || !location?.startTag || !location.endTag)
131
76
  return html;
132
- const openTagEnd = html.indexOf('>', titleStart);
133
- if (openTagEnd < 0)
134
- return html;
135
- const closingTagStart = html.toLowerCase().indexOf('</title>', openTagEnd + 1);
136
- if (closingTagStart < 0)
137
- return html;
138
- const closingTagEnd = html.indexOf('>', closingTagStart);
139
- if (closingTagEnd < 0)
140
- return html;
141
- const openTag = html.slice(titleStart, openTagEnd + 1);
142
- const rawContent = html.slice(openTagEnd + 1, closingTagStart);
143
- const closeTag = html.slice(closingTagStart, closingTagEnd + 1);
144
- const decoded = decodeHtmlEntitiesForTitle(rawContent);
77
+ const decoded = title.childNodes.filter(defaultTreeAdapter.isTextNode).map((node) => node.value).join('');
145
78
  const safe = sanitizePreviewTitle(decoded);
146
- return html.slice(0, titleStart) + openTag + safe + closeTag + html.slice(closingTagEnd + 1);
79
+ return html.slice(0, location.startTag.endOffset) + safe + html.slice(location.endTag.startOffset);
147
80
  }
148
81
  function serializeHtmlDocument(doc) {
149
82
  const doctype = doc.doctype ? '<!doctype html>\n' : '';
@@ -205,133 +138,8 @@ export function injectBeforeBodyEnd(doc, payload) {
205
138
  }
206
139
  return doc + payload;
207
140
  }
208
- function escapeAttr(value) {
209
- return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
210
- }
211
- function injectBaseHref(doc, baseHref) {
212
- const safeHref = escapeAttr(baseHref);
213
- const tag = `<base href="${safeHref}">`;
214
- if (/<head[^>]*>/i.test(doc)) {
215
- return doc.replace(/<head[^>]*>/i, (m) => `${m}${tag}`);
216
- }
217
- if (/<html[^>]*>/i.test(doc)) {
218
- return doc.replace(/<html[^>]*>/i, (m) => `${m}<head>${tag}</head>`);
219
- }
220
- return tag + doc;
221
- }
222
- // Sandboxed iframes (`sandbox="allow-scripts"`, deliberately without
223
- // `allow-same-origin` — see `react/components/SrcDocSandbox.tsx`) raise a
224
- // SecurityError on first `localStorage`/`sessionStorage` access. Many
225
- // freeform-generated artifacts call `localStorage.getItem(...)` at the top
226
- // of an IIFE with no try/catch — when it throws, the whole script aborts
227
- // and the artifact renders blank. Installing a same-origin in-memory shim
228
- // before any user script runs lets those artifacts degrade gracefully
229
- // (position/state just doesn't persist across reloads) instead of crashing.
230
- // The same script also intercepts same-document anchor navigation and
231
- // `target="_blank"` links so they behave sanely inside a sandboxed iframe
232
- // (`allow-popups`/`allow-popups-to-escape-sandbox` still required on the
233
- // iframe itself for the latter to actually open).
234
- function injectSandboxShim(doc) {
235
- const shim = `<script data-jini-sandbox-shim>(function(){
236
- function makeStore(){
237
- var data = {};
238
- var api = {
239
- getItem: function(k){ return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
240
- setItem: function(k, v){ data[k] = String(v); },
241
- removeItem: function(k){ delete data[k]; },
242
- clear: function(){ data = {}; },
243
- key: function(i){ return Object.keys(data)[i] || null; }
244
- };
245
- Object.defineProperty(api, 'length', { get: function(){ return Object.keys(data).length; } });
246
- return api;
247
- }
248
- function tryShim(name){
249
- var works = false;
250
- try { works = !!window[name] && typeof window[name].getItem === 'function'; void window[name].length; }
251
- catch (_) { works = false; }
252
- if (works) return;
253
- try { Object.defineProperty(window, name, { configurable: true, value: makeStore() }); }
254
- catch (_) { try { window[name] = makeStore(); } catch (__) {} }
255
- }
256
- tryShim('localStorage');
257
- tryShim('sessionStorage');
258
- document.addEventListener('click', (e) => {
259
- if (!e.target || !(e.target instanceof Element)) return;
260
- var link = e.target.closest('a[href]');
261
- if (!link) return;
262
- var href = link.getAttribute('href');
263
- if (href === null) return;
264
- var isAnchor = href.startsWith('#') || href === '';
265
- if (isAnchor) {
266
- e.preventDefault();
267
- if (href === '' || href === '#') {
268
- window.scrollTo({ top: 0 });
269
- history.replaceState(null, '', ' ');
270
- } else {
271
- var targetId = href.slice(1);
272
- var target = targetId ? document.getElementById(targetId) : null;
273
- if (target) {
274
- target.scrollIntoView();
275
- location.hash === href && history.replaceState(null, '', ' ');
276
- location.hash = href;
277
- }
278
- }
279
- } else if (link.getAttribute('target') === '_blank') {
280
- e.preventDefault();
281
- let safe = false;
282
- try {
283
- var url = new URL(href, location.href);
284
- safe =
285
- url.protocol === 'http:' ||
286
- url.protocol === 'https:' ||
287
- url.protocol === 'mailto:';
288
- } catch (_) {}
289
- safe && window.open(href, '_blank', 'noopener,noreferrer');
290
- }
291
- });
292
- })();</script>`;
293
- if (/<head[^>]*>/i.test(doc))
294
- return doc.replace(/<head[^>]*>/i, (m) => `${m}${shim}`);
295
- if (/<body[^>]*>/i.test(doc))
296
- return doc.replace(/<body[^>]*>/i, (m) => `${m}${shim}`);
297
- return shim + doc;
298
- }
299
- function injectPreviewFocusGuard(doc) {
300
- const script = `<script data-jini-preview-focus-guard>(function(){
301
- var lastTrustedInputAt = 0;
302
- function userActivated(){
303
- return Date.now() - lastTrustedInputAt < 1000;
304
- }
305
- function markTrustedInput(event){
306
- if (event && event.isTrusted) lastTrustedInputAt = Date.now();
307
- }
308
- document.addEventListener('pointerdown', function(event){
309
- markTrustedInput(event);
310
- }, true);
311
- document.addEventListener('keydown', function(event){
312
- markTrustedInput(event);
313
- }, true);
314
- try {
315
- var nativeWindowFocus = window.focus && window.focus.bind(window);
316
- Object.defineProperty(window, 'focus', {
317
- configurable: true,
318
- writable: true,
319
- value: function(){
320
- if (userActivated() && nativeWindowFocus) return nativeWindowFocus();
321
- }
322
- });
323
- } catch (_) {}
324
- try {
325
- var nativeElementFocus = HTMLElement.prototype.focus;
326
- Object.defineProperty(HTMLElement.prototype, 'focus', {
327
- configurable: true,
328
- writable: true,
329
- value: function(options){
330
- if (userActivated()) return nativeElementFocus.call(this, options);
331
- }
332
- });
333
- } catch (_) {}
334
- })();</script>`;
141
+ /** Srcdoc keeps its head/body/prepend fallback instead of the generic builder's string-only head insertion. */
142
+ function injectSandboxScript(doc, script) {
335
143
  if (/<head[^>]*>/i.test(doc))
336
144
  return doc.replace(/<head[^>]*>/i, (m) => `${m}${script}`);
337
145
  if (/<body[^>]*>/i.test(doc))
@@ -421,24 +229,13 @@ export function canActivateSrcDocTransport(state) {
421
229
  export function buildSrcDoc(html, options = {}) {
422
230
  if (options.lazyTransport)
423
231
  return buildLazySrcDocTransport();
424
- const head = html.trimStart().slice(0, 64).toLowerCase();
425
- const isFullDoc = head.startsWith('<!doctype') || head.startsWith('<html');
426
- const wrapped = isFullDoc
427
- ? html
428
- : `<!doctype html>
429
- <html>
430
- <head>
431
- <meta charset="utf-8" />
432
- <meta name="viewport" content="width=device-width, initial-scale=1" />
433
- </head>
434
- <body>${html}</body>
435
- </html>`;
232
+ const wrapped = wrapFragmentAsDocument(html);
436
233
  const withSafeTitle = sanitizeTitleInDoc(wrapped);
437
234
  const cspValue = options.csp === false ? null : options.csp === true || options.csp === undefined ? DEFAULT_SRC_DOC_CSP : options.csp;
438
235
  const withCsp = cspValue ? injectContentSecurityPolicy(withSafeTitle, cspValue) : withSafeTitle;
439
236
  const withBase = options.baseHref ? injectBaseHref(withCsp, options.baseHref) : withCsp;
440
- const withShim = injectSandboxShim(withBase);
441
- const withFocusGuard = options.previewFocusGuard ? injectPreviewFocusGuard(withShim) : withShim;
237
+ const withShim = injectSandboxScript(withBase, buildStorageScript({ format: 'srcdoc' }));
238
+ const withFocusGuard = options.previewFocusGuard ? injectSandboxScript(withShim, buildFocusScript({ format: 'srcdoc' })) : withShim;
442
239
  const ctx = { baseHref: options.baseHref, ...options.bridgeContext };
443
240
  const withBridges = options.bridges?.length
444
241
  ? applySrcDocBridges(withFocusGuard, options.bridges, ctx)
@@ -36,7 +36,7 @@
36
36
  lightness band, chroma floor, CVD >= 8 (worst 10.1 deutan light, 10.9 dark), normal-vision >= 15,
37
37
  >= 3:1 contrast — all pass. Dark is stepped for the dark surface, not a flip of light: dark
38
38
  series 1 sits a step lighter, because the light orange's chroma reads grey as a large fill on a
39
- near-black surface. This was Tovu admin's palette first (Tovu b183ca5a8). */
39
+ near-black surface. This was an adopting admin shell's palette first (host commit b183ca5a8). */
40
40
  :root, [data-color-scheme="light"], [data-theme="light"] {
41
41
  --jini-chart-default-1: oklch(55.29% 0.1129 43.4);
42
42
  --jini-chart-default-2: oklch(50% 0.11 240);
@@ -47,7 +47,7 @@
47
47
  }
48
48
 
49
49
  /* Both dark attributes: the base tokens above flip on `data-color-scheme`, while hosts that follow
50
- this package's `data-theme` convention on <html> (sketch-editor, shiki, Tovu's admin) flip that. */
50
+ this package's `data-theme` convention on <html> (sketch-editor, shiki, host admin shells) flip that. */
51
51
  [data-color-scheme="dark"], [data-theme="dark"] {
52
52
  --jini-chart-default-1: oklch(62% 0.11 44);
53
53
  --jini-chart-default-2: oklch(64% 0.11 240);
@@ -1,4 +1,4 @@
1
- import type { AdminTheme, AdminThemeDocument, AdminThemeTarget } from './types.js';
1
+ import type { AdminTheme, AdminThemeDocument, AdminThemeTarget } from '../core/theme/types.js';
2
2
  /** Install both palettes on the injected scope and load fonts. Cleanup restores the host. */
3
3
  export declare function applyAdminTheme({ theme }: {
4
4
  readonly theme: AdminTheme;
@@ -1,5 +1,5 @@
1
- import { defaultAdminTheme } from './default.js';
2
- import { adminThemeColorKeys, validateAdminTheme } from './validation.js';
1
+ import { defaultAdminTheme } from '../core/theme/default.js';
2
+ import { adminThemeColorKeys, validateAdminTheme } from '../core/theme/validation.js';
3
3
  const fontLeases = new WeakMap();
4
4
  function acquireFont(document, href) {
5
5
  const leases = fontLeases.get(document) ?? new Map();
@@ -1,4 +1,4 @@
1
- import type { ColorScheme, ColorSchemePreference, MatchMediaPort } from './types.js';
1
+ import type { ColorScheme, ColorSchemePreference, MatchMediaPort } from '../core/theme/types.js';
2
2
  /** Explicit choices are deterministic; system resolution requires the host's media port. */
3
3
  export declare function resolveColorScheme({ preference }: {
4
4
  readonly preference: ColorSchemePreference;
@@ -1,4 +1,2 @@
1
- import type { AdminTheme } from './types.js';
2
- /** Neutral chrome. Products supply their own brand theme instead of forking CSS. */
3
- export declare const defaultAdminTheme: AdminTheme;
1
+ export * from '../core/theme/default.js';
4
2
  //# sourceMappingURL=default.d.ts.map
@@ -1,11 +1,3 @@
1
- /** Neutral chrome. Products supply their own brand theme instead of forking CSS. */
2
- export const defaultAdminTheme = Object.freeze({
3
- name: 'neutral',
4
- fonts: Object.freeze({ body: 'system-ui, sans-serif', heading: 'system-ui, sans-serif', mono: 'ui-monospace, monospace' }),
5
- light: Object.freeze({ primary: '#363636', primaryInk: '#ffffff', bg: '#ffffff', surface: '#f5f5f5',
6
- text: '#202020', muted: '#666666', border: '#d9d9d9', danger: '#b42318', success: '#18763b', warning: '#845800' }),
7
- dark: Object.freeze({ primary: '#e0e0e0', primaryInk: '#202020', bg: '#171717', surface: '#242424',
8
- text: '#f0f0f0', muted: '#b3b3b3', border: '#454545', danger: '#ff8b80', success: '#79d69b', warning: '#ebc36b' }),
9
- radius: '8px', density: 1,
10
- });
1
+ // Compatibility path; the shared implementation lives in core.
2
+ export * from '../core/theme/default.js';
11
3
  //# sourceMappingURL=default.js.map