@pascal-app/editor 0.4.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 (165) hide show
  1. package/package.json +62 -0
  2. package/src/components/editor/custom-camera-controls.tsx +387 -0
  3. package/src/components/editor/editor-layout-v2.tsx +220 -0
  4. package/src/components/editor/export-manager.tsx +78 -0
  5. package/src/components/editor/first-person-controls.tsx +249 -0
  6. package/src/components/editor/floating-action-menu.tsx +231 -0
  7. package/src/components/editor/floorplan-panel.tsx +9609 -0
  8. package/src/components/editor/grid.tsx +161 -0
  9. package/src/components/editor/index.tsx +928 -0
  10. package/src/components/editor/node-action-menu.tsx +66 -0
  11. package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
  12. package/src/components/editor/selection-manager.tsx +897 -0
  13. package/src/components/editor/site-edge-labels.tsx +90 -0
  14. package/src/components/editor/thumbnail-generator.tsx +166 -0
  15. package/src/components/editor/wall-measurement-label.tsx +258 -0
  16. package/src/components/feedback-dialog.tsx +265 -0
  17. package/src/components/pascal-radio.tsx +280 -0
  18. package/src/components/preview-button.tsx +16 -0
  19. package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
  20. package/src/components/systems/roof/roof-edit-system.tsx +69 -0
  21. package/src/components/systems/stair/stair-edit-system.tsx +69 -0
  22. package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
  23. package/src/components/systems/zone/zone-system.tsx +87 -0
  24. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
  25. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
  26. package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
  27. package/src/components/tools/door/door-math.ts +110 -0
  28. package/src/components/tools/door/door-tool.tsx +293 -0
  29. package/src/components/tools/door/move-door-tool.tsx +373 -0
  30. package/src/components/tools/item/item-tool.tsx +26 -0
  31. package/src/components/tools/item/move-tool.tsx +90 -0
  32. package/src/components/tools/item/placement-math.ts +85 -0
  33. package/src/components/tools/item/placement-strategies.ts +556 -0
  34. package/src/components/tools/item/placement-types.ts +117 -0
  35. package/src/components/tools/item/use-draft-node.ts +227 -0
  36. package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
  37. package/src/components/tools/roof/move-roof-tool.tsx +288 -0
  38. package/src/components/tools/roof/roof-tool.tsx +318 -0
  39. package/src/components/tools/select/box-select-tool.tsx +626 -0
  40. package/src/components/tools/shared/cursor-sphere.tsx +119 -0
  41. package/src/components/tools/shared/polygon-editor.tsx +361 -0
  42. package/src/components/tools/site/site-boundary-editor.tsx +42 -0
  43. package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
  44. package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
  45. package/src/components/tools/slab/slab-tool.tsx +322 -0
  46. package/src/components/tools/stair/stair-defaults.ts +7 -0
  47. package/src/components/tools/stair/stair-tool.tsx +194 -0
  48. package/src/components/tools/tool-manager.tsx +120 -0
  49. package/src/components/tools/wall/wall-drafting.ts +140 -0
  50. package/src/components/tools/wall/wall-tool.tsx +210 -0
  51. package/src/components/tools/window/move-window-tool.tsx +410 -0
  52. package/src/components/tools/window/window-math.ts +117 -0
  53. package/src/components/tools/window/window-tool.tsx +303 -0
  54. package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
  55. package/src/components/tools/zone/zone-tool.tsx +364 -0
  56. package/src/components/ui/action-menu/action-button.tsx +59 -0
  57. package/src/components/ui/action-menu/camera-actions.tsx +74 -0
  58. package/src/components/ui/action-menu/control-modes.tsx +240 -0
  59. package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
  60. package/src/components/ui/action-menu/index.tsx +152 -0
  61. package/src/components/ui/action-menu/structure-tools.tsx +100 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +397 -0
  63. package/src/components/ui/command-palette/editor-commands.tsx +396 -0
  64. package/src/components/ui/command-palette/index.tsx +730 -0
  65. package/src/components/ui/controls/action-button.tsx +33 -0
  66. package/src/components/ui/controls/material-picker.tsx +194 -0
  67. package/src/components/ui/controls/metric-control.tsx +262 -0
  68. package/src/components/ui/controls/panel-section.tsx +65 -0
  69. package/src/components/ui/controls/segmented-control.tsx +45 -0
  70. package/src/components/ui/controls/slider-control.tsx +245 -0
  71. package/src/components/ui/controls/toggle-control.tsx +38 -0
  72. package/src/components/ui/floating-level-selector.tsx +355 -0
  73. package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
  74. package/src/components/ui/helpers/helper-manager.tsx +33 -0
  75. package/src/components/ui/helpers/item-helper.tsx +40 -0
  76. package/src/components/ui/helpers/roof-helper.tsx +16 -0
  77. package/src/components/ui/helpers/slab-helper.tsx +20 -0
  78. package/src/components/ui/helpers/wall-helper.tsx +20 -0
  79. package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
  80. package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
  81. package/src/components/ui/panels/ceiling-panel.tsx +230 -0
  82. package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
  83. package/src/components/ui/panels/door-panel.tsx +600 -0
  84. package/src/components/ui/panels/item-panel.tsx +306 -0
  85. package/src/components/ui/panels/panel-manager.tsx +59 -0
  86. package/src/components/ui/panels/panel-wrapper.tsx +80 -0
  87. package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
  88. package/src/components/ui/panels/reference-panel.tsx +177 -0
  89. package/src/components/ui/panels/roof-panel.tsx +262 -0
  90. package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
  91. package/src/components/ui/panels/slab-panel.tsx +228 -0
  92. package/src/components/ui/panels/stair-panel.tsx +304 -0
  93. package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
  94. package/src/components/ui/panels/wall-panel.tsx +123 -0
  95. package/src/components/ui/panels/window-panel.tsx +441 -0
  96. package/src/components/ui/primitives/button.tsx +69 -0
  97. package/src/components/ui/primitives/card.tsx +75 -0
  98. package/src/components/ui/primitives/color-dot.tsx +61 -0
  99. package/src/components/ui/primitives/context-menu.tsx +227 -0
  100. package/src/components/ui/primitives/dialog.tsx +129 -0
  101. package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
  102. package/src/components/ui/primitives/error-boundary.tsx +52 -0
  103. package/src/components/ui/primitives/input.tsx +21 -0
  104. package/src/components/ui/primitives/number-input.tsx +187 -0
  105. package/src/components/ui/primitives/opacity-control.tsx +79 -0
  106. package/src/components/ui/primitives/popover.tsx +42 -0
  107. package/src/components/ui/primitives/separator.tsx +28 -0
  108. package/src/components/ui/primitives/sheet.tsx +130 -0
  109. package/src/components/ui/primitives/shortcut-token.tsx +64 -0
  110. package/src/components/ui/primitives/sidebar.tsx +855 -0
  111. package/src/components/ui/primitives/skeleton.tsx +13 -0
  112. package/src/components/ui/primitives/slider.tsx +58 -0
  113. package/src/components/ui/primitives/switch.tsx +29 -0
  114. package/src/components/ui/primitives/tooltip.tsx +57 -0
  115. package/src/components/ui/scene-loader.tsx +40 -0
  116. package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
  117. package/src/components/ui/sidebar/icon-rail.tsx +147 -0
  118. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
  119. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
  120. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
  121. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
  122. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
  123. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
  125. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
  126. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
  127. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
  128. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
  130. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
  131. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
  134. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
  135. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
  136. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
  137. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
  138. package/src/components/ui/sidebar/tab-bar.tsx +39 -0
  139. package/src/components/ui/slider-demo.tsx +36 -0
  140. package/src/components/ui/slider.tsx +81 -0
  141. package/src/components/ui/viewer-toolbar.tsx +342 -0
  142. package/src/components/viewer-overlay.tsx +499 -0
  143. package/src/components/viewer-zone-system.tsx +48 -0
  144. package/src/contexts/presets-context.tsx +121 -0
  145. package/src/hooks/use-auto-save.ts +194 -0
  146. package/src/hooks/use-contextual-tools.ts +52 -0
  147. package/src/hooks/use-grid-events.ts +106 -0
  148. package/src/hooks/use-keyboard.ts +214 -0
  149. package/src/hooks/use-mobile.ts +19 -0
  150. package/src/hooks/use-reduced-motion.ts +20 -0
  151. package/src/index.tsx +33 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/level-selection.ts +31 -0
  154. package/src/lib/scene.ts +394 -0
  155. package/src/lib/sfx/index.ts +2 -0
  156. package/src/lib/sfx-bus.ts +49 -0
  157. package/src/lib/sfx-player.ts +60 -0
  158. package/src/lib/utils.ts +43 -0
  159. package/src/store/use-audio.tsx +45 -0
  160. package/src/store/use-command-registry.ts +36 -0
  161. package/src/store/use-editor.tsx +522 -0
  162. package/src/store/use-palette-view-registry.ts +45 -0
  163. package/src/store/use-upload.ts +90 -0
  164. package/src/three-types.ts +3 -0
  165. package/tsconfig.json +9 -0
@@ -0,0 +1,187 @@
1
+ 'use client'
2
+
3
+ import NumberFlow from '@number-flow/react'
4
+ import { useScene } from '@pascal-app/core'
5
+ import { useCallback, useRef, useState } from 'react'
6
+
7
+ interface NumberInputProps {
8
+ label: string
9
+ value: number
10
+ onChange: (value: number) => void
11
+ min?: number
12
+ max?: number
13
+ precision?: number
14
+ step?: number
15
+ className?: string
16
+ }
17
+
18
+ export function NumberInput({
19
+ label,
20
+ value,
21
+ onChange,
22
+ min,
23
+ max,
24
+ precision = 2,
25
+ step = 0.1,
26
+ className = '',
27
+ }: NumberInputProps) {
28
+ const [isEditing, setIsEditing] = useState(false)
29
+ const [isDragging, setIsDragging] = useState(false)
30
+ const [inputValue, setInputValue] = useState(value.toFixed(precision))
31
+ const startXRef = useRef(0)
32
+ const startValueRef = useRef(0)
33
+ const labelRef = useRef<HTMLDivElement>(null)
34
+
35
+ const clamp = useCallback(
36
+ (val: number) => {
37
+ if (min !== undefined && val < min) return min
38
+ if (max !== undefined && val > max) return max
39
+ return val
40
+ },
41
+ [min, max],
42
+ )
43
+
44
+ const handleLabelMouseDown = useCallback(
45
+ (e: React.MouseEvent) => {
46
+ if (isEditing) return
47
+ e.preventDefault()
48
+ setIsDragging(true)
49
+ startXRef.current = e.clientX
50
+ startValueRef.current = value
51
+
52
+ // Pause history tracking during drag
53
+ useScene.temporal.getState().pause()
54
+
55
+ let finalValue = value
56
+
57
+ const handleMouseMove = (moveEvent: MouseEvent) => {
58
+ const deltaX = moveEvent.clientX - startXRef.current
59
+
60
+ // Determine step size based on modifier keys
61
+ let dragStep = step // Default from prop
62
+ if (moveEvent.shiftKey) {
63
+ dragStep = step * 10 // Coarse
64
+ } else if (moveEvent.altKey) {
65
+ dragStep = step * 0.1 // Fine
66
+ }
67
+
68
+ const deltaValue = deltaX * dragStep
69
+ const newValue = clamp(startValueRef.current + deltaValue)
70
+ const newFinalValue = Number.parseFloat(newValue.toFixed(precision))
71
+
72
+ // Only call onChange if value actually changed (avoid extra processing on tiny moves)
73
+ if (newFinalValue !== finalValue) {
74
+ finalValue = newFinalValue
75
+ onChange(finalValue)
76
+ }
77
+ }
78
+
79
+ const handleMouseUp = () => {
80
+ setIsDragging(false)
81
+ document.removeEventListener('mousemove', handleMouseMove)
82
+ document.removeEventListener('mouseup', handleMouseUp)
83
+
84
+ // Reset to initial value while still paused (no history entry)
85
+ // Then resume and apply final value (creates single history entry)
86
+ if (finalValue !== startValueRef.current) {
87
+ onChange(startValueRef.current)
88
+ useScene.temporal.getState().resume()
89
+ onChange(finalValue)
90
+ } else {
91
+ useScene.temporal.getState().resume()
92
+ }
93
+ }
94
+
95
+ document.addEventListener('mousemove', handleMouseMove)
96
+ document.addEventListener('mouseup', handleMouseUp)
97
+ },
98
+ [isEditing, value, onChange, clamp, precision, step],
99
+ )
100
+
101
+ const handleValueClick = useCallback(() => {
102
+ setIsEditing(true)
103
+ setInputValue(value.toFixed(precision))
104
+ }, [value, precision])
105
+
106
+ const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
107
+ setInputValue(e.target.value)
108
+ }, [])
109
+
110
+ const handleInputBlur = useCallback(() => {
111
+ const numValue = Number.parseFloat(inputValue)
112
+ if (!Number.isNaN(numValue)) {
113
+ onChange(clamp(Number.parseFloat(numValue.toFixed(precision))))
114
+ }
115
+ setIsEditing(false)
116
+ }, [inputValue, onChange, clamp, precision])
117
+
118
+ const handleInputKeyDown = useCallback(
119
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
120
+ if (e.key === 'Enter') {
121
+ const numValue = Number.parseFloat(inputValue)
122
+ if (!Number.isNaN(numValue)) {
123
+ onChange(clamp(Number.parseFloat(numValue.toFixed(precision))))
124
+ }
125
+ setIsEditing(false)
126
+ } else if (e.key === 'Escape') {
127
+ setInputValue(value.toFixed(precision))
128
+ setIsEditing(false)
129
+ }
130
+ },
131
+ [inputValue, onChange, value, clamp, precision],
132
+ )
133
+
134
+ return (
135
+ <div className={`${className} group/input relative`}>
136
+ <div
137
+ className={`pointer-events-none absolute inset-y-0 left-0 bg-primary/10 transition-all duration-75 dark:bg-primary/20 ${isDragging ? 'opacity-100' : 'opacity-0'}`}
138
+ style={{
139
+ width: `${Math.min(100, Math.max(0, ((value - (min ?? Math.min(0, value))) / ((max ?? Math.max(10, value)) - (min ?? Math.min(0, value)))) * 100))}%`,
140
+ borderTopRightRadius: value >= (max ?? Math.max(10, value)) ? '0.5rem' : '0',
141
+ borderBottomRightRadius: value >= (max ?? Math.max(10, value)) ? '0.5rem' : '0',
142
+ borderTopLeftRadius: '0.5rem',
143
+ borderBottomLeftRadius: '0.5rem',
144
+ }}
145
+ />
146
+ <div
147
+ className={`relative z-10 flex items-center overflow-hidden rounded-lg border shadow-[0_1px_2px_0px_rgba(0,0,0,0.05)] transition-all focus-within:border-primary focus-within:ring-1 focus-within:ring-primary ${isDragging ? 'border-neutral-300 bg-transparent ring-1 ring-neutral-200/60 dark:border-border dark:ring-border/50' : 'border-neutral-200/60 bg-white hover:border-neutral-300 dark:border-border/50 dark:bg-accent/30 dark:hover:border-border/80'}`}
148
+ >
149
+ <div
150
+ className={`z-10 select-none truncate py-1.5 pr-1 pl-2 font-barlow font-medium text-muted-foreground text-xs ${
151
+ isDragging
152
+ ? 'cursor-ew-resize text-foreground'
153
+ : 'hover:cursor-ew-resize hover:text-foreground'
154
+ } transition-colors`}
155
+ onMouseDown={handleLabelMouseDown}
156
+ ref={labelRef}
157
+ >
158
+ {label}
159
+ </div>
160
+ {isEditing ? (
161
+ <input
162
+ autoFocus
163
+ className="z-10 min-w-0 flex-1 bg-transparent px-2 py-1.5 text-right font-medium font-mono text-foreground text-sm outline-none placeholder:text-muted-foreground/50"
164
+ onBlur={handleInputBlur}
165
+ onChange={handleInputChange}
166
+ onKeyDown={handleInputKeyDown}
167
+ size={1}
168
+ type="text"
169
+ value={inputValue}
170
+ />
171
+ ) : (
172
+ <div
173
+ className={
174
+ 'z-10 min-w-0 flex-1 cursor-text truncate px-2 py-1.5 text-right font-medium font-mono text-foreground text-sm tabular-nums tracking-tight transition-colors hover:bg-black/5 dark:hover:bg-white/5'
175
+ }
176
+ onClick={handleValueClick}
177
+ >
178
+ <NumberFlow
179
+ format={{ minimumFractionDigits: precision, maximumFractionDigits: precision }}
180
+ value={Number(value.toFixed(precision))}
181
+ />
182
+ </div>
183
+ )}
184
+ </div>
185
+ </div>
186
+ )
187
+ }
@@ -0,0 +1,79 @@
1
+ 'use client'
2
+
3
+ import { Eye, EyeOff } from 'lucide-react'
4
+ import { useState } from 'react'
5
+ import { Button } from '../../../components/ui/primitives/button'
6
+ import { Popover, PopoverContent, PopoverTrigger } from '../../../components/ui/primitives/popover'
7
+ import { Slider } from '../../../components/ui/primitives/slider'
8
+ import { cn } from '../../../lib/utils'
9
+
10
+ interface OpacityControlProps {
11
+ visible?: boolean
12
+ opacity?: number
13
+ onVisibilityToggle: () => void
14
+ onOpacityChange: (opacity: number) => void
15
+ className?: string
16
+ }
17
+
18
+ export function OpacityControl({
19
+ visible = true,
20
+ opacity = 100,
21
+ onVisibilityToggle,
22
+ onOpacityChange,
23
+ className,
24
+ }: OpacityControlProps) {
25
+ const [isOpen, setIsOpen] = useState(false)
26
+ const actualOpacity = opacity ?? 100
27
+ const isHidden = visible === false || actualOpacity === 0
28
+
29
+ return (
30
+ <Popover onOpenChange={setIsOpen} open={isOpen}>
31
+ <div className={cn('flex items-center gap-1', className)}>
32
+ {!isHidden && actualOpacity < 100 && (
33
+ <span className="text-muted-foreground text-xs">{actualOpacity}%</span>
34
+ )}
35
+ <PopoverTrigger asChild>
36
+ <Button
37
+ className={cn(
38
+ 'h-5 w-5 p-0 transition-opacity',
39
+ isHidden ? 'opacity-100' : 'opacity-0 group-hover/item:opacity-100',
40
+ )}
41
+ onClick={(e) => {
42
+ e.stopPropagation()
43
+ // If clicking the button (not opening popover), toggle visibility
44
+ if (!isOpen) {
45
+ onVisibilityToggle()
46
+ }
47
+ }}
48
+ size="sm"
49
+ variant="ghost"
50
+ >
51
+ {isHidden ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
52
+ </Button>
53
+ </PopoverTrigger>
54
+ <PopoverContent
55
+ align="end"
56
+ className="w-48 p-3"
57
+ onClick={(e) => e.stopPropagation()}
58
+ side="right"
59
+ >
60
+ <div className="space-y-2">
61
+ <div className="flex items-center justify-between">
62
+ <span className="font-medium text-sm">Opacity</span>
63
+ <span className="text-muted-foreground text-xs">{actualOpacity}%</span>
64
+ </div>
65
+ <Slider
66
+ max={100}
67
+ min={0}
68
+ onValueChange={(values: number[]) => {
69
+ if (values[0] !== undefined) onOpacityChange(values[0])
70
+ }}
71
+ step={1}
72
+ value={[actualOpacity]}
73
+ />
74
+ </div>
75
+ </PopoverContent>
76
+ </div>
77
+ </Popover>
78
+ )
79
+ }
@@ -0,0 +1,42 @@
1
+ 'use client'
2
+
3
+ import * as PopoverPrimitive from '@radix-ui/react-popover'
4
+ import type * as React from 'react'
5
+
6
+ import { cn } from '../../../lib/utils'
7
+
8
+ function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
9
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />
10
+ }
11
+
12
+ function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
13
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
14
+ }
15
+
16
+ function PopoverContent({
17
+ className,
18
+ align = 'center',
19
+ sideOffset = 4,
20
+ ...props
21
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
22
+ return (
23
+ <PopoverPrimitive.Portal>
24
+ <PopoverPrimitive.Content
25
+ align={align}
26
+ className={cn(
27
+ '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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in',
28
+ className,
29
+ )}
30
+ data-slot="popover-content"
31
+ sideOffset={sideOffset}
32
+ {...props}
33
+ />
34
+ </PopoverPrimitive.Portal>
35
+ )
36
+ }
37
+
38
+ function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
39
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
40
+ }
41
+
42
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
@@ -0,0 +1,28 @@
1
+ 'use client'
2
+
3
+ import * as SeparatorPrimitive from '@radix-ui/react-separator'
4
+ import type * as React from 'react'
5
+
6
+ import { cn } from '../../../lib/utils'
7
+
8
+ function Separator({
9
+ className,
10
+ orientation = 'horizontal',
11
+ decorative = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
14
+ return (
15
+ <SeparatorPrimitive.Root
16
+ className={cn(
17
+ 'shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
18
+ className,
19
+ )}
20
+ data-slot="separator"
21
+ decorative={decorative}
22
+ orientation={orientation}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ export { Separator }
@@ -0,0 +1,130 @@
1
+ 'use client'
2
+
3
+ import * as SheetPrimitive from '@radix-ui/react-dialog'
4
+ import { XIcon } from 'lucide-react'
5
+ import type * as React from 'react'
6
+
7
+ import { cn } from '../../../lib/utils'
8
+
9
+ function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
10
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />
11
+ }
12
+
13
+ function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
14
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
15
+ }
16
+
17
+ function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
18
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
19
+ }
20
+
21
+ function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
22
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
23
+ }
24
+
25
+ function SheetOverlay({
26
+ className,
27
+ ...props
28
+ }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
29
+ return (
30
+ <SheetPrimitive.Overlay
31
+ className={cn(
32
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in',
33
+ className,
34
+ )}
35
+ data-slot="sheet-overlay"
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function SheetContent({
42
+ className,
43
+ children,
44
+ side = 'right',
45
+ ...props
46
+ }: React.ComponentProps<typeof SheetPrimitive.Content> & {
47
+ side?: 'top' | 'right' | 'bottom' | 'left'
48
+ }) {
49
+ return (
50
+ <SheetPortal>
51
+ <SheetOverlay />
52
+ <SheetPrimitive.Content
53
+ className={cn(
54
+ 'fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:duration-300 data-[state=open]:duration-500',
55
+ side === 'right' &&
56
+ 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
57
+ side === 'left' &&
58
+ 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
59
+ side === 'top' &&
60
+ 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
61
+ side === 'bottom' &&
62
+ 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
63
+ className,
64
+ )}
65
+ data-slot="sheet-content"
66
+ {...props}
67
+ >
68
+ {children}
69
+ <SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
70
+ <XIcon className="size-4" />
71
+ <span className="sr-only">Close</span>
72
+ </SheetPrimitive.Close>
73
+ </SheetPrimitive.Content>
74
+ </SheetPortal>
75
+ )
76
+ }
77
+
78
+ function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
79
+ return (
80
+ <div
81
+ className={cn('flex flex-col gap-1.5 p-4', className)}
82
+ data-slot="sheet-header"
83
+ {...props}
84
+ />
85
+ )
86
+ }
87
+
88
+ function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
89
+ return (
90
+ <div
91
+ className={cn('mt-auto flex flex-col gap-2 p-4', className)}
92
+ data-slot="sheet-footer"
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
99
+ return (
100
+ <SheetPrimitive.Title
101
+ className={cn('font-semibold text-foreground', className)}
102
+ data-slot="sheet-title"
103
+ {...props}
104
+ />
105
+ )
106
+ }
107
+
108
+ function SheetDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof SheetPrimitive.Description>) {
112
+ return (
113
+ <SheetPrimitive.Description
114
+ className={cn('text-muted-foreground text-sm', className)}
115
+ data-slot="sheet-description"
116
+ {...props}
117
+ />
118
+ )
119
+ }
120
+
121
+ export {
122
+ Sheet,
123
+ SheetTrigger,
124
+ SheetClose,
125
+ SheetContent,
126
+ SheetHeader,
127
+ SheetFooter,
128
+ SheetTitle,
129
+ SheetDescription,
130
+ }
@@ -0,0 +1,64 @@
1
+ import { Icon } from '@iconify/react'
2
+ import type * as React from 'react'
3
+
4
+ import { cn } from '../../../lib/utils'
5
+
6
+ const MOUSE_SHORTCUTS = {
7
+ Click: {
8
+ icon: 'ph:mouse-left-click-fill',
9
+ label: 'Left click',
10
+ },
11
+ 'Left click': {
12
+ icon: 'ph:mouse-left-click-fill',
13
+ label: 'Left click',
14
+ },
15
+ 'Middle click': {
16
+ icon: 'qlementine-icons:mouse-middle-button-16',
17
+ label: 'Middle click',
18
+ },
19
+ 'Right click': {
20
+ icon: 'ph:mouse-right-click-fill',
21
+ label: 'Right click',
22
+ },
23
+ } as const
24
+
25
+ type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
26
+ value: string
27
+ displayValue?: string
28
+ }
29
+
30
+ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTokenProps) {
31
+ const mouseShortcut =
32
+ value in MOUSE_SHORTCUTS ? MOUSE_SHORTCUTS[value as keyof typeof MOUSE_SHORTCUTS] : null
33
+
34
+ return (
35
+ <kbd
36
+ aria-label={mouseShortcut?.label ?? displayValue ?? value}
37
+ className={cn(
38
+ 'inline-flex h-6 items-center rounded border border-border bg-muted px-2 font-medium font-mono text-[11px] text-muted-foreground',
39
+ mouseShortcut && 'justify-center px-1.5',
40
+ className,
41
+ )}
42
+ title={mouseShortcut?.label ?? value}
43
+ {...props}
44
+ >
45
+ {mouseShortcut ? (
46
+ <>
47
+ <Icon
48
+ aria-hidden="true"
49
+ className="shrink-0"
50
+ color="currentColor"
51
+ height={14}
52
+ icon={mouseShortcut.icon}
53
+ width={14}
54
+ />
55
+ <span className="sr-only">{mouseShortcut.label}</span>
56
+ </>
57
+ ) : (
58
+ (displayValue ?? value)
59
+ )}
60
+ </kbd>
61
+ )
62
+ }
63
+
64
+ export { ShortcutToken }