makersclaw 0.8.0 → 0.8.1
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 +9 -3
- package/package.json +1 -1
- package/r/activity-log.json +52 -0
- package/r/agent-changes.json +51 -0
- package/r/agent-chat.json +53 -0
- package/r/agent-column.json +85 -0
- package/r/agent-dock.json +57 -0
- package/r/agent-kit.json +49 -0
- package/r/agent-marks.json +49 -0
- package/r/agent-pane.json +93 -0
- package/r/ai-action.json +56 -0
- package/r/annotate.json +3 -3
- package/r/approval-request.json +2 -2
- package/r/approval.json +54 -0
- package/r/ask-bar.json +50 -0
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/automation-editor.json +66 -0
- package/r/automation-list.json +57 -0
- package/r/automation-model.json +118 -0
- package/r/automations-screen.json +71 -0
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +2 -2
- package/r/chat-pane.json +84 -0
- package/r/chat.json +2 -2
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/connect-card.json +51 -0
- package/r/contact-section.json +2 -2
- package/r/core.json +13 -7
- package/r/cost.json +51 -0
- package/r/csv-import.json +3 -3
- package/r/data-picker.json +60 -0
- package/r/data-table.json +2 -2
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/first-run.json +63 -0
- package/r/fix-note.json +57 -0
- package/r/flow-canvas.json +2 -2
- package/r/flow-editor.json +79 -0
- package/r/footer.json +2 -2
- package/r/gantt.json +3 -3
- package/r/job-panel.json +2 -2
- package/r/json-editor.json +3 -3
- package/r/library-shelf.json +56 -0
- package/r/map.json +2 -2
- package/r/margin-notes.json +67 -0
- package/r/menubar.json +2 -2
- package/r/mind-map.json +3 -3
- package/r/navigation-menu.json +2 -2
- package/r/needs-you.json +55 -0
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +3 -3
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +2 -2
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +2 -2
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-grid.json +52 -0
- package/r/record-notes.json +4 -4
- package/r/registry.json +4086 -1512
- package/r/run-history.json +52 -0
- package/r/run-view.json +80 -0
- package/r/schedule-card.json +54 -0
- package/r/schedule-engine.json +79 -0
- package/r/schedule-gantt.json +83 -0
- package/r/schedule-ruler.json +64 -0
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +2 -2
- package/r/source-receipt.json +52 -0
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +3 -3
- package/r/step-picker.json +53 -0
- package/r/step-settings.json +72 -0
- package/r/suggestion.json +58 -0
- package/r/sum-bar.json +64 -0
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/tier-map.json +67 -0
- package/r/time-grid.json +73 -0
- package/r/time-strip.json +64 -0
- package/r/top-bar.json +66 -0
- package/r/transaction-list.json +2 -2
- package/r/trigger-picker.json +61 -0
- package/r/whiteboard.json +3 -3
- package/r/workflow-view.json +55 -0
- package/registry/blocks/activity-log/README.md +112 -0
- package/registry/blocks/activity-log/activity-log.jsx +351 -0
- package/registry/blocks/activity-log/block.json +33 -0
- package/registry/blocks/activity-log/demo.jsx +48 -0
- package/registry/blocks/activity-log/index.js +4 -0
- package/registry/blocks/agent-changes/README.md +94 -0
- package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
- package/registry/blocks/agent-changes/block.json +28 -0
- package/registry/blocks/agent-changes/demo.jsx +39 -0
- package/registry/blocks/agent-changes/index.js +3 -0
- package/registry/blocks/agent-chat/README.md +145 -0
- package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
- package/registry/blocks/agent-chat/block.json +30 -0
- package/registry/blocks/agent-chat/demo.jsx +165 -0
- package/registry/blocks/agent-chat/index.js +3 -0
- package/registry/blocks/agent-column/README.md +197 -0
- package/registry/blocks/agent-column/agent-column.jsx +492 -0
- package/registry/blocks/agent-column/block.json +67 -0
- package/registry/blocks/agent-column/column-picker.jsx +163 -0
- package/registry/blocks/agent-column/demo.jsx +50 -0
- package/registry/blocks/agent-column/index.js +7 -0
- package/registry/blocks/agent-column/model.js +164 -0
- package/registry/blocks/agent-column/receipt.jsx +483 -0
- package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
- package/registry/blocks/agent-dock/README.md +210 -0
- package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
- package/registry/blocks/agent-dock/block.json +47 -0
- package/registry/blocks/agent-dock/demo.jsx +153 -0
- package/registry/blocks/agent-dock/dock-model.js +118 -0
- package/registry/blocks/agent-dock/index.js +5 -0
- package/registry/blocks/agent-kit/README.md +73 -0
- package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
- package/registry/blocks/agent-kit/block.json +10 -0
- package/registry/blocks/agent-kit/demo.jsx +35 -0
- package/registry/blocks/agent-kit/index.js +3 -0
- package/registry/blocks/agent-marks/README.md +80 -0
- package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
- package/registry/blocks/agent-marks/block.json +27 -0
- package/registry/blocks/agent-marks/demo.jsx +49 -0
- package/registry/blocks/agent-marks/index.js +3 -0
- package/registry/blocks/agent-pane/README.md +225 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
- package/registry/blocks/agent-pane/block.json +41 -0
- package/registry/blocks/agent-pane/composer.jsx +6 -0
- package/registry/blocks/agent-pane/demo.jsx +233 -0
- package/registry/blocks/agent-pane/dock-context.js +10 -0
- package/registry/blocks/agent-pane/index.js +4 -0
- package/registry/blocks/agent-pane/prose.jsx +84 -0
- package/registry/blocks/agent-pane/questions.jsx +134 -0
- package/registry/blocks/agent-pane/transcript.jsx +268 -0
- package/registry/blocks/agent-pane/work.jsx +368 -0
- package/registry/blocks/ai-action/README.md +94 -0
- package/registry/blocks/ai-action/ai-action.jsx +188 -0
- package/registry/blocks/ai-action/block.json +34 -0
- package/registry/blocks/ai-action/demo.jsx +93 -0
- package/registry/blocks/ai-action/index.js +3 -0
- package/registry/blocks/approval/README.md +145 -0
- package/registry/blocks/approval/approval.jsx +163 -0
- package/registry/blocks/approval/block.json +31 -0
- package/registry/blocks/approval/demo.jsx +121 -0
- package/registry/blocks/approval/index.js +3 -0
- package/registry/blocks/ask-bar/README.md +105 -0
- package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
- package/registry/blocks/ask-bar/block.json +26 -0
- package/registry/blocks/ask-bar/demo.jsx +78 -0
- package/registry/blocks/ask-bar/index.js +3 -0
- package/registry/blocks/automation-editor/README.md +291 -0
- package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
- package/registry/blocks/automation-editor/block.json +39 -0
- package/registry/blocks/automation-editor/demo.jsx +284 -0
- package/registry/blocks/automation-editor/index.js +4 -0
- package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
- package/registry/blocks/automation-list/README.md +129 -0
- package/registry/blocks/automation-list/automation-list.jsx +476 -0
- package/registry/blocks/automation-list/block.json +35 -0
- package/registry/blocks/automation-list/demo.jsx +67 -0
- package/registry/blocks/automation-list/index.js +3 -0
- package/registry/blocks/automation-model/README.md +109 -0
- package/registry/blocks/automation-model/block.json +135 -0
- package/registry/blocks/automation-model/catalog.js +256 -0
- package/registry/blocks/automation-model/check.js +161 -0
- package/registry/blocks/automation-model/demo.jsx +42 -0
- package/registry/blocks/automation-model/edit.js +262 -0
- package/registry/blocks/automation-model/fixes.js +287 -0
- package/registry/blocks/automation-model/glyph.jsx +13 -0
- package/registry/blocks/automation-model/graph.js +410 -0
- package/registry/blocks/automation-model/index.js +33 -0
- package/registry/blocks/automation-model/overlay.js +175 -0
- package/registry/blocks/automation-model/parts.jsx +94 -0
- package/registry/blocks/automation-model/schedule.js +53 -0
- package/registry/blocks/automation-model/templates.js +128 -0
- package/registry/blocks/automation-model/values.js +29 -0
- package/registry/blocks/automation-model/words.js +439 -0
- package/registry/blocks/automations-screen/README.md +207 -0
- package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
- package/registry/blocks/automations-screen/block.json +47 -0
- package/registry/blocks/automations-screen/demo.jsx +364 -0
- package/registry/blocks/automations-screen/index.js +4 -0
- package/registry/blocks/automations-screen/screen-model.js +129 -0
- package/registry/blocks/chat-pane/README.md +143 -0
- package/registry/blocks/chat-pane/block.json +48 -0
- package/registry/blocks/chat-pane/chat-attach.js +166 -0
- package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
- package/registry/blocks/chat-pane/chat-context.js +17 -0
- package/registry/blocks/chat-pane/chat-model.js +43 -0
- package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
- package/registry/blocks/chat-pane/chats-list.jsx +52 -0
- package/registry/blocks/chat-pane/demo.jsx +103 -0
- package/registry/blocks/chat-pane/index.js +9 -0
- package/registry/blocks/connect-card/README.md +99 -0
- package/registry/blocks/connect-card/block.json +10 -0
- package/registry/blocks/connect-card/connect-card.jsx +103 -0
- package/registry/blocks/connect-card/demo.jsx +49 -0
- package/registry/blocks/connect-card/index.js +3 -0
- package/registry/blocks/cost/README.md +115 -0
- package/registry/blocks/cost/block.json +10 -0
- package/registry/blocks/cost/cost.jsx +176 -0
- package/registry/blocks/cost/demo.jsx +66 -0
- package/registry/blocks/cost/index.js +3 -0
- package/registry/blocks/data-picker/README.md +81 -0
- package/registry/blocks/data-picker/block.json +38 -0
- package/registry/blocks/data-picker/data-picker.jsx +210 -0
- package/registry/blocks/data-picker/demo.jsx +34 -0
- package/registry/blocks/data-picker/fields.js +89 -0
- package/registry/blocks/data-picker/index.js +4 -0
- package/registry/blocks/data-table/demo.jsx +1 -1
- package/registry/blocks/first-run/README.md +118 -0
- package/registry/blocks/first-run/block.json +11 -0
- package/registry/blocks/first-run/demo.jsx +88 -0
- package/registry/blocks/first-run/first-run-model.js +51 -0
- package/registry/blocks/first-run/first-run.jsx +156 -0
- package/registry/blocks/first-run/index.js +5 -0
- package/registry/blocks/first-run/probe-rules.js +26 -0
- package/registry/blocks/fix-note/README.md +79 -0
- package/registry/blocks/fix-note/block.json +35 -0
- package/registry/blocks/fix-note/demo.jsx +30 -0
- package/registry/blocks/fix-note/fix-note.jsx +235 -0
- package/registry/blocks/fix-note/index.js +3 -0
- package/registry/blocks/flow-editor/README.md +108 -0
- package/registry/blocks/flow-editor/block.json +44 -0
- package/registry/blocks/flow-editor/demo.jsx +115 -0
- package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
- package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
- package/registry/blocks/flow-editor/index.js +6 -0
- package/registry/blocks/flow-editor/minimap.jsx +78 -0
- package/registry/blocks/flow-editor/outline.jsx +134 -0
- package/registry/blocks/library-shelf/README.md +117 -0
- package/registry/blocks/library-shelf/block.json +28 -0
- package/registry/blocks/library-shelf/demo.jsx +68 -0
- package/registry/blocks/library-shelf/index.js +3 -0
- package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
- package/registry/blocks/library-shelf/probe-rules.js +15 -0
- package/registry/blocks/margin-notes/README.md +222 -0
- package/registry/blocks/margin-notes/block.json +10 -0
- package/registry/blocks/margin-notes/demo.jsx +144 -0
- package/registry/blocks/margin-notes/index.js +20 -0
- package/registry/blocks/margin-notes/margin-model.js +224 -0
- package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
- package/registry/blocks/needs-you/README.md +177 -0
- package/registry/blocks/needs-you/block.json +34 -0
- package/registry/blocks/needs-you/demo.jsx +143 -0
- package/registry/blocks/needs-you/index.js +3 -0
- package/registry/blocks/needs-you/needs-you.jsx +441 -0
- package/registry/blocks/pace-bar/README.md +230 -0
- package/registry/blocks/pace-bar/block.json +10 -0
- package/registry/blocks/pace-bar/demo.jsx +255 -0
- package/registry/blocks/pace-bar/index.js +22 -0
- package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
- package/registry/blocks/pace-bar/pace-model.js +288 -0
- package/registry/blocks/record-grid/README.md +100 -0
- package/registry/blocks/record-grid/block.json +28 -0
- package/registry/blocks/record-grid/demo.jsx +43 -0
- package/registry/blocks/record-grid/index.js +3 -0
- package/registry/blocks/record-grid/record-grid.jsx +251 -0
- package/registry/blocks/run-history/README.md +138 -0
- package/registry/blocks/run-history/block.json +10 -0
- package/registry/blocks/run-history/demo.jsx +74 -0
- package/registry/blocks/run-history/index.js +3 -0
- package/registry/blocks/run-history/run-history.jsx +218 -0
- package/registry/blocks/run-view/README.md +174 -0
- package/registry/blocks/run-view/block.json +50 -0
- package/registry/blocks/run-view/demo.jsx +286 -0
- package/registry/blocks/run-view/index.js +5 -0
- package/registry/blocks/run-view/run-controls.jsx +266 -0
- package/registry/blocks/run-view/run-view.jsx +236 -0
- package/registry/blocks/run-view/runs-list.jsx +99 -0
- package/registry/blocks/schedule-card/README.md +121 -0
- package/registry/blocks/schedule-card/block.json +10 -0
- package/registry/blocks/schedule-card/demo.jsx +82 -0
- package/registry/blocks/schedule-card/index.js +3 -0
- package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
- package/registry/blocks/schedule-engine/README.md +137 -0
- package/registry/blocks/schedule-engine/block.json +10 -0
- package/registry/blocks/schedule-engine/demo.jsx +82 -0
- package/registry/blocks/schedule-engine/engine.js +769 -0
- package/registry/blocks/schedule-engine/index.js +9 -0
- package/registry/blocks/schedule-engine/ops.js +71 -0
- package/registry/blocks/schedule-engine/place.js +294 -0
- package/registry/blocks/schedule-engine/tree.js +79 -0
- package/registry/blocks/schedule-engine/waits.js +80 -0
- package/registry/blocks/schedule-gantt/README.md +159 -0
- package/registry/blocks/schedule-gantt/block.json +10 -0
- package/registry/blocks/schedule-gantt/demo.jsx +223 -0
- package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
- package/registry/blocks/schedule-gantt/geometry.js +176 -0
- package/registry/blocks/schedule-gantt/index.js +26 -0
- package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
- package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
- package/registry/blocks/schedule-gantt/track.jsx +190 -0
- package/registry/blocks/schedule-ruler/README.md +148 -0
- package/registry/blocks/schedule-ruler/block.json +10 -0
- package/registry/blocks/schedule-ruler/demo.jsx +181 -0
- package/registry/blocks/schedule-ruler/index.js +21 -0
- package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
- package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
- package/registry/blocks/source-receipt/README.md +97 -0
- package/registry/blocks/source-receipt/block.json +30 -0
- package/registry/blocks/source-receipt/demo.jsx +71 -0
- package/registry/blocks/source-receipt/index.js +3 -0
- package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
- package/registry/blocks/step-picker/README.md +78 -0
- package/registry/blocks/step-picker/block.json +31 -0
- package/registry/blocks/step-picker/demo.jsx +23 -0
- package/registry/blocks/step-picker/index.js +3 -0
- package/registry/blocks/step-picker/step-picker.jsx +144 -0
- package/registry/blocks/step-settings/README.md +100 -0
- package/registry/blocks/step-settings/block.json +44 -0
- package/registry/blocks/step-settings/demo.jsx +93 -0
- package/registry/blocks/step-settings/fields.jsx +58 -0
- package/registry/blocks/step-settings/flow-settings.jsx +115 -0
- package/registry/blocks/step-settings/index.js +5 -0
- package/registry/blocks/step-settings/step-settings.jsx +290 -0
- package/registry/blocks/suggestion/README.md +171 -0
- package/registry/blocks/suggestion/block.json +40 -0
- package/registry/blocks/suggestion/demo.jsx +91 -0
- package/registry/blocks/suggestion/index.js +3 -0
- package/registry/blocks/suggestion/suggestion.jsx +436 -0
- package/registry/blocks/sum-bar/README.md +122 -0
- package/registry/blocks/sum-bar/block.json +10 -0
- package/registry/blocks/sum-bar/demo.jsx +110 -0
- package/registry/blocks/sum-bar/index.js +5 -0
- package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
- package/registry/blocks/sum-bar/sum-model.js +126 -0
- package/registry/blocks/tier-map/README.md +217 -0
- package/registry/blocks/tier-map/block.json +10 -0
- package/registry/blocks/tier-map/demo.jsx +222 -0
- package/registry/blocks/tier-map/index.js +24 -0
- package/registry/blocks/tier-map/tier-map.jsx +574 -0
- package/registry/blocks/tier-map/tier-model.js +318 -0
- package/registry/blocks/time-grid/README.md +305 -0
- package/registry/blocks/time-grid/block.json +10 -0
- package/registry/blocks/time-grid/demo.jsx +279 -0
- package/registry/blocks/time-grid/grid-model.js +322 -0
- package/registry/blocks/time-grid/index.js +25 -0
- package/registry/blocks/time-grid/time-grid.jsx +1325 -0
- package/registry/blocks/time-strip/README.md +141 -0
- package/registry/blocks/time-strip/block.json +10 -0
- package/registry/blocks/time-strip/demo.jsx +129 -0
- package/registry/blocks/time-strip/index.js +19 -0
- package/registry/blocks/time-strip/strip-model.js +178 -0
- package/registry/blocks/time-strip/time-strip.jsx +260 -0
- package/registry/blocks/top-bar/README.md +149 -0
- package/registry/blocks/top-bar/block.json +35 -0
- package/registry/blocks/top-bar/demo.jsx +78 -0
- package/registry/blocks/top-bar/index.js +5 -0
- package/registry/blocks/top-bar/probe-rules.js +74 -0
- package/registry/blocks/top-bar/top-bar-model.js +152 -0
- package/registry/blocks/top-bar/top-bar.jsx +244 -0
- package/registry/blocks/trigger-picker/README.md +98 -0
- package/registry/blocks/trigger-picker/block.json +38 -0
- package/registry/blocks/trigger-picker/demo.jsx +59 -0
- package/registry/blocks/trigger-picker/index.js +3 -0
- package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
- package/registry/blocks/workflow-view/README.md +123 -0
- package/registry/blocks/workflow-view/block.json +10 -0
- package/registry/blocks/workflow-view/demo.jsx +90 -0
- package/registry/blocks/workflow-view/index.js +3 -0
- package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
- package/src/install.mjs +77 -16
- package/src/registry.mjs +3 -0
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +627 -24
- package/templates/next/files/check/automation-format.ts +2103 -0
- package/templates/next/files/check/automation-package.ts +142 -0
- package/templates/next/files/check/run.mjs +968 -9
- package/templates/next/files/components/ui/field.jsx +17 -4
- package/templates/next/files/components/ui/icon.jsx +13 -0
- package/templates/next/files/components/ui/index.js +2 -1
- package/templates/next/files/components/ui/layout.jsx +77 -2
- package/templates/next/files/components/ui/notice.jsx +70 -0
- package/templates/next/files/components/ui/page.jsx +124 -0
- package/templates/next/files/components/ui/stat.jsx +22 -7
- package/templates/next/files/components/ui/toast.jsx +6 -3
- package/templates/next/files/lib/automations.js +660 -0
- package/templates/next/files/lib/claw.js +241 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1627 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Button, Card, Empty, Icon, Notice, Skeleton, cx } from "../../ui";
|
|
5
|
+
import { barOf, buildTierModel, countText, elbowOf, fitKey, neighbour, openPath, tiersOf, togglePath, toneOf } from "./tier-model";
|
|
6
|
+
|
|
7
|
+
// A map of cards in tiers, from a flat list where each item names its parent.
|
|
8
|
+
// The first tier is the tops; pressing a card opens its kids as the next tier
|
|
9
|
+
// beneath it (pressing it again closes them), so ONE LAYER OPENS AT A TIME.
|
|
10
|
+
// Items with no place in the tree sit together in an "unparented" group;
|
|
11
|
+
// drafts (`ghost`) sit in a group of their own, dashed, and can serve drafts.
|
|
12
|
+
//
|
|
13
|
+
// Props in, callbacks out (`onSelect`, `onOpenChange`): the block never reads
|
|
14
|
+
// or writes data. The elbow lines are CSS only: every tier is a grid of equal
|
|
15
|
+
// columns and the lines are placed in percent of its width, so nothing is
|
|
16
|
+
// measured, there is no observer, and the server draws what the browser
|
|
17
|
+
// draws. Below the width a tier needs to sit in one row, it stacks and the
|
|
18
|
+
// lines become a rail down its left. README.md beside this file lists every
|
|
19
|
+
// prop.
|
|
20
|
+
|
|
21
|
+
// ---- layout classes (literal, so Tailwind sees every one)
|
|
22
|
+
//
|
|
23
|
+
// Each table is keyed by a tier's fit key (tier-model.js `fitKey`): the most
|
|
24
|
+
// equal columns this tier, or any tier above it, needs. The width a key asks
|
|
25
|
+
// for is `thresholdRem` (11 rem a column); test/tier-map.test.mjs checks the
|
|
26
|
+
// numbers below against it. Key 9 is a tier too wide to ever sit in a row.
|
|
27
|
+
|
|
28
|
+
// A tier's grid: from its width, one row of equal columns (--cols); below it, a stack.
|
|
29
|
+
const GRID = {
|
|
30
|
+
1: "@min-[0rem]:grid-cols-(--cols) @min-[0rem]:gap-y-0",
|
|
31
|
+
2: "@min-[22rem]:grid-cols-(--cols) @min-[22rem]:gap-y-0",
|
|
32
|
+
3: "@min-[33rem]:grid-cols-(--cols) @min-[33rem]:gap-y-0",
|
|
33
|
+
4: "@min-[44rem]:grid-cols-(--cols) @min-[44rem]:gap-y-0",
|
|
34
|
+
5: "@min-[55rem]:grid-cols-(--cols) @min-[55rem]:gap-y-0",
|
|
35
|
+
6: "@min-[66rem]:grid-cols-(--cols) @min-[66rem]:gap-y-0",
|
|
36
|
+
7: "@min-[77rem]:grid-cols-(--cols) @min-[77rem]:gap-y-0",
|
|
37
|
+
8: "@min-[88rem]:grid-cols-(--cols) @min-[88rem]:gap-y-0",
|
|
38
|
+
9: "",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// A card's cell stops being a box from the tier's width, so its card and its kids are the tier's own grid items.
|
|
42
|
+
const CELL = {
|
|
43
|
+
1: "@min-[0rem]:contents",
|
|
44
|
+
2: "@min-[22rem]:contents",
|
|
45
|
+
3: "@min-[33rem]:contents",
|
|
46
|
+
4: "@min-[44rem]:contents",
|
|
47
|
+
5: "@min-[55rem]:contents",
|
|
48
|
+
6: "@min-[66rem]:contents",
|
|
49
|
+
7: "@min-[77rem]:contents",
|
|
50
|
+
8: "@min-[88rem]:contents",
|
|
51
|
+
9: "",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// The card itself, from the tier's width: its own column (--c) in row 1, centred in it.
|
|
55
|
+
const SLOT = {
|
|
56
|
+
1: "@min-[0rem]:col-start-(--c) @min-[0rem]:row-start-1 @min-[0rem]:justify-items-center @min-[0rem]:px-1.5",
|
|
57
|
+
2: "@min-[22rem]:col-start-(--c) @min-[22rem]:row-start-1 @min-[22rem]:justify-items-center @min-[22rem]:px-1.5",
|
|
58
|
+
3: "@min-[33rem]:col-start-(--c) @min-[33rem]:row-start-1 @min-[33rem]:justify-items-center @min-[33rem]:px-1.5",
|
|
59
|
+
4: "@min-[44rem]:col-start-(--c) @min-[44rem]:row-start-1 @min-[44rem]:justify-items-center @min-[44rem]:px-1.5",
|
|
60
|
+
5: "@min-[55rem]:col-start-(--c) @min-[55rem]:row-start-1 @min-[55rem]:justify-items-center @min-[55rem]:px-1.5",
|
|
61
|
+
6: "@min-[66rem]:col-start-(--c) @min-[66rem]:row-start-1 @min-[66rem]:justify-items-center @min-[66rem]:px-1.5",
|
|
62
|
+
7: "@min-[77rem]:col-start-(--c) @min-[77rem]:row-start-1 @min-[77rem]:justify-items-center @min-[77rem]:px-1.5",
|
|
63
|
+
8: "@min-[88rem]:col-start-(--c) @min-[88rem]:row-start-1 @min-[88rem]:justify-items-center @min-[88rem]:px-1.5",
|
|
64
|
+
9: "",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// The block under an open card: from the parent tier's width, a whole row of its own (row 2).
|
|
68
|
+
const KIDS = {
|
|
69
|
+
1: "@min-[0rem]:row-start-2 @min-[0rem]:mt-0",
|
|
70
|
+
2: "@min-[22rem]:row-start-2 @min-[22rem]:mt-0",
|
|
71
|
+
3: "@min-[33rem]:row-start-2 @min-[33rem]:mt-0",
|
|
72
|
+
4: "@min-[44rem]:row-start-2 @min-[44rem]:mt-0",
|
|
73
|
+
5: "@min-[55rem]:row-start-2 @min-[55rem]:mt-0",
|
|
74
|
+
6: "@min-[66rem]:row-start-2 @min-[66rem]:mt-0",
|
|
75
|
+
7: "@min-[77rem]:row-start-2 @min-[77rem]:mt-0",
|
|
76
|
+
8: "@min-[88rem]:row-start-2 @min-[88rem]:mt-0",
|
|
77
|
+
9: "",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ...flush left once its own tier sits in a row; before that it hangs indented off the rail.
|
|
81
|
+
const INDENT = {
|
|
82
|
+
1: "@min-[0rem]:ml-0",
|
|
83
|
+
2: "@min-[22rem]:ml-0",
|
|
84
|
+
3: "@min-[33rem]:ml-0",
|
|
85
|
+
4: "@min-[44rem]:ml-0",
|
|
86
|
+
5: "@min-[55rem]:ml-0",
|
|
87
|
+
6: "@min-[66rem]:ml-0",
|
|
88
|
+
7: "@min-[77rem]:ml-0",
|
|
89
|
+
8: "@min-[88rem]:ml-0",
|
|
90
|
+
9: "",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// The strip of lines above the kids: only from the parent tier's width.
|
|
94
|
+
const HEAD = {
|
|
95
|
+
1: "hidden @min-[0rem]:block",
|
|
96
|
+
2: "hidden @min-[22rem]:block",
|
|
97
|
+
3: "hidden @min-[33rem]:block",
|
|
98
|
+
4: "hidden @min-[44rem]:block",
|
|
99
|
+
5: "hidden @min-[55rem]:block",
|
|
100
|
+
6: "hidden @min-[66rem]:block",
|
|
101
|
+
7: "hidden @min-[77rem]:block",
|
|
102
|
+
8: "hidden @min-[88rem]:block",
|
|
103
|
+
9: "hidden",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// The bar across the kids' tops: only from the kids' tier width.
|
|
107
|
+
const BAR = {
|
|
108
|
+
1: "hidden @min-[0rem]:block",
|
|
109
|
+
2: "hidden @min-[22rem]:block",
|
|
110
|
+
3: "hidden @min-[33rem]:block",
|
|
111
|
+
4: "hidden @min-[44rem]:block",
|
|
112
|
+
5: "hidden @min-[55rem]:block",
|
|
113
|
+
6: "hidden @min-[66rem]:block",
|
|
114
|
+
7: "hidden @min-[77rem]:block",
|
|
115
|
+
8: "hidden @min-[88rem]:block",
|
|
116
|
+
9: "hidden",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// The line from a parent that sits in a row to the rail of kids that stack.
|
|
120
|
+
const LOW = {
|
|
121
|
+
1: "hidden @max-[0rem]:block",
|
|
122
|
+
2: "hidden @max-[22rem]:block",
|
|
123
|
+
3: "hidden @max-[33rem]:block",
|
|
124
|
+
4: "hidden @max-[44rem]:block",
|
|
125
|
+
5: "hidden @max-[55rem]:block",
|
|
126
|
+
6: "hidden @max-[66rem]:block",
|
|
127
|
+
7: "hidden @max-[77rem]:block",
|
|
128
|
+
8: "hidden @max-[88rem]:block",
|
|
129
|
+
9: "block",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// A kid's elbow to the rail, only while its own tier is stacked.
|
|
133
|
+
const STUB = {
|
|
134
|
+
1: "@min-[0rem]:before:hidden @min-[0rem]:after:hidden",
|
|
135
|
+
2: "@min-[22rem]:before:hidden @min-[22rem]:after:hidden",
|
|
136
|
+
3: "@min-[33rem]:before:hidden @min-[33rem]:after:hidden",
|
|
137
|
+
4: "@min-[44rem]:before:hidden @min-[44rem]:after:hidden",
|
|
138
|
+
5: "@min-[55rem]:before:hidden @min-[55rem]:after:hidden",
|
|
139
|
+
6: "@min-[66rem]:before:hidden @min-[66rem]:after:hidden",
|
|
140
|
+
7: "@min-[77rem]:before:hidden @min-[77rem]:after:hidden",
|
|
141
|
+
8: "@min-[88rem]:before:hidden @min-[88rem]:after:hidden",
|
|
142
|
+
9: "",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// The elbow to the rail while a tier is stacked (the kids of a stacked tier
|
|
146
|
+
// hang off a line down their left). The last kid's line ends at its elbow.
|
|
147
|
+
const RAIL_CELL =
|
|
148
|
+
"relative before:absolute before:-left-3 before:top-6 before:w-3 before:border-t before:border-input after:absolute after:-left-3 after:bottom-0 after:border-l after:border-input";
|
|
149
|
+
// A kid's line starts 0.75 rem above it, at the bottom of the card (or the
|
|
150
|
+
// kid) above; the last kid's line stops at its elbow.
|
|
151
|
+
const RAIL_TOP = "after:-top-3";
|
|
152
|
+
const RAIL_END = "after:bottom-auto after:h-9";
|
|
153
|
+
const railOf = (i, count) => cx(RAIL_CELL, RAIL_TOP, i === count - 1 && RAIL_END);
|
|
154
|
+
|
|
155
|
+
const FILL = {
|
|
156
|
+
ink: "bg-foreground",
|
|
157
|
+
quiet: "bg-muted-foreground",
|
|
158
|
+
info: "bg-info",
|
|
159
|
+
success: "bg-success",
|
|
160
|
+
warning: "bg-warning",
|
|
161
|
+
danger: "bg-destructive",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const GROUP_HEAD = "text-2xs font-medium uppercase tracking-wider text-muted-foreground";
|
|
165
|
+
const CARD_BOX = "w-full min-w-0 max-w-[26rem]";
|
|
166
|
+
|
|
167
|
+
// ---- one context for the tiers
|
|
168
|
+
|
|
169
|
+
const Ctx = createContext(null);
|
|
170
|
+
const useTiers = () => useContext(Ctx);
|
|
171
|
+
|
|
172
|
+
const sameId = (a, b) => a != null && b != null && String(a) === String(b);
|
|
173
|
+
|
|
174
|
+
// ---- the card
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* One card. `state` is what the map knows about it:
|
|
178
|
+
* { id, item, open, selected, count, tier, ghost, serves, pressable,
|
|
179
|
+
* holderProps, primaryProps, toggleProps, select, toggle }
|
|
180
|
+
* A `renderCard` of your own spreads `holderProps` on its root and
|
|
181
|
+
* `primaryProps` (and `toggleProps`) on the buttons that take the focus.
|
|
182
|
+
*/
|
|
183
|
+
function TierCard({ node, serves }) {
|
|
184
|
+
const t = useTiers();
|
|
185
|
+
const { item } = node;
|
|
186
|
+
const open = t.openIds.has(node.id);
|
|
187
|
+
const kids = node.kids.length;
|
|
188
|
+
const selected = sameId(t.selected, node.id);
|
|
189
|
+
const ghost = node.ghost;
|
|
190
|
+
const hasOpen = node.group === "tree" && kids > 0;
|
|
191
|
+
const pressable = Boolean(t.onSelect) || Boolean(item.href) || hasOpen;
|
|
192
|
+
const roving = t.tabId === node.id ? 0 : -1;
|
|
193
|
+
const kidsId = `${t.uid}-kids-${node.id}`;
|
|
194
|
+
|
|
195
|
+
const toggle = () => t.toggle(node.id);
|
|
196
|
+
const select = () => {
|
|
197
|
+
if (t.onSelect) t.onSelect(item);
|
|
198
|
+
else if (hasOpen) toggle();
|
|
199
|
+
};
|
|
200
|
+
const state = {
|
|
201
|
+
id: node.id,
|
|
202
|
+
item,
|
|
203
|
+
open,
|
|
204
|
+
selected,
|
|
205
|
+
count: kids,
|
|
206
|
+
tier: node.tier,
|
|
207
|
+
ghost,
|
|
208
|
+
serves,
|
|
209
|
+
pressable,
|
|
210
|
+
select,
|
|
211
|
+
toggle: hasOpen ? toggle : undefined,
|
|
212
|
+
holderProps: { "data-tier-id": node.id },
|
|
213
|
+
primaryProps: {
|
|
214
|
+
"data-tier-card": node.id,
|
|
215
|
+
tabIndex: roving,
|
|
216
|
+
onClick: select,
|
|
217
|
+
onFocus: () => t.setTab(node.id),
|
|
218
|
+
"aria-current": selected ? "true" : undefined,
|
|
219
|
+
"aria-expanded": hasOpen && !t.onSelect ? open : undefined,
|
|
220
|
+
"aria-controls": hasOpen && !t.onSelect && open ? kidsId : undefined,
|
|
221
|
+
},
|
|
222
|
+
toggleProps: hasOpen
|
|
223
|
+
? { "data-tier-toggle": node.id, tabIndex: roving, onClick: toggle, onFocus: () => t.setTab(node.id), "aria-expanded": open, "aria-controls": open ? kidsId : undefined }
|
|
224
|
+
: null,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const custom = t.renderCard ? t.renderCard(item, state) : null;
|
|
228
|
+
if (custom != null && custom !== false) return custom;
|
|
229
|
+
|
|
230
|
+
const bar = barOf(item);
|
|
231
|
+
const tone = toneOf(item.tone, bar?.share);
|
|
232
|
+
const lead = item.mark != null || item.kicker != null || item.by != null;
|
|
233
|
+
const body = (
|
|
234
|
+
<>
|
|
235
|
+
{lead ? (
|
|
236
|
+
<span className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
|
|
237
|
+
{item.mark != null ? <span className="inline-flex shrink-0 items-center">{item.mark}</span> : null}
|
|
238
|
+
{item.kicker != null ? <span className="min-w-0 truncate">{item.kicker}</span> : null}
|
|
239
|
+
{item.by != null ? <span className="ml-auto inline-flex shrink-0 items-center">{item.by}</span> : null}
|
|
240
|
+
</span>
|
|
241
|
+
) : null}
|
|
242
|
+
<span className="block font-semibold leading-snug [overflow-wrap:anywhere]">{item.title}</span>
|
|
243
|
+
{bar ? (
|
|
244
|
+
<span className="flex items-center gap-2 text-xs">
|
|
245
|
+
<span aria-hidden="true" className="relative h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-muted">
|
|
246
|
+
<span className={cx("absolute inset-y-0 left-0 w-(--fill) rounded-full", FILL[tone])} style={{ "--fill": `${bar.pct}%` }} />
|
|
247
|
+
</span>
|
|
248
|
+
<span data-nums className="shrink-0 font-semibold">
|
|
249
|
+
{bar.pct}%
|
|
250
|
+
</span>
|
|
251
|
+
</span>
|
|
252
|
+
) : null}
|
|
253
|
+
{serves ? <span className="block text-xs text-muted-foreground [overflow-wrap:anywhere]">{serves}</span> : null}
|
|
254
|
+
{item.foot != null ? <span className="block text-xs text-muted-foreground [overflow-wrap:anywhere]">{item.foot}</span> : null}
|
|
255
|
+
</>
|
|
256
|
+
);
|
|
257
|
+
const look = cx(
|
|
258
|
+
"grid w-full min-w-0 gap-1.5 p-3 text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
259
|
+
pressable ? "hover:bg-accent" : "cursor-default",
|
|
260
|
+
);
|
|
261
|
+
const primary = item.href ? (
|
|
262
|
+
<a href={item.href} {...state.primaryProps} className={look}>
|
|
263
|
+
{body}
|
|
264
|
+
</a>
|
|
265
|
+
) : (
|
|
266
|
+
<button type="button" {...state.primaryProps} aria-disabled={pressable ? undefined : "true"} className={look}>
|
|
267
|
+
{body}
|
|
268
|
+
</button>
|
|
269
|
+
);
|
|
270
|
+
const ring = selected ? "ring-2 ring-ring" : open ? "ring-1 ring-primary" : "";
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div {...state.holderProps} className={CARD_BOX}>
|
|
274
|
+
<Card as="div" padding="none" className={cx(ring, ghost && "border-dashed")}>
|
|
275
|
+
{primary}
|
|
276
|
+
{state.toggleProps ? (
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
{...state.toggleProps}
|
|
280
|
+
className="flex w-full min-w-0 items-center gap-1.5 border-t border-border px-3 py-1.5 text-left text-xs text-muted-foreground outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
281
|
+
>
|
|
282
|
+
<Icon name="chevron-right" size={14} className={cx("shrink-0 transition-transform motion-reduce:transition-none", open && "rotate-90")} />
|
|
283
|
+
<span className="min-w-0">
|
|
284
|
+
{countText(kids, t.childrenLabel)}
|
|
285
|
+
{open ? ` · ${t.openWord}` : ""}
|
|
286
|
+
</span>
|
|
287
|
+
</button>
|
|
288
|
+
) : null}
|
|
289
|
+
</Card>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export { TierCard };
|
|
295
|
+
|
|
296
|
+
// ---- the tiers
|
|
297
|
+
|
|
298
|
+
function Tier({ ids, depth, above, hangs }) {
|
|
299
|
+
const t = useTiers();
|
|
300
|
+
const count = ids.length;
|
|
301
|
+
const n = fitKey(Math.max(count, above));
|
|
302
|
+
const openId = t.path[depth] ?? null;
|
|
303
|
+
return (
|
|
304
|
+
<div className={cx("grid grid-cols-1 gap-y-3", GRID[n])} style={{ "--cols": `repeat(${count}, minmax(0, 1fr))` }}>
|
|
305
|
+
{ids.map((id, i) => {
|
|
306
|
+
const node = t.model.nodes.get(id);
|
|
307
|
+
return (
|
|
308
|
+
<div
|
|
309
|
+
key={id}
|
|
310
|
+
className={cx("min-w-0", hangs ? cx(railOf(i, count), STUB[n]) : "relative", CELL[n])}
|
|
311
|
+
style={{ "--c": i + 1 }}
|
|
312
|
+
>
|
|
313
|
+
<div className={cx("grid min-w-0 justify-items-start", SLOT[n])}>
|
|
314
|
+
<TierCard node={node} />
|
|
315
|
+
</div>
|
|
316
|
+
{id === openId ? <Kids node={node} depth={depth} fit={n} index={i} count={count} /> : null}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
})}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// What hangs under an open card: the strip of lines (from the width its tier
|
|
325
|
+
// sits in a row) and the next tier.
|
|
326
|
+
function Kids({ node, depth, fit, index, count }) {
|
|
327
|
+
const t = useTiers();
|
|
328
|
+
const kidFit = fitKey(Math.max(node.kids.length, fit));
|
|
329
|
+
const e = elbowOf(index, count, node.kids.length);
|
|
330
|
+
return (
|
|
331
|
+
<div id={`${t.uid}-kids-${node.id}`} className={cx("col-span-full mt-3 min-w-0", KIDS[fit])}>
|
|
332
|
+
<div aria-hidden="true" className={cx("relative h-8", HEAD[fit])} style={{ "--p": `${e.parent}%` }}>
|
|
333
|
+
<span className="absolute left-(--p) top-0 h-1/2 border-l border-input" />
|
|
334
|
+
<div className={cx("absolute inset-0", LOW[kidFit])}>
|
|
335
|
+
<span className="absolute left-3 top-1/2 w-(--back) border-t border-input" style={{ "--back": `calc(${e.parent}% - 0.75rem)` }} />
|
|
336
|
+
<span className="absolute left-3 top-1/2 bottom-0 border-l border-input" />
|
|
337
|
+
</div>
|
|
338
|
+
<div className={cx("absolute inset-0", BAR[kidFit])}>
|
|
339
|
+
<span className="absolute left-(--from) top-1/2 w-(--across) border-t border-input" style={{ "--from": `${e.from}%`, "--across": `${e.to - e.from}%` }} />
|
|
340
|
+
{e.kids.map((at, i) => (
|
|
341
|
+
<span key={i} className="absolute left-(--at) top-1/2 bottom-0 border-l border-input" style={{ "--at": `${at}%` }} />
|
|
342
|
+
))}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
<div className={cx("ml-6 min-w-0", INDENT[kidFit])}>
|
|
346
|
+
<Tier ids={node.kids} depth={depth + 1} above={fit} hangs />
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---- the drafts: a forest that is always open, a rail down each tree
|
|
353
|
+
|
|
354
|
+
function GhostBranch({ id }) {
|
|
355
|
+
const t = useTiers();
|
|
356
|
+
const node = t.model.nodes.get(id);
|
|
357
|
+
const parent = node.parent != null ? t.model.nodes.get(node.parent) : null;
|
|
358
|
+
const serves = node.up == null && parent && t.servesLabel ? `${t.servesLabel} ${parent.item.title}` : null;
|
|
359
|
+
return (
|
|
360
|
+
<div className="grid min-w-0 content-start gap-3">
|
|
361
|
+
<TierCard node={node} serves={serves} />
|
|
362
|
+
{node.kids.length ? (
|
|
363
|
+
<div className="ml-6 grid min-w-0 gap-3">
|
|
364
|
+
{node.kids.map((kid, i) => (
|
|
365
|
+
<div key={kid} className={cx("min-w-0", railOf(i, node.kids.length))}>
|
|
366
|
+
<GhostBranch id={kid} />
|
|
367
|
+
</div>
|
|
368
|
+
))}
|
|
369
|
+
</div>
|
|
370
|
+
) : null}
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---- the whole map
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The map. Props: `items`, `tops`, `topsLabel`, `label`, `childrenLabel`,
|
|
379
|
+
* `openWord`, `unparentedLabel`, `unparentedNote`, `ghostLabel`, `ghostNote`,
|
|
380
|
+
* `servesLabel`, `open`, `defaultOpen`, `onOpenChange`, `onSelect`,
|
|
381
|
+
* `selected`, `renderCard`, `loading`, `error`, `onRetry`, `empty`,
|
|
382
|
+
* `className`.
|
|
383
|
+
*/
|
|
384
|
+
export function TierMap({
|
|
385
|
+
items,
|
|
386
|
+
tops,
|
|
387
|
+
topsLabel,
|
|
388
|
+
label = "Tiers",
|
|
389
|
+
childrenLabel = "below",
|
|
390
|
+
openWord = "open",
|
|
391
|
+
unparentedLabel = "Not under anything",
|
|
392
|
+
unparentedNote,
|
|
393
|
+
ghostLabel = "Drafts",
|
|
394
|
+
ghostNote,
|
|
395
|
+
servesLabel = "Serves",
|
|
396
|
+
open,
|
|
397
|
+
defaultOpen,
|
|
398
|
+
onOpenChange,
|
|
399
|
+
onSelect,
|
|
400
|
+
selected,
|
|
401
|
+
renderCard,
|
|
402
|
+
loading = false,
|
|
403
|
+
error,
|
|
404
|
+
onRetry,
|
|
405
|
+
empty,
|
|
406
|
+
className,
|
|
407
|
+
}) {
|
|
408
|
+
const uid = useId().replace(/:/g, "");
|
|
409
|
+
const root = useRef(null);
|
|
410
|
+
const model = useMemo(() => buildTierModel(items, { tops }), [items, tops]);
|
|
411
|
+
const [inner, setInner] = useState(() => (Array.isArray(defaultOpen) ? defaultOpen : []));
|
|
412
|
+
const [tab, setTab] = useState(null);
|
|
413
|
+
const [focusReq, setFocusReq] = useState(null);
|
|
414
|
+
|
|
415
|
+
const path = useMemo(() => openPath(model, open !== undefined ? open : inner), [model, open, inner]);
|
|
416
|
+
const openIds = useMemo(() => new Set(path), [path]);
|
|
417
|
+
|
|
418
|
+
// The cards on screen, in the order the keys walk them.
|
|
419
|
+
const shown = useMemo(() => {
|
|
420
|
+
const ids = [];
|
|
421
|
+
for (const tier of tiersOf(model, path)) ids.push(...tier.ids);
|
|
422
|
+
ids.push(...model.unparented, ...model.ghostOrder.map((g) => g.id));
|
|
423
|
+
return ids;
|
|
424
|
+
}, [model, path]);
|
|
425
|
+
const tabId = tab != null && shown.includes(tab) ? tab : shown[0] ?? null;
|
|
426
|
+
|
|
427
|
+
function commit(next) {
|
|
428
|
+
if (open === undefined) setInner(next);
|
|
429
|
+
if (onOpenChange) onOpenChange(next);
|
|
430
|
+
}
|
|
431
|
+
function toggle(id) {
|
|
432
|
+
const next = togglePath(model, path, id);
|
|
433
|
+
if (next !== path) commit(next);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// A key moves the focus to a card that may not be drawn yet (↓ on a closed
|
|
437
|
+
// card opens it first), so the move waits for the render that draws it.
|
|
438
|
+
const pathKey = path.join("\u0000");
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (focusReq == null || !root.current) return;
|
|
441
|
+
const cards = root.current.querySelectorAll("[data-tier-card]");
|
|
442
|
+
for (const el of cards) {
|
|
443
|
+
if (el.getAttribute("data-tier-card") === focusReq) {
|
|
444
|
+
el.focus();
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
setFocusReq(null);
|
|
449
|
+
}, [focusReq, pathKey]);
|
|
450
|
+
|
|
451
|
+
function onKeyDown(e) {
|
|
452
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
453
|
+
const holder = e.target instanceof Element ? e.target.closest("[data-tier-id]") : null;
|
|
454
|
+
if (!holder) return;
|
|
455
|
+
const id = holder.getAttribute("data-tier-id");
|
|
456
|
+
const node = model.nodes.get(id);
|
|
457
|
+
if (!node) return;
|
|
458
|
+
const go = (to) => {
|
|
459
|
+
if (to == null) return;
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
setTab(to);
|
|
462
|
+
setFocusReq(to);
|
|
463
|
+
};
|
|
464
|
+
if (e.key === "ArrowDown") {
|
|
465
|
+
if (!node.kids.length) return;
|
|
466
|
+
if (node.group === "tree" && !openIds.has(id)) toggle(id);
|
|
467
|
+
go(node.kids[0]);
|
|
468
|
+
} else if (e.key === "Escape") {
|
|
469
|
+
if (node.group !== "tree") return;
|
|
470
|
+
if (openIds.has(id)) {
|
|
471
|
+
e.preventDefault();
|
|
472
|
+
toggle(id);
|
|
473
|
+
} else if (node.up != null) {
|
|
474
|
+
e.preventDefault();
|
|
475
|
+
toggle(node.up);
|
|
476
|
+
go(node.up);
|
|
477
|
+
}
|
|
478
|
+
} else if (["ArrowLeft", "ArrowRight", "ArrowUp", "Home", "End"].includes(e.key)) {
|
|
479
|
+
go(neighbour(model, id, e.key));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const ctx = {
|
|
484
|
+
model,
|
|
485
|
+
path,
|
|
486
|
+
openIds,
|
|
487
|
+
uid,
|
|
488
|
+
tabId,
|
|
489
|
+
setTab,
|
|
490
|
+
toggle,
|
|
491
|
+
onSelect,
|
|
492
|
+
selected,
|
|
493
|
+
renderCard,
|
|
494
|
+
childrenLabel,
|
|
495
|
+
openWord,
|
|
496
|
+
servesLabel,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
if (loading) {
|
|
500
|
+
return (
|
|
501
|
+
<div aria-busy="true" aria-label={`${label}, loading`} className={cx("@container min-w-0", className)}>
|
|
502
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,12rem),1fr))] gap-3">
|
|
503
|
+
{[0, 1, 2].map((i) => (
|
|
504
|
+
<Skeleton key={i} shape="rect" className="h-24 w-full" />
|
|
505
|
+
))}
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (error) {
|
|
511
|
+
return (
|
|
512
|
+
<div className={cx("@container min-w-0", className)}>
|
|
513
|
+
<Notice
|
|
514
|
+
tone="danger"
|
|
515
|
+
action={
|
|
516
|
+
onRetry ? (
|
|
517
|
+
<Button size="sm" onClick={onRetry}>
|
|
518
|
+
Try again
|
|
519
|
+
</Button>
|
|
520
|
+
) : undefined
|
|
521
|
+
}
|
|
522
|
+
>
|
|
523
|
+
{typeof error === "string" || typeof error === "object" ? error : "The map could not be loaded."}
|
|
524
|
+
</Notice>
|
|
525
|
+
</div>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (!model.order.length) {
|
|
529
|
+
return <div className={cx("@container min-w-0", className)}>{empty ?? <Empty icon="network" title="Nothing to map yet" body="Cards appear here once there is something to put on the map." />}</div>;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const parked = model.unparented;
|
|
533
|
+
const drafts = model.ghostOrder.length;
|
|
534
|
+
return (
|
|
535
|
+
<Ctx.Provider value={ctx}>
|
|
536
|
+
<div ref={root} role="group" aria-label={label} onKeyDown={onKeyDown} className={cx("@container grid min-w-0 gap-8", className)}>
|
|
537
|
+
{model.tops.length ? (
|
|
538
|
+
<section aria-label={typeof topsLabel === "string" ? topsLabel : label} className="grid min-w-0 gap-3">
|
|
539
|
+
{topsLabel ? <h3 className={GROUP_HEAD}>{topsLabel}</h3> : null}
|
|
540
|
+
<Tier ids={model.tops} depth={0} above={1} hangs={false} />
|
|
541
|
+
</section>
|
|
542
|
+
) : null}
|
|
543
|
+
{parked.length ? (
|
|
544
|
+
<section aria-label={typeof unparentedLabel === "string" ? unparentedLabel : "Unparented"} className="grid min-w-0 gap-3">
|
|
545
|
+
<h3 className={GROUP_HEAD}>
|
|
546
|
+
{unparentedLabel} {"·"} {parked.length}
|
|
547
|
+
</h3>
|
|
548
|
+
<div className="grid min-w-0 gap-4 @2xl:grid-cols-[minmax(0,1fr)_minmax(0,22rem)] @2xl:items-start">
|
|
549
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,14rem),1fr))] gap-3">
|
|
550
|
+
{parked.map((id) => (
|
|
551
|
+
<div key={id} className="grid min-w-0 justify-items-start">
|
|
552
|
+
<TierCard node={model.nodes.get(id)} />
|
|
553
|
+
</div>
|
|
554
|
+
))}
|
|
555
|
+
</div>
|
|
556
|
+
{unparentedNote ? <div className="grid min-w-0 content-start gap-2 text-sm text-muted-foreground">{unparentedNote}</div> : null}
|
|
557
|
+
</div>
|
|
558
|
+
</section>
|
|
559
|
+
) : null}
|
|
560
|
+
{drafts ? (
|
|
561
|
+
<section aria-label={typeof ghostLabel === "string" ? ghostLabel : "Drafts"} className="grid min-w-0 gap-3">
|
|
562
|
+
<h3 className={GROUP_HEAD}>{ghostLabel}</h3>
|
|
563
|
+
{ghostNote ? <div className="text-sm text-muted-foreground">{ghostNote}</div> : null}
|
|
564
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,16rem),1fr))] gap-3">
|
|
565
|
+
{model.ghostRoots.map((id) => (
|
|
566
|
+
<GhostBranch key={id} id={id} />
|
|
567
|
+
))}
|
|
568
|
+
</div>
|
|
569
|
+
</section>
|
|
570
|
+
) : null}
|
|
571
|
+
</div>
|
|
572
|
+
</Ctx.Provider>
|
|
573
|
+
);
|
|
574
|
+
}
|