sqlite-hub 1.2.0 → 1.5.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 +113 -71
- package/bin/sqlite-hub.js +723 -116
- package/docs/API.md +152 -0
- package/docs/CLI.md +249 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/changelog.md +149 -0
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/guidelines/DESIGN.md +55 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +4 -0
- package/examples/api/generate-types.js +52 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +63 -0
- package/frontend/js/app.js +310 -10
- package/frontend/js/components/connectionCard.js +4 -2
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/generateTypesDropdown.js +33 -0
- package/frontend/js/components/metricCard.js +1 -1
- package/frontend/js/components/modal.js +555 -21
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryHistoryDetail.js +3 -3
- package/frontend/js/components/queryHistoryHeader.js +1 -1
- package/frontend/js/components/queryHistoryPanel.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +13 -3
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +38 -6
- package/frontend/js/store.js +870 -8
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +581 -37
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +17 -1
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +80 -30
- package/frontend/js/views/structure.js +12 -1
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +258 -232
- package/frontend/styles/structure-graph.css +32 -68
- package/frontend/styles/tailwind.css +8 -4
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +28 -21
- package/frontend/styles/views.css +102 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +321 -2
- package/server/routes/logs.js +127 -0
- package/server/routes/structure.js +22 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +59 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/structureService.js +7 -0
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +773 -169
- package/server/services/typeGenerationService.js +663 -0
- package/tailwind.config.cjs +0 -1
- package/.github/funding.yml +0 -2
- package/.github/workflows/ci.yml +0 -36
- package/database.sqlite +0 -0
- package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
- package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
- package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
- package/frontend/assets/mockups/connections_1200.webp +0 -0
- package/frontend/assets/mockups/data_1_1200.webp +0 -0
- package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
- package/frontend/assets/mockups/documents_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
- package/frontend/assets/mockups/overview_1200.webp +0 -0
- package/frontend/assets/mockups/settings_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
- package/frontend/assets/mockups/structure_1_1200.webp +0 -0
- package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
- package/tests/api-token-auth.test.js +0 -279
- package/tests/backup-manager.test.js +0 -140
- package/tests/backups-view.test.js +0 -70
- package/tests/charts-height-preset-storage.test.js +0 -60
- package/tests/charts-route-state.test.js +0 -144
- package/tests/check-constraint-options.test.js +0 -90
- package/tests/cli-args.test.js +0 -113
- package/tests/cli-service-delegation.test.js +0 -140
- package/tests/connection-removal.test.js +0 -52
- package/tests/connections-file-dialog-route.test.js +0 -89
- package/tests/copy-column-modal.test.js +0 -131
- package/tests/database-command-service.test.js +0 -174
- package/tests/database-documents.test.js +0 -85
- package/tests/documents-view.test.js +0 -132
- package/tests/dropdown-button.test.js +0 -101
- package/tests/email-preview.test.js +0 -89
- package/tests/export-blob.test.js +0 -99
- package/tests/export-filenames.test.js +0 -38
- package/tests/file-path-preview.test.js +0 -165
- package/tests/form-controls.test.js +0 -34
- package/tests/json-preview.test.js +0 -49
- package/tests/local-request-security.test.js +0 -85
- package/tests/markdown-documents.test.js +0 -103
- package/tests/native-file-dialog.test.js +0 -105
- package/tests/query-editor.test.js +0 -28
- package/tests/query-history-detail.test.js +0 -37
- package/tests/query-history-header.test.js +0 -30
- package/tests/query-results-truncation.test.js +0 -20
- package/tests/risky-sql.test.js +0 -30
- package/tests/row-editor-json.test.js +0 -82
- package/tests/row-editor-null-values.test.js +0 -184
- package/tests/row-editor-timestamp-preview.test.js +0 -192
- package/tests/security-paths.test.js +0 -84
- package/tests/settings-api-tokens-route.test.js +0 -97
- package/tests/settings-metadata.test.js +0 -114
- package/tests/settings-view.test.js +0 -108
- package/tests/sql-formatter.test.js +0 -173
- package/tests/sql-highlight.test.js +0 -38
- package/tests/sql-identifier-safety.test.js +0 -171
- package/tests/sql-result-limit.test.js +0 -66
- package/tests/structure-view.test.js +0 -60
- package/tests/table-designer-v2-unique-constraints.test.js +0 -78
- package/tests/table-scroll-state.test.js +0 -70
- package/tests/text-cell-stats.test.js +0 -38
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
MEDIA_TAGGING_DEFAULT_TAG_TABLE,
|
|
18
18
|
MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL,
|
|
19
19
|
} from '../lib/mediaTaggingDefaults.js';
|
|
20
|
+
import { SYNTHETIC_GENERATOR_TYPES, getSyntheticGeneratorLabel } from '../utils/syntheticData.js';
|
|
20
21
|
|
|
21
22
|
function renderField({ label, name, type = 'text', placeholder = '', value = '' }) {
|
|
22
23
|
return `
|
|
@@ -126,7 +127,7 @@ function renderError(error) {
|
|
|
126
127
|
|
|
127
128
|
return `
|
|
128
129
|
<div class="border border-error/20 bg-error-container/20 px-4 py-3 text-sm text-error">
|
|
129
|
-
<div class="font-
|
|
130
|
+
<div class="font-body text-xs font-bold uppercase tracking-[0.18em]">${escapeHtml(
|
|
130
131
|
error.code || 'Request failed',
|
|
131
132
|
)}</div>
|
|
132
133
|
<div class="mt-1 text-on-surface">${escapeHtml(error.message)}</div>
|
|
@@ -225,7 +226,7 @@ function renderEditConnectionForm(modal) {
|
|
|
225
226
|
label: 'Upload image',
|
|
226
227
|
name: 'logoFile',
|
|
227
228
|
accept: '.png,.jpg,.jpeg,.webp,image/png,image/jpeg,image/webp',
|
|
228
|
-
helpText: 'Allowed formats: PNG, JPG, WEBP.
|
|
229
|
+
helpText: 'Allowed formats: PNG, JPG, WEBP.',
|
|
229
230
|
})}
|
|
230
231
|
${
|
|
231
232
|
connection.logoUrl
|
|
@@ -449,15 +450,25 @@ function renderDeleteBackupForm(modal) {
|
|
|
449
450
|
].join('');
|
|
450
451
|
}
|
|
451
452
|
|
|
452
|
-
function
|
|
453
|
+
function renderEditBackupForm(modal) {
|
|
453
454
|
return `
|
|
454
|
-
<form class="space-y-5" data-form="edit-backup
|
|
455
|
+
<form class="space-y-5" data-form="edit-backup">
|
|
455
456
|
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
456
|
-
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
457
|
-
<div class="mt-2 font-
|
|
458
|
-
${escapeHtml(modal.
|
|
457
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">Primary Key</div>
|
|
458
|
+
<div class="mt-2 font-mono text-[10px] uppercase tracking-[0.12em] text-on-surface/80">
|
|
459
|
+
${escapeHtml(modal.backupId ?? 'n/a')}
|
|
459
460
|
</div>
|
|
460
461
|
</div>
|
|
462
|
+
<label class="block space-y-2">
|
|
463
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Name</span>
|
|
464
|
+
<input
|
|
465
|
+
class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest px-3 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
466
|
+
name="name"
|
|
467
|
+
placeholder="Backup name"
|
|
468
|
+
required
|
|
469
|
+
value="${escapeHtml(modal.backupName ?? 'Backup')}"
|
|
470
|
+
/>
|
|
471
|
+
</label>
|
|
461
472
|
<label class="block space-y-2">
|
|
462
473
|
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Notes</span>
|
|
463
474
|
<textarea
|
|
@@ -471,7 +482,7 @@ function renderEditBackupNotesForm(modal) {
|
|
|
471
482
|
<button class="standard-button" data-action="close-modal" type="button">Cancel</button>
|
|
472
483
|
<button class="signature-button" type="submit" ${modal.submitting ? 'disabled' : ''}>
|
|
473
484
|
<span class="material-symbols-outlined text-sm">save</span>
|
|
474
|
-
${modal.submitting ? 'Saving...' : 'Save
|
|
485
|
+
${modal.submitting ? 'Saving...' : 'Save backup'}
|
|
475
486
|
</button>
|
|
476
487
|
</div>
|
|
477
488
|
</form>
|
|
@@ -490,7 +501,7 @@ function renderBackupSafetyForm(modal) {
|
|
|
490
501
|
</p>
|
|
491
502
|
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
492
503
|
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">Safety Backup</div>
|
|
493
|
-
<div class="mt-2 font-
|
|
504
|
+
<div class="mt-2 font-body text-sm font-black uppercase text-on-surface">${escapeHtml(
|
|
494
505
|
modal.backupNameBeforeOperation ?? modal.backupName ?? 'Before operation',
|
|
495
506
|
)}</div>
|
|
496
507
|
</div>
|
|
@@ -512,6 +523,512 @@ function renderBackupSafetyForm(modal) {
|
|
|
512
523
|
`;
|
|
513
524
|
}
|
|
514
525
|
|
|
526
|
+
function renderTypeGenerationSelect({ label, field, value, options = [] }) {
|
|
527
|
+
return renderSelectField({
|
|
528
|
+
label,
|
|
529
|
+
name: field,
|
|
530
|
+
value,
|
|
531
|
+
bind: 'type-generation-field',
|
|
532
|
+
options,
|
|
533
|
+
}).replace('<select ', `<select data-type-generation-field="${escapeHtml(field)}" `);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function renderTypeGenerationCheckbox({ label, field, checked }) {
|
|
537
|
+
return `
|
|
538
|
+
<label class="standard-checkbox">
|
|
539
|
+
<input
|
|
540
|
+
${checked ? 'checked' : ''}
|
|
541
|
+
data-bind="type-generation-field"
|
|
542
|
+
data-type-generation-field="${escapeHtml(field)}"
|
|
543
|
+
type="checkbox"
|
|
544
|
+
/>
|
|
545
|
+
<span>${escapeHtml(label)}</span>
|
|
546
|
+
</label>
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function renderTypeGenerationCodePreview(code) {
|
|
551
|
+
const text = String(code ?? '');
|
|
552
|
+
const normalized =
|
|
553
|
+
text.includes('\n') || text.includes('\\n')
|
|
554
|
+
? text.replace(/\\n/g, '\n')
|
|
555
|
+
: text
|
|
556
|
+
.replace(/\{\s*/g, '{\n ')
|
|
557
|
+
.replace(/;\s*/g, ';\n ')
|
|
558
|
+
.replace(/\s*\}\s*$/g, '\n}');
|
|
559
|
+
|
|
560
|
+
return normalized
|
|
561
|
+
.replace(/\r\n/g, '\n')
|
|
562
|
+
.split('\n')
|
|
563
|
+
.map(line => `<span class="block whitespace-pre">${line ? escapeHtml(line) : ' '}</span>`)
|
|
564
|
+
.join('');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function renderGenerateTypesForm(modal) {
|
|
568
|
+
const options = modal.options ?? {};
|
|
569
|
+
const result = modal.result ?? {};
|
|
570
|
+
const target = modal.target ?? 'typescript';
|
|
571
|
+
const isAllTables = modal.scope === 'all';
|
|
572
|
+
const fileCount = result.files?.length ?? modal.tableNames?.length ?? 0;
|
|
573
|
+
const subjectLabel = isAllTables
|
|
574
|
+
? `all ${fileCount || ''} tables`.replace(/\s+/g, ' ').trim()
|
|
575
|
+
: `the schema of "${modal.tableName ?? ''}"`;
|
|
576
|
+
const downloadLabel = result.files?.length ? 'Download Files' : 'Download';
|
|
577
|
+
|
|
578
|
+
return `
|
|
579
|
+
<div class="space-y-5">
|
|
580
|
+
<p class="text-sm leading-6 text-on-surface-variant/70">
|
|
581
|
+
Generate application types from ${escapeHtml(subjectLabel)}.
|
|
582
|
+
</p>
|
|
583
|
+
<div class="grid gap-5 xl:grid-cols-[24rem_minmax(0,1fr)]">
|
|
584
|
+
<div class="min-w-0 space-y-4">
|
|
585
|
+
<div class="space-y-4">
|
|
586
|
+
${renderTypeGenerationSelect({
|
|
587
|
+
label: 'Target',
|
|
588
|
+
field: 'target',
|
|
589
|
+
value: target,
|
|
590
|
+
options: [
|
|
591
|
+
{ value: 'typescript', label: 'TypeScript' },
|
|
592
|
+
{ value: 'rust', label: 'Rust' },
|
|
593
|
+
{ value: 'kotlin', label: 'Kotlin' },
|
|
594
|
+
{ value: 'swift', label: 'Swift' },
|
|
595
|
+
],
|
|
596
|
+
})}
|
|
597
|
+
${renderTypeGenerationSelect({
|
|
598
|
+
label: 'Property naming',
|
|
599
|
+
field: 'propertyNaming',
|
|
600
|
+
value: options.propertyNaming ?? 'camel',
|
|
601
|
+
options: [
|
|
602
|
+
{ value: 'preserve', label: 'Preserve' },
|
|
603
|
+
{ value: 'camel', label: 'camelCase' },
|
|
604
|
+
{ value: 'pascal', label: 'PascalCase' },
|
|
605
|
+
{ value: 'snake', label: 'snake_case' },
|
|
606
|
+
],
|
|
607
|
+
})}
|
|
608
|
+
${renderTypeGenerationSelect({
|
|
609
|
+
label: 'Nullable handling',
|
|
610
|
+
field: 'nullableMode',
|
|
611
|
+
value: options.nullableMode ?? 'native',
|
|
612
|
+
options: [
|
|
613
|
+
{ value: 'native', label: 'Native' },
|
|
614
|
+
{ value: 'optional', label: 'Optional (TypeScript)' },
|
|
615
|
+
],
|
|
616
|
+
})}
|
|
617
|
+
${
|
|
618
|
+
target === 'typescript'
|
|
619
|
+
? renderTypeGenerationSelect({
|
|
620
|
+
label: 'JSON type',
|
|
621
|
+
field: 'jsonType',
|
|
622
|
+
value: options.jsonType ?? 'unknown',
|
|
623
|
+
options: [
|
|
624
|
+
{ value: 'unknown', label: 'unknown' },
|
|
625
|
+
{ value: 'record', label: 'Record<string, unknown>' },
|
|
626
|
+
{ value: 'json-value', label: 'JsonValue' },
|
|
627
|
+
],
|
|
628
|
+
})
|
|
629
|
+
: ''
|
|
630
|
+
}
|
|
631
|
+
</div>
|
|
632
|
+
<div class="grid gap-2">
|
|
633
|
+
${renderTypeGenerationCheckbox({
|
|
634
|
+
label: 'Export declaration',
|
|
635
|
+
field: 'exportDeclaration',
|
|
636
|
+
checked: options.exportDeclaration !== false,
|
|
637
|
+
})}
|
|
638
|
+
${renderTypeGenerationCheckbox({
|
|
639
|
+
label: 'Include default values as comments',
|
|
640
|
+
field: 'includeDefaultsAsComments',
|
|
641
|
+
checked: Boolean(options.includeDefaultsAsComments),
|
|
642
|
+
})}
|
|
643
|
+
${renderTypeGenerationCheckbox({
|
|
644
|
+
label: 'Include generated columns',
|
|
645
|
+
field: 'includeGeneratedColumns',
|
|
646
|
+
checked: options.includeGeneratedColumns !== false,
|
|
647
|
+
})}
|
|
648
|
+
${renderTypeGenerationCheckbox({
|
|
649
|
+
label: 'Include hidden columns',
|
|
650
|
+
field: 'includeHiddenColumns',
|
|
651
|
+
checked: Boolean(options.includeHiddenColumns),
|
|
652
|
+
})}
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
<div class="min-w-0 space-y-3">
|
|
656
|
+
<div class="flex items-center justify-between gap-3">
|
|
657
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container">Code Preview</div>
|
|
658
|
+
<div class="truncate font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
|
|
659
|
+
${escapeHtml(result.fileName ?? '')}
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
${
|
|
663
|
+
result.files?.length
|
|
664
|
+
? `<div class="flex flex-wrap gap-2">${result.files
|
|
665
|
+
.map(
|
|
666
|
+
file =>
|
|
667
|
+
`<span class="border border-outline-variant/20 bg-surface-highest px-2 py-1 font-mono text-[10px] uppercase tracking-[0.14em] text-on-surface-variant/70">${escapeHtml(
|
|
668
|
+
file.fileName ?? file.tableName ?? 'types',
|
|
669
|
+
)}</span>`,
|
|
670
|
+
)
|
|
671
|
+
.join('')}</div>`
|
|
672
|
+
: ''
|
|
673
|
+
}
|
|
674
|
+
<pre class="type-generation-code-preview custom-scrollbar border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 font-mono text-xs leading-6 text-on-surface"><code>${renderTypeGenerationCodePreview(
|
|
675
|
+
modal.loading ? 'Generating...' : (result.code ?? ''),
|
|
676
|
+
)}</code></pre>
|
|
677
|
+
${renderError(modal.error)}
|
|
678
|
+
</div>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="flex items-center justify-between gap-3 pt-2">
|
|
681
|
+
<button class="standard-button" data-action="close-modal" type="button">Cancel</button>
|
|
682
|
+
<div class="flex items-center gap-2">
|
|
683
|
+
<button class="standard-button" data-action="copy-generated-types" type="button" ${
|
|
684
|
+
result.code ? '' : 'disabled'
|
|
685
|
+
}>
|
|
686
|
+
<span class="material-symbols-outlined text-sm">content_copy</span>
|
|
687
|
+
Copy Code
|
|
688
|
+
</button>
|
|
689
|
+
<button class="signature-button" data-action="download-generated-types" type="button" ${
|
|
690
|
+
result.code ? '' : 'disabled'
|
|
691
|
+
}>
|
|
692
|
+
<span class="material-symbols-outlined text-sm">download</span>
|
|
693
|
+
${escapeHtml(downloadLabel)}
|
|
694
|
+
</button>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
</div>
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function renderSyntheticGeneratorOptions(value) {
|
|
702
|
+
return SYNTHETIC_GENERATOR_TYPES.map(
|
|
703
|
+
type =>
|
|
704
|
+
`<option value="${escapeHtml(type.value)}" ${String(type.value) === String(value) ? 'selected' : ''}>${escapeHtml(
|
|
705
|
+
type.label,
|
|
706
|
+
)}</option>`,
|
|
707
|
+
).join('');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function renderSyntheticOptionInput({
|
|
711
|
+
columnName,
|
|
712
|
+
option,
|
|
713
|
+
type = 'text',
|
|
714
|
+
value = '',
|
|
715
|
+
min = null,
|
|
716
|
+
max = null,
|
|
717
|
+
step = null,
|
|
718
|
+
placeholder = '',
|
|
719
|
+
}) {
|
|
720
|
+
const attributes = [
|
|
721
|
+
`class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"`,
|
|
722
|
+
placeholder ? `aria-label="${escapeHtml(placeholder)}"` : '',
|
|
723
|
+
`data-bind="generate-data-mapping"`,
|
|
724
|
+
`data-column-name="${escapeHtml(columnName)}"`,
|
|
725
|
+
`data-field="${escapeHtml(option)}"`,
|
|
726
|
+
placeholder ? `placeholder="${escapeHtml(placeholder)}"` : '',
|
|
727
|
+
`type="${escapeHtml(type)}"`,
|
|
728
|
+
`value="${escapeHtml(value)}"`,
|
|
729
|
+
min !== null ? `min="${escapeHtml(min)}"` : '',
|
|
730
|
+
max !== null ? `max="${escapeHtml(max)}"` : '',
|
|
731
|
+
step !== null ? `step="${escapeHtml(step)}"` : '',
|
|
732
|
+
].filter(Boolean);
|
|
733
|
+
|
|
734
|
+
return `<input ${attributes.join(' ')} />`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function renderSyntheticOptionSelect({ columnName, option, value = '', options = [] }) {
|
|
738
|
+
return `
|
|
739
|
+
<select
|
|
740
|
+
class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
741
|
+
data-bind="generate-data-mapping"
|
|
742
|
+
data-column-name="${escapeHtml(columnName)}"
|
|
743
|
+
data-field="${escapeHtml(option)}"
|
|
744
|
+
>
|
|
745
|
+
${options
|
|
746
|
+
.map(
|
|
747
|
+
item =>
|
|
748
|
+
`<option value="${escapeHtml(item.value)}" ${String(item.value) === String(value) ? 'selected' : ''}>${escapeHtml(
|
|
749
|
+
item.label,
|
|
750
|
+
)}</option>`,
|
|
751
|
+
)
|
|
752
|
+
.join('')}
|
|
753
|
+
</select>
|
|
754
|
+
`;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function renderSyntheticMappingOptions(mapping) {
|
|
758
|
+
const options = mapping.options ?? {};
|
|
759
|
+
const columnName = mapping.columnName;
|
|
760
|
+
|
|
761
|
+
switch (mapping.generator) {
|
|
762
|
+
case 'static':
|
|
763
|
+
return renderSyntheticOptionInput({
|
|
764
|
+
columnName,
|
|
765
|
+
option: 'value',
|
|
766
|
+
value: options.value ?? '',
|
|
767
|
+
placeholder: 'Value',
|
|
768
|
+
});
|
|
769
|
+
case 'randomInteger':
|
|
770
|
+
return `
|
|
771
|
+
<div class="synthetic-generator-options-grid synthetic-generator-options-grid--two">
|
|
772
|
+
${renderSyntheticOptionInput({
|
|
773
|
+
columnName,
|
|
774
|
+
option: 'min',
|
|
775
|
+
type: 'number',
|
|
776
|
+
value: options.min ?? 1,
|
|
777
|
+
step: 1,
|
|
778
|
+
placeholder: 'Min',
|
|
779
|
+
})}
|
|
780
|
+
${renderSyntheticOptionInput({
|
|
781
|
+
columnName,
|
|
782
|
+
option: 'max',
|
|
783
|
+
type: 'number',
|
|
784
|
+
value: options.max ?? 1000,
|
|
785
|
+
step: 1,
|
|
786
|
+
placeholder: 'Max',
|
|
787
|
+
})}
|
|
788
|
+
</div>
|
|
789
|
+
`;
|
|
790
|
+
case 'randomDecimal':
|
|
791
|
+
return `
|
|
792
|
+
<div class="synthetic-generator-options-grid synthetic-generator-options-grid--three">
|
|
793
|
+
${renderSyntheticOptionInput({
|
|
794
|
+
columnName,
|
|
795
|
+
option: 'min',
|
|
796
|
+
type: 'number',
|
|
797
|
+
value: options.min ?? 0,
|
|
798
|
+
step: '0.01',
|
|
799
|
+
placeholder: 'Min',
|
|
800
|
+
})}
|
|
801
|
+
${renderSyntheticOptionInput({
|
|
802
|
+
columnName,
|
|
803
|
+
option: 'max',
|
|
804
|
+
type: 'number',
|
|
805
|
+
value: options.max ?? 1000,
|
|
806
|
+
step: '0.01',
|
|
807
|
+
placeholder: 'Max',
|
|
808
|
+
})}
|
|
809
|
+
${renderSyntheticOptionInput({
|
|
810
|
+
columnName,
|
|
811
|
+
option: 'decimals',
|
|
812
|
+
type: 'number',
|
|
813
|
+
value: options.decimals ?? 2,
|
|
814
|
+
min: 0,
|
|
815
|
+
max: 8,
|
|
816
|
+
step: 1,
|
|
817
|
+
placeholder: 'Decimals',
|
|
818
|
+
})}
|
|
819
|
+
</div>
|
|
820
|
+
`;
|
|
821
|
+
case 'boolean':
|
|
822
|
+
return renderSyntheticOptionInput({
|
|
823
|
+
columnName,
|
|
824
|
+
option: 'trueProbability',
|
|
825
|
+
type: 'number',
|
|
826
|
+
value: options.trueProbability ?? 50,
|
|
827
|
+
min: 0,
|
|
828
|
+
max: 100,
|
|
829
|
+
step: 1,
|
|
830
|
+
placeholder: 'True %',
|
|
831
|
+
});
|
|
832
|
+
case 'timestamp':
|
|
833
|
+
return `
|
|
834
|
+
<div class="synthetic-generator-options-grid synthetic-generator-options-grid--timestamp">
|
|
835
|
+
${renderSyntheticOptionSelect({
|
|
836
|
+
columnName,
|
|
837
|
+
option: 'range',
|
|
838
|
+
value: options.range ?? 'last30',
|
|
839
|
+
options: [
|
|
840
|
+
{ value: 'last30', label: 'Last 30 days' },
|
|
841
|
+
{ value: 'last365', label: 'Last 365 days' },
|
|
842
|
+
{ value: 'custom', label: 'Custom' },
|
|
843
|
+
],
|
|
844
|
+
})}
|
|
845
|
+
${
|
|
846
|
+
options.range === 'custom'
|
|
847
|
+
? `${renderSyntheticOptionInput({
|
|
848
|
+
columnName,
|
|
849
|
+
option: 'from',
|
|
850
|
+
type: 'datetime-local',
|
|
851
|
+
value: options.from ?? '',
|
|
852
|
+
placeholder: 'From',
|
|
853
|
+
})}
|
|
854
|
+
${renderSyntheticOptionInput({
|
|
855
|
+
columnName,
|
|
856
|
+
option: 'to',
|
|
857
|
+
type: 'datetime-local',
|
|
858
|
+
value: options.to ?? '',
|
|
859
|
+
placeholder: 'To',
|
|
860
|
+
})}`
|
|
861
|
+
: ''
|
|
862
|
+
}
|
|
863
|
+
</div>
|
|
864
|
+
`;
|
|
865
|
+
case 'oneOf':
|
|
866
|
+
return renderSyntheticOptionInput({
|
|
867
|
+
columnName,
|
|
868
|
+
option: 'values',
|
|
869
|
+
value: options.values ?? '',
|
|
870
|
+
placeholder: 'Comma values',
|
|
871
|
+
});
|
|
872
|
+
case 'skip':
|
|
873
|
+
default:
|
|
874
|
+
return `<span class="font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">No options</span>`;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function formatSyntheticPreviewValue(value) {
|
|
879
|
+
if (value === null || value === undefined) {
|
|
880
|
+
return 'NULL';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (typeof value === 'object') {
|
|
884
|
+
return JSON.stringify(value);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return String(value);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function renderSyntheticPreview(modal) {
|
|
891
|
+
const rows = (modal.previewRows ?? []).slice(0, 3);
|
|
892
|
+
const columns = modal.previewColumns?.length
|
|
893
|
+
? modal.previewColumns
|
|
894
|
+
: (modal.columns ?? []).map(column => column.name);
|
|
895
|
+
|
|
896
|
+
if (!rows.length) {
|
|
897
|
+
return `
|
|
898
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 text-sm text-on-surface-variant/55">
|
|
899
|
+
Preview creates up to 3 rows without writing to the database.
|
|
900
|
+
</div>
|
|
901
|
+
`;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return `
|
|
905
|
+
<div class="synthetic-generator-preview">
|
|
906
|
+
<table>
|
|
907
|
+
<thead>
|
|
908
|
+
<tr>
|
|
909
|
+
${columns.map(column => `<th>${escapeHtml(column)}</th>`).join('')}
|
|
910
|
+
</tr>
|
|
911
|
+
</thead>
|
|
912
|
+
<tbody>
|
|
913
|
+
${rows
|
|
914
|
+
.map(
|
|
915
|
+
row => `
|
|
916
|
+
<tr>
|
|
917
|
+
${columns
|
|
918
|
+
.map(
|
|
919
|
+
column =>
|
|
920
|
+
`<td title="${escapeHtml(formatSyntheticPreviewValue(row[column]))}">${escapeHtml(
|
|
921
|
+
truncateMiddle(formatSyntheticPreviewValue(row[column]), 56),
|
|
922
|
+
)}</td>`,
|
|
923
|
+
)
|
|
924
|
+
.join('')}
|
|
925
|
+
</tr>
|
|
926
|
+
`,
|
|
927
|
+
)
|
|
928
|
+
.join('')}
|
|
929
|
+
</tbody>
|
|
930
|
+
</table>
|
|
931
|
+
</div>
|
|
932
|
+
`;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export function renderGenerateDataForm(modal) {
|
|
936
|
+
const columns = modal.columns ?? [];
|
|
937
|
+
const mappings = modal.mappings ?? [];
|
|
938
|
+
const disabledAttribute = modal.submitting || modal.previewLoading ? 'disabled aria-disabled="true"' : '';
|
|
939
|
+
|
|
940
|
+
return `
|
|
941
|
+
<form class="generate-data-modal-form" data-form="generate-data">
|
|
942
|
+
<div class="synthetic-generator-count-row">
|
|
943
|
+
<label class="block space-y-2">
|
|
944
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Rows</span>
|
|
945
|
+
<input
|
|
946
|
+
class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
947
|
+
data-bind="generate-data-field"
|
|
948
|
+
data-field="rowCount"
|
|
949
|
+
max="10000"
|
|
950
|
+
min="1"
|
|
951
|
+
name="rowCount"
|
|
952
|
+
step="1"
|
|
953
|
+
type="number"
|
|
954
|
+
value="${escapeHtml(modal.rowCount ?? 100)}"
|
|
955
|
+
/>
|
|
956
|
+
</label>
|
|
957
|
+
<div class="flex items-end text-sm leading-6 text-on-surface-variant/60">
|
|
958
|
+
Create test rows for "${escapeHtml(modal.tableName ?? '')}".
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
<div class="synthetic-generator-grid custom-scrollbar">
|
|
962
|
+
<div class="synthetic-generator-grid__header">
|
|
963
|
+
<div>Column</div>
|
|
964
|
+
<div>SQLite Type</div>
|
|
965
|
+
<div>Generator</div>
|
|
966
|
+
<div>Options</div>
|
|
967
|
+
</div>
|
|
968
|
+
${
|
|
969
|
+
columns.length
|
|
970
|
+
? mappings
|
|
971
|
+
.map(mapping => {
|
|
972
|
+
const column = columns.find(item => item.name === mapping.columnName) ?? {};
|
|
973
|
+
const sqliteType = column.declaredType || column.affinity || 'ANY';
|
|
974
|
+
|
|
975
|
+
return `
|
|
976
|
+
<div class="synthetic-generator-grid__row">
|
|
977
|
+
<div class="min-w-0">
|
|
978
|
+
<div class="synthetic-generator-column-name">${escapeHtml(mapping.columnName)}</div>
|
|
979
|
+
${
|
|
980
|
+
mapping.note
|
|
981
|
+
? `<div class="synthetic-generator-column-note">${escapeHtml(mapping.note)}</div>`
|
|
982
|
+
: ''
|
|
983
|
+
}
|
|
984
|
+
</div>
|
|
985
|
+
<div class="synthetic-generator-sqlite-type">
|
|
986
|
+
${escapeHtml(sqliteType)}
|
|
987
|
+
${column.notNull ? '<span>NOT NULL</span>' : ''}
|
|
988
|
+
</div>
|
|
989
|
+
<div>
|
|
990
|
+
<select
|
|
991
|
+
class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
992
|
+
data-bind="generate-data-mapping"
|
|
993
|
+
data-column-name="${escapeHtml(mapping.columnName)}"
|
|
994
|
+
data-field="generator"
|
|
995
|
+
title="${escapeHtml(getSyntheticGeneratorLabel(mapping.generator))}"
|
|
996
|
+
>
|
|
997
|
+
${renderSyntheticGeneratorOptions(mapping.generator)}
|
|
998
|
+
</select>
|
|
999
|
+
</div>
|
|
1000
|
+
<div class="min-w-0">
|
|
1001
|
+
${renderSyntheticMappingOptions(mapping)}
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
`;
|
|
1005
|
+
})
|
|
1006
|
+
.join('')
|
|
1007
|
+
: `<div class="px-4 py-6 text-sm text-on-surface-variant/55">No writable columns available.</div>`
|
|
1008
|
+
}
|
|
1009
|
+
</div>
|
|
1010
|
+
<div class="space-y-3">
|
|
1011
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container">Preview</div>
|
|
1012
|
+
${renderSyntheticPreview(modal)}
|
|
1013
|
+
</div>
|
|
1014
|
+
${renderError(modal.error)}
|
|
1015
|
+
<div class="flex items-center justify-between gap-3 pt-2">
|
|
1016
|
+
<button class="standard-button" data-action="close-modal" type="button">Cancel</button>
|
|
1017
|
+
<div class="flex flex-wrap items-center justify-end gap-2">
|
|
1018
|
+
<button class="standard-button" data-action="preview-generate-data" type="button" ${disabledAttribute}>
|
|
1019
|
+
<span class="material-symbols-outlined text-sm">visibility</span>
|
|
1020
|
+
${modal.previewLoading ? 'Previewing...' : 'Preview'}
|
|
1021
|
+
</button>
|
|
1022
|
+
<button class="signature-button" type="submit" ${modal.submitting ? 'disabled aria-disabled="true"' : ''}>
|
|
1023
|
+
<span class="material-symbols-outlined text-sm">auto_awesome</span>
|
|
1024
|
+
${modal.submitting ? 'Inserting...' : 'Insert Rows'}
|
|
1025
|
+
</button>
|
|
1026
|
+
</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
</form>
|
|
1029
|
+
`;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
515
1032
|
function renderDeleteRowConfirmForm(modal) {
|
|
516
1033
|
const rowPreview = modal.rowPreview ?? [];
|
|
517
1034
|
const rowLabelMarkup = modal.rowLabel
|
|
@@ -1172,7 +1689,7 @@ function renderTextExportModal(modal, action) {
|
|
|
1172
1689
|
<span class="material-symbols-outlined text-xl">${escapeHtml(option.icon)}</span>
|
|
1173
1690
|
</span>
|
|
1174
1691
|
<span class="min-w-0">
|
|
1175
|
-
<span class="block truncate font-
|
|
1692
|
+
<span class="block truncate font-body text-lg font-black uppercase tracking-normal text-primary-container">
|
|
1176
1693
|
${escapeHtml(option.label)}
|
|
1177
1694
|
</span>
|
|
1178
1695
|
<span class="mt-1 block font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
@@ -1806,16 +2323,26 @@ export function renderModal(state) {
|
|
|
1806
2323
|
title: 'Delete Backup',
|
|
1807
2324
|
body: renderDeleteBackupForm(modal),
|
|
1808
2325
|
},
|
|
1809
|
-
'edit-backup
|
|
1810
|
-
eyebrow: 'Backups //
|
|
1811
|
-
title: 'Edit Backup
|
|
1812
|
-
body:
|
|
2326
|
+
'edit-backup': {
|
|
2327
|
+
eyebrow: 'Backups // Edit',
|
|
2328
|
+
title: 'Edit Backup',
|
|
2329
|
+
body: renderEditBackupForm(modal),
|
|
1813
2330
|
},
|
|
1814
2331
|
'backup-safety': {
|
|
1815
2332
|
eyebrow: 'Backups // Safety check',
|
|
1816
2333
|
title: 'Create a safety backup?',
|
|
1817
2334
|
body: renderBackupSafetyForm(modal),
|
|
1818
2335
|
},
|
|
2336
|
+
'generate-types': {
|
|
2337
|
+
eyebrow: 'Structure // Type generation',
|
|
2338
|
+
title: 'Generate Types',
|
|
2339
|
+
body: renderGenerateTypesForm(modal),
|
|
2340
|
+
},
|
|
2341
|
+
'generate-data': {
|
|
2342
|
+
eyebrow: 'Data Browser // Synthetic rows',
|
|
2343
|
+
title: 'Generate Synthetic Data',
|
|
2344
|
+
body: renderGenerateDataForm(modal),
|
|
2345
|
+
},
|
|
1819
2346
|
'edit-connection': {
|
|
1820
2347
|
eyebrow: 'Registry // Update saved SQLite target',
|
|
1821
2348
|
title: 'Edit Connection',
|
|
@@ -1904,25 +2431,32 @@ export function renderModal(state) {
|
|
|
1904
2431
|
return '';
|
|
1905
2432
|
}
|
|
1906
2433
|
|
|
2434
|
+
const modalBodyClass =
|
|
2435
|
+
modal.kind === 'generate-data'
|
|
2436
|
+
? 'app-modal-body app-modal-body--no-scroll px-6 py-6'
|
|
2437
|
+
: 'app-modal-body custom-scrollbar space-y-5 px-6 py-6';
|
|
2438
|
+
|
|
1907
2439
|
return `
|
|
1908
2440
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
|
|
1909
|
-
<div class="w-full ${
|
|
2441
|
+
<div class="app-modal-shell w-full ${
|
|
1910
2442
|
modal.kind === 'chart-editor' ||
|
|
1911
2443
|
modal.kind === 'document-insert-table' ||
|
|
1912
2444
|
modal.kind === 'document-insert-note' ||
|
|
1913
2445
|
modal.kind === 'row-update-preview' ||
|
|
1914
2446
|
modal.kind === 'table-designer-constraints'
|
|
1915
2447
|
? 'max-w-3xl'
|
|
1916
|
-
: modal.kind === '
|
|
1917
|
-
? 'max-w-
|
|
1918
|
-
: '
|
|
2448
|
+
: modal.kind === 'generate-types' || modal.kind === 'generate-data'
|
|
2449
|
+
? 'max-w-6xl'
|
|
2450
|
+
: modal.kind === 'query-export' || modal.kind === 'data-export' || modal.kind === 'backup-safety'
|
|
2451
|
+
? 'max-w-4xl'
|
|
2452
|
+
: 'max-w-xl'
|
|
1919
2453
|
} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
1920
|
-
<div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
|
|
2454
|
+
<div class="shrink-0 flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
|
|
1921
2455
|
<div>
|
|
1922
2456
|
<div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
|
|
1923
2457
|
${escapeHtml(config.eyebrow)}
|
|
1924
2458
|
</div>
|
|
1925
|
-
<h2 class="mt-2 font-
|
|
2459
|
+
<h2 class="mt-2 font-body text-3xl font-black uppercase tracking-tight text-primary-container">
|
|
1926
2460
|
${escapeHtml(config.title)}
|
|
1927
2461
|
</h2>
|
|
1928
2462
|
</div>
|
|
@@ -1934,7 +2468,7 @@ export function renderModal(state) {
|
|
|
1934
2468
|
<span class="material-symbols-outlined">close</span>
|
|
1935
2469
|
</button>
|
|
1936
2470
|
</div>
|
|
1937
|
-
<div class="
|
|
2471
|
+
<div class="${modalBodyClass}">${config.body}</div>
|
|
1938
2472
|
</div>
|
|
1939
2473
|
</div>
|
|
1940
2474
|
`;
|
|
@@ -16,7 +16,7 @@ export function renderPageHeader({ eyebrow = '', title, subtitle = '', actions =
|
|
|
16
16
|
`
|
|
17
17
|
: ''
|
|
18
18
|
}
|
|
19
|
-
<h1 class="text-5xl font-
|
|
19
|
+
<h1 class="text-5xl font-body font-bold text-[#FCE300] tracking-tighter uppercase">${escapeHtml(title)}</h1>
|
|
20
20
|
${
|
|
21
21
|
subtitle
|
|
22
22
|
? `<p class="text-xs font-mono text-on-surface/40 mt-1 uppercase tracking-widest">${escapeHtml(
|