@ttt-productions/ui-core 0.11.18 → 0.11.20

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 (60) hide show
  1. package/dist/react/components/date-time-picker.d.ts +43 -0
  2. package/dist/react/components/date-time-picker.d.ts.map +1 -0
  3. package/dist/react/components/date-time-picker.js +188 -0
  4. package/dist/react/components/date-time-picker.js.map +1 -0
  5. package/dist/react/index.d.ts +2 -0
  6. package/dist/react/index.d.ts.map +1 -1
  7. package/dist/react/index.js +1 -0
  8. package/dist/react/index.js.map +1 -1
  9. package/package.json +4 -6
  10. package/src/index.ts +0 -3
  11. package/src/lib/format-numbers.ts +0 -17
  12. package/src/lib/utils.ts +0 -9
  13. package/src/react/components/accordion.tsx +0 -53
  14. package/src/react/components/alert-dialog.tsx +0 -131
  15. package/src/react/components/alert.tsx +0 -59
  16. package/src/react/components/avatar.tsx +0 -51
  17. package/src/react/components/badge.tsx +0 -31
  18. package/src/react/components/button.tsx +0 -102
  19. package/src/react/components/card.tsx +0 -54
  20. package/src/react/components/checkbox.tsx +0 -28
  21. package/src/react/components/chunk-error-recovery.tsx +0 -57
  22. package/src/react/components/collapsible.tsx +0 -11
  23. package/src/react/components/consequence-dialog.tsx +0 -282
  24. package/src/react/components/date-picker.tsx +0 -567
  25. package/src/react/components/dialog.tsx +0 -114
  26. package/src/react/components/dropdown-menu.tsx +0 -200
  27. package/src/react/components/end-of-list-indicator.tsx +0 -27
  28. package/src/react/components/form.tsx +0 -174
  29. package/src/react/components/input.tsx +0 -23
  30. package/src/react/components/label.tsx +0 -21
  31. package/src/react/components/layout/screen-adaptive-view.tsx +0 -106
  32. package/src/react/components/list-pagination.tsx +0 -101
  33. package/src/react/components/menubar.tsx +0 -220
  34. package/src/react/components/popover.tsx +0 -31
  35. package/src/react/components/progress.tsx +0 -27
  36. package/src/react/components/radio-group.tsx +0 -36
  37. package/src/react/components/relative-time.tsx +0 -56
  38. package/src/react/components/scroll-area.tsx +0 -56
  39. package/src/react/components/scroll-to-top-button.tsx +0 -85
  40. package/src/react/components/search-dropdown.tsx +0 -233
  41. package/src/react/components/select.tsx +0 -165
  42. package/src/react/components/separator.tsx +0 -26
  43. package/src/react/components/sheet.tsx +0 -140
  44. package/src/react/components/skeleton.tsx +0 -16
  45. package/src/react/components/slider.tsx +0 -25
  46. package/src/react/components/spinner.tsx +0 -49
  47. package/src/react/components/switch.tsx +0 -41
  48. package/src/react/components/table.tsx +0 -121
  49. package/src/react/components/tabs.tsx +0 -59
  50. package/src/react/components/textarea.tsx +0 -22
  51. package/src/react/components/toast.tsx +0 -193
  52. package/src/react/components/toaster.tsx +0 -43
  53. package/src/react/components/tooltip.tsx +0 -28
  54. package/src/react/hooks/use-async-action.ts +0 -63
  55. package/src/react/hooks/use-cursor-page.ts +0 -139
  56. package/src/react/hooks/use-media-query.tsx +0 -26
  57. package/src/react/hooks/use-paged-list.ts +0 -106
  58. package/src/react/hooks/use-return-scroll.ts +0 -149
  59. package/src/react/hooks/use-toast.tsx +0 -176
  60. package/src/react/index.ts +0 -85
@@ -1,200 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
- import { Slottable } from "@radix-ui/react-slot"
6
- import { Check, ChevronRight, Circle } from "lucide-react"
7
-
8
- import { cn } from "../../lib/utils.js"
9
- import { Spinner } from "./spinner.js"
10
-
11
- const DropdownMenu = DropdownMenuPrimitive.Root
12
- const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
13
- const DropdownMenuGroup = DropdownMenuPrimitive.Group
14
- const DropdownMenuPortal = DropdownMenuPrimitive.Portal
15
- const DropdownMenuSub = DropdownMenuPrimitive.Sub
16
- const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
17
-
18
- const DropdownMenuSubTrigger = React.forwardRef<
19
- React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
20
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
21
- inset?: boolean
22
- }
23
- >(({ className, inset, children, ...props }, ref) => (
24
- <DropdownMenuPrimitive.SubTrigger
25
- ref={ref}
26
- className={cn(
27
- "flex cursor-default gap-2 select-none items-center rounded-xs px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
28
- inset && "pl-8",
29
- className
30
- )}
31
- {...props}
32
- >
33
- {children}
34
- <ChevronRight className="ml-auto" />
35
- </DropdownMenuPrimitive.SubTrigger>
36
- ))
37
- DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
38
-
39
- const DropdownMenuSubContent = React.forwardRef<
40
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
41
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
42
- >(({ className, ...props }, ref) => (
43
- <DropdownMenuPrimitive.SubContent
44
- ref={ref}
45
- className={cn(
46
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground elevation-raised data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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",
47
- className
48
- )}
49
- {...props}
50
- />
51
- ))
52
- DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
53
-
54
- const DropdownMenuContent = React.forwardRef<
55
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
56
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
57
- >(({ className, sideOffset = 4, ...props }, ref) => (
58
- <DropdownMenuPrimitive.Portal>
59
- <DropdownMenuPrimitive.Content
60
- ref={ref}
61
- sideOffset={sideOffset}
62
- className={cn(
63
- "z-50 min-w-[8rem] overflow-hidden rounded-md border !bg-card p-1 text-popover-foreground elevation-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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",
64
- className
65
- )}
66
- {...props}
67
- />
68
- </DropdownMenuPrimitive.Portal>
69
- ))
70
- DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
71
-
72
- const DropdownMenuItem = React.forwardRef<
73
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
74
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
75
- inset?: boolean
76
- /**
77
- * The action this item started is in flight: the item disables itself, sets
78
- * `aria-busy`, and shows the spinner in place of its `icon`. Keeping the menu open
79
- * meanwhile is the caller's `onSelect` (`event.preventDefault()`, close on settle).
80
- */
81
- pending?: boolean
82
- /** Leading icon. While `pending`, the spinner takes its place. */
83
- icon?: React.ReactNode
84
- }
85
- >(({ className, inset, pending = false, icon, disabled, children, ...props }, ref) => (
86
- <DropdownMenuPrimitive.Item
87
- ref={ref}
88
- className={cn(
89
- "relative flex cursor-default select-none items-center gap-2 rounded-xs px-2 py-1.5 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 app-dropdown-item",
90
- inset && "pl-8",
91
- className
92
- )}
93
- {...props}
94
- {...(pending ? { "aria-busy": true, "data-pending": "" } : {})}
95
- disabled={disabled || pending || undefined}
96
- >
97
- {/* Direct siblings, never a fragment: under `asChild` Radix Slot finds the Slottable
98
- among its direct children and renders the icon/spinner inside the child element. */}
99
- {pending ? <Spinner size="xs" /> : icon}
100
- <Slottable>{children}</Slottable>
101
- </DropdownMenuPrimitive.Item>
102
- ))
103
- DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
104
-
105
- const DropdownMenuCheckboxItem = React.forwardRef<
106
- React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
107
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
108
- >(({ className, children, checked, ...props }, ref) => (
109
- <DropdownMenuPrimitive.CheckboxItem
110
- ref={ref}
111
- className={cn(
112
- "relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
113
- className
114
- )}
115
- checked={checked}
116
- {...props}
117
- >
118
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
119
- <DropdownMenuPrimitive.ItemIndicator>
120
- <Check className="h-4 w-4" />
121
- </DropdownMenuPrimitive.ItemIndicator>
122
- </span>
123
- {children}
124
- </DropdownMenuPrimitive.CheckboxItem>
125
- ))
126
- DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
127
-
128
- const DropdownMenuRadioItem = React.forwardRef<
129
- React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
130
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
131
- >(({ className, children, ...props }, ref) => (
132
- <DropdownMenuPrimitive.RadioItem
133
- ref={ref}
134
- className={cn(
135
- "relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
136
- className
137
- )}
138
- {...props}
139
- >
140
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
141
- <DropdownMenuPrimitive.ItemIndicator>
142
- <Circle className="h-2 w-2 fill-current" />
143
- </DropdownMenuPrimitive.ItemIndicator>
144
- </span>
145
- {children}
146
- </DropdownMenuPrimitive.RadioItem>
147
- ))
148
- DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
149
-
150
- const DropdownMenuLabel = React.forwardRef<
151
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
152
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
153
- inset?: boolean
154
- }
155
- >(({ className, inset, ...props }, ref) => (
156
- <DropdownMenuPrimitive.Label
157
- ref={ref}
158
- className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
159
- {...props}
160
- />
161
- ))
162
- DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163
-
164
- const DropdownMenuSeparator = React.forwardRef<
165
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
166
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
167
- >(({ className, ...props }, ref) => (
168
- <DropdownMenuPrimitive.Separator
169
- ref={ref}
170
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
171
- {...props}
172
- />
173
- ))
174
- DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175
-
176
- const DropdownMenuShortcut = ({
177
- className,
178
- ...props
179
- }: React.HTMLAttributes<HTMLSpanElement>) => (
180
- <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
181
- )
182
- DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183
-
184
- export {
185
- DropdownMenu,
186
- DropdownMenuTrigger,
187
- DropdownMenuContent,
188
- DropdownMenuItem,
189
- DropdownMenuCheckboxItem,
190
- DropdownMenuRadioItem,
191
- DropdownMenuLabel,
192
- DropdownMenuSeparator,
193
- DropdownMenuShortcut,
194
- DropdownMenuGroup,
195
- DropdownMenuPortal,
196
- DropdownMenuSub,
197
- DropdownMenuSubContent,
198
- DropdownMenuSubTrigger,
199
- DropdownMenuRadioGroup,
200
- }
@@ -1,27 +0,0 @@
1
- import * as React from "react";
2
- import { CheckCircle2 } from "lucide-react";
3
- import { cn } from "../../lib/utils.js";
4
-
5
- export interface EndOfListIndicatorProps {
6
- message?: string;
7
- className?: string;
8
- icon?: React.ReactNode;
9
- }
10
-
11
- export function EndOfListIndicator({
12
- message = "You've reached the end!",
13
- className,
14
- icon,
15
- }: EndOfListIndicatorProps) {
16
- return (
17
- <div
18
- className={cn(
19
- "flex flex-col items-center gap-2 py-8 text-center text-muted-foreground",
20
- className,
21
- )}
22
- >
23
- {icon ?? <CheckCircle2 className="h-6 w-6 text-[color:var(--success-icon)]" />}
24
- <p className="text-sm">{message}</p>
25
- </div>
26
- );
27
- }
@@ -1,174 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as LabelPrimitive from "@radix-ui/react-label"
5
- import { Slot } from "@radix-ui/react-slot"
6
- import {
7
- Controller,
8
- FormProvider,
9
- useFormContext,
10
- type ControllerProps,
11
- type FieldPath,
12
- type FieldValues,
13
- } from "react-hook-form"
14
-
15
- import { cn } from "../../lib/utils.js"
16
- import { Label } from "./label.js"
17
-
18
- const Form = FormProvider
19
-
20
- type FormFieldContextValue<
21
- TFieldValues extends FieldValues = FieldValues,
22
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
23
- > = {
24
- name: TName
25
- }
26
-
27
- const FormFieldContext = React.createContext<FormFieldContextValue>(
28
- {} as FormFieldContextValue
29
- )
30
-
31
- const FormField = <
32
- TFieldValues extends FieldValues = FieldValues,
33
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
34
- >({
35
- ...props
36
- }: ControllerProps<TFieldValues, TName>) => {
37
- return (
38
- <FormFieldContext.Provider value={{ name: props.name }}>
39
- <Controller {...props} />
40
- </FormFieldContext.Provider>
41
- )
42
- }
43
-
44
- const useFormField = () => {
45
- const fieldContext = React.useContext(FormFieldContext)
46
- const itemContext = React.useContext(FormItemContext)
47
- const { getFieldState, formState } = useFormContext()
48
-
49
- const fieldState = getFieldState(fieldContext.name, formState)
50
-
51
- if (!fieldContext) {
52
- throw new Error("useFormField should be used within <FormField>")
53
- }
54
-
55
- const { id } = itemContext
56
-
57
- return {
58
- id,
59
- name: fieldContext.name,
60
- formItemId: `${id}-form-item`,
61
- formDescriptionId: `${id}-form-item-description`,
62
- formMessageId: `${id}-form-item-message`,
63
- ...fieldState,
64
- }
65
- }
66
-
67
- type FormItemContextValue = {
68
- id: string
69
- }
70
-
71
- const FormItemContext = React.createContext<FormItemContextValue>(
72
- {} as FormItemContextValue
73
- )
74
-
75
- const FormItem = React.forwardRef<
76
- HTMLDivElement,
77
- React.HTMLAttributes<HTMLDivElement>
78
- >(({ className, ...props }, ref) => {
79
- const id = React.useId()
80
-
81
- return (
82
- <FormItemContext.Provider value={{ id }}>
83
- <div ref={ref} className={cn("space-y-2", className)} {...props} />
84
- </FormItemContext.Provider>
85
- )
86
- })
87
- FormItem.displayName = "FormItem"
88
-
89
- const FormLabel = React.forwardRef<
90
- React.ElementRef<typeof LabelPrimitive.Root>,
91
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
92
- >(({ className, ...props }, ref) => {
93
- const { error, formItemId } = useFormField()
94
-
95
- return (
96
- <Label
97
- ref={ref}
98
- className={cn(error && "text-destructive", className)}
99
- htmlFor={formItemId}
100
- {...props}
101
- />
102
- )
103
- })
104
- FormLabel.displayName = "FormLabel"
105
-
106
- const FormControl = React.forwardRef<
107
- React.ElementRef<typeof Slot>,
108
- React.ComponentPropsWithoutRef<typeof Slot>
109
- >(({ ...props }, ref) => {
110
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111
-
112
- return (
113
- <Slot
114
- ref={ref}
115
- id={formItemId}
116
- aria-describedby={
117
- !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
118
- }
119
- aria-invalid={!!error}
120
- {...props}
121
- />
122
- )
123
- })
124
- FormControl.displayName = "FormControl"
125
-
126
- const FormDescription = React.forwardRef<
127
- HTMLParagraphElement,
128
- React.HTMLAttributes<HTMLParagraphElement>
129
- >(({ className, ...props }, ref) => {
130
- const { formDescriptionId } = useFormField()
131
-
132
- return (
133
- <p
134
- ref={ref}
135
- id={formDescriptionId}
136
- className={cn("text-sm text-muted-foreground break-words", className)}
137
- {...props}
138
- />
139
- )
140
- })
141
- FormDescription.displayName = "FormDescription"
142
-
143
- const FormMessage = React.forwardRef<
144
- HTMLParagraphElement,
145
- React.HTMLAttributes<HTMLParagraphElement>
146
- >(({ className, children, ...props }, ref) => {
147
- const { error, formMessageId } = useFormField()
148
- const body = error ? String(error?.message ?? "") : children
149
-
150
- if (!body) return null
151
-
152
- return (
153
- <p
154
- ref={ref}
155
- id={formMessageId}
156
- className={cn("text-sm font-medium text-destructive break-words", className)}
157
- {...props}
158
- >
159
- {body}
160
- </p>
161
- )
162
- })
163
- FormMessage.displayName = "FormMessage"
164
-
165
- export {
166
- useFormField,
167
- Form,
168
- FormItem,
169
- FormLabel,
170
- FormControl,
171
- FormDescription,
172
- FormMessage,
173
- FormField,
174
- }
@@ -1,23 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { cn } from "../../lib/utils.js"
5
-
6
- const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
7
- ({ className, type, ...props }, ref) => {
8
- return (
9
- <input
10
- type={type}
11
- className={cn(
12
- "flex h-10 w-full rounded-md border-2 border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
13
- className
14
- )}
15
- ref={ref}
16
- {...props}
17
- />
18
- )
19
- }
20
- )
21
- Input.displayName = "Input"
22
-
23
- export { Input }
@@ -1,21 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as LabelPrimitive from "@radix-ui/react-label"
5
- import { cva, type VariantProps } from "class-variance-authority"
6
-
7
- import { cn } from "../../lib/utils.js"
8
-
9
- const labelVariants = cva(
10
- "text-sm font-bold leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11
- )
12
-
13
- const Label = React.forwardRef<
14
- React.ElementRef<typeof LabelPrimitive.Root>,
15
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
16
- >(({ className, ...props }, ref) => (
17
- <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
18
- ))
19
- Label.displayName = LabelPrimitive.Root.displayName
20
-
21
- export { Label }
@@ -1,106 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { cn } from "../../../lib/utils.js";
5
- import { useMediaQuery } from "../../hooks/use-media-query.js";
6
-
7
- export type MaxWidthOption =
8
- | "none"
9
- | "sm"
10
- | "md"
11
- | "lg"
12
- | "xl"
13
- | "2xl"
14
- | "3xl"
15
- | "4xl"
16
- | "5xl"
17
- | "6xl"
18
- | "7xl"
19
- | "full";
20
-
21
- export interface ScreenAdaptiveViewProps {
22
- /** Default content (mobile + base) */
23
- children: React.ReactNode;
24
-
25
- /** Optional content to use on lg and up */
26
- lgContent?: React.ReactNode;
27
-
28
- /** Optional content to use on xl and up */
29
- xlContent?: React.ReactNode;
30
-
31
- /** Max width applied at lg (>=1024px) */
32
- lgMaxWidth?: MaxWidthOption;
33
-
34
- /** Max width applied at xl (>=1280px) */
35
- xlMaxWidth?: MaxWidthOption;
36
-
37
- /** App-only escape hatch (try to avoid) */
38
- className?: string;
39
-
40
- /** App-only escape hatch (try to avoid) */
41
- innerClassName?: string;
42
- }
43
-
44
- function widthClass(opt: MaxWidthOption | undefined) {
45
- switch (opt) {
46
- case "none":
47
- return "screen-max-w-none";
48
- case "sm":
49
- return "screen-max-w-sm";
50
- case "md":
51
- return "screen-max-w-md";
52
- case "lg":
53
- return "screen-max-w-lg";
54
- case "xl":
55
- return "screen-max-w-xl";
56
- case "2xl":
57
- return "screen-max-w-2xl";
58
- case "3xl":
59
- return "screen-max-w-3xl";
60
- case "4xl":
61
- return "screen-max-w-4xl";
62
- case "5xl":
63
- return "screen-max-w-5xl";
64
- case "6xl":
65
- return "screen-max-w-6xl";
66
- case "7xl":
67
- return "screen-max-w-7xl";
68
- case "full":
69
- return "screen-max-w-full";
70
- default:
71
- return "screen-max-w-6xl";
72
- }
73
- }
74
-
75
- export function ScreenAdaptiveView({
76
- children,
77
- lgContent,
78
- xlContent,
79
- lgMaxWidth = "4xl",
80
- xlMaxWidth = "6xl",
81
- className,
82
- innerClassName,
83
- }: ScreenAdaptiveViewProps) {
84
- const isLg = useMediaQuery("(min-width: 1024px)");
85
- const isXl = useMediaQuery("(min-width: 1280px)");
86
-
87
- // SSR-safe default: render base children as mobile (no max-width)
88
- // After hydration, swaps to lg/xl content if provided.
89
- const contentToShow =
90
- (isXl && xlContent !== undefined && xlContent !== null)
91
- ? xlContent
92
- : (isLg && lgContent !== undefined && lgContent !== null)
93
- ? lgContent
94
- : children;
95
-
96
- const sizeClass =
97
- isXl ? widthClass(xlMaxWidth) : isLg ? widthClass(lgMaxWidth) : "screen-max-w-full";
98
-
99
- return (
100
- <div className={cn("screen-adaptive-root", className)}>
101
- <div className={cn("screen-adaptive-inner", sizeClass, innerClassName)}>
102
- {contentToShow}
103
- </div>
104
- </div>
105
- );
106
- }
@@ -1,101 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import { ChevronLeft, ChevronRight } from "lucide-react";
5
- import { Button } from "./button.js";
6
- import { cn } from "../../lib/utils.js";
7
- import type { ListPaginationState } from "../hooks/use-paged-list.js";
8
-
9
- export interface ListPaginationProps {
10
- /**
11
- * Page state from `usePagedList` (known total),
12
- * `useCursorPage().paginationFor(hasMore)` (unknown total), or any
13
- * caller-owned state of the same shape.
14
- */
15
- pagination: ListPaginationState;
16
- /**
17
- * Disables BOTH controls while the next page is in flight, without hiding the
18
- * row. Server-paged surfaces pass their query's `isFetching`; a slice-paged
19
- * list has nothing to wait for and omits it.
20
- */
21
- busy?: boolean;
22
- /** Optional extra classes. Merged with the component's own classes. */
23
- className?: string;
24
- }
25
-
26
- /**
27
- * The ONE Previous / counter / Next control for paginated lists, in both
28
- * flavors. Pair it with the hook that owns the page state (`usePagedList` for a
29
- * client-side slice, `useCursorPage` — or the caller's own query state — for a
30
- * cursor feed; a cursor feed passes `pager.paginationFor(hasMore)`, which binds
31
- * the query's answer to these controls); this component owns only the controls,
32
- * so every paginated list reads and behaves the same (ENG-002, FRONTEND-001).
33
- *
34
- * The counter follows the flavor: a known `totalPages` reads "2 of 5", an
35
- * omitted one reads "Page 2". Everything else is identical in both flavors —
36
- * which is exactly why there is one component and not two.
37
- *
38
- * It renders NOTHING when neither direction is available, so no call site
39
- * repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real
40
- * `disabled` on the buttons (announced, and unclickable), and the counter is a
41
- * `status` live region so a page change is announced to screen readers.
42
- */
43
- export function ListPagination({ pagination, busy = false, className }: ListPaginationProps) {
44
- const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } =
45
- pagination;
46
-
47
- // The control that started the in-flight page shows the spinner. Cleared when `busy`
48
- // ends — a render-phase adjustment, so there is no effect and no stale frame.
49
- const [clicked, setClicked] = useState<"previous" | "next" | null>(null);
50
- const [wasBusy, setWasBusy] = useState(busy);
51
- if (busy !== wasBusy) {
52
- setWasBusy(busy);
53
- if (!busy) setClicked(null);
54
- }
55
- const previousPending = busy && clicked === "previous";
56
- const nextPending = busy && clicked === "next";
57
-
58
- // One visibility rule for both flavors. For a known total this is exactly
59
- // `totalPages > 1`: page state is clamped into `1..totalPages`, so a
60
- // single-page list has no reachable neighbour in either direction, and a
61
- // multi-page list always has one. For a cursor feed it is the hand-rolled
62
- // `page > 1 || hasMore`. `busy` deliberately does not hide the row — the
63
- // controls grey out in place instead of vanishing mid-fetch.
64
- if (!canPreviousPage && !canNextPage) return null;
65
-
66
- return (
67
- <div className={cn("flex w-full items-center justify-between gap-4 mt-4", className)}>
68
- <Button
69
- variant="outline"
70
- size="sm"
71
- onClick={() => {
72
- setClicked("previous");
73
- goToPreviousPage();
74
- }}
75
- disabled={busy || !canPreviousPage}
76
- pending={previousPending}
77
- icon={<ChevronLeft className="icon-xs" />}
78
- className="h-11 min-w-11"
79
- >
80
- Previous
81
- </Button>
82
- <span className="text-counter" role="status">
83
- {totalPages === undefined ? `Page ${currentPage}` : `${currentPage} of ${totalPages}`}
84
- </span>
85
- <Button
86
- variant="outline"
87
- size="sm"
88
- onClick={() => {
89
- setClicked("next");
90
- goToNextPage();
91
- }}
92
- disabled={busy || !canNextPage}
93
- pending={nextPending}
94
- className="h-11 min-w-11"
95
- >
96
- Next
97
- {nextPending ? null : <ChevronRight className="icon-xs" />}
98
- </Button>
99
- </div>
100
- );
101
- }