Skip to content

packages/core/src/lib/layout/grid.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

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 built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.