@moloodev/ui 0.0.0-stage → 0.1.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 (93) hide show
  1. package/package.json +139 -3
  2. package/registry/components.ts +135 -0
  3. package/registry/fonts.css +81 -0
  4. package/registry/suit-OFL.txt +94 -0
  5. package/registry/wanted-sans-OFL.txt +93 -0
  6. package/src/components/ui/accordion.tsx +74 -0
  7. package/src/components/ui/alert-dialog.tsx +187 -0
  8. package/src/components/ui/alert.tsx +83 -0
  9. package/src/components/ui/aspect-ratio.tsx +22 -0
  10. package/src/components/ui/attachment.tsx +207 -0
  11. package/src/components/ui/avatar.tsx +109 -0
  12. package/src/components/ui/badge.tsx +54 -0
  13. package/src/components/ui/breadcrumb.tsx +126 -0
  14. package/src/components/ui/bubble.tsx +128 -0
  15. package/src/components/ui/button-group.tsx +103 -0
  16. package/src/components/ui/button.tsx +68 -0
  17. package/src/components/ui/calendar.tsx +222 -0
  18. package/src/components/ui/card.tsx +103 -0
  19. package/src/components/ui/carousel.tsx +242 -0
  20. package/src/components/ui/chart.tsx +398 -0
  21. package/src/components/ui/checkbox.tsx +28 -0
  22. package/src/components/ui/collapsible.tsx +40 -0
  23. package/src/components/ui/combobox.tsx +299 -0
  24. package/src/components/ui/command.tsx +218 -0
  25. package/src/components/ui/context-menu.tsx +271 -0
  26. package/src/components/ui/data-table.tsx +919 -0
  27. package/src/components/ui/date-picker.tsx +65 -0
  28. package/src/components/ui/dialog.tsx +224 -0
  29. package/src/components/ui/drawer.tsx +231 -0
  30. package/src/components/ui/dropdown-menu.tsx +268 -0
  31. package/src/components/ui/dropzone.tsx +186 -0
  32. package/src/components/ui/empty.tsx +101 -0
  33. package/src/components/ui/field.tsx +238 -0
  34. package/src/components/ui/form.tsx +83 -0
  35. package/src/components/ui/highlighted-source.tsx +200 -0
  36. package/src/components/ui/hover-card.tsx +51 -0
  37. package/src/components/ui/image-field.tsx +796 -0
  38. package/src/components/ui/input-group.tsx +149 -0
  39. package/src/components/ui/input-otp.tsx +86 -0
  40. package/src/components/ui/input-step.ts +19 -0
  41. package/src/components/ui/input.tsx +100 -0
  42. package/src/components/ui/item.tsx +201 -0
  43. package/src/components/ui/kbd.tsx +91 -0
  44. package/src/components/ui/kind-icon-glob.ts +24 -0
  45. package/src/components/ui/kind-icon-load.ts +52 -0
  46. package/src/components/ui/kind-icon-static.ts +46 -0
  47. package/src/components/ui/kind-icon.tsx +84 -0
  48. package/src/components/ui/kind-mark.tsx +65 -0
  49. package/src/components/ui/label.tsx +29 -0
  50. package/src/components/ui/marker.tsx +71 -0
  51. package/src/components/ui/menubar.tsx +277 -0
  52. package/src/components/ui/message-scroller.tsx +137 -0
  53. package/src/components/ui/message.tsx +92 -0
  54. package/src/components/ui/moloo-logo.tsx +45 -0
  55. package/src/components/ui/native-select.tsx +61 -0
  56. package/src/components/ui/navigation-menu.tsx +168 -0
  57. package/src/components/ui/outline-list.tsx +73 -0
  58. package/src/components/ui/pagination.tsx +130 -0
  59. package/src/components/ui/panel.tsx +108 -0
  60. package/src/components/ui/popover.tsx +90 -0
  61. package/src/components/ui/progress.tsx +117 -0
  62. package/src/components/ui/radio-group.tsx +38 -0
  63. package/src/components/ui/resizable.tsx +52 -0
  64. package/src/components/ui/scroll-area.tsx +54 -0
  65. package/src/components/ui/select.tsx +205 -0
  66. package/src/components/ui/separator.tsx +25 -0
  67. package/src/components/ui/sheet.tsx +140 -0
  68. package/src/components/ui/sidebar.tsx +746 -0
  69. package/src/components/ui/skeleton.tsx +13 -0
  70. package/src/components/ui/slider.tsx +52 -0
  71. package/src/components/ui/spinner.tsx +18 -0
  72. package/src/components/ui/switch.tsx +32 -0
  73. package/src/components/ui/table.tsx +116 -0
  74. package/src/components/ui/tabs.tsx +83 -0
  75. package/src/components/ui/tag-list-input.tsx +194 -0
  76. package/src/components/ui/textarea.tsx +18 -0
  77. package/src/components/ui/timeline.tsx +265 -0
  78. package/src/components/ui/toast.tsx +237 -0
  79. package/src/components/ui/toggle-group.tsx +90 -0
  80. package/src/components/ui/toggle.tsx +45 -0
  81. package/src/components/ui/tooltip.tsx +66 -0
  82. package/src/css.d.ts +1 -0
  83. package/src/hooks/use-clipboard.ts +82 -0
  84. package/src/hooks/use-mobile.ts +21 -0
  85. package/src/index.ts +106 -0
  86. package/src/kind-icon.ts +4 -0
  87. package/src/lib/format-relative-time.ts +41 -0
  88. package/src/lib/list-selection.ts +61 -0
  89. package/src/lib/utils.ts +6 -0
  90. package/src/moloo.css +360 -0
  91. package/src/styles.css +36 -0
  92. package/src/typeset.css +518 -0
  93. package/README.md +0 -3
@@ -0,0 +1,149 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Button } from "./button"
8
+ import { Input } from "./input"
9
+ import { Textarea } from "./textarea"
10
+
11
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
12
+ return (
13
+ <div
14
+ data-slot="input-group"
15
+ role="group"
16
+ className={cn(
17
+ "group/input-group moloo-field moloo-focus-within relative flex h-9 w-full min-w-0 items-stretch rounded-md transition-colors outline-none",
18
+ "has-disabled:opacity-50",
19
+ "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:shadow-[var(--moloo-invalid-shadow)]",
20
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto",
21
+ "has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
22
+ "in-data-[slot=combobox-content]:focus-within:outline-none",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ const inputGroupAddonVariants = cva(
31
+ "flex h-auto cursor-text items-center justify-center gap-1.5 self-stretch text-sm font-semibold text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-md [&>svg:not([class*='size-'])]:size-4",
32
+ {
33
+ variants: {
34
+ align: {
35
+ "inline-start":
36
+ "order-first pl-2.5 pr-1.5 has-[>button]:pl-1",
37
+ "inline-end":
38
+ "order-last pl-1.5 pr-2.5 has-[>button]:pr-1",
39
+ "block-start":
40
+ "order-first w-full justify-start px-2.5 py-2 group-has-[>input]/input-group:pt-2",
41
+ "block-end":
42
+ "order-last w-full justify-start px-2.5 py-2 group-has-[>input]/input-group:pb-2",
43
+ },
44
+ },
45
+ defaultVariants: {
46
+ align: "inline-start",
47
+ },
48
+ }
49
+ )
50
+
51
+ function InputGroupAddon({
52
+ className,
53
+ align = "inline-start",
54
+ ...props
55
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
56
+ return (
57
+ <div
58
+ role="group"
59
+ data-slot="input-group-addon"
60
+ data-align={align}
61
+ className={cn(inputGroupAddonVariants({ align }), className)}
62
+ onClick={(e) => {
63
+ if ((e.target as HTMLElement).closest("button")) {
64
+ return
65
+ }
66
+ e.currentTarget.parentElement?.querySelector("input")?.focus()
67
+ }}
68
+ {...props}
69
+ />
70
+ )
71
+ }
72
+
73
+ function InputGroupButton({
74
+ className,
75
+ type = "button",
76
+ variant = "ghost",
77
+ size = "xs",
78
+ ...props
79
+ }: Omit<React.ComponentProps<typeof Button>, "size" | "type"> & {
80
+ size?: "xs" | "sm" | "icon-xs" | "icon-sm"
81
+ type?: "button" | "submit" | "reset"
82
+ }) {
83
+ return (
84
+ <Button
85
+ type={type}
86
+ data-size={size}
87
+ variant={variant}
88
+ size={size}
89
+ className={cn(
90
+ "shadow-none hover:shadow-none active:shadow-none hover:translate-none active:translate-none",
91
+ className
92
+ )}
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
99
+ return (
100
+ <span
101
+ className={cn(
102
+ "flex items-center gap-2 text-sm font-semibold text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
103
+ className
104
+ )}
105
+ {...props}
106
+ />
107
+ )
108
+ }
109
+
110
+ function InputGroupInput({
111
+ className,
112
+ ...props
113
+ }: React.ComponentProps<typeof Input>) {
114
+ return (
115
+ <Input
116
+ data-slot="input-group-control"
117
+ className={cn(
118
+ "h-full min-w-0 flex-1 rounded-none border-0 bg-transparent px-2.5 py-0 shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:shadow-none dark:bg-transparent dark:disabled:bg-transparent",
119
+ className
120
+ )}
121
+ {...props}
122
+ />
123
+ )
124
+ }
125
+
126
+ function InputGroupTextarea({
127
+ className,
128
+ ...props
129
+ }: React.ComponentProps<"textarea">) {
130
+ return (
131
+ <Textarea
132
+ data-slot="input-group-control"
133
+ className={cn(
134
+ "min-h-16 flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:shadow-none dark:bg-transparent dark:disabled:bg-transparent",
135
+ className
136
+ )}
137
+ {...props}
138
+ />
139
+ )
140
+ }
141
+
142
+ export {
143
+ InputGroup,
144
+ InputGroupAddon,
145
+ InputGroupButton,
146
+ InputGroupText,
147
+ InputGroupInput,
148
+ InputGroupTextarea,
149
+ }
@@ -0,0 +1,86 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { OTPInput, OTPInputContext } from "input-otp"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { IconMinus } from "@tabler/icons-react"
8
+
9
+ function InputOTP({
10
+ className,
11
+ containerClassName,
12
+ ...props
13
+ }: React.ComponentProps<typeof OTPInput> & {
14
+ containerClassName?: string
15
+ }) {
16
+ return (
17
+ <OTPInput
18
+ data-slot="input-otp"
19
+ containerClassName={cn(
20
+ "flex items-center has-disabled:opacity-50",
21
+ containerClassName
22
+ )}
23
+ spellCheck={false}
24
+ className={cn("disabled:cursor-not-allowed", className)}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
31
+ return (
32
+ <div
33
+ data-slot="input-otp-group"
34
+ className={cn("flex items-center isolate", className)}
35
+ {...props}
36
+ />
37
+ )
38
+ }
39
+
40
+ function InputOTPSlot({
41
+ index,
42
+ className,
43
+ ...props
44
+ }: React.ComponentProps<"div"> & {
45
+ index: number
46
+ }) {
47
+ const inputOTPContext = React.useContext(OTPInputContext)
48
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
49
+
50
+ return (
51
+ <div
52
+ data-slot="input-otp-slot"
53
+ data-active={isActive}
54
+ className={cn(
55
+ "relative flex size-9 items-center justify-center border-2 border-border bg-[var(--field-bg)] text-sm font-semibold transition-colors outline-none",
56
+ "rounded-none not-first:-ml-[2px] first:rounded-l-md last:rounded-r-md",
57
+ "aria-invalid:border-destructive",
58
+ "data-[active=true]:z-10 data-[active=true]:border-destructive",
59
+ className
60
+ )}
61
+ {...props}
62
+ >
63
+ {char}
64
+ {hasFakeCaret && (
65
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
66
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
67
+ </div>
68
+ )}
69
+ </div>
70
+ )
71
+ }
72
+
73
+ function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="input-otp-separator"
77
+ className="flex items-center [&_svg:not([class*='size-'])]:size-4"
78
+ role="separator"
79
+ {...props}
80
+ >
81
+ <IconMinus />
82
+ </div>
83
+ )
84
+ }
85
+
86
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
@@ -0,0 +1,19 @@
1
+ export type StepDirection = "up" | "down"
2
+
3
+ export function commitStep(input: HTMLInputElement, direction: StepDirection): void {
4
+ if (input.disabled || input.readOnly) return
5
+
6
+ const previous = input.value
7
+ try {
8
+ if (direction === "up") input.stepUp()
9
+ else input.stepDown()
10
+ } catch (error) {
11
+ if (error instanceof DOMException && error.name === "InvalidStateError") return
12
+ throw error
13
+ }
14
+
15
+ if (input.value === previous) return
16
+ const view = input.ownerDocument.defaultView
17
+ if (view == null) return
18
+ input.dispatchEvent(new view.Event("input", { bubbles: true }))
19
+ }
@@ -0,0 +1,100 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Input as InputPrimitive } from "@base-ui/react/input"
5
+ import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import { commitStep, type StepDirection } from "./input-step"
9
+
10
+ type InputProps = React.ComponentProps<"input"> & {
11
+ stepper?: boolean
12
+ }
13
+
14
+ function StepButton({
15
+ direction,
16
+ disabled,
17
+ onStep,
18
+ }: {
19
+ direction: StepDirection
20
+ disabled: boolean
21
+ onStep: (event: React.MouseEvent<HTMLButtonElement>, direction: StepDirection) => void
22
+ }) {
23
+ const Icon = direction === "up" ? IconChevronUp : IconChevronDown
24
+ return (
25
+ <button
26
+ type="button"
27
+ tabIndex={-1}
28
+ aria-label={direction === "up" ? "Increase" : "Decrease"}
29
+ disabled={disabled}
30
+ className="flex flex-1 items-center justify-center text-muted-foreground hover:text-foreground disabled:pointer-events-none"
31
+ onMouseDown={(event) => {
32
+ event.preventDefault()
33
+ }}
34
+ onClick={(event) => {
35
+ onStep(event, direction)
36
+ }}
37
+ >
38
+ <Icon className="pointer-events-none size-3" aria-hidden="true" />
39
+ </button>
40
+ )
41
+ }
42
+
43
+ function SteppedInput({
44
+ className,
45
+ disabled,
46
+ readOnly,
47
+ ...props
48
+ }: React.ComponentProps<"input">) {
49
+ function step(event: React.MouseEvent<HTMLButtonElement>, direction: StepDirection) {
50
+ const input = event.currentTarget.closest("[data-slot=input-stepper]")?.querySelector("input")
51
+ if (!(input instanceof HTMLInputElement)) return
52
+ commitStep(input, direction)
53
+ input.focus({ preventScroll: true })
54
+ }
55
+
56
+ const buttonsDisabled = disabled === true || readOnly === true
57
+
58
+ return (
59
+ <span
60
+ data-slot="input-stepper"
61
+ className={cn(
62
+ "moloo-field moloo-focus-within relative flex h-9 w-full min-w-0 items-stretch transition-colors has-[input:disabled]:pointer-events-none has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:shadow-[var(--moloo-invalid-shadow)]",
63
+ className,
64
+ )}
65
+ >
66
+ <InputPrimitive
67
+ data-slot="input"
68
+ {...props}
69
+ type="number"
70
+ disabled={disabled}
71
+ readOnly={readOnly}
72
+ className="moloo-number-input h-full w-full min-w-0 rounded-none border-0 bg-transparent py-1 pl-2.5 pr-7 text-sm font-semibold text-foreground shadow-none outline-none placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed"
73
+ />
74
+ <span className="absolute inset-y-0 right-0 flex w-5 flex-col">
75
+ <StepButton direction="up" disabled={buttonsDisabled} onStep={step} />
76
+ <StepButton direction="down" disabled={buttonsDisabled} onStep={step} />
77
+ </span>
78
+ </span>
79
+ )
80
+ }
81
+
82
+ function Input({ className, type, stepper, ...props }: InputProps) {
83
+ if (type === "number" && stepper !== false) {
84
+ return <SteppedInput className={className} {...props} />
85
+ }
86
+
87
+ return (
88
+ <InputPrimitive
89
+ type={type}
90
+ data-slot="input"
91
+ className={cn(
92
+ "moloo-field moloo-focus h-9 w-full min-w-0 px-2.5 py-1 text-sm font-semibold text-foreground transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-[var(--moloo-invalid-shadow)]",
93
+ className,
94
+ )}
95
+ {...props}
96
+ />
97
+ )
98
+ }
99
+
100
+ export { Input }
@@ -0,0 +1,201 @@
1
+ import * as React from "react"
2
+ import { mergeProps } from "@base-ui/react/merge-props"
3
+ import { useRender } from "@base-ui/react/use-render"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Separator } from "./separator"
8
+
9
+ function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
10
+ return (
11
+ <div
12
+ role="list"
13
+ data-slot="item-group"
14
+ className={cn(
15
+ "group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function ItemSeparator({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<typeof Separator>) {
27
+ return (
28
+ <Separator
29
+ data-slot="item-separator"
30
+ orientation="horizontal"
31
+ className={cn("my-2", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ const itemVariants = cva(
38
+ "group/item flex w-full flex-wrap items-center rounded-md border-2 text-sm transition-colors duration-100 outline-none moloo-focus [a]:transition-colors [a]:hover:bg-muted",
39
+ {
40
+ variants: {
41
+ variant: {
42
+ default: "border-transparent",
43
+ outline: "border-border",
44
+ muted: "border-transparent bg-muted/50",
45
+ },
46
+ size: {
47
+ default: "gap-2.5 px-3 py-2.5",
48
+ sm: "gap-2.5 px-3 py-2.5",
49
+ xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0",
50
+ },
51
+ },
52
+ defaultVariants: {
53
+ variant: "default",
54
+ size: "default",
55
+ },
56
+ }
57
+ )
58
+
59
+ function Item({
60
+ className,
61
+ variant = "default",
62
+ size = "default",
63
+ render,
64
+ ...props
65
+ }: useRender.ComponentProps<"div"> & VariantProps<typeof itemVariants>) {
66
+ return useRender({
67
+ defaultTagName: "div",
68
+ props: mergeProps<"div">(
69
+ {
70
+ className: cn(itemVariants({ variant, size, className })),
71
+ },
72
+ props
73
+ ),
74
+ render,
75
+ state: {
76
+ slot: "item",
77
+ variant,
78
+ size,
79
+ },
80
+ })
81
+ }
82
+
83
+ const itemMediaVariants = cva(
84
+ "flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none",
85
+ {
86
+ variants: {
87
+ variant: {
88
+ default: "bg-transparent",
89
+ icon: "[&_svg:not([class*='size-'])]:size-4",
90
+ image:
91
+ "size-10 overflow-hidden rounded-md group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
92
+ },
93
+ },
94
+ defaultVariants: {
95
+ variant: "default",
96
+ },
97
+ }
98
+ )
99
+
100
+ function ItemMedia({
101
+ className,
102
+ variant = "default",
103
+ ...props
104
+ }: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
105
+ return (
106
+ <div
107
+ data-slot="item-media"
108
+ data-variant={variant}
109
+ className={cn(itemMediaVariants({ variant, className }))}
110
+ {...props}
111
+ />
112
+ )
113
+ }
114
+
115
+ function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
116
+ return (
117
+ <div
118
+ data-slot="item-content"
119
+ className={cn(
120
+ "flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none",
121
+ className
122
+ )}
123
+ {...props}
124
+ />
125
+ )
126
+ }
127
+
128
+ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
129
+ return (
130
+ <div
131
+ data-slot="item-title"
132
+ className={cn(
133
+ "line-clamp-1 flex w-fit items-center gap-2 text-sm font-semibold underline-offset-4",
134
+ className
135
+ )}
136
+ {...props}
137
+ />
138
+ )
139
+ }
140
+
141
+ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
142
+ return (
143
+ <p
144
+ data-slot="item-description"
145
+ className={cn(
146
+ "line-clamp-2 text-left text-sm/relaxed font-normal text-muted-foreground group-data-[size=xs]/item:text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ )
152
+ }
153
+
154
+ function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
155
+ return (
156
+ <div
157
+ data-slot="item-actions"
158
+ className={cn("flex items-center gap-2", className)}
159
+ {...props}
160
+ />
161
+ )
162
+ }
163
+
164
+ function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
165
+ return (
166
+ <div
167
+ data-slot="item-header"
168
+ className={cn(
169
+ "flex basis-full items-center justify-between gap-2",
170
+ className
171
+ )}
172
+ {...props}
173
+ />
174
+ )
175
+ }
176
+
177
+ function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
178
+ return (
179
+ <div
180
+ data-slot="item-footer"
181
+ className={cn(
182
+ "flex basis-full items-center justify-between gap-2",
183
+ className
184
+ )}
185
+ {...props}
186
+ />
187
+ )
188
+ }
189
+
190
+ export {
191
+ Item,
192
+ ItemMedia,
193
+ ItemContent,
194
+ ItemActions,
195
+ ItemGroup,
196
+ ItemSeparator,
197
+ ItemTitle,
198
+ ItemDescription,
199
+ ItemHeader,
200
+ ItemFooter,
201
+ }
@@ -0,0 +1,91 @@
1
+ import { cn } from "../../lib/utils"
2
+
3
+ /**
4
+ * Converts a key name to its platform symbol.
5
+ *
6
+ * @example
7
+ * kbdKey("cmd") // "⌘"
8
+ * kbdKey("shift") // "⇧"
9
+ * kbdKey("ctrl") // "⌃"
10
+ * kbdKey("alt") // "⌥"
11
+ * kbdKey("esc") // "⎋"
12
+ * kbdKey("enter") // "↵"
13
+ * kbdKey("backspace") // "⌫"
14
+ * kbdKey("tab") // "⇥"
15
+ * kbdKey("up") // "↑"
16
+ */
17
+ const KEY_MAP: Record<string, string> = {
18
+ cmd: "⌘",
19
+ command: "⌘",
20
+ meta: "⌘",
21
+ ctrl: "⌃",
22
+ control: "⌃",
23
+ alt: "⌥",
24
+ option: "⌥",
25
+ shift: "⇧",
26
+ enter: "↵",
27
+ return: "↵",
28
+ backspace: "⌫",
29
+ delete: "⌦",
30
+ escape: "⎋",
31
+ esc: "⎋",
32
+ tab: "⇥",
33
+ up: "↑",
34
+ down: "↓",
35
+ left: "←",
36
+ right: "→",
37
+ space: "␣",
38
+ capslock: "⇪",
39
+ }
40
+
41
+ function kbdKey(key: string): string {
42
+ return KEY_MAP[key.toLowerCase()] ?? key.toUpperCase()
43
+ }
44
+
45
+ function Kbd({
46
+ className,
47
+ keys,
48
+ children,
49
+ ...props
50
+ }: React.ComponentProps<"kbd"> & {
51
+ /** Key ids rendered with `kbdKey`, one chip each. Children are used when omitted. */
52
+ keys?: readonly string[]
53
+ }) {
54
+ if (keys && keys.length > 0) {
55
+ return (
56
+ <KbdGroup className={className}>
57
+ {keys.map((key) => (
58
+ <Kbd key={key}>{kbdKey(key)}</Kbd>
59
+ ))}
60
+ </KbdGroup>
61
+ )
62
+ }
63
+
64
+ return (
65
+ <kbd
66
+ data-slot="kbd"
67
+ className={cn(
68
+ "pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-md border-2 border-border bg-muted px-1 font-sans text-xs font-semibold text-muted-foreground shadow-[0_2px_0_0] shadow-border select-none",
69
+ "in-data-[slot=tooltip-content]:border-background/30 in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background in-data-[slot=tooltip-content]:shadow-background/20",
70
+ "dark:in-data-[slot=tooltip-content]:bg-background/10",
71
+ "[&_svg:not([class*='size-'])]:size-3",
72
+ className
73
+ )}
74
+ {...props}
75
+ >
76
+ {children}
77
+ </kbd>
78
+ )
79
+ }
80
+
81
+ function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
82
+ return (
83
+ <div
84
+ data-slot="kbd-group"
85
+ className={cn("inline-flex items-center gap-1", className)}
86
+ {...props}
87
+ />
88
+ )
89
+ }
90
+
91
+ export { Kbd, KbdGroup, kbdKey }
@@ -0,0 +1,24 @@
1
+ import type { Icon } from "@tabler/icons-react"
2
+
3
+ type IconLoader = () => Promise<{ default: Icon }>
4
+
5
+ type IconGlob = {
6
+ glob<Module>(pattern: string): Record<string, () => Promise<Module>>
7
+ }
8
+
9
+ let paths: Record<string, IconLoader> = {}
10
+ try {
11
+ paths = (import.meta as ImportMeta & IconGlob).glob<{ default: Icon }>(
12
+ "/node_modules/@tabler/icons-react/dist/esm/icons/Icon*.mjs",
13
+ )
14
+ } catch {
15
+ paths = {}
16
+ }
17
+
18
+ export const tablerIconLoaders: Map<string, IconLoader> = new Map(
19
+ Object.entries(paths).map(([path, loader]): [string, IconLoader] => {
20
+ const file = path.slice(path.lastIndexOf("/") + 1)
21
+ const name = file.endsWith(".mjs") ? file.slice(0, -".mjs".length) : file
22
+ return [name, loader]
23
+ }),
24
+ )