makersclaw 0.8.0 → 0.8.2
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 +234 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +246 -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 +1677 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// SumBar in every state — the gallery's and the smoke test's surface. Not
|
|
4
|
+
// installed into apps: `makersclaw add sum-bar` lays index.js, sum-bar.jsx and
|
|
5
|
+
// sum-model.js only.
|
|
6
|
+
// A project's score two reds, the sum below both caps; then a cap that
|
|
7
|
+
// bites (the sum 56, held at 49); then one red holding a
|
|
8
|
+
// 74.
|
|
9
|
+
// Nothing taken off a full bar.
|
|
10
|
+
// Pressable onSelect: every cut is a button, one selected.
|
|
11
|
+
// Sizes, legend sm, md, lg; the legend; the receipt turned off.
|
|
12
|
+
// Odd input a negative cost, a cost past the total, text for a
|
|
13
|
+
// cost, a zero total.
|
|
14
|
+
// Narrow and wide a 320 px container and a wide one.
|
|
15
|
+
// Loading, failed.
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { SumBar } from "../components/blocks/sum-bar";
|
|
18
|
+
|
|
19
|
+
const BANDS = [
|
|
20
|
+
{ from: 0, to: 50, tone: "danger", label: "off track" },
|
|
21
|
+
{ from: 50, to: 75, tone: "warning", label: "at risk" },
|
|
22
|
+
{ from: 75, to: 100, tone: "success", label: "on track" },
|
|
23
|
+
];
|
|
24
|
+
const POSTS = [
|
|
25
|
+
{ at: 74, label: "cap 74 · one red" },
|
|
26
|
+
{ at: 49, label: "cap 49 · two reds" },
|
|
27
|
+
];
|
|
28
|
+
const SIGNALS = [
|
|
29
|
+
{ id: "schedule", label: "Schedule", cost: 22, tone: "danger" },
|
|
30
|
+
{ id: "budget", label: "Budget", cost: 25, tone: "danger" },
|
|
31
|
+
{ id: "late", label: "Late work", cost: 5, tone: "warning" },
|
|
32
|
+
{ id: "blocked", label: "Blocked", cost: 8, tone: "warning" },
|
|
33
|
+
{ id: "activity", label: "Activity", cost: 0 },
|
|
34
|
+
{ id: "client", label: "Client", cost: 5, tone: "warning" },
|
|
35
|
+
];
|
|
36
|
+
const CAPPED = [
|
|
37
|
+
{ id: "schedule", label: "Schedule", cost: 14, tone: "danger" },
|
|
38
|
+
{ id: "budget", label: "Budget", cost: 8, tone: "warning" },
|
|
39
|
+
{ id: "late", label: "Late work", cost: 8, tone: "warning" },
|
|
40
|
+
{ id: "blocked", label: "Blocked", cost: 10, tone: "danger" },
|
|
41
|
+
{ id: "activity", label: "Activity", cost: 4, tone: "warning" },
|
|
42
|
+
];
|
|
43
|
+
const ONE_RED = [
|
|
44
|
+
{ id: "blocked", label: "Blocked", cost: 8, tone: "danger" },
|
|
45
|
+
{ id: "client", label: "Client", cost: 0 },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function Section({ title, note, children }) {
|
|
49
|
+
return (
|
|
50
|
+
<section className="grid min-w-0 gap-2">
|
|
51
|
+
<h2 className="text-sm font-semibold">{title}</h2>
|
|
52
|
+
{note ? <p className="text-xs text-muted-foreground">{note}</p> : null}
|
|
53
|
+
{children}
|
|
54
|
+
</section>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default function Demo() {
|
|
59
|
+
const [on, setOn] = useState("budget");
|
|
60
|
+
return (
|
|
61
|
+
<div className="grid min-w-0 gap-8 p-4">
|
|
62
|
+
<Section title="Two reds: the sum is below both caps" note="100 − 22 − 25 − 5 − 8 − 0 − 5 = 35. The caps change nothing here.">
|
|
63
|
+
<SumBar label="Website rebuild, score sheet" size="lg" parts={SIGNALS} result={35} posts={POSTS} bands={BANDS} unit="points" />
|
|
64
|
+
</Section>
|
|
65
|
+
|
|
66
|
+
<Section title="A cap that bites" note="The sum is 56; two reds hold it at 49. The stretch between is dotted.">
|
|
67
|
+
<SumBar label="Online banking help centre, score sheet" size="lg" parts={CAPPED} result={49} posts={POSTS} bands={BANDS} />
|
|
68
|
+
</Section>
|
|
69
|
+
|
|
70
|
+
<Section title="One red holding a 74" note="Only 8 points lost, yet the cap says 74.">
|
|
71
|
+
<SumBar label="Mortgage calculator copy, score sheet" parts={ONE_RED} result={74} posts={POSTS} bands={BANDS} />
|
|
72
|
+
</Section>
|
|
73
|
+
|
|
74
|
+
<Section title="Nothing taken off">
|
|
75
|
+
<SumBar label="Launch campaign, score sheet" parts={[{ id: "a", label: "Schedule", cost: 0 }, { id: "b", label: "Budget", cost: 0 }]} posts={POSTS} bands={BANDS} />
|
|
76
|
+
<SumBar label="No parts at all" parts={[]} />
|
|
77
|
+
</Section>
|
|
78
|
+
|
|
79
|
+
<Section title="Pressable" note="Every cut is a button. The pressed one is ringed.">
|
|
80
|
+
<SumBar label="Press a cut" parts={SIGNALS} result={35} selected={on} onSelect={(p) => setOn(p.id)} bands={BANDS} legend />
|
|
81
|
+
<p className="text-xs text-muted-foreground">Pressed: {SIGNALS.find((p) => p.id === on)?.label}</p>
|
|
82
|
+
</Section>
|
|
83
|
+
|
|
84
|
+
<Section title="Sizes, no receipt" note="sm and md under a title, without the written sum.">
|
|
85
|
+
<SumBar size="sm" label="Small" parts={CAPPED} result={49} receipt={false} />
|
|
86
|
+
<SumBar size="md" label="Medium" parts={CAPPED} result={49} receipt={false} />
|
|
87
|
+
</Section>
|
|
88
|
+
|
|
89
|
+
<Section title="Small cuts" note="Under 5% wide: the cost is in the tooltip and the receipt.">
|
|
90
|
+
<SumBar label="Small cuts" parts={[{ id: "a", label: "Late", cost: 3, tone: "warning" }, { id: "b", label: "Blocked", cost: 2, tone: "warning" }, { id: "c", label: "Client", cost: 1 }]} bands={BANDS} />
|
|
91
|
+
</Section>
|
|
92
|
+
|
|
93
|
+
<Section title="Odd input" note="A negative cost, a cost past the total, text for a cost, a zero total.">
|
|
94
|
+
<SumBar label="Odd costs" parts={[{ id: "a", label: "Negative", cost: -5 }, { id: "b", label: "Text", cost: "12" }, { id: "c", label: "Past the total", cost: 400, tone: "danger" }, { id: "d", label: "Nothing", cost: "n/a" }]} />
|
|
95
|
+
<SumBar label="Zero total" total={0} parts={[{ id: "a", label: "A", cost: 10 }]} />
|
|
96
|
+
</Section>
|
|
97
|
+
|
|
98
|
+
<Section title="Narrow (a 320 px container)">
|
|
99
|
+
<div className="w-80 max-w-full">
|
|
100
|
+
<SumBar label="Narrow" parts={SIGNALS} result={35} posts={POSTS} bands={BANDS} legend />
|
|
101
|
+
</div>
|
|
102
|
+
</Section>
|
|
103
|
+
|
|
104
|
+
<Section title="Loading, failed">
|
|
105
|
+
<SumBar label="Loading" status="loading" />
|
|
106
|
+
<SumBar label="Failed" status="failed" error="The check could not be read." onRetry={() => {}} />
|
|
107
|
+
</Section>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The SumBar block — `makersclaw add sum-bar`. Fixed: compose it through its
|
|
2
|
+
// props (README.md beside this file). The model's helpers are plain
|
|
3
|
+
// functions; they work on the server too.
|
|
4
|
+
export { SumBar } from "./sum-bar";
|
|
5
|
+
export { bandsOf, costOf, cutsOf, labelledCuts, lostOf, postsOf, receiptOf, resultOf, sumWords, totalOf } from "./sum-model";
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button, Notice, Skeleton, cx } from "../../ui";
|
|
4
|
+
import { bandsOf, cutsOf, labelledCuts, postsOf, receiptOf, resultOf, sumWords } from "./sum-model";
|
|
5
|
+
|
|
6
|
+
// A total with its parts cut out of it. The bar runs from 0 to the total;
|
|
7
|
+
// what is left is solid, each part's cost is a hatched block cut from the
|
|
8
|
+
// top end downward, and a post stands where a rule holds the result down (a
|
|
9
|
+
// cap). Under it, the sum written out so no number is taken on trust.
|
|
10
|
+
//
|
|
11
|
+
// Props in, one callback out (`onSelect`); the block imports only React and
|
|
12
|
+
// the kit, reads no data and works nothing out beyond placing what it is
|
|
13
|
+
// given. Positions are percentages, so the server draws what the browser
|
|
14
|
+
// draws. README.md beside this file lists every prop.
|
|
15
|
+
|
|
16
|
+
// Literal class names, so Tailwind sees every one of them.
|
|
17
|
+
const CUT = {
|
|
18
|
+
neutral: "border-border text-muted-foreground bg-muted",
|
|
19
|
+
info: "border-info text-info bg-info/10",
|
|
20
|
+
warning: "border-warning text-warning bg-warning/10",
|
|
21
|
+
danger: "border-destructive text-destructive bg-destructive/10",
|
|
22
|
+
};
|
|
23
|
+
const BAND = { success: "bg-success", warning: "bg-warning", danger: "bg-destructive", info: "bg-info", neutral: "bg-muted-foreground" };
|
|
24
|
+
const SIZE = { sm: "h-6", md: "h-9", lg: "h-12" };
|
|
25
|
+
|
|
26
|
+
const HATCH = { backgroundImage: "repeating-linear-gradient(135deg, currentColor 0 1px, transparent 1px 5px)" };
|
|
27
|
+
const DOTS = { backgroundImage: "repeating-linear-gradient(90deg, currentColor 0 2px, transparent 2px 6px)" };
|
|
28
|
+
|
|
29
|
+
const say = (n) => (Number.isInteger(n) ? String(n) : String(Math.round(n * 1000) / 1000));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The bar.
|
|
33
|
+
*
|
|
34
|
+
* Props: `total` (100), `parts` `[{ id, label, cost, tone?, note? }]` (taken
|
|
35
|
+
* off in the order given, from the top end), `result` (the number after a
|
|
36
|
+
* rule held it down; default total minus the costs), `posts` `[{ at, label,
|
|
37
|
+
* tone? }]`, `bands` `[{ from, to, tone, label? }]`, `selected`, `onSelect(part)`,
|
|
38
|
+
* `size` (sm | md | lg), `names` (the names under the cuts, default true),
|
|
39
|
+
* `legend` (a list of the parts under the bar), `receipt` (the written sum,
|
|
40
|
+
* default true), `unit`, `noun`, `label`, `status`, `error`, `onRetry`,
|
|
41
|
+
* `className`.
|
|
42
|
+
*/
|
|
43
|
+
export function SumBar({
|
|
44
|
+
total = 100,
|
|
45
|
+
parts,
|
|
46
|
+
result,
|
|
47
|
+
posts,
|
|
48
|
+
bands,
|
|
49
|
+
selected,
|
|
50
|
+
onSelect,
|
|
51
|
+
size = "md",
|
|
52
|
+
names = true,
|
|
53
|
+
legend = false,
|
|
54
|
+
receipt = true,
|
|
55
|
+
unit = "",
|
|
56
|
+
noun = "point",
|
|
57
|
+
label = "Score sheet",
|
|
58
|
+
status = "ready",
|
|
59
|
+
error,
|
|
60
|
+
onRetry,
|
|
61
|
+
className,
|
|
62
|
+
}) {
|
|
63
|
+
if (status === "loading") {
|
|
64
|
+
return (
|
|
65
|
+
<div role="status" aria-label={`${label}, loading`} className={cx("grid gap-2", className)}>
|
|
66
|
+
<Skeleton className={cx("w-full", SIZE[size] ?? SIZE.md)} />
|
|
67
|
+
<Skeleton className="h-3 w-2/3" />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (status === "failed") {
|
|
72
|
+
return (
|
|
73
|
+
<Notice
|
|
74
|
+
tone="danger"
|
|
75
|
+
className={className}
|
|
76
|
+
action={
|
|
77
|
+
onRetry ? (
|
|
78
|
+
<Button size="sm" onClick={onRetry}>
|
|
79
|
+
Try again
|
|
80
|
+
</Button>
|
|
81
|
+
) : undefined
|
|
82
|
+
}
|
|
83
|
+
>
|
|
84
|
+
{error || "The sum could not be read."}
|
|
85
|
+
</Notice>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const r = resultOf({ total, parts, result });
|
|
90
|
+
const cuts = cutsOf({ total, parts });
|
|
91
|
+
const pins = postsOf({ total: r.total, posts });
|
|
92
|
+
const strips = bandsOf({ total: r.total, bands });
|
|
93
|
+
const named = names ? labelledCuts(cuts) : [];
|
|
94
|
+
const keptPct = Math.round((Math.min(r.kept, r.result) / r.total) * 100000) / 1000;
|
|
95
|
+
const resultPct = Math.round((r.result / r.total) * 100000) / 1000;
|
|
96
|
+
const sum = receiptOf({ total, parts, result, unit });
|
|
97
|
+
const inside = resultPct >= 12;
|
|
98
|
+
const words = sumWords({ total, parts, result, posts, noun });
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div role="group" aria-label={label} className={cx("@container grid min-w-0 gap-2", className)}>
|
|
102
|
+
<div className="relative min-w-0 pt-6">
|
|
103
|
+
{pins.map((p) => (
|
|
104
|
+
<span
|
|
105
|
+
key={`post-label-${p.index}`}
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
className={cx("absolute top-0 left-(--at) -translate-x-1/2 whitespace-nowrap text-[11px] font-semibold leading-none", p.tone === "danger" ? "text-destructive" : "text-muted-foreground")}
|
|
108
|
+
style={{ "--at": `${p.pct}%` }}
|
|
109
|
+
>
|
|
110
|
+
{p.label}
|
|
111
|
+
</span>
|
|
112
|
+
))}
|
|
113
|
+
<div className={cx("relative overflow-hidden rounded-[4px] border border-border bg-muted", SIZE[size] ?? SIZE.md)} role="img" aria-label={words}>
|
|
114
|
+
{resultPct > 0 ? (
|
|
115
|
+
<span aria-hidden="true" className="absolute inset-y-0 left-0 w-(--w) bg-foreground" style={{ "--w": `${Math.min(keptPct, resultPct)}%` }} />
|
|
116
|
+
) : null}
|
|
117
|
+
{r.held > 0 ? (
|
|
118
|
+
<span
|
|
119
|
+
aria-hidden="true"
|
|
120
|
+
title={`Held down by a rule: ${say(r.kept)} on the sum, ${say(r.result)} after it`}
|
|
121
|
+
className="absolute inset-y-0 left-(--at) w-(--width) text-muted-foreground"
|
|
122
|
+
style={{ ...DOTS, "--at": `${resultPct}%`, "--width": `${Math.round((r.held / r.total) * 100000) / 1000}%` }}
|
|
123
|
+
/>
|
|
124
|
+
) : null}
|
|
125
|
+
{inside ? (
|
|
126
|
+
<span aria-hidden="true" className="absolute inset-y-0 left-2 flex items-center text-sm font-semibold tabular-nums text-background">
|
|
127
|
+
{say(r.result)}
|
|
128
|
+
</span>
|
|
129
|
+
) : null}
|
|
130
|
+
{cuts
|
|
131
|
+
.filter((c) => c.cut)
|
|
132
|
+
.map((c) => {
|
|
133
|
+
const tone = CUT[c.tone] ?? CUT.neutral;
|
|
134
|
+
const on = selected != null && c.id === selected;
|
|
135
|
+
const className = cx("absolute inset-y-0 left-(--at) w-(--width) overflow-hidden border-x outline-none focus-visible:ring-2 focus-visible:ring-ring", tone, on && "ring-2 ring-ring");
|
|
136
|
+
const style = { "--at": `${c.leftPct}%`, "--width": `${c.widthPct}%` };
|
|
137
|
+
const inner = (
|
|
138
|
+
<>
|
|
139
|
+
<span aria-hidden="true" className="absolute inset-0 opacity-60" style={HATCH} />
|
|
140
|
+
{c.widthPct >= 5 ? <span className="relative flex h-full items-center justify-center text-[11px] font-semibold tabular-nums text-foreground">−{say(c.cost)}</span> : null}
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
return onSelect ? (
|
|
144
|
+
<button key={c.id ?? c.index} type="button" aria-label={`${c.label ?? "Part"}, took ${say(c.cost)}`} aria-pressed={on} title={`${c.label ?? "Part"} took ${say(c.cost)}`} className={className} style={style} onClick={() => onSelect(c)}>
|
|
145
|
+
{inner}
|
|
146
|
+
</button>
|
|
147
|
+
) : (
|
|
148
|
+
<span key={c.id ?? c.index} title={`${c.label ?? "Part"} took ${say(c.cost)}`} className={className} style={style}>
|
|
149
|
+
{inner}
|
|
150
|
+
</span>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
{pins.map((p) => (
|
|
154
|
+
<span key={`post-${p.index}`} aria-hidden="true" className={cx("absolute inset-y-0 left-(--at) w-0 border-l-2 border-dashed", p.tone === "danger" ? "border-destructive" : "border-muted-foreground")} style={{ "--at": `${p.pct}%` }} />
|
|
155
|
+
))}
|
|
156
|
+
{!inside && resultPct > 0 ? (
|
|
157
|
+
<span aria-hidden="true" className="absolute inset-y-0 left-(--at) ml-1.5 flex items-center text-sm font-semibold tabular-nums" style={{ "--at": `${resultPct}%` }}>
|
|
158
|
+
{say(r.result)}
|
|
159
|
+
</span>
|
|
160
|
+
) : null}
|
|
161
|
+
</div>
|
|
162
|
+
{strips.length ? (
|
|
163
|
+
<div className="relative mt-1 h-1.5 overflow-hidden rounded-[2px]" aria-hidden="true">
|
|
164
|
+
{strips.map((b) => (
|
|
165
|
+
<span key={b.index} className={cx("absolute inset-y-0 left-(--at) w-(--width)", BAND[b.tone] ?? BAND.neutral)} style={{ "--at": `${b.leftPct}%`, "--width": `${b.widthPct}%` }} />
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
) : null}
|
|
169
|
+
{strips.some((b) => b.label) ? (
|
|
170
|
+
<div className="relative mt-1 h-4 text-[11px] text-muted-foreground" aria-hidden="true">
|
|
171
|
+
{strips.map((b) =>
|
|
172
|
+
b.label ? (
|
|
173
|
+
<span key={b.index} className="absolute top-0 left-(--at) -translate-x-1/2 whitespace-nowrap" style={{ "--at": `${b.leftPct + b.widthPct / 2}%` }}>
|
|
174
|
+
{b.label}
|
|
175
|
+
</span>
|
|
176
|
+
) : null,
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
{named.length ? (
|
|
181
|
+
<div className="relative mt-1 hidden h-4 text-[11px] text-muted-foreground @lg:block" aria-hidden="true">
|
|
182
|
+
{named.map((c) => (
|
|
183
|
+
<span key={c.id ?? c.index} className="absolute top-0 left-(--at) -translate-x-1/2 whitespace-nowrap" style={{ "--at": `${c.leftPct + c.widthPct / 2}%` }}>
|
|
184
|
+
{c.label}
|
|
185
|
+
</span>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
) : null}
|
|
189
|
+
</div>
|
|
190
|
+
{legend && cuts.length ? (
|
|
191
|
+
<ul className="m-0 grid list-none gap-1 p-0 text-xs @md:grid-cols-2">
|
|
192
|
+
{cuts.map((c) => (
|
|
193
|
+
<li key={c.id ?? c.index} className="flex min-w-0 items-baseline gap-2">
|
|
194
|
+
<span className="truncate">{c.label}</span>
|
|
195
|
+
<span className="ml-auto shrink-0 tabular-nums text-muted-foreground">{c.cost > 0 ? `−${say(c.cost)}` : "0"}</span>
|
|
196
|
+
</li>
|
|
197
|
+
))}
|
|
198
|
+
</ul>
|
|
199
|
+
) : null}
|
|
200
|
+
{receipt ? (
|
|
201
|
+
<p className="m-0 text-xs tabular-nums text-muted-foreground">
|
|
202
|
+
<span className="font-medium text-foreground">{sum.text}</span>
|
|
203
|
+
{sum.held > 0 ? <span> · a rule holds it down by {say(sum.held)}</span> : null}
|
|
204
|
+
{sum.raised > 0 ? <span> · a rule lifts it by {say(sum.raised)}</span> : null}
|
|
205
|
+
</p>
|
|
206
|
+
) : null}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// sum-bar: the arithmetic. Plain functions, no React, no clock: they work on
|
|
2
|
+
// the server and in a test.
|
|
3
|
+
//
|
|
4
|
+
// A SUM starts at a total (100) and takes costs off it, one per part. What is
|
|
5
|
+
// left is the result. A rule may hold the result down (a cap): then the result
|
|
6
|
+
// is lower than total minus the costs, and the stretch between the two is
|
|
7
|
+
// "held". The block draws what the app has already worked out; these helpers
|
|
8
|
+
// only place it and say it.
|
|
9
|
+
|
|
10
|
+
/** A cost: a finite number, never below 0. Anything else counts as 0. */
|
|
11
|
+
export function costOf(value) {
|
|
12
|
+
const n = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
13
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const round = (n) => Math.round(n * 1000) / 1000;
|
|
17
|
+
const clampPct = (n) => Math.min(100, Math.max(0, n));
|
|
18
|
+
|
|
19
|
+
/** The total as a positive number (default 100). */
|
|
20
|
+
export function totalOf(total) {
|
|
21
|
+
const n = Number(total);
|
|
22
|
+
return Number.isFinite(n) && n > 0 ? n : 100;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Everything the parts cost, together. */
|
|
26
|
+
export function lostOf(parts) {
|
|
27
|
+
return round((parts ?? []).reduce((sum, p) => sum + costOf(p?.cost), 0));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What is left once the costs are off, and what the result is:
|
|
32
|
+
* `kept` = total minus the costs (never below 0), `result` = the app's own
|
|
33
|
+
* number when it gives one (a cap held it down), else `kept`.
|
|
34
|
+
* `held` is how far a rule pushed the result below `kept`.
|
|
35
|
+
*/
|
|
36
|
+
export function resultOf({ total, parts, result }) {
|
|
37
|
+
const whole = totalOf(total);
|
|
38
|
+
const lost = lostOf(parts);
|
|
39
|
+
const kept = Math.max(0, round(whole - lost));
|
|
40
|
+
const given = typeof result === "number" && Number.isFinite(result) ? Math.min(Math.max(result, 0), whole) : null;
|
|
41
|
+
const final = given == null ? kept : round(given);
|
|
42
|
+
return { total: whole, lost, kept, result: final, held: final < kept ? round(kept - final) : 0, raised: final > kept ? round(final - kept) : 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Each part's place on the bar. The costs are cut out from the total
|
|
47
|
+
* downward, in the order the parts are given: the first part sits at the top
|
|
48
|
+
* end. A part that would run below 0 is cut short there. Percentages of the
|
|
49
|
+
* total.
|
|
50
|
+
*/
|
|
51
|
+
export function cutsOf({ total, parts }) {
|
|
52
|
+
const whole = totalOf(total);
|
|
53
|
+
let top = whole;
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const [index, p] of (parts ?? []).entries()) {
|
|
56
|
+
const cost = costOf(p?.cost);
|
|
57
|
+
const low = Math.max(0, top - cost);
|
|
58
|
+
out.push({
|
|
59
|
+
...p,
|
|
60
|
+
index,
|
|
61
|
+
cost,
|
|
62
|
+
from: round(low),
|
|
63
|
+
to: round(top),
|
|
64
|
+
leftPct: round(clampPct((low / whole) * 100)),
|
|
65
|
+
widthPct: round(clampPct(((top - low) / whole) * 100)),
|
|
66
|
+
cut: cost > 0,
|
|
67
|
+
});
|
|
68
|
+
top = low;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A post's place (a cap, a threshold) as a percentage. Posts out of range are dropped. */
|
|
74
|
+
export function postsOf({ total, posts }) {
|
|
75
|
+
const whole = totalOf(total);
|
|
76
|
+
return (posts ?? [])
|
|
77
|
+
.map((p, index) => ({ ...p, index, at: Number(p?.at) }))
|
|
78
|
+
.filter((p) => Number.isFinite(p.at) && p.at >= 0 && p.at <= whole)
|
|
79
|
+
.map((p) => ({ ...p, pct: round(clampPct((p.at / whole) * 100)) }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The coloured bands under the bar (off track, at risk, on track), as percentages. */
|
|
83
|
+
export function bandsOf({ total, bands }) {
|
|
84
|
+
const whole = totalOf(total);
|
|
85
|
+
return (bands ?? [])
|
|
86
|
+
.map((b, index) => ({ ...b, index, from: Number(b?.from), to: Number(b?.to) }))
|
|
87
|
+
.filter((b) => Number.isFinite(b.from) && Number.isFinite(b.to) && b.to > b.from)
|
|
88
|
+
.map((b) => {
|
|
89
|
+
const from = Math.max(0, b.from);
|
|
90
|
+
const to = Math.min(whole, b.to);
|
|
91
|
+
return { ...b, leftPct: round(clampPct((from / whole) * 100)), widthPct: round(clampPct(((to - from) / whole) * 100)) };
|
|
92
|
+
})
|
|
93
|
+
.filter((b) => b.widthPct > 0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const num = (n) => (Number.isInteger(n) ? String(n) : String(round(n)));
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The receipt, as a sum anyone can check: "100 − 22 − 25 − 5 − 8 − 0 − 5 = 35".
|
|
100
|
+
* Every part is a term, a part that cost nothing included. When a rule held
|
|
101
|
+
* the result down: "100 − 44 = 56, held at 49".
|
|
102
|
+
*/
|
|
103
|
+
export function receiptOf({ total, parts, result, unit = "" }) {
|
|
104
|
+
const r = resultOf({ total, parts, result });
|
|
105
|
+
const list = parts ?? [];
|
|
106
|
+
const terms = list.map((p) => ({ id: p?.id ?? p?.label, label: p?.label ?? "", cost: costOf(p?.cost) }));
|
|
107
|
+
const sum = [num(r.total), ...terms.map((t) => `− ${num(t.cost)}`)].join(" ");
|
|
108
|
+
const text = r.held > 0 ? `${r.total} − ${num(r.lost)} = ${num(r.kept)}, held at ${num(r.result)}` : list.length ? `${sum} = ${num(r.result)}` : `${num(r.total)} = ${num(r.result)}`;
|
|
109
|
+
return { ...r, terms, text: unit ? `${text} ${unit}` : text };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The whole bar in a sentence for a screen reader. */
|
|
113
|
+
export function sumWords({ total, parts, result, posts, noun = "point" }) {
|
|
114
|
+
const r = resultOf({ total, parts, result });
|
|
115
|
+
const list = (parts ?? []).filter((p) => costOf(p?.cost) > 0);
|
|
116
|
+
const plural = (n) => (n === 1 ? noun : `${noun}s`);
|
|
117
|
+
const took = list.length ? ` ${list.map((p) => `${p.label ?? "A part"} took ${num(costOf(p.cost))}`).join(", ")}.` : " Nothing was taken off.";
|
|
118
|
+
const held = r.held > 0 ? ` A rule holds it at ${num(r.result)} where the sum is ${num(r.kept)}.` : "";
|
|
119
|
+
const pins = (posts ?? []).filter((p) => p?.label).map((p) => `${p.label} at ${num(Number(p.at))}`);
|
|
120
|
+
return `${num(r.result)} of ${num(r.total)} ${plural(r.total)}.${took}${held}${pins.length ? ` Posts: ${pins.join(", ")}.` : ""}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Which of the parts to show a name under, when the bar is too short for all of them. */
|
|
124
|
+
export function labelledCuts(cuts, minPct = 7) {
|
|
125
|
+
return cuts.filter((c) => c.cut && c.widthPct >= minPct);
|
|
126
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# TierMap
|
|
2
|
+
|
|
3
|
+
Cards in tiers, from a flat list where each item names its parent.
|
|
4
|
+
|
|
5
|
+
The first tier is the tops. Press a card and the cards under it open as the
|
|
6
|
+
next tier beneath, joined to it by elbow lines. Press it again and they close.
|
|
7
|
+
**One layer opens at a time**: opening a card closes what was open beside it.
|
|
8
|
+
A card with something under it says how many ("6 serving it") and whether it
|
|
9
|
+
is open.
|
|
10
|
+
|
|
11
|
+
Items that sit under nothing and are not tops are shown together in an
|
|
12
|
+
*unparented* group, with a note and a slot for an action. Items marked `ghost`
|
|
13
|
+
(drafts: counted nowhere yet) are drawn dashed in a group of their own, and a
|
|
14
|
+
draft can serve another draft.
|
|
15
|
+
|
|
16
|
+
It draws a structure the app has already worked out. It moves nothing between
|
|
17
|
+
parents and collects nothing. For a person-and-manager chart you pan and zoom
|
|
18
|
+
use `org-chart`; for a free map of ideas `mind-map`; for nodes and edges you
|
|
19
|
+
draw `flow-canvas`; for a table with rows that expand `data-table`.
|
|
20
|
+
|
|
21
|
+
Born in a goals tracker, on the screen "How goals fit": company goals in the
|
|
22
|
+
first tier, the team goals that serve them beneath. The same block holds
|
|
23
|
+
initiatives under goals, work under a roadmap theme, an account's
|
|
24
|
+
sub-accounts, a strategy map.
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
import { TierMap } from "../components/blocks/tier-map"; // a Makersclaw app ("./blocks/tier-map" from components/)
|
|
28
|
+
import { TierMap } from "@/components/makersclaw/blocks/tier-map"; // a shadcn project
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
A client component. No other block and no npm package. The lines are CSS only:
|
|
32
|
+
each tier is a grid of equal columns and the lines are placed in percent of
|
|
33
|
+
its width, so nothing is measured, there is no observer, and the server draws
|
|
34
|
+
what the browser draws. The helpers (`buildTierModel`, `openPath`,
|
|
35
|
+
`togglePath`, `neighbour`, `elbowOf`...) are plain functions and work on the
|
|
36
|
+
server too.
|
|
37
|
+
|
|
38
|
+
## The data
|
|
39
|
+
|
|
40
|
+
One row per card, as a table holds them:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
[
|
|
44
|
+
{ id: "c1", title: "Reach $1M in annual revenue", kicker: "Company", by: <Avatar name="Dana" size="xs" />, value: 0.62, tone: "warning", foot: "6 teams serve it" },
|
|
45
|
+
{ id: "s1", parent: "c1", title: "Close the enterprise gap", kicker: "Sales", value: 0.58 },
|
|
46
|
+
{ id: "g1", ghost: true, title: "Grow MRR to $80k", kicker: "Draft" },
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `id` required, unique (a repeat is ignored). Numbers are read as text.
|
|
51
|
+
- `parent` the parent's `id`. Missing, empty, itself, or **an id that is not in
|
|
52
|
+
the list**: the card has no parent. A **loop** of parents (a names b, b names
|
|
53
|
+
a) is broken at the card that comes first in the list, which becomes
|
|
54
|
+
parentless, and never hangs the map.
|
|
55
|
+
- `title` the card's heading. Long words break; nothing overflows.
|
|
56
|
+
- `kicker` the small line above the title ("Sales", "Sub-account").
|
|
57
|
+
- `mark` a node before the kicker (the app's status mark: a `StatusDot`, an
|
|
58
|
+
icon).
|
|
59
|
+
- `by` a node at the card's top right (an `Avatar`). It sits inside the
|
|
60
|
+
card's button, so it must not be interactive.
|
|
61
|
+
- `value` 0 to 1: a bar with its percent. Missing, `null` or not a number: no
|
|
62
|
+
bar. A draft never has one.
|
|
63
|
+
- `tone` the bar's colour, **a status only**: `ink` (default) · `quiet` · `info`
|
|
64
|
+
· `success` · `warning` · `danger` · `auto` (ink, and success when full).
|
|
65
|
+
- `foot` one muted line at the card's foot.
|
|
66
|
+
- `ghost` `true`: a draft. Dashed, no bar, never in the tiers, never counted.
|
|
67
|
+
A draft under a draft is drawn under it; a draft under a real card shows
|
|
68
|
+
`Serves <that card's title>`.
|
|
69
|
+
- `href` the card is also a link (the app's record page).
|
|
70
|
+
- Any other key is kept and handed back to `onSelect` and `renderCard`.
|
|
71
|
+
|
|
72
|
+
Where a card goes:
|
|
73
|
+
|
|
74
|
+
| It is | and it goes |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| a top (named in `tops`, or by default a real card with no parent and kids) | the first tier |
|
|
77
|
+
| under a top, or under a card in the tiers | the tier beneath its parent when that card is open |
|
|
78
|
+
| real, and not reachable from any top | the unparented group |
|
|
79
|
+
| `ghost` | the drafts group |
|
|
80
|
+
|
|
81
|
+
A card named in `tops` is drawn as a top and nowhere else, even if it names a
|
|
82
|
+
parent. A real card whose parent is a draft is unparented.
|
|
83
|
+
|
|
84
|
+
## Props
|
|
85
|
+
|
|
86
|
+
- `items` required. The rows above. With none, `empty` shows.
|
|
87
|
+
- `tops` ids drawn as the first tier, in that order. Default: the real cards
|
|
88
|
+
with no parent that have kids. Name them yourself to show a top that has
|
|
89
|
+
nothing under it yet.
|
|
90
|
+
- `topsLabel` the heading over the first tier ("Company · Q3 2026"). A node.
|
|
91
|
+
- `label` the map's name for a reader. Default "Tiers".
|
|
92
|
+
- `childrenLabel` the words after the count on a card with kids: a string
|
|
93
|
+
("serving it" gives "6 serving it") or `(count) => text`. Default "below".
|
|
94
|
+
- `openWord` said after the count while a card is open. Default "open".
|
|
95
|
+
- `open` the open path, controlled: an array of ids, **one per tier** (a top,
|
|
96
|
+
then one of its kids, then one of theirs). An id that cannot be open there
|
|
97
|
+
ends the path at that point.
|
|
98
|
+
- `defaultOpen` the same, uncontrolled (the initial path). Default none.
|
|
99
|
+
- `onOpenChange(ids)` called with the new path when a card opens or closes.
|
|
100
|
+
- `onSelect(item)` makes a card's title and body a button that selects it (open
|
|
101
|
+
the record); the count and the chevron then open the tier. Without it the
|
|
102
|
+
whole card opens the tier, and a card with nothing under it does nothing.
|
|
103
|
+
- `selected` the id marked as current.
|
|
104
|
+
- `unparentedLabel` the group's heading. Default "Not under anything"; the
|
|
105
|
+
count is added ("Serves nothing · 1"). The group is drawn only when there is
|
|
106
|
+
something in it.
|
|
107
|
+
- `unparentedNote` a node beside the cards: the note and the action, as you
|
|
108
|
+
compose them ("Serves nothing: is it work the company needs, or work nobody
|
|
109
|
+
asked for?" and a Button "Link to a company goal").
|
|
110
|
+
- `ghostLabel` the drafts group's heading. Default "Drafts".
|
|
111
|
+
- `ghostNote` a node under that heading.
|
|
112
|
+
- `servesLabel` the word before the parent's title on a draft that serves a
|
|
113
|
+
real card. Default "Serves"; `""` leaves the line out.
|
|
114
|
+
- `renderCard(item, state)` replaces the default card. Return nothing (`null`)
|
|
115
|
+
to keep the default for that card. `state` is `{ id, item, open, selected,
|
|
116
|
+
count, tier, ghost, serves, pressable, select, toggle, holderProps,
|
|
117
|
+
primaryProps, toggleProps }`. Spread `holderProps` on your root,
|
|
118
|
+
`primaryProps` on the element that takes the focus, and `toggleProps` (null
|
|
119
|
+
when the card has nothing under it) on the button that opens the tier; the
|
|
120
|
+
arrow keys, the roving focus and the tier lines depend on them.
|
|
121
|
+
- `loading` three placeholder cards.
|
|
122
|
+
- `error` the words for a failure (a string or a node). Pass plain words:
|
|
123
|
+
the block shows them as given. With `onRetry`, a Try again button.
|
|
124
|
+
- `empty` a node shown when there are no items. Default: "Nothing to map yet".
|
|
125
|
+
- `className` layout: width and margin. Never colour.
|
|
126
|
+
|
|
127
|
+
`TierCard` is exported too, for a `renderCard` that wants the default card in
|
|
128
|
+
some cases. Helpers, pure and without React: `buildTierModel(items, { tops })`,
|
|
129
|
+
`openPath(model, ids)`, `togglePath(model, path, id)`, `pathTo(model, id)`
|
|
130
|
+
(the ids to open so that a card is on screen), `tiersOf`, `neighbour`,
|
|
131
|
+
`siblingsOf`, `elbowOf`, `centreOf`, `barOf`, `toneOf`, `countText`, `fitKey`,
|
|
132
|
+
`thresholdRem`.
|
|
133
|
+
|
|
134
|
+
## Example
|
|
135
|
+
|
|
136
|
+
```jsx
|
|
137
|
+
const [path, setPath] = useState(["c1"]);
|
|
138
|
+
|
|
139
|
+
<TierMap
|
|
140
|
+
label="How goals fit"
|
|
141
|
+
items={goals} // [{ id, parent, title, kicker, by, value, tone, ghost }]
|
|
142
|
+
tops={companyGoalIds}
|
|
143
|
+
topsLabel="Company · Q3 2026"
|
|
144
|
+
childrenLabel={(n) => `${n} serving it`}
|
|
145
|
+
open={path}
|
|
146
|
+
onOpenChange={setPath}
|
|
147
|
+
selected={goalId}
|
|
148
|
+
onSelect={(goal) => router.push(`?goal=${goal.id}`)}
|
|
149
|
+
unparentedLabel="Serves nothing"
|
|
150
|
+
unparentedNote={
|
|
151
|
+
<>
|
|
152
|
+
<p>Is it work the company needs, or work nobody asked for?</p>
|
|
153
|
+
<Button size="sm" onClick={linkIt}>Link to a company goal</Button>
|
|
154
|
+
</>
|
|
155
|
+
}
|
|
156
|
+
ghostLabel="Q4 2026 drafts · aligned already"
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
// Open the parents of a goal a link points at:
|
|
160
|
+
import { buildTierModel, pathTo } from "../components/blocks/tier-map";
|
|
161
|
+
const path = pathTo(buildTierModel(goals, { tops: companyGoalIds }), goalId);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Keys
|
|
165
|
+
|
|
166
|
+
Cards are buttons (a link when they have an `href`), with one tab stop for the
|
|
167
|
+
whole map; the arrows move inside it.
|
|
168
|
+
|
|
169
|
+
- Left and right: the card beside, in the same tier. Home and End: the first
|
|
170
|
+
and the last.
|
|
171
|
+
- Down: into the first card under this one (opening it first if it is closed).
|
|
172
|
+
- Up: to the parent.
|
|
173
|
+
- Escape: close this card's tier; on a card that is not open, close its
|
|
174
|
+
parent's tier and move to the parent.
|
|
175
|
+
- Enter and Space press the focused button: select the card, or open its tier.
|
|
176
|
+
|
|
177
|
+
## Layout
|
|
178
|
+
|
|
179
|
+
Everything follows the block's own width (container queries), never the
|
|
180
|
+
window's. A tier sits in **one row of equal columns** when the container has
|
|
181
|
+
11 rem for each of its cards (and for each card of the tiers above it, so a
|
|
182
|
+
tier never sits in a row under one that stacks); cards are at most 26 rem wide
|
|
183
|
+
and centred in their column, and an elbow runs from the open card across to
|
|
184
|
+
each card under it. A tier of more than eight cards always stacks.
|
|
185
|
+
|
|
186
|
+
Below that, the tier **stacks**: cards go one under another, full width, and
|
|
187
|
+
the cards under an open card hang from a line down their left. In between
|
|
188
|
+
(the parent in a row, its kids too many for one), the line leaves the parent
|
|
189
|
+
and runs left to that rail.
|
|
190
|
+
|
|
191
|
+
## States drawn
|
|
192
|
+
|
|
193
|
+
- **Empty**, **one top with nothing under it**, **many tops**, **one open**.
|
|
194
|
+
- **A deep path**: three tiers open, controlled or not.
|
|
195
|
+
- **Very long titles, kickers, feet and names**: they wrap or truncate; no
|
|
196
|
+
card grows past its column.
|
|
197
|
+
- **Narrow** (a 320 px container): tiers stack, cards full width, a rail.
|
|
198
|
+
- **Wide** (past 1000 px): a tier of six in one row with its elbows.
|
|
199
|
+
- **Unparented group** with its note and action; **drafts group** where a draft
|
|
200
|
+
serves a draft and one serves a real card.
|
|
201
|
+
- **A loop of parents** (broken), **a parent that is not in the list**, a card
|
|
202
|
+
that names itself.
|
|
203
|
+
- **Read-only** (no `onSelect`): a card only opens its tier.
|
|
204
|
+
- **Loading**, **failed** (with Try again).
|
|
205
|
+
- Reduced motion: the chevron does not turn.
|
|
206
|
+
|
|
207
|
+
## Notes for the app that places it
|
|
208
|
+
|
|
209
|
+
- **Colour is a signal.** The bar is ink unless the goal is behind (warning,
|
|
210
|
+
danger) or done (success). Work out the status yourself; the block has no
|
|
211
|
+
words for "behind" and no thresholds.
|
|
212
|
+
- Put the numbers in words as well: `foot` is the place ("Own key results 82%").
|
|
213
|
+
- The count on a card is the cards drawn under it. A draft is never counted.
|
|
214
|
+
- Keep the open path in the address (`?open=c1,s1`) if a member should be able
|
|
215
|
+
to send the page; `pathTo` gives the path for a card a link points at.
|
|
216
|
+
- It does not fetch. Give it every item of the map at once; a list of hundreds
|
|
217
|
+
is fine, only the open tiers are drawn.
|