@gunjo/ui 0.0.1-alpha.0 → 0.0.1-alpha.2

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 (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +90 -0
  3. package/README.md +52 -91
  4. package/package.json +47 -6
  5. package/src/components/display/Accordion.tsx +185 -0
  6. package/src/components/display/AccordionGroup.tsx +155 -0
  7. package/src/components/display/ActionDataTable.tsx +413 -0
  8. package/src/components/display/ActivityTimelineCard.tsx +483 -0
  9. package/src/components/display/AnalyticsCard.tsx +167 -0
  10. package/src/components/display/AssetCard.tsx +242 -0
  11. package/src/components/display/AssetGrid.tsx +164 -0
  12. package/src/components/display/Avatar.tsx +127 -0
  13. package/src/components/display/AvatarGroup.tsx +131 -0
  14. package/src/components/{atoms → display}/Badge.tsx +3 -3
  15. package/src/components/display/BarChart.tsx +247 -0
  16. package/src/components/{molecules → display}/Card.tsx +1 -1
  17. package/src/components/display/Carousel.tsx +593 -0
  18. package/src/components/display/ChartLegend.tsx +124 -0
  19. package/src/components/display/ChatMessage.tsx +382 -0
  20. package/src/components/display/ChoroplethMap.tsx +613 -0
  21. package/src/components/display/Code.tsx +42 -0
  22. package/src/components/display/CodeBlock.tsx +338 -0
  23. package/src/components/display/ColorSwatch.tsx +71 -0
  24. package/src/components/display/ConcentricProgressCard.tsx +545 -0
  25. package/src/components/display/DataTable.tsx +522 -0
  26. package/src/components/display/DistributionBar.tsx +102 -0
  27. package/src/components/display/DocNote.tsx +36 -0
  28. package/src/components/display/DonutChart.tsx +257 -0
  29. package/src/components/display/EmptyState.tsx +44 -0
  30. package/src/components/display/FileTree.tsx +180 -0
  31. package/src/components/display/GaugeChart.tsx +219 -0
  32. package/src/components/display/HeatmapChart.tsx +266 -0
  33. package/src/components/display/Icon.tsx +66 -0
  34. package/src/components/display/ImagePreview.tsx +140 -0
  35. package/src/components/{atoms → display}/Img.tsx +46 -12
  36. package/src/components/display/LabeledDonutCard.tsx +475 -0
  37. package/src/components/display/LineChart.tsx +464 -0
  38. package/src/components/{molecules → display}/List.tsx +20 -13
  39. package/src/components/display/MarkdownRenderer.tsx +157 -0
  40. package/src/components/display/MetadataList.tsx +81 -0
  41. package/src/components/display/MiniDistributionBarCard.tsx +314 -0
  42. package/src/components/display/PieChart.tsx +234 -0
  43. package/src/components/display/QuadrantMatrix.tsx +330 -0
  44. package/src/components/display/RadarChart.tsx +335 -0
  45. package/src/components/display/RadialBarChart.tsx +264 -0
  46. package/src/components/display/RetentionCohortCard.tsx +350 -0
  47. package/src/components/display/RibbonChart.tsx +618 -0
  48. package/src/components/display/SearchableAccordion.tsx +270 -0
  49. package/src/components/display/SegmentTimelineCard.tsx +452 -0
  50. package/src/components/display/SegmentedGaugeCard.tsx +607 -0
  51. package/src/components/display/Spacer.tsx +51 -0
  52. package/src/components/display/SparklineChart.tsx +394 -0
  53. package/src/components/display/StackedBarChart.tsx +393 -0
  54. package/src/components/display/Statistic.tsx +70 -0
  55. package/src/components/{molecules → display}/Table.tsx +22 -7
  56. package/src/components/display/Tag.tsx +80 -0
  57. package/src/components/display/TagEditor.tsx +141 -0
  58. package/src/components/display/Timeline.tsx +121 -0
  59. package/src/components/{atoms → display}/ToolPill.tsx +42 -18
  60. package/src/components/display/TreeView.tsx +226 -0
  61. package/src/components/display/chart-tooltip.tsx +423 -0
  62. package/src/components/display/chart-utils.ts +71 -0
  63. package/src/components/display/circular-chart-utils.ts +147 -0
  64. package/src/components/display/generated/default-variant-keys.ts +90 -0
  65. package/src/components/display/generated/variant-keys.ts +169 -0
  66. package/src/components/{atoms → feedback}/Alert.tsx +12 -5
  67. package/src/components/feedback/Banner.tsx +90 -0
  68. package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
  69. package/src/components/feedback/ProgressWidget.tsx +44 -0
  70. package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
  71. package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
  72. package/src/components/feedback/StatusScreen.tsx +148 -0
  73. package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
  74. package/src/components/feedback/Toast.tsx +108 -0
  75. package/src/components/feedback/ToastProvider.tsx +78 -0
  76. package/src/components/feedback/generated/default-variant-keys.ts +16 -0
  77. package/src/components/feedback/generated/variant-keys.ts +21 -0
  78. package/src/components/generated/component-manifest.ts +1568 -454
  79. package/src/components/generated/component-style-hints.ts +1958 -718
  80. package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
  81. package/src/components/inputs/Calendar.tsx +212 -0
  82. package/src/components/inputs/ChatComposer.tsx +75 -0
  83. package/src/components/inputs/ChatInput.tsx +528 -0
  84. package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
  85. package/src/components/inputs/Combobox.tsx +175 -0
  86. package/src/components/inputs/CopyButton.tsx +187 -0
  87. package/src/components/inputs/DatePicker.tsx +519 -0
  88. package/src/components/inputs/DateRangePicker.tsx +878 -0
  89. package/src/components/inputs/EditableField.tsx +182 -0
  90. package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
  91. package/src/components/inputs/FilterButton.tsx +163 -0
  92. package/src/components/{molecules → inputs}/Form.tsx +20 -3
  93. package/src/components/{atoms → inputs}/Input.tsx +2 -0
  94. package/src/components/inputs/InputOTP.tsx +75 -0
  95. package/src/components/inputs/Mention.tsx +279 -0
  96. package/src/components/inputs/NumberInput.tsx +109 -0
  97. package/src/components/inputs/PasswordGroup.tsx +138 -0
  98. package/src/components/inputs/PasswordInput.tsx +74 -0
  99. package/src/components/inputs/PasswordRequirementList.tsx +96 -0
  100. package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
  101. package/src/components/inputs/PhoneInput.tsx +99 -0
  102. package/src/components/inputs/PostalCodeInput.tsx +98 -0
  103. package/src/components/inputs/RangeSlider.tsx +129 -0
  104. package/src/components/inputs/SearchInput.tsx +76 -0
  105. package/src/components/inputs/Select.tsx +39 -0
  106. package/src/components/{atoms → inputs}/Slider.tsx +18 -5
  107. package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
  108. package/src/components/{atoms → inputs}/Switch.tsx +15 -4
  109. package/src/components/inputs/TagInput.tsx +114 -0
  110. package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
  111. package/src/components/inputs/TimePicker.tsx +150 -0
  112. package/src/components/inputs/Toggle.tsx +48 -0
  113. package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
  114. package/src/components/inputs/TooltipButton.tsx +148 -0
  115. package/src/components/inputs/VoiceInputButton.tsx +317 -0
  116. package/src/components/inputs/calendar-holidays.ts +56 -0
  117. package/src/components/inputs/generated/default-variant-keys.ts +32 -0
  118. package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
  119. package/src/components/layout/AspectRatio.tsx +12 -0
  120. package/src/components/layout/AssetInspectorPanel.tsx +416 -0
  121. package/src/components/layout/Cluster.tsx +56 -0
  122. package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
  123. package/src/components/layout/Container.tsx +43 -0
  124. package/src/components/layout/DeviceFrame.tsx +227 -0
  125. package/src/components/layout/Grid.tsx +65 -0
  126. package/src/components/layout/HStack.tsx +73 -0
  127. package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
  128. package/src/components/layout/MarqueeFrame.tsx +158 -0
  129. package/src/components/layout/Resizable.tsx +94 -0
  130. package/src/components/layout/ScrollArea.tsx +71 -0
  131. package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
  132. package/src/components/layout/VStack.tsx +69 -0
  133. package/src/components/layout/generated/default-variant-keys.ts +16 -0
  134. package/src/components/layout/generated/variant-keys.ts +21 -0
  135. package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
  136. package/src/components/navigation/Command.tsx +266 -0
  137. package/src/components/navigation/CommandPalette.tsx +83 -0
  138. package/src/components/navigation/DocumentPager.tsx +171 -0
  139. package/src/components/navigation/Footer.tsx +88 -0
  140. package/src/components/navigation/Header.tsx +80 -0
  141. package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
  142. package/src/components/navigation/NavigationMenu.tsx +128 -0
  143. package/src/components/navigation/PageAside.tsx +84 -0
  144. package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
  145. package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
  146. package/src/components/navigation/Sidebar.tsx +223 -0
  147. package/src/components/navigation/SidebarItem.tsx +160 -0
  148. package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
  149. package/src/components/navigation/TextLink.tsx +71 -0
  150. package/src/components/navigation/generated/default-variant-keys.ts +12 -0
  151. package/src/components/navigation/generated/variant-keys.ts +13 -0
  152. package/src/components/overlay/AIChatInput.tsx +5 -0
  153. package/src/components/overlay/AIChatMessage.tsx +6 -0
  154. package/src/components/overlay/AlertDialog.tsx +145 -0
  155. package/src/components/overlay/ChatPanel.tsx +180 -0
  156. package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
  157. package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
  158. package/src/components/overlay/Drawer.tsx +131 -0
  159. package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
  160. package/src/components/overlay/FloatingPanel.tsx +90 -0
  161. package/src/components/overlay/HoverCard.tsx +36 -0
  162. package/src/components/overlay/MediaLightbox.tsx +403 -0
  163. package/src/components/overlay/MediaPickerDialog.tsx +198 -0
  164. package/src/components/overlay/Modal.tsx +103 -0
  165. package/src/components/overlay/OnboardingFlow.tsx +172 -0
  166. package/src/components/overlay/Popover.tsx +36 -0
  167. package/src/components/overlay/ShareModal.tsx +324 -0
  168. package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
  169. package/src/components/overlay/Tooltip.tsx +130 -0
  170. package/src/components/overlay/generated/default-variant-keys.ts +14 -0
  171. package/src/components/overlay/generated/variant-keys.ts +17 -0
  172. package/src/components/patterns/BlogTemplate.tsx +46 -0
  173. package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
  174. package/src/components/patterns/DocsTemplate.tsx +41 -0
  175. package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
  176. package/src/components/patterns/OnboardingTemplate.tsx +32 -0
  177. package/src/components/patterns/PricingTemplate.tsx +106 -0
  178. package/src/globals.css +173 -22
  179. package/src/index.ts +177 -76
  180. package/tailwind-theme-extend.cjs +48 -3
  181. package/design/atoms-metadata.json +0 -82
  182. package/design/molecules-metadata.json +0 -130
  183. package/design/organisms-metadata.json +0 -38
  184. package/design/templates-metadata.json +0 -38
  185. package/src/components/atoms/Avatar.tsx +0 -57
  186. package/src/components/atoms/Select.tsx +0 -28
  187. package/src/components/atoms/generated/default-variant-keys.ts +0 -36
  188. package/src/components/molecules/AIChatInput.tsx +0 -140
  189. package/src/components/molecules/AIChatMessage.tsx +0 -109
  190. package/src/components/molecules/Accordion.tsx +0 -99
  191. package/src/components/molecules/Calendar.tsx +0 -60
  192. package/src/components/molecules/Carousel.tsx +0 -261
  193. package/src/components/molecules/Command.tsx +0 -152
  194. package/src/components/molecules/FilterButton.tsx +0 -133
  195. package/src/components/molecules/HoverCard.tsx +0 -29
  196. package/src/components/molecules/Modal.tsx +0 -66
  197. package/src/components/molecules/Popover.tsx +0 -31
  198. package/src/components/molecules/ProgressWidget.tsx +0 -40
  199. package/src/components/molecules/Resizable.tsx +0 -47
  200. package/src/components/molecules/ScrollArea.tsx +0 -48
  201. package/src/components/molecules/SidebarItem.tsx +0 -134
  202. package/src/components/molecules/Toast.tsx +0 -57
  203. package/src/components/molecules/Tooltip.tsx +0 -30
  204. package/src/components/molecules/generated/default-variant-keys.ts +0 -22
  205. package/src/components/molecules/generated/variant-keys.ts +0 -33
  206. package/src/components/organisms/CommandPalette.tsx +0 -58
  207. package/src/components/organisms/FloatingPanel.tsx +0 -46
  208. package/src/components/organisms/ShareModal.tsx +0 -182
  209. package/src/components/organisms/ToastProvider.tsx +0 -49
  210. /package/src/components/{atoms → display}/Kbd.tsx +0 -0
  211. /package/src/components/{atoms → display}/Separator.tsx +0 -0
  212. /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
  213. /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
  214. /package/src/components/{atoms → inputs}/Button.tsx +0 -0
  215. /package/src/components/{atoms → inputs}/Label.tsx +0 -0
  216. /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
  217. /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
  218. /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
  219. /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
  220. /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
  221. /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
  222. /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
  223. /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
  224. /package/src/components/{templates → patterns}/SettingsTemplate.tsx +0 -0
@@ -0,0 +1,227 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconLock as Lock, IconDeviceDesktop as Monitor, IconDeviceMobile as Smartphone, IconDeviceTablet as Tablet } from "@tabler/icons-react";
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { deviceFrameDefaultVariantKey } from "./generated/default-variant-keys"
8
+ import type { DeviceFrameVariantKey } from "./generated/variant-keys"
9
+ import { TooltipButton } from "../inputs/TooltipButton"
10
+
11
+ export type MarqueeViewport = "desktop" | "tablet" | "mobile"
12
+
13
+ export interface DeviceFrameLabels {
14
+ url?: string
15
+ desktop?: string
16
+ tablet?: string
17
+ mobile?: string
18
+ }
19
+
20
+ export interface DeviceFrameProps extends React.HTMLAttributes<HTMLDivElement> {
21
+ /** Fake host shown in the URL bar, without a trailing slash. */
22
+ host: string
23
+ /** Current path inside the fake host. */
24
+ path: string
25
+ /** Path used when the URL bar is submitted as `/`. */
26
+ defaultPath?: string
27
+ /** Optional tab title shown beside the URL on tablet and desktop frames. */
28
+ tabTitle?: string
29
+ viewport: MarqueeViewport
30
+ variant?: DeviceFrameVariantKey
31
+ onViewportChange: (next: MarqueeViewport) => void
32
+ /** Optional whitelist for URL-bar navigation. */
33
+ navigablePaths?: string[]
34
+ /** Called with the normalized path after a valid URL-bar submit. */
35
+ onPathChange?: (path: string) => void
36
+ labels?: DeviceFrameLabels
37
+ children: React.ReactNode
38
+ }
39
+
40
+ const VIEWPORTS: {
41
+ key: MarqueeViewport
42
+ Icon: typeof Monitor
43
+ labelKey: keyof DeviceFrameLabels
44
+ fallback: string
45
+ }[] = [
46
+ { key: "desktop", Icon: Monitor, labelKey: "desktop", fallback: "Desktop" },
47
+ { key: "tablet", Icon: Tablet, labelKey: "tablet", fallback: "Tablet" },
48
+ { key: "mobile", Icon: Smartphone, labelKey: "mobile", fallback: "Mobile" },
49
+ ]
50
+
51
+ interface DeviceFrameClassNames {
52
+ root: string
53
+ shell: string
54
+ chrome: string
55
+ url: string
56
+ viewportActive: string
57
+ viewportIdle: string
58
+ }
59
+
60
+ const variantClasses: Record<DeviceFrameVariantKey, DeviceFrameClassNames> = {
61
+ default: {
62
+ root: "p-0",
63
+ shell: "overflow-hidden rounded-xl border border-border/60 bg-background shadow-2xl",
64
+ chrome: "border-b border-border/60 bg-muted/40",
65
+ url: "border-border/40 bg-background/60 focus-within:border-primary focus-within:ring-primary-border",
66
+ viewportActive: "bg-foreground/10 text-foreground",
67
+ viewportIdle: "text-muted-foreground hover:bg-foreground/5 hover:text-foreground",
68
+ },
69
+ windows11: {
70
+ root: "p-0",
71
+ shell: "overflow-hidden rounded-lg border border-border bg-background shadow-xl",
72
+ chrome: "border-b border-border bg-background",
73
+ url: "border-border/70 bg-muted/50 focus-within:border-primary focus-within:ring-primary-border",
74
+ viewportActive: "bg-primary-subtle text-primary-subtle-foreground",
75
+ viewportIdle: "text-muted-foreground hover:bg-muted hover:text-foreground",
76
+ },
77
+ }
78
+
79
+ function normalizeFramePath(value: string, host: string, defaultPath: string) {
80
+ let candidate = value
81
+ .trim()
82
+ .replace(/^https?:\/\//, "")
83
+ .replace(new RegExp(`^${host.replace(/\./g, "\\.")}`), "")
84
+
85
+ if (!candidate.startsWith("/")) candidate = `/${candidate}`
86
+ return candidate === "/" ? defaultPath : candidate
87
+ }
88
+
89
+ const DeviceFrame = React.forwardRef<HTMLDivElement, DeviceFrameProps>(
90
+ (
91
+ {
92
+ host,
93
+ path,
94
+ defaultPath = "/",
95
+ tabTitle,
96
+ viewport,
97
+ variant = deviceFrameDefaultVariantKey,
98
+ onViewportChange,
99
+ navigablePaths,
100
+ onPathChange,
101
+ labels,
102
+ children,
103
+ className,
104
+ ...props
105
+ },
106
+ ref
107
+ ) => {
108
+ const classes = variantClasses[variant]
109
+ const [draftUrl, setDraftUrl] = React.useState(`${host}${path}`)
110
+ const [editing, setEditing] = React.useState(false)
111
+ const inputRef = React.useRef<HTMLInputElement | null>(null)
112
+
113
+ React.useEffect(() => {
114
+ if (!editing) setDraftUrl(`${host}${path}`)
115
+ }, [host, path, editing])
116
+
117
+ const commitDraftUrl = () => {
118
+ const candidate = normalizeFramePath(draftUrl, host, defaultPath)
119
+ if (!navigablePaths || navigablePaths.includes(candidate)) {
120
+ inputRef.current?.blur()
121
+ onPathChange?.(candidate)
122
+ return
123
+ }
124
+ setDraftUrl(`${host}${path}`)
125
+ inputRef.current?.blur()
126
+ }
127
+
128
+ const isCompact = viewport === "mobile"
129
+ const chromeOuter = cn(
130
+ isCompact
131
+ ? "flex items-center gap-2 px-2 py-2.5"
132
+ : "flex items-center gap-3 px-4 py-2.5",
133
+ classes.chrome
134
+ )
135
+ const urlBoxClass = cn(
136
+ isCompact
137
+ ? "flex min-w-0 w-full items-center gap-1.5 rounded-md border px-2 py-1 text-xs focus-within:ring-1"
138
+ : "flex min-w-0 w-full max-w-md items-center gap-2 rounded-md border px-3 py-1 text-xs focus-within:ring-1",
139
+ classes.url
140
+ )
141
+ const isWindows11 = variant === "windows11"
142
+
143
+ return (
144
+ <div ref={ref} className={cn("w-full", classes.root, className)} {...props}>
145
+ <div className={classes.shell}>
146
+ <div className={chromeOuter}>
147
+ {!isCompact && !isWindows11 ? (
148
+ <div className="flex items-center gap-1.5" aria-hidden="true">
149
+ <span className="h-3 w-3 rounded-full bg-destructive" />
150
+ <span className="h-3 w-3 rounded-full bg-warning" />
151
+ <span className="h-3 w-3 rounded-full bg-success" />
152
+ </div>
153
+ ) : null}
154
+ <form
155
+ onSubmit={(event) => {
156
+ event.preventDefault()
157
+ commitDraftUrl()
158
+ }}
159
+ className="flex min-w-0 flex-1 items-center justify-center"
160
+ >
161
+ <div className={urlBoxClass}>
162
+ <Lock className="h-3 w-3 shrink-0 text-muted-foreground" aria-hidden="true" />
163
+ {tabTitle && !isCompact ? (
164
+ <span className="shrink-0 font-medium text-foreground">
165
+ {tabTitle}
166
+ </span>
167
+ ) : null}
168
+ <input
169
+ ref={inputRef}
170
+ type="text"
171
+ value={draftUrl}
172
+ onChange={(event) => setDraftUrl(event.currentTarget.value)}
173
+ onKeyDown={(event) => {
174
+ if (event.key === "Enter") {
175
+ event.preventDefault()
176
+ commitDraftUrl()
177
+ }
178
+ }}
179
+ onFocus={() => setEditing(true)}
180
+ onBlur={() => setEditing(false)}
181
+ className="min-w-0 flex-1 truncate bg-transparent font-mono text-xs text-muted-foreground outline-none placeholder:text-muted-foreground/60"
182
+ spellCheck={false}
183
+ aria-label={labels?.url ?? "URL"}
184
+ />
185
+ </div>
186
+ </form>
187
+ <div className="flex shrink-0 items-center gap-0.5">
188
+ {VIEWPORTS.map(({ key, Icon, labelKey, fallback }) => {
189
+ const label = labels?.[labelKey] ?? fallback
190
+ return (
191
+ <TooltipButton
192
+ key={key}
193
+ type="button"
194
+ onClick={() => onViewportChange(key)}
195
+ aria-label={label}
196
+ aria-pressed={viewport === key}
197
+ tooltip={label}
198
+ tooltipSide="bottom"
199
+ variant="ghost"
200
+ size="icon"
201
+ className={cn(
202
+ "h-7 w-7 rounded-md p-1.5",
203
+ viewport === key ? classes.viewportActive : classes.viewportIdle
204
+ )}
205
+ >
206
+ <Icon className="h-3.5 w-3.5" />
207
+ </TooltipButton>
208
+ )
209
+ })}
210
+ </div>
211
+ {!isCompact && isWindows11 ? (
212
+ <div className="ml-1 flex h-6 shrink-0 items-center overflow-hidden rounded-md border border-border/60 text-muted-foreground" aria-hidden="true">
213
+ <span className="flex h-6 w-8 items-center justify-center text-xs leading-none">-</span>
214
+ <span className="flex h-6 w-8 items-center justify-center text-[10px] leading-none">□</span>
215
+ <span className="flex h-6 w-8 items-center justify-center text-xs leading-none">×</span>
216
+ </div>
217
+ ) : null}
218
+ </div>
219
+ <div className="bg-background">{children}</div>
220
+ </div>
221
+ </div>
222
+ )
223
+ }
224
+ )
225
+ DeviceFrame.displayName = "DeviceFrame"
226
+
227
+ export { DeviceFrame }
@@ -0,0 +1,65 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const GAP_MAP = {
6
+ 0: "gap-0",
7
+ 1: "gap-1",
8
+ 2: "gap-2",
9
+ 3: "gap-3",
10
+ 4: "gap-4",
11
+ 5: "gap-5",
12
+ 6: "gap-6",
13
+ 8: "gap-8",
14
+ } as const
15
+
16
+ const COLS_MAP = {
17
+ 1: "grid-cols-1",
18
+ 2: "grid-cols-2",
19
+ 3: "grid-cols-3",
20
+ 4: "grid-cols-4",
21
+ 5: "grid-cols-5",
22
+ 6: "grid-cols-6",
23
+ 12: "grid-cols-12",
24
+ } as const
25
+
26
+ export interface GridProps extends React.HTMLAttributes<HTMLDivElement> {
27
+ /** Fixed number of columns. Mutually exclusive with `minItemWidth`. */
28
+ cols?: keyof typeof COLS_MAP
29
+ /**
30
+ * If set, uses CSS grid auto-fit with this minimum item width (px).
31
+ * Items wrap and fill available space responsively.
32
+ */
33
+ minItemWidth?: number
34
+ gap?: keyof typeof GAP_MAP
35
+ }
36
+
37
+ const Grid = React.forwardRef<HTMLDivElement, GridProps>(
38
+ ({ className, cols, minItemWidth, gap = 4, style, ...props }, ref) => {
39
+ const useAutoFit = minItemWidth !== undefined
40
+ const computedStyle = useAutoFit
41
+ ? {
42
+ ...style,
43
+ gridTemplateColumns: `repeat(auto-fit, minmax(${minItemWidth}px, 1fr))`,
44
+ }
45
+ : style
46
+
47
+ return (
48
+ <div
49
+ ref={ref}
50
+ style={computedStyle}
51
+ className={cn(
52
+ "grid",
53
+ !useAutoFit && cols !== undefined && COLS_MAP[cols],
54
+ !useAutoFit && cols === undefined && "grid-cols-3",
55
+ GAP_MAP[gap],
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ )
61
+ }
62
+ )
63
+ Grid.displayName = "Grid"
64
+
65
+ export { Grid }
@@ -0,0 +1,73 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const GAP_MAP = {
6
+ 0: "gap-0",
7
+ 1: "gap-1",
8
+ 2: "gap-2",
9
+ 3: "gap-3",
10
+ 4: "gap-4",
11
+ 5: "gap-5",
12
+ 6: "gap-6",
13
+ 8: "gap-8",
14
+ 10: "gap-10",
15
+ 12: "gap-12",
16
+ } as const
17
+
18
+ const ALIGN_MAP = {
19
+ start: "items-start",
20
+ center: "items-center",
21
+ end: "items-end",
22
+ baseline: "items-baseline",
23
+ stretch: "items-stretch",
24
+ } as const
25
+
26
+ const JUSTIFY_MAP = {
27
+ start: "justify-start",
28
+ center: "justify-center",
29
+ end: "justify-end",
30
+ between: "justify-between",
31
+ around: "justify-around",
32
+ evenly: "justify-evenly",
33
+ } as const
34
+
35
+ export interface HStackProps extends React.HTMLAttributes<HTMLDivElement> {
36
+ gap?: keyof typeof GAP_MAP
37
+ align?: keyof typeof ALIGN_MAP
38
+ justify?: keyof typeof JUSTIFY_MAP
39
+ wrap?: boolean
40
+ inline?: boolean
41
+ }
42
+
43
+ const HStack = React.forwardRef<HTMLDivElement, HStackProps>(
44
+ (
45
+ {
46
+ className,
47
+ gap = 2,
48
+ align = "center",
49
+ justify = "start",
50
+ wrap = false,
51
+ inline = false,
52
+ ...props
53
+ },
54
+ ref
55
+ ) => (
56
+ <div
57
+ ref={ref}
58
+ className={cn(
59
+ inline ? "inline-flex" : "flex",
60
+ "flex-row",
61
+ wrap && "flex-wrap",
62
+ GAP_MAP[gap],
63
+ ALIGN_MAP[align],
64
+ JUSTIFY_MAP[justify],
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ )
70
+ )
71
+ HStack.displayName = "HStack"
72
+
73
+ export { HStack }
@@ -5,25 +5,26 @@ import { cn } from "../../lib/utils"
5
5
 
6
6
  export interface InspectorPanelProps extends React.HTMLAttributes<HTMLDivElement> {
7
7
  title?: string
8
+ header?: React.ReactNode
8
9
  footer?: React.ReactNode
9
10
  }
10
11
 
11
12
  export const InspectorPanel = React.forwardRef<HTMLDivElement, InspectorPanelProps>(
12
- ({ className, title, children, footer, ...props }, ref) => {
13
+ ({ className, title, header, children, footer, ...props }, ref) => {
13
14
  return (
14
15
  <div
15
16
  ref={ref}
16
17
  className={cn(
17
- "flex flex-col w-[320px] h-[420px] w-full h-full bg-background border-l border-border",
18
+ "flex h-full w-full flex-col border-l border-border bg-background w-[320px] h-[420px]",
18
19
  className
19
20
  )}
20
21
  {...props}
21
22
  >
22
- {title && (
23
+ {header ?? (title && (
23
24
  <div className="flex items-center h-12 px-4 border-b border-border bg-muted/30">
24
25
  <h3 className="text-sm font-semibold text-foreground">{title}</h3>
25
26
  </div>
26
- )}
27
+ ))}
27
28
  <div className="flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6">
28
29
  {children}
29
30
  </div>
@@ -57,7 +58,7 @@ export const InspectorField = React.forwardRef<
57
58
  HTMLDivElement,
58
59
  React.HTMLAttributes<HTMLDivElement> & { label: string }
59
60
  >(({ className, label, children, ...props }, ref) => (
60
- <div ref={ref} className={cn("grid gap-1.5", className)} {...props}>
61
+ <div ref={ref} className={cn("grid min-w-0 gap-1.5", className)} {...props}>
61
62
  <label className="text-xs font-medium text-foreground">{label}</label>
62
63
  {children}
63
64
  </div>
@@ -0,0 +1,158 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import { DeviceFrame, type DeviceFrameLabels, type MarqueeViewport } from "./DeviceFrame"
7
+ import { marqueeFrameDefaultVariantKey } from "./generated/default-variant-keys"
8
+ import type { MarqueeFrameVariantKey } from "./generated/variant-keys"
9
+
10
+ export type { MarqueeViewport } from "./DeviceFrame"
11
+
12
+ export const MARQUEE_VIEWPORT_SIZES: Record<
13
+ MarqueeViewport,
14
+ { width: number; height: number }
15
+ > = {
16
+ desktop: { width: 1280, height: 720 },
17
+ tablet: { width: 768, height: 1024 },
18
+ mobile: { width: 375, height: 667 },
19
+ }
20
+
21
+ export interface MarqueeFrameProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
22
+ host?: string
23
+ path: string
24
+ defaultPath?: string
25
+ tabTitle?: string
26
+ navigablePaths?: string[]
27
+ onPathChange?: (path: string) => void
28
+ storageKey?: string
29
+ initialViewport?: MarqueeViewport
30
+ variant?: MarqueeFrameVariantKey
31
+ viewportSizes?: Record<MarqueeViewport, { width: number; height: number }>
32
+ maxCanvasHeight?: number
33
+ labels?: DeviceFrameLabels
34
+ children: (viewport: MarqueeViewport) => React.ReactNode
35
+ }
36
+
37
+ const VIEWPORTS: MarqueeViewport[] = ["desktop", "tablet", "mobile"]
38
+
39
+ const variantClasses: Record<MarqueeFrameVariantKey, { root: string }> = {
40
+ default: { root: "" },
41
+ desktop: { root: "" },
42
+ tablet: { root: "" },
43
+ mobile: { root: "" },
44
+ }
45
+
46
+ const MarqueeFrame = React.forwardRef<HTMLDivElement, MarqueeFrameProps>(
47
+ (
48
+ {
49
+ host = "gunjo.example",
50
+ path,
51
+ defaultPath = "/",
52
+ tabTitle = "GunjoUI",
53
+ navigablePaths,
54
+ onPathChange,
55
+ storageKey,
56
+ initialViewport = "desktop",
57
+ variant = marqueeFrameDefaultVariantKey,
58
+ viewportSizes = MARQUEE_VIEWPORT_SIZES,
59
+ maxCanvasHeight = 720,
60
+ labels,
61
+ children,
62
+ className,
63
+ ...props
64
+ },
65
+ ref
66
+ ) => {
67
+ const classes = variantClasses[variant]
68
+ const containerRef = React.useRef<HTMLDivElement | null>(null)
69
+ const [viewport, setViewport] = React.useState<MarqueeViewport>(initialViewport)
70
+ const [scale, setScale] = React.useState(1)
71
+
72
+ React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement)
73
+
74
+ React.useEffect(() => {
75
+ if (!storageKey) return
76
+ const stored = window.sessionStorage.getItem(storageKey)
77
+ if (VIEWPORTS.includes(stored as MarqueeViewport)) {
78
+ setViewport(stored as MarqueeViewport)
79
+ }
80
+ }, [storageKey])
81
+
82
+ const { width: viewportWidth, height: viewportHeight } = viewportSizes[viewport]
83
+
84
+ React.useLayoutEffect(() => {
85
+ const node = containerRef.current
86
+ if (!node) return
87
+ const updateScale = (available: number) => {
88
+ const widthRatio = Math.max(0, available - 8) / viewportWidth
89
+ const heightRatio = maxCanvasHeight / viewportHeight
90
+ const next = Math.min(1, widthRatio, heightRatio)
91
+ const stableNext = Math.round((next > 0 ? next : 1) * 1000) / 1000
92
+ setScale((current) => (Math.abs(current - stableNext) < 0.001 ? current : stableNext))
93
+ }
94
+ updateScale(node.clientWidth)
95
+ const resizeObserver = new ResizeObserver((entries) => {
96
+ const entry = entries[0]
97
+ updateScale(entry?.contentRect.width ?? node.clientWidth)
98
+ })
99
+ resizeObserver.observe(node)
100
+ return () => resizeObserver.disconnect()
101
+ }, [maxCanvasHeight, viewportHeight, viewportWidth])
102
+
103
+ const handleViewportChange = (next: MarqueeViewport) => {
104
+ setViewport(next)
105
+ if (storageKey) window.sessionStorage.setItem(storageKey, next)
106
+ }
107
+
108
+ return (
109
+ <div
110
+ className={cn("w-full bg-muted/40 p-0 px-4 py-8 sm:px-8 lg:py-12", classes.root, className)}
111
+ data-slot="marquee-frame"
112
+ {...props}
113
+ >
114
+ <div ref={containerRef} className="mx-auto w-full" data-slot="marquee-frame-scale-container">
115
+ <div
116
+ className="mx-auto"
117
+ data-slot="marquee-frame-scaled-box"
118
+ style={{
119
+ height: viewportHeight * scale + 56,
120
+ width: viewportWidth * scale,
121
+ }}
122
+ >
123
+ <div
124
+ data-slot="marquee-frame-transform"
125
+ style={{
126
+ transform: `scale(${scale})`,
127
+ transformOrigin: "top left",
128
+ width: viewportWidth,
129
+ }}
130
+ >
131
+ <DeviceFrame
132
+ host={host}
133
+ path={path}
134
+ defaultPath={defaultPath}
135
+ tabTitle={tabTitle}
136
+ viewport={viewport}
137
+ onViewportChange={handleViewportChange}
138
+ navigablePaths={navigablePaths}
139
+ onPathChange={onPathChange}
140
+ labels={labels}
141
+ >
142
+ <div
143
+ className="overflow-hidden bg-background"
144
+ style={{ width: viewportWidth, height: viewportHeight }}
145
+ >
146
+ {children(viewport)}
147
+ </div>
148
+ </DeviceFrame>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ )
154
+ }
155
+ )
156
+ MarqueeFrame.displayName = "MarqueeFrame"
157
+
158
+ export { MarqueeFrame }
@@ -0,0 +1,94 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconGripVertical as GripVertical } from "@tabler/icons-react";
5
+ import { Panel, Group as PanelGroup, Separator as PanelResizeHandle, type GroupImperativeHandle, type GroupProps } from "react-resizable-panels"
6
+
7
+ import { cn } from "../../lib/utils"
8
+
9
+ type ResizablePanelGroupProps = Omit<GroupProps, "orientation"> & {
10
+ direction: "vertical" | "horizontal"
11
+ }
12
+
13
+ const ResizablePanelGroupResetContext = React.createContext<(() => void) | null>(null)
14
+
15
+ const ResizablePanelGroup = ({
16
+ className,
17
+ direction,
18
+ defaultLayout,
19
+ groupRef,
20
+ ...props
21
+ }: ResizablePanelGroupProps) => {
22
+ const internalGroupRef = React.useRef<GroupImperativeHandle | null>(null)
23
+ const setGroupRef = React.useCallback((node: GroupImperativeHandle | null) => {
24
+ internalGroupRef.current = node
25
+
26
+ if (typeof groupRef === "function") {
27
+ groupRef(node)
28
+ return
29
+ }
30
+
31
+ if (groupRef) {
32
+ (groupRef as React.MutableRefObject<GroupImperativeHandle | null>).current = node
33
+ }
34
+ }, [groupRef])
35
+ const resetLayout = React.useCallback(() => {
36
+ if (defaultLayout) {
37
+ internalGroupRef.current?.setLayout(defaultLayout)
38
+ }
39
+ }, [defaultLayout])
40
+
41
+ return (
42
+ <ResizablePanelGroupResetContext.Provider value={defaultLayout ? resetLayout : null}>
43
+ <PanelGroup
44
+ groupRef={setGroupRef}
45
+ defaultLayout={defaultLayout}
46
+ orientation={direction}
47
+ className={cn(
48
+ "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ </ResizablePanelGroupResetContext.Provider>
54
+ )
55
+ }
56
+
57
+ const ResizablePanel = Panel
58
+
59
+ const ResizableHandle = ({
60
+ withHandle,
61
+ className,
62
+ onDoubleClick,
63
+ ...props
64
+ }: React.ComponentProps<typeof PanelResizeHandle> & {
65
+ withHandle?: boolean
66
+ }) => {
67
+ const resetLayout = React.useContext(ResizablePanelGroupResetContext)
68
+ const handleDoubleClick = React.useCallback((event: React.MouseEvent<HTMLDivElement>) => {
69
+ onDoubleClick?.(event)
70
+
71
+ if (!event.defaultPrevented) {
72
+ resetLayout?.()
73
+ }
74
+ }, [onDoubleClick, resetLayout])
75
+
76
+ return (
77
+ <PanelResizeHandle
78
+ className={cn(
79
+ "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[aria-orientation=horizontal]]:h-px [&[aria-orientation=horizontal]]:w-full [&[aria-orientation=horizontal]]:after:left-0 [&[aria-orientation=horizontal]]:after:top-1/2 [&[aria-orientation=horizontal]]:after:h-1 [&[aria-orientation=horizontal]]:after:w-full [&[aria-orientation=horizontal]]:after:-translate-y-1/2 [&[aria-orientation=horizontal]]:after:translate-x-0 [&[aria-orientation=horizontal]>div]:rotate-90",
80
+ className
81
+ )}
82
+ onDoubleClick={handleDoubleClick}
83
+ {...props}
84
+ >
85
+ {withHandle && (
86
+ <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
87
+ <GripVertical className="h-2.5 w-2.5" />
88
+ </div>
89
+ )}
90
+ </PanelResizeHandle>
91
+ )
92
+ }
93
+
94
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle }