sqlite-hub 2.1.0 → 2.2.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.
- package/README.md +7 -0
- package/docs/MCP.md +2 -2
- package/docs/changelog.md +6 -0
- package/docs/todo.md +2 -0
- package/frontend/js/api.js +7 -0
- package/frontend/js/app.js +329 -7
- package/frontend/js/components/modal.js +152 -2
- package/frontend/js/store.js +1332 -219
- package/frontend/js/utils/markdownDocuments.js +21 -4
- package/frontend/js/views/connections.js +3 -8
- package/frontend/js/views/data.js +34 -5
- package/frontend/js/views/documents.js +153 -59
- package/frontend/styles/base.css +9 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +12 -0
- package/package.json +1 -1
- package/server/routes/backups.js +37 -1
- package/server/routes/charts.js +28 -0
- package/server/routes/data.js +29 -1
- package/server/routes/documents.js +61 -5
- package/server/routes/settings.js +32 -0
- package/server/routes/tableDesigner.js +20 -1
- package/server/server.js +3 -3
- package/server/services/mcpToolService.js +4 -2
- package/server/services/sqlite/connectionManager.js +2 -0
- package/server/services/sqlite/overviewService.js +1 -0
- package/server/services/storage/appStateStore.js +287 -6
- package/server/utils/fileValidation.js +5 -0
- package/server/utils/userActionLog.js +49 -0
|
@@ -2,6 +2,8 @@ import { escapeHtml } from './format.js';
|
|
|
2
2
|
|
|
3
3
|
const FENCED_CODE_PATTERN = /^\s*(```|~~~)/;
|
|
4
4
|
const TASK_LINE_PATTERN = /^(\s*)([-*+])\s+\[([ xX])\]\s+(.*?)(\r?)$/;
|
|
5
|
+
const SQLITE_HUB_MAGIC_COMMENT_PATTERN =
|
|
6
|
+
/^\s*<!--\s*\/?sqlite-hub:magic(?:\s+[^>]*)?-->\s*$/i;
|
|
5
7
|
|
|
6
8
|
function decodeBasicHtmlEntities(value = '') {
|
|
7
9
|
return String(value)
|
|
@@ -26,6 +28,15 @@ function sanitizeRenderedMarkdown(html = '') {
|
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
function openRenderedMarkdownLinksInNewTabs(html = '') {
|
|
32
|
+
return String(html).replace(/<a\b([^>]*)>/gi, (match, attributes = '') => {
|
|
33
|
+
const targetAttribute = /\starget\s*=/i.test(attributes) ? '' : ' target="_blank"';
|
|
34
|
+
const relAttribute = /\srel\s*=/i.test(attributes) ? '' : ' rel="noopener noreferrer"';
|
|
35
|
+
|
|
36
|
+
return `<a${attributes}${targetAttribute}${relAttribute}>`;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
function renderFallbackMarkdown(markdown = '') {
|
|
30
41
|
const lines = String(markdown).split(/\r?\n/);
|
|
31
42
|
const html = [];
|
|
@@ -108,13 +119,13 @@ function renderMarkdownBlock(markdown = '') {
|
|
|
108
119
|
return renderFallbackMarkdown(markdown);
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
return sanitizeRenderedMarkdown(
|
|
122
|
+
return openRenderedMarkdownLinksInNewTabs(sanitizeRenderedMarkdown(
|
|
112
123
|
parser(source, {
|
|
113
124
|
async: false,
|
|
114
125
|
breaks: false,
|
|
115
126
|
gfm: true,
|
|
116
127
|
}),
|
|
117
|
-
);
|
|
128
|
+
));
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
function renderMarkdownInline(markdown = '') {
|
|
@@ -127,13 +138,13 @@ function renderMarkdownInline(markdown = '') {
|
|
|
127
138
|
return source;
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
return sanitizeRenderedMarkdown(
|
|
141
|
+
return openRenderedMarkdownLinksInNewTabs(sanitizeRenderedMarkdown(
|
|
131
142
|
parser(source, {
|
|
132
143
|
async: false,
|
|
133
144
|
breaks: false,
|
|
134
145
|
gfm: true,
|
|
135
146
|
}),
|
|
136
|
-
);
|
|
147
|
+
));
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
function renderTaskList(items = []) {
|
|
@@ -193,6 +204,12 @@ export function renderMarkdownPreview(markdown = '') {
|
|
|
193
204
|
};
|
|
194
205
|
|
|
195
206
|
lines.forEach((line, lineIndex) => {
|
|
207
|
+
if (!inFence && SQLITE_HUB_MAGIC_COMMENT_PATTERN.test(line)) {
|
|
208
|
+
flushTasks();
|
|
209
|
+
flushMarkdown();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
const fenceLine = FENCED_CODE_PATTERN.test(line);
|
|
197
214
|
|
|
198
215
|
if (fenceLine) {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { renderConnectionCard } from '../components/connectionCard.js';
|
|
2
2
|
import { renderPageHeader } from '../components/pageHeader.js';
|
|
3
3
|
import { escapeHtml } from '../utils/format.js';
|
|
4
|
-
import {
|
|
5
|
-
filterConnections,
|
|
6
|
-
getConnectionTagCounts,
|
|
7
|
-
} from '../utils/connectionRegistry.js';
|
|
4
|
+
import { filterConnections, getConnectionTagCounts } from '../utils/connectionRegistry.js';
|
|
8
5
|
|
|
9
6
|
function renderConnectionsActionButton({
|
|
10
7
|
label,
|
|
@@ -175,9 +172,7 @@ function renderConnectionsBody(state, visibleConnections) {
|
|
|
175
172
|
|
|
176
173
|
return `
|
|
177
174
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
178
|
-
${visibleConnections
|
|
179
|
-
.map(connection => renderConnectionCard(connection, state.connections.active?.id))
|
|
180
|
-
.join('')}
|
|
175
|
+
${visibleConnections.map(connection => renderConnectionCard(connection, state.connections.active?.id)).join('')}
|
|
181
176
|
</div>
|
|
182
177
|
`;
|
|
183
178
|
}
|
|
@@ -191,7 +186,7 @@ export function renderConnectionsView(state) {
|
|
|
191
186
|
${
|
|
192
187
|
state.connections.active
|
|
193
188
|
? renderConnectionsActionButton({
|
|
194
|
-
label: state.connections.backupLoading ? '
|
|
189
|
+
label: state.connections.backupLoading ? 'Create Backup...' : 'CreateBackup',
|
|
195
190
|
icon: 'inventory_2',
|
|
196
191
|
action: 'create-backup',
|
|
197
192
|
disabled: state.connections.backupLoading,
|
|
@@ -32,6 +32,33 @@ function getFilteredTables(tables, searchQuery) {
|
|
|
32
32
|
return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function buildDataSidebarSignature(state) {
|
|
36
|
+
if (state?.route?.name !== 'data') {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const dataBrowser = state.dataBrowser ?? {};
|
|
41
|
+
const tablesVisible = dataBrowser.tablesVisible !== false;
|
|
42
|
+
|
|
43
|
+
if (!tablesVisible) {
|
|
44
|
+
return 'data-sidebar:hidden';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tables = dataBrowser.tables ?? [];
|
|
48
|
+
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
loadingEmpty: Boolean(dataBrowser.loading && !tables.length),
|
|
51
|
+
tableSearchQuery: dataBrowser.tableSearchQuery ?? '',
|
|
52
|
+
tables: tables.map(table => [
|
|
53
|
+
table.name,
|
|
54
|
+
table.columnCount ?? 0,
|
|
55
|
+
Boolean(table.isVirtual),
|
|
56
|
+
Boolean(table.isShadow),
|
|
57
|
+
table.tableKind ?? '',
|
|
58
|
+
]),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
function renderDataTableSearch(state) {
|
|
36
63
|
return `
|
|
37
64
|
<label class="table-designer-sidebar__search">
|
|
@@ -92,13 +119,15 @@ function renderTableList(state) {
|
|
|
92
119
|
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
93
120
|
}"
|
|
94
121
|
data-action="navigate"
|
|
122
|
+
data-data-table-item
|
|
123
|
+
data-data-table-name="${escapeHtml(table.name)}"
|
|
95
124
|
data-to="/data/${encodeURIComponent(table.name)}"
|
|
96
125
|
type="button"
|
|
97
126
|
>
|
|
98
127
|
<div class="flex min-w-0 items-center gap-2">
|
|
99
128
|
<div class="table-designer-sidebar__item-name min-w-0 flex-1 ${
|
|
100
129
|
table.name === activeName ? 'is-active' : ''
|
|
101
|
-
}">
|
|
130
|
+
}" data-data-table-name-label>
|
|
102
131
|
${escapeHtml(table.name)}
|
|
103
132
|
</div>
|
|
104
133
|
${renderVirtualTableBadge(table)}
|
|
@@ -688,12 +717,12 @@ export function renderDataView(state) {
|
|
|
688
717
|
|
|
689
718
|
return {
|
|
690
719
|
main: `
|
|
691
|
-
<section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
|
|
692
|
-
<div class="data-view-grid ${tablesVisible ? 'data-view-grid--with-subnavi' : ''}">
|
|
720
|
+
<section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden" data-data-view>
|
|
721
|
+
<div class="data-view-grid ${tablesVisible ? 'data-view-grid--with-subnavi' : ''}" data-data-view-grid>
|
|
693
722
|
${
|
|
694
723
|
tablesVisible
|
|
695
724
|
? `
|
|
696
|
-
<aside class="data-view__sidebar subnavi-panel">
|
|
725
|
+
<aside class="data-view__sidebar subnavi-panel" data-data-sidebar>
|
|
697
726
|
<div class="subnavi-header">
|
|
698
727
|
<div>
|
|
699
728
|
<div class="subnavi-header-title">Tables</div>
|
|
@@ -708,7 +737,7 @@ export function renderDataView(state) {
|
|
|
708
737
|
`
|
|
709
738
|
: ''
|
|
710
739
|
}
|
|
711
|
-
<section class="flex min-h-0 flex-col overflow-hidden">
|
|
740
|
+
<section class="flex min-h-0 flex-col overflow-hidden" data-data-workspace>
|
|
712
741
|
${renderWorkspaceHeader(state)}
|
|
713
742
|
${renderWorkspaceError(state)}
|
|
714
743
|
${renderTableSurface(state)}
|
|
@@ -35,7 +35,7 @@ function renderDocumentListItem(item, selectedId) {
|
|
|
35
35
|
<span class="documents-list-item__title" title="${escapeHtml(item.filename)}">
|
|
36
36
|
${escapeHtml(item.filename)}
|
|
37
37
|
</span>
|
|
38
|
-
<span class="documents-list-item__meta">
|
|
38
|
+
<span class="documents-list-item__meta" data-document-list-meta>
|
|
39
39
|
${escapeHtml(formatCompactDateTime(item.updatedAt))} // ${formatNumber(item.contentLength ?? 0)} chars
|
|
40
40
|
</span>
|
|
41
41
|
</span>
|
|
@@ -77,7 +77,45 @@ function renderDocumentsSearch(documents) {
|
|
|
77
77
|
|
|
78
78
|
function renderDocumentsSidebar(documents) {
|
|
79
79
|
const items = documents.items ?? [];
|
|
80
|
+
const folders = documents.folders ?? [];
|
|
80
81
|
const filteredItems = getFilteredDocuments(items, documents.searchQuery);
|
|
82
|
+
const searchActive = Boolean(String(documents.searchQuery ?? '').trim());
|
|
83
|
+
const rootItems = filteredItems.filter(item => !item.folderId);
|
|
84
|
+
const renderFolderGroup = (label, groupItems, options = {}) => {
|
|
85
|
+
if (searchActive && !groupItems.length) {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `
|
|
90
|
+
<div class="documents-folder-group">
|
|
91
|
+
<div class="px-3 pb-1 pt-3 text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/55">
|
|
92
|
+
${escapeHtml(label)}
|
|
93
|
+
</div>
|
|
94
|
+
${
|
|
95
|
+
groupItems.length
|
|
96
|
+
? groupItems.map(item => renderDocumentListItem(item, documents.selectedId)).join('')
|
|
97
|
+
: `<div class="documents-list-empty">${escapeHtml(options.emptyText ?? 'No documents')}</div>`
|
|
98
|
+
}
|
|
99
|
+
</div>
|
|
100
|
+
`;
|
|
101
|
+
};
|
|
102
|
+
const folderGroups = folders
|
|
103
|
+
.map(folder =>
|
|
104
|
+
renderFolderGroup(
|
|
105
|
+
folder.name,
|
|
106
|
+
filteredItems.filter(item => String(item.folderId ?? '') === String(folder.id)),
|
|
107
|
+
{ emptyText: 'Empty folder' },
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
.join('');
|
|
111
|
+
const rootGroup = renderFolderGroup('No Folder', rootItems, {
|
|
112
|
+
emptyText: folders.length ? 'No root documents' : 'No documents yet',
|
|
113
|
+
});
|
|
114
|
+
const listMarkup = rootGroup || folderGroups
|
|
115
|
+
? `${rootGroup}${folderGroups}`
|
|
116
|
+
: `<div class="documents-list-empty">${
|
|
117
|
+
items.length ? 'No documents match the current search.' : 'No documents yet'
|
|
118
|
+
}</div>`;
|
|
81
119
|
|
|
82
120
|
return `
|
|
83
121
|
<aside class="documents-view__sidebar subnavi-panel">
|
|
@@ -89,13 +127,7 @@ function renderDocumentsSidebar(documents) {
|
|
|
89
127
|
</div>
|
|
90
128
|
${renderDocumentsSearch(documents)}
|
|
91
129
|
<div class="documents-view__sidebar-body subnavi-list custom-scrollbar">
|
|
92
|
-
${
|
|
93
|
-
filteredItems.length
|
|
94
|
-
? filteredItems.map(item => renderDocumentListItem(item, documents.selectedId)).join('')
|
|
95
|
-
: `<div class="documents-list-empty">${
|
|
96
|
-
items.length ? 'No documents match the current search.' : 'No documents yet'
|
|
97
|
-
}</div>`
|
|
98
|
-
}
|
|
130
|
+
${listMarkup}
|
|
99
131
|
</div>
|
|
100
132
|
</aside>
|
|
101
133
|
`;
|
|
@@ -118,18 +150,18 @@ function renderNewDocumentDropdown(documents, className = '') {
|
|
|
118
150
|
${renderDropdownButton({
|
|
119
151
|
disabled: documents.saving,
|
|
120
152
|
icon: 'add',
|
|
121
|
-
label: 'New
|
|
122
|
-
title: 'New
|
|
153
|
+
label: 'New',
|
|
154
|
+
title: 'New',
|
|
123
155
|
items: [
|
|
124
156
|
{
|
|
125
157
|
action: 'create-document',
|
|
126
158
|
icon: 'draft',
|
|
127
|
-
label: '
|
|
159
|
+
label: 'New Document',
|
|
128
160
|
},
|
|
129
161
|
{
|
|
130
|
-
action: '
|
|
131
|
-
icon: '
|
|
132
|
-
label: '
|
|
162
|
+
action: 'open-create-document-folder-modal',
|
|
163
|
+
icon: 'create_new_folder',
|
|
164
|
+
label: 'New Folder',
|
|
133
165
|
},
|
|
134
166
|
],
|
|
135
167
|
})}
|
|
@@ -137,40 +169,86 @@ function renderNewDocumentDropdown(documents, className = '') {
|
|
|
137
169
|
`;
|
|
138
170
|
}
|
|
139
171
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
172
|
+
function renderDocumentsViewDropdown(documents) {
|
|
173
|
+
const documentsVisible = documents.documentsVisible !== false;
|
|
174
|
+
const editorVisible = documents.editorVisible !== false;
|
|
175
|
+
const previewVisible = documents.previewVisible !== false;
|
|
142
176
|
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
return renderDropdownButton({
|
|
178
|
+
icon: 'visibility',
|
|
179
|
+
label: 'View',
|
|
180
|
+
title: 'View options',
|
|
181
|
+
items: [
|
|
182
|
+
{
|
|
183
|
+
action: 'toggle-document-pane',
|
|
184
|
+
dataAttributes: { pane: 'editor' },
|
|
185
|
+
icon: editorVisible ? 'visibility_off' : 'edit_note',
|
|
186
|
+
label: `${editorVisible ? 'Hide' : 'Show'} Editor`,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
action: 'toggle-document-pane',
|
|
190
|
+
dataAttributes: { pane: 'preview' },
|
|
191
|
+
icon: previewVisible ? 'visibility_off' : 'visibility',
|
|
192
|
+
label: `${previewVisible ? 'Hide' : 'Show'} Preview`,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
action: 'toggle-documents-panel',
|
|
196
|
+
icon: documentsVisible ? 'visibility_off' : 'description',
|
|
197
|
+
label: `${documentsVisible ? 'Hide' : 'Show'} Documents`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
154
201
|
}
|
|
155
202
|
|
|
156
|
-
function
|
|
157
|
-
const
|
|
158
|
-
const visible = isEditor ? documents.editorVisible : documents.previewVisible;
|
|
159
|
-
const label = `${visible ? 'Hide' : 'Show'} ${isEditor ? 'Editor' : 'Preview'}`;
|
|
160
|
-
const icon = visible ? 'visibility_off' : isEditor ? 'edit_note' : 'visibility';
|
|
203
|
+
function renderDocumentsFileDropdown(documents) {
|
|
204
|
+
const exportDisabled = documents.saving || documents.detailLoading || !documents.selectedId;
|
|
161
205
|
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
206
|
+
return renderDropdownButton({
|
|
207
|
+
disabled: documents.saving,
|
|
208
|
+
icon: 'folder_open',
|
|
209
|
+
label: 'File',
|
|
210
|
+
title: 'File actions',
|
|
211
|
+
items: [
|
|
212
|
+
{
|
|
213
|
+
action: 'import-document-markdown',
|
|
214
|
+
icon: 'upload_file',
|
|
215
|
+
label: 'Import MD',
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
action: 'export-document-markdown',
|
|
219
|
+
disabled: exportDisabled,
|
|
220
|
+
icon: 'download',
|
|
221
|
+
label: 'Export MD',
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renderDocumentsMoveDropdown(documents) {
|
|
228
|
+
const folders = documents.folders ?? [];
|
|
229
|
+
const selectedFolderId = String(documents.selected?.folderId ?? '');
|
|
230
|
+
|
|
231
|
+
return renderDropdownButton({
|
|
232
|
+
disabled: documents.saving || documents.detailLoading || !documents.selectedId,
|
|
233
|
+
icon: 'drive_file_move',
|
|
234
|
+
label: 'Move to',
|
|
235
|
+
title: 'Move document to folder',
|
|
236
|
+
items: [
|
|
237
|
+
{
|
|
238
|
+
action: 'move-document-to-folder',
|
|
239
|
+
disabled: !selectedFolderId,
|
|
240
|
+
icon: 'folder_off',
|
|
241
|
+
label: 'No Folder',
|
|
242
|
+
},
|
|
243
|
+
...folders.map(folder => ({
|
|
244
|
+
action: 'move-document-to-folder',
|
|
245
|
+
dataAttributes: { folderId: folder.id },
|
|
246
|
+
disabled: selectedFolderId === String(folder.id),
|
|
247
|
+
icon: 'folder',
|
|
248
|
+
label: folder.name,
|
|
249
|
+
})),
|
|
250
|
+
],
|
|
251
|
+
});
|
|
174
252
|
}
|
|
175
253
|
|
|
176
254
|
function renderDocumentsTitlebar(documents, options = {}) {
|
|
@@ -179,7 +257,6 @@ function renderDocumentsTitlebar(documents, options = {}) {
|
|
|
179
257
|
|
|
180
258
|
return `
|
|
181
259
|
<div class="documents-titlebar">
|
|
182
|
-
${renderDocumentsPanelToggle(documents)}
|
|
183
260
|
${
|
|
184
261
|
showFilename
|
|
185
262
|
? `
|
|
@@ -216,19 +293,33 @@ function renderDocumentsTitlebar(documents, options = {}) {
|
|
|
216
293
|
{
|
|
217
294
|
action: 'open-document-insert-note-modal',
|
|
218
295
|
icon: 'note_add',
|
|
219
|
-
label: 'Insert Note',
|
|
296
|
+
label: 'Insert Query Note',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
action: 'open-document-insert-table-definition-modal',
|
|
300
|
+
icon: 'schema',
|
|
301
|
+
label: 'Insert Table Definition',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
action: 'insert-document-saved-queries',
|
|
305
|
+
icon: 'bookmark',
|
|
306
|
+
label: 'Insert Saved Queries',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
action: 'insert-document-time-metadata',
|
|
310
|
+
icon: 'schedule',
|
|
311
|
+
label: 'Insert Time Metadata',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
action: 'insert-document-database-info',
|
|
315
|
+
icon: 'database',
|
|
316
|
+
label: 'Insert Database Info',
|
|
220
317
|
},
|
|
221
318
|
],
|
|
222
319
|
})}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
type="button"
|
|
227
|
-
${disabled ? 'disabled aria-disabled="true"' : ''}
|
|
228
|
-
>
|
|
229
|
-
<span class="material-symbols-outlined">download</span>
|
|
230
|
-
Export .md
|
|
231
|
-
</button>
|
|
320
|
+
${renderDocumentsViewDropdown(documents)}
|
|
321
|
+
${renderDocumentsFileDropdown(documents)}
|
|
322
|
+
${renderDocumentsMoveDropdown(documents)}
|
|
232
323
|
<button
|
|
233
324
|
class="delete-button"
|
|
234
325
|
data-action="delete-document"
|
|
@@ -239,7 +330,10 @@ function renderDocumentsTitlebar(documents, options = {}) {
|
|
|
239
330
|
Delete
|
|
240
331
|
</button>
|
|
241
332
|
`
|
|
242
|
-
:
|
|
333
|
+
: `
|
|
334
|
+
${renderDocumentsViewDropdown(documents)}
|
|
335
|
+
${renderDocumentsFileDropdown(documents)}
|
|
336
|
+
`
|
|
243
337
|
}
|
|
244
338
|
</div>
|
|
245
339
|
`;
|
|
@@ -249,7 +343,6 @@ function renderDocumentEditor(documents) {
|
|
|
249
343
|
return `
|
|
250
344
|
<section class="documents-pane documents-pane--editor ${documents.editorVisible ? '' : 'documents-pane--collapsed'}">
|
|
251
345
|
<div class="documents-pane__header">
|
|
252
|
-
${renderDocumentPaneToggle(documents, 'editor')}
|
|
253
346
|
<span class="documents-pane__meta">${formatNumber(documents.draftContent.length)} chars</span>
|
|
254
347
|
</div>
|
|
255
348
|
${
|
|
@@ -271,11 +364,12 @@ function renderDocumentEditor(documents) {
|
|
|
271
364
|
}
|
|
272
365
|
|
|
273
366
|
function renderDocumentPreview(documents) {
|
|
367
|
+
const saveState = documents.dirty ? 'unsaved' : 'saved';
|
|
368
|
+
|
|
274
369
|
return `
|
|
275
370
|
<section class="documents-pane documents-pane--preview ${documents.previewVisible ? '' : 'documents-pane--collapsed'}">
|
|
276
371
|
<div class="documents-pane__header">
|
|
277
|
-
${
|
|
278
|
-
<span class="documents-pane__meta">${escapeHtml(documents.dirty ? 'unsaved' : 'saved')}</span>
|
|
372
|
+
<span class="documents-pane__meta" data-document-save-state="${saveState}">${escapeHtml(saveState)}</span>
|
|
279
373
|
</div>
|
|
280
374
|
${
|
|
281
375
|
documents.previewVisible
|
package/frontend/styles/base.css
CHANGED
|
@@ -70,6 +70,15 @@ textarea:disabled {
|
|
|
70
70
|
padding: 0 var(--control-padding-inline);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
.control-input:focus,
|
|
74
|
+
.control-input:focus-visible,
|
|
75
|
+
.control-select:focus,
|
|
76
|
+
.control-select:focus-visible {
|
|
77
|
+
border-color: var(--color-primary-container);
|
|
78
|
+
box-shadow: none;
|
|
79
|
+
outline: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
input.control-input--ghost {
|
|
74
83
|
appearance: none;
|
|
75
84
|
background: transparent;
|