pith-ui 0.1.1 → 0.2.0-app.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.
Files changed (140) hide show
  1. package/AI.md +33 -8
  2. package/API.md +424 -268
  3. package/APP-API.md +2019 -0
  4. package/APP.md +137 -0
  5. package/README.md +37 -4
  6. package/dist/app/files.d.ts +68 -0
  7. package/dist/app/files.d.ts.map +1 -0
  8. package/dist/app/files.js +177 -0
  9. package/dist/app/files.js.map +1 -0
  10. package/dist/app/index.d.ts +57 -0
  11. package/dist/app/index.d.ts.map +1 -0
  12. package/dist/app/index.js +300 -0
  13. package/dist/app/index.js.map +1 -0
  14. package/dist/app/internal.d.ts +31 -0
  15. package/dist/app/internal.d.ts.map +1 -0
  16. package/dist/app/internal.js +2 -0
  17. package/dist/app/internal.js.map +1 -0
  18. package/dist/app/resources.d.ts +89 -0
  19. package/dist/app/resources.d.ts.map +1 -0
  20. package/dist/app/resources.js +190 -0
  21. package/dist/app/resources.js.map +1 -0
  22. package/dist/app/session.d.ts +14 -0
  23. package/dist/app/session.d.ts.map +1 -0
  24. package/dist/app/session.js +12 -0
  25. package/dist/app/session.js.map +1 -0
  26. package/dist/app/settings.d.ts +109 -0
  27. package/dist/app/settings.d.ts.map +1 -0
  28. package/dist/app/settings.js +279 -0
  29. package/dist/app/settings.js.map +1 -0
  30. package/dist/app/workspace.d.ts +228 -0
  31. package/dist/app/workspace.d.ts.map +1 -0
  32. package/dist/app/workspace.js +402 -0
  33. package/dist/app/workspace.js.map +1 -0
  34. package/dist/composer.d.ts.map +1 -1
  35. package/dist/composer.js +37 -27
  36. package/dist/composer.js.map +1 -1
  37. package/dist/content/conversation-reader.d.ts.map +1 -1
  38. package/dist/content/conversation-reader.js +8 -1
  39. package/dist/content/conversation-reader.js.map +1 -1
  40. package/dist/content/conversation-view.d.ts.map +1 -1
  41. package/dist/content/conversation-view.js +34 -8
  42. package/dist/content/conversation-view.js.map +1 -1
  43. package/dist/content/image-viewer.d.ts.map +1 -1
  44. package/dist/content/image-viewer.js +11 -3
  45. package/dist/content/image-viewer.js.map +1 -1
  46. package/dist/content/paged-preview.d.ts +1 -1
  47. package/dist/content/paged-preview.d.ts.map +1 -1
  48. package/dist/content/paged-preview.js +4 -2
  49. package/dist/content/paged-preview.js.map +1 -1
  50. package/dist/content/views.d.ts +3 -0
  51. package/dist/content/views.d.ts.map +1 -1
  52. package/dist/content/views.js +110 -41
  53. package/dist/content/views.js.map +1 -1
  54. package/dist/content/welcome.d.ts.map +1 -1
  55. package/dist/content/welcome.js +5 -2
  56. package/dist/content/welcome.js.map +1 -1
  57. package/dist/dom.js +1 -1
  58. package/dist/dom.js.map +1 -1
  59. package/dist/files/panels.d.ts +2 -0
  60. package/dist/files/panels.d.ts.map +1 -1
  61. package/dist/files/panels.js +202 -128
  62. package/dist/files/panels.js.map +1 -1
  63. package/dist/files/systems.d.ts.map +1 -1
  64. package/dist/files/systems.js +37 -64
  65. package/dist/files/systems.js.map +1 -1
  66. package/dist/forms/actions.d.ts +24 -2
  67. package/dist/forms/actions.d.ts.map +1 -1
  68. package/dist/forms/actions.js +54 -25
  69. package/dist/forms/actions.js.map +1 -1
  70. package/dist/forms/custom-connection.d.ts.map +1 -1
  71. package/dist/forms/custom-connection.js +3 -2
  72. package/dist/forms/custom-connection.js.map +1 -1
  73. package/dist/forms/history.d.ts +1 -1
  74. package/dist/forms/history.d.ts.map +1 -1
  75. package/dist/forms/history.js +78 -15
  76. package/dist/forms/history.js.map +1 -1
  77. package/dist/forms/identity.d.ts.map +1 -1
  78. package/dist/forms/identity.js +9 -25
  79. package/dist/forms/identity.js.map +1 -1
  80. package/dist/forms/model-editor.js +3 -3
  81. package/dist/forms/model-editor.js.map +1 -1
  82. package/dist/forms/panels.d.ts +9 -2
  83. package/dist/forms/panels.d.ts.map +1 -1
  84. package/dist/forms/panels.js +79 -88
  85. package/dist/forms/panels.js.map +1 -1
  86. package/dist/forms/provider-login.d.ts.map +1 -1
  87. package/dist/forms/provider-login.js +20 -36
  88. package/dist/forms/provider-login.js.map +1 -1
  89. package/dist/forms/resource-editor.js +5 -4
  90. package/dist/forms/resource-editor.js.map +1 -1
  91. package/dist/foundations.d.ts +1 -1
  92. package/dist/foundations.d.ts.map +1 -1
  93. package/dist/foundations.js +22 -38
  94. package/dist/foundations.js.map +1 -1
  95. package/dist/navigation/context-menu.d.ts +7 -1
  96. package/dist/navigation/context-menu.d.ts.map +1 -1
  97. package/dist/navigation/context-menu.js +60 -57
  98. package/dist/navigation/context-menu.js.map +1 -1
  99. package/dist/navigation/navigation.d.ts +13 -0
  100. package/dist/navigation/navigation.d.ts.map +1 -1
  101. package/dist/navigation/navigation.js +96 -130
  102. package/dist/navigation/navigation.js.map +1 -1
  103. package/dist/operation.d.ts +12 -0
  104. package/dist/operation.d.ts.map +1 -0
  105. package/dist/operation.js +49 -0
  106. package/dist/operation.js.map +1 -0
  107. package/dist/primitives.d.ts +8 -1
  108. package/dist/primitives.d.ts.map +1 -1
  109. package/dist/primitives.js +114 -143
  110. package/dist/primitives.js.map +1 -1
  111. package/dist/reconcile.d.ts +11 -0
  112. package/dist/reconcile.d.ts.map +1 -0
  113. package/dist/reconcile.js +36 -0
  114. package/dist/reconcile.js.map +1 -0
  115. package/dist/select.d.ts +14 -0
  116. package/dist/select.d.ts.map +1 -0
  117. package/dist/select.js +304 -0
  118. package/dist/select.js.map +1 -0
  119. package/dist/styles.css +439 -370
  120. package/dist/surface.d.ts +31 -0
  121. package/dist/surface.d.ts.map +1 -1
  122. package/dist/surface.js +193 -3
  123. package/dist/surface.js.map +1 -1
  124. package/dist/tasks/approval.d.ts.map +1 -1
  125. package/dist/tasks/approval.js +83 -45
  126. package/dist/tasks/approval.js.map +1 -1
  127. package/dist/tasks/permission-element.d.ts +3 -0
  128. package/dist/tasks/permission-element.d.ts.map +1 -0
  129. package/dist/tasks/permission-element.js +10 -0
  130. package/dist/tasks/permission-element.js.map +1 -0
  131. package/dist/tasks/permission.d.ts.map +1 -1
  132. package/dist/tasks/permission.js +37 -18
  133. package/dist/tasks/permission.js.map +1 -1
  134. package/dist/tasks/runtime.d.ts.map +1 -1
  135. package/dist/tasks/runtime.js +32 -15
  136. package/dist/tasks/runtime.js.map +1 -1
  137. package/dist/workspace.d.ts.map +1 -1
  138. package/dist/workspace.js +93 -40
  139. package/dist/workspace.js.map +1 -1
  140. package/package.json +42 -8
@@ -2,6 +2,8 @@ import { dialogHeading, escape, icon, uniqueId, setText } from '../dom.js';
2
2
  import { createPagedPreview } from '../content/paged-preview.js';
3
3
  import { adoptProductParts } from '../foundations.js';
4
4
  import { createButton } from '../primitives.js';
5
+ import { bindContentClose, installAnchoredSurface } from '../surface.js';
6
+ import { reconcileKeyed } from '../reconcile.js';
5
7
  export function fileActionsMarkup(options) {
6
8
  const scope = `data-idle-action ${options.kind === 'resource' ? 'data-idle-workspace' : 'data-idle-conversation'}="${escape(options.ownerId)}"`;
7
9
  const attributes = `${scope} data-file-kind="${escape(options.kind)}" data-file-owner="${escape(options.ownerId)}" data-file-path="${escape(options.path)}"`;
@@ -13,14 +15,25 @@ export function createFileActions(options) {
13
15
  const template = document.createElement('template');
14
16
  template.innerHTML = fileActionsMarkup(options);
15
17
  const element = template.content.firstElementChild;
18
+ const parts = adoptProductParts(element);
19
+ let destroyed = false;
16
20
  const click = (event) => {
17
21
  const target = event.target.closest('button');
18
- if (target && !target.disabled)
22
+ if (target && element.contains(target) && !target.disabled) {
23
+ event.stopPropagation();
19
24
  options.onAction?.(target.dataset.fileAction || 'preview');
25
+ }
20
26
  };
21
27
  if (options.onAction)
22
28
  element.addEventListener('click', click);
23
- return { element, destroy() { element.removeEventListener('click', click); element.remove(); } };
29
+ return {
30
+ element,
31
+ setDisabled(disabled) { if (!destroyed)
32
+ for (const button of element.querySelectorAll('button'))
33
+ parts.action(button).update({ disabled }); },
34
+ destroy() { if (destroyed)
35
+ return; destroyed = true; parts.destroy(); element.removeEventListener('click', click); element.remove(); },
36
+ };
24
37
  }
25
38
  export function createArtifactPanel(options) {
26
39
  const { container: element, control, toggle, count } = options;
@@ -31,37 +44,35 @@ export function createArtifactPanel(options) {
31
44
  const error = element.querySelector('[role="alert"]');
32
45
  const parts = adoptProductParts(element);
33
46
  const entries = new Map();
34
- let open = false;
35
47
  let destroyed = false;
48
+ const surface = installAnchoredSurface({
49
+ trigger: toggle,
50
+ panel: element,
51
+ position: false,
52
+ closeOnFocusOut: true,
53
+ exclusions: [control, ...(options.dismissExclusions ?? [])],
54
+ canOpen: () => !control.hidden,
55
+ canEscape: () => !document.querySelector('dialog[open]'),
56
+ onToggle(open) { if (open)
57
+ element.focus(); },
58
+ });
36
59
  function setOpen(value, restoreFocus = false) {
37
60
  if (destroyed)
38
61
  return;
39
- open = value && !control.hidden;
40
- element.hidden = !open;
41
- toggle.setAttribute('aria-expanded', String(open));
42
- if (open)
43
- element.focus();
44
- else if (restoreFocus && !control.hidden)
45
- toggle.focus();
62
+ if (value)
63
+ surface.open();
64
+ else
65
+ surface.close(restoreFocus && !control.hidden);
46
66
  }
47
67
  const abort = new AbortController();
48
- toggle.addEventListener('click', () => setOpen(!open), { signal: abort.signal });
68
+ toggle.addEventListener('click', () => setOpen(!surface.isOpen()), { signal: abort.signal });
49
69
  element.addEventListener('click', event => { if (event.target.closest('[data-artifacts-close]'))
50
70
  setOpen(false, true); }, { signal: abort.signal });
51
- const outside = (event) => { const target = event.target; if (open && !element.contains(target) && !control.contains(target) && !options.dismissExclusions?.some(node => node.contains(target)))
52
- setOpen(false); };
53
- document.addEventListener('click', outside, { signal: abort.signal });
54
- document.addEventListener('focusin', outside, { signal: abort.signal });
55
- document.addEventListener('keydown', event => { if (event.key === 'Escape' && open && !document.querySelector('dialog[open]')) {
56
- event.preventDefault();
57
- setOpen(false, true);
58
- } }, { signal: abort.signal });
59
71
  return {
60
- element, setOpen, get open() { return open; },
72
+ element, setOpen, get open() { return surface.isOpen(); },
61
73
  update(state) {
62
74
  if (destroyed)
63
75
  return;
64
- const focused = document.activeElement;
65
76
  control.hidden = !state.ownerId || (!state.files.length && !state.error);
66
77
  if (control.hidden)
67
78
  setOpen(false);
@@ -74,61 +85,42 @@ export function createArtifactPanel(options) {
74
85
  loading.hidden = !state.loading;
75
86
  error.hidden = !state.error;
76
87
  parts.feedback(error).update({ message: state.error, tone: 'error' });
77
- const keep = new Set(state.files.map(file => `${state.ownerId}/${file.path}`));
78
- for (const [key, entry] of entries)
79
- if (!keep.has(key)) {
80
- entry.element.remove();
81
- entries.delete(key);
82
- }
83
- let cursor = rows.firstElementChild;
84
- for (const file of state.files) {
85
- const key = `${state.ownerId}/${file.path}`;
86
- let entry = entries.get(key);
87
- if (!entry) {
88
+ reconcileKeyed(rows, entries, state.files, {
89
+ key: file => `${state.ownerId}/${file.path}`,
90
+ element: entry => entry.element,
91
+ create(file) {
88
92
  const article = document.createElement('article');
89
93
  article.className = 'pui-file-card';
90
94
  article.innerHTML = `${icon('file')}<div class="pui-file-info"><strong></strong><span class="pui-file-path">${escape(file.path)}</span></div>${fileActionsMarkup({ kind: 'artifact', ownerId: state.ownerId, path: file.path, preview: true, copyMode: state.copyMode })}`;
91
- entry = { element: article, name: article.querySelector('strong'), copy: article.querySelector('[data-file-action="copy"], [data-file-action="copy-path"]') };
92
- entries.set(key, entry);
93
- }
94
- setText(entry.name, file.name);
95
- const copyAction = state.copyMode === 'file' ? 'copy' : 'copy-path';
96
- if (entry.copy.dataset.fileAction !== copyAction)
97
- entry.copy.dataset.fileAction = copyAction;
98
- if (entry.copy.dataset.copyPending !== 'true')
99
- setText(entry.copy, state.copyMode === 'file' ? 'Copy file' : 'Copy path');
100
- if (entry.element !== cursor)
101
- rows.insertBefore(entry.element, cursor);
102
- cursor = entry.element.nextElementSibling;
103
- }
104
- if (focused instanceof HTMLElement && element.contains(focused) && document.activeElement !== focused)
105
- focused.focus({ preventScroll: true });
95
+ return { element: article, name: article.querySelector('strong'), copy: article.querySelector('[data-file-action="copy"], [data-file-action="copy-path"]') };
96
+ },
97
+ update(entry, file) {
98
+ setText(entry.name, file.name);
99
+ const copyAction = state.copyMode === 'file' ? 'copy' : 'copy-path';
100
+ if (entry.copy.dataset.fileAction !== copyAction)
101
+ entry.copy.dataset.fileAction = copyAction;
102
+ if (entry.copy.dataset.copyPending !== 'true')
103
+ setText(entry.copy, state.copyMode === 'file' ? 'Copy file' : 'Copy path');
104
+ },
105
+ destroy() { },
106
+ });
107
+ parts.update();
106
108
  },
107
- destroy() { destroyed = true; abort.abort(); parts.destroy(); entries.clear(); element.replaceChildren(); },
109
+ destroy() { destroyed = true; surface.destroy(); abort.abort(); parts.destroy(); entries.clear(); element.replaceChildren(); },
108
110
  };
109
111
  }
110
112
  export function createResourcesPanel(options = {}) {
111
113
  const element = options.container || document.createElement('div');
112
114
  const ids = { titleId: options.titleId || uniqueId('pui-resources-title'), dialogId: options.dialogId || uniqueId('pui-resources-dialog'), listId: options.listId || uniqueId('pui-resources-list'), errorId: options.errorId || uniqueId('pui-resources-error') };
113
- const close = (event) => {
114
- if (!event.target.closest('[data-close]'))
115
- return;
116
- if (options.onClose)
117
- options.onClose();
118
- else {
119
- const dialog = element.closest('dialog');
120
- if (dialog?.open)
121
- dialog.close();
122
- else
123
- element.hidden = true;
124
- }
125
- };
126
- element.addEventListener('click', close);
115
+ const close = bindContentClose(element, { dialogId: ids.dialogId, onClose: options.onClose });
127
116
  let list, error;
128
117
  let destroyed = false;
129
118
  const parts = adoptProductParts(element);
130
119
  const buttons = [];
131
120
  const rowButtons = [];
121
+ const sections = new Map();
122
+ let diagnostics;
123
+ let instructionButton;
132
124
  const createActions = new Map();
133
125
  let createDisabled = false;
134
126
  const button = (label, onAction, row = false, create = false) => { const block = createButton({ label, appearance: 'secondary', onAction, disabled: create && createDisabled }); (row ? rowButtons : buttons).push(block); if (create)
@@ -137,25 +129,53 @@ export function createResourcesPanel(options = {}) {
137
129
  createActions.delete(block);
138
130
  block.destroy();
139
131
  } };
140
- function row(entry, type) {
132
+ function updateSlot(parent, slot, source) {
133
+ if (slot.source === source)
134
+ return;
135
+ for (const node of slot.nodes)
136
+ if (node.parentNode === parent)
137
+ node.remove();
138
+ slot.source = source;
139
+ slot.nodes = source ? source instanceof DocumentFragment ? [...source.childNodes] : [source] : [];
140
+ }
141
+ function row(type) {
141
142
  const outer = document.createElement('article');
142
143
  outer.className = type === 'instructions' ? 'pui-resource-card' : 'pui-file-card';
143
144
  const body = type === 'instructions' ? document.createElement('div') : outer;
144
145
  if (type === 'instructions')
145
146
  body.className = 'pui-file-card';
146
- body.innerHTML = `${type === 'template' ? '' : icon('file')}<div class="pui-file-info"><strong>${escape(entry.name)}</strong>${type !== 'instructions' ? `<p>${escape(entry.description || '')}</p>` : ''}<span class="pui-file-path">${escape(entry.path)}</span></div>`;
147
- if (entry.fileActions)
148
- body.append(entry.fileActions);
149
- if (entry.actions)
150
- body.append(entry.actions);
147
+ body.innerHTML = `${type === 'template' ? '' : icon('file')}<div class="pui-file-info"><strong></strong>${type !== 'instructions' ? '<p></p>' : ''}<span class="pui-file-path"></span></div>`;
148
+ let content;
151
149
  if (type === 'instructions') {
152
150
  outer.append(body);
153
151
  const details = document.createElement('details');
154
152
  details.className = 'pui-resource-preview';
155
- details.innerHTML = `<summary>View instructions</summary><pre>${escape(entry.content || '')}</pre>`;
153
+ details.innerHTML = '<summary>View instructions</summary><pre></pre>';
156
154
  outer.append(details);
155
+ content = details.querySelector('pre');
156
+ }
157
+ return { element: outer, body, name: body.querySelector('strong'), description: body.querySelector('p') || undefined, path: body.querySelector('.pui-file-path'), content, slots: [{ nodes: [] }, { nodes: [] }] };
158
+ }
159
+ function updateRow(row, entry) {
160
+ setText(row.name, entry.name);
161
+ setText(row.path, entry.path);
162
+ if (row.description)
163
+ setText(row.description, entry.description || '');
164
+ if (row.content)
165
+ setText(row.content, entry.content || '');
166
+ updateSlot(row.body, row.slots[0], entry.fileActions);
167
+ updateSlot(row.body, row.slots[1], entry.actions);
168
+ let cursor = row.body.querySelector('.pui-file-info').nextSibling;
169
+ for (const node of row.slots.flatMap(slot => slot.nodes)) {
170
+ if (node !== cursor)
171
+ row.body.insertBefore(node, cursor);
172
+ cursor = node.nextSibling;
157
173
  }
158
- return outer;
174
+ }
175
+ function section() {
176
+ const element = document.createElement('section');
177
+ element.className = 'pui-resource-section';
178
+ return { element, rows: new Map(), create: { nodes: [] } };
159
179
  }
160
180
  return {
161
181
  element,
@@ -164,6 +184,9 @@ export function createResourcesPanel(options = {}) {
164
184
  return;
165
185
  clearButtons();
166
186
  clearButtons(rowButtons);
187
+ instructionButton = undefined;
188
+ sections.clear();
189
+ diagnostics = undefined;
167
190
  element.hidden = false;
168
191
  element.innerHTML = `${dialogHeading('Workspace resources', ids.titleId, ids.dialogId, workspaceName)}<p class="pui-modal-description">Instructions, skills and prompt templates are loaded by Pith for each task. Edit them here or open their Markdown files.</p><div class="pui-feature-toolbar" data-resources-toolbar></div><div id="${escape(ids.listId)}" data-resources-list><p class="pui-feature-hint">Loading workspace resources…</p></div><div id="${escape(ids.errorId)}" data-resources-error class="pui-form-error" role="alert"></div>`;
169
192
  const actions = element.querySelector('[data-resources-toolbar]');
@@ -183,64 +206,96 @@ export function createResourcesPanel(options = {}) {
183
206
  update(state) {
184
207
  if (destroyed || !list)
185
208
  return;
186
- clearButtons(rowButtons);
187
- list.replaceChildren();
209
+ if (!sections.size)
210
+ list.replaceChildren();
188
211
  for (const [title, entries, type, empty] of [
189
212
  ['Instructions', state.instructions, 'instructions', 'No workspace instructions found.'],
190
213
  ['Skills', state.skills, 'skill', 'No skills found. Add .pi/skills/<skill-name>/SKILL.md in this workspace, then refresh.'],
191
214
  ['Prompt templates', state.templates, 'template', 'No prompt templates yet.'],
192
215
  ]) {
193
- const section = document.createElement('section');
194
- section.className = 'pui-resource-section';
195
- const heading = document.createElement('h3');
196
- heading.textContent = title;
197
- section.append(heading);
198
- if (entries.length)
199
- entries.forEach((entry) => section.append(row(entry, type)));
200
- else {
201
- const hint = document.createElement('p');
202
- hint.className = 'pui-feature-hint';
203
- hint.textContent = empty;
204
- section.append(hint);
216
+ let current = sections.get(type);
217
+ if (!current) {
218
+ current = section();
219
+ sections.set(type, current);
220
+ list.append(current.element);
221
+ }
222
+ const items = [{ key: 'heading', kind: 'heading' }, ...entries.map(entry => ({ key: `entry:${entry.path}`, kind: 'entry', entry }))];
223
+ if (!entries.length)
224
+ items.push({ key: 'empty', kind: 'empty' });
225
+ reconcileKeyed(current.element, current.rows, items, {
226
+ key: item => item.key, element: row => row.element,
227
+ create(item) {
228
+ if (item.kind === 'entry')
229
+ return row(type);
230
+ const element = document.createElement(item.kind === 'heading' ? 'h3' : 'p');
231
+ if (item.kind === 'empty')
232
+ element.className = 'pui-feature-hint';
233
+ return { element };
234
+ },
235
+ update(row, item) { if (item.entry)
236
+ updateRow(row, item.entry);
237
+ else
238
+ setText(row.element, item.kind === 'heading' ? title : empty); },
239
+ destroy() { },
240
+ });
241
+ if (type === 'instructions') {
242
+ let create = state.createInstructions;
243
+ if (!create && state.canCreateInstructions) {
244
+ if (!instructionButton) {
245
+ const element = button('Create AGENTS.md in this workspace', () => options.onCreate?.('instructions'), true, true);
246
+ instructionButton = rowButtons.find(block => block.element === element);
247
+ }
248
+ create = instructionButton.element;
249
+ }
250
+ updateSlot(current.element, current.create, create);
251
+ for (const node of current.create.nodes)
252
+ if (node.parentNode !== current.element)
253
+ current.element.append(node);
205
254
  }
206
- if (type === 'instructions' && state.createInstructions)
207
- section.append(state.createInstructions);
208
- else if (type === 'instructions' && state.canCreateInstructions)
209
- section.append(button('Create AGENTS.md in this workspace', () => options.onCreate?.('instructions'), true, true));
210
- list.append(section);
211
255
  }
212
- parts.update();
213
256
  if (state.diagnostics.length) {
214
- const section = document.createElement('section');
215
- section.className = 'pui-resource-section pui-resource-diagnostics';
216
- section.innerHTML = `<h3>Resource notes</h3>${state.diagnostics.map((note) => `<p>${escape(note)}</p>`).join('')}`;
217
- list.append(section);
257
+ if (!diagnostics) {
258
+ diagnostics = section();
259
+ diagnostics.element.classList.add('pui-resource-diagnostics');
260
+ list.append(diagnostics.element);
261
+ }
262
+ reconcileKeyed(diagnostics.element, diagnostics.rows, [{ key: 'heading', text: 'Resource notes' }, ...state.diagnostics.map((text, index) => ({ key: `note:${index}`, text }))], {
263
+ key: value => value.key, element: row => row.element,
264
+ create(value) { return { element: document.createElement(value.key === 'heading' ? 'h3' : 'p') }; },
265
+ update(row, value) { setText(row.element, value.text); }, destroy() { },
266
+ });
218
267
  }
268
+ else if (diagnostics) {
269
+ diagnostics.element.remove();
270
+ diagnostics = undefined;
271
+ }
272
+ parts.update();
219
273
  },
220
- destroy() { destroyed = true; clearButtons(); clearButtons(rowButtons); parts.destroy(); element.removeEventListener('click', close); element.replaceChildren(); },
274
+ destroy() { if (destroyed)
275
+ return; destroyed = true; clearButtons(); clearButtons(rowButtons); sections.clear(); diagnostics = undefined; parts.destroy(); close.destroy(); element.replaceChildren(); },
221
276
  };
222
277
  }
223
278
  export function createFilePreview(options = {}) {
224
279
  const element = options.container || document.createElement('div');
225
280
  const ids = { titleId: options.titleId || uniqueId('pui-preview-title'), dialogId: options.dialogId || uniqueId('pui-preview-dialog'), bodyId: options.bodyId || uniqueId('pui-preview-body') };
226
- const close = (event) => {
227
- if (!event.target.closest('[data-close]'))
228
- return;
229
- if (options.onClose)
230
- options.onClose();
231
- else {
232
- const dialog = element.closest('dialog');
233
- if (dialog?.open)
234
- dialog.close();
235
- else
236
- element.hidden = true;
237
- }
238
- };
239
- element.addEventListener('click', close);
281
+ const close = bindContentClose(element, { dialogId: ids.dialogId, onClose: options.onClose });
240
282
  let body;
241
283
  let page;
284
+ let image;
285
+ let note;
286
+ let mode;
242
287
  let destroyed = false;
243
- function clear() { page?.destroy(); page = undefined; element.replaceChildren(); body = undefined; }
288
+ function clear() {
289
+ if (destroyed)
290
+ return;
291
+ page?.destroy();
292
+ page = undefined;
293
+ image = undefined;
294
+ note = undefined;
295
+ mode = undefined;
296
+ element.replaceChildren();
297
+ body = undefined;
298
+ }
244
299
  return {
245
300
  element, clear,
246
301
  open(input) {
@@ -255,32 +310,51 @@ export function createFilePreview(options = {}) {
255
310
  update(state) {
256
311
  if (destroyed || !body)
257
312
  return;
258
- page?.destroy();
259
- page = undefined;
260
- body.replaceChildren();
313
+ const nextMode = state.error ? 'error' : state.kind;
314
+ if (mode !== nextMode) {
315
+ page?.destroy();
316
+ page = undefined;
317
+ image = undefined;
318
+ note = undefined;
319
+ body.replaceChildren();
320
+ mode = nextMode;
321
+ }
261
322
  if (state.error)
262
- body.textContent = state.error;
323
+ setText(body, state.error);
263
324
  else if (state.kind === 'image') {
264
- const image = document.createElement('img');
265
- image.className = 'pui-file-preview-image';
266
- image.src = state.imageSrc || '';
267
- image.alt = state.imageAlt || '';
268
- image.setAttribute('role', 'button');
269
- image.tabIndex = 0;
270
- image.title = 'Click to enlarge';
271
- body.append(image);
325
+ if (!image) {
326
+ image = document.createElement('img');
327
+ image.className = 'pui-file-preview-image';
328
+ image.setAttribute('role', 'button');
329
+ image.tabIndex = 0;
330
+ image.title = 'Click to enlarge';
331
+ body.append(image);
332
+ }
333
+ if (image.getAttribute('src') !== (state.imageSrc || ''))
334
+ image.src = state.imageSrc || '';
335
+ if (image.alt !== (state.imageAlt || ''))
336
+ image.alt = state.imageAlt || '';
272
337
  }
338
+ else if (page)
339
+ page.update({ text: state.text || '', kind: state.kind });
273
340
  else
274
- page = createPagedPreview({ container: body, text: state.text || '', kind: state.kind });
341
+ page = createPagedPreview({ container: body, text: state.text || '', kind: state.kind, onCopyCode: options.onCopyCode });
275
342
  if (state.truncated) {
276
- const note = document.createElement('p');
277
- note.className = 'pui-preview-note';
278
- note.textContent = 'Showing the first 8 MiB. Use Open for the complete file.';
279
- body.append(note);
343
+ if (!note) {
344
+ note = document.createElement('p');
345
+ note.className = 'pui-preview-note';
346
+ note.textContent = 'Showing the first 8 MiB. Use Open for the complete file.';
347
+ }
348
+ if (note.parentElement !== body)
349
+ body.append(note);
350
+ }
351
+ else {
352
+ note?.remove();
353
+ note = undefined;
280
354
  }
281
355
  },
282
356
  destroy() { if (destroyed)
283
- return; clear(); element.removeEventListener('click', close); destroyed = true; },
357
+ return; clear(); close.destroy(); destroyed = true; },
284
358
  };
285
359
  }
286
360
  //# sourceMappingURL=panels.js.map