pith-ui 0.1.0 → 0.1.4

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 (124) hide show
  1. package/AI.md +20 -1
  2. package/API.md +834 -272
  3. package/README.md +3 -1
  4. package/dist/composer.d.ts.map +1 -1
  5. package/dist/composer.js +48 -29
  6. package/dist/composer.js.map +1 -1
  7. package/dist/content/conversation-reader.d.ts.map +1 -1
  8. package/dist/content/conversation-reader.js +8 -1
  9. package/dist/content/conversation-reader.js.map +1 -1
  10. package/dist/content/conversation-view.d.ts +21 -4
  11. package/dist/content/conversation-view.d.ts.map +1 -1
  12. package/dist/content/conversation-view.js +72 -13
  13. package/dist/content/conversation-view.js.map +1 -1
  14. package/dist/content/image-viewer.d.ts +4 -2
  15. package/dist/content/image-viewer.d.ts.map +1 -1
  16. package/dist/content/image-viewer.js +36 -15
  17. package/dist/content/image-viewer.js.map +1 -1
  18. package/dist/content/paged-preview.d.ts +1 -1
  19. package/dist/content/paged-preview.d.ts.map +1 -1
  20. package/dist/content/paged-preview.js +10 -5
  21. package/dist/content/paged-preview.js.map +1 -1
  22. package/dist/content/views.d.ts +3 -0
  23. package/dist/content/views.d.ts.map +1 -1
  24. package/dist/content/views.js +110 -41
  25. package/dist/content/views.js.map +1 -1
  26. package/dist/content/welcome.d.ts.map +1 -1
  27. package/dist/content/welcome.js +19 -7
  28. package/dist/content/welcome.js.map +1 -1
  29. package/dist/dom.js +1 -1
  30. package/dist/dom.js.map +1 -1
  31. package/dist/files/panels.d.ts +8 -1
  32. package/dist/files/panels.d.ts.map +1 -1
  33. package/dist/files/panels.js +235 -120
  34. package/dist/files/panels.js.map +1 -1
  35. package/dist/files/systems.d.ts.map +1 -1
  36. package/dist/files/systems.js +81 -91
  37. package/dist/files/systems.js.map +1 -1
  38. package/dist/forms/actions.d.ts +28 -7
  39. package/dist/forms/actions.d.ts.map +1 -1
  40. package/dist/forms/actions.js +59 -11
  41. package/dist/forms/actions.js.map +1 -1
  42. package/dist/forms/custom-connection.d.ts +2 -1
  43. package/dist/forms/custom-connection.d.ts.map +1 -1
  44. package/dist/forms/custom-connection.js +36 -28
  45. package/dist/forms/custom-connection.js.map +1 -1
  46. package/dist/forms/dialog-frame.d.ts +2 -0
  47. package/dist/forms/dialog-frame.d.ts.map +1 -1
  48. package/dist/forms/dialog-frame.js +46 -26
  49. package/dist/forms/dialog-frame.js.map +1 -1
  50. package/dist/forms/history.d.ts +1 -1
  51. package/dist/forms/history.d.ts.map +1 -1
  52. package/dist/forms/history.js +78 -15
  53. package/dist/forms/history.js.map +1 -1
  54. package/dist/forms/identity.d.ts +12 -0
  55. package/dist/forms/identity.d.ts.map +1 -1
  56. package/dist/forms/identity.js +16 -0
  57. package/dist/forms/identity.js.map +1 -1
  58. package/dist/forms/model-editor.d.ts +8 -3
  59. package/dist/forms/model-editor.d.ts.map +1 -1
  60. package/dist/forms/model-editor.js +73 -62
  61. package/dist/forms/model-editor.js.map +1 -1
  62. package/dist/forms/panels.d.ts +103 -7
  63. package/dist/forms/panels.d.ts.map +1 -1
  64. package/dist/forms/panels.js +183 -77
  65. package/dist/forms/panels.js.map +1 -1
  66. package/dist/forms/provider-login.d.ts.map +1 -1
  67. package/dist/forms/provider-login.js +25 -39
  68. package/dist/forms/provider-login.js.map +1 -1
  69. package/dist/forms/resource-editor.d.ts +9 -5
  70. package/dist/forms/resource-editor.d.ts.map +1 -1
  71. package/dist/forms/resource-editor.js +20 -9
  72. package/dist/forms/resource-editor.js.map +1 -1
  73. package/dist/foundations.d.ts +95 -0
  74. package/dist/foundations.d.ts.map +1 -0
  75. package/dist/foundations.js +299 -0
  76. package/dist/foundations.js.map +1 -0
  77. package/dist/navigation/context-menu.d.ts +8 -1
  78. package/dist/navigation/context-menu.d.ts.map +1 -1
  79. package/dist/navigation/context-menu.js +67 -50
  80. package/dist/navigation/context-menu.js.map +1 -1
  81. package/dist/navigation/navigation.d.ts +55 -7
  82. package/dist/navigation/navigation.d.ts.map +1 -1
  83. package/dist/navigation/navigation.js +167 -18
  84. package/dist/navigation/navigation.js.map +1 -1
  85. package/dist/operation.d.ts +12 -0
  86. package/dist/operation.d.ts.map +1 -0
  87. package/dist/operation.js +49 -0
  88. package/dist/operation.js.map +1 -0
  89. package/dist/primitives.d.ts +11 -1
  90. package/dist/primitives.d.ts.map +1 -1
  91. package/dist/primitives.js +164 -301
  92. package/dist/primitives.js.map +1 -1
  93. package/dist/reconcile.d.ts +11 -0
  94. package/dist/reconcile.d.ts.map +1 -0
  95. package/dist/reconcile.js +36 -0
  96. package/dist/reconcile.js.map +1 -0
  97. package/dist/select.d.ts +14 -0
  98. package/dist/select.d.ts.map +1 -0
  99. package/dist/select.js +304 -0
  100. package/dist/select.js.map +1 -0
  101. package/dist/styles.css +452 -382
  102. package/dist/surface.d.ts +60 -0
  103. package/dist/surface.d.ts.map +1 -0
  104. package/dist/surface.js +344 -0
  105. package/dist/surface.js.map +1 -0
  106. package/dist/tasks/approval.d.ts.map +1 -1
  107. package/dist/tasks/approval.js +86 -45
  108. package/dist/tasks/approval.js.map +1 -1
  109. package/dist/tasks/permission-element.d.ts +3 -0
  110. package/dist/tasks/permission-element.d.ts.map +1 -0
  111. package/dist/tasks/permission-element.js +10 -0
  112. package/dist/tasks/permission-element.js.map +1 -0
  113. package/dist/tasks/permission.d.ts +5 -3
  114. package/dist/tasks/permission.d.ts.map +1 -1
  115. package/dist/tasks/permission.js +47 -22
  116. package/dist/tasks/permission.js.map +1 -1
  117. package/dist/tasks/runtime.d.ts.map +1 -1
  118. package/dist/tasks/runtime.js +30 -13
  119. package/dist/tasks/runtime.js.map +1 -1
  120. package/dist/workspace.d.ts +58 -4
  121. package/dist/workspace.d.ts.map +1 -1
  122. package/dist/workspace.js +202 -44
  123. package/dist/workspace.js.map +1 -1
  124. package/package.json +1 -1
@@ -1,8 +1,13 @@
1
1
  import { dialogHeading, escape, icon, uniqueId, setText } from '../dom.js';
2
2
  import { createPagedPreview } from '../content/paged-preview.js';
3
+ import { adoptProductParts } from '../foundations.js';
4
+ import { createButton } from '../primitives.js';
5
+ import { bindContentClose, installAnchoredSurface } from '../surface.js';
6
+ import { reconcileKeyed } from '../reconcile.js';
3
7
  export function fileActionsMarkup(options) {
4
- const attributes = `data-file-kind="${escape(options.kind)}" data-file-owner="${escape(options.ownerId)}" data-file-path="${escape(options.path)}"`;
5
- const preview = options.preview ? `<button type="button" class="pui-secondary-button" data-preview-path="${escape(options.path)}" data-preview-id="${escape(options.ownerId)}">Preview</button>` : '';
8
+ const scope = `data-idle-action ${options.kind === 'resource' ? 'data-idle-workspace' : 'data-idle-conversation'}="${escape(options.ownerId)}"`;
9
+ const attributes = `${scope} data-file-kind="${escape(options.kind)}" data-file-owner="${escape(options.ownerId)}" data-file-path="${escape(options.path)}"`;
10
+ const preview = options.preview ? `<button type="button" class="pui-secondary-button" ${scope} data-preview-path="${escape(options.path)}" data-preview-id="${escape(options.ownerId)}">Preview</button>` : '';
6
11
  const copy = options.copyMode && options.copyMode !== 'none' ? `<button type="button" class="pui-secondary-button" ${attributes} data-file-action="${options.copyMode === 'file' ? 'copy' : 'copy-path'}">${options.copyMode === 'file' ? 'Copy file' : 'Copy path'}</button>` : '';
7
12
  return `<div class="pui-file-actions">${preview}<button type="button" class="pui-secondary-button" ${attributes} data-file-action="open">Open</button><button type="button" class="pui-secondary-button" ${attributes} data-file-action="reveal">Reveal</button>${copy}</div>`;
8
13
  }
@@ -10,14 +15,25 @@ export function createFileActions(options) {
10
15
  const template = document.createElement('template');
11
16
  template.innerHTML = fileActionsMarkup(options);
12
17
  const element = template.content.firstElementChild;
18
+ const parts = adoptProductParts(element);
19
+ let destroyed = false;
13
20
  const click = (event) => {
14
21
  const target = event.target.closest('button');
15
- if (target && !target.disabled)
22
+ if (target && element.contains(target) && !target.disabled) {
23
+ event.stopPropagation();
16
24
  options.onAction?.(target.dataset.fileAction || 'preview');
25
+ }
17
26
  };
18
27
  if (options.onAction)
19
28
  element.addEventListener('click', click);
20
- 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
+ };
21
37
  }
22
38
  export function createArtifactPanel(options) {
23
39
  const { container: element, control, toggle, count } = options;
@@ -26,26 +42,37 @@ export function createArtifactPanel(options) {
26
42
  const loading = element.querySelector('[role="status"]');
27
43
  const rows = element.querySelector('[data-file-rows]');
28
44
  const error = element.querySelector('[role="alert"]');
45
+ const parts = adoptProductParts(element);
29
46
  const entries = new Map();
30
- let open = false;
31
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
+ });
32
59
  function setOpen(value, restoreFocus = false) {
33
60
  if (destroyed)
34
61
  return;
35
- open = value && !control.hidden;
36
- element.hidden = !open;
37
- toggle.setAttribute('aria-expanded', String(open));
38
- if (open)
39
- element.focus();
40
- else if (restoreFocus && !control.hidden)
41
- toggle.focus();
62
+ if (value)
63
+ surface.open();
64
+ else
65
+ surface.close(restoreFocus && !control.hidden);
42
66
  }
67
+ const abort = new AbortController();
68
+ toggle.addEventListener('click', () => setOpen(!surface.isOpen()), { signal: abort.signal });
69
+ element.addEventListener('click', event => { if (event.target.closest('[data-artifacts-close]'))
70
+ setOpen(false, true); }, { signal: abort.signal });
43
71
  return {
44
- element, setOpen, get open() { return open; },
72
+ element, setOpen, get open() { return surface.isOpen(); },
45
73
  update(state) {
46
74
  if (destroyed)
47
75
  return;
48
- const focused = document.activeElement;
49
76
  control.hidden = !state.ownerId || (!state.files.length && !state.error);
50
77
  if (control.hidden)
51
78
  setOpen(false);
@@ -57,149 +84,218 @@ export function createArtifactPanel(options) {
57
84
  total.textContent = number;
58
85
  loading.hidden = !state.loading;
59
86
  error.hidden = !state.error;
60
- error.textContent = state.error;
61
- const keep = new Set(state.files.map(file => `${state.ownerId}/${file.path}`));
62
- for (const [key, entry] of entries)
63
- if (!keep.has(key)) {
64
- entry.element.remove();
65
- entries.delete(key);
66
- }
67
- let cursor = rows.firstElementChild;
68
- for (const file of state.files) {
69
- const key = `${state.ownerId}/${file.path}`;
70
- let entry = entries.get(key);
71
- if (!entry) {
87
+ parts.feedback(error).update({ message: state.error, tone: 'error' });
88
+ reconcileKeyed(rows, entries, state.files, {
89
+ key: file => `${state.ownerId}/${file.path}`,
90
+ element: entry => entry.element,
91
+ create(file) {
72
92
  const article = document.createElement('article');
73
93
  article.className = 'pui-file-card';
74
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 })}`;
75
- entry = { element: article, name: article.querySelector('strong'), copy: article.querySelector('[data-file-action="copy"], [data-file-action="copy-path"]') };
76
- entries.set(key, entry);
77
- }
78
- setText(entry.name, file.name);
79
- const copyAction = state.copyMode === 'file' ? 'copy' : 'copy-path';
80
- if (entry.copy.dataset.fileAction !== copyAction)
81
- entry.copy.dataset.fileAction = copyAction;
82
- if (entry.copy.dataset.copyPending !== 'true')
83
- setText(entry.copy, state.copyMode === 'file' ? 'Copy file' : 'Copy path');
84
- if (entry.element !== cursor)
85
- rows.insertBefore(entry.element, cursor);
86
- cursor = entry.element.nextElementSibling;
87
- }
88
- if (focused instanceof HTMLElement && element.contains(focused) && document.activeElement !== focused)
89
- 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();
90
108
  },
91
- destroy() { destroyed = true; entries.clear(); element.replaceChildren(); },
109
+ destroy() { destroyed = true; surface.destroy(); abort.abort(); parts.destroy(); entries.clear(); element.replaceChildren(); },
92
110
  };
93
111
  }
94
112
  export function createResourcesPanel(options = {}) {
95
113
  const element = options.container || document.createElement('div');
96
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') };
97
- const close = (event) => {
98
- if (!event.target.closest('[data-close]'))
99
- return;
100
- if (options.onClose)
101
- options.onClose();
102
- else {
103
- const dialog = element.closest('dialog');
104
- if (dialog?.open)
105
- dialog.close();
106
- else
107
- element.hidden = true;
108
- }
109
- };
110
- element.addEventListener('click', close);
115
+ const close = bindContentClose(element, { dialogId: ids.dialogId, onClose: options.onClose });
111
116
  let list, error;
112
117
  let destroyed = false;
113
- function row(entry, type) {
118
+ const parts = adoptProductParts(element);
119
+ const buttons = [];
120
+ const rowButtons = [];
121
+ const sections = new Map();
122
+ let diagnostics;
123
+ let instructionButton;
124
+ const createActions = new Map();
125
+ let createDisabled = false;
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)
127
+ createActions.set(block, label); return block.element; };
128
+ const clearButtons = (list = buttons) => { for (const block of list.splice(0)) {
129
+ createActions.delete(block);
130
+ block.destroy();
131
+ } };
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) {
114
142
  const outer = document.createElement('article');
115
143
  outer.className = type === 'instructions' ? 'pui-resource-card' : 'pui-file-card';
116
144
  const body = type === 'instructions' ? document.createElement('div') : outer;
117
145
  if (type === 'instructions')
118
146
  body.className = 'pui-file-card';
119
- 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>`;
120
- if (entry.fileActions)
121
- body.append(entry.fileActions);
122
- if (entry.actions)
123
- 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;
124
149
  if (type === 'instructions') {
125
150
  outer.append(body);
126
151
  const details = document.createElement('details');
127
152
  details.className = 'pui-resource-preview';
128
- details.innerHTML = `<summary>View instructions</summary><pre>${escape(entry.content || '')}</pre>`;
153
+ details.innerHTML = '<summary>View instructions</summary><pre></pre>';
129
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;
130
173
  }
131
- 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: [] } };
132
179
  }
133
180
  return {
134
181
  element,
135
182
  open(workspaceName, toolbar) {
136
183
  if (destroyed)
137
184
  return;
185
+ clearButtons();
186
+ clearButtons(rowButtons);
187
+ instructionButton = undefined;
188
+ sections.clear();
189
+ diagnostics = undefined;
138
190
  element.hidden = false;
139
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>`;
140
- element.querySelector('[data-resources-toolbar]').append(toolbar);
192
+ const actions = element.querySelector('[data-resources-toolbar]');
193
+ if (toolbar)
194
+ actions.append(toolbar);
195
+ else
196
+ actions.append(button('Refresh', () => options.onRefresh?.()), button('Add skill', () => options.onCreate?.('skill'), false, true), button('Add prompt template', () => options.onCreate?.('template'), false, true));
141
197
  list = element.querySelector('[data-resources-list]');
142
198
  error = element.querySelector('[data-resources-error]');
199
+ parts.layout(actions, 'toolbar').update({});
143
200
  },
144
201
  setError(message) { if (!destroyed && error)
145
- error.textContent = message; },
202
+ parts.feedback(error).update({ message, tone: 'error' }); },
203
+ setCreateDisabled(disabled) { if (destroyed)
204
+ return; createDisabled = disabled; for (const [block, label] of createActions)
205
+ block.update({ label, appearance: 'secondary', disabled }); },
146
206
  update(state) {
147
207
  if (destroyed || !list)
148
208
  return;
149
- list.replaceChildren();
209
+ if (!sections.size)
210
+ list.replaceChildren();
150
211
  for (const [title, entries, type, empty] of [
151
212
  ['Instructions', state.instructions, 'instructions', 'No workspace instructions found.'],
152
213
  ['Skills', state.skills, 'skill', 'No skills found. Add .pi/skills/<skill-name>/SKILL.md in this workspace, then refresh.'],
153
214
  ['Prompt templates', state.templates, 'template', 'No prompt templates yet.'],
154
215
  ]) {
155
- const section = document.createElement('section');
156
- section.className = 'pui-resource-section';
157
- const heading = document.createElement('h3');
158
- heading.textContent = title;
159
- section.append(heading);
160
- if (entries.length)
161
- entries.forEach((entry) => section.append(row(entry, type)));
162
- else {
163
- const hint = document.createElement('p');
164
- hint.className = 'pui-feature-hint';
165
- hint.textContent = empty;
166
- 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);
167
254
  }
168
- if (type === 'instructions' && state.createInstructions)
169
- section.append(state.createInstructions);
170
- list.append(section);
171
255
  }
172
256
  if (state.diagnostics.length) {
173
- const section = document.createElement('section');
174
- section.className = 'pui-resource-section pui-resource-diagnostics';
175
- section.innerHTML = `<h3>Resource notes</h3>${state.diagnostics.map((note) => `<p>${escape(note)}</p>`).join('')}`;
176
- 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
+ });
267
+ }
268
+ else if (diagnostics) {
269
+ diagnostics.element.remove();
270
+ diagnostics = undefined;
177
271
  }
272
+ parts.update();
178
273
  },
179
- destroy() { destroyed = true; 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(); },
180
276
  };
181
277
  }
182
278
  export function createFilePreview(options = {}) {
183
279
  const element = options.container || document.createElement('div');
184
280
  const ids = { titleId: options.titleId || uniqueId('pui-preview-title'), dialogId: options.dialogId || uniqueId('pui-preview-dialog'), bodyId: options.bodyId || uniqueId('pui-preview-body') };
185
- const close = (event) => {
186
- if (!event.target.closest('[data-close]'))
187
- return;
188
- if (options.onClose)
189
- options.onClose();
190
- else {
191
- const dialog = element.closest('dialog');
192
- if (dialog?.open)
193
- dialog.close();
194
- else
195
- element.hidden = true;
196
- }
197
- };
198
- element.addEventListener('click', close);
281
+ const close = bindContentClose(element, { dialogId: ids.dialogId, onClose: options.onClose });
199
282
  let body;
200
283
  let page;
284
+ let image;
285
+ let note;
286
+ let mode;
201
287
  let destroyed = false;
202
- 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
+ }
203
299
  return {
204
300
  element, clear,
205
301
  open(input) {
@@ -214,32 +310,51 @@ export function createFilePreview(options = {}) {
214
310
  update(state) {
215
311
  if (destroyed || !body)
216
312
  return;
217
- page?.destroy();
218
- page = undefined;
219
- 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
+ }
220
322
  if (state.error)
221
- body.textContent = state.error;
323
+ setText(body, state.error);
222
324
  else if (state.kind === 'image') {
223
- const image = document.createElement('img');
224
- image.className = 'pui-file-preview-image';
225
- image.src = state.imageSrc || '';
226
- image.alt = state.imageAlt || '';
227
- image.setAttribute('role', 'button');
228
- image.tabIndex = 0;
229
- image.title = 'Click to enlarge';
230
- 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 || '';
231
337
  }
338
+ else if (page)
339
+ page.update({ text: state.text || '', kind: state.kind });
232
340
  else
233
- page = createPagedPreview({ container: body, text: state.text || '', kind: state.kind });
341
+ page = createPagedPreview({ container: body, text: state.text || '', kind: state.kind, onCopyCode: options.onCopyCode });
234
342
  if (state.truncated) {
235
- const note = document.createElement('p');
236
- note.className = 'pui-preview-note';
237
- note.textContent = 'Showing the first 8 MiB. Use Open for the complete file.';
238
- 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;
239
354
  }
240
355
  },
241
356
  destroy() { if (destroyed)
242
- return; clear(); element.removeEventListener('click', close); destroyed = true; },
357
+ return; clear(); close.destroy(); destroyed = true; },
243
358
  };
244
359
  }
245
360
  //# sourceMappingURL=panels.js.map