@djangocfg/ui-tools 2.1.417 → 2.1.419
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/dist/audio-player/index.cjs +1 -2
- package/dist/audio-player/index.cjs.map +1 -1
- package/dist/audio-player/index.d.cts +3 -11
- package/dist/audio-player/index.d.ts +3 -11
- package/dist/audio-player/index.mjs +1 -2
- package/dist/audio-player/index.mjs.map +1 -1
- package/dist/file-icon/index.cjs +3 -3
- package/dist/file-icon/index.cjs.map +1 -1
- package/dist/file-icon/index.mjs +3 -3
- package/dist/file-icon/index.mjs.map +1 -1
- package/dist/tree/index.cjs +0 -3
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.mjs +0 -3
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +117 -36
- package/src/common/FloatingToolbar/actions/CopyAction.tsx +31 -0
- package/src/{components → common}/FloatingToolbar/actions/DownloadAction.tsx +15 -10
- package/src/common/FloatingToolbar/actions/ExpandAction.tsx +33 -0
- package/src/common/FloatingToolbar/actions/FullscreenAction.tsx +38 -0
- package/src/{components → common}/FloatingToolbar/index.tsx +39 -0
- package/src/lib/http.ts +64 -0
- package/src/tools/chat/index.ts +1 -1
- package/src/tools/chat/launcher/ChatFAB.tsx +66 -74
- package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +2 -3
- package/src/tools/chat/lazy.tsx +1 -1
- package/src/tools/chat/messages/MessageBubble.tsx +1 -1
- package/src/tools/chat/messages/blocks/builtin.tsx +1 -1
- package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +2 -2
- package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +12 -1
- package/src/tools/data/DataGrid/lazy.tsx +1 -1
- package/src/tools/data/DataTable/lazy.tsx +1 -1
- package/src/tools/data/JsonTree/JsonViewer.tsx +720 -0
- package/src/tools/data/JsonTree/README.md +126 -73
- package/src/tools/data/JsonTree/index.tsx +3 -95
- package/src/tools/data/JsonTree/lazy.tsx +10 -50
- package/src/tools/data/JsonTree/types.ts +82 -63
- package/src/tools/data/Kanban/lazy.tsx +1 -1
- package/src/tools/data/Listbox/lazy.tsx +1 -1
- package/src/tools/data/Masonry/lazy.tsx +1 -1
- package/src/tools/data/Timeline/lazy.tsx +1 -1
- package/src/tools/data/Tree/components/TreeRow.tsx +0 -11
- package/src/tools/data/Tree/lazy.tsx +1 -1
- package/src/tools/dev/Map/lazy.tsx +1 -1
- package/src/tools/dev/Mermaid/Mermaid.client.tsx +2 -2
- package/src/tools/dev/Mermaid/lazy.tsx +1 -1
- package/src/tools/dev/api/ApiRefTable/ApiRefTable.tsx +65 -0
- package/src/tools/dev/api/ApiRefTable/README.md +31 -0
- package/src/tools/dev/api/ApiRefTable/components/Row.tsx +96 -0
- package/src/tools/dev/api/ApiRefTable/components/TypeDisplay.tsx +44 -0
- package/src/tools/dev/api/ApiRefTable/index.ts +6 -0
- package/src/tools/dev/api/ApiRefTable/lazy.tsx +21 -0
- package/src/tools/dev/api/ApiRefTable/types.ts +82 -0
- package/src/tools/dev/api/ApiRefTable/utils.ts +42 -0
- package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/ApiIntroSection.tsx +1 -1
- package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +1 -1
- package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Header/index.tsx +1 -1
- package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +7 -21
- package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/RequestPanel.tsx +1 -1
- package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/PrettyView.tsx +13 -19
- package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/types.ts +1 -1
- package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/lazy.tsx +1 -1
- package/src/tools/dev/api/RequestViewer/README.md +33 -0
- package/src/tools/dev/api/RequestViewer/RequestViewer.tsx +121 -0
- package/src/tools/dev/api/RequestViewer/components/BodyTab.tsx +44 -0
- package/src/tools/dev/api/RequestViewer/components/EmptyState.tsx +13 -0
- package/src/tools/dev/api/RequestViewer/components/HeadersTab.tsx +78 -0
- package/src/tools/dev/api/RequestViewer/components/TimingTab.tsx +113 -0
- package/src/tools/dev/api/RequestViewer/components/utils.ts +31 -0
- package/src/tools/dev/api/RequestViewer/index.ts +16 -0
- package/src/tools/dev/api/RequestViewer/lazy.tsx +30 -0
- package/src/tools/dev/api/RequestViewer/types.ts +81 -0
- package/src/tools/dev/code/DiffViewer/DiffViewer.tsx +144 -0
- package/src/tools/dev/code/DiffViewer/README.md +33 -0
- package/src/tools/dev/code/DiffViewer/components/CopyButton.tsx +49 -0
- package/src/tools/dev/code/DiffViewer/components/DiffLineContent.tsx +48 -0
- package/src/tools/dev/code/DiffViewer/components/SplitView.tsx +220 -0
- package/src/tools/dev/code/DiffViewer/components/UnifiedView.tsx +154 -0
- package/src/tools/dev/code/DiffViewer/hooks/useDiff.ts +47 -0
- package/src/tools/dev/code/DiffViewer/hooks/useHighlighter.ts +54 -0
- package/src/tools/dev/code/DiffViewer/index.ts +22 -0
- package/src/tools/dev/code/DiffViewer/lazy.tsx +22 -0
- package/src/tools/dev/code/DiffViewer/types.ts +109 -0
- package/src/tools/dev/code/DiffViewer/utils/computeDiff.ts +159 -0
- package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/CollapseToggle.tsx +1 -1
- package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/MarkdownMessage.tsx +1 -1
- package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/components.tsx +2 -2
- package/src/tools/dev/{PrettyCode → code/PrettyCode}/PrettyCode.client.tsx +2 -2
- package/src/tools/dev/{PrettyCode → code/PrettyCode}/lazy.tsx +1 -1
- package/src/tools/dev/ops/EnvTable/EnvTable.tsx +228 -0
- package/src/tools/dev/ops/EnvTable/README.md +29 -0
- package/src/tools/dev/ops/EnvTable/hooks/useEnvMask.ts +121 -0
- package/src/tools/dev/ops/EnvTable/index.ts +12 -0
- package/src/tools/dev/ops/EnvTable/lazy.tsx +21 -0
- package/src/tools/dev/ops/EnvTable/types.ts +76 -0
- package/src/tools/dev/ops/LogViewer/LogViewer.tsx +194 -0
- package/src/tools/dev/ops/LogViewer/README.md +30 -0
- package/src/tools/dev/ops/LogViewer/components/LogRow.tsx +151 -0
- package/src/tools/dev/ops/LogViewer/components/Toolbar.tsx +199 -0
- package/src/tools/dev/ops/LogViewer/hooks/useAutoScroll.ts +68 -0
- package/src/tools/dev/ops/LogViewer/hooks/useLogFilter.ts +58 -0
- package/src/tools/dev/ops/LogViewer/index.ts +18 -0
- package/src/tools/dev/ops/LogViewer/lazy.tsx +25 -0
- package/src/tools/dev/ops/LogViewer/types.ts +142 -0
- package/src/tools/dev/ops/LogViewer/utils/ansi.ts +231 -0
- package/src/tools/forms/CodeEditor/components/Editor.tsx +19 -0
- package/src/tools/forms/CodeEditor/hooks/useEditorTheme.ts +13 -73
- package/src/tools/forms/CodeEditor/lazy.tsx +1 -1
- package/src/tools/forms/CodeEditor/types/index.ts +7 -0
- package/src/tools/forms/FileUpload/lazy.tsx +1 -1
- package/src/tools/forms/JsonEditor/JsonEditor.tsx +115 -0
- package/src/tools/forms/JsonEditor/index.ts +1 -0
- package/src/tools/forms/JsonEditor/lazy.tsx +24 -0
- package/src/tools/forms/JsonForm/index.ts +1 -1
- package/src/tools/forms/JsonForm/lazy.tsx +1 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +40 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +1 -1
- package/src/tools/forms/MarkdownEditor/styles.css +174 -21
- package/src/tools/forms/NotionEditor/CustomKeymap.ts +48 -0
- package/src/tools/forms/NotionEditor/LinkDialog.tsx +133 -0
- package/src/tools/forms/NotionEditor/NotionEditor.tsx +304 -0
- package/src/tools/forms/NotionEditor/README.md +237 -0
- package/src/tools/forms/NotionEditor/SlashExtension.ts +32 -0
- package/src/tools/forms/NotionEditor/SlashList.tsx +136 -0
- package/src/tools/forms/NotionEditor/TaskItemView.tsx +41 -0
- package/src/tools/forms/NotionEditor/createSlashSuggestion.ts +121 -0
- package/src/tools/forms/NotionEditor/extensions.ts +105 -0
- package/src/tools/forms/NotionEditor/index.ts +1 -0
- package/src/tools/forms/NotionEditor/lazy.tsx +44 -0
- package/src/tools/forms/NotionEditor/slashItems.ts +159 -0
- package/src/tools/forms/NotionEditor/styles.css +478 -0
- package/src/tools/forms/NotionEditor/types.ts +28 -0
- package/src/tools/index.ts +153 -13
- package/src/tools/input/Combobox/lazy.tsx +1 -1
- package/src/tools/input/CronScheduler/components/CronPreview.README.md +28 -0
- package/src/tools/input/CronScheduler/components/CronPreview.tsx +136 -0
- package/src/tools/input/CronScheduler/components/index.ts +3 -0
- package/src/tools/input/CronScheduler/index.tsx +5 -1
- package/src/tools/input/CronScheduler/lazy.tsx +5 -1
- package/src/tools/input/CronScheduler/utils/cron-next-runs.ts +122 -0
- package/src/tools/input/CronScheduler/utils/index.ts +1 -0
- package/src/tools/input/Scroller/lazy.tsx +1 -1
- package/src/tools/input/Sortable/lazy.tsx +1 -1
- package/src/tools/input/SpeechRecognition/lazy.tsx +1 -1
- package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +41 -36
- package/src/tools/media/AudioPlayer/PlayerShell.tsx +3 -11
- package/src/tools/media/AudioPlayer/types.ts +4 -11
- package/src/tools/media/ImageViewer/components/ImageToolbar.tsx +58 -47
- package/src/tools/media/ImageViewer/components/ImageViewer.tsx +35 -19
- package/src/tools/media/ImageViewer/lazy.tsx +1 -1
- package/src/tools/media/ImageViewer/types.ts +4 -0
- package/src/tools/media/LottiePlayer/lazy.tsx +1 -1
- package/src/tools/media/VideoPlayer/VideoPlayer.tsx +47 -1
- package/src/tools/media/VideoPlayer/parts/fullscreen.tsx +21 -4
- package/src/tools/media/VideoPlayer/parts/pip.tsx +21 -4
- package/src/tools/media/VideoPlayer/parts/play-button.tsx +21 -4
- package/src/tools/media/VideoPlayer/parts/playback-rate.tsx +19 -3
- package/src/tools/media/VideoPlayer/parts/volume.tsx +237 -18
- package/src/tools/media/VideoPlayer/styles/video-player.css +87 -7
- package/src/tools/media/VideoPlayer/types.ts +4 -0
- package/src/tools/overlay/ResponsiveDialog/lazy.tsx +1 -1
- package/src/tools/overlay/ScrollSpy/lazy.tsx +1 -1
- package/src/tools/overlay/SelectionToolbar/lazy.tsx +1 -1
- package/src/tools/overlay/Tour/lazy.tsx +1 -1
- package/src/tools/visual/Marquee/lazy.tsx +1 -1
- package/src/tools/visual/QRCode/lazy.tsx +1 -1
- package/src/tools/visual/charts/ActivityGraph/ActivityGraph.tsx +195 -0
- package/src/tools/visual/charts/ActivityGraph/README.md +28 -0
- package/src/tools/visual/charts/ActivityGraph/index.ts +8 -0
- package/src/tools/visual/charts/ActivityGraph/lazy.tsx +21 -0
- package/src/tools/visual/charts/ActivityGraph/types.ts +59 -0
- package/src/tools/visual/charts/ActivityGraph/utils.ts +88 -0
- package/src/tools/visual/charts/CommitGraph/CommitGraph.tsx +80 -0
- package/src/tools/visual/charts/CommitGraph/README.md +28 -0
- package/src/tools/visual/charts/CommitGraph/components/CommitDetail.tsx +107 -0
- package/src/tools/visual/charts/CommitGraph/components/CommitRow.tsx +122 -0
- package/src/tools/visual/charts/CommitGraph/components/Rails.tsx +171 -0
- package/src/tools/visual/charts/CommitGraph/hooks/useGraphLayout.ts +169 -0
- package/src/tools/visual/charts/CommitGraph/hooks/useLaneColors.ts +45 -0
- package/src/tools/visual/charts/CommitGraph/index.ts +14 -0
- package/src/tools/visual/charts/CommitGraph/lazy.tsx +25 -0
- package/src/tools/visual/charts/CommitGraph/types.ts +85 -0
- package/src/tools/visual/charts/CommitGraph/utils.ts +53 -0
- package/src/tools/visual/{Gauge → charts/Gauge}/lazy.tsx +1 -1
- package/src/tools/visual/charts/Sparkline/README.md +29 -0
- package/src/tools/visual/charts/Sparkline/Sparkline.tsx +217 -0
- package/src/tools/visual/charts/Sparkline/index.ts +9 -0
- package/src/tools/visual/charts/Sparkline/lazy.tsx +26 -0
- package/src/tools/visual/charts/Sparkline/types.ts +58 -0
- package/src/tools/visual/design/ColorPalette/ColorPalette.tsx +129 -0
- package/src/tools/visual/design/ColorPalette/README.md +34 -0
- package/src/tools/visual/design/ColorPalette/components/Swatch.tsx +102 -0
- package/src/tools/visual/design/ColorPalette/hooks/useCopyToClipboard.ts +41 -0
- package/src/tools/visual/design/ColorPalette/index.ts +12 -0
- package/src/tools/visual/design/ColorPalette/lazy.tsx +21 -0
- package/src/tools/visual/design/ColorPalette/types.ts +63 -0
- package/src/tools/visual/design/ColorPalette/utils.ts +83 -0
- package/src/tools/visual/{ColorPicker → design/ColorPicker}/lazy.tsx +1 -1
- package/src/tools/visual/{FileIcon → design/FileIcon}/treeAdapter.tsx +1 -1
- package/src/tools/visual/{Fps → indicators/Fps}/lazy.tsx +1 -1
- package/src/tools/visual/{Rating → indicators/Rating}/lazy.tsx +1 -1
- package/src/tools/visual/indicators/StatusIndicator/README.md +28 -0
- package/src/tools/visual/indicators/StatusIndicator/StatusIndicator.tsx +83 -0
- package/src/tools/visual/indicators/StatusIndicator/index.ts +14 -0
- package/src/tools/visual/indicators/StatusIndicator/lazy.tsx +21 -0
- package/src/tools/visual/indicators/StatusIndicator/types.ts +133 -0
- package/src/components/FloatingToolbar/actions/CopyAction.tsx +0 -22
- package/src/components/FloatingToolbar/actions/ExpandAction.tsx +0 -25
- package/src/components/FloatingToolbar/actions/FullscreenAction.tsx +0 -30
- package/src/tools/data/JsonTree/components/JsonContent.tsx +0 -197
- package/src/tools/data/JsonTree/hooks/useJsonExpand.ts +0 -50
- /package/src/{components → common}/FloatingToolbar/FloatingToolbar.css +0 -0
- /package/src/{components → common}/FloatingToolbar/actions/index.ts +0 -0
- /package/src/{components → common}/FloatingToolbar/hooks/useElementCorner.ts +0 -0
- /package/src/{components → common}/FloatingToolbar/hooks/useScrollIsolation.ts +0 -0
- /package/src/{components → common}/index.ts +0 -0
- /package/src/{components → common}/lazy-wrapper.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/README.md +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/DocsView.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Parameters/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Responses/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Section/defaults.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/Section/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/context.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/store/index.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/store/selectors.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/EndpointDoc/types.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/SchemaCopyMenu.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/BrandHeader.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/CategoryBlock.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/EndpointRow.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/SchemaSection.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/SearchInput.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/SidebarBody.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/Toolbar.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/buildVM.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/types.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/Sidebar/useDebouncedValue.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/SlideInPlayground.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/TryItSheet.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/anchor.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/grouping.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/DocsLayout/sidebarLabel.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/index.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/BodyFormEditor.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/EndpointDraftSync.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/EndpointResetButton.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/PreviewView.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/RawView.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/StatusBar.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/ViewTabs.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/detectContent.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ResponsePanel/useResponseView.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/SendButton.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/components/shared/ui.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/constants.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/context/PlaygroundContext.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/hooks/index.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/hooks/useDocsUrlSync.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/hooks/useEndpointDraft.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/hooks/useMobile.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/hooks/useOpenApiSchema.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/index.tsx +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/types.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/apiKeyManager.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/codeSamples.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/formatters.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/index.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/operationToHar.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/sampler.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/schemaExport.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/scrollParent.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/url.ts +0 -0
- /package/src/tools/dev/{OpenapiViewer → api/OpenapiViewer}/utils/versionManager.ts +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/ActionRow.tsx +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/ChatMessageRow.tsx +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/CodeBlock.tsx +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/README.md +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/index.ts +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/linkRules.ts +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/plainText.ts +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/sanitize.ts +0 -0
- /package/src/tools/dev/{MarkdownMessage → code/MarkdownMessage}/types.ts +0 -0
- /package/src/tools/dev/{PrettyCode → code/PrettyCode}/README.md +0 -0
- /package/src/tools/dev/{PrettyCode → code/PrettyCode}/index.tsx +0 -0
- /package/src/tools/dev/{PrettyCode → code/PrettyCode}/registerPrismLanguages.ts +0 -0
- /package/src/tools/visual/{Gauge → charts/Gauge}/Gauge.tsx +0 -0
- /package/src/tools/visual/{Gauge → charts/Gauge}/index.ts +0 -0
- /package/src/tools/visual/{Gauge → charts/Gauge}/types.ts +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/ColorPicker.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/context/ColorPickerContext.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/context/ColorPickerStore.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/context/index.ts +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/index.ts +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/lib/color-utils.ts +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/parts/ColorPickerAlphaSlider.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/parts/ColorPickerArea.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/parts/ColorPickerEyeDropper.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/parts/ColorPickerFormatSelect.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/parts/ColorPickerHueSlider.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/parts/ColorPickerInput.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/parts/ColorPickerSwatch.tsx +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/parts/index.ts +0 -0
- /package/src/tools/visual/{ColorPicker → design/ColorPicker}/types.ts +0 -0
- /package/src/tools/visual/{FileIcon → design/FileIcon}/FileIcon.tsx +0 -0
- /package/src/tools/visual/{FileIcon → design/FileIcon}/get-file-icon.ts +0 -0
- /package/src/tools/visual/{FileIcon → design/FileIcon}/icons/icon-data.ts +0 -0
- /package/src/tools/visual/{FileIcon → design/FileIcon}/index.ts +0 -0
- /package/src/tools/visual/{FileIcon → design/FileIcon}/loader.ts +0 -0
- /package/src/tools/visual/{FileIcon → design/FileIcon}/specialFolders.ts +0 -0
- /package/src/tools/visual/{Fps → indicators/Fps}/Fps.tsx +0 -0
- /package/src/tools/visual/{Fps → indicators/Fps}/index.ts +0 -0
- /package/src/tools/visual/{Fps → indicators/Fps}/types.ts +0 -0
- /package/src/tools/visual/{Rating → indicators/Rating}/Rating.tsx +0 -0
- /package/src/tools/visual/{Rating → indicators/Rating}/index.ts +0 -0
- /package/src/tools/visual/{Rating → indicators/Rating}/types.ts +0 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JsonViewer — collapsible, syntax-coloured JSON tree.
|
|
5
|
+
*
|
|
6
|
+
* Ported from jalco/ui's JsonViewer (MIT, Justin Levine —
|
|
7
|
+
* https://ui.justinlevine.me). Modifications:
|
|
8
|
+
* - Removed Shiki theme module + the 65-theme palette table; we only
|
|
9
|
+
* use Tailwind tokens so the viewer follows the host theme via
|
|
10
|
+
* normal CSS-var cascade. No `colorTheme` prop.
|
|
11
|
+
* - Replaced jalco's `<button>` toolbar with ui-core <Tooltip>/<Button>
|
|
12
|
+
* primitives so it picks up shared focus/hover styling.
|
|
13
|
+
* - New `toolbar` prop: `'auto' | 'always' | 'never'`. `'auto'` fades
|
|
14
|
+
* the toolbar in on hover / keyboard focus inside the container
|
|
15
|
+
* (ChatGPT-style). `'always'` keeps it visible — debug-panel mode.
|
|
16
|
+
* - New `actions` prop: list of buttons to mount (`'search' | 'expand'
|
|
17
|
+
* | 'copy' | 'download'`). Default omits download — file viewers
|
|
18
|
+
* don't need to "download" something that already lives on disk.
|
|
19
|
+
* - `bordered={false}` strips the card chrome for host panes that own
|
|
20
|
+
* it already (avoids the historic double-border bug).
|
|
21
|
+
*
|
|
22
|
+
* The recursive tree code (collapsedPaths Set keyed by string path,
|
|
23
|
+
* buildPath helper, hasSearchMatch recursion, HighlightMatch span) is
|
|
24
|
+
* jalco's original — see `_legacy/` for the pre-refactor JsonContent
|
|
25
|
+
* wrapper around `react-json-tree`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
Check,
|
|
30
|
+
ChevronRight,
|
|
31
|
+
Copy,
|
|
32
|
+
CopyPlus,
|
|
33
|
+
Download,
|
|
34
|
+
FoldHorizontal,
|
|
35
|
+
Search,
|
|
36
|
+
UnfoldHorizontal,
|
|
37
|
+
X,
|
|
38
|
+
} from 'lucide-react';
|
|
39
|
+
import {
|
|
40
|
+
memo,
|
|
41
|
+
useCallback,
|
|
42
|
+
useEffect,
|
|
43
|
+
useMemo,
|
|
44
|
+
useRef,
|
|
45
|
+
useState,
|
|
46
|
+
type CSSProperties,
|
|
47
|
+
} from 'react';
|
|
48
|
+
import { Button, Input, Tooltip, TooltipContent, TooltipTrigger } from '@djangocfg/ui-core';
|
|
49
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
50
|
+
import type { JsonAction, JsonTreeProps, JsonValue } from './types';
|
|
51
|
+
|
|
52
|
+
// ─── helpers ────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function typeOf(value: JsonValue): string {
|
|
55
|
+
if (value === null) return 'null';
|
|
56
|
+
if (Array.isArray(value)) return 'array';
|
|
57
|
+
return typeof value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function countEntries(value: JsonValue): number {
|
|
61
|
+
if (Array.isArray(value)) return value.length;
|
|
62
|
+
if (value !== null && typeof value === 'object') return Object.keys(value).length;
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Build a JS access path: `root.users[0].name`. Falls back to bracket
|
|
67
|
+
* syntax for keys that aren't valid identifiers. */
|
|
68
|
+
function buildPath(parent: string, key: string | number): string {
|
|
69
|
+
if (parent === '') return String(key);
|
|
70
|
+
if (typeof key === 'number') return `${parent}[${key}]`;
|
|
71
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) return `${parent}.${key}`;
|
|
72
|
+
return `${parent}["${key}"]`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function matchesSearch(key: string | number, value: JsonValue, query: string): boolean {
|
|
76
|
+
const q = query.toLowerCase();
|
|
77
|
+
if (String(key).toLowerCase().includes(q)) return true;
|
|
78
|
+
if (value === null) return 'null'.includes(q);
|
|
79
|
+
if (typeof value !== 'object') return String(value).toLowerCase().includes(q);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasSearchMatch(value: JsonValue, key: string | number, query: string): boolean {
|
|
84
|
+
if (!query) return false;
|
|
85
|
+
if (matchesSearch(key, value, query)) return true;
|
|
86
|
+
if (value !== null && typeof value === 'object') {
|
|
87
|
+
const entries = Array.isArray(value)
|
|
88
|
+
? (value.map((v, i) => [i, v] as const))
|
|
89
|
+
: (Object.entries(value) as readonly (readonly [string, JsonValue])[]);
|
|
90
|
+
return entries.some(([k, v]) => hasSearchMatch(v, k, query));
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Token palette per theme.
|
|
97
|
+
*
|
|
98
|
+
* Light is hand-tuned after VSCode Light+ / Xcode (deep blue keys,
|
|
99
|
+
* brick-red strings, forest-green numbers) — pastels read as washed-out
|
|
100
|
+
* on a white background. Dark mirrors the One Dark family.
|
|
101
|
+
*/
|
|
102
|
+
const TOKEN_CLASS: Record<string, string> = {
|
|
103
|
+
key: 'text-[#001080] dark:text-violet-400',
|
|
104
|
+
string: 'text-[#a31515] dark:text-emerald-400',
|
|
105
|
+
number: 'text-[#098658] dark:text-sky-400',
|
|
106
|
+
boolean: 'text-[#0550ae] dark:text-amber-400',
|
|
107
|
+
null: 'text-neutral-500 dark:text-muted-foreground/60',
|
|
108
|
+
punctuation: 'text-neutral-700 dark:text-muted-foreground',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function TokenSpan({
|
|
112
|
+
token,
|
|
113
|
+
children,
|
|
114
|
+
className,
|
|
115
|
+
italic,
|
|
116
|
+
}: {
|
|
117
|
+
token: keyof typeof TOKEN_CLASS;
|
|
118
|
+
children: React.ReactNode;
|
|
119
|
+
className?: string;
|
|
120
|
+
italic?: boolean;
|
|
121
|
+
}) {
|
|
122
|
+
return (
|
|
123
|
+
<span className={cn(TOKEN_CLASS[token], italic && 'italic', className)}>{children}</span>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function HighlightMatch({ text, query }: { text: string; query: string }) {
|
|
128
|
+
if (!query) return <>{text}</>;
|
|
129
|
+
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
130
|
+
if (idx === -1) return <>{text}</>;
|
|
131
|
+
return (
|
|
132
|
+
<>
|
|
133
|
+
{text.slice(0, idx)}
|
|
134
|
+
<mark className="rounded-sm bg-amber-200/60 px-0.5 text-inherit dark:bg-amber-500/30">
|
|
135
|
+
{text.slice(idx, idx + query.length)}
|
|
136
|
+
</mark>
|
|
137
|
+
{text.slice(idx + query.length)}
|
|
138
|
+
</>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── recursive node ─────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
interface JsonNodeProps {
|
|
145
|
+
keyName: string | number;
|
|
146
|
+
value: JsonValue;
|
|
147
|
+
path: string;
|
|
148
|
+
depth: number;
|
|
149
|
+
searchQuery: string;
|
|
150
|
+
collapsedPaths: Set<string>;
|
|
151
|
+
onToggle: (path: string) => void;
|
|
152
|
+
isLast: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function JsonNode({
|
|
156
|
+
keyName,
|
|
157
|
+
value,
|
|
158
|
+
path,
|
|
159
|
+
depth,
|
|
160
|
+
searchQuery,
|
|
161
|
+
collapsedPaths,
|
|
162
|
+
onToggle,
|
|
163
|
+
isLast,
|
|
164
|
+
}: JsonNodeProps) {
|
|
165
|
+
const type = typeOf(value);
|
|
166
|
+
const isExpandable = type === 'object' || type === 'array';
|
|
167
|
+
const count = isExpandable ? countEntries(value) : 0;
|
|
168
|
+
|
|
169
|
+
const isCollapsed = collapsedPaths.has(path);
|
|
170
|
+
const isExpanded = isExpandable && !isCollapsed;
|
|
171
|
+
|
|
172
|
+
const openBracket = type === 'array' ? '[' : '{';
|
|
173
|
+
const closeBracket = type === 'array' ? ']' : '}';
|
|
174
|
+
const comma = isLast ? '' : ',';
|
|
175
|
+
|
|
176
|
+
const nodeMatches = searchQuery && matchesSearch(keyName, value, searchQuery);
|
|
177
|
+
|
|
178
|
+
const handleToggle = useCallback(() => {
|
|
179
|
+
if (isExpandable) onToggle(path);
|
|
180
|
+
}, [isExpandable, onToggle, path]);
|
|
181
|
+
|
|
182
|
+
const [pathCopied, setPathCopied] = useState(false);
|
|
183
|
+
const handleCopyPath = useCallback(() => {
|
|
184
|
+
void navigator.clipboard?.writeText(path).then(() => {
|
|
185
|
+
setPathCopied(true);
|
|
186
|
+
setTimeout(() => setPathCopied(false), 1500);
|
|
187
|
+
});
|
|
188
|
+
}, [path]);
|
|
189
|
+
|
|
190
|
+
const rowClass = cn(
|
|
191
|
+
'group flex items-center gap-0 py-px hover:bg-muted/40 rounded-sm',
|
|
192
|
+
nodeMatches && 'bg-amber-100/40 dark:bg-amber-900/20',
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const rowStyle: CSSProperties = { paddingLeft: `${depth * 20 + 8}px` };
|
|
196
|
+
|
|
197
|
+
const copyIconClass =
|
|
198
|
+
'ml-1 inline-flex items-center justify-center rounded p-0.5 text-muted-foreground/0 transition-colors group-hover:text-muted-foreground hover:!text-foreground focus-visible:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring';
|
|
199
|
+
|
|
200
|
+
function renderKey() {
|
|
201
|
+
return (
|
|
202
|
+
<TokenSpan token="key">
|
|
203
|
+
{typeof keyName === 'string' ? (
|
|
204
|
+
<>
|
|
205
|
+
"
|
|
206
|
+
<HighlightMatch text={keyName} query={searchQuery} />
|
|
207
|
+
"
|
|
208
|
+
</>
|
|
209
|
+
) : (
|
|
210
|
+
keyName
|
|
211
|
+
)}
|
|
212
|
+
</TokenSpan>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderValue() {
|
|
217
|
+
if (typeof value === 'string') {
|
|
218
|
+
return (
|
|
219
|
+
<TokenSpan token="string">
|
|
220
|
+
"
|
|
221
|
+
<HighlightMatch text={value} query={searchQuery} />
|
|
222
|
+
"
|
|
223
|
+
</TokenSpan>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (value === null) {
|
|
227
|
+
return (
|
|
228
|
+
<TokenSpan token="null" italic>
|
|
229
|
+
{searchQuery ? <HighlightMatch text="null" query={searchQuery} /> : 'null'}
|
|
230
|
+
</TokenSpan>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (typeof value === 'number') {
|
|
234
|
+
return (
|
|
235
|
+
<TokenSpan token="number">
|
|
236
|
+
<HighlightMatch text={String(value)} query={searchQuery} />
|
|
237
|
+
</TokenSpan>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (typeof value === 'boolean') {
|
|
241
|
+
return (
|
|
242
|
+
<TokenSpan token="boolean">
|
|
243
|
+
<HighlightMatch text={String(value)} query={searchQuery} />
|
|
244
|
+
</TokenSpan>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return <span>{String(value)}</span>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!isExpandable) {
|
|
251
|
+
return (
|
|
252
|
+
<div className={rowClass} style={rowStyle}>
|
|
253
|
+
<span className="w-4 shrink-0" />
|
|
254
|
+
<span className="font-mono">
|
|
255
|
+
{renderKey()}
|
|
256
|
+
<TokenSpan token="punctuation">: </TokenSpan>
|
|
257
|
+
{renderValue()}
|
|
258
|
+
<TokenSpan token="punctuation">{comma}</TokenSpan>
|
|
259
|
+
</span>
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={handleCopyPath}
|
|
263
|
+
aria-label={`Copy path: ${path}`}
|
|
264
|
+
className={copyIconClass}
|
|
265
|
+
>
|
|
266
|
+
{pathCopied ? (
|
|
267
|
+
<CopyPlus className="size-3 text-emerald-500" />
|
|
268
|
+
) : (
|
|
269
|
+
<CopyPlus className="size-3" />
|
|
270
|
+
)}
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const entries = Array.isArray(value)
|
|
277
|
+
? value.map((v, i) => [i, v] as [number, JsonValue])
|
|
278
|
+
: (Object.entries(value as Record<string, JsonValue>) as [string, JsonValue][]);
|
|
279
|
+
|
|
280
|
+
const displayEntries = searchQuery
|
|
281
|
+
? entries.filter(([k, v]) => hasSearchMatch(v, k, searchQuery))
|
|
282
|
+
: entries;
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div>
|
|
286
|
+
<div className={rowClass} style={rowStyle}>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={handleToggle}
|
|
290
|
+
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
291
|
+
className="flex size-4 shrink-0 items-center justify-center transition-transform text-muted-foreground"
|
|
292
|
+
>
|
|
293
|
+
<ChevronRight
|
|
294
|
+
className={cn('size-3 transition-transform', isExpanded && 'rotate-90')}
|
|
295
|
+
/>
|
|
296
|
+
</button>
|
|
297
|
+
<span className="font-mono">
|
|
298
|
+
{renderKey()}
|
|
299
|
+
<TokenSpan token="punctuation">: </TokenSpan>
|
|
300
|
+
<TokenSpan token="punctuation">{openBracket}</TokenSpan>
|
|
301
|
+
{!isExpanded && (
|
|
302
|
+
<>
|
|
303
|
+
<span className="mx-1 text-[10px] text-muted-foreground/60">
|
|
304
|
+
{count} {count === 1 ? 'item' : 'items'}
|
|
305
|
+
</span>
|
|
306
|
+
<TokenSpan token="punctuation">
|
|
307
|
+
{closeBracket}
|
|
308
|
+
{comma}
|
|
309
|
+
</TokenSpan>
|
|
310
|
+
</>
|
|
311
|
+
)}
|
|
312
|
+
</span>
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
onClick={handleCopyPath}
|
|
316
|
+
aria-label={`Copy path: ${path}`}
|
|
317
|
+
className={copyIconClass}
|
|
318
|
+
>
|
|
319
|
+
{pathCopied ? (
|
|
320
|
+
<CopyPlus className="size-3 text-emerald-500" />
|
|
321
|
+
) : (
|
|
322
|
+
<CopyPlus className="size-3" />
|
|
323
|
+
)}
|
|
324
|
+
</button>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{isExpanded && (
|
|
328
|
+
<>
|
|
329
|
+
{displayEntries.map(([k, v], i) => (
|
|
330
|
+
<JsonNode
|
|
331
|
+
key={`${k}-${i}`}
|
|
332
|
+
keyName={k}
|
|
333
|
+
value={v}
|
|
334
|
+
path={buildPath(path, k)}
|
|
335
|
+
depth={depth + 1}
|
|
336
|
+
searchQuery={searchQuery}
|
|
337
|
+
collapsedPaths={collapsedPaths}
|
|
338
|
+
onToggle={onToggle}
|
|
339
|
+
isLast={i === displayEntries.length - 1}
|
|
340
|
+
/>
|
|
341
|
+
))}
|
|
342
|
+
<div
|
|
343
|
+
className="font-mono text-muted-foreground"
|
|
344
|
+
style={{ paddingLeft: `${depth * 20 + 8 + 16}px` }}
|
|
345
|
+
>
|
|
346
|
+
{closeBracket}
|
|
347
|
+
{comma}
|
|
348
|
+
</div>
|
|
349
|
+
</>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── path collectors for expand/collapse-all ────────────────────────────
|
|
356
|
+
|
|
357
|
+
function collectInitialCollapsed(
|
|
358
|
+
value: JsonValue,
|
|
359
|
+
path: string,
|
|
360
|
+
maxDepth: number | true,
|
|
361
|
+
depth: number,
|
|
362
|
+
result: Set<string>,
|
|
363
|
+
): void {
|
|
364
|
+
if (value === null || typeof value !== 'object') return;
|
|
365
|
+
if (maxDepth !== true && depth >= maxDepth) result.add(path);
|
|
366
|
+
const entries = Array.isArray(value)
|
|
367
|
+
? value.map((v, i) => [i, v] as const)
|
|
368
|
+
: Object.entries(value);
|
|
369
|
+
for (const [k, v] of entries) {
|
|
370
|
+
collectInitialCollapsed(v, buildPath(path, k), maxDepth, depth + 1, result);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function collectAllExpandable(value: JsonValue, path: string, result: Set<string>): void {
|
|
375
|
+
if (value === null || typeof value !== 'object') return;
|
|
376
|
+
result.add(path);
|
|
377
|
+
const entries = Array.isArray(value)
|
|
378
|
+
? value.map((v, i) => [i, v] as const)
|
|
379
|
+
: Object.entries(value);
|
|
380
|
+
for (const [k, v] of entries) {
|
|
381
|
+
collectAllExpandable(v, buildPath(path, k), result);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── toolbar ────────────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
interface ToolbarProps {
|
|
388
|
+
title?: string;
|
|
389
|
+
badge?: string;
|
|
390
|
+
actions: readonly JsonAction[];
|
|
391
|
+
searchOpen: boolean;
|
|
392
|
+
copiedAll: boolean;
|
|
393
|
+
compact?: boolean;
|
|
394
|
+
onToggleSearch: () => void;
|
|
395
|
+
onExpandAll: () => void;
|
|
396
|
+
onCollapseAll: () => void;
|
|
397
|
+
onCopy: () => void;
|
|
398
|
+
onDownload: () => void;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function Toolbar({
|
|
402
|
+
title,
|
|
403
|
+
badge,
|
|
404
|
+
actions,
|
|
405
|
+
searchOpen,
|
|
406
|
+
copiedAll,
|
|
407
|
+
compact = false,
|
|
408
|
+
onToggleSearch,
|
|
409
|
+
onExpandAll,
|
|
410
|
+
onCollapseAll,
|
|
411
|
+
onCopy,
|
|
412
|
+
onDownload,
|
|
413
|
+
}: ToolbarProps) {
|
|
414
|
+
// Compact density: smaller hit area, smaller icons, no key/items badge.
|
|
415
|
+
// Used by embedded contexts (e.g. RequestViewer body for 2-key error
|
|
416
|
+
// responses) where the default toolbar visually outweighs the JSON.
|
|
417
|
+
const btnSize = compact ? 'size-6' : 'size-7';
|
|
418
|
+
const iconSize = compact ? 'size-3' : 'size-3.5';
|
|
419
|
+
const rowPadding = compact ? 'px-2 py-1' : 'px-3 py-1.5';
|
|
420
|
+
// Each action button is wrapped in a Tooltip for the keyboard / mouse
|
|
421
|
+
// discoverability win. `asChild` keeps the underlying <button> as the
|
|
422
|
+
// trigger so focus styling stays on the visible element.
|
|
423
|
+
const renderBtn = (
|
|
424
|
+
key: string,
|
|
425
|
+
label: string,
|
|
426
|
+
icon: React.ReactNode,
|
|
427
|
+
onClick: () => void,
|
|
428
|
+
active?: boolean,
|
|
429
|
+
) => (
|
|
430
|
+
<Tooltip key={key}>
|
|
431
|
+
<TooltipTrigger asChild>
|
|
432
|
+
<Button
|
|
433
|
+
type="button"
|
|
434
|
+
variant="ghost"
|
|
435
|
+
size="icon"
|
|
436
|
+
onClick={onClick}
|
|
437
|
+
aria-label={label}
|
|
438
|
+
aria-pressed={active}
|
|
439
|
+
className={cn(btnSize, 'text-muted-foreground hover:text-foreground', active && 'bg-muted text-foreground')}
|
|
440
|
+
>
|
|
441
|
+
{icon}
|
|
442
|
+
</Button>
|
|
443
|
+
</TooltipTrigger>
|
|
444
|
+
<TooltipContent side="top">{label}</TooltipContent>
|
|
445
|
+
</Tooltip>
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const iconCls = iconSize;
|
|
449
|
+
return (
|
|
450
|
+
<div className={cn('flex items-center justify-between gap-2 border-b border-border/40', rowPadding)}>
|
|
451
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
452
|
+
{title ? (
|
|
453
|
+
<h3 className="truncate text-xs font-medium text-foreground">{title}</h3>
|
|
454
|
+
) : null}
|
|
455
|
+
{badge && !compact ? (
|
|
456
|
+
<span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
457
|
+
{badge}
|
|
458
|
+
</span>
|
|
459
|
+
) : null}
|
|
460
|
+
</div>
|
|
461
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
462
|
+
{actions.includes('search')
|
|
463
|
+
? renderBtn(
|
|
464
|
+
'search',
|
|
465
|
+
searchOpen ? 'Close search' : 'Search',
|
|
466
|
+
<Search className={iconCls} />,
|
|
467
|
+
onToggleSearch,
|
|
468
|
+
searchOpen,
|
|
469
|
+
)
|
|
470
|
+
: null}
|
|
471
|
+
{actions.includes('expand') ? (
|
|
472
|
+
<>
|
|
473
|
+
{renderBtn(
|
|
474
|
+
'expand',
|
|
475
|
+
'Expand all',
|
|
476
|
+
<UnfoldHorizontal className={iconCls} />,
|
|
477
|
+
onExpandAll,
|
|
478
|
+
)}
|
|
479
|
+
{renderBtn(
|
|
480
|
+
'collapse',
|
|
481
|
+
'Collapse all',
|
|
482
|
+
<FoldHorizontal className={iconCls} />,
|
|
483
|
+
onCollapseAll,
|
|
484
|
+
)}
|
|
485
|
+
</>
|
|
486
|
+
) : null}
|
|
487
|
+
{actions.includes('copy')
|
|
488
|
+
? renderBtn(
|
|
489
|
+
'copy',
|
|
490
|
+
copiedAll ? 'Copied!' : 'Copy JSON',
|
|
491
|
+
copiedAll ? (
|
|
492
|
+
<Check className={cn(iconCls, 'text-emerald-500')} />
|
|
493
|
+
) : (
|
|
494
|
+
<Copy className={iconCls} />
|
|
495
|
+
),
|
|
496
|
+
onCopy,
|
|
497
|
+
)
|
|
498
|
+
: null}
|
|
499
|
+
{actions.includes('download')
|
|
500
|
+
? renderBtn(
|
|
501
|
+
'download',
|
|
502
|
+
'Download JSON',
|
|
503
|
+
<Download className={iconCls} />,
|
|
504
|
+
onDownload,
|
|
505
|
+
)
|
|
506
|
+
: null}
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ─── component ──────────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
const DEFAULT_ACTIONS: readonly JsonAction[] = ['search', 'expand', 'copy'];
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* JsonTree — interactive viewer for arbitrary JSON.
|
|
518
|
+
*
|
|
519
|
+
* Default view: card with a hover-revealed toolbar (search / expand /
|
|
520
|
+
* copy) and the tree expanded two levels deep. Toolbar fades in when
|
|
521
|
+
* the container is hovered or keyboard-focused.
|
|
522
|
+
*
|
|
523
|
+
* For debug surfaces that benefit from the toolbar being always
|
|
524
|
+
* visible, pass `toolbar="always"`. For host panes that own the
|
|
525
|
+
* surrounding chrome (cmdop file preview, OpenAPI response panel),
|
|
526
|
+
* pass `bordered={false}`.
|
|
527
|
+
*/
|
|
528
|
+
/** Map `size` → Tailwind text-size class for the tree body. */
|
|
529
|
+
const SIZE_CLASS: Record<'sm' | 'md' | 'lg', string> = {
|
|
530
|
+
sm: 'text-xs',
|
|
531
|
+
md: 'text-sm',
|
|
532
|
+
lg: 'text-base',
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
export const JsonTree = memo(function JsonTree({
|
|
536
|
+
data,
|
|
537
|
+
title,
|
|
538
|
+
rootName = 'root',
|
|
539
|
+
toolbar = 'auto',
|
|
540
|
+
actions = DEFAULT_ACTIONS,
|
|
541
|
+
defaultExpandedDepth = 2,
|
|
542
|
+
bordered = true,
|
|
543
|
+
size = 'sm',
|
|
544
|
+
className,
|
|
545
|
+
downloadFilename = 'data.json',
|
|
546
|
+
compactHeader = false,
|
|
547
|
+
}: JsonTreeProps) {
|
|
548
|
+
// Treat the data uniformly as `JsonValue` for the recursion. We only
|
|
549
|
+
// need this cast at the boundary; everything inside is typed.
|
|
550
|
+
const root = data as JsonValue;
|
|
551
|
+
|
|
552
|
+
const [collapsedPaths, setCollapsedPaths] = useState<Set<string>>(() => {
|
|
553
|
+
if (defaultExpandedDepth === true) return new Set();
|
|
554
|
+
const collapsed = new Set<string>();
|
|
555
|
+
collectInitialCollapsed(root, rootName, defaultExpandedDepth, 0, collapsed);
|
|
556
|
+
return collapsed;
|
|
557
|
+
});
|
|
558
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
559
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
560
|
+
const [copiedAll, setCopiedAll] = useState(false);
|
|
561
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
562
|
+
|
|
563
|
+
const togglePath = useCallback((path: string) => {
|
|
564
|
+
setCollapsedPaths((prev) => {
|
|
565
|
+
const next = new Set(prev);
|
|
566
|
+
if (next.has(path)) next.delete(path);
|
|
567
|
+
else next.add(path);
|
|
568
|
+
return next;
|
|
569
|
+
});
|
|
570
|
+
}, []);
|
|
571
|
+
|
|
572
|
+
const expandAll = useCallback(() => setCollapsedPaths(new Set()), []);
|
|
573
|
+
const collapseAll = useCallback(() => {
|
|
574
|
+
const all = new Set<string>();
|
|
575
|
+
collectAllExpandable(root, rootName, all);
|
|
576
|
+
setCollapsedPaths(all);
|
|
577
|
+
}, [root, rootName]);
|
|
578
|
+
|
|
579
|
+
const jsonString = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
|
580
|
+
|
|
581
|
+
const copyJson = useCallback(() => {
|
|
582
|
+
void navigator.clipboard?.writeText(jsonString).then(() => {
|
|
583
|
+
setCopiedAll(true);
|
|
584
|
+
setTimeout(() => setCopiedAll(false), 1500);
|
|
585
|
+
});
|
|
586
|
+
}, [jsonString]);
|
|
587
|
+
|
|
588
|
+
const downloadJson = useCallback(() => {
|
|
589
|
+
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
590
|
+
const url = URL.createObjectURL(blob);
|
|
591
|
+
const a = document.createElement('a');
|
|
592
|
+
a.href = url;
|
|
593
|
+
a.download = downloadFilename;
|
|
594
|
+
a.click();
|
|
595
|
+
URL.revokeObjectURL(url);
|
|
596
|
+
}, [jsonString, downloadFilename]);
|
|
597
|
+
|
|
598
|
+
const toggleSearch = useCallback(() => {
|
|
599
|
+
setSearchOpen((prev) => {
|
|
600
|
+
const next = !prev;
|
|
601
|
+
if (next) {
|
|
602
|
+
requestAnimationFrame(() => searchRef.current?.focus());
|
|
603
|
+
} else {
|
|
604
|
+
setSearchQuery('');
|
|
605
|
+
}
|
|
606
|
+
return next;
|
|
607
|
+
});
|
|
608
|
+
}, []);
|
|
609
|
+
|
|
610
|
+
// Any non-empty query expands the whole tree so matches inside
|
|
611
|
+
// collapsed nodes become visible — restoring the user's previous
|
|
612
|
+
// collapse state would be friendlier; for now match jalco's behaviour.
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
if (searchQuery) setCollapsedPaths(new Set());
|
|
615
|
+
}, [searchQuery]);
|
|
616
|
+
|
|
617
|
+
const isExpandable = root !== null && typeof root === 'object';
|
|
618
|
+
const type = typeOf(root);
|
|
619
|
+
const badge = isExpandable
|
|
620
|
+
? `${countEntries(root)} ${type === 'array' ? 'items' : 'keys'}`
|
|
621
|
+
: undefined;
|
|
622
|
+
|
|
623
|
+
const showToolbar = toolbar !== 'never';
|
|
624
|
+
const toolbarHover = toolbar === 'auto';
|
|
625
|
+
|
|
626
|
+
return (
|
|
627
|
+
<div
|
|
628
|
+
data-slot="json-tree"
|
|
629
|
+
// `group/jsontree` powers the hover-reveal toolbar in `auto` mode:
|
|
630
|
+
// the toolbar listens to `group-hover/jsontree:` + focus-within
|
|
631
|
+
// and fades in. Keyboard users still see it when focus lands
|
|
632
|
+
// inside the container.
|
|
633
|
+
className={cn(
|
|
634
|
+
'group/jsontree relative w-full',
|
|
635
|
+
SIZE_CLASS[size],
|
|
636
|
+
bordered && 'overflow-hidden rounded-md border border-border/60 bg-card',
|
|
637
|
+
className,
|
|
638
|
+
)}
|
|
639
|
+
>
|
|
640
|
+
{showToolbar ? (
|
|
641
|
+
<div
|
|
642
|
+
className={cn(
|
|
643
|
+
'transition-opacity duration-150',
|
|
644
|
+
toolbarHover &&
|
|
645
|
+
'opacity-0 group-hover/jsontree:opacity-100 group-focus-within/jsontree:opacity-100',
|
|
646
|
+
)}
|
|
647
|
+
>
|
|
648
|
+
<Toolbar
|
|
649
|
+
title={title}
|
|
650
|
+
badge={badge}
|
|
651
|
+
actions={actions}
|
|
652
|
+
searchOpen={searchOpen}
|
|
653
|
+
copiedAll={copiedAll}
|
|
654
|
+
compact={compactHeader}
|
|
655
|
+
onToggleSearch={toggleSearch}
|
|
656
|
+
onExpandAll={expandAll}
|
|
657
|
+
onCollapseAll={collapseAll}
|
|
658
|
+
onCopy={copyJson}
|
|
659
|
+
onDownload={downloadJson}
|
|
660
|
+
/>
|
|
661
|
+
</div>
|
|
662
|
+
) : null}
|
|
663
|
+
|
|
664
|
+
{showToolbar && searchOpen ? (
|
|
665
|
+
<div className="flex items-center gap-2 border-b border-border/40 px-3 py-1.5">
|
|
666
|
+
<Search className="size-3.5 shrink-0 text-muted-foreground" aria-hidden />
|
|
667
|
+
<Input
|
|
668
|
+
ref={searchRef}
|
|
669
|
+
type="text"
|
|
670
|
+
value={searchQuery}
|
|
671
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
672
|
+
placeholder="Filter keys and values…"
|
|
673
|
+
className="h-7 border-0 bg-transparent px-0 font-mono shadow-none focus-visible:ring-0"
|
|
674
|
+
/>
|
|
675
|
+
{searchQuery ? (
|
|
676
|
+
<button
|
|
677
|
+
type="button"
|
|
678
|
+
onClick={() => setSearchQuery('')}
|
|
679
|
+
aria-label="Clear search"
|
|
680
|
+
className="inline-flex items-center justify-center rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground"
|
|
681
|
+
>
|
|
682
|
+
<X className="size-3" />
|
|
683
|
+
</button>
|
|
684
|
+
) : null}
|
|
685
|
+
</div>
|
|
686
|
+
) : null}
|
|
687
|
+
|
|
688
|
+
<div className="overflow-auto py-1">
|
|
689
|
+
{isExpandable ? (
|
|
690
|
+
<JsonNode
|
|
691
|
+
keyName={rootName}
|
|
692
|
+
value={root}
|
|
693
|
+
path={rootName}
|
|
694
|
+
depth={0}
|
|
695
|
+
searchQuery={searchQuery}
|
|
696
|
+
collapsedPaths={collapsedPaths}
|
|
697
|
+
onToggle={togglePath}
|
|
698
|
+
isLast
|
|
699
|
+
/>
|
|
700
|
+
) : (
|
|
701
|
+
<div className="px-4 py-2 font-mono">
|
|
702
|
+
<TokenSpan token="key">{rootName}</TokenSpan>
|
|
703
|
+
<TokenSpan token="punctuation">: </TokenSpan>
|
|
704
|
+
{typeof root === 'string' ? (
|
|
705
|
+
<TokenSpan token="string">"{root}"</TokenSpan>
|
|
706
|
+
) : typeof root === 'number' ? (
|
|
707
|
+
<TokenSpan token="number">{String(root)}</TokenSpan>
|
|
708
|
+
) : typeof root === 'boolean' ? (
|
|
709
|
+
<TokenSpan token="boolean">{String(root)}</TokenSpan>
|
|
710
|
+
) : (
|
|
711
|
+
<TokenSpan token="null" italic>
|
|
712
|
+
null
|
|
713
|
+
</TokenSpan>
|
|
714
|
+
)}
|
|
715
|
+
</div>
|
|
716
|
+
)}
|
|
717
|
+
</div>
|
|
718
|
+
</div>
|
|
719
|
+
);
|
|
720
|
+
});
|