@astuteo/breakout-grid 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2523 @@
1
+ (function() {
2
+ "use strict";
3
+ const VERSION = `v${"5.1.0"}`;
4
+ const LOREM_CONTENT = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
5
+
6
+ Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet.`;
7
+ const GRID_AREAS = [
8
+ { name: "full", label: "Full", className: ".col-full", color: "rgba(239, 68, 68, 0.25)", borderColor: "rgb(239, 68, 68)" },
9
+ { name: "full-limit", label: "Full Limit", className: ".col-full-limit", color: "rgba(220, 38, 38, 0.25)", borderColor: "rgb(220, 38, 38)" },
10
+ { name: "feature", label: "Feature", className: ".col-feature", color: "rgba(6, 182, 212, 0.25)", borderColor: "rgb(6, 182, 212)" },
11
+ { name: "popout", label: "Popout", className: ".col-popout", color: "rgba(34, 197, 94, 0.25)", borderColor: "rgb(34, 197, 94)" },
12
+ { name: "content", label: "Content", className: ".col-content", color: "rgba(168, 85, 247, 0.25)", borderColor: "rgb(168, 85, 247)" }
13
+ ];
14
+ const CONFIG_OPTIONS = {
15
+ // Base measurements
16
+ baseGap: { value: "1rem", desc: "Minimum gap between columns. Use rem.", cssVar: "--config-base-gap", liveVar: "--base-gap" },
17
+ maxGap: { value: "15rem", desc: "Maximum gap cap for ultra-wide. Use rem.", cssVar: "--config-max-gap", liveVar: "--max-gap" },
18
+ contentMin: { value: "53rem", desc: "Min width for content column (~848px). Use rem.", cssVar: "--config-content-min", liveVar: "--content-min" },
19
+ contentMax: { value: "61rem", desc: "Max width for content column (~976px). Use rem.", cssVar: "--config-content-max", liveVar: "--content-max" },
20
+ contentBase: { value: "75vw", desc: "Preferred width for content (fluid). Use vw.", cssVar: "--config-content-base", liveVar: "--content-base" },
21
+ // Track widths
22
+ popoutWidth: { value: "5rem", desc: "Popout extends beyond content. Use rem.", cssVar: "--config-popout", liveVar: null },
23
+ featureMin: { value: "0rem", desc: "Minimum feature track width (floor)", cssVar: "--config-feature-min", liveVar: null },
24
+ featureScale: { value: "12vw", desc: "Fluid feature track scaling", cssVar: "--config-feature-scale", liveVar: null },
25
+ featureMax: { value: "12rem", desc: "Maximum feature track width (ceiling)", cssVar: "--config-feature-max", liveVar: null },
26
+ fullLimit: { value: "115rem", desc: "Max width for col-full-limit. Use rem.", cssVar: "--config-full-limit", liveVar: "--full-limit" },
27
+ // Default column
28
+ defaultCol: { value: "content", desc: "Default column when no col-* class", type: "select", options: ["content", "popout", "feature", "full"], cssVar: "--config-default-col" }
29
+ };
30
+ const GAP_SCALE_OPTIONS = {
31
+ default: { value: "4vw", desc: "Mobile/default gap scaling. Use vw.", cssVar: "--config-gap-scale-default" },
32
+ lg: { value: "5vw", desc: "Large screens (1024px+). Use vw.", cssVar: "--config-gap-scale-lg" },
33
+ xl: { value: "6vw", desc: "Extra large (1280px+). Use vw.", cssVar: "--config-gap-scale-xl" }
34
+ };
35
+ const BREAKOUT_OPTIONS = {
36
+ min: { value: "1rem", desc: "Minimum breakout padding (floor)", cssVar: "--config-breakout-min" },
37
+ scale: { value: "5vw", desc: "Fluid breakout scaling", cssVar: "--config-breakout-scale" }
38
+ // max is popoutWidth
39
+ };
40
+ const BREAKPOINT_OPTIONS = {
41
+ lg: { value: "1024", desc: "Large breakpoint (px)", cssVar: "--config-breakpoint-lg" },
42
+ xl: { value: "1280", desc: "Extra large breakpoint (px)", cssVar: "--config-breakpoint-xl" }
43
+ };
44
+ function createInitialState() {
45
+ return {
46
+ // UI State
47
+ isVisible: false,
48
+ showLabels: true,
49
+ showClassNames: true,
50
+ showMeasurements: true,
51
+ showPixelWidths: false,
52
+ showGapPadding: false,
53
+ showBreakoutPadding: false,
54
+ showAdvanced: false,
55
+ showLoremIpsum: false,
56
+ showEditor: false,
57
+ showDiagram: false,
58
+ editMode: false,
59
+ viewportWidth: window.innerWidth,
60
+ selectedArea: null,
61
+ hoveredArea: null,
62
+ editValues: {},
63
+ copySuccess: false,
64
+ configCopied: false,
65
+ editorPos: { x: 20, y: 100 },
66
+ isDragging: false,
67
+ dragOffset: { x: 0, y: 0 },
68
+ // Column resize drag state
69
+ resizingColumn: null,
70
+ resizeStartX: 0,
71
+ resizeStartValue: 0,
72
+ // Panel collapse state
73
+ controlPanelCollapsed: false,
74
+ configEditorCollapsed: false,
75
+ // Computed column widths in pixels (pre-initialized for reactivity)
76
+ columnWidths: {
77
+ full: 0,
78
+ "full-limit": 0,
79
+ feature: 0,
80
+ popout: 0,
81
+ content: 0,
82
+ center: 0
83
+ },
84
+ // Current breakpoint for gap scale (mobile, lg, xl)
85
+ currentBreakpoint: "mobile",
86
+ // Spacing panel state
87
+ spacingPanelCollapsed: false,
88
+ spacingPanelPos: { x: 16, y: 16 },
89
+ isDraggingSpacing: false,
90
+ dragOffsetSpacing: { x: 0, y: 0 },
91
+ // Restore config modal
92
+ showRestoreModal: false,
93
+ restoreInput: "",
94
+ restoreError: null,
95
+ // Section copy feedback
96
+ sectionCopied: null
97
+ };
98
+ }
99
+ const BUILD_VERSION = "5.1.0";
100
+ function generateCSSExport(c, version = BUILD_VERSION) {
101
+ var _a, _b, _c, _d, _e;
102
+ const VERSION2 = version;
103
+ const breakoutMin = c.breakoutMin || "1rem";
104
+ const breakoutScale = c.breakoutScale || "5vw";
105
+ const breakpointLg = ((_a = c.breakpoints) == null ? void 0 : _a.lg) || "1024";
106
+ const breakpointXl = ((_b = c.breakpoints) == null ? void 0 : _b.xl) || "1280";
107
+ return `/**
108
+ * Breakout Grid - Objects Layer (ITCSS)
109
+ * Version: ${VERSION2}
110
+ *
111
+ * Documentation: https://github.com/astuteo-llc/breakout-grid
112
+ *
113
+ * ============================================================================
114
+ * TABLE OF CONTENTS
115
+ * ============================================================================
116
+ *
117
+ * CONFIGURATION
118
+ * - Configuration Variables ........... Customizable :root variables
119
+ * - Computed Values ................... Auto-calculated (do not edit)
120
+ *
121
+ * GRID CONTAINERS
122
+ * - Grid Container - Main ............. .grid-cols-breakout
123
+ * - Subgrid ........................... .grid-cols-breakout-subgrid
124
+ * - Left/Right Aligned Variants ....... .grid-cols-{area}-{left|right}
125
+ * - Breakout Modifiers ................ .breakout-to-{content|popout|feature}
126
+ * - Breakout None ..................... .breakout-none, .breakout-none-flex
127
+ *
128
+ * COLUMN UTILITIES
129
+ * - Basic ............................. .col-{full|feature|popout|content|center}
130
+ * - Start/End ......................... .col-start-*, .col-end-*
131
+ * - Left/Right Spans .................. .col-*-left, .col-*-right
132
+ * - Advanced Spans .................... .col-*-to-*
133
+ * - Full Limit ........................ .col-full-limit
134
+ *
135
+ * SPACING UTILITIES
136
+ * - Padding ........................... .p-breakout, .p-gap, .p-*-to-content
137
+ * - Margins ........................... .m-breakout, .m-gap, .-m-*
138
+ *
139
+ * ============================================================================
140
+ * INTEGRATION (ITCSS + Tailwind v4)
141
+ * ============================================================================
142
+ *
143
+ * Add this file to your Objects layer. In your main CSS file:
144
+ *
145
+ * @import 'tailwindcss';
146
+ *
147
+ * @import './_settings.fonts.css';
148
+ * @import './_objects.breakout-grid.css'; <-- Add here (Objects layer)
149
+ * @import './_utilities.global.css';
150
+ *
151
+ * @layer components {
152
+ * @import './_components.hero.css';
153
+ * ...
154
+ * }
155
+ *
156
+ * ============================================================================
157
+ * QUICK START
158
+ * ============================================================================
159
+ *
160
+ * <main class="grid-cols-breakout">
161
+ * <article class="col-content">Reading width</article>
162
+ * <figure class="col-feature">Wider for images</figure>
163
+ * <div class="col-full">Edge to edge</div>
164
+ * </main>
165
+ *
166
+ */
167
+
168
+ /* ============================================================================
169
+ CONFIGURATION VARIABLES
170
+ ============================================================================
171
+ To restore this grid in the visualizer, copy from here to END CONFIGURATION.
172
+ Paste into the "Restore" dialog at:
173
+ https://github.com/astuteo-llc/breakout-grid
174
+ ============================================================================ */
175
+
176
+ :root {
177
+ /* Content (text width) */
178
+ --content-min: ${c.contentMin};
179
+ --content-base: ${c.contentBase};
180
+ --content-max: ${c.contentMax};
181
+
182
+ /* Default column for children without col-* class */
183
+ --default-col: ${c.defaultCol || "content"};
184
+
185
+ /* Track widths */
186
+ --popout-width: ${c.popoutWidth};
187
+ --full-limit: ${c.fullLimit};
188
+
189
+ /* Feature track */
190
+ --feature-min: ${c.featureMin};
191
+ --feature-scale: ${c.featureScale};
192
+ --feature-max: ${c.featureMax};
193
+
194
+ /* Outer margins */
195
+ --base-gap: ${c.baseGap};
196
+ --max-gap: ${c.maxGap};
197
+
198
+ /* Responsive scale */
199
+ --gap-scale-default: ${((_c = c.gapScale) == null ? void 0 : _c.default) || "4vw"};
200
+ --gap-scale-lg: ${((_d = c.gapScale) == null ? void 0 : _d.lg) || "5vw"};
201
+ --gap-scale-xl: ${((_e = c.gapScale) == null ? void 0 : _e.xl) || "6vw"};
202
+
203
+ /* Breakout padding */
204
+ --breakout-min: ${breakoutMin};
205
+ --breakout-scale: ${breakoutScale};
206
+
207
+ /* Breakpoints (used in media queries below) */
208
+ /* --breakpoint-lg: ${breakpointLg}px; */
209
+ /* --breakpoint-xl: ${breakpointXl}px; */
210
+ }
211
+
212
+ /* ============================================================================
213
+ END CONFIGURATION
214
+ ============================================================================ */
215
+
216
+
217
+ /* ============================================================================
218
+ COMPUTED VALUES - DO NOT EDIT
219
+ ============================================================================
220
+ These are calculated from the customizable variables above.
221
+ Editing these directly will break the grid calculations.
222
+ ============================================================================ */
223
+
224
+ :root {
225
+ /* Responsive gap: scales between base and max based on viewport */
226
+ --gap: clamp(var(--base-gap), var(--gap-scale-default), var(--max-gap));
227
+
228
+ /* Computed gap: larger value for full-width spacing */
229
+ --computed-gap: max(var(--gap), calc((100vw - var(--content)) / 10));
230
+
231
+ /* Content width: fluid between min/max, respects gap on both sides */
232
+ --content: min(clamp(var(--content-min), var(--content-base), var(--content-max)), 100% - var(--gap) * 2);
233
+
234
+ /* Content inset: for left/right aligned grids (single gap) */
235
+ --content-inset: min(clamp(var(--content-min), var(--content-base), var(--content-max)), calc(100% - var(--gap)));
236
+
237
+ /* Half content: used for center alignment */
238
+ --content-half: calc(var(--content) / 2);
239
+
240
+ /* Track definitions for grid-template-columns */
241
+ --full: minmax(var(--gap), 1fr);
242
+ --feature: minmax(0, clamp(var(--feature-min), var(--feature-scale), var(--feature-max)));
243
+ --popout: minmax(0, var(--popout-width));
244
+
245
+ /* Alignment padding: for aligning content inside wider columns */
246
+ --breakout-padding: clamp(var(--breakout-min), var(--breakout-scale), var(--popout-width));
247
+ --popout-to-content: clamp(var(--breakout-min), var(--breakout-scale), var(--popout-width));
248
+ --feature-to-content: calc(clamp(var(--feature-min), var(--feature-scale), var(--feature-max)) + var(--popout-width));
249
+ }
250
+
251
+ /* Responsive gap scaling */
252
+ @media (min-width: ${breakpointLg}px) {
253
+ :root {
254
+ --gap: clamp(var(--base-gap), var(--gap-scale-lg), var(--max-gap));
255
+ }
256
+ }
257
+
258
+ @media (min-width: ${breakpointXl}px) {
259
+ :root {
260
+ --gap: clamp(var(--base-gap), var(--gap-scale-xl), var(--max-gap));
261
+ }
262
+ }
263
+
264
+ /* ========================================
265
+ Grid Container - Main
266
+ ========================================
267
+
268
+ The primary grid container. Apply to any element that should use
269
+ the breakout grid system. All direct children default to the
270
+ content column unless given a col-* class.
271
+
272
+ Basic usage:
273
+ <main class="grid-cols-breakout">
274
+ <article>Default content width</article>
275
+ <figure class="col-feature">Wider for images</figure>
276
+ <div class="col-full">Edge to edge</div>
277
+ </main>
278
+ */
279
+ .grid-cols-breakout {
280
+ display: grid;
281
+ grid-template-columns:
282
+ [full-start] var(--full)
283
+ [feature-start] var(--feature)
284
+ [popout-start] var(--popout)
285
+ [content-start] var(--content-half) [center-start center-end] var(--content-half) [content-end]
286
+ var(--popout) [popout-end]
287
+ var(--feature) [feature-end]
288
+ var(--full) [full-end];
289
+ }
290
+
291
+ /* Default column for direct children without explicit col-* class */
292
+ [class*='grid-cols-breakout'] > *:not([class*='col-']),
293
+ [class*='grid-cols-feature'] > *:not([class*='col-']),
294
+ [class*='grid-cols-popout'] > *:not([class*='col-']),
295
+ [class*='grid-cols-content'] > *:not([class*='col-']) {
296
+ grid-column: var(--default-col, content);
297
+ }
298
+
299
+ /* ----------------------------------------
300
+ Subgrid - Nested Alignment
301
+ ----------------------------------------
302
+
303
+ Use subgrid when you need children of a spanning element to align
304
+ with the parent grid's tracks. The child inherits the parent's
305
+ column lines.
306
+
307
+ Example - Card grid inside a feature-width container:
308
+ <div class="col-feature grid-cols-breakout-subgrid">
309
+ <h2 class="col-content">Title aligns with content</h2>
310
+ <div class="col-feature">Full width of parent</div>
311
+ </div>
312
+
313
+ Browser support: ~93% (check caniuse.com/css-subgrid)
314
+ */
315
+ .grid-cols-breakout-subgrid {
316
+ display: grid;
317
+ grid-template-columns: subgrid;
318
+ }
319
+
320
+ /* ========================================
321
+ Grid Container - Left/Right Aligned Variants
322
+ ========================================
323
+
324
+ Use these when content should anchor to one side instead of centering.
325
+ Common for asymmetric layouts, sidebars, or split-screen designs.
326
+
327
+ Left variants: Content anchors to left edge, right side has outer tracks
328
+ Right variants: Content anchors to right edge, left side has outer tracks
329
+
330
+ Example - Image left, text right:
331
+ <section class="grid-cols-feature-left">
332
+ <figure class="col-feature">Image anchored left</figure>
333
+ <div class="col-content">Text in content area</div>
334
+ </section>
335
+
336
+ Example - Sidebar layout:
337
+ <div class="grid-cols-content-right">
338
+ <aside class="col-full">Sidebar fills left</aside>
339
+ <main class="col-content">Main content right-aligned</main>
340
+ </div>
341
+ */
342
+ .grid-cols-feature-left {
343
+ display: grid;
344
+ grid-template-columns:
345
+ [full-start] var(--full)
346
+ [feature-start] var(--feature)
347
+ [popout-start] var(--popout)
348
+ [content-start] var(--content-inset) [content-end]
349
+ var(--popout) [popout-end]
350
+ var(--feature) [feature-end full-end];
351
+ }
352
+
353
+ .grid-cols-popout-left {
354
+ display: grid;
355
+ grid-template-columns:
356
+ [full-start] var(--full)
357
+ [feature-start] var(--feature)
358
+ [popout-start] var(--popout)
359
+ [content-start] var(--content-inset) [content-end]
360
+ var(--popout) [popout-end full-end];
361
+ }
362
+
363
+ .grid-cols-content-left {
364
+ display: grid;
365
+ grid-template-columns:
366
+ [full-start] var(--full)
367
+ [feature-start] var(--feature)
368
+ [popout-start] var(--popout)
369
+ [content-start] var(--content-inset) [content-end full-end];
370
+ }
371
+
372
+ /* ========================================
373
+ Grid Container - Right Aligned Variants
374
+ ======================================== */
375
+ .grid-cols-feature-right {
376
+ display: grid;
377
+ grid-template-columns:
378
+ [full-start feature-start] var(--feature)
379
+ [popout-start] var(--popout)
380
+ [content-start] var(--content-inset) [content-end]
381
+ var(--popout) [popout-end]
382
+ var(--feature) [feature-end]
383
+ var(--full) [full-end];
384
+ }
385
+
386
+ .grid-cols-popout-right {
387
+ display: grid;
388
+ grid-template-columns:
389
+ [full-start popout-start] var(--popout)
390
+ [content-start] var(--content-inset) [content-end]
391
+ var(--popout) [popout-end]
392
+ var(--feature) [feature-end]
393
+ var(--full) [full-end];
394
+ }
395
+
396
+ .grid-cols-content-right {
397
+ display: grid;
398
+ grid-template-columns:
399
+ [full-start content-start] var(--content-inset) [content-end]
400
+ var(--popout) [popout-end]
401
+ var(--feature) [feature-end]
402
+ var(--full) [full-end];
403
+ }
404
+
405
+ /* ========================================
406
+ Breakout Modifiers (for nested grids)
407
+ ========================================
408
+
409
+ When you nest a grid inside another element (like inside col-feature),
410
+ the nested grid doesn't know about the parent's constraints. Use these
411
+ modifiers to "reset" the grid to fit its container.
412
+
413
+ breakout-to-content: Collapses all tracks - nested grid fills container
414
+ breakout-to-popout: Keeps popout tracks, collapses feature/full
415
+ breakout-to-feature: Keeps feature+popout tracks, collapses full
416
+
417
+ Example - Full-width hero with nested content grid:
418
+ <div class="col-full bg-blue-500">
419
+ <div class="grid-cols-breakout breakout-to-content">
420
+ <h1>This h1 fills the blue container</h1>
421
+ <p class="col-content">But content still works!</p>
422
+ </div>
423
+ </div>
424
+
425
+ Example - Feature-width card with internal grid:
426
+ <article class="col-feature">
427
+ <div class="grid-cols-breakout breakout-to-feature">
428
+ <img class="col-feature">Full width of card</img>
429
+ <p class="col-content">Padded text inside</p>
430
+ </div>
431
+ </article>
432
+ */
433
+ .grid-cols-breakout.breakout-to-content {
434
+ grid-template-columns: [full-start feature-start popout-start content-start center-start] minmax(0, 1fr) [center-end content-end popout-end feature-end full-end];
435
+ }
436
+
437
+ .grid-cols-breakout.breakout-to-popout {
438
+ grid-template-columns: [full-start feature-start popout-start] var(--popout) [content-start center-start] minmax(0, 1fr) [center-end content-end] var(--popout) [popout-end feature-end full-end];
439
+ }
440
+
441
+ .grid-cols-breakout.breakout-to-feature {
442
+ grid-template-columns: [full-start feature-start] var(--feature) [popout-start] var(--popout) [content-start center-start] minmax(0, 1fr) [center-end content-end] var(--popout) [popout-end] var(--feature) [feature-end full-end];
443
+ }
444
+
445
+ /* ----------------------------------------
446
+ Breakout None - Disable Grid
447
+ ----------------------------------------
448
+
449
+ Use when you need to escape the grid entirely. Useful for:
450
+ - Sidebar layouts where one column shouldn't use grid
451
+ - Components that manage their own layout
452
+ - CMS blocks that shouldn't inherit grid behavior
453
+
454
+ Example - Two-column layout with sidebar:
455
+ <div class="grid grid-cols-[300px_1fr]">
456
+ <aside class="breakout-none">Sidebar - no grid here</aside>
457
+ <main class="grid-cols-breakout">Main content uses grid</main>
458
+ </div>
459
+ */
460
+ .breakout-none { display: block; }
461
+ .breakout-none-flex { display: flex; }
462
+ .breakout-none-grid { display: grid; }
463
+
464
+ /* ========================================
465
+ Column Utilities - Basic
466
+ ========================================
467
+
468
+ Place elements in specific grid tracks. These are the core utilities
469
+ you'll use most often.
470
+
471
+ col-full: Edge to edge (viewport width minus gap)
472
+ col-feature: Wide content (images, videos, heroes)
473
+ col-popout: Slightly wider than content (pull quotes, callouts)
474
+ col-content: Standard reading width (articles, text)
475
+ col-center: Centered within content (rare, for precise centering)
476
+ */
477
+ .col-full { grid-column: full; }
478
+ .col-feature { grid-column: feature; }
479
+ .col-popout { grid-column: popout; }
480
+ .col-content { grid-column: content; }
481
+ .col-center { grid-column: center; }
482
+
483
+ /* Backward compatibility: col-narrow maps to content */
484
+ .col-narrow { grid-column: content; }
485
+
486
+ /* ========================================
487
+ Column Utilities - Start/End
488
+ ========================================
489
+
490
+ Fine-grained control for custom spans. Combine start and end
491
+ utilities to create any span you need.
492
+
493
+ Example - Span from popout to feature on right:
494
+ <div class="col-start-popout col-end-feature">
495
+ Custom span
496
+ </div>
497
+ */
498
+ .col-start-full { grid-column-start: full-start; }
499
+ .col-start-feature { grid-column-start: feature-start; }
500
+ .col-start-popout { grid-column-start: popout-start; }
501
+ .col-start-content { grid-column-start: content-start; }
502
+ .col-start-center { grid-column-start: center-start; }
503
+
504
+ /* Backward compatibility */
505
+ .col-start-narrow { grid-column-start: content-start; }
506
+
507
+ .col-end-full { grid-column-end: full-end; }
508
+ .col-end-feature { grid-column-end: feature-end; }
509
+ .col-end-popout { grid-column-end: popout-end; }
510
+ .col-end-content { grid-column-end: content-end; }
511
+ .col-end-center { grid-column-end: center-end; }
512
+
513
+ /* Backward compatibility */
514
+ .col-end-narrow { grid-column-end: content-end; }
515
+
516
+ /* ========================================
517
+ Column Utilities - Left/Right Spans
518
+ ========================================
519
+
520
+ Asymmetric spans that anchor to one edge. Perfect for:
521
+ - Split layouts (image left, text right)
522
+ - Overlapping elements
523
+ - Pull quotes that bleed to one edge
524
+
525
+ Pattern: col-{track}-left = full-start → {track}-end
526
+ col-{track}-right = {track}-start → full-end
527
+
528
+ Example - Image bleeds left, caption stays in content:
529
+ <figure class="col-content-left">
530
+ <img class="w-full">Spans from left edge to content</img>
531
+ </figure>
532
+
533
+ Example - Quote pulls right:
534
+ <blockquote class="col-popout-right">
535
+ Spans from popout through to right edge
536
+ </blockquote>
537
+ */
538
+ .col-feature-left { grid-column: full-start / feature-end; }
539
+ .col-feature-right { grid-column: feature-start / full-end; }
540
+ .col-popout-left { grid-column: full-start / popout-end; }
541
+ .col-popout-right { grid-column: popout-start / full-end; }
542
+ .col-content-left { grid-column: full-start / content-end; }
543
+ .col-content-right { grid-column: content-start / full-end; }
544
+ .col-center-left { grid-column: full-start / center-end; }
545
+ .col-center-right { grid-column: center-start / full-end; }
546
+
547
+ /* Backward compatibility */
548
+ .col-narrow-left { grid-column: full-start / content-end; }
549
+ .col-narrow-right { grid-column: content-start / full-end; }
550
+
551
+ /* ========================================
552
+ Column Utilities - Advanced Spans
553
+ ========================================
554
+
555
+ Partial spans between non-adjacent tracks. Use when you need
556
+ elements that span from an inner track outward but not all
557
+ the way to the edge.
558
+
559
+ Example - Card that spans feature to content (not to edge):
560
+ <div class="col-feature-to-content">
561
+ Wide but doesn't bleed to viewport edge
562
+ </div>
563
+ */
564
+ /* Feature to other columns */
565
+ .col-feature-to-popout { grid-column: feature-start / popout-end; }
566
+ .col-feature-to-content { grid-column: feature-start / content-end; }
567
+ .col-feature-to-center { grid-column: feature-start / center-end; }
568
+
569
+ /* Popout to other columns */
570
+ .col-popout-to-content { grid-column: popout-start / content-end; }
571
+ .col-popout-to-center { grid-column: popout-start / center-end; }
572
+ .col-popout-to-feature { grid-column: popout-start / feature-end; }
573
+
574
+ /* Content to other columns */
575
+ .col-content-to-center { grid-column: content-start / center-end; }
576
+ .col-content-to-popout { grid-column: content-start / popout-end; }
577
+ .col-content-to-feature { grid-column: content-start / feature-end; }
578
+
579
+ /* ----------------------------------------
580
+ Full Limit - Capped Full Width
581
+ ----------------------------------------
582
+
583
+ Goes edge-to-edge like col-full, but caps at --full-limit on
584
+ ultra-wide screens. Prevents content from becoming too wide
585
+ on large monitors while still being full-width on normal screens.
586
+
587
+ Example - Hero that doesn't get absurdly wide:
588
+ <section class="col-full-limit">
589
+ Full width up to ${c.fullLimit}, then centered
590
+ </section>
591
+ */
592
+ .col-full-limit {
593
+ grid-column: full;
594
+ width: 100%;
595
+ max-width: var(--full-limit);
596
+ margin-left: auto;
597
+ margin-right: auto;
598
+ box-sizing: border-box;
599
+ }
600
+
601
+ /* ========================================
602
+ Padding Utilities
603
+ ========================================
604
+
605
+ Match padding to grid measurements for alignment. These utilities
606
+ help content inside non-grid elements align with the grid.
607
+
608
+ --breakout-padding: Fluid padding that matches popout track behavior
609
+ --gap: Matches the outer grid gap
610
+ --computed-gap: Larger gap for full-width elements
611
+ --popout-to-content: Align edges with content track from popout
612
+ --feature-to-content: Align edges with content track from feature
613
+
614
+ Example - Full-width section with content-aligned padding:
615
+ <section class="col-full bg-gray-100 px-feature-to-content">
616
+ <p>This text aligns with content column above/below</p>
617
+ </section>
618
+
619
+ Example - Card with consistent internal spacing:
620
+ <div class="col-popout p-breakout">
621
+ Padding scales with the grid
622
+ </div>
623
+ */
624
+ .p-breakout { padding: var(--breakout-padding); }
625
+ .px-breakout { padding-left: var(--breakout-padding); padding-right: var(--breakout-padding); }
626
+ .py-breakout { padding-top: var(--breakout-padding); padding-bottom: var(--breakout-padding); }
627
+ .pl-breakout { padding-left: var(--breakout-padding); }
628
+ .pr-breakout { padding-right: var(--breakout-padding); }
629
+ .pt-breakout { padding-top: var(--breakout-padding); }
630
+ .pb-breakout { padding-bottom: var(--breakout-padding); }
631
+
632
+ /* Gap-based padding */
633
+ .p-gap { padding: var(--gap); }
634
+ .px-gap { padding-left: var(--gap); padding-right: var(--gap); }
635
+ .py-gap { padding-top: var(--gap); padding-bottom: var(--gap); }
636
+ .pl-gap { padding-left: var(--gap); }
637
+ .pr-gap { padding-right: var(--gap); }
638
+ .pt-gap { padding-top: var(--gap); }
639
+ .pb-gap { padding-bottom: var(--gap); }
640
+
641
+ /* Full-gap padding (computed, for full-width elements) */
642
+ .p-full-gap { padding: var(--computed-gap); }
643
+ .px-full-gap { padding-left: var(--computed-gap); padding-right: var(--computed-gap); }
644
+ .py-full-gap { padding-top: var(--computed-gap); padding-bottom: var(--computed-gap); }
645
+ .pl-full-gap { padding-left: var(--computed-gap); }
646
+ .pr-full-gap { padding-right: var(--computed-gap); }
647
+ .pt-full-gap { padding-top: var(--computed-gap); }
648
+ .pb-full-gap { padding-bottom: var(--computed-gap); }
649
+
650
+ /* Popout-width padding */
651
+ .p-popout { padding: var(--popout); }
652
+ .px-popout { padding-left: var(--popout); padding-right: var(--popout); }
653
+ .py-popout { padding-top: var(--popout); padding-bottom: var(--popout); }
654
+ .pl-popout { padding-left: var(--popout); }
655
+ .pr-popout { padding-right: var(--popout); }
656
+ .pt-popout { padding-top: var(--popout); }
657
+ .pb-popout { padding-bottom: var(--popout); }
658
+
659
+ /* Alignment padding - align content inside wider columns */
660
+ .p-popout-to-content { padding: var(--popout-to-content); }
661
+ .px-popout-to-content { padding-left: var(--popout-to-content); padding-right: var(--popout-to-content); }
662
+ .py-popout-to-content { padding-top: var(--popout-to-content); padding-bottom: var(--popout-to-content); }
663
+ .pt-popout-to-content { padding-top: var(--popout-to-content); }
664
+ .pr-popout-to-content { padding-right: var(--popout-to-content); }
665
+ .pb-popout-to-content { padding-bottom: var(--popout-to-content); }
666
+ .pl-popout-to-content { padding-left: var(--popout-to-content); }
667
+
668
+ .p-feature-to-content { padding: var(--feature-to-content); }
669
+ .px-feature-to-content { padding-left: var(--feature-to-content); padding-right: var(--feature-to-content); }
670
+ .py-feature-to-content { padding-top: var(--feature-to-content); padding-bottom: var(--feature-to-content); }
671
+ .pt-feature-to-content { padding-top: var(--feature-to-content); }
672
+ .pr-feature-to-content { padding-right: var(--feature-to-content); }
673
+ .pb-feature-to-content { padding-bottom: var(--feature-to-content); }
674
+ .pl-feature-to-content { padding-left: var(--feature-to-content); }
675
+
676
+ /* ========================================
677
+ Margin Utilities
678
+ ========================================
679
+
680
+ Same values as padding utilities, but for margins. Includes
681
+ negative variants for pulling elements outside their container.
682
+
683
+ Example - Pull image outside its container:
684
+ <div class="col-content">
685
+ <img class="-mx-breakout">Bleeds into popout area</img>
686
+ </div>
687
+
688
+ Example - Overlap previous section:
689
+ <section class="-mt-gap">
690
+ Pulls up into the section above
691
+ </section>
692
+ */
693
+ .m-breakout { margin: var(--breakout-padding); }
694
+ .mx-breakout { margin-left: var(--breakout-padding); margin-right: var(--breakout-padding); }
695
+ .my-breakout { margin-top: var(--breakout-padding); margin-bottom: var(--breakout-padding); }
696
+ .ml-breakout { margin-left: var(--breakout-padding); }
697
+ .mr-breakout { margin-right: var(--breakout-padding); }
698
+ .mt-breakout { margin-top: var(--breakout-padding); }
699
+ .mb-breakout { margin-bottom: var(--breakout-padding); }
700
+
701
+ /* Negative margins */
702
+ .-m-breakout { margin: calc(var(--breakout-padding) * -1); }
703
+ .-mx-breakout { margin-left: calc(var(--breakout-padding) * -1); margin-right: calc(var(--breakout-padding) * -1); }
704
+ .-my-breakout { margin-top: calc(var(--breakout-padding) * -1); margin-bottom: calc(var(--breakout-padding) * -1); }
705
+ .-ml-breakout { margin-left: calc(var(--breakout-padding) * -1); }
706
+ .-mr-breakout { margin-right: calc(var(--breakout-padding) * -1); }
707
+ .-mt-breakout { margin-top: calc(var(--breakout-padding) * -1); }
708
+ .-mb-breakout { margin-bottom: calc(var(--breakout-padding) * -1); }
709
+
710
+ /* Gap-based margins */
711
+ .m-gap { margin: var(--gap); }
712
+ .mx-gap { margin-left: var(--gap); margin-right: var(--gap); }
713
+ .my-gap { margin-top: var(--gap); margin-bottom: var(--gap); }
714
+ .ml-gap { margin-left: var(--gap); }
715
+ .mr-gap { margin-right: var(--gap); }
716
+ .mt-gap { margin-top: var(--gap); }
717
+ .mb-gap { margin-bottom: var(--gap); }
718
+
719
+ /* Negative margins */
720
+ .-m-gap { margin: calc(var(--gap) * -1); }
721
+ .-mx-gap { margin-left: calc(var(--gap) * -1); margin-right: calc(var(--gap) * -1); }
722
+ .-my-gap { margin-top: calc(var(--gap) * -1); margin-bottom: calc(var(--gap) * -1); }
723
+ .-ml-gap { margin-left: calc(var(--gap) * -1); }
724
+ .-mr-gap { margin-right: calc(var(--gap) * -1); }
725
+ .-mt-gap { margin-top: calc(var(--gap) * -1); }
726
+ .-mb-gap { margin-bottom: calc(var(--gap) * -1); }
727
+
728
+ /* Full-gap margins */
729
+ .m-full-gap { margin: var(--computed-gap); }
730
+ .mx-full-gap { margin-left: var(--computed-gap); margin-right: var(--computed-gap); }
731
+ .my-full-gap { margin-top: var(--computed-gap); margin-bottom: var(--computed-gap); }
732
+ .ml-full-gap { margin-left: var(--computed-gap); }
733
+ .mr-full-gap { margin-right: var(--computed-gap); }
734
+ .mt-full-gap { margin-top: var(--computed-gap); }
735
+ .mb-full-gap { margin-bottom: var(--computed-gap); }
736
+
737
+ /* Negative margins */
738
+ .-m-full-gap { margin: calc(var(--computed-gap) * -1); }
739
+ .-mx-full-gap { margin-left: calc(var(--computed-gap) * -1); margin-right: calc(var(--computed-gap) * -1); }
740
+ .-my-full-gap { margin-top: calc(var(--computed-gap) * -1); margin-bottom: calc(var(--computed-gap) * -1); }
741
+ .-ml-full-gap { margin-left: calc(var(--computed-gap) * -1); }
742
+ .-mr-full-gap { margin-right: calc(var(--computed-gap) * -1); }
743
+ .-mt-full-gap { margin-top: calc(var(--computed-gap) * -1); }
744
+ .-mb-full-gap { margin-bottom: calc(var(--computed-gap) * -1); }
745
+
746
+ /* Popout-width margins */
747
+ .m-popout { margin: var(--popout); }
748
+ .mx-popout { margin-left: var(--popout); margin-right: var(--popout); }
749
+ .my-popout { margin-top: var(--popout); margin-bottom: var(--popout); }
750
+ .ml-popout { margin-left: var(--popout); }
751
+ .mr-popout { margin-right: var(--popout); }
752
+ .mt-popout { margin-top: var(--popout); }
753
+ .mb-popout { margin-bottom: var(--popout); }
754
+
755
+ /* Negative margins */
756
+ .-m-popout { margin: calc(var(--popout) * -1); }
757
+ .-mx-popout { margin-left: calc(var(--popout) * -1); margin-right: calc(var(--popout) * -1); }
758
+ .-my-popout { margin-top: calc(var(--popout) * -1); margin-bottom: calc(var(--popout) * -1); }
759
+ .-ml-popout { margin-left: calc(var(--popout) * -1); }
760
+ .-mr-popout { margin-right: calc(var(--popout) * -1); }
761
+ .-mt-popout { margin-top: calc(var(--popout) * -1); }
762
+ .-mb-popout { margin-bottom: calc(var(--popout) * -1); }
763
+ `;
764
+ }
765
+ const methods = {
766
+ // Initialize
767
+ init() {
768
+ const saved = localStorage.getItem("breakoutGridVisualizerVisible");
769
+ if (saved !== null) {
770
+ this.isVisible = saved === "true";
771
+ }
772
+ const editorOpen = localStorage.getItem("breakoutGridEditorOpen");
773
+ if (editorOpen === "true") {
774
+ this.showEditor = true;
775
+ this.editMode = true;
776
+ this.$nextTick(() => this.loadCurrentValues());
777
+ }
778
+ const editorPos = localStorage.getItem("breakoutGridEditorPos");
779
+ if (editorPos) {
780
+ try {
781
+ this.editorPos = JSON.parse(editorPos);
782
+ } catch (e) {
783
+ }
784
+ }
785
+ const spacingPos = localStorage.getItem("breakoutGridSpacingPos");
786
+ if (spacingPos) {
787
+ try {
788
+ this.spacingPanelPos = JSON.parse(spacingPos);
789
+ } catch (e) {
790
+ }
791
+ }
792
+ const spacingCollapsed = localStorage.getItem("breakoutGridSpacingCollapsed");
793
+ if (spacingCollapsed !== null) {
794
+ this.spacingPanelCollapsed = spacingCollapsed === "true";
795
+ }
796
+ window.addEventListener("keydown", (e) => {
797
+ if ((e.ctrlKey || e.metaKey) && e.key === "g") {
798
+ e.preventDefault();
799
+ this.toggle();
800
+ }
801
+ });
802
+ window.addEventListener("resize", () => {
803
+ this.viewportWidth = window.innerWidth;
804
+ this.updateColumnWidths();
805
+ this.updateCurrentBreakpoint();
806
+ if (this.editMode) {
807
+ this.updateGapLive();
808
+ }
809
+ });
810
+ this.updateCurrentBreakpoint();
811
+ console.log("Breakout Grid Visualizer loaded. Press Ctrl/Cmd + G to toggle.");
812
+ },
813
+ // Toggle visibility
814
+ toggle() {
815
+ this.isVisible = !this.isVisible;
816
+ localStorage.setItem("breakoutGridVisualizerVisible", this.isVisible);
817
+ },
818
+ // Update column widths by querying DOM elements
819
+ updateColumnWidths() {
820
+ this.$nextTick(() => {
821
+ this.gridAreas.forEach((area) => {
822
+ const el = document.querySelector(`.breakout-visualizer-grid .col-${area.name}`);
823
+ if (el) {
824
+ this.columnWidths[area.name] = Math.round(el.getBoundingClientRect().width);
825
+ }
826
+ });
827
+ });
828
+ },
829
+ // Detect current breakpoint based on viewport width
830
+ updateCurrentBreakpoint() {
831
+ const width = window.innerWidth;
832
+ if (width >= 1280) {
833
+ this.currentBreakpoint = "xl";
834
+ } else if (width >= 1024) {
835
+ this.currentBreakpoint = "lg";
836
+ } else {
837
+ this.currentBreakpoint = "mobile";
838
+ }
839
+ },
840
+ // Update --gap live based on current breakpoint and edit values
841
+ updateGapLive() {
842
+ const scaleKey = this.currentBreakpoint === "mobile" ? "default" : this.currentBreakpoint;
843
+ const base = this.editValues.baseGap || this.configOptions.baseGap.value;
844
+ const max = this.editValues.maxGap || this.configOptions.maxGap.value;
845
+ const scale = this.editValues[`gapScale_${scaleKey}`] || this.gapScaleOptions[scaleKey].value;
846
+ document.documentElement.style.setProperty("--gap", `clamp(${base}, ${scale}, ${max})`);
847
+ this.updateColumnWidths();
848
+ },
849
+ // Check if content width exceeds comfortable reading width (55rem)
850
+ getContentReadabilityWarning() {
851
+ const contentMax = parseFloat(this.editValues.contentMax || this.configOptions.contentMax.value);
852
+ if (contentMax > 55) {
853
+ return `Content max (${contentMax}rem) exceeds 55rem—may be wide for reading. Ideal for prose: 45–55rem.`;
854
+ }
855
+ return null;
856
+ },
857
+ // Check if configured track widths would exceed viewport
858
+ getTrackOverflowWarning() {
859
+ const contentMax = parseFloat(this.editValues.contentMax || this.configOptions.contentMax.value) * 16;
860
+ const featureMax = parseFloat(this.editValues.featureMax || this.configOptions.featureMax.value) * 16;
861
+ const popoutWidth = parseFloat(this.editValues.popoutWidth || this.configOptions.popoutWidth.value) * 16;
862
+ const featurePx = featureMax * 2;
863
+ const popoutPx = popoutWidth * 2;
864
+ const totalFixed = contentMax + featurePx + popoutPx;
865
+ if (totalFixed > this.viewportWidth) {
866
+ return `Tracks exceed viewport by ~${Math.round(totalFixed - this.viewportWidth)}px — outer columns will compress`;
867
+ }
868
+ return null;
869
+ },
870
+ // Get computed CSS variable value
871
+ getCSSVariable(varName) {
872
+ const value = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
873
+ return value || "Not set";
874
+ },
875
+ // Helper to load options from CSS variables
876
+ loadOptionsFromCSS(options, prefix = "") {
877
+ Object.keys(options).forEach((key) => {
878
+ const opt = options[key];
879
+ const editKey = prefix ? `${prefix}_${key}` : key;
880
+ if (opt.cssVar) {
881
+ const computed = this.getCSSVariable(opt.cssVar);
882
+ this.editValues[editKey] = computed && computed !== "Not set" && computed !== "" ? computed : opt.value;
883
+ } else {
884
+ this.editValues[editKey] = opt.value;
885
+ }
886
+ });
887
+ },
888
+ // Load current values from CSS variables where available
889
+ loadCurrentValues() {
890
+ this.loadOptionsFromCSS(this.configOptions);
891
+ this.loadOptionsFromCSS(this.gapScaleOptions, "gapScale");
892
+ this.loadOptionsFromCSS(this.breakoutOptions, "breakout");
893
+ },
894
+ // Generate export config object
895
+ generateConfigExport() {
896
+ var _a, _b, _c, _d;
897
+ const config = {};
898
+ Object.keys(this.configOptions).forEach((key) => {
899
+ config[key] = this.editValues[key] || this.configOptions[key].value;
900
+ });
901
+ config.gapScale = {};
902
+ Object.keys(this.gapScaleOptions).forEach((key) => {
903
+ config.gapScale[key] = this.editValues[`gapScale_${key}`] || this.gapScaleOptions[key].value;
904
+ });
905
+ config.breakoutMin = this.editValues.breakout_min || this.breakoutOptions.min.value;
906
+ config.breakoutScale = this.editValues.breakout_scale || this.breakoutOptions.scale.value;
907
+ config.breakpoints = {
908
+ lg: this.editValues.breakpoint_lg || ((_b = (_a = this.breakpointOptions) == null ? void 0 : _a.lg) == null ? void 0 : _b.value) || "1024",
909
+ xl: this.editValues.breakpoint_xl || ((_d = (_c = this.breakpointOptions) == null ? void 0 : _c.xl) == null ? void 0 : _d.value) || "1280"
910
+ };
911
+ return config;
912
+ },
913
+ // Format config object with single quotes for values, no quotes for keys
914
+ formatConfig(obj, indent = 2) {
915
+ const pad = " ".repeat(indent);
916
+ const lines = ["{"];
917
+ const entries = Object.entries(obj);
918
+ entries.forEach(([key, value], i) => {
919
+ const comma = i < entries.length - 1 ? "," : "";
920
+ if (typeof value === "object" && value !== null) {
921
+ lines.push(`${pad}${key}: ${this.formatConfig(value, indent + 2).replace(/\n/g, "\n" + pad)}${comma}`);
922
+ } else {
923
+ lines.push(`${pad}${key}: '${value}'${comma}`);
924
+ }
925
+ });
926
+ lines.push("}");
927
+ return lines.join("\n");
928
+ },
929
+ // Section definitions for partial copying
930
+ configSections: {
931
+ content: {
932
+ keys: ["contentMin", "contentBase", "contentMax"],
933
+ label: "Content"
934
+ },
935
+ defaultCol: {
936
+ keys: ["defaultCol"],
937
+ label: "Default Column"
938
+ },
939
+ tracks: {
940
+ keys: ["popoutWidth", "fullLimit"],
941
+ label: "Track Widths"
942
+ },
943
+ feature: {
944
+ keys: ["featureMin", "featureScale", "featureMax"],
945
+ label: "Feature"
946
+ },
947
+ gap: {
948
+ keys: ["baseGap", "maxGap"],
949
+ nested: { gapScale: ["default", "lg", "xl"] },
950
+ label: "Gap"
951
+ },
952
+ breakout: {
953
+ keys: ["breakoutMin", "breakoutScale"],
954
+ label: "Breakout"
955
+ }
956
+ },
957
+ // Copy a specific section to clipboard
958
+ copySection(sectionName) {
959
+ const section = this.configSections[sectionName];
960
+ if (!section) return;
961
+ const config = {};
962
+ section.keys.forEach((key) => {
963
+ if (this.configOptions[key]) {
964
+ config[key] = this.editValues[key] || this.configOptions[key].value;
965
+ } else if (key === "breakoutMin") {
966
+ config[key] = this.editValues.breakout_min || this.breakoutOptions.min.value;
967
+ } else if (key === "breakoutScale") {
968
+ config[key] = this.editValues.breakout_scale || this.breakoutOptions.scale.value;
969
+ }
970
+ });
971
+ if (section.nested) {
972
+ Object.keys(section.nested).forEach((nestedKey) => {
973
+ config[nestedKey] = {};
974
+ section.nested[nestedKey].forEach((subKey) => {
975
+ config[nestedKey][subKey] = this.editValues[`gapScale_${subKey}`] || this.gapScaleOptions[subKey].value;
976
+ });
977
+ });
978
+ }
979
+ const configStr = this.formatConfigFlat(config);
980
+ navigator.clipboard.writeText(configStr).then(() => {
981
+ this.sectionCopied = sectionName;
982
+ setTimeout(() => this.sectionCopied = null, 1500);
983
+ });
984
+ },
985
+ // Format config as flat key-value pairs (no wrapping braces)
986
+ formatConfigFlat(obj) {
987
+ const lines = [];
988
+ const entries = Object.entries(obj);
989
+ entries.forEach(([key, value], i) => {
990
+ const comma = i < entries.length - 1 ? "," : ",";
991
+ if (typeof value === "object" && value !== null) {
992
+ lines.push(`${key}: ${this.formatConfig(value)}${comma}`);
993
+ } else {
994
+ lines.push(`${key}: '${value}'${comma}`);
995
+ }
996
+ });
997
+ return lines.join("\n");
998
+ },
999
+ // Copy config to clipboard as CSS variables
1000
+ copyConfig() {
1001
+ var _a, _b, _c, _d, _e;
1002
+ const config = this.generateConfigExport();
1003
+ const lines = [
1004
+ ":root {",
1005
+ ` /* Content (text width) */`,
1006
+ ` --content-min: ${config.contentMin};`,
1007
+ ` --content-base: ${config.contentBase};`,
1008
+ ` --content-max: ${config.contentMax};`,
1009
+ ` /* Default column */`,
1010
+ ` --default-col: ${config.defaultCol || "content"};`,
1011
+ ` /* Track widths */`,
1012
+ ` --popout-width: ${config.popoutWidth};`,
1013
+ ` --full-limit: ${config.fullLimit};`,
1014
+ ` /* Feature track */`,
1015
+ ` --feature-min: ${config.featureMin};`,
1016
+ ` --feature-scale: ${config.featureScale};`,
1017
+ ` --feature-max: ${config.featureMax};`,
1018
+ ` /* Outer margins */`,
1019
+ ` --base-gap: ${config.baseGap};`,
1020
+ ` --max-gap: ${config.maxGap};`,
1021
+ ` /* Responsive scale */`,
1022
+ ` --gap-scale-default: ${((_a = config.gapScale) == null ? void 0 : _a.default) || "4vw"};`,
1023
+ ` --gap-scale-lg: ${((_b = config.gapScale) == null ? void 0 : _b.lg) || "5vw"};`,
1024
+ ` --gap-scale-xl: ${((_c = config.gapScale) == null ? void 0 : _c.xl) || "6vw"};`,
1025
+ ` /* Breakout padding */`,
1026
+ ` --breakout-min: ${config.breakoutMin || "1rem"};`,
1027
+ ` --breakout-scale: ${config.breakoutScale || "5vw"};`,
1028
+ ` /* Breakpoints */`,
1029
+ ` /* --breakpoint-lg: ${((_d = config.breakpoints) == null ? void 0 : _d.lg) || "1024"}px; */`,
1030
+ ` /* --breakpoint-xl: ${((_e = config.breakpoints) == null ? void 0 : _e.xl) || "1280"}px; */`,
1031
+ "}"
1032
+ ];
1033
+ const configStr = lines.join("\n");
1034
+ navigator.clipboard.writeText(configStr).then(() => {
1035
+ this.copySuccess = true;
1036
+ this.configCopied = true;
1037
+ setTimeout(() => this.copySuccess = false, 2e3);
1038
+ });
1039
+ },
1040
+ // Generate and download standalone CSS file
1041
+ downloadCSS() {
1042
+ const css = this.generateCSSExport(this.generateConfigExport());
1043
+ const blob = new Blob([css], { type: "text/css" });
1044
+ const url = URL.createObjectURL(blob);
1045
+ const a = document.createElement("a");
1046
+ a.href = url;
1047
+ a.download = `_objects.breakout-grid.css`;
1048
+ a.click();
1049
+ URL.revokeObjectURL(url);
1050
+ },
1051
+ // Parse CSS value into number and unit (e.g., "4rem" -> { num: 4, unit: "rem" })
1052
+ parseValue(val) {
1053
+ const match = String(val).match(/^([\d.]+)(.*)$/);
1054
+ if (match) {
1055
+ return { num: parseFloat(match[1]), unit: match[2] || "rem" };
1056
+ }
1057
+ return { num: 0, unit: "rem" };
1058
+ },
1059
+ // Get the numeric part of a config value
1060
+ getNumericValue(key) {
1061
+ const val = this.editValues[key] || this.configOptions[key].value;
1062
+ return this.parseValue(val).num;
1063
+ },
1064
+ // Get the unit part of a config value
1065
+ getUnit(key) {
1066
+ const val = this.editValues[key] || this.configOptions[key].value;
1067
+ return this.parseValue(val).unit;
1068
+ },
1069
+ // Check if a field should have unit selection (rem-based fields only)
1070
+ hasUnitSelector(key) {
1071
+ const unit = this.getUnit(key);
1072
+ return unit === "rem" || unit === "ch" || unit === "px";
1073
+ },
1074
+ // Available units for selection
1075
+ unitOptions: ["rem", "ch", "px"],
1076
+ // Update just the unit, keeping the numeric value
1077
+ updateUnit(key, newUnit) {
1078
+ const num = this.getNumericValue(key);
1079
+ this.updateConfigValue(key, num + newUnit);
1080
+ },
1081
+ // Update just the numeric part, keeping the unit
1082
+ updateNumericValue(key, num) {
1083
+ if (key === "content" && num < 1) num = 1;
1084
+ if (key === "baseGap" && num < 0) num = 0;
1085
+ if (key === "popoutWidth" && num < 0) num = 0;
1086
+ if ((key === "featureMin" || key === "featureScale" || key === "featureMax") && num < 0) num = 0;
1087
+ const unit = this.getUnit(key);
1088
+ this.updateConfigValue(key, num + unit);
1089
+ },
1090
+ // Generic getter for prefixed options (gapScale, breakout)
1091
+ getPrefixedNumeric(prefix, options, key) {
1092
+ const val = this.editValues[`${prefix}_${key}`] || options[key].value;
1093
+ return this.parseValue(val).num;
1094
+ },
1095
+ getPrefixedUnit(prefix, options, key) {
1096
+ const val = this.editValues[`${prefix}_${key}`] || options[key].value;
1097
+ return this.parseValue(val).unit;
1098
+ },
1099
+ // Gap scale helpers (use generic)
1100
+ getGapScaleNumeric(key) {
1101
+ return this.getPrefixedNumeric("gapScale", this.gapScaleOptions, key);
1102
+ },
1103
+ getGapScaleUnit(key) {
1104
+ return this.getPrefixedUnit("gapScale", this.gapScaleOptions, key);
1105
+ },
1106
+ updateGapScaleNumeric(key, num) {
1107
+ this.editValues[`gapScale_${key}`] = num + this.getGapScaleUnit(key);
1108
+ this.configCopied = false;
1109
+ this.updateGapLive();
1110
+ },
1111
+ // Breakout helpers (use generic)
1112
+ getBreakoutNumeric(key) {
1113
+ return this.getPrefixedNumeric("breakout", this.breakoutOptions, key);
1114
+ },
1115
+ getBreakoutUnit(key) {
1116
+ return this.getPrefixedUnit("breakout", this.breakoutOptions, key);
1117
+ },
1118
+ updateBreakoutNumeric(key, num) {
1119
+ this.editValues[`breakout_${key}`] = num + this.getBreakoutUnit(key);
1120
+ this.configCopied = false;
1121
+ this.updateBreakoutLive();
1122
+ },
1123
+ // Update --breakout-padding live
1124
+ updateBreakoutLive() {
1125
+ const min = this.editValues.breakout_min || this.breakoutOptions.min.value;
1126
+ const scale = this.editValues.breakout_scale || this.breakoutOptions.scale.value;
1127
+ const max = this.editValues.popoutWidth || this.configOptions.popoutWidth.value;
1128
+ document.documentElement.style.setProperty("--breakout-padding", `clamp(${min}, ${scale}, ${max})`);
1129
+ },
1130
+ // Update a config value (and live CSS var if applicable)
1131
+ updateConfigValue(key, value) {
1132
+ this.editValues[key] = value;
1133
+ this.configCopied = false;
1134
+ const opt = this.configOptions[key];
1135
+ if (opt && opt.liveVar) {
1136
+ document.documentElement.style.setProperty(opt.liveVar, value);
1137
+ }
1138
+ if (key === "popoutWidth") {
1139
+ document.documentElement.style.setProperty("--popout", `minmax(0, ${value})`);
1140
+ this.updateBreakoutLive();
1141
+ }
1142
+ if (key === "featureMin" || key === "featureScale" || key === "featureMax") {
1143
+ const featureMin = this.editValues.featureMin || this.configOptions.featureMin.value;
1144
+ const featureScale = this.editValues.featureScale || this.configOptions.featureScale.value;
1145
+ const featureMax = this.editValues.featureMax || this.configOptions.featureMax.value;
1146
+ document.documentElement.style.setProperty("--feature", `minmax(0, clamp(${featureMin}, ${featureScale}, ${featureMax}))`);
1147
+ }
1148
+ if (key === "content") {
1149
+ document.documentElement.style.setProperty("--content", `minmax(0, ${value})`);
1150
+ }
1151
+ if (key === "baseGap" || key === "maxGap") {
1152
+ this.updateGapLive();
1153
+ }
1154
+ },
1155
+ // Select a grid area
1156
+ selectArea(areaName) {
1157
+ this.selectedArea = this.selectedArea === areaName ? null : areaName;
1158
+ },
1159
+ // Check if area is selected
1160
+ isSelected(areaName) {
1161
+ return this.selectedArea === areaName;
1162
+ },
1163
+ // Restore all CSS variable overrides to original values
1164
+ restoreCSSVariables() {
1165
+ Object.keys(this.configOptions).forEach((key) => {
1166
+ const opt = this.configOptions[key];
1167
+ if (opt.liveVar) {
1168
+ document.documentElement.style.removeProperty(opt.liveVar);
1169
+ }
1170
+ });
1171
+ document.documentElement.style.removeProperty("--popout");
1172
+ document.documentElement.style.removeProperty("--feature");
1173
+ document.documentElement.style.removeProperty("--content");
1174
+ document.documentElement.style.removeProperty("--breakout-padding");
1175
+ document.documentElement.style.removeProperty("--popout-to-content");
1176
+ this.editValues = {};
1177
+ this.configCopied = false;
1178
+ },
1179
+ // Toggle edit mode
1180
+ toggleEditMode() {
1181
+ this.editMode = !this.editMode;
1182
+ if (this.editMode) {
1183
+ this.loadCurrentValues();
1184
+ } else {
1185
+ this.restoreCSSVariables();
1186
+ }
1187
+ },
1188
+ // Check if any values have been edited and not yet copied
1189
+ hasUnsavedEdits() {
1190
+ return Object.keys(this.editValues).length > 0 && !this.configCopied;
1191
+ },
1192
+ // Open floating editor
1193
+ openEditor() {
1194
+ this.showEditor = true;
1195
+ this.editMode = true;
1196
+ this.loadCurrentValues();
1197
+ localStorage.setItem("breakoutGridEditorOpen", "true");
1198
+ },
1199
+ // Close floating editor
1200
+ closeEditor(force = false) {
1201
+ if (!force && this.hasUnsavedEdits()) {
1202
+ if (!confirm("You have unsaved config changes. Close without copying?")) {
1203
+ return;
1204
+ }
1205
+ }
1206
+ this.showEditor = false;
1207
+ this.editMode = false;
1208
+ this.restoreCSSVariables();
1209
+ localStorage.setItem("breakoutGridEditorOpen", "false");
1210
+ },
1211
+ // Generic drag handling for panels
1212
+ _dragConfigs: {
1213
+ editor: { pos: "editorPos", dragging: "isDragging", offset: "dragOffset", storage: "breakoutGridEditorPos" },
1214
+ spacing: { pos: "spacingPanelPos", dragging: "isDraggingSpacing", offset: "dragOffsetSpacing", storage: "breakoutGridSpacingPos" }
1215
+ },
1216
+ startPanelDrag(e, panel) {
1217
+ const cfg = this._dragConfigs[panel];
1218
+ this[cfg.dragging] = true;
1219
+ this[cfg.offset] = { x: e.clientX - this[cfg.pos].x, y: e.clientY - this[cfg.pos].y };
1220
+ },
1221
+ onPanelDrag(e, panel) {
1222
+ const cfg = this._dragConfigs[panel];
1223
+ if (this[cfg.dragging]) {
1224
+ this[cfg.pos] = { x: e.clientX - this[cfg.offset].x, y: e.clientY - this[cfg.offset].y };
1225
+ }
1226
+ },
1227
+ stopPanelDrag(panel) {
1228
+ const cfg = this._dragConfigs[panel];
1229
+ if (this[cfg.dragging]) localStorage.setItem(cfg.storage, JSON.stringify(this[cfg.pos]));
1230
+ this[cfg.dragging] = false;
1231
+ },
1232
+ // Editor drag (shorthand)
1233
+ startDrag(e) {
1234
+ this.startPanelDrag(e, "editor");
1235
+ },
1236
+ onDrag(e) {
1237
+ this.onPanelDrag(e, "editor");
1238
+ },
1239
+ stopDrag() {
1240
+ this.stopPanelDrag("editor");
1241
+ },
1242
+ // Spacing drag (shorthand)
1243
+ startDragSpacing(e) {
1244
+ this.startPanelDrag(e, "spacing");
1245
+ },
1246
+ onDragSpacing(e) {
1247
+ this.onPanelDrag(e, "spacing");
1248
+ },
1249
+ stopDragSpacing() {
1250
+ this.stopPanelDrag("spacing");
1251
+ },
1252
+ // Column resize drag handling
1253
+ startColumnResize(e, columnType) {
1254
+ if (!this.editMode) return;
1255
+ e.preventDefault();
1256
+ e.stopPropagation();
1257
+ this.resizingColumn = columnType;
1258
+ this.resizeStartX = e.clientX;
1259
+ const currentVal = this.editValues[columnType] || this.configOptions[columnType].value;
1260
+ this.resizeStartValue = this.parseValue(currentVal).num;
1261
+ },
1262
+ onColumnResize(e) {
1263
+ if (!this.resizingColumn) return;
1264
+ const deltaX = e.clientX - this.resizeStartX;
1265
+ const col = this.resizingColumn;
1266
+ const unit = this.getUnit(col);
1267
+ let pxPerUnit;
1268
+ if (unit === "vw") {
1269
+ pxPerUnit = window.innerWidth / 100;
1270
+ } else if (unit === "rem") {
1271
+ pxPerUnit = parseFloat(getComputedStyle(document.documentElement).fontSize);
1272
+ } else {
1273
+ pxPerUnit = 1;
1274
+ }
1275
+ const isRightHandle = col === "contentMax" || col === "contentBase";
1276
+ const delta = isRightHandle ? deltaX / pxPerUnit : -deltaX / pxPerUnit;
1277
+ let newValue = this.resizeStartValue + delta;
1278
+ if (newValue < 0) newValue = 0;
1279
+ newValue = Math.round(newValue * 10) / 10;
1280
+ this.updateConfigValue(col, newValue + unit);
1281
+ this.updateColumnWidths();
1282
+ },
1283
+ stopColumnResize() {
1284
+ this.resizingColumn = null;
1285
+ },
1286
+ // Map column names to their config keys for resizing
1287
+ getResizeConfig(colName) {
1288
+ const map = {
1289
+ "full-limit": "fullLimit",
1290
+ "feature": "featureScale",
1291
+ "popout": "popoutWidth"
1292
+ // content has its own integrated handles for min/max/base
1293
+ // feature has its own integrated handles for min/scale/max
1294
+ };
1295
+ return map[colName] || null;
1296
+ },
1297
+ // Parse a CSS variables string into a config object
1298
+ parseConfigString(input) {
1299
+ const str = input.trim();
1300
+ const config = { gapScale: {}, breakpoints: {} };
1301
+ const varMap = {
1302
+ "--base-gap": "baseGap",
1303
+ "--max-gap": "maxGap",
1304
+ "--content-min": "contentMin",
1305
+ "--content-max": "contentMax",
1306
+ "--content-base": "contentBase",
1307
+ "--popout-width": "popoutWidth",
1308
+ "--feature-min": "featureMin",
1309
+ "--feature-scale": "featureScale",
1310
+ "--feature-max": "featureMax",
1311
+ "--full-limit": "fullLimit",
1312
+ "--breakout-min": "breakoutMin",
1313
+ "--breakout-scale": "breakoutScale",
1314
+ "--default-col": "defaultCol"
1315
+ };
1316
+ const gapScaleMap = {
1317
+ "--gap-scale-default": "default",
1318
+ "--gap-scale-lg": "lg",
1319
+ "--gap-scale-xl": "xl"
1320
+ };
1321
+ const breakpointMap = {
1322
+ "--breakpoint-lg": "lg",
1323
+ "--breakpoint-xl": "xl"
1324
+ };
1325
+ const varRegex = /(?:\/\*\s*)?(--[\w-]+)\s*:\s*([^;*]+);?\s*(?:\*\/)?/g;
1326
+ let match;
1327
+ let foundAny = false;
1328
+ while ((match = varRegex.exec(str)) !== null) {
1329
+ const [, varName, value] = match;
1330
+ let trimmedValue = value.trim();
1331
+ foundAny = true;
1332
+ if (varMap[varName]) {
1333
+ config[varMap[varName]] = trimmedValue;
1334
+ } else if (gapScaleMap[varName]) {
1335
+ config.gapScale[gapScaleMap[varName]] = trimmedValue;
1336
+ } else if (breakpointMap[varName]) {
1337
+ config.breakpoints[breakpointMap[varName]] = trimmedValue.replace(/px$/, "");
1338
+ }
1339
+ }
1340
+ if (!foundAny) {
1341
+ throw new Error('Invalid format. Paste CSS variables from "Copy Variables".');
1342
+ }
1343
+ return config;
1344
+ },
1345
+ // Open restore modal
1346
+ openRestoreModal() {
1347
+ this.showRestoreModal = true;
1348
+ this.restoreInput = "";
1349
+ this.restoreError = null;
1350
+ },
1351
+ // Close restore modal
1352
+ closeRestoreModal() {
1353
+ this.showRestoreModal = false;
1354
+ this.restoreInput = "";
1355
+ this.restoreError = null;
1356
+ },
1357
+ // Apply a parsed config to the editor
1358
+ restoreConfig() {
1359
+ this.restoreError = null;
1360
+ try {
1361
+ const config = this.parseConfigString(this.restoreInput);
1362
+ Object.keys(this.configOptions).forEach((key) => {
1363
+ if (config[key] !== void 0) {
1364
+ this.editValues[key] = config[key];
1365
+ this.updateConfigValue(key, config[key]);
1366
+ }
1367
+ });
1368
+ if (config.gapScale) {
1369
+ Object.keys(this.gapScaleOptions).forEach((key) => {
1370
+ if (config.gapScale[key] !== void 0) {
1371
+ this.editValues[`gapScale_${key}`] = config.gapScale[key];
1372
+ }
1373
+ });
1374
+ this.updateGapLive();
1375
+ }
1376
+ if (config.breakoutMin !== void 0) {
1377
+ this.editValues.breakout_min = config.breakoutMin;
1378
+ }
1379
+ if (config.breakoutScale !== void 0) {
1380
+ this.editValues.breakout_scale = config.breakoutScale;
1381
+ }
1382
+ this.updateBreakoutLive();
1383
+ if (config.breakpoints) {
1384
+ if (config.breakpoints.lg !== void 0) {
1385
+ this.editValues.breakpoint_lg = config.breakpoints.lg;
1386
+ }
1387
+ if (config.breakpoints.xl !== void 0) {
1388
+ this.editValues.breakpoint_xl = config.breakpoints.xl;
1389
+ }
1390
+ }
1391
+ this.updateColumnWidths();
1392
+ this.closeRestoreModal();
1393
+ this.configCopied = false;
1394
+ } catch (e) {
1395
+ this.restoreError = e.message;
1396
+ }
1397
+ }
1398
+ };
1399
+ const template = `
1400
+ <div x-show="isVisible"
1401
+ x-transition
1402
+ class="breakout-grid-visualizer"
1403
+ @mousemove.window="onColumnResize($event)"
1404
+ @mouseup.window="stopColumnResize()"
1405
+ style="position: fixed; inset: 0; pointer-events: none; z-index: 9999;">
1406
+
1407
+ <!-- Edit Mode Backdrop - fades page content -->
1408
+ <div x-show="editMode"
1409
+ x-transition:enter="transition ease-out duration-200"
1410
+ x-transition:enter-start="opacity-0"
1411
+ x-transition:enter-end="opacity-100"
1412
+ x-transition:leave="transition ease-in duration-150"
1413
+ x-transition:leave-start="opacity-100"
1414
+ x-transition:leave-end="opacity-0"
1415
+ style="position: absolute; inset: 0; background: rgba(255, 255, 255, 0.85); z-index: 1;"></div>
1416
+
1417
+ <!-- Advanced Span Examples Overlay -->
1418
+ <div x-show="showAdvanced"
1419
+ class="grid-cols-breakout"
1420
+ style="position: absolute; inset: 0; height: 100%; pointer-events: auto; z-index: 5;">
1421
+
1422
+ <!-- Left-anchored: full-start to feature-end -->
1423
+ <div x-data="{ hovered: false }"
1424
+ @mouseenter="hovered = true"
1425
+ @mouseleave="hovered = false"
1426
+ :style="{
1427
+ gridColumn: 'full-start / feature-end',
1428
+ background: hovered ? 'linear-gradient(135deg, rgba(236, 72, 153, 0.6) 0%, rgba(139, 92, 246, 0.6) 100%)' : 'linear-gradient(135deg, rgba(236, 72, 153, 0.25) 0%, rgba(139, 92, 246, 0.25) 100%)',
1429
+ border: '3px solid rgb(168, 85, 247)',
1430
+ margin: '1rem 0',
1431
+ padding: '1rem',
1432
+ display: 'flex',
1433
+ alignItems: 'center',
1434
+ justifyContent: 'flex-start',
1435
+ transition: 'background 0.2s ease'
1436
+ }">
1437
+ <div style="background: rgb(139, 92, 246);
1438
+ color: white;
1439
+ padding: 0.75rem 1rem;
1440
+ font-size: 0.75rem;
1441
+ font-weight: 700;
1442
+ text-align: left;
1443
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
1444
+ <div style="font-family: monospace; margin-bottom: 0.25rem;">.col-feature-left</div>
1445
+ <div style="font-size: 0.625rem; opacity: 0.9; font-weight: 500;">Left edge → feature boundary</div>
1446
+ </div>
1447
+ </div>
1448
+
1449
+ <!-- Right-anchored: feature-start to full-end -->
1450
+ <div x-data="{ hovered: false }"
1451
+ @mouseenter="hovered = true"
1452
+ @mouseleave="hovered = false"
1453
+ :style="{
1454
+ gridColumn: 'feature-start / full-end',
1455
+ background: hovered ? 'linear-gradient(135deg, rgba(34, 197, 94, 0.6) 0%, rgba(59, 130, 246, 0.6) 100%)' : 'linear-gradient(135deg, rgba(34, 197, 94, 0.25) 0%, rgba(59, 130, 246, 0.25) 100%)',
1456
+ border: '3px solid rgb(34, 197, 94)',
1457
+ margin: '1rem 0',
1458
+ padding: '1rem',
1459
+ display: 'flex',
1460
+ alignItems: 'center',
1461
+ justifyContent: 'flex-end',
1462
+ transition: 'background 0.2s ease'
1463
+ }">
1464
+ <div style="background: rgb(34, 197, 94);
1465
+ color: white;
1466
+ padding: 0.75rem 1rem;
1467
+ font-size: 0.75rem;
1468
+ font-weight: 700;
1469
+ text-align: right;
1470
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
1471
+ <div style="font-family: monospace; margin-bottom: 0.25rem;">.col-feature-right</div>
1472
+ <div style="font-size: 0.625rem; opacity: 0.9; font-weight: 500;">Feature boundary → right edge</div>
1473
+ </div>
1474
+ </div>
1475
+
1476
+ <!-- To center point: full-start to center-end -->
1477
+ <div x-data="{ hovered: false }"
1478
+ @mouseenter="hovered = true"
1479
+ @mouseleave="hovered = false"
1480
+ :style="{
1481
+ gridColumn: 'full-start / center-end',
1482
+ background: hovered ? 'linear-gradient(135deg, rgba(251, 146, 60, 0.6) 0%, rgba(234, 179, 8, 0.6) 100%)' : 'linear-gradient(135deg, rgba(251, 146, 60, 0.25) 0%, rgba(234, 179, 8, 0.25) 100%)',
1483
+ border: '3px solid rgb(234, 179, 8)',
1484
+ margin: '1rem 0',
1485
+ padding: '1rem',
1486
+ display: 'flex',
1487
+ alignItems: 'center',
1488
+ justifyContent: 'flex-end',
1489
+ transition: 'background 0.2s ease'
1490
+ }">
1491
+ <div style="background: rgb(234, 179, 8);
1492
+ color: white;
1493
+ padding: 0.75rem 1rem;
1494
+ font-size: 0.75rem;
1495
+ font-weight: 700;
1496
+ text-align: right;
1497
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
1498
+ <div style="font-family: monospace; margin-bottom: 0.25rem;">.col-center-left</div>
1499
+ <div style="font-size: 0.625rem; opacity: 0.9; font-weight: 500;">Left edge → center point</div>
1500
+ </div>
1501
+ </div>
1502
+
1503
+ <!-- Nested grid example: breakout-to-feature inside col-feature -->
1504
+ <div x-data="{ hovered: false }"
1505
+ @mouseenter="hovered = true"
1506
+ @mouseleave="hovered = false"
1507
+ :style="{
1508
+ gridColumn: 'feature',
1509
+ border: '3px dashed rgb(59, 130, 246)',
1510
+ margin: '1rem 0',
1511
+ background: hovered ? 'rgba(59, 130, 246, 0.3)' : 'rgba(59, 130, 246, 0.05)',
1512
+ transition: 'background 0.2s ease',
1513
+ padding: '0.5rem'
1514
+ }">
1515
+ <div style="font-size: 0.625rem; font-family: monospace; color: rgb(30, 64, 175); margin-bottom: 0.5rem; padding: 0.25rem;">
1516
+ Parent: .col-feature container
1517
+ </div>
1518
+ <div class="grid-cols-breakout breakout-to-feature"
1519
+ style="background: rgba(59, 130, 246, 0.1);">
1520
+ <div style="grid-column: feature;
1521
+ background: rgba(59, 130, 246, 0.3);
1522
+ padding: 0.5rem;
1523
+ font-size: 0.625rem;
1524
+ font-family: monospace;
1525
+ color: rgb(30, 64, 175);">
1526
+ .col-feature → fills container
1527
+ </div>
1528
+ <div style="grid-column: content;
1529
+ background: rgb(59, 130, 246);
1530
+ color: white;
1531
+ padding: 0.75rem 1rem;
1532
+ font-size: 0.75rem;
1533
+ font-weight: 700;
1534
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
1535
+ <div style="font-family: monospace; margin-bottom: 0.5rem;">.col-content → has margins</div>
1536
+ <div style="font-size: 0.625rem; opacity: 0.9; font-weight: 500; margin-bottom: 0.75rem;">breakout-to-feature collapses outer tracks</div>
1537
+ <pre style="font-size: 0.5rem; background: rgba(0,0,0,0.2); padding: 0.5rem; margin: 0; white-space: pre-wrap; text-align: left;">&lt;div class="col-feature"&gt;
1538
+ &lt;div class="grid-cols-breakout breakout-to-feature"&gt;
1539
+ &lt;div class="col-feature"&gt;Fills container&lt;/div&gt;
1540
+ &lt;p class="col-content"&gt;Has margins&lt;/p&gt;
1541
+ &lt;/div&gt;
1542
+ &lt;/div&gt;</pre>
1543
+ </div>
1544
+ </div>
1545
+ </div>
1546
+
1547
+ <!-- Subgrid example: child aligns to parent grid tracks -->
1548
+ <div x-data="{ hovered: false }"
1549
+ @mouseenter="hovered = true"
1550
+ @mouseleave="hovered = false"
1551
+ :style="{
1552
+ gridColumn: 'feature-start / full-end',
1553
+ display: 'grid',
1554
+ gridTemplateColumns: 'subgrid',
1555
+ border: '3px solid rgb(236, 72, 153)',
1556
+ margin: '1rem 0',
1557
+ background: hovered ? 'rgba(236, 72, 153, 0.15)' : 'rgba(236, 72, 153, 0.05)',
1558
+ transition: 'background 0.2s ease'
1559
+ }">
1560
+ <!-- Parent label -->
1561
+ <div style="grid-column: 1 / -1;
1562
+ font-size: 0.625rem;
1563
+ font-family: monospace;
1564
+ color: rgb(157, 23, 77);
1565
+ padding: 0.5rem;
1566
+ background: rgba(236, 72, 153, 0.1);">
1567
+ Parent: .col-feature-right .grid-cols-breakout-subgrid
1568
+ </div>
1569
+ <!-- Child spanning feature (wider, lighter) -->
1570
+ <div style="grid-column: feature;
1571
+ background: rgba(236, 72, 153, 0.3);
1572
+ padding: 0.5rem;
1573
+ margin: 0.5rem 0;
1574
+ font-size: 0.625rem;
1575
+ font-family: monospace;
1576
+ color: rgb(157, 23, 77);">
1577
+ Child: .col-feature (aligns to feature area)
1578
+ </div>
1579
+ <!-- Child using subgrid to align to content (darker) -->
1580
+ <div style="grid-column: content;
1581
+ background: rgb(236, 72, 153);
1582
+ color: white;
1583
+ padding: 0.75rem 1rem;
1584
+ margin: 0.5rem 0;
1585
+ font-size: 0.75rem;
1586
+ font-weight: 700;
1587
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
1588
+ <div style="font-family: monospace; margin-bottom: 0.5rem;">Child: .col-content</div>
1589
+ <div style="font-size: 0.625rem; opacity: 0.9; font-weight: 500; margin-bottom: 0.75rem;">Subgrid lets children align to parent's named lines</div>
1590
+ <pre style="font-size: 0.5rem; background: rgba(0,0,0,0.2); padding: 0.5rem; margin: 0; white-space: pre-wrap; text-align: left;">&lt;div class="col-feature-right grid-cols-breakout-subgrid"&gt;
1591
+ &lt;div class="col-feature"&gt;Aligns to feature!&lt;/div&gt;
1592
+ &lt;div class="col-content"&gt;Aligns to content!&lt;/div&gt;
1593
+ &lt;/div&gt;</pre>
1594
+ <div style="margin-top: 0.75rem;">
1595
+ <a href="https://caniuse.com/css-subgrid" target="_blank" rel="noopener" style="display: inline-block; background: rgba(255,255,255,0.2); color: white; text-decoration: none; padding: 0.375rem 0.75rem; border-radius: 0.25rem; font-size: 0.625rem; font-weight: 600; border: 1px solid rgba(255,255,255,0.3);">Check browser support</a>
1596
+ <span style="font-size: 0.5rem; opacity: 0.7; margin-left: 0.5rem;">(~90% as of Jan 2025)</span>
1597
+ </div>
1598
+ </div>
1599
+ </div>
1600
+
1601
+ </div>
1602
+
1603
+ <!-- Spacing Panel -->
1604
+ <div x-show="!showAdvanced"
1605
+ x-init="updateCurrentBreakpoint()"
1606
+ @mousemove.window="onDragSpacing($event)"
1607
+ @mouseup.window="stopDragSpacing()"
1608
+ :style="{
1609
+ position: 'fixed',
1610
+ left: spacingPanelPos.x + 'px',
1611
+ top: spacingPanelPos.y + 'px',
1612
+ zIndex: 30,
1613
+ pointerEvents: 'auto',
1614
+ background: '#f7f7f7',
1615
+ borderRadius: '8px',
1616
+ boxShadow: '0 4px 24px rgba(0, 0, 0, 0.15)',
1617
+ width: '220px',
1618
+ fontFamily: 'system-ui, -apple-system, sans-serif',
1619
+ overflow: 'hidden'
1620
+ }">
1621
+ <!-- Header -->
1622
+ <div @mousedown="startDragSpacing($event)"
1623
+ @dblclick="spacingPanelCollapsed = !spacingPanelCollapsed; localStorage.setItem('breakoutGridSpacingCollapsed', spacingPanelCollapsed)"
1624
+ style="padding: 8px 12px; background: #1a1a2e; color: white; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none;">
1625
+ <div style="display: flex; align-items: center; gap: 8px;">
1626
+ <span style="font-weight: 600; font-size: 12px;">Spacing</span>
1627
+ <span style="font-size: 10px; font-weight: 600; color: white; background: transparent; border: 1.5px solid rgba(255,255,255,0.5); padding: 1px 6px; border-radius: 3px;" x-text="'@' + currentBreakpoint"></span>
1628
+ </div>
1629
+ <button @click.stop="spacingPanelCollapsed = !spacingPanelCollapsed; localStorage.setItem('breakoutGridSpacingCollapsed', spacingPanelCollapsed)" style="background: transparent; border: none; color: rgba(255,255,255,0.6); cursor: pointer; font-size: 14px; line-height: 1; padding: 0;" x-text="spacingPanelCollapsed ? '+' : '−'"></button>
1630
+ </div>
1631
+ <!-- Content -->
1632
+ <div x-show="!spacingPanelCollapsed" style="padding: 12px;">
1633
+ <!-- Gap -->
1634
+ <div style="display: flex; flex-direction: column; gap: 8px;">
1635
+ <div style="display: flex; align-items: center; gap: 8px;">
1636
+ <span style="font-size: 10px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px;">Gap</span>
1637
+ <span style="font-size: 9px; color: #9ca3af;">outer margins</span>
1638
+ </div>
1639
+ <div style="display: flex; align-items: flex-end; gap: 8px;">
1640
+ <div style="width: var(--gap); height: 24px; background: #f97316; min-width: 20px;"></div>
1641
+ <div style="width: 24px; height: var(--gap); background: #f97316; min-height: 20px;"></div>
1642
+ </div>
1643
+ <div style="font-size: 9px; font-family: 'SF Mono', Monaco, monospace; color: #6b7280;">
1644
+ clamp(<span style="color: #10b981; font-weight: 600;" x-text="editValues.baseGap || configOptions.baseGap.value"></span>, <span style="color: #6366f1; font-weight: 600;" x-text="editValues['gapScale_' + (currentBreakpoint === 'mobile' ? 'default' : currentBreakpoint)] || gapScaleOptions[currentBreakpoint === 'mobile' ? 'default' : currentBreakpoint].value"></span>, <span style="color: #10b981; font-weight: 600;" x-text="editValues.maxGap || configOptions.maxGap.value"></span>)
1645
+ </div>
1646
+ </div>
1647
+ <!-- Breakout Padding -->
1648
+ <div style="display: flex; flex-direction: column; gap: 8px; padding-top: 12px; margin-top: 12px; border-top: 1px solid #e5e5e5;">
1649
+ <div style="display: flex; align-items: center; gap: 8px;">
1650
+ <span style="font-size: 10px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px;">Breakout</span>
1651
+ <span style="font-size: 9px; color: #9ca3af;">p-breakout / m-breakout</span>
1652
+ </div>
1653
+ <div style="display: flex; align-items: flex-end; gap: 8px;">
1654
+ <div style="width: var(--breakout-padding); height: 24px; background: #8b5cf6; min-width: 20px;"></div>
1655
+ <div style="width: 24px; height: var(--breakout-padding); background: #8b5cf6; min-height: 20px;"></div>
1656
+ </div>
1657
+ <div style="font-size: 9px; font-family: 'SF Mono', Monaco, monospace; color: #6b7280;">
1658
+ clamp(<span style="color: #8b5cf6; font-weight: 600;" x-text="editValues.breakout_min || breakoutOptions.min.value"></span>, <span style="color: #8b5cf6; font-weight: 600;" x-text="editValues.breakout_scale || breakoutOptions.scale.value"></span>, <span style="color: #10b981; font-weight: 600;" x-text="editValues.popoutWidth || configOptions.popoutWidth.value"></span>)
1659
+ </div>
1660
+ <!-- Editable breakout values -->
1661
+ <div style="display: flex; gap: 8px; margin-top: 4px;">
1662
+ <div style="flex: 1;">
1663
+ <div style="font-size: 8px; color: #9ca3af; margin-bottom: 2px;">min</div>
1664
+ <div style="display: flex; align-items: center; gap: 2px;">
1665
+ <input type="number" :value="getBreakoutNumeric('min')" @input="updateBreakoutNumeric('min', $event.target.value)" step="0.5"
1666
+ style="width: 100%; padding: 4px 6px; font-size: 10px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 3px; background: white; text-align: right;">
1667
+ <span style="font-size: 9px; color: #9ca3af;" x-text="getBreakoutUnit('min')"></span>
1668
+ </div>
1669
+ </div>
1670
+ <div style="flex: 1;">
1671
+ <div style="font-size: 8px; color: #9ca3af; margin-bottom: 2px;">scale</div>
1672
+ <div style="display: flex; align-items: center; gap: 2px;">
1673
+ <input type="number" :value="getBreakoutNumeric('scale')" @input="updateBreakoutNumeric('scale', $event.target.value)" step="1"
1674
+ style="width: 100%; padding: 4px 6px; font-size: 10px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 3px; background: white; text-align: right;">
1675
+ <span style="font-size: 9px; color: #9ca3af;" x-text="getBreakoutUnit('scale')"></span>
1676
+ </div>
1677
+ </div>
1678
+ </div>
1679
+ <div style="font-size: 8px; color: #9ca3af; font-style: italic;">max = popout width</div>
1680
+ </div>
1681
+ </div>
1682
+ </div>
1683
+
1684
+ <!-- Grid Overlay (hidden in Advanced mode) -->
1685
+ <div x-show="!showAdvanced" x-init="$watch('isVisible', v => v && setTimeout(() => updateColumnWidths(), 50)); setTimeout(() => updateColumnWidths(), 100)" class="grid-cols-breakout breakout-visualizer-grid" style="height: 100%; position: relative; z-index: 2;">
1686
+ <template x-for="area in gridAreas" :key="area.name">
1687
+ <div :class="'col-' + area.name"
1688
+ @click="selectArea(area.name)"
1689
+ @mouseenter="hoveredArea = area.name"
1690
+ @mouseleave="hoveredArea = null"
1691
+ :style="{
1692
+ backgroundColor: (hoveredArea === area.name || isSelected(area.name)) ? area.color.replace('0.25', '0.6') : area.color,
1693
+ borderLeft: '1px solid ' + area.borderColor,
1694
+ borderRight: '1px solid ' + area.borderColor,
1695
+ position: 'relative',
1696
+ height: '100%',
1697
+ pointerEvents: 'auto',
1698
+ cursor: 'pointer',
1699
+ transition: 'background-color 0.2s'
1700
+ }">
1701
+
1702
+ <!-- Label (centered) -->
1703
+ <div x-show="showLabels"
1704
+ :style="{
1705
+ position: 'absolute',
1706
+ top: '50%',
1707
+ left: '50%',
1708
+ transform: 'translate(-50%, -50%)',
1709
+ backgroundColor: area.borderColor,
1710
+ color: 'white',
1711
+ padding: '0.75rem 1rem',
1712
+ borderRadius: '0.375rem',
1713
+ fontSize: '0.75rem',
1714
+ fontWeight: '600',
1715
+ textTransform: 'uppercase',
1716
+ letterSpacing: '0.05em',
1717
+ whiteSpace: 'nowrap',
1718
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
1719
+ opacity: isSelected(area.name) ? '1' : '0.8',
1720
+ textAlign: 'center',
1721
+ zIndex: '10'
1722
+ }">
1723
+ <div x-text="area.label"></div>
1724
+ <div x-show="showClassNames"
1725
+ :style="{
1726
+ fontSize: '0.625rem',
1727
+ fontWeight: '500',
1728
+ textTransform: 'none',
1729
+ marginTop: '0.25rem',
1730
+ opacity: '0.9',
1731
+ fontFamily: 'monospace'
1732
+ }" x-text="area.className"></div>
1733
+ <div x-show="showPixelWidths && columnWidths[area.name] > 0"
1734
+ :style="{
1735
+ fontSize: '0.625rem',
1736
+ fontWeight: '600',
1737
+ textTransform: 'none',
1738
+ marginTop: '0.25rem',
1739
+ opacity: '0.75',
1740
+ fontFamily: 'monospace',
1741
+ backgroundColor: 'rgba(0,0,0,0.2)',
1742
+ padding: '0.125rem 0.375rem',
1743
+ borderRadius: '0.25rem'
1744
+ }" x-text="columnWidths[area.name] + 'px'"></div>
1745
+ </div>
1746
+
1747
+ <!-- Lorem Ipsum Content (behind label) -->
1748
+ <div x-show="showLoremIpsum"
1749
+ :style="{
1750
+ position: 'absolute',
1751
+ inset: '0',
1752
+ padding: showGapPadding ? 'var(--gap)' : (showBreakoutPadding ? 'var(--breakout-padding)' : '1.5rem 0'),
1753
+ boxSizing: 'border-box',
1754
+ overflow: 'hidden',
1755
+ whiteSpace: 'pre-line',
1756
+ fontSize: '1.125rem',
1757
+ lineHeight: '1.75',
1758
+ color: 'white',
1759
+ textShadow: '0 1px 2px rgba(0,0,0,0.3)',
1760
+ zIndex: '1'
1761
+ }" x-text="loremContent"></div>
1762
+
1763
+ <!-- p-gap / px-gap Padding Overlay -->
1764
+ <div x-show="showGapPadding"
1765
+ :style="{
1766
+ position: 'absolute',
1767
+ inset: 'var(--gap)',
1768
+ border: '2px dotted ' + area.borderColor,
1769
+ backgroundColor: area.color.replace('0.1', '0.2'),
1770
+ pointerEvents: 'none',
1771
+ zIndex: '10'
1772
+ }">
1773
+ <div :style="{
1774
+ position: 'absolute',
1775
+ top: '0.5rem',
1776
+ left: '0.5rem',
1777
+ fontSize: '0.625rem',
1778
+ fontWeight: '700',
1779
+ color: area.borderColor,
1780
+ textTransform: 'uppercase',
1781
+ letterSpacing: '0.05em',
1782
+ backgroundColor: 'white',
1783
+ padding: '0.25rem 0.5rem',
1784
+ borderRadius: '0.25rem',
1785
+ boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
1786
+ }">p-gap</div>
1787
+ </div>
1788
+
1789
+ <!-- p-breakout / px-breakout Padding Overlay -->
1790
+ <div x-show="showBreakoutPadding"
1791
+ :style="{
1792
+ position: 'absolute',
1793
+ inset: 'var(--breakout-padding)',
1794
+ border: '3px dashed ' + area.borderColor,
1795
+ backgroundColor: area.color.replace('0.1', '0.25'),
1796
+ pointerEvents: 'none',
1797
+ zIndex: '10'
1798
+ }">
1799
+ <div :style="{
1800
+ position: 'absolute',
1801
+ top: '0.5rem',
1802
+ left: '0.5rem',
1803
+ fontSize: '0.625rem',
1804
+ fontWeight: '700',
1805
+ color: area.borderColor,
1806
+ textTransform: 'uppercase',
1807
+ letterSpacing: '0.05em',
1808
+ backgroundColor: 'white',
1809
+ padding: '0.25rem 0.5rem',
1810
+ borderRadius: '0.25rem',
1811
+ boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
1812
+ }">p-breakout</div>
1813
+ </div>
1814
+
1815
+ <!-- Drag Handle - Left (edit mode only, for resizable columns) -->
1816
+ <div x-show="editMode && hoveredArea === area.name && getResizeConfig(area.name)"
1817
+ @mousedown.stop="startColumnResize($event, getResizeConfig(area.name))"
1818
+ :style="{
1819
+ position: 'absolute',
1820
+ left: '-4px',
1821
+ top: '0',
1822
+ width: '16px',
1823
+ height: '100%',
1824
+ cursor: 'ew-resize',
1825
+ pointerEvents: 'auto',
1826
+ zIndex: '100',
1827
+ display: 'flex',
1828
+ alignItems: 'center',
1829
+ justifyContent: 'center'
1830
+ }">
1831
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 4px;">
1832
+ <div :style="{
1833
+ width: '10px',
1834
+ height: '100px',
1835
+ background: area.borderColor,
1836
+ borderRadius: '5px',
1837
+ boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
1838
+ border: '2px solid white'
1839
+ }"></div>
1840
+ <div :style="{
1841
+ background: area.borderColor,
1842
+ color: 'white',
1843
+ padding: '4px 8px',
1844
+ borderRadius: '4px',
1845
+ fontSize: '10px',
1846
+ fontWeight: '700',
1847
+ whiteSpace: 'nowrap',
1848
+ boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
1849
+ }" x-text="area.name === 'content' ? '← min' : '↔'"></div>
1850
+ </div>
1851
+ </div>
1852
+
1853
+ <!-- Content Min/Base/Max Visual Guides with integrated handles (edit mode only) -->
1854
+ <template x-if="editMode && area.name === 'content'">
1855
+ <div style="position: absolute; inset: 0; pointer-events: none; z-index: 50; overflow: visible;">
1856
+ <!-- Max boundary (outer, dotted) with drag handle - can overflow to show full width -->
1857
+ <div :style="{
1858
+ position: 'absolute',
1859
+ top: '0',
1860
+ bottom: '0',
1861
+ left: '50%',
1862
+ transform: 'translateX(-50%)',
1863
+ width: editValues.contentMax || configOptions.contentMax.value,
1864
+ border: '3px dotted rgba(139, 92, 246, 0.9)',
1865
+ boxSizing: 'border-box',
1866
+ background: 'rgba(139, 92, 246, 0.05)'
1867
+ }">
1868
+ <div style="position: absolute; top: 8px; right: 8px; background: rgba(139, 92, 246, 0.95); color: white; padding: 3px 8px; border-radius: 3px; font-size: 10px; font-weight: 700;">
1869
+ max: <span x-text="editValues.contentMax || configOptions.contentMax.value"></span>
1870
+ </div>
1871
+ <!-- Max drag handle on right edge, at top - show on hover or when selected -->
1872
+ <div x-show="hoveredArea === 'content' || selectedArea === 'content'"
1873
+ @mousedown.stop="startColumnResize($event, 'contentMax')"
1874
+ style="position: absolute; right: -8px; top: 8px; width: 16px; height: 60px; cursor: ew-resize; pointer-events: auto; display: flex; align-items: center; justify-content: center;">
1875
+ <div style="width: 8px; height: 100%; background: rgb(139, 92, 246); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.4); border: 2px solid white;"></div>
1876
+ </div>
1877
+ </div>
1878
+ <!-- Base boundary (middle, solid) - half height, inset, can overflow -->
1879
+ <div :style="{
1880
+ position: 'absolute',
1881
+ top: '25%',
1882
+ bottom: '25%',
1883
+ left: '50%',
1884
+ transform: 'translateX(-50%)',
1885
+ width: editValues.contentBase || configOptions.contentBase.value,
1886
+ border: '3px solid rgba(236, 72, 153, 1)',
1887
+ background: 'rgba(236, 72, 153, 0.5)',
1888
+ boxSizing: 'border-box',
1889
+ borderRadius: '4px'
1890
+ }">
1891
+ <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(236, 72, 153, 0.95); color: white; padding: 3px 8px; border-radius: 3px; font-size: 10px; font-weight: 700; white-space: nowrap;">
1892
+ base: <span x-text="editValues.contentBase || configOptions.contentBase.value"></span>
1893
+ </div>
1894
+ <!-- Base drag handle on right edge - show on hover or when selected -->
1895
+ <div x-show="hoveredArea === 'content' || selectedArea === 'content'"
1896
+ @mousedown.stop="startColumnResize($event, 'contentBase')"
1897
+ style="position: absolute; right: -8px; top: 50%; transform: translateY(-50%); width: 16px; height: 40px; cursor: ew-resize; pointer-events: auto; display: flex; align-items: center; justify-content: center;">
1898
+ <div style="width: 8px; height: 100%; background: rgb(236, 72, 153); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.4); border: 2px solid white;"></div>
1899
+ </div>
1900
+ </div>
1901
+ <!-- Min boundary (inner, dashed) with drag handle - can overflow to show full width -->
1902
+ <div :style="{
1903
+ position: 'absolute',
1904
+ top: '0',
1905
+ bottom: '0',
1906
+ left: '50%',
1907
+ transform: 'translateX(-50%)',
1908
+ width: editValues.contentMin || configOptions.contentMin.value,
1909
+ border: '3px dashed rgba(168, 85, 247, 0.9)',
1910
+ background: 'rgba(168, 85, 247, 0.15)',
1911
+ boxSizing: 'border-box'
1912
+ }">
1913
+ <div style="position: absolute; top: 8px; left: 8px; background: rgba(168, 85, 247, 0.95); color: white; padding: 3px 8px; border-radius: 3px; font-size: 10px; font-weight: 700;">
1914
+ min: <span x-text="editValues.contentMin || configOptions.contentMin.value"></span>
1915
+ </div>
1916
+ <!-- Min drag handle on left edge, at top - show on hover or when selected -->
1917
+ <div x-show="hoveredArea === 'content' || selectedArea === 'content'"
1918
+ @mousedown.stop="startColumnResize($event, 'contentMin')"
1919
+ style="position: absolute; left: -8px; top: 8px; width: 16px; height: 60px; cursor: ew-resize; pointer-events: auto; display: flex; align-items: center; justify-content: center;">
1920
+ <div style="width: 8px; height: 100%; background: rgb(168, 85, 247); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.4); border: 2px solid white;"></div>
1921
+ </div>
1922
+ </div>
1923
+ </div>
1924
+ </template>
1925
+
1926
+ <!-- Feature Min/Scale/Max Visual Guides with integrated handles (edit mode only) -->
1927
+ <template x-if="editMode && area.name === 'feature'">
1928
+ <div style="position: absolute; inset: 0; pointer-events: none; z-index: 50; overflow: visible;">
1929
+ <!-- Max boundary (outer, dotted) - anchored from right edge (content side) -->
1930
+ <div :style="{
1931
+ position: 'absolute',
1932
+ top: '0',
1933
+ bottom: '0',
1934
+ right: '0',
1935
+ width: editValues.featureMax || configOptions.featureMax.value,
1936
+ border: '3px dotted rgba(6, 182, 212, 0.9)',
1937
+ boxSizing: 'border-box',
1938
+ background: 'rgba(6, 182, 212, 0.05)'
1939
+ }">
1940
+ <div style="position: absolute; top: 8px; left: 8px; background: rgba(6, 182, 212, 0.95); color: white; padding: 3px 8px; border-radius: 3px; font-size: 10px; font-weight: 700;">
1941
+ max: <span x-text="editValues.featureMax || configOptions.featureMax.value"></span>
1942
+ </div>
1943
+ <!-- Max drag handle on left edge -->
1944
+ <div x-show="hoveredArea === 'feature' || selectedArea === 'feature'"
1945
+ @mousedown.stop="startColumnResize($event, 'featureMax')"
1946
+ style="position: absolute; left: -8px; top: 8px; width: 16px; height: 60px; cursor: ew-resize; pointer-events: auto; display: flex; align-items: center; justify-content: center;">
1947
+ <div style="width: 8px; height: 100%; background: rgb(6, 182, 212); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.4); border: 2px solid white;"></div>
1948
+ </div>
1949
+ </div>
1950
+ <!-- Scale boundary (middle, solid) - half height, inset -->
1951
+ <div :style="{
1952
+ position: 'absolute',
1953
+ top: '25%',
1954
+ bottom: '25%',
1955
+ right: '0',
1956
+ width: editValues.featureScale || configOptions.featureScale.value,
1957
+ border: '3px solid rgba(14, 165, 233, 1)',
1958
+ background: 'rgba(14, 165, 233, 0.5)',
1959
+ boxSizing: 'border-box',
1960
+ borderRadius: '4px'
1961
+ }">
1962
+ <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(14, 165, 233, 0.95); color: white; padding: 3px 8px; border-radius: 3px; font-size: 10px; font-weight: 700; white-space: nowrap;">
1963
+ scale: <span x-text="editValues.featureScale || configOptions.featureScale.value"></span>
1964
+ </div>
1965
+ <!-- Scale drag handle on left edge -->
1966
+ <div x-show="hoveredArea === 'feature' || selectedArea === 'feature'"
1967
+ @mousedown.stop="startColumnResize($event, 'featureScale')"
1968
+ style="position: absolute; left: -8px; top: 50%; transform: translateY(-50%); width: 16px; height: 40px; cursor: ew-resize; pointer-events: auto; display: flex; align-items: center; justify-content: center;">
1969
+ <div style="width: 8px; height: 100%; background: rgb(14, 165, 233); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.4); border: 2px solid white;"></div>
1970
+ </div>
1971
+ </div>
1972
+ <!-- Min boundary (inner, dashed) -->
1973
+ <div :style="{
1974
+ position: 'absolute',
1975
+ top: '0',
1976
+ bottom: '0',
1977
+ right: '0',
1978
+ width: editValues.featureMin || configOptions.featureMin.value,
1979
+ border: '3px dashed rgba(56, 189, 248, 0.9)',
1980
+ background: 'rgba(56, 189, 248, 0.15)',
1981
+ boxSizing: 'border-box'
1982
+ }">
1983
+ <div style="position: absolute; bottom: 8px; left: 8px; background: rgba(56, 189, 248, 0.95); color: white; padding: 3px 8px; border-radius: 3px; font-size: 10px; font-weight: 700;">
1984
+ min: <span x-text="editValues.featureMin || configOptions.featureMin.value"></span>
1985
+ </div>
1986
+ <!-- Min drag handle on left edge, at bottom -->
1987
+ <div x-show="hoveredArea === 'feature' || selectedArea === 'feature'"
1988
+ @mousedown.stop="startColumnResize($event, 'featureMin')"
1989
+ style="position: absolute; left: -8px; bottom: 8px; width: 16px; height: 60px; cursor: ew-resize; pointer-events: auto; display: flex; align-items: center; justify-content: center;">
1990
+ <div style="width: 8px; height: 100%; background: rgb(56, 189, 248); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.4); border: 2px solid white;"></div>
1991
+ </div>
1992
+ </div>
1993
+ </div>
1994
+ </template>
1995
+ </div>
1996
+ </template>
1997
+ </div>
1998
+
1999
+
2000
+ <!-- Control Panel - Ubiquiti-style -->
2001
+ <div :style="{
2002
+ position: 'fixed',
2003
+ bottom: '12px',
2004
+ right: '12px',
2005
+ pointerEvents: 'auto',
2006
+ background: '#f7f7f7',
2007
+ borderRadius: '8px',
2008
+ boxShadow: '0 4px 24px rgba(0, 0, 0, 0.15)',
2009
+ width: '200px',
2010
+ fontFamily: 'system-ui, -apple-system, sans-serif',
2011
+ zIndex: '10000',
2012
+ overflow: 'hidden'
2013
+ }">
2014
+
2015
+ <!-- Header -->
2016
+ <div @dblclick="controlPanelCollapsed = !controlPanelCollapsed"
2017
+ style="padding: 8px 12px; background: #1a1a2e; color: white; display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none;">
2018
+ <div style="display: flex; align-items: center; gap: 8px;">
2019
+ <span style="font-weight: 600; font-size: 12px;">Grid</span>
2020
+ <span style="font-size: 10px; color: rgba(255,255,255,0.5);" x-text="version"></span>
2021
+ <span x-show="controlPanelCollapsed" style="font-size: 10px; color: rgba(255,255,255,0.4);">...</span>
2022
+ </div>
2023
+ <div style="display: flex; align-items: center; gap: 8px;">
2024
+ <span style="font-size: 11px; font-variant-numeric: tabular-nums; color: rgba(255,255,255,0.7);" x-text="viewportWidth + 'px'"></span>
2025
+ <button @click.stop="toggle()" style="background: transparent; border: none; color: rgba(255,255,255,0.6); cursor: pointer; font-size: 16px; line-height: 1; padding: 0;">&times;</button>
2026
+ </div>
2027
+ </div>
2028
+
2029
+ <!-- Collapsible Content -->
2030
+ <div x-show="!controlPanelCollapsed">
2031
+ <!-- Action Buttons -->
2032
+ <div style="padding: 8px; background: white; border-bottom: 1px solid #e5e5e5; display: flex; gap: 6px;">
2033
+ <button @click="openEditor()"
2034
+ :style="{
2035
+ flex: 1,
2036
+ padding: '6px 8px',
2037
+ fontSize: '10px',
2038
+ fontWeight: '600',
2039
+ border: 'none',
2040
+ borderRadius: '4px',
2041
+ cursor: 'pointer',
2042
+ background: showEditor ? '#1a1a2e' : '#e5e5e5',
2043
+ color: showEditor ? 'white' : '#374151'
2044
+ }">
2045
+ Config
2046
+ </button>
2047
+ <button @click="showDiagram = !showDiagram; if(showDiagram && Object.keys(editValues).length === 0) loadCurrentValues()"
2048
+ :style="{
2049
+ flex: 1,
2050
+ padding: '6px 8px',
2051
+ fontSize: '10px',
2052
+ fontWeight: '600',
2053
+ border: 'none',
2054
+ borderRadius: '4px',
2055
+ cursor: 'pointer',
2056
+ background: showDiagram ? '#1a1a2e' : '#e5e5e5',
2057
+ color: showDiagram ? 'white' : '#374151'
2058
+ }">
2059
+ Diagram
2060
+ </button>
2061
+ </div>
2062
+
2063
+ <!-- Display Options -->
2064
+ <div style="padding: 8px 12px; background: white; border-bottom: 1px solid #e5e5e5;">
2065
+ <div style="font-size: 9px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px;">Display</div>
2066
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px;">
2067
+ <label style="display: flex; align-items: center; cursor: pointer; font-size: 11px; color: #374151;">
2068
+ <input type="checkbox" x-model="showLabels" style="margin-right: 6px; cursor: pointer; accent-color: #1a1a2e;">
2069
+ Labels
2070
+ </label>
2071
+ <label style="display: flex; align-items: center; cursor: pointer; font-size: 11px; color: #374151;">
2072
+ <input type="checkbox" x-model="showClassNames" style="margin-right: 6px; cursor: pointer; accent-color: #1a1a2e;">
2073
+ Classes
2074
+ </label>
2075
+ <label style="display: flex; align-items: center; cursor: pointer; font-size: 11px; color: #374151;">
2076
+ <input type="checkbox" x-model="showMeasurements" style="margin-right: 6px; cursor: pointer; accent-color: #1a1a2e;">
2077
+ Values
2078
+ </label>
2079
+ <label style="display: flex; align-items: center; cursor: pointer; font-size: 11px; color: #374151;">
2080
+ <input type="checkbox" x-model="showLoremIpsum" style="margin-right: 6px; cursor: pointer; accent-color: #1a1a2e;">
2081
+ Lorem
2082
+ </label>
2083
+ <label style="display: flex; align-items: center; cursor: pointer; font-size: 11px; color: #374151;">
2084
+ <input type="checkbox" x-model="showPixelWidths" style="margin-right: 6px; cursor: pointer; accent-color: #1a1a2e;">
2085
+ Pixels
2086
+ </label>
2087
+ </div>
2088
+ </div>
2089
+
2090
+ <!-- Padding Options -->
2091
+ <div style="padding: 8px 12px; background: white; border-bottom: 1px solid #e5e5e5;">
2092
+ <div style="font-size: 9px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px;">Padding</div>
2093
+ <div style="display: flex; gap: 12px;">
2094
+ <label style="display: flex; align-items: center; cursor: pointer; font-size: 11px; color: #374151;">
2095
+ <input type="checkbox" x-model="showGapPadding" style="margin-right: 6px; cursor: pointer; accent-color: #1a1a2e;">
2096
+ p-gap
2097
+ </label>
2098
+ <label style="display: flex; align-items: center; cursor: pointer; font-size: 11px; color: #374151;">
2099
+ <input type="checkbox" x-model="showBreakoutPadding" style="margin-right: 6px; cursor: pointer; accent-color: #1a1a2e;">
2100
+ p-breakout
2101
+ </label>
2102
+ </div>
2103
+ </div>
2104
+
2105
+ <!-- Advanced -->
2106
+ <div style="padding: 8px 12px; background: white;">
2107
+ <label style="display: flex; align-items: center; cursor: pointer; font-size: 11px; color: #374151;">
2108
+ <input type="checkbox" x-model="showAdvanced" style="margin-right: 6px; cursor: pointer; accent-color: #1a1a2e;">
2109
+ Advanced Spans
2110
+ </label>
2111
+ </div>
2112
+
2113
+ <!-- Footer -->
2114
+ <div style="padding: 6px 12px; background: #f7f7f7; border-top: 1px solid #e5e5e5;">
2115
+ <div style="font-size: 9px; color: #9ca3af; text-align: center;">
2116
+ <kbd style="background: #e5e5e5; padding: 1px 4px; border-radius: 2px; font-size: 9px; font-weight: 600; color: #374151;">⌘G</kbd> toggle
2117
+ </div>
2118
+ </div>
2119
+
2120
+ <!-- Selected Area Info -->
2121
+ <div x-show="selectedArea" style="padding: 8px 12px; background: #f0f9ff; border-top: 1px solid #e5e5e5;">
2122
+ <div style="font-size: 11px; color: #1a1a2e; font-weight: 600; font-family: monospace;" x-text="gridAreas.find(a => a.name === selectedArea)?.className || ''"></div>
2123
+ </div>
2124
+ </div><!-- End Collapsible Content -->
2125
+
2126
+ </div>
2127
+
2128
+ <!-- Floating Editor Window - Ubiquiti-style -->
2129
+ <div x-show="showEditor"
2130
+ @mousedown.self="startDrag($event)"
2131
+ @mousemove.window="onDrag($event)"
2132
+ @mouseup.window="stopDrag()"
2133
+ :style="{
2134
+ position: 'fixed',
2135
+ left: editorPos.x + 'px',
2136
+ top: editorPos.y + 'px',
2137
+ width: '280px',
2138
+ maxHeight: '85vh',
2139
+ background: '#f7f7f7',
2140
+ borderRadius: '8px',
2141
+ boxShadow: '0 4px 24px rgba(0, 0, 0, 0.15)',
2142
+ pointerEvents: 'auto',
2143
+ zIndex: '10001',
2144
+ overflow: 'hidden',
2145
+ fontFamily: 'system-ui, -apple-system, sans-serif'
2146
+ }">
2147
+ <!-- Editor Header (draggable) -->
2148
+ <div @mousedown="startDrag($event)"
2149
+ @dblclick="configEditorCollapsed = !configEditorCollapsed"
2150
+ style="padding: 10px 12px; background: #1a1a2e; color: white; cursor: move; display: flex; justify-content: space-between; align-items: center; user-select: none;">
2151
+ <div style="display: flex; align-items: center; gap: 8px;">
2152
+ <span style="font-weight: 600; font-size: 12px; letter-spacing: 0.3px;">Grid Config</span>
2153
+ <span x-show="configEditorCollapsed" style="font-size: 10px; color: rgba(255,255,255,0.4);">...</span>
2154
+ </div>
2155
+ <button @click.stop="closeEditor()" style="background: transparent; border: none; color: rgba(255,255,255,0.6); padding: 2px 6px; cursor: pointer; font-size: 16px; line-height: 1;">&times;</button>
2156
+ </div>
2157
+ <!-- Editor Content -->
2158
+ <div x-show="!configEditorCollapsed" style="max-height: calc(85vh - 40px); overflow-y: auto;">
2159
+ <!-- Workflow tip -->
2160
+ <div style="background: #e8f4f8; padding: 8px 12px; font-size: 10px; color: #1a1a2e; line-height: 1.4; border-bottom: 1px solid #e5e5e5;">
2161
+ Start with <strong>content</strong>, then build outward: popout → feature → full
2162
+ </div>
2163
+
2164
+ <!-- Track overflow warning -->
2165
+ <div x-show="getTrackOverflowWarning()"
2166
+ style="background: #fef3c7; padding: 8px 12px; font-size: 10px; color: #92400e; line-height: 1.4; border-bottom: 1px solid #fcd34d;">
2167
+ <span style="font-weight: 600;">⚠️</span> <span x-text="getTrackOverflowWarning()"></span>
2168
+ </div>
2169
+
2170
+ <!-- Content Section -->
2171
+ <div style="padding: 8px 12px; background: white; border-bottom: 1px solid #e5e5e5;">
2172
+ <div @click="copySection('content')" style="font-size: 9px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; cursor: pointer; display: flex; align-items: center; gap: 6px;" :style="{ color: sectionCopied === 'content' ? '#10b981' : '#6b7280' }">
2173
+ <span x-text="sectionCopied === 'content' ? '✓ Copied' : 'Content (Text Width)'"></span>
2174
+ </div>
2175
+ <template x-for="key in ['contentMin', 'contentBase', 'contentMax']" :key="'ed_'+key">
2176
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #f3f4f6;">
2177
+ <span style="font-size: 11px; color: #374151;" x-text="key.replace('content', '').toLowerCase()"></span>
2178
+ <div style="display: flex; align-items: center; gap: 4px;">
2179
+ <input type="number" :value="getNumericValue(key)" @input="updateNumericValue(key, $event.target.value)" step="1"
2180
+ style="width: 72px; padding: 6px 8px; font-size: 11px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; text-align: right;">
2181
+ <select x-show="hasUnitSelector(key)" @change="updateUnit(key, $event.target.value)" :value="getUnit(key)"
2182
+ style="padding: 6px 4px; font-size: 10px; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; color: #6b7280; cursor: pointer; width: 50px; text-align: center;">
2183
+ <template x-for="u in unitOptions" :key="u">
2184
+ <option :value="u" :selected="getUnit(key) === u" x-text="u"></option>
2185
+ </template>
2186
+ </select>
2187
+ <span x-show="!hasUnitSelector(key)" style="font-size: 10px; color: #9ca3af; width: 50px; text-align: center; display: inline-block;" x-text="getUnit(key)"></span>
2188
+ </div>
2189
+ </div>
2190
+ </template>
2191
+ <!-- Readability warning -->
2192
+ <div x-show="getContentReadabilityWarning()"
2193
+ x-data="{ expanded: false }"
2194
+ style="margin-top: 6px; padding: 6px 8px; background: #fef3c7; border-radius: 4px; border: 1px solid #fcd34d;">
2195
+ <div @click="expanded = !expanded" style="display: flex; align-items: flex-start; gap: 6px; cursor: pointer;">
2196
+ <svg style="width: 14px; height: 14px; color: #b45309; flex-shrink: 0; margin-top: 1px;" fill="currentColor" viewBox="0 0 20 20">
2197
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
2198
+ </svg>
2199
+ <div style="flex: 1;">
2200
+ <div style="font-size: 10px; font-weight: 600; color: #92400e;">Wide for reading</div>
2201
+ <div x-show="!expanded" style="font-size: 9px; color: #b45309; margin-top: 2px;">Ideal: 45–55rem for prose. Click for details.</div>
2202
+ <div x-show="expanded" x-transition style="font-size: 9px; color: #78350f; margin-top: 4px; line-height: 1.4;">
2203
+ At 16px base, 55rem+ can hit 100+ characters/line—too wide for comfortable reading.<br><br>
2204
+ <strong>Guidelines (at 1rem/16px):</strong><br>
2205
+ • 45ch ≈ 35–40rem (min)<br>
2206
+ • 66ch ≈ 45–50rem (ideal)<br>
2207
+ • 75ch ≈ 50–55rem (max)<br><br>
2208
+ Fine for mixed layouts; consider tightening for prose-heavy pages.
2209
+ </div>
2210
+ </div>
2211
+ </div>
2212
+ </div>
2213
+ </div>
2214
+
2215
+ <!-- Default Column Section -->
2216
+ <div style="padding: 8px 12px; background: white; border-bottom: 1px solid #e5e5e5;">
2217
+ <div style="display: flex; align-items: center; justify-content: space-between;">
2218
+ <div @click="copySection('defaultCol')" style="cursor: pointer;">
2219
+ <div style="font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;" :style="{ color: sectionCopied === 'defaultCol' ? '#10b981' : '#6b7280' }" x-text="sectionCopied === 'defaultCol' ? '✓ Copied' : 'Default Column'"></div>
2220
+ <div style="font-size: 9px; color: #9ca3af; margin-top: 2px;">For children without col-* class</div>
2221
+ </div>
2222
+ <select @change="editValues.defaultCol = $event.target.value; configCopied = false"
2223
+ :value="editValues.defaultCol || configOptions.defaultCol.value"
2224
+ style="padding: 6px 8px; font-size: 11px; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; cursor: pointer;">
2225
+ <template x-for="opt in configOptions.defaultCol.options" :key="opt">
2226
+ <option :value="opt" :selected="(editValues.defaultCol || configOptions.defaultCol.value) === opt" x-text="opt"></option>
2227
+ </template>
2228
+ </select>
2229
+ </div>
2230
+ </div>
2231
+
2232
+ <!-- Track Widths Section -->
2233
+ <div style="padding: 8px 12px; background: white; border-bottom: 1px solid #e5e5e5;">
2234
+ <div @click="copySection('tracks')" style="font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; cursor: pointer;" :style="{ color: sectionCopied === 'tracks' ? '#10b981' : '#6b7280' }" x-text="sectionCopied === 'tracks' ? '✓ Copied' : 'Track Widths'"></div>
2235
+ <template x-for="key in ['popoutWidth', 'fullLimit']" :key="'ed_'+key">
2236
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #f3f4f6;">
2237
+ <span style="font-size: 11px; color: #374151;" x-text="key.replace('Width', '')"></span>
2238
+ <div style="display: flex; align-items: center; gap: 4px;">
2239
+ <input type="number" :value="getNumericValue(key)" @input="updateNumericValue(key, $event.target.value)" step="1"
2240
+ style="width: 72px; padding: 6px 8px; font-size: 11px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; text-align: right;">
2241
+ <select x-show="hasUnitSelector(key)" @change="updateUnit(key, $event.target.value)" :value="getUnit(key)"
2242
+ style="padding: 6px 4px; font-size: 10px; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; color: #6b7280; cursor: pointer; width: 50px; text-align: center;">
2243
+ <template x-for="u in unitOptions" :key="u">
2244
+ <option :value="u" :selected="getUnit(key) === u" x-text="u"></option>
2245
+ </template>
2246
+ </select>
2247
+ <span x-show="!hasUnitSelector(key)" style="font-size: 10px; color: #9ca3af; width: 50px; text-align: center; display: inline-block;" x-text="getUnit(key)"></span>
2248
+ </div>
2249
+ </div>
2250
+ </template>
2251
+ </div>
2252
+
2253
+ <!-- Feature Section (Track Width) -->
2254
+ <div style="padding: 8px 12px; background: white; border-bottom: 1px solid #e5e5e5;">
2255
+ <div @click="copySection('feature')" style="font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; cursor: pointer;" :style="{ color: sectionCopied === 'feature' ? '#10b981' : '#6b7280' }" x-text="sectionCopied === 'feature' ? '✓ Copied' : 'Feature (Track Width)'"></div>
2256
+ <!-- featureMin (locked) -->
2257
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #f3f4f6;">
2258
+ <span style="font-size: 11px; color: #9ca3af;">min <span style="font-size: 8px;" title="Must be 0 for track to collapse">(locked)</span></span>
2259
+ <div style="display: flex; align-items: center; gap: 4px;">
2260
+ <input type="number" :value="getNumericValue('featureMin')" disabled
2261
+ style="width: 72px; padding: 6px 8px; font-size: 11px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 4px; background: #f3f4f6; color: #9ca3af; text-align: right; cursor: not-allowed;">
2262
+ <select disabled style="padding: 6px 4px; font-size: 10px; border: 1px solid #e5e5e5; border-radius: 4px; background: #f3f4f6; color: #9ca3af; width: 50px; text-align: center; cursor: not-allowed;">
2263
+ <option selected>rem</option>
2264
+ </select>
2265
+ </div>
2266
+ </div>
2267
+ <!-- featureScale and featureMax -->
2268
+ <template x-for="key in ['featureScale', 'featureMax']" :key="'ed_'+key">
2269
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #f3f4f6;">
2270
+ <span style="font-size: 11px; color: #374151;" x-text="key.replace('feature', '').toLowerCase()"></span>
2271
+ <div style="display: flex; align-items: center; gap: 4px;">
2272
+ <input type="number" :value="getNumericValue(key)" @input="updateNumericValue(key, $event.target.value)" step="1"
2273
+ style="width: 72px; padding: 6px 8px; font-size: 11px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; text-align: right;">
2274
+ <select x-show="hasUnitSelector(key)" @change="updateUnit(key, $event.target.value)" :value="getUnit(key)"
2275
+ style="padding: 6px 4px; font-size: 10px; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; color: #6b7280; cursor: pointer; width: 50px; text-align: center;">
2276
+ <template x-for="u in unitOptions" :key="u">
2277
+ <option :value="u" :selected="getUnit(key) === u" x-text="u"></option>
2278
+ </template>
2279
+ </select>
2280
+ <span x-show="!hasUnitSelector(key)" style="font-size: 10px; color: #9ca3af; width: 50px; text-align: center; display: inline-block;" x-text="getUnit(key)"></span>
2281
+ </div>
2282
+ </div>
2283
+ </template>
2284
+ </div>
2285
+
2286
+ <!-- Gap Section (Outer Margins) -->
2287
+ <div style="padding: 8px 12px; background: white; border-bottom: 1px solid #e5e5e5;">
2288
+ <div @click="copySection('gap')" style="font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; cursor: pointer;" :style="{ color: sectionCopied === 'gap' ? '#10b981' : '#6b7280' }" x-text="sectionCopied === 'gap' ? '✓ Copied' : 'Outer Margins'"></div>
2289
+ <div style="font-size: 9px; color: #9ca3af; margin-bottom: 8px; line-height: 1.4;">Space between viewport edge and content. Auto-centers your layout.</div>
2290
+
2291
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #f3f4f6;">
2292
+ <div>
2293
+ <span style="font-size: 11px; color: #374151;">min</span>
2294
+ <span style="font-size: 9px; color: #9ca3af; margin-left: 4px;">floor</span>
2295
+ <span style="font-size: 8px; color: #10b981; margin-left: 4px; font-weight: 500;">live</span>
2296
+ </div>
2297
+ <div style="display: flex; align-items: center; gap: 4px;">
2298
+ <input type="number" :value="getNumericValue('baseGap')" @input="updateNumericValue('baseGap', $event.target.value)" step="0.5"
2299
+ style="width: 72px; padding: 6px 8px; font-size: 11px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; text-align: right;">
2300
+ <select @change="updateUnit('baseGap', $event.target.value)" :value="getUnit('baseGap')"
2301
+ style="padding: 6px 4px; font-size: 10px; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; color: #6b7280; cursor: pointer; width: 50px; text-align: center;">
2302
+ <template x-for="u in unitOptions" :key="u">
2303
+ <option :value="u" :selected="getUnit('baseGap') === u" x-text="u"></option>
2304
+ </template>
2305
+ </select>
2306
+ </div>
2307
+ </div>
2308
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #f3f4f6;">
2309
+ <div>
2310
+ <span style="font-size: 11px; color: #374151;">max</span>
2311
+ <span style="font-size: 9px; color: #9ca3af; margin-left: 4px;">ceiling</span>
2312
+ <span style="font-size: 8px; color: #10b981; margin-left: 4px; font-weight: 500;">live</span>
2313
+ </div>
2314
+ <div style="display: flex; align-items: center; gap: 4px;">
2315
+ <input type="number" :value="getNumericValue('maxGap')" @input="updateNumericValue('maxGap', $event.target.value)" step="1"
2316
+ style="width: 72px; padding: 6px 8px; font-size: 11px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; text-align: right;">
2317
+ <select @change="updateUnit('maxGap', $event.target.value)" :value="getUnit('maxGap')"
2318
+ style="padding: 6px 4px; font-size: 10px; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; color: #6b7280; cursor: pointer; width: 50px; text-align: center;">
2319
+ <template x-for="u in unitOptions" :key="u">
2320
+ <option :value="u" :selected="getUnit('maxGap') === u" x-text="u"></option>
2321
+ </template>
2322
+ </select>
2323
+ </div>
2324
+ </div>
2325
+
2326
+ <div style="font-size: 9px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; margin: 10px 0 2px;">Responsive Scale <span style="font-size: 8px; color: #10b981; font-weight: 500; text-transform: none;">live preview</span></div>
2327
+ <div style="font-size: 9px; color: #9ca3af; margin-bottom: 6px; line-height: 1.4;">Fluid value (vw) that grows with viewport. Active breakpoint previews live.</div>
2328
+ <template x-for="key in Object.keys(gapScaleOptions)" :key="'ed_gs_'+key">
2329
+ <div :style="{
2330
+ display: 'flex',
2331
+ alignItems: 'center',
2332
+ justifyContent: 'space-between',
2333
+ padding: '4px 6px',
2334
+ margin: '0 -6px',
2335
+ borderBottom: '1px solid #f3f4f6',
2336
+ borderRadius: '4px',
2337
+ background: (key === 'default' && currentBreakpoint === 'mobile') || key === currentBreakpoint ? 'rgba(249, 115, 22, 0.1)' : 'transparent',
2338
+ border: (key === 'default' && currentBreakpoint === 'mobile') || key === currentBreakpoint ? '1px solid rgba(249, 115, 22, 0.3)' : '1px solid transparent'
2339
+ }">
2340
+ <div style="display: flex; align-items: center; gap: 6px;">
2341
+ <span style="font-size: 11px; color: #374151;" x-text="key === 'default' ? 'mobile' : key"></span>
2342
+ <span x-show="(key === 'default' && currentBreakpoint === 'mobile') || key === currentBreakpoint"
2343
+ style="font-size: 8px; font-weight: 600; color: #f97316;">ACTIVE</span>
2344
+ </div>
2345
+ <div style="display: flex; align-items: center; gap: 4px;">
2346
+ <input type="number" :value="getGapScaleNumeric(key)" @input="updateGapScaleNumeric(key, $event.target.value)" step="1"
2347
+ style="width: 72px; padding: 6px 8px; font-size: 11px; font-family: 'SF Mono', Monaco, monospace; border: 1px solid #e5e5e5; border-radius: 4px; background: #f9fafb; text-align: right;">
2348
+ <span style="font-size: 10px; color: #9ca3af; width: 50px; text-align: center; display: inline-block;" x-text="getGapScaleUnit(key)"></span>
2349
+ </div>
2350
+ </div>
2351
+ </template>
2352
+
2353
+ <!-- Live formula preview -->
2354
+ <div style="margin-top: 8px; padding: 8px; background: #f3f4f6; border-radius: 4px; font-family: 'SF Mono', Monaco, monospace; font-size: 9px; line-height: 1.6;">
2355
+ <div style="color: #6b7280; margin-bottom: 4px;">Generated CSS:</div>
2356
+ <div style="color: #374151;"><span style="color: #9ca3af;">mobile:</span> clamp(<span x-text="editValues.baseGap || configOptions.baseGap.value"></span>, <span x-text="editValues.gapScale_default || gapScaleOptions.default.value"></span>, <span x-text="editValues.maxGap || configOptions.maxGap.value"></span>)</div>
2357
+ <div style="color: #374151;"><span style="color: #9ca3af;">lg:</span> clamp(<span x-text="editValues.baseGap || configOptions.baseGap.value"></span>, <span x-text="editValues.gapScale_lg || gapScaleOptions.lg.value"></span>, <span x-text="editValues.maxGap || configOptions.maxGap.value"></span>)</div>
2358
+ <div style="color: #374151;"><span style="color: #9ca3af;">xl:</span> clamp(<span x-text="editValues.baseGap || configOptions.baseGap.value"></span>, <span x-text="editValues.gapScale_xl || gapScaleOptions.xl.value"></span>, <span x-text="editValues.maxGap || configOptions.maxGap.value"></span>)</div>
2359
+ </div>
2360
+ </div>
2361
+
2362
+ <!-- Action Buttons -->
2363
+ <div style="padding: 10px 12px; background: #f7f7f7; display: flex; gap: 8px;">
2364
+ <button @click="copyConfig()" :style="{ flex: 1, padding: '8px', fontSize: '11px', fontWeight: '600', border: 'none', borderRadius: '4px', cursor: 'pointer', background: copySuccess ? '#10b981' : '#1a1a2e', color: 'white', transition: 'background 0.2s' }">
2365
+ <span x-text="copySuccess ? '✓ Copied' : 'Copy Variables'"></span>
2366
+ </button>
2367
+ <button @click="openRestoreModal()" style="padding: 8px 12px; font-size: 11px; font-weight: 600; border: 1px solid #e5e5e5; border-radius: 4px; cursor: pointer; background: white; color: #374151;" title="Restore from CSS variables">
2368
+ Restore
2369
+ </button>
2370
+ <button @click="downloadCSS()" style="padding: 8px 12px; font-size: 11px; font-weight: 600; border: 1px solid #e5e5e5; border-radius: 4px; cursor: pointer; background: white; color: #374151;">
2371
+ CSS
2372
+ </button>
2373
+ </div>
2374
+ </div>
2375
+ </div>
2376
+
2377
+ <!-- Restore Config Modal -->
2378
+ <div x-show="showRestoreModal"
2379
+ x-transition:enter="transition ease-out duration-200"
2380
+ x-transition:enter-start="opacity-0"
2381
+ x-transition:enter-end="opacity-100"
2382
+ x-transition:leave="transition ease-in duration-150"
2383
+ x-transition:leave-start="opacity-100"
2384
+ x-transition:leave-end="opacity-0"
2385
+ style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 10002; pointer-events: auto;">
2386
+ <div @click.stop style="background: white; border-radius: 8px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); width: 400px; max-width: 90vw; font-family: system-ui, -apple-system, sans-serif;">
2387
+ <!-- Modal Header -->
2388
+ <div style="padding: 12px 16px; background: #1a1a2e; color: white; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center;">
2389
+ <span style="font-weight: 600; font-size: 13px;">Restore Config</span>
2390
+ <button @click="closeRestoreModal()" style="background: transparent; border: none; color: rgba(255,255,255,0.6); cursor: pointer; font-size: 18px; line-height: 1;">&times;</button>
2391
+ </div>
2392
+ <!-- Modal Body -->
2393
+ <div style="padding: 16px;">
2394
+ <p style="font-size: 12px; color: #6b7280; margin: 0 0 8px 0; line-height: 1.5;">Paste the <code style="background: #f3f4f6; padding: 1px 4px; border-radius: 3px;">:root { }</code> block from your exported CSS file:</p>
2395
+ <p style="font-size: 10px; color: #9ca3af; margin: 0 0 12px 0;">Look for "CONFIGURATION VARIABLES" section in _objects.breakout-grid.css</p>
2396
+ <textarea x-model="restoreInput"
2397
+ @keydown.meta.enter="restoreConfig()"
2398
+ @keydown.ctrl.enter="restoreConfig()"
2399
+ placeholder=":root {
2400
+ --base-gap: 1rem;
2401
+ --max-gap: 15rem;
2402
+ --content-min: 53rem;
2403
+ ...
2404
+ }"
2405
+ style="width: 100%; height: 200px; padding: 12px; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; border: 1px solid #e5e5e5; border-radius: 4px; resize: vertical; box-sizing: border-box;"></textarea>
2406
+ <!-- Error message -->
2407
+ <div x-show="restoreError" x-text="restoreError" style="margin-top: 8px; padding: 8px 12px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 4px; color: #dc2626; font-size: 11px;"></div>
2408
+ <p style="font-size: 10px; color: #9ca3af; margin: 8px 0 0 0;">Press ⌘/Ctrl + Enter to apply</p>
2409
+ </div>
2410
+ <!-- Modal Footer -->
2411
+ <div style="padding: 12px 16px; background: #f7f7f7; border-radius: 0 0 8px 8px; display: flex; justify-content: flex-end; gap: 8px;">
2412
+ <button @click="closeRestoreModal()" style="padding: 8px 16px; font-size: 11px; font-weight: 600; border: 1px solid #e5e5e5; border-radius: 4px; cursor: pointer; background: white; color: #374151;">Cancel</button>
2413
+ <button @click="restoreConfig()" style="padding: 8px 16px; font-size: 11px; font-weight: 600; border: none; border-radius: 4px; cursor: pointer; background: #1a1a2e; color: white;">Apply</button>
2414
+ </div>
2415
+ </div>
2416
+ </div>
2417
+
2418
+ <!-- Grid Diagram -->
2419
+ <div x-show="showDiagram"
2420
+ style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border-radius: 0.5rem; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); pointer-events: auto; z-index: 10001; padding: 1.5rem; max-width: 90vw;">
2421
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
2422
+ <span style="font-weight: 700; font-size: 0.875rem; color: #111827;">Breakout Grid Structure</span>
2423
+ <button @click="showDiagram = false" style="background: #ef4444; border: none; color: white; padding: 0.25rem 0.5rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.625rem; font-weight: 600;">Close</button>
2424
+ </div>
2425
+ <!-- Visual Diagram -->
2426
+ <div style="font-family: Monaco, monospace; font-size: 0.625rem; line-height: 1.8;">
2427
+ <!-- Column structure visualization -->
2428
+ <div style="display: flex; align-items: stretch; border: 2px solid #e5e7eb; border-radius: 0.25rem; overflow: hidden; min-height: 120px;">
2429
+ <!-- Full left -->
2430
+ <div style="background: rgba(239, 68, 68, 0.2); padding: 0.5rem 0.25rem; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px dashed #e5e7eb; min-width: 40px;">
2431
+ <div style="writing-mode: vertical-rl; transform: rotate(180deg); color: #dc2626; font-weight: 600;">full</div>
2432
+ <div style="color: #9ca3af; font-size: 0.5rem;">1fr</div>
2433
+ </div>
2434
+ <!-- Feature left -->
2435
+ <div style="background: rgba(6, 182, 212, 0.2); padding: 0.5rem 0.25rem; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px dashed #e5e7eb; min-width: 50px;">
2436
+ <div style="color: #0891b2; font-weight: 600;">feature</div>
2437
+ <div style="color: #9ca3af; font-size: 0.5rem;" x-text="(editValues.featureMin || configOptions.featureMin.value) + ' - ' + (editValues.featureMax || configOptions.featureMax.value)"></div>
2438
+ </div>
2439
+ <!-- Popout left -->
2440
+ <div style="background: rgba(34, 197, 94, 0.2); padding: 0.5rem 0.25rem; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px dashed #e5e7eb; min-width: 40px;">
2441
+ <div style="color: #15803d; font-weight: 600;">popout</div>
2442
+ <div style="color: #9ca3af; font-size: 0.5rem;" x-text="editValues.popoutWidth || configOptions.popoutWidth.value"></div>
2443
+ </div>
2444
+ <!-- Content -->
2445
+ <div style="background: rgba(168, 85, 247, 0.2); padding: 0.5rem; display: flex; flex-direction: column; justify-content: center; align-items: center; flex: 1; min-width: 80px;">
2446
+ <div style="color: #7c3aed; font-weight: 700;">content</div>
2447
+ <div style="color: #9ca3af; font-size: 0.5rem;" x-text="'(' + (editValues.contentMin || configOptions.contentMin.value) + ' - ' + (editValues.contentMax || configOptions.contentMax.value) + ')'"></div>
2448
+ </div>
2449
+ <!-- Popout right -->
2450
+ <div style="background: rgba(34, 197, 94, 0.2); padding: 0.5rem 0.25rem; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px dashed #e5e7eb; min-width: 40px;">
2451
+ <div style="color: #15803d; font-weight: 600;">popout</div>
2452
+ <div style="color: #9ca3af; font-size: 0.5rem;" x-text="editValues.popoutWidth || configOptions.popoutWidth.value"></div>
2453
+ </div>
2454
+ <!-- Feature right -->
2455
+ <div style="background: rgba(6, 182, 212, 0.2); padding: 0.5rem 0.25rem; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px dashed #e5e7eb; min-width: 50px;">
2456
+ <div style="color: #0891b2; font-weight: 600;">feature</div>
2457
+ <div style="color: #9ca3af; font-size: 0.5rem;" x-text="(editValues.featureMin || configOptions.featureMin.value) + ' - ' + (editValues.featureMax || configOptions.featureMax.value)"></div>
2458
+ </div>
2459
+ <!-- Full right -->
2460
+ <div style="background: rgba(239, 68, 68, 0.2); padding: 0.5rem 0.25rem; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px dashed #e5e7eb; min-width: 40px;">
2461
+ <div style="writing-mode: vertical-rl; transform: rotate(180deg); color: #dc2626; font-weight: 600;">full</div>
2462
+ <div style="color: #9ca3af; font-size: 0.5rem;">1fr</div>
2463
+ </div>
2464
+ </div>
2465
+ <!-- Legend -->
2466
+ <div style="margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 0.75rem; font-size: 0.5625rem;">
2467
+ <div><span style="display: inline-block; width: 12px; height: 12px; background: rgba(239, 68, 68, 0.3); border-radius: 2px; vertical-align: middle; margin-right: 0.25rem;"></span>.col-full</div>
2468
+ <div><span style="display: inline-block; width: 12px; height: 12px; background: rgba(6, 182, 212, 0.3); border-radius: 2px; vertical-align: middle; margin-right: 0.25rem;"></span>.col-feature</div>
2469
+ <div><span style="display: inline-block; width: 12px; height: 12px; background: rgba(34, 197, 94, 0.3); border-radius: 2px; vertical-align: middle; margin-right: 0.25rem;"></span>.col-popout</div>
2470
+ <div><span style="display: inline-block; width: 12px; height: 12px; background: rgba(168, 85, 247, 0.3); border-radius: 2px; vertical-align: middle; margin-right: 0.25rem;"></span>.col-content</div>
2471
+ </div>
2472
+ <!-- Padding explanation -->
2473
+ <div style="margin-top: 1rem; padding: 0.75rem; background: #f9fafb; border-radius: 0.25rem; font-size: 0.5625rem; color: #4b5563;">
2474
+ <div style="font-weight: 700; margin-bottom: 0.25rem;">px-breakout aligns full-width content:</div>
2475
+ <div>Uses <span style="color: #3b82f6;" x-text="editValues.popoutWidth || configOptions.popoutWidth.value"></span> padding so content aligns with .col-content edge</div>
2476
+ </div>
2477
+ </div>
2478
+ </div>
2479
+
2480
+ </div>
2481
+ `;
2482
+ (function() {
2483
+ document.addEventListener("alpine:init", () => {
2484
+ Alpine.data("breakoutGridVisualizer", () => ({
2485
+ // Constants
2486
+ version: VERSION,
2487
+ loremContent: LOREM_CONTENT,
2488
+ // Configuration
2489
+ gridAreas: GRID_AREAS,
2490
+ configOptions: CONFIG_OPTIONS,
2491
+ gapScaleOptions: GAP_SCALE_OPTIONS,
2492
+ breakoutOptions: BREAKOUT_OPTIONS,
2493
+ breakpointOptions: BREAKPOINT_OPTIONS,
2494
+ // State
2495
+ ...createInitialState(),
2496
+ // Methods
2497
+ ...methods,
2498
+ // CSS export
2499
+ generateCSSExport,
2500
+ cssExportVersion: BUILD_VERSION,
2501
+ // Template
2502
+ template
2503
+ }));
2504
+ });
2505
+ function injectVisualizer() {
2506
+ if (document.getElementById("breakout-grid-visualizer-root")) return;
2507
+ const container = document.createElement("div");
2508
+ container.id = "breakout-grid-visualizer-root";
2509
+ container.setAttribute("x-data", "breakoutGridVisualizer");
2510
+ container.setAttribute("x-html", "template");
2511
+ document.body.appendChild(container);
2512
+ console.log("Breakout Grid Visualizer injected. Press Ctrl/Cmd + G to toggle.");
2513
+ }
2514
+ if (document.readyState === "loading") {
2515
+ document.addEventListener("DOMContentLoaded", () => {
2516
+ setTimeout(injectVisualizer, 10);
2517
+ });
2518
+ } else {
2519
+ document.addEventListener("alpine:initialized", injectVisualizer);
2520
+ setTimeout(injectVisualizer, 100);
2521
+ }
2522
+ })();
2523
+ })();