sqlite-hub 0.11.1 → 0.16.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 (52) hide show
  1. package/README.md +52 -13
  2. package/database.sqlite +0 -0
  3. package/frontend/js/api.js +64 -12
  4. package/frontend/js/app.js +723 -37
  5. package/frontend/js/components/emptyState.js +42 -46
  6. package/frontend/js/components/modal.js +526 -2
  7. package/frontend/js/components/queryEditor.js +12 -3
  8. package/frontend/js/components/queryResults.js +79 -17
  9. package/frontend/js/components/rowEditorPanel.js +345 -12
  10. package/frontend/js/components/sidebar.js +106 -23
  11. package/frontend/js/components/tableDesignerEditor.js +69 -11
  12. package/frontend/js/store.js +437 -26
  13. package/frontend/js/utils/copyColumnExport.js +117 -0
  14. package/frontend/js/utils/exportFilenames.js +32 -0
  15. package/frontend/js/utils/filePathPreview.js +315 -0
  16. package/frontend/js/utils/format.js +1 -1
  17. package/frontend/js/utils/rowEditorJson.js +65 -0
  18. package/frontend/js/utils/sqlFormatter.js +691 -0
  19. package/frontend/js/utils/tableDesigner.js +178 -6
  20. package/frontend/js/utils/textCellStats.js +20 -0
  21. package/frontend/js/utils/timestampPreview.js +264 -0
  22. package/frontend/js/views/charts.js +3 -1
  23. package/frontend/js/views/data.js +39 -4
  24. package/frontend/js/views/editor.js +50 -1
  25. package/frontend/js/views/settings.js +1 -4
  26. package/frontend/js/views/structure.js +154 -212
  27. package/frontend/styles/base.css +6 -0
  28. package/frontend/styles/components.css +463 -2
  29. package/frontend/styles/structure-graph.css +0 -3
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/tokens.css +95 -95
  32. package/package.json +2 -3
  33. package/server/routes/export.js +97 -12
  34. package/server/services/sqlite/dataBrowserService.js +2 -68
  35. package/server/services/sqlite/exportService.js +74 -15
  36. package/server/services/sqlite/introspection.js +209 -1
  37. package/server/services/sqlite/sqlExecutor.js +31 -0
  38. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  39. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  40. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  41. package/server/services/sqlite/tableDesignerService.js +1 -1
  42. package/server/services/sqlite/tableFilter.js +75 -0
  43. package/server/utils/csv.js +30 -4
  44. package/tests/check-constraint-options.test.js +90 -0
  45. package/tests/export-filenames.test.js +34 -0
  46. package/tests/file-path-preview.test.js +165 -0
  47. package/tests/row-editor-json.test.js +82 -0
  48. package/tests/row-editor-timestamp-preview.test.js +192 -0
  49. package/tests/sql-formatter.test.js +173 -0
  50. package/tests/sql-highlight.test.js +38 -0
  51. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  52. package/tests/text-cell-stats.test.js +38 -0
@@ -9,31 +9,93 @@ function getSortIcon(columnName, sortColumn, sortDirection) {
9
9
  return sortDirection === "desc" ? "south" : "north";
10
10
  }
11
11
 
12
- function renderSortableHeader(columnName, sortColumn, sortDirection) {
12
+ const COPY_COLUMN_ACTIONS = [
13
+ { mode: "column", label: "Copy column" },
14
+ { mode: "column-with-header", label: "Copy column with header" },
15
+ { mode: "first-10", label: "Copy first 10" },
16
+ { mode: "markdown-todo", label: "Export as Markdown Todo" },
17
+ ];
18
+
19
+ function renderColumnActionMenu(columnName, resultScope) {
20
+ return `
21
+ <details class="query-result-column-menu" data-copy-column-menu>
22
+ <summary
23
+ aria-label="Column actions for ${escapeHtml(columnName)}"
24
+ class="query-result-column-menu__toggle"
25
+ title="Column actions"
26
+ >
27
+ <span class="material-symbols-outlined" aria-hidden="true">more_vert</span>
28
+ </summary>
29
+ <div class="query-result-column-menu__panel" role="menu">
30
+ ${COPY_COLUMN_ACTIONS.map(
31
+ (item) => `
32
+ <button
33
+ class="query-result-column-menu__item"
34
+ data-action="open-copy-column-modal"
35
+ data-column-name="${escapeHtml(columnName)}"
36
+ data-copy-mode="${escapeHtml(item.mode)}"
37
+ data-result-scope="${escapeHtml(resultScope)}"
38
+ role="menuitem"
39
+ type="button"
40
+ >
41
+ ${escapeHtml(item.label)}
42
+ </button>
43
+ `
44
+ ).join("")}
45
+ </div>
46
+ </details>
47
+ `;
48
+ }
49
+
50
+ function renderSortableHeader(columnName, sortColumn, sortDirection, { resultScope, sortAction }) {
13
51
  const isActive = columnName === sortColumn;
52
+ const labelMarkup = `<span class="query-result-column-label truncate" title="${escapeHtml(columnName)}">${escapeHtml(
53
+ columnName
54
+ )}</span>`;
14
55
 
15
56
  return `
16
- <button
17
- class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
18
- isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
19
- }"
20
- data-action="sort-editor-results-column"
57
+ <div
58
+ class="query-result-column-header"
21
59
  data-column-name="${escapeHtml(columnName)}"
22
- type="button"
60
+ data-result-column-header
61
+ data-result-scope="${escapeHtml(resultScope)}"
23
62
  >
24
- <span class="truncate">${escapeHtml(columnName)}</span>
25
- <span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
26
- columnName,
27
- sortColumn,
28
- sortDirection
29
- )}</span>
30
- </button>
63
+ ${
64
+ sortAction
65
+ ? `
66
+ <button
67
+ class="query-result-column-sort ${
68
+ isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
69
+ }"
70
+ data-action="${escapeHtml(sortAction)}"
71
+ data-column-name="${escapeHtml(columnName)}"
72
+ type="button"
73
+ >
74
+ ${labelMarkup}
75
+ <span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
76
+ columnName,
77
+ sortColumn,
78
+ sortDirection
79
+ )}</span>
80
+ </button>
81
+ `
82
+ : `<span class="query-result-column-static text-on-surface-variant">${labelMarkup}</span>`
83
+ }
84
+ ${renderColumnActionMenu(columnName, resultScope)}
85
+ </div>
31
86
  `;
32
87
  }
33
88
 
34
89
  export function renderQueryResultsPane(
35
90
  result,
36
- { selectedRowIndex = null, editable = false, sortColumn = null, sortDirection = null } = {}
91
+ {
92
+ selectedRowIndex = null,
93
+ editable = false,
94
+ sortColumn = null,
95
+ sortDirection = null,
96
+ resultScope = "editor",
97
+ sortAction = "sort-editor-results-column",
98
+ } = {}
37
99
  ) {
38
100
  if (!result) {
39
101
  return `
@@ -48,8 +110,8 @@ export function renderQueryResultsPane(
48
110
 
49
111
  const columns = (result.columns ?? []).map((columnName) => ({
50
112
  headerClassName:
51
- "border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
52
- renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection),
113
+ "query-result-header-cell border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
114
+ renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection, { resultScope, sortAction }),
53
115
  cellClassName: "px-4 py-3 align-top text-on-surface",
54
116
  render: (row) => {
55
117
  const value = formatCellValue(row[columnName]);
@@ -1,4 +1,19 @@
1
1
  import { escapeHtml } from "../utils/format.js";
2
+ import {
3
+ compactPathForDisplay,
4
+ detectFilePathValue,
5
+ getPathTypeLabel,
6
+ } from "../utils/filePathPreview.js";
7
+ import {
8
+ getTimestampPreviewForField,
9
+ isProtectedKeyColumn,
10
+ } from "../utils/timestampPreview.js";
11
+ import {
12
+ formatTextCellCharacterCount,
13
+ getTextCellCharacterCount,
14
+ } from "../utils/textCellStats.js";
15
+
16
+ const URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
2
17
 
3
18
  function getJsonPreview(value) {
4
19
  if (value === null || value === undefined) {
@@ -28,6 +43,241 @@ function getJsonPreview(value) {
28
43
  }
29
44
  }
30
45
 
46
+ function getUrlValue(value) {
47
+ const text = String(value ?? "").trim();
48
+
49
+ if (!URL_PATTERN.test(text)) {
50
+ return null;
51
+ }
52
+
53
+ try {
54
+ const url = new URL(text);
55
+
56
+ return ["http:", "https:"].includes(url.protocol) ? url.href : null;
57
+ } catch (error) {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function withUrlBadge(badges = [], url) {
63
+ if (!url) {
64
+ return badges;
65
+ }
66
+
67
+ const hasUrlBadge = badges.some((badge) => {
68
+ const label = typeof badge === "object" ? badge.label : badge;
69
+ return String(label ?? "").toUpperCase() === "URL";
70
+ });
71
+
72
+ return hasUrlBadge ? badges : [...badges, { label: "URL", tone: "url" }];
73
+ }
74
+
75
+ function getAllowedValues(field) {
76
+ const seen = new Set();
77
+
78
+ return (Array.isArray(field.allowedValues) ? field.allowedValues : [])
79
+ .map((value) => String(value))
80
+ .filter((value) => {
81
+ if (seen.has(value)) {
82
+ return false;
83
+ }
84
+
85
+ seen.add(value);
86
+ return true;
87
+ });
88
+ }
89
+
90
+ function withCheckBadge(badges = [], allowedValues = []) {
91
+ if (!allowedValues.length) {
92
+ return badges;
93
+ }
94
+
95
+ const hasCheckBadge = badges.some((badge) => {
96
+ const label = typeof badge === "object" ? badge.label : badge;
97
+ return String(label ?? "").toUpperCase() === "CHECK";
98
+ });
99
+
100
+ return hasCheckBadge ? badges : [...badges, { label: "CHECK", tone: "check" }];
101
+ }
102
+
103
+ function withFilePathBadge(badges = [], filePathPreview) {
104
+ if (!filePathPreview) {
105
+ return badges;
106
+ }
107
+
108
+ const hasPathBadge = badges.some((badge) => {
109
+ const label = typeof badge === "object" ? badge.label : badge;
110
+ return String(label ?? "").toUpperCase() === "PATH";
111
+ });
112
+
113
+ return hasPathBadge ? badges : [...badges, { label: "PATH", tone: "filepath" }];
114
+ }
115
+
116
+ function renderOpenUrlButton(url) {
117
+ if (!url) {
118
+ return "";
119
+ }
120
+
121
+ return `
122
+ <div class="mt-2">
123
+ <button
124
+ class="standard-button"
125
+ data-action="open-row-editor-url"
126
+ data-url="${escapeHtml(url)}"
127
+ type="button"
128
+ >
129
+ <span class="material-symbols-outlined text-sm">open_in_new</span>
130
+ Open in tab
131
+ </button>
132
+ </div>
133
+ `;
134
+ }
135
+
136
+ function renderAllowedValuesSelect(field, allowedValues) {
137
+ const currentValue = String(field.value ?? "");
138
+ const hasCurrentAllowedValue = allowedValues.includes(currentValue);
139
+ const shouldRenderEmptyOption = field.notNull !== true;
140
+ const shouldRenderCurrentOption =
141
+ currentValue !== "" && !hasCurrentAllowedValue;
142
+ const options = [
143
+ shouldRenderEmptyOption
144
+ ? `<option value="" ${currentValue === "" ? "selected" : ""}>NULL / empty</option>`
145
+ : "",
146
+ shouldRenderCurrentOption
147
+ ? `<option value="${escapeHtml(currentValue)}" selected>${escapeHtml(currentValue)}</option>`
148
+ : "",
149
+ ...allowedValues.map(
150
+ (value) =>
151
+ `<option value="${escapeHtml(value)}" ${
152
+ value === currentValue ? "selected" : ""
153
+ }>${escapeHtml(value)}</option>`
154
+ ),
155
+ ].join("");
156
+
157
+ return `
158
+ <select
159
+ class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
160
+ data-row-editor-initial-value="${escapeHtml(currentValue)}"
161
+ data-row-editor-text-source
162
+ data-row-editor-timestamp-source
163
+ name="field:${escapeHtml(field.name)}"
164
+ >
165
+ ${options}
166
+ </select>
167
+ `;
168
+ }
169
+
170
+ function getFieldColumnName(field = {}) {
171
+ return String(field.sourceColumn ?? field.name ?? "");
172
+ }
173
+
174
+ function getFieldRawValue(field = {}) {
175
+ return field.rawValue === undefined ? field.value : field.rawValue;
176
+ }
177
+
178
+ function getFieldTimestampPreview(field = {}, tableMeta = {}) {
179
+ return getTimestampPreviewForField({
180
+ columnName: getFieldColumnName(field),
181
+ value: getFieldRawValue(field),
182
+ tableMeta,
183
+ });
184
+ }
185
+
186
+ function renderTimestampPreview(field = {}, tableMeta = {}) {
187
+ const preview = getFieldTimestampPreview(field, tableMeta);
188
+
189
+ if (preview.kind === "protected-key") {
190
+ return `
191
+ <div class="text-[11px] text-on-surface-variant/55">
192
+ Key column - shown as raw value
193
+ </div>
194
+ `;
195
+ }
196
+
197
+ return `
198
+ <div
199
+ class="text-[11px] text-on-surface-variant/70"
200
+ data-row-editor-timestamp-preview
201
+ ${preview.kind === "timestamp" ? "" : "hidden"}
202
+ >
203
+ ${
204
+ preview.kind === "timestamp"
205
+ ? `Interpretiert als Datum: ${escapeHtml(preview.formatted)}`
206
+ : ""
207
+ }
208
+ </div>
209
+ `;
210
+ }
211
+
212
+ function renderTextCharacterCountBadge(value) {
213
+ const count = getTextCellCharacterCount(value);
214
+
215
+ return `
216
+ <span
217
+ class="border px-2 py-1 text-[9px] ${getFieldBadgeClassName("char-count")}"
218
+ data-row-editor-char-count
219
+ ${count === null ? "hidden" : ""}
220
+ >
221
+ ${count === null ? "" : escapeHtml(formatTextCellCharacterCount(count))}
222
+ </span>
223
+ `;
224
+ }
225
+
226
+ function renderFieldBadgesWithCharacterCount(badges = [], value) {
227
+ const [typeBadge, ...remainingBadges] = badges;
228
+
229
+ return [
230
+ typeBadge ? renderFieldBadge(typeBadge) : "",
231
+ renderTextCharacterCountBadge(value),
232
+ ...remainingBadges.map((badge) => renderFieldBadge(badge)),
233
+ ].join("");
234
+ }
235
+
236
+ function getFieldFilePathPreview(field = {}, tableMeta = {}) {
237
+ return detectFilePathValue(getFieldRawValue(field), getFieldColumnName(field), tableMeta);
238
+ }
239
+
240
+ function renderFilePathPreview(field = {}, tableMeta = {}) {
241
+ if (isProtectedKeyColumn(getFieldColumnName(field), tableMeta)) {
242
+ return "";
243
+ }
244
+
245
+ const preview = getFieldFilePathPreview(field, tableMeta);
246
+
247
+ return `
248
+ <div
249
+ class="border border-outline-variant/10 bg-surface-container px-4 py-4 text-xs text-on-surface"
250
+ data-row-editor-filepath-preview
251
+ ${preview ? "" : "hidden"}
252
+ >
253
+ <div class="flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/75">
254
+ <span class="material-symbols-outlined text-sm">folder</span>
255
+ Interpretiert als Filepath
256
+ </div>
257
+ <div class="mt-3 grid gap-2 font-mono text-[11px] leading-5">
258
+ <div>
259
+ <span class="text-on-surface-variant/55">Filename:</span>
260
+ <span data-row-editor-filepath-filename>${escapeHtml(preview?.fileName ?? "N/A")}</span>
261
+ </div>
262
+ <div data-row-editor-filepath-directory-row ${preview?.directory ? "" : "hidden"}>
263
+ <span class="text-on-surface-variant/55">Directory:</span>
264
+ <span data-row-editor-filepath-directory title="${escapeHtml(preview?.directory ?? "")}">
265
+ ${escapeHtml(preview?.directory ? compactPathForDisplay(preview.directory, 72) : "")}
266
+ </span>
267
+ </div>
268
+ <div>
269
+ <span class="text-on-surface-variant/55">Extension:</span>
270
+ <span data-row-editor-filepath-extension>${escapeHtml(preview?.extension ?? "N/A")}</span>
271
+ </div>
272
+ <div>
273
+ <span class="text-on-surface-variant/55">Type:</span>
274
+ <span data-row-editor-filepath-type>${escapeHtml(preview ? getPathTypeLabel(preview.pathType) : "N/A")}</span>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ `;
279
+ }
280
+
31
281
  function renderJsonViewer(prettyJson, title = "JSON Viewer") {
32
282
  return `
33
283
  <div class="border border-outline-variant/10 bg-surface-container px-4 py-4">
@@ -41,37 +291,66 @@ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
41
291
  `;
42
292
  }
43
293
 
44
- function renderReadonlyField(label, value) {
45
- const badges = Array.isArray(label?.badges) ? label.badges : [];
294
+ function renderReadonlyField(field = {}, tableMeta = {}) {
295
+ const label = field.label ?? field.name;
296
+ const value = field.value;
297
+ const rawValue = getFieldRawValue(field);
298
+ const columnName = getFieldColumnName(field);
299
+ const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
300
+ const filePathPreview = getFieldFilePathPreview({ ...field, rawValue }, tableMeta);
301
+ const url = getUrlValue(value);
302
+ const badges = withFilePathBadge(withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url), filePathPreview);
46
303
  const displayLabel = typeof label === "object" ? label.label : label;
47
304
  const jsonPreview = getJsonPreview(value);
48
305
 
49
306
  return `
50
- <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
307
+ <div
308
+ class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3"
309
+ data-row-editor-column-name="${escapeHtml(columnName)}"
310
+ data-row-editor-field
311
+ data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
312
+ ${
313
+ url ? "data-row-editor-url-field" : ""
314
+ }>
51
315
  <div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
52
316
  <span>${escapeHtml(displayLabel)}</span>
53
- ${badges.map((badge) => renderFieldBadge(badge)).join("")}
317
+ ${renderFieldBadgesWithCharacterCount(badges, rawValue)}
54
318
  </div>
55
319
  ${
56
320
  jsonPreview
57
321
  ? `<div class="mt-2">${renderJsonViewer(jsonPreview)}</div>`
58
322
  : `<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>`
59
323
  }
324
+ ${renderTimestampPreview({ ...field, rawValue }, tableMeta)}
325
+ ${renderFilePathPreview({ ...field, rawValue }, tableMeta)}
326
+ ${renderOpenUrlButton(url)}
60
327
  </div>
61
328
  `;
62
329
  }
63
330
 
64
- function renderEditableField(field) {
65
- const badges = Array.isArray(field.badges) ? field.badges : [];
331
+ function renderEditableField(field, tableMeta = {}) {
332
+ const columnName = getFieldColumnName(field);
333
+ const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
334
+ const url = getUrlValue(field.value);
335
+ const allowedValues = getAllowedValues(field);
336
+ const filePathPreview = getFieldFilePathPreview(field, tableMeta);
337
+ const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
338
+ const badges = withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview);
66
339
  const jsonPreview = getJsonPreview(field.value);
67
340
  const inputType = field.inputType === "number" ? "number" : "text";
68
341
  const numberStep = field.numberStep === "1" ? "1" : "any";
69
342
 
70
343
  return `
71
- <label class="block space-y-2">
344
+ <div
345
+ class="block space-y-2"
346
+ data-row-editor-column-name="${escapeHtml(columnName)}"
347
+ data-row-editor-field
348
+ data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
349
+ ${url ? "data-row-editor-url-field" : ""}
350
+ >
72
351
  <span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
73
352
  <span>${escapeHtml(field.label ?? field.name)}</span>
74
- ${badges.map((badge) => renderFieldBadge(badge)).join("")}
353
+ ${renderFieldBadgesWithCharacterCount(badges, inputType === "number" ? null : field.value ?? "")}
75
354
  </span>
76
355
  ${
77
356
  jsonPreview
@@ -79,27 +358,51 @@ function renderEditableField(field) {
79
358
  : ""
80
359
  }
81
360
  ${
82
- inputType === "number" && !jsonPreview
361
+ allowedValues.length && !jsonPreview
362
+ ? renderAllowedValuesSelect(field, allowedValues)
363
+ : inputType === "number" && !jsonPreview
83
364
  ? `
84
365
  <input
85
366
  class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
367
+ data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
368
+ data-row-editor-timestamp-source
86
369
  name="field:${escapeHtml(field.name)}"
87
370
  step="${escapeHtml(numberStep)}"
88
371
  type="number"
89
372
  value="${escapeHtml(field.value ?? "")}"
90
373
  />
91
374
  `
375
+ : url && !jsonPreview
376
+ ? `
377
+ <input
378
+ class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
379
+ data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
380
+ data-row-editor-text-source
381
+ data-row-editor-timestamp-source
382
+ name="field:${escapeHtml(field.name)}"
383
+ data-row-editor-url-input
384
+ spellcheck="false"
385
+ type="text"
386
+ value="${escapeHtml(field.value ?? "")}"
387
+ />
388
+ ${renderOpenUrlButton(url)}
389
+ `
92
390
  : `
93
391
  <textarea
94
392
  class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container ${
95
393
  jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
96
394
  }"
395
+ data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
396
+ data-row-editor-text-source
397
+ data-row-editor-timestamp-source
97
398
  name="field:${escapeHtml(field.name)}"
98
399
  spellcheck="false"
99
400
  >${escapeHtml(field.value ?? "")}</textarea>
100
401
  `
101
402
  }
102
- </label>
403
+ ${renderTimestampPreview(field, tableMeta)}
404
+ ${renderFilePathPreview(field, tableMeta)}
405
+ </div>
103
406
  `;
104
407
  }
105
408
 
@@ -112,6 +415,22 @@ function getFieldBadgeClassName(tone) {
112
415
  return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
113
416
  }
114
417
 
418
+ if (tone === "url") {
419
+ return "border-primary-container/35 bg-primary-container/15 text-primary-container";
420
+ }
421
+
422
+ if (tone === "check") {
423
+ return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
424
+ }
425
+
426
+ if (tone === "filepath") {
427
+ return "border-outline-variant/30 bg-surface-container-high text-on-surface";
428
+ }
429
+
430
+ if (tone === "char-count") {
431
+ return "border-outline-variant/25 bg-surface-container text-on-surface-variant";
432
+ }
433
+
115
434
  return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
116
435
  }
117
436
 
@@ -135,11 +454,13 @@ export function renderRowEditorPanel({
135
454
  hiddenFields = [],
136
455
  editableFields = [],
137
456
  readonlyFields = [],
457
+ tableMeta = {},
138
458
  disabledMessage = "",
139
459
  saveError = null,
140
460
  saving = false,
141
461
  deleting = false,
142
462
  reloadAction = "",
463
+ jsonActionsEnabled = false,
143
464
  submitLabel = "Save",
144
465
  deleteAction = "",
145
466
  deleteRowIndex = null,
@@ -158,6 +479,18 @@ export function renderRowEditorPanel({
158
479
  '" type="button">Reload</button>',
159
480
  ].join("")
160
481
  : "",
482
+ jsonActionsEnabled
483
+ ? [
484
+ '<button class="standard-button" data-action="copy-row-editor-json" type="button">',
485
+ '<span class="material-symbols-outlined text-sm">content_copy</span>',
486
+ "Copy as JSON",
487
+ "</button>",
488
+ '<button class="standard-button" data-action="export-row-editor-json" type="button">',
489
+ '<span class="material-symbols-outlined text-sm">download</span>',
490
+ "Export as JSON",
491
+ "</button>",
492
+ ].join("")
493
+ : "",
161
494
  canSubmit
162
495
  ? [
163
496
  '<button class="standard-button" form="',
@@ -254,7 +587,7 @@ export function renderRowEditorPanel({
254
587
  ? `
255
588
  <div class="space-y-4">
256
589
  ${editableFields
257
- .map((field) => renderEditableField(field))
590
+ .map((field) => renderEditableField(field, tableMeta))
258
591
  .join("")}
259
592
  </div>
260
593
  `
@@ -286,7 +619,7 @@ export function renderRowEditorPanel({
286
619
  </div>
287
620
  <div class="space-y-3">
288
621
  ${readonlyFields
289
- .map((field) => renderReadonlyField(field.label ?? field.name, field.value))
622
+ .map((field) => renderReadonlyField(field, tableMeta))
290
623
  .join("")}
291
624
  </div>
292
625
  </div>