@puzzle-ui/ui 0.3.0 → 0.5.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 (240) hide show
  1. package/README.md +70 -70
  2. package/dist/components/actions/button/button.css +5 -2
  3. package/dist/components/actions/button-group/button-group.css +16 -17
  4. package/dist/components/data-display/badge/badge.css +15 -2
  5. package/dist/components/data-display/badge/index.cjs +13 -2
  6. package/dist/components/data-display/badge/index.cjs.map +1 -1
  7. package/dist/components/data-display/badge/index.d.cts +7 -2
  8. package/dist/components/data-display/badge/index.d.ts +7 -2
  9. package/dist/components/data-display/badge/index.js +13 -2
  10. package/dist/components/data-display/badge/index.js.map +1 -1
  11. package/dist/components/data-display/empty-state/empty-state.css +128 -0
  12. package/dist/components/data-display/empty-state/empty-state.utils.cjs +6 -0
  13. package/dist/components/data-display/empty-state/empty-state.utils.cjs.map +1 -0
  14. package/dist/components/data-display/empty-state/empty-state.utils.js +6 -0
  15. package/dist/components/data-display/empty-state/empty-state.utils.js.map +1 -0
  16. package/dist/components/data-display/empty-state/index.cjs +49 -0
  17. package/dist/components/data-display/empty-state/index.cjs.map +1 -0
  18. package/dist/components/data-display/empty-state/index.d.cts +19 -0
  19. package/dist/components/data-display/empty-state/index.d.ts +19 -0
  20. package/dist/components/data-display/empty-state/index.js +44 -0
  21. package/dist/components/data-display/empty-state/index.js.map +1 -0
  22. package/dist/components/data-display/rating/index.cjs +84 -0
  23. package/dist/components/data-display/rating/index.cjs.map +1 -0
  24. package/dist/components/data-display/rating/index.d.cts +3 -0
  25. package/dist/components/data-display/rating/index.d.ts +3 -0
  26. package/dist/components/data-display/rating/index.js +84 -0
  27. package/dist/components/data-display/rating/index.js.map +1 -0
  28. package/dist/components/data-display/rating/rating.css +72 -0
  29. package/dist/components/data-display/rating/rating.types.d.cts +15 -0
  30. package/dist/components/data-display/rating/rating.types.d.ts +15 -0
  31. package/dist/components/data-display/rating/rating.utils.cjs +27 -0
  32. package/dist/components/data-display/rating/rating.utils.cjs.map +1 -0
  33. package/dist/components/data-display/rating/rating.utils.js +25 -0
  34. package/dist/components/data-display/rating/rating.utils.js.map +1 -0
  35. package/dist/components/editors/text-editor/editor.cjs.map +1 -1
  36. package/dist/components/editors/text-editor/editor.js.map +1 -1
  37. package/dist/components/editors/text-editor/text-document.cjs.map +1 -1
  38. package/dist/components/editors/text-editor/text-document.js.map +1 -1
  39. package/dist/components/editors/text-editor/text-parser.cjs.map +1 -1
  40. package/dist/components/editors/text-editor/text-parser.js.map +1 -1
  41. package/dist/components/editors/text-editor/text-renderer.cjs.map +1 -1
  42. package/dist/components/editors/text-editor/text-renderer.js.map +1 -1
  43. package/dist/components/forms/autocomplete/index.cjs.map +1 -1
  44. package/dist/components/forms/autocomplete/index.js.map +1 -1
  45. package/dist/components/forms/checkbox/checkbox.css +2 -10
  46. package/dist/components/forms/color-picker/color-picker.css +351 -0
  47. package/dist/components/forms/color-picker/color-picker.types.d.cts +25 -0
  48. package/dist/components/forms/color-picker/color-picker.types.d.ts +25 -0
  49. package/dist/components/forms/color-picker/color-picker.utils.cjs +116 -0
  50. package/dist/components/forms/color-picker/color-picker.utils.cjs.map +1 -0
  51. package/dist/components/forms/color-picker/color-picker.utils.js +109 -0
  52. package/dist/components/forms/color-picker/color-picker.utils.js.map +1 -0
  53. package/dist/components/forms/color-picker/index.cjs +340 -0
  54. package/dist/components/forms/color-picker/index.cjs.map +1 -0
  55. package/dist/components/forms/color-picker/index.d.cts +3 -0
  56. package/dist/components/forms/color-picker/index.d.ts +3 -0
  57. package/dist/components/forms/color-picker/index.js +340 -0
  58. package/dist/components/forms/color-picker/index.js.map +1 -0
  59. package/dist/components/forms/file-upload/file-upload.utils.cjs.map +1 -1
  60. package/dist/components/forms/file-upload/file-upload.utils.js.map +1 -1
  61. package/dist/components/forms/input/number-input.js +5 -5
  62. package/dist/components/forms/input/number-input.js.map +1 -1
  63. package/dist/components/forms/input-group/index.d.cts +3 -0
  64. package/dist/components/forms/input-group/index.d.ts +3 -0
  65. package/dist/components/forms/input-group/input-group.cjs +18 -0
  66. package/dist/components/forms/input-group/input-group.cjs.map +1 -0
  67. package/dist/components/forms/input-group/input-group.css +62 -0
  68. package/dist/components/forms/input-group/input-group.d.cts +7 -0
  69. package/dist/components/forms/input-group/input-group.d.ts +7 -0
  70. package/dist/components/forms/input-group/input-group.js +18 -0
  71. package/dist/components/forms/input-group/input-group.js.map +1 -0
  72. package/dist/components/forms/input-group/input-group.types.d.cts +7 -0
  73. package/dist/components/forms/input-group/input-group.types.d.ts +7 -0
  74. package/dist/components/forms/input-group/input-group.utils.cjs +5 -0
  75. package/dist/components/forms/input-group/input-group.utils.cjs.map +1 -0
  76. package/dist/components/forms/input-group/input-group.utils.js +5 -0
  77. package/dist/components/forms/input-group/input-group.utils.js.map +1 -0
  78. package/dist/components/forms/radio-group/index.d.cts +2 -1
  79. package/dist/components/forms/radio-group/index.d.ts +2 -1
  80. package/dist/components/forms/select/index.cjs.map +1 -1
  81. package/dist/components/forms/select/index.js.map +1 -1
  82. package/dist/components/forms/slider/index.cjs +1 -1
  83. package/dist/components/forms/slider/index.cjs.map +1 -1
  84. package/dist/components/forms/slider/index.js +1 -1
  85. package/dist/components/forms/slider/index.js.map +1 -1
  86. package/dist/components/forms/stepper/index.d.cts +4 -2
  87. package/dist/components/forms/stepper/index.d.ts +4 -2
  88. package/dist/components/forms/time-picker/index.cjs +108 -0
  89. package/dist/components/forms/time-picker/index.cjs.map +1 -0
  90. package/dist/components/forms/time-picker/index.d.cts +3 -0
  91. package/dist/components/forms/time-picker/index.d.ts +3 -0
  92. package/dist/components/forms/time-picker/index.js +108 -0
  93. package/dist/components/forms/time-picker/index.js.map +1 -0
  94. package/dist/components/forms/time-picker/time-picker.css +166 -0
  95. package/dist/components/forms/time-picker/time-picker.types.d.cts +34 -0
  96. package/dist/components/forms/time-picker/time-picker.types.d.ts +34 -0
  97. package/dist/components/forms/time-picker/time-picker.utils.cjs +25 -0
  98. package/dist/components/forms/time-picker/time-picker.utils.cjs.map +1 -0
  99. package/dist/components/forms/time-picker/time-picker.utils.js +24 -0
  100. package/dist/components/forms/time-picker/time-picker.utils.js.map +1 -0
  101. package/dist/components/forms/token-input/index.cjs +189 -0
  102. package/dist/components/forms/token-input/index.cjs.map +1 -0
  103. package/dist/components/forms/token-input/index.d.cts +3 -0
  104. package/dist/components/forms/token-input/index.d.ts +3 -0
  105. package/dist/components/forms/token-input/index.js +189 -0
  106. package/dist/components/forms/token-input/index.js.map +1 -0
  107. package/dist/components/forms/token-input/token-input.css +145 -0
  108. package/dist/components/forms/token-input/token-input.types.d.cts +48 -0
  109. package/dist/components/forms/token-input/token-input.types.d.ts +48 -0
  110. package/dist/components/forms/token-input/token-input.utils.cjs +30 -0
  111. package/dist/components/forms/token-input/token-input.utils.cjs.map +1 -0
  112. package/dist/components/forms/token-input/token-input.utils.js +24 -0
  113. package/dist/components/forms/token-input/token-input.utils.js.map +1 -0
  114. package/dist/components/layout/aspect-ratio/aspect-ratio.css +14 -0
  115. package/dist/components/layout/aspect-ratio/aspect-ratio.utils.cjs +11 -0
  116. package/dist/components/layout/aspect-ratio/aspect-ratio.utils.cjs.map +1 -0
  117. package/dist/components/layout/aspect-ratio/aspect-ratio.utils.js +11 -0
  118. package/dist/components/layout/aspect-ratio/aspect-ratio.utils.js.map +1 -0
  119. package/dist/components/layout/aspect-ratio/index.cjs +16 -0
  120. package/dist/components/layout/aspect-ratio/index.cjs.map +1 -0
  121. package/dist/components/layout/aspect-ratio/index.d.cts +11 -0
  122. package/dist/components/layout/aspect-ratio/index.d.ts +11 -0
  123. package/dist/components/layout/aspect-ratio/index.js +16 -0
  124. package/dist/components/layout/aspect-ratio/index.js.map +1 -0
  125. package/dist/components/layout/container/container.css +1 -0
  126. package/dist/components/layout/page-layout/page-layout.css +1 -0
  127. package/dist/components/layout/resizable-panels/index.cjs +1 -1
  128. package/dist/components/layout/resizable-panels/index.cjs.map +1 -1
  129. package/dist/components/layout/resizable-panels/index.js +1 -1
  130. package/dist/components/layout/resizable-panels/index.js.map +1 -1
  131. package/dist/components/layout/resizable-panels/resizable-panels.utils.cjs.map +1 -1
  132. package/dist/components/layout/resizable-panels/resizable-panels.utils.js.map +1 -1
  133. package/dist/components/layout/scroll-area/index.cjs +27 -0
  134. package/dist/components/layout/scroll-area/index.cjs.map +1 -0
  135. package/dist/components/layout/scroll-area/index.d.cts +15 -0
  136. package/dist/components/layout/scroll-area/index.d.ts +15 -0
  137. package/dist/components/layout/scroll-area/index.js +27 -0
  138. package/dist/components/layout/scroll-area/index.js.map +1 -0
  139. package/dist/components/layout/scroll-area/scroll-area.css +25 -0
  140. package/dist/components/layout/scroll-area/scroll-area.utils.cjs +8 -0
  141. package/dist/components/layout/scroll-area/scroll-area.utils.cjs.map +1 -0
  142. package/dist/components/layout/scroll-area/scroll-area.utils.d.cts +2 -0
  143. package/dist/components/layout/scroll-area/scroll-area.utils.d.ts +2 -0
  144. package/dist/components/layout/scroll-area/scroll-area.utils.js +8 -0
  145. package/dist/components/layout/scroll-area/scroll-area.utils.js.map +1 -0
  146. package/dist/components/layout/section/index.cjs +10 -4
  147. package/dist/components/layout/section/index.cjs.map +1 -1
  148. package/dist/components/layout/section/index.d.cts +1 -1
  149. package/dist/components/layout/section/index.d.ts +1 -1
  150. package/dist/components/layout/section/index.js +11 -5
  151. package/dist/components/layout/section/index.js.map +1 -1
  152. package/dist/components/layout/section/section.css +9 -3
  153. package/dist/components/layout/section/types.d.cts +5 -5
  154. package/dist/components/layout/section/types.d.ts +5 -5
  155. package/dist/components/layout/separator/index.cjs +18 -0
  156. package/dist/components/layout/separator/index.cjs.map +1 -0
  157. package/dist/components/layout/separator/index.d.cts +17 -0
  158. package/dist/components/layout/separator/index.d.ts +17 -0
  159. package/dist/components/layout/separator/index.js +18 -0
  160. package/dist/components/layout/separator/index.js.map +1 -0
  161. package/dist/components/layout/separator/separator.css +15 -0
  162. package/dist/components/layout/separator/separator.utils.cjs +11 -0
  163. package/dist/components/layout/separator/separator.utils.cjs.map +1 -0
  164. package/dist/components/layout/separator/separator.utils.d.cts +2 -0
  165. package/dist/components/layout/separator/separator.utils.d.ts +2 -0
  166. package/dist/components/layout/separator/separator.utils.js +11 -0
  167. package/dist/components/layout/separator/separator.utils.js.map +1 -0
  168. package/dist/components/layout/stack/index.cjs +18 -2
  169. package/dist/components/layout/stack/index.cjs.map +1 -1
  170. package/dist/components/layout/stack/index.d.cts +8 -6
  171. package/dist/components/layout/stack/index.d.ts +8 -6
  172. package/dist/components/layout/stack/index.js +18 -2
  173. package/dist/components/layout/stack/index.js.map +1 -1
  174. package/dist/components/layout/stack/stack.css +118 -138
  175. package/dist/components/navigation/accordion/index.cjs.map +1 -1
  176. package/dist/components/navigation/accordion/index.js.map +1 -1
  177. package/dist/components/navigation/carousel/index.cjs +5 -2
  178. package/dist/components/navigation/carousel/index.cjs.map +1 -1
  179. package/dist/components/navigation/carousel/index.js +5 -2
  180. package/dist/components/navigation/carousel/index.js.map +1 -1
  181. package/dist/components/navigation/pagination/index.cjs +146 -0
  182. package/dist/components/navigation/pagination/index.cjs.map +1 -0
  183. package/dist/components/navigation/pagination/index.d.cts +3 -0
  184. package/dist/components/navigation/pagination/index.d.ts +3 -0
  185. package/dist/components/navigation/pagination/index.js +146 -0
  186. package/dist/components/navigation/pagination/index.js.map +1 -0
  187. package/dist/components/navigation/pagination/pagination.css +134 -0
  188. package/dist/components/navigation/pagination/pagination.types.d.cts +38 -0
  189. package/dist/components/navigation/pagination/pagination.types.d.ts +38 -0
  190. package/dist/components/navigation/pagination/pagination.utils.cjs +54 -0
  191. package/dist/components/navigation/pagination/pagination.utils.cjs.map +1 -0
  192. package/dist/components/navigation/pagination/pagination.utils.js +53 -0
  193. package/dist/components/navigation/pagination/pagination.utils.js.map +1 -0
  194. package/dist/components/navigation/tabs/index.d.cts +3 -2
  195. package/dist/components/navigation/tabs/index.d.ts +3 -2
  196. package/dist/components/navigation/tree-view/index.cjs.map +1 -1
  197. package/dist/components/navigation/tree-view/index.js.map +1 -1
  198. package/dist/components/overlays/context-menu/context-menu.css +151 -0
  199. package/dist/components/overlays/context-menu/context-menu.types.d.cts +69 -0
  200. package/dist/components/overlays/context-menu/context-menu.types.d.ts +69 -0
  201. package/dist/components/overlays/context-menu/context-menu.utils.cjs +46 -0
  202. package/dist/components/overlays/context-menu/context-menu.utils.cjs.map +1 -0
  203. package/dist/components/overlays/context-menu/context-menu.utils.js +40 -0
  204. package/dist/components/overlays/context-menu/context-menu.utils.js.map +1 -0
  205. package/dist/components/overlays/context-menu/index.cjs +830 -0
  206. package/dist/components/overlays/context-menu/index.cjs.map +1 -0
  207. package/dist/components/overlays/context-menu/index.d.cts +42 -0
  208. package/dist/components/overlays/context-menu/index.d.ts +42 -0
  209. package/dist/components/overlays/context-menu/index.js +818 -0
  210. package/dist/components/overlays/context-menu/index.js.map +1 -0
  211. package/dist/components/overlays/dropdown-menu/dropdown-menu-content.cjs.map +1 -1
  212. package/dist/components/overlays/dropdown-menu/dropdown-menu-content.js.map +1 -1
  213. package/dist/components/overlays/dropdown-menu/dropdown-menu.utils.cjs.map +1 -1
  214. package/dist/components/overlays/dropdown-menu/dropdown-menu.utils.js.map +1 -1
  215. package/dist/components/typography/heading.cjs +6 -2
  216. package/dist/components/typography/heading.cjs.map +1 -1
  217. package/dist/components/typography/heading.d.cts +3 -2
  218. package/dist/components/typography/heading.d.ts +3 -2
  219. package/dist/components/typography/heading.js +6 -2
  220. package/dist/components/typography/heading.js.map +1 -1
  221. package/dist/components/typography/text.cjs +6 -2
  222. package/dist/components/typography/text.cjs.map +1 -1
  223. package/dist/components/typography/text.d.cts +3 -2
  224. package/dist/components/typography/text.d.ts +3 -2
  225. package/dist/components/typography/text.js +6 -2
  226. package/dist/components/typography/text.js.map +1 -1
  227. package/dist/components/utilities/preview/index.cjs +89 -21
  228. package/dist/components/utilities/preview/index.cjs.map +1 -1
  229. package/dist/components/utilities/preview/index.d.cts +6 -2
  230. package/dist/components/utilities/preview/index.d.ts +6 -2
  231. package/dist/components/utilities/preview/index.js +89 -21
  232. package/dist/components/utilities/preview/index.js.map +1 -1
  233. package/dist/components/utilities/preview/preview.css +26 -11
  234. package/dist/components/utilities/transition/index.d.cts +1 -1
  235. package/dist/components/utilities/transition/index.d.ts +1 -1
  236. package/dist/index.cjs +136 -97
  237. package/dist/index.d.cts +23 -2
  238. package/dist/index.d.ts +23 -2
  239. package/dist/index.js +12 -1
  240. package/package.json +7 -6
@@ -9,6 +9,7 @@ let _puzzle_ui_hooks = require("@puzzle-ui/hooks");
9
9
  let react_dom = require("react-dom");
10
10
  let _puzzle_ui_icons = require("@puzzle-ui/icons");
11
11
  let _puzzle_ui_theme_scope_context = require("@puzzle-ui/theme/scope-context");
12
+ let _puzzle_ui_theme = require("@puzzle-ui/theme");
12
13
  // src/components/utilities/preview/index.tsx
13
14
  const PREVIEW_DEVICES = {
14
15
  mobile: {
@@ -61,7 +62,7 @@ const PreviewBackground = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
61
62
  patternOpacity: .25,
62
63
  ...props
63
64
  });
64
- const Preview = ({ children, className, defaultDevice = "mobile", code, height, align, justify, hideDevices = false }) => {
65
+ const Preview = ({ children, className, defaultDevice = "mobile", code, fit = "content", height, padding = "var(--puzzle-space-8, 2rem)", align = "center", justify = "center", hideDevices = false }) => {
65
66
  const generatedId = (0, react.useId)();
66
67
  const frameRootId = `${generatedId}-root`;
67
68
  const previewTabId = `${generatedId}-tab-preview`;
@@ -73,15 +74,28 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
73
74
  const [frameRoot, setFrameRoot] = (0, react.useState)(null);
74
75
  const [scale, setScale] = (0, react.useState)(1);
75
76
  const [copied, setCopied] = (0, react.useState)(false);
77
+ const [contentHeight, setContentHeight] = (0, react.useState)(PREVIEW_DEVICES.mobile.height);
78
+ const [contentMeasured, setContentMeasured] = (0, react.useState)(false);
76
79
  const containerRef = (0, react.useRef)(null);
80
+ const contentRef = (0, react.useRef)(null);
77
81
  const iframeRef = (0, react.useRef)(null);
78
82
  const copyTimeoutRef = (0, react.useRef)(null);
79
83
  const config = PREVIEW_DEVICES[device];
80
- const frameHeight = height ?? config.height;
84
+ const fitsContent = height === void 0 && fit === "content";
85
+ const frameHeight = height ?? (fitsContent ? contentHeight : config.height);
86
+ const showPlaceholder = fitsContent && !contentMeasured;
81
87
  const hasCode = code !== void 0;
82
88
  const [direction, setDirection] = (0, react.useState)("ltr");
89
+ const [desktopControlsVisible, setDesktopControlsVisible] = (0, react.useState)(true);
83
90
  const sectionRef = (0, react.useRef)(null);
84
91
  const isKeyboardNavigation = (0, react.useRef)(false);
92
+ (0, react.useEffect)(() => {
93
+ const query = window.matchMedia(`(min-width: ${_puzzle_ui_theme.BREAKPOINTS.desktop}px)`);
94
+ const update = () => setDesktopControlsVisible(query.matches);
95
+ update();
96
+ query.addEventListener("change", update);
97
+ return () => query.removeEventListener("change", update);
98
+ }, []);
85
99
  (0, react.useLayoutEffect)(() => {
86
100
  const section = sectionRef.current;
87
101
  if (section) {
@@ -95,7 +109,10 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
95
109
  initialActiveId: activeTab
96
110
  });
97
111
  const deviceFocus = (0, _puzzle_ui_hooks.useRovingFocus)({
98
- items: PREVIEW_DEVICE_ITEMS,
112
+ items: PREVIEW_DEVICE_ITEMS.map((item) => ({
113
+ ...item,
114
+ disabled: item.id === "desktop" && !desktopControlsVisible
115
+ })),
99
116
  dir: direction,
100
117
  initialActiveId: device
101
118
  });
@@ -142,6 +159,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
142
159
  targetRoot.style.setProperty("height", "100%", "important");
143
160
  targetRoot.style.setProperty("min-height", "100%", "important");
144
161
  targetRoot.style.setProperty("margin", "0", "important");
162
+ targetRoot.style.setProperty("padding", "0", "important");
145
163
  targetRoot.style.setProperty("overflow-x", "hidden", "important");
146
164
  targetRoot.style.setProperty("overflow-y", "auto", "important");
147
165
  targetRoot.style.setProperty("scrollbar-width", "none", "important");
@@ -153,6 +171,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
153
171
  body.style.setProperty("height", "auto", "important");
154
172
  body.style.setProperty("min-height", "100%", "important");
155
173
  body.style.setProperty("margin", "0", "important");
174
+ body.style.setProperty("padding", "0", "important");
156
175
  body.style.setProperty("overflow", "visible", "important");
157
176
  frameDocument.getElementById(frameRootId)?.style.removeProperty("min-height");
158
177
  const scrollbarStyle = frameDocument.createElement("style");
@@ -174,6 +193,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
174
193
  frameHead.appendChild(scrollbarStyle);
175
194
  }, [frameRootId]);
176
195
  const updateScale = (0, react.useCallback)((availableWidth) => {
196
+ if (availableWidth <= 0) return;
177
197
  const nextScale = Math.min(1, Math.max(0, availableWidth) / config.width);
178
198
  setScale((current) => Math.abs(current - nextScale) < .001 ? current : nextScale);
179
199
  }, [config.width]);
@@ -182,7 +202,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
182
202
  if (!container) return;
183
203
  const measure = () => {
184
204
  const styles = window.getComputedStyle(container);
185
- const horizontalPadding = parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight);
205
+ const horizontalPadding = (Number.parseFloat(styles.paddingLeft) || 0) + (Number.parseFloat(styles.paddingRight) || 0);
186
206
  updateScale(container.clientWidth - horizontalPadding);
187
207
  };
188
208
  measure();
@@ -228,6 +248,33 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
228
248
  (0, react.useEffect)(() => {
229
249
  initializeFrame();
230
250
  }, [initializeFrame]);
251
+ useIsomorphicLayoutEffect(() => {
252
+ const content = contentRef.current;
253
+ if (!fitsContent || !frameRoot || !content || activeTab !== "preview") return;
254
+ const measure = () => {
255
+ const measuredHeight = Math.ceil(Math.max(content.getBoundingClientRect().height, content.scrollHeight));
256
+ setContentHeight(Math.min(PREVIEW_DEVICES.mobile.height, Math.max(1, measuredHeight)));
257
+ setContentMeasured(true);
258
+ };
259
+ measure();
260
+ const observer = new ResizeObserver(measure);
261
+ observer.observe(content);
262
+ const mutations = new MutationObserver(measure);
263
+ mutations.observe(content, {
264
+ subtree: true,
265
+ childList: true,
266
+ characterData: true,
267
+ attributes: true
268
+ });
269
+ return () => {
270
+ observer.disconnect();
271
+ mutations.disconnect();
272
+ };
273
+ }, [
274
+ activeTab,
275
+ fitsContent,
276
+ frameRoot
277
+ ]);
231
278
  const handleCopyCode = (0, react.useCallback)(async () => {
232
279
  if (code === void 0) return;
233
280
  try {
@@ -251,18 +298,26 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
251
298
  width: config.width,
252
299
  height: frameHeight,
253
300
  transform: `scale(${scale})`,
254
- transformOrigin: "top left"
301
+ transformOrigin: "top left",
302
+ visibility: showPlaceholder ? "hidden" : void 0
255
303
  };
256
304
  const contentStyle = {
257
305
  display: "flex",
258
306
  boxSizing: "border-box",
259
- minHeight: "100vh",
307
+ padding,
308
+ minHeight: fitsContent ? void 0 : "100vh",
260
309
  alignItems: align ? ALIGN_VALUES[align] : void 0,
261
310
  justifyContent: justify ? JUSTIFY_VALUES[justify] : void 0
262
311
  };
263
312
  const viewportStyle = {
264
313
  width: config.width * scale,
265
- aspectRatio: `${config.width} / ${frameHeight}`
314
+ height: showPlaceholder ? void 0 : frameHeight * scale,
315
+ maxHeight: showPlaceholder ? PREVIEW_DEVICES.mobile.height * scale : void 0
316
+ };
317
+ const placeholderStyle = {
318
+ ...contentStyle,
319
+ width: config.width,
320
+ zoom: scale === 1 ? void 0 : scale
266
321
  };
267
322
  const panelProps = {
268
323
  id: panelId,
@@ -313,6 +368,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
313
368
  role: "group",
314
369
  "aria-label": "Device size",
315
370
  children: Object.keys(PREVIEW_DEVICES).map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
371
+ "data-device": item,
316
372
  ref: deviceFocus.getItemRef(item),
317
373
  type: "button",
318
374
  className: (0, _puzzle_ui_core.cn)("puzzle-preview__device", device === item && "puzzle-preview__device--active"),
@@ -353,21 +409,33 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
353
409
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
354
410
  className: "puzzle-preview__viewport",
355
411
  style: viewportStyle,
356
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
357
- ref: iframeRef,
358
- className: "puzzle-preview__frame",
359
- srcDoc: createPreviewDocument(frameRootId),
360
- title: `${config.label} component preview`,
361
- onLoad: initializeFrame,
362
- style: frameStyle
363
- }), frameRoot && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_theme_scope_context.ThemeScopeProvider, {
364
- scopeId: themeScope?.scopeId ?? null,
365
- theme: themeScope?.theme ?? "system",
366
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
367
- style: contentStyle,
412
+ children: [
413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
414
+ ref: iframeRef,
415
+ className: "puzzle-preview__frame",
416
+ srcDoc: createPreviewDocument(frameRootId),
417
+ title: `${config.label} component preview`,
418
+ onLoad: initializeFrame,
419
+ style: frameStyle
420
+ }),
421
+ showPlaceholder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
422
+ className: "puzzle-preview__placeholder",
423
+ style: placeholderStyle,
424
+ "aria-hidden": "true",
425
+ inert: true,
426
+ "data-preview-placeholder": "",
368
427
  children
369
- })
370
- }), frameRoot)]
428
+ }),
429
+ frameRoot && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_puzzle_ui_theme_scope_context.ThemeScopeProvider, {
430
+ scopeId: themeScope?.scopeId ?? null,
431
+ theme: themeScope?.theme ?? "system",
432
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
433
+ ref: contentRef,
434
+ style: contentStyle,
435
+ children
436
+ })
437
+ }), frameRoot)
438
+ ]
371
439
  })
372
440
  }), activeTab === "code" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
373
441
  className: "puzzle-preview__code",
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["useEffect","useLayoutEffect","Background","useId","useThemeScope","useState","useRef","useRovingFocus","useCallback","cn","Icon","createPortal","ThemeScopeProvider","highlightCode"],"sources":["../../../../src/components/utilities/preview/index.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentProps,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { cn } from \"@puzzle-ui/core\";\nimport { useRovingFocus } from \"@puzzle-ui/hooks\";\nimport { Icon } from \"@puzzle-ui/icons\";\nimport { ThemeScopeProvider, useThemeScope } from \"@puzzle-ui/theme/scope-context\";\n\nimport { Background } from \"../../layout/background\";\nimport { highlightCode } from \"./highlight\";\n\nimport \"./preview.css\";\n\ntype PreviewDevice = \"mobile\" | \"tablet\" | \"desktop\";\ntype PreviewTab = \"preview\" | \"code\";\n\ntype DeviceConfig = {\n label: string;\n width: number;\n height: number;\n};\n\ntype PreviewAlign = \"start\" | \"center\" | \"end\" | \"stretch\";\ntype PreviewJustify =\n | \"start\"\n | \"center\"\n | \"end\"\n | \"between\"\n | \"around\"\n | \"evenly\";\n\ntype PreviewProps = {\n children: ReactNode;\n className?: string;\n defaultDevice?: PreviewDevice;\n code?: string;\n /** Frame height in pixels. Defaults to the selected device's height. */\n height?: number;\n /** Vertical alignment of the content within the frame. */\n align?: PreviewAlign;\n /** Horizontal distribution of the content within the frame. */\n justify?: PreviewJustify;\n /** Hides the device selector. The default device is still used. */\n hideDevices?: boolean;\n};\n\nconst PREVIEW_DEVICES: Record<PreviewDevice, DeviceConfig> = {\n mobile: { label: \"Mobile\", width: 390, height: 844 },\n tablet: { label: \"Tablet\", width: 768, height: 1024 },\n desktop: { label: \"Desktop\", width: 1024, height: 768 },\n};\n\nconst PREVIEW_DEVICE_ITEMS = (\n Object.keys(PREVIEW_DEVICES) as PreviewDevice[]\n).map((id) => ({ id }));\n\nconst ALIGN_VALUES: Record<PreviewAlign, string> = {\n start: \"flex-start\",\n center: \"center\",\n end: \"flex-end\",\n stretch: \"stretch\",\n};\n\nconst JUSTIFY_VALUES: Record<PreviewJustify, string> = {\n start: \"flex-start\",\n center: \"center\",\n end: \"flex-end\",\n between: \"space-between\",\n around: \"space-around\",\n evenly: \"space-evenly\",\n};\n\nconst createPreviewDocument = (rootId: string) => `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n </head>\n <body>\n <div id=\"${rootId}\"></div>\n </body>\n</html>`;\n\nconst useIsomorphicLayoutEffect =\n typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nconst PreviewBackground = (props: ComponentProps<\"div\">) => (\n <Background\n pattern=\"blueprint\"\n size={20}\n majorSize={100}\n lineWidth={1}\n patternOpacity={0.25}\n {...props}\n />\n);\n\nconst Preview = ({\n children,\n className,\n defaultDevice = \"mobile\",\n code,\n height,\n align,\n justify,\n hideDevices = false,\n}: PreviewProps) => {\n const generatedId = useId();\n const frameRootId = `${generatedId}-root`;\n const previewTabId = `${generatedId}-tab-preview`;\n const codeTabId = `${generatedId}-tab-code`;\n const panelId = `${generatedId}-panel`;\n\n const themeScope = useThemeScope();\n\n const [device, setDevice] = useState<PreviewDevice>(defaultDevice);\n const [activeTab, setActiveTab] = useState<PreviewTab>(\"preview\");\n const [frameRoot, setFrameRoot] = useState<HTMLElement | null>(null);\n const [scale, setScale] = useState(1);\n const [copied, setCopied] = useState(false);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const config = PREVIEW_DEVICES[device];\n const frameHeight = height ?? config.height;\n const hasCode = code !== undefined;\n const [direction, setDirection] = useState<\"ltr\" | \"rtl\">(\"ltr\");\n const sectionRef = useRef<HTMLElement>(null);\n const isKeyboardNavigation = useRef(false);\n\n useLayoutEffect(() => {\n const section = sectionRef.current;\n\n if (section) {\n const isRtl =\n getComputedStyle(section).direction === \"rtl\" ||\n section.closest(\"[dir]\")?.getAttribute(\"dir\")?.toLowerCase() === \"rtl\";\n\n setDirection(isRtl ? \"rtl\" : \"ltr\");\n }\n }, []);\n\n const rovingFocus = useRovingFocus<HTMLButtonElement>({\n items: hasCode ? [{ id: \"preview\" }, { id: \"code\" }] : [{ id: \"preview\" }],\n dir: direction,\n initialActiveId: activeTab,\n });\n\n const deviceFocus = useRovingFocus<HTMLButtonElement>({\n items: PREVIEW_DEVICE_ITEMS,\n dir: direction,\n initialActiveId: device,\n });\n\n const handleTabKeyDown = (\n event: KeyboardEvent<HTMLButtonElement>,\n tab: PreviewTab,\n ) => {\n isKeyboardNavigation.current = true;\n\n try {\n rovingFocus.onItemKeyDown(event, tab);\n } finally {\n isKeyboardNavigation.current = false;\n }\n };\n\n const handleTabFocus = (tab: PreviewTab) => {\n rovingFocus.onItemFocus(tab);\n\n if (isKeyboardNavigation.current) {\n setActiveTab(tab);\n }\n };\n\n const syncStyles = useCallback(() => {\n const frameDocument = iframeRef.current?.contentDocument;\n\n if (!frameDocument) return;\n\n const frameHead = frameDocument.head;\n\n frameHead\n .querySelectorAll(\"[data-puzzle-preview-style]\")\n .forEach((element) => element.remove());\n\n document\n .querySelectorAll(\n [\n \"head link[rel='stylesheet']\",\n \"head style\",\n \"style[data-puzzle-theme]\",\n \"style[data-puzzle-theme-scope-style]\",\n ].join(\", \"),\n )\n .forEach((element) => {\n const clone = element.cloneNode(true) as HTMLElement;\n\n clone.setAttribute(\"data-puzzle-preview-style\", \"\");\n frameHead.appendChild(clone);\n });\n\n const sourceRoot = document.documentElement;\n const targetRoot = frameDocument.documentElement;\n\n targetRoot.style.cssText = sourceRoot.style.cssText;\n targetRoot.className = sourceRoot.className;\n\n Array.from(sourceRoot.attributes).forEach((attribute) => {\n if (attribute.name === \"style\" || attribute.name === \"class\") {\n return;\n }\n\n targetRoot.setAttribute(attribute.name, attribute.value);\n });\n\n const body = frameDocument.body;\n\n body.className = document.body.className;\n body.style.cssText = document.body.style.cssText;\n\n targetRoot.style.setProperty(\"box-sizing\", \"border-box\", \"important\");\n targetRoot.style.setProperty(\"width\", \"100%\", \"important\");\n targetRoot.style.setProperty(\"height\", \"100%\", \"important\");\n targetRoot.style.setProperty(\"min-height\", \"100%\", \"important\");\n targetRoot.style.setProperty(\"margin\", \"0\", \"important\");\n targetRoot.style.setProperty(\"overflow-x\", \"hidden\", \"important\");\n targetRoot.style.setProperty(\"overflow-y\", \"auto\", \"important\");\n targetRoot.style.setProperty(\"scrollbar-width\", \"none\", \"important\");\n targetRoot.style.setProperty(\"-ms-overflow-style\", \"none\", \"important\");\n\n body.style.setProperty(\"display\", \"block\", \"important\");\n body.style.setProperty(\"position\", \"static\", \"important\");\n body.style.setProperty(\"box-sizing\", \"border-box\", \"important\");\n body.style.setProperty(\"width\", \"100%\", \"important\");\n body.style.setProperty(\"height\", \"auto\", \"important\");\n body.style.setProperty(\"min-height\", \"100%\", \"important\");\n body.style.setProperty(\"margin\", \"0\", \"important\");\n body.style.setProperty(\"overflow\", \"visible\", \"important\");\n\n const previewRoot = frameDocument.getElementById(frameRootId);\n\n previewRoot?.style.removeProperty(\"min-height\");\n\n const scrollbarStyle = frameDocument.createElement(\"style\");\n\n scrollbarStyle.setAttribute(\"data-puzzle-preview-style\", \"\");\n scrollbarStyle.textContent = `\n html,\n body {\n scrollbar-width: none !important;\n -ms-overflow-style: none !important;\n }\n\n html::-webkit-scrollbar,\n body::-webkit-scrollbar {\n display: none !important;\n width: 0 !important;\n height: 0 !important;\n }\n `;\n\n frameHead.appendChild(scrollbarStyle);\n }, [frameRootId]);\n\n const updateScale = useCallback(\n (availableWidth: number) => {\n const nextScale = Math.min(1, Math.max(0, availableWidth) / config.width);\n\n setScale((current) =>\n Math.abs(current - nextScale) < 0.001 ? current : nextScale,\n );\n },\n [config.width],\n );\n\n useIsomorphicLayoutEffect(() => {\n const container = containerRef.current;\n\n if (!container) return;\n\n const measure = () => {\n const styles = window.getComputedStyle(container);\n const horizontalPadding =\n parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight);\n\n updateScale(container.clientWidth - horizontalPadding);\n };\n\n measure();\n\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n\n if (entry) {\n updateScale(entry.contentRect.width);\n }\n });\n\n observer.observe(container);\n\n return () => observer.disconnect();\n }, [updateScale]);\n\n useEffect(() => {\n const isStyleNode = (node: Node) =>\n node instanceof Element &&\n (node.matches(\"style, link[rel='stylesheet']\") ||\n node.querySelector(\"style, link[rel='stylesheet']\") !== null);\n\n const headObserver = new MutationObserver(syncStyles);\n\n headObserver.observe(document.head, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: true,\n });\n\n const bodyObserver = new MutationObserver((mutations) => {\n const affectsStyles = mutations.some((mutation) => {\n const target =\n mutation.target instanceof Element\n ? mutation.target\n : mutation.target.parentElement;\n\n return (\n target?.closest(\n \"style[data-puzzle-theme], style[data-puzzle-theme-scope-style]\",\n ) != null ||\n Array.from(mutation.addedNodes).some(isStyleNode) ||\n Array.from(mutation.removedNodes).some(isStyleNode)\n );\n });\n\n if (affectsStyles) {\n syncStyles();\n }\n });\n\n bodyObserver.observe(document.body, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: true,\n attributeFilter: [\"data-theme\"],\n });\n\n return () => {\n headObserver.disconnect();\n bodyObserver.disconnect();\n };\n }, [syncStyles]);\n\n const initializeFrame = useCallback(() => {\n const frameDocument = iframeRef.current?.contentDocument;\n const root = frameDocument?.getElementById(frameRootId);\n\n if (!root) return;\n\n syncStyles();\n setFrameRoot(root);\n }, [frameRootId, syncStyles]);\n\n useEffect(() => {\n initializeFrame();\n }, [initializeFrame]);\n\n const handleCopyCode = useCallback(async () => {\n if (code === undefined) return;\n\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n\n if (copyTimeoutRef.current) {\n clearTimeout(copyTimeoutRef.current);\n }\n\n copyTimeoutRef.current = setTimeout(() => {\n setCopied(false);\n }, 2000);\n } catch {\n setCopied(false);\n }\n }, [code]);\n\n useEffect(\n () => () => {\n if (copyTimeoutRef.current) {\n clearTimeout(copyTimeoutRef.current);\n }\n },\n [],\n );\n\n useEffect(() => {\n if (!hasCode && activeTab === \"code\") {\n setActiveTab(\"preview\");\n }\n }, [activeTab, hasCode]);\n\n const frameStyle: CSSProperties = {\n width: config.width,\n height: frameHeight,\n transform: `scale(${scale})`,\n transformOrigin: \"top left\",\n };\n\n const contentStyle: CSSProperties = {\n display: \"flex\",\n boxSizing: \"border-box\",\n minHeight: \"100vh\",\n alignItems: align ? ALIGN_VALUES[align] : undefined,\n justifyContent: justify ? JUSTIFY_VALUES[justify] : undefined,\n };\n\n const viewportStyle: CSSProperties = {\n width: config.width * scale,\n aspectRatio: `${config.width} / ${frameHeight}`,\n };\n\n const panelProps = {\n id: panelId,\n role: \"tabpanel\",\n \"aria-labelledby\": activeTab === \"preview\" ? previewTabId : codeTabId,\n };\n\n return (\n <section ref={sectionRef} className={cn(\"puzzle-preview\", className)}>\n <div className=\"puzzle-preview__toolbar\">\n <div\n className=\"puzzle-preview__tabs\"\n role=\"tablist\"\n aria-label=\"Preview view\"\n >\n <button\n type=\"button\"\n role=\"tab\"\n id={previewTabId}\n ref={rovingFocus.getItemRef(\"preview\")}\n tabIndex={activeTab === \"preview\" ? 0 : -1}\n onFocus={() => handleTabFocus(\"preview\")}\n onKeyDown={(event) => handleTabKeyDown(event, \"preview\")}\n aria-selected={activeTab === \"preview\"}\n aria-controls={panelId}\n className={cn(\n \"puzzle-preview__tab\",\n activeTab === \"preview\" && \"puzzle-preview__tab--active\",\n )}\n onClick={() => setActiveTab(\"preview\")}\n >\n Preview\n </button>\n\n {hasCode && (\n <button\n type=\"button\"\n role=\"tab\"\n id={codeTabId}\n ref={rovingFocus.getItemRef(\"code\")}\n tabIndex={activeTab === \"code\" ? 0 : -1}\n onFocus={() => handleTabFocus(\"code\")}\n onKeyDown={(event) => handleTabKeyDown(event, \"code\")}\n aria-selected={activeTab === \"code\"}\n aria-controls={panelId}\n className={cn(\n \"puzzle-preview__tab\",\n activeTab === \"code\" && \"puzzle-preview__tab--active\",\n )}\n onClick={() => setActiveTab(\"code\")}\n >\n Code\n </button>\n )}\n </div>\n\n {activeTab === \"preview\" ? (\n <div className=\"puzzle-preview__toolbar-actions\">\n {!hideDevices && (\n <div\n className=\"puzzle-preview__devices\"\n role=\"group\"\n aria-label=\"Device size\"\n >\n {(Object.keys(PREVIEW_DEVICES) as PreviewDevice[]).map(\n (item) => (\n <button\n key={item}\n ref={deviceFocus.getItemRef(item)}\n type=\"button\"\n className={cn(\n \"puzzle-preview__device\",\n device === item && \"puzzle-preview__device--active\",\n )}\n aria-pressed={device === item}\n tabIndex={deviceFocus.getItemTabIndex(item)}\n onFocus={() => deviceFocus.onItemFocus(item)}\n onKeyDown={(event) =>\n deviceFocus.onItemKeyDown(event, item)\n }\n onClick={() => setDevice(item)}\n >\n {PREVIEW_DEVICES[item].label}\n </button>\n ),\n )}\n </div>\n )}\n\n <span className=\"puzzle-preview__dimensions\">\n {config.width} × {frameHeight}\n </span>\n </div>\n ) : (\n <button\n type=\"button\"\n className=\"puzzle-preview__copy\"\n onClick={handleCopyCode}\n aria-label={copied ? \"Code copied\" : \"Copy code\"}\n >\n <Icon\n name={copied ? \"tabler:check\" : \"tabler:copy\"}\n size={16}\n aria-hidden=\"true\"\n />\n <span>{copied ? \"Copied\" : \"Copy code\"}</span>\n </button>\n )}\n </div>\n\n <PreviewBackground {...panelProps} className=\"puzzle-preview__panel\">\n <div\n ref={containerRef}\n className=\"puzzle-preview__stage\"\n hidden={activeTab !== \"preview\"}\n >\n <div className=\"puzzle-preview__viewport\" style={viewportStyle}>\n <iframe\n ref={iframeRef}\n className=\"puzzle-preview__frame\"\n srcDoc={createPreviewDocument(frameRootId)}\n title={`${config.label} component preview`}\n onLoad={initializeFrame}\n style={frameStyle}\n />\n\n {frameRoot &&\n createPortal(\n <ThemeScopeProvider\n scopeId={themeScope?.scopeId ?? null}\n theme={themeScope?.theme ?? \"system\"}\n >\n <div style={contentStyle}>{children}</div>\n </ThemeScopeProvider>,\n frameRoot,\n )}\n </div>\n </div>\n\n {activeTab === \"code\" && (\n <div className=\"puzzle-preview__code\">\n <pre>\n <code className=\"language-tsx\">\n {code === undefined ? null : highlightCode(code)}\n </code>\n </pre>\n </div>\n )}\n </PreviewBackground>\n\n <span\n className=\"puzzle-preview__copy-status\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {copied ? \"Code copied to clipboard.\" : \"\"}\n </span>\n </section>\n );\n};\n\nexport {\n Preview,\n type PreviewProps,\n type PreviewDevice,\n type PreviewAlign,\n type PreviewJustify,\n};\n"],"mappings":";;;;;;;;;;;;AA2DA,MAAM,kBAAuD;CAC3D,QAAQ;EAAE,OAAO;EAAU,OAAO;EAAK,QAAQ;CAAI;CACnD,QAAQ;EAAE,OAAO;EAAU,OAAO;EAAK,QAAQ;CAAK;CACpD,SAAS;EAAE,OAAO;EAAW,OAAO;EAAM,QAAQ;CAAI;AACxD;AAEA,MAAM,uBACJ,OAAO,KAAK,eAAe,CAAC,CAC5B,KAAK,QAAQ,EAAE,GAAG,EAAE;AAEtB,MAAM,eAA6C;CACjD,OAAO;CACP,QAAQ;CACR,KAAK;CACL,SAAS;AACX;AAEA,MAAM,iBAAiD;CACrD,OAAO;CACP,QAAQ;CACR,KAAK;CACL,SAAS;CACT,QAAQ;CACR,QAAQ;AACV;AAEA,MAAM,yBAAyB,WAAmB;;;;;;;eAOnC,OAAO;;;AAItB,MAAM,4BACJ,OAAO,WAAW,cAAcA,MAAAA,YAAYC,MAAAA;AAE9C,MAAM,qBAAqB,UACzB,iBAAA,GAAA,kBAAA,IAAA,CAACC,cAAAA,YAAD;CACE,SAAQ;CACR,MAAM;CACN,WAAW;CACX,WAAW;CACX,gBAAgB;CAChB,GAAI;AACL,CAAA;AAGH,MAAM,WAAW,EACf,UACA,WACA,gBAAgB,UAChB,MACA,QACA,OACA,SACA,cAAc,YACI;CAClB,MAAM,eAAA,GAAcC,MAAAA,MAAAA,CAAM;CAC1B,MAAM,cAAc,GAAG,YAAY;CACnC,MAAM,eAAe,GAAG,YAAY;CACpC,MAAM,YAAY,GAAG,YAAY;CACjC,MAAM,UAAU,GAAG,YAAY;CAE/B,MAAM,cAAA,GAAaC,+BAAAA,cAAAA,CAAc;CAEjC,MAAM,CAAC,QAAQ,cAAA,GAAaC,MAAAA,SAAAA,CAAwB,aAAa;CACjE,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAAqB,SAAS;CAChE,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAA6B,IAAI;CACnE,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,CAAC;CACpC,MAAM,CAAC,QAAQ,cAAA,GAAaA,MAAAA,SAAAA,CAAS,KAAK;CAE1C,MAAM,gBAAA,GAAeC,MAAAA,OAAAA,CAAuB,IAAI;CAChD,MAAM,aAAA,GAAYA,MAAAA,OAAAA,CAA0B,IAAI;CAChD,MAAM,kBAAA,GAAiBA,MAAAA,OAAAA,CAA6C,IAAI;CAExE,MAAM,SAAS,gBAAgB;CAC/B,MAAM,cAAc,UAAU,OAAO;CACrC,MAAM,UAAU,SAAS,KAAA;CACzB,MAAM,CAAC,WAAW,iBAAA,GAAgBD,MAAAA,SAAAA,CAAwB,KAAK;CAC/D,MAAM,cAAA,GAAaC,MAAAA,OAAAA,CAAoB,IAAI;CAC3C,MAAM,wBAAA,GAAuBA,MAAAA,OAAAA,CAAO,KAAK;CAEzC,CAAA,GAAA,MAAA,gBAAA,OAAsB;EACpB,MAAM,UAAU,WAAW;EAE3B,IAAI,SAAS;GACX,MAAM,QACJ,iBAAiB,OAAO,CAAC,CAAC,cAAc,SACxC,QAAQ,QAAQ,OAAO,CAAC,EAAE,aAAa,KAAK,CAAC,EAAE,YAAY,MAAM;GAEnE,aAAa,QAAQ,QAAQ,KAAK;EACpC;CACF,GAAG,CAAC,CAAC;CAEL,MAAM,eAAA,GAAcC,iBAAAA,eAAAA,CAAkC;EACpD,OAAO,UAAU,CAAC,EAAE,IAAI,UAAU,GAAG,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,UAAU,CAAC;EACzE,KAAK;EACL,iBAAiB;CACnB,CAAC;CAED,MAAM,eAAA,GAAcA,iBAAAA,eAAAA,CAAkC;EACpD,OAAO;EACP,KAAK;EACL,iBAAiB;CACnB,CAAC;CAED,MAAM,oBACJ,OACA,QACG;EACH,qBAAqB,UAAU;EAE/B,IAAI;GACF,YAAY,cAAc,OAAO,GAAG;EACtC,UAAU;GACR,qBAAqB,UAAU;EACjC;CACF;CAEA,MAAM,kBAAkB,QAAoB;EAC1C,YAAY,YAAY,GAAG;EAE3B,IAAI,qBAAqB,SACvB,aAAa,GAAG;CAEpB;CAEA,MAAM,cAAA,GAAaC,MAAAA,YAAAA,OAAkB;EACnC,MAAM,gBAAgB,UAAU,SAAS;EAEzC,IAAI,CAAC,eAAe;EAEpB,MAAM,YAAY,cAAc;EAEhC,UACG,iBAAiB,6BAA6B,CAAC,CAC/C,SAAS,YAAY,QAAQ,OAAO,CAAC;EAExC,SACG,iBACC;GACE;GACA;GACA;GACA;EACF,CAAC,CAAC,KAAK,IAAI,CACb,CAAC,CACA,SAAS,YAAY;GACpB,MAAM,QAAQ,QAAQ,UAAU,IAAI;GAEpC,MAAM,aAAa,6BAA6B,EAAE;GAClD,UAAU,YAAY,KAAK;EAC7B,CAAC;EAEH,MAAM,aAAa,SAAS;EAC5B,MAAM,aAAa,cAAc;EAEjC,WAAW,MAAM,UAAU,WAAW,MAAM;EAC5C,WAAW,YAAY,WAAW;EAElC,MAAM,KAAK,WAAW,UAAU,CAAC,CAAC,SAAS,cAAc;GACvD,IAAI,UAAU,SAAS,WAAW,UAAU,SAAS,SACnD;GAGF,WAAW,aAAa,UAAU,MAAM,UAAU,KAAK;EACzD,CAAC;EAED,MAAM,OAAO,cAAc;EAE3B,KAAK,YAAY,SAAS,KAAK;EAC/B,KAAK,MAAM,UAAU,SAAS,KAAK,MAAM;EAEzC,WAAW,MAAM,YAAY,cAAc,cAAc,WAAW;EACpE,WAAW,MAAM,YAAY,SAAS,QAAQ,WAAW;EACzD,WAAW,MAAM,YAAY,UAAU,QAAQ,WAAW;EAC1D,WAAW,MAAM,YAAY,cAAc,QAAQ,WAAW;EAC9D,WAAW,MAAM,YAAY,UAAU,KAAK,WAAW;EACvD,WAAW,MAAM,YAAY,cAAc,UAAU,WAAW;EAChE,WAAW,MAAM,YAAY,cAAc,QAAQ,WAAW;EAC9D,WAAW,MAAM,YAAY,mBAAmB,QAAQ,WAAW;EACnE,WAAW,MAAM,YAAY,sBAAsB,QAAQ,WAAW;EAEtE,KAAK,MAAM,YAAY,WAAW,SAAS,WAAW;EACtD,KAAK,MAAM,YAAY,YAAY,UAAU,WAAW;EACxD,KAAK,MAAM,YAAY,cAAc,cAAc,WAAW;EAC9D,KAAK,MAAM,YAAY,SAAS,QAAQ,WAAW;EACnD,KAAK,MAAM,YAAY,UAAU,QAAQ,WAAW;EACpD,KAAK,MAAM,YAAY,cAAc,QAAQ,WAAW;EACxD,KAAK,MAAM,YAAY,UAAU,KAAK,WAAW;EACjD,KAAK,MAAM,YAAY,YAAY,WAAW,WAAW;EAIzD,cAFkC,eAAe,WAEvC,CAAC,EAAE,MAAM,eAAe,YAAY;EAE9C,MAAM,iBAAiB,cAAc,cAAc,OAAO;EAE1D,eAAe,aAAa,6BAA6B,EAAE;EAC3D,eAAe,cAAc;;;;;;;;;;;;;;EAe7B,UAAU,YAAY,cAAc;CACtC,GAAG,CAAC,WAAW,CAAC;CAEhB,MAAM,eAAA,GAAcA,MAAAA,YAAAA,EACjB,mBAA2B;EAC1B,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,cAAc,IAAI,OAAO,KAAK;EAExE,UAAU,YACR,KAAK,IAAI,UAAU,SAAS,IAAI,OAAQ,UAAU,SACpD;CACF,GACA,CAAC,OAAO,KAAK,CACf;CAEA,gCAAgC;EAC9B,MAAM,YAAY,aAAa;EAE/B,IAAI,CAAC,WAAW;EAEhB,MAAM,gBAAgB;GACpB,MAAM,SAAS,OAAO,iBAAiB,SAAS;GAChD,MAAM,oBACJ,WAAW,OAAO,WAAW,IAAI,WAAW,OAAO,YAAY;GAEjE,YAAY,UAAU,cAAc,iBAAiB;EACvD;EAEA,QAAQ;EAER,MAAM,WAAW,IAAI,gBAAgB,YAAY;GAC/C,MAAM,QAAQ,QAAQ;GAEtB,IAAI,OACF,YAAY,MAAM,YAAY,KAAK;EAEvC,CAAC;EAED,SAAS,QAAQ,SAAS;EAE1B,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,WAAW,CAAC;CAEhB,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,eAAe,SACnB,gBAAgB,YACf,KAAK,QAAQ,+BAA+B,KAC3C,KAAK,cAAc,+BAA+B,MAAM;EAE5D,MAAM,eAAe,IAAI,iBAAiB,UAAU;EAEpD,aAAa,QAAQ,SAAS,MAAM;GAClC,WAAW;GACX,SAAS;GACT,eAAe;GACf,YAAY;EACd,CAAC;EAED,MAAM,eAAe,IAAI,kBAAkB,cAAc;GAgBvD,IAfsB,UAAU,MAAM,aAAa;IAMjD,QAJE,SAAS,kBAAkB,UACvB,SAAS,SACT,SAAS,OAAO,cAAA,EAGZ,QACN,gEACF,KAAK,QACL,MAAM,KAAK,SAAS,UAAU,CAAC,CAAC,KAAK,WAAW,KAChD,MAAM,KAAK,SAAS,YAAY,CAAC,CAAC,KAAK,WAAW;GAEtD,CAEgB,GACd,WAAW;EAEf,CAAC;EAED,aAAa,QAAQ,SAAS,MAAM;GAClC,WAAW;GACX,SAAS;GACT,eAAe;GACf,YAAY;GACZ,iBAAiB,CAAC,YAAY;EAChC,CAAC;EAED,aAAa;GACX,aAAa,WAAW;GACxB,aAAa,WAAW;EAC1B;CACF,GAAG,CAAC,UAAU,CAAC;CAEf,MAAM,mBAAA,GAAkBA,MAAAA,YAAAA,OAAkB;EAExC,MAAM,QADgB,UAAU,SAAS,gBAAA,EACb,eAAe,WAAW;EAEtD,IAAI,CAAC,MAAM;EAEX,WAAW;EACX,aAAa,IAAI;CACnB,GAAG,CAAC,aAAa,UAAU,CAAC;CAE5B,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,gBAAgB;CAClB,GAAG,CAAC,eAAe,CAAC;CAEpB,MAAM,kBAAA,GAAiBA,MAAAA,YAAAA,CAAY,YAAY;EAC7C,IAAI,SAAS,KAAA,GAAW;EAExB,IAAI;GACF,MAAM,UAAU,UAAU,UAAU,IAAI;GACxC,UAAU,IAAI;GAEd,IAAI,eAAe,SACjB,aAAa,eAAe,OAAO;GAGrC,eAAe,UAAU,iBAAiB;IACxC,UAAU,KAAK;GACjB,GAAG,GAAI;EACT,QAAQ;GACN,UAAU,KAAK;EACjB;CACF,GAAG,CAAC,IAAI,CAAC;CAET,CAAA,GAAA,MAAA,UAAA,aACc;EACV,IAAI,eAAe,SACjB,aAAa,eAAe,OAAO;CAEvC,GACA,CAAC,CACH;CAEA,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,IAAI,CAAC,WAAW,cAAc,QAC5B,aAAa,SAAS;CAE1B,GAAG,CAAC,WAAW,OAAO,CAAC;CAEvB,MAAM,aAA4B;EAChC,OAAO,OAAO;EACd,QAAQ;EACR,WAAW,SAAS,MAAM;EAC1B,iBAAiB;CACnB;CAEA,MAAM,eAA8B;EAClC,SAAS;EACT,WAAW;EACX,WAAW;EACX,YAAY,QAAQ,aAAa,SAAS,KAAA;EAC1C,gBAAgB,UAAU,eAAe,WAAW,KAAA;CACtD;CAEA,MAAM,gBAA+B;EACnC,OAAO,OAAO,QAAQ;EACtB,aAAa,GAAG,OAAO,MAAM,KAAK;CACpC;CAEA,MAAM,aAAa;EACjB,IAAI;EACJ,MAAM;EACN,mBAAmB,cAAc,YAAY,eAAe;CAC9D;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,WAAD;EAAS,KAAK;EAAY,YAAA,GAAWC,gBAAAA,GAAAA,CAAG,kBAAkB,SAAS;EAAnE,UAAA;GACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KACE,WAAU;KACV,MAAK;KACL,cAAW;KAHb,UAAA,CAKE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MACE,MAAK;MACL,MAAK;MACL,IAAI;MACJ,KAAK,YAAY,WAAW,SAAS;MACrC,UAAU,cAAc,YAAY,IAAI;MACxC,eAAe,eAAe,SAAS;MACvC,YAAY,UAAU,iBAAiB,OAAO,SAAS;MACvD,iBAAe,cAAc;MAC7B,iBAAe;MACf,YAAA,GAAWA,gBAAAA,GAAAA,CACT,uBACA,cAAc,aAAa,6BAC7B;MACA,eAAe,aAAa,SAAS;MACtC,UAAA;KAEO,CAAA,GAEP,WACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MACE,MAAK;MACL,MAAK;MACL,IAAI;MACJ,KAAK,YAAY,WAAW,MAAM;MAClC,UAAU,cAAc,SAAS,IAAI;MACrC,eAAe,eAAe,MAAM;MACpC,YAAY,UAAU,iBAAiB,OAAO,MAAM;MACpD,iBAAe,cAAc;MAC7B,iBAAe;MACf,YAAA,GAAWA,gBAAAA,GAAAA,CACT,uBACA,cAAc,UAAU,6BAC1B;MACA,eAAe,aAAa,MAAM;MACnC,UAAA;KAEO,CAAA,CAEP;IAEJ,CAAA,GAAA,cAAc,YACb,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACG,CAAC,eACA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,cAAW;MAET,UAAA,OAAO,KAAK,eAAe,CAAC,CAAqB,KAChD,SACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OAEE,KAAK,YAAY,WAAW,IAAI;OAChC,MAAK;OACL,YAAA,GAAWA,gBAAAA,GAAAA,CACT,0BACA,WAAW,QAAQ,gCACrB;OACA,gBAAc,WAAW;OACzB,UAAU,YAAY,gBAAgB,IAAI;OAC1C,eAAe,YAAY,YAAY,IAAI;OAC3C,YAAY,UACV,YAAY,cAAc,OAAO,IAAI;OAEvC,eAAe,UAAU,IAAI;OAE5B,UAAA,gBAAgB,KAAK,CAAC;MACjB,GAhBD,IAgBC,CAEZ;KACG,CAAA,GAGP,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;MAAM,WAAU;MAAhB,UAAA;OACG,OAAO;OAAM;OAAI;MACd;KACH,CAAA,CAAA;IAEL,CAAA,IAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,SAAS;KACT,cAAY,SAAS,gBAAgB;KAJvC,UAAA,CAME,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,MAAD;MACE,MAAM,SAAS,iBAAiB;MAChC,MAAM;MACN,eAAY;KACb,CAAA,GACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,SAAS,WAAW,YAAkB,CAAA,CACvC;IAEP,CAAA,CAAA;;GAEL,iBAAA,GAAA,kBAAA,KAAA,CAAC,mBAAD;IAAmB,GAAI;IAAY,WAAU;IAA7C,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;KACE,KAAK;KACL,WAAU;KACV,QAAQ,cAAc;KAEtB,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAU;MAA2B,OAAO;MAAjD,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OACE,KAAK;OACL,WAAU;OACV,QAAQ,sBAAsB,WAAW;OACzC,OAAO,GAAG,OAAO,MAAM;OACvB,QAAQ;OACR,OAAO;MACR,CAAA,GAEA,cAAA,GACCC,UAAAA,aAAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,+BAAAA,oBAAD;OACE,SAAS,YAAY,WAAW;OAChC,OAAO,YAAY,SAAS;OAE5B,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;QAAK,OAAO;QAAe;OAAc,CAAA;MACvB,CAAA,GACpB,SACF,CACC;;IACF,CAAA,GAEJ,cAAc,UACb,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;KAAK,WAAU;KACb,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD,EAAA,UACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAU;MACb,UAAA,SAAS,KAAA,IAAY,OAAOC,kBAAAA,cAAc,IAAI;KAC3C,CAAA,EACH,CAAA;IACF,CAAA,CAEU;;GAEnB,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;IACE,WAAU;IACV,MAAK;IACL,aAAU;IAET,UAAA,SAAS,8BAA8B;GACpC,CAAA;EACC;;AAEb"}
1
+ {"version":3,"file":"index.cjs","names":["useEffect","useLayoutEffect","Background","useId","useThemeScope","useState","useRef","BREAKPOINTS","useRovingFocus","useCallback","cn","Icon","createPortal","ThemeScopeProvider","highlightCode"],"sources":["../../../../src/components/utilities/preview/index.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n type ComponentProps,\n type CSSProperties,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { cn } from \"@puzzle-ui/core\";\nimport { useRovingFocus } from \"@puzzle-ui/hooks\";\nimport { Icon } from \"@puzzle-ui/icons\";\nimport { BREAKPOINTS } from \"@puzzle-ui/theme\";\nimport {\n ThemeScopeProvider,\n useThemeScope,\n} from \"@puzzle-ui/theme/scope-context\";\n\nimport { Background } from \"../../layout/background\";\nimport { highlightCode } from \"./highlight\";\n\nimport \"./preview.css\";\n\ntype PreviewDevice = \"mobile\" | \"tablet\" | \"desktop\";\ntype PreviewTab = \"preview\" | \"code\";\n\ntype DeviceConfig = {\n label: string;\n width: number;\n height: number;\n};\n\ntype PreviewAlign = \"start\" | \"center\" | \"end\" | \"stretch\";\ntype PreviewJustify =\n | \"start\"\n | \"center\"\n | \"end\"\n | \"between\"\n | \"around\"\n | \"evenly\";\n\ntype PreviewProps = {\n children: ReactNode;\n className?: string;\n defaultDevice?: PreviewDevice;\n code?: string;\n /** Size to content (capped at mobile height) or the selected device. */\n fit?: \"content\" | \"device\";\n /** Frame height in pixels. Overrides fit. */\n height?: number;\n /** Space inside the preview frame, around the content. Numbers are pixels. */\n padding?: number | string;\n /** Vertical alignment of the content within the frame. */\n align?: PreviewAlign;\n /** Horizontal distribution of the content within the frame. */\n justify?: PreviewJustify;\n /** Hides the device selector. The default device is still used. */\n hideDevices?: boolean;\n};\n\nconst PREVIEW_DEVICES: Record<PreviewDevice, DeviceConfig> = {\n mobile: { label: \"Mobile\", width: 390, height: 844 },\n tablet: { label: \"Tablet\", width: 768, height: 1024 },\n desktop: { label: \"Desktop\", width: 1024, height: 768 },\n};\n\nconst PREVIEW_DEVICE_ITEMS = (\n Object.keys(PREVIEW_DEVICES) as PreviewDevice[]\n).map((id) => ({ id }));\n\nconst ALIGN_VALUES: Record<PreviewAlign, string> = {\n start: \"flex-start\",\n center: \"center\",\n end: \"flex-end\",\n stretch: \"stretch\",\n};\n\nconst JUSTIFY_VALUES: Record<PreviewJustify, string> = {\n start: \"flex-start\",\n center: \"center\",\n end: \"flex-end\",\n between: \"space-between\",\n around: \"space-around\",\n evenly: \"space-evenly\",\n};\n\nconst createPreviewDocument = (rootId: string) => `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n </head>\n <body>\n <div id=\"${rootId}\"></div>\n </body>\n</html>`;\n\nconst useIsomorphicLayoutEffect =\n typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nconst PreviewBackground = (props: ComponentProps<\"div\">) => (\n <Background\n pattern=\"blueprint\"\n size={20}\n majorSize={100}\n lineWidth={1}\n patternOpacity={0.25}\n {...props}\n />\n);\n\nconst Preview = ({\n children,\n className,\n defaultDevice = \"mobile\",\n code,\n fit = \"content\",\n height,\n padding = \"var(--puzzle-space-8, 2rem)\",\n align = \"center\",\n justify = \"center\",\n hideDevices = false,\n}: PreviewProps) => {\n const generatedId = useId();\n const frameRootId = `${generatedId}-root`;\n const previewTabId = `${generatedId}-tab-preview`;\n const codeTabId = `${generatedId}-tab-code`;\n const panelId = `${generatedId}-panel`;\n\n const themeScope = useThemeScope();\n\n const [device, setDevice] = useState<PreviewDevice>(defaultDevice);\n const [activeTab, setActiveTab] = useState<PreviewTab>(\"preview\");\n const [frameRoot, setFrameRoot] = useState<HTMLElement | null>(null);\n const [scale, setScale] = useState(1);\n const [copied, setCopied] = useState(false);\n const [contentHeight, setContentHeight] = useState(PREVIEW_DEVICES.mobile.height);\n const [contentMeasured, setContentMeasured] = useState(false);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const config = PREVIEW_DEVICES[device];\n const fitsContent = height === undefined && fit === \"content\";\n const frameHeight = height ?? (fitsContent ? contentHeight : config.height);\n // Until the iframe content is measured, an inert inline copy sizes the viewport so SSR has no height jump.\n const showPlaceholder = fitsContent && !contentMeasured;\n const hasCode = code !== undefined;\n const [direction, setDirection] = useState<\"ltr\" | \"rtl\">(\"ltr\");\n const [desktopControlsVisible, setDesktopControlsVisible] = useState(true);\n const sectionRef = useRef<HTMLElement>(null);\n const isKeyboardNavigation = useRef(false);\n\n useEffect(() => {\n const query = window.matchMedia(`(min-width: ${BREAKPOINTS.desktop}px)`);\n const update = () => setDesktopControlsVisible(query.matches);\n update();\n query.addEventListener(\"change\", update);\n return () => query.removeEventListener(\"change\", update);\n }, []);\n\n useLayoutEffect(() => {\n const section = sectionRef.current;\n\n if (section) {\n const isRtl =\n getComputedStyle(section).direction === \"rtl\" ||\n section.closest(\"[dir]\")?.getAttribute(\"dir\")?.toLowerCase() === \"rtl\";\n\n setDirection(isRtl ? \"rtl\" : \"ltr\");\n }\n }, []);\n\n const rovingFocus = useRovingFocus<HTMLButtonElement>({\n items: hasCode ? [{ id: \"preview\" }, { id: \"code\" }] : [{ id: \"preview\" }],\n dir: direction,\n initialActiveId: activeTab,\n });\n\n const deviceFocus = useRovingFocus<HTMLButtonElement>({\n items: PREVIEW_DEVICE_ITEMS.map((item) => ({\n ...item,\n disabled: item.id === \"desktop\" && !desktopControlsVisible,\n })),\n dir: direction,\n initialActiveId: device,\n });\n\n const handleTabKeyDown = (\n event: KeyboardEvent<HTMLButtonElement>,\n tab: PreviewTab,\n ) => {\n isKeyboardNavigation.current = true;\n\n try {\n rovingFocus.onItemKeyDown(event, tab);\n } finally {\n isKeyboardNavigation.current = false;\n }\n };\n\n const handleTabFocus = (tab: PreviewTab) => {\n rovingFocus.onItemFocus(tab);\n\n if (isKeyboardNavigation.current) {\n setActiveTab(tab);\n }\n };\n\n const syncStyles = useCallback(() => {\n const frameDocument = iframeRef.current?.contentDocument;\n\n if (!frameDocument) return;\n\n const frameHead = frameDocument.head;\n\n frameHead\n .querySelectorAll(\"[data-puzzle-preview-style]\")\n .forEach((element) => element.remove());\n\n document\n .querySelectorAll(\n [\n \"head link[rel='stylesheet']\",\n \"head style\",\n \"style[data-puzzle-theme]\",\n \"style[data-puzzle-theme-scope-style]\",\n ].join(\", \"),\n )\n .forEach((element) => {\n const clone = element.cloneNode(true) as HTMLElement;\n\n clone.setAttribute(\"data-puzzle-preview-style\", \"\");\n frameHead.appendChild(clone);\n });\n\n const sourceRoot = document.documentElement;\n const targetRoot = frameDocument.documentElement;\n\n targetRoot.style.cssText = sourceRoot.style.cssText;\n targetRoot.className = sourceRoot.className;\n\n Array.from(sourceRoot.attributes).forEach((attribute) => {\n if (attribute.name === \"style\" || attribute.name === \"class\") {\n return;\n }\n\n targetRoot.setAttribute(attribute.name, attribute.value);\n });\n\n const body = frameDocument.body;\n\n body.className = document.body.className;\n body.style.cssText = document.body.style.cssText;\n\n targetRoot.style.setProperty(\"box-sizing\", \"border-box\", \"important\");\n targetRoot.style.setProperty(\"width\", \"100%\", \"important\");\n targetRoot.style.setProperty(\"height\", \"100%\", \"important\");\n targetRoot.style.setProperty(\"min-height\", \"100%\", \"important\");\n targetRoot.style.setProperty(\"margin\", \"0\", \"important\");\n targetRoot.style.setProperty(\"padding\", \"0\", \"important\");\n targetRoot.style.setProperty(\"overflow-x\", \"hidden\", \"important\");\n targetRoot.style.setProperty(\"overflow-y\", \"auto\", \"important\");\n targetRoot.style.setProperty(\"scrollbar-width\", \"none\", \"important\");\n targetRoot.style.setProperty(\"-ms-overflow-style\", \"none\", \"important\");\n\n body.style.setProperty(\"display\", \"block\", \"important\");\n body.style.setProperty(\"position\", \"static\", \"important\");\n body.style.setProperty(\"box-sizing\", \"border-box\", \"important\");\n body.style.setProperty(\"width\", \"100%\", \"important\");\n body.style.setProperty(\"height\", \"auto\", \"important\");\n body.style.setProperty(\"min-height\", \"100%\", \"important\");\n body.style.setProperty(\"margin\", \"0\", \"important\");\n body.style.setProperty(\"padding\", \"0\", \"important\");\n body.style.setProperty(\"overflow\", \"visible\", \"important\");\n\n const previewRoot = frameDocument.getElementById(frameRootId);\n\n previewRoot?.style.removeProperty(\"min-height\");\n\n const scrollbarStyle = frameDocument.createElement(\"style\");\n\n scrollbarStyle.setAttribute(\"data-puzzle-preview-style\", \"\");\n scrollbarStyle.textContent = `\n html,\n body {\n scrollbar-width: none !important;\n -ms-overflow-style: none !important;\n }\n\n html::-webkit-scrollbar,\n body::-webkit-scrollbar {\n display: none !important;\n width: 0 !important;\n height: 0 !important;\n }\n `;\n\n frameHead.appendChild(scrollbarStyle);\n }, [frameRootId]);\n\n const updateScale = useCallback(\n (availableWidth: number) => {\n if (availableWidth <= 0) return;\n\n const nextScale = Math.min(1, Math.max(0, availableWidth) / config.width);\n\n setScale((current) =>\n Math.abs(current - nextScale) < 0.001 ? current : nextScale,\n );\n },\n [config.width],\n );\n\n useIsomorphicLayoutEffect(() => {\n const container = containerRef.current;\n\n if (!container) return;\n\n const measure = () => {\n const styles = window.getComputedStyle(container);\n const horizontalPadding =\n (Number.parseFloat(styles.paddingLeft) || 0) +\n (Number.parseFloat(styles.paddingRight) || 0);\n\n updateScale(container.clientWidth - horizontalPadding);\n };\n\n measure();\n\n const observer = new ResizeObserver((entries) => {\n const entry = entries[0];\n\n if (entry) {\n updateScale(entry.contentRect.width);\n }\n });\n\n observer.observe(container);\n\n return () => observer.disconnect();\n }, [updateScale]);\n\n useEffect(() => {\n const isStyleNode = (node: Node) =>\n node instanceof Element &&\n (node.matches(\"style, link[rel='stylesheet']\") ||\n node.querySelector(\"style, link[rel='stylesheet']\") !== null);\n\n const headObserver = new MutationObserver(syncStyles);\n\n headObserver.observe(document.head, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: true,\n });\n\n const bodyObserver = new MutationObserver((mutations) => {\n const affectsStyles = mutations.some((mutation) => {\n const target =\n mutation.target instanceof Element\n ? mutation.target\n : mutation.target.parentElement;\n\n return (\n target?.closest(\n \"style[data-puzzle-theme], style[data-puzzle-theme-scope-style]\",\n ) != null ||\n Array.from(mutation.addedNodes).some(isStyleNode) ||\n Array.from(mutation.removedNodes).some(isStyleNode)\n );\n });\n\n if (affectsStyles) {\n syncStyles();\n }\n });\n\n bodyObserver.observe(document.body, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: true,\n attributeFilter: [\"data-theme\"],\n });\n\n return () => {\n headObserver.disconnect();\n bodyObserver.disconnect();\n };\n }, [syncStyles]);\n\n const initializeFrame = useCallback(() => {\n const frameDocument = iframeRef.current?.contentDocument;\n const root = frameDocument?.getElementById(frameRootId);\n\n if (!root) return;\n\n syncStyles();\n setFrameRoot(root);\n }, [frameRootId, syncStyles]);\n\n useEffect(() => {\n initializeFrame();\n }, [initializeFrame]);\n\n useIsomorphicLayoutEffect(() => {\n const content = contentRef.current;\n\n if (!fitsContent || !frameRoot || !content || activeTab !== \"preview\") return;\n\n const measure = () => {\n const measuredHeight = Math.ceil(\n Math.max(content.getBoundingClientRect().height, content.scrollHeight),\n );\n\n setContentHeight(\n Math.min(PREVIEW_DEVICES.mobile.height, Math.max(1, measuredHeight)),\n );\n setContentMeasured(true);\n };\n\n measure();\n\n const observer = new ResizeObserver(measure);\n observer.observe(content);\n\n const mutations = new MutationObserver(measure);\n mutations.observe(content, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n });\n\n return () => {\n observer.disconnect();\n mutations.disconnect();\n };\n }, [activeTab, fitsContent, frameRoot]);\n\n const handleCopyCode = useCallback(async () => {\n if (code === undefined) return;\n\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n\n if (copyTimeoutRef.current) {\n clearTimeout(copyTimeoutRef.current);\n }\n\n copyTimeoutRef.current = setTimeout(() => {\n setCopied(false);\n }, 2000);\n } catch {\n setCopied(false);\n }\n }, [code]);\n\n useEffect(\n () => () => {\n if (copyTimeoutRef.current) {\n clearTimeout(copyTimeoutRef.current);\n }\n },\n [],\n );\n\n useEffect(() => {\n if (!hasCode && activeTab === \"code\") {\n setActiveTab(\"preview\");\n }\n }, [activeTab, hasCode]);\n\n const frameStyle: CSSProperties = {\n width: config.width,\n height: frameHeight,\n transform: `scale(${scale})`,\n transformOrigin: \"top left\",\n visibility: showPlaceholder ? \"hidden\" : undefined,\n };\n\n const contentStyle: CSSProperties = {\n display: \"flex\",\n boxSizing: \"border-box\",\n padding,\n minHeight: fitsContent ? undefined : \"100vh\",\n alignItems: align ? ALIGN_VALUES[align] : undefined,\n justifyContent: justify ? JUSTIFY_VALUES[justify] : undefined,\n };\n\n const viewportStyle: CSSProperties = {\n width: config.width * scale,\n height: showPlaceholder ? undefined : frameHeight * scale,\n maxHeight: showPlaceholder\n ? PREVIEW_DEVICES.mobile.height * scale\n : undefined,\n };\n\n const placeholderStyle: CSSProperties = {\n ...contentStyle,\n width: config.width,\n zoom: scale === 1 ? undefined : scale,\n };\n\n const panelProps = {\n id: panelId,\n role: \"tabpanel\",\n \"aria-labelledby\": activeTab === \"preview\" ? previewTabId : codeTabId,\n };\n\n return (\n <section ref={sectionRef} className={cn(\"puzzle-preview\", className)}>\n <div className=\"puzzle-preview__toolbar\">\n <div\n className=\"puzzle-preview__tabs\"\n role=\"tablist\"\n aria-label=\"Preview view\"\n >\n <button\n type=\"button\"\n role=\"tab\"\n id={previewTabId}\n ref={rovingFocus.getItemRef(\"preview\")}\n tabIndex={activeTab === \"preview\" ? 0 : -1}\n onFocus={() => handleTabFocus(\"preview\")}\n onKeyDown={(event) => handleTabKeyDown(event, \"preview\")}\n aria-selected={activeTab === \"preview\"}\n aria-controls={panelId}\n className={cn(\n \"puzzle-preview__tab\",\n activeTab === \"preview\" && \"puzzle-preview__tab--active\",\n )}\n onClick={() => setActiveTab(\"preview\")}\n >\n Preview\n </button>\n\n {hasCode && (\n <button\n type=\"button\"\n role=\"tab\"\n id={codeTabId}\n ref={rovingFocus.getItemRef(\"code\")}\n tabIndex={activeTab === \"code\" ? 0 : -1}\n onFocus={() => handleTabFocus(\"code\")}\n onKeyDown={(event) => handleTabKeyDown(event, \"code\")}\n aria-selected={activeTab === \"code\"}\n aria-controls={panelId}\n className={cn(\n \"puzzle-preview__tab\",\n activeTab === \"code\" && \"puzzle-preview__tab--active\",\n )}\n onClick={() => setActiveTab(\"code\")}\n >\n Code\n </button>\n )}\n </div>\n\n {activeTab === \"preview\" ? (\n <div className=\"puzzle-preview__toolbar-actions\">\n {!hideDevices && (\n <div\n className=\"puzzle-preview__devices\"\n role=\"group\"\n aria-label=\"Device size\"\n >\n {(Object.keys(PREVIEW_DEVICES) as PreviewDevice[]).map(\n (item) => (\n <button\n key={item}\n data-device={item}\n ref={deviceFocus.getItemRef(item)}\n type=\"button\"\n className={cn(\n \"puzzle-preview__device\",\n device === item && \"puzzle-preview__device--active\",\n )}\n aria-pressed={device === item}\n tabIndex={deviceFocus.getItemTabIndex(item)}\n onFocus={() => deviceFocus.onItemFocus(item)}\n onKeyDown={(event) =>\n deviceFocus.onItemKeyDown(event, item)\n }\n onClick={() => setDevice(item)}\n >\n {PREVIEW_DEVICES[item].label}\n </button>\n ),\n )}\n </div>\n )}\n\n <span className=\"puzzle-preview__dimensions\">\n {config.width} × {frameHeight}\n </span>\n </div>\n ) : (\n <button\n type=\"button\"\n className=\"puzzle-preview__copy\"\n onClick={handleCopyCode}\n aria-label={copied ? \"Code copied\" : \"Copy code\"}\n >\n <Icon\n name={copied ? \"tabler:check\" : \"tabler:copy\"}\n size={16}\n aria-hidden=\"true\"\n />\n <span>{copied ? \"Copied\" : \"Copy code\"}</span>\n </button>\n )}\n </div>\n\n <PreviewBackground {...panelProps} className=\"puzzle-preview__panel\">\n <div\n ref={containerRef}\n className=\"puzzle-preview__stage\"\n hidden={activeTab !== \"preview\"}\n >\n <div className=\"puzzle-preview__viewport\" style={viewportStyle}>\n <iframe\n ref={iframeRef}\n className=\"puzzle-preview__frame\"\n srcDoc={createPreviewDocument(frameRootId)}\n title={`${config.label} component preview`}\n onLoad={initializeFrame}\n style={frameStyle}\n />\n\n {showPlaceholder && (\n <div\n className=\"puzzle-preview__placeholder\"\n style={placeholderStyle}\n aria-hidden=\"true\"\n inert\n data-preview-placeholder=\"\"\n >\n {children}\n </div>\n )}\n\n {frameRoot &&\n createPortal(\n <ThemeScopeProvider\n scopeId={themeScope?.scopeId ?? null}\n theme={themeScope?.theme ?? \"system\"}\n >\n <div ref={contentRef} style={contentStyle}>{children}</div>\n </ThemeScopeProvider>,\n frameRoot,\n )}\n </div>\n </div>\n\n {activeTab === \"code\" && (\n <div className=\"puzzle-preview__code\">\n <pre>\n <code className=\"language-tsx\">\n {code === undefined ? null : highlightCode(code)}\n </code>\n </pre>\n </div>\n )}\n </PreviewBackground>\n\n <span\n className=\"puzzle-preview__copy-status\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {copied ? \"Code copied to clipboard.\" : \"\"}\n </span>\n </section>\n );\n};\n\nexport {\n Preview,\n type PreviewProps,\n type PreviewDevice,\n type PreviewAlign,\n type PreviewJustify,\n};\n"],"mappings":";;;;;;;;;;;;;AAmEA,MAAM,kBAAuD;CAC3D,QAAQ;EAAE,OAAO;EAAU,OAAO;EAAK,QAAQ;CAAI;CACnD,QAAQ;EAAE,OAAO;EAAU,OAAO;EAAK,QAAQ;CAAK;CACpD,SAAS;EAAE,OAAO;EAAW,OAAO;EAAM,QAAQ;CAAI;AACxD;AAEA,MAAM,uBACJ,OAAO,KAAK,eAAe,CAAC,CAC5B,KAAK,QAAQ,EAAE,GAAG,EAAE;AAEtB,MAAM,eAA6C;CACjD,OAAO;CACP,QAAQ;CACR,KAAK;CACL,SAAS;AACX;AAEA,MAAM,iBAAiD;CACrD,OAAO;CACP,QAAQ;CACR,KAAK;CACL,SAAS;CACT,QAAQ;CACR,QAAQ;AACV;AAEA,MAAM,yBAAyB,WAAmB;;;;;;;eAOnC,OAAO;;;AAItB,MAAM,4BACJ,OAAO,WAAW,cAAcA,MAAAA,YAAYC,MAAAA;AAE9C,MAAM,qBAAqB,UACzB,iBAAA,GAAA,kBAAA,IAAA,CAACC,cAAAA,YAAD;CACE,SAAQ;CACR,MAAM;CACN,WAAW;CACX,WAAW;CACX,gBAAgB;CAChB,GAAI;AACL,CAAA;AAGH,MAAM,WAAW,EACf,UACA,WACA,gBAAgB,UAChB,MACA,MAAM,WACN,QACA,UAAU,+BACV,QAAQ,UACR,UAAU,UACV,cAAc,YACI;CAClB,MAAM,eAAA,GAAcC,MAAAA,MAAAA,CAAM;CAC1B,MAAM,cAAc,GAAG,YAAY;CACnC,MAAM,eAAe,GAAG,YAAY;CACpC,MAAM,YAAY,GAAG,YAAY;CACjC,MAAM,UAAU,GAAG,YAAY;CAE/B,MAAM,cAAA,GAAaC,+BAAAA,cAAAA,CAAc;CAEjC,MAAM,CAAC,QAAQ,cAAA,GAAaC,MAAAA,SAAAA,CAAwB,aAAa;CACjE,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAAqB,SAAS;CAChE,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,CAA6B,IAAI;CACnE,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,CAAC;CACpC,MAAM,CAAC,QAAQ,cAAA,GAAaA,MAAAA,SAAAA,CAAS,KAAK;CAC1C,MAAM,CAAC,eAAe,qBAAA,GAAoBA,MAAAA,SAAAA,CAAS,gBAAgB,OAAO,MAAM;CAChF,MAAM,CAAC,iBAAiB,uBAAA,GAAsBA,MAAAA,SAAAA,CAAS,KAAK;CAE5D,MAAM,gBAAA,GAAeC,MAAAA,OAAAA,CAAuB,IAAI;CAChD,MAAM,cAAA,GAAaA,MAAAA,OAAAA,CAAuB,IAAI;CAC9C,MAAM,aAAA,GAAYA,MAAAA,OAAAA,CAA0B,IAAI;CAChD,MAAM,kBAAA,GAAiBA,MAAAA,OAAAA,CAA6C,IAAI;CAExE,MAAM,SAAS,gBAAgB;CAC/B,MAAM,cAAc,WAAW,KAAA,KAAa,QAAQ;CACpD,MAAM,cAAc,WAAW,cAAc,gBAAgB,OAAO;CAEpE,MAAM,kBAAkB,eAAe,CAAC;CACxC,MAAM,UAAU,SAAS,KAAA;CACzB,MAAM,CAAC,WAAW,iBAAA,GAAgBD,MAAAA,SAAAA,CAAwB,KAAK;CAC/D,MAAM,CAAC,wBAAwB,8BAAA,GAA6BA,MAAAA,SAAAA,CAAS,IAAI;CACzE,MAAM,cAAA,GAAaC,MAAAA,OAAAA,CAAoB,IAAI;CAC3C,MAAM,wBAAA,GAAuBA,MAAAA,OAAAA,CAAO,KAAK;CAEzC,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,QAAQ,OAAO,WAAW,eAAeC,iBAAAA,YAAY,QAAQ,IAAI;EACvE,MAAM,eAAe,0BAA0B,MAAM,OAAO;EAC5D,OAAO;EACP,MAAM,iBAAiB,UAAU,MAAM;EACvC,aAAa,MAAM,oBAAoB,UAAU,MAAM;CACzD,GAAG,CAAC,CAAC;CAEL,CAAA,GAAA,MAAA,gBAAA,OAAsB;EACpB,MAAM,UAAU,WAAW;EAE3B,IAAI,SAAS;GACX,MAAM,QACJ,iBAAiB,OAAO,CAAC,CAAC,cAAc,SACxC,QAAQ,QAAQ,OAAO,CAAC,EAAE,aAAa,KAAK,CAAC,EAAE,YAAY,MAAM;GAEnE,aAAa,QAAQ,QAAQ,KAAK;EACpC;CACF,GAAG,CAAC,CAAC;CAEL,MAAM,eAAA,GAAcC,iBAAAA,eAAAA,CAAkC;EACpD,OAAO,UAAU,CAAC,EAAE,IAAI,UAAU,GAAG,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,UAAU,CAAC;EACzE,KAAK;EACL,iBAAiB;CACnB,CAAC;CAED,MAAM,eAAA,GAAcA,iBAAAA,eAAAA,CAAkC;EACpD,OAAO,qBAAqB,KAAK,UAAU;GACzC,GAAG;GACH,UAAU,KAAK,OAAO,aAAa,CAAC;EACtC,EAAE;EACF,KAAK;EACL,iBAAiB;CACnB,CAAC;CAED,MAAM,oBACJ,OACA,QACG;EACH,qBAAqB,UAAU;EAE/B,IAAI;GACF,YAAY,cAAc,OAAO,GAAG;EACtC,UAAU;GACR,qBAAqB,UAAU;EACjC;CACF;CAEA,MAAM,kBAAkB,QAAoB;EAC1C,YAAY,YAAY,GAAG;EAE3B,IAAI,qBAAqB,SACvB,aAAa,GAAG;CAEpB;CAEA,MAAM,cAAA,GAAaC,MAAAA,YAAAA,OAAkB;EACnC,MAAM,gBAAgB,UAAU,SAAS;EAEzC,IAAI,CAAC,eAAe;EAEpB,MAAM,YAAY,cAAc;EAEhC,UACG,iBAAiB,6BAA6B,CAAC,CAC/C,SAAS,YAAY,QAAQ,OAAO,CAAC;EAExC,SACG,iBACC;GACE;GACA;GACA;GACA;EACF,CAAC,CAAC,KAAK,IAAI,CACb,CAAC,CACA,SAAS,YAAY;GACpB,MAAM,QAAQ,QAAQ,UAAU,IAAI;GAEpC,MAAM,aAAa,6BAA6B,EAAE;GAClD,UAAU,YAAY,KAAK;EAC7B,CAAC;EAEH,MAAM,aAAa,SAAS;EAC5B,MAAM,aAAa,cAAc;EAEjC,WAAW,MAAM,UAAU,WAAW,MAAM;EAC5C,WAAW,YAAY,WAAW;EAElC,MAAM,KAAK,WAAW,UAAU,CAAC,CAAC,SAAS,cAAc;GACvD,IAAI,UAAU,SAAS,WAAW,UAAU,SAAS,SACnD;GAGF,WAAW,aAAa,UAAU,MAAM,UAAU,KAAK;EACzD,CAAC;EAED,MAAM,OAAO,cAAc;EAE3B,KAAK,YAAY,SAAS,KAAK;EAC/B,KAAK,MAAM,UAAU,SAAS,KAAK,MAAM;EAEzC,WAAW,MAAM,YAAY,cAAc,cAAc,WAAW;EACpE,WAAW,MAAM,YAAY,SAAS,QAAQ,WAAW;EACzD,WAAW,MAAM,YAAY,UAAU,QAAQ,WAAW;EAC1D,WAAW,MAAM,YAAY,cAAc,QAAQ,WAAW;EAC9D,WAAW,MAAM,YAAY,UAAU,KAAK,WAAW;EACvD,WAAW,MAAM,YAAY,WAAW,KAAK,WAAW;EACxD,WAAW,MAAM,YAAY,cAAc,UAAU,WAAW;EAChE,WAAW,MAAM,YAAY,cAAc,QAAQ,WAAW;EAC9D,WAAW,MAAM,YAAY,mBAAmB,QAAQ,WAAW;EACnE,WAAW,MAAM,YAAY,sBAAsB,QAAQ,WAAW;EAEtE,KAAK,MAAM,YAAY,WAAW,SAAS,WAAW;EACtD,KAAK,MAAM,YAAY,YAAY,UAAU,WAAW;EACxD,KAAK,MAAM,YAAY,cAAc,cAAc,WAAW;EAC9D,KAAK,MAAM,YAAY,SAAS,QAAQ,WAAW;EACnD,KAAK,MAAM,YAAY,UAAU,QAAQ,WAAW;EACpD,KAAK,MAAM,YAAY,cAAc,QAAQ,WAAW;EACxD,KAAK,MAAM,YAAY,UAAU,KAAK,WAAW;EACjD,KAAK,MAAM,YAAY,WAAW,KAAK,WAAW;EAClD,KAAK,MAAM,YAAY,YAAY,WAAW,WAAW;EAIzD,cAFkC,eAAe,WAEvC,CAAC,EAAE,MAAM,eAAe,YAAY;EAE9C,MAAM,iBAAiB,cAAc,cAAc,OAAO;EAE1D,eAAe,aAAa,6BAA6B,EAAE;EAC3D,eAAe,cAAc;;;;;;;;;;;;;;EAe7B,UAAU,YAAY,cAAc;CACtC,GAAG,CAAC,WAAW,CAAC;CAEhB,MAAM,eAAA,GAAcA,MAAAA,YAAAA,EACjB,mBAA2B;EAC1B,IAAI,kBAAkB,GAAG;EAEzB,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,cAAc,IAAI,OAAO,KAAK;EAExE,UAAU,YACR,KAAK,IAAI,UAAU,SAAS,IAAI,OAAQ,UAAU,SACpD;CACF,GACA,CAAC,OAAO,KAAK,CACf;CAEA,gCAAgC;EAC9B,MAAM,YAAY,aAAa;EAE/B,IAAI,CAAC,WAAW;EAEhB,MAAM,gBAAgB;GACpB,MAAM,SAAS,OAAO,iBAAiB,SAAS;GAChD,MAAM,qBACH,OAAO,WAAW,OAAO,WAAW,KAAK,MACzC,OAAO,WAAW,OAAO,YAAY,KAAK;GAE7C,YAAY,UAAU,cAAc,iBAAiB;EACvD;EAEA,QAAQ;EAER,MAAM,WAAW,IAAI,gBAAgB,YAAY;GAC/C,MAAM,QAAQ,QAAQ;GAEtB,IAAI,OACF,YAAY,MAAM,YAAY,KAAK;EAEvC,CAAC;EAED,SAAS,QAAQ,SAAS;EAE1B,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,WAAW,CAAC;CAEhB,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,eAAe,SACnB,gBAAgB,YACf,KAAK,QAAQ,+BAA+B,KAC3C,KAAK,cAAc,+BAA+B,MAAM;EAE5D,MAAM,eAAe,IAAI,iBAAiB,UAAU;EAEpD,aAAa,QAAQ,SAAS,MAAM;GAClC,WAAW;GACX,SAAS;GACT,eAAe;GACf,YAAY;EACd,CAAC;EAED,MAAM,eAAe,IAAI,kBAAkB,cAAc;GAgBvD,IAfsB,UAAU,MAAM,aAAa;IAMjD,QAJE,SAAS,kBAAkB,UACvB,SAAS,SACT,SAAS,OAAO,cAAA,EAGZ,QACN,gEACF,KAAK,QACL,MAAM,KAAK,SAAS,UAAU,CAAC,CAAC,KAAK,WAAW,KAChD,MAAM,KAAK,SAAS,YAAY,CAAC,CAAC,KAAK,WAAW;GAEtD,CAEgB,GACd,WAAW;EAEf,CAAC;EAED,aAAa,QAAQ,SAAS,MAAM;GAClC,WAAW;GACX,SAAS;GACT,eAAe;GACf,YAAY;GACZ,iBAAiB,CAAC,YAAY;EAChC,CAAC;EAED,aAAa;GACX,aAAa,WAAW;GACxB,aAAa,WAAW;EAC1B;CACF,GAAG,CAAC,UAAU,CAAC;CAEf,MAAM,mBAAA,GAAkBA,MAAAA,YAAAA,OAAkB;EAExC,MAAM,QADgB,UAAU,SAAS,gBAAA,EACb,eAAe,WAAW;EAEtD,IAAI,CAAC,MAAM;EAEX,WAAW;EACX,aAAa,IAAI;CACnB,GAAG,CAAC,aAAa,UAAU,CAAC;CAE5B,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,gBAAgB;CAClB,GAAG,CAAC,eAAe,CAAC;CAEpB,gCAAgC;EAC9B,MAAM,UAAU,WAAW;EAE3B,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,WAAW,cAAc,WAAW;EAEvE,MAAM,gBAAgB;GACpB,MAAM,iBAAiB,KAAK,KAC1B,KAAK,IAAI,QAAQ,sBAAsB,CAAC,CAAC,QAAQ,QAAQ,YAAY,CACvE;GAEA,iBACE,KAAK,IAAI,gBAAgB,OAAO,QAAQ,KAAK,IAAI,GAAG,cAAc,CAAC,CACrE;GACA,mBAAmB,IAAI;EACzB;EAEA,QAAQ;EAER,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,OAAO;EAExB,MAAM,YAAY,IAAI,iBAAiB,OAAO;EAC9C,UAAU,QAAQ,SAAS;GACzB,SAAS;GACT,WAAW;GACX,eAAe;GACf,YAAY;EACd,CAAC;EAED,aAAa;GACX,SAAS,WAAW;GACpB,UAAU,WAAW;EACvB;CACF,GAAG;EAAC;EAAW;EAAa;CAAS,CAAC;CAEtC,MAAM,kBAAA,GAAiBA,MAAAA,YAAAA,CAAY,YAAY;EAC7C,IAAI,SAAS,KAAA,GAAW;EAExB,IAAI;GACF,MAAM,UAAU,UAAU,UAAU,IAAI;GACxC,UAAU,IAAI;GAEd,IAAI,eAAe,SACjB,aAAa,eAAe,OAAO;GAGrC,eAAe,UAAU,iBAAiB;IACxC,UAAU,KAAK;GACjB,GAAG,GAAI;EACT,QAAQ;GACN,UAAU,KAAK;EACjB;CACF,GAAG,CAAC,IAAI,CAAC;CAET,CAAA,GAAA,MAAA,UAAA,aACc;EACV,IAAI,eAAe,SACjB,aAAa,eAAe,OAAO;CAEvC,GACA,CAAC,CACH;CAEA,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,IAAI,CAAC,WAAW,cAAc,QAC5B,aAAa,SAAS;CAE1B,GAAG,CAAC,WAAW,OAAO,CAAC;CAEvB,MAAM,aAA4B;EAChC,OAAO,OAAO;EACd,QAAQ;EACR,WAAW,SAAS,MAAM;EAC1B,iBAAiB;EACjB,YAAY,kBAAkB,WAAW,KAAA;CAC3C;CAEA,MAAM,eAA8B;EAClC,SAAS;EACT,WAAW;EACX;EACA,WAAW,cAAc,KAAA,IAAY;EACrC,YAAY,QAAQ,aAAa,SAAS,KAAA;EAC1C,gBAAgB,UAAU,eAAe,WAAW,KAAA;CACtD;CAEA,MAAM,gBAA+B;EACnC,OAAO,OAAO,QAAQ;EACtB,QAAQ,kBAAkB,KAAA,IAAY,cAAc;EACpD,WAAW,kBACP,gBAAgB,OAAO,SAAS,QAChC,KAAA;CACN;CAEA,MAAM,mBAAkC;EACtC,GAAG;EACH,OAAO,OAAO;EACd,MAAM,UAAU,IAAI,KAAA,IAAY;CAClC;CAEA,MAAM,aAAa;EACjB,IAAI;EACJ,MAAM;EACN,mBAAmB,cAAc,YAAY,eAAe;CAC9D;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,WAAD;EAAS,KAAK;EAAY,YAAA,GAAWC,gBAAAA,GAAAA,CAAG,kBAAkB,SAAS;EAAnE,UAAA;GACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KACE,WAAU;KACV,MAAK;KACL,cAAW;KAHb,UAAA,CAKE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MACE,MAAK;MACL,MAAK;MACL,IAAI;MACJ,KAAK,YAAY,WAAW,SAAS;MACrC,UAAU,cAAc,YAAY,IAAI;MACxC,eAAe,eAAe,SAAS;MACvC,YAAY,UAAU,iBAAiB,OAAO,SAAS;MACvD,iBAAe,cAAc;MAC7B,iBAAe;MACf,YAAA,GAAWA,gBAAAA,GAAAA,CACT,uBACA,cAAc,aAAa,6BAC7B;MACA,eAAe,aAAa,SAAS;MACtC,UAAA;KAEO,CAAA,GAEP,WACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MACE,MAAK;MACL,MAAK;MACL,IAAI;MACJ,KAAK,YAAY,WAAW,MAAM;MAClC,UAAU,cAAc,SAAS,IAAI;MACrC,eAAe,eAAe,MAAM;MACpC,YAAY,UAAU,iBAAiB,OAAO,MAAM;MACpD,iBAAe,cAAc;MAC7B,iBAAe;MACf,YAAA,GAAWA,gBAAAA,GAAAA,CACT,uBACA,cAAc,UAAU,6BAC1B;MACA,eAAe,aAAa,MAAM;MACnC,UAAA;KAEO,CAAA,CAEP;IAEJ,CAAA,GAAA,cAAc,YACb,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACG,CAAC,eACA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,cAAW;MAET,UAAA,OAAO,KAAK,eAAe,CAAC,CAAqB,KAChD,SACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;OAEE,eAAa;OACb,KAAK,YAAY,WAAW,IAAI;OAChC,MAAK;OACL,YAAA,GAAWA,gBAAAA,GAAAA,CACT,0BACA,WAAW,QAAQ,gCACrB;OACA,gBAAc,WAAW;OACzB,UAAU,YAAY,gBAAgB,IAAI;OAC1C,eAAe,YAAY,YAAY,IAAI;OAC3C,YAAY,UACV,YAAY,cAAc,OAAO,IAAI;OAEvC,eAAe,UAAU,IAAI;OAE5B,UAAA,gBAAgB,KAAK,CAAC;MACjB,GAjBD,IAiBC,CAEZ;KACG,CAAA,GAGP,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;MAAM,WAAU;MAAhB,UAAA;OACG,OAAO;OAAM;OAAI;MACd;KACH,CAAA,CAAA;IAEL,CAAA,IAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,SAAS;KACT,cAAY,SAAS,gBAAgB;KAJvC,UAAA,CAME,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,MAAD;MACE,MAAM,SAAS,iBAAiB;MAChC,MAAM;MACN,eAAY;KACb,CAAA,GACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,SAAS,WAAW,YAAkB,CAAA,CACvC;IAEP,CAAA,CAAA;;GAEL,iBAAA,GAAA,kBAAA,KAAA,CAAC,mBAAD;IAAmB,GAAI;IAAY,WAAU;IAA7C,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;KACE,KAAK;KACL,WAAU;KACV,QAAQ,cAAc;KAEtB,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;MAAK,WAAU;MAA2B,OAAO;MAAjD,UAAA;OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;QACE,KAAK;QACL,WAAU;QACV,QAAQ,sBAAsB,WAAW;QACzC,OAAO,GAAG,OAAO,MAAM;QACvB,QAAQ;QACR,OAAO;OACR,CAAA;OAEA,mBACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;QACE,WAAU;QACV,OAAO;QACP,eAAY;QACZ,OAAA;QACA,4BAAyB;QAExB;OACE,CAAA;OAGN,cAAA,GACCC,UAAAA,aAAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,+BAAAA,oBAAD;QACE,SAAS,YAAY,WAAW;QAChC,OAAO,YAAY,SAAS;QAE5B,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;SAAK,KAAK;SAAY,OAAO;SAAe;QAAc,CAAA;OACxC,CAAA,GACpB,SACF;MACC;;IACF,CAAA,GAEJ,cAAc,UACb,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;KAAK,WAAU;KACb,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD,EAAA,UACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAU;MACb,UAAA,SAAS,KAAA,IAAY,OAAOC,kBAAAA,cAAc,IAAI;KAC3C,CAAA,EACH,CAAA;IACF,CAAA,CAEU;;GAEnB,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;IACE,WAAU;IACV,MAAK;IACL,aAAU;IAET,UAAA,SAAS,8BAA8B;GACpC,CAAA;EACC;;AAEb"}
@@ -7,8 +7,12 @@ type PreviewProps = {
7
7
  className?: string;
8
8
  defaultDevice?: PreviewDevice;
9
9
  code?: string;
10
- /** Frame height in pixels. Defaults to the selected device's height. */
10
+ /** Size to content (capped at mobile height) or the selected device. */
11
+ fit?: "content" | "device";
12
+ /** Frame height in pixels. Overrides fit. */
11
13
  height?: number;
14
+ /** Space inside the preview frame, around the content. Numbers are pixels. */
15
+ padding?: number | string;
12
16
  /** Vertical alignment of the content within the frame. */
13
17
  align?: PreviewAlign;
14
18
  /** Horizontal distribution of the content within the frame. */
@@ -16,5 +20,5 @@ type PreviewProps = {
16
20
  /** Hides the device selector. The default device is still used. */
17
21
  hideDevices?: boolean;
18
22
  };
19
- declare const Preview: ({ children, className, defaultDevice, code, height, align, justify, hideDevices }: PreviewProps) => import("react").JSX.Element;
23
+ declare const Preview: ({ children, className, defaultDevice, code, fit, height, padding, align, justify, hideDevices }: PreviewProps) => import("react").JSX.Element;
20
24
  export { Preview, type PreviewAlign, type PreviewDevice, type PreviewJustify, type PreviewProps };
@@ -7,8 +7,12 @@ type PreviewProps = {
7
7
  className?: string;
8
8
  defaultDevice?: PreviewDevice;
9
9
  code?: string;
10
- /** Frame height in pixels. Defaults to the selected device's height. */
10
+ /** Size to content (capped at mobile height) or the selected device. */
11
+ fit?: "content" | "device";
12
+ /** Frame height in pixels. Overrides fit. */
11
13
  height?: number;
14
+ /** Space inside the preview frame, around the content. Numbers are pixels. */
15
+ padding?: number | string;
12
16
  /** Vertical alignment of the content within the frame. */
13
17
  align?: PreviewAlign;
14
18
  /** Horizontal distribution of the content within the frame. */
@@ -16,5 +20,5 @@ type PreviewProps = {
16
20
  /** Hides the device selector. The default device is still used. */
17
21
  hideDevices?: boolean;
18
22
  };
19
- declare const Preview: ({ children, className, defaultDevice, code, height, align, justify, hideDevices }: PreviewProps) => import("react").JSX.Element;
23
+ declare const Preview: ({ children, className, defaultDevice, code, fit, height, padding, align, justify, hideDevices }: PreviewProps) => import("react").JSX.Element;
20
24
  export { Preview, type PreviewAlign, type PreviewDevice, type PreviewJustify, type PreviewProps };
@@ -9,6 +9,7 @@ import { useRovingFocus } from "@puzzle-ui/hooks";
9
9
  import { createPortal } from "react-dom";
10
10
  import { Icon } from "@puzzle-ui/icons";
11
11
  import { ThemeScopeProvider, useThemeScope } from "@puzzle-ui/theme/scope-context";
12
+ import { BREAKPOINTS as BREAKPOINTS$1 } from "@puzzle-ui/theme";
12
13
  // src/components/utilities/preview/index.tsx
13
14
  const PREVIEW_DEVICES = {
14
15
  mobile: {
@@ -61,7 +62,7 @@ const PreviewBackground = (props) => /* @__PURE__ */ jsx(Background, {
61
62
  patternOpacity: .25,
62
63
  ...props
63
64
  });
64
- const Preview = ({ children, className, defaultDevice = "mobile", code, height, align, justify, hideDevices = false }) => {
65
+ const Preview = ({ children, className, defaultDevice = "mobile", code, fit = "content", height, padding = "var(--puzzle-space-8, 2rem)", align = "center", justify = "center", hideDevices = false }) => {
65
66
  const generatedId = useId();
66
67
  const frameRootId = `${generatedId}-root`;
67
68
  const previewTabId = `${generatedId}-tab-preview`;
@@ -73,15 +74,28 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
73
74
  const [frameRoot, setFrameRoot] = useState(null);
74
75
  const [scale, setScale] = useState(1);
75
76
  const [copied, setCopied] = useState(false);
77
+ const [contentHeight, setContentHeight] = useState(PREVIEW_DEVICES.mobile.height);
78
+ const [contentMeasured, setContentMeasured] = useState(false);
76
79
  const containerRef = useRef(null);
80
+ const contentRef = useRef(null);
77
81
  const iframeRef = useRef(null);
78
82
  const copyTimeoutRef = useRef(null);
79
83
  const config = PREVIEW_DEVICES[device];
80
- const frameHeight = height ?? config.height;
84
+ const fitsContent = height === void 0 && fit === "content";
85
+ const frameHeight = height ?? (fitsContent ? contentHeight : config.height);
86
+ const showPlaceholder = fitsContent && !contentMeasured;
81
87
  const hasCode = code !== void 0;
82
88
  const [direction, setDirection] = useState("ltr");
89
+ const [desktopControlsVisible, setDesktopControlsVisible] = useState(true);
83
90
  const sectionRef = useRef(null);
84
91
  const isKeyboardNavigation = useRef(false);
92
+ useEffect(() => {
93
+ const query = window.matchMedia(`(min-width: ${BREAKPOINTS$1.desktop}px)`);
94
+ const update = () => setDesktopControlsVisible(query.matches);
95
+ update();
96
+ query.addEventListener("change", update);
97
+ return () => query.removeEventListener("change", update);
98
+ }, []);
85
99
  useLayoutEffect(() => {
86
100
  const section = sectionRef.current;
87
101
  if (section) {
@@ -95,7 +109,10 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
95
109
  initialActiveId: activeTab
96
110
  });
97
111
  const deviceFocus = useRovingFocus({
98
- items: PREVIEW_DEVICE_ITEMS,
112
+ items: PREVIEW_DEVICE_ITEMS.map((item) => ({
113
+ ...item,
114
+ disabled: item.id === "desktop" && !desktopControlsVisible
115
+ })),
99
116
  dir: direction,
100
117
  initialActiveId: device
101
118
  });
@@ -142,6 +159,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
142
159
  targetRoot.style.setProperty("height", "100%", "important");
143
160
  targetRoot.style.setProperty("min-height", "100%", "important");
144
161
  targetRoot.style.setProperty("margin", "0", "important");
162
+ targetRoot.style.setProperty("padding", "0", "important");
145
163
  targetRoot.style.setProperty("overflow-x", "hidden", "important");
146
164
  targetRoot.style.setProperty("overflow-y", "auto", "important");
147
165
  targetRoot.style.setProperty("scrollbar-width", "none", "important");
@@ -153,6 +171,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
153
171
  body.style.setProperty("height", "auto", "important");
154
172
  body.style.setProperty("min-height", "100%", "important");
155
173
  body.style.setProperty("margin", "0", "important");
174
+ body.style.setProperty("padding", "0", "important");
156
175
  body.style.setProperty("overflow", "visible", "important");
157
176
  frameDocument.getElementById(frameRootId)?.style.removeProperty("min-height");
158
177
  const scrollbarStyle = frameDocument.createElement("style");
@@ -174,6 +193,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
174
193
  frameHead.appendChild(scrollbarStyle);
175
194
  }, [frameRootId]);
176
195
  const updateScale = useCallback((availableWidth) => {
196
+ if (availableWidth <= 0) return;
177
197
  const nextScale = Math.min(1, Math.max(0, availableWidth) / config.width);
178
198
  setScale((current) => Math.abs(current - nextScale) < .001 ? current : nextScale);
179
199
  }, [config.width]);
@@ -182,7 +202,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
182
202
  if (!container) return;
183
203
  const measure = () => {
184
204
  const styles = window.getComputedStyle(container);
185
- const horizontalPadding = parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight);
205
+ const horizontalPadding = (Number.parseFloat(styles.paddingLeft) || 0) + (Number.parseFloat(styles.paddingRight) || 0);
186
206
  updateScale(container.clientWidth - horizontalPadding);
187
207
  };
188
208
  measure();
@@ -228,6 +248,33 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
228
248
  useEffect(() => {
229
249
  initializeFrame();
230
250
  }, [initializeFrame]);
251
+ useIsomorphicLayoutEffect(() => {
252
+ const content = contentRef.current;
253
+ if (!fitsContent || !frameRoot || !content || activeTab !== "preview") return;
254
+ const measure = () => {
255
+ const measuredHeight = Math.ceil(Math.max(content.getBoundingClientRect().height, content.scrollHeight));
256
+ setContentHeight(Math.min(PREVIEW_DEVICES.mobile.height, Math.max(1, measuredHeight)));
257
+ setContentMeasured(true);
258
+ };
259
+ measure();
260
+ const observer = new ResizeObserver(measure);
261
+ observer.observe(content);
262
+ const mutations = new MutationObserver(measure);
263
+ mutations.observe(content, {
264
+ subtree: true,
265
+ childList: true,
266
+ characterData: true,
267
+ attributes: true
268
+ });
269
+ return () => {
270
+ observer.disconnect();
271
+ mutations.disconnect();
272
+ };
273
+ }, [
274
+ activeTab,
275
+ fitsContent,
276
+ frameRoot
277
+ ]);
231
278
  const handleCopyCode = useCallback(async () => {
232
279
  if (code === void 0) return;
233
280
  try {
@@ -251,18 +298,26 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
251
298
  width: config.width,
252
299
  height: frameHeight,
253
300
  transform: `scale(${scale})`,
254
- transformOrigin: "top left"
301
+ transformOrigin: "top left",
302
+ visibility: showPlaceholder ? "hidden" : void 0
255
303
  };
256
304
  const contentStyle = {
257
305
  display: "flex",
258
306
  boxSizing: "border-box",
259
- minHeight: "100vh",
307
+ padding,
308
+ minHeight: fitsContent ? void 0 : "100vh",
260
309
  alignItems: align ? ALIGN_VALUES[align] : void 0,
261
310
  justifyContent: justify ? JUSTIFY_VALUES[justify] : void 0
262
311
  };
263
312
  const viewportStyle = {
264
313
  width: config.width * scale,
265
- aspectRatio: `${config.width} / ${frameHeight}`
314
+ height: showPlaceholder ? void 0 : frameHeight * scale,
315
+ maxHeight: showPlaceholder ? PREVIEW_DEVICES.mobile.height * scale : void 0
316
+ };
317
+ const placeholderStyle = {
318
+ ...contentStyle,
319
+ width: config.width,
320
+ zoom: scale === 1 ? void 0 : scale
266
321
  };
267
322
  const panelProps = {
268
323
  id: panelId,
@@ -313,6 +368,7 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
313
368
  role: "group",
314
369
  "aria-label": "Device size",
315
370
  children: Object.keys(PREVIEW_DEVICES).map((item) => /* @__PURE__ */ jsx("button", {
371
+ "data-device": item,
316
372
  ref: deviceFocus.getItemRef(item),
317
373
  type: "button",
318
374
  className: cn("puzzle-preview__device", device === item && "puzzle-preview__device--active"),
@@ -353,21 +409,33 @@ const Preview = ({ children, className, defaultDevice = "mobile", code, height,
353
409
  children: /* @__PURE__ */ jsxs("div", {
354
410
  className: "puzzle-preview__viewport",
355
411
  style: viewportStyle,
356
- children: [/* @__PURE__ */ jsx("iframe", {
357
- ref: iframeRef,
358
- className: "puzzle-preview__frame",
359
- srcDoc: createPreviewDocument(frameRootId),
360
- title: `${config.label} component preview`,
361
- onLoad: initializeFrame,
362
- style: frameStyle
363
- }), frameRoot && createPortal(/* @__PURE__ */ jsx(ThemeScopeProvider, {
364
- scopeId: themeScope?.scopeId ?? null,
365
- theme: themeScope?.theme ?? "system",
366
- children: /* @__PURE__ */ jsx("div", {
367
- style: contentStyle,
412
+ children: [
413
+ /* @__PURE__ */ jsx("iframe", {
414
+ ref: iframeRef,
415
+ className: "puzzle-preview__frame",
416
+ srcDoc: createPreviewDocument(frameRootId),
417
+ title: `${config.label} component preview`,
418
+ onLoad: initializeFrame,
419
+ style: frameStyle
420
+ }),
421
+ showPlaceholder && /* @__PURE__ */ jsx("div", {
422
+ className: "puzzle-preview__placeholder",
423
+ style: placeholderStyle,
424
+ "aria-hidden": "true",
425
+ inert: true,
426
+ "data-preview-placeholder": "",
368
427
  children
369
- })
370
- }), frameRoot)]
428
+ }),
429
+ frameRoot && createPortal(/* @__PURE__ */ jsx(ThemeScopeProvider, {
430
+ scopeId: themeScope?.scopeId ?? null,
431
+ theme: themeScope?.theme ?? "system",
432
+ children: /* @__PURE__ */ jsx("div", {
433
+ ref: contentRef,
434
+ style: contentStyle,
435
+ children
436
+ })
437
+ }), frameRoot)
438
+ ]
371
439
  })
372
440
  }), activeTab === "code" && /* @__PURE__ */ jsx("div", {
373
441
  className: "puzzle-preview__code",