@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +81 -2
- package/check.mjs +236 -0
- package/components/accordion/accordion.d.ts +13 -0
- package/components/accordion/accordion.js +35 -0
- package/components/accordion/accordion.module.css +15 -0
- package/components/action-button/action-button.d.ts +11 -0
- package/components/action-button/action-button.js +57 -0
- package/components/action-button/action-button.module.css +15 -0
- package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
- package/components/activity-heatmap/activity-heatmap.js +303 -0
- package/components/activity-heatmap/activity-heatmap.module.css +83 -0
- package/components/alert/alert.d.ts +13 -0
- package/components/alert/alert.js +79 -0
- package/components/alert/alert.module.css +15 -0
- package/components/animated-counter/animated-counter.d.ts +13 -0
- package/components/animated-counter/animated-counter.js +73 -0
- package/components/animated-counter/animated-counter.module.css +12 -0
- package/components/announcement-bar/announcement-bar.d.ts +53 -0
- package/components/announcement-bar/announcement-bar.js +187 -0
- package/components/announcement-bar/announcement-bar.module.css +60 -0
- package/components/avatar/avatar.d.ts +8 -0
- package/components/avatar/avatar.js +18 -0
- package/components/avatar/avatar.module.css +60 -0
- package/components/avatar-group/avatar-group.d.ts +13 -0
- package/components/avatar-group/avatar-group.js +24 -0
- package/components/avatar-group/avatar-group.module.css +27 -0
- package/components/badge/badge.d.ts +10 -0
- package/components/badge/badge.js +66 -0
- package/components/badge/badge.module.css +52 -0
- package/components/bar-chart/bar-chart.d.ts +32 -0
- package/components/bar-chart/bar-chart.js +327 -0
- package/components/bar-chart/bar-chart.module.css +49 -0
- package/components/billing-toggle/billing-toggle.d.ts +35 -0
- package/components/billing-toggle/billing-toggle.js +83 -0
- package/components/billing-toggle/billing-toggle.module.css +139 -0
- package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
- package/components/bottom-sheet/bottom-sheet.js +386 -0
- package/components/bottom-sheet/bottom-sheet.module.css +69 -0
- package/components/breadcrumb/breadcrumb.d.ts +13 -0
- package/components/breadcrumb/breadcrumb.js +19 -0
- package/components/breadcrumb/breadcrumb.module.css +12 -0
- package/components/brush-chart/brush-chart.d.ts +45 -0
- package/components/brush-chart/brush-chart.js +426 -0
- package/components/brush-chart/brush-chart.module.css +69 -0
- package/components/button/button.d.ts +10 -0
- package/components/button/button.js +97 -0
- package/components/button/button.module.css +39 -0
- package/components/button-group/button-group.d.ts +59 -0
- package/components/button-group/button-group.js +280 -0
- package/components/button-group/button-group.module.css +87 -0
- package/components/button-link/button-link.d.ts +24 -0
- package/components/button-link/button-link.js +11 -0
- package/components/button-link/button-link.module.css +7 -0
- package/components/calendar/calendar.d.ts +22 -0
- package/components/calendar/calendar.js +253 -0
- package/components/calendar/calendar.module.css +51 -0
- package/components/card/card.d.ts +19 -0
- package/components/card/card.js +108 -0
- package/components/card/card.module.css +51 -0
- package/components/card-stack/card-stack.d.ts +34 -0
- package/components/card-stack/card-stack.js +291 -0
- package/components/card-stack/card-stack.module.css +63 -0
- package/components/carousel/carousel.d.ts +28 -0
- package/components/carousel/carousel.js +332 -0
- package/components/carousel/carousel.module.css +42 -0
- package/components/chat-thread/chat-thread.d.ts +135 -0
- package/components/chat-thread/chat-thread.js +406 -0
- package/components/chat-thread/chat-thread.module.css +242 -0
- package/components/checkbox/checkbox.d.ts +7 -0
- package/components/checkbox/checkbox.js +23 -0
- package/components/checkbox/checkbox.module.css +15 -0
- package/components/chip-group/chip-group.d.ts +24 -0
- package/components/chip-group/chip-group.js +196 -0
- package/components/chip-group/chip-group.module.css +38 -0
- package/components/code-block/code-block.d.ts +11 -0
- package/components/code-block/code-block.js +133 -0
- package/components/code-block/code-block.module.css +43 -0
- package/components/color-picker/color-picker.d.ts +40 -0
- package/components/color-picker/color-picker.js +449 -0
- package/components/color-picker/color-picker.module.css +111 -0
- package/components/combobox/combobox.d.ts +19 -0
- package/components/combobox/combobox.js +167 -0
- package/components/combobox/combobox.module.css +28 -0
- package/components/command-palette/command-palette.d.ts +20 -0
- package/components/command-palette/command-palette.js +151 -0
- package/components/command-palette/command-palette.module.css +94 -0
- package/components/comment-thread/comment-thread.d.ts +120 -0
- package/components/comment-thread/comment-thread.js +343 -0
- package/components/comment-thread/comment-thread.module.css +195 -0
- package/components/confirm-morph/confirm-morph.d.ts +65 -0
- package/components/confirm-morph/confirm-morph.js +355 -0
- package/components/confirm-morph/confirm-morph.module.css +146 -0
- package/components/context-menu/context-menu.d.ts +18 -0
- package/components/context-menu/context-menu.js +148 -0
- package/components/context-menu/context-menu.module.css +17 -0
- package/components/copy-button/copy-button.d.ts +10 -0
- package/components/copy-button/copy-button.js +69 -0
- package/components/copy-button/copy-button.module.css +21 -0
- package/components/countdown/countdown.d.ts +31 -0
- package/components/countdown/countdown.js +177 -0
- package/components/countdown/countdown.module.css +49 -0
- package/components/date-picker/date-picker.d.ts +17 -0
- package/components/date-picker/date-picker.js +100 -0
- package/components/date-picker/date-picker.module.css +34 -0
- package/components/date-range-picker/date-range-picker.d.ts +44 -0
- package/components/date-range-picker/date-range-picker.js +534 -0
- package/components/date-range-picker/date-range-picker.module.css +214 -0
- package/components/dialog/dialog.d.ts +11 -0
- package/components/dialog/dialog.js +52 -0
- package/components/dialog/dialog.module.css +36 -0
- package/components/donut-chart/donut-chart.d.ts +46 -0
- package/components/donut-chart/donut-chart.js +382 -0
- package/components/donut-chart/donut-chart.module.css +48 -0
- package/components/drawer/drawer.d.ts +20 -0
- package/components/drawer/drawer.js +111 -0
- package/components/drawer/drawer.module.css +97 -0
- package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
- package/components/dropdown-menu/dropdown-menu.js +39 -0
- package/components/dropdown-menu/dropdown-menu.module.css +38 -0
- package/components/elastic-slider/elastic-slider.d.ts +39 -0
- package/components/elastic-slider/elastic-slider.js +261 -0
- package/components/elastic-slider/elastic-slider.module.css +40 -0
- package/components/empty-state/empty-state.d.ts +12 -0
- package/components/empty-state/empty-state.js +69 -0
- package/components/empty-state/empty-state.module.css +13 -0
- package/components/expandable-card/expandable-card.d.ts +13 -0
- package/components/expandable-card/expandable-card.js +89 -0
- package/components/expandable-card/expandable-card.module.css +22 -0
- package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
- package/components/expanding-button-group/expanding-button-group.js +258 -0
- package/components/expanding-button-group/expanding-button-group.module.css +44 -0
- package/components/expanding-search/expanding-search.d.ts +38 -0
- package/components/expanding-search/expanding-search.js +242 -0
- package/components/expanding-search/expanding-search.module.css +52 -0
- package/components/file-dropzone/file-dropzone.d.ts +44 -0
- package/components/file-dropzone/file-dropzone.js +379 -0
- package/components/file-dropzone/file-dropzone.module.css +115 -0
- package/components/file-upload/file-upload.d.ts +22 -0
- package/components/file-upload/file-upload.js +192 -0
- package/components/file-upload/file-upload.module.css +49 -0
- package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
- package/components/filter-toolbar/filter-toolbar.js +446 -0
- package/components/filter-toolbar/filter-toolbar.module.css +70 -0
- package/components/floating-button-group/floating-button-group.d.ts +51 -0
- package/components/floating-button-group/floating-button-group.js +215 -0
- package/components/floating-button-group/floating-button-group.module.css +74 -0
- package/components/gauge/gauge.d.ts +18 -0
- package/components/gauge/gauge.js +101 -0
- package/components/gauge/gauge.module.css +22 -0
- package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
- package/components/hold-to-confirm/hold-to-confirm.js +165 -0
- package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
- package/components/hover-card/hover-card.d.ts +38 -0
- package/components/hover-card/hover-card.js +145 -0
- package/components/hover-card/hover-card.module.css +20 -0
- package/components/image-compare/image-compare.d.ts +27 -0
- package/components/image-compare/image-compare.js +267 -0
- package/components/image-compare/image-compare.module.css +25 -0
- package/components/in-view-title/in-view-title.d.ts +17 -0
- package/components/in-view-title/in-view-title.js +70 -0
- package/components/in-view-title/in-view-title.module.css +13 -0
- package/components/inline-edit/inline-edit.d.ts +27 -0
- package/components/inline-edit/inline-edit.js +315 -0
- package/components/inline-edit/inline-edit.module.css +73 -0
- package/components/input/input.d.ts +7 -0
- package/components/input/input.js +60 -0
- package/components/input/input.module.css +22 -0
- package/components/json-viewer/json-viewer.d.ts +48 -0
- package/components/json-viewer/json-viewer.js +463 -0
- package/components/json-viewer/json-viewer.module.css +92 -0
- package/components/line-chart/line-chart.d.ts +58 -0
- package/components/line-chart/line-chart.js +383 -0
- package/components/line-chart/line-chart.module.css +60 -0
- package/components/mention-input/mention-input.d.ts +72 -0
- package/components/mention-input/mention-input.js +417 -0
- package/components/mention-input/mention-input.module.css +134 -0
- package/components/metric-card/metric-card.d.ts +8 -0
- package/components/metric-card/metric-card.js +44 -0
- package/components/metric-card/metric-card.module.css +18 -0
- package/components/money-input/money-input.d.ts +48 -0
- package/components/money-input/money-input.js +503 -0
- package/components/money-input/money-input.module.css +77 -0
- package/components/morph-select/morph-select.d.ts +50 -0
- package/components/morph-select/morph-select.js +395 -0
- package/components/morph-select/morph-select.module.css +88 -0
- package/components/multi-select/multi-select.d.ts +18 -0
- package/components/multi-select/multi-select.js +105 -0
- package/components/multi-select/multi-select.module.css +37 -0
- package/components/notification-center/notification-center.d.ts +24 -0
- package/components/notification-center/notification-center.js +146 -0
- package/components/notification-center/notification-center.module.css +79 -0
- package/components/number-field/number-field.d.ts +41 -0
- package/components/number-field/number-field.js +455 -0
- package/components/number-field/number-field.module.css +71 -0
- package/components/otp-input/otp-input.d.ts +13 -0
- package/components/otp-input/otp-input.js +129 -0
- package/components/otp-input/otp-input.module.css +28 -0
- package/components/pagination/pagination.d.ts +7 -0
- package/components/pagination/pagination.js +60 -0
- package/components/pagination/pagination.module.css +16 -0
- package/components/password-field/password-field.d.ts +14 -0
- package/components/password-field/password-field.js +51 -0
- package/components/password-field/password-field.module.css +27 -0
- package/components/password-strength/password-strength.d.ts +107 -0
- package/components/password-strength/password-strength.js +161 -0
- package/components/password-strength/password-strength.module.css +57 -0
- package/components/phone-input/phone-input.d.ts +73 -0
- package/components/phone-input/phone-input.js +612 -0
- package/components/phone-input/phone-input.module.css +98 -0
- package/components/popover/popover.d.ts +6 -0
- package/components/popover/popover.js +19 -0
- package/components/popover/popover.module.css +32 -0
- package/components/progress/progress.d.ts +8 -0
- package/components/progress/progress.js +42 -0
- package/components/progress/progress.module.css +1 -0
- package/components/radio-cards/radio-cards.d.ts +35 -0
- package/components/radio-cards/radio-cards.js +110 -0
- package/components/radio-cards/radio-cards.module.css +84 -0
- package/components/radio-group/radio-group.d.ts +13 -0
- package/components/radio-group/radio-group.js +55 -0
- package/components/radio-group/radio-group.module.css +30 -0
- package/components/resizable-panels/resizable-panels.d.ts +37 -0
- package/components/resizable-panels/resizable-panels.js +338 -0
- package/components/resizable-panels/resizable-panels.module.css +40 -0
- package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
- package/components/rich-text-editor/rich-text-editor.js +1733 -0
- package/components/rich-text-editor/rich-text-editor.module.css +201 -0
- package/components/ridgeline/ridgeline.d.ts +37 -0
- package/components/ridgeline/ridgeline.js +346 -0
- package/components/ridgeline/ridgeline.module.css +58 -0
- package/components/scroll-area/scroll-area.d.ts +40 -0
- package/components/scroll-area/scroll-area.js +153 -0
- package/components/scroll-area/scroll-area.module.css +79 -0
- package/components/search-field/search-field.d.ts +7 -0
- package/components/search-field/search-field.js +20 -0
- package/components/search-field/search-field.module.css +17 -0
- package/components/segmented-control/segmented-control.d.ts +16 -0
- package/components/segmented-control/segmented-control.js +56 -0
- package/components/segmented-control/segmented-control.module.css +15 -0
- package/components/select/select.d.ts +15 -0
- package/components/select/select.js +36 -0
- package/components/select/select.module.css +35 -0
- package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
- package/components/shortcut-recorder/shortcut-recorder.js +380 -0
- package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
- package/components/signature-pad/signature-pad.d.ts +72 -0
- package/components/signature-pad/signature-pad.js +450 -0
- package/components/signature-pad/signature-pad.module.css +84 -0
- package/components/skeleton/skeleton.d.ts +13 -0
- package/components/skeleton/skeleton.js +53 -0
- package/components/skeleton/skeleton.module.css +11 -0
- package/components/slider/slider.d.ts +45 -0
- package/components/slider/slider.js +347 -0
- package/components/slider/slider.module.css +45 -0
- package/components/slope-chart/slope-chart.d.ts +38 -0
- package/components/slope-chart/slope-chart.js +183 -0
- package/components/slope-chart/slope-chart.module.css +51 -0
- package/components/slot-text/slot-text.d.ts +26 -0
- package/components/slot-text/slot-text.js +119 -0
- package/components/slot-text/slot-text.module.css +19 -0
- package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
- package/components/sortable-data-table/sortable-data-table.js +240 -0
- package/components/sortable-data-table/sortable-data-table.module.css +116 -0
- package/components/sparkline/sparkline.d.ts +19 -0
- package/components/sparkline/sparkline.js +239 -0
- package/components/sparkline/sparkline.module.css +26 -0
- package/components/split-button/split-button.d.ts +19 -0
- package/components/split-button/split-button.js +33 -0
- package/components/split-button/split-button.module.css +51 -0
- package/components/stepper/stepper.d.ts +36 -0
- package/components/stepper/stepper.js +108 -0
- package/components/stepper/stepper.module.css +74 -0
- package/components/streamgraph/streamgraph.d.ts +51 -0
- package/components/streamgraph/streamgraph.js +376 -0
- package/components/streamgraph/streamgraph.module.css +55 -0
- package/components/swipe-actions/swipe-actions.d.ts +35 -0
- package/components/swipe-actions/swipe-actions.js +330 -0
- package/components/swipe-actions/swipe-actions.module.css +64 -0
- package/components/switch/switch.d.ts +7 -0
- package/components/switch/switch.js +45 -0
- package/components/switch/switch.module.css +12 -0
- package/components/tabs/tabs.d.ts +8 -0
- package/components/tabs/tabs.js +138 -0
- package/components/tabs/tabs.module.css +27 -0
- package/components/tag-input/tag-input.d.ts +15 -0
- package/components/tag-input/tag-input.js +150 -0
- package/components/tag-input/tag-input.module.css +34 -0
- package/components/text-morph/text-morph.d.ts +13 -0
- package/components/text-morph/text-morph.js +69 -0
- package/components/text-morph/text-morph.module.css +8 -0
- package/components/text-reveal/text-reveal.d.ts +14 -0
- package/components/text-reveal/text-reveal.js +20 -0
- package/components/text-reveal/text-reveal.module.css +13 -0
- package/components/text-shimmer/text-shimmer.d.ts +18 -0
- package/components/text-shimmer/text-shimmer.js +90 -0
- package/components/text-shimmer/text-shimmer.module.css +9 -0
- package/components/textarea/textarea.d.ts +7 -0
- package/components/textarea/textarea.js +56 -0
- package/components/textarea/textarea.module.css +22 -0
- package/components/theme-switch/theme-switch.d.ts +11 -0
- package/components/theme-switch/theme-switch.js +37 -0
- package/components/theme-switch/theme-switch.module.css +31 -0
- package/components/time-picker/time-picker.d.ts +13 -0
- package/components/time-picker/time-picker.js +95 -0
- package/components/time-picker/time-picker.module.css +23 -0
- package/components/timeline/timeline.d.ts +47 -0
- package/components/timeline/timeline.js +171 -0
- package/components/timeline/timeline.module.css +51 -0
- package/components/toast/toast.d.ts +8 -0
- package/components/toast/toast.js +118 -0
- package/components/toast/toast.module.css +100 -0
- package/components/toast-stack/toast-stack.d.ts +57 -0
- package/components/toast-stack/toast-stack.js +399 -0
- package/components/toast-stack/toast-stack.module.css +61 -0
- package/components/tooltip/tooltip.d.ts +8 -0
- package/components/tooltip/tooltip.js +62 -0
- package/components/tooltip/tooltip.module.css +18 -0
- package/components/tree-view/tree-view.d.ts +17 -0
- package/components/tree-view/tree-view.js +147 -0
- package/components/tree-view/tree-view.module.css +22 -0
- package/components/treemap/treemap.d.ts +35 -0
- package/components/treemap/treemap.js +375 -0
- package/components/treemap/treemap.module.css +59 -0
- package/components/usage-meter/usage-meter.d.ts +24 -0
- package/components/usage-meter/usage-meter.js +296 -0
- package/components/usage-meter/usage-meter.module.css +61 -0
- package/components/user-menu/user-menu.d.ts +62 -0
- package/components/user-menu/user-menu.js +316 -0
- package/components/user-menu/user-menu.module.css +97 -0
- package/components/waffle-chart/waffle-chart.d.ts +39 -0
- package/components/waffle-chart/waffle-chart.js +244 -0
- package/components/waffle-chart/waffle-chart.module.css +52 -0
- package/components.json +1330 -0
- package/eslint.js +53 -0
- package/lib/escape.d.ts +19 -0
- package/lib/escape.js +50 -0
- package/lib/media.d.ts +353 -0
- package/lib/media.js +65 -0
- package/lib/menu-highlight.d.ts +28 -0
- package/lib/menu-highlight.js +43 -0
- package/lib/motion-tokens.d.ts +60 -0
- package/lib/motion-tokens.js +25 -0
- package/lib/return-focus.d.ts +9 -0
- package/lib/return-focus.js +53 -0
- package/lib/silicon-provider.d.ts +60 -0
- package/lib/silicon-provider.js +47 -0
- package/lib/use-copy-feedback.d.ts +8 -0
- package/lib/use-copy-feedback.js +36 -0
- package/lib/use-today.d.ts +2 -0
- package/lib/use-today.js +29 -0
- package/package.json +76 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useInView, useMotionValue, useReducedMotion, useSpring } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import styles from "./treemap.module.css";
|
|
7
|
+
const { spring, ease, blur, duration } = motionTokens;
|
|
8
|
+
const physical = ({ visualDuration, bounce }, restDelta = .0005) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
|
|
9
|
+
const zoom = physical({ visualDuration: .62, bounce: .06 });
|
|
10
|
+
const reveal = physical({ visualDuration: .9, bounce: 0 });
|
|
11
|
+
const glide = physical(spring.snappy, .01);
|
|
12
|
+
const follow = { stiffness: glide.stiffness, damping: glide.damping, restDelta: .01 };
|
|
13
|
+
/** Top level branches take the four series hues in order; a fifth and beyond stay neutral rather than reuse a hue. */
|
|
14
|
+
const hueOf = (index) => index < 0 ? "var(--series-1)" : index < 4 ? `var(--series-${index + 1})` : "var(--foreground)";
|
|
15
|
+
const GAP = 2, PAD = 3, HEADER = 22, TIP = 14, STEPS = 5;
|
|
16
|
+
const grouped = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
|
|
17
|
+
const percent = new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 1 });
|
|
18
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
19
|
+
const mix = (a, b, t) => a + (b - a) * t;
|
|
20
|
+
const inset = (r, top, side) => ({ x: r.x + side, y: r.y + top, w: Math.max(0, r.w - side * 2), h: Math.max(0, r.h - top - side) });
|
|
21
|
+
function flatten(root) {
|
|
22
|
+
const flat = new Map();
|
|
23
|
+
const visit = (node, depth, parent, path) => {
|
|
24
|
+
const kids = node.children ?? [];
|
|
25
|
+
let value = 0, weighted = 0, colored = 0;
|
|
26
|
+
if (kids.length)
|
|
27
|
+
for (const kid of kids) {
|
|
28
|
+
const r = visit(kid, depth + 1, node.id, depth === 0 ? [] : [...path, node.label]);
|
|
29
|
+
value += r.value;
|
|
30
|
+
weighted += r.weighted;
|
|
31
|
+
colored += r.colored;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
value = Math.max(0, node.value ?? 0);
|
|
35
|
+
if (node.color !== undefined && Number.isFinite(node.color)) {
|
|
36
|
+
weighted = node.color * value;
|
|
37
|
+
colored = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
flat.set(node.id, { id: node.id, label: node.label, value, color: colored ? weighted / colored : null, depth, parent, children: kids.map(kid => kid.id), path: depth === 0 ? [node.label] : [...path, node.label] });
|
|
41
|
+
return { value, weighted, colored };
|
|
42
|
+
};
|
|
43
|
+
visit(root, 0, null, []);
|
|
44
|
+
return flat;
|
|
45
|
+
}
|
|
46
|
+
/** Squarified layout: rows of tiles along the short side, each row closed when adding a tile would make the worst aspect ratio worse. */
|
|
47
|
+
function squarify(items, rect, out) {
|
|
48
|
+
const list = items.filter(item => item.value > 0).sort((a, b) => b.value - a.value);
|
|
49
|
+
const total = list.reduce((sum, item) => sum + item.value, 0);
|
|
50
|
+
if (!total || rect.w <= 0 || rect.h <= 0) {
|
|
51
|
+
for (const item of items)
|
|
52
|
+
out.set(item.id, { x: rect.x, y: rect.y, w: 0, h: 0 });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const scale = (rect.w * rect.h) / total;
|
|
56
|
+
let { x, y, w, h } = rect, i = 0;
|
|
57
|
+
const worst = (row, side) => { const sum = row.reduce((a, b) => a + b, 0), max = Math.max(...row), min = Math.min(...row); return Math.max((side * side * max) / (sum * sum), (sum * sum) / (side * side * min)); };
|
|
58
|
+
while (i < list.length) {
|
|
59
|
+
const side = Math.min(w, h), row = [list[i].value * scale];
|
|
60
|
+
let j = i + 1;
|
|
61
|
+
while (j < list.length) {
|
|
62
|
+
const next = [...row, list[j].value * scale];
|
|
63
|
+
if (worst(next, side) > worst(row, side))
|
|
64
|
+
break;
|
|
65
|
+
row.push(list[j].value * scale);
|
|
66
|
+
j++;
|
|
67
|
+
}
|
|
68
|
+
const sum = row.reduce((a, b) => a + b, 0);
|
|
69
|
+
if (w >= h) {
|
|
70
|
+
const cw = sum / h;
|
|
71
|
+
let cy = y;
|
|
72
|
+
for (let k = i; k < j; k++) {
|
|
73
|
+
const th = (list[k].value * scale) / cw;
|
|
74
|
+
out.set(list[k].id, { x, y: cy, w: cw, h: th });
|
|
75
|
+
cy += th;
|
|
76
|
+
}
|
|
77
|
+
x += cw;
|
|
78
|
+
w -= cw;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const rh = sum / w;
|
|
82
|
+
let cx = x;
|
|
83
|
+
for (let k = i; k < j; k++) {
|
|
84
|
+
const tw = (list[k].value * scale) / rh;
|
|
85
|
+
out.set(list[k].id, { x: cx, y, w: tw, h: rh });
|
|
86
|
+
cx += tw;
|
|
87
|
+
}
|
|
88
|
+
y += rh;
|
|
89
|
+
h -= rh;
|
|
90
|
+
}
|
|
91
|
+
i = j;
|
|
92
|
+
}
|
|
93
|
+
for (const item of items)
|
|
94
|
+
if (!out.has(item.id))
|
|
95
|
+
out.set(item.id, { x: rect.x, y: rect.y, w: 0, h: 0 });
|
|
96
|
+
}
|
|
97
|
+
/** Rects of every descendant when `focus` fills the view: its children fill the view, and each branch holds its own children under a header. */
|
|
98
|
+
function layoutFrom(flat, focus, view) {
|
|
99
|
+
const out = new Map();
|
|
100
|
+
const nest = (id, rect, top) => {
|
|
101
|
+
const node = flat.get(id);
|
|
102
|
+
if (!node.children.length)
|
|
103
|
+
return;
|
|
104
|
+
const room = top ? rect : inset(rect, rect.h > 48 && rect.w > 64 ? HEADER : PAD, PAD);
|
|
105
|
+
const placed = new Map();
|
|
106
|
+
squarify(node.children.map(child => ({ id: child, value: flat.get(child).value })), room, placed);
|
|
107
|
+
for (const [child, r] of placed) {
|
|
108
|
+
const gapped = { x: r.x + GAP / 2, y: r.y + GAP / 2, w: Math.max(0, r.w - GAP), h: Math.max(0, r.h - GAP) };
|
|
109
|
+
out.set(child, gapped);
|
|
110
|
+
nest(child, gapped, false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
nest(focus, view, true);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/** The affine map that carries rect `from` onto rect `to`, applied to `r`. */
|
|
117
|
+
const carry = (r, from, to) => { const sx = to.w / (from.w || 1), sy = to.h / (from.h || 1); return { x: to.x + (r.x - from.x) * sx, y: to.y + (r.y - from.y) * sy, w: r.w * sx, h: r.h * sy }; };
|
|
118
|
+
const subscribeNothing = () => () => { };
|
|
119
|
+
function useReducedMotionSafe() {
|
|
120
|
+
const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
|
|
121
|
+
return !!useReducedMotion() && hydrated;
|
|
122
|
+
}
|
|
123
|
+
export function Treemap({ data, label, formatValue = value => grouped.format(value), colorLabel, formatColor = value => grouped.format(value), colorDomain, focus, defaultFocus, onFocusChange, height = 420, emptyLabel = "No data yet", ref, className }) {
|
|
124
|
+
const reduced = useReducedMotionSafe();
|
|
125
|
+
const figure = useRef(null);
|
|
126
|
+
const stage = useRef(null);
|
|
127
|
+
const tip = useRef(null);
|
|
128
|
+
const tiles = useRef(new Map());
|
|
129
|
+
useImperativeHandle(ref, () => figure.current);
|
|
130
|
+
const inView = useInView(figure, { once: true, amount: .25 });
|
|
131
|
+
const signature = JSON.stringify(data);
|
|
132
|
+
const flat = useMemo(() => flatten(data), [signature]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
133
|
+
const root = flat.get(data.id);
|
|
134
|
+
const nodes = useMemo(() => [...flat.values()].filter(node => node.depth > 0), [flat]);
|
|
135
|
+
const [ownFocus, setOwnFocus] = useState(defaultFocus ?? data.id);
|
|
136
|
+
const wanted = flat.get(focus ?? ownFocus);
|
|
137
|
+
const center = wanted && wanted.children.length ? wanted : root;
|
|
138
|
+
const zoomTo = (id) => { if (focus === undefined)
|
|
139
|
+
setOwnFocus(id); onFocusChange?.(id); };
|
|
140
|
+
const [active, setActive] = useState(null);
|
|
141
|
+
const [pointer, setPointer] = useState(null);
|
|
142
|
+
const [width, setWidth] = useState(0);
|
|
143
|
+
useLayoutEffect(() => {
|
|
144
|
+
const node = stage.current;
|
|
145
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
146
|
+
return;
|
|
147
|
+
const observer = new ResizeObserver(() => setWidth(node.clientWidth));
|
|
148
|
+
observer.observe(node);
|
|
149
|
+
setWidth(node.clientWidth);
|
|
150
|
+
return () => observer.disconnect();
|
|
151
|
+
}, []);
|
|
152
|
+
const view = useMemo(() => ({ x: 0, y: 0, w: width, h: height }), [width, height]);
|
|
153
|
+
const target = useMemo(() => layoutFrom(flat, center.id, view), [flat, center.id, view]);
|
|
154
|
+
const levelOf = (node) => node.depth - center.depth;
|
|
155
|
+
// Color scale: a few neutral steps from light to dark, so shade reads as more without adding a hue.
|
|
156
|
+
const leaves = nodes.filter(node => !node.children.length && node.color !== null);
|
|
157
|
+
const [lo, hi] = colorDomain ?? (leaves.length ? [Math.min(...leaves.map(node => node.color)), Math.max(...leaves.map(node => node.color))] : [0, 1]);
|
|
158
|
+
const colored = colorLabel !== undefined && leaves.length > 0;
|
|
159
|
+
const shade = (value) => { if (!colored || value === null)
|
|
160
|
+
return 34; const t = clamp((value - lo) / (hi - lo || 1), 0, 1); return Math.round(14 + Math.round(t * (STEPS - 1)) / (STEPS - 1) * 44); };
|
|
161
|
+
const branchOf = useMemo(() => { const map = new Map(); root.children.forEach((id, index) => { const walk = (at) => { map.set(at, index); flat.get(at)?.children.forEach(walk); }; walk(id); }); return map; }, [flat, root]);
|
|
162
|
+
// Every tile keeps its on screen rect; a spring carries it to the new layout. Tiles entering or leaving ride the same zoom as the tile they belong to.
|
|
163
|
+
const drawn = useRef(new Map());
|
|
164
|
+
const levels = useRef(new Map());
|
|
165
|
+
const lastFocus = useRef(null);
|
|
166
|
+
const paint = () => {
|
|
167
|
+
for (const node of nodes) {
|
|
168
|
+
const el = tiles.current.get(node.id), d = drawn.current.get(node.id);
|
|
169
|
+
if (!el)
|
|
170
|
+
continue;
|
|
171
|
+
if (!d || d.o < .01 || d.w < .5 || d.h < .5) {
|
|
172
|
+
el.style.visibility = "hidden";
|
|
173
|
+
el.style.pointerEvents = "none";
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
el.style.visibility = "visible";
|
|
177
|
+
el.style.transform = `translate3d(${d.x.toFixed(2)}px, ${d.y.toFixed(2)}px, 0)`;
|
|
178
|
+
el.style.width = `${d.w.toFixed(2)}px`;
|
|
179
|
+
el.style.height = `${d.h.toFixed(2)}px`;
|
|
180
|
+
el.style.opacity = d.o.toFixed(3);
|
|
181
|
+
el.style.pointerEvents = d.o > .6 ? "auto" : "none";
|
|
182
|
+
const level = levels.current.get(node.id) ?? 0;
|
|
183
|
+
el.dataset.level = String(level);
|
|
184
|
+
el.dataset.room = d.w > 64 && d.h > 38 ? (d.h > 54 ? "full" : "name") : "none";
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const paintRef = useRef(paint);
|
|
188
|
+
useLayoutEffect(() => { paintRef.current = paint; });
|
|
189
|
+
const shown = (inView || reduced) && width > 0;
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!shown)
|
|
192
|
+
return;
|
|
193
|
+
const prev = lastFocus.current;
|
|
194
|
+
const isUnder = (id, ancestor) => { for (let at = flat.get(id); at; at = at.parent ? flat.get(at.parent) : undefined)
|
|
195
|
+
if (at.id === ancestor)
|
|
196
|
+
return true; return false; };
|
|
197
|
+
const visible = (node) => { const l = node.depth - center.depth; return l >= 1 && l <= 2 && isUnder(node.id, center.id); };
|
|
198
|
+
const nextVisible = new Set(nodes.filter(visible).map(node => node.id));
|
|
199
|
+
const before = new Map(drawn.current);
|
|
200
|
+
const wasVisible = new Set([...before].filter(([, d]) => d.o > .5).map(([id]) => id));
|
|
201
|
+
// The pivot: the tile that grows to fill the view, or the view that shrinks back into its tile.
|
|
202
|
+
let toOld = null, toNew = null;
|
|
203
|
+
if (prev && prev.id !== center.id && before.size) {
|
|
204
|
+
if (isUnder(center.id, prev.id)) {
|
|
205
|
+
const inOld = prev.layout.get(center.id);
|
|
206
|
+
if (inOld) {
|
|
207
|
+
toOld = r => carry(r, view, inOld);
|
|
208
|
+
toNew = r => carry(r, inOld, view);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (isUnder(prev.id, center.id)) {
|
|
212
|
+
const inNew = target.get(prev.id);
|
|
213
|
+
if (inNew) {
|
|
214
|
+
toOld = r => carry(r, inNew, view);
|
|
215
|
+
toNew = r => carry(r, view, inNew);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const from = new Map(), to = new Map();
|
|
220
|
+
const first = !before.size;
|
|
221
|
+
for (const node of nodes) {
|
|
222
|
+
const goal = target.get(node.id), now = before.get(node.id);
|
|
223
|
+
const inNext = nextVisible.has(node.id), inPrev = wasVisible.has(node.id);
|
|
224
|
+
if (inNext && goal) {
|
|
225
|
+
to.set(node.id, { ...goal, o: 1 });
|
|
226
|
+
if (inPrev && now)
|
|
227
|
+
from.set(node.id, now);
|
|
228
|
+
else if (first)
|
|
229
|
+
from.set(node.id, { x: goal.x + goal.w / 2, y: goal.y + goal.h / 2, w: 0, h: 0, o: 0 });
|
|
230
|
+
else
|
|
231
|
+
from.set(node.id, { ...(toOld ? toOld(goal) : goal), o: 0 });
|
|
232
|
+
levels.current.set(node.id, levelOf(node));
|
|
233
|
+
}
|
|
234
|
+
else if (inPrev && now) {
|
|
235
|
+
from.set(node.id, now);
|
|
236
|
+
to.set(node.id, { ...(toNew ? toNew(now) : now), o: 0 });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const node of nodes)
|
|
240
|
+
if (!from.has(node.id))
|
|
241
|
+
drawn.current.set(node.id, { x: 0, y: 0, w: 0, h: 0, o: 0 });
|
|
242
|
+
lastFocus.current = { id: center.id, layout: target };
|
|
243
|
+
const finish = () => { for (const [id, d] of to)
|
|
244
|
+
drawn.current.set(id, d); paintRef.current(); };
|
|
245
|
+
if (reduced) {
|
|
246
|
+
finish();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const controls = animate(0, 1, { ...(first ? reveal : zoom), onUpdate: t => {
|
|
250
|
+
for (const [id, a] of from) {
|
|
251
|
+
const b = to.get(id);
|
|
252
|
+
// On the first reveal, tiles grow in a quick wave from the largest.
|
|
253
|
+
const local = first ? clamp((t - Math.min(.3, (1 - b.w * b.h / (view.w * view.h || 1)) * .3)) / .7, 0, 1) : t;
|
|
254
|
+
drawn.current.set(id, { x: mix(a.x, b.x, local), y: mix(a.y, b.y, local), w: mix(a.w, b.w, local), h: mix(a.h, b.h, local), o: mix(a.o, b.o, local) });
|
|
255
|
+
}
|
|
256
|
+
paintRef.current();
|
|
257
|
+
}, onComplete: finish });
|
|
258
|
+
return () => controls.stop();
|
|
259
|
+
}, [shown, target, reduced]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
260
|
+
useLayoutEffect(() => { paintRef.current(); });
|
|
261
|
+
const activeNode = active ? flat.get(active) ?? null : null;
|
|
262
|
+
const parentOf = (node) => node.parent ? flat.get(node.parent) : node;
|
|
263
|
+
const share = (node, of) => of.value ? node.value / of.value : 0;
|
|
264
|
+
// Tooltip: trails the pointer, or sits by the tile in focus, and stays inside the chart.
|
|
265
|
+
const tipX = useMotionValue(0), tipY = useMotionValue(0);
|
|
266
|
+
const tipSpringX = useSpring(tipX, follow), tipSpringY = useSpring(tipY, follow);
|
|
267
|
+
const wasOn = useRef(false);
|
|
268
|
+
useLayoutEffect(() => {
|
|
269
|
+
const bubble = tip.current;
|
|
270
|
+
if (!bubble || !activeNode || !width) {
|
|
271
|
+
wasOn.current = false;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const r = target.get(activeNode.id);
|
|
275
|
+
const ax = pointer?.x ?? (r ? r.x + r.w / 2 : 0), ay = pointer?.y ?? (r ? r.y + Math.min(r.h / 2, 28) : 0);
|
|
276
|
+
const tw = bubble.offsetWidth, th = bubble.offsetHeight;
|
|
277
|
+
let left = ax + TIP;
|
|
278
|
+
if (left + tw > width)
|
|
279
|
+
left = ax - TIP - tw;
|
|
280
|
+
let top = ay + TIP;
|
|
281
|
+
if (top + th > height)
|
|
282
|
+
top = ay - TIP - th;
|
|
283
|
+
tipX.set(clamp(left, 0, Math.max(0, width - tw)));
|
|
284
|
+
tipY.set(clamp(top, 0, Math.max(0, height - th)));
|
|
285
|
+
if (!wasOn.current || reduced) {
|
|
286
|
+
tipSpringX.jump(tipX.get());
|
|
287
|
+
tipSpringY.jump(tipY.get());
|
|
288
|
+
}
|
|
289
|
+
wasOn.current = true;
|
|
290
|
+
});
|
|
291
|
+
// Keyboard: arrows hop to the nearest tile in that direction on the top level, Enter zooms in, Escape zooms out.
|
|
292
|
+
const topTiles = center.children.map(id => flat.get(id)).filter(node => node.value > 0);
|
|
293
|
+
const topOf = (node) => { let at = node; while (at && at.parent && at.parent !== center.id)
|
|
294
|
+
at = flat.get(at.parent) ?? null; return at && at.parent === center.id ? at : null; };
|
|
295
|
+
const onKeyDown = (event) => {
|
|
296
|
+
const current = topOf(activeNode);
|
|
297
|
+
const dirs = { ArrowRight: [1, 0], ArrowLeft: [-1, 0], ArrowDown: [0, 1], ArrowUp: [0, -1] };
|
|
298
|
+
if (event.key in dirs) {
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
setPointer(null);
|
|
301
|
+
if (!current) {
|
|
302
|
+
setActive(topTiles[0]?.id ?? null);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const [dx, dy] = dirs[event.key], o = target.get(current.id), ox = o.x + o.w / 2, oy = o.y + o.h / 2;
|
|
306
|
+
let best = null, score = Infinity;
|
|
307
|
+
for (const node of topTiles) {
|
|
308
|
+
if (node.id === current.id)
|
|
309
|
+
continue;
|
|
310
|
+
const r = target.get(node.id);
|
|
311
|
+
if (!r)
|
|
312
|
+
continue;
|
|
313
|
+
const vx = r.x + r.w / 2 - ox, vy = r.y + r.h / 2 - oy, along = vx * dx + vy * dy, across = Math.abs(vx * dy - vy * dx);
|
|
314
|
+
if (along <= 1)
|
|
315
|
+
continue;
|
|
316
|
+
const s = along + across * 1.5;
|
|
317
|
+
if (s < score) {
|
|
318
|
+
score = s;
|
|
319
|
+
best = node;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (best)
|
|
323
|
+
setActive(best.id);
|
|
324
|
+
}
|
|
325
|
+
else if (event.key === "Home" || event.key === "End") {
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
setPointer(null);
|
|
328
|
+
setActive((event.key === "Home" ? topTiles[0] : topTiles[topTiles.length - 1])?.id ?? null);
|
|
329
|
+
}
|
|
330
|
+
else if (event.key === "Enter" || event.key === " ") {
|
|
331
|
+
if (current?.children.length) {
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
zoomTo(current.id);
|
|
334
|
+
setActive(current.children[0]);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else if (event.key === "Escape" || event.key === "Backspace") {
|
|
338
|
+
if (center.parent) {
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
zoomTo(center.parent);
|
|
341
|
+
setActive(center.id);
|
|
342
|
+
}
|
|
343
|
+
else if (active) {
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
setActive(null);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const onPointerMove = (event, id) => {
|
|
350
|
+
if (event.pointerType !== "mouse")
|
|
351
|
+
return;
|
|
352
|
+
const rect = stage.current?.getBoundingClientRect();
|
|
353
|
+
if (rect)
|
|
354
|
+
setPointer({ x: event.clientX - rect.left, y: event.clientY - rect.top });
|
|
355
|
+
setActive(id);
|
|
356
|
+
};
|
|
357
|
+
const onTileClick = (node) => { const top = topOf(node); if (top?.children.length)
|
|
358
|
+
zoomTo(top.id);
|
|
359
|
+
else if (!activeNode || activeNode.id !== node.id)
|
|
360
|
+
setActive(node.id); };
|
|
361
|
+
const trail = [];
|
|
362
|
+
for (let at = center; at; at = at.parent ? flat.get(at.parent) : undefined)
|
|
363
|
+
trail.unshift(at);
|
|
364
|
+
const empty = !root.value;
|
|
365
|
+
const describe = (node) => `${node.path.join(", ")}, ${formatValue(node.value)}, ${percent.format(share(node, parentOf(node)))} of ${parentOf(node).label}${colored && node.color !== null ? `, ${colorLabel} ${formatColor(node.color)}` : ""}`;
|
|
366
|
+
const focusTop = topOf(activeNode);
|
|
367
|
+
return _jsxs("figure", { ref: figure, className: [styles.figure, className].filter(Boolean).join(" "), "aria-label": label, children: [_jsxs("div", { className: styles.bar, children: [_jsx("nav", { className: styles.crumbs, "aria-label": `${label} path`, children: _jsx("ol", { className: styles.crumbList, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: trail.map((node, index) => _jsxs(motion.li, { className: styles.crumb, layout: reduced ? false : "position", initial: { opacity: 0, x: reduced ? 0 : -6, filter: reduced ? "none" : `blur(${blur.subtle}px)` }, animate: { opacity: 1, x: 0, filter: "blur(0px)" }, exit: { opacity: 0, x: reduced ? 0 : -6, transition: { duration: duration.instant, ease: [...ease.exit] } }, transition: { ...spring.smooth }, children: [index > 0 && _jsx("span", { className: styles.crumbSep, "aria-hidden": "true", children: "/" }), index < trail.length - 1 ? _jsx("button", { type: "button", className: styles.crumbButton, onClick: () => { zoomTo(node.id); setActive(null); }, children: node.label }) : _jsx("span", { className: styles.crumbCurrent, "aria-current": "location", children: node.label })] }, node.id)) }) }) }), _jsx("p", { className: styles.total, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.totalValue, initial: { opacity: 0, y: reduced ? 0 : 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: reduced ? 0 : -6 }, transition: { duration: duration.standard, ease: [...ease.enter] }, children: formatValue(center.value) }, formatValue(center.value)) }) })] }), _jsxs("div", { ref: stage, className: styles.stage, style: { height }, "data-active": activeNode ? true : undefined, role: "group", tabIndex: empty ? -1 : 0, "aria-roledescription": "treemap", "aria-label": `${label}, ${center.label}. Arrow keys move between tiles, Enter zooms in, Escape zooms out.`, onKeyDown: onKeyDown, onBlur: () => setActive(null), onPointerLeave: () => { setActive(null); setPointer(null); }, children: [nodes.map(node => {
|
|
368
|
+
const top = topOf(node);
|
|
369
|
+
return _jsxs("div", { ref: element => { if (element)
|
|
370
|
+
tiles.current.set(node.id, element);
|
|
371
|
+
else
|
|
372
|
+
tiles.current.delete(node.id); }, className: styles.tile, "data-branch": node.children.length > 0 || undefined, "data-active": active === node.id || undefined, "data-lineage": focusTop?.id === node.id && active !== node.id || undefined, "data-dim": focusTop !== null && top !== null && focusTop.id !== top.id || undefined, style: { "--hue": hueOf(branchOf.get(node.id) ?? -1), "--fill": `color-mix(in oklab, ${hueOf(branchOf.get(node.id) ?? -1)} ${shade(node.color)}%, var(--surface))`, zIndex: node.depth }, onPointerMove: event => { event.stopPropagation(); onPointerMove(event, node.id); }, onClick: event => { event.stopPropagation(); onTileClick(node); }, children: [_jsx("span", { className: styles.tileLabel, children: node.label }), _jsx("span", { className: styles.tileValue, children: formatValue(node.value) })] }, node.id);
|
|
373
|
+
}), empty && _jsx("p", { className: styles.message, children: emptyLabel }), _jsx(motion.div, { ref: tip, className: styles.tooltip, style: { x: reduced ? tipX : tipSpringX, y: reduced ? tipY : tipSpringY }, "aria-hidden": "true", children: activeNode && _jsxs(_Fragment, { children: [_jsx("p", { className: styles.tipTitle, children: activeNode.path.slice(0, -1).join(" / ") || root.label }), _jsxs("p", { className: styles.tipValue, children: [_jsx("span", { className: styles.tipName, children: activeNode.label }), _jsx("span", { className: styles.tipAmount, children: formatValue(activeNode.value) })] }), _jsxs("p", { className: styles.tipNote, children: [percent.format(share(activeNode, parentOf(activeNode))), " of ", parentOf(activeNode).label] }), colored && activeNode.color !== null && _jsxs("p", { className: styles.tipNote, children: [colorLabel, " ", formatColor(activeNode.color)] })] }) })] }), colored && _jsxs("div", { className: styles.scale, "aria-hidden": "true", children: [_jsx("span", { className: styles.scaleLabel, children: colorLabel }), _jsx("span", { className: styles.scaleEnd, children: formatColor(lo) }), _jsx("span", { className: styles.steps, children: Array.from({ length: STEPS }, (_, i) => _jsx("span", { className: styles.step, style: { "--fill": `color-mix(in oklab, ${center.depth === 0 ? "var(--foreground)" : hueOf(branchOf.get(center.id) ?? -1)} ${Math.round(14 + (i / (STEPS - 1)) * 44)}%, var(--surface))` } }, i)) }), _jsx("span", { className: styles.scaleEnd, children: formatColor(hi) })] }), _jsx("p", { className: styles.srOnly, "aria-live": "polite", "aria-atomic": "true", children: activeNode ? describe(activeNode) : `${center.label}, ${formatValue(center.value)}` }), !empty && _jsx("div", { className: styles.srOnly, children: _jsxs("table", { children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Path" }), _jsx("th", { scope: "col", children: "Value" }), _jsx("th", { scope: "col", children: "Share of parent" }), colored && _jsx("th", { scope: "col", children: colorLabel })] }) }), _jsx("tbody", { children: nodes.map(node => _jsxs("tr", { children: [_jsx("th", { scope: "row", children: node.path.join(" / ") }), _jsx("td", { children: formatValue(node.value) }), _jsx("td", { children: percent.format(share(node, parentOf(node))) }), colored && _jsx("td", { children: node.color === null ? "" : formatColor(node.color) })] }, node.id)) })] }) })] });
|
|
374
|
+
}
|
|
375
|
+
export default Treemap;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
.figure { display: grid; min-width: 0; gap: var(--space-group); margin: 0; color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
2
|
+
|
|
3
|
+
/* Series colors (--series-1 to --series-4) come from registry/foundation.css. */
|
|
4
|
+
|
|
5
|
+
/* Breadcrumb on the left, the total of what fills the view on the right. */
|
|
6
|
+
.bar { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: var(--space-group); }
|
|
7
|
+
/* Pull the trail back by the crumb padding, so the first label lines up with the tiles and the title. */
|
|
8
|
+
.crumbs { min-width: 0; margin-left: -4px; }
|
|
9
|
+
.crumbList { position: relative; display: flex; min-width: 0; min-height: 32px; flex-wrap: wrap; align-items: center; margin: 0; padding: 0; list-style: none; }
|
|
10
|
+
.crumb { display: inline-flex; align-items: center; min-width: 0; }
|
|
11
|
+
.crumbSep { padding: 0 4px; color: var(--border-strong); font-size: var(--text-sm); }
|
|
12
|
+
.crumbButton { min-height: 32px; padding: 0 6px; margin: 0 -2px; border: 0; border-radius: var(--radius-detail); background: transparent; color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); white-space: nowrap; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
|
|
13
|
+
.crumbButton:active { transform: scale(.97); }
|
|
14
|
+
.crumbCurrent { padding: 0 4px; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; }
|
|
15
|
+
@media (hover: hover) and (pointer: fine) { .crumbButton:hover { background: color-mix(in oklab, var(--foreground) 5%, transparent); color: var(--foreground); } }
|
|
16
|
+
.total { position: relative; display: inline-grid; flex: none; justify-items: end; overflow: hidden; margin: 0; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
|
|
17
|
+
.totalValue { display: block; }
|
|
18
|
+
|
|
19
|
+
/* Tiles are absolutely placed and sized each frame, so text never scales while a tile grows or shrinks. */
|
|
20
|
+
.stage { position: relative; min-width: 0; overflow: hidden; border-radius: var(--radius-control); outline: none; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
|
|
21
|
+
.tile { position: absolute; top: 0; left: 0; display: flex; flex-direction: column; gap: 1px; overflow: hidden; padding: 6px 8px; border-radius: var(--radius-detail); background: var(--fill); color: var(--foreground); cursor: pointer; visibility: hidden; will-change: transform, width, height; transition: background-color var(--duration-considered) var(--ease-standard), color var(--duration-considered) var(--ease-standard), filter var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
|
|
22
|
+
.tile[data-branch][data-level="1"] { padding: 3px 8px; border-radius: var(--radius-detail); background: color-mix(in oklab, var(--hue) 9%, var(--surface)); color: var(--foreground); cursor: zoom-in; }
|
|
23
|
+
.tileLabel, .tileValue { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); line-height: 16px; opacity: 0; transition: opacity var(--duration-fast) var(--ease-standard); }
|
|
24
|
+
.tileLabel { font-weight: 500; }
|
|
25
|
+
.tileValue { opacity: 0; font-variant-numeric: tabular-nums; }
|
|
26
|
+
.tile[data-room="name"] .tileLabel, .tile[data-room="full"] .tileLabel, .tile[data-room="full"] .tileValue { opacity: 1; }
|
|
27
|
+
.tile[data-room="full"] .tileValue { opacity: .72; }
|
|
28
|
+
/* A branch on the top level prints its name and total on one header line. */
|
|
29
|
+
.tile[data-branch][data-level="1"] { flex-direction: row; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
30
|
+
.tile[data-branch][data-level="1"] .tileValue { flex: none; opacity: .6; }
|
|
31
|
+
.tile[data-branch][data-level="1"][data-room="none"] .tileLabel, .tile[data-branch][data-level="1"][data-room="none"] .tileValue { opacity: 0; }
|
|
32
|
+
.tile[data-branch][data-level="1"][data-room="name"] .tileValue { opacity: .6; }
|
|
33
|
+
.tile[data-level="2"] { padding: 5px 7px; }
|
|
34
|
+
|
|
35
|
+
.tile[data-active] { box-shadow: inset 0 0 0 2px var(--foreground); }
|
|
36
|
+
.tile[data-lineage] { box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--hue) 70%, transparent); }
|
|
37
|
+
.stage[data-active] .tile[data-dim] { filter: opacity(.5); }
|
|
38
|
+
|
|
39
|
+
.message { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; color: var(--text-muted); font-size: var(--text-sm); pointer-events: none; }
|
|
40
|
+
|
|
41
|
+
.tooltip { position: absolute; top: 0; left: 0; z-index: 50; display: grid; min-width: 160px; max-width: min(240px, 100%); grid-template-columns: minmax(0, 1fr); gap: 2px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface-raised); box-shadow: var(--shadow-floating); opacity: 0; pointer-events: none; scale: .96; transition: opacity var(--duration-fast) var(--ease-standard), scale var(--duration-fast) var(--ease-standard); }
|
|
42
|
+
.stage[data-active] .tooltip { opacity: 1; scale: 1; }
|
|
43
|
+
.tipTitle { margin: 0; overflow: hidden; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
|
|
44
|
+
.tipValue { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0; font-size: var(--text-sm); line-height: 1.35; }
|
|
45
|
+
.tipName { min-width: 0; overflow: hidden; color: var(--foreground); text-overflow: ellipsis; white-space: nowrap; }
|
|
46
|
+
.tipAmount { flex: none; color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
47
|
+
.tipNote { margin: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
|
|
48
|
+
|
|
49
|
+
/* The color scale: stepped swatches from light to dark, never a gradient. */
|
|
50
|
+
.scale { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
|
|
51
|
+
.scaleLabel { margin-right: 4px; color: var(--text-secondary); }
|
|
52
|
+
.steps { display: inline-flex; gap: 2px; }
|
|
53
|
+
.step { width: 18px; height: 10px; border-radius: var(--radius-detail); background: var(--fill); }
|
|
54
|
+
|
|
55
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
56
|
+
|
|
57
|
+
@media (prefers-reduced-motion: reduce) {
|
|
58
|
+
.tile, .tileLabel, .tileValue, .tooltip, .crumbButton { transition: none; }
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface UsageMeterSegment {
|
|
2
|
+
/** Stable identity. Keep it the same while the value changes, so the segment re-flows instead of being replaced. */
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
value: number;
|
|
6
|
+
}
|
|
7
|
+
/** Use a usage meter for a fixed allowance, such as storage or seats, when people need to see what takes the space and how close they are to the limit. */
|
|
8
|
+
export interface UsageMeterProps {
|
|
9
|
+
/** What is measured, such as "Workspace storage". */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Used amounts in display order. Up to four read clearly. */
|
|
12
|
+
segments: UsageMeterSegment[];
|
|
13
|
+
/** The plan limit, in the same unit as the segments. */
|
|
14
|
+
limit: number;
|
|
15
|
+
unit?: string;
|
|
16
|
+
/** Digits after the decimal point. */
|
|
17
|
+
decimals?: number;
|
|
18
|
+
freeLabel?: string;
|
|
19
|
+
overLabel?: string;
|
|
20
|
+
/** Share of the limit at which the meter warns that it is almost full. */
|
|
21
|
+
warnAt?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function UsageMeter({ label, segments, limit, unit, decimals, freeLabel, overLabel, warnAt }: UsageMeterProps): import("react").JSX.Element;
|
|
24
|
+
export default UsageMeter;
|