@momentumworld/ui-react 0.17.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.
Files changed (138) hide show
  1. package/dist/chunk-2HQPCV2L.js +895 -0
  2. package/dist/chunk-2HQPCV2L.js.map +1 -0
  3. package/dist/form.d.ts +175 -0
  4. package/dist/form.js +356 -0
  5. package/dist/form.js.map +1 -0
  6. package/dist/index.d.ts +2162 -0
  7. package/dist/index.js +11640 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/input-JCZs61MO.d.ts +24 -0
  10. package/package.json +90 -0
  11. package/src/components/form/checkbox-group-field.tsx +101 -0
  12. package/src/components/form/date-field.tsx +79 -0
  13. package/src/components/form/date-range-field.tsx +106 -0
  14. package/src/components/form/form-context.ts +10 -0
  15. package/src/components/form/form.tsx +54 -0
  16. package/src/components/form/number-field.tsx +69 -0
  17. package/src/components/form/select-field.tsx +76 -0
  18. package/src/components/form/submit-button.tsx +28 -0
  19. package/src/components/form/text-field.tsx +107 -0
  20. package/src/components/layout/dashboard-header.tsx +54 -0
  21. package/src/components/layout/dashboard-panel.tsx +34 -0
  22. package/src/components/layout/dashboard-topbar.tsx +221 -0
  23. package/src/components/theme-provider.tsx +403 -0
  24. package/src/components/ui/accordion.tsx +69 -0
  25. package/src/components/ui/alert-dialog.tsx +169 -0
  26. package/src/components/ui/alert.tsx +80 -0
  27. package/src/components/ui/animated-counter.tsx +109 -0
  28. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  29. package/src/components/ui/app-store-buttons.tsx +182 -0
  30. package/src/components/ui/aspect-ratio.tsx +23 -0
  31. package/src/components/ui/aurora-orb.tsx +163 -0
  32. package/src/components/ui/aurora-text.tsx +76 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +150 -0
  35. package/src/components/ui/avatar.tsx +285 -0
  36. package/src/components/ui/badge-group.tsx +160 -0
  37. package/src/components/ui/badge.tsx +172 -0
  38. package/src/components/ui/breadcrumb.tsx +112 -0
  39. package/src/components/ui/button.tsx +77 -0
  40. package/src/components/ui/calendar.tsx +137 -0
  41. package/src/components/ui/card.tsx +244 -0
  42. package/src/components/ui/carousel.tsx +258 -0
  43. package/src/components/ui/chart.tsx +379 -0
  44. package/src/components/ui/chat-divider.tsx +67 -0
  45. package/src/components/ui/chat.tsx +494 -0
  46. package/src/components/ui/checkbox-group.tsx +16 -0
  47. package/src/components/ui/checkbox.tsx +82 -0
  48. package/src/components/ui/collapsible.tsx +45 -0
  49. package/src/components/ui/color-picker.tsx +389 -0
  50. package/src/components/ui/combobox.tsx +411 -0
  51. package/src/components/ui/command.tsx +264 -0
  52. package/src/components/ui/confetti.tsx +196 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/context-meter.tsx +134 -0
  55. package/src/components/ui/credit-card.tsx +214 -0
  56. package/src/components/ui/data-table-paged.tsx +163 -0
  57. package/src/components/ui/date-picker.tsx +154 -0
  58. package/src/components/ui/dialog.tsx +196 -0
  59. package/src/components/ui/drawer.tsx +135 -0
  60. package/src/components/ui/empty.tsx +127 -0
  61. package/src/components/ui/fab.tsx +45 -0
  62. package/src/components/ui/featured-icon.tsx +149 -0
  63. package/src/components/ui/field.tsx +88 -0
  64. package/src/components/ui/fieldset.tsx +29 -0
  65. package/src/components/ui/form.tsx +17 -0
  66. package/src/components/ui/frame.tsx +82 -0
  67. package/src/components/ui/generic-empty.tsx +142 -0
  68. package/src/components/ui/group.tsx +97 -0
  69. package/src/components/ui/help-tip.tsx +94 -0
  70. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  71. package/src/components/ui/input-group.tsx +102 -0
  72. package/src/components/ui/input-otp.tsx +96 -0
  73. package/src/components/ui/input.tsx +66 -0
  74. package/src/components/ui/item.tsx +198 -0
  75. package/src/components/ui/kbd.tsx +30 -0
  76. package/src/components/ui/label.tsx +28 -0
  77. package/src/components/ui/markdown.tsx +188 -0
  78. package/src/components/ui/menu.tsx +312 -0
  79. package/src/components/ui/menubar.tsx +93 -0
  80. package/src/components/ui/meter.tsx +67 -0
  81. package/src/components/ui/multi-select.tsx +308 -0
  82. package/src/components/ui/navigation-menu.tsx +143 -0
  83. package/src/components/ui/number-field.tsx +160 -0
  84. package/src/components/ui/pagination-controls.tsx +74 -0
  85. package/src/components/ui/pagination.tsx +149 -0
  86. package/src/components/ui/pipeline.tsx +102 -0
  87. package/src/components/ui/popover.tsx +119 -0
  88. package/src/components/ui/preview-card.tsx +55 -0
  89. package/src/components/ui/progress.tsx +289 -0
  90. package/src/components/ui/qr-code.tsx +150 -0
  91. package/src/components/ui/radio-group.tsx +103 -0
  92. package/src/components/ui/resizable.tsx +56 -0
  93. package/src/components/ui/scroll-area.tsx +90 -0
  94. package/src/components/ui/scroller.tsx +38 -0
  95. package/src/components/ui/section-header.tsx +118 -0
  96. package/src/components/ui/segmented-control.tsx +83 -0
  97. package/src/components/ui/select.tsx +181 -0
  98. package/src/components/ui/separator.tsx +23 -0
  99. package/src/components/ui/sheet.tsx +224 -0
  100. package/src/components/ui/sidebar.tsx +774 -0
  101. package/src/components/ui/skeleton.tsx +16 -0
  102. package/src/components/ui/slider.tsx +108 -0
  103. package/src/components/ui/smooth-scroll.tsx +206 -0
  104. package/src/components/ui/social-button.tsx +247 -0
  105. package/src/components/ui/spinner-on-demand.tsx +32 -0
  106. package/src/components/ui/spinner.tsx +18 -0
  107. package/src/components/ui/stat.tsx +187 -0
  108. package/src/components/ui/stepper.tsx +167 -0
  109. package/src/components/ui/switch.tsx +56 -0
  110. package/src/components/ui/table.tsx +126 -0
  111. package/src/components/ui/tabs.tsx +90 -0
  112. package/src/components/ui/tag.tsx +242 -0
  113. package/src/components/ui/target-countdown.tsx +46 -0
  114. package/src/components/ui/text-editor.tsx +313 -0
  115. package/src/components/ui/textarea.tsx +51 -0
  116. package/src/components/ui/thinking-orb.tsx +6 -0
  117. package/src/components/ui/time-picker.tsx +308 -0
  118. package/src/components/ui/timeline.tsx +116 -0
  119. package/src/components/ui/toast.tsx +268 -0
  120. package/src/components/ui/toggle-group.tsx +101 -0
  121. package/src/components/ui/toggle.tsx +45 -0
  122. package/src/components/ui/toolbar.tsx +89 -0
  123. package/src/components/ui/tooltip.tsx +102 -0
  124. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  125. package/src/components/ui/video-player.tsx +275 -0
  126. package/src/components/upload/avatar-upload-base.tsx +131 -0
  127. package/src/components/upload/image-upload-base.tsx +112 -0
  128. package/src/form.ts +17 -0
  129. package/src/index.ts +143 -0
  130. package/src/lib/hooks/use-callback-ref.ts +15 -0
  131. package/src/lib/hooks/use-first-render.ts +11 -0
  132. package/src/lib/hooks/use-hover.ts +53 -0
  133. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  134. package/src/lib/hooks/use-media-query.ts +164 -0
  135. package/src/lib/utils/css.ts +6 -0
  136. package/src/styles.css +329 -0
  137. package/src/types/helpers.ts +24 -0
  138. package/src/types/react.d.ts +7 -0
@@ -0,0 +1,196 @@
1
+ "use client";
2
+
3
+ import confetti from "canvas-confetti";
4
+ import type {
5
+ GlobalOptions as ConfettiGlobalOptions,
6
+ CreateTypes as ConfettiInstance,
7
+ Options as ConfettiOptions,
8
+ } from "canvas-confetti";
9
+ import {
10
+ createContext,
11
+ forwardRef,
12
+ useCallback,
13
+ useContext,
14
+ useEffect,
15
+ useImperativeHandle,
16
+ useMemo,
17
+ useRef,
18
+ type ReactNode,
19
+ } from "react";
20
+ import type * as React from "react";
21
+
22
+ import { Button, type ButtonProps } from "./button";
23
+
24
+ /** The brand celebration palette — soft violet / pink / peach / cream. */
25
+ export const CONFETTI_COLORS = ["#a786ff", "#fd8bbc", "#eca184", "#f8deb1"] as const;
26
+
27
+ function prefersReducedMotion(): boolean {
28
+ return (
29
+ typeof window !== "undefined" &&
30
+ !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
31
+ );
32
+ }
33
+
34
+ export interface SideCannonsOptions {
35
+ /** How long the cannons keep firing, in ms (default 3000). */
36
+ durationMs?: number;
37
+ /** Confetti colors (default {@link CONFETTI_COLORS}). */
38
+ colors?: readonly string[];
39
+ /** Particles emitted per side per frame (default 2). */
40
+ particleCount?: number;
41
+ /** z-index of the confetti canvas (default 100). */
42
+ zIndex?: number;
43
+ }
44
+
45
+ /**
46
+ * Fires confetti from the left and right edges of the viewport for a few
47
+ * seconds — the "side cannons" celebration. Purely imperative, so it can be
48
+ * triggered from an effect (e.g. the first time a page loads) without rendering
49
+ * anything. No-ops on the server and when the user prefers reduced motion.
50
+ *
51
+ * @example
52
+ * useEffect(() => { sideCannons(); }, []);
53
+ */
54
+ export function sideCannons(options: SideCannonsOptions = {}): void {
55
+ if (typeof window === "undefined" || prefersReducedMotion()) return;
56
+
57
+ const {
58
+ durationMs = 3000,
59
+ colors = CONFETTI_COLORS,
60
+ particleCount = 2,
61
+ zIndex = 100,
62
+ } = options;
63
+
64
+ const end = performance.now() + durationMs;
65
+ const shared = { particleCount, spread: 55, startVelocity: 60, colors: [...colors], zIndex };
66
+
67
+ const frame = () => {
68
+ if (performance.now() > end) return;
69
+ void confetti({ ...shared, angle: 60, origin: { x: 0, y: 0.5 } });
70
+ void confetti({ ...shared, angle: 120, origin: { x: 1, y: 0.5 } });
71
+ requestAnimationFrame(frame);
72
+ };
73
+
74
+ frame();
75
+ }
76
+
77
+ type ConfettiApi = {
78
+ /** Fire the confetti instance, merging `options` over the component's. */
79
+ fire: (options?: ConfettiOptions) => void;
80
+ };
81
+
82
+ export type ConfettiRef = ConfettiApi | null;
83
+
84
+ export type ConfettiProps = React.ComponentPropsWithRef<"canvas"> & {
85
+ /** Per-shot options merged into every `fire()` call. */
86
+ options?: ConfettiOptions;
87
+ /** Global options passed to `confetti.create` (resize / worker). */
88
+ globalOptions?: ConfettiGlobalOptions;
89
+ /** Skip the automatic fire on mount — drive it via the ref instead. */
90
+ manualstart?: boolean;
91
+ children?: ReactNode;
92
+ };
93
+
94
+ const ConfettiContext = createContext<ConfettiApi>({} as ConfettiApi);
95
+
96
+ /**
97
+ * A canvas-bound confetti emitter. Renders its own `<canvas>` and exposes an
98
+ * imperative `fire()` via ref (and to descendants through context). By default
99
+ * it fires once on mount; pass `manualstart` to control it yourself.
100
+ *
101
+ * For a full-screen, no-render celebration prefer {@link sideCannons}.
102
+ */
103
+ const ConfettiComponent = forwardRef<ConfettiRef, ConfettiProps>((props, ref) => {
104
+ const {
105
+ options,
106
+ globalOptions = { resize: true, useWorker: true },
107
+ manualstart = false,
108
+ children,
109
+ ...rest
110
+ } = props;
111
+ const instanceRef = useRef<ConfettiInstance | null>(null);
112
+
113
+ const canvasRef = useCallback(
114
+ (node: HTMLCanvasElement | null) => {
115
+ if (node !== null) {
116
+ if (instanceRef.current) return;
117
+ instanceRef.current = confetti.create(node, { ...globalOptions, resize: true });
118
+ } else if (instanceRef.current) {
119
+ instanceRef.current.reset();
120
+ instanceRef.current = null;
121
+ }
122
+ },
123
+ [globalOptions],
124
+ );
125
+
126
+ const fire = useCallback(
127
+ async (opts: ConfettiOptions = {}) => {
128
+ if (prefersReducedMotion()) return;
129
+ try {
130
+ await instanceRef.current?.({ ...options, ...opts });
131
+ } catch (error) {
132
+ console.error("Confetti error:", error);
133
+ }
134
+ },
135
+ [options],
136
+ );
137
+
138
+ const api = useMemo<ConfettiApi>(() => ({ fire }), [fire]);
139
+
140
+ useImperativeHandle(ref, () => api, [api]);
141
+
142
+ useEffect(() => {
143
+ if (!manualstart) void fire();
144
+ }, [manualstart, fire]);
145
+
146
+ return (
147
+ <ConfettiContext.Provider value={api}>
148
+ <canvas data-slot="confetti" ref={canvasRef} {...rest} />
149
+ {children}
150
+ </ConfettiContext.Provider>
151
+ );
152
+ });
153
+ ConfettiComponent.displayName = "Confetti";
154
+
155
+ export const Confetti = ConfettiComponent;
156
+
157
+ /** Access the nearest {@link Confetti}'s imperative `fire()` from a child. */
158
+ export function useConfetti(): ConfettiApi {
159
+ return useContext(ConfettiContext);
160
+ }
161
+
162
+ export interface ConfettiButtonProps extends ButtonProps {
163
+ /** Confetti options; origin defaults to the button's center. */
164
+ options?: ConfettiOptions & ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
165
+ }
166
+
167
+ /**
168
+ * A {@link Button} that bursts confetti from its own center on click. Handy for
169
+ * "Claim", "Done", or other one-off celebratory actions.
170
+ */
171
+ function ConfettiButtonComponent({ options, children, onClick, ...props }: ConfettiButtonProps) {
172
+ const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
173
+ onClick?.(event);
174
+ if (prefersReducedMotion()) return;
175
+ try {
176
+ const rect = event.currentTarget.getBoundingClientRect();
177
+ const x = rect.left + rect.width / 2;
178
+ const y = rect.top + rect.height / 2;
179
+ await confetti({
180
+ ...options,
181
+ origin: { x: x / window.innerWidth, y: y / window.innerHeight },
182
+ });
183
+ } catch (error) {
184
+ console.error("Confetti button error:", error);
185
+ }
186
+ };
187
+
188
+ return (
189
+ <Button data-slot="confetti-button" onClick={handleClick} {...props}>
190
+ {children}
191
+ </Button>
192
+ );
193
+ }
194
+ ConfettiButtonComponent.displayName = "ConfettiButton";
195
+
196
+ export const ConfettiButton = ConfettiButtonComponent;
@@ -0,0 +1,271 @@
1
+ "use client";
2
+
3
+ import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";
4
+ import { CheckIcon, ChevronRightIcon } from "lucide-react";
5
+ import type * as React from "react";
6
+ import { cn } from "../../lib/utils/css";
7
+
8
+ function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
9
+ return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
10
+ }
11
+
12
+ function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
13
+ return (
14
+ <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
15
+ );
16
+ }
17
+
18
+ function ContextMenuTrigger({
19
+ className,
20
+ ...props
21
+ }: ContextMenuPrimitive.Trigger.Props) {
22
+ return (
23
+ <ContextMenuPrimitive.Trigger
24
+ data-slot="context-menu-trigger"
25
+ className={cn("select-none", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function ContextMenuContent({
32
+ className,
33
+ align = "start",
34
+ alignOffset = 4,
35
+ side = "right",
36
+ sideOffset = 0,
37
+ ...props
38
+ }: ContextMenuPrimitive.Popup.Props &
39
+ Pick<
40
+ ContextMenuPrimitive.Positioner.Props,
41
+ "align" | "alignOffset" | "side" | "sideOffset"
42
+ >) {
43
+ return (
44
+ <ContextMenuPrimitive.Portal>
45
+ <ContextMenuPrimitive.Positioner
46
+ className="isolate z-50 outline-none"
47
+ align={align}
48
+ alignOffset={alignOffset}
49
+ side={side}
50
+ sideOffset={sideOffset}
51
+ >
52
+ <ContextMenuPrimitive.Popup
53
+ data-slot="context-menu-content"
54
+ className={cn(
55
+ "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none",
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ </ContextMenuPrimitive.Positioner>
61
+ </ContextMenuPrimitive.Portal>
62
+ );
63
+ }
64
+
65
+ function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
66
+ return (
67
+ <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
68
+ );
69
+ }
70
+
71
+ function ContextMenuLabel({
72
+ className,
73
+ inset,
74
+ ...props
75
+ }: ContextMenuPrimitive.GroupLabel.Props & {
76
+ inset?: boolean;
77
+ }) {
78
+ return (
79
+ <ContextMenuPrimitive.GroupLabel
80
+ data-slot="context-menu-label"
81
+ data-inset={inset}
82
+ className={cn(
83
+ "text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7",
84
+ className,
85
+ )}
86
+ {...props}
87
+ />
88
+ );
89
+ }
90
+
91
+ function ContextMenuItem({
92
+ className,
93
+ inset,
94
+ variant = "default",
95
+ ...props
96
+ }: ContextMenuPrimitive.Item.Props & {
97
+ inset?: boolean;
98
+ variant?: "default" | "destructive";
99
+ }) {
100
+ return (
101
+ <ContextMenuPrimitive.Item
102
+ data-slot="context-menu-item"
103
+ data-inset={inset}
104
+ data-variant={variant}
105
+ className={cn(
106
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
107
+ className,
108
+ )}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
115
+ return (
116
+ <ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
117
+ );
118
+ }
119
+
120
+ function ContextMenuSubTrigger({
121
+ className,
122
+ inset,
123
+ children,
124
+ ...props
125
+ }: ContextMenuPrimitive.SubmenuTrigger.Props & {
126
+ inset?: boolean;
127
+ }) {
128
+ return (
129
+ <ContextMenuPrimitive.SubmenuTrigger
130
+ data-slot="context-menu-sub-trigger"
131
+ data-inset={inset}
132
+ className={cn(
133
+ "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0",
134
+ className,
135
+ )}
136
+ {...props}
137
+ >
138
+ {children}
139
+ <ChevronRightIcon className="cn-rtl-flip ml-auto" />
140
+ </ContextMenuPrimitive.SubmenuTrigger>
141
+ );
142
+ }
143
+
144
+ function ContextMenuSubContent({
145
+ ...props
146
+ }: React.ComponentProps<typeof ContextMenuContent>) {
147
+ return (
148
+ <ContextMenuContent
149
+ data-slot="context-menu-sub-content"
150
+ className="shadow-lg"
151
+ side="right"
152
+ {...props}
153
+ />
154
+ );
155
+ }
156
+
157
+ function ContextMenuCheckboxItem({
158
+ className,
159
+ children,
160
+ checked,
161
+ inset,
162
+ ...props
163
+ }: ContextMenuPrimitive.CheckboxItem.Props & {
164
+ inset?: boolean;
165
+ }) {
166
+ return (
167
+ <ContextMenuPrimitive.CheckboxItem
168
+ data-slot="context-menu-checkbox-item"
169
+ data-inset={inset}
170
+ className={cn(
171
+ "focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
172
+ className,
173
+ )}
174
+ checked={checked}
175
+ {...props}
176
+ >
177
+ <span className="absolute right-2 pointer-events-none">
178
+ <ContextMenuPrimitive.CheckboxItemIndicator>
179
+ <CheckIcon />
180
+ </ContextMenuPrimitive.CheckboxItemIndicator>
181
+ </span>
182
+ {children}
183
+ </ContextMenuPrimitive.CheckboxItem>
184
+ );
185
+ }
186
+
187
+ function ContextMenuRadioGroup({
188
+ ...props
189
+ }: ContextMenuPrimitive.RadioGroup.Props) {
190
+ return (
191
+ <ContextMenuPrimitive.RadioGroup
192
+ data-slot="context-menu-radio-group"
193
+ {...props}
194
+ />
195
+ );
196
+ }
197
+
198
+ function ContextMenuRadioItem({
199
+ className,
200
+ children,
201
+ inset,
202
+ ...props
203
+ }: ContextMenuPrimitive.RadioItem.Props & {
204
+ inset?: boolean;
205
+ }) {
206
+ return (
207
+ <ContextMenuPrimitive.RadioItem
208
+ data-slot="context-menu-radio-item"
209
+ data-inset={inset}
210
+ className={cn(
211
+ "focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
212
+ className,
213
+ )}
214
+ {...props}
215
+ >
216
+ <span className="absolute right-2 pointer-events-none">
217
+ <ContextMenuPrimitive.RadioItemIndicator>
218
+ <CheckIcon />
219
+ </ContextMenuPrimitive.RadioItemIndicator>
220
+ </span>
221
+ {children}
222
+ </ContextMenuPrimitive.RadioItem>
223
+ );
224
+ }
225
+
226
+ function ContextMenuSeparator({
227
+ className,
228
+ ...props
229
+ }: ContextMenuPrimitive.Separator.Props) {
230
+ return (
231
+ <ContextMenuPrimitive.Separator
232
+ data-slot="context-menu-separator"
233
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
234
+ {...props}
235
+ />
236
+ );
237
+ }
238
+
239
+ function ContextMenuShortcut({
240
+ className,
241
+ ...props
242
+ }: React.ComponentProps<"span">) {
243
+ return (
244
+ <span
245
+ data-slot="context-menu-shortcut"
246
+ className={cn(
247
+ "text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
248
+ className,
249
+ )}
250
+ {...props}
251
+ />
252
+ );
253
+ }
254
+
255
+ export {
256
+ ContextMenu,
257
+ ContextMenuTrigger,
258
+ ContextMenuContent,
259
+ ContextMenuItem,
260
+ ContextMenuCheckboxItem,
261
+ ContextMenuRadioItem,
262
+ ContextMenuLabel,
263
+ ContextMenuSeparator,
264
+ ContextMenuShortcut,
265
+ ContextMenuGroup,
266
+ ContextMenuPortal,
267
+ ContextMenuSub,
268
+ ContextMenuSubContent,
269
+ ContextMenuSubTrigger,
270
+ ContextMenuRadioGroup,
271
+ };
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { cn } from "../../lib/utils/css";
6
+
7
+ type ContextMeterState = "ok" | "warn" | "summarizing" | "full";
8
+
9
+ interface ContextMeterProps extends Omit<React.SVGProps<SVGSVGElement>, "children"> {
10
+ /** Tokens currently in context. Used with `budget` when `pct` is not given. */
11
+ used?: number;
12
+ /** The model's context budget in tokens. */
13
+ budget?: number;
14
+ /** Fill fraction 0..1. Takes precedence over `used`/`budget` when provided. */
15
+ pct?: number;
16
+ /** Visual state; derived from the thresholds below when omitted. */
17
+ state?: ContextMeterState;
18
+ /** Threshold fractions — mirror the backend `config.context` values. */
19
+ warnAt?: number;
20
+ summarizeAt?: number;
21
+ lockAt?: number;
22
+ /** Diameter in px. */
23
+ size?: number;
24
+ /** Ring thickness in px. */
25
+ strokeWidth?: number;
26
+ /** Render the rounded percentage in the ring's center. */
27
+ showLabel?: boolean;
28
+ className?: string;
29
+ }
30
+
31
+ const STATE_COLOR: Record<ContextMeterState, string> = {
32
+ ok: "text-muted-foreground",
33
+ warn: "text-amber-500",
34
+ summarizing: "text-sky-500",
35
+ full: "text-destructive",
36
+ };
37
+
38
+ function deriveState(
39
+ pct: number,
40
+ warnAt: number,
41
+ summarizeAt: number,
42
+ lockAt: number,
43
+ ): ContextMeterState {
44
+ if (pct >= lockAt) return "full";
45
+ if (pct >= summarizeAt) return "summarizing";
46
+ if (pct >= warnAt) return "warn";
47
+ return "ok";
48
+ }
49
+
50
+ /**
51
+ * A compact radial gauge for how full an agent conversation's context window is.
52
+ *
53
+ * Feed it the backend `context_meter` payload (`pct`/`state` + thresholds) or a
54
+ * raw `used`/`budget` pair. The ring colors shift as the conversation approaches
55
+ * the summarize (folds older messages) and lock (must start a new chat) points,
56
+ * mirroring Claude Code's context indicator. Theme-aware and accessible
57
+ * (`role="meter"`).
58
+ */
59
+ function ContextMeter({
60
+ used,
61
+ budget,
62
+ pct,
63
+ state,
64
+ warnAt = 0.75,
65
+ summarizeAt = 0.85,
66
+ lockAt = 0.98,
67
+ size = 24,
68
+ strokeWidth = 2.5,
69
+ showLabel = false,
70
+ className,
71
+ ...props
72
+ }: ContextMeterProps) {
73
+ const fraction = Math.max(
74
+ 0,
75
+ Math.min(1, pct ?? (budget ? (used ?? 0) / budget : 0)),
76
+ );
77
+ const resolvedState = state ?? deriveState(fraction, warnAt, summarizeAt, lockAt);
78
+ const percent = Math.round(fraction * 100);
79
+
80
+ const radius = (size - strokeWidth) / 2;
81
+ const circumference = 2 * Math.PI * radius;
82
+ const dash = circumference * fraction;
83
+ const center = size / 2;
84
+
85
+ return (
86
+ <span
87
+ className={cn("relative inline-flex items-center justify-center", STATE_COLOR[resolvedState])}
88
+ data-slot="context-meter"
89
+ data-state={resolvedState}
90
+ style={{ width: size, height: size }}
91
+ >
92
+ <svg
93
+ width={size}
94
+ height={size}
95
+ viewBox={`0 0 ${size} ${size}`}
96
+ role="meter"
97
+ aria-valuemin={0}
98
+ aria-valuemax={100}
99
+ aria-valuenow={percent}
100
+ aria-label={`Context ${percent}% used`}
101
+ className={cn("-rotate-90", className)}
102
+ {...props}
103
+ >
104
+ <circle
105
+ cx={center}
106
+ cy={center}
107
+ r={radius}
108
+ fill="none"
109
+ strokeWidth={strokeWidth}
110
+ className="stroke-muted"
111
+ />
112
+ <circle
113
+ cx={center}
114
+ cy={center}
115
+ r={radius}
116
+ fill="none"
117
+ strokeWidth={strokeWidth}
118
+ strokeLinecap="round"
119
+ stroke="currentColor"
120
+ strokeDasharray={`${dash} ${circumference}`}
121
+ className="transition-[stroke-dasharray] duration-500 ease-out"
122
+ />
123
+ </svg>
124
+ {showLabel ? (
125
+ <span className="absolute font-medium text-[0.5rem] text-foreground tabular-nums">
126
+ {percent}
127
+ </span>
128
+ ) : null}
129
+ </span>
130
+ );
131
+ }
132
+
133
+ export { ContextMeter };
134
+ export type { ContextMeterProps, ContextMeterState };