@arbidocs/blocks 0.4.118 → 0.4.120

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.
@@ -0,0 +1,207 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var ui = require('@arbidocs/react/ui');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/components/UsageMeter.tsx
8
+ function usageMeterState(used, allowance) {
9
+ if (allowance <= 0) return used > 0 ? "over" : "under";
10
+ const ratio = used / allowance;
11
+ return ratio >= 1 ? "over" : ratio >= 0.8 ? "near" : "under";
12
+ }
13
+ var STATE_TONE = {
14
+ under: "bg-primary",
15
+ near: "bg-yellow-500",
16
+ over: "bg-red-500"
17
+ };
18
+ var pctOf = (n, scale) => scale > 0 ? Math.max(0, Math.min(100, n / scale * 100)) : 0;
19
+ var SWATCH = {
20
+ plan: "bg-muted-foreground/40",
21
+ topUps: "bg-primary/30",
22
+ autoRoom: "bg-muted"
23
+ };
24
+ var HATCH = {
25
+ backgroundImage: "repeating-linear-gradient(135deg, currentColor 0 1.5px, transparent 1.5px 5px)"
26
+ };
27
+ function AllowanceBar({ allowance, label }) {
28
+ const { used, plan } = allowance;
29
+ const topUps = Math.max(0, allowance.topUps ?? 0);
30
+ const room = Math.max(0, allowance.autoRoom ?? 0);
31
+ const total = plan + topUps;
32
+ const scale = Math.max(total + room, used, Number.EPSILON);
33
+ const state = usageMeterState(used, total);
34
+ const usedPct = total > 0 ? Math.round(used / total * 100) : used > 0 ? 100 : 0;
35
+ const markPlan = plan > 0 && plan < scale && (topUps > 0 || room > 0);
36
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative h-2 overflow-hidden rounded-full bg-muted", "data-slot": "usage-meter-bar", children: [
37
+ topUps > 0 && /* @__PURE__ */ jsxRuntime.jsx(
38
+ "div",
39
+ {
40
+ className: ui.cn("absolute inset-y-0", SWATCH.topUps),
41
+ style: { left: `${pctOf(plan, scale)}%`, width: `${pctOf(topUps, scale)}%` },
42
+ "data-slot": "usage-meter-topups"
43
+ }
44
+ ),
45
+ room > 0 && /* @__PURE__ */ jsxRuntime.jsx(
46
+ "div",
47
+ {
48
+ className: "absolute inset-y-0 text-muted-foreground/35",
49
+ style: { left: `${pctOf(total, scale)}%`, width: `${pctOf(room, scale)}%`, ...HATCH },
50
+ "data-slot": "usage-meter-auto-room"
51
+ }
52
+ ),
53
+ /* @__PURE__ */ jsxRuntime.jsx(
54
+ "div",
55
+ {
56
+ className: ui.cn(
57
+ "absolute inset-y-0 left-0 rounded-full transition-[width]",
58
+ STATE_TONE[state]
59
+ ),
60
+ style: { width: `${pctOf(Math.min(used, scale), scale)}%` },
61
+ role: "progressbar",
62
+ "aria-valuenow": Math.min(usedPct, 100),
63
+ "aria-valuemin": 0,
64
+ "aria-valuemax": 100,
65
+ "aria-label": label,
66
+ "data-state": state
67
+ }
68
+ ),
69
+ markPlan && /* @__PURE__ */ jsxRuntime.jsx(
70
+ "div",
71
+ {
72
+ className: "absolute inset-y-0 w-0.5 -translate-x-1/2 bg-foreground/70",
73
+ style: { left: `${pctOf(plan, scale)}%` },
74
+ "aria-hidden": true,
75
+ "data-slot": "usage-meter-plan-mark"
76
+ }
77
+ ),
78
+ room > 0 && total > 0 && /* @__PURE__ */ jsxRuntime.jsx(
79
+ "div",
80
+ {
81
+ className: "absolute inset-y-0 w-px bg-foreground/40",
82
+ style: { left: `${pctOf(total, scale)}%` },
83
+ "aria-hidden": true
84
+ }
85
+ )
86
+ ] });
87
+ }
88
+ function UsageMeter({
89
+ icon: Icon,
90
+ label,
91
+ value,
92
+ pct = 0,
93
+ hint,
94
+ allowance,
95
+ legend,
96
+ details,
97
+ actions,
98
+ className,
99
+ testId
100
+ }) {
101
+ const clamped = Math.max(0, Math.min(100, pct));
102
+ const tone = clamped >= 100 ? "bg-red-500" : clamped >= 50 ? "bg-yellow-500" : "bg-primary";
103
+ const state = allowance ? usageMeterState(allowance.used, allowance.plan + Math.max(0, allowance.topUps ?? 0)) : void 0;
104
+ const legendItems = legend ? [
105
+ ["plan", legend.plan],
106
+ ["topUps", legend.topUps],
107
+ ["autoRoom", legend.autoRoom]
108
+ ].filter(([, text]) => !!text) : [];
109
+ const meter = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
110
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 text-sm", children: [
111
+ /* @__PURE__ */ jsxRuntime.jsxs(
112
+ "span",
113
+ {
114
+ className: ui.cn(
115
+ "flex min-w-0 items-center gap-1.5",
116
+ allowance ? "font-medium text-foreground" : "text-muted-foreground"
117
+ ),
118
+ children: [
119
+ Icon && /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "size-3.5 shrink-0" }),
120
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
121
+ label,
122
+ hint ? ` (${hint})` : ""
123
+ ] })
124
+ ]
125
+ }
126
+ ),
127
+ /* @__PURE__ */ jsxRuntime.jsx(
128
+ "span",
129
+ {
130
+ className: "shrink-0 font-medium tabular-nums text-foreground",
131
+ "data-slot": "usage-meter-value",
132
+ children: value
133
+ }
134
+ )
135
+ ] }),
136
+ allowance ? /* @__PURE__ */ jsxRuntime.jsx(AllowanceBar, { allowance, label }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1.5 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsxRuntime.jsx(
137
+ "div",
138
+ {
139
+ className: ui.cn("h-full rounded-full transition-[width]", tone),
140
+ style: { width: `${clamped}%` },
141
+ role: "progressbar",
142
+ "aria-valuenow": clamped,
143
+ "aria-valuemin": 0,
144
+ "aria-valuemax": 100,
145
+ "aria-label": label
146
+ }
147
+ ) }),
148
+ legendItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
149
+ "ul",
150
+ {
151
+ className: "flex flex-wrap gap-x-4 gap-y-1 text-xs tabular-nums text-muted-foreground",
152
+ "data-slot": "usage-meter-legend",
153
+ children: legendItems.map(([key, text]) => /* @__PURE__ */ jsxRuntime.jsxs(
154
+ "li",
155
+ {
156
+ className: "flex items-center gap-1.5",
157
+ "data-slot": `usage-meter-legend-${key}`,
158
+ children: [
159
+ /* @__PURE__ */ jsxRuntime.jsx(
160
+ "span",
161
+ {
162
+ className: ui.cn(
163
+ "inline-block size-2 rounded-sm",
164
+ SWATCH[key],
165
+ key === "autoRoom" && "text-muted-foreground/60"
166
+ ),
167
+ style: key === "autoRoom" ? HATCH : void 0,
168
+ "aria-hidden": true
169
+ }
170
+ ),
171
+ text
172
+ ]
173
+ },
174
+ key
175
+ ))
176
+ }
177
+ ),
178
+ details && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1 text-xs text-muted-foreground", children: details })
179
+ ] });
180
+ return /* @__PURE__ */ jsxRuntime.jsxs(
181
+ "div",
182
+ {
183
+ className: ui.cn(
184
+ actions ? "flex flex-wrap items-start gap-x-6 gap-y-3" : "space-y-1.5",
185
+ className
186
+ ),
187
+ "data-testid": testId,
188
+ "data-state": state,
189
+ children: [
190
+ meter,
191
+ actions && /* @__PURE__ */ jsxRuntime.jsx(
192
+ "div",
193
+ {
194
+ className: "flex shrink-0 flex-col items-stretch gap-2 sm:w-64",
195
+ "data-slot": "usage-meter-actions",
196
+ children: actions
197
+ }
198
+ )
199
+ ]
200
+ }
201
+ );
202
+ }
203
+
204
+ exports.UsageMeter = UsageMeter;
205
+ exports.usageMeterState = usageMeterState;
206
+ //# sourceMappingURL=chunk-A7NCSZAP.cjs.map
207
+ //# sourceMappingURL=chunk-A7NCSZAP.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/UsageMeter.tsx"],"names":["jsxs","jsx","cn"],"mappings":"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","file":"chunk-A7NCSZAP.cjs","sourcesContent":["import type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@arbidocs/react/ui'\n\n/**\n * What an allowance is made of, in the meter's own unit (£ of AI usage, GB, users): what was used, the plan's part\n * of the allowance, the top-ups on top of it, and how much more automatic top-ups may still add before a usage cap\n * (or budget) stops them.\n */\nexport interface UsageMeterAllowance {\n used: number\n /** The plan's part of the allowance (marked on the bar when top-ups or automatic room follow it). */\n plan: number\n /** Top-ups on top of the plan: the allowance now is `plan + topUps`. */\n topUps?: number\n /**\n * Room automatic top-ups may still add before the cap stops them (drawn hatched after the allowance).\n * `null` = no limit (nothing drawn: the room is unbounded); omitted = not applicable.\n */\n autoRoom?: number | null\n}\n\n/** Pressure on an allowance: under 80% used, 80% to under 100%, or at / over it. */\nexport type UsageMeterState = 'under' | 'near' | 'over'\n\n/** The allowance meter's pressure state for `used` of `allowance`. */\nexport function usageMeterState(used: number, allowance: number): UsageMeterState {\n if (allowance <= 0) return used > 0 ? 'over' : 'under'\n const ratio = used / allowance\n return ratio >= 1 ? 'over' : ratio >= 0.8 ? 'near' : 'under'\n}\n\nconst STATE_TONE: Record<UsageMeterState, string> = {\n under: 'bg-primary',\n near: 'bg-yellow-500',\n over: 'bg-red-500',\n}\n\nconst pctOf = (n: number, scale: number) =>\n scale > 0 ? Math.max(0, Math.min(100, (n / scale) * 100)) : 0\n\n/** Legend swatches, matching the bar's segments. */\nconst SWATCH = {\n plan: 'bg-muted-foreground/40',\n topUps: 'bg-primary/30',\n autoRoom: 'bg-muted',\n}\n\nconst HATCH = {\n backgroundImage: 'repeating-linear-gradient(135deg, currentColor 0 1.5px, transparent 1.5px 5px)',\n} as const\n\n/** The segmented bar of an allowance meter: used, the plan mark, top-ups, and the automatic room. */\nfunction AllowanceBar({ allowance, label }: { allowance: UsageMeterAllowance; label: string }) {\n const { used, plan } = allowance\n const topUps = Math.max(0, allowance.topUps ?? 0)\n const room = Math.max(0, allowance.autoRoom ?? 0)\n const total = plan + topUps\n const scale = Math.max(total + room, used, Number.EPSILON)\n const state = usageMeterState(used, total)\n const usedPct = total > 0 ? Math.round((used / total) * 100) : used > 0 ? 100 : 0\n const markPlan = plan > 0 && plan < scale && (topUps > 0 || room > 0)\n return (\n <div className=\"relative h-2 overflow-hidden rounded-full bg-muted\" data-slot=\"usage-meter-bar\">\n {topUps > 0 && (\n <div\n className={cn('absolute inset-y-0', SWATCH.topUps)}\n style={{ left: `${pctOf(plan, scale)}%`, width: `${pctOf(topUps, scale)}%` }}\n data-slot=\"usage-meter-topups\"\n />\n )}\n {room > 0 && (\n <div\n className=\"absolute inset-y-0 text-muted-foreground/35\"\n style={{ left: `${pctOf(total, scale)}%`, width: `${pctOf(room, scale)}%`, ...HATCH }}\n data-slot=\"usage-meter-auto-room\"\n />\n )}\n <div\n className={cn(\n 'absolute inset-y-0 left-0 rounded-full transition-[width]',\n STATE_TONE[state]\n )}\n style={{ width: `${pctOf(Math.min(used, scale), scale)}%` }}\n role=\"progressbar\"\n aria-valuenow={Math.min(usedPct, 100)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={label}\n data-state={state}\n />\n {markPlan && (\n <div\n className=\"absolute inset-y-0 w-0.5 -translate-x-1/2 bg-foreground/70\"\n style={{ left: `${pctOf(plan, scale)}%` }}\n aria-hidden\n data-slot=\"usage-meter-plan-mark\"\n />\n )}\n {room > 0 && total > 0 && (\n <div\n className=\"absolute inset-y-0 w-px bg-foreground/40\"\n style={{ left: `${pctOf(total, scale)}%` }}\n aria-hidden\n />\n )}\n </div>\n )\n}\n\n/**\n * One labelled quota meter: icon + label on the left, used-of-limit on the right, a thin bar underneath.\n *\n * Simple mode (`pct`): the bar's tone follows pressure with the shared green → yellow → red gradation — green while\n * there's room, yellow past the halfway mark, red at the limit — so a stack of meters reads at a glance which quota\n * is the one to think about.\n *\n * Allowance mode (`allowance`): the bar spans the allowance plus what automatic top-ups may still add; it marks the\n * plan's part, shades the top-ups, hatches the automatic room, and fills what was used (red at the allowance, yellow\n * from 80%). `legend` names the segments; `details` (next period, cap) sit under the bar and `actions` (+ / −) beside\n * it. The meter's `data-state` is `under` / `near` / `over`.\n */\nexport function UsageMeter({\n icon: Icon,\n label,\n value,\n pct = 0,\n hint,\n allowance,\n legend,\n details,\n actions,\n className,\n testId,\n}: {\n icon?: LucideIcon\n label: string\n /** Formatted used-of-limit text, e.g. \"1,200 / 25,000\" or \"40 / 500 MB\". */\n value: string\n /** Simple mode: fill percentage, 0–100 (values are clamped). Ignored with `allowance`. */\n pct?: number\n /** Inline parenthetical after the label, e.g. \"resets May 31, 2025\" —\n * same font as the label, so the meter never grows an extra row. */\n hint?: string\n /** Allowance mode: the allowance's parts, in the meter's unit. */\n allowance?: UsageMeterAllowance\n /** Formatted legend entries for the allowance's segments (each shown only when set). */\n legend?: { plan?: string; topUps?: string; autoRoom?: string }\n details?: ReactNode\n actions?: ReactNode\n className?: string\n testId?: string\n}) {\n const clamped = Math.max(0, Math.min(100, pct))\n // Shared green → yellow → red gradation (same thresholds as the project-card\n // quota badges): green with room, yellow past the halfway mark, red at limit.\n const tone = clamped >= 100 ? 'bg-red-500' : clamped >= 50 ? 'bg-yellow-500' : 'bg-primary'\n const state = allowance\n ? usageMeterState(allowance.used, allowance.plan + Math.max(0, allowance.topUps ?? 0))\n : undefined\n const legendItems = legend\n ? (\n [\n ['plan', legend.plan],\n ['topUps', legend.topUps],\n ['autoRoom', legend.autoRoom],\n ] as const\n ).filter(([, text]) => !!text)\n : []\n\n const meter = (\n <div className=\"min-w-0 flex-1 space-y-1.5\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span\n className={cn(\n 'flex min-w-0 items-center gap-1.5',\n allowance ? 'font-medium text-foreground' : 'text-muted-foreground'\n )}\n >\n {Icon && <Icon className=\"size-3.5 shrink-0\" />}\n <span className=\"truncate\">\n {label}\n {hint ? ` (${hint})` : ''}\n </span>\n </span>\n <span\n className=\"shrink-0 font-medium tabular-nums text-foreground\"\n data-slot=\"usage-meter-value\"\n >\n {value}\n </span>\n </div>\n {allowance ? (\n <AllowanceBar allowance={allowance} label={label} />\n ) : (\n <div className=\"h-1.5 overflow-hidden rounded-full bg-muted\">\n <div\n className={cn('h-full rounded-full transition-[width]', tone)}\n style={{ width: `${clamped}%` }}\n role=\"progressbar\"\n aria-valuenow={clamped}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={label}\n />\n </div>\n )}\n {legendItems.length > 0 && (\n <ul\n className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs tabular-nums text-muted-foreground\"\n data-slot=\"usage-meter-legend\"\n >\n {legendItems.map(([key, text]) => (\n <li\n key={key}\n className=\"flex items-center gap-1.5\"\n data-slot={`usage-meter-legend-${key}`}\n >\n <span\n className={cn(\n 'inline-block size-2 rounded-sm',\n SWATCH[key],\n key === 'autoRoom' && 'text-muted-foreground/60'\n )}\n style={key === 'autoRoom' ? HATCH : undefined}\n aria-hidden\n />\n {text}\n </li>\n ))}\n </ul>\n )}\n {details && <div className=\"space-y-1 text-xs text-muted-foreground\">{details}</div>}\n </div>\n )\n\n return (\n <div\n className={cn(\n actions ? 'flex flex-wrap items-start gap-x-6 gap-y-3' : 'space-y-1.5',\n className\n )}\n data-testid={testId}\n data-state={state}\n >\n {meter}\n {actions && (\n <div\n className=\"flex shrink-0 flex-col items-stretch gap-2 sm:w-64\"\n data-slot=\"usage-meter-actions\"\n >\n {actions}\n </div>\n )}\n </div>\n )\n}\n"]}
@@ -0,0 +1,204 @@
1
+ "use client";
2
+ import { cn } from '@arbidocs/react/ui';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/components/UsageMeter.tsx
6
+ function usageMeterState(used, allowance) {
7
+ if (allowance <= 0) return used > 0 ? "over" : "under";
8
+ const ratio = used / allowance;
9
+ return ratio >= 1 ? "over" : ratio >= 0.8 ? "near" : "under";
10
+ }
11
+ var STATE_TONE = {
12
+ under: "bg-primary",
13
+ near: "bg-yellow-500",
14
+ over: "bg-red-500"
15
+ };
16
+ var pctOf = (n, scale) => scale > 0 ? Math.max(0, Math.min(100, n / scale * 100)) : 0;
17
+ var SWATCH = {
18
+ plan: "bg-muted-foreground/40",
19
+ topUps: "bg-primary/30",
20
+ autoRoom: "bg-muted"
21
+ };
22
+ var HATCH = {
23
+ backgroundImage: "repeating-linear-gradient(135deg, currentColor 0 1.5px, transparent 1.5px 5px)"
24
+ };
25
+ function AllowanceBar({ allowance, label }) {
26
+ const { used, plan } = allowance;
27
+ const topUps = Math.max(0, allowance.topUps ?? 0);
28
+ const room = Math.max(0, allowance.autoRoom ?? 0);
29
+ const total = plan + topUps;
30
+ const scale = Math.max(total + room, used, Number.EPSILON);
31
+ const state = usageMeterState(used, total);
32
+ const usedPct = total > 0 ? Math.round(used / total * 100) : used > 0 ? 100 : 0;
33
+ const markPlan = plan > 0 && plan < scale && (topUps > 0 || room > 0);
34
+ return /* @__PURE__ */ jsxs("div", { className: "relative h-2 overflow-hidden rounded-full bg-muted", "data-slot": "usage-meter-bar", children: [
35
+ topUps > 0 && /* @__PURE__ */ jsx(
36
+ "div",
37
+ {
38
+ className: cn("absolute inset-y-0", SWATCH.topUps),
39
+ style: { left: `${pctOf(plan, scale)}%`, width: `${pctOf(topUps, scale)}%` },
40
+ "data-slot": "usage-meter-topups"
41
+ }
42
+ ),
43
+ room > 0 && /* @__PURE__ */ jsx(
44
+ "div",
45
+ {
46
+ className: "absolute inset-y-0 text-muted-foreground/35",
47
+ style: { left: `${pctOf(total, scale)}%`, width: `${pctOf(room, scale)}%`, ...HATCH },
48
+ "data-slot": "usage-meter-auto-room"
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsx(
52
+ "div",
53
+ {
54
+ className: cn(
55
+ "absolute inset-y-0 left-0 rounded-full transition-[width]",
56
+ STATE_TONE[state]
57
+ ),
58
+ style: { width: `${pctOf(Math.min(used, scale), scale)}%` },
59
+ role: "progressbar",
60
+ "aria-valuenow": Math.min(usedPct, 100),
61
+ "aria-valuemin": 0,
62
+ "aria-valuemax": 100,
63
+ "aria-label": label,
64
+ "data-state": state
65
+ }
66
+ ),
67
+ markPlan && /* @__PURE__ */ jsx(
68
+ "div",
69
+ {
70
+ className: "absolute inset-y-0 w-0.5 -translate-x-1/2 bg-foreground/70",
71
+ style: { left: `${pctOf(plan, scale)}%` },
72
+ "aria-hidden": true,
73
+ "data-slot": "usage-meter-plan-mark"
74
+ }
75
+ ),
76
+ room > 0 && total > 0 && /* @__PURE__ */ jsx(
77
+ "div",
78
+ {
79
+ className: "absolute inset-y-0 w-px bg-foreground/40",
80
+ style: { left: `${pctOf(total, scale)}%` },
81
+ "aria-hidden": true
82
+ }
83
+ )
84
+ ] });
85
+ }
86
+ function UsageMeter({
87
+ icon: Icon,
88
+ label,
89
+ value,
90
+ pct = 0,
91
+ hint,
92
+ allowance,
93
+ legend,
94
+ details,
95
+ actions,
96
+ className,
97
+ testId
98
+ }) {
99
+ const clamped = Math.max(0, Math.min(100, pct));
100
+ const tone = clamped >= 100 ? "bg-red-500" : clamped >= 50 ? "bg-yellow-500" : "bg-primary";
101
+ const state = allowance ? usageMeterState(allowance.used, allowance.plan + Math.max(0, allowance.topUps ?? 0)) : void 0;
102
+ const legendItems = legend ? [
103
+ ["plan", legend.plan],
104
+ ["topUps", legend.topUps],
105
+ ["autoRoom", legend.autoRoom]
106
+ ].filter(([, text]) => !!text) : [];
107
+ const meter = /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
108
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 text-sm", children: [
109
+ /* @__PURE__ */ jsxs(
110
+ "span",
111
+ {
112
+ className: cn(
113
+ "flex min-w-0 items-center gap-1.5",
114
+ allowance ? "font-medium text-foreground" : "text-muted-foreground"
115
+ ),
116
+ children: [
117
+ Icon && /* @__PURE__ */ jsx(Icon, { className: "size-3.5 shrink-0" }),
118
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
119
+ label,
120
+ hint ? ` (${hint})` : ""
121
+ ] })
122
+ ]
123
+ }
124
+ ),
125
+ /* @__PURE__ */ jsx(
126
+ "span",
127
+ {
128
+ className: "shrink-0 font-medium tabular-nums text-foreground",
129
+ "data-slot": "usage-meter-value",
130
+ children: value
131
+ }
132
+ )
133
+ ] }),
134
+ allowance ? /* @__PURE__ */ jsx(AllowanceBar, { allowance, label }) : /* @__PURE__ */ jsx("div", { className: "h-1.5 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx(
135
+ "div",
136
+ {
137
+ className: cn("h-full rounded-full transition-[width]", tone),
138
+ style: { width: `${clamped}%` },
139
+ role: "progressbar",
140
+ "aria-valuenow": clamped,
141
+ "aria-valuemin": 0,
142
+ "aria-valuemax": 100,
143
+ "aria-label": label
144
+ }
145
+ ) }),
146
+ legendItems.length > 0 && /* @__PURE__ */ jsx(
147
+ "ul",
148
+ {
149
+ className: "flex flex-wrap gap-x-4 gap-y-1 text-xs tabular-nums text-muted-foreground",
150
+ "data-slot": "usage-meter-legend",
151
+ children: legendItems.map(([key, text]) => /* @__PURE__ */ jsxs(
152
+ "li",
153
+ {
154
+ className: "flex items-center gap-1.5",
155
+ "data-slot": `usage-meter-legend-${key}`,
156
+ children: [
157
+ /* @__PURE__ */ jsx(
158
+ "span",
159
+ {
160
+ className: cn(
161
+ "inline-block size-2 rounded-sm",
162
+ SWATCH[key],
163
+ key === "autoRoom" && "text-muted-foreground/60"
164
+ ),
165
+ style: key === "autoRoom" ? HATCH : void 0,
166
+ "aria-hidden": true
167
+ }
168
+ ),
169
+ text
170
+ ]
171
+ },
172
+ key
173
+ ))
174
+ }
175
+ ),
176
+ details && /* @__PURE__ */ jsx("div", { className: "space-y-1 text-xs text-muted-foreground", children: details })
177
+ ] });
178
+ return /* @__PURE__ */ jsxs(
179
+ "div",
180
+ {
181
+ className: cn(
182
+ actions ? "flex flex-wrap items-start gap-x-6 gap-y-3" : "space-y-1.5",
183
+ className
184
+ ),
185
+ "data-testid": testId,
186
+ "data-state": state,
187
+ children: [
188
+ meter,
189
+ actions && /* @__PURE__ */ jsx(
190
+ "div",
191
+ {
192
+ className: "flex shrink-0 flex-col items-stretch gap-2 sm:w-64",
193
+ "data-slot": "usage-meter-actions",
194
+ children: actions
195
+ }
196
+ )
197
+ ]
198
+ }
199
+ );
200
+ }
201
+
202
+ export { UsageMeter, usageMeterState };
203
+ //# sourceMappingURL=chunk-IBZJYCYM.js.map
204
+ //# sourceMappingURL=chunk-IBZJYCYM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/UsageMeter.tsx"],"names":[],"mappings":"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","file":"chunk-IBZJYCYM.js","sourcesContent":["import type { ReactNode } from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@arbidocs/react/ui'\n\n/**\n * What an allowance is made of, in the meter's own unit (£ of AI usage, GB, users): what was used, the plan's part\n * of the allowance, the top-ups on top of it, and how much more automatic top-ups may still add before a usage cap\n * (or budget) stops them.\n */\nexport interface UsageMeterAllowance {\n used: number\n /** The plan's part of the allowance (marked on the bar when top-ups or automatic room follow it). */\n plan: number\n /** Top-ups on top of the plan: the allowance now is `plan + topUps`. */\n topUps?: number\n /**\n * Room automatic top-ups may still add before the cap stops them (drawn hatched after the allowance).\n * `null` = no limit (nothing drawn: the room is unbounded); omitted = not applicable.\n */\n autoRoom?: number | null\n}\n\n/** Pressure on an allowance: under 80% used, 80% to under 100%, or at / over it. */\nexport type UsageMeterState = 'under' | 'near' | 'over'\n\n/** The allowance meter's pressure state for `used` of `allowance`. */\nexport function usageMeterState(used: number, allowance: number): UsageMeterState {\n if (allowance <= 0) return used > 0 ? 'over' : 'under'\n const ratio = used / allowance\n return ratio >= 1 ? 'over' : ratio >= 0.8 ? 'near' : 'under'\n}\n\nconst STATE_TONE: Record<UsageMeterState, string> = {\n under: 'bg-primary',\n near: 'bg-yellow-500',\n over: 'bg-red-500',\n}\n\nconst pctOf = (n: number, scale: number) =>\n scale > 0 ? Math.max(0, Math.min(100, (n / scale) * 100)) : 0\n\n/** Legend swatches, matching the bar's segments. */\nconst SWATCH = {\n plan: 'bg-muted-foreground/40',\n topUps: 'bg-primary/30',\n autoRoom: 'bg-muted',\n}\n\nconst HATCH = {\n backgroundImage: 'repeating-linear-gradient(135deg, currentColor 0 1.5px, transparent 1.5px 5px)',\n} as const\n\n/** The segmented bar of an allowance meter: used, the plan mark, top-ups, and the automatic room. */\nfunction AllowanceBar({ allowance, label }: { allowance: UsageMeterAllowance; label: string }) {\n const { used, plan } = allowance\n const topUps = Math.max(0, allowance.topUps ?? 0)\n const room = Math.max(0, allowance.autoRoom ?? 0)\n const total = plan + topUps\n const scale = Math.max(total + room, used, Number.EPSILON)\n const state = usageMeterState(used, total)\n const usedPct = total > 0 ? Math.round((used / total) * 100) : used > 0 ? 100 : 0\n const markPlan = plan > 0 && plan < scale && (topUps > 0 || room > 0)\n return (\n <div className=\"relative h-2 overflow-hidden rounded-full bg-muted\" data-slot=\"usage-meter-bar\">\n {topUps > 0 && (\n <div\n className={cn('absolute inset-y-0', SWATCH.topUps)}\n style={{ left: `${pctOf(plan, scale)}%`, width: `${pctOf(topUps, scale)}%` }}\n data-slot=\"usage-meter-topups\"\n />\n )}\n {room > 0 && (\n <div\n className=\"absolute inset-y-0 text-muted-foreground/35\"\n style={{ left: `${pctOf(total, scale)}%`, width: `${pctOf(room, scale)}%`, ...HATCH }}\n data-slot=\"usage-meter-auto-room\"\n />\n )}\n <div\n className={cn(\n 'absolute inset-y-0 left-0 rounded-full transition-[width]',\n STATE_TONE[state]\n )}\n style={{ width: `${pctOf(Math.min(used, scale), scale)}%` }}\n role=\"progressbar\"\n aria-valuenow={Math.min(usedPct, 100)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={label}\n data-state={state}\n />\n {markPlan && (\n <div\n className=\"absolute inset-y-0 w-0.5 -translate-x-1/2 bg-foreground/70\"\n style={{ left: `${pctOf(plan, scale)}%` }}\n aria-hidden\n data-slot=\"usage-meter-plan-mark\"\n />\n )}\n {room > 0 && total > 0 && (\n <div\n className=\"absolute inset-y-0 w-px bg-foreground/40\"\n style={{ left: `${pctOf(total, scale)}%` }}\n aria-hidden\n />\n )}\n </div>\n )\n}\n\n/**\n * One labelled quota meter: icon + label on the left, used-of-limit on the right, a thin bar underneath.\n *\n * Simple mode (`pct`): the bar's tone follows pressure with the shared green → yellow → red gradation — green while\n * there's room, yellow past the halfway mark, red at the limit — so a stack of meters reads at a glance which quota\n * is the one to think about.\n *\n * Allowance mode (`allowance`): the bar spans the allowance plus what automatic top-ups may still add; it marks the\n * plan's part, shades the top-ups, hatches the automatic room, and fills what was used (red at the allowance, yellow\n * from 80%). `legend` names the segments; `details` (next period, cap) sit under the bar and `actions` (+ / −) beside\n * it. The meter's `data-state` is `under` / `near` / `over`.\n */\nexport function UsageMeter({\n icon: Icon,\n label,\n value,\n pct = 0,\n hint,\n allowance,\n legend,\n details,\n actions,\n className,\n testId,\n}: {\n icon?: LucideIcon\n label: string\n /** Formatted used-of-limit text, e.g. \"1,200 / 25,000\" or \"40 / 500 MB\". */\n value: string\n /** Simple mode: fill percentage, 0–100 (values are clamped). Ignored with `allowance`. */\n pct?: number\n /** Inline parenthetical after the label, e.g. \"resets May 31, 2025\" —\n * same font as the label, so the meter never grows an extra row. */\n hint?: string\n /** Allowance mode: the allowance's parts, in the meter's unit. */\n allowance?: UsageMeterAllowance\n /** Formatted legend entries for the allowance's segments (each shown only when set). */\n legend?: { plan?: string; topUps?: string; autoRoom?: string }\n details?: ReactNode\n actions?: ReactNode\n className?: string\n testId?: string\n}) {\n const clamped = Math.max(0, Math.min(100, pct))\n // Shared green → yellow → red gradation (same thresholds as the project-card\n // quota badges): green with room, yellow past the halfway mark, red at limit.\n const tone = clamped >= 100 ? 'bg-red-500' : clamped >= 50 ? 'bg-yellow-500' : 'bg-primary'\n const state = allowance\n ? usageMeterState(allowance.used, allowance.plan + Math.max(0, allowance.topUps ?? 0))\n : undefined\n const legendItems = legend\n ? (\n [\n ['plan', legend.plan],\n ['topUps', legend.topUps],\n ['autoRoom', legend.autoRoom],\n ] as const\n ).filter(([, text]) => !!text)\n : []\n\n const meter = (\n <div className=\"min-w-0 flex-1 space-y-1.5\">\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span\n className={cn(\n 'flex min-w-0 items-center gap-1.5',\n allowance ? 'font-medium text-foreground' : 'text-muted-foreground'\n )}\n >\n {Icon && <Icon className=\"size-3.5 shrink-0\" />}\n <span className=\"truncate\">\n {label}\n {hint ? ` (${hint})` : ''}\n </span>\n </span>\n <span\n className=\"shrink-0 font-medium tabular-nums text-foreground\"\n data-slot=\"usage-meter-value\"\n >\n {value}\n </span>\n </div>\n {allowance ? (\n <AllowanceBar allowance={allowance} label={label} />\n ) : (\n <div className=\"h-1.5 overflow-hidden rounded-full bg-muted\">\n <div\n className={cn('h-full rounded-full transition-[width]', tone)}\n style={{ width: `${clamped}%` }}\n role=\"progressbar\"\n aria-valuenow={clamped}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={label}\n />\n </div>\n )}\n {legendItems.length > 0 && (\n <ul\n className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs tabular-nums text-muted-foreground\"\n data-slot=\"usage-meter-legend\"\n >\n {legendItems.map(([key, text]) => (\n <li\n key={key}\n className=\"flex items-center gap-1.5\"\n data-slot={`usage-meter-legend-${key}`}\n >\n <span\n className={cn(\n 'inline-block size-2 rounded-sm',\n SWATCH[key],\n key === 'autoRoom' && 'text-muted-foreground/60'\n )}\n style={key === 'autoRoom' ? HATCH : undefined}\n aria-hidden\n />\n {text}\n </li>\n ))}\n </ul>\n )}\n {details && <div className=\"space-y-1 text-xs text-muted-foreground\">{details}</div>}\n </div>\n )\n\n return (\n <div\n className={cn(\n actions ? 'flex flex-wrap items-start gap-x-6 gap-y-3' : 'space-y-1.5',\n className\n )}\n data-testid={testId}\n data-state={state}\n >\n {meter}\n {actions && (\n <div\n className=\"flex shrink-0 flex-col items-stretch gap-2 sm:w-64\"\n data-slot=\"usage-meter-actions\"\n >\n {actions}\n </div>\n )}\n </div>\n )\n}\n"]}
@@ -1,21 +1,58 @@
1
+ import type { ReactNode } from 'react';
1
2
  import type { LucideIcon } from 'lucide-react';
2
3
  /**
3
- * One labelled quota meter: icon + label on the left, used-of-limit on the
4
- * right, a thin bar underneath. The bar's tone follows pressure with the shared
5
- * green → yellow → red gradation — green while there's room, yellow past the
6
- * halfway mark, red at the limit — so a stack of meters reads at a glance which
7
- * quota is the one to think about.
4
+ * What an allowance is made of, in the meter's own unit (£ of AI usage, GB, users): what was used, the plan's part
5
+ * of the allowance, the top-ups on top of it, and how much more automatic top-ups may still add before a usage cap
6
+ * (or budget) stops them.
8
7
  */
9
- export declare function UsageMeter({ icon: Icon, label, value, pct, hint, className, testId, }: {
8
+ export interface UsageMeterAllowance {
9
+ used: number;
10
+ /** The plan's part of the allowance (marked on the bar when top-ups or automatic room follow it). */
11
+ plan: number;
12
+ /** Top-ups on top of the plan: the allowance now is `plan + topUps`. */
13
+ topUps?: number;
14
+ /**
15
+ * Room automatic top-ups may still add before the cap stops them (drawn hatched after the allowance).
16
+ * `null` = no limit (nothing drawn: the room is unbounded); omitted = not applicable.
17
+ */
18
+ autoRoom?: number | null;
19
+ }
20
+ /** Pressure on an allowance: under 80% used, 80% to under 100%, or at / over it. */
21
+ export type UsageMeterState = 'under' | 'near' | 'over';
22
+ /** The allowance meter's pressure state for `used` of `allowance`. */
23
+ export declare function usageMeterState(used: number, allowance: number): UsageMeterState;
24
+ /**
25
+ * One labelled quota meter: icon + label on the left, used-of-limit on the right, a thin bar underneath.
26
+ *
27
+ * Simple mode (`pct`): the bar's tone follows pressure with the shared green → yellow → red gradation — green while
28
+ * there's room, yellow past the halfway mark, red at the limit — so a stack of meters reads at a glance which quota
29
+ * is the one to think about.
30
+ *
31
+ * Allowance mode (`allowance`): the bar spans the allowance plus what automatic top-ups may still add; it marks the
32
+ * plan's part, shades the top-ups, hatches the automatic room, and fills what was used (red at the allowance, yellow
33
+ * from 80%). `legend` names the segments; `details` (next period, cap) sit under the bar and `actions` (+ / −) beside
34
+ * it. The meter's `data-state` is `under` / `near` / `over`.
35
+ */
36
+ export declare function UsageMeter({ icon: Icon, label, value, pct, hint, allowance, legend, details, actions, className, testId, }: {
10
37
  icon?: LucideIcon;
11
38
  label: string;
12
39
  /** Formatted used-of-limit text, e.g. "1,200 / 25,000" or "40 / 500 MB". */
13
40
  value: string;
14
- /** Fill percentage, 0–100 (values are clamped). */
15
- pct: number;
41
+ /** Simple mode: fill percentage, 0–100 (values are clamped). Ignored with `allowance`. */
42
+ pct?: number;
16
43
  /** Inline parenthetical after the label, e.g. "resets May 31, 2025" —
17
44
  * same font as the label, so the meter never grows an extra row. */
18
45
  hint?: string;
46
+ /** Allowance mode: the allowance's parts, in the meter's unit. */
47
+ allowance?: UsageMeterAllowance;
48
+ /** Formatted legend entries for the allowance's segments (each shown only when set). */
49
+ legend?: {
50
+ plan?: string;
51
+ topUps?: string;
52
+ autoRoom?: string;
53
+ };
54
+ details?: ReactNode;
55
+ actions?: ReactNode;
19
56
  className?: string;
20
57
  testId?: string;
21
58
  }): import("react").JSX.Element;