@morphika/webframe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/admin/assets.ts +4 -0
- package/admin/database.ts +4 -0
- package/admin/index.ts +6 -0
- package/admin/login.ts +4 -0
- package/admin/navigation.ts +4 -0
- package/admin/pages-editor.ts +4 -0
- package/admin/pages.ts +4 -0
- package/admin/projects-editor.ts +4 -0
- package/admin/projects.ts +4 -0
- package/admin/settings.ts +4 -0
- package/admin/setup.ts +4 -0
- package/admin/storage.ts +4 -0
- package/admin/styles.ts +4 -0
- package/app/(site)/[slug]/loading.tsx +20 -0
- package/app/(site)/[slug]/page.tsx +83 -0
- package/app/(site)/error.tsx +32 -0
- package/app/(site)/layout.tsx +53 -0
- package/app/(site)/loading.tsx +20 -0
- package/app/(site)/not-found.tsx +41 -0
- package/app/(site)/page.tsx +43 -0
- package/app/(site)/preview/page.tsx +99 -0
- package/app/(site)/work/[slug]/loading.tsx +23 -0
- package/app/(site)/work/[slug]/page.tsx +84 -0
- package/app/admin/assets/page.tsx +573 -0
- package/app/admin/database/page.tsx +302 -0
- package/app/admin/error.tsx +53 -0
- package/app/admin/layout.tsx +273 -0
- package/app/admin/login/page.tsx +88 -0
- package/app/admin/navigation/page.tsx +157 -0
- package/app/admin/page.tsx +17 -0
- package/app/admin/pages/[slug]/page.tsx +849 -0
- package/app/admin/pages/page.tsx +588 -0
- package/app/admin/projects/[slug]/page.tsx +3 -0
- package/app/admin/projects/page.tsx +669 -0
- package/app/admin/settings/page.tsx +132 -0
- package/app/admin/setup/page.tsx +64 -0
- package/app/admin/storage/page.tsx +518 -0
- package/app/admin/styles/page.tsx +243 -0
- package/app/api/admin/assets/file/route.ts +81 -0
- package/app/api/admin/assets/health/route.ts +170 -0
- package/app/api/admin/assets/register/route.ts +163 -0
- package/app/api/admin/assets/registry/route.ts +98 -0
- package/app/api/admin/assets/relink/confirm/route.ts +242 -0
- package/app/api/admin/assets/relink/route.ts +202 -0
- package/app/api/admin/assets/scan/route.ts +271 -0
- package/app/api/admin/auth/route.ts +160 -0
- package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
- package/app/api/admin/custom-sections/route.ts +127 -0
- package/app/api/admin/database/route.ts +53 -0
- package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
- package/app/api/admin/pages/[slug]/route.ts +617 -0
- package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
- package/app/api/admin/pages/route.ts +129 -0
- package/app/api/admin/preview/route.ts +53 -0
- package/app/api/admin/r2/connect/route.ts +181 -0
- package/app/api/admin/r2/delete/route.ts +198 -0
- package/app/api/admin/r2/disconnect/route.ts +42 -0
- package/app/api/admin/r2/rename/route.ts +265 -0
- package/app/api/admin/r2/status/route.ts +106 -0
- package/app/api/admin/r2/upload-url/route.ts +148 -0
- package/app/api/admin/revalidate/route.ts +55 -0
- package/app/api/admin/settings/route.ts +279 -0
- package/app/api/admin/setup/complete/route.ts +51 -0
- package/app/api/admin/setup/route.ts +118 -0
- package/app/api/admin/storage/switch/route.ts +117 -0
- package/app/api/admin/styles/fonts/route.ts +97 -0
- package/app/api/admin/styles/route.ts +304 -0
- package/app/api/assets/[...path]/route.ts +98 -0
- package/app/api/custom-sections/[id]/route.ts +43 -0
- package/app/api/draft-mode/disable/route.ts +10 -0
- package/app/api/draft-mode/enable/route.ts +26 -0
- package/app/api/projects/route.ts +42 -0
- package/app/api/styles/route.ts +88 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +7 -0
- package/app/layout.tsx +53 -0
- package/app/robots.ts +17 -0
- package/app/sitemap.ts +48 -0
- package/app/studio/[[...index]]/page.tsx +8 -0
- package/components/admin/MetadataEditor.tsx +173 -0
- package/components/admin/PublishToggle.tsx +130 -0
- package/components/admin/icons.tsx +40 -0
- package/components/admin/nav-builder/NavBuilder.tsx +182 -0
- package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
- package/components/admin/nav-builder/NavGridCell.tsx +48 -0
- package/components/admin/nav-builder/NavGridItem.tsx +189 -0
- package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
- package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
- package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
- package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
- package/components/admin/nav-builder/index.ts +10 -0
- package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
- package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
- package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
- package/components/admin/setup-wizard/DoneStep.tsx +187 -0
- package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
- package/components/admin/setup-wizard/StorageStep.tsx +308 -0
- package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
- package/components/admin/setup-wizard/index.ts +9 -0
- package/components/admin/styles/ColorsEditor.tsx +214 -0
- package/components/admin/styles/FontsEditor.tsx +258 -0
- package/components/admin/styles/GridLayoutEditor.tsx +292 -0
- package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
- package/components/admin/styles/TypographyEditor.tsx +266 -0
- package/components/admin/styles/index.ts +9 -0
- package/components/admin/styles/shared.tsx +68 -0
- package/components/blocks/BlockRenderer.tsx +404 -0
- package/components/blocks/ButtonBlockRenderer.tsx +52 -0
- package/components/blocks/CoverBlockRenderer.tsx +239 -0
- package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
- package/components/blocks/EnterAnimationWrapper.tsx +140 -0
- package/components/blocks/HoverAnimationWrapper.tsx +308 -0
- package/components/blocks/ImageBlockRenderer.tsx +61 -0
- package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
- package/components/blocks/PageBackground.tsx +28 -0
- package/components/blocks/PageNavAnimation.tsx +35 -0
- package/components/blocks/PageNavColor.tsx +24 -0
- package/components/blocks/PageRenderer.tsx +142 -0
- package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
- package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
- package/components/blocks/SectionRenderer.tsx +170 -0
- package/components/blocks/SectionV2Renderer.tsx +330 -0
- package/components/blocks/ShaderCanvas.tsx +392 -0
- package/components/blocks/SpacerBlockRenderer.tsx +17 -0
- package/components/blocks/TextBlockRenderer.tsx +87 -0
- package/components/blocks/TypewriterRichText.tsx +464 -0
- package/components/blocks/TypewriterWrapper.tsx +149 -0
- package/components/blocks/VideoBlockRenderer.tsx +304 -0
- package/components/blocks/index.ts +2 -0
- package/components/builder/AssetBrowser.tsx +2 -0
- package/components/builder/BlockLivePreview.tsx +101 -0
- package/components/builder/BlockTypePicker.tsx +178 -0
- package/components/builder/BuilderCanvas.tsx +354 -0
- package/components/builder/CanvasMinimap.tsx +200 -0
- package/components/builder/CanvasToolbar.tsx +202 -0
- package/components/builder/ColorPicker.tsx +243 -0
- package/components/builder/ColorSwatchPicker.tsx +274 -0
- package/components/builder/ColumnDragContext.tsx +51 -0
- package/components/builder/ColumnDragOverlay.tsx +110 -0
- package/components/builder/CustomSectionInstanceCard.tsx +97 -0
- package/components/builder/DeviceFrame.tsx +123 -0
- package/components/builder/DndWrapper.tsx +337 -0
- package/components/builder/InsertionLines.tsx +186 -0
- package/components/builder/ParallaxGroupCanvas.tsx +228 -0
- package/components/builder/ParallaxSlideHeader.tsx +113 -0
- package/components/builder/ReadOnlyFrame.tsx +417 -0
- package/components/builder/SectionEditorBar.tsx +288 -0
- package/components/builder/SectionTypePicker.tsx +422 -0
- package/components/builder/SectionV2Canvas.tsx +297 -0
- package/components/builder/SectionV2Column.tsx +488 -0
- package/components/builder/SettingsPanel.tsx +911 -0
- package/components/builder/SortableBlock.tsx +230 -0
- package/components/builder/SortableRow.tsx +362 -0
- package/components/builder/VirtualAssetGrid.tsx +397 -0
- package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
- package/components/builder/asset-browser/FileLightbox.tsx +116 -0
- package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
- package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
- package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
- package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
- package/components/builder/asset-browser/helpers.ts +88 -0
- package/components/builder/asset-browser/index.ts +1 -0
- package/components/builder/asset-browser/types.ts +49 -0
- package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
- package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
- package/components/builder/asset-browser/useR2Operations.ts +189 -0
- package/components/builder/blockStyles.tsx +295 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
- package/components/builder/editors/CoverBlockEditor.tsx +488 -0
- package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
- package/components/builder/editors/HoverEffectPicker.tsx +209 -0
- package/components/builder/editors/ImageBlockEditor.tsx +206 -0
- package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
- package/components/builder/editors/ProjectGridEditor.tsx +648 -0
- package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
- package/components/builder/editors/StaggerSettings.tsx +108 -0
- package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
- package/components/builder/editors/TextBlockEditor.tsx +462 -0
- package/components/builder/editors/TextStylePicker.tsx +183 -0
- package/components/builder/editors/VideoBlockEditor.tsx +278 -0
- package/components/builder/editors/index.ts +10 -0
- package/components/builder/editors/shared.tsx +345 -0
- package/components/builder/hooks/useColumnDrag.ts +472 -0
- package/components/builder/hooks/useColumnResize.ts +221 -0
- package/components/builder/index.ts +12 -0
- package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
- package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
- package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
- package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
- package/components/builder/live-preview/index.ts +10 -0
- package/components/builder/live-preview/shared.tsx +153 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
- package/components/builder/settings-panel/BlockSettings.tsx +94 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
- package/components/builder/settings-panel/LayoutTab.tsx +310 -0
- package/components/builder/settings-panel/PageSettings.tsx +200 -0
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
- package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
- package/components/builder/settings-panel/index.ts +19 -0
- package/components/builder/settings-panel/responsive-helpers.ts +524 -0
- package/components/ui/CustomCursor.tsx +118 -0
- package/components/ui/NavContentLightbox.tsx +152 -0
- package/components/ui/Navbar.tsx +582 -0
- package/components/ui/PortfolioTracker.tsx +87 -0
- package/components/ui/ScrollToTop.tsx +47 -0
- package/lib/animation/enter-presets.ts +147 -0
- package/lib/animation/enter-resolve.ts +90 -0
- package/lib/animation/enter-types.ts +128 -0
- package/lib/animation/hover-effect-presets.ts +210 -0
- package/lib/animation/hover-effect-types.ts +126 -0
- package/lib/asset-retry.ts +111 -0
- package/lib/assets.ts +92 -0
- package/lib/audit.ts +35 -0
- package/lib/auth-token.ts +94 -0
- package/lib/auth.ts +13 -0
- package/lib/builder/cascade-helpers.ts +51 -0
- package/lib/builder/cascade.ts +533 -0
- package/lib/builder/constants.ts +103 -0
- package/lib/builder/defaults.ts +182 -0
- package/lib/builder/history.ts +48 -0
- package/lib/builder/index.ts +21 -0
- package/lib/builder/layout-styles.ts +344 -0
- package/lib/builder/masonry.ts +166 -0
- package/lib/builder/responsive.ts +156 -0
- package/lib/builder/serializer.ts +845 -0
- package/lib/builder/store-blocks.ts +193 -0
- package/lib/builder/store-canvas.ts +319 -0
- package/lib/builder/store-helpers.ts +490 -0
- package/lib/builder/store-sections.ts +709 -0
- package/lib/builder/store.ts +333 -0
- package/lib/builder/templates.ts +297 -0
- package/lib/builder/types.ts +374 -0
- package/lib/builder/utils.ts +37 -0
- package/lib/color-utils.ts +116 -0
- package/lib/config/index.ts +57 -0
- package/lib/config/types.ts +122 -0
- package/lib/contexts/AssetContext.tsx +79 -0
- package/lib/contexts/NavAnimationContext.tsx +44 -0
- package/lib/contexts/NavColorContext.tsx +38 -0
- package/lib/contexts/PageExitContext.tsx +194 -0
- package/lib/contexts/ThumbStatusContext.tsx +83 -0
- package/lib/csrf-client.ts +34 -0
- package/lib/csrf.ts +68 -0
- package/lib/format-utils.ts +24 -0
- package/lib/hooks/useViewport.ts +42 -0
- package/lib/logger.ts +81 -0
- package/lib/revalidate.ts +23 -0
- package/lib/sanitize.ts +91 -0
- package/lib/sanity/client.ts +8 -0
- package/lib/sanity/queries.ts +486 -0
- package/lib/sanity/types.ts +869 -0
- package/lib/sanity/writeClient.ts +24 -0
- package/lib/security.ts +402 -0
- package/lib/setup/detect.ts +156 -0
- package/lib/shader/glsl/index.ts +27 -0
- package/lib/shader/glsl/pixelate.ts +51 -0
- package/lib/shader/glsl/rgb-shift.ts +45 -0
- package/lib/shader/glsl/ripple.ts +46 -0
- package/lib/shader/glsl/vertex.ts +14 -0
- package/lib/storage/index.ts +211 -0
- package/lib/storage/r2-adapter.ts +286 -0
- package/lib/storage/types.ts +125 -0
- package/lib/styles/provider.tsx +267 -0
- package/lib/thumbnails/generate.ts +151 -0
- package/lib/utils.ts +6 -0
- package/package.json +212 -0
- package/sanity/compose.ts +65 -0
- package/sanity/sanity.config.ts +126 -0
- package/sanity/schemas/assetRegistry.ts +301 -0
- package/sanity/schemas/blocks/blockLayout.ts +90 -0
- package/sanity/schemas/blocks/buttonBlock.ts +82 -0
- package/sanity/schemas/blocks/coverBlock.ts +229 -0
- package/sanity/schemas/blocks/imageBlock.ts +58 -0
- package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
- package/sanity/schemas/blocks/index.ts +9 -0
- package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
- package/sanity/schemas/blocks/spacerBlock.ts +41 -0
- package/sanity/schemas/blocks/textBlock.ts +139 -0
- package/sanity/schemas/blocks/videoBlock.ts +80 -0
- package/sanity/schemas/customSection.ts +69 -0
- package/sanity/schemas/customSectionInstance.ts +163 -0
- package/sanity/schemas/index.ts +111 -0
- package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
- package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
- package/sanity/schemas/objects/parallaxGroup.ts +66 -0
- package/sanity/schemas/objects/parallaxSlide.ts +217 -0
- package/sanity/schemas/objects/typewriterConfig.ts +38 -0
- package/sanity/schemas/page.ts +162 -0
- package/sanity/schemas/pageSection.ts +157 -0
- package/sanity/schemas/pageSectionV2.ts +269 -0
- package/sanity/schemas/siteSettings.ts +256 -0
- package/sanity/schemas/siteStyles.ts +210 -0
- package/site/error.ts +4 -0
- package/site/index.ts +8 -0
- package/site/not-found.ts +4 -0
- package/site/page.ts +4 -0
- package/site/preview.ts +4 -0
- package/site/robots.ts +4 -0
- package/site/sitemap.ts +4 -0
- package/site/work.ts +4 -0
- package/studio/index.ts +4 -0
- package/styles/admin.css +85 -0
- package/styles/animations.css +237 -0
- package/styles/base.css +148 -0
- package/styles/globals.css +10 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push Cascade Engine — V2 Grid System
|
|
3
|
+
*
|
|
4
|
+
* The central algorithm driving resize, drag & drop, and add column.
|
|
5
|
+
* A single, reusable pure function engine.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* 1. Horizontal push within a row: When column A grows or is inserted,
|
|
9
|
+
* everything to its right shifts rightward.
|
|
10
|
+
* 2. Overflow → drop to next row: If a column exceeds grid_columns,
|
|
11
|
+
* it moves to the next row at position 1, keeping its span.
|
|
12
|
+
* 3. Cascade continues: If the dropped column collides with columns in
|
|
13
|
+
* the destination row, horizontal push applies again.
|
|
14
|
+
* 4. Always terminates: In the worst case, each column occupies its own row.
|
|
15
|
+
* 5. Column constraints: Span 1–grid_columns (integer). Grid positions are integers.
|
|
16
|
+
* 6. Atomic undo: The entire cascade is a single history entry.
|
|
17
|
+
*
|
|
18
|
+
* Session 83: V2 Grid System — Phase 1.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { SectionColumn } from "../../lib/sanity/types";
|
|
22
|
+
import { logger } from "../../lib/logger";
|
|
23
|
+
|
|
24
|
+
// ============================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================
|
|
27
|
+
|
|
28
|
+
/** Minimal column representation for cascade computation */
|
|
29
|
+
export interface CascadeColumn {
|
|
30
|
+
_key: string;
|
|
31
|
+
grid_column: number; // 1-based
|
|
32
|
+
grid_row: number; // 1-based
|
|
33
|
+
span: number; // 1–gridColumns
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================
|
|
37
|
+
// Core Cascade
|
|
38
|
+
// ============================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compute the push cascade for a flat list of columns within a section.
|
|
42
|
+
*
|
|
43
|
+
* Given a set of columns (potentially with overlapping or overflowing positions),
|
|
44
|
+
* resolves all conflicts by pushing columns rightward and dropping to new rows
|
|
45
|
+
* when they overflow.
|
|
46
|
+
*
|
|
47
|
+
* @param columns - The columns to resolve (will NOT be mutated)
|
|
48
|
+
* @param gridColumns - Total columns in the grid (default 12)
|
|
49
|
+
* @returns A new array of columns with valid, non-overlapping positions
|
|
50
|
+
*/
|
|
51
|
+
export function computeCascade(
|
|
52
|
+
columns: CascadeColumn[],
|
|
53
|
+
gridColumns: number = 12
|
|
54
|
+
): CascadeColumn[] {
|
|
55
|
+
if (columns.length === 0) return [];
|
|
56
|
+
|
|
57
|
+
// Defensive validation — warn on invalid input but don't throw (self-healing)
|
|
58
|
+
if (gridColumns < 1 || !Number.isInteger(gridColumns)) {
|
|
59
|
+
logger.warn("[Cascade]", "Invalid gridColumns, defaulting to 12", { gridColumns });
|
|
60
|
+
gridColumns = 12;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Deep clone to avoid mutation
|
|
64
|
+
const cols = columns.map((c) => {
|
|
65
|
+
// Clamp and warn on out-of-range values
|
|
66
|
+
const span = Math.max(1, Math.min(Math.round(c.span), gridColumns));
|
|
67
|
+
const grid_column = Math.max(1, Math.round(c.grid_column));
|
|
68
|
+
const grid_row = Math.max(1, Math.round(c.grid_row));
|
|
69
|
+
|
|
70
|
+
if (span !== c.span || grid_column !== c.grid_column || grid_row !== c.grid_row) {
|
|
71
|
+
logger.warn("[Cascade]", "Clamped invalid column values", {
|
|
72
|
+
key: c._key,
|
|
73
|
+
original: { span: c.span, grid_column: c.grid_column, grid_row: c.grid_row },
|
|
74
|
+
clamped: { span, grid_column, grid_row },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ...c, span, grid_column, grid_row };
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Convergence loop: resolve overlaps and overflows across all rows
|
|
82
|
+
// until no more changes occur. Handles cascading effects where
|
|
83
|
+
// pushing a column to a new row creates new conflicts.
|
|
84
|
+
const maxIterations = cols.length * gridColumns; // Safety limit
|
|
85
|
+
let stable = false;
|
|
86
|
+
let passes = 0;
|
|
87
|
+
while (!stable && passes < maxIterations) {
|
|
88
|
+
passes++;
|
|
89
|
+
stable = true;
|
|
90
|
+
const allRows = [...new Set(cols.map((c) => c.grid_row))].sort((a, b) => a - b);
|
|
91
|
+
|
|
92
|
+
for (const row of allRows) {
|
|
93
|
+
const rowCols = cols
|
|
94
|
+
.filter((c) => c.grid_row === row)
|
|
95
|
+
.sort((a, b) => a.grid_column - b.grid_column);
|
|
96
|
+
|
|
97
|
+
if (rowCols.length <= 1) continue;
|
|
98
|
+
|
|
99
|
+
// Resolve overlaps
|
|
100
|
+
const hadOverlap = resolveRowOverlaps(rowCols);
|
|
101
|
+
if (hadOverlap) stable = false;
|
|
102
|
+
|
|
103
|
+
// Check for overflows
|
|
104
|
+
for (const col of rowCols) {
|
|
105
|
+
if (col.grid_column + col.span - 1 > gridColumns) {
|
|
106
|
+
col.grid_row = row + 1;
|
|
107
|
+
col.grid_column = 1;
|
|
108
|
+
stable = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!stable) {
|
|
115
|
+
logger.warn("[Cascade]", "Convergence limit reached — layout may have unresolved overlaps", {
|
|
116
|
+
passes,
|
|
117
|
+
maxIterations,
|
|
118
|
+
columnCount: cols.length,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return cols;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve overlaps within a single row by pushing columns rightward.
|
|
127
|
+
* Columns must be sorted by grid_column (ascending).
|
|
128
|
+
* Mutates the columns in place.
|
|
129
|
+
*
|
|
130
|
+
* @returns true if any columns were moved
|
|
131
|
+
*/
|
|
132
|
+
function resolveRowOverlaps(rowCols: CascadeColumn[]): boolean {
|
|
133
|
+
let moved = false;
|
|
134
|
+
|
|
135
|
+
for (let i = 1; i < rowCols.length; i++) {
|
|
136
|
+
const prev = rowCols[i - 1];
|
|
137
|
+
const curr = rowCols[i];
|
|
138
|
+
const prevEnd = prev.grid_column + prev.span; // 1 past the end of prev
|
|
139
|
+
|
|
140
|
+
if (curr.grid_column < prevEnd) {
|
|
141
|
+
// Overlap — push current to just after prev
|
|
142
|
+
curr.grid_column = prevEnd;
|
|
143
|
+
moved = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return moved;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get the maximum row number across all columns.
|
|
152
|
+
*/
|
|
153
|
+
function getMaxRow(cols: CascadeColumn[]): number {
|
|
154
|
+
if (cols.length === 0) return 0;
|
|
155
|
+
return Math.max(...cols.map((c) => c.grid_row));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================
|
|
159
|
+
// Preset Detection
|
|
160
|
+
// ============================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Detect which preset a set of columns matches, if any.
|
|
164
|
+
* Returns "custom" if no preset matches.
|
|
165
|
+
*/
|
|
166
|
+
export function detectPreset(
|
|
167
|
+
columns: CascadeColumn[],
|
|
168
|
+
gridColumns: number = 12
|
|
169
|
+
): "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3" | "custom" {
|
|
170
|
+
// All columns must be on row 1 for a preset match
|
|
171
|
+
if (columns.length === 0) return "full";
|
|
172
|
+
if (columns.some((c) => c.grid_row !== 1)) return "custom";
|
|
173
|
+
|
|
174
|
+
const sorted = [...columns].sort((a, b) => a.grid_column - b.grid_column);
|
|
175
|
+
const spans = sorted.map((c) => c.span);
|
|
176
|
+
|
|
177
|
+
// Check contiguous placement (no gaps)
|
|
178
|
+
let expectedCol = 1;
|
|
179
|
+
for (const col of sorted) {
|
|
180
|
+
if (col.grid_column !== expectedCol) return "custom";
|
|
181
|
+
expectedCol = col.grid_column + col.span;
|
|
182
|
+
}
|
|
183
|
+
// Must fill exactly gridColumns
|
|
184
|
+
if (expectedCol - 1 !== gridColumns) return "custom";
|
|
185
|
+
|
|
186
|
+
// Match against known presets
|
|
187
|
+
const key = spans.join(",");
|
|
188
|
+
|
|
189
|
+
// For 12-column grid, specific common presets
|
|
190
|
+
if (gridColumns === 12) {
|
|
191
|
+
if (key === "12") return "full";
|
|
192
|
+
if (key === "6,6") return "halves";
|
|
193
|
+
if (key === "4,4,4") return "thirds";
|
|
194
|
+
if (key === "3,3,3,3") return "quarters";
|
|
195
|
+
if (key === "4,8") return "1/3+2/3";
|
|
196
|
+
if (key === "8,4") return "2/3+1/3";
|
|
197
|
+
} else {
|
|
198
|
+
// Generic preset detection for non-12 grids
|
|
199
|
+
if (spans.length === 1 && spans[0] === gridColumns) return "full";
|
|
200
|
+
if (spans.length === 2 && spans[0] === spans[1] && spans[0] * 2 === gridColumns) return "halves";
|
|
201
|
+
if (spans.length === 3 && spans.every((s) => s === spans[0]) && spans[0] * 3 === gridColumns) return "thirds";
|
|
202
|
+
if (spans.length === 4 && spans.every((s) => s === spans[0]) && spans[0] * 4 === gridColumns) return "quarters";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return "custom";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================
|
|
209
|
+
// Column Operations (use cascade internally)
|
|
210
|
+
// ============================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resize a column to a new span, applying push cascade.
|
|
214
|
+
*
|
|
215
|
+
* @param columns - All columns in the section
|
|
216
|
+
* @param columnKey - The column to resize
|
|
217
|
+
* @param newSpan - The desired new span
|
|
218
|
+
* @param gridColumns - Total columns in the grid
|
|
219
|
+
* @returns Updated columns after cascade
|
|
220
|
+
*/
|
|
221
|
+
export function resizeColumn(
|
|
222
|
+
columns: CascadeColumn[],
|
|
223
|
+
columnKey: string,
|
|
224
|
+
newSpan: number,
|
|
225
|
+
gridColumns: number = 12
|
|
226
|
+
): CascadeColumn[] {
|
|
227
|
+
if (!columns.find((c) => c._key === columnKey)) {
|
|
228
|
+
logger.warn("[Cascade]", "resizeColumn: column not found", { columnKey });
|
|
229
|
+
return columns.map((c) => ({ ...c }));
|
|
230
|
+
}
|
|
231
|
+
const clampedSpan = Math.max(1, Math.min(newSpan, gridColumns));
|
|
232
|
+
const updated = columns.map((c) =>
|
|
233
|
+
c._key === columnKey ? { ...c, span: clampedSpan } : { ...c }
|
|
234
|
+
);
|
|
235
|
+
return computeCascade(updated, gridColumns);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resize a column from the left edge (stretch left).
|
|
240
|
+
* Moves the column's start position leftward and increases span.
|
|
241
|
+
*
|
|
242
|
+
* When expanding left into a neighbor, the neighbor is compressed
|
|
243
|
+
* (its span shrinks to make room) down to a minimum of 1 column.
|
|
244
|
+
* Only the immediate left neighbor is affected — no recursive cascade.
|
|
245
|
+
*
|
|
246
|
+
* Returns null only when truly impossible (column not found, or the
|
|
247
|
+
* neighbor is already at span 1 and can't shrink further).
|
|
248
|
+
*
|
|
249
|
+
* The `neighborAtMinimum` flag in the result signals that the left
|
|
250
|
+
* neighbor has been compressed to its minimum span (1), so the UI
|
|
251
|
+
* can show a visual indicator that further expansion is blocked.
|
|
252
|
+
*
|
|
253
|
+
* @param columns - All columns in the section
|
|
254
|
+
* @param columnKey - The column to resize
|
|
255
|
+
* @param newGridColumn - The desired new start position
|
|
256
|
+
* @param gridColumns - Total columns in the grid
|
|
257
|
+
* @returns Updated columns + metadata, or null if completely blocked
|
|
258
|
+
*/
|
|
259
|
+
export interface ResizeLeftResult {
|
|
260
|
+
columns: CascadeColumn[];
|
|
261
|
+
/** Key of the left neighbor that was compressed, if any */
|
|
262
|
+
compressedNeighborKey: string | null;
|
|
263
|
+
/** True when the left neighbor is at minimum span (1) — further expansion blocked */
|
|
264
|
+
neighborAtMinimum: boolean;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function resizeColumnLeft(
|
|
268
|
+
columns: CascadeColumn[],
|
|
269
|
+
columnKey: string,
|
|
270
|
+
newGridColumn: number,
|
|
271
|
+
gridColumns: number = 12
|
|
272
|
+
): ResizeLeftResult | null {
|
|
273
|
+
const col = columns.find((c) => c._key === columnKey);
|
|
274
|
+
if (!col) return null;
|
|
275
|
+
|
|
276
|
+
// The right edge stays fixed: rightEdge = col.grid_column + col.span - 1
|
|
277
|
+
const rightEdge = col.grid_column + col.span - 1;
|
|
278
|
+
|
|
279
|
+
// Clamp: can't go past position 1 on the left, and can't go past
|
|
280
|
+
// rightEdge on the right (minimum span of 1)
|
|
281
|
+
let clampedStart = Math.max(1, Math.min(newGridColumn, rightEdge));
|
|
282
|
+
|
|
283
|
+
// Find the column immediately to the left in the same row
|
|
284
|
+
const sameRowCols = columns
|
|
285
|
+
.filter((c) => c.grid_row === col.grid_row && c._key !== columnKey)
|
|
286
|
+
.sort((a, b) => a.grid_column - b.grid_column);
|
|
287
|
+
|
|
288
|
+
const leftNeighbor = sameRowCols
|
|
289
|
+
.filter((c) => c.grid_column + c.span - 1 < col.grid_column)
|
|
290
|
+
.pop(); // rightmost column that ends before us
|
|
291
|
+
|
|
292
|
+
let compressedNeighborKey: string | null = null;
|
|
293
|
+
let neighborAtMinimum = false;
|
|
294
|
+
let neighborNewSpan: number | null = null;
|
|
295
|
+
|
|
296
|
+
if (leftNeighbor) {
|
|
297
|
+
// Hard floor: neighbor can't shrink below span 1
|
|
298
|
+
const neighborAbsoluteMin = leftNeighbor.grid_column + 1; // neighbor at span 1 ends here
|
|
299
|
+
const freeStart = leftNeighbor.grid_column + leftNeighbor.span; // current free position
|
|
300
|
+
|
|
301
|
+
if (clampedStart < freeStart) {
|
|
302
|
+
// We're pushing into the neighbor — compress it
|
|
303
|
+
// The neighbor's new span = clampedStart - neighbor.grid_column
|
|
304
|
+
const desiredNeighborSpan = clampedStart - leftNeighbor.grid_column;
|
|
305
|
+
|
|
306
|
+
if (desiredNeighborSpan < 1) {
|
|
307
|
+
// Can't compress further — clamp our start to neighbor's absolute minimum
|
|
308
|
+
clampedStart = neighborAbsoluteMin;
|
|
309
|
+
neighborNewSpan = 1;
|
|
310
|
+
neighborAtMinimum = true;
|
|
311
|
+
} else {
|
|
312
|
+
neighborNewSpan = desiredNeighborSpan;
|
|
313
|
+
neighborAtMinimum = desiredNeighborSpan === 1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
compressedNeighborKey = leftNeighbor._key;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Compute new span from the fixed right edge
|
|
321
|
+
const newSpan = rightEdge - clampedStart + 1;
|
|
322
|
+
|
|
323
|
+
if (newSpan < 1 || newSpan > gridColumns) return null;
|
|
324
|
+
|
|
325
|
+
// If nothing changed (same position as current), return null
|
|
326
|
+
if (clampedStart === col.grid_column && !compressedNeighborKey) return null;
|
|
327
|
+
|
|
328
|
+
const updated = columns.map((c) => {
|
|
329
|
+
if (c._key === columnKey) {
|
|
330
|
+
return { ...c, grid_column: clampedStart, span: newSpan };
|
|
331
|
+
}
|
|
332
|
+
if (c._key === compressedNeighborKey && neighborNewSpan !== null) {
|
|
333
|
+
return { ...c, span: neighborNewSpan };
|
|
334
|
+
}
|
|
335
|
+
return { ...c };
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return { columns: updated, compressedNeighborKey, neighborAtMinimum };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Add a new column at a specific gap position.
|
|
343
|
+
* The column fills the available gap space.
|
|
344
|
+
*
|
|
345
|
+
* @param columns - All columns in the section
|
|
346
|
+
* @param gridRow - The row to add the column in
|
|
347
|
+
* @param gridColumn - The start position for the new column
|
|
348
|
+
* @param span - The span for the new column
|
|
349
|
+
* @param columnKey - The _key for the new column
|
|
350
|
+
* @param gridColumns - Total columns in the grid
|
|
351
|
+
* @returns Updated columns with the new column added, after cascade
|
|
352
|
+
*/
|
|
353
|
+
export function addColumn(
|
|
354
|
+
columns: CascadeColumn[],
|
|
355
|
+
gridRow: number,
|
|
356
|
+
gridColumn: number,
|
|
357
|
+
span: number,
|
|
358
|
+
columnKey: string,
|
|
359
|
+
gridColumns: number = 12
|
|
360
|
+
): CascadeColumn[] {
|
|
361
|
+
const newCol: CascadeColumn = {
|
|
362
|
+
_key: columnKey,
|
|
363
|
+
grid_column: gridColumn,
|
|
364
|
+
grid_row: gridRow,
|
|
365
|
+
span: Math.max(1, Math.min(span, gridColumns)),
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const updated = [...columns.map((c) => ({ ...c })), newCol];
|
|
369
|
+
return computeCascade(updated, gridColumns);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Move a column to a new position (drag & drop).
|
|
374
|
+
*
|
|
375
|
+
* @param columns - All columns in the section
|
|
376
|
+
* @param columnKey - The column to move
|
|
377
|
+
* @param targetRow - The destination row
|
|
378
|
+
* @param targetColumn - The destination grid column
|
|
379
|
+
* @param gridColumns - Total columns in the grid
|
|
380
|
+
* @returns Updated columns after cascade
|
|
381
|
+
*/
|
|
382
|
+
export function moveColumn(
|
|
383
|
+
columns: CascadeColumn[],
|
|
384
|
+
columnKey: string,
|
|
385
|
+
targetRow: number,
|
|
386
|
+
targetColumn: number,
|
|
387
|
+
gridColumns: number = 12
|
|
388
|
+
): CascadeColumn[] {
|
|
389
|
+
if (!columns.find((c) => c._key === columnKey)) {
|
|
390
|
+
logger.warn("[Cascade]", "moveColumn: column not found", { columnKey });
|
|
391
|
+
return columns.map((c) => ({ ...c }));
|
|
392
|
+
}
|
|
393
|
+
const updated = columns.map((c) =>
|
|
394
|
+
c._key === columnKey
|
|
395
|
+
? { ...c, grid_row: targetRow, grid_column: Math.max(1, Math.min(targetColumn, gridColumns)) }
|
|
396
|
+
: { ...c }
|
|
397
|
+
);
|
|
398
|
+
return computeCascade(updated, gridColumns);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Delete a column. Returns the remaining columns (no cascade needed
|
|
403
|
+
* since deletion only creates empty space, and per the spec,
|
|
404
|
+
* remaining columns do NOT collapse into the gap).
|
|
405
|
+
*
|
|
406
|
+
* @param columns - All columns in the section
|
|
407
|
+
* @param columnKey - The column to delete
|
|
408
|
+
* @returns Updated columns with the column removed
|
|
409
|
+
*/
|
|
410
|
+
export function deleteColumn(
|
|
411
|
+
columns: CascadeColumn[],
|
|
412
|
+
columnKey: string
|
|
413
|
+
): CascadeColumn[] {
|
|
414
|
+
return columns.filter((c) => c._key !== columnKey).map((c) => ({ ...c }));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ============================================
|
|
418
|
+
// Gap Detection (for + Add Column buttons)
|
|
419
|
+
// ============================================
|
|
420
|
+
|
|
421
|
+
export interface GridGap {
|
|
422
|
+
grid_row: number;
|
|
423
|
+
grid_column: number; // start position of the gap
|
|
424
|
+
span: number; // width of the gap in grid columns
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Find all empty gaps in the grid where "+ Add Column" buttons should appear.
|
|
429
|
+
*
|
|
430
|
+
* @param columns - All columns in the section
|
|
431
|
+
* @param gridColumns - Total columns in the grid
|
|
432
|
+
* @returns Array of gaps sorted by row then column
|
|
433
|
+
*/
|
|
434
|
+
export function findGaps(
|
|
435
|
+
columns: CascadeColumn[],
|
|
436
|
+
gridColumns: number = 12
|
|
437
|
+
): GridGap[] {
|
|
438
|
+
if (columns.length === 0) {
|
|
439
|
+
// Entire grid is one big gap
|
|
440
|
+
return [{ grid_row: 1, grid_column: 1, span: gridColumns }];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const gaps: GridGap[] = [];
|
|
444
|
+
const maxRow = getMaxRow(columns);
|
|
445
|
+
|
|
446
|
+
for (let row = 1; row <= maxRow; row++) {
|
|
447
|
+
const rowCols = columns
|
|
448
|
+
.filter((c) => c.grid_row === row)
|
|
449
|
+
.sort((a, b) => a.grid_column - b.grid_column);
|
|
450
|
+
|
|
451
|
+
if (rowCols.length === 0) {
|
|
452
|
+
// Empty row — entire row is a gap
|
|
453
|
+
gaps.push({ grid_row: row, grid_column: 1, span: gridColumns });
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Check gap before first column
|
|
458
|
+
if (rowCols[0].grid_column > 1) {
|
|
459
|
+
gaps.push({
|
|
460
|
+
grid_row: row,
|
|
461
|
+
grid_column: 1,
|
|
462
|
+
span: rowCols[0].grid_column - 1,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Check gaps between columns
|
|
467
|
+
for (let i = 0; i < rowCols.length - 1; i++) {
|
|
468
|
+
const endOfCurrent = rowCols[i].grid_column + rowCols[i].span;
|
|
469
|
+
const startOfNext = rowCols[i + 1].grid_column;
|
|
470
|
+
if (startOfNext > endOfCurrent) {
|
|
471
|
+
gaps.push({
|
|
472
|
+
grid_row: row,
|
|
473
|
+
grid_column: endOfCurrent,
|
|
474
|
+
span: startOfNext - endOfCurrent,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check gap after last column
|
|
480
|
+
const lastCol = rowCols[rowCols.length - 1];
|
|
481
|
+
const endOfLast = lastCol.grid_column + lastCol.span;
|
|
482
|
+
if (endOfLast <= gridColumns) {
|
|
483
|
+
gaps.push({
|
|
484
|
+
grid_row: row,
|
|
485
|
+
grid_column: endOfLast,
|
|
486
|
+
span: gridColumns - endOfLast + 1,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return gaps;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ============================================
|
|
495
|
+
// Preset → Columns factory
|
|
496
|
+
// ============================================
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Create columns from a preset layout.
|
|
500
|
+
*
|
|
501
|
+
* @param preset - The layout preset
|
|
502
|
+
* @param gridColumns - Total columns in the grid (default 12)
|
|
503
|
+
* @param keyFactory - Function to generate unique keys
|
|
504
|
+
* @returns Array of columns matching the preset layout
|
|
505
|
+
*/
|
|
506
|
+
export function columnsFromPreset(
|
|
507
|
+
preset: "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3",
|
|
508
|
+
gridColumns: number = 12,
|
|
509
|
+
keyFactory: () => string = () => Math.random().toString(36).substring(2, 14)
|
|
510
|
+
): CascadeColumn[] {
|
|
511
|
+
const presetSpans: Record<string, number[]> = {
|
|
512
|
+
full: [gridColumns],
|
|
513
|
+
halves: [gridColumns / 2, gridColumns / 2],
|
|
514
|
+
thirds: [gridColumns / 3, gridColumns / 3, gridColumns / 3],
|
|
515
|
+
quarters: [gridColumns / 4, gridColumns / 4, gridColumns / 4, gridColumns / 4],
|
|
516
|
+
"1/3+2/3": [Math.round(gridColumns / 3), gridColumns - Math.round(gridColumns / 3)],
|
|
517
|
+
"2/3+1/3": [gridColumns - Math.round(gridColumns / 3), Math.round(gridColumns / 3)],
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const spans = presetSpans[preset] || [gridColumns];
|
|
521
|
+
let currentCol = 1;
|
|
522
|
+
|
|
523
|
+
return spans.map((span) => {
|
|
524
|
+
const col: CascadeColumn = {
|
|
525
|
+
_key: keyFactory(),
|
|
526
|
+
grid_column: currentCol,
|
|
527
|
+
grid_row: 1,
|
|
528
|
+
span: Math.round(span),
|
|
529
|
+
};
|
|
530
|
+
currentCol += Math.round(span);
|
|
531
|
+
return col;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for the builder and public site renderers.
|
|
3
|
+
* Consolidates duplicated maps and utility functions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// ============================================
|
|
8
|
+
// Responsive Breakpoints
|
|
9
|
+
// ============================================
|
|
10
|
+
// Single source of truth for all media query breakpoints.
|
|
11
|
+
// Import these instead of hardcoding numeric values.
|
|
12
|
+
|
|
13
|
+
export const BREAKPOINTS = {
|
|
14
|
+
/** Phone max-width breakpoint (640px) */
|
|
15
|
+
phone: 640,
|
|
16
|
+
/** Scroll animation disable breakpoint (768px) */
|
|
17
|
+
mobileAnimation: 768,
|
|
18
|
+
/** Tablet max-width breakpoint (1024px) */
|
|
19
|
+
tablet: 1024,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// Default Grid Width
|
|
24
|
+
// ============================================
|
|
25
|
+
// The default page grid max-width in pixels.
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_GRID_WIDTH = "1445";
|
|
28
|
+
export const DEFAULT_GRID_WIDTH_PX = `${DEFAULT_GRID_WIDTH}px`;
|
|
29
|
+
|
|
30
|
+
// ============================================
|
|
31
|
+
// Default Page Colors
|
|
32
|
+
// ============================================
|
|
33
|
+
// Default background and text colors for new pages.
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_BG_COLOR = "#ffffff";
|
|
36
|
+
export const DEFAULT_TEXT_COLOR = "#000000";
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// Admin UI Colors
|
|
40
|
+
// ============================================
|
|
41
|
+
// Accent colors used throughout the admin/builder interface.
|
|
42
|
+
// These are also available as CSS custom properties:
|
|
43
|
+
// --admin-accent, --admin-accent-dark
|
|
44
|
+
|
|
45
|
+
export const ADMIN_ACCENT = "#076bff";
|
|
46
|
+
export const ADMIN_ACCENT_DARK = "#0559d4";
|
|
47
|
+
export const ADMIN_ACCENT_SHADOW = "rgba(7,107,255,0.06)";
|
|
48
|
+
export const ADMIN_ERROR = "#ed3821";
|
|
49
|
+
export const ADMIN_ERROR_DARK = "#d42f1a";
|
|
50
|
+
|
|
51
|
+
// ============================================
|
|
52
|
+
// Builder Semantic Color System
|
|
53
|
+
// ============================================
|
|
54
|
+
// Each UI entity in the builder has a dedicated color to provide
|
|
55
|
+
// visual differentiation at a glance. This is a design rule that
|
|
56
|
+
// MUST be followed across all builder components.
|
|
57
|
+
//
|
|
58
|
+
// BLUE (#076bff) — Columns: outlines, resize handles, drag grip,
|
|
59
|
+
// span badge, column selection/hover chrome.
|
|
60
|
+
// ORANGE (#e28b00) — Blocks: "+ Add Block" buttons, block toolbar
|
|
61
|
+
// pill, block selection ring, block-level actions.
|
|
62
|
+
// GREEN (#22c55e) — Drop zones: gap drop targets during drag,
|
|
63
|
+
// insertion lines, "Drop Here" labels, swap target
|
|
64
|
+
// highlight (green border + tinted background).
|
|
65
|
+
// VIOLET (#8b5cf6) — Custom sections: saved section cards, custom
|
|
66
|
+
// section instance badges, section editor chrome,
|
|
67
|
+
// "Create New" custom section button.
|
|
68
|
+
//
|
|
69
|
+
// When adding new builder UI, pick the color that matches the entity
|
|
70
|
+
// being represented, not the action being performed. For example, a
|
|
71
|
+
// delete button on a column is BLUE (it belongs to the column chrome),
|
|
72
|
+
// while a delete button on a block toolbar is ORANGE.
|
|
73
|
+
|
|
74
|
+
export const BUILDER_BLUE = "#076bff"; // Columns
|
|
75
|
+
export const BUILDER_ORANGE = "#e28b00"; // Blocks
|
|
76
|
+
export const BUILDER_GREEN = "#22c55e"; // Drop zones
|
|
77
|
+
export const BUILDER_VIOLET = "#8b5cf6"; // Custom sections
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Padding map for Row settings (in pixels)
|
|
81
|
+
* Used by builder (SortableRow, ReadOnlyFrame) and public site (RowRenderer)
|
|
82
|
+
*/
|
|
83
|
+
export const PADDING_MAP: Record<string, string> = {
|
|
84
|
+
none: "0",
|
|
85
|
+
small: "16px",
|
|
86
|
+
medium: "32px",
|
|
87
|
+
large: "64px",
|
|
88
|
+
xlarge: "96px",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Padding map for Row settings (in Tailwind classes)
|
|
93
|
+
* Used only by public site (RowRenderer) when using class-based padding
|
|
94
|
+
*/
|
|
95
|
+
export const PADDING_CLASSES_MAP: Record<string, string> = {
|
|
96
|
+
none: "py-0",
|
|
97
|
+
small: "py-4",
|
|
98
|
+
medium: "py-8",
|
|
99
|
+
large: "py-16",
|
|
100
|
+
xlarge: "py-24",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
|