packages/core/src/lib/layout/grid.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import { preparedStyleValue, preparedStyleQuery } from '../style/responsive.js';
2 import { definePrimitive } from '../define-primitive.js';
3 import { clonePibblElement } from '../element/create-element.js';
4 import { getChildPrimitiveReceiverToken } from '../element/metadata.js';
5 import type { PibblElement } from '../element/types.js';
6 import { GLOBAL_STATE } from '../global-state.js';
7 import { layoutDiagnostic } from '../style/diagnostics.js';
8 import {
9 type PibblStructuralEventProps,
10 wireStructuralEvents,
11 } from '../components/structural-events.js';
12 import { normalizeEdges } from '../style/normalize.js';
13 import { resolveLength } from '../style/resolve-length.js';
14 import { setPreNormalizedStyle } from '../style/resolve-dispatch.js';
15 import type {
16 BoxStyle,
17 GridItemStyle,
18 LayoutItemStyle,
19 ResolvedBoxStyle,
20 } from '../style/types.js';
21 import type {
22 CanvasMeasurementService,
23 PibblNodeInput,
24 MeasureInput,
25 MeasureResult,
26 RenderingContext2D,
27 SystemStyle,
28 } from '../types.js';
29 import {
30 childWithUsedSize,
31 prepareDirectChildStyle,
32 materializeLayoutChildren,
33 placeDirectChildren,
34 prepareLayoutChildren,
35 resolveContainerBox,
36 resolveDirectChildBox,
37 } from './absolute.js';
38 import { pushLayoutBox } from './context.js';
39 import { recordLayoutEvaluation } from './diagnostics.js';
40 import { assertSupportedChildPosition } from './positioning-diagnostics.js';
41 import { measureElement } from './measure.js';
42 import type { Constraints, LayoutBox } from './types.js';
43
44 /**
45 * A fixed logical size, percentage of available space, or fractional share of remaining grid space.
46 *
47 * @see {@link GridStyle}
48 */
49 export type GridTrack = number | `${number}%` | `${number}fr`;
50 type GridAlignment = 'stretch' | 'start' | 'center' | 'end';
51 type ParsedGridTrack =
52 | Readonly<{ kind: 'fixed'; value: number }>
53 | Readonly<{ kind: 'percentage'; factor: number }>
54 | Readonly<{ kind: 'fraction'; factor: number }>;
55
56 /**
57 * Supported geometry and presentation properties for Grid.
58 *
59 * @see {@link BoxStyle}
60 * @see {@link LayoutItemStyle}
61 * @see {@link GridTrack}
62 * @see {@link Grid}
63 */
64 export interface GridStyle extends BoxStyle, LayoutItemStyle {
65 /** Explicit grid column track definitions. See {@link GridTrack}. */
66 gridTemplateColumns: readonly GridTrack[];
67 /** Explicit grid row track definitions. See {@link GridTrack}. */
68 gridTemplateRows: readonly GridTrack[];
69 /** Default inline-axis alignment of grid or overlay children. See {@link GridAlignment}. */
70 justifyItems?: GridAlignment;
71 /** Default alignment of children on the cross or block axis. See {@link GridAlignment}. */
72 alignItems?: GridAlignment;
73 /** Spacing between adjacent items. See {@link GridStyle}. */
74 gap?: number;
75 /** Space between adjacent rows or items on the row axis. See {@link GridStyle}. */
76 rowGap?: number;
77 /** Space between adjacent columns or items on the column axis. See {@link GridStyle}. */
78 columnGap?: number;
79 }
80
81 /**
82 * Authored inputs for Grid, including the declared data and presentation options.
83 *
84 * @see {@link PibblNodeInput}
85 * @see {@link Grid}
86 */
87 export interface GridProps extends PibblStructuralEventProps {
88 /** Descendant content or the callback that supplies it. See {@link PibblNodeInput}. */
89 children: PibblNodeInput;
90 }
91
92 export interface GridMeasurementInput {
93 service: CanvasMeasurementService;
94 measureElement(
95 element: PibblElement<any>,
96 constraints: Readonly<Constraints>,
97 ): MeasureResult;
98 }
99
100 interface ResolvedGridStyle extends SystemStyle {
101 box: ResolvedBoxStyle;
102 overflow: 'visible' | 'clip';
103 columns: readonly ParsedGridTrack[];
104 rows: readonly ParsedGridTrack[];
105 justifyItems: GridAlignment;
106 alignItems: GridAlignment;
107 rowGap: number;
108 columnGap: number;
109 }
110
111 interface GridPlacement {
112 box: LayoutBox;
113 element: PibblElement<any>;
114 }
115
116 function renderGrid(
117 props: GridProps,
118 style: Readonly<ResolvedGridStyle>,
119 ctx: RenderingContext2D,
120 ) {
121 wireStructuralEvents(props);
122 const children = prepareLayoutChildren(props.children, 'Grid');
123 recordLayoutEvaluation();
124 const content = style.box.contentBox;
125 ctx.translate(content.x, content.y);
126 const localContent = Object.freeze({
127 x: 0,
128 y: 0,
129 width: content.width,
130 height: content.height,
131 });
132 const releaseLayout = pushLayoutBox(localContent);
133 GLOBAL_STATE.componentRefs!.onAfterRender = releaseLayout;
134 const service = {
135 measureText(value: string, font: string) {
136 ctx.save();
137 try {
138 ctx.font = font;
139 return ctx.measureText(value);
140 } finally {
141 ctx.restore();
142 }
143 },
144 };
145 const placements = resolveGridPlacements(
146 localContent,
147 children,
148 style,
149 {
150 service,
151 measureElement: (element, constraints) =>
152 measureElement(element, constraints, service),
153 },
154 );
155 let index = 0;
156 return placeDirectChildren(
157 children,
158 localContent,
159 () => placements[index++],
160 style.overflow,
161 );
162 }
163
164 /**
165 * Describes explicit Grid layout for JSX or createElement authoring.
166 *
167 * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
168 * props and style types.
169 * @throws When called directly; Pibbl mounts this component through JSX or createElement.
170 *
171 * @see {@link GridProps}
172 * @see {@link GridStyle}
173 */
174 export const Grid = definePrimitive<
175 GridProps,
176 GridStyle,
177 GridStyle,
178 ResolvedGridStyle
179 >(renderGrid, {
180 childInput: 'structural',
181 resolveStyle: (style, context) => ({
182 box: resolveContainerBox(style, context, 'Grid', 'grid'),
183 overflow: style.overflow ?? 'visible',
184 ...normalizeGridOptions(style, context.allocation),
185 }),
186 measure: (input) => measureGrid(input),
187 });
188
189 export function layoutGrid(
190 container: Readonly<LayoutBox>,
191 children: PibblNodeInput,
192 style: Readonly<GridStyle>,
193 measurement: GridMeasurementInput,
194 ): LayoutBox[] {
195 const prepared = materializeLayoutChildren(children, 'Grid');
196 const options = normalizeGridOptions(style, container);
197 return resolveGridPlacements(container, prepared, options, measurement)
198 .map((placement) => placement.box);
199 }
200
201 function resolveGridPlacements(
202 container: Readonly<LayoutBox>,
203 children: readonly PibblElement<any>[],
204 options: Readonly<ReturnType<typeof normalizeGridOptions>>,
205 measurement: GridMeasurementInput,
206 ): GridPlacement[] {
207 assertFiniteContainer(container);
208 const columns = resolveParsedGridTracks(
209 options.columns,
210 container.width,
211 options.columnGap,
212 'columns',
213 container,
214 );
215 const rows = resolveParsedGridTracks(
216 options.rows,
217 container.height,
218 options.rowGap,
219 'rows',
220 container,
221 );
222 const placements: GridPlacement[] = [];
223
224 for (const child of children) {
225 const receiverToken = getChildPrimitiveReceiverToken(
226 GLOBAL_STATE.renderTransaction,
227 GLOBAL_STATE.primitiveReceiverToken,
228 placements.length,
229 );
230 const prepared = prepareDirectChildStyle(child, receiverToken, container);
231 const normalized = preparedStyleValue(prepared) as Readonly<
232 BoxStyle & GridItemStyle & LayoutItemStyle & Record<string, unknown>
233 >;
234 assertSupportedChildPosition('Grid', child, normalized);
235 rejectUnsupportedItemFields(child, normalized, container);
236 const columnStart = placementInteger(
237 normalized.gridColumnStart,
238 'gridColumnStart',
239 child,
240 container,
241 true,
242 );
243 const rowStart = placementInteger(
244 normalized.gridRowStart,
245 'gridRowStart',
246 child,
247 container,
248 true,
249 );
250 const columnSpan = placementInteger(
251 normalized.gridColumnSpan ?? 1,
252 'gridColumnSpan',
253 child,
254 container,
255 false,
256 );
257 const rowSpan = placementInteger(
258 normalized.gridRowSpan ?? 1,
259 'gridRowSpan',
260 child,
261 container,
262 false,
263 );
264 validateStart(
265 columnStart,
266 columns.length,
267 'gridColumnStart',
268 child,
269 container,
270 );
271 validateStart(
272 rowStart,
273 rows.length,
274 'gridRowStart',
275 child,
276 container,
277 );
278 validateSpan(
279 columnStart,
280 columnSpan,
281 columns.length,
282 'gridColumnSpan',
283 child,
284 container,
285 );
286 validateSpan(
287 rowStart,
288 rowSpan,
289 rows.length,
290 'gridRowSpan',
291 child,
292 container,
293 );
294 const area = {
295 x: container.x + trackOffset(columns, columnStart - 1, options.columnGap),
296 y: container.y + trackOffset(rows, rowStart - 1, options.rowGap),
297 width: trackSpan(columns, columnStart - 1, columnSpan, options.columnGap),
298 height: trackSpan(rows, rowStart - 1, rowSpan, options.rowGap),
299 };
300 const widthAuto =
301 normalized.width === undefined || normalized.width === 'auto';
302 const heightAuto =
303 normalized.height === undefined || normalized.height === 'auto';
304 const justify = itemAlignment(
305 normalized.justifySelf,
306 options.justifyItems,
307 'justifySelf',
308 child,
309 container,
310 );
311 const align = itemAlignment(
312 normalized.alignSelf,
313 options.alignItems,
314 'alignSelf',
315 child,
316 container,
317 );
318 let measured: MeasureResult | undefined;
319 if (
320 (widthAuto && justify !== 'stretch') ||
321 (heightAuto && align !== 'stretch')
322 ) {
323 const measurementChild = clonePibblElement(child);
324 setPreNormalizedStyle(
325 measurementChild,
326 normalized,
327 child,
328 receiverToken,
329 preparedStyleQuery(prepared),
330 );
331 measured = measurement.measureElement(measurementChild, {
332 minWidth: 0,
333 maxWidth: area.width,
334 minHeight: 0,
335 maxHeight: area.height,
336 });
337 if (measured.status === 'unsupported') {
338 throw childDiagnostic(
339 child,
340 widthAuto && justify !== 'stretch' ? 'width' : 'height',
341 'auto',
342 container,
343 `measurement is required but unsupported: ${measured.reason}`,
344 );
345 }
346 }
347 const padding = normalizeEdges(
348 normalized.padding,
349 'padding',
350 childContext(child, container),
351 );
352 const usedStyle = {
353 ...normalized,
354 width: widthAuto
355 ? justify === 'stretch'
356 ? Math.max(0, area.width - padding.left - padding.right)
357 : measured!.status === 'measured'
358 ? measured!.size.width
359 : 0
360 : normalized.width,
361 height: heightAuto
362 ? align === 'stretch'
363 ? Math.max(0, area.height - padding.top - padding.bottom)
364 : measured!.status === 'measured'
365 ? measured!.size.height
366 : 0
367 : normalized.height,
368 };
369 const resolved = resolveDirectChildBox(area, child, 'grid', usedStyle);
370 const border = resolved.borderBox;
371 const box = {
372 x: area.x + alignmentOffset(area.width, border.width, justify),
373 y: area.y + alignmentOffset(area.height, border.height, align),
374 width: border.width,
375 height: border.height,
376 };
377 placements.push({
378 box,
379 element: childWithUsedSize(
380 child,
381 normalized,
382 resolved.contentBox.width,
383 resolved.contentBox.height,
384 receiverToken,
385 preparedStyleQuery(prepared),
386 ),
387 });
388 }
389 return placements;
390 }
391
392 export function resolveGridTracks(
393 tracks: readonly GridTrack[],
394 bound: number,
395 gap: number,
396 axis: 'columns' | 'rows',
397 ): number[] {
398 const container = {
399 x: 0,
400 y: 0,
401 width: axis === 'columns' ? bound : 0,
402 height: axis === 'rows' ? bound : 0,
403 };
404 const parsed = parseGridTracks(tracks, axis, container);
405 return resolveParsedGridTracks(parsed, bound, gap, axis, container);
406 }
407
408 function resolveParsedGridTracks(
409 tracks: readonly ParsedGridTrack[],
410 bound: number,
411 gap: number,
412 axis: 'columns' | 'rows',
413 container: Readonly<LayoutBox>,
414 ): number[] {
415 if (!Number.isFinite(bound) || bound < 0) {
416 throw optionDiagnostic(
417 axis,
418 bound,
419 container,
420 'requires a finite nonnegative track bound',
421 );
422 }
423 const resolved = new Array<number>(tracks.length).fill(0);
424 const fractions: { index: number; factor: number }[] = [];
425 let allocated =
426 Math.max(0, tracks.length - 1) * nonnegativeOption(gap, 'gap', container);
427 for (let index = 0; index < tracks.length; index++) {
428 const track = tracks[index];
429 if (track.kind === 'fixed') {
430 resolved[index] = track.value;
431 allocated += track.value;
432 } else if (track.kind === 'percentage') {
433 const size = (bound * track.factor) / 100;
434 if (!Number.isFinite(size)) throw trackDiagnostic(track, axis, container);
435 resolved[index] = size;
436 allocated += size;
437 } else {
438 fractions.push({ index, factor: track.factor });
439 }
440 }
441 const totalFr = fractions.reduce((total, track) => total + track.factor, 0);
442 if (!Number.isFinite(allocated) || !Number.isFinite(totalFr)) {
443 throw trackDiagnostic(tracks, axis, container);
444 }
445 const remaining = Math.max(0, bound - allocated);
446 const unit = remaining / Math.max(1, totalFr);
447 for (const track of fractions) resolved[track.index] = unit * track.factor;
448 if (resolved.some((value) => !Number.isFinite(value) || value < 0)) {
449 throw trackDiagnostic(tracks, axis, container);
450 }
451 return resolved;
452 }
453
454 function parseGridTracks(
455 tracks: readonly GridTrack[],
456 axis: 'columns' | 'rows',
457 container: Readonly<LayoutBox>,
458 ): readonly ParsedGridTrack[] {
459 if (!Array.isArray(tracks) || tracks.length === 0) {
460 throw optionDiagnostic(
461 axis === 'columns' ? 'gridTemplateColumns' : 'gridTemplateRows',
462 tracks,
463 container,
464 `requires a nonempty explicit ${axis} list`,
465 );
466 }
467 return tracks.map(track => {
468 if (typeof track === 'number') {
469 if (!Number.isFinite(track) || track < 0) {
470 throw trackDiagnostic(track, axis, container);
471 }
472 return Object.freeze({ kind: 'fixed', value: track });
473 }
474 if (typeof track !== 'string') {
475 throw trackDiagnostic(track, axis, container);
476 }
477 const match = /^(?:\d+(?:\.\d+)?|\.\d+)(%|fr)$/.exec(track);
478 if (!match) throw trackDiagnostic(track, axis, container);
479 const factor = Number.parseFloat(track);
480 if (!Number.isFinite(factor) || factor < 0) {
481 throw trackDiagnostic(track, axis, container);
482 }
483 return Object.freeze(match[1] === '%' ?
484 { kind: 'percentage', factor } :
485 { kind: 'fraction', factor });
486 });
487 }
488
489 function measureGrid(input: MeasureInput<GridProps, GridStyle>): MeasureResult {
490 prepareLayoutChildren(input.props.children, 'Grid');
491 const diagnosticBox = {
492 x: 0,
493 y: 0,
494 width: Number.isFinite(input.constraints.maxWidth)
495 ? input.constraints.maxWidth
496 : 0,
497 height: Number.isFinite(input.constraints.maxHeight)
498 ? input.constraints.maxHeight
499 : 0,
500 };
501 const options = normalizeGridOptions(input.style, diagnosticBox);
502 const widthDefinite =
503 input.style.width !== undefined && input.style.width !== 'auto';
504 const heightDefinite =
505 input.style.height !== undefined && input.style.height !== 'auto';
506 if (!widthDefinite) {
507 const unsupported = intrinsicTrackReason(
508 options.columns,
509 'columns',
510 );
511 if (unsupported) return { status: 'unsupported', reason: unsupported };
512 }
513 if (!heightDefinite) {
514 const unsupported = intrinsicTrackReason(options.rows, 'rows');
515 if (unsupported) return { status: 'unsupported', reason: unsupported };
516 }
517 const width = widthDefinite
518 ? measuredLength(
519 input.style.width!,
520 input.constraints.maxWidth,
521 'width',
522 input.constraints,
523 )
524 : intrinsicTrackSize(
525 options.columns,
526 options.columnGap,
527 'columns',
528 );
529 const height = heightDefinite
530 ? measuredLength(
531 input.style.height!,
532 input.constraints.maxHeight,
533 'height',
534 input.constraints,
535 )
536 : intrinsicTrackSize(options.rows, options.rowGap, 'rows');
537 return { status: 'measured', size: { width, height } };
538 }
539
540 function intrinsicTrackSize(
541 tracks: readonly ParsedGridTrack[],
542 gap: number,
543 axis: 'columns' | 'rows',
544 ): number {
545 const size =
546 tracks.reduce((total, track) => total +
547 (track.kind === 'fixed' ? track.value : 0), 0) +
548 Math.max(0, tracks.length - 1) * gap;
549 if (!Number.isFinite(size))
550 throw trackDiagnostic(tracks, axis, { x: 0, y: 0, width: 0, height: 0 });
551 return size;
552 }
553
554 function intrinsicTrackReason(
555 tracks: readonly ParsedGridTrack[],
556 axis: 'columns' | 'rows',
557 ): string | undefined {
558 if (tracks.every(track => track.kind === 'fixed')) return undefined;
559 const kind = tracks.some(track => track.kind === 'fraction')
560 ? 'fractional'
561 : 'percentage';
562 return `Grid ${kind} intrinsic measurement for ${axis} is unsupported because it is allocation-dependent`;
563 }
564
565 function measuredLength(
566 value: Exclude<BoxStyle['width'], undefined>,
567 reference: number,
568 property: string,
569 constraints: Readonly<Constraints>,
570 ): number {
571 const resolved = resolveLength(value, reference, property, {
572 component: 'Grid',
573 algorithm: 'measurement',
574 constraints,
575 });
576 if (resolved === undefined || resolved < 0) {
577 throw layoutDiagnostic({
578 component: 'Grid',
579 property,
580 value,
581 algorithm: 'measurement',
582 constraints,
583 reason: 'requires a definite nonnegative container size',
584 });
585 }
586 return resolved;
587 }
588
589 function normalizeGridOptions(
590 style: Readonly<GridStyle>,
591 container: Readonly<LayoutBox>,
592 ) {
593 if (
594 !Array.isArray(style.gridTemplateColumns) ||
595 style.gridTemplateColumns.length === 0
596 ) {
597 throw optionDiagnostic(
598 'gridTemplateColumns',
599 style.gridTemplateColumns,
600 container,
601 'requires a nonempty explicit columns list',
602 );
603 }
604 if (
605 !Array.isArray(style.gridTemplateRows) ||
606 style.gridTemplateRows.length === 0
607 ) {
608 throw optionDiagnostic(
609 'gridTemplateRows',
610 style.gridTemplateRows,
611 container,
612 'requires a nonempty explicit rows list',
613 );
614 }
615 const justifyItems = containerAlignment(
616 style.justifyItems ?? 'stretch',
617 'justifyItems',
618 container,
619 );
620 const alignItems = containerAlignment(
621 style.alignItems ?? 'stretch',
622 'alignItems',
623 container,
624 );
625 const gap = nonnegativeOption(style.gap ?? 0, 'gap', container);
626 return {
627 columns: parseGridTracks(
628 style.gridTemplateColumns,
629 'columns',
630 container,
631 ),
632 rows: parseGridTracks(style.gridTemplateRows, 'rows', container),
633 justifyItems,
634 alignItems,
635 rowGap: nonnegativeOption(style.rowGap ?? gap, 'rowGap', container),
636 columnGap: nonnegativeOption(
637 style.columnGap ?? gap,
638 'columnGap',
639 container,
640 ),
641 };
642 }
643
644 function containerAlignment(
645 value: unknown,
646 property: string,
647 container: Readonly<LayoutBox>,
648 ): GridAlignment {
649 if (
650 value === 'stretch' ||
651 value === 'start' ||
652 value === 'center' ||
653 value === 'end'
654 )
655 return value;
656 throw optionDiagnostic(
657 property,
658 value,
659 container,
660 'uses an unsupported grid alignment',
661 );
662 }
663
664 function itemAlignment(
665 value: unknown,
666 fallback: GridAlignment,
667 property: string,
668 child: PibblElement<any>,
669 container: Readonly<LayoutBox>,
670 ): GridAlignment {
671 if (value === undefined || value === 'auto') return fallback;
672 if (
673 value === 'stretch' ||
674 value === 'start' ||
675 value === 'center' ||
676 value === 'end'
677 )
678 return value;
679 throw childDiagnostic(
680 child,
681 property,
682 value,
683 container,
684 'uses an unsupported grid alignment',
685 );
686 }
687
688 function placementInteger(
689 value: unknown,
690 property: string,
691 child: PibblElement<any>,
692 container: Readonly<LayoutBox>,
693 required: boolean,
694 ): number {
695 if (value === undefined && required) {
696 throw childDiagnostic(
697 child,
698 property,
699 value,
700 container,
701 'is required for explicit grid placement',
702 );
703 }
704 if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
705 throw childDiagnostic(
706 child,
707 property,
708 value,
709 container,
710 'must be a positive integer',
711 );
712 }
713 return value;
714 }
715
716 function validateSpan(
717 start: number,
718 span: number,
719 trackCount: number,
720 property: string,
721 child: PibblElement<any>,
722 container: Readonly<LayoutBox>,
723 ): void {
724 if (start - 1 + span > trackCount) {
725 throw childDiagnostic(
726 child,
727 property,
728 span,
729 container,
730 `grid area start ${start} plus span ${span} exceeds the explicit ${trackCount}-track list`,
731 );
732 }
733 }
734
735 function validateStart(
736 start: number,
737 trackCount: number,
738 property: string,
739 child: PibblElement<any>,
740 container: Readonly<LayoutBox>,
741 ): void {
742 if (start > trackCount) {
743 throw childDiagnostic(
744 child,
745 property,
746 start,
747 container,
748 `must reference the explicit ${trackCount}-track list`,
749 );
750 }
751 }
752
753 function trackOffset(
754 tracks: readonly number[],
755 index: number,
756 gap: number,
757 ): number {
758 return (
759 tracks.slice(0, index).reduce((total, size) => total + size, 0) +
760 index * gap
761 );
762 }
763
764 function trackSpan(
765 tracks: readonly number[],
766 index: number,
767 span: number,
768 gap: number,
769 ): number {
770 return (
771 tracks.slice(index, index + span).reduce((total, size) => total + size, 0) +
772 Math.max(0, span - 1) * gap
773 );
774 }
775
776 function alignmentOffset(
777 area: number,
778 item: number,
779 alignment: GridAlignment,
780 ): number {
781 if (alignment === 'center') return (area - item) / 2;
782 if (alignment === 'end') return area - item;
783 return 0;
784 }
785
786 function nonnegativeOption(
787 value: number,
788 property: string,
789 container: Readonly<LayoutBox>,
790 ): number {
791 if (!Number.isFinite(value) || value < 0) {
792 throw optionDiagnostic(
793 property,
794 value,
795 container,
796 'requires a finite nonnegative number',
797 );
798 }
799 return value;
800 }
801
802 function assertFiniteContainer(container: Readonly<LayoutBox>): void {
803 for (const [property, value] of Object.entries(container)) {
804 if (
805 !Number.isFinite(value) ||
806 ((property === 'width' || property === 'height') && value < 0)
807 ) {
808 throw optionDiagnostic(
809 property,
810 value,
811 container,
812 'requires finite nonnegative container geometry',
813 );
814 }
815 }
816 }
817
818 function rejectUnsupportedItemFields(
819 child: PibblElement<any>,
820 style: Readonly<Record<string, unknown>>,
821 container: Readonly<LayoutBox>,
822 ): void {
823 for (const property of ['gridColumnEnd', 'gridRowEnd', 'gridArea', 'order']) {
824 if (style[property] !== undefined) {
825 throw childDiagnostic(
826 child,
827 property,
828 style[property],
829 container,
830 'is outside the explicit Grid subset',
831 );
832 }
833 }
834 }
835
836 function trackDiagnostic(
837 value: unknown,
838 axis: string,
839 container: Readonly<LayoutBox>,
840 ) {
841 return optionDiagnostic(
842 axis === 'columns' ? 'gridTemplateColumns' : 'gridTemplateRows',
843 value,
844 container,
845 'contains an unsupported track; use finite numbers, exact decimal percentages, or decimal fr units',
846 );
847 }
848
849 function optionDiagnostic(
850 property: string,
851 value: unknown,
852 container: Readonly<LayoutBox>,
853 reason: string,
854 ) {
855 return layoutDiagnostic({
856 component: 'Grid',
857 property,
858 value,
859 algorithm: 'grid',
860 constraints: constraintsFor(container),
861 reason,
862 });
863 }
864
865 function childDiagnostic(
866 child: PibblElement<any>,
867 property: string,
868 value: unknown,
869 container: Readonly<LayoutBox>,
870 reason: string,
871 ) {
872 return layoutDiagnostic({
873 component: child.type.name || 'Anonymous',
874 property,
875 value,
876 algorithm: 'grid',
877 constraints: constraintsFor(container),
878 reason,
879 });
880 }
881
882 function childContext(child: PibblElement<any>, container: Readonly<LayoutBox>) {
883 return {
884 component: child.type.name || 'Anonymous',
885 algorithm: 'grid',
886 constraints: constraintsFor(container),
887 };
888 }
889
890 function constraintsFor(container: Readonly<LayoutBox>) {
891 return {
892 minWidth: 0,
893 maxWidth: container.width,
894 minHeight: 0,
895 maxHeight: container.height,
896 };
897 }
898
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.