@xenide-io/the-old-ui-theme 0.9.7 → 0.9.8

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.
@@ -3742,6 +3742,166 @@ var Chip = forwardRef11(function Chip2(_a, ref) {
3742
3742
  });
3743
3743
  Chip.displayName = "Chip";
3744
3744
 
3745
+ // src/lib/chart/use-ph-chart-tokens.ts
3746
+ import { useEffect as useEffect5, useState as useState10 } from "react";
3747
+ var DEFAULT_TOKENS = {
3748
+ series: ["#1d4aff", "#621da6", "#42827e", "#ce0e74", "#f14f58", "#529a0a", "#fe729e"],
3749
+ muted: "hsl(220 9% 90%)",
3750
+ surface: "#ffffff",
3751
+ textMuted: "hsl(220 9% 46%)",
3752
+ grid: "hsl(220 13% 91%)",
3753
+ borderStrong: "hsl(220 13% 80%)"
3754
+ };
3755
+ function readTokens() {
3756
+ if (typeof document === "undefined") return DEFAULT_TOKENS;
3757
+ const style = getComputedStyle(document.documentElement);
3758
+ const read = (key, fallback) => style.getPropertyValue(key).trim() || fallback;
3759
+ return {
3760
+ series: [
3761
+ read("--ph-data-1", DEFAULT_TOKENS.series[0]),
3762
+ read("--ph-data-2", DEFAULT_TOKENS.series[1]),
3763
+ read("--ph-data-3", DEFAULT_TOKENS.series[2]),
3764
+ read("--ph-data-4", DEFAULT_TOKENS.series[3]),
3765
+ read("--ph-data-5", DEFAULT_TOKENS.series[4]),
3766
+ read("--ph-data-6", DEFAULT_TOKENS.series[5]),
3767
+ read("--ph-data-7", DEFAULT_TOKENS.series[6])
3768
+ ],
3769
+ muted: read("--ph-muted", DEFAULT_TOKENS.muted),
3770
+ surface: read("--ph-surface", DEFAULT_TOKENS.surface),
3771
+ textMuted: read("--ph-mutedtext", DEFAULT_TOKENS.textMuted),
3772
+ grid: read("--ph-border", DEFAULT_TOKENS.grid),
3773
+ borderStrong: read("--ph-border-strong", DEFAULT_TOKENS.borderStrong)
3774
+ };
3775
+ }
3776
+ function useChartTokens() {
3777
+ const [tokens, setTokens] = useState10(DEFAULT_TOKENS);
3778
+ useEffect5(() => {
3779
+ setTokens(readTokens());
3780
+ }, []);
3781
+ return tokens;
3782
+ }
3783
+
3784
+ // src/components/ui/Charts.tsx
3785
+ import { jsx as jsx42, jsxs as jsxs36 } from "react/jsx-runtime";
3786
+ function percent(value, total) {
3787
+ return total > 0 ? Math.round(value / total * 100) : 0;
3788
+ }
3789
+ function EmptyChart() {
3790
+ return /* @__PURE__ */ jsx42("p", { className: "text-sm text-ph-subtle", children: "No data for this period." });
3791
+ }
3792
+ function sliceColour(slice, index, series) {
3793
+ var _a;
3794
+ return (_a = slice.colour) != null ? _a : series[index % series.length];
3795
+ }
3796
+ function DonutChart({ slices, label, formatValue, className }) {
3797
+ const { series } = useChartTokens();
3798
+ const visible = slices.filter((slice) => slice.value > 0);
3799
+ const total = visible.reduce((sum, slice) => sum + slice.value, 0);
3800
+ if (total <= 0) return /* @__PURE__ */ jsx42(EmptyChart, {});
3801
+ const stops = visible.map((slice, index) => {
3802
+ const colour = sliceColour(slice, index, series);
3803
+ const before = visible.slice(0, index).reduce((sum, item) => sum + item.value, 0);
3804
+ const start = before / total * 360;
3805
+ const end = (before + slice.value) / total * 360;
3806
+ return `${colour} ${start}deg ${end}deg`;
3807
+ }).join(", ");
3808
+ return /* @__PURE__ */ jsxs36("div", { className: cn("flex flex-col items-center gap-4 sm:flex-row sm:items-center", className), children: [
3809
+ /* @__PURE__ */ jsx42(
3810
+ "div",
3811
+ {
3812
+ role: "img",
3813
+ "aria-label": label,
3814
+ className: "relative h-36 w-36 shrink-0 rounded-full",
3815
+ style: { background: `conic-gradient(${stops})` },
3816
+ children: /* @__PURE__ */ jsx42(
3817
+ "span",
3818
+ {
3819
+ className: "absolute inset-[22%] rounded-full bg-ph-surface ring-1 ring-ph-border",
3820
+ "aria-hidden": true
3821
+ }
3822
+ )
3823
+ }
3824
+ ),
3825
+ /* @__PURE__ */ jsx42("ul", { className: "min-w-0 w-full flex-1 space-y-2", children: visible.map((slice, index) => {
3826
+ const colour = sliceColour(slice, index, series);
3827
+ return /* @__PURE__ */ jsxs36("li", { className: "flex items-center justify-between gap-3 text-sm", children: [
3828
+ /* @__PURE__ */ jsxs36("span", { className: "flex min-w-0 items-center gap-2 text-ph-subtle", children: [
3829
+ /* @__PURE__ */ jsx42("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: { background: colour } }),
3830
+ /* @__PURE__ */ jsx42("span", { className: "truncate", children: slice.label })
3831
+ ] }),
3832
+ /* @__PURE__ */ jsx42("span", { className: "shrink-0 tabular-nums text-ph-ink", children: formatValue ? formatValue(slice.value) : `${percent(slice.value, total)}%` })
3833
+ ] }, slice.label);
3834
+ }) })
3835
+ ] });
3836
+ }
3837
+ function BarChart({ items, formatValue, className }) {
3838
+ const { series } = useChartTokens();
3839
+ const visible = items.filter((item) => item.value > 0);
3840
+ const max = Math.max(...visible.map((item) => item.value), 1);
3841
+ if (!visible.length) return /* @__PURE__ */ jsx42(EmptyChart, {});
3842
+ return /* @__PURE__ */ jsx42("ul", { className: cn("space-y-3", className), children: visible.map((item, index) => {
3843
+ const colour = sliceColour(item, index, series);
3844
+ return /* @__PURE__ */ jsxs36("li", { children: [
3845
+ /* @__PURE__ */ jsxs36("div", { className: "mb-1 flex items-center justify-between gap-3 text-sm", children: [
3846
+ /* @__PURE__ */ jsx42("span", { className: "truncate text-ph-subtle", children: item.label }),
3847
+ /* @__PURE__ */ jsx42("span", { className: "shrink-0 tabular-nums text-ph-ink", children: formatValue ? formatValue(item.value) : item.value.toLocaleString("en-AU") })
3848
+ ] }),
3849
+ /* @__PURE__ */ jsx42("div", { className: "h-2.5 rounded-full bg-ph-muted ring-1 ring-ph-border", children: /* @__PURE__ */ jsx42(
3850
+ "div",
3851
+ {
3852
+ className: "h-full rounded-full transition-[width] duration-300 ease-out motion-reduce:transition-none",
3853
+ style: {
3854
+ width: `${Math.max(item.value / max * 100, 2)}%`,
3855
+ background: colour
3856
+ }
3857
+ }
3858
+ ) })
3859
+ ] }, item.label);
3860
+ }) });
3861
+ }
3862
+ function StackedBarChart({
3863
+ slices,
3864
+ label,
3865
+ formatValue,
3866
+ className
3867
+ }) {
3868
+ const { series } = useChartTokens();
3869
+ const visible = slices.filter((slice) => slice.value > 0);
3870
+ const total = visible.reduce((sum, slice) => sum + slice.value, 0);
3871
+ if (total <= 0) return /* @__PURE__ */ jsx42(EmptyChart, {});
3872
+ return /* @__PURE__ */ jsxs36("div", { className: cn("space-y-3", className), children: [
3873
+ /* @__PURE__ */ jsx42(
3874
+ "div",
3875
+ {
3876
+ role: "img",
3877
+ "aria-label": label,
3878
+ className: "flex h-3 overflow-hidden rounded-full bg-ph-muted ring-1 ring-ph-border",
3879
+ children: visible.map((slice, index) => /* @__PURE__ */ jsx42(
3880
+ "span",
3881
+ {
3882
+ className: "h-full",
3883
+ style: {
3884
+ width: `${Math.max(slice.value / total * 100, 2)}%`,
3885
+ background: sliceColour(slice, index, series)
3886
+ }
3887
+ },
3888
+ slice.label
3889
+ ))
3890
+ }
3891
+ ),
3892
+ /* @__PURE__ */ jsx42("ul", { className: "space-y-2", children: visible.map((slice, index) => {
3893
+ const colour = sliceColour(slice, index, series);
3894
+ return /* @__PURE__ */ jsxs36("li", { className: "flex items-center justify-between gap-3 text-sm", children: [
3895
+ /* @__PURE__ */ jsxs36("span", { className: "flex min-w-0 items-center gap-2 text-ph-subtle", children: [
3896
+ /* @__PURE__ */ jsx42("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: { background: colour } }),
3897
+ /* @__PURE__ */ jsx42("span", { className: "truncate", children: slice.label })
3898
+ ] }),
3899
+ /* @__PURE__ */ jsx42("span", { className: "shrink-0 tabular-nums text-ph-ink", children: formatValue ? formatValue(slice.value) : `${percent(slice.value, total)}%` })
3900
+ ] }, slice.label);
3901
+ }) })
3902
+ ] });
3903
+ }
3904
+
3745
3905
  export {
3746
3906
  ButtonChrome,
3747
3907
  cn,
@@ -3908,5 +4068,9 @@ export {
3908
4068
  Link,
3909
4069
  Spinner,
3910
4070
  Dot,
3911
- Chip
4071
+ Chip,
4072
+ useChartTokens,
4073
+ DonutChart,
4074
+ BarChart,
4075
+ StackedBarChart
3912
4076
  };
package/dist/index.mjs CHANGED
@@ -158,7 +158,7 @@ import {
158
158
  isThemeId,
159
159
  persistTheme,
160
160
  readStoredTheme
161
- } from "./chunk-2EO5VCXP.mjs";
161
+ } from "./chunk-6SVJAU5K.mjs";
162
162
  import "./chunk-FWCSY2DS.mjs";
163
163
  export {
164
164
  Accordion,
package/dist/suite.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  Toggle,
13
13
  Tooltip,
14
14
  cn
15
- } from "./chunk-2EO5VCXP.mjs";
15
+ } from "./chunk-6SVJAU5K.mjs";
16
16
  import {
17
17
  SUITE_THEME_CHANNEL,
18
18
  SUITE_THEME_COOKIE,
package/dist/ui.d.mts CHANGED
@@ -55,4 +55,40 @@ interface CollapsibleSectionProps {
55
55
  }
56
56
  declare function CollapsibleSection({ title, children, defaultOpen, id, }: CollapsibleSectionProps): react.JSX.Element;
57
57
 
58
- export { CodeBlock, CollapsibleSection, type CollapsibleSectionProps, ComponentDocs, type ComponentDocsProps, type ComponentPropRow, FilterBarProps, FilterChipsProps, FilterControls, type FilterControlsProps, ShowcaseWrapper };
58
+ interface ChartTokens {
59
+ series: string[];
60
+ muted: string;
61
+ surface: string;
62
+ textMuted: string;
63
+ grid: string;
64
+ borderStrong: string;
65
+ }
66
+ declare function useChartTokens(): ChartTokens;
67
+
68
+ interface ChartSlice {
69
+ label: string;
70
+ value: number;
71
+ colour?: string;
72
+ }
73
+ interface DonutChartProps {
74
+ slices: readonly ChartSlice[];
75
+ label: string;
76
+ formatValue?: (value: number) => string;
77
+ className?: string;
78
+ }
79
+ interface BarChartProps {
80
+ items: readonly ChartSlice[];
81
+ formatValue?: (value: number) => string;
82
+ className?: string;
83
+ }
84
+ interface StackedBarChartProps {
85
+ slices: readonly ChartSlice[];
86
+ label: string;
87
+ formatValue?: (value: number) => string;
88
+ className?: string;
89
+ }
90
+ declare function DonutChart({ slices, label, formatValue, className }: DonutChartProps): react.JSX.Element;
91
+ declare function BarChart({ items, formatValue, className }: BarChartProps): react.JSX.Element;
92
+ declare function StackedBarChart({ slices, label, formatValue, className, }: StackedBarChartProps): react.JSX.Element;
93
+
94
+ export { BarChart, type BarChartProps, type ChartSlice, type ChartTokens, CodeBlock, CollapsibleSection, type CollapsibleSectionProps, ComponentDocs, type ComponentDocsProps, type ComponentPropRow, DonutChart, type DonutChartProps, FilterBarProps, FilterChipsProps, FilterControls, type FilterControlsProps, ShowcaseWrapper, StackedBarChart, type StackedBarChartProps, useChartTokens };
package/dist/ui.d.ts CHANGED
@@ -55,4 +55,40 @@ interface CollapsibleSectionProps {
55
55
  }
56
56
  declare function CollapsibleSection({ title, children, defaultOpen, id, }: CollapsibleSectionProps): react.JSX.Element;
57
57
 
58
- export { CodeBlock, CollapsibleSection, type CollapsibleSectionProps, ComponentDocs, type ComponentDocsProps, type ComponentPropRow, FilterBarProps, FilterChipsProps, FilterControls, type FilterControlsProps, ShowcaseWrapper };
58
+ interface ChartTokens {
59
+ series: string[];
60
+ muted: string;
61
+ surface: string;
62
+ textMuted: string;
63
+ grid: string;
64
+ borderStrong: string;
65
+ }
66
+ declare function useChartTokens(): ChartTokens;
67
+
68
+ interface ChartSlice {
69
+ label: string;
70
+ value: number;
71
+ colour?: string;
72
+ }
73
+ interface DonutChartProps {
74
+ slices: readonly ChartSlice[];
75
+ label: string;
76
+ formatValue?: (value: number) => string;
77
+ className?: string;
78
+ }
79
+ interface BarChartProps {
80
+ items: readonly ChartSlice[];
81
+ formatValue?: (value: number) => string;
82
+ className?: string;
83
+ }
84
+ interface StackedBarChartProps {
85
+ slices: readonly ChartSlice[];
86
+ label: string;
87
+ formatValue?: (value: number) => string;
88
+ className?: string;
89
+ }
90
+ declare function DonutChart({ slices, label, formatValue, className }: DonutChartProps): react.JSX.Element;
91
+ declare function BarChart({ items, formatValue, className }: BarChartProps): react.JSX.Element;
92
+ declare function StackedBarChart({ slices, label, formatValue, className, }: StackedBarChartProps): react.JSX.Element;
93
+
94
+ export { BarChart, type BarChartProps, type ChartSlice, type ChartTokens, CodeBlock, CollapsibleSection, type CollapsibleSectionProps, ComponentDocs, type ComponentDocsProps, type ComponentPropRow, DonutChart, type DonutChartProps, FilterBarProps, FilterChipsProps, FilterControls, type FilterControlsProps, ShowcaseWrapper, StackedBarChart, type StackedBarChartProps, useChartTokens };
package/dist/ui.js CHANGED
@@ -68,6 +68,7 @@ __export(ui_exports, {
68
68
  Avatar: () => Avatar,
69
69
  AvatarGroup: () => AvatarGroup,
70
70
  Badge: () => Badge,
71
+ BarChart: () => BarChart,
71
72
  Button: () => Button,
72
73
  ButtonChrome: () => ButtonChrome,
73
74
  Calendar: () => Calendar,
@@ -80,6 +81,7 @@ __export(ui_exports, {
80
81
  CommandPalette: () => CommandPalette,
81
82
  ComponentDocs: () => ComponentDocs,
82
83
  Display: () => Display,
84
+ DonutChart: () => DonutChart,
83
85
  Dot: () => Dot,
84
86
  DropdownButton: () => DropdownButton,
85
87
  DropdownItem: () => DropdownItem,
@@ -127,6 +129,7 @@ __export(ui_exports, {
127
129
  Small: () => Small,
128
130
  SortMenu: () => SortMenu,
129
131
  Spinner: () => Spinner,
132
+ StackedBarChart: () => StackedBarChart,
130
133
  Stat: () => Stat,
131
134
  Table: () => Table,
132
135
  Textarea: () => Textarea,
@@ -134,7 +137,8 @@ __export(ui_exports, {
134
137
  ThemeSwitcher: () => ThemeSwitcher,
135
138
  Toggle: () => Toggle,
136
139
  Tooltip: () => Tooltip,
137
- TooltipProvider: () => TooltipProvider
140
+ TooltipProvider: () => TooltipProvider,
141
+ useChartTokens: () => useChartTokens
138
142
  });
139
143
  module.exports = __toCommonJS(ui_exports);
140
144
 
@@ -3847,6 +3851,166 @@ var Chip = (0, import_react25.forwardRef)(function Chip2(_a, ref) {
3847
3851
  );
3848
3852
  });
3849
3853
  Chip.displayName = "Chip";
3854
+
3855
+ // src/lib/chart/use-ph-chart-tokens.ts
3856
+ var import_react26 = require("react");
3857
+ var DEFAULT_TOKENS = {
3858
+ series: ["#1d4aff", "#621da6", "#42827e", "#ce0e74", "#f14f58", "#529a0a", "#fe729e"],
3859
+ muted: "hsl(220 9% 90%)",
3860
+ surface: "#ffffff",
3861
+ textMuted: "hsl(220 9% 46%)",
3862
+ grid: "hsl(220 13% 91%)",
3863
+ borderStrong: "hsl(220 13% 80%)"
3864
+ };
3865
+ function readTokens() {
3866
+ if (typeof document === "undefined") return DEFAULT_TOKENS;
3867
+ const style = getComputedStyle(document.documentElement);
3868
+ const read = (key, fallback) => style.getPropertyValue(key).trim() || fallback;
3869
+ return {
3870
+ series: [
3871
+ read("--ph-data-1", DEFAULT_TOKENS.series[0]),
3872
+ read("--ph-data-2", DEFAULT_TOKENS.series[1]),
3873
+ read("--ph-data-3", DEFAULT_TOKENS.series[2]),
3874
+ read("--ph-data-4", DEFAULT_TOKENS.series[3]),
3875
+ read("--ph-data-5", DEFAULT_TOKENS.series[4]),
3876
+ read("--ph-data-6", DEFAULT_TOKENS.series[5]),
3877
+ read("--ph-data-7", DEFAULT_TOKENS.series[6])
3878
+ ],
3879
+ muted: read("--ph-muted", DEFAULT_TOKENS.muted),
3880
+ surface: read("--ph-surface", DEFAULT_TOKENS.surface),
3881
+ textMuted: read("--ph-mutedtext", DEFAULT_TOKENS.textMuted),
3882
+ grid: read("--ph-border", DEFAULT_TOKENS.grid),
3883
+ borderStrong: read("--ph-border-strong", DEFAULT_TOKENS.borderStrong)
3884
+ };
3885
+ }
3886
+ function useChartTokens() {
3887
+ const [tokens, setTokens] = (0, import_react26.useState)(DEFAULT_TOKENS);
3888
+ (0, import_react26.useEffect)(() => {
3889
+ setTokens(readTokens());
3890
+ }, []);
3891
+ return tokens;
3892
+ }
3893
+
3894
+ // src/components/ui/Charts.tsx
3895
+ var import_jsx_runtime42 = require("react/jsx-runtime");
3896
+ function percent(value, total) {
3897
+ return total > 0 ? Math.round(value / total * 100) : 0;
3898
+ }
3899
+ function EmptyChart() {
3900
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { className: "text-sm text-ph-subtle", children: "No data for this period." });
3901
+ }
3902
+ function sliceColour(slice, index, series) {
3903
+ var _a;
3904
+ return (_a = slice.colour) != null ? _a : series[index % series.length];
3905
+ }
3906
+ function DonutChart({ slices, label, formatValue, className }) {
3907
+ const { series } = useChartTokens();
3908
+ const visible = slices.filter((slice) => slice.value > 0);
3909
+ const total = visible.reduce((sum, slice) => sum + slice.value, 0);
3910
+ if (total <= 0) return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(EmptyChart, {});
3911
+ const stops = visible.map((slice, index) => {
3912
+ const colour = sliceColour(slice, index, series);
3913
+ const before = visible.slice(0, index).reduce((sum, item) => sum + item.value, 0);
3914
+ const start = before / total * 360;
3915
+ const end = (before + slice.value) / total * 360;
3916
+ return `${colour} ${start}deg ${end}deg`;
3917
+ }).join(", ");
3918
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: cn("flex flex-col items-center gap-4 sm:flex-row sm:items-center", className), children: [
3919
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3920
+ "div",
3921
+ {
3922
+ role: "img",
3923
+ "aria-label": label,
3924
+ className: "relative h-36 w-36 shrink-0 rounded-full",
3925
+ style: { background: `conic-gradient(${stops})` },
3926
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3927
+ "span",
3928
+ {
3929
+ className: "absolute inset-[22%] rounded-full bg-ph-surface ring-1 ring-ph-border",
3930
+ "aria-hidden": true
3931
+ }
3932
+ )
3933
+ }
3934
+ ),
3935
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { className: "min-w-0 w-full flex-1 space-y-2", children: visible.map((slice, index) => {
3936
+ const colour = sliceColour(slice, index, series);
3937
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("li", { className: "flex items-center justify-between gap-3 text-sm", children: [
3938
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "flex min-w-0 items-center gap-2 text-ph-subtle", children: [
3939
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: { background: colour } }),
3940
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "truncate", children: slice.label })
3941
+ ] }),
3942
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "shrink-0 tabular-nums text-ph-ink", children: formatValue ? formatValue(slice.value) : `${percent(slice.value, total)}%` })
3943
+ ] }, slice.label);
3944
+ }) })
3945
+ ] });
3946
+ }
3947
+ function BarChart({ items, formatValue, className }) {
3948
+ const { series } = useChartTokens();
3949
+ const visible = items.filter((item) => item.value > 0);
3950
+ const max = Math.max(...visible.map((item) => item.value), 1);
3951
+ if (!visible.length) return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(EmptyChart, {});
3952
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { className: cn("space-y-3", className), children: visible.map((item, index) => {
3953
+ const colour = sliceColour(item, index, series);
3954
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("li", { children: [
3955
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "mb-1 flex items-center justify-between gap-3 text-sm", children: [
3956
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "truncate text-ph-subtle", children: item.label }),
3957
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "shrink-0 tabular-nums text-ph-ink", children: formatValue ? formatValue(item.value) : item.value.toLocaleString("en-AU") })
3958
+ ] }),
3959
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "h-2.5 rounded-full bg-ph-muted ring-1 ring-ph-border", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3960
+ "div",
3961
+ {
3962
+ className: "h-full rounded-full transition-[width] duration-300 ease-out motion-reduce:transition-none",
3963
+ style: {
3964
+ width: `${Math.max(item.value / max * 100, 2)}%`,
3965
+ background: colour
3966
+ }
3967
+ }
3968
+ ) })
3969
+ ] }, item.label);
3970
+ }) });
3971
+ }
3972
+ function StackedBarChart({
3973
+ slices,
3974
+ label,
3975
+ formatValue,
3976
+ className
3977
+ }) {
3978
+ const { series } = useChartTokens();
3979
+ const visible = slices.filter((slice) => slice.value > 0);
3980
+ const total = visible.reduce((sum, slice) => sum + slice.value, 0);
3981
+ if (total <= 0) return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(EmptyChart, {});
3982
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: cn("space-y-3", className), children: [
3983
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3984
+ "div",
3985
+ {
3986
+ role: "img",
3987
+ "aria-label": label,
3988
+ className: "flex h-3 overflow-hidden rounded-full bg-ph-muted ring-1 ring-ph-border",
3989
+ children: visible.map((slice, index) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
3990
+ "span",
3991
+ {
3992
+ className: "h-full",
3993
+ style: {
3994
+ width: `${Math.max(slice.value / total * 100, 2)}%`,
3995
+ background: sliceColour(slice, index, series)
3996
+ }
3997
+ },
3998
+ slice.label
3999
+ ))
4000
+ }
4001
+ ),
4002
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("ul", { className: "space-y-2", children: visible.map((slice, index) => {
4003
+ const colour = sliceColour(slice, index, series);
4004
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("li", { className: "flex items-center justify-between gap-3 text-sm", children: [
4005
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "flex min-w-0 items-center gap-2 text-ph-subtle", children: [
4006
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: { background: colour } }),
4007
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "truncate", children: slice.label })
4008
+ ] }),
4009
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "shrink-0 tabular-nums text-ph-ink", children: formatValue ? formatValue(slice.value) : `${percent(slice.value, total)}%` })
4010
+ ] }, slice.label);
4011
+ }) })
4012
+ ] });
4013
+ }
3850
4014
  // Annotate the CommonJS export names for ESM import in node:
3851
4015
  0 && (module.exports = {
3852
4016
  Accordion,
@@ -3857,6 +4021,7 @@ Chip.displayName = "Chip";
3857
4021
  Avatar,
3858
4022
  AvatarGroup,
3859
4023
  Badge,
4024
+ BarChart,
3860
4025
  Button,
3861
4026
  ButtonChrome,
3862
4027
  Calendar,
@@ -3869,6 +4034,7 @@ Chip.displayName = "Chip";
3869
4034
  CommandPalette,
3870
4035
  ComponentDocs,
3871
4036
  Display,
4037
+ DonutChart,
3872
4038
  Dot,
3873
4039
  DropdownButton,
3874
4040
  DropdownItem,
@@ -3916,6 +4082,7 @@ Chip.displayName = "Chip";
3916
4082
  Small,
3917
4083
  SortMenu,
3918
4084
  Spinner,
4085
+ StackedBarChart,
3919
4086
  Stat,
3920
4087
  Table,
3921
4088
  Textarea,
@@ -3923,5 +4090,6 @@ Chip.displayName = "Chip";
3923
4090
  ThemeSwitcher,
3924
4091
  Toggle,
3925
4092
  Tooltip,
3926
- TooltipProvider
4093
+ TooltipProvider,
4094
+ useChartTokens
3927
4095
  });
package/dist/ui.mjs CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  Avatar,
9
9
  AvatarGroup,
10
10
  Badge,
11
+ BarChart,
11
12
  Button,
12
13
  ButtonChrome,
13
14
  Calendar,
@@ -20,6 +21,7 @@ import {
20
21
  CommandPalette,
21
22
  ComponentDocs,
22
23
  Display,
24
+ DonutChart,
23
25
  Dot,
24
26
  DropdownButton,
25
27
  DropdownItem,
@@ -67,6 +69,7 @@ import {
67
69
  Small,
68
70
  SortMenu,
69
71
  Spinner,
72
+ StackedBarChart,
70
73
  Stat,
71
74
  Table,
72
75
  Textarea,
@@ -74,8 +77,9 @@ import {
74
77
  ThemeSwitcher,
75
78
  Toggle,
76
79
  Tooltip,
77
- TooltipProvider
78
- } from "./chunk-2EO5VCXP.mjs";
80
+ TooltipProvider,
81
+ useChartTokens
82
+ } from "./chunk-6SVJAU5K.mjs";
79
83
  import "./chunk-FWCSY2DS.mjs";
80
84
  export {
81
85
  Accordion,
@@ -86,6 +90,7 @@ export {
86
90
  Avatar,
87
91
  AvatarGroup,
88
92
  Badge,
93
+ BarChart,
89
94
  Button,
90
95
  ButtonChrome,
91
96
  Calendar,
@@ -98,6 +103,7 @@ export {
98
103
  CommandPalette,
99
104
  ComponentDocs,
100
105
  Display,
106
+ DonutChart,
101
107
  Dot,
102
108
  DropdownButton,
103
109
  DropdownItem,
@@ -145,6 +151,7 @@ export {
145
151
  Small,
146
152
  SortMenu,
147
153
  Spinner,
154
+ StackedBarChart,
148
155
  Stat,
149
156
  Table,
150
157
  Textarea,
@@ -152,5 +159,6 @@ export {
152
159
  ThemeSwitcher,
153
160
  Toggle,
154
161
  Tooltip,
155
- TooltipProvider
162
+ TooltipProvider,
163
+ useChartTokens
156
164
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenide-io/the-old-ui-theme",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "Props-driven React components and theme tokens inspired by classic interface design — optimized for Next.js apps and internal tools.",
5
5
  "author": "Xenide",
6
6
  "license": "MIT",
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ import { useChartTokens } from "@/lib/chart/use-ph-chart-tokens";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ export { useChartTokens, type ChartTokens } from "@/lib/chart/use-ph-chart-tokens";
7
+
8
+ export interface ChartSlice {
9
+ label: string;
10
+ value: number;
11
+ colour?: string;
12
+ }
13
+
14
+ export interface DonutChartProps {
15
+ slices: readonly ChartSlice[];
16
+ label: string;
17
+ formatValue?: (value: number) => string;
18
+ className?: string;
19
+ }
20
+
21
+ export interface BarChartProps {
22
+ items: readonly ChartSlice[];
23
+ formatValue?: (value: number) => string;
24
+ className?: string;
25
+ }
26
+
27
+ export interface StackedBarChartProps {
28
+ slices: readonly ChartSlice[];
29
+ label: string;
30
+ formatValue?: (value: number) => string;
31
+ className?: string;
32
+ }
33
+
34
+ function percent(value: number, total: number): number {
35
+ return total > 0 ? Math.round((value / total) * 100) : 0;
36
+ }
37
+
38
+ function EmptyChart() {
39
+ return <p className="text-sm text-ph-subtle">No data for this period.</p>;
40
+ }
41
+
42
+ function sliceColour(slice: ChartSlice, index: number, series: string[]): string {
43
+ return slice.colour ?? series[index % series.length];
44
+ }
45
+
46
+ export function DonutChart({ slices, label, formatValue, className }: DonutChartProps) {
47
+ const { series } = useChartTokens();
48
+ const visible = slices.filter((slice) => slice.value > 0);
49
+ const total = visible.reduce((sum, slice) => sum + slice.value, 0);
50
+ if (total <= 0) return <EmptyChart />;
51
+
52
+ const stops = visible
53
+ .map((slice, index) => {
54
+ const colour = sliceColour(slice, index, series);
55
+ const before = visible
56
+ .slice(0, index)
57
+ .reduce((sum, item) => sum + item.value, 0);
58
+ const start = (before / total) * 360;
59
+ const end = ((before + slice.value) / total) * 360;
60
+ return `${colour} ${start}deg ${end}deg`;
61
+ })
62
+ .join(", ");
63
+
64
+ return (
65
+ <div className={cn("flex flex-col items-center gap-4 sm:flex-row sm:items-center", className)}>
66
+ <div
67
+ role="img"
68
+ aria-label={label}
69
+ className="relative h-36 w-36 shrink-0 rounded-full"
70
+ style={{ background: `conic-gradient(${stops})` }}
71
+ >
72
+ <span
73
+ className="absolute inset-[22%] rounded-full bg-ph-surface ring-1 ring-ph-border"
74
+ aria-hidden
75
+ />
76
+ </div>
77
+ <ul className="min-w-0 w-full flex-1 space-y-2">
78
+ {visible.map((slice, index) => {
79
+ const colour = sliceColour(slice, index, series);
80
+ return (
81
+ <li key={slice.label} className="flex items-center justify-between gap-3 text-sm">
82
+ <span className="flex min-w-0 items-center gap-2 text-ph-subtle">
83
+ <span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ background: colour }} />
84
+ <span className="truncate">{slice.label}</span>
85
+ </span>
86
+ <span className="shrink-0 tabular-nums text-ph-ink">
87
+ {formatValue ? formatValue(slice.value) : `${percent(slice.value, total)}%`}
88
+ </span>
89
+ </li>
90
+ );
91
+ })}
92
+ </ul>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ export function BarChart({ items, formatValue, className }: BarChartProps) {
98
+ const { series } = useChartTokens();
99
+ const visible = items.filter((item) => item.value > 0);
100
+ const max = Math.max(...visible.map((item) => item.value), 1);
101
+ if (!visible.length) return <EmptyChart />;
102
+
103
+ return (
104
+ <ul className={cn("space-y-3", className)}>
105
+ {visible.map((item, index) => {
106
+ const colour = sliceColour(item, index, series);
107
+ return (
108
+ <li key={item.label}>
109
+ <div className="mb-1 flex items-center justify-between gap-3 text-sm">
110
+ <span className="truncate text-ph-subtle">{item.label}</span>
111
+ <span className="shrink-0 tabular-nums text-ph-ink">
112
+ {formatValue ? formatValue(item.value) : item.value.toLocaleString("en-AU")}
113
+ </span>
114
+ </div>
115
+ <div className="h-2.5 rounded-full bg-ph-muted ring-1 ring-ph-border">
116
+ <div
117
+ className="h-full rounded-full transition-[width] duration-300 ease-out motion-reduce:transition-none"
118
+ style={{
119
+ width: `${Math.max((item.value / max) * 100, 2)}%`,
120
+ background: colour,
121
+ }}
122
+ />
123
+ </div>
124
+ </li>
125
+ );
126
+ })}
127
+ </ul>
128
+ );
129
+ }
130
+
131
+ export function StackedBarChart({
132
+ slices,
133
+ label,
134
+ formatValue,
135
+ className,
136
+ }: StackedBarChartProps) {
137
+ const { series } = useChartTokens();
138
+ const visible = slices.filter((slice) => slice.value > 0);
139
+ const total = visible.reduce((sum, slice) => sum + slice.value, 0);
140
+ if (total <= 0) return <EmptyChart />;
141
+
142
+ return (
143
+ <div className={cn("space-y-3", className)}>
144
+ <div
145
+ role="img"
146
+ aria-label={label}
147
+ className="flex h-3 overflow-hidden rounded-full bg-ph-muted ring-1 ring-ph-border"
148
+ >
149
+ {visible.map((slice, index) => (
150
+ <span
151
+ key={slice.label}
152
+ className="h-full"
153
+ style={{
154
+ width: `${Math.max((slice.value / total) * 100, 2)}%`,
155
+ background: sliceColour(slice, index, series),
156
+ }}
157
+ />
158
+ ))}
159
+ </div>
160
+ <ul className="space-y-2">
161
+ {visible.map((slice, index) => {
162
+ const colour = sliceColour(slice, index, series);
163
+ return (
164
+ <li key={slice.label} className="flex items-center justify-between gap-3 text-sm">
165
+ <span className="flex min-w-0 items-center gap-2 text-ph-subtle">
166
+ <span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ background: colour }} />
167
+ <span className="truncate">{slice.label}</span>
168
+ </span>
169
+ <span className="shrink-0 tabular-nums text-ph-ink">
170
+ {formatValue ? formatValue(slice.value) : `${percent(slice.value, total)}%`}
171
+ </span>
172
+ </li>
173
+ );
174
+ })}
175
+ </ul>
176
+ </div>
177
+ );
178
+ }
@@ -0,0 +1,30 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { BarChart, DonutChart, StackedBarChart } from "./Charts";
5
+
6
+ const slices = [
7
+ { label: "Tides", value: 40 },
8
+ { label: "TurtleTime", value: 60 },
9
+ ];
10
+
11
+ describe("Charts", () => {
12
+ it("renders a labelled donut and a bar for each slice", () => {
13
+ render(<DonutChart label="Time by source" slices={slices} />);
14
+ expect(screen.getByRole("img", { name: "Time by source" })).toBeInTheDocument();
15
+ expect(screen.getByText("Tides")).toBeInTheDocument();
16
+ expect(screen.getByText("40%")).toBeInTheDocument();
17
+ });
18
+
19
+ it("renders bars with formatted values", () => {
20
+ render(<BarChart items={slices} formatValue={(value) => `${value}h`} />);
21
+ expect(screen.getByText("60h")).toBeInTheDocument();
22
+ });
23
+
24
+ it("renders a stacked mix and an empty state", () => {
25
+ const { rerender } = render(<StackedBarChart label="Work mix" slices={slices} />);
26
+ expect(screen.getByRole("img", { name: "Work mix" })).toBeInTheDocument();
27
+ rerender(<StackedBarChart label="Work mix" slices={[{ label: "None", value: 0 }]} />);
28
+ expect(screen.getByText("No data for this period.")).toBeInTheDocument();
29
+ });
30
+ });
@@ -264,3 +264,14 @@ export {
264
264
  Chip as Chip,
265
265
  type ChipProps as ChipProps,
266
266
  } from "@/components/ui/Chip";
267
+ export {
268
+ BarChart as BarChart,
269
+ DonutChart as DonutChart,
270
+ StackedBarChart as StackedBarChart,
271
+ useChartTokens as useChartTokens,
272
+ type BarChartProps as BarChartProps,
273
+ type ChartSlice as ChartSlice,
274
+ type ChartTokens as ChartTokens,
275
+ type DonutChartProps as DonutChartProps,
276
+ type StackedBarChartProps as StackedBarChartProps,
277
+ } from "@/components/ui/Charts";