@ceebee/ui 1.9.0 → 1.10.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.
@@ -1,6 +1,7 @@
1
1
  # Third-party notices
2
2
 
3
- `@ceebee/ui` includes software from these MIT-licensed projects:
3
+ `@ceebee/ui` includes software from the projects below. Most are MIT-licensed; the one Apache-2.0
4
+ dependency names its licence in its own section.
4
5
 
5
6
  ## Ant Design
6
7
 
@@ -34,3 +35,20 @@ Copyright © 2018-present iamkun
34
35
 
35
36
  Date and time components use `dayjs` 1.11.18 as their date engine. It is licensed under the MIT
36
37
  License.
38
+
39
+ ## @dnd-kit
40
+
41
+ Copyright © 2021, Claudéric Demers
42
+
43
+ `Board` in `@ceebee/ui/client` uses `@dnd-kit/core` 6.3.1, `@dnd-kit/sortable` 10.0.0 and
44
+ `@dnd-kit/utilities` 3.2.2 as its drag-and-drop runtime. They are licensed under the MIT License.
45
+
46
+ ## Lightweight Charts™
47
+
48
+ Copyright © 2023 TradingView, Inc.
49
+
50
+ `ProgressCurve` in `@ceebee/ui/client` draws its canvas with `lightweight-charts` 5.2.1, a
51
+ dependency loaded on demand. It is licensed under the **Apache License, Version 2.0**; a copy is in
52
+ the package at `node_modules/lightweight-charts/LICENSE`, and the attribution is retained here
53
+ rather than painted into a consumer's chart, where the library's `attributionLogo` option is turned
54
+ off.
package/dist/client.d.ts CHANGED
@@ -493,6 +493,336 @@ declare const Board: typeof BoardRoot & {
493
493
  Skeleton: typeof BoardSkeleton;
494
494
  };
495
495
 
496
+ /**
497
+ * What a dated series means, with no chart and no DOM in sight.
498
+ *
499
+ * Kept pure for the usual reason and one extra: these charts are drawn on a **canvas**, so nothing
500
+ * about them is inspectable from the outside. These functions are the only testable description of
501
+ * what the picture claims, and they are also what feeds the text alternative a screen reader reads
502
+ * and what a forced-colors viewer gets instead of the bitmap.
503
+ */
504
+ /** One reading: a day, and a value on that day. */
505
+ interface SeriesPoint {
506
+ /** `YYYY-MM-DD`. A calendar day, not an instant. */
507
+ day: string;
508
+ value: number;
509
+ }
510
+ /**
511
+ * `YYYY-MM-DD`, or null when it is not a day at all.
512
+ *
513
+ * The pattern is not the check: `2026-02-30` matches it and is not a day. The round-trip through `Date`
514
+ * settles that — but **an impossible month makes an Invalid Date, and `toISOString()` throws on one**
515
+ * rather than returning anything falsy. So the time value is tested before it is formatted; otherwise
516
+ * `2026-13-01` takes the whole chart down instead of being ignored.
517
+ */
518
+ declare function asDay(value: string): string | null;
519
+ /**
520
+ * The points as the chart substrate demands them: ascending, and one per day.
521
+ *
522
+ * This is not tidying. The library **throws** on a repeated or out-of-order time rather than ignoring
523
+ * it, so a source that reports the same day twice — a correction, two crews reporting the same site —
524
+ * would take the whole screen down. The later value wins, because a correction is the point of sending
525
+ * the day again.
526
+ */
527
+ declare function seriesPoints(points: readonly SeriesPoint[]): SeriesPoint[];
528
+ /** The value a series holds on a day: its last reading at or before it, or null before it starts. */
529
+ declare function valueOn(points: readonly SeriesPoint[], day: string): number | null;
530
+ /**
531
+ * Every day any series reports, with each series' value on it — the accessible rendering, not a caption.
532
+ *
533
+ * A canvas has no DOM, so this is the *only* thing a screen reader can be given, and it is what a
534
+ * forced-colors viewer sees in place of the bitmap. It is therefore the same data, not a summary. A
535
+ * value is carried forward from the last reading: a day nobody reported is not a day of nothing, and
536
+ * showing it as zero would say the measure collapsed.
537
+ */
538
+ declare function alignRows(series: readonly {
539
+ key: string;
540
+ points: readonly SeriesPoint[];
541
+ }[]): {
542
+ day: string;
543
+ values: Record<string, number | null>;
544
+ }[];
545
+ /** The span the axis has to cover, or null when there is nothing to draw. */
546
+ declare function seriesSpan(series: readonly {
547
+ points: readonly SeriesPoint[];
548
+ }[]): {
549
+ from: string;
550
+ to: string;
551
+ } | null;
552
+ /**
553
+ * The reported day closest to a given one, or null when nothing was reported.
554
+ *
555
+ * A marker attaches to a data point — the substrate has no vertical line of its own — so anything a
556
+ * chart wants to mark has to land on a day some series actually reports.
557
+ */
558
+ declare function nearestDay(days: readonly string[], day: string): string | null;
559
+ /** One decimal, because a figure read to four is false precision on a number somebody estimated. */
560
+ declare function round1(value: number): number;
561
+
562
+ /** The colours a canvas is drawn in, resolved from Tokens rather than written down. */
563
+ interface ChartPalette {
564
+ text: string;
565
+ muted: string;
566
+ grid: string;
567
+ background: string;
568
+ font: string;
569
+ /** One entry per series, in the order the series were given. */
570
+ series: string[];
571
+ /** Where a baseline series sits above its base, and below it. */
572
+ above: string;
573
+ below: string;
574
+ }
575
+ /** How thick, and solid or dashed. A plan reads as a reference; delivery reads as the fact. */
576
+ type SeriesEmphasis = 'reference' | 'primary';
577
+ interface SeriesSpec {
578
+ /** Stable key, used in the accessible table and to look a value up. */
579
+ key: string;
580
+ /** What a reader calls it. */
581
+ label: string;
582
+ points: SeriesPoint[];
583
+ emphasis?: SeriesEmphasis;
584
+ /** The Token this series takes its colour from, e.g. `--cb-tone-brand`. */
585
+ colorToken: string;
586
+ }
587
+ /**
588
+ * A fixed vertical range, when autoscale would lie.
589
+ *
590
+ * A percentage chart pins 0–100: autoscale stretches a job at 30% to the full height of the plot,
591
+ * which reads as *nearly there* — the opposite of the truth. A money chart normally does not pin,
592
+ * because there is no natural ceiling.
593
+ */
594
+ interface ValueRange {
595
+ min: number;
596
+ max: number;
597
+ }
598
+ /**
599
+ * A baseline: the value the series is read against, coloured differently above and below.
600
+ *
601
+ * Zero on a cash projection is the obvious one — the day the balance crosses it is the whole point of
602
+ * the chart — and a minimum buffer is the useful one, because a business that must keep 50 juta on hand
603
+ * is in trouble well before it reaches zero.
604
+ */
605
+ interface Baseline {
606
+ value: number;
607
+ label?: string;
608
+ }
609
+ interface TimeSeriesChartProps {
610
+ /** Accessible name. It is the chart's title as a screen reader announces it. */
611
+ label: string;
612
+ series: SeriesSpec[];
613
+ /** Turns a value into the text on the price scale and in the table. */
614
+ format: (value: number) => string;
615
+ range?: ValueRange;
616
+ /** Draws one series against a base value, shaded above and below. Only the first series is drawn. */
617
+ baseline?: Baseline;
618
+ /** A day to mark, and what to call it. The mark lands on the nearest day any series reports. */
619
+ mark?: {
620
+ day: string;
621
+ label: string;
622
+ };
623
+ height?: number;
624
+ emptyLabel?: string;
625
+ tableLabel?: string;
626
+ dayLabel?: string;
627
+ loading?: boolean;
628
+ className?: string;
629
+ }
630
+ interface TimeSeriesChartSkeletonProps {
631
+ height?: number;
632
+ label?: string;
633
+ className?: string;
634
+ }
635
+
636
+ /**
637
+ * A dated chart: any number of series over calendar days, or one series read against a baseline.
638
+ *
639
+ * It is drawn on a canvas, and that has one consequence which shapes this whole file: **the picture is
640
+ * not in the DOM.** A screen reader finds nothing, a forced-colors viewer gets a bitmap the system
641
+ * cannot recolour, and no test can look at it. So the table below the plot is not a caption or a
642
+ * fallback — it is the chart's other rendering, always present, built from the same numbers, and it is
643
+ * what a forced-colors viewer sees *instead of* the canvas.
644
+ *
645
+ * This is the primitive the library's dated Compositions are built from. It carries no domain meaning:
646
+ * it does not know what a plan is or what money is, only how to draw dated values honestly and how to
647
+ * say the same thing in words.
648
+ */
649
+ declare function TimeSeriesChart({ label, series, format, range, baseline, mark, height, emptyLabel, tableLabel, dayLabel, loading, className, }: TimeSeriesChartProps): react.JSX.Element;
650
+
651
+ /** Holds the chart's geometry while the readings load, so the page does not jump when they land. */
652
+ declare function TimeSeriesChartSkeleton({ height, label, className, }: TimeSeriesChartSkeletonProps): react.JSX.Element;
653
+
654
+ interface BalanceCurveProps {
655
+ /** Accessible name. It is the chart's title as a screen reader announces it. */
656
+ label: string;
657
+ /** The running balance, by day. One point per day the balance is known for. */
658
+ balances: SeriesPoint[];
659
+ /**
660
+ * How a value is written. The library holds no currency: a product passes its own formatter, so the
661
+ * same chart serves rupiah, a percentage of a budget, or litres of diesel.
662
+ */
663
+ format: (value: number) => string;
664
+ /**
665
+ * The line the balance is read against, and what to call it. Defaults to zero.
666
+ *
667
+ * Not always zero, and that is the point: a business that must keep a minimum on hand is in trouble
668
+ * well before it reaches nothing.
669
+ */
670
+ threshold?: {
671
+ value: number;
672
+ label?: string;
673
+ };
674
+ height?: number;
675
+ seriesLabel?: string;
676
+ emptyLabel?: string;
677
+ tableLabel?: string;
678
+ /** How the reading is worded. Each takes the formatted figures the component computed. */
679
+ belowLabel?: (from: string, lowest: string, days: number) => string;
680
+ clearLabel?: (lowest: string) => string;
681
+ loading?: boolean;
682
+ className?: string;
683
+ }
684
+ interface BalanceCurveSkeletonProps {
685
+ height?: number;
686
+ label?: string;
687
+ className?: string;
688
+ }
689
+
690
+ /** Holds the chart's geometry while the balances load, so the page does not jump when they land. */
691
+ declare function BalanceCurveSkeleton({ height, label, className, }: BalanceCurveSkeletonProps): react.JSX.Element;
692
+
693
+ /**
694
+ * A running balance over days, read against a line: a cash projection, a budget remaining, a tank.
695
+ *
696
+ * The substrate draws this as a **baseline** series rather than a line, which is not decoration: it
697
+ * shades above and below the threshold in the semantic colours, so the stretch that is in trouble is
698
+ * coloured by its own values instead of by a tag somebody has to read. Seven red tags in a row say
699
+ * nothing about how far down the dip goes; a shaded area says it at a glance.
700
+ *
701
+ * The sentence above the plot is the other half. A chart shows the shape; the sentence gives the two
702
+ * figures a decision needs — the day it goes under and the lowest it gets — so nobody has to read a
703
+ * position off an axis to act on it.
704
+ */
705
+ declare function BalanceCurveRoot({ label, balances, format, threshold, height, seriesLabel, emptyLabel, tableLabel, belowLabel, clearLabel, loading, className, }: BalanceCurveProps): react.JSX.Element;
706
+ /** The Composition and its Skeleton, so a loading page keeps the chart's geometry. */
707
+ declare const BalanceCurve: typeof BalanceCurveRoot & {
708
+ Skeleton: typeof BalanceCurveSkeleton;
709
+ };
710
+
711
+ /**
712
+ * What a running balance says about itself.
713
+ *
714
+ * A balance chart exists to answer two questions a list of days cannot: **when does this dip below the
715
+ * line, and how far down does it go.** Both are computed here rather than left to the reader, because
716
+ * the reader's alternative is scanning thirty rows — which is exactly the thing the chart replaces.
717
+ */
718
+
719
+ interface BalanceReading {
720
+ /** The lowest balance over the window, and the day it happens. Null when there is nothing to read. */
721
+ lowest: number | null;
722
+ lowestDay: string | null;
723
+ /** The first day the balance is below the threshold, or null when it never is. */
724
+ firstBelowDay: string | null;
725
+ /** How many days sit below the threshold. Zero is the answer somebody is hoping for. */
726
+ daysBelow: number;
727
+ /** The balance on the last day of the window. */
728
+ closing: number | null;
729
+ }
730
+ /**
731
+ * The reading, against a threshold.
732
+ *
733
+ * The threshold is not always zero, and that is the point of it being a parameter: a business that must
734
+ * keep fifty million on hand is in trouble well before it reaches nothing, and a chart drawn against
735
+ * zero would tell it everything is fine right up to the day it is not.
736
+ *
737
+ * "Below" is strict. A balance sitting exactly on the threshold has met it, and reporting that as a
738
+ * breach would cry wolf on the one day the business did precisely what it planned to.
739
+ */
740
+ declare function balanceReading(points: readonly SeriesPoint[], threshold: number): BalanceReading;
741
+
742
+ /** One reading: a day, and the share of the work complete by then. */
743
+ interface CurvePoint {
744
+ /** `YYYY-MM-DD`. A calendar day, not an instant — progress is reported for a day's work. */
745
+ day: string;
746
+ /** 0–100. */
747
+ percent: number;
748
+ }
749
+ interface CurveReading {
750
+ plannedPercent: number | null;
751
+ actualPercent: number | null;
752
+ /** Actual minus planned, in points. Negative is behind. Null when either side is unknown. */
753
+ gap: number | null;
754
+ }
755
+ /** The library's generic point shape, from this Block's `percent`-named one. */
756
+ declare function toPoints(points: readonly CurvePoint[]): SeriesPoint[];
757
+ /**
758
+ * The reading on a given day: what was planned, what was reached, and the gap.
759
+ *
760
+ * Both sides carry the last report forward — a day nobody reported is not a day of no progress, and
761
+ * drawing it as zero would say the work went backwards. A plan states its shape at the points it
762
+ * states them, so it is read the same way.
763
+ */
764
+ declare function readingOn(planned: readonly CurvePoint[], actual: readonly CurvePoint[], day: string): CurveReading;
765
+ /** Every day either series reports, with both values and the gap — what a screen reader is given. */
766
+ declare function curveRows(planned: readonly CurvePoint[], actual: readonly CurvePoint[]): {
767
+ day: string;
768
+ plannedPercent: number | null;
769
+ actualPercent: number | null;
770
+ gap: number | null;
771
+ }[];
772
+
773
+ interface ProgressCurveProps {
774
+ /** What the plan said would be complete, by day. */
775
+ planned: CurvePoint[];
776
+ /** What was actually reported complete, by day. */
777
+ actual: CurvePoint[];
778
+ /** Accessible name. It is the chart's title as a screen reader announces it, so name the job. */
779
+ label: string;
780
+ /**
781
+ * `YYYY-MM-DD`. The day the reading above the plot is taken on — both series carry their last value
782
+ * at or before it forward. Without it the reading is taken on the last day anything was reported.
783
+ *
784
+ * It is deliberately not drawn on the plot: a mark attaches to a data point, and today is usually not
785
+ * one. What is marked is the last report (see `lastReportLabel`).
786
+ */
787
+ today?: string;
788
+ height?: number;
789
+ plannedLabel?: string;
790
+ actualLabel?: string;
791
+ /** Text on the mark sitting at the last reported day. */
792
+ lastReportLabel?: string;
793
+ /** Shown in place of the chart when neither series has a single usable day. */
794
+ emptyLabel?: string;
795
+ /** The table a screen reader reads, and what a forced-colors viewer sees instead of the canvas. */
796
+ tableLabel?: string;
797
+ aheadLabel?: string;
798
+ behindLabel?: string;
799
+ onTrackLabel?: string;
800
+ loading?: boolean;
801
+ className?: string;
802
+ }
803
+ interface ProgressCurveSkeletonProps {
804
+ height?: number;
805
+ label?: string;
806
+ className?: string;
807
+ }
808
+
809
+ /** Holds the chart's geometry while the readings load, so the page does not jump when they land. */
810
+ declare function ProgressCurveSkeleton({ height, label, className, }: ProgressCurveSkeletonProps): react.JSX.Element;
811
+
812
+ /**
813
+ * An S-curve: what the plan said would be done by each day, against what was.
814
+ *
815
+ * The canvas, the Token reading, the forced-colors path and the accessible table all belong to
816
+ * `TimeSeriesChart`. What is here is the part that is about *progress*: a scale pinned to 0–100%,
817
+ * because autoscale stretches a job at 30% to the full height of the plot and reads as "nearly
818
+ * there"; and the reading in words above it, which is the one number somebody opened this to find.
819
+ */
820
+ declare function ProgressCurveRoot({ planned, actual, label, today, height, plannedLabel, actualLabel, lastReportLabel, emptyLabel, tableLabel, aheadLabel, behindLabel, onTrackLabel, loading, className, }: ProgressCurveProps): react.JSX.Element;
821
+ /** The Composition and its Skeleton, so a loading page keeps the chart's geometry. */
822
+ declare const ProgressCurve: typeof ProgressCurveRoot & {
823
+ Skeleton: typeof ProgressCurveSkeleton;
824
+ };
825
+
496
826
  interface DiagramSkeletonProps {
497
827
  label?: string;
498
828
  }
@@ -583,4 +913,4 @@ declare const DiagramEditor: typeof DiagramEditorRoot & {
583
913
  Skeleton: typeof DiagramSkeleton;
584
914
  };
585
915
 
586
- export { Board, type BoardCard, type BoardColumn, type BoardLabels, type BoardMove, type BoardMoveResult, type BoardPosition, type BoardProps, type BoardShape, type BoardSkeletonProps, type CeebeeAntStyleCache, CeebeeAntStyleProvider, type CeebeeAntStyleProviderProps, Checklist, type ChecklistProps, type ChecklistTask, type Command, CommandPalette, type CommandPaletteProps, DEFAULT_LABELS, Diagram, type DiagramEdge, DiagramEditor, type DiagramEditorProps, type DiagramNode, type DiagramPosition, type DiagramProps, type DiagramRemoval, type DiagramRenameTarget, type DiagramShape, type DiagramSkeletonProps, type DurationToken, type Labels, LabelsProvider, type LabelsProviderProps, Modal, type ModalProps, type MotionHelpers, MotionProvider, type MotionProviderProps, type MotionSettings, type NavItem, type NavSection, type PaletteCommand, PanZoomCanvas, type PanZoomCanvasProps, type PanZoomCanvasSkeletonProps, type RankedCommand, Reveal, type RevealProps, Sidebar, type SidebarProps, type SpringPreset, Stagger, type StaggerProps, StickerGroup, type StickerGroupProps, type StickerGroupSkeletonProps, type StickerItem, ThemeBridge, type ThemeBridgeProps, type ThemeChoice, ThemeProvider, type ThemeProviderProps, type ToastOptions, type ToastPosition, ToastProvider, type ToastProviderProps, TopBar, type TopBarProps, createCeebeeAntStyleCache, extractCeebeeAntStyles, filterCommands, groupCommands, rankCommand, useLabels, useMotionSettings, useTheme, useToast };
916
+ export { BalanceCurve, type BalanceCurveProps, BalanceCurveSkeleton, type BalanceCurveSkeletonProps, type BalanceReading, type Baseline, Board, type BoardCard, type BoardColumn, type BoardLabels, type BoardMove, type BoardMoveResult, type BoardPosition, type BoardProps, type BoardShape, type BoardSkeletonProps, type CeebeeAntStyleCache, CeebeeAntStyleProvider, type CeebeeAntStyleProviderProps, type ChartPalette, Checklist, type ChecklistProps, type ChecklistTask, type Command, CommandPalette, type CommandPaletteProps, type CurvePoint, type CurveReading, DEFAULT_LABELS, Diagram, type DiagramEdge, DiagramEditor, type DiagramEditorProps, type DiagramNode, type DiagramPosition, type DiagramProps, type DiagramRemoval, type DiagramRenameTarget, type DiagramShape, type DiagramSkeletonProps, type DurationToken, type Labels, LabelsProvider, type LabelsProviderProps, Modal, type ModalProps, type MotionHelpers, MotionProvider, type MotionProviderProps, type MotionSettings, type NavItem, type NavSection, type PaletteCommand, PanZoomCanvas, type PanZoomCanvasProps, type PanZoomCanvasSkeletonProps, ProgressCurve, type ProgressCurveProps, ProgressCurveSkeleton, type ProgressCurveSkeletonProps, type RankedCommand, Reveal, type RevealProps, type SeriesEmphasis, type SeriesPoint, type SeriesSpec, Sidebar, type SidebarProps, type SpringPreset, Stagger, type StaggerProps, StickerGroup, type StickerGroupProps, type StickerGroupSkeletonProps, type StickerItem, ThemeBridge, type ThemeBridgeProps, type ThemeChoice, ThemeProvider, type ThemeProviderProps, TimeSeriesChart, type TimeSeriesChartProps, TimeSeriesChartSkeleton, type TimeSeriesChartSkeletonProps, type ToastOptions, type ToastPosition, ToastProvider, type ToastProviderProps, TopBar, type TopBarProps, type ValueRange, alignRows, asDay, balanceReading, createCeebeeAntStyleCache, curveRows, extractCeebeeAntStyles, filterCommands, groupCommands, nearestDay, rankCommand, readingOn, round1, seriesPoints, seriesSpan, toPoints, useLabels, useMotionSettings, useTheme, useToast, valueOn };
package/dist/client.js CHANGED
@@ -122,6 +122,66 @@ function ModalRoot({
122
122
  }
123
123
  var Modal = Object.assign(ModalRoot, Modal$1);
124
124
 
125
+ // src/lib/css-probe.ts
126
+ function createCssProbe(root) {
127
+ const probe = document.createElement("span");
128
+ probe.style.position = "fixed";
129
+ probe.style.pointerEvents = "none";
130
+ probe.style.visibility = "hidden";
131
+ root.append(probe);
132
+ return {
133
+ color: (name) => resolveCssColor(probe, name),
134
+ length: (name) => resolveCssLength(probe, name),
135
+ done: () => probe.remove()
136
+ };
137
+ }
138
+ function resolveCssLength(probe, name) {
139
+ probe.style.width = `var(${name})`;
140
+ const value = Number.parseFloat(getComputedStyle(probe).width);
141
+ probe.style.removeProperty("width");
142
+ return Number.isFinite(value) ? value : void 0;
143
+ }
144
+ function resolveCssColor(probe, name) {
145
+ probe.style.color = `var(${name})`;
146
+ const value = getComputedStyle(probe).color;
147
+ probe.style.removeProperty("color");
148
+ if (!value) return void 0;
149
+ if (value.startsWith("var(")) return void 0;
150
+ if (/^(?:#|rgb|hsl|hsv)/i.test(value)) return value;
151
+ const canvas = document.createElement("canvas");
152
+ canvas.width = 1;
153
+ canvas.height = 1;
154
+ const context = canvas.getContext("2d", { willReadFrequently: true });
155
+ if (!context) return void 0;
156
+ context.clearRect(0, 0, 1, 1);
157
+ context.fillStyle = value;
158
+ context.fillRect(0, 0, 1, 1);
159
+ const [red = 0, green = 0, blue = 0, alpha = 255] = context.getImageData(0, 0, 1, 1).data;
160
+ return `rgba(${red}, ${green}, ${blue}, ${alpha / 255})`;
161
+ }
162
+ var SKIN_LINK_ID = "cb-skin";
163
+ function watchTokens(onChange) {
164
+ const rootObserver = new MutationObserver(onChange);
165
+ rootObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
166
+ const headObserver = new MutationObserver((records) => {
167
+ const skinChanged = records.some((record) => [...record.addedNodes, ...record.removedNodes].some((node) => node instanceof HTMLElement && node.id === SKIN_LINK_ID));
168
+ if (skinChanged) onChange();
169
+ });
170
+ headObserver.observe(document.head, { childList: true });
171
+ const onSkinLoad = (event) => {
172
+ if (event.target instanceof HTMLElement && event.target.id === SKIN_LINK_ID) onChange();
173
+ };
174
+ document.addEventListener("load", onSkinLoad, true);
175
+ const dark = window.matchMedia("(prefers-color-scheme: dark)");
176
+ dark.addEventListener("change", onChange);
177
+ return () => {
178
+ rootObserver.disconnect();
179
+ headObserver.disconnect();
180
+ document.removeEventListener("load", onSkinLoad, true);
181
+ dark.removeEventListener("change", onChange);
182
+ };
183
+ }
184
+
125
185
  // src/theme/ant-theme-seeds.generated.ts
126
186
  var generatedCeebeeAntSeeds = {
127
187
  "ceebee": {
@@ -792,7 +852,6 @@ var THEME_MODE_COOKIE = "cb-theme-mode";
792
852
  function serializeThemeModeCookie(mode) {
793
853
  return `${THEME_MODE_COOKIE}=${mode}; Path=/; Max-Age=31536000; SameSite=Lax`;
794
854
  }
795
- var SKIN_LINK_ID = "cb-skin";
796
855
  function ThemeBridge({
797
856
  children,
798
857
  mode,
@@ -806,27 +865,12 @@ function ThemeBridge({
806
865
  }, []);
807
866
  useEffect(() => {
808
867
  refresh();
809
- const rootObserver = new MutationObserver(refresh);
810
- rootObserver.observe(document.documentElement, {
811
- attributes: true,
812
- attributeFilter: ["data-theme"]
813
- });
814
- const headObserver = new MutationObserver((records) => {
815
- const skinChanged = records.some((record) => [...record.addedNodes, ...record.removedNodes].some((node) => node instanceof HTMLElement && node.id === SKIN_LINK_ID));
816
- if (skinChanged) refresh();
817
- });
818
- headObserver.observe(document.head, { childList: true });
819
- const onSkinLoad = (event) => {
820
- if (event.target instanceof HTMLElement && event.target.id === SKIN_LINK_ID) refresh();
821
- };
822
- document.addEventListener("load", onSkinLoad, true);
868
+ const stopWatchingTokens = watchTokens(refresh);
823
869
  const coarsePointer = window.matchMedia("(pointer: coarse)");
824
870
  const onPointerChange = () => refresh();
825
871
  coarsePointer.addEventListener("change", onPointerChange);
826
872
  return () => {
827
- rootObserver.disconnect();
828
- headObserver.disconnect();
829
- document.removeEventListener("load", onSkinLoad, true);
873
+ stopWatchingTokens();
830
874
  coarsePointer.removeEventListener("change", onPointerChange);
831
875
  };
832
876
  }, [refresh]);
@@ -853,13 +897,9 @@ function mergeComponents(base, override) {
853
897
  return merged;
854
898
  }
855
899
  function readCeebeeThemeToken(root) {
856
- const probe = document.createElement("span");
857
- probe.style.position = "fixed";
858
- probe.style.pointerEvents = "none";
859
- probe.style.visibility = "hidden";
860
- root.append(probe);
861
- const color = (name) => resolveCssColor(probe, name);
862
- const length = (name) => resolveCssLength(probe, name);
900
+ const probe = createCssProbe(root);
901
+ const color = (name) => probe.color(name);
902
+ const length = (name) => probe.length(name);
863
903
  const tokens = {
864
904
  colorPrimary: color("--cb-tone-brand"),
865
905
  colorInfo: color("--cb-tone-info"),
@@ -919,7 +959,7 @@ function readCeebeeThemeToken(root) {
919
959
  dotActiveBorderColor: trackBg
920
960
  };
921
961
  }
922
- probe.remove();
962
+ probe.done();
923
963
  return {
924
964
  token: Object.fromEntries(
925
965
  Object.entries(tokens).filter(([, value]) => value !== void 0)
@@ -927,30 +967,6 @@ function readCeebeeThemeToken(root) {
927
967
  components
928
968
  };
929
969
  }
930
- function resolveCssLength(probe, name) {
931
- probe.style.width = `var(${name})`;
932
- const value = Number.parseFloat(getComputedStyle(probe).width);
933
- probe.style.removeProperty("width");
934
- return Number.isFinite(value) ? value : void 0;
935
- }
936
- function resolveCssColor(probe, name) {
937
- probe.style.color = `var(${name})`;
938
- const value = getComputedStyle(probe).color;
939
- probe.style.removeProperty("color");
940
- if (!value) return void 0;
941
- if (value.startsWith("var(")) return void 0;
942
- if (/^(?:#|rgb|hsl|hsv)/i.test(value)) return value;
943
- const canvas = document.createElement("canvas");
944
- canvas.width = 1;
945
- canvas.height = 1;
946
- const context = canvas.getContext("2d", { willReadFrequently: true });
947
- if (!context) return void 0;
948
- context.clearRect(0, 0, 1, 1);
949
- context.fillStyle = value;
950
- context.fillRect(0, 0, 1, 1);
951
- const [red = 0, green = 0, blue = 0, alpha = 255] = context.getImageData(0, 0, 1, 1).data;
952
- return `rgba(${red}, ${green}, ${blue}, ${alpha / 255})`;
953
- }
954
970
  function CeebeeAntStyleProvider({ cache, children }) {
955
971
  return /* @__PURE__ */ jsx(StyleProvider, { cache, children });
956
972
  }
@@ -2035,6 +2051,455 @@ function useNarrow(query) {
2035
2051
  return narrow;
2036
2052
  }
2037
2053
  var Board = Object.assign(BoardRoot, { Skeleton: BoardSkeleton });
2054
+
2055
+ // src/data/time-series/time-series.chart.ts
2056
+ async function mountTimeSeries(host, shape, palette) {
2057
+ const { BaselineSeries, ColorType, LineSeries, LineStyle, createChart, createSeriesMarkers } = await import('lightweight-charts');
2058
+ const chart = createChart(host, {
2059
+ autoSize: true,
2060
+ /* A dashboard chart is read, not traded. Panning and zooming would let somebody scroll the data
2061
+ off the screen and leave them looking at a fragment with no way back, so the whole span is
2062
+ fitted and the viewport is fixed. */
2063
+ handleScroll: false,
2064
+ handleScale: false
2065
+ });
2066
+ const scale = shape.range ? () => ({ priceRange: { minValue: shape.range?.min ?? 0, maxValue: shape.range?.max ?? 0 } }) : void 0;
2067
+ const common = { priceLineVisible: false, lastValueVisible: false, autoscaleInfoProvider: scale };
2068
+ const drawn = shape.baseline ? [
2069
+ chart.addSeries(BaselineSeries, {
2070
+ ...common,
2071
+ baseValue: { type: "price", price: shape.baseline.value },
2072
+ lineWidth: 2
2073
+ })
2074
+ ] : shape.series.map(
2075
+ (series) => chart.addSeries(LineSeries, {
2076
+ ...common,
2077
+ lineWidth: series.emphasis === "reference" ? 2 : 3,
2078
+ lineStyle: series.emphasis === "reference" ? LineStyle.Dashed : LineStyle.Solid
2079
+ })
2080
+ );
2081
+ const markedSeries = drawn.at(-1);
2082
+ const markers = markedSeries ? createSeriesMarkers(markedSeries, []) : null;
2083
+ let current = palette;
2084
+ const apply = (next) => {
2085
+ current = next;
2086
+ chart.applyOptions({
2087
+ layout: {
2088
+ background: { type: ColorType.Solid, color: next.background },
2089
+ textColor: next.text,
2090
+ fontFamily: next.font,
2091
+ /* Apache-2.0 asks that the NOTICE travel with the distribution, not that a logo be painted
2092
+ into a consumer's dashboard. The attribution lives in THIRD_PARTY_NOTICES.md. */
2093
+ attributionLogo: false
2094
+ },
2095
+ grid: { vertLines: { color: next.grid }, horzLines: { color: next.grid } },
2096
+ rightPriceScale: { borderColor: next.grid, scaleMargins: { top: 0.1, bottom: 0.08 } },
2097
+ timeScale: { borderColor: next.grid, fixLeftEdge: true, fixRightEdge: true },
2098
+ crosshair: { vertLine: { color: next.muted }, horzLine: { color: next.muted } },
2099
+ localization: { priceFormatter: shape.format }
2100
+ });
2101
+ drawn.forEach((series, index) => {
2102
+ if (shape.baseline) {
2103
+ series.applyOptions({
2104
+ topLineColor: next.above,
2105
+ topFillColor1: next.above,
2106
+ topFillColor2: next.background,
2107
+ bottomLineColor: next.below,
2108
+ bottomFillColor1: next.background,
2109
+ bottomFillColor2: next.below,
2110
+ crosshairMarkerBorderColor: next.background
2111
+ });
2112
+ return;
2113
+ }
2114
+ series.applyOptions({
2115
+ color: next.series[index] ?? next.text,
2116
+ crosshairMarkerBorderColor: next.background
2117
+ });
2118
+ });
2119
+ };
2120
+ apply(palette);
2121
+ return {
2122
+ setData(series) {
2123
+ drawn.forEach((drawnSeries, index) => {
2124
+ const points = series[index]?.points ?? [];
2125
+ drawnSeries.setData(points.map((point) => ({ time: point.day, value: point.value })));
2126
+ });
2127
+ chart.timeScale().fitContent();
2128
+ },
2129
+ mark(day, text) {
2130
+ markers?.setMarkers(day === null ? [] : [{
2131
+ time: day,
2132
+ position: "belowBar",
2133
+ shape: "arrowUp",
2134
+ color: current.series[current.series.length - 1] ?? current.text,
2135
+ text
2136
+ }]);
2137
+ },
2138
+ applyPalette: apply,
2139
+ destroy() {
2140
+ chart.remove();
2141
+ }
2142
+ };
2143
+ }
2144
+
2145
+ // src/data/time-series/time-series.math.ts
2146
+ function asDay(value) {
2147
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
2148
+ const at = (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).getTime();
2149
+ if (Number.isNaN(at)) return null;
2150
+ return new Date(at).toISOString().startsWith(value) ? value : null;
2151
+ }
2152
+ function seriesPoints(points) {
2153
+ const byDay = /* @__PURE__ */ new Map();
2154
+ for (const point of points) {
2155
+ if (asDay(point.day) === null) continue;
2156
+ byDay.set(point.day, point.value);
2157
+ }
2158
+ return [...byDay].map(([day, value]) => ({ day, value })).sort((a, b) => a.day.localeCompare(b.day));
2159
+ }
2160
+ function valueOn(points, day) {
2161
+ const reached = seriesPoints(points).filter((point) => point.day <= day);
2162
+ const last = reached[reached.length - 1];
2163
+ return last ? last.value : null;
2164
+ }
2165
+ function alignRows(series) {
2166
+ const days = [...new Set(series.flatMap((one) => seriesPoints(one.points).map((point) => point.day)))].sort();
2167
+ return days.map((day) => {
2168
+ const values = {};
2169
+ for (const one of series) values[one.key] = valueOn(one.points, day);
2170
+ return { day, values };
2171
+ });
2172
+ }
2173
+ function seriesSpan(series) {
2174
+ const days = series.flatMap((one) => seriesPoints(one.points).map((point) => point.day)).sort();
2175
+ const from = days[0];
2176
+ const to = days[days.length - 1];
2177
+ return from && to ? { from, to } : null;
2178
+ }
2179
+ function nearestDay(days, day) {
2180
+ const known = days.filter((candidate) => asDay(candidate) !== null);
2181
+ if (known.length === 0 || asDay(day) === null) return null;
2182
+ const distance2 = (candidate) => Math.abs((/* @__PURE__ */ new Date(`${candidate}T00:00:00Z`)).getTime() - (/* @__PURE__ */ new Date(`${day}T00:00:00Z`)).getTime());
2183
+ return known.reduce((closest, candidate) => distance2(candidate) < distance2(closest) ? candidate : closest);
2184
+ }
2185
+ function round1(value) {
2186
+ return Math.round(value * 10) / 10;
2187
+ }
2188
+ function TimeSeriesChartSkeleton({
2189
+ height = 260,
2190
+ label = "Loading chart",
2191
+ className
2192
+ }) {
2193
+ return /* @__PURE__ */ jsxs("div", { className: cn("cb-chart", "cb-chart--skeleton", className), role: "status", "aria-label": label, children: [
2194
+ /* @__PURE__ */ jsx("span", { className: "cb-chart__skeleton-head", "aria-hidden": "true" }),
2195
+ /* @__PURE__ */ jsx("span", { className: "cb-chart__skeleton-plot", style: { height }, "aria-hidden": "true" })
2196
+ ] });
2197
+ }
2198
+ function TimeSeriesChart({
2199
+ label,
2200
+ series,
2201
+ format,
2202
+ range,
2203
+ baseline,
2204
+ mark,
2205
+ height = 260,
2206
+ emptyLabel = "Nothing has been reported yet.",
2207
+ tableLabel = "Readings by day",
2208
+ dayLabel = "Day",
2209
+ loading = false,
2210
+ className
2211
+ }) {
2212
+ const host = useRef(null);
2213
+ const [forcedColors, setForcedColors] = useState(false);
2214
+ const cleaned = useMemo(
2215
+ () => series.map((one) => ({ ...one, points: seriesPoints(one.points) })),
2216
+ [series]
2217
+ );
2218
+ const rows = useMemo(() => alignRows(cleaned), [cleaned]);
2219
+ const empty = rows.length === 0;
2220
+ const markDay = useMemo(
2221
+ () => mark ? nearestDay(rows.map((row) => row.day), mark.day) : null,
2222
+ [rows, mark]
2223
+ );
2224
+ useEffect(() => {
2225
+ const query = window.matchMedia("(forced-colors: active)");
2226
+ const read = () => setForcedColors(query.matches);
2227
+ read();
2228
+ query.addEventListener("change", read);
2229
+ return () => query.removeEventListener("change", read);
2230
+ }, []);
2231
+ const tokens = useMemo(() => cleaned.map((one) => one.colorToken), [cleaned]);
2232
+ useEffect(() => {
2233
+ const element = host.current;
2234
+ if (!element || empty || forcedColors || loading) return;
2235
+ let chart = null;
2236
+ let cancelled = false;
2237
+ let palette = readPalette(element, tokens);
2238
+ if (!palette) return;
2239
+ void mountTimeSeries(element, { series: cleaned, format, range, baseline }, palette).then((mounted) => {
2240
+ if (cancelled) {
2241
+ mounted.destroy();
2242
+ return;
2243
+ }
2244
+ chart = mounted;
2245
+ const current = readPalette(element, tokens) ?? palette;
2246
+ if (current) mounted.applyPalette(current);
2247
+ mounted.setData(cleaned);
2248
+ mounted.mark(markDay, mark?.label ?? "");
2249
+ });
2250
+ const stopWatchingTokens = watchTokens(() => {
2251
+ const next = readPalette(element, tokens);
2252
+ if (!next) return;
2253
+ palette = next;
2254
+ chart?.applyPalette(next);
2255
+ });
2256
+ return () => {
2257
+ cancelled = true;
2258
+ stopWatchingTokens();
2259
+ chart?.destroy();
2260
+ chart = null;
2261
+ };
2262
+ }, [cleaned, tokens, format, range, baseline, markDay, mark, empty, forcedColors, loading]);
2263
+ if (loading) return /* @__PURE__ */ jsx(TimeSeriesChartSkeleton, { height, className });
2264
+ return /* @__PURE__ */ jsxs("div", { className: cn("cb-chart", className), "data-forced-colors": forcedColors || void 0, children: [
2265
+ empty ? /* @__PURE__ */ jsx("p", { className: "cb-chart__empty", style: { minHeight: height }, children: emptyLabel }) : null,
2266
+ empty || forcedColors ? null : /* @__PURE__ */ jsx("div", { ref: host, className: "cb-chart__canvas", style: { height }, role: "img", "aria-label": label }),
2267
+ empty ? null : /* @__PURE__ */ jsx("div", { className: "cb-chart__rows", children: /* @__PURE__ */ jsxs("table", { className: "cb-chart__table", children: [
2268
+ /* @__PURE__ */ jsx("caption", { children: tableLabel }),
2269
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2270
+ /* @__PURE__ */ jsx("th", { scope: "col", children: dayLabel }),
2271
+ cleaned.map((one) => /* @__PURE__ */ jsx("th", { scope: "col", children: one.label }, one.key))
2272
+ ] }) }),
2273
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row) => /* @__PURE__ */ jsxs("tr", { "data-marked": row.day === markDay || void 0, children: [
2274
+ /* @__PURE__ */ jsx("th", { scope: "row", children: row.day }),
2275
+ cleaned.map((one) => {
2276
+ const value = row.values[one.key];
2277
+ return /* @__PURE__ */ jsx("td", { children: value === null || value === void 0 ? "\u2014" : format(value) }, one.key);
2278
+ })
2279
+ ] }, row.day)) })
2280
+ ] }) })
2281
+ ] });
2282
+ }
2283
+ function readPalette(host, seriesTokens) {
2284
+ const probe = createCssProbe(host);
2285
+ const text = probe.color("--cb-fg-muted");
2286
+ const muted = probe.color("--cb-fg-subtle");
2287
+ const grid = probe.color("--cb-border");
2288
+ const background = probe.color("--cb-surface");
2289
+ const above = probe.color("--cb-tone-success");
2290
+ const below = probe.color("--cb-tone-danger");
2291
+ const seriesColors = seriesTokens.map((token) => probe.color(token));
2292
+ probe.done();
2293
+ const font = getComputedStyle(host).fontFamily;
2294
+ if (!text || !muted || !grid || !background || !above || !below || !font) return null;
2295
+ if (seriesColors.some((colour) => colour === void 0)) return null;
2296
+ return {
2297
+ text,
2298
+ muted,
2299
+ grid,
2300
+ background,
2301
+ font,
2302
+ above,
2303
+ below,
2304
+ series: seriesColors.filter((colour) => colour !== void 0)
2305
+ };
2306
+ }
2307
+
2308
+ // src/data/balance-curve/balance-curve.math.ts
2309
+ function balanceReading(points, threshold) {
2310
+ const ordered = seriesPoints(points);
2311
+ if (ordered.length === 0) {
2312
+ return { lowest: null, lowestDay: null, firstBelowDay: null, daysBelow: 0, closing: null };
2313
+ }
2314
+ let lowestPoint = ordered[0];
2315
+ for (const point of ordered) {
2316
+ if (lowestPoint === void 0 || point.value < lowestPoint.value) lowestPoint = point;
2317
+ }
2318
+ const below = ordered.filter((point) => point.value < threshold);
2319
+ return {
2320
+ lowest: lowestPoint?.value ?? null,
2321
+ lowestDay: lowestPoint?.day ?? null,
2322
+ firstBelowDay: below[0]?.day ?? null,
2323
+ daysBelow: below.length,
2324
+ closing: ordered.at(-1)?.value ?? null
2325
+ };
2326
+ }
2327
+ function BalanceCurveSkeleton({
2328
+ height = 260,
2329
+ label = "Loading balance",
2330
+ className
2331
+ }) {
2332
+ return /* @__PURE__ */ jsx(TimeSeriesChartSkeleton, { height, label, className });
2333
+ }
2334
+ function BalanceCurveRoot({
2335
+ label,
2336
+ balances,
2337
+ format,
2338
+ threshold = { value: 0 },
2339
+ height = 260,
2340
+ seriesLabel = "Balance",
2341
+ emptyLabel = "Nothing to project yet.",
2342
+ tableLabel = "Balance by day",
2343
+ belowLabel = (from, lowest, days) => `Below the line from ${from} \u2014 lowest ${lowest}, ${days} day(s) under.`,
2344
+ clearLabel = (lowest) => `Stays above the line. Lowest point ${lowest}.`,
2345
+ loading = false,
2346
+ className
2347
+ }) {
2348
+ const points = useMemo(() => seriesPoints(balances), [balances]);
2349
+ const reading = useMemo(() => balanceReading(points, threshold.value), [points, threshold.value]);
2350
+ const series = useMemo(
2351
+ () => [{ key: "balance", label: seriesLabel, points, emphasis: "primary", colorToken: "--cb-tone-brand" }],
2352
+ [points, seriesLabel]
2353
+ );
2354
+ if (loading) return /* @__PURE__ */ jsx(BalanceCurveSkeleton, { height, className });
2355
+ const breached = reading.firstBelowDay !== null;
2356
+ return /* @__PURE__ */ jsxs("figure", { className: cn("cb-balance-curve", className), children: [
2357
+ /* @__PURE__ */ jsxs("figcaption", { className: "cb-balance-curve__head", children: [
2358
+ /* @__PURE__ */ jsx("span", { className: "cb-balance-curve__label", children: label }),
2359
+ threshold.label ? /* @__PURE__ */ jsx("span", { className: "cb-balance-curve__threshold", children: threshold.label }) : null
2360
+ ] }),
2361
+ reading.lowest === null ? null : /* @__PURE__ */ jsx("p", { className: "cb-balance-curve__reading", "data-state": breached ? "below" : "clear", children: breached && reading.firstBelowDay ? belowLabel(reading.firstBelowDay, format(reading.lowest), reading.daysBelow) : clearLabel(format(reading.lowest)) }),
2362
+ /* @__PURE__ */ jsx(
2363
+ TimeSeriesChart,
2364
+ {
2365
+ label,
2366
+ series,
2367
+ format,
2368
+ baseline: threshold,
2369
+ mark: reading.lowestDay ? { day: reading.lowestDay, label: format(reading.lowest ?? 0) } : void 0,
2370
+ height,
2371
+ emptyLabel,
2372
+ tableLabel
2373
+ }
2374
+ )
2375
+ ] });
2376
+ }
2377
+ var BalanceCurve = Object.assign(BalanceCurveRoot, { Skeleton: BalanceCurveSkeleton });
2378
+
2379
+ // src/data/progress-curve/progress-curve.math.ts
2380
+ function toPoints(points) {
2381
+ return seriesPoints(points.map((point) => ({ day: point.day, value: point.percent })));
2382
+ }
2383
+ function readingOn(planned, actual, day) {
2384
+ const plannedPercent = valueOn(toPoints(planned), day);
2385
+ const actualPercent = valueOn(toPoints(actual), day);
2386
+ return {
2387
+ plannedPercent,
2388
+ actualPercent,
2389
+ gap: plannedPercent === null || actualPercent === null ? null : round1(actualPercent - plannedPercent)
2390
+ };
2391
+ }
2392
+ function curveRows(planned, actual) {
2393
+ return alignRows([
2394
+ { key: "planned", points: toPoints(planned) },
2395
+ { key: "actual", points: toPoints(actual) }
2396
+ ]).map((row) => {
2397
+ const plannedPercent = row.values.planned ?? null;
2398
+ const actualPercent = row.values.actual ?? null;
2399
+ return {
2400
+ day: row.day,
2401
+ plannedPercent,
2402
+ actualPercent,
2403
+ gap: plannedPercent === null || actualPercent === null ? null : round1(actualPercent - plannedPercent)
2404
+ };
2405
+ });
2406
+ }
2407
+ function ProgressCurveSkeleton({
2408
+ height = 260,
2409
+ label = "Loading progress",
2410
+ className
2411
+ }) {
2412
+ return /* @__PURE__ */ jsx(TimeSeriesChartSkeleton, { height, label, className });
2413
+ }
2414
+ function ProgressCurveRoot({
2415
+ planned,
2416
+ actual,
2417
+ label,
2418
+ today,
2419
+ height = 260,
2420
+ plannedLabel = "Planned",
2421
+ actualLabel = "Actual",
2422
+ lastReportLabel = "Last report",
2423
+ emptyLabel = "Nothing has been reported yet.",
2424
+ tableLabel = "Progress by day",
2425
+ aheadLabel = "ahead of plan",
2426
+ behindLabel = "behind plan",
2427
+ onTrackLabel = "on plan",
2428
+ loading = false,
2429
+ className
2430
+ }) {
2431
+ const rows = useMemo(() => curveRows(planned, actual), [planned, actual]);
2432
+ const actualPoints = useMemo(() => toPoints(actual), [actual]);
2433
+ const lastReport = actualPoints.at(-1)?.day ?? null;
2434
+ const latest = useMemo(() => {
2435
+ if (!today) return rows[rows.length - 1];
2436
+ const day = asDay(today);
2437
+ return day === null ? rows[rows.length - 1] : { day, ...readingOn(planned, actual, day) };
2438
+ }, [today, rows, planned, actual]);
2439
+ const series = useMemo(
2440
+ () => [
2441
+ { key: "planned", label: plannedLabel, points: toPoints(planned), emphasis: "reference", colorToken: "--cb-fg-subtle" },
2442
+ { key: "actual", label: actualLabel, points: toPoints(actual), emphasis: "primary", colorToken: "--cb-tone-brand" }
2443
+ ],
2444
+ [planned, actual, plannedLabel, actualLabel]
2445
+ );
2446
+ if (loading) return /* @__PURE__ */ jsx(ProgressCurveSkeleton, { height, className });
2447
+ return /* @__PURE__ */ jsxs("figure", { className: cn("cb-progress-curve", className), children: [
2448
+ /* @__PURE__ */ jsxs("figcaption", { className: "cb-progress-curve__head", children: [
2449
+ /* @__PURE__ */ jsx("span", { className: "cb-progress-curve__label", children: label }),
2450
+ /* @__PURE__ */ jsx("span", { className: "cb-chart-legend", children: series.map((one) => /* @__PURE__ */ jsx("span", { className: "cb-chart-legend__key", "data-emphasis": one.emphasis, "data-series": one.key, children: one.label }, one.key)) })
2451
+ ] }),
2452
+ latest ? /* @__PURE__ */ jsxs("p", { className: "cb-progress-curve__reading", "data-state": stateOf(latest.gap), children: [
2453
+ /* @__PURE__ */ jsx("strong", { children: percent(latest.actualPercent) }),
2454
+ " ",
2455
+ actualLabel.toLowerCase(),
2456
+ " \xB7 ",
2457
+ percent(latest.plannedPercent),
2458
+ " ",
2459
+ plannedLabel.toLowerCase(),
2460
+ latest.gap === null ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
2461
+ " \u2014 ",
2462
+ Math.abs(latest.gap),
2463
+ " ",
2464
+ gapWord(latest.gap, { aheadLabel, behindLabel, onTrackLabel })
2465
+ ] }),
2466
+ /* @__PURE__ */ jsxs("span", { className: "cb-progress-curve__on-day", children: [
2467
+ " (",
2468
+ latest.day,
2469
+ ")"
2470
+ ] })
2471
+ ] }) : null,
2472
+ /* @__PURE__ */ jsx(
2473
+ TimeSeriesChart,
2474
+ {
2475
+ label,
2476
+ series,
2477
+ format: percentOf,
2478
+ range: { min: 0, max: 100 },
2479
+ mark: lastReport ? { day: lastReport, label: lastReportLabel } : void 0,
2480
+ height,
2481
+ emptyLabel,
2482
+ tableLabel
2483
+ }
2484
+ )
2485
+ ] });
2486
+ }
2487
+ var ProgressCurve = Object.assign(ProgressCurveRoot, { Skeleton: ProgressCurveSkeleton });
2488
+ var percentOf = (value) => `${Math.round(value)}%`;
2489
+ function percent(value) {
2490
+ return value === null ? "\u2014" : `${round1(value)}%`;
2491
+ }
2492
+ function stateOf(gap) {
2493
+ if (gap === null) return "unknown";
2494
+ if (gap < 0) return "behind";
2495
+ if (gap > 0) return "ahead";
2496
+ return "on-plan";
2497
+ }
2498
+ function gapWord(gap, words) {
2499
+ if (gap < 0) return words.behindLabel;
2500
+ if (gap > 0) return words.aheadLabel;
2501
+ return words.onTrackLabel;
2502
+ }
2038
2503
  function DiagramNodeView({ data, selected }) {
2039
2504
  return /* @__PURE__ */ jsxs(
2040
2505
  "div",
@@ -2342,4 +2807,4 @@ function DiagramEditorRoot(props) {
2342
2807
  }
2343
2808
  var DiagramEditor = Object.assign(DiagramEditorRoot, { Skeleton: DiagramSkeleton });
2344
2809
 
2345
- export { Board, CeebeeAntStyleProvider, Checklist, CommandPalette, DEFAULT_LABELS, Diagram, DiagramEditor, LabelsProvider, Modal, MotionProvider, PanZoomCanvas, Reveal, Sidebar, Stagger, StickerGroup, ThemeBridge, ThemeProvider, ToastProvider, TopBar, createCeebeeAntStyleCache, extractCeebeeAntStyles, filterCommands, groupCommands, rankCommand, useLabels, useMotionSettings, useTheme, useToast };
2810
+ export { BalanceCurve, BalanceCurveSkeleton, Board, CeebeeAntStyleProvider, Checklist, CommandPalette, DEFAULT_LABELS, Diagram, DiagramEditor, LabelsProvider, Modal, MotionProvider, PanZoomCanvas, ProgressCurve, ProgressCurveSkeleton, Reveal, Sidebar, Stagger, StickerGroup, ThemeBridge, ThemeProvider, TimeSeriesChart, TimeSeriesChartSkeleton, ToastProvider, TopBar, alignRows, asDay, balanceReading, createCeebeeAntStyleCache, curveRows, extractCeebeeAntStyles, filterCommands, groupCommands, nearestDay, rankCommand, readingOn, round1, seriesPoints, seriesSpan, toPoints, useLabels, useMotionSettings, useTheme, useToast, valueOn };
package/dist/styles.css CHANGED
@@ -930,6 +930,21 @@ svg.react-flow__connectionline {
930
930
  .cb-container--lg { max-width: 76rem; }
931
931
  .cb-container--full { max-width: none; }
932
932
 
933
+ /* Text for a screen reader and no one else. It is in the layout file rather than beside any one
934
+ component because more than one already asked for it: `cb-visually-hidden` was being written in
935
+ markup with no rule behind it, so the text it hides was simply visible. */
936
+ .cb-visually-hidden:not(:focus):not(:active) {
937
+ position: absolute;
938
+ width: 1px;
939
+ height: 1px;
940
+ margin: -1px;
941
+ padding: 0;
942
+ overflow: hidden;
943
+ clip-path: inset(50%);
944
+ white-space: nowrap;
945
+ border: 0;
946
+ }
947
+
933
948
  /* foundation/page-container/page-container.css */
934
949
  .cb-page-container { display: flex; flex-direction: column; gap: var(--cb-space-4); padding-block: var(--cb-space-5); color: var(--cb-fg); font-family: var(--cb-font-sans); }
935
950
  .cb-page-container__breadcrumb { color: var(--cb-fg-muted); font-size: var(--cb-text-sm); }
@@ -1360,6 +1375,50 @@ svg.react-flow__connectionline {
1360
1375
  line-height: var(--cb-leading-normal);
1361
1376
  }
1362
1377
 
1378
+ /* data/balance-curve/balance-curve.css */
1379
+ .cb-balance-curve {
1380
+ display: flex;
1381
+ flex-direction: column;
1382
+ gap: var(--cb-space-2);
1383
+ margin: 0;
1384
+ font-family: var(--cb-font-sans);
1385
+ color: var(--cb-fg);
1386
+ }
1387
+
1388
+ .cb-balance-curve__head {
1389
+ display: flex;
1390
+ flex-wrap: wrap;
1391
+ align-items: baseline;
1392
+ justify-content: space-between;
1393
+ gap: var(--cb-space-2);
1394
+ }
1395
+
1396
+ .cb-balance-curve__label {
1397
+ font-size: var(--cb-text-md);
1398
+ font-weight: 600;
1399
+ }
1400
+
1401
+ .cb-balance-curve__threshold {
1402
+ font-size: var(--cb-text-xs);
1403
+ color: var(--cb-fg-muted);
1404
+ }
1405
+
1406
+ .cb-balance-curve__reading {
1407
+ margin: 0;
1408
+ font-size: var(--cb-text-sm);
1409
+ font-variant-numeric: tabular-nums;
1410
+ color: var(--cb-fg-muted);
1411
+ }
1412
+
1413
+ /* The one place a balance chart spends colour weight: the sentence that says it goes under. The plot
1414
+ already carries the same meaning in its shading, so this is reinforcement, not the only signal. */
1415
+ .cb-balance-curve__reading[data-state="below"] {
1416
+ color: var(--cb-tone-danger);
1417
+ font-weight: 600;
1418
+ }
1419
+
1420
+ .cb-balance-curve__reading[data-state="clear"] { color: var(--cb-tone-success); }
1421
+
1363
1422
  /* data/board/board.css */
1364
1423
  /* Board — columns of cards a person moves by pointer or by keyboard.
1365
1424
  Every value is a Token. The drag lift, the gap animation and the drop marker all have a
@@ -2141,6 +2200,50 @@ svg.react-flow__connectionline {
2141
2200
  .cb-pan-zoom-canvas__toolbar { border-color: CanvasText; }
2142
2201
  }
2143
2202
 
2203
+ /* data/progress-curve/progress-curve.css */
2204
+ .cb-progress-curve {
2205
+ display: flex;
2206
+ flex-direction: column;
2207
+ gap: var(--cb-space-2);
2208
+ margin: 0;
2209
+ font-family: var(--cb-font-sans);
2210
+ color: var(--cb-fg);
2211
+ }
2212
+
2213
+ .cb-progress-curve__head {
2214
+ display: flex;
2215
+ flex-wrap: wrap;
2216
+ align-items: baseline;
2217
+ justify-content: space-between;
2218
+ gap: var(--cb-space-2);
2219
+ }
2220
+
2221
+ .cb-progress-curve__label {
2222
+ font-size: var(--cb-text-md);
2223
+ font-weight: 600;
2224
+ }
2225
+
2226
+ /* The legend marks take the colour of the series they name — the same Tokens the canvas is drawn from,
2227
+ so the two renderings cannot drift apart. */
2228
+ .cb-progress-curve .cb-chart-legend__key[data-series="planned"] { color: var(--cb-fg-subtle); }
2229
+ .cb-progress-curve .cb-chart-legend__key[data-series="actual"] { color: var(--cb-tone-brand); }
2230
+
2231
+ .cb-progress-curve__reading {
2232
+ margin: 0;
2233
+ font-size: var(--cb-text-sm);
2234
+ font-variant-numeric: tabular-nums;
2235
+ color: var(--cb-fg-muted);
2236
+ }
2237
+
2238
+ .cb-progress-curve__reading strong {
2239
+ font-size: var(--cb-text-xl);
2240
+ color: var(--cb-fg);
2241
+ }
2242
+
2243
+ .cb-progress-curve__reading[data-state="behind"] { color: var(--cb-tone-danger); }
2244
+ .cb-progress-curve__reading[data-state="ahead"] { color: var(--cb-tone-success); }
2245
+ .cb-progress-curve__on-day { color: var(--cb-fg-subtle); }
2246
+
2144
2247
  /* data/sticker-group/sticker-group.css */
2145
2248
  .cb-sticker-group {
2146
2249
  display: flex;
@@ -2230,6 +2333,138 @@ svg.react-flow__connectionline {
2230
2333
  }
2231
2334
  }
2232
2335
 
2336
+ /* data/time-series/time-series.css */
2337
+ .cb-chart {
2338
+ display: flex;
2339
+ flex-direction: column;
2340
+ gap: var(--cb-space-2);
2341
+ font-family: var(--cb-font-sans);
2342
+ color: var(--cb-fg);
2343
+ }
2344
+
2345
+ .cb-chart__canvas {
2346
+ inline-size: 100%;
2347
+ border-radius: var(--cb-radius-md);
2348
+ overflow: hidden;
2349
+ }
2350
+
2351
+ .cb-chart__empty {
2352
+ display: grid;
2353
+ place-items: center;
2354
+ margin: 0;
2355
+ padding: var(--cb-space-4);
2356
+ border: var(--cb-border-width) dashed var(--cb-border);
2357
+ border-radius: var(--cb-radius-md);
2358
+ color: var(--cb-fg-muted);
2359
+ font-size: var(--cb-text-sm);
2360
+ }
2361
+
2362
+ /* The chart's other rendering. Hidden from sight, never from the accessibility tree: a canvas gives a
2363
+ screen reader nothing at all, so this table is the chart as far as one is concerned.
2364
+
2365
+ The geometry is on the wrapper rather than on the table, because a table cannot be shrunk: it
2366
+ refuses to go under its content's width and height whatever is asked of it, so these rules on the
2367
+ table itself left an invisible box the size of the data lying across the plot. */
2368
+ .cb-chart__rows {
2369
+ position: absolute;
2370
+ inline-size: 1px;
2371
+ block-size: 1px;
2372
+ margin: -1px;
2373
+ padding: 0;
2374
+ overflow: hidden;
2375
+ clip-path: inset(50%);
2376
+ white-space: nowrap;
2377
+ border: 0;
2378
+ }
2379
+
2380
+ /* Forced colors: the canvas is replaced by the table rather than sitting there as a bitmap the system
2381
+ cannot recolour. The component stops rendering the canvas at all in this mode, so this rule only has
2382
+ to bring the table back into view. */
2383
+ @media (forced-colors: active) {
2384
+ .cb-chart__rows {
2385
+ position: static;
2386
+ inline-size: 100%;
2387
+ block-size: auto;
2388
+ margin: 0;
2389
+ overflow: visible;
2390
+ clip-path: none;
2391
+ white-space: normal;
2392
+ }
2393
+
2394
+ .cb-chart__table {
2395
+ inline-size: 100%;
2396
+ border-collapse: collapse;
2397
+ font-size: var(--cb-text-sm);
2398
+ font-variant-numeric: tabular-nums;
2399
+ }
2400
+
2401
+ .cb-chart__table caption {
2402
+ text-align: start;
2403
+ padding-block-end: var(--cb-space-1);
2404
+ }
2405
+
2406
+ .cb-chart__table th,
2407
+ .cb-chart__table td {
2408
+ border: var(--cb-border-width) solid;
2409
+ padding: var(--cb-space-1) var(--cb-space-2);
2410
+ text-align: end;
2411
+ }
2412
+
2413
+ .cb-chart__table th[scope="row"],
2414
+ .cb-chart__table th[scope="col"]:first-child {
2415
+ text-align: start;
2416
+ }
2417
+
2418
+ .cb-chart__table tr[data-marked] { font-weight: 700; }
2419
+ }
2420
+
2421
+ .cb-chart--skeleton { gap: var(--cb-space-2); }
2422
+
2423
+ .cb-chart__skeleton-head,
2424
+ .cb-chart__skeleton-plot {
2425
+ display: block;
2426
+ background: var(--cb-bg-subtle);
2427
+ border-radius: var(--cb-radius-sm);
2428
+ }
2429
+
2430
+ .cb-chart__skeleton-head {
2431
+ block-size: var(--cb-text-md);
2432
+ inline-size: calc(var(--cb-space-8) * 3);
2433
+ }
2434
+
2435
+ .cb-chart__skeleton-plot { border-radius: var(--cb-radius-md); }
2436
+
2437
+ /* A legend mark carries the same treatment as the line it names: a reference is thin and dashed, the
2438
+ primary series solid and thick. Colour is never the only difference — dash versus solid survives a
2439
+ monochrome print and every kind of colour blindness. */
2440
+ .cb-chart-legend {
2441
+ display: flex;
2442
+ flex-wrap: wrap;
2443
+ gap: var(--cb-space-3);
2444
+ font-size: var(--cb-text-xs);
2445
+ color: var(--cb-fg-muted);
2446
+ }
2447
+
2448
+ .cb-chart-legend__key {
2449
+ display: inline-flex;
2450
+ align-items: center;
2451
+ gap: var(--cb-space-1);
2452
+ }
2453
+
2454
+ .cb-chart-legend__key::before {
2455
+ content: '';
2456
+ inline-size: var(--cb-space-4);
2457
+ block-size: 0;
2458
+ border-block-start-width: var(--cb-border-width);
2459
+ border-block-start-style: solid;
2460
+ border-color: currentColor;
2461
+ }
2462
+
2463
+ .cb-chart-legend__key[data-emphasis="reference"]::before { border-block-start-style: dashed; }
2464
+ .cb-chart-legend__key[data-emphasis="primary"]::before {
2465
+ border-block-start-width: calc(var(--cb-border-width) * 2);
2466
+ }
2467
+
2233
2468
  /* nav/nav.css */
2234
2469
  .cb-menu-positioner {
2235
2470
  position: absolute;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceebee/ui",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Ceebee's design system: tokens, themed primitives, motion, and onboarding.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,7 +60,8 @@
60
60
  "@dnd-kit/utilities": "3.2.2",
61
61
  "@xyflow/react": "12.11.6",
62
62
  "antd": "6.6.1",
63
- "dayjs": "1.11.18"
63
+ "dayjs": "1.11.18",
64
+ "lightweight-charts": "5.2.1"
64
65
  },
65
66
  "scripts": {
66
67
  "build": "tsup && node ./stamp-client.mjs && node ./build-css.mjs",