@boostdev/design-system-components 2.1.0 → 2.2.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.
Files changed (34) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +40 -0
  3. package/dist/client.cjs +303 -60
  4. package/dist/client.css +568 -527
  5. package/dist/client.d.cts +39 -1
  6. package/dist/client.d.ts +39 -1
  7. package/dist/client.js +309 -60
  8. package/dist/index.cjs +303 -60
  9. package/dist/index.css +568 -527
  10. package/dist/index.d.cts +39 -1
  11. package/dist/index.d.ts +39 -1
  12. package/dist/index.js +309 -60
  13. package/dist/web-components/index.d.ts +129 -1
  14. package/dist/web-components/index.js +311 -0
  15. package/package.json +1 -1
  16. package/src/components/layout/Grid/Grid.mdx +244 -0
  17. package/src/components/layout/Grid/Grid.module.css +59 -0
  18. package/src/components/layout/Grid/Grid.spec.tsx +401 -0
  19. package/src/components/layout/Grid/Grid.stories.tsx +160 -0
  20. package/src/components/layout/Grid/Grid.tsx +85 -0
  21. package/src/components/layout/Grid/GridItem.tsx +150 -0
  22. package/src/components/layout/Grid/autoSpan.ts +77 -0
  23. package/src/components/layout/Grid/index.ts +4 -0
  24. package/src/components/layout/Grid/masonry.ts +134 -0
  25. package/src/index.ts +2 -0
  26. package/src/web-components/index.ts +4 -0
  27. package/src/web-components/layout/BdsGrid.mdx +210 -0
  28. package/src/web-components/layout/BdsGrid.stories.tsx +209 -0
  29. package/src/web-components/layout/BdsGridItem.mdx +52 -0
  30. package/src/web-components/layout/BdsGridItem.stories.tsx +72 -0
  31. package/src/web-components/layout/bds-grid-item.spec.ts +102 -0
  32. package/src/web-components/layout/bds-grid-item.ts +177 -0
  33. package/src/web-components/layout/bds-grid.spec.ts +62 -0
  34. package/src/web-components/layout/bds-grid.ts +184 -0
@@ -41,6 +41,134 @@ export { BdsLink } from './ui/bds-link.js';
41
41
  import * as lit from 'lit';
42
42
  import { LitElement } from 'lit';
43
43
 
44
+ type GridVariant = 'main' | 'page' | 'funnel' | 'custom';
45
+ /**
46
+ * `<bds-grid>` — framework-agnostic Grid layout custom element.
47
+ *
48
+ * Attributes:
49
+ * variant — "main" (default) | "page" | "funnel" | "custom"
50
+ * columns — number; column count when variant="custom"
51
+ * is-centered — boolean; centers the grid and caps width via --bds-container_max-width
52
+ * is-masonry — boolean; enables masonry layout per CSS Grid Level 3.
53
+ * Uses native `display: grid-lanes` / `grid-template-rows: masonry`
54
+ * when supported, otherwise runs a JS polyfill.
55
+ * auto-span-media — boolean; child `<bds-grid-item>`s without a `column-span`
56
+ * attribute resolve their span from the intrinsic aspect
57
+ * ratio of their first `<img>`/`<video>` child.
58
+ *
59
+ * Slots:
60
+ * (default) — grid content; typically `<bds-grid-item>` children
61
+ */
62
+ declare class BdsGrid extends LitElement {
63
+ static styles: lit.CSSResult;
64
+ static properties: {
65
+ variant: {
66
+ type: StringConstructor;
67
+ reflect: boolean;
68
+ };
69
+ columns: {
70
+ type: NumberConstructor;
71
+ reflect: boolean;
72
+ };
73
+ isCentered: {
74
+ type: BooleanConstructor;
75
+ attribute: string;
76
+ reflect: boolean;
77
+ };
78
+ isMasonry: {
79
+ type: BooleanConstructor;
80
+ attribute: string;
81
+ reflect: boolean;
82
+ };
83
+ autoSpanMedia: {
84
+ type: BooleanConstructor;
85
+ attribute: string;
86
+ reflect: boolean;
87
+ };
88
+ };
89
+ variant: GridVariant;
90
+ columns: number | undefined;
91
+ isCentered: boolean;
92
+ isMasonry: boolean;
93
+ autoSpanMedia: boolean;
94
+ private _ro?;
95
+ private _mo?;
96
+ private _frame;
97
+ constructor();
98
+ disconnectedCallback(): void;
99
+ updated(): void;
100
+ private _gridEl;
101
+ private _items;
102
+ private _setupMasonry;
103
+ private _scheduleMasonry;
104
+ private _teardownMasonry;
105
+ render(): lit.TemplateResult<1>;
106
+ }
107
+ declare global {
108
+ interface HTMLElementTagNameMap {
109
+ 'bds-grid': BdsGrid;
110
+ }
111
+ }
112
+
113
+ type GridItemSpanValue = 'full' | 'three-quarters' | 'two-thirds' | 'half' | 'one-third' | 'one-quarter' | 'auto' | number;
114
+ /**
115
+ * `<bds-grid-item>` — grid cell for `<bds-grid>`.
116
+ *
117
+ * Attributes:
118
+ * column-span — 'full' | 'three-quarters' | 'two-thirds' | 'half' | 'one-third' | 'one-quarter' | 'auto' | number
119
+ * `'auto'` derives the span from the intrinsic aspect ratio of the first
120
+ * `<img>` or `<video>` child (see Grid.mdx for thresholds).
121
+ * If the parent `<bds-grid>` sets `auto-span-media` and this element has
122
+ * no `column-span`, the item behaves as if `column-span="auto"`.
123
+ * row-span — number
124
+ * start-column — number (escape hatch; overrides column-span)
125
+ * end-column — number (escape hatch; overrides column-span)
126
+ *
127
+ * @example
128
+ * <bds-grid-item column-span="three-quarters">Content</bds-grid-item>
129
+ */
130
+ declare class BdsGridItem extends LitElement {
131
+ static styles: lit.CSSResult;
132
+ static properties: {
133
+ columnSpan: {
134
+ attribute: string;
135
+ };
136
+ rowSpan: {
137
+ attribute: string;
138
+ type: NumberConstructor;
139
+ };
140
+ startColumn: {
141
+ attribute: string;
142
+ type: NumberConstructor;
143
+ };
144
+ endColumn: {
145
+ attribute: string;
146
+ type: NumberConstructor;
147
+ };
148
+ };
149
+ columnSpan: GridItemSpanValue | undefined;
150
+ rowSpan: number | undefined;
151
+ startColumn: number | undefined;
152
+ endColumn: number | undefined;
153
+ private _autoSpan;
154
+ private _mediaCleanup?;
155
+ private _slotObserver?;
156
+ constructor();
157
+ connectedCallback(): void;
158
+ disconnectedCallback(): void;
159
+ private _effectiveColumnSpan;
160
+ private _teardownMedia;
161
+ private _resolveAutoSpan;
162
+ private _setAutoSpan;
163
+ updated(): void;
164
+ render(): lit.TemplateResult<1>;
165
+ }
166
+ declare global {
167
+ interface HTMLElementTagNameMap {
168
+ 'bds-grid-item': BdsGridItem;
169
+ }
170
+ }
171
+
44
172
  type ButtonGroupOrientation = 'horizontal' | 'vertical';
45
173
  /**
46
174
  * `<bds-button-group>` — groups related buttons into a flex container.
@@ -608,4 +736,4 @@ declare global {
608
736
  }
609
737
  }
610
738
 
611
- export { BdsButtonGroup, BdsCalendar, BdsCarousel, BdsCheckboxGroup, BdsDropdownMenu, BdsDropdownMenuItem, BdsFormInput, BdsPagination, BdsRadioGroup, BdsTable };
739
+ export { BdsButtonGroup, BdsCalendar, BdsCarousel, BdsCheckboxGroup, BdsDropdownMenu, BdsDropdownMenuItem, BdsFormInput, BdsGrid, BdsGridItem, BdsPagination, BdsRadioGroup, BdsTable };
@@ -126,6 +126,315 @@ import {
126
126
  i2
127
127
  } from "./chunk-LGEZYFUU.js";
128
128
 
129
+ // src/components/layout/Grid/masonry.ts
130
+ import { useLayoutEffect } from "react";
131
+ var ROW_UNIT_PX = 1;
132
+ function supportsNativeMasonry() {
133
+ if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
134
+ return CSS.supports("display", "grid-lanes") || CSS.supports("grid-template-rows", "masonry");
135
+ }
136
+ function applyMasonryLayout(container, rawChildren) {
137
+ const cs = window.getComputedStyle(container);
138
+ const targetGap = parseFloat(cs.columnGap) || 0;
139
+ container.style.gridAutoRows = `${ROW_UNIT_PX}px`;
140
+ container.style.rowGap = "0px";
141
+ container.style.gridAutoFlow = "dense";
142
+ const children = rawChildren.filter(
143
+ (el) => el.nodeType === 1 && window.getComputedStyle(el).display !== "none"
144
+ );
145
+ for (const child of children) {
146
+ child.style.gridRow = "";
147
+ child.style.gridRowStart = "";
148
+ child.style.gridRowEnd = "";
149
+ }
150
+ const heights = children.map((child) => child.offsetHeight);
151
+ for (let i3 = 0; i3 < children.length; i3++) {
152
+ const span = Math.max(1, Math.ceil(heights[i3] + targetGap));
153
+ children[i3].style.gridRowEnd = `span ${span}`;
154
+ }
155
+ }
156
+ function clearMasonryLayout(container, rawChildren) {
157
+ container.style.gridAutoRows = "";
158
+ container.style.gridAutoFlow = "";
159
+ container.style.rowGap = "";
160
+ for (const child of rawChildren) {
161
+ if (child.nodeType !== 1) continue;
162
+ child.style.gridRow = "";
163
+ child.style.gridRowStart = "";
164
+ child.style.gridRowEnd = "";
165
+ }
166
+ }
167
+
168
+ // src/web-components/layout/bds-grid.ts
169
+ var BdsGrid = class extends i2 {
170
+ static styles = i`
171
+ :host {
172
+ display: block;
173
+ }
174
+
175
+ .grid {
176
+ display: grid;
177
+ gap: var(--grid_gap, var(--bds-grid_gap));
178
+ box-sizing: border-box;
179
+ }
180
+
181
+ .grid.--main {
182
+ grid-template-columns: var(--bds-grid_template-columns);
183
+ }
184
+
185
+ .grid.--page {
186
+ grid-template-columns: 1fr;
187
+ grid-column: 1 / -1;
188
+ }
189
+
190
+ .grid.--funnel {
191
+ grid-template-columns: var(--bds-grid_template-columns);
192
+ max-inline-size: var(--bds-container_max-width--narrow);
193
+ margin-inline: auto;
194
+ }
195
+
196
+ .grid.--custom {
197
+ grid-template-columns: repeat(var(--grid_columns, 4), minmax(0, 1fr));
198
+ }
199
+
200
+ .grid.--centered {
201
+ margin-inline: auto;
202
+ max-inline-size: var(--bds-container_max-width);
203
+ padding-inline: var(--bds-container_spacing-inline);
204
+ }
205
+
206
+ @supports (grid-template-rows: masonry) {
207
+ .grid.--masonry {
208
+ grid-template-rows: masonry;
209
+ }
210
+ }
211
+
212
+ @supports (display: grid-lanes) {
213
+ .grid.--masonry {
214
+ display: grid-lanes;
215
+ }
216
+ }
217
+ `;
218
+ static properties = {
219
+ variant: { type: String, reflect: true },
220
+ columns: { type: Number, reflect: true },
221
+ isCentered: { type: Boolean, attribute: "is-centered", reflect: true },
222
+ isMasonry: { type: Boolean, attribute: "is-masonry", reflect: true },
223
+ autoSpanMedia: { type: Boolean, attribute: "auto-span-media", reflect: true }
224
+ };
225
+ _ro;
226
+ _mo;
227
+ _frame = 0;
228
+ constructor() {
229
+ super();
230
+ this.variant = "main";
231
+ this.columns = void 0;
232
+ this.isCentered = false;
233
+ this.isMasonry = false;
234
+ this.autoSpanMedia = false;
235
+ }
236
+ disconnectedCallback() {
237
+ super.disconnectedCallback();
238
+ this._teardownMasonry();
239
+ }
240
+ updated() {
241
+ if (this.isMasonry && !supportsNativeMasonry()) {
242
+ this._setupMasonry();
243
+ this._scheduleMasonry();
244
+ } else {
245
+ this._teardownMasonry();
246
+ }
247
+ }
248
+ _gridEl() {
249
+ return this.renderRoot.querySelector(".grid");
250
+ }
251
+ _items() {
252
+ return Array.from(this.children).filter((el) => el.nodeType === 1);
253
+ }
254
+ _setupMasonry() {
255
+ if (this._ro || this._mo) return;
256
+ const container = this._gridEl();
257
+ if (!container) return;
258
+ if (typeof ResizeObserver !== "undefined") {
259
+ this._ro = new ResizeObserver(() => this._scheduleMasonry());
260
+ for (const child of this._items()) this._ro.observe(child);
261
+ }
262
+ if (typeof MutationObserver !== "undefined") {
263
+ this._mo = new MutationObserver(() => {
264
+ if (this._ro) for (const child of this._items()) this._ro.observe(child);
265
+ this._scheduleMasonry();
266
+ });
267
+ this._mo.observe(this, { childList: true });
268
+ }
269
+ }
270
+ _scheduleMasonry() {
271
+ cancelAnimationFrame(this._frame);
272
+ this._frame = requestAnimationFrame(() => {
273
+ const container = this._gridEl();
274
+ if (container) applyMasonryLayout(container, this._items());
275
+ });
276
+ }
277
+ _teardownMasonry() {
278
+ cancelAnimationFrame(this._frame);
279
+ this._ro?.disconnect();
280
+ this._mo?.disconnect();
281
+ this._ro = void 0;
282
+ this._mo = void 0;
283
+ const container = this._gridEl();
284
+ if (container) clearMasonryLayout(container, this._items());
285
+ }
286
+ render() {
287
+ const classes = [
288
+ "grid",
289
+ `--${this.variant}`,
290
+ this.isCentered ? "--centered" : "",
291
+ this.isMasonry ? "--masonry" : ""
292
+ ].filter(Boolean).join(" ");
293
+ const styleAttr = this.variant === "custom" && this.columns !== void 0 ? `--grid_columns: ${this.columns}` : "";
294
+ return T`<div class=${classes} style=${styleAttr}><slot></slot></div>`;
295
+ }
296
+ };
297
+ customElements.define("bds-grid", BdsGrid);
298
+
299
+ // src/components/layout/Grid/autoSpan.ts
300
+ function aspectToColumnSpan(ratio) {
301
+ if (!Number.isFinite(ratio) || ratio <= 0) return "one-quarter";
302
+ if (ratio > 3.5) return "full";
303
+ if (ratio > 2.2) return "three-quarters";
304
+ if (ratio > 1.5) return "half";
305
+ return "one-quarter";
306
+ }
307
+ function findMediaChild(root) {
308
+ return root.querySelector("img, video");
309
+ }
310
+ function readMediaAspect(media) {
311
+ if (media instanceof HTMLImageElement) {
312
+ if (media.naturalWidth > 0 && media.naturalHeight > 0) {
313
+ return media.naturalWidth / media.naturalHeight;
314
+ }
315
+ return null;
316
+ }
317
+ if (media.videoWidth > 0 && media.videoHeight > 0) {
318
+ return media.videoWidth / media.videoHeight;
319
+ }
320
+ return null;
321
+ }
322
+ function mediaReadyEvent(media) {
323
+ if (media instanceof HTMLImageElement) return media.complete ? null : "load";
324
+ if (media.readyState < 1) return "loadedmetadata";
325
+ return null;
326
+ }
327
+
328
+ // src/web-components/layout/bds-grid-item.ts
329
+ var SPAN_VAR = {
330
+ full: "var(--bds-grid_span-100)",
331
+ "three-quarters": "span var(--bds-grid_columns-75)",
332
+ "two-thirds": "span var(--bds-grid_columns-66)",
333
+ half: "span var(--bds-grid_columns-50)",
334
+ "one-third": "span var(--bds-grid_columns-33)",
335
+ "one-quarter": "span var(--bds-grid_columns-25)"
336
+ };
337
+ function resolveSpan(value) {
338
+ if (value === void 0 || value === null) return SPAN_VAR.full;
339
+ if (typeof value === "number") return `span ${value}`;
340
+ if (!(value in SPAN_VAR)) {
341
+ const n = Number(value);
342
+ if (Number.isFinite(n)) return `span ${n}`;
343
+ return SPAN_VAR.full;
344
+ }
345
+ return SPAN_VAR[value];
346
+ }
347
+ var BdsGridItem = class extends i2 {
348
+ static styles = i`
349
+ :host {
350
+ display: block;
351
+ min-inline-size: 0;
352
+ }
353
+ `;
354
+ static properties = {
355
+ columnSpan: { attribute: "column-span" },
356
+ rowSpan: { attribute: "row-span", type: Number },
357
+ startColumn: { attribute: "start-column", type: Number },
358
+ endColumn: { attribute: "end-column", type: Number }
359
+ };
360
+ _autoSpan = "one-quarter";
361
+ _mediaCleanup;
362
+ _slotObserver;
363
+ constructor() {
364
+ super();
365
+ this.columnSpan = void 0;
366
+ this.rowSpan = void 0;
367
+ this.startColumn = void 0;
368
+ this.endColumn = void 0;
369
+ }
370
+ connectedCallback() {
371
+ super.connectedCallback();
372
+ if (typeof MutationObserver !== "undefined") {
373
+ this._slotObserver = new MutationObserver(() => this._resolveAutoSpan());
374
+ this._slotObserver.observe(this, { childList: true, subtree: true });
375
+ }
376
+ }
377
+ disconnectedCallback() {
378
+ super.disconnectedCallback();
379
+ this._teardownMedia();
380
+ this._slotObserver?.disconnect();
381
+ this._slotObserver = void 0;
382
+ }
383
+ _effectiveColumnSpan() {
384
+ if (this.columnSpan !== void 0 && this.columnSpan !== null) return this.columnSpan;
385
+ const parent = this.closest("bds-grid");
386
+ if (parent?.autoSpanMedia) return "auto";
387
+ return "full";
388
+ }
389
+ _teardownMedia() {
390
+ this._mediaCleanup?.();
391
+ this._mediaCleanup = void 0;
392
+ }
393
+ _resolveAutoSpan() {
394
+ this._teardownMedia();
395
+ if (this._effectiveColumnSpan() !== "auto") return;
396
+ const media = findMediaChild(this);
397
+ if (!media) {
398
+ this._setAutoSpan("one-quarter");
399
+ return;
400
+ }
401
+ const aspect = readMediaAspect(media);
402
+ if (aspect !== null) {
403
+ this._setAutoSpan(aspectToColumnSpan(aspect));
404
+ return;
405
+ }
406
+ const event = mediaReadyEvent(media);
407
+ if (!event) {
408
+ this._setAutoSpan("one-quarter");
409
+ return;
410
+ }
411
+ const handler = () => {
412
+ const a = readMediaAspect(media);
413
+ if (a !== null) this._setAutoSpan(aspectToColumnSpan(a));
414
+ };
415
+ media.addEventListener(event, handler, { once: true });
416
+ this._mediaCleanup = () => media.removeEventListener(event, handler);
417
+ }
418
+ _setAutoSpan(next) {
419
+ if (this._autoSpan === next) return;
420
+ this._autoSpan = next;
421
+ this.requestUpdate();
422
+ }
423
+ updated() {
424
+ const effective = this._effectiveColumnSpan();
425
+ if (effective === "auto") this._resolveAutoSpan();
426
+ const hasExplicitRange = this.startColumn !== void 0 || this.endColumn !== void 0;
427
+ const resolved = effective === "auto" ? this._autoSpan : effective;
428
+ const gridColumn = hasExplicitRange ? `${this.startColumn ?? "auto"} / ${this.endColumn ?? "auto"}` : resolveSpan(resolved);
429
+ this.style.gridColumn = gridColumn;
430
+ this.style.gridRow = this.rowSpan !== void 0 ? `span ${this.rowSpan}` : "";
431
+ }
432
+ render() {
433
+ return T`<slot></slot>`;
434
+ }
435
+ };
436
+ customElements.define("bds-grid-item", BdsGridItem);
437
+
129
438
  // src/web-components/ui/bds-button-group.ts
130
439
  var BdsButtonGroup = class extends i2 {
131
440
  static styles = i`
@@ -1631,6 +1940,8 @@ export {
1631
1940
  BdsDropdownMenuItem,
1632
1941
  BdsFileInput,
1633
1942
  BdsFormInput,
1943
+ BdsGrid,
1944
+ BdsGridItem,
1634
1945
  BdsIconWrapper,
1635
1946
  BdsLink,
1636
1947
  BdsLoading,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-components",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "BoostDev React component library: accessible, token-driven components built on @boostdev/design-system-foundation",
5
5
  "keywords": [
6
6
  "React",