diff --git a/packages/renderer-parity.json b/packages/renderer-parity.json
index 2238e503e..3ae60bd0e 100644
--- a/packages/renderer-parity.json
+++ b/packages/renderer-parity.json
@@ -134,6 +134,8 @@
"artisanpack/accordion-title",
"artisanpack/accordion-body",
"artisanpack/tabs",
- "artisanpack/tab-section"
+ "artisanpack/tab-section",
+ "artisanpack/grid",
+ "artisanpack/grid-item"
]
}
diff --git a/packages/visual-editor-renderer-blade/resources/assets/frontend/grid.css b/packages/visual-editor-renderer-blade/resources/assets/frontend/grid.css
new file mode 100644
index 000000000..78f27aacc
--- /dev/null
+++ b/packages/visual-editor-renderer-blade/resources/assets/frontend/grid.css
@@ -0,0 +1,312 @@
+/**
+ * Grid family styles (#498).
+ *
+ * Loaded in the editor via `blocks/{grid,grid-item}/index.ts`. On the
+ * public frontend the Blade renderer serves an identical copy from
+ * `packages/visual-editor-renderer-blade/resources/assets/frontend/grid.css`,
+ * wired up by ``. React and Vue renderers emit
+ * the same class names so a single stylesheet covers all three.
+ *
+ * Per-breakpoint column count and item span are encoded as static
+ * classes — `ap-grid-has-N-{bp}-columns`, `ap-grid-item-span-N-{bp}-columns`,
+ * `ap-grid-item-span-N-{bp}-row` — and emitted by the renderers on each
+ * instance. Mobile-first cascade: base rules first, then each registered
+ * Tailwind breakpoint (sm 640, md 768, lg 1024, xl 1280, 2xl 1536) in
+ * ascending min-width order, so a later breakpoint's class overrides the
+ * earlier one for the wider viewport.
+ */
+
+.ap-grid {
+ display: grid;
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+ height: 100%;
+}
+
+.ap-grid-item {
+ display: flex;
+ flex-grow: 1;
+ min-width: 0;
+ margin-block-start: 0;
+ margin-block-end: 0;
+}
+
+.ap-grid-item > * {
+ width: 100%;
+}
+
+.ap-grid-item.ap-grid-item-layout-normal {
+ align-items: stretch;
+ justify-content: flex-start;
+}
+
+.ap-grid-item.ap-grid-item-layout-equal {
+ align-items: stretch;
+ justify-content: space-between;
+ flex-direction: column;
+}
+
+.ap-grid-item.ap-grid-item-layout-center {
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+
+.ap-grid-item.ap-grid-item-layout-bottom {
+ flex-direction: column;
+ justify-content: flex-end;
+}
+
+.ap-grid-item.ap-grid-item-layout-last-bottom {
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+/* === base (no media query) === */
+.ap-grid.ap-grid-has-1-base-columns { grid-template-columns: repeat(1, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-2-base-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-3-base-columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-4-base-columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-5-base-columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-6-base-columns { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-7-base-columns { grid-template-columns: repeat(7, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-8-base-columns { grid-template-columns: repeat(8, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-9-base-columns { grid-template-columns: repeat(9, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-10-base-columns { grid-template-columns: repeat(10, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-11-base-columns { grid-template-columns: repeat(11, minmax(0, 1fr)); }
+.ap-grid.ap-grid-has-12-base-columns { grid-template-columns: repeat(12, minmax(0, 1fr)); }
+
+.ap-grid-item.ap-grid-item-span-1-base-columns { grid-column: span 1; }
+.ap-grid-item.ap-grid-item-span-2-base-columns { grid-column: span 2; }
+.ap-grid-item.ap-grid-item-span-3-base-columns { grid-column: span 3; }
+.ap-grid-item.ap-grid-item-span-4-base-columns { grid-column: span 4; }
+.ap-grid-item.ap-grid-item-span-5-base-columns { grid-column: span 5; }
+.ap-grid-item.ap-grid-item-span-6-base-columns { grid-column: span 6; }
+.ap-grid-item.ap-grid-item-span-7-base-columns { grid-column: span 7; }
+.ap-grid-item.ap-grid-item-span-8-base-columns { grid-column: span 8; }
+.ap-grid-item.ap-grid-item-span-9-base-columns { grid-column: span 9; }
+.ap-grid-item.ap-grid-item-span-10-base-columns { grid-column: span 10; }
+.ap-grid-item.ap-grid-item-span-11-base-columns { grid-column: span 11; }
+.ap-grid-item.ap-grid-item-span-12-base-columns { grid-column: span 12; }
+
+.ap-grid-item.ap-grid-item-span-1-base-row { grid-row: span 1; }
+.ap-grid-item.ap-grid-item-span-2-base-row { grid-row: span 2; }
+.ap-grid-item.ap-grid-item-span-3-base-row { grid-row: span 3; }
+.ap-grid-item.ap-grid-item-span-4-base-row { grid-row: span 4; }
+.ap-grid-item.ap-grid-item-span-5-base-row { grid-row: span 5; }
+.ap-grid-item.ap-grid-item-span-6-base-row { grid-row: span 6; }
+.ap-grid-item.ap-grid-item-span-7-base-row { grid-row: span 7; }
+.ap-grid-item.ap-grid-item-span-8-base-row { grid-row: span 8; }
+.ap-grid-item.ap-grid-item-span-9-base-row { grid-row: span 9; }
+.ap-grid-item.ap-grid-item-span-10-base-row { grid-row: span 10; }
+.ap-grid-item.ap-grid-item-span-11-base-row { grid-row: span 11; }
+.ap-grid-item.ap-grid-item-span-12-base-row { grid-row: span 12; }
+
+/* === sm: 640px === */
+@media (min-width: 640px) {
+ .ap-grid.ap-grid-has-1-sm-columns { grid-template-columns: repeat(1, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-2-sm-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-3-sm-columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-4-sm-columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-5-sm-columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-6-sm-columns { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-7-sm-columns { grid-template-columns: repeat(7, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-8-sm-columns { grid-template-columns: repeat(8, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-9-sm-columns { grid-template-columns: repeat(9, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-10-sm-columns { grid-template-columns: repeat(10, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-11-sm-columns { grid-template-columns: repeat(11, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-12-sm-columns { grid-template-columns: repeat(12, minmax(0, 1fr)); }
+
+ .ap-grid-item.ap-grid-item-span-1-sm-columns { grid-column: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-sm-columns { grid-column: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-sm-columns { grid-column: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-sm-columns { grid-column: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-sm-columns { grid-column: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-sm-columns { grid-column: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-sm-columns { grid-column: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-sm-columns { grid-column: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-sm-columns { grid-column: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-sm-columns { grid-column: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-sm-columns { grid-column: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-sm-columns { grid-column: span 12; }
+
+ .ap-grid-item.ap-grid-item-span-1-sm-row { grid-row: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-sm-row { grid-row: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-sm-row { grid-row: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-sm-row { grid-row: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-sm-row { grid-row: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-sm-row { grid-row: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-sm-row { grid-row: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-sm-row { grid-row: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-sm-row { grid-row: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-sm-row { grid-row: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-sm-row { grid-row: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-sm-row { grid-row: span 12; }
+}
+
+/* === md: 768px === */
+@media (min-width: 768px) {
+ .ap-grid.ap-grid-has-1-md-columns { grid-template-columns: repeat(1, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-2-md-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-3-md-columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-4-md-columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-5-md-columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-6-md-columns { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-7-md-columns { grid-template-columns: repeat(7, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-8-md-columns { grid-template-columns: repeat(8, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-9-md-columns { grid-template-columns: repeat(9, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-10-md-columns { grid-template-columns: repeat(10, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-11-md-columns { grid-template-columns: repeat(11, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-12-md-columns { grid-template-columns: repeat(12, minmax(0, 1fr)); }
+
+ .ap-grid-item.ap-grid-item-span-1-md-columns { grid-column: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-md-columns { grid-column: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-md-columns { grid-column: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-md-columns { grid-column: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-md-columns { grid-column: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-md-columns { grid-column: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-md-columns { grid-column: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-md-columns { grid-column: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-md-columns { grid-column: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-md-columns { grid-column: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-md-columns { grid-column: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-md-columns { grid-column: span 12; }
+
+ .ap-grid-item.ap-grid-item-span-1-md-row { grid-row: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-md-row { grid-row: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-md-row { grid-row: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-md-row { grid-row: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-md-row { grid-row: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-md-row { grid-row: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-md-row { grid-row: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-md-row { grid-row: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-md-row { grid-row: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-md-row { grid-row: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-md-row { grid-row: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-md-row { grid-row: span 12; }
+}
+
+/* === lg: 1024px === */
+@media (min-width: 1024px) {
+ .ap-grid.ap-grid-has-1-lg-columns { grid-template-columns: repeat(1, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-2-lg-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-3-lg-columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-4-lg-columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-5-lg-columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-6-lg-columns { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-7-lg-columns { grid-template-columns: repeat(7, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-8-lg-columns { grid-template-columns: repeat(8, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-9-lg-columns { grid-template-columns: repeat(9, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-10-lg-columns { grid-template-columns: repeat(10, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-11-lg-columns { grid-template-columns: repeat(11, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-12-lg-columns { grid-template-columns: repeat(12, minmax(0, 1fr)); }
+
+ .ap-grid-item.ap-grid-item-span-1-lg-columns { grid-column: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-lg-columns { grid-column: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-lg-columns { grid-column: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-lg-columns { grid-column: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-lg-columns { grid-column: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-lg-columns { grid-column: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-lg-columns { grid-column: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-lg-columns { grid-column: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-lg-columns { grid-column: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-lg-columns { grid-column: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-lg-columns { grid-column: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-lg-columns { grid-column: span 12; }
+
+ .ap-grid-item.ap-grid-item-span-1-lg-row { grid-row: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-lg-row { grid-row: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-lg-row { grid-row: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-lg-row { grid-row: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-lg-row { grid-row: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-lg-row { grid-row: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-lg-row { grid-row: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-lg-row { grid-row: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-lg-row { grid-row: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-lg-row { grid-row: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-lg-row { grid-row: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-lg-row { grid-row: span 12; }
+}
+
+/* === xl: 1280px === */
+@media (min-width: 1280px) {
+ .ap-grid.ap-grid-has-1-xl-columns { grid-template-columns: repeat(1, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-2-xl-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-3-xl-columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-4-xl-columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-5-xl-columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-6-xl-columns { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-7-xl-columns { grid-template-columns: repeat(7, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-8-xl-columns { grid-template-columns: repeat(8, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-9-xl-columns { grid-template-columns: repeat(9, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-10-xl-columns { grid-template-columns: repeat(10, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-11-xl-columns { grid-template-columns: repeat(11, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-12-xl-columns { grid-template-columns: repeat(12, minmax(0, 1fr)); }
+
+ .ap-grid-item.ap-grid-item-span-1-xl-columns { grid-column: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-xl-columns { grid-column: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-xl-columns { grid-column: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-xl-columns { grid-column: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-xl-columns { grid-column: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-xl-columns { grid-column: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-xl-columns { grid-column: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-xl-columns { grid-column: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-xl-columns { grid-column: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-xl-columns { grid-column: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-xl-columns { grid-column: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-xl-columns { grid-column: span 12; }
+
+ .ap-grid-item.ap-grid-item-span-1-xl-row { grid-row: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-xl-row { grid-row: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-xl-row { grid-row: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-xl-row { grid-row: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-xl-row { grid-row: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-xl-row { grid-row: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-xl-row { grid-row: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-xl-row { grid-row: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-xl-row { grid-row: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-xl-row { grid-row: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-xl-row { grid-row: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-xl-row { grid-row: span 12; }
+}
+
+/* === 2xl: 1536px === */
+@media (min-width: 1536px) {
+ .ap-grid.ap-grid-has-1-2xl-columns { grid-template-columns: repeat(1, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-2-2xl-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-3-2xl-columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-4-2xl-columns { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-5-2xl-columns { grid-template-columns: repeat(5, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-6-2xl-columns { grid-template-columns: repeat(6, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-7-2xl-columns { grid-template-columns: repeat(7, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-8-2xl-columns { grid-template-columns: repeat(8, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-9-2xl-columns { grid-template-columns: repeat(9, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-10-2xl-columns { grid-template-columns: repeat(10, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-11-2xl-columns { grid-template-columns: repeat(11, minmax(0, 1fr)); }
+ .ap-grid.ap-grid-has-12-2xl-columns { grid-template-columns: repeat(12, minmax(0, 1fr)); }
+
+ .ap-grid-item.ap-grid-item-span-1-2xl-columns { grid-column: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-2xl-columns { grid-column: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-2xl-columns { grid-column: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-2xl-columns { grid-column: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-2xl-columns { grid-column: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-2xl-columns { grid-column: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-2xl-columns { grid-column: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-2xl-columns { grid-column: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-2xl-columns { grid-column: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-2xl-columns { grid-column: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-2xl-columns { grid-column: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-2xl-columns { grid-column: span 12; }
+
+ .ap-grid-item.ap-grid-item-span-1-2xl-row { grid-row: span 1; }
+ .ap-grid-item.ap-grid-item-span-2-2xl-row { grid-row: span 2; }
+ .ap-grid-item.ap-grid-item-span-3-2xl-row { grid-row: span 3; }
+ .ap-grid-item.ap-grid-item-span-4-2xl-row { grid-row: span 4; }
+ .ap-grid-item.ap-grid-item-span-5-2xl-row { grid-row: span 5; }
+ .ap-grid-item.ap-grid-item-span-6-2xl-row { grid-row: span 6; }
+ .ap-grid-item.ap-grid-item-span-7-2xl-row { grid-row: span 7; }
+ .ap-grid-item.ap-grid-item-span-8-2xl-row { grid-row: span 8; }
+ .ap-grid-item.ap-grid-item-span-9-2xl-row { grid-row: span 9; }
+ .ap-grid-item.ap-grid-item-span-10-2xl-row { grid-row: span 10; }
+ .ap-grid-item.ap-grid-item-span-11-2xl-row { grid-row: span 11; }
+ .ap-grid-item.ap-grid-item-span-12-2xl-row { grid-row: span 12; }
+}
diff --git a/packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/grid-item.blade.php b/packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/grid-item.blade.php
new file mode 100644
index 000000000..4eb140077
--- /dev/null
+++ b/packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/grid-item.blade.php
@@ -0,0 +1,75 @@
+@php
+ use ArtisanPackUI\VisualEditor\Responsive\BreakpointRegistry;
+ use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;
+
+ $validInnerLayouts = [ 'normal', 'equal', 'center', 'bottom', 'last-bottom' ];
+
+ $clampSpan = static function ( $value, int $fallback = 1 ): int {
+ $int = is_numeric( $value ) ? (int) $value : $fallback;
+ if ( $int < 1 ) {
+ return 1;
+ }
+ if ( $int > 12 ) {
+ return 12;
+ }
+ return $int;
+ };
+
+ $innerLayoutRaw = is_string( $attributes['innerLayout'] ?? null )
+ ? $attributes['innerLayout']
+ : 'normal';
+ $innerLayout = in_array( $innerLayoutRaw, $validInnerLayouts, true )
+ ? $innerLayoutRaw
+ : 'normal';
+
+ $baseColumnSpan = $clampSpan( $attributes['gridColumnSpan'] ?? null, 1 );
+ $baseRowSpan = $clampSpan( $attributes['gridRowSpan'] ?? null, 1 );
+
+ $spanClasses = [
+ 'ap-grid-item-span-' . $baseColumnSpan . '-base-columns',
+ 'ap-grid-item-span-' . $baseRowSpan . '-base-row',
+ ];
+
+ $registry = app( BreakpointRegistry::class );
+
+ $collectOverrides = static function ( $overrides, string $suffix, int $base, callable $clamp ) use ( $registry ): array {
+ $classes = [];
+
+ if ( ! is_array( $overrides ) ) {
+ return $classes;
+ }
+
+ foreach ( $overrides as $bp => $value ) {
+ if ( BreakpointRegistry::BASE_KEY === $bp ) {
+ continue;
+ }
+ if ( ! is_numeric( $value ) ) {
+ continue;
+ }
+ if ( null === $registry->get( (string) $bp ) ) {
+ continue;
+ }
+
+ $classes[] = 'ap-grid-item-span-' . $clamp( $value, $base ) . '-' . $bp . '-' . $suffix;
+ }
+
+ return $classes;
+ };
+
+ $spanClasses = array_merge(
+ $spanClasses,
+ $collectOverrides( $attributes['responsive']['gridColumnSpan'] ?? null, 'columns', $baseColumnSpan, $clampSpan ),
+ $collectOverrides( $attributes['responsive']['gridRowSpan'] ?? null, 'row', $baseRowSpan, $clampSpan ),
+ );
+
+ $baseClasses = array_merge(
+ [
+ 'ap-grid-item',
+ 'ap-grid-item-layout-' . $innerLayout,
+ ],
+ $spanClasses,
+ );
+@endphp
+
+ {!! $innerBlocksHtml !!}
+
diff --git a/packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/grid.blade.php b/packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/grid.blade.php
new file mode 100644
index 000000000..34dc7f014
--- /dev/null
+++ b/packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/grid.blade.php
@@ -0,0 +1,48 @@
+@php
+ use ArtisanPackUI\VisualEditor\Responsive\BreakpointRegistry;
+ use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;
+
+ $clampColumns = static function ( $value, int $fallback ): int {
+ $int = is_numeric( $value ) ? (int) $value : $fallback;
+ if ( $int < 1 ) {
+ return 1;
+ }
+ if ( $int > 12 ) {
+ return 12;
+ }
+ return $int;
+ };
+
+ $baseColumns = $clampColumns( $attributes['numColumns'] ?? null, 4 );
+
+ // Per-breakpoint overrides. Mobile-first CSS cascade handles the rest:
+ // emit `ap-grid-has-N-{bp}-columns` for every breakpoint that carries
+ // its own value. Ascending min-width ordering in grid.css means a
+ // later breakpoint's class wins for the wider viewport.
+ $breakpointClasses = [ 'ap-grid-has-' . $baseColumns . '-base-columns' ];
+
+ $responsiveColumns = $attributes['responsive']['numColumns'] ?? [];
+
+ if ( is_array( $responsiveColumns ) ) {
+ $registry = app( BreakpointRegistry::class );
+
+ foreach ( $responsiveColumns as $bp => $value ) {
+ if ( BreakpointRegistry::BASE_KEY === $bp ) {
+ continue;
+ }
+ if ( ! is_numeric( $value ) ) {
+ continue;
+ }
+ if ( null === $registry->get( (string) $bp ) ) {
+ continue;
+ }
+
+ $breakpointClasses[] = 'ap-grid-has-' . $clampColumns( $value, $baseColumns ) . '-' . $bp . '-columns';
+ }
+ }
+
+ $baseClasses = array_merge( [ 'ap-grid' ], $breakpointClasses );
+@endphp
+
+ {!! $innerBlocksHtml !!}
+
diff --git a/packages/visual-editor-renderer-blade/resources/views/components/blocks-styles.blade.php b/packages/visual-editor-renderer-blade/resources/views/components/blocks-styles.blade.php
index 90238212d..ab7342640 100644
--- a/packages/visual-editor-renderer-blade/resources/views/components/blocks-styles.blade.php
+++ b/packages/visual-editor-renderer-blade/resources/views/components/blocks-styles.blade.php
@@ -7,6 +7,10 @@
@endif
+{{-- Grid family CSS is layout-only (no interactivity script), so it
+ loads independently of $emitInteractive. Hosts that opt out of
+ accordion/tabs interactivity still want grid blocks to render. --}}
+
@if( '' !== $themeTokensCss )
@endif
diff --git a/packages/visual-editor-renderer-blade/src/View/Components/BlocksStylesComponent.php b/packages/visual-editor-renderer-blade/src/View/Components/BlocksStylesComponent.php
index 678f346aa..de861b99a 100644
--- a/packages/visual-editor-renderer-blade/src/View/Components/BlocksStylesComponent.php
+++ b/packages/visual-editor-renderer-blade/src/View/Components/BlocksStylesComponent.php
@@ -61,6 +61,8 @@ class BlocksStylesComponent extends Component
public string $tabsStyleHref;
+ public string $gridStyleHref;
+
public string $interactivityScriptSrc;
public bool $emitBlockLibrary;
@@ -93,6 +95,7 @@ public function __construct(
$this->themeHref = $base . '/theme.css';
$this->accordionStyleHref = $base . '/frontend/accordion.css';
$this->tabsStyleHref = $base . '/frontend/tabs.css';
+ $this->gridStyleHref = $base . '/frontend/grid.css';
$this->interactivityScriptSrc = $base . '/frontend/interactivity.js';
$this->emitBlockLibrary = $bundle;
$this->emitInteractive = $interactive;
diff --git a/packages/visual-editor-renderer-blade/tests/Feature/Responsive/GridResponsiveRenderTest.php b/packages/visual-editor-renderer-blade/tests/Feature/Responsive/GridResponsiveRenderTest.php
new file mode 100644
index 000000000..d65541fa2
--- /dev/null
+++ b/packages/visual-editor-renderer-blade/tests/Feature/Responsive/GridResponsiveRenderTest.php
@@ -0,0 +1,85 @@
+reset();
+} );
+
+it( 'emits per-breakpoint grid column classes on the wrapper from attributes.responsive.numColumns', function () {
+ $tree = [
+ [
+ 'clientId' => 'grid-1',
+ 'name' => 'artisanpack/grid',
+ 'attributes' => [
+ 'numColumns' => 1,
+ 'responsive' => [
+ 'numColumns' => [
+ 'md' => 2,
+ 'lg' => 4,
+ ],
+ ],
+ ],
+ 'innerBlocks' => [],
+ ],
+ ];
+
+ $rendered = $this->stripGlobalStyles( Blade::render( '', [ 'tree' => $tree ] ) );
+
+ expect( $rendered )->toContain( 'ap-grid-has-1-base-columns' )
+ ->and( $rendered )->toContain( 'ap-grid-has-2-md-columns' )
+ ->and( $rendered )->toContain( 'ap-grid-has-4-lg-columns' )
+ // Static-class approach: no accumulated