@getdom/studio 0.1.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/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
3
|
+
import DomIconButton from '../../components/icon-button/DomIconButton.vue';
|
|
4
|
+
import DomTooltip from '../../components/tooltip/DomTooltip.vue';
|
|
5
|
+
import DomCodeBlock from '../code-block/DomCodeBlock.vue';
|
|
6
|
+
|
|
7
|
+
const copyIcon = 'M8 8h10v10H8V8Zm-4-4h10v3H7v7H4V4Z';
|
|
8
|
+
const chevronIcon = 'M9 6l6 6-6 6';
|
|
9
|
+
const modeValues = ['tree', 'code'];
|
|
10
|
+
const themeModeValues = ['inherit', 'light', 'dark'];
|
|
11
|
+
|
|
12
|
+
defineOptions({
|
|
13
|
+
__doc: {
|
|
14
|
+
name: 'JSON viewer',
|
|
15
|
+
tag: '<DomJsonViewer>',
|
|
16
|
+
description: 'A read-only JSON viewer with a collapsible tree, formatted source view, syntax highlighting, and copy support.',
|
|
17
|
+
icon: 'M5 5h5v5H5V5Zm9 0h5v5h-5V5ZM5 14h5v5H5v-5Zm9 0h5v5h-5v-5Z',
|
|
18
|
+
events: [
|
|
19
|
+
{ name: 'update:mode', payload: "'tree' | 'code'", description: 'Fired when the built-in mode toggle changes the active view.' },
|
|
20
|
+
{ name: 'copy', payload: 'string', description: 'Fired after formatted JSON is copied to the clipboard.' },
|
|
21
|
+
],
|
|
22
|
+
playground: {
|
|
23
|
+
initial: {
|
|
24
|
+
title: 'API response',
|
|
25
|
+
filename: 'response.json',
|
|
26
|
+
value: {
|
|
27
|
+
ok: true,
|
|
28
|
+
data: {
|
|
29
|
+
id: 'project_dom_studio',
|
|
30
|
+
status: 'active',
|
|
31
|
+
features: ['components', 'blocks', 'theme'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const props = defineProps({
|
|
40
|
+
value: {
|
|
41
|
+
default: null,
|
|
42
|
+
_edit: { component: 'DomJsonInput', description: 'A JSON-serializable value, or a raw JSON string when parseStrings is enabled.' },
|
|
43
|
+
},
|
|
44
|
+
title: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: '',
|
|
47
|
+
_edit: { description: 'Optional label rendered in the viewer toolbar.' },
|
|
48
|
+
},
|
|
49
|
+
description: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: '',
|
|
52
|
+
_edit: { description: 'Optional supporting text rendered below the title.' },
|
|
53
|
+
},
|
|
54
|
+
filename: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: '',
|
|
57
|
+
_edit: { description: 'Optional filename shown in the toolbar and used by the source view.' },
|
|
58
|
+
},
|
|
59
|
+
mode: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: 'tree',
|
|
62
|
+
_edit: { options: ['tree', 'code'], description: 'Initial display mode.' },
|
|
63
|
+
},
|
|
64
|
+
showModeToggle: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: true,
|
|
67
|
+
_edit: { description: 'Show the Tree / Code segmented control.' },
|
|
68
|
+
},
|
|
69
|
+
showToolbar: {
|
|
70
|
+
type: Boolean,
|
|
71
|
+
default: true,
|
|
72
|
+
_edit: { description: 'Render the viewer toolbar.' },
|
|
73
|
+
},
|
|
74
|
+
copyable: {
|
|
75
|
+
type: Boolean,
|
|
76
|
+
default: true,
|
|
77
|
+
_edit: { description: 'Show a copy button when the toolbar is visible.' },
|
|
78
|
+
},
|
|
79
|
+
parseStrings: {
|
|
80
|
+
type: Boolean,
|
|
81
|
+
default: true,
|
|
82
|
+
_edit: { description: 'Treat string values as raw JSON source and format them when they parse.' },
|
|
83
|
+
},
|
|
84
|
+
indent: {
|
|
85
|
+
type: Number,
|
|
86
|
+
default: 2,
|
|
87
|
+
_edit: { description: 'Spaces used when formatting JSON in code mode and clipboard output.' },
|
|
88
|
+
},
|
|
89
|
+
expandedDepth: {
|
|
90
|
+
type: Number,
|
|
91
|
+
default: 5,
|
|
92
|
+
_edit: { description: 'Default branch depth to expand in tree mode. Use 0 to collapse everything.' },
|
|
93
|
+
},
|
|
94
|
+
previewLines: {
|
|
95
|
+
type: Number,
|
|
96
|
+
default: 0,
|
|
97
|
+
_edit: { description: 'Maximum visible lines before the viewer scrolls. Use 0 for no cap.' },
|
|
98
|
+
},
|
|
99
|
+
density: {
|
|
100
|
+
type: String,
|
|
101
|
+
default: 'comfortable',
|
|
102
|
+
_edit: { options: ['compact', 'comfortable'], description: 'Tree row spacing.' },
|
|
103
|
+
},
|
|
104
|
+
framed: {
|
|
105
|
+
type: Boolean,
|
|
106
|
+
default: true,
|
|
107
|
+
_edit: { description: 'Render the standard card frame around the viewer.' },
|
|
108
|
+
},
|
|
109
|
+
themeMode: {
|
|
110
|
+
type: String,
|
|
111
|
+
default: 'inherit',
|
|
112
|
+
_edit: {
|
|
113
|
+
options: ['inherit', 'light', 'dark'],
|
|
114
|
+
description: 'Force a light or dark viewer theme, or inherit the surrounding application theme.',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
emptyText: {
|
|
118
|
+
type: String,
|
|
119
|
+
default: 'No JSON value.',
|
|
120
|
+
_edit: { description: 'Message shown when the supplied value is empty.' },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const emit = defineEmits(['update:mode', 'copy']);
|
|
125
|
+
const internalMode = ref(normalizeMode(props.mode));
|
|
126
|
+
const expandedPaths = ref(new Set());
|
|
127
|
+
const copyStatus = ref('idle');
|
|
128
|
+
let copyTimer = 0;
|
|
129
|
+
|
|
130
|
+
const requestedMode = computed(resolveRequestedMode);
|
|
131
|
+
const viewerState = computed(resolveViewerState);
|
|
132
|
+
const formattedCode = computed(resolveFormattedCode);
|
|
133
|
+
const expansionSeedKey = computed(resolveExpansionSeedKey);
|
|
134
|
+
const treeRows = computed(resolveTreeRows);
|
|
135
|
+
const bodyStyle = computed(resolveBodyStyle);
|
|
136
|
+
const frameClasses = computed(resolveFrameClasses);
|
|
137
|
+
const resolvedThemeMode = computed(resolveThemeMode);
|
|
138
|
+
const showToolbarContent = computed(resolveShowToolbarContent);
|
|
139
|
+
const displayTitle = computed(resolveDisplayTitle);
|
|
140
|
+
const copyLabel = computed(resolveCopyLabel);
|
|
141
|
+
|
|
142
|
+
watch(requestedMode, syncRequestedMode, { immediate: true });
|
|
143
|
+
watch(expansionSeedKey, seedDefaultExpandedPaths, { immediate: true });
|
|
144
|
+
|
|
145
|
+
onBeforeUnmount(clearCopyTimer);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolves the normalized mode requested by the public prop.
|
|
149
|
+
*
|
|
150
|
+
* @returns {'tree' | 'code'} Supported display mode.
|
|
151
|
+
*/
|
|
152
|
+
function resolveRequestedMode() {
|
|
153
|
+
return normalizeMode(props.mode);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates the parsed value and formatted source used by both viewer modes.
|
|
158
|
+
*
|
|
159
|
+
* @returns {{ value: unknown, code: string, invalid: boolean, error: string }} Normalized viewer state.
|
|
160
|
+
*/
|
|
161
|
+
function resolveViewerState() {
|
|
162
|
+
return normalizeSourceValue(props.value, props.parseStrings, props.indent);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Returns the formatted source currently shown in code mode and copied to the clipboard.
|
|
167
|
+
*
|
|
168
|
+
* @returns {string} Display-ready JSON source.
|
|
169
|
+
*/
|
|
170
|
+
function resolveFormattedCode() {
|
|
171
|
+
return viewerState.value.code;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Builds a stable key that reseeds branch expansion when the input or default depth changes.
|
|
176
|
+
*
|
|
177
|
+
* @returns {string} Expansion seed key.
|
|
178
|
+
*/
|
|
179
|
+
function resolveExpansionSeedKey() {
|
|
180
|
+
return `${props.expandedDepth}:${formattedCode.value}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Flattens the parsed JSON tree into renderable rows.
|
|
185
|
+
*
|
|
186
|
+
* @returns {Array<object>} Rows for tree mode.
|
|
187
|
+
*/
|
|
188
|
+
function resolveTreeRows() {
|
|
189
|
+
return flattenJsonRows(viewerState.value.value, expandedPaths.value);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Calculates the scroll clamp for long tree views.
|
|
194
|
+
*
|
|
195
|
+
* @returns {Record<string, string>} Inline style object.
|
|
196
|
+
*/
|
|
197
|
+
function resolveBodyStyle() {
|
|
198
|
+
if (!props.previewLines) return {};
|
|
199
|
+
const lineHeight = props.density === 'compact' ? 20 : 22;
|
|
200
|
+
return { maxHeight: `${Math.round((props.previewLines * lineHeight) + 24)}px` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolves the wrapper frame classes from the framed prop.
|
|
205
|
+
*
|
|
206
|
+
* @returns {Array<string>} Tailwind class list.
|
|
207
|
+
*/
|
|
208
|
+
function resolveFrameClasses() {
|
|
209
|
+
return [
|
|
210
|
+
'w-full min-w-0 overflow-hidden',
|
|
211
|
+
props.framed ? 'rounded-xl border border-border skin-card' : '',
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Normalizes the requested local theme mode.
|
|
217
|
+
*
|
|
218
|
+
* @returns {'inherit' | 'light' | 'dark'} Supported local theme mode.
|
|
219
|
+
*/
|
|
220
|
+
function resolveThemeMode() {
|
|
221
|
+
return themeModeValues.includes(props.themeMode) ? props.themeMode : 'inherit';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Determines whether the toolbar has anything useful to render.
|
|
226
|
+
*
|
|
227
|
+
* @returns {boolean} True when toolbar content should be visible.
|
|
228
|
+
*/
|
|
229
|
+
function resolveShowToolbarContent() {
|
|
230
|
+
return Boolean(props.showToolbar && (displayTitle.value || props.description || props.showModeToggle || props.copyable));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Chooses the most useful toolbar title.
|
|
235
|
+
*
|
|
236
|
+
* @returns {string} Title text.
|
|
237
|
+
*/
|
|
238
|
+
function resolveDisplayTitle() {
|
|
239
|
+
return props.title || props.filename;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolves the current accessible label for the copy control.
|
|
244
|
+
*
|
|
245
|
+
* @returns {string} Copy button label.
|
|
246
|
+
*/
|
|
247
|
+
function resolveCopyLabel() {
|
|
248
|
+
if (copyStatus.value === 'copied') return 'Copied JSON';
|
|
249
|
+
if (copyStatus.value === 'failed') return 'Copy failed';
|
|
250
|
+
return 'Copy JSON';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Normalizes unknown mode values to a supported display mode.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} mode Candidate mode.
|
|
257
|
+
* @returns {'tree' | 'code'} Supported mode.
|
|
258
|
+
*/
|
|
259
|
+
function normalizeMode(mode) {
|
|
260
|
+
return modeValues.includes(mode) ? mode : 'tree';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Synchronizes internal mode when the controlled prop changes.
|
|
265
|
+
*
|
|
266
|
+
* @param {'tree' | 'code'} mode Normalized mode.
|
|
267
|
+
* @returns {void}
|
|
268
|
+
*/
|
|
269
|
+
function syncRequestedMode(mode) {
|
|
270
|
+
internalMode.value = mode;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Sets the active display mode and notifies parent components.
|
|
275
|
+
*
|
|
276
|
+
* @param {'tree' | 'code'} mode Next mode.
|
|
277
|
+
* @returns {void}
|
|
278
|
+
*/
|
|
279
|
+
function setMode(mode) {
|
|
280
|
+
const normalized = normalizeMode(mode);
|
|
281
|
+
internalMode.value = normalized;
|
|
282
|
+
emit('update:mode', normalized);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resets expanded branches from the current value and expanded-depth prop.
|
|
287
|
+
*
|
|
288
|
+
* @returns {void}
|
|
289
|
+
*/
|
|
290
|
+
function seedDefaultExpandedPaths() {
|
|
291
|
+
const paths = new Set();
|
|
292
|
+
collectDefaultExpandedPaths(viewerState.value.value, paths, {
|
|
293
|
+
depth: 0,
|
|
294
|
+
maxDepth: Math.max(0, Number(props.expandedDepth) || 0),
|
|
295
|
+
path: '$',
|
|
296
|
+
seen: new WeakSet(),
|
|
297
|
+
});
|
|
298
|
+
expandedPaths.value = paths;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Toggles a branch row open or closed.
|
|
303
|
+
*
|
|
304
|
+
* @param {object} row Rendered branch row.
|
|
305
|
+
* @returns {void}
|
|
306
|
+
*/
|
|
307
|
+
function toggleRow(row) {
|
|
308
|
+
if (!row?.toggleable) return;
|
|
309
|
+
const next = new Set(expandedPaths.value);
|
|
310
|
+
if (next.has(row.path)) next.delete(row.path);
|
|
311
|
+
else next.add(row.path);
|
|
312
|
+
expandedPaths.value = next;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Copies the formatted JSON source to the user's clipboard.
|
|
317
|
+
*
|
|
318
|
+
* @returns {Promise<void>} Resolves once the copy attempt has completed.
|
|
319
|
+
*/
|
|
320
|
+
async function copyJson() {
|
|
321
|
+
if (!props.copyable) return;
|
|
322
|
+
try {
|
|
323
|
+
await writeClipboardText(formattedCode.value);
|
|
324
|
+
markCopyStatus('copied');
|
|
325
|
+
emit('copy', formattedCode.value);
|
|
326
|
+
} catch {
|
|
327
|
+
markCopyStatus('failed');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Updates the transient copy status and clears it after a short delay.
|
|
333
|
+
*
|
|
334
|
+
* @param {'idle' | 'copied' | 'failed'} status Copy status.
|
|
335
|
+
* @returns {void}
|
|
336
|
+
*/
|
|
337
|
+
function markCopyStatus(status) {
|
|
338
|
+
clearCopyTimer();
|
|
339
|
+
copyStatus.value = status;
|
|
340
|
+
if (typeof window === 'undefined') return;
|
|
341
|
+
copyTimer = window.setTimeout(resetCopyStatus, 1400);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Clears any pending copy-status timeout.
|
|
346
|
+
*
|
|
347
|
+
* @returns {void}
|
|
348
|
+
*/
|
|
349
|
+
function clearCopyTimer() {
|
|
350
|
+
if (!copyTimer || typeof window === 'undefined') return;
|
|
351
|
+
window.clearTimeout(copyTimer);
|
|
352
|
+
copyTimer = 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Returns the copy affordance to its idle state after transient feedback.
|
|
357
|
+
*
|
|
358
|
+
* @returns {void}
|
|
359
|
+
*/
|
|
360
|
+
function resetCopyStatus() {
|
|
361
|
+
copyStatus.value = 'idle';
|
|
362
|
+
copyTimer = 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Writes text to the clipboard, using a textarea fallback for older browsers.
|
|
367
|
+
*
|
|
368
|
+
* @param {string} text Text to copy.
|
|
369
|
+
* @returns {Promise<void>} Resolves when the text is copied.
|
|
370
|
+
*/
|
|
371
|
+
async function writeClipboardText(text) {
|
|
372
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
373
|
+
await navigator.clipboard.writeText(text);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
copyWithTextarea(text);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Copies text through a temporary textarea for browsers without Clipboard API support.
|
|
381
|
+
*
|
|
382
|
+
* @param {string} text Text to copy.
|
|
383
|
+
* @returns {void}
|
|
384
|
+
*/
|
|
385
|
+
function copyWithTextarea(text) {
|
|
386
|
+
if (typeof document === 'undefined') throw new Error('Clipboard is unavailable.');
|
|
387
|
+
const textarea = document.createElement('textarea');
|
|
388
|
+
textarea.value = text;
|
|
389
|
+
textarea.setAttribute('readonly', '');
|
|
390
|
+
textarea.style.position = 'fixed';
|
|
391
|
+
textarea.style.inset = '0 auto auto -9999px';
|
|
392
|
+
document.body.appendChild(textarea);
|
|
393
|
+
textarea.select();
|
|
394
|
+
const copied = document.execCommand('copy');
|
|
395
|
+
document.body.removeChild(textarea);
|
|
396
|
+
if (!copied) throw new Error('Copy command failed.');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Normalizes any supported input into canonical JSON-ish state.
|
|
401
|
+
*
|
|
402
|
+
* @param {unknown} source Source value or raw JSON text.
|
|
403
|
+
* @param {boolean} parseStrings Whether strings should be parsed as JSON.
|
|
404
|
+
* @param {number} indent Formatting indentation.
|
|
405
|
+
* @returns {{ value: unknown, code: string, invalid: boolean, error: string }} Normalized state.
|
|
406
|
+
*/
|
|
407
|
+
function normalizeSourceValue(source, parseStrings, indent) {
|
|
408
|
+
if (typeof source === 'string' && parseStrings) {
|
|
409
|
+
return normalizeRawJsonString(source, indent);
|
|
410
|
+
}
|
|
411
|
+
return normalizeSerializableValue(source, indent);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Parses and formats raw JSON text while preserving invalid source for diagnosis.
|
|
416
|
+
*
|
|
417
|
+
* @param {string} source Raw JSON source.
|
|
418
|
+
* @param {number} indent Formatting indentation.
|
|
419
|
+
* @returns {{ value: unknown, code: string, invalid: boolean, error: string }} Normalized state.
|
|
420
|
+
*/
|
|
421
|
+
function normalizeRawJsonString(source, indent) {
|
|
422
|
+
if (!source.trim()) {
|
|
423
|
+
return { value: null, code: '', invalid: false, error: '' };
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
const parsed = JSON.parse(source);
|
|
427
|
+
return normalizeSerializableValue(parsed, indent);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
return {
|
|
430
|
+
value: source,
|
|
431
|
+
code: source,
|
|
432
|
+
invalid: true,
|
|
433
|
+
error: error?.message || 'Invalid JSON.',
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Formats a JSON-serializable value and parses it back into canonical tree data.
|
|
440
|
+
*
|
|
441
|
+
* @param {unknown} source Source value.
|
|
442
|
+
* @param {number} indent Formatting indentation.
|
|
443
|
+
* @returns {{ value: unknown, code: string, invalid: boolean, error: string }} Normalized state.
|
|
444
|
+
*/
|
|
445
|
+
function normalizeSerializableValue(source, indent) {
|
|
446
|
+
const code = stringifyJson(source, indent);
|
|
447
|
+
try {
|
|
448
|
+
return {
|
|
449
|
+
value: code ? JSON.parse(code) : null,
|
|
450
|
+
code,
|
|
451
|
+
invalid: false,
|
|
452
|
+
error: '',
|
|
453
|
+
};
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return {
|
|
456
|
+
value: source == null ? null : String(source),
|
|
457
|
+
code: source == null ? '' : String(source),
|
|
458
|
+
invalid: true,
|
|
459
|
+
error: error?.message || 'Value could not be serialized as JSON.',
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Safely stringifies values that may contain unsupported JSON data.
|
|
466
|
+
*
|
|
467
|
+
* @param {unknown} value Source value.
|
|
468
|
+
* @param {number} indent Formatting indentation.
|
|
469
|
+
* @returns {string} JSON source.
|
|
470
|
+
*/
|
|
471
|
+
function stringifyJson(value, indent) {
|
|
472
|
+
if (value === undefined) return '';
|
|
473
|
+
const json = JSON.stringify(value, createJsonReplacer(), clampIndent(indent));
|
|
474
|
+
return json == null ? '' : json;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Limits JSON indentation to a readable browser-safe range.
|
|
479
|
+
*
|
|
480
|
+
* @param {number} indent Candidate indentation.
|
|
481
|
+
* @returns {number} Clamped indentation.
|
|
482
|
+
*/
|
|
483
|
+
function clampIndent(indent) {
|
|
484
|
+
const numeric = Number(indent);
|
|
485
|
+
if (!Number.isFinite(numeric)) return 2;
|
|
486
|
+
return Math.min(8, Math.max(0, Math.round(numeric)));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Creates a JSON replacer that protects the viewer from circular or unsupported values.
|
|
491
|
+
*
|
|
492
|
+
* @returns {(key: string, value: unknown) => unknown} JSON.stringify replacer.
|
|
493
|
+
*/
|
|
494
|
+
function createJsonReplacer() {
|
|
495
|
+
const seen = new WeakSet();
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Normalizes edge-case values while JSON.stringify walks the tree.
|
|
499
|
+
*
|
|
500
|
+
* @param {string} key Object key supplied by JSON.stringify.
|
|
501
|
+
* @param {unknown} value Candidate value.
|
|
502
|
+
* @returns {unknown} JSON-safe replacement value.
|
|
503
|
+
*/
|
|
504
|
+
return function jsonReplacer(key, value) {
|
|
505
|
+
void key;
|
|
506
|
+
if (typeof value === 'bigint') return value.toString();
|
|
507
|
+
if (typeof value === 'function') return `[Function${value.name ? ` ${value.name}` : ''}]`;
|
|
508
|
+
if (typeof value === 'symbol') return value.toString();
|
|
509
|
+
if (!value || typeof value !== 'object') return value;
|
|
510
|
+
if (seen.has(value)) return '[Circular]';
|
|
511
|
+
seen.add(value);
|
|
512
|
+
return value;
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Builds a flat row list for a JSON value.
|
|
518
|
+
*
|
|
519
|
+
* @param {unknown} value Root JSON value.
|
|
520
|
+
* @param {Set<string>} expanded Expanded branch paths.
|
|
521
|
+
* @returns {Array<object>} Renderable rows.
|
|
522
|
+
*/
|
|
523
|
+
function flattenJsonRows(value, expanded) {
|
|
524
|
+
if (value == null && !formattedCode.value) {
|
|
525
|
+
return [{ type: 'empty', id: '$empty', depth: 0, text: props.emptyText }];
|
|
526
|
+
}
|
|
527
|
+
const rows = [];
|
|
528
|
+
appendValueRows(rows, {
|
|
529
|
+
value,
|
|
530
|
+
path: '$',
|
|
531
|
+
depth: 0,
|
|
532
|
+
propertyKey: '',
|
|
533
|
+
parentKind: '',
|
|
534
|
+
trailing: false,
|
|
535
|
+
}, expanded);
|
|
536
|
+
return rows;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Appends tree rows for any JSON value.
|
|
541
|
+
*
|
|
542
|
+
* @param {Array<object>} rows Mutable output rows.
|
|
543
|
+
* @param {object} context Value render context.
|
|
544
|
+
* @param {Set<string>} expanded Expanded branch paths.
|
|
545
|
+
* @returns {void}
|
|
546
|
+
*/
|
|
547
|
+
function appendValueRows(rows, context, expanded) {
|
|
548
|
+
const kind = jsonKind(context.value);
|
|
549
|
+
if (kind === 'array' || kind === 'object') {
|
|
550
|
+
appendBranchRows(rows, context, expanded, kind);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
rows.push({
|
|
554
|
+
type: 'leaf',
|
|
555
|
+
id: context.path,
|
|
556
|
+
depth: context.depth,
|
|
557
|
+
propertyKey: context.parentKind === 'object' ? context.propertyKey : '',
|
|
558
|
+
valueKind: kind,
|
|
559
|
+
value: scalarDisplayValue(context.value, kind),
|
|
560
|
+
trailing: context.trailing,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Appends expandable rows for objects and arrays.
|
|
566
|
+
*
|
|
567
|
+
* @param {Array<object>} rows Mutable output rows.
|
|
568
|
+
* @param {object} context Value render context.
|
|
569
|
+
* @param {Set<string>} expanded Expanded branch paths.
|
|
570
|
+
* @param {'array' | 'object'} kind Branch kind.
|
|
571
|
+
* @returns {void}
|
|
572
|
+
*/
|
|
573
|
+
function appendBranchRows(rows, context, expanded, kind) {
|
|
574
|
+
const entries = branchEntries(context.value, kind, context.path);
|
|
575
|
+
const openToken = kind === 'array' ? '[' : '{';
|
|
576
|
+
const closeToken = kind === 'array' ? ']' : '}';
|
|
577
|
+
const propertyKey = context.parentKind === 'object' ? context.propertyKey : '';
|
|
578
|
+
|
|
579
|
+
if (!entries.length) {
|
|
580
|
+
rows.push({
|
|
581
|
+
type: 'leaf',
|
|
582
|
+
id: context.path,
|
|
583
|
+
depth: context.depth,
|
|
584
|
+
propertyKey,
|
|
585
|
+
valueKind: kind,
|
|
586
|
+
value: `${openToken}${closeToken}`,
|
|
587
|
+
trailing: context.trailing,
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const isExpanded = expanded.has(context.path);
|
|
593
|
+
rows.push({
|
|
594
|
+
type: 'branch',
|
|
595
|
+
id: `${context.path}:open`,
|
|
596
|
+
path: context.path,
|
|
597
|
+
depth: context.depth,
|
|
598
|
+
propertyKey,
|
|
599
|
+
openToken,
|
|
600
|
+
closeToken,
|
|
601
|
+
expanded: isExpanded,
|
|
602
|
+
toggleable: true,
|
|
603
|
+
summary: branchSummary(entries.length, kind),
|
|
604
|
+
trailing: context.trailing,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
if (!isExpanded) return;
|
|
608
|
+
|
|
609
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
610
|
+
const entry = entries[index];
|
|
611
|
+
appendValueRows(rows, {
|
|
612
|
+
value: entry.value,
|
|
613
|
+
path: entry.path,
|
|
614
|
+
depth: context.depth + 1,
|
|
615
|
+
propertyKey: entry.key,
|
|
616
|
+
parentKind: kind,
|
|
617
|
+
trailing: index < entries.length - 1,
|
|
618
|
+
}, expanded);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
rows.push({
|
|
622
|
+
type: 'closing',
|
|
623
|
+
id: `${context.path}:close`,
|
|
624
|
+
depth: context.depth,
|
|
625
|
+
closeToken,
|
|
626
|
+
trailing: context.trailing,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Collects branch paths that should be open by default.
|
|
632
|
+
*
|
|
633
|
+
* @param {unknown} value Current JSON value.
|
|
634
|
+
* @param {Set<string>} paths Mutable path set.
|
|
635
|
+
* @param {{ depth: number, maxDepth: number, path: string, seen: WeakSet<object> }} options Traversal options.
|
|
636
|
+
* @returns {void}
|
|
637
|
+
*/
|
|
638
|
+
function collectDefaultExpandedPaths(value, paths, options) {
|
|
639
|
+
if (!isBranchValue(value)) return;
|
|
640
|
+
if (options.seen.has(value)) return;
|
|
641
|
+
options.seen.add(value);
|
|
642
|
+
if (options.depth >= options.maxDepth) return;
|
|
643
|
+
paths.add(options.path);
|
|
644
|
+
const kind = Array.isArray(value) ? 'array' : 'object';
|
|
645
|
+
const entries = branchEntries(value, kind, options.path);
|
|
646
|
+
for (const entry of entries) {
|
|
647
|
+
collectDefaultExpandedPaths(entry.value, paths, {
|
|
648
|
+
depth: options.depth + 1,
|
|
649
|
+
maxDepth: options.maxDepth,
|
|
650
|
+
path: entry.path,
|
|
651
|
+
seen: options.seen,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Returns true when a value has nested JSON children.
|
|
658
|
+
*
|
|
659
|
+
* @param {unknown} value Candidate value.
|
|
660
|
+
* @returns {boolean} True for arrays and plain objects.
|
|
661
|
+
*/
|
|
662
|
+
function isBranchValue(value) {
|
|
663
|
+
return Array.isArray(value) || Boolean(value && typeof value === 'object');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Resolves the viewer token kind for a JSON value.
|
|
668
|
+
*
|
|
669
|
+
* @param {unknown} value Candidate value.
|
|
670
|
+
* @returns {'array' | 'object' | 'string' | 'number' | 'boolean' | 'null'} Token kind.
|
|
671
|
+
*/
|
|
672
|
+
function jsonKind(value) {
|
|
673
|
+
if (Array.isArray(value)) return 'array';
|
|
674
|
+
if (value === null) return 'null';
|
|
675
|
+
if (typeof value === 'object') return 'object';
|
|
676
|
+
if (typeof value === 'number') return 'number';
|
|
677
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
678
|
+
return 'string';
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Builds child entries for an object or array branch.
|
|
683
|
+
*
|
|
684
|
+
* @param {unknown} value Branch value.
|
|
685
|
+
* @param {'array' | 'object'} kind Branch kind.
|
|
686
|
+
* @param {string} parentPath Stable parent path.
|
|
687
|
+
* @returns {Array<{ key: string, path: string, value: unknown }>} Child entries.
|
|
688
|
+
*/
|
|
689
|
+
function branchEntries(value, kind, parentPath) {
|
|
690
|
+
const entries = [];
|
|
691
|
+
if (kind === 'array') {
|
|
692
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
693
|
+
entries.push({
|
|
694
|
+
key: String(index),
|
|
695
|
+
path: `${parentPath}[${index}]`,
|
|
696
|
+
value: value[index],
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
return entries;
|
|
700
|
+
}
|
|
701
|
+
for (const [key, item] of Object.entries(value)) {
|
|
702
|
+
entries.push({
|
|
703
|
+
key,
|
|
704
|
+
path: jsonChildPath(parentPath, key),
|
|
705
|
+
value: item,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return entries;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Creates a child path from a parent path and property key.
|
|
713
|
+
*
|
|
714
|
+
* @param {string} parent Parent path.
|
|
715
|
+
* @param {string} key Property key.
|
|
716
|
+
* @returns {string} Stable child path.
|
|
717
|
+
*/
|
|
718
|
+
function jsonChildPath(parent, key) {
|
|
719
|
+
return `${parent}.${encodeURIComponent(key)}`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Builds the collapsed branch summary text.
|
|
724
|
+
*
|
|
725
|
+
* @param {number} count Child count.
|
|
726
|
+
* @param {'array' | 'object'} kind Branch kind.
|
|
727
|
+
* @returns {string} Summary label.
|
|
728
|
+
*/
|
|
729
|
+
function branchSummary(count, kind) {
|
|
730
|
+
const noun = kind === 'array' ? 'item' : 'key';
|
|
731
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Formats scalar values as JSON tokens.
|
|
736
|
+
*
|
|
737
|
+
* @param {unknown} value Scalar value.
|
|
738
|
+
* @param {string} kind Token kind.
|
|
739
|
+
* @returns {string} Display text.
|
|
740
|
+
*/
|
|
741
|
+
function scalarDisplayValue(value, kind) {
|
|
742
|
+
if (kind === 'string') return JSON.stringify(value);
|
|
743
|
+
if (kind === 'null') return 'null';
|
|
744
|
+
return String(value);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Resolves token color classes for syntax-like tree rendering.
|
|
749
|
+
*
|
|
750
|
+
* @param {string} kind Token kind.
|
|
751
|
+
* @returns {string} Syntax token class string.
|
|
752
|
+
*/
|
|
753
|
+
function tokenClass(kind) {
|
|
754
|
+
if (kind === 'string') return 'dom-json-viewer__token-string';
|
|
755
|
+
if (kind === 'number') return 'dom-json-viewer__token-number';
|
|
756
|
+
if (kind === 'boolean') return 'dom-json-viewer__token-boolean';
|
|
757
|
+
if (kind === 'null') return 'dom-json-viewer__token-null';
|
|
758
|
+
if (kind === 'key') return 'dom-json-viewer__token-key';
|
|
759
|
+
if (kind === 'punctuation') return 'dom-json-viewer__token-punctuation';
|
|
760
|
+
if (kind === 'muted') return 'dom-json-viewer__token-muted';
|
|
761
|
+
return 'dom-json-viewer__token-text';
|
|
762
|
+
}
|
|
763
|
+
</script>
|
|
764
|
+
|
|
765
|
+
<template>
|
|
766
|
+
<div
|
|
767
|
+
class="dom-json-viewer"
|
|
768
|
+
:class="frameClasses"
|
|
769
|
+
:data-theme="resolvedThemeMode === 'inherit' ? undefined : resolvedThemeMode"
|
|
770
|
+
>
|
|
771
|
+
<div
|
|
772
|
+
v-if="showToolbarContent"
|
|
773
|
+
class="flex flex-col gap-3 border-b border-border bg-secondary/45 px-2 py-2 sm:flex-row sm:items-center sm:justify-between"
|
|
774
|
+
>
|
|
775
|
+
<div v-if="displayTitle || description" class="min-w-0">
|
|
776
|
+
<p v-if="displayTitle" class="truncate text-sm font-semibold text-canvas-fg">{{ displayTitle }}</p>
|
|
777
|
+
<p v-if="description" class="mt-0.5 text-xs leading-5 text-muted-fg">{{ description }}</p>
|
|
778
|
+
</div>
|
|
779
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
780
|
+
<div
|
|
781
|
+
v-if="showModeToggle"
|
|
782
|
+
class="inline-flex rounded-lg border border-border bg-canvas p-0.5"
|
|
783
|
+
aria-label="JSON viewer mode"
|
|
784
|
+
>
|
|
785
|
+
<button
|
|
786
|
+
type="button"
|
|
787
|
+
class="rounded-md px-2.5 py-1 text-xs font-medium transition"
|
|
788
|
+
:class="internalMode === 'tree' ? 'bg-secondary text-canvas-fg shadow-sm' : 'text-muted-fg hover:text-canvas-fg'"
|
|
789
|
+
:aria-pressed="internalMode === 'tree'"
|
|
790
|
+
@click="setMode('tree')"
|
|
791
|
+
>
|
|
792
|
+
Tree
|
|
793
|
+
</button>
|
|
794
|
+
<button
|
|
795
|
+
type="button"
|
|
796
|
+
class="rounded-md px-2.5 py-1 text-xs font-medium transition"
|
|
797
|
+
:class="internalMode === 'code' ? 'bg-secondary text-canvas-fg shadow-sm' : 'text-muted-fg hover:text-canvas-fg'"
|
|
798
|
+
:aria-pressed="internalMode === 'code'"
|
|
799
|
+
@click="setMode('code')"
|
|
800
|
+
>
|
|
801
|
+
Code
|
|
802
|
+
</button>
|
|
803
|
+
</div>
|
|
804
|
+
<DomTooltip v-if="copyable" :text="copyLabel">
|
|
805
|
+
<DomIconButton
|
|
806
|
+
:label="copyLabel"
|
|
807
|
+
:icon="copyIcon"
|
|
808
|
+
size="sm"
|
|
809
|
+
variant="ghost"
|
|
810
|
+
:active="copyStatus === 'copied'"
|
|
811
|
+
@click="copyJson"
|
|
812
|
+
/>
|
|
813
|
+
</DomTooltip>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
817
|
+
<div
|
|
818
|
+
v-if="viewerState.invalid"
|
|
819
|
+
class="border-b border-destructive/25 bg-destructive/10 px-3 py-2 text-xs leading-5 text-destructive"
|
|
820
|
+
>
|
|
821
|
+
{{ viewerState.error }}
|
|
822
|
+
</div>
|
|
823
|
+
|
|
824
|
+
<div class="dom-json-viewer__body min-w-0 overflow-auto" :style="bodyStyle">
|
|
825
|
+
<DomCodeBlock
|
|
826
|
+
v-if="internalMode === 'code'"
|
|
827
|
+
:code="formattedCode"
|
|
828
|
+
:filename="filename"
|
|
829
|
+
:preview-lines="0"
|
|
830
|
+
:framed="false"
|
|
831
|
+
:theme-mode="resolvedThemeMode"
|
|
832
|
+
lang="json"
|
|
833
|
+
/>
|
|
834
|
+
|
|
835
|
+
<div
|
|
836
|
+
v-else
|
|
837
|
+
class="dom-json-viewer__tree min-w-max py-3 font-mono text-[12.5px]"
|
|
838
|
+
:class="density === 'compact' ? 'leading-5' : 'leading-[1.375rem]'"
|
|
839
|
+
>
|
|
840
|
+
<div
|
|
841
|
+
v-for="row in treeRows"
|
|
842
|
+
:key="row.id"
|
|
843
|
+
class="group flex items-start whitespace-pre px-3"
|
|
844
|
+
:class="row.type === 'empty' ? tokenClass('muted') : 'hover:bg-secondary/60'"
|
|
845
|
+
>
|
|
846
|
+
<div
|
|
847
|
+
class="flex min-w-0 items-start"
|
|
848
|
+
:style="{ paddingLeft: `${row.depth * 1.125}rem` }"
|
|
849
|
+
>
|
|
850
|
+
<button
|
|
851
|
+
v-if="row.toggleable"
|
|
852
|
+
type="button"
|
|
853
|
+
class="-ml-1 mr-1 mt-[0.125rem] grid size-4 shrink-0 place-items-center rounded text-muted-fg transition hover:bg-secondary hover:text-canvas-fg"
|
|
854
|
+
:aria-label="row.expanded ? 'Collapse JSON branch' : 'Expand JSON branch'"
|
|
855
|
+
@click="toggleRow(row)"
|
|
856
|
+
>
|
|
857
|
+
<svg
|
|
858
|
+
viewBox="0 0 24 24"
|
|
859
|
+
class="size-3 transition-transform"
|
|
860
|
+
:class="row.expanded ? 'rotate-90' : ''"
|
|
861
|
+
fill="none"
|
|
862
|
+
aria-hidden="true"
|
|
863
|
+
>
|
|
864
|
+
<path :d="chevronIcon" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
865
|
+
</svg>
|
|
866
|
+
</button>
|
|
867
|
+
<span v-else class="mr-1 size-4 shrink-0"></span>
|
|
868
|
+
|
|
869
|
+
<span v-if="row.type === 'empty'">{{ row.text }}</span>
|
|
870
|
+
|
|
871
|
+
<template v-else-if="row.type === 'closing'">
|
|
872
|
+
<span :class="tokenClass('punctuation')">{{ row.closeToken }}</span>
|
|
873
|
+
<span v-if="row.trailing" :class="tokenClass('punctuation')">,</span>
|
|
874
|
+
</template>
|
|
875
|
+
|
|
876
|
+
<template v-else>
|
|
877
|
+
<span v-if="row.propertyKey" :class="tokenClass('key')">{{ JSON.stringify(row.propertyKey) }}</span>
|
|
878
|
+
<span v-if="row.propertyKey" class="mr-1" :class="tokenClass('punctuation')">:</span>
|
|
879
|
+
<template v-if="row.type === 'branch'">
|
|
880
|
+
<span :class="tokenClass('punctuation')">{{ row.openToken }}</span>
|
|
881
|
+
<template v-if="!row.expanded">
|
|
882
|
+
<span class="px-1" :class="tokenClass('muted')">...</span>
|
|
883
|
+
<span :class="tokenClass('punctuation')">{{ row.closeToken }}</span>
|
|
884
|
+
<span v-if="row.trailing" :class="tokenClass('punctuation')">,</span>
|
|
885
|
+
<span class="ml-2 text-[11px]" :class="tokenClass('muted')">{{ row.summary }}</span>
|
|
886
|
+
</template>
|
|
887
|
+
</template>
|
|
888
|
+
<template v-else>
|
|
889
|
+
<span :class="tokenClass(row.valueKind)">{{ row.value }}</span>
|
|
890
|
+
<span v-if="row.trailing" :class="tokenClass('punctuation')">,</span>
|
|
891
|
+
</template>
|
|
892
|
+
</template>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
</template>
|
|
899
|
+
|
|
900
|
+
<style scoped>
|
|
901
|
+
.dom-json-viewer {
|
|
902
|
+
--dom-json-token-background: #ffffff;
|
|
903
|
+
--dom-json-token-text: #000000;
|
|
904
|
+
--dom-json-token-key: #0451a5;
|
|
905
|
+
--dom-json-token-string: #a31515;
|
|
906
|
+
--dom-json-token-number: #098658;
|
|
907
|
+
--dom-json-token-boolean: #0000ff;
|
|
908
|
+
--dom-json-token-null: #0000ff;
|
|
909
|
+
--dom-json-token-punctuation: #000000;
|
|
910
|
+
--dom-json-token-muted: #6a737d;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
:global([data-theme="dark"] .dom-json-viewer:not([data-theme="light"])),
|
|
914
|
+
.dom-json-viewer[data-theme="dark"] {
|
|
915
|
+
--dom-json-token-background: #1e1e1e;
|
|
916
|
+
--dom-json-token-text: #d4d4d4;
|
|
917
|
+
--dom-json-token-key: #9cdcfe;
|
|
918
|
+
--dom-json-token-string: #ce9178;
|
|
919
|
+
--dom-json-token-number: #b5cea8;
|
|
920
|
+
--dom-json-token-boolean: #569cd6;
|
|
921
|
+
--dom-json-token-null: #569cd6;
|
|
922
|
+
--dom-json-token-punctuation: #d4d4d4;
|
|
923
|
+
--dom-json-token-muted: #858585;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.dom-json-viewer__body {
|
|
927
|
+
background-color: var(--dom-json-token-background);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.dom-json-viewer__tree,
|
|
931
|
+
.dom-json-viewer__token-text {
|
|
932
|
+
color: var(--dom-json-token-text);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.dom-json-viewer__token-key {
|
|
936
|
+
color: var(--dom-json-token-key);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.dom-json-viewer__token-string {
|
|
940
|
+
color: var(--dom-json-token-string);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.dom-json-viewer__token-number {
|
|
944
|
+
color: var(--dom-json-token-number);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.dom-json-viewer__token-boolean {
|
|
948
|
+
color: var(--dom-json-token-boolean);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.dom-json-viewer__token-null {
|
|
952
|
+
color: var(--dom-json-token-null);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.dom-json-viewer__token-punctuation {
|
|
956
|
+
color: var(--dom-json-token-punctuation);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.dom-json-viewer__token-muted {
|
|
960
|
+
color: var(--dom-json-token-muted);
|
|
961
|
+
}
|
|
962
|
+
</style>
|