@anchrd/intel 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +496 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.js +416 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +128 -0
- package/dist/adapters/content/content.types.d.ts +38 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-audit.d.ts +62 -0
- package/dist/adapters/db/db-audit.js +150 -0
- package/dist/adapters/db/db-boards.d.ts +6 -0
- package/dist/adapters/db/db-boards.js +324 -0
- package/dist/adapters/db/db-feed.d.ts +30 -0
- package/dist/adapters/db/db-feed.js +276 -0
- package/dist/adapters/db/db-flows.d.ts +42 -0
- package/dist/adapters/db/db-flows.js +1311 -0
- package/dist/adapters/db/db-grants.d.ts +133 -0
- package/dist/adapters/db/db-grants.js +269 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +200 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-prompts.d.ts +46 -0
- package/dist/adapters/db/db-prompts.js +144 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +1687 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +26 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +77 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +301 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +129 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +139 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +87 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/audit/audit.d.ts +2 -0
- package/dist/audit/audit.js +87 -0
- package/dist/audit/audit.types.d.ts +23 -0
- package/dist/audit/audit.types.js +1 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +284 -0
- package/dist/auth/auth.types.d.ts +108 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/boards/boards.d.ts +44 -0
- package/dist/boards/boards.js +447 -0
- package/dist/boards/boards.types.d.ts +117 -0
- package/dist/boards/boards.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +192 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1194 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +262 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/contract/audit.d.ts +50 -0
- package/dist/contract/audit.js +72 -0
- package/dist/contract/board.d.ts +150 -0
- package/dist/contract/board.js +262 -0
- package/dist/contract/bundle.d.ts +147 -0
- package/dist/contract/bundle.js +139 -0
- package/dist/contract/contract.d.ts +112 -0
- package/dist/contract/contract.js +143 -0
- package/dist/contract/feed.d.ts +126 -0
- package/dist/contract/feed.js +149 -0
- package/dist/contract/flow-run.d.ts +346 -0
- package/dist/contract/flow-run.js +194 -0
- package/dist/contract/flow.d.ts +1057 -0
- package/dist/contract/flow.js +552 -0
- package/dist/contract/node.d.ts +551 -0
- package/dist/contract/node.js +456 -0
- package/dist/contract/share.d.ts +216 -0
- package/dist/contract/share.js +134 -0
- package/dist/contract/table.d.ts +171 -0
- package/dist/contract/table.js +117 -0
- package/dist/contract/tool.d.ts +122 -0
- package/dist/contract/tool.js +172 -0
- package/dist/feed/feed.d.ts +2 -0
- package/dist/feed/feed.js +77 -0
- package/dist/feed/feed.types.d.ts +23 -0
- package/dist/feed/feed.types.js +1 -0
- package/dist/flows/flows.d.ts +41 -0
- package/dist/flows/flows.js +2216 -0
- package/dist/flows/flows.types.d.ts +339 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +9 -0
- package/dist/http/http.js +874 -0
- package/dist/http/http.types.d.ts +22 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +239 -0
- package/dist/indexing/indexing.types.d.ts +11 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +194 -0
- package/dist/intel/intel.types.d.ts +24 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +1292 -0
- package/dist/mcp/mcp.types.d.ts +25 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/nodes/document-links/document-links.d.ts +20 -0
- package/dist/nodes/document-links/document-links.js +57 -0
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1667 -0
- package/dist/nodes/nodes.types.d.ts +511 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/prompts/prompts.d.ts +2 -0
- package/dist/prompts/prompts.js +65 -0
- package/dist/prompts/prompts.types.d.ts +71 -0
- package/dist/prompts/prompts.types.js +1 -0
- package/dist/shared/callers/callers.d.ts +28 -0
- package/dist/shared/callers/callers.js +30 -0
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/document-text/document-text.d.ts +21 -0
- package/dist/shared/document-text/document-text.js +31 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
- package/dist/shared/gate-authorization/gate-authorization.js +26 -0
- package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
- package/dist/shared/grant-expiry/grant-expiry.js +26 -0
- package/dist/shared/intel-error/intel-error.d.ts +22 -0
- package/dist/shared/intel-error/intel-error.js +27 -0
- package/dist/shared/plain-title/plain-title.d.ts +16 -0
- package/dist/shared/plain-title/plain-title.js +45 -0
- package/dist/shared/problem-details/problem-details.d.ts +30 -0
- package/dist/shared/problem-details/problem-details.js +18 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +2 -0
- package/dist/shared/sha256/sha256.js +14 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +177 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +207 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/migrations/0007_no_node_waits.sql +69 -0
- package/migrations/0008_three_layers.sql +130 -0
- package/migrations/0009_no_context_policy.sql +48 -0
- package/migrations/0010_no_run_waits_a_year.sql +27 -0
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +97 -0
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/migrations/0020_cascade_purge_replay.sql +11 -0
- package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
- package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
- package/migrations/0023_a_board_and_its_tasks.sql +116 -0
- package/migrations/0024_the_archive_is_a_column.sql +33 -0
- package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
- package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
- package/migrations/0027_the_runs_of_every_flow.sql +27 -0
- package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
- package/package.json +118 -0
- package/src/contract/CLAUDE.md +152 -0
- package/src/contract/README.md +102 -0
- package/src/contract/audit.ts +85 -0
- package/src/contract/board.ts +306 -0
- package/src/contract/bundle.ts +157 -0
- package/src/contract/contract.ts +161 -0
- package/src/contract/feed.ts +165 -0
- package/src/contract/flow-run.ts +241 -0
- package/src/contract/flow.ts +675 -0
- package/src/contract/node.ts +583 -0
- package/src/contract/share.ts +196 -0
- package/src/contract/table.ts +178 -0
- package/src/contract/tool.ts +202 -0
- package/ui/README.md +110 -0
- package/ui/components.json +21 -0
- package/ui/index.html +13 -0
- package/ui/package.json +11 -0
- package/ui/scripts/lint-tokens.mjs +187 -0
- package/ui/src/access-summary/access-summary.tsx +385 -0
- package/ui/src/app/action-slot/action-slot.tsx +27 -0
- package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
- package/ui/src/app/app-tree/app-tree.tsx +1175 -0
- package/ui/src/app/app.tsx +87 -0
- package/ui/src/app/header-search/header-search.tsx +294 -0
- package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
- package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
- package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
- package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
- package/ui/src/app/tree-move/tree-move.tsx +427 -0
- package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
- package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
- package/ui/src/app/user-footer/user-footer.tsx +188 -0
- package/ui/src/app/view-menu/view-menu.tsx +115 -0
- package/ui/src/app-root/app-root.tsx +109 -0
- package/ui/src/app-root/app-root.types.ts +27 -0
- package/ui/src/archive/archive.tsx +417 -0
- package/ui/src/attachment-detail/attachment-detail.ts +52 -0
- package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
- package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
- package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
- package/ui/src/board/board-assignee/board-assignee.ts +48 -0
- package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
- package/ui/src/board/board-chip/board-chip.tsx +46 -0
- package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
- package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
- package/ui/src/board/board-data/board-data.ts +195 -0
- package/ui/src/board/board-data/board-data.types.ts +38 -0
- package/ui/src/board/board-dates/board-dates.ts +40 -0
- package/ui/src/board/board-dates/board-dates.tsx +105 -0
- package/ui/src/board/board-kanban/board-kanban.ts +162 -0
- package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
- package/ui/src/board/board-open-task/board-open-task.ts +32 -0
- package/ui/src/board/board-panel/board-panel.tsx +259 -0
- package/ui/src/board/board-settings/board-settings.tsx +226 -0
- package/ui/src/board/board-status/board-status.tsx +60 -0
- package/ui/src/board/board-stripes/board-stripes.ts +128 -0
- package/ui/src/board/board-table/board-table.ts +53 -0
- package/ui/src/board/board-table/board-table.tsx +610 -0
- package/ui/src/board/board-task/board-task.ts +108 -0
- package/ui/src/board/board-task/board-task.tsx +335 -0
- package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
- package/ui/src/branding/branding.tsx +17 -0
- package/ui/src/branding/custom-logo.ts +1 -0
- package/ui/src/branding/favicon.default.svg +5 -0
- package/ui/src/branding/favicon.svg +5 -0
- package/ui/src/components/ui/avatar.tsx +39 -0
- package/ui/src/components/ui/button.tsx +64 -0
- package/ui/src/components/ui/collapsible.tsx +20 -0
- package/ui/src/components/ui/command.tsx +160 -0
- package/ui/src/components/ui/dialog.tsx +143 -0
- package/ui/src/components/ui/dropdown-menu.tsx +187 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/popover.tsx +41 -0
- package/ui/src/components/ui/select.tsx +163 -0
- package/ui/src/components/ui/separator.tsx +26 -0
- package/ui/src/components/ui/sheet.tsx +136 -0
- package/ui/src/components/ui/sidebar.tsx +693 -0
- package/ui/src/components/ui/skeleton.tsx +13 -0
- package/ui/src/components/ui/switch.tsx +25 -0
- package/ui/src/components/ui/table.tsx +82 -0
- package/ui/src/components/ui/tabs.tsx +52 -0
- package/ui/src/components/ui/tooltip.tsx +51 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
- package/ui/src/data/query-client.ts +18 -0
- package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
- package/ui/src/data/request-refusal/request-refusal.ts +51 -0
- package/ui/src/data/sign-in/sign-in.ts +97 -0
- package/ui/src/data/sign-in/sign-in.types.ts +15 -0
- package/ui/src/document-link/document-link.tsx +114 -0
- package/ui/src/document-markdown/document-markdown.ts +195 -0
- package/ui/src/editor-schema/editor-schema.ts +23 -0
- package/ui/src/entry-picker/entry-picker.tsx +238 -0
- package/ui/src/feed/feed.tsx +344 -0
- package/ui/src/file-preview/file-preview-view.tsx +496 -0
- package/ui/src/file-preview/file-preview.tsx +28 -0
- package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
- package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
- package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
- package/ui/src/file-preview/word-file-preview.tsx +5 -0
- package/ui/src/flow-runs/flow-runs.tsx +238 -0
- package/ui/src/flows/flows.tsx +1646 -0
- package/ui/src/flows/node-icon/node-icon.ts +34 -0
- package/ui/src/flows/node-palette/node-palette.tsx +214 -0
- package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
- package/ui/src/folder-contents/folder-contents.tsx +321 -0
- package/ui/src/folder-index/folder-index.ts +55 -0
- package/ui/src/frontmatter/frontmatter.tsx +193 -0
- package/ui/src/graph-pane/graph-pane.tsx +53 -0
- package/ui/src/hooks/use-mobile.ts +19 -0
- package/ui/src/i18n/custom.json +4 -0
- package/ui/src/i18n/de.json +609 -0
- package/ui/src/i18n/en.json +609 -0
- package/ui/src/i18n/es.json +609 -0
- package/ui/src/i18n/i18n-context.tsx +65 -0
- package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/ui/src/i18n/i18n.ts +32 -0
- package/ui/src/i18n/i18n.types.ts +49 -0
- package/ui/src/kind-icon.ts +97 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +68 -0
- package/ui/src/modal/modal.tsx +53 -0
- package/ui/src/node-details/node-details.tsx +545 -0
- package/ui/src/node-editor/node-editor.tsx +304 -0
- package/ui/src/node-graph/graph-notice.tsx +21 -0
- package/ui/src/node-graph/node-graph.ts +214 -0
- package/ui/src/node-graph/node-graph.tsx +295 -0
- package/ui/src/node-import/node-import.tsx +418 -0
- package/ui/src/node-save-error.ts +46 -0
- package/ui/src/node-source/node-source.tsx +128 -0
- package/ui/src/node-table/node-table.tsx +107 -0
- package/ui/src/nodes/nodes.tsx +240 -0
- package/ui/src/rename-resource/rename-resource.ts +80 -0
- package/ui/src/resolved-names/resolved-names.tsx +67 -0
- package/ui/src/resource-error.ts +33 -0
- package/ui/src/resource-menu/resource-menu.tsx +1156 -0
- package/ui/src/router/router-context.ts +6 -0
- package/ui/src/router/router.tsx +67 -0
- package/ui/src/router/router.types.ts +8 -0
- package/ui/src/router/selection-search.ts +88 -0
- package/ui/src/save-button/save-button.tsx +94 -0
- package/ui/src/section-hint/section-hint.tsx +40 -0
- package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
- package/ui/src/styles.css +189 -0
- package/ui/src/table-columns/table-columns.tsx +379 -0
- package/ui/src/theme/custom.css +1 -0
- package/ui/src/theme/theme-context.tsx +89 -0
- package/ui/src/theme/theme.ts +70 -0
- package/ui/src/time/relative-time.tsx +41 -0
- package/ui/src/time/time-context.tsx +78 -0
- package/ui/src/time/time.ts +165 -0
- package/ui/src/title-row/title-row.tsx +185 -0
- package/ui/src/tools/tools.tsx +546 -0
- package/ui/src/user-name/user-name.ts +61 -0
- package/ui/src/verbatim/verbatim.tsx +68 -0
- package/ui/tsconfig.json +17 -0
- package/ui/vite.config.ts +129 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { BlockNoteDocument } from "@contract/node.ts";
|
|
2
|
+
import type { IntelEditor, IntelEditorPartialBlock } from "@/editor-schema/editor-schema.ts";
|
|
3
|
+
import { FrontmatterBlockType, splitFrontmatter } from "@/frontmatter/frontmatter.tsx";
|
|
4
|
+
import { VerbatimBlockType } from "@/verbatim/verbatim.tsx";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The bridge between a document's markdown and the editor's blocks, in both directions.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ **The markdown is the truth; the editor is a nicer view onto it** (Jack, 2026-08-30). That
|
|
10
|
+
* sentence was not true when it was written. The editor writes with `blocksToMarkdownLossy` — the
|
|
11
|
+
* name is BlockNote's, not ours — and what it does not understand it rewrites into what it does.
|
|
12
|
+
* Exactly one construct had a way out: the head, given one in #657 after it had already cost data.
|
|
13
|
+
* The comment there names the damage, and it is worth repeating because it is the reason this
|
|
14
|
+
* module exists: *"Opening a document and saving it left its metadata unfindable, and nothing
|
|
15
|
+
* anywhere went red."*
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ `payload.markdown` is not decoration. `indexing.ts` pulls that field and nothing else into
|
|
18
|
+
* full text and vectors, so it is the whole of what a search can match, and `bundle.ts` writes it
|
|
19
|
+
* out as the document's `.md`. A document can be right on screen and unfindable.
|
|
20
|
+
*
|
|
21
|
+
* **The way out, generalised from the head:** a piece the editor cannot give back is parked as raw
|
|
22
|
+
* text and written back byte for byte (#826). Two rules follow from that, and both are load-bearing:
|
|
23
|
+
*
|
|
24
|
+
* - **What the editor OWNS, it may re-spell.** `- a` comes back as `* a`, `_kursiv_` as `*kursiv*`,
|
|
25
|
+
* a table padded to its widest cell. That is one markdown construct written another way, and the
|
|
26
|
+
* document says the same thing afterwards.
|
|
27
|
+
* - **What it does not own, it does not touch.** An HTML block, a comment, a math block, a
|
|
28
|
+
* directive: measured, those come back as their inner text, as an empty string, or mangled with
|
|
29
|
+
* backslashes. They are parked.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ **The separation between blocks is normalised, their CONTENT is not.** Blocks are joined with
|
|
32
|
+
* one blank line and the document ends with one newline, which is what the serialiser has always
|
|
33
|
+
* produced. A document already in that form comes back byte for byte; one with three blank lines
|
|
34
|
+
* between two paragraphs loses two of them on the first save, and nothing else.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Everything markdown uses to SPELL something, plus whitespace. What is left over is what the
|
|
38
|
+
// author wrote — and a re-spelling may not change that by a single character.
|
|
39
|
+
//
|
|
40
|
+
// ⚠️ This is the whole classifier, and it is deliberately not a byte comparison: measured against
|
|
41
|
+
// BlockNote 0.52, a byte comparison parks every `-` bulleted list, every `_kursiv_`, every table
|
|
42
|
+
// and every setext heading, because the serialiser answers each of them in its own spelling. A
|
|
43
|
+
// document written by an agent is full of those, and parking them would make most of it read-only
|
|
44
|
+
// for a difference nobody can see.
|
|
45
|
+
const SPELLING = /[\s*_\-#>|=+~`.:()[\]!]/g;
|
|
46
|
+
|
|
47
|
+
function spoken(markdown: string): string {
|
|
48
|
+
return markdown.replace(SPELLING, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The document cut into top-level pieces, at blank lines.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ A fence is not cut into. Everything else may be: an HTML block with a blank line in it becomes
|
|
55
|
+
* two pieces, both parked, and the join puts the blank line back — the bytes come out the same
|
|
56
|
+
* either way.
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ **A fence is closed by its own marker AND at least its own length**, never by the marker
|
|
59
|
+
* alone. Four backticks around a block that shows three is how one writes about markdown in
|
|
60
|
+
* markdown; a guard that only compares the character closes the outer fence at the inner one, cuts
|
|
61
|
+
* the rest at the next blank line, and the join then writes one blank line where the code had two.
|
|
62
|
+
* Found in review on this pull request, and its case is in the test file below.
|
|
63
|
+
*/
|
|
64
|
+
function segmentsOf(body: string): string[] {
|
|
65
|
+
const segments: string[] = [];
|
|
66
|
+
let current: string[] = [];
|
|
67
|
+
let fence: { marker: string; length: number } | null = null;
|
|
68
|
+
for (const line of body.split("\n")) {
|
|
69
|
+
const run = /^\s{0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
|
70
|
+
if (fence === null) {
|
|
71
|
+
if (run) fence = { marker: run[0] ?? "", length: run.length };
|
|
72
|
+
} else if (run && run[0] === fence.marker && run.length >= fence.length) {
|
|
73
|
+
fence = null;
|
|
74
|
+
}
|
|
75
|
+
if (fence === null && line.trim() === "") {
|
|
76
|
+
if (current.length > 0) segments.push(current.join("\n"));
|
|
77
|
+
current = [];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
current.push(line);
|
|
81
|
+
}
|
|
82
|
+
if (current.length > 0) segments.push(current.join("\n"));
|
|
83
|
+
return segments;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One piece, either as blocks the editor owns or as one parked block.
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ A piece that throws while being parsed is parked rather than lost. A parser that gives up is
|
|
90
|
+
* the clearest case there is of something the editor does not understand.
|
|
91
|
+
*/
|
|
92
|
+
function blocksFor(editor: IntelEditor, segment: string): IntelEditorPartialBlock[] {
|
|
93
|
+
const parked = [{ type: VerbatimBlockType, props: { raw: segment } } as IntelEditorPartialBlock];
|
|
94
|
+
try {
|
|
95
|
+
const parsed = editor.tryParseMarkdownToBlocks(segment) as IntelEditorPartialBlock[];
|
|
96
|
+
const back = editor.blocksToMarkdownLossy(parsed as never) as unknown as string;
|
|
97
|
+
return spoken(back) === spoken(segment) ? parsed : parked;
|
|
98
|
+
} catch {
|
|
99
|
+
return parked;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Markdown to blocks, with the head lifted out of remark's way and everything the editor cannot
|
|
105
|
+
* give back parked as it stands.
|
|
106
|
+
*
|
|
107
|
+
* ⚠️ Synchronous, because both halves of the probe are: `tryParseMarkdownToBlocks` and
|
|
108
|
+
* `blocksToMarkdownLossy` return their answer directly in BlockNote 0.52. Making this `async` would
|
|
109
|
+
* put the load path behind a tick, and the effect in `node-editor.tsx` moves `savedSnapshot`
|
|
110
|
+
* immediately after it — a gap there is a document that reports itself unsaved the moment it opens
|
|
111
|
+
* (#115).
|
|
112
|
+
*/
|
|
113
|
+
export function markdownToBlocks(editor: IntelEditor, markdown: string): IntelEditorPartialBlock[] {
|
|
114
|
+
const { raw, body } = splitFrontmatter(markdown);
|
|
115
|
+
const blocks = segmentsOf(body).flatMap((segment) => blocksFor(editor, segment));
|
|
116
|
+
if (raw === null) return blocks;
|
|
117
|
+
return [{ type: FrontmatterBlockType, props: { raw } } as IntelEditorPartialBlock, ...blocks];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function rawOf(block: { props?: { raw?: unknown } } | undefined): string {
|
|
121
|
+
return typeof block?.props?.raw === "string" ? block.props.raw : "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Blocks to markdown: the head as a head, the parked pieces as they went in, the rest to the
|
|
126
|
+
* editor's own serialiser.
|
|
127
|
+
*
|
|
128
|
+
* ⚠️ The head and the parked pieces are found by TYPE, never by position. A block is a block: the
|
|
129
|
+
* drag handle moves them, and typing above the first one pushes it down. A positional guard would
|
|
130
|
+
* fall through to `blocksToMarkdownLossy` for exactly those documents, and that path serialises the
|
|
131
|
+
* block's rendered DOM — `payload.markdown` would carry the word "Metadata" where the metadata used
|
|
132
|
+
* to be (#657).
|
|
133
|
+
*
|
|
134
|
+
* ⚠️ The runs between two parked pieces are serialised TOGETHER, not one block at a time. A list is
|
|
135
|
+
* several blocks and one construct; serialising its items separately would write a list per item.
|
|
136
|
+
*/
|
|
137
|
+
export function blocksToMarkdown(editor: IntelEditor, blocks: unknown[]): string {
|
|
138
|
+
const all = blocks as { type?: string; props?: { raw?: unknown } }[];
|
|
139
|
+
const at = all.findIndex((block) => block?.type === FrontmatterBlockType);
|
|
140
|
+
// A document has ONE head, and it belongs at the top wherever the block ended up. Any further
|
|
141
|
+
// head block stays in the body and is serialised by the spec's own `toExternalHTML` — as a code
|
|
142
|
+
// block, which is visible and lossless without claiming to be a second head.
|
|
143
|
+
const body = all.filter((_, index) => index !== at);
|
|
144
|
+
const chunks: string[] = [];
|
|
145
|
+
let run: unknown[] = [];
|
|
146
|
+
const flush = () => {
|
|
147
|
+
if (run.length === 0) return;
|
|
148
|
+
chunks.push(editor.blocksToMarkdownLossy(run as never) as unknown as string);
|
|
149
|
+
run = [];
|
|
150
|
+
};
|
|
151
|
+
for (const block of body) {
|
|
152
|
+
if (block?.type === VerbatimBlockType) {
|
|
153
|
+
flush();
|
|
154
|
+
chunks.push(rawOf(block));
|
|
155
|
+
} else {
|
|
156
|
+
run.push(block);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
flush();
|
|
160
|
+
// One blank line between blocks and one newline at the end: the shape the serialiser has always
|
|
161
|
+
// produced, applied to the joins as well so that a parked piece and a written one are separated
|
|
162
|
+
// the same way.
|
|
163
|
+
const text = chunks
|
|
164
|
+
.map((chunk) => chunk.replace(/\n+$/, ""))
|
|
165
|
+
.filter((chunk) => chunk.length > 0)
|
|
166
|
+
.join("\n\n");
|
|
167
|
+
const document = text.length === 0 ? "" : `${text}\n`;
|
|
168
|
+
if (at < 0) return document;
|
|
169
|
+
// ⚠️ The blank line after the closing fence is part of the contract, not tidiness: without it the
|
|
170
|
+
// first line of the body sits against the fence, and a `# Title` there would be read back as part
|
|
171
|
+
// of nothing in particular the next time round.
|
|
172
|
+
return `---\n${rawOf(all[at])}\n---\n\n${document}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The markdown OF a stored document, whichever way it was stored.
|
|
177
|
+
*
|
|
178
|
+
* ⚠️ Two shapes reach this. What the editor writes is a BlockNote payload, and `markdown` beside its
|
|
179
|
+
* blocks is the whole of what anybody outside the editor reads — the same field `indexing.ts` pulls
|
|
180
|
+
* on the server (`document-text.ts` answers the same question there, for the same reason). What an
|
|
181
|
+
* agent writes over MCP is `text/markdown`, and that content IS its text.
|
|
182
|
+
*
|
|
183
|
+
* ⚠️ A payload that will not parse falls back to the raw content rather than to an empty box. An
|
|
184
|
+
* empty box invites somebody to type into it and save, and that save would replace a document
|
|
185
|
+
* nobody could read with one sentence.
|
|
186
|
+
*/
|
|
187
|
+
export function documentMarkdown(content: string | null): string {
|
|
188
|
+
if (!content) return "";
|
|
189
|
+
try {
|
|
190
|
+
const parsed = BlockNoteDocument.safeParse(JSON.parse(content));
|
|
191
|
+
return parsed.success ? parsed.data.markdown : content;
|
|
192
|
+
} catch {
|
|
193
|
+
return content;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BlockNoteSchema } from "@blocknote/core";
|
|
2
|
+
import { documentLinkSpec } from "@/document-link/document-link.tsx";
|
|
3
|
+
import { frontmatterSpec } from "@/frontmatter/frontmatter.tsx";
|
|
4
|
+
import { verbatimSpec } from "@/verbatim/verbatim.tsx";
|
|
5
|
+
|
|
6
|
+
// One schema for the editor and for everything that reads its documents back: BlockNote's defaults
|
|
7
|
+
// plus what Intel adds — the inline element of #41, the head block of #657 and, since #826, the
|
|
8
|
+
// block that carries whatever the editor cannot give back unchanged.
|
|
9
|
+
//
|
|
10
|
+
// ⚠️ `extend` rather than `create({ inlineContentSpecs: { ...defaults, documentLink } })`. Both
|
|
11
|
+
// build the same schema at run time, but only `extend` keeps the added types in the schema's own
|
|
12
|
+
// generics under this repo's `exactOptionalPropertyTypes` — with the spread form the editor infers
|
|
13
|
+
// BlockNote's default schema, typechecks, and then refuses to insert a document link.
|
|
14
|
+
export const intelEditorSchema = BlockNoteSchema.create().extend({
|
|
15
|
+
inlineContentSpecs: { documentLink: documentLinkSpec },
|
|
16
|
+
blockSpecs: { frontmatter: frontmatterSpec(), verbatim: verbatimSpec() },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// The schema carries its own editor and block types as declaration-only properties, which is the
|
|
20
|
+
// only way to name them here: writing `BlockNoteEditor<typeof schema.blockSchema, ...>` by hand
|
|
21
|
+
// fails BlockNote's own constraint under `exactOptionalPropertyTypes`.
|
|
22
|
+
export type IntelEditor = (typeof intelEditorSchema)["BlockNoteEditor"];
|
|
23
|
+
export type IntelEditorPartialBlock = (typeof intelEditorSchema)["PartialBlock"];
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { NodeKind } from "@contract/node.ts";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { ChevronRight, Folder, Search } from "lucide-react";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
6
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
7
|
+
|
|
8
|
+
// What this picker offers, whichever side of the shared tree it came from. Deliberately the four
|
|
9
|
+
// fields a picker needs and not the whole record: a Node and a Flow are two different things
|
|
10
|
+
// (ADR-0004), and merging them into one row is only allowed where the row is the entire question.
|
|
11
|
+
export type PickerEntry = { id: string; title: string; parentId: string | null; kind: PickerKind };
|
|
12
|
+
export type PickerKind = NodeKind | "flow";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One picker for everything a node or a flow can name: search by name, or click down through the
|
|
16
|
+
* tree from the top-level folders.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ Both halves read ONE source — the authorized flat graph the editor already loads. Not because
|
|
19
|
+
* a search endpoint would be hard, but because `searchNodes` answers a different question: it
|
|
20
|
+
* searches PASSAGES and hands back citations. Someone looking for a document by name would get
|
|
21
|
+
* results ranked by what is written inside it, which is the wrong list in a picker. Filtering names
|
|
22
|
+
* here is also instant, and it cannot disagree with the tree about what exists.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ What the caller may not see is absent from the graph, so it is absent here — and an empty
|
|
25
|
+
* result never says which of the two it is (#17, #19).
|
|
26
|
+
*/
|
|
27
|
+
export function EntryPicker({
|
|
28
|
+
kind,
|
|
29
|
+
flows = false,
|
|
30
|
+
value,
|
|
31
|
+
onSelect,
|
|
32
|
+
label,
|
|
33
|
+
}: {
|
|
34
|
+
// Which kind of node may be chosen. `null` offers every kind — what the Flow step uses.
|
|
35
|
+
//
|
|
36
|
+
// ⚠️ A list is not a convenience (#255). A caller that accepts a folder, a document OR a table
|
|
37
|
+
// has one question to ask, and the alternatives — one picker per kind, or a picker that offers
|
|
38
|
+
// everything and refuses afterwards — are either three dialogs or a rejection after the fact.
|
|
39
|
+
// What may be picked has to be the same question the caller can answer in one place.
|
|
40
|
+
kind: NodeKind | NodeKind[] | null;
|
|
41
|
+
// ⚠️ Flows are a second list, not a kind of node, so they are asked for rather than assumed. A
|
|
42
|
+
// caller that may aim at either says so; every other caller wants nodes only and pays for no
|
|
43
|
+
// second request.
|
|
44
|
+
flows?: boolean;
|
|
45
|
+
value: string;
|
|
46
|
+
onSelect(entryId: string, entryKind: PickerKind): void;
|
|
47
|
+
label: string;
|
|
48
|
+
}) {
|
|
49
|
+
const { data } = useIntelRouterContext();
|
|
50
|
+
const i18n = useI18n();
|
|
51
|
+
const [query, setQuery] = useState("");
|
|
52
|
+
const [openFolder, setOpenFolder] = useState<string | null>(null);
|
|
53
|
+
const graph = useQuery({
|
|
54
|
+
queryKey: ["node-graph"],
|
|
55
|
+
queryFn: () => data.getNodeGraph(),
|
|
56
|
+
});
|
|
57
|
+
const flowList = useQuery({
|
|
58
|
+
queryKey: ["flows"],
|
|
59
|
+
queryFn: () => data.listFlows(),
|
|
60
|
+
enabled: flows,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const nodes = useMemo<PickerEntry[]>(() => {
|
|
64
|
+
const fromNodes = (graph.data?.nodes ?? []).map((node) => ({
|
|
65
|
+
id: node.id,
|
|
66
|
+
title: node.title,
|
|
67
|
+
parentId: node.parentId,
|
|
68
|
+
kind: node.kind as PickerKind,
|
|
69
|
+
}));
|
|
70
|
+
if (!flows) return fromNodes;
|
|
71
|
+
return [
|
|
72
|
+
...fromNodes,
|
|
73
|
+
...(flowList.data?.items ?? []).map((flow) => ({
|
|
74
|
+
id: flow.id,
|
|
75
|
+
title: flow.title,
|
|
76
|
+
parentId: flow.parentId,
|
|
77
|
+
kind: "flow" as const,
|
|
78
|
+
})),
|
|
79
|
+
];
|
|
80
|
+
}, [graph.data, flowList.data, flows]);
|
|
81
|
+
// A flow is offered exactly when it was asked for — it is only in `nodes` at all in that case,
|
|
82
|
+
// and saying so here keeps "may this be picked" one rule rather than two that must agree.
|
|
83
|
+
//
|
|
84
|
+
// ⚠️ The array is rebuilt from `kind` on every render when a caller passes a literal, so the set
|
|
85
|
+
// is keyed by its contents rather than by identity — a `useMemo` on the array itself would be a
|
|
86
|
+
// memo that never hits.
|
|
87
|
+
const wanted = Array.isArray(kind) ? kind.join(",") : kind;
|
|
88
|
+
const pickable = useMemo(() => {
|
|
89
|
+
const allowed = wanted === null ? null : new Set(wanted.split(","));
|
|
90
|
+
return (entry: PickerEntry) =>
|
|
91
|
+
entry.kind === "flow" || allowed === null || allowed.has(entry.kind);
|
|
92
|
+
}, [wanted]);
|
|
93
|
+
const eligible = useMemo(() => nodes.filter(pickable), [nodes, pickable]);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether an entry stands on the picker's start level.
|
|
97
|
+
*
|
|
98
|
+
* ⚠️ The root is not `parentId === null` but the upper edge of what the reader may see — the same
|
|
99
|
+
* rule #429 gave the server in `levelPredicate`, at the second surface (#447). A recipient whose
|
|
100
|
+
* only access is a NESTED share carries a `parentId` pointing at a folder that is not in the list
|
|
101
|
+
* at all: under the old rule their start level was empty and STAYED empty, because the folder
|
|
102
|
+
* that would open it can never be clicked. Only searching reached them.
|
|
103
|
+
*
|
|
104
|
+
* ⚠️ Nothing is added by this. The list is what `getNodeGraph()` — and `listFlows()` where flows
|
|
105
|
+
* were asked for — answered, and the answer is already the authorized one. What changes is only
|
|
106
|
+
* which level an entry stands on.
|
|
107
|
+
*/
|
|
108
|
+
const rooted = useMemo(() => {
|
|
109
|
+
const present = new Set(nodes.map((node) => node.id));
|
|
110
|
+
return (entry: PickerEntry) => entry.parentId === null || !present.has(entry.parentId);
|
|
111
|
+
}, [nodes]);
|
|
112
|
+
|
|
113
|
+
// Searching looks at every eligible node wherever it sits; browsing looks at one level. The two
|
|
114
|
+
// are the same list seen two ways, which is why a result can be picked from either.
|
|
115
|
+
const searching = query.trim().length > 0;
|
|
116
|
+
const shown = useMemo(() => {
|
|
117
|
+
if (searching) {
|
|
118
|
+
const needle = query.trim().toLowerCase();
|
|
119
|
+
return eligible.filter((node) => node.title.toLowerCase().includes(needle)).slice(0, 50);
|
|
120
|
+
}
|
|
121
|
+
return nodes.filter((node) =>
|
|
122
|
+
openFolder === null ? rooted(node) : node.parentId === openFolder,
|
|
123
|
+
);
|
|
124
|
+
}, [searching, query, eligible, nodes, openFolder, rooted]);
|
|
125
|
+
|
|
126
|
+
// The way back up, as the chain of folders that leads to the open one.
|
|
127
|
+
const trail = useMemo(() => {
|
|
128
|
+
const chain: PickerEntry[] = [];
|
|
129
|
+
let current = openFolder;
|
|
130
|
+
while (current !== null) {
|
|
131
|
+
const folder = nodes.find((node) => node.id === current);
|
|
132
|
+
if (!folder) break;
|
|
133
|
+
chain.unshift(folder);
|
|
134
|
+
// ⚠️ The chain ends where the start level begins, and by the same rule that decides it. A
|
|
135
|
+
// folder at the upper edge has an ancestor the reader cannot see; walking into it would look
|
|
136
|
+
// for a level that does not exist here, and the "top" button above leads to this one.
|
|
137
|
+
current = rooted(folder) ? null : folder.parentId;
|
|
138
|
+
}
|
|
139
|
+
return chain;
|
|
140
|
+
}, [openFolder, nodes, rooted]);
|
|
141
|
+
|
|
142
|
+
const chosen = nodes.find((node) => node.id === value) ?? null;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="mt-4">
|
|
146
|
+
<span className="block text-sm font-medium">{label}</span>
|
|
147
|
+
{chosen ? (
|
|
148
|
+
<p className="mt-1 truncate text-sm text-muted-foreground">{chosen.title}</p>
|
|
149
|
+
) : (
|
|
150
|
+
<p className="mt-1 text-sm text-muted-foreground">{i18n.t("flows.linkEmpty")}</p>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
<div className="mt-2 rounded-lg border">
|
|
154
|
+
<label className="flex items-center gap-2 border-b px-2 py-1.5">
|
|
155
|
+
<Search aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" />
|
|
156
|
+
<span className="sr-only">{i18n.t("picker.search")}</span>
|
|
157
|
+
<input
|
|
158
|
+
type="search"
|
|
159
|
+
value={query}
|
|
160
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
161
|
+
placeholder={i18n.t("picker.search")}
|
|
162
|
+
className="w-full bg-transparent text-sm outline-none"
|
|
163
|
+
/>
|
|
164
|
+
</label>
|
|
165
|
+
|
|
166
|
+
{/* The trail is hidden while searching: a result can come from anywhere, so "where you are"
|
|
167
|
+
would be a claim about a place the list is no longer showing. */}
|
|
168
|
+
{!searching && trail.length > 0 ? (
|
|
169
|
+
<div className="flex flex-wrap items-center gap-1 border-b px-2 py-1 text-xs">
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={() => setOpenFolder(null)}
|
|
173
|
+
className="rounded px-1 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
|
174
|
+
>
|
|
175
|
+
{i18n.t("picker.top")}
|
|
176
|
+
</button>
|
|
177
|
+
{trail.map((folder) => (
|
|
178
|
+
<span key={folder.id} className="flex items-center gap-1">
|
|
179
|
+
<ChevronRight aria-hidden="true" className="size-3 text-muted-foreground" />
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={() => setOpenFolder(folder.id)}
|
|
183
|
+
className="rounded px-1 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
|
184
|
+
>
|
|
185
|
+
{folder.title}
|
|
186
|
+
</button>
|
|
187
|
+
</span>
|
|
188
|
+
))}
|
|
189
|
+
</div>
|
|
190
|
+
) : null}
|
|
191
|
+
|
|
192
|
+
<ul className="max-h-56 overflow-y-auto py-1">
|
|
193
|
+
{shown.map((node) => {
|
|
194
|
+
// A folder is two things while browsing: somewhere to go, and — when folders are what
|
|
195
|
+
// is being picked — something to choose. It gets both, side by side, rather than one
|
|
196
|
+
// control that has to guess which was meant.
|
|
197
|
+
const canOpen = node.kind === "folder" && !searching;
|
|
198
|
+
const canPick = pickable(node);
|
|
199
|
+
return (
|
|
200
|
+
<li key={`${node.kind}:${node.id}`} className="flex items-center gap-1 px-1">
|
|
201
|
+
{canPick ? (
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => onSelect(node.id, node.kind)}
|
|
205
|
+
aria-current={node.id === value}
|
|
206
|
+
className={`min-w-0 flex-1 truncate rounded px-2 py-1.5 text-left text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring ${
|
|
207
|
+
node.id === value ? "bg-muted font-medium" : ""
|
|
208
|
+
}`}
|
|
209
|
+
>
|
|
210
|
+
{node.title}
|
|
211
|
+
<span className="sr-only"> — {i18n.t(`node.kind.${node.kind}`)}</span>
|
|
212
|
+
</button>
|
|
213
|
+
) : (
|
|
214
|
+
<span className="min-w-0 flex-1 truncate px-2 py-1.5 text-sm text-muted-foreground">
|
|
215
|
+
{node.title}
|
|
216
|
+
</span>
|
|
217
|
+
)}
|
|
218
|
+
{canOpen ? (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={() => setOpenFolder(node.id)}
|
|
222
|
+
aria-label={i18n.t("picker.open", { title: node.title })}
|
|
223
|
+
className="inline-flex size-7 shrink-0 items-center justify-center rounded outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
224
|
+
>
|
|
225
|
+
<Folder aria-hidden="true" className="size-4" />
|
|
226
|
+
</button>
|
|
227
|
+
) : null}
|
|
228
|
+
</li>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
{shown.length === 0 ? (
|
|
232
|
+
<li className="px-3 py-2 text-sm text-muted-foreground">{i18n.t("picker.nothing")}</li>
|
|
233
|
+
) : null}
|
|
234
|
+
</ul>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|