pith-ui 0.0.0-stage → 0.1.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.
Files changed (126) hide show
  1. package/AI.md +67 -0
  2. package/API.md +3281 -0
  3. package/LICENSE +661 -0
  4. package/README.md +80 -2
  5. package/dist/composer.d.ts +38 -0
  6. package/dist/composer.d.ts.map +1 -0
  7. package/dist/composer.js +114 -0
  8. package/dist/composer.js.map +1 -0
  9. package/dist/content/conversation-reader.d.ts +19 -0
  10. package/dist/content/conversation-reader.d.ts.map +1 -0
  11. package/dist/content/conversation-reader.js +280 -0
  12. package/dist/content/conversation-reader.js.map +1 -0
  13. package/dist/content/conversation-view.d.ts +48 -0
  14. package/dist/content/conversation-view.d.ts.map +1 -0
  15. package/dist/content/conversation-view.js +107 -0
  16. package/dist/content/conversation-view.js.map +1 -0
  17. package/dist/content/image-viewer.d.ts +23 -0
  18. package/dist/content/image-viewer.d.ts.map +1 -0
  19. package/dist/content/image-viewer.js +101 -0
  20. package/dist/content/image-viewer.js.map +1 -0
  21. package/dist/content/markdown.d.ts +12 -0
  22. package/dist/content/markdown.d.ts.map +1 -0
  23. package/dist/content/markdown.js +68 -0
  24. package/dist/content/markdown.js.map +1 -0
  25. package/dist/content/paged-preview.d.ts +15 -0
  26. package/dist/content/paged-preview.d.ts.map +1 -0
  27. package/dist/content/paged-preview.js +105 -0
  28. package/dist/content/paged-preview.js.map +1 -0
  29. package/dist/content/views.d.ts +9 -0
  30. package/dist/content/views.d.ts.map +1 -0
  31. package/dist/content/views.js +53 -0
  32. package/dist/content/views.js.map +1 -0
  33. package/dist/content/welcome.d.ts +18 -0
  34. package/dist/content/welcome.d.ts.map +1 -0
  35. package/dist/content/welcome.js +45 -0
  36. package/dist/content/welcome.js.map +1 -0
  37. package/dist/core.d.ts +36 -0
  38. package/dist/core.d.ts.map +1 -0
  39. package/dist/core.js +44 -0
  40. package/dist/core.js.map +1 -0
  41. package/dist/dom.d.ts +14 -0
  42. package/dist/dom.d.ts.map +1 -0
  43. package/dist/dom.js +69 -0
  44. package/dist/dom.js.map +1 -0
  45. package/dist/files/panels.d.ts +95 -0
  46. package/dist/files/panels.d.ts.map +1 -0
  47. package/dist/files/panels.js +245 -0
  48. package/dist/files/panels.js.map +1 -0
  49. package/dist/files/systems.d.ts +91 -0
  50. package/dist/files/systems.d.ts.map +1 -0
  51. package/dist/files/systems.js +256 -0
  52. package/dist/files/systems.js.map +1 -0
  53. package/dist/forms/actions.d.ts +27 -0
  54. package/dist/forms/actions.d.ts.map +1 -0
  55. package/dist/forms/actions.js +19 -0
  56. package/dist/forms/actions.js.map +1 -0
  57. package/dist/forms/custom-connection.d.ts +61 -0
  58. package/dist/forms/custom-connection.d.ts.map +1 -0
  59. package/dist/forms/custom-connection.js +65 -0
  60. package/dist/forms/custom-connection.js.map +1 -0
  61. package/dist/forms/dialog-frame.d.ts +39 -0
  62. package/dist/forms/dialog-frame.d.ts.map +1 -0
  63. package/dist/forms/dialog-frame.js +75 -0
  64. package/dist/forms/dialog-frame.js.map +1 -0
  65. package/dist/forms/history.d.ts +16 -0
  66. package/dist/forms/history.d.ts.map +1 -0
  67. package/dist/forms/history.js +21 -0
  68. package/dist/forms/history.js.map +1 -0
  69. package/dist/forms/identity.d.ts +4 -0
  70. package/dist/forms/identity.d.ts.map +1 -0
  71. package/dist/forms/identity.js +21 -0
  72. package/dist/forms/identity.js.map +1 -0
  73. package/dist/forms/model-editor.d.ts +35 -0
  74. package/dist/forms/model-editor.d.ts.map +1 -0
  75. package/dist/forms/model-editor.js +78 -0
  76. package/dist/forms/model-editor.js.map +1 -0
  77. package/dist/forms/panels.d.ts +57 -0
  78. package/dist/forms/panels.d.ts.map +1 -0
  79. package/dist/forms/panels.js +224 -0
  80. package/dist/forms/panels.js.map +1 -0
  81. package/dist/forms/provider-login.d.ts +22 -0
  82. package/dist/forms/provider-login.d.ts.map +1 -0
  83. package/dist/forms/provider-login.js +114 -0
  84. package/dist/forms/provider-login.js.map +1 -0
  85. package/dist/forms/resource-editor.d.ts +23 -0
  86. package/dist/forms/resource-editor.d.ts.map +1 -0
  87. package/dist/forms/resource-editor.js +24 -0
  88. package/dist/forms/resource-editor.js.map +1 -0
  89. package/dist/index.d.ts +28 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +28 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/navigation/context-menu.d.ts +25 -0
  94. package/dist/navigation/context-menu.d.ts.map +1 -0
  95. package/dist/navigation/context-menu.js +67 -0
  96. package/dist/navigation/context-menu.js.map +1 -0
  97. package/dist/navigation/navigation.d.ts +57 -0
  98. package/dist/navigation/navigation.d.ts.map +1 -0
  99. package/dist/navigation/navigation.js +185 -0
  100. package/dist/navigation/navigation.js.map +1 -0
  101. package/dist/primitives.d.ts +307 -0
  102. package/dist/primitives.d.ts.map +1 -0
  103. package/dist/primitives.js +741 -0
  104. package/dist/primitives.js.map +1 -0
  105. package/dist/styles.css +849 -0
  106. package/dist/tasks/approval.d.ts +25 -0
  107. package/dist/tasks/approval.d.ts.map +1 -0
  108. package/dist/tasks/approval.js +61 -0
  109. package/dist/tasks/approval.js.map +1 -0
  110. package/dist/tasks/permission.d.ts +35 -0
  111. package/dist/tasks/permission.d.ts.map +1 -0
  112. package/dist/tasks/permission.js +67 -0
  113. package/dist/tasks/permission.js.map +1 -0
  114. package/dist/tasks/runtime.d.ts +31 -0
  115. package/dist/tasks/runtime.d.ts.map +1 -0
  116. package/dist/tasks/runtime.js +34 -0
  117. package/dist/tasks/runtime.js.map +1 -0
  118. package/dist/theme.d.ts +4 -0
  119. package/dist/theme.d.ts.map +1 -0
  120. package/dist/theme.js +6 -0
  121. package/dist/theme.js.map +1 -0
  122. package/dist/workspace.d.ts +38 -0
  123. package/dist/workspace.d.ts.map +1 -0
  124. package/dist/workspace.js +183 -0
  125. package/dist/workspace.js.map +1 -0
  126. package/package.json +27 -4
package/README.md CHANGED
@@ -1,3 +1,81 @@
1
- # Temporary Holding Version
1
+ # Pith UI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Native TypeScript UI building blocks for AI-assisted application development. Choose a complete block, provide typed content and callbacks, and reuse its reviewed layout and interaction rules.
4
+
5
+ The source is TypeScript and CSS. The package contains compiled ESM, declarations and a separate stylesheet. It uses browser DOM APIs, native controls and system fonts; no UI framework or animation runtime is required.
6
+
7
+ ## Try the components
8
+
9
+ The [interactive showroom](https://minifish-org.github.io/pith-ui/) groups **design foundations → base components → product blocks → composed pages**. It exposes real controls, theme and state comparisons, CSS-pixel measurements, spacing rulers and motion playback. Showroom data and actions are simulated and labelled accordingly. Automated checks do not substitute for human acceptance.
10
+
11
+ Run it locally with Node 22.18 or newer:
12
+
13
+ ```sh
14
+ npm ci
15
+ npm run dev
16
+ ```
17
+
18
+ ## Use the package
19
+
20
+ ```sh
21
+ npm install --save-exact pith-ui@0.1.0
22
+ ```
23
+
24
+ ```ts
25
+ import { createTextEditDialog, setAppearance } from 'pith-ui';
26
+ import 'pith-ui/styles.css';
27
+
28
+ // Include document.body when dialogs are mounted there.
29
+ setAppearance(document.body, 'system');
30
+
31
+ const editor = createTextEditDialog({
32
+ title: 'Edit message',
33
+ onSubmit: async (text) => {
34
+ const saved = await saveMessage(text); // Your existing business action.
35
+ return saved ? { ok: true } : { ok: false, error: 'Save failed. Your edit is retained.' };
36
+ },
37
+ });
38
+ editor.open({ value: 'Original message' });
39
+ // When this owner is released:
40
+ // editor.destroy();
41
+ ```
42
+
43
+ `saveMessage` is an application callback, not a Pith UI API. [Complete, type-checked examples](https://github.com/minifish-org/pith-ui/blob/main/docs/examples.ts) show how to connect it and clean up ownership.
44
+
45
+ ## What belongs where
46
+
47
+ | Pith UI | Application |
48
+ |---|---|
49
+ | Visual tokens, component layout, native controls | Branding and business data |
50
+ | Focus, disclosure, menu, local feedback, reading position | HTTP/WebSocket, authentication, persistence |
51
+ | Pending/error presentation and content rendering | Permission, identity and target validation |
52
+ | Sanitized Markdown, code/diff/file/image presentation | File acquisition, native actions, object-URL ownership |
53
+ | Interactive examples and component regression tests | End-to-end product policy and release validation |
54
+
55
+ The first consumer is Pith Desk. Its existing controllers keep their request, draft, approval and recovery semantics while shared views move into this library.
56
+
57
+ ## Find a block
58
+
59
+ - **Foundations:** color, typography, spacing, dimensions, borders, surfaces, focus and motion.
60
+ - **Base components:** actions, fields, status, feedback, dialog/menu/popover, form/layout and scrolling.
61
+ - **Product blocks:** navigation, conversation reader, composer/queue, models, tasks/approval, settings/connections, OAuth/MCP, resources, files, branch history and system information.
62
+ - **Pages:** working compositions of those blocks, rather than separate copies of their implementations.
63
+
64
+ Read the bundled [exact API reference](./API.md), [complete catalog](https://github.com/minifish-org/pith-ui/blob/main/docs/catalog.md), [integration guide](https://github.com/minifish-org/pith-ui/blob/main/docs/guide.md), [AI usage instructions](https://github.com/minifish-org/pith-ui/blob/main/AI.md), [validation guide](https://github.com/minifish-org/pith-ui/blob/main/docs/validation.md) and [release procedure](https://github.com/minifish-org/pith-ui/blob/main/docs/releasing.md).
65
+
66
+ ## Develop and validate
67
+
68
+ ```sh
69
+ npm run check
70
+ npm run build
71
+ npm test
72
+ node scripts/catalog.ts
73
+ node scripts/api-docs.ts --check
74
+ npx --no-install playwright install chromium webkit
75
+ npm run test:browser
76
+ npm run showroom:build
77
+ ```
78
+
79
+ The lockfile fixes tool and dependency versions. All handwritten logic, including new scripts and tests, is TypeScript; JavaScript and declarations are generated build products. The library and showroom build separately, so measurement tools and fixtures do not ship to applications.
80
+
81
+ Pith UI is licensed under [AGPL-3.0-only](./LICENSE). Runtime dependencies retain their own licenses.
@@ -0,0 +1,38 @@
1
+ import type { UIBlock } from './core.js';
2
+ export interface PendingMessage {
3
+ id: string;
4
+ text: string;
5
+ mode: string;
6
+ imageCount?: number;
7
+ }
8
+ export interface PendingMessagesState {
9
+ items: readonly PendingMessage[];
10
+ running: boolean;
11
+ busy: boolean;
12
+ }
13
+ export interface PendingMessagesOptions {
14
+ container?: HTMLElement;
15
+ onAction?: (action: 'edit' | 'delete' | 'steer', id: string) => void;
16
+ }
17
+ export declare function createPendingMessages(options?: PendingMessagesOptions): UIBlock<PendingMessagesState>;
18
+ export interface AttachmentItem {
19
+ id: string;
20
+ url: string;
21
+ name: string;
22
+ }
23
+ export declare function createDropFeedback(options: {
24
+ container: HTMLElement;
25
+ }): UIBlock<{
26
+ active: boolean;
27
+ disabled?: boolean;
28
+ }>;
29
+ export interface AttachmentsState {
30
+ items: readonly AttachmentItem[];
31
+ disabled?: boolean;
32
+ }
33
+ export declare function createAttachments(options?: {
34
+ container?: HTMLElement;
35
+ onRemove?: (id: string) => void;
36
+ onOpen?: (image: HTMLImageElement) => void;
37
+ }): UIBlock<AttachmentsState>;
38
+ //# sourceMappingURL=composer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,MAAM,WAAW,cAAc;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAAE;AAChG,MAAM,WAAW,oBAAoB;IAAG,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;CAAE;AAC5G,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACtE;AACD,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAuCzG;AAED,MAAM,WAAW,cAAc;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAE1E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAAE,SAAS,EAAE,WAAW,CAAA;CAAE,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAQxH;AACD,MAAM,WAAW,gBAAgB;IAAG,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAAE;AAC3F,wBAAgB,iBAAiB,CAAC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuBnL"}
@@ -0,0 +1,114 @@
1
+ import { escape, icon } from './dom.js';
2
+ export function createPendingMessages(options = {}) {
3
+ const element = options.container ?? document.createElement('section');
4
+ element.classList.add('pui-queued-messages');
5
+ element.setAttribute('aria-label', 'Pending messages');
6
+ let destroyed = false;
7
+ const rows = new Map();
8
+ const heading = document.createElement('div');
9
+ heading.className = 'pui-section-heading';
10
+ heading.textContent = 'Pending messages';
11
+ element.append(heading);
12
+ const click = (event) => {
13
+ const button = event.target.closest('[data-queue-action]');
14
+ if (button && !button.disabled)
15
+ options.onAction?.(button.dataset.queueAction, button.dataset.queuedMessage);
16
+ };
17
+ element.addEventListener('click', click);
18
+ return { element, update(state) {
19
+ if (destroyed)
20
+ return;
21
+ element.hidden = !state.items.length;
22
+ const keep = new Set(state.items.map(item => item.id));
23
+ for (const [key, row] of rows)
24
+ if (!keep.has(key)) {
25
+ row.remove();
26
+ rows.delete(key);
27
+ }
28
+ let cursor = heading.nextElementSibling;
29
+ for (const item of state.items) {
30
+ let row = rows.get(item.id);
31
+ if (!row) {
32
+ row = document.createElement('div');
33
+ row.className = 'pui-queued-message';
34
+ row.innerHTML = `<span class="pui-queue-badge">Instruction</span><div class="pui-queued-message-content"><span class="pui-queued-message-text"></span><small class="pui-queue-image-count"></small></div><div class="pui-queued-message-actions"><button type="button" class="pui-quiet-icon pui-queue-steer" data-queue-action="steer" title="Steer the current task at its next turn">${icon('steer')}<span>Steer</span></button><button type="button" class="pui-quiet-icon" data-queue-action="edit" aria-label="Edit pending message" title="Edit message">${icon('edit')}</button><button type="button" class="pui-quiet-icon" data-queue-action="delete" aria-label="Delete pending message" title="Delete message">${icon('trash')}</button></div>`;
35
+ rows.set(item.id, row);
36
+ }
37
+ row.querySelector('.pui-queue-badge').hidden = item.mode !== 'steer';
38
+ const text = row.querySelector('.pui-queued-message-text');
39
+ const value = item.text || 'Image attachment';
40
+ if (text.textContent !== value)
41
+ text.textContent = value;
42
+ text.title = item.text;
43
+ const count = row.querySelector('.pui-queue-image-count');
44
+ const label = item.imageCount ? `${item.imageCount} image${item.imageCount === 1 ? '' : 's'}` : '';
45
+ if (count.textContent !== label)
46
+ count.textContent = label;
47
+ count.hidden = !item.imageCount;
48
+ for (const button of row.querySelectorAll('[data-queue-action]')) {
49
+ button.dataset.queuedMessage = item.id;
50
+ button.disabled = state.busy;
51
+ if (button.dataset.queueAction === 'steer')
52
+ button.hidden = item.mode !== 'follow-up' || !state.running;
53
+ }
54
+ if (row !== cursor)
55
+ element.insertBefore(row, cursor);
56
+ cursor = row.nextElementSibling;
57
+ }
58
+ }, destroy() { if (destroyed)
59
+ return; destroyed = true; element.removeEventListener('click', click); rows.clear(); element.replaceChildren(); } };
60
+ }
61
+ export function createDropFeedback(options) {
62
+ const element = options.container;
63
+ let destroyed = false;
64
+ return { element, update(state) {
65
+ if (destroyed)
66
+ return;
67
+ if (state.active && !state.disabled)
68
+ element.classList.add('pui-drag-images');
69
+ else
70
+ element.classList.remove('pui-drag-images');
71
+ }, destroy() { destroyed = true; element.classList.remove('pui-drag-images'); } };
72
+ }
73
+ export function createAttachments(options = {}) {
74
+ const element = options.container ?? document.createElement('div');
75
+ element.classList.add('pui-draft-images');
76
+ element.setAttribute('aria-label', 'Image attachments');
77
+ const click = (event) => {
78
+ const target = event.target;
79
+ const button = target.closest('[data-remove-image]');
80
+ if (button && !button.disabled)
81
+ options.onRemove?.(button.dataset.removeImage);
82
+ if (target instanceof HTMLImageElement)
83
+ options.onOpen?.(target);
84
+ };
85
+ element.addEventListener('click', click);
86
+ return { element, update(state) {
87
+ element.hidden = !state.items.length;
88
+ const keys = new Set(state.items.map(item => item.id));
89
+ for (const node of element.querySelectorAll('[data-attachment-id]'))
90
+ if (!keys.has(node.dataset.attachmentId))
91
+ node.remove();
92
+ for (const item of state.items) {
93
+ let figure = [...element.children].find(node => node.dataset.attachmentId === item.id);
94
+ if (!figure) {
95
+ figure = document.createElement('figure');
96
+ figure.className = 'pui-draft-image';
97
+ figure.dataset.attachmentId = item.id;
98
+ figure.innerHTML = `<img role="button" tabindex="0" title="Click to enlarge" /><figcaption></figcaption><button type="button" class="pui-remove-image">${icon('close')}</button>`;
99
+ }
100
+ const image = figure.querySelector('img');
101
+ if (image.getAttribute('src') !== item.url)
102
+ image.src = item.url;
103
+ image.alt = item.name;
104
+ figure.querySelector('figcaption').textContent = item.name;
105
+ const button = figure.querySelector('button');
106
+ button.dataset.removeImage = item.id;
107
+ button.setAttribute('aria-label', 'Remove ' + item.name);
108
+ button.disabled = !!state.disabled;
109
+ if (element.children[state.items.indexOf(item)] !== figure)
110
+ element.insertBefore(figure, element.children[state.items.indexOf(item)] ?? null);
111
+ }
112
+ }, destroy() { element.removeEventListener('click', click); element.replaceChildren(); } };
113
+ }
114
+ //# sourceMappingURL=composer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer.js","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { UIBlock } from './core.js';\nimport { escape, icon } from './dom.js';\n\nexport interface PendingMessage { id: string; text: string; mode: string; imageCount?: number; }\nexport interface PendingMessagesState { items: readonly PendingMessage[]; running: boolean; busy: boolean; }\nexport interface PendingMessagesOptions {\n container?: HTMLElement;\n onAction?: (action: 'edit' | 'delete' | 'steer', id: string) => void;\n}\nexport function createPendingMessages(options: PendingMessagesOptions = {}): UIBlock<PendingMessagesState> {\n const element = options.container ?? document.createElement('section');\n element.classList.add('pui-queued-messages'); element.setAttribute('aria-label', 'Pending messages');\n let destroyed = false;\n const rows = new Map<string, HTMLElement>();\n const heading = document.createElement('div'); heading.className = 'pui-section-heading'; heading.textContent = 'Pending messages';\n element.append(heading);\n const click = (event: Event) => {\n const button = (event.target as Element).closest<HTMLButtonElement>('[data-queue-action]');\n if (button && !button.disabled) options.onAction?.(button.dataset.queueAction as 'edit' | 'delete' | 'steer', button.dataset.queuedMessage!);\n };\n element.addEventListener('click', click);\n return { element, update(state) {\n if (destroyed) return;\n element.hidden = !state.items.length;\n const keep = new Set(state.items.map(item => item.id));\n for (const [key, row] of rows) if (!keep.has(key)) { row.remove(); rows.delete(key); }\n let cursor: Element | null = heading.nextElementSibling;\n for (const item of state.items) {\n let row = rows.get(item.id);\n if (!row) {\n row = document.createElement('div'); row.className = 'pui-queued-message';\n row.innerHTML = `<span class=\"pui-queue-badge\">Instruction</span><div class=\"pui-queued-message-content\"><span class=\"pui-queued-message-text\"></span><small class=\"pui-queue-image-count\"></small></div><div class=\"pui-queued-message-actions\"><button type=\"button\" class=\"pui-quiet-icon pui-queue-steer\" data-queue-action=\"steer\" title=\"Steer the current task at its next turn\">${icon('steer')}<span>Steer</span></button><button type=\"button\" class=\"pui-quiet-icon\" data-queue-action=\"edit\" aria-label=\"Edit pending message\" title=\"Edit message\">${icon('edit')}</button><button type=\"button\" class=\"pui-quiet-icon\" data-queue-action=\"delete\" aria-label=\"Delete pending message\" title=\"Delete message\">${icon('trash')}</button></div>`;\n rows.set(item.id, row);\n }\n row.querySelector<HTMLElement>('.pui-queue-badge')!.hidden = item.mode !== 'steer';\n const text = row.querySelector<HTMLElement>('.pui-queued-message-text')!;\n const value = item.text || 'Image attachment'; if (text.textContent !== value) text.textContent = value; text.title = item.text;\n const count = row.querySelector<HTMLElement>('.pui-queue-image-count')!;\n const label = item.imageCount ? `${item.imageCount} image${item.imageCount === 1 ? '' : 's'}` : '';\n if (count.textContent !== label) count.textContent = label; count.hidden = !item.imageCount;\n for (const button of row.querySelectorAll<HTMLButtonElement>('[data-queue-action]')) {\n button.dataset.queuedMessage = item.id; button.disabled = state.busy;\n if (button.dataset.queueAction === 'steer') button.hidden = item.mode !== 'follow-up' || !state.running;\n }\n if (row !== cursor) element.insertBefore(row, cursor);\n cursor = row.nextElementSibling;\n }\n }, destroy() { if (destroyed) return; destroyed = true; element.removeEventListener('click', click); rows.clear(); element.replaceChildren(); } };\n}\n\nexport interface AttachmentItem { id: string; url: string; name: string; }\n\nexport function createDropFeedback(options: { container: HTMLElement }): UIBlock<{ active: boolean; disabled?: boolean }> {\n const element = options.container;\n let destroyed = false;\n return { element, update(state) {\n if (destroyed) return;\n if (state.active && !state.disabled) element.classList.add('pui-drag-images');\n else element.classList.remove('pui-drag-images');\n }, destroy() { destroyed = true; element.classList.remove('pui-drag-images'); } };\n}\nexport interface AttachmentsState { items: readonly AttachmentItem[]; disabled?: boolean; }\nexport function createAttachments(options: { container?: HTMLElement; onRemove?: (id: string) => void; onOpen?: (image: HTMLImageElement) => void } = {}): UIBlock<AttachmentsState> {\n const element = options.container ?? document.createElement('div'); element.classList.add('pui-draft-images'); element.setAttribute('aria-label', 'Image attachments');\n const click = (event: Event) => {\n const target = event.target as Element;\n const button = target.closest<HTMLButtonElement>('[data-remove-image]');\n if (button && !button.disabled) options.onRemove?.(button.dataset.removeImage!);\n if (target instanceof HTMLImageElement) options.onOpen?.(target);\n };\n element.addEventListener('click', click);\n return { element, update(state) {\n element.hidden = !state.items.length;\n const keys = new Set(state.items.map(item => item.id));\n for (const node of element.querySelectorAll<HTMLElement>('[data-attachment-id]')) if (!keys.has(node.dataset.attachmentId!)) node.remove();\n for (const item of state.items) {\n let figure = [...element.children].find(node => (node as HTMLElement).dataset.attachmentId === item.id) as HTMLElement | undefined;\n if (!figure) { figure = document.createElement('figure'); figure.className = 'pui-draft-image'; figure.dataset.attachmentId = item.id;\n figure.innerHTML = `<img role=\"button\" tabindex=\"0\" title=\"Click to enlarge\" /><figcaption></figcaption><button type=\"button\" class=\"pui-remove-image\">${icon('close')}</button>`; }\n const image = figure.querySelector('img')!; if (image.getAttribute('src') !== item.url) image.src = item.url; image.alt = item.name;\n figure.querySelector('figcaption')!.textContent = item.name;\n const button = figure.querySelector('button')!; button.dataset.removeImage = item.id; button.setAttribute('aria-label', 'Remove ' + item.name); button.disabled = !!state.disabled;\n if (element.children[state.items.indexOf(item)] !== figure) element.insertBefore(figure, element.children[state.items.indexOf(item)] ?? null);\n }\n }, destroy() { element.removeEventListener('click', click); element.replaceChildren(); } };\n}\n"]}
@@ -0,0 +1,19 @@
1
+ export interface TextRange {
2
+ start: number;
3
+ end: number;
4
+ }
5
+ export declare function findRanges(text: string, query: string): TextRange[];
6
+ export interface ConversationReaderOptions {
7
+ keyboardTarget?: EventTarget;
8
+ isKeyboardBlocked?: () => boolean;
9
+ }
10
+ export declare function installConversationReader(scroll: HTMLElement, messages: HTMLElement, bar: HTMLElement, latest: HTMLButtonElement, options?: ConversationReaderOptions): {
11
+ beforeRender(id: string): void;
12
+ afterRender(): void;
13
+ prune(ids: string[]): void;
14
+ followLatest: () => void;
15
+ openFind: () => void;
16
+ closeFind: () => void;
17
+ destroy(): void;
18
+ };
19
+ //# sourceMappingURL=conversation-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-reader.d.ts","sourceRoot":"","sources":["../../src/content/conversation-reader.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAGzD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,CAInE;AAKD,MAAM,WAAW,yBAAyB;IACxC,cAAc,CAAC,EAAE,WAAW,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC;CACnC;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,GAAE,yBAA8B;qBA2LrJ,MAAM,GAAG,IAAI;mBAQf,IAAI;eAYR,MAAM,EAAE,GAAG,IAAI;wBArJH,IAAI;oBAiFR,IAAI;qBAMH,IAAI;eAkEb,IAAI;EAYlB"}
@@ -0,0 +1,280 @@
1
+ // Literal, case-insensitive matches, with offsets into the original text.
2
+ export function findRanges(text, query) {
3
+ if (!query)
4
+ return [];
5
+ const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'giu');
6
+ return [...text.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length }));
7
+ }
8
+ export function installConversationReader(scroll, messages, bar, latest, options = {}) {
9
+ let destroyed = false;
10
+ const removals = [];
11
+ function listen(target, type, listener, settings) {
12
+ target.addEventListener(type, listener, settings);
13
+ removals.push(() => target.removeEventListener?.(type, listener, settings));
14
+ }
15
+ const field = bar.querySelector('input');
16
+ const count = bar.querySelector('[data-find-count]');
17
+ const previous = bar.querySelector('[data-find-previous]');
18
+ const next = bar.querySelector('[data-find-next]');
19
+ const close = bar.querySelector('[data-find-close]');
20
+ const positions = new Map();
21
+ let activeID = '';
22
+ let matches = [];
23
+ let selected = -1;
24
+ let frame = 0;
25
+ let pendingPosition;
26
+ let restoring = false;
27
+ let searchSignature = '';
28
+ let contentHeight = scroll.scrollHeight;
29
+ let viewportHeight = scroll.clientHeight;
30
+ let viewportWidth = scroll.clientWidth;
31
+ const nearBottom = () => scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 80;
32
+ const layoutChanged = () => contentHeight !== scroll.scrollHeight || viewportHeight !== scroll.clientHeight || viewportWidth !== scroll.clientWidth;
33
+ function recordLayout() {
34
+ contentHeight = scroll.scrollHeight;
35
+ viewportHeight = scroll.clientHeight;
36
+ viewportWidth = scroll.clientWidth;
37
+ }
38
+ function capture(follow = nearBottom()) {
39
+ const top = scroll.getBoundingClientRect().top;
40
+ const anchor = [...messages.querySelectorAll('[data-reading-id]')].find((element) => element.getBoundingClientRect().bottom > top);
41
+ return { top: scroll.scrollTop, follow, anchor: anchor?.dataset.readingId, offset: anchor ? anchor.getBoundingClientRect().top - top : undefined };
42
+ }
43
+ function updateLatest() { latest.hidden = !messages.childElementCount || nearBottom(); }
44
+ function remember() {
45
+ if (activeID && !restoring && !pendingPosition)
46
+ positions.set(activeID, capture());
47
+ recordLayout();
48
+ updateLatest();
49
+ }
50
+ function restore(position) {
51
+ restoring = true;
52
+ if (position.follow)
53
+ scroll.scrollTop = scroll.scrollHeight;
54
+ else {
55
+ const anchor = [...messages.querySelectorAll('[data-reading-id]')].find((element) => element.dataset.readingId === position.anchor);
56
+ scroll.scrollTop = anchor && position.offset !== undefined
57
+ ? scroll.scrollTop + anchor.getBoundingClientRect().top - scroll.getBoundingClientRect().top - position.offset
58
+ : position.top;
59
+ }
60
+ restoring = false;
61
+ // Following is user intent. Layout changes (including image decoding) must
62
+ // not turn it off, or turn a history reader back into a follower.
63
+ if (activeID)
64
+ positions.set(activeID, capture(position.follow));
65
+ recordLayout();
66
+ updateLatest();
67
+ }
68
+ function followLatest() {
69
+ const position = { top: 0, follow: true };
70
+ if (pendingPosition)
71
+ pendingPosition = position;
72
+ restore(position);
73
+ }
74
+ let cachedMatches = new WeakMap();
75
+ function clearMarks(root = messages) {
76
+ for (const mark of root.querySelectorAll('mark[data-find-match]'))
77
+ mark.replaceWith(document.createTextNode(mark.textContent || ''));
78
+ root.normalize();
79
+ if (root === messages)
80
+ cachedMatches = new WeakMap();
81
+ else
82
+ cachedMatches.delete(root);
83
+ }
84
+ function markMatches() {
85
+ const previousKey = matches[selected]?.key;
86
+ matches = [];
87
+ const query = field.value;
88
+ if (!bar.hidden && query) {
89
+ for (const root of messages.querySelectorAll('[data-reading-id]')) {
90
+ const cached = cachedMatches.get(root);
91
+ if (cached?.query === query && cached.version === root.dataset.renderVersion) {
92
+ matches.push(...cached.matches);
93
+ continue;
94
+ }
95
+ clearMarks(root);
96
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
97
+ acceptNode: (node) => node.parentElement?.closest('button, summary, script, style, [data-find-ignore]') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT,
98
+ });
99
+ const nodes = [];
100
+ let text = '', block = null;
101
+ while (walker.nextNode()) {
102
+ const node = walker.currentNode;
103
+ const parent = node.parentElement?.closest('p, pre, li, h1, h2, h3, h4, h5, h6, .pui-user-text, .pui-system-message') || root;
104
+ if (text && parent !== block)
105
+ text += '\n';
106
+ block = parent;
107
+ const start = text.length;
108
+ text += node.data;
109
+ nodes.push({ node, start, end: text.length });
110
+ }
111
+ const ranges = findRanges(text, query);
112
+ const base = matches.length;
113
+ matches.push(...ranges.map((range) => ({ key: `${root.dataset.readingId}:${range.start}`, parts: [] })));
114
+ for (const { node, start, end } of nodes) {
115
+ const intersections = ranges.map((range, index) => ({ start: Math.max(start, range.start) - start, end: Math.min(end, range.end) - start, index: base + index })).filter((range) => range.end > range.start);
116
+ if (!intersections.length)
117
+ continue;
118
+ const fragment = document.createDocumentFragment();
119
+ let offset = 0;
120
+ for (const range of intersections) {
121
+ fragment.append(document.createTextNode(node.data.slice(offset, range.start)));
122
+ const mark = document.createElement('mark');
123
+ mark.dataset.findMatch = String(range.index);
124
+ mark.textContent = node.data.slice(range.start, range.end);
125
+ matches[range.index].parts.push(mark);
126
+ fragment.append(mark);
127
+ offset = range.end;
128
+ }
129
+ fragment.append(document.createTextNode(node.data.slice(offset)));
130
+ node.replaceWith(fragment);
131
+ }
132
+ cachedMatches.set(root, { query, version: root.dataset.renderVersion, matches: matches.slice(base) });
133
+ }
134
+ }
135
+ else
136
+ clearMarks();
137
+ const kept = matches.findIndex((match) => match.key === previousKey);
138
+ selected = matches.length ? kept >= 0 ? kept : Math.max(0, Math.min(selected, matches.length - 1)) : -1;
139
+ updateSelection();
140
+ }
141
+ function updateSelection() {
142
+ matches.forEach((match, index) => match.parts.forEach((part) => part.classList.toggle('pui-find-current', index === selected)));
143
+ count.textContent = field.value ? matches.length ? `${selected + 1} / ${matches.length}` : 'No matches' : '';
144
+ previous.disabled = next.disabled = !matches.length;
145
+ }
146
+ function reveal() {
147
+ const target = matches[selected]?.parts[0];
148
+ if (!target)
149
+ return;
150
+ for (let parent = target.parentElement; parent && parent !== messages; parent = parent.parentElement)
151
+ if (parent instanceof HTMLDetailsElement)
152
+ parent.open = true;
153
+ scroll.scrollTop += target.getBoundingClientRect().top - scroll.getBoundingClientRect().top - scroll.clientHeight / 3;
154
+ remember();
155
+ }
156
+ function move(direction) {
157
+ if (!matches.length)
158
+ return;
159
+ selected = (selected + direction + matches.length) % matches.length;
160
+ updateSelection();
161
+ reveal();
162
+ }
163
+ function openFind() {
164
+ bar.hidden = false;
165
+ markMatches();
166
+ field.focus();
167
+ field.select();
168
+ }
169
+ function closeFind() {
170
+ bar.hidden = true;
171
+ clearMarks();
172
+ matches = [];
173
+ selected = -1;
174
+ scroll.focus({ preventScroll: true });
175
+ updateLatest();
176
+ }
177
+ listen(field, 'input', () => { selected = -1; matches = []; markMatches(); reveal(); });
178
+ listen(field, 'keydown', (event) => {
179
+ if (event.key === 'Enter' && !event.isComposing) {
180
+ event.preventDefault();
181
+ move(event.shiftKey ? -1 : 1);
182
+ }
183
+ });
184
+ listen(previous, 'click', () => move(-1));
185
+ listen(next, 'click', () => move(1));
186
+ listen(close, 'click', closeFind);
187
+ listen(options.keyboardTarget || document, 'keydown', (event) => {
188
+ if (event.isComposing || options.isKeyboardBlocked?.())
189
+ return;
190
+ if ((event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === 'f') {
191
+ event.preventDefault();
192
+ openFind();
193
+ }
194
+ else if (event.key === 'Escape' && !bar.hidden) {
195
+ event.preventDefault();
196
+ closeFind();
197
+ }
198
+ });
199
+ listen(latest, 'click', followLatest);
200
+ listen(scroll, 'scroll', () => {
201
+ if (restoring || pendingPosition)
202
+ return;
203
+ const saved = positions.get(activeID);
204
+ // A resize can clamp scrollTop and dispatch a scroll before ResizeObserver.
205
+ // Only scrolling within a stable layout changes the following preference.
206
+ if (saved && layoutChanged())
207
+ restore(saved);
208
+ else if (!saved || Math.abs(scroll.scrollTop - saved.top) > 1)
209
+ remember();
210
+ else
211
+ updateLatest();
212
+ }, { passive: true });
213
+ // Async image decoding and resizing must not displace an existing anchor.
214
+ const observer = new ResizeObserver(() => {
215
+ if (destroyed)
216
+ return;
217
+ if (!pendingPosition && activeID) {
218
+ const saved = positions.get(activeID);
219
+ if (saved)
220
+ restore(saved);
221
+ }
222
+ updateLatest();
223
+ });
224
+ observer.observe(messages);
225
+ observer.observe(scroll);
226
+ return {
227
+ beforeRender(id) {
228
+ if (destroyed)
229
+ return;
230
+ if (activeID && !positions.has(activeID))
231
+ positions.set(activeID, capture());
232
+ pendingPosition = id === activeID ? pendingPosition || positions.get(activeID) || capture() : positions.get(id) || { top: 0, follow: true };
233
+ if (id !== activeID) {
234
+ selected = -1;
235
+ matches = [];
236
+ searchSignature = '';
237
+ }
238
+ activeID = id;
239
+ cancelAnimationFrame(frame);
240
+ },
241
+ afterRender() {
242
+ if (destroyed)
243
+ return;
244
+ const signature = messages.innerHTML;
245
+ if (!bar.hidden && signature !== searchSignature) {
246
+ markMatches();
247
+ searchSignature = messages.innerHTML;
248
+ }
249
+ frame = requestAnimationFrame(() => {
250
+ if (destroyed)
251
+ return;
252
+ const position = pendingPosition;
253
+ pendingPosition = undefined;
254
+ if (position)
255
+ restore(position);
256
+ else
257
+ updateLatest();
258
+ });
259
+ },
260
+ prune(ids) { const keep = new Set(ids); for (const id of positions.keys())
261
+ if (!keep.has(id))
262
+ positions.delete(id); },
263
+ followLatest,
264
+ openFind,
265
+ closeFind,
266
+ destroy() {
267
+ if (destroyed)
268
+ return;
269
+ destroyed = true;
270
+ cancelAnimationFrame(frame);
271
+ observer.disconnect();
272
+ removals.splice(0).forEach((remove) => remove());
273
+ clearMarks();
274
+ positions.clear();
275
+ matches = [];
276
+ pendingPosition = undefined;
277
+ },
278
+ };
279
+ }
280
+ //# sourceMappingURL=conversation-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-reader.js","sourceRoot":"","sources":["../../src/content/conversation-reader.ts"],"names":[],"mappings":"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","sourcesContent":["export interface TextRange { start: number; end: number }\n\n// Literal, case-insensitive matches, with offsets into the original text.\nexport function findRanges(text: string, query: string): TextRange[] {\n if (!query) return [];\n const pattern = new RegExp(query.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'giu');\n return [...text.matchAll(pattern)].map((match) => ({ start: match.index!, end: match.index! + match[0].length }));\n}\n\ninterface ReadingPosition { top: number; follow: boolean; anchor?: string; offset?: number }\ninterface Match { key: string; parts: HTMLElement[] }\n\nexport interface ConversationReaderOptions {\n keyboardTarget?: EventTarget;\n isKeyboardBlocked?: () => boolean;\n}\n\nexport function installConversationReader(scroll: HTMLElement, messages: HTMLElement, bar: HTMLElement, latest: HTMLButtonElement, options: ConversationReaderOptions = {}) {\n let destroyed = false;\n const removals: Array<() => void> = [];\n function listen<T extends Event>(target: EventTarget, type: string, listener: (event: T) => void, settings?: AddEventListenerOptions): void {\n target.addEventListener(type, listener as EventListener, settings);\n removals.push(() => target.removeEventListener?.(type, listener as EventListener, settings));\n }\n const field = bar.querySelector<HTMLInputElement>('input')!;\n const count = bar.querySelector<HTMLElement>('[data-find-count]')!;\n const previous = bar.querySelector<HTMLButtonElement>('[data-find-previous]')!;\n const next = bar.querySelector<HTMLButtonElement>('[data-find-next]')!;\n const close = bar.querySelector<HTMLButtonElement>('[data-find-close]')!;\n const positions = new Map<string, ReadingPosition>();\n let activeID = '';\n let matches: Match[] = [];\n let selected = -1;\n let frame = 0;\n let pendingPosition: ReadingPosition | undefined;\n let restoring = false;\n let searchSignature = '';\n let contentHeight = scroll.scrollHeight;\n let viewportHeight = scroll.clientHeight;\n let viewportWidth = scroll.clientWidth;\n\n const nearBottom = () => scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 80;\n const layoutChanged = () => contentHeight !== scroll.scrollHeight || viewportHeight !== scroll.clientHeight || viewportWidth !== scroll.clientWidth;\n function recordLayout(): void {\n contentHeight = scroll.scrollHeight;\n viewportHeight = scroll.clientHeight;\n viewportWidth = scroll.clientWidth;\n }\n function capture(follow = nearBottom()): ReadingPosition {\n const top = scroll.getBoundingClientRect().top;\n const anchor = [...messages.querySelectorAll<HTMLElement>('[data-reading-id]')].find((element) => element.getBoundingClientRect().bottom > top);\n return { top: scroll.scrollTop, follow, anchor: anchor?.dataset.readingId, offset: anchor ? anchor.getBoundingClientRect().top - top : undefined };\n }\n function updateLatest(): void { latest.hidden = !messages.childElementCount || nearBottom(); }\n function remember(): void {\n if (activeID && !restoring && !pendingPosition) positions.set(activeID, capture());\n recordLayout();\n updateLatest();\n }\n function restore(position: ReadingPosition): void {\n restoring = true;\n if (position.follow) scroll.scrollTop = scroll.scrollHeight;\n else {\n const anchor = [...messages.querySelectorAll<HTMLElement>('[data-reading-id]')].find((element) => element.dataset.readingId === position.anchor);\n scroll.scrollTop = anchor && position.offset !== undefined\n ? scroll.scrollTop + anchor.getBoundingClientRect().top - scroll.getBoundingClientRect().top - position.offset\n : position.top;\n }\n restoring = false;\n // Following is user intent. Layout changes (including image decoding) must\n // not turn it off, or turn a history reader back into a follower.\n if (activeID) positions.set(activeID, capture(position.follow));\n recordLayout();\n updateLatest();\n }\n function followLatest(): void {\n const position = { top: 0, follow: true };\n if (pendingPosition) pendingPosition = position;\n restore(position);\n }\n\n let cachedMatches = new WeakMap<HTMLElement, { query: string; version?: string; matches: typeof matches }>();\n function clearMarks(root: HTMLElement = messages): void {\n for (const mark of root.querySelectorAll('mark[data-find-match]')) mark.replaceWith(document.createTextNode(mark.textContent || ''));\n root.normalize();\n if (root === messages) cachedMatches = new WeakMap();\n else cachedMatches.delete(root);\n }\n function markMatches(): void {\n const previousKey = matches[selected]?.key;\n matches = [];\n const query = field.value;\n if (!bar.hidden && query) {\n for (const root of messages.querySelectorAll<HTMLElement>('[data-reading-id]')) {\n const cached = cachedMatches.get(root);\n if (cached?.query === query && cached.version === root.dataset.renderVersion) { matches.push(...cached.matches); continue; }\n clearMarks(root);\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode: (node) => node.parentElement?.closest('button, summary, script, style, [data-find-ignore]') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT,\n });\n const nodes: { node: Text; start: number; end: number }[] = [];\n let text = '', block: Element | null = null;\n while (walker.nextNode()) {\n const node = walker.currentNode as Text;\n const parent = node.parentElement?.closest('p, pre, li, h1, h2, h3, h4, h5, h6, .pui-user-text, .pui-system-message') || root;\n if (text && parent !== block) text += '\\n';\n block = parent;\n const start = text.length;\n text += node.data;\n nodes.push({ node, start, end: text.length });\n }\n const ranges = findRanges(text, query);\n const base = matches.length;\n matches.push(...ranges.map((range) => ({ key: `${root.dataset.readingId}:${range.start}`, parts: [] })));\n for (const { node, start, end } of nodes) {\n const intersections = ranges.map((range, index) => ({ start: Math.max(start, range.start) - start, end: Math.min(end, range.end) - start, index: base + index })).filter((range) => range.end > range.start);\n if (!intersections.length) continue;\n const fragment = document.createDocumentFragment();\n let offset = 0;\n for (const range of intersections) {\n fragment.append(document.createTextNode(node.data.slice(offset, range.start)));\n const mark = document.createElement('mark');\n mark.dataset.findMatch = String(range.index);\n mark.textContent = node.data.slice(range.start, range.end);\n matches[range.index].parts.push(mark);\n fragment.append(mark);\n offset = range.end;\n }\n fragment.append(document.createTextNode(node.data.slice(offset)));\n node.replaceWith(fragment);\n }\n cachedMatches.set(root, { query, version: root.dataset.renderVersion, matches: matches.slice(base) });\n }\n } else clearMarks();\n const kept = matches.findIndex((match) => match.key === previousKey);\n selected = matches.length ? kept >= 0 ? kept : Math.max(0, Math.min(selected, matches.length - 1)) : -1;\n updateSelection();\n }\n function updateSelection(): void {\n matches.forEach((match, index) => match.parts.forEach((part) => part.classList.toggle('pui-find-current', index === selected)));\n count.textContent = field.value ? matches.length ? `${selected + 1} / ${matches.length}` : 'No matches' : '';\n previous.disabled = next.disabled = !matches.length;\n }\n function reveal(): void {\n const target = matches[selected]?.parts[0];\n if (!target) return;\n for (let parent = target.parentElement; parent && parent !== messages; parent = parent.parentElement) if (parent instanceof HTMLDetailsElement) parent.open = true;\n scroll.scrollTop += target.getBoundingClientRect().top - scroll.getBoundingClientRect().top - scroll.clientHeight / 3;\n remember();\n }\n function move(direction: number): void {\n if (!matches.length) return;\n selected = (selected + direction + matches.length) % matches.length;\n updateSelection();\n reveal();\n }\n function openFind(): void {\n bar.hidden = false;\n markMatches();\n field.focus();\n field.select();\n }\n function closeFind(): void {\n bar.hidden = true;\n clearMarks();\n matches = [];\n selected = -1;\n scroll.focus({ preventScroll: true });\n updateLatest();\n }\n listen(field, 'input', () => { selected = -1; matches = []; markMatches(); reveal(); });\n listen<KeyboardEvent>(field, 'keydown', (event) => {\n if (event.key === 'Enter' && !event.isComposing) { event.preventDefault(); move(event.shiftKey ? -1 : 1); }\n });\n listen(previous, 'click', () => move(-1));\n listen(next, 'click', () => move(1));\n listen(close, 'click', closeFind);\n listen<KeyboardEvent>(options.keyboardTarget || document, 'keydown', (event) => {\n if (event.isComposing || options.isKeyboardBlocked?.()) return;\n if ((event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === 'f') { event.preventDefault(); openFind(); }\n else if (event.key === 'Escape' && !bar.hidden) { event.preventDefault(); closeFind(); }\n });\n listen(latest, 'click', followLatest);\n listen(scroll, 'scroll', () => {\n if (restoring || pendingPosition) return;\n const saved = positions.get(activeID);\n // A resize can clamp scrollTop and dispatch a scroll before ResizeObserver.\n // Only scrolling within a stable layout changes the following preference.\n if (saved && layoutChanged()) restore(saved);\n else if (!saved || Math.abs(scroll.scrollTop - saved.top) > 1) remember();\n else updateLatest();\n }, { passive: true });\n // Async image decoding and resizing must not displace an existing anchor.\n const observer = new ResizeObserver(() => {\n if (destroyed) return;\n if (!pendingPosition && activeID) {\n const saved = positions.get(activeID);\n if (saved) restore(saved);\n }\n updateLatest();\n });\n observer.observe(messages);\n observer.observe(scroll);\n return {\n beforeRender(id: string): void {\n if (destroyed) return;\n if (activeID && !positions.has(activeID)) positions.set(activeID, capture());\n pendingPosition = id === activeID ? pendingPosition || positions.get(activeID) || capture() : positions.get(id) || { top: 0, follow: true };\n if (id !== activeID) { selected = -1; matches = []; searchSignature = ''; }\n activeID = id;\n cancelAnimationFrame(frame);\n },\n afterRender(): void {\n if (destroyed) return;\n const signature = messages.innerHTML;\n if (!bar.hidden && signature !== searchSignature) { markMatches(); searchSignature = messages.innerHTML; }\n frame = requestAnimationFrame(() => {\n if (destroyed) return;\n const position = pendingPosition;\n pendingPosition = undefined;\n if (position) restore(position);\n else updateLatest();\n });\n },\n prune(ids: string[]): void { const keep = new Set(ids); for (const id of positions.keys()) if (!keep.has(id)) positions.delete(id); },\n followLatest,\n openFind,\n closeFind,\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n cancelAnimationFrame(frame);\n observer.disconnect();\n removals.splice(0).forEach((remove) => remove());\n clearMarks();\n positions.clear();\n matches = [];\n pendingPosition = undefined;\n },\n };\n}\n"]}