@astryxdesign/cli 0.6.3 → 0.6.4-canary.0e1fbdb
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/CHANGELOG.md +194 -0
- package/README.md +152 -107
- package/api/blog/blog.doc.mjs +1 -0
- package/api/build/_adapter.d.mts +50 -0
- package/api/build/_adapter.mjs +60 -0
- package/api/build/build.doc.mjs +22 -10
- package/api/build/build.test.mjs +219 -8
- package/api/build/build.type.d.mts +91 -2
- package/api/build/build.type.mjs +52 -8
- package/api/build/help/help.d.mts +12 -5
- package/api/build/help/help.mjs +69 -6
- package/api/build/kit/kit.d.mts +4 -1
- package/api/build/kit/kit.mjs +208 -53
- package/api/build/kit/rank.d.mts +44 -0
- package/api/build/kit/rank.mjs +432 -0
- package/api/build/kit/rank.test.mjs +196 -0
- package/api/component/_adapter.d.mts +31 -12
- package/api/component/_adapter.mjs +79 -15
- package/api/component/component.d.mts +6 -3
- package/api/component/component.doc.mjs +49 -19
- package/api/component/component.mjs +339 -22
- package/api/component/component.test.mjs +38 -0
- package/api/component/component.type.d.mts +47 -11
- package/api/component/component.type.mjs +76 -24
- package/api/component/detail/blocks/blocks.d.mts +2 -1
- package/api/component/detail/blocks/blocks.mjs +4 -3
- package/api/component/list/list.d.mts +0 -5
- package/api/component/list/list.mjs +40 -11
- package/api/discover/_adapter.d.mts +114 -6
- package/api/discover/_adapter.mjs +372 -17
- package/api/discover/_adapter.test.mjs +215 -0
- package/api/discover/_catalog-view.d.mts +115 -0
- package/api/discover/_catalog-view.mjs +203 -0
- package/api/discover/_catalog-view.test.mjs +128 -0
- package/api/discover/detail/detail.d.mts +18 -6
- package/api/discover/detail/detail.mjs +67 -13
- package/api/discover/detail/detail.test.mjs +85 -0
- package/api/discover/detail/item/item.d.mts +26 -0
- package/api/discover/detail/item/item.mjs +78 -0
- package/api/discover/detail/item/item.test.mjs +73 -0
- package/api/discover/discover.d.mts +3 -9
- package/api/discover/discover.doc.mjs +62 -18
- package/api/discover/discover.mjs +220 -36
- package/api/discover/discover.test.mjs +11 -2
- package/api/discover/discover.type.d.mts +150 -11
- package/api/discover/discover.type.mjs +107 -17
- package/api/discover/list/list.d.mts +20 -6
- package/api/discover/list/list.mjs +45 -12
- package/api/discover/list/list.test.mjs +46 -0
- package/api/discover/search/search.d.mts +18 -16
- package/api/discover/search/search.mjs +102 -56
- package/api/discover/search/search.test.mjs +144 -10
- package/api/docs/_adapter.d.mts +277 -41
- package/api/docs/_adapter.mjs +993 -108
- package/api/docs/compiled-topics.test.mjs +78 -0
- package/api/docs/detail/detail.mjs +22 -63
- package/api/docs/detail/section/section.d.mts +1 -1
- package/api/docs/detail/section/section.mjs +54 -19
- package/api/docs/detail/section/section.test.mjs +50 -0
- package/api/docs/docOverlays.test.mjs +27 -1
- package/api/docs/docs.d.mts +10 -3
- package/api/docs/docs.doc.mjs +55 -16
- package/api/docs/docs.mjs +53 -10
- package/api/docs/docs.type.d.mts +221 -5
- package/api/docs/docs.type.mjs +153 -11
- package/api/docs/index/index.d.mts +18 -0
- package/api/docs/index/index.mjs +40 -0
- package/api/docs/index/index.test.mjs +62 -0
- package/api/docs/list/list.mjs +28 -12
- package/api/docs/node/node.d.mts +43 -0
- package/api/docs/node/node.mjs +192 -0
- package/api/docs/reference-blocks.test.mjs +406 -0
- package/api/doctor/doctor.d.mts +104 -1
- package/api/doctor/doctor.doc.mjs +18 -8
- package/api/doctor/doctor.mjs +635 -7
- package/api/doctor/doctor.test.mjs +732 -11
- package/api/doctor/doctor.type.d.mts +1 -1
- package/api/doctor/doctor.type.mjs +1 -1
- package/api/gap-report/gap-report.doc.mjs +27 -14
- package/api/hook/_adapter.mjs +19 -5
- package/api/hook/hook.doc.mjs +7 -3
- package/api/hook/hook.type.d.mts +3 -3
- package/api/hook/hook.type.mjs +11 -11
- package/api/hook/list/list.d.mts +2 -2
- package/api/hook/list/list.mjs +69 -17
- package/api/index.d.mts +2 -1
- package/api/index.mjs +6 -3
- package/api/init/init.doc.mjs +22 -12
- package/api/init/init.test.mjs +41 -1
- package/api/init/remove/remove.mjs +1 -1
- package/api/init/run/run.mjs +20 -10
- package/api/integration/add-contribution.component-names.test.mjs +120 -0
- package/api/integration/add-contribution.d.mts +2 -1
- package/api/integration/add-contribution.mjs +130 -15
- package/api/integration/add-contribution.test.mjs +258 -7
- package/api/integration/add-helpers.d.mts +5 -2
- package/api/integration/add-helpers.mjs +36 -9
- package/api/integration/add-theme.mjs +56 -65
- package/api/integration/add-theme.test.mjs +139 -21
- package/api/integration/authoring-checks.mjs +138 -28
- package/api/integration/authoring-checks.test.mjs +179 -7
- package/api/integration/authoring-checks.type.mjs +6 -1
- package/api/integration/integration-authoring.type.d.mts +3 -1
- package/api/integration/integration-authoring.type.mjs +2 -0
- package/api/integration/integration-block-exports.test.mjs +10 -6
- package/api/integration/integrationAdd.doc.mjs +14 -4
- package/api/integration/integrationAddAgentDoc.doc.mjs +1 -0
- package/api/integration/integrationAddCodemod.doc.mjs +1 -0
- package/api/integration/integrationAddComponent.doc.mjs +2 -1
- package/api/integration/integrationAddDoc.doc.mjs +8 -1
- package/api/integration/integrationAddTemplate.doc.mjs +1 -0
- package/api/integration/integrationAddTheme.doc.mjs +6 -5
- package/api/integration/integrationComponentConflicts.doc.mjs +1 -0
- package/api/integration/integrationDocConflicts.doc.mjs +2 -1
- package/api/integration/integrationPackCheck.doc.mjs +5 -4
- package/api/integration/integrationTemplateConflicts.doc.d.mts +3 -0
- package/api/integration/integrationTemplateConflicts.doc.mjs +4 -0
- package/api/integration/pack-check.lifecycle-output.test.mjs +107 -0
- package/api/integration/pack-check.mjs +160 -11
- package/api/integration/pack-check.test.mjs +477 -47
- package/api/integration/pack-check.type.d.mts +26 -2
- package/api/integration/pack-check.type.mjs +15 -2
- package/api/integration/summarizeIssues.doc.mjs +1 -0
- package/api/integration/template-conflict-compatibility.test.mjs +73 -0
- package/api/integration/validate-integration-fixes.test.mjs +1389 -0
- package/api/integration/validate-integration.mjs +52 -102
- package/api/integration/validate-integration.test.mjs +179 -26
- package/api/integration/validate-unread-theme-folders.test.mjs +110 -0
- package/api/integration/validateIntegration.doc.mjs +3 -2
- package/api/json/assertResponse.doc.mjs +2 -1
- package/api/json/envelope-types.test.mjs +76 -0
- package/api/json/index.ts +2 -0
- package/api/json/isError.doc.mjs +2 -1
- package/api/json/parseResponse.doc.mjs +3 -2
- package/api/layout/_adapter.mjs +20 -5
- package/api/layout/expand/expand.mjs +7 -5
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.mjs +2 -1
- package/api/layout/layoutCheck.doc.mjs +1 -0
- package/api/layout/layoutExpand.doc.mjs +2 -1
- package/api/layout/layoutGrammar.doc.mjs +1 -0
- package/api/search/search-return-type.test.mjs +54 -0
- package/api/search/search.d.mts +89 -12
- package/api/search/search.doc.mjs +8 -2
- package/api/search/search.mjs +697 -97
- package/api/search/search.type.d.mts +15 -3
- package/api/search/search.type.mjs +5 -2
- package/api/swizzle/copy/copy.mjs +28 -11
- package/api/swizzle/swizzle.doc.mjs +8 -5
- package/api/swizzle/swizzle.type.d.mts +2 -2
- package/api/swizzle/swizzle.type.mjs +2 -2
- package/api/template/copy/copy.mjs +18 -24
- package/api/template/copy/copy.test.mjs +26 -0
- package/api/template/list/list.mjs +1 -0
- package/api/template/table-floating-bulk-actions.test.mjs +66 -0
- package/api/template/template-integration.test.mjs +1072 -3
- package/api/template/template-suffix.test.mjs +41 -21
- package/api/template/template.d.mts +1 -1
- package/api/template/template.doc.mjs +32 -9
- package/api/template/template.mjs +45 -8
- package/api/template/template.type.d.mts +12 -14
- package/api/template/template.type.mjs +15 -14
- package/api/theme/_adapter.d.mts +2 -3
- package/api/theme/_adapter.mjs +4 -5
- package/api/theme/add/add.binary.test.mjs +84 -0
- package/api/theme/add/add.mjs +31 -22
- package/api/theme/add/add.rollback.test.mjs +158 -0
- package/api/theme/add/add.staging.test.mjs +83 -0
- package/api/theme/add/add.test.mjs +14 -1
- package/api/theme/build/build.family.test.mjs +7 -12
- package/api/theme/build/build.mjs +140 -59
- package/api/theme/build/build.public-component-vars.test.mjs +1 -1
- package/api/theme/build/build.receipt-doc.test.mjs +111 -0
- package/api/theme/build/build.rollback.test.mjs +148 -0
- package/api/theme/build/build.test.mjs +127 -0
- package/api/theme/build/font-warning.mjs +3 -3
- package/api/theme/build/font-warning.test.mjs +5 -2
- package/api/theme/generateTonalPalette.doc.mjs +2 -2
- package/api/theme/integration-themes.test.mjs +39 -28
- package/api/theme/list/list.test.mjs +19 -20
- package/api/theme/listThemes.doc.mjs +6 -5
- package/api/theme/palette/generate/generate.mjs +8 -3
- package/api/theme/palette/generate/generate.test.mjs +96 -0
- package/api/theme/palette/generate/generator.d.mts +10 -13
- package/api/theme/palette/generate/generator.mjs +15 -4
- package/api/theme/palette/generate/generator.test.mjs +10 -0
- package/api/theme/template/template.mjs +11 -2
- package/api/theme/template/template.test.mjs +20 -0
- package/api/theme/theme.type.d.mts +170 -11
- package/api/theme/theme.type.mjs +94 -27
- package/api/theme/themeAdd.doc.mjs +12 -12
- package/api/theme/themeBuild.doc.mjs +21 -17
- package/api/theme/themeList.doc.mjs +6 -3
- package/api/theme/themeListAvailable.doc.mjs +6 -3
- package/api/theme/themePaletteGenerate.doc.mjs +16 -8
- package/api/theme/themeTargets.doc.mjs +4 -2
- package/api/theme/themeTemplate.doc.mjs +8 -3
- package/api/upgrade/_adapter.d.mts +32 -5
- package/api/upgrade/_adapter.mjs +139 -22
- package/api/upgrade/list/list.mjs +2 -1
- package/api/upgrade/list/list.test.mjs +73 -0
- package/api/upgrade/project-context.test.mjs +272 -0
- package/api/upgrade/provider-agreement.test.mjs +152 -0
- package/api/upgrade/run/files-changed.test.mjs +111 -0
- package/api/upgrade/run/run.mjs +358 -59
- package/api/upgrade/status/status.mjs +2 -2
- package/api/upgrade/upgrade.doc.mjs +32 -23
- package/api/upgrade/upgrade.type.d.mts +43 -5
- package/api/upgrade/upgrade.type.mjs +29 -13
- package/assets/codemods/__tests__/registry.test.mjs +1 -0
- package/assets/codemods/__tests__/runner.test.mjs +332 -8
- package/assets/codemods/file-count.test.mjs +163 -0
- package/assets/codemods/integration-discovery.mjs +48 -4
- package/assets/codemods/integration-discovery.test.mjs +73 -0
- package/assets/codemods/integration-runner.mjs +59 -7
- package/assets/codemods/integration-runner.protection.test.mjs +153 -0
- package/assets/codemods/registry.mjs +1 -0
- package/assets/codemods/run-codemod.mjs +177 -34
- package/assets/codemods/runner.mjs +353 -104
- package/assets/codemods/term-log.mjs +32 -8
- package/assets/codemods/term-log.test.mjs +19 -1
- package/assets/codemods/transform-prop.mjs +109 -0
- package/assets/codemods/transform-prop.test.mjs +95 -0
- package/assets/codemods/transforms/v0.0.14/__tests__/rename-status-variants.test.mjs +86 -165
- package/assets/codemods/transforms/v0.0.14/rename-status-variants.mjs +72 -210
- package/assets/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +83 -115
- package/assets/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +57 -186
- package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
- package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
- package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
- package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
- package/assets/codemods/transforms/v0.6.4/__tests__/migrate-native-picker-to-presentation.test.mjs +63 -0
- package/assets/codemods/transforms/v0.6.4/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +220 -0
- package/assets/codemods/transforms/v0.6.4/index.mjs +31 -0
- package/assets/codemods/transforms/v0.6.4/migrate-native-picker-to-presentation.mjs +148 -0
- package/assets/codemods/transforms/v0.6.4/migrate-theme-catalog-to-descriptors.mjs +141 -0
- package/assets/docs/README.md +12 -1
- package/assets/docs/authoring.doc.mjs +14 -0
- package/assets/docs/browser-support.doc.mjs +11 -11
- package/assets/docs/color.doc.mjs +8 -2
- package/assets/docs/elevation.doc.mjs +6 -4
- package/assets/docs/getting-started.doc.mjs +6 -17
- package/assets/docs/icons.doc.mjs +2 -21
- package/assets/docs/illustrations.doc.mjs +7 -15
- package/assets/docs/internationalization.doc.mjs +7 -5
- package/assets/docs/layout.doc.dense.mjs +132 -84
- package/assets/docs/layout.doc.mjs +134 -78
- package/assets/docs/migration.doc.mjs +19 -21
- package/assets/docs/motion.doc.mjs +16 -3
- package/assets/docs/principles.doc.dense.mjs +5 -5
- package/assets/docs/principles.doc.mjs +14 -6
- package/assets/docs/principles.doc.zh.mjs +6 -6
- package/assets/docs/shape.doc.mjs +8 -3
- package/assets/docs/spacing.doc.mjs +7 -2
- package/assets/docs/styling-libraries.doc.mjs +10 -6
- package/assets/docs/styling.doc.mjs +22 -26
- package/assets/docs/theme.doc.dense.mjs +58 -18
- package/assets/docs/theme.doc.mjs +60 -50
- package/assets/docs/theme.doc.zh.mjs +9 -8
- package/assets/docs/tokens.doc.dense.mjs +2 -2
- package/assets/docs/tokens.doc.mjs +390 -9
- package/assets/docs/tokens.doc.zh.mjs +2 -2
- package/assets/docs/tree/add-a-component.doc.mjs +75 -0
- package/assets/docs/tree/add-a-theme.doc.mjs +85 -0
- package/assets/docs/tree/add-a-topic.doc.mjs +144 -0
- package/assets/docs/tree/agent-guidance.doc.mjs +138 -0
- package/assets/docs/tree/api.doc.mjs +30 -0
- package/assets/docs/tree/block-template.doc.mjs +130 -0
- package/assets/docs/tree/build-the-template.doc.mjs +28 -0
- package/assets/docs/tree/building-blocks.doc.mjs +46 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +137 -0
- package/assets/docs/tree/checks.doc.mjs +119 -0
- package/assets/docs/tree/cli.doc.mjs +23 -0
- package/assets/docs/tree/codemods.doc.mjs +147 -0
- package/assets/docs/tree/commands.doc.mjs +25 -0
- package/assets/docs/tree/component-family.doc.mjs +113 -0
- package/assets/docs/tree/component-imports.doc.mjs +69 -0
- package/assets/docs/tree/component-lookups.doc.mjs +149 -0
- package/assets/docs/tree/components.doc.mjs +23 -0
- package/assets/docs/tree/configuration.doc.mjs +23 -0
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +182 -0
- package/assets/docs/tree/define-the-theme.doc.mjs +118 -0
- package/assets/docs/tree/describe-the-component.doc.mjs +57 -0
- package/assets/docs/tree/docs.doc.mjs +21 -0
- package/assets/docs/tree/document-the-template.doc.mjs +28 -0
- package/assets/docs/tree/document-the-theme.doc.mjs +68 -0
- package/assets/docs/tree/export-template-assets.doc.mjs +147 -0
- package/assets/docs/tree/extend-or-replace.doc.mjs +103 -0
- package/assets/docs/tree/fonts-and-assets.doc.mjs +106 -0
- package/assets/docs/tree/generate-a-palette.doc.mjs +66 -0
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +105 -0
- package/assets/docs/tree/help.doc.mjs +16 -0
- package/assets/docs/tree/integrations.doc.mjs +40 -0
- package/assets/docs/tree/links.doc.mjs +98 -0
- package/assets/docs/tree/package-and-test.doc.mjs +32 -0
- package/assets/docs/tree/page-template.doc.mjs +71 -0
- package/assets/docs/tree/publishing.doc.mjs +111 -0
- package/assets/docs/tree/quick-start.doc.mjs +272 -0
- package/assets/docs/tree/replace-a-core-component.doc.mjs +104 -0
- package/assets/docs/tree/replace-a-core-template.doc.mjs +172 -0
- package/assets/docs/tree/sections-and-placement.doc.mjs +108 -0
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +59 -0
- package/assets/docs/tree/ship.doc.mjs +16 -0
- package/assets/docs/tree/short-and-findable.doc.mjs +108 -0
- package/assets/docs/tree/single-component.doc.mjs +165 -0
- package/assets/docs/tree/start-a-template.doc.mjs +143 -0
- package/assets/docs/tree/subcomponent.doc.mjs +115 -0
- package/assets/docs/tree/template-assets.doc.mjs +64 -0
- package/assets/docs/tree/template-doc-overview.doc.mjs +109 -0
- package/assets/docs/tree/template-fonts.doc.mjs +102 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +452 -0
- package/assets/docs/tree/template-icons.doc.mjs +97 -0
- package/assets/docs/tree/template-images-media.doc.mjs +127 -0
- package/assets/docs/tree/template-styles.doc.mjs +93 -0
- package/assets/docs/tree/templates.doc.mjs +34 -0
- package/assets/docs/tree/test-in-an-app.doc.mjs +115 -0
- package/assets/docs/tree/test-template-in-app.doc.mjs +128 -0
- package/assets/docs/tree/themes.doc.mjs +39 -0
- package/assets/docs/tree/troubleshooting.doc.mjs +149 -0
- package/assets/docs/tree/upgrading.doc.mjs +103 -0
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +51 -0
- package/assets/docs/tree/verify-packed-template.doc.mjs +77 -0
- package/assets/docs/tree/versioning.doc.mjs +161 -0
- package/assets/docs/tree/write-good-templates.doc.mjs +64 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +154 -0
- package/assets/docs/typography.doc.mjs +24 -4
- package/assets/docs/working-with-ai.doc.mjs +34 -26
- package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.doc.mjs +1 -1
- package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.tsx +1 -3
- package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +1 -1
- package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
- package/assets/templates/blocks/components/Item/ItemDocumentTabs.doc.mjs +14 -0
- package/assets/templates/blocks/components/Item/ItemDocumentTabs.tsx +100 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.tsx +61 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.tsx +126 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.tsx +86 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.tsx +99 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.tsx +122 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.tsx +95 -0
- package/assets/templates/blocks/components/Table/TableBulkActionsTable.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/TableBulkActionsTable.tsx +71 -0
- package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.doc.mjs +19 -0
- package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.tsx +139 -0
- package/assets/templates/blocks/components/TimeInput/TimeInputConstrained.tsx +1 -0
- package/assets/templates/blocks/components/Timer/TimerFormats.doc.mjs +14 -0
- package/assets/templates/blocks/components/Timer/TimerFormats.tsx +34 -0
- package/assets/templates/blocks/components/Timer/TimerInline.doc.mjs +14 -0
- package/assets/templates/blocks/components/Timer/TimerInline.tsx +14 -0
- package/assets/templates/blocks/components/Timer/TimerShowcase.doc.mjs +13 -0
- package/assets/templates/blocks/components/Timer/TimerShowcase.tsx +47 -0
- package/assets/templates/blocks/components/Timer/TimerTypography.doc.mjs +14 -0
- package/assets/templates/blocks/components/Timer/TimerTypography.tsx +31 -0
- package/assets/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +19 -3
- package/assets/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +383 -65
- package/assets/templates/pages/table-tree/page.tsx +1704 -0
- package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
- package/assets/templates/themes/butter/butterTheme.doc.mjs +11 -0
- package/assets/templates/themes/chocolate/chocolateTheme.doc.mjs +11 -0
- package/assets/templates/themes/gothic/gothicTheme.doc.mjs +11 -0
- package/assets/templates/themes/matcha/matchaTheme.doc.mjs +11 -0
- package/assets/templates/themes/neutral/neutralTheme.doc.mjs +11 -0
- package/assets/templates/themes/stone/stoneTheme.doc.mjs +11 -0
- package/assets/templates/themes/y2k/y2kTheme.doc.mjs +11 -0
- package/authoring/_shared/contract.ts +22 -0
- package/authoring/codemod/codemod.doc.mjs +7 -2
- package/authoring/codemod/parse.d.mts +8 -8
- package/authoring/codemod/parse.mjs +8 -6
- package/authoring/codemod/type.ts +12 -0
- package/authoring/config/config.doc.mjs +11 -3
- package/authoring/config/debug-composition.test.mjs +92 -0
- package/authoring/config/parse.d.mts +15 -13
- package/authoring/config/parse.mjs +27 -8
- package/authoring/config/parse.test.mjs +8 -0
- package/authoring/config/type.ts +29 -6
- package/authoring/debug/debug.doc.d.mts +11 -0
- package/authoring/debug/debug.doc.mjs +182 -0
- package/authoring/debug/parse.d.mts +8 -8
- package/authoring/debug/parse.mjs +3 -3
- package/authoring/discover/discover.doc.d.mts +13 -0
- package/authoring/discover/discover.doc.mjs +138 -0
- package/authoring/discover/parse.d.mts +24 -0
- package/authoring/discover/parse.mjs +128 -0
- package/authoring/discover/parse.test.mjs +124 -0
- package/authoring/discover/type.ts +87 -0
- package/authoring/doctypes/_schema.d.mts +792 -24
- package/authoring/doctypes/_schema.mjs +549 -39
- package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
- package/authoring/doctypes/base/graph-fields.doc.mjs +64 -0
- package/authoring/doctypes/base/type.ts +43 -0
- package/authoring/doctypes/command/command.doc.mjs +4 -3
- package/authoring/doctypes/command/parse.d.mts +2 -2
- package/authoring/doctypes/command/parse.mjs +1 -1
- package/authoring/doctypes/command/type.ts +5 -4
- package/authoring/doctypes/component/component.doc.mjs +12 -3
- package/authoring/doctypes/component/parse.d.mts +2 -2
- package/authoring/doctypes/component/parse.mjs +1 -1
- package/authoring/doctypes/component/type.ts +14 -5
- package/authoring/doctypes/doctypes-new.test.mjs +48 -6
- package/authoring/doctypes/enum/enum.doc.mjs +1 -1
- package/authoring/doctypes/enum/parse.d.mts +2 -2
- package/authoring/doctypes/enum/parse.mjs +1 -1
- package/authoring/doctypes/enum/type.ts +4 -2
- package/authoring/doctypes/function/function.doc.mjs +7 -2
- package/authoring/doctypes/function/parse.d.mts +2 -2
- package/authoring/doctypes/function/parse.mjs +1 -1
- package/authoring/doctypes/function/type.ts +9 -4
- package/authoring/doctypes/hook/hook.doc.mjs +4 -0
- package/authoring/doctypes/hook/parse.d.mts +2 -2
- package/authoring/doctypes/hook/parse.mjs +1 -1
- package/authoring/doctypes/hook/type.ts +5 -4
- package/authoring/doctypes/legacy.d.mts +8 -6
- package/authoring/doctypes/legacy.mjs +5 -4
- package/authoring/doctypes/load-contract.test.mjs +233 -0
- package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
- package/authoring/doctypes/namespace/namespace.doc.mjs +128 -0
- package/authoring/doctypes/namespace/parse.d.mts +12 -0
- package/authoring/doctypes/namespace/parse.mjs +25 -0
- package/authoring/doctypes/namespace/parse.test.mjs +163 -0
- package/authoring/doctypes/namespace/type.ts +74 -0
- package/authoring/doctypes/parse.d.mts +22 -18
- package/authoring/doctypes/parse.mjs +22 -11
- package/authoring/doctypes/parse.test.mjs +77 -3
- package/authoring/doctypes/reference/parse.d.mts +2 -2
- package/authoring/doctypes/reference/parse.mjs +8 -5
- package/authoring/doctypes/reference/reference.doc.mjs +55 -6
- package/authoring/doctypes/reference/type.ts +75 -7
- package/authoring/doctypes/schema/parse.d.mts +2 -2
- package/authoring/doctypes/schema/parse.mjs +1 -1
- package/authoring/doctypes/schema/schema.doc.mjs +2 -2
- package/authoring/doctypes/schema/type.ts +4 -4
- package/authoring/doctypes/template/parse.d.mts +94 -1
- package/authoring/doctypes/template/parse.mjs +40 -2
- package/authoring/doctypes/template/parse.test.mjs +26 -2
- package/authoring/doctypes/template/template.doc.mjs +13 -3
- package/authoring/doctypes/template/type.ts +13 -2
- package/authoring/doctypes/theme/parse.d.mts +35 -0
- package/authoring/doctypes/theme/parse.mjs +76 -0
- package/authoring/doctypes/theme/theme.doc.d.mts +9 -0
- package/authoring/doctypes/theme/theme.doc.mjs +79 -0
- package/authoring/doctypes/theme/type.ts +42 -0
- package/authoring/doctypes/types.ts +12 -10
- package/authoring/gap-report/gap-report.doc.d.mts +12 -0
- package/authoring/gap-report/gap-report.doc.mjs +183 -0
- package/authoring/gap-report/parse.d.mts +10 -10
- package/authoring/gap-report/parse.mjs +6 -6
- package/authoring/gap-report/type.ts +1 -1
- package/authoring/identity/identity.doc.d.mts +9 -0
- package/authoring/identity/identity.doc.mjs +61 -0
- package/authoring/identity/type.ts +132 -0
- package/authoring/index.d.mts +3 -0
- package/authoring/index.d.ts +62 -17
- package/authoring/index.mjs +4 -1
- package/authoring/integration/integration.doc.mjs +22 -13
- package/authoring/integration/parse.d.mts +2 -2
- package/authoring/integration/parse.mjs +1 -1
- package/authoring/integration/parse.test.mjs +10 -1
- package/authoring/integration/schema.d.mts +6 -4
- package/authoring/integration/schema.mjs +9 -3
- package/authoring/integration/type.ts +19 -8
- package/authoring/shadcn/receipt.d.mts +6 -6
- package/clients/cli/__tests__/cliManifest.test.ts +27 -29
- package/clients/cli/command-load-failure.test.mjs +83 -0
- package/clients/cli/commands/blog.doc.mjs +1 -1
- package/clients/cli/commands/blog.mjs +23 -8
- package/clients/cli/commands/blog.test.mjs +42 -1
- package/clients/cli/commands/build-theme.adaptations.test.mjs +100 -1
- package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
- package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
- package/clients/cli/commands/build-theme.mjs +16 -50
- package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
- package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
- package/clients/cli/commands/build.doc.mjs +16 -8
- package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
- package/clients/cli/commands/build.mjs +137 -114
- package/clients/cli/commands/build.playbook.test.mjs +75 -0
- package/clients/cli/commands/build.text-fields.test.mjs +81 -0
- package/clients/cli/commands/component/index.mjs +153 -61
- package/clients/cli/commands/component-batch.test.mjs +341 -0
- package/clients/cli/commands/component-ownership.test.mjs +92 -3
- package/clients/cli/commands/component-package.test.mjs +46 -0
- package/clients/cli/commands/component-resolution.test.mjs +21 -0
- package/clients/cli/commands/component.doc.mjs +28 -10
- package/clients/cli/commands/component.test.mjs +19 -0
- package/clients/cli/commands/detail-levels.test.mjs +2 -2
- package/clients/cli/commands/discover.broken-integration.test.mjs +52 -5
- package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
- package/clients/cli/commands/discover.doc.mjs +55 -9
- package/clients/cli/commands/discover.mjs +393 -118
- package/clients/cli/commands/discover.sources.test.mjs +267 -0
- package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
- package/clients/cli/commands/docs.doc.mjs +28 -6
- package/clients/cli/commands/docs.mjs +295 -38
- package/clients/cli/commands/doctor-integration-components.doc.mjs +1 -1
- package/clients/cli/commands/doctor-integration-docs.doc.mjs +6 -5
- package/clients/cli/commands/doctor-integration-templates.doc.mjs +15 -8
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +1 -1
- package/clients/cli/commands/doctor-integration.doc.mjs +1 -1
- package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
- package/clients/cli/commands/doctor-integration.test.mjs +143 -8
- package/clients/cli/commands/doctor.doc.mjs +4 -2
- package/clients/cli/commands/doctor.mjs +108 -37
- package/clients/cli/commands/doctor.test.mjs +42 -0
- package/clients/cli/commands/gap-report.doc.mjs +27 -15
- package/clients/cli/commands/gap-report.test.mjs +72 -0
- package/clients/cli/commands/hook/index.mjs +7 -17
- package/clients/cli/commands/hook.doc.mjs +1 -1
- package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
- package/clients/cli/commands/init.doc.mjs +24 -10
- package/clients/cli/commands/init.flag-help.test.mjs +153 -0
- package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
- package/clients/cli/commands/integration-add.doc.mjs +39 -13
- package/clients/cli/commands/integration-authoring.test.mjs +74 -19
- package/clients/cli/commands/integration-pack.doc.mjs +6 -10
- package/clients/cli/commands/integration-real-world.test.mjs +4 -10
- package/clients/cli/commands/integration-verify.doc.mjs +22 -0
- package/clients/cli/commands/integration.doc.mjs +5 -5
- package/clients/cli/commands/integration.mjs +75 -43
- package/clients/cli/commands/interactive-guard.test.mjs +101 -24
- package/clients/cli/commands/json-contract.test.mjs +33 -0
- package/clients/cli/commands/layout-check.doc.mjs +15 -4
- package/clients/cli/commands/layout-expand.doc.mjs +22 -5
- package/clients/cli/commands/layout-grammar.doc.mjs +1 -1
- package/clients/cli/commands/layout.doc.mjs +3 -3
- package/clients/cli/commands/layout.mjs +21 -9
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/manifest.doc.mjs +2 -2
- package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
- package/clients/cli/commands/search.doc.mjs +16 -6
- package/clients/cli/commands/search.mjs +49 -11
- package/clients/cli/commands/search.test.mjs +92 -0
- package/clients/cli/commands/setup-nudge.test.mjs +6 -0
- package/clients/cli/commands/swizzle.doc.mjs +4 -3
- package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
- package/clients/cli/commands/template.doc.mjs +53 -14
- package/clients/cli/commands/template.flag-help.test.mjs +117 -0
- package/clients/cli/commands/template.mjs +4 -91
- package/clients/cli/commands/template.path-help.test.mjs +40 -0
- package/clients/cli/commands/text-json-parity.test.mjs +725 -0
- package/clients/cli/commands/theme-add.doc.mjs +5 -4
- package/clients/cli/commands/theme-build.doc.mjs +8 -7
- package/clients/cli/commands/theme-list.doc.mjs +2 -2
- package/clients/cli/commands/theme-palette-generate.doc.mjs +12 -7
- package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
- package/clients/cli/commands/theme-palette.doc.mjs +2 -3
- package/clients/cli/commands/theme-targets.behavior.test.mjs +4 -3
- package/clients/cli/commands/theme-targets.doc.mjs +3 -3
- package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
- package/clients/cli/commands/theme-template.doc.mjs +2 -2
- package/clients/cli/commands/theme.doc.mjs +3 -2
- package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
- package/clients/cli/commands/upgrade.doc.mjs +83 -12
- package/clients/cli/commands/upgrade.file-protection.test.mjs +228 -0
- package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
- package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
- package/clients/cli/commands/upgrade.mjs +29 -7
- package/clients/cli/formatters/index.mjs +164 -1
- package/clients/cli/formatters/index.test.mjs +97 -0
- package/clients/cli/index.mjs +47 -34
- package/clients/cli/latest-version-env.test.mjs +50 -0
- package/clients/cli/lib/cli-error.test.mjs +7 -0
- package/clients/cli/lib/component-format.mjs +9 -9
- package/clients/cli/lib/component-format.test.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +56 -6
- package/clients/cli/lib/define-command.test.mjs +54 -0
- package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
- package/clients/cli/lib/exit-codes.test.mjs +113 -0
- package/clients/cli/lib/hook-format.mjs +19 -10
- package/clients/cli/lib/json-shim.mjs +62 -16
- package/clients/cli/lib/json-shim.test.mjs +83 -0
- package/clients/cli/lib/manifest.d.ts +2 -0
- package/clients/cli/lib/manifest.mjs +53 -6
- package/clients/cli/lib/manifest.test.mjs +22 -2
- package/clients/cli/lib/parse-error-format.test.mjs +81 -0
- package/foundation/agent-docs/agent-docs.d.mts +7 -2
- package/foundation/agent-docs/agent-docs.mjs +83 -13
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
- package/foundation/config/integration-debug.test.mjs +28 -3
- package/foundation/config/project-themes.test.mjs +11 -19
- package/foundation/config/project.d.mts +20 -11
- package/foundation/config/project.mjs +263 -91
- package/foundation/config/project.test.mjs +270 -21
- package/foundation/discovery/authoring-self-docs.d.mts +87 -0
- package/foundation/discovery/authoring-self-docs.mjs +237 -0
- package/foundation/discovery/authoring-self-docs.test.mjs +174 -0
- package/foundation/discovery/authoring-surface.d.mts +74 -0
- package/foundation/discovery/authoring-surface.mjs +525 -0
- package/foundation/discovery/authoring-surface.test.mjs +392 -0
- package/foundation/discovery/cli-self-docs.d.mts +119 -0
- package/foundation/discovery/cli-self-docs.mjs +504 -0
- package/foundation/discovery/cli-self-docs.test.mjs +395 -0
- package/foundation/discovery/component-discovery.d.mts +39 -1
- package/foundation/discovery/component-discovery.mjs +50 -1
- package/foundation/discovery/component-loader.d.mts +35 -38
- package/foundation/discovery/component-loader.mjs +53 -222
- package/foundation/discovery/docs-discovery.d.mts +119 -11
- package/foundation/discovery/docs-discovery.mjs +427 -108
- package/foundation/discovery/docs-discovery.test.mjs +386 -20
- package/foundation/discovery/docs-output-budget.d.mts +28 -0
- package/foundation/discovery/docs-output-budget.mjs +50 -0
- package/foundation/discovery/docs-section-key.d.mts +116 -0
- package/foundation/discovery/docs-section-key.mjs +322 -0
- package/foundation/discovery/docs-section-key.test.mjs +246 -0
- package/foundation/discovery/template-adapter.d.mts +113 -11
- package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
- package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
- package/foundation/discovery/template-adapter.mjs +774 -83
- package/foundation/discovery/template-adapter.test.mjs +57 -0
- package/foundation/discovery/template-conflict-release.d.mts +13 -0
- package/foundation/discovery/template-conflict-release.mjs +40 -0
- package/foundation/discovery/template-conflict-release.test.mjs +40 -0
- package/foundation/discovery/theme-discovery.d.mts +67 -7
- package/foundation/discovery/theme-discovery.mjs +916 -186
- package/foundation/discovery/theme-discovery.test.mjs +613 -219
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/doc-compiler/bundle.d.mts +47 -0
- package/foundation/doc-compiler/bundle.mjs +278 -0
- package/foundation/doc-compiler/bundle.test.mjs +266 -0
- package/foundation/doc-compiler/compile.d.mts +343 -0
- package/foundation/doc-compiler/compile.mjs +558 -0
- package/foundation/doc-compiler/diagnostics.d.mts +126 -0
- package/foundation/doc-compiler/diagnostics.mjs +305 -0
- package/foundation/doc-compiler/doc-compiler.test.mjs +714 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +1643 -0
- package/foundation/doc-compiler/import.d.mts +24 -0
- package/foundation/doc-compiler/import.mjs +59 -0
- package/foundation/doc-compiler/inputs.d.mts +102 -0
- package/foundation/doc-compiler/inputs.mjs +291 -0
- package/foundation/doc-compiler/inputs.test.mjs +298 -0
- package/foundation/doc-compiler/ir.d.mts +22 -0
- package/foundation/doc-compiler/ir.mjs +471 -0
- package/foundation/doc-compiler/lenses.d.mts +36 -0
- package/foundation/doc-compiler/lenses.mjs +173 -0
- package/foundation/doc-compiler/links.d.mts +162 -0
- package/foundation/doc-compiler/links.mjs +294 -0
- package/foundation/doc-compiler/links.test.mjs +192 -0
- package/foundation/doc-compiler/lower-doc.test.mjs +495 -0
- package/foundation/doc-compiler/overlays.d.mts +37 -0
- package/foundation/doc-compiler/overlays.mjs +206 -0
- package/foundation/doc-compiler/parse-readable.d.mts +9 -0
- package/foundation/doc-compiler/parse-readable.mjs +29 -0
- package/foundation/doc-compiler/read.d.mts +127 -0
- package/foundation/doc-compiler/read.mjs +325 -0
- package/foundation/doc-compiler/read.test.mjs +313 -0
- package/foundation/doc-compiler/source.d.mts +33 -0
- package/foundation/doc-compiler/source.mjs +128 -0
- package/foundation/doc-compiler/tree.d.mts +292 -0
- package/foundation/doc-compiler/tree.mjs +881 -0
- package/foundation/fs/file-protection.d.mts +33 -0
- package/foundation/fs/file-protection.mjs +825 -0
- package/foundation/fs/file-protection.test.mjs +250 -0
- package/foundation/fs/module-loader.d.mts +1 -0
- package/foundation/fs/module-loader.mjs +50 -1
- package/foundation/fs/module-loader.stdout.test.mjs +332 -0
- package/foundation/fs/path-safety.d.mts +3 -2
- package/foundation/fs/path-safety.mjs +49 -19
- package/foundation/fs/path-safety.test.mjs +50 -0
- package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
- package/foundation/identity/provider-identity.d.mts +90 -0
- package/foundation/identity/provider-identity.mjs +320 -0
- package/foundation/identity/provider-identity.test.mjs +254 -0
- package/foundation/identity/providers.d.mts +7 -0
- package/foundation/identity/providers.mjs +16 -0
- package/foundation/integrations/autolink.d.mts +58 -1
- package/foundation/integrations/autolink.mjs +143 -45
- package/foundation/integrations/autolink.test.mjs +1 -1
- package/foundation/integrations/cli-requirement.d.mts +65 -0
- package/foundation/integrations/cli-requirement.mjs +189 -0
- package/foundation/integrations/cli-requirement.test.mjs +89 -0
- package/foundation/integrations/contribution-fixes.d.mts +145 -0
- package/foundation/integrations/contribution-fixes.mjs +1284 -0
- package/foundation/integrations/contribution-inventory.d.mts +3 -2
- package/foundation/integrations/contribution-inventory.mjs +28 -25
- package/foundation/integrations/contribution-inventory.test.mjs +86 -27
- package/foundation/integrations/integration-warnings.d.mts +9 -2
- package/foundation/integrations/integration-warnings.mjs +52 -21
- package/foundation/integrations/integration-warnings.test.mjs +74 -1
- package/foundation/integrations/integrations.d.mts +63 -3
- package/foundation/integrations/integrations.mjs +122 -9
- package/foundation/integrations/integrations.test.mjs +415 -1
- package/foundation/integrations/provider-conflicts.test.mjs +125 -0
- package/foundation/integrations/provider-ledger.test.mjs +275 -0
- package/foundation/integrations/provider-resolution.d.mts +152 -0
- package/foundation/integrations/provider-resolution.mjs +576 -0
- package/foundation/integrations/provider-resolution.test.mjs +369 -0
- package/foundation/integrations/theme-descriptor.d.mts +8 -0
- package/foundation/integrations/theme-descriptor.mjs +44 -0
- package/foundation/integrations/validate-contributions.d.mts +2 -0
- package/foundation/integrations/validate-contributions.mjs +131 -29
- package/foundation/response/base.d.ts +8 -4
- package/foundation/response/batch.type.d.mts +33 -0
- package/foundation/response/batch.type.mjs +34 -0
- package/foundation/response/error-codes.d.mts +3 -1
- package/foundation/response/error-codes.d.ts +2 -0
- package/foundation/response/error-codes.doc.mjs +19 -12
- package/foundation/response/error-codes.mjs +8 -2
- package/foundation/response/error-codes.test.mjs +166 -14
- package/foundation/response/json-contract.test.mjs +57 -17
- package/foundation/response/json.d.mts +4 -2
- package/foundation/response/json.mjs +8 -10
- package/foundation/response/response-types.doc.d.mts +7 -2
- package/foundation/response/response-types.doc.mjs +69 -25
- package/foundation/response/response-types.doc.test.mjs +181 -0
- package/foundation/response/response.doc.mjs +12 -11
- package/foundation/text/string-utils.d.mts +8 -0
- package/foundation/text/string-utils.mjs +40 -10
- package/foundation/xle/expand.d.mts +2 -0
- package/foundation/xle/expand.mjs +4 -3
- package/foundation/xle/expand.test.mjs +54 -0
- package/foundation/xle/xle.test.mjs +13 -0
- package/package.json +10 -11
- package/api/docs/docs.test.mjs +0 -83
- package/api/docs/integrationDocs.test.mjs +0 -208
- package/api/search/search.test.mjs +0 -389
- package/assets/docs/cli-integrations.doc.mjs +0 -367
- package/assets/templates/themes/manifest.json +0 -95
- package/clients/cli/commands/docs.test.mjs +0 -102
- package/clients/cli/lib/update-check.mjs +0 -83
- package/clients/cli/lib/update-check.test.mjs +0 -137
- package/clients/cli/update-hint-commands.test.mjs +0 -54
- package/foundation/agent-docs/agent-docs.test.mjs +0 -1141
|
@@ -0,0 +1,1704 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A repository you can drill into: folder, subfolder, file, in one table whose
|
|
7
|
+
* folder rows carry the newest commit from everything beneath them.
|
|
8
|
+
*
|
|
9
|
+
* A tree table earns its complexity when the parent rows are *derived from*
|
|
10
|
+
* the children. A plain folder listing does not qualify — a folder with a
|
|
11
|
+
* blank column has nothing to say at its own level, so a tree view is enough,
|
|
12
|
+
* and a table just adds a header row that is true of no row beneath it. What
|
|
13
|
+
* makes a repository qualify is that a folder has a real answer: its last
|
|
14
|
+
* commit is the most recent commit inside it. Then "src changed 2 hours ago"
|
|
15
|
+
* and "src/components is what changed" are the same question at two zoom
|
|
16
|
+
* levels, which is exactly what this shape is for.
|
|
17
|
+
*
|
|
18
|
+
* ## Extending this template
|
|
19
|
+
*
|
|
20
|
+
* **Two hooks, and neither one is the tree.** `useTableTreeState` owns the
|
|
21
|
+
* expanded set and flattens the nested data into the visible rows;
|
|
22
|
+
* `useTableTreeData` is the render plugin that draws the per-level indent and
|
|
23
|
+
* the chevron in the tree column. You pass the first one's `treeConfig`
|
|
24
|
+
* straight into the second. Hand-rolling this — a Set of open ids plus a
|
|
25
|
+
* recursive flatten on every render — is the usual way tree tables get built,
|
|
26
|
+
* and it costs you the ARIA (`aria-level`, `aria-expanded`), the expand-all
|
|
27
|
+
* control, and the guarantee that every level shares one column grid.
|
|
28
|
+
*
|
|
29
|
+
* **Sorting composes with the hierarchy instead of flattening it.**
|
|
30
|
+
* `useTableSortableState` returns an `applySort` that is handed to the tree's
|
|
31
|
+
* `sortSiblings`, so a sort reorders each sibling group in place and children
|
|
32
|
+
* never leave their parent. Sorting by date does not interleave a file with
|
|
33
|
+
* the folder that contains it.
|
|
34
|
+
*
|
|
35
|
+
* **Rollups are computed from the leaves, never stored.** `withRollups` walks
|
|
36
|
+
* the fixture once and gives every folder the newest commit beneath it, so a
|
|
37
|
+
* folder can never disagree with its contents. Message and date are copied
|
|
38
|
+
* from the *same* child rather than reduced separately — taking the newest
|
|
39
|
+
* date and the longest message would describe two different commits on one
|
|
40
|
+
* row. Hardcoding either is the bug this shape exists to prevent.
|
|
41
|
+
*
|
|
42
|
+
* **Search prunes the tree and takes the chevrons with it.** A query keeps any
|
|
43
|
+
* branch with a match anywhere beneath it and force-expands what survives, so
|
|
44
|
+
* results are never hidden behind a collapsed parent. While a query is active
|
|
45
|
+
* the expanded set is derived rather than stored, and manual toggling is
|
|
46
|
+
* suspended; clearing the box restores exactly the expansion state the user had
|
|
47
|
+
* before searching.
|
|
48
|
+
*
|
|
49
|
+
* **A pruned tree has to be re-rolled.** `withRollups` runs again on the
|
|
50
|
+
* pruned result, because removing children changes what every ancestor
|
|
51
|
+
* contains. Skipping that second pass is the subtle bug in most filtered tree
|
|
52
|
+
* tables: the rows are correct, the parents are stale, and a folder quietly
|
|
53
|
+
* advertises a commit that no surviving file beneath it carries. If you add a
|
|
54
|
+
* filter of any kind, re-roll after it.
|
|
55
|
+
*
|
|
56
|
+
* **Ids are paths, and that is load-bearing.** Every node's id is its full
|
|
57
|
+
* path, so the parent lookup for ArrowLeft and the row keys all fall out of
|
|
58
|
+
* string operations instead of a second index. If you swap in data keyed by
|
|
59
|
+
* uuid, you need an explicit `parentId` and the keyboard plugin has to walk
|
|
60
|
+
* that instead.
|
|
61
|
+
*
|
|
62
|
+
* **Arrow keys are a local plugin, not a core one.** `useTableTreeData` gives
|
|
63
|
+
* you the ARIA but no key handling, so `useTreeKeyboardNav` below implements
|
|
64
|
+
* the APG treegrid keys through `transformTable` (one `onKeyDown`) and
|
|
65
|
+
* `transformBodyRow` (roving `tabIndex`). It is written as a real
|
|
66
|
+
* `TablePlugin` rather than a wrapper div so it composes with the other
|
|
67
|
+
* plugins in the same `plugins={{}}` bag. Lift it into core if you need it in
|
|
68
|
+
* more than one place.
|
|
69
|
+
*
|
|
70
|
+
* **The rows carry a cursor and a selection, and they are different things.**
|
|
71
|
+
* The roving `tabIndex` is the cursor — wherever the arrows last left off. The
|
|
72
|
+
* selection is the folder the header trail and the README are describing, and
|
|
73
|
+
* it moves from the name cell or from a breadcrumb, never from an arrow key.
|
|
74
|
+
* The keyboard plugin owns the paint for both because it holds the only
|
|
75
|
+
* `transformBodyRow` in the bag: `aria-selected` and a washed row for the
|
|
76
|
+
* selection, `tabIndex` for the cursor. Because selection is read there, it is
|
|
77
|
+
* also a dependency of that memo — a breadcrumb click changes nothing else the
|
|
78
|
+
* memo watches, so leaving it out strands the tint and the ARIA state on the
|
|
79
|
+
* previously selected row. If you need real multi-row selection, reach for
|
|
80
|
+
* `useTableSelection` rather than widening this one.
|
|
81
|
+
*
|
|
82
|
+
* **The tree column wraps and does not truncate.** That is a documented
|
|
83
|
+
* limitation of `useTableTreeData`: `textOverflow="truncate"` does not reach
|
|
84
|
+
* inside the tree column, because the indent and expander need the cell to be a
|
|
85
|
+
* flex row. Name and the commit subject both take `proportional()` so the two
|
|
86
|
+
* variable-length columns share the slack; the date is `pixel()` because its
|
|
87
|
+
* text has a known ceiling.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
import {
|
|
91
|
+
useCallback,
|
|
92
|
+
useEffect,
|
|
93
|
+
useLayoutEffect,
|
|
94
|
+
useMemo,
|
|
95
|
+
useRef,
|
|
96
|
+
useState,
|
|
97
|
+
} from 'react';
|
|
98
|
+
import {
|
|
99
|
+
HStack,
|
|
100
|
+
Layout,
|
|
101
|
+
LayoutContent,
|
|
102
|
+
LayoutPanel,
|
|
103
|
+
StackItem,
|
|
104
|
+
VStack,
|
|
105
|
+
} from '@astryxdesign/core/Layout';
|
|
106
|
+
import {Heading, Text} from '@astryxdesign/core/Text';
|
|
107
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
108
|
+
import {Avatar} from '@astryxdesign/core/Avatar';
|
|
109
|
+
import {Badge} from '@astryxdesign/core/Badge';
|
|
110
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
111
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
112
|
+
import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
|
|
113
|
+
import {Section} from '@astryxdesign/core/Section';
|
|
114
|
+
import {SizeProvider} from '@astryxdesign/core/SizeContext';
|
|
115
|
+
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
116
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
117
|
+
import {Markdown} from '@astryxdesign/core/Markdown';
|
|
118
|
+
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
119
|
+
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
120
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
121
|
+
import {Breadcrumbs, BreadcrumbItem} from '@astryxdesign/core/Breadcrumbs';
|
|
122
|
+
import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
|
|
123
|
+
import {colorVars} from '@astryxdesign/core/theme/tokens.stylex';
|
|
124
|
+
import {Token} from '@astryxdesign/core/Token';
|
|
125
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
126
|
+
import {Tab, TabList} from '@astryxdesign/core/TabList';
|
|
127
|
+
import {Toolbar} from '@astryxdesign/core/Toolbar';
|
|
128
|
+
import {
|
|
129
|
+
Table,
|
|
130
|
+
pixel,
|
|
131
|
+
proportional,
|
|
132
|
+
useTableColumnResize,
|
|
133
|
+
useTableSortable,
|
|
134
|
+
useTableSortableState,
|
|
135
|
+
useTableTreeData,
|
|
136
|
+
useTableTreeState,
|
|
137
|
+
} from '@astryxdesign/core/Table';
|
|
138
|
+
import type {TableColumn, TablePlugin} from '@astryxdesign/core/Table';
|
|
139
|
+
import * as stylex from '@stylexjs/stylex';
|
|
140
|
+
/*
|
|
141
|
+
* Lucide throughout, not Heroicons. A repository page needs a branch and a
|
|
142
|
+
* fork glyph and Heroicons has no git iconography at all, so mixing sets was
|
|
143
|
+
* the only alternative — two libraries drawn on different grids, at different
|
|
144
|
+
* stroke weights, sitting side by side in the same toolbar.
|
|
145
|
+
*/
|
|
146
|
+
import {
|
|
147
|
+
BookOpen,
|
|
148
|
+
ChartColumn,
|
|
149
|
+
Code,
|
|
150
|
+
Eye,
|
|
151
|
+
File,
|
|
152
|
+
Folder,
|
|
153
|
+
GitBranch,
|
|
154
|
+
GitFork,
|
|
155
|
+
Scale,
|
|
156
|
+
Search,
|
|
157
|
+
ShieldCheck,
|
|
158
|
+
Star,
|
|
159
|
+
Tag,
|
|
160
|
+
} from 'lucide-react';
|
|
161
|
+
|
|
162
|
+
// ============= DATA =============
|
|
163
|
+
|
|
164
|
+
type EntryKind =
|
|
165
|
+
'folder' | 'typescript' | 'json' | 'yaml' | 'markdown' | 'text';
|
|
166
|
+
|
|
167
|
+
interface RepoEntry extends Record<string, unknown> {
|
|
168
|
+
/** Full path. Doubles as the ArrowLeft parent link and the row key. */
|
|
169
|
+
id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
kind: EntryKind;
|
|
172
|
+
/** Empty on folders — `withRollups` copies the newest commit beneath them. */
|
|
173
|
+
commitMessage: string;
|
|
174
|
+
/** Epoch ms. Zero on folders — `withRollups` takes the newest descendant. */
|
|
175
|
+
committedAt: number;
|
|
176
|
+
children?: RepoEntry[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The repository whose tree fills the table. */
|
|
180
|
+
const ROOT_NAME = 'canvas-editor';
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
* Every file gets the same glyph. Only one distinction earns its keep in this
|
|
184
|
+
* column — folder or not, because that is what predicts whether a row expands.
|
|
185
|
+
* Splitting files further by extension paints four near-identical shapes that
|
|
186
|
+
* repeat the name sitting next to them.
|
|
187
|
+
*/
|
|
188
|
+
const KIND_ICON: Record<
|
|
189
|
+
EntryKind,
|
|
190
|
+
React.ComponentType<React.SVGProps<SVGSVGElement>>
|
|
191
|
+
> = {
|
|
192
|
+
folder: Folder,
|
|
193
|
+
typescript: File,
|
|
194
|
+
json: File,
|
|
195
|
+
yaml: File,
|
|
196
|
+
markdown: File,
|
|
197
|
+
text: File,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A fixed clock. Every age is authored as an offset from it, so "2 hours ago"
|
|
202
|
+
* stays "2 hours ago" in each environment and in the docsite's snapshots —
|
|
203
|
+
* which a real `Date.now()` could not promise.
|
|
204
|
+
*/
|
|
205
|
+
const NOW = Date.UTC(2026, 8, 9, 16, 0);
|
|
206
|
+
|
|
207
|
+
const minutesAgo = (count: number): number => NOW - count * 60_000;
|
|
208
|
+
const hoursAgo = (count: number): number => NOW - count * 3_600_000;
|
|
209
|
+
const daysAgo = (count: number): number => NOW - count * 86_400_000;
|
|
210
|
+
|
|
211
|
+
function file(
|
|
212
|
+
path: string,
|
|
213
|
+
kind: EntryKind,
|
|
214
|
+
commitMessage: string,
|
|
215
|
+
committedAt: number,
|
|
216
|
+
): RepoEntry {
|
|
217
|
+
return {
|
|
218
|
+
id: path,
|
|
219
|
+
name: path.slice(path.lastIndexOf('/') + 1),
|
|
220
|
+
kind,
|
|
221
|
+
commitMessage,
|
|
222
|
+
committedAt,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function folder(path: string, children: RepoEntry[]): RepoEntry {
|
|
227
|
+
return {
|
|
228
|
+
id: path,
|
|
229
|
+
name: path.slice(path.lastIndexOf('/') + 1),
|
|
230
|
+
kind: 'folder',
|
|
231
|
+
commitMessage: '',
|
|
232
|
+
committedAt: 0,
|
|
233
|
+
children,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const REPO_TREE: RepoEntry[] = [
|
|
238
|
+
folder('.github', [
|
|
239
|
+
folder('.github/workflows', [
|
|
240
|
+
file(
|
|
241
|
+
'.github/workflows/ci.yml',
|
|
242
|
+
'yaml',
|
|
243
|
+
'ci: cache the pnpm store between runs (#412)',
|
|
244
|
+
hoursAgo(3),
|
|
245
|
+
),
|
|
246
|
+
file(
|
|
247
|
+
'.github/workflows/release.yml',
|
|
248
|
+
'yaml',
|
|
249
|
+
'ci: publish npm provenance on tagged releases (#398)',
|
|
250
|
+
daysAgo(12),
|
|
251
|
+
),
|
|
252
|
+
]),
|
|
253
|
+
file(
|
|
254
|
+
'.github/PULL_REQUEST_TEMPLATE.md',
|
|
255
|
+
'markdown',
|
|
256
|
+
'docs: ask for a repro link in bug reports (#377)',
|
|
257
|
+
daysAgo(61),
|
|
258
|
+
),
|
|
259
|
+
]),
|
|
260
|
+
folder('docs', [
|
|
261
|
+
folder('docs/architecture', [
|
|
262
|
+
file(
|
|
263
|
+
'docs/architecture/rendering.md',
|
|
264
|
+
'markdown',
|
|
265
|
+
'docs: explain the dirty-rect redraw path (#405)',
|
|
266
|
+
daysAgo(5),
|
|
267
|
+
),
|
|
268
|
+
file(
|
|
269
|
+
'docs/architecture/state.md',
|
|
270
|
+
'markdown',
|
|
271
|
+
'docs: describe undo stack ownership (#390)',
|
|
272
|
+
daysAgo(21),
|
|
273
|
+
),
|
|
274
|
+
]),
|
|
275
|
+
// The one nested README. GitHub renders whichever README sits in the
|
|
276
|
+
// folder you are looking at, so this folder gets a preview and the others
|
|
277
|
+
// get none — which is the behaviour worth showing.
|
|
278
|
+
file(
|
|
279
|
+
'docs/README.md',
|
|
280
|
+
'markdown',
|
|
281
|
+
'docs: add an index for the docs folder (#411)',
|
|
282
|
+
daysAgo(3),
|
|
283
|
+
),
|
|
284
|
+
file(
|
|
285
|
+
'docs/contributing.md',
|
|
286
|
+
'markdown',
|
|
287
|
+
'docs: document the release checklist (#402)',
|
|
288
|
+
daysAgo(8),
|
|
289
|
+
),
|
|
290
|
+
file(
|
|
291
|
+
'docs/getting-started.md',
|
|
292
|
+
'markdown',
|
|
293
|
+
'docs: add a first-plugin walkthrough (#369)',
|
|
294
|
+
daysAgo(64),
|
|
295
|
+
),
|
|
296
|
+
]),
|
|
297
|
+
folder('src', [
|
|
298
|
+
folder('src/components', [
|
|
299
|
+
// Three levels deep, so the header trail can outgrow the bar and the
|
|
300
|
+
// collapse menu has something to collapse.
|
|
301
|
+
folder('src/components/canvas', [
|
|
302
|
+
file(
|
|
303
|
+
'src/components/canvas/Canvas.tsx',
|
|
304
|
+
'typescript',
|
|
305
|
+
'fix: keep pointer capture through a drag (#417)',
|
|
306
|
+
hoursAgo(2),
|
|
307
|
+
),
|
|
308
|
+
file(
|
|
309
|
+
'src/components/canvas/layers.ts',
|
|
310
|
+
'typescript',
|
|
311
|
+
'feat: let a group be locked without hiding it (#413)',
|
|
312
|
+
hoursAgo(9),
|
|
313
|
+
),
|
|
314
|
+
file(
|
|
315
|
+
'src/components/canvas/pointer.ts',
|
|
316
|
+
'typescript',
|
|
317
|
+
'fix: release capture when the window blurs (#417)',
|
|
318
|
+
hoursAgo(2),
|
|
319
|
+
),
|
|
320
|
+
]),
|
|
321
|
+
file(
|
|
322
|
+
'src/components/Inspector.tsx',
|
|
323
|
+
'typescript',
|
|
324
|
+
'feat: group the transform fields into one panel (#409)',
|
|
325
|
+
daysAgo(4),
|
|
326
|
+
),
|
|
327
|
+
file(
|
|
328
|
+
'src/components/Toolbar.tsx',
|
|
329
|
+
'typescript',
|
|
330
|
+
'refactor: derive tool state from the store (#396)',
|
|
331
|
+
daysAgo(14),
|
|
332
|
+
),
|
|
333
|
+
]),
|
|
334
|
+
folder('src/lib', [
|
|
335
|
+
file(
|
|
336
|
+
'src/lib/geometry.ts',
|
|
337
|
+
'typescript',
|
|
338
|
+
'perf: stop reallocating the bounds buffer (#414)',
|
|
339
|
+
hoursAgo(20),
|
|
340
|
+
),
|
|
341
|
+
file(
|
|
342
|
+
'src/lib/serialize.ts',
|
|
343
|
+
'typescript',
|
|
344
|
+
'fix: round-trip empty groups without loss (#401)',
|
|
345
|
+
daysAgo(9),
|
|
346
|
+
),
|
|
347
|
+
file(
|
|
348
|
+
'src/lib/shapes.ts',
|
|
349
|
+
'typescript',
|
|
350
|
+
'feat: add polygon and star primitives (#411)',
|
|
351
|
+
daysAgo(3),
|
|
352
|
+
),
|
|
353
|
+
]),
|
|
354
|
+
file(
|
|
355
|
+
'src/main.ts',
|
|
356
|
+
'typescript',
|
|
357
|
+
'chore: move bootstrap behind a feature flag (#415)',
|
|
358
|
+
daysAgo(1),
|
|
359
|
+
),
|
|
360
|
+
file(
|
|
361
|
+
'src/ui.tsx',
|
|
362
|
+
'typescript',
|
|
363
|
+
'fix: restore focus after the modal closes (#407)',
|
|
364
|
+
daysAgo(6),
|
|
365
|
+
),
|
|
366
|
+
]),
|
|
367
|
+
folder('tests', [
|
|
368
|
+
file(
|
|
369
|
+
'tests/geometry.test.ts',
|
|
370
|
+
'typescript',
|
|
371
|
+
'test: cover degenerate bounding boxes (#414)',
|
|
372
|
+
hoursAgo(20),
|
|
373
|
+
),
|
|
374
|
+
file(
|
|
375
|
+
'tests/serialize.test.ts',
|
|
376
|
+
'typescript',
|
|
377
|
+
'test: add a fixture for nested groups (#401)',
|
|
378
|
+
daysAgo(9),
|
|
379
|
+
),
|
|
380
|
+
]),
|
|
381
|
+
file(
|
|
382
|
+
'.gitignore',
|
|
383
|
+
'text',
|
|
384
|
+
'chore: ignore local coverage output (#380)',
|
|
385
|
+
daysAgo(49),
|
|
386
|
+
),
|
|
387
|
+
file(
|
|
388
|
+
'LICENSE',
|
|
389
|
+
'text',
|
|
390
|
+
'chore: bump the copyright year (#352)',
|
|
391
|
+
daysAgo(243),
|
|
392
|
+
),
|
|
393
|
+
file(
|
|
394
|
+
'README.md',
|
|
395
|
+
'markdown',
|
|
396
|
+
'docs: rewrite the install section (#418)',
|
|
397
|
+
minutesAgo(30),
|
|
398
|
+
),
|
|
399
|
+
file('package.json', 'json', 'chore: bump vite to 6.2 (#416)', hoursAgo(18)),
|
|
400
|
+
file(
|
|
401
|
+
'tsconfig.json',
|
|
402
|
+
'json',
|
|
403
|
+
'build: turn on verbatimModuleSyntax (#393)',
|
|
404
|
+
daysAgo(35),
|
|
405
|
+
),
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Give every folder the newest commit beneath it — message and date together,
|
|
410
|
+
* taken from the same child so the two columns never describe different
|
|
411
|
+
* commits. Runs bottom-up, so a folder always agrees with what is inside it.
|
|
412
|
+
*/
|
|
413
|
+
function withRollups(nodes: RepoEntry[]): RepoEntry[] {
|
|
414
|
+
return nodes.map(node => {
|
|
415
|
+
if (!node.children || node.children.length === 0) {
|
|
416
|
+
return node;
|
|
417
|
+
}
|
|
418
|
+
const children = withRollups(node.children);
|
|
419
|
+
const newest = children.reduce((latest, child) =>
|
|
420
|
+
child.committedAt > latest.committedAt ? child : latest,
|
|
421
|
+
);
|
|
422
|
+
return {
|
|
423
|
+
...node,
|
|
424
|
+
children,
|
|
425
|
+
commitMessage: newest.commitMessage,
|
|
426
|
+
committedAt: newest.committedAt,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const ROLLED_UP = withRollups(REPO_TREE);
|
|
432
|
+
|
|
433
|
+
const DEFAULT_EXPANDED = ['src'];
|
|
434
|
+
|
|
435
|
+
// ============= README =============
|
|
436
|
+
|
|
437
|
+
/*
|
|
438
|
+
* Rendered by core's `Markdown`, which parses in-house and draws through the
|
|
439
|
+
* same Heading/Text/List/CodeBlock/Table primitives the rest of the page uses
|
|
440
|
+
* — so the preview inherits the theme instead of shipping its own prose CSS.
|
|
441
|
+
*/
|
|
442
|
+
const README = `## canvas-editor
|
|
443
|
+
|
|
444
|
+
A small, embeddable vector canvas. Draw shapes, group them, and serialise the
|
|
445
|
+
result to JSON that round-trips without loss.
|
|
446
|
+
|
|
447
|
+
### Install
|
|
448
|
+
|
|
449
|
+
\`\`\`bash
|
|
450
|
+
npm install canvas-editor
|
|
451
|
+
\`\`\`
|
|
452
|
+
|
|
453
|
+
### Usage
|
|
454
|
+
|
|
455
|
+
\`\`\`ts
|
|
456
|
+
const {Canvas} = await import('canvas-editor');
|
|
457
|
+
|
|
458
|
+
const canvas = new Canvas(document.querySelector('#root'));
|
|
459
|
+
canvas.add({kind: 'rect', x: 0, y: 0, width: 120, height: 80});
|
|
460
|
+
\`\`\`
|
|
461
|
+
|
|
462
|
+
### Status
|
|
463
|
+
|
|
464
|
+
| Area | State | Notes |
|
|
465
|
+
| ----------- | -------- | -------------------------------------- |
|
|
466
|
+
| Rendering | Stable | Dirty-rect redraw, no full repaints |
|
|
467
|
+
| Serialising | Stable | Round-trips groups and nested shapes |
|
|
468
|
+
| Plugins | Beta | API may change before 1.0 |
|
|
469
|
+
|
|
470
|
+
### Contributing
|
|
471
|
+
|
|
472
|
+
Read [the contributing guide](#) first. In short:
|
|
473
|
+
|
|
474
|
+
- One change per pull request, with a test that fails without it.
|
|
475
|
+
- Run \`npm test\` before pushing.
|
|
476
|
+
- Conventional commits — the changelog is generated from them.
|
|
477
|
+
|
|
478
|
+
> Issues tagged \`good first issue\` are a reasonable place to start.
|
|
479
|
+
`;
|
|
480
|
+
|
|
481
|
+
const DOCS_README = `## Documentation
|
|
482
|
+
|
|
483
|
+
How the canvas is put together, and how to work on it.
|
|
484
|
+
|
|
485
|
+
### Start here
|
|
486
|
+
|
|
487
|
+
- **getting-started.md** — install the package and draw your first shape.
|
|
488
|
+
- **contributing.md** — branch naming, the release checklist, how to get review.
|
|
489
|
+
|
|
490
|
+
### Architecture
|
|
491
|
+
|
|
492
|
+
The \`architecture/\` folder covers the parts that are hard to infer from the
|
|
493
|
+
source: how the dirty-rect redraw path decides what to repaint, and who owns
|
|
494
|
+
the undo stack.
|
|
495
|
+
`;
|
|
496
|
+
|
|
497
|
+
/*
|
|
498
|
+
* Which folders have a README to show. Keyed by folder id, with the empty
|
|
499
|
+
* string standing for the repo root.
|
|
500
|
+
*
|
|
501
|
+
* Whether a preview appears is decided by the tree, not by this map — a
|
|
502
|
+
* folder shows one only if it actually contains a README file. This just
|
|
503
|
+
* supplies the body for the folders that do, so the two cannot disagree
|
|
504
|
+
* about which folders have one.
|
|
505
|
+
*/
|
|
506
|
+
const READMES: Record<string, string> = {
|
|
507
|
+
'': README,
|
|
508
|
+
docs: DOCS_README,
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The README to show for the folder currently selected, or `null` when that
|
|
513
|
+
* folder has none — the file list stands on its own and the preview card is
|
|
514
|
+
* dropped rather than left empty.
|
|
515
|
+
*
|
|
516
|
+
* Ids are paths, so the folder is reached by walking its segments.
|
|
517
|
+
*/
|
|
518
|
+
function readmeFor(folderId: string | null): string | null {
|
|
519
|
+
let entries = REPO_TREE;
|
|
520
|
+
|
|
521
|
+
if (folderId != null) {
|
|
522
|
+
for (const segment of folderId.split('/')) {
|
|
523
|
+
const next: RepoEntry | undefined = entries.find(
|
|
524
|
+
entry => entry.name === segment && entry.kind === 'folder',
|
|
525
|
+
);
|
|
526
|
+
if (next?.children == null) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
entries = next.children;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const hasReadme = entries.some(
|
|
534
|
+
entry => entry.kind !== 'folder' && /^readme\.md$/i.test(entry.name),
|
|
535
|
+
);
|
|
536
|
+
return hasReadme ? (READMES[folderId ?? ''] ?? null) : null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ============= SEARCH =============
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Shared by `pruneTree` and `countMatches` so the rows on screen and the
|
|
543
|
+
* tally beside the search field can never disagree about what a match is.
|
|
544
|
+
*/
|
|
545
|
+
function isMatch(node: RepoEntry, needle: string): boolean {
|
|
546
|
+
return (
|
|
547
|
+
node.name.toLowerCase().includes(needle) ||
|
|
548
|
+
node.commitMessage.toLowerCase().includes(needle)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Counts only true matches. `pruneTree` also keeps ancestors, so the visible
|
|
554
|
+
* row count runs ahead of the number of things actually found.
|
|
555
|
+
*/
|
|
556
|
+
function countMatches(nodes: RepoEntry[], needle: string): number {
|
|
557
|
+
return nodes.reduce(
|
|
558
|
+
(total, node) =>
|
|
559
|
+
total +
|
|
560
|
+
(isMatch(node, needle) ? 1 : 0) +
|
|
561
|
+
(node.children ? countMatches(node.children, needle) : 0),
|
|
562
|
+
0,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Keep any branch with a match at or beneath it; drop everything else. */
|
|
567
|
+
function pruneTree(nodes: RepoEntry[], needle: string): RepoEntry[] {
|
|
568
|
+
const kept: RepoEntry[] = [];
|
|
569
|
+
for (const node of nodes) {
|
|
570
|
+
const isSelfMatch = isMatch(node, needle);
|
|
571
|
+
const children = node.children ? pruneTree(node.children, needle) : [];
|
|
572
|
+
if (isSelfMatch || children.length > 0) {
|
|
573
|
+
kept.push(
|
|
574
|
+
node.children
|
|
575
|
+
? {
|
|
576
|
+
...node,
|
|
577
|
+
children:
|
|
578
|
+
isSelfMatch && children.length === 0 ? node.children : children,
|
|
579
|
+
}
|
|
580
|
+
: node,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return kept;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function collectIds(nodes: RepoEntry[], into: Set<string>): Set<string> {
|
|
588
|
+
for (const node of nodes) {
|
|
589
|
+
if (node.children && node.children.length > 0) {
|
|
590
|
+
into.add(node.id);
|
|
591
|
+
collectIds(node.children, into);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return into;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ============= FORMATTING =============
|
|
598
|
+
|
|
599
|
+
/*
|
|
600
|
+
* `numeric: 'auto'` is what turns a single unit into "yesterday", "last week"
|
|
601
|
+
* and "last month" instead of "1 day ago" — the phrasing a repository browser
|
|
602
|
+
* uses. The locale is pinned so the docsite's snapshots do not move with the
|
|
603
|
+
* runner's own.
|
|
604
|
+
*/
|
|
605
|
+
const relativeFormat = new Intl.RelativeTimeFormat('en-US', {numeric: 'auto'});
|
|
606
|
+
|
|
607
|
+
// Largest first: the loop reports in the biggest unit that fits.
|
|
608
|
+
const AGE_UNITS: ReadonlyArray<readonly [Intl.RelativeTimeFormatUnit, number]> =
|
|
609
|
+
[
|
|
610
|
+
['year', 365 * 86_400_000],
|
|
611
|
+
['month', 30 * 86_400_000],
|
|
612
|
+
['week', 7 * 86_400_000],
|
|
613
|
+
['day', 86_400_000],
|
|
614
|
+
['hour', 3_600_000],
|
|
615
|
+
['minute', 60_000],
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
function commitAge(epochMs: number): string {
|
|
619
|
+
const elapsed = NOW - epochMs;
|
|
620
|
+
for (const [unit, size] of AGE_UNITS) {
|
|
621
|
+
if (elapsed >= size) {
|
|
622
|
+
return relativeFormat.format(-Math.floor(elapsed / size), unit);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return 'just now';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ============= KEYBOARD =============
|
|
629
|
+
|
|
630
|
+
const styles = stylex.create({
|
|
631
|
+
/*
|
|
632
|
+
* Toolbar pads its own contents by 12px, but the table body sits on a 16px
|
|
633
|
+
* content line. The difference is added out here so the two agree.
|
|
634
|
+
*/
|
|
635
|
+
headerContainer: {
|
|
636
|
+
paddingInline: '4px',
|
|
637
|
+
},
|
|
638
|
+
/*
|
|
639
|
+
* The wash for the selected folder.
|
|
640
|
+
*
|
|
641
|
+
* `--color-background-blue`, not `--color-accent-muted`: the theme in use
|
|
642
|
+
* neutralises the accent family to grey (#f1f1f1 light, #262626 dark), so an
|
|
643
|
+
* accent wash would be invisible against the zebra stripe. The categorical
|
|
644
|
+
* colours are left chromatic on purpose, which is what makes this read blue.
|
|
645
|
+
*
|
|
646
|
+
* Painted as an inset shadow rather than a background so it survives the
|
|
647
|
+
* zebra stripe, which sets `background-color` on this same row and would
|
|
648
|
+
* otherwise replace it.
|
|
649
|
+
*/
|
|
650
|
+
selectedRow: {
|
|
651
|
+
boxShadow: `inset 0 0 0 100vmax ${colorVars['--color-background-blue']}`,
|
|
652
|
+
},
|
|
653
|
+
// Inside the TabList so its divider border keeps spanning the whole card.
|
|
654
|
+
readmeTabs: {
|
|
655
|
+
paddingInline: '4px',
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
/** Header width below which the view switcher and search give up their space. */
|
|
660
|
+
const NARROW_HEADER = 720;
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* How many crumbs stay inline before the middle folds into a menu. Three
|
|
664
|
+
* keeps the repo, the parent and the current folder — one more than that and
|
|
665
|
+
* the trail starts competing with the rest of the toolbar for the row.
|
|
666
|
+
*/
|
|
667
|
+
const MAX_INLINE_CRUMBS = 3;
|
|
668
|
+
|
|
669
|
+
/** Stands in for the folded-away crumbs while mapping over the trail. */
|
|
670
|
+
const ELLIPSIS = Symbol('ellipsis');
|
|
671
|
+
|
|
672
|
+
/** One entry in the header trail. `id` is null for the repository root. */
|
|
673
|
+
interface Crumb {
|
|
674
|
+
label: string;
|
|
675
|
+
id: string | null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Reports the observed width of whatever the returned ref is attached to.
|
|
680
|
+
*
|
|
681
|
+
* A container query cannot drive this header. Passing `centerContent` at all
|
|
682
|
+
* is what puts Toolbar into its three-column grid, so hiding the control
|
|
683
|
+
* inside leaves the middle column still holding its share of the row. Whether
|
|
684
|
+
* to pass the slot is a render decision, which puts the breakpoint in JS — and
|
|
685
|
+
* once it is there, the search reads the same number so both halves of the
|
|
686
|
+
* header agree on where narrow starts.
|
|
687
|
+
*/
|
|
688
|
+
function useMeasuredWidth(
|
|
689
|
+
ref: React.RefObject<HTMLElement | null>,
|
|
690
|
+
): number | null {
|
|
691
|
+
// Null until measured, so the first paint is not committed to a guess.
|
|
692
|
+
const [width, setWidth] = useState<number | null>(null);
|
|
693
|
+
|
|
694
|
+
useLayoutEffect(() => {
|
|
695
|
+
const element = ref.current;
|
|
696
|
+
if (element == null) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const observer = new ResizeObserver(entries => {
|
|
700
|
+
setWidth(entries[0].contentRect.width);
|
|
701
|
+
});
|
|
702
|
+
observer.observe(element);
|
|
703
|
+
return () => observer.disconnect();
|
|
704
|
+
}, [ref]);
|
|
705
|
+
|
|
706
|
+
return width;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
interface TreeKeyboardNavConfig {
|
|
710
|
+
/** The flattened, currently visible rows, in render order. */
|
|
711
|
+
rows: ReadonlyArray<RepoEntry>;
|
|
712
|
+
focusedId: string | null;
|
|
713
|
+
onFocusedIdChange: (id: string) => void;
|
|
714
|
+
expandedIds: ReadonlySet<string>;
|
|
715
|
+
onExpand: (id: string) => void;
|
|
716
|
+
onCollapse: (id: string) => void;
|
|
717
|
+
/** The folder whose contents the header trail is describing, if any. */
|
|
718
|
+
selectedId: string | null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* APG treegrid keys over the visible rows: Up/Down move, Right expands then
|
|
723
|
+
* descends, Left collapses then ascends, Home/End jump to the ends.
|
|
724
|
+
*
|
|
725
|
+
* Roving `tabIndex` means the table is one tab stop; once inside, the arrows
|
|
726
|
+
* own the movement. Focus itself is moved by the page effect that watches
|
|
727
|
+
* `focusedId`, not from here, so a click and a keypress converge on one path.
|
|
728
|
+
*/
|
|
729
|
+
function useTreeKeyboardNav({
|
|
730
|
+
rows,
|
|
731
|
+
focusedId,
|
|
732
|
+
onFocusedIdChange,
|
|
733
|
+
expandedIds,
|
|
734
|
+
onExpand,
|
|
735
|
+
onCollapse,
|
|
736
|
+
selectedId,
|
|
737
|
+
}: TreeKeyboardNavConfig): TablePlugin<RepoEntry> {
|
|
738
|
+
const handleKeyDown = useCallback(
|
|
739
|
+
(event: React.KeyboardEvent<HTMLTableElement>) => {
|
|
740
|
+
if (event.defaultPrevented || rows.length === 0) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const index = rows.findIndex(row => row.id === focusedId);
|
|
744
|
+
const current = index >= 0 ? rows[index] : undefined;
|
|
745
|
+
const hasChildren = Boolean(current?.children?.length);
|
|
746
|
+
const isExpanded = current != null && expandedIds.has(current.id);
|
|
747
|
+
|
|
748
|
+
let nextId: string | undefined;
|
|
749
|
+
|
|
750
|
+
switch (event.key) {
|
|
751
|
+
case 'ArrowDown':
|
|
752
|
+
nextId = rows[index + 1]?.id ?? rows[0]?.id;
|
|
753
|
+
break;
|
|
754
|
+
case 'ArrowUp':
|
|
755
|
+
nextId = index > 0 ? rows[index - 1].id : rows[0]?.id;
|
|
756
|
+
break;
|
|
757
|
+
case 'ArrowRight':
|
|
758
|
+
if (current == null) {
|
|
759
|
+
nextId = rows[0]?.id;
|
|
760
|
+
} else if (hasChildren && !isExpanded) {
|
|
761
|
+
onExpand(current.id);
|
|
762
|
+
} else if (hasChildren) {
|
|
763
|
+
// Already open, so Right descends to the first child.
|
|
764
|
+
nextId = rows[index + 1]?.id;
|
|
765
|
+
}
|
|
766
|
+
break;
|
|
767
|
+
case 'ArrowLeft':
|
|
768
|
+
if (current == null) {
|
|
769
|
+
nextId = rows[0]?.id;
|
|
770
|
+
} else if (hasChildren && isExpanded) {
|
|
771
|
+
onCollapse(current.id);
|
|
772
|
+
} else {
|
|
773
|
+
// Ids are paths, so the parent is a prefix of this row's id.
|
|
774
|
+
const cut = current.id.lastIndexOf('/');
|
|
775
|
+
const parentId = cut > 0 ? current.id.slice(0, cut) : undefined;
|
|
776
|
+
nextId = rows.find(row => row.id === parentId)?.id;
|
|
777
|
+
}
|
|
778
|
+
break;
|
|
779
|
+
case 'Home':
|
|
780
|
+
nextId = rows[0]?.id;
|
|
781
|
+
break;
|
|
782
|
+
case 'End':
|
|
783
|
+
nextId = rows[rows.length - 1]?.id;
|
|
784
|
+
break;
|
|
785
|
+
default:
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Every branch above either moved focus or toggled a row, so the arrow
|
|
790
|
+
// key must not also scroll the page.
|
|
791
|
+
event.preventDefault();
|
|
792
|
+
if (nextId != null && nextId !== focusedId) {
|
|
793
|
+
onFocusedIdChange(nextId);
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
[rows, focusedId, expandedIds, onExpand, onCollapse, onFocusedIdChange],
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
return useMemo(
|
|
800
|
+
() => ({
|
|
801
|
+
transformTable: props => ({
|
|
802
|
+
...props,
|
|
803
|
+
htmlProps: {...props.htmlProps, onKeyDown: handleKeyDown},
|
|
804
|
+
}),
|
|
805
|
+
transformBodyRow: (props, item) => ({
|
|
806
|
+
...props,
|
|
807
|
+
/*
|
|
808
|
+
* The selected folder is washed rather than outlined, so the row
|
|
809
|
+
* still reads as part of the list. The tint rides on this plugin
|
|
810
|
+
* because it owns the only `transformBodyRow` in the bag — a second
|
|
811
|
+
* plugin transforming the same row would have to be merged by hand.
|
|
812
|
+
*/
|
|
813
|
+
xstyle: [...props.xstyle, item.id === selectedId && styles.selectedRow],
|
|
814
|
+
htmlProps: {
|
|
815
|
+
...props.htmlProps,
|
|
816
|
+
/*
|
|
817
|
+
* Roving tabindex: one stop for the whole table. Selection and the
|
|
818
|
+
* cursor are separate — the cursor is wherever the arrows left off,
|
|
819
|
+
* the selection is the folder the header trail is describing.
|
|
820
|
+
*/
|
|
821
|
+
tabIndex: item.id === focusedId ? 0 : -1,
|
|
822
|
+
'data-row-id': item.id,
|
|
823
|
+
'aria-selected': item.id === selectedId ? true : undefined,
|
|
824
|
+
onClick: () => onFocusedIdChange(item.id),
|
|
825
|
+
},
|
|
826
|
+
}),
|
|
827
|
+
}),
|
|
828
|
+
[handleKeyDown, focusedId, selectedId, onFocusedIdChange],
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ============= COLUMNS =============
|
|
833
|
+
|
|
834
|
+
/*
|
|
835
|
+
* A factory, not a constant: the name cell has to reach the handler that
|
|
836
|
+
* moves the trail in the header, and that handler only exists inside the
|
|
837
|
+
* page component.
|
|
838
|
+
*/
|
|
839
|
+
function buildColumns(
|
|
840
|
+
onSelectFolder: (id: string) => void,
|
|
841
|
+
): TableColumn<RepoEntry>[] {
|
|
842
|
+
return [
|
|
843
|
+
{
|
|
844
|
+
key: 'name',
|
|
845
|
+
header: 'Name',
|
|
846
|
+
// Deep rows spend their width on indentation before the name even starts,
|
|
847
|
+
// so this column needs a floor well above the 120px default.
|
|
848
|
+
width: proportional(2, {minWidth: 240}),
|
|
849
|
+
sortable: true,
|
|
850
|
+
renderCell: entry => (
|
|
851
|
+
<HStack gap={2} vAlign="center">
|
|
852
|
+
<Icon
|
|
853
|
+
icon={KIND_ICON[entry.kind]}
|
|
854
|
+
size="sm"
|
|
855
|
+
color={entry.kind === 'folder' ? 'accent' : 'secondary'}
|
|
856
|
+
// Lucide draws strokes only, so filling the folder is the sole way
|
|
857
|
+
// to weight it against the files. `fill` reaches the svg because
|
|
858
|
+
// Icon forwards unknown props through to the component it renders.
|
|
859
|
+
fill={entry.kind === 'folder' ? 'currentColor' : 'none'}
|
|
860
|
+
/>
|
|
861
|
+
{entry.kind === 'folder' ? (
|
|
862
|
+
/*
|
|
863
|
+
* A folder is a place rather than a document, so activating it
|
|
864
|
+
* selects it and the header trail follows. The href is what makes
|
|
865
|
+
* it a link for the keyboard and the context menu; the default is
|
|
866
|
+
* suppressed because nothing is actually fetched.
|
|
867
|
+
*/
|
|
868
|
+
<Link
|
|
869
|
+
href="#"
|
|
870
|
+
onClick={event => {
|
|
871
|
+
event.preventDefault();
|
|
872
|
+
onSelectFolder(entry.id);
|
|
873
|
+
}}>
|
|
874
|
+
{entry.name}
|
|
875
|
+
</Link>
|
|
876
|
+
) : (
|
|
877
|
+
<Link href="#">{entry.name}</Link>
|
|
878
|
+
)}
|
|
879
|
+
</HStack>
|
|
880
|
+
),
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
// Subject lines are long and vary a lot, so this column takes the slack
|
|
884
|
+
// rather than truncating at a width picked in advance.
|
|
885
|
+
key: 'message',
|
|
886
|
+
header: 'Last commit message',
|
|
887
|
+
width: proportional(3, {minWidth: 200}),
|
|
888
|
+
sortable: true,
|
|
889
|
+
renderCell: entry => <Text color="secondary">{entry.commitMessage}</Text>,
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
key: 'committed',
|
|
893
|
+
header: 'Last commit date',
|
|
894
|
+
width: pixel(160),
|
|
895
|
+
align: 'end',
|
|
896
|
+
sortable: true,
|
|
897
|
+
renderCell: entry => (
|
|
898
|
+
<Text color="secondary">{commitAge(entry.committedAt)}</Text>
|
|
899
|
+
),
|
|
900
|
+
},
|
|
901
|
+
];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ============= REPO CHROME =============
|
|
905
|
+
|
|
906
|
+
/*
|
|
907
|
+
* The supporting detail around the file list. None of it is interactive here —
|
|
908
|
+
* it exists so the table is read in the context a real repository gives it,
|
|
909
|
+
* and so the layout is exercised at its true density rather than with a bare
|
|
910
|
+
* table floating in an empty page.
|
|
911
|
+
*/
|
|
912
|
+
|
|
913
|
+
const REPO_DESCRIPTION =
|
|
914
|
+
'A small, embeddable vector canvas. Draw shapes, group them, and serialise the result to JSON that round-trips without loss.';
|
|
915
|
+
|
|
916
|
+
const TOPICS = [
|
|
917
|
+
'canvas',
|
|
918
|
+
'vector-graphics',
|
|
919
|
+
'typescript',
|
|
920
|
+
'editor',
|
|
921
|
+
'svg',
|
|
922
|
+
'2d-graphics',
|
|
923
|
+
'wasm',
|
|
924
|
+
];
|
|
925
|
+
|
|
926
|
+
const ABOUT_LINKS = [
|
|
927
|
+
{label: 'Readme', icon: BookOpen},
|
|
928
|
+
{label: 'MIT license', icon: Scale},
|
|
929
|
+
{label: 'Code of conduct', icon: ShieldCheck},
|
|
930
|
+
{label: 'Security policy', icon: ShieldCheck},
|
|
931
|
+
{label: 'Activity', icon: ChartColumn},
|
|
932
|
+
];
|
|
933
|
+
|
|
934
|
+
/** The count reads as a noun; the button beside it has to read as a verb. */
|
|
935
|
+
const STAT_ACTION: Record<string, string> = {
|
|
936
|
+
stars: 'Star',
|
|
937
|
+
watching: 'Watch',
|
|
938
|
+
forks: 'Fork',
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
/*
|
|
942
|
+
* The two ref counts beside the branch selector. Tag rather than Tags: the
|
|
943
|
+
* plural glyph reads as a stack at 16px, which is noise next to a branch icon
|
|
944
|
+
* that is already busy.
|
|
945
|
+
*/
|
|
946
|
+
const REF_COUNTS = [
|
|
947
|
+
{label: '171 Branches', icon: GitBranch},
|
|
948
|
+
{label: '56 Tags', icon: Tag},
|
|
949
|
+
];
|
|
950
|
+
|
|
951
|
+
const REPO_STATS = [
|
|
952
|
+
{label: 'stars', value: '12.6k', icon: Star},
|
|
953
|
+
{label: 'watching', value: '97', icon: Eye},
|
|
954
|
+
{label: 'forks', value: '1.3k', icon: GitFork},
|
|
955
|
+
];
|
|
956
|
+
|
|
957
|
+
const CONTRIBUTORS = [
|
|
958
|
+
'Ada Okafor',
|
|
959
|
+
'Bo Lindqvist',
|
|
960
|
+
'Cass Moreau',
|
|
961
|
+
'Dev Raman',
|
|
962
|
+
'Elif Aydın',
|
|
963
|
+
'Finn Ashworth',
|
|
964
|
+
'Gia Petrova',
|
|
965
|
+
'Hana Sato',
|
|
966
|
+
'Ines Duarte',
|
|
967
|
+
'Jo Mbeki',
|
|
968
|
+
'Kit Nakamura',
|
|
969
|
+
'Lena Voss',
|
|
970
|
+
];
|
|
971
|
+
|
|
972
|
+
/** The sidebar GitHub puts beside the file list. */
|
|
973
|
+
function AboutPanel() {
|
|
974
|
+
return (
|
|
975
|
+
<VStack gap={6}>
|
|
976
|
+
<VStack gap={3}>
|
|
977
|
+
<Heading level={3}>About</Heading>
|
|
978
|
+
<Text color="secondary">{REPO_DESCRIPTION}</Text>
|
|
979
|
+
<Link href="#" isExternalLink>
|
|
980
|
+
canvas-editor.dev
|
|
981
|
+
</Link>
|
|
982
|
+
{/*
|
|
983
|
+
Topics are chips you can click, which rules out Badge — its docs
|
|
984
|
+
name clickable badges as an anti-pattern. Token is the chip that
|
|
985
|
+
takes an onClick and paints a hover state to match.
|
|
986
|
+
*/}
|
|
987
|
+
<HStack gap={1} wrap="wrap">
|
|
988
|
+
{TOPICS.map(topic => (
|
|
989
|
+
<Token key={topic} size="sm" label={topic} onClick={() => {}} />
|
|
990
|
+
))}
|
|
991
|
+
</HStack>
|
|
992
|
+
{/*
|
|
993
|
+
Not a List. ListItem is a row with its own inline padding and hit
|
|
994
|
+
area, so it hangs its text further in than the description, the
|
|
995
|
+
topics and the stats above it — the one thing this column needs is
|
|
996
|
+
a single left edge. An icon beside a Link starts where they start.
|
|
997
|
+
*/}
|
|
998
|
+
<VStack gap={2}>
|
|
999
|
+
{ABOUT_LINKS.map(link => (
|
|
1000
|
+
<HStack key={link.label} gap={2} vAlign="center">
|
|
1001
|
+
<Icon icon={link.icon} size="sm" color="secondary" />
|
|
1002
|
+
<Link href="#">{link.label}</Link>
|
|
1003
|
+
</HStack>
|
|
1004
|
+
))}
|
|
1005
|
+
</VStack>
|
|
1006
|
+
{/*
|
|
1007
|
+
Same row shape as the links above: one per line, icon at sm beside
|
|
1008
|
+
a default-size Link. These used to be supporting-size with the count
|
|
1009
|
+
bolded and the three of them wrapping two-then-one, which read as a
|
|
1010
|
+
different kind of thing sitting in the middle of the same column.
|
|
1011
|
+
*/}
|
|
1012
|
+
<VStack gap={2}>
|
|
1013
|
+
{REPO_STATS.map(stat => (
|
|
1014
|
+
<HStack key={stat.label} gap={2} vAlign="center">
|
|
1015
|
+
<Icon icon={stat.icon} size="sm" color="secondary" />
|
|
1016
|
+
<Link href="#">
|
|
1017
|
+
{stat.value} {stat.label}
|
|
1018
|
+
</Link>
|
|
1019
|
+
</HStack>
|
|
1020
|
+
))}
|
|
1021
|
+
</VStack>
|
|
1022
|
+
</VStack>
|
|
1023
|
+
|
|
1024
|
+
<Divider />
|
|
1025
|
+
|
|
1026
|
+
<VStack gap={3}>
|
|
1027
|
+
<Heading level={3}>Releases</Heading>
|
|
1028
|
+
<HStack gap={2} vAlign="center">
|
|
1029
|
+
<Icon icon={Tag} size="sm" color="success" />
|
|
1030
|
+
<Text weight="bold">v2.4.0</Text>
|
|
1031
|
+
<Badge label="Latest" variant="success" />
|
|
1032
|
+
</HStack>
|
|
1033
|
+
<Text type="supporting" color="secondary">
|
|
1034
|
+
{commitAge(daysAgo(11))} · 36 releases
|
|
1035
|
+
</Text>
|
|
1036
|
+
</VStack>
|
|
1037
|
+
|
|
1038
|
+
<Divider />
|
|
1039
|
+
|
|
1040
|
+
<VStack gap={3}>
|
|
1041
|
+
<Heading level={3}>Contributors</Heading>
|
|
1042
|
+
{/*
|
|
1043
|
+
AvatarGroup overlaps its children by design, which fights a wrapping
|
|
1044
|
+
grid — so this is a plain wrapped row of avatars, which is the shape
|
|
1045
|
+
GitHub uses and the shape the group's own docs point you to here.
|
|
1046
|
+
*/}
|
|
1047
|
+
<HStack gap={1} wrap="wrap">
|
|
1048
|
+
{CONTRIBUTORS.map(name => (
|
|
1049
|
+
<Avatar key={name} name={name} size={32} />
|
|
1050
|
+
))}
|
|
1051
|
+
</HStack>
|
|
1052
|
+
<Link href="#">+ 61 contributors</Link>
|
|
1053
|
+
</VStack>
|
|
1054
|
+
</VStack>
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* The bar between the repo header and the file list: which ref you are looking
|
|
1060
|
+
* at on the left, and what you can do with it on the right.
|
|
1061
|
+
*/
|
|
1062
|
+
function BranchBar({
|
|
1063
|
+
query,
|
|
1064
|
+
onQueryChange,
|
|
1065
|
+
searchRef,
|
|
1066
|
+
isSearching,
|
|
1067
|
+
matchCount,
|
|
1068
|
+
isNarrow,
|
|
1069
|
+
isSearchOpen,
|
|
1070
|
+
onSearchOpen,
|
|
1071
|
+
}: {
|
|
1072
|
+
query: string;
|
|
1073
|
+
onQueryChange: (value: string) => void;
|
|
1074
|
+
searchRef: React.RefObject<HTMLInputElement | null>;
|
|
1075
|
+
isSearching: boolean;
|
|
1076
|
+
matchCount: number;
|
|
1077
|
+
isNarrow: boolean;
|
|
1078
|
+
isSearchOpen: boolean;
|
|
1079
|
+
onSearchOpen: () => void;
|
|
1080
|
+
}) {
|
|
1081
|
+
return (
|
|
1082
|
+
/*
|
|
1083
|
+
* One height for the whole row. Toolbar gets this by wrapping its slots in
|
|
1084
|
+
* a SizeProvider, and this bar is a plain HStack, so it does the same.
|
|
1085
|
+
* Button and TextInput read the context; IconButton and DropdownMenu do
|
|
1086
|
+
* not, so those two are told directly — DropdownMenu takes it on its
|
|
1087
|
+
* `button` object, where it otherwise defaults to `md`.
|
|
1088
|
+
*/
|
|
1089
|
+
<SizeProvider value="sm">
|
|
1090
|
+
<HStack gap={2} vAlign="center" wrap="wrap">
|
|
1091
|
+
<DropdownMenu
|
|
1092
|
+
button={{
|
|
1093
|
+
label: 'main',
|
|
1094
|
+
size: 'sm',
|
|
1095
|
+
icon: <Icon icon={GitBranch} size="sm" />,
|
|
1096
|
+
}}
|
|
1097
|
+
hasChevron
|
|
1098
|
+
items={[
|
|
1099
|
+
{label: 'main', onClick: () => {}},
|
|
1100
|
+
{label: 'next', onClick: () => {}},
|
|
1101
|
+
{type: 'divider'},
|
|
1102
|
+
{label: 'View all branches', onClick: () => {}},
|
|
1103
|
+
]}
|
|
1104
|
+
/>
|
|
1105
|
+
{/*
|
|
1106
|
+
Ghost is the flat variant — no fill, no border. The count and the
|
|
1107
|
+
noun ride in one `label` because Button takes a string, so the
|
|
1108
|
+
number cannot be bolded separately the way it is in the stats row.
|
|
1109
|
+
|
|
1110
|
+
These drop to icons at the same width where the search field
|
|
1111
|
+
collapses. `label` carries the full "171 Branches" in both forms, so
|
|
1112
|
+
what a screen reader hears does not change with the viewport — only
|
|
1113
|
+
whether the text is painted alongside the glyph.
|
|
1114
|
+
*/}
|
|
1115
|
+
<HStack gap={1} vAlign="center">
|
|
1116
|
+
{REF_COUNTS.map(ref =>
|
|
1117
|
+
isNarrow ? (
|
|
1118
|
+
<IconButton
|
|
1119
|
+
key={ref.label}
|
|
1120
|
+
variant="ghost"
|
|
1121
|
+
size="sm"
|
|
1122
|
+
icon={<Icon icon={ref.icon} size="sm" />}
|
|
1123
|
+
label={ref.label}
|
|
1124
|
+
onClick={() => {}}
|
|
1125
|
+
/>
|
|
1126
|
+
) : (
|
|
1127
|
+
<Button
|
|
1128
|
+
key={ref.label}
|
|
1129
|
+
variant="ghost"
|
|
1130
|
+
size="sm"
|
|
1131
|
+
label={ref.label}
|
|
1132
|
+
onClick={() => {}}
|
|
1133
|
+
/>
|
|
1134
|
+
),
|
|
1135
|
+
)}
|
|
1136
|
+
</HStack>
|
|
1137
|
+
<StackItem size="fill">
|
|
1138
|
+
<HStack gap={2} hAlign="end" vAlign="center">
|
|
1139
|
+
{isSearching && matchCount > 0 && (
|
|
1140
|
+
<Text type="supporting" color="secondary">
|
|
1141
|
+
{matchCount} {matchCount === 1 ? 'result' : 'results'}
|
|
1142
|
+
</Text>
|
|
1143
|
+
)}
|
|
1144
|
+
{isNarrow && !isSearchOpen ? (
|
|
1145
|
+
<IconButton
|
|
1146
|
+
variant="ghost"
|
|
1147
|
+
size="sm"
|
|
1148
|
+
icon={<Icon icon={Search} size="sm" />}
|
|
1149
|
+
label="Go to file"
|
|
1150
|
+
onClick={onSearchOpen}
|
|
1151
|
+
/>
|
|
1152
|
+
) : (
|
|
1153
|
+
<TextInput
|
|
1154
|
+
label="Go to file"
|
|
1155
|
+
isLabelHidden
|
|
1156
|
+
placeholder="Go to file"
|
|
1157
|
+
startIcon={Search}
|
|
1158
|
+
value={query}
|
|
1159
|
+
onChange={onQueryChange}
|
|
1160
|
+
hasClear
|
|
1161
|
+
ref={searchRef}
|
|
1162
|
+
/>
|
|
1163
|
+
)}
|
|
1164
|
+
<DropdownMenu
|
|
1165
|
+
button={{label: 'Add file', size: 'sm'}}
|
|
1166
|
+
hasChevron
|
|
1167
|
+
items={[
|
|
1168
|
+
{label: 'Create new file', onClick: () => {}},
|
|
1169
|
+
{label: 'Upload files', onClick: () => {}},
|
|
1170
|
+
]}
|
|
1171
|
+
/>
|
|
1172
|
+
<Button
|
|
1173
|
+
label="Code"
|
|
1174
|
+
variant="primary"
|
|
1175
|
+
icon={<Icon icon={Code} size="sm" />}
|
|
1176
|
+
/>
|
|
1177
|
+
</HStack>
|
|
1178
|
+
</StackItem>
|
|
1179
|
+
</HStack>
|
|
1180
|
+
</SizeProvider>
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// ============= README =============
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* The repository's own documentation, under the file list.
|
|
1188
|
+
*
|
|
1189
|
+
* The tabs swap content in place rather than navigating, so they are a real
|
|
1190
|
+
* `role="tablist"` and each `Tab` names the panel it controls. `Markdown`
|
|
1191
|
+
* takes `headingLevelStart={2}` because the page's h1 is the repository name
|
|
1192
|
+
* up in the header — leaving it at 1 would put a second top-level heading in
|
|
1193
|
+
* the outline for what is really a section of this page.
|
|
1194
|
+
*/
|
|
1195
|
+
function ReadmePanel({
|
|
1196
|
+
markdown,
|
|
1197
|
+
hasRepoTabs,
|
|
1198
|
+
}: {
|
|
1199
|
+
markdown: string;
|
|
1200
|
+
/*
|
|
1201
|
+
* Code of conduct, licence and security policy are repository-level files
|
|
1202
|
+
* that live at the root, so they are not offered while you are looking
|
|
1203
|
+
* inside a folder — only that folder's own README is.
|
|
1204
|
+
*/
|
|
1205
|
+
hasRepoTabs: boolean;
|
|
1206
|
+
}) {
|
|
1207
|
+
const [tab, setTab] = useState('readme');
|
|
1208
|
+
const activeTab = hasRepoTabs ? tab : 'readme';
|
|
1209
|
+
|
|
1210
|
+
return (
|
|
1211
|
+
<Card padding={0}>
|
|
1212
|
+
{/*
|
|
1213
|
+
`hasDivider` is a border on the TabList itself, so the inset has to go
|
|
1214
|
+
inside it: padding moves the tabs in 4px while the border still runs
|
|
1215
|
+
the full width of the card. Putting that padding on the Section
|
|
1216
|
+
instead would carry the rule in with it and stop it short at both ends.
|
|
1217
|
+
*/}
|
|
1218
|
+
<Section variant="transparent" padding={0} paddingBlockStart={1}>
|
|
1219
|
+
<TabList
|
|
1220
|
+
value={activeTab}
|
|
1221
|
+
onChange={setTab}
|
|
1222
|
+
role="tablist"
|
|
1223
|
+
size="sm"
|
|
1224
|
+
hasDivider
|
|
1225
|
+
xstyle={styles.readmeTabs}>
|
|
1226
|
+
<Tab value="readme" label="Readme" panelId="repo-readme" />
|
|
1227
|
+
{hasRepoTabs && (
|
|
1228
|
+
<Tab
|
|
1229
|
+
value="conduct"
|
|
1230
|
+
label="Code of conduct"
|
|
1231
|
+
panelId="repo-conduct"
|
|
1232
|
+
/>
|
|
1233
|
+
)}
|
|
1234
|
+
{hasRepoTabs && (
|
|
1235
|
+
<Tab value="license" label="MIT license" panelId="repo-license" />
|
|
1236
|
+
)}
|
|
1237
|
+
{hasRepoTabs && (
|
|
1238
|
+
<Tab value="security" label="Security" panelId="repo-security" />
|
|
1239
|
+
)}
|
|
1240
|
+
</TabList>
|
|
1241
|
+
</Section>
|
|
1242
|
+
<Section variant="transparent" padding={6} id={`repo-${activeTab}`}>
|
|
1243
|
+
{activeTab === 'readme' ? (
|
|
1244
|
+
// Prose already caps at Markdown's default 680px measure; centring
|
|
1245
|
+
// it stops that column hugging the left edge of a much wider card.
|
|
1246
|
+
<Markdown headingLevelStart={2} contentAlign="center">
|
|
1247
|
+
{markdown}
|
|
1248
|
+
</Markdown>
|
|
1249
|
+
) : (
|
|
1250
|
+
<Text color="secondary">
|
|
1251
|
+
{TAB_PLACEHOLDER[activeTab as keyof typeof TAB_PLACEHOLDER]}
|
|
1252
|
+
</Text>
|
|
1253
|
+
)}
|
|
1254
|
+
</Section>
|
|
1255
|
+
</Card>
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/*
|
|
1260
|
+
* The other three tabs exist to show the shape of the control; a template that
|
|
1261
|
+
* shipped four full documents would be teaching markdown, not layout.
|
|
1262
|
+
*/
|
|
1263
|
+
const TAB_PLACEHOLDER = {
|
|
1264
|
+
conduct: 'The Contributor Covenant, version 2.1, applies to this project.',
|
|
1265
|
+
license: 'MIT. Do what you like, keep the notice, expect no warranty.',
|
|
1266
|
+
security: 'Report vulnerabilities privately through a security advisory.',
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
// ============= PAGE =============
|
|
1270
|
+
|
|
1271
|
+
export default function FileTreeTemplate() {
|
|
1272
|
+
const [query, setQuery] = useState('');
|
|
1273
|
+
const [manualExpandedIds, setManualExpandedIds] = useState<
|
|
1274
|
+
ReadonlySet<string>
|
|
1275
|
+
>(() => new Set(DEFAULT_EXPANDED));
|
|
1276
|
+
const [focusedId, setFocusedId] = useState<string | null>(null);
|
|
1277
|
+
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
|
1278
|
+
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
|
1279
|
+
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
1280
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
1281
|
+
/*
|
|
1282
|
+
* Two regions, two numbers. The header spans the whole page, so it is what
|
|
1283
|
+
* decides whether the repo actions keep their labels. The branch bar sits in
|
|
1284
|
+
* the content column beside the sidebar and can be far narrower than the
|
|
1285
|
+
* page — measuring the header for it would leave the search field expanded
|
|
1286
|
+
* in a column that cannot hold it.
|
|
1287
|
+
*/
|
|
1288
|
+
const headerRef = useRef<HTMLDivElement>(null);
|
|
1289
|
+
const headerWidth = useMeasuredWidth(headerRef);
|
|
1290
|
+
const isNarrowHeader = headerWidth != null && headerWidth <= NARROW_HEADER;
|
|
1291
|
+
|
|
1292
|
+
const bodyRef = useRef<HTMLDivElement>(null);
|
|
1293
|
+
const bodyWidth = useMeasuredWidth(bodyRef);
|
|
1294
|
+
const isNarrowBody = bodyWidth != null && bodyWidth <= NARROW_HEADER;
|
|
1295
|
+
|
|
1296
|
+
/*
|
|
1297
|
+
* Below this the sidebar would starve the table — 296px off an already
|
|
1298
|
+
* short page leaves the commit column clipped. It moves under the README
|
|
1299
|
+
* instead, which is where GitHub puts it at the same size.
|
|
1300
|
+
*/
|
|
1301
|
+
const isSidebarInline = useMediaQuery('(min-width: 1024px)');
|
|
1302
|
+
|
|
1303
|
+
/*
|
|
1304
|
+
* The field is only hidden, never unmounted, so `hasAutoFocus` would not
|
|
1305
|
+
* fire on the way back in — the button reveals an existing element rather
|
|
1306
|
+
* than mounting a new one.
|
|
1307
|
+
*/
|
|
1308
|
+
useEffect(() => {
|
|
1309
|
+
if (isSearchOpen) {
|
|
1310
|
+
searchRef.current?.focus();
|
|
1311
|
+
}
|
|
1312
|
+
}, [isSearchOpen]);
|
|
1313
|
+
|
|
1314
|
+
/*
|
|
1315
|
+
* Collapsing with a query still applied would hide the reason the tree is
|
|
1316
|
+
* pruned, so an empty field is the only way back to the button. Bound
|
|
1317
|
+
* natively because blur does not bubble and TextInput takes no `onBlur`.
|
|
1318
|
+
*/
|
|
1319
|
+
useEffect(() => {
|
|
1320
|
+
const input = searchRef.current;
|
|
1321
|
+
if (input == null) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const collapseIfEmpty = () => {
|
|
1325
|
+
if (query === '') {
|
|
1326
|
+
setIsSearchOpen(false);
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
input.addEventListener('blur', collapseIfEmpty);
|
|
1330
|
+
return () => input.removeEventListener('blur', collapseIfEmpty);
|
|
1331
|
+
// `isSearchOpen` mounts and unmounts the field, so the listener has to be
|
|
1332
|
+
// rebound onto whichever element is currently there.
|
|
1333
|
+
}, [query, isSearchOpen]);
|
|
1334
|
+
|
|
1335
|
+
const needle = query.trim().toLowerCase();
|
|
1336
|
+
const isSearching = needle.length > 0;
|
|
1337
|
+
|
|
1338
|
+
// Pruning changes what a folder contains, so the rollups have to be
|
|
1339
|
+
// recomputed from the survivors. Reusing the unpruned totals would leave a
|
|
1340
|
+
// folder claiming more bytes than the files visible underneath it add up to.
|
|
1341
|
+
const data = useMemo(
|
|
1342
|
+
() => (isSearching ? withRollups(pruneTree(ROLLED_UP, needle)) : ROLLED_UP),
|
|
1343
|
+
[isSearching, needle],
|
|
1344
|
+
);
|
|
1345
|
+
|
|
1346
|
+
// While searching, expansion is derived from the pruned tree so no match can
|
|
1347
|
+
// hide behind a collapsed parent; manual state is parked, not overwritten.
|
|
1348
|
+
const searchExpandedIds = useMemo(
|
|
1349
|
+
() => (isSearching ? collectIds(data, new Set<string>()) : null),
|
|
1350
|
+
[isSearching, data],
|
|
1351
|
+
);
|
|
1352
|
+
const expandedIds = searchExpandedIds ?? manualExpandedIds;
|
|
1353
|
+
|
|
1354
|
+
const sortable = useTableSortableState<RepoEntry>({
|
|
1355
|
+
data,
|
|
1356
|
+
comparators: {
|
|
1357
|
+
// Folders first, then by name — the ordering every repo browser uses.
|
|
1358
|
+
name: (a, b) =>
|
|
1359
|
+
Number(b.kind === 'folder') - Number(a.kind === 'folder') ||
|
|
1360
|
+
a.name.localeCompare(b.name),
|
|
1361
|
+
message: (a, b) => a.commitMessage.localeCompare(b.commitMessage),
|
|
1362
|
+
committed: (a, b) => a.committedAt - b.committedAt,
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
const {visibleData, treeConfig} = useTableTreeState<RepoEntry>({
|
|
1367
|
+
data,
|
|
1368
|
+
idKey: 'id',
|
|
1369
|
+
expandedIds,
|
|
1370
|
+
onExpandedIdsChange: isSearching ? () => {} : setManualExpandedIds,
|
|
1371
|
+
// Reorders each sibling group in place, so children never leave their
|
|
1372
|
+
// parent — this is what keeps sorting and hierarchy compatible.
|
|
1373
|
+
sortSiblings: sortable.applySort,
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
const setExpanded = useCallback(
|
|
1377
|
+
(id: string, shouldExpand: boolean) => {
|
|
1378
|
+
if (isSearching) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
setManualExpandedIds(previous => {
|
|
1382
|
+
const next = new Set(previous);
|
|
1383
|
+
if (shouldExpand) {
|
|
1384
|
+
next.add(id);
|
|
1385
|
+
} else {
|
|
1386
|
+
next.delete(id);
|
|
1387
|
+
}
|
|
1388
|
+
return next;
|
|
1389
|
+
});
|
|
1390
|
+
},
|
|
1391
|
+
[isSearching],
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
const expandRow = useCallback(
|
|
1395
|
+
(id: string) => setExpanded(id, true),
|
|
1396
|
+
[setExpanded],
|
|
1397
|
+
);
|
|
1398
|
+
const collapseRow = useCallback(
|
|
1399
|
+
(id: string) => setExpanded(id, false),
|
|
1400
|
+
[setExpanded],
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1403
|
+
/*
|
|
1404
|
+
* Selecting a folder is what the header trail reads from. Ids are paths, so
|
|
1405
|
+
* the trail is the id split on '/' — there is no second structure to keep in
|
|
1406
|
+
* step with the tree.
|
|
1407
|
+
*/
|
|
1408
|
+
const selectFolder = useCallback(
|
|
1409
|
+
(id: string) => {
|
|
1410
|
+
setSelectedFolderId(id);
|
|
1411
|
+
// Selecting a folder you cannot see into is a dead end, so open it.
|
|
1412
|
+
setExpanded(id, true);
|
|
1413
|
+
},
|
|
1414
|
+
[setExpanded],
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
/*
|
|
1418
|
+
* The header trail, root first. Ids are paths, so each crumb's target is
|
|
1419
|
+
* just the prefix up to that segment — there is no second structure to keep
|
|
1420
|
+
* in step with the tree.
|
|
1421
|
+
*/
|
|
1422
|
+
const crumbs = useMemo((): Crumb[] => {
|
|
1423
|
+
const segments =
|
|
1424
|
+
selectedFolderId == null ? [] : selectedFolderId.split('/');
|
|
1425
|
+
return [
|
|
1426
|
+
{label: ROOT_NAME, id: null},
|
|
1427
|
+
...segments.map((segment, index) => ({
|
|
1428
|
+
label: segment,
|
|
1429
|
+
id: segments.slice(0, index + 1).join('/'),
|
|
1430
|
+
})),
|
|
1431
|
+
];
|
|
1432
|
+
}, [selectedFolderId]);
|
|
1433
|
+
|
|
1434
|
+
/*
|
|
1435
|
+
* Keep the first crumb and the last two; everything between them goes to
|
|
1436
|
+
* the menu. The first is the repo, the last is where you are, and the one
|
|
1437
|
+
* before it is the usual "up one level" target — those three carry almost
|
|
1438
|
+
* all the navigation, so the middle is what can afford to fold away.
|
|
1439
|
+
*/
|
|
1440
|
+
const hiddenCrumbs = useMemo(
|
|
1441
|
+
() =>
|
|
1442
|
+
crumbs.length > MAX_INLINE_CRUMBS
|
|
1443
|
+
? crumbs.slice(1, crumbs.length - 2)
|
|
1444
|
+
: [],
|
|
1445
|
+
[crumbs],
|
|
1446
|
+
);
|
|
1447
|
+
|
|
1448
|
+
const collapsedCrumbs = useMemo(
|
|
1449
|
+
(): Array<Crumb | typeof ELLIPSIS> =>
|
|
1450
|
+
hiddenCrumbs.length === 0
|
|
1451
|
+
? crumbs
|
|
1452
|
+
: [crumbs[0], ELLIPSIS, ...crumbs.slice(crumbs.length - 2)],
|
|
1453
|
+
[crumbs, hiddenCrumbs],
|
|
1454
|
+
);
|
|
1455
|
+
|
|
1456
|
+
const columns = useMemo(() => buildColumns(selectFolder), [selectFolder]);
|
|
1457
|
+
|
|
1458
|
+
const readme = useMemo(() => readmeFor(selectedFolderId), [selectedFolderId]);
|
|
1459
|
+
|
|
1460
|
+
// Focus follows `focusedId`, whether it moved by arrow key or by click, so
|
|
1461
|
+
// there is one path to keep the roving tabindex and the DOM in agreement.
|
|
1462
|
+
const shouldFocusRef = useRef(false);
|
|
1463
|
+
const moveFocus = useCallback((id: string) => {
|
|
1464
|
+
shouldFocusRef.current = true;
|
|
1465
|
+
setFocusedId(id);
|
|
1466
|
+
}, []);
|
|
1467
|
+
|
|
1468
|
+
useEffect(() => {
|
|
1469
|
+
if (!shouldFocusRef.current || focusedId == null) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
shouldFocusRef.current = false;
|
|
1473
|
+
bodyRef.current
|
|
1474
|
+
?.querySelector<HTMLElement>(`tr[data-row-id="${CSS.escape(focusedId)}"]`)
|
|
1475
|
+
?.focus();
|
|
1476
|
+
}, [focusedId, visibleData]);
|
|
1477
|
+
|
|
1478
|
+
const tree = useTableTreeData({...treeConfig, hasExpandAllControl: true});
|
|
1479
|
+
const sort = useTableSortable<RepoEntry>(sortable.sortConfig);
|
|
1480
|
+
const resize = useTableColumnResize<RepoEntry>({
|
|
1481
|
+
columnWidths,
|
|
1482
|
+
onColumnResizeEnd: updates =>
|
|
1483
|
+
setColumnWidths(previous => ({...previous, ...updates})),
|
|
1484
|
+
columns: columns as TableColumn<Record<string, unknown>>[],
|
|
1485
|
+
/*
|
|
1486
|
+
* No global `minWidth` on purpose: it takes precedence over every column's
|
|
1487
|
+
* own floor, so setting one here would flatten Name's 240px down to a
|
|
1488
|
+
* single shared number. Each column carries its own instead — the fixed
|
|
1489
|
+
* ones floor at their declared width, Name at the minimum that still fits
|
|
1490
|
+
* an indented filename.
|
|
1491
|
+
*/
|
|
1492
|
+
});
|
|
1493
|
+
const keyboard = useTreeKeyboardNav({
|
|
1494
|
+
rows: visibleData,
|
|
1495
|
+
focusedId,
|
|
1496
|
+
onFocusedIdChange: moveFocus,
|
|
1497
|
+
expandedIds,
|
|
1498
|
+
onExpand: expandRow,
|
|
1499
|
+
onCollapse: collapseRow,
|
|
1500
|
+
selectedId: selectedFolderId,
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
const matchCount = useMemo(
|
|
1504
|
+
() => (isSearching ? countMatches(ROLLED_UP, needle) : 0),
|
|
1505
|
+
[isSearching, needle],
|
|
1506
|
+
);
|
|
1507
|
+
|
|
1508
|
+
return (
|
|
1509
|
+
<Layout
|
|
1510
|
+
/*
|
|
1511
|
+
* `fill` — the default — pins the layout to the viewport and scrolls the
|
|
1512
|
+
* content area internally, which would trap the README below the fold
|
|
1513
|
+
* with no way to reach it. `auto` lets the page itself scroll.
|
|
1514
|
+
*/
|
|
1515
|
+
height="auto"
|
|
1516
|
+
/*
|
|
1517
|
+
* Past this the commit column strands its date miles from its message.
|
|
1518
|
+
* Layout centres each slot inside the cap and leaves dividers
|
|
1519
|
+
* full-bleed, so the page keeps its edge-to-edge rules on a wide screen.
|
|
1520
|
+
*/
|
|
1521
|
+
contentWidth={1440}
|
|
1522
|
+
end={
|
|
1523
|
+
!isSidebarInline ? undefined : (
|
|
1524
|
+
<LayoutPanel
|
|
1525
|
+
width={296}
|
|
1526
|
+
padding={4}
|
|
1527
|
+
role="complementary"
|
|
1528
|
+
label="About this repository"
|
|
1529
|
+
/*
|
|
1530
|
+
* One scroller for the page, as on the reference: the sidebar
|
|
1531
|
+
* travels with the README rather than holding its own scrollbar
|
|
1532
|
+
* next to the page's.
|
|
1533
|
+
*/
|
|
1534
|
+
isScrollable={false}>
|
|
1535
|
+
<AboutPanel />
|
|
1536
|
+
</LayoutPanel>
|
|
1537
|
+
)
|
|
1538
|
+
}
|
|
1539
|
+
header={
|
|
1540
|
+
<Toolbar
|
|
1541
|
+
label="Repository"
|
|
1542
|
+
size="sm"
|
|
1543
|
+
dividers={['bottom']}
|
|
1544
|
+
xstyle={styles.headerContainer}
|
|
1545
|
+
startContent={
|
|
1546
|
+
<HStack gap={2} vAlign="center">
|
|
1547
|
+
{/*
|
|
1548
|
+
The repo name is the first crumb rather than a heading sitting
|
|
1549
|
+
beside the trail. As a heading it was 17px against the
|
|
1550
|
+
segments' 14px, so selecting a folder put two type sizes on one
|
|
1551
|
+
line; as a crumb it cannot drift from them. The page still owes
|
|
1552
|
+
the outline an h1, which no longer has anything to attach to,
|
|
1553
|
+
so it is supplied for assistive tech only.
|
|
1554
|
+
*/}
|
|
1555
|
+
<VisuallyHidden>
|
|
1556
|
+
<Heading level={1}>{ROOT_NAME}</Heading>
|
|
1557
|
+
</VisuallyHidden>
|
|
1558
|
+
<Breadcrumbs label="Folder path">
|
|
1559
|
+
{/*
|
|
1560
|
+
Breadcrumbs wraps rather than truncates when it runs out of
|
|
1561
|
+
room, which in a single-row toolbar pushes the whole header
|
|
1562
|
+
taller. Past MAX_INLINE_CRUMBS the middle folds into a menu
|
|
1563
|
+
on a "…" item, so the trail keeps its height and the ends —
|
|
1564
|
+
the repo you are in and the folder you are looking at — stay
|
|
1565
|
+
visible, which are the two you navigate from.
|
|
1566
|
+
*/}
|
|
1567
|
+
{collapsedCrumbs.map((crumb, index) =>
|
|
1568
|
+
crumb === ELLIPSIS ? (
|
|
1569
|
+
<BreadcrumbItem
|
|
1570
|
+
key="ellipsis"
|
|
1571
|
+
menu={hiddenCrumbs.map(hidden => ({
|
|
1572
|
+
label: hidden.label,
|
|
1573
|
+
onClick: () => setSelectedFolderId(hidden.id),
|
|
1574
|
+
}))}>
|
|
1575
|
+
…
|
|
1576
|
+
</BreadcrumbItem>
|
|
1577
|
+
) : (
|
|
1578
|
+
<BreadcrumbItem
|
|
1579
|
+
key={crumb.id ?? ROOT_NAME}
|
|
1580
|
+
isCurrent={index === collapsedCrumbs.length - 1}
|
|
1581
|
+
onClick={
|
|
1582
|
+
index === collapsedCrumbs.length - 1
|
|
1583
|
+
? undefined
|
|
1584
|
+
: () => setSelectedFolderId(crumb.id)
|
|
1585
|
+
}>
|
|
1586
|
+
{crumb.label}
|
|
1587
|
+
</BreadcrumbItem>
|
|
1588
|
+
),
|
|
1589
|
+
)}
|
|
1590
|
+
</Breadcrumbs>
|
|
1591
|
+
{/*
|
|
1592
|
+
Badge has no outline variant, so GitHub's grey-ringed pill is
|
|
1593
|
+
a filled neutral one here. It is also read-only by contract,
|
|
1594
|
+
which suits a status word that is not a control.
|
|
1595
|
+
*/}
|
|
1596
|
+
<Badge label="Public" variant="neutral" />
|
|
1597
|
+
</HStack>
|
|
1598
|
+
}
|
|
1599
|
+
ref={headerRef}
|
|
1600
|
+
endContent={
|
|
1601
|
+
/*
|
|
1602
|
+
* GitHub's three social counts. The leading slot on Button is
|
|
1603
|
+
* `icon`, not `startContent`, and `endContent` is documented to
|
|
1604
|
+
* take a Badge — which is exactly the attached-count shape.
|
|
1605
|
+
* Narrow drops the labels rather than wrapping the row.
|
|
1606
|
+
*/
|
|
1607
|
+
<>
|
|
1608
|
+
{REPO_STATS.map(stat =>
|
|
1609
|
+
isNarrowHeader ? (
|
|
1610
|
+
<IconButton
|
|
1611
|
+
key={stat.label}
|
|
1612
|
+
variant="secondary"
|
|
1613
|
+
size="sm"
|
|
1614
|
+
icon={<Icon icon={stat.icon} size="sm" />}
|
|
1615
|
+
label={`${stat.label}: ${stat.value}`}
|
|
1616
|
+
onClick={() => {}}
|
|
1617
|
+
/>
|
|
1618
|
+
) : (
|
|
1619
|
+
<Button
|
|
1620
|
+
key={stat.label}
|
|
1621
|
+
size="sm"
|
|
1622
|
+
label={STAT_ACTION[stat.label]}
|
|
1623
|
+
icon={<Icon icon={stat.icon} size="sm" />}
|
|
1624
|
+
endContent={<Badge label={stat.value} />}
|
|
1625
|
+
/>
|
|
1626
|
+
),
|
|
1627
|
+
)}
|
|
1628
|
+
</>
|
|
1629
|
+
}
|
|
1630
|
+
/>
|
|
1631
|
+
}
|
|
1632
|
+
content={
|
|
1633
|
+
<LayoutContent padding={4}>
|
|
1634
|
+
{data.length === 0 ? (
|
|
1635
|
+
<EmptyState
|
|
1636
|
+
title="No matching files"
|
|
1637
|
+
description="No file or folder in this project matches your search."
|
|
1638
|
+
actions={
|
|
1639
|
+
<Button
|
|
1640
|
+
label="Clear search"
|
|
1641
|
+
variant="secondary"
|
|
1642
|
+
onClick={() => setQuery('')}
|
|
1643
|
+
/>
|
|
1644
|
+
}
|
|
1645
|
+
/>
|
|
1646
|
+
) : (
|
|
1647
|
+
<VStack gap={6} ref={bodyRef}>
|
|
1648
|
+
<BranchBar
|
|
1649
|
+
query={query}
|
|
1650
|
+
onQueryChange={setQuery}
|
|
1651
|
+
searchRef={searchRef}
|
|
1652
|
+
isSearching={isSearching}
|
|
1653
|
+
matchCount={matchCount}
|
|
1654
|
+
isNarrow={isNarrowBody}
|
|
1655
|
+
isSearchOpen={isSearchOpen}
|
|
1656
|
+
onSearchOpen={() => setIsSearchOpen(true)}
|
|
1657
|
+
/>
|
|
1658
|
+
{/*
|
|
1659
|
+
The file list reads as one bounded object, the way GitHub
|
|
1660
|
+
boxes it. `padding={0}` matters twice: it lets rows reach the
|
|
1661
|
+
border so the zebra stripes are full-bleed inside the card,
|
|
1662
|
+
and it zeroes the container padding variables that Table's own
|
|
1663
|
+
edge-bleed subtracts from its margins.
|
|
1664
|
+
*/}
|
|
1665
|
+
<Card padding={0}>
|
|
1666
|
+
<Table<RepoEntry>
|
|
1667
|
+
data={visibleData}
|
|
1668
|
+
columns={columns}
|
|
1669
|
+
idKey="id"
|
|
1670
|
+
density="compact"
|
|
1671
|
+
// Rows are separated by tint, not by rule.
|
|
1672
|
+
dividers="none"
|
|
1673
|
+
isStriped
|
|
1674
|
+
// Without this a narrowed column wraps mid-word and the row
|
|
1675
|
+
// heights go ragged. It does not reach the tree column,
|
|
1676
|
+
// which wraps by design — see the note at the top.
|
|
1677
|
+
textOverflow="truncate"
|
|
1678
|
+
plugins={{tree, sort, resize, keyboard}}
|
|
1679
|
+
/>
|
|
1680
|
+
</Card>
|
|
1681
|
+
{readme != null && (
|
|
1682
|
+
<ReadmePanel
|
|
1683
|
+
markdown={readme}
|
|
1684
|
+
hasRepoTabs={selectedFolderId == null}
|
|
1685
|
+
/>
|
|
1686
|
+
)}
|
|
1687
|
+
{/*
|
|
1688
|
+
Same panel, different slot: once the layout drops the sidebar
|
|
1689
|
+
the metadata is not discarded, it just stacks under the file
|
|
1690
|
+
list and its README the way it does on a phone.
|
|
1691
|
+
*/}
|
|
1692
|
+
{!isSidebarInline && (
|
|
1693
|
+
<>
|
|
1694
|
+
<Divider />
|
|
1695
|
+
<AboutPanel />
|
|
1696
|
+
</>
|
|
1697
|
+
)}
|
|
1698
|
+
</VStack>
|
|
1699
|
+
)}
|
|
1700
|
+
</LayoutContent>
|
|
1701
|
+
}
|
|
1702
|
+
/>
|
|
1703
|
+
);
|
|
1704
|
+
}
|