@boostdev/design-system-components 2.0.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 (55) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +42 -1
  3. package/dist/client.cjs +305 -60
  4. package/dist/client.css +577 -523
  5. package/dist/client.d.cts +42 -2
  6. package/dist/client.d.ts +42 -2
  7. package/dist/client.js +311 -60
  8. package/dist/index.cjs +305 -60
  9. package/dist/index.css +577 -523
  10. package/dist/index.d.cts +42 -2
  11. package/dist/index.d.ts +42 -2
  12. package/dist/index.js +311 -60
  13. package/dist/native/index.cjs +5 -2
  14. package/dist/native/index.d.cts +3 -1
  15. package/dist/native/index.d.ts +3 -1
  16. package/dist/native/index.js +5 -2
  17. package/dist/web-components/{chunk-3REOIRDW.js → chunk-N3TN6WCH.js} +26 -1
  18. package/dist/web-components/index.d.ts +129 -1
  19. package/dist/web-components/index.js +312 -1
  20. package/dist/web-components/interaction/bds-button.d.ts +7 -0
  21. package/dist/web-components/interaction/bds-button.js +1 -1
  22. package/package.json +1 -1
  23. package/src/components/interaction/Button/Button.mdx +81 -36
  24. package/src/components/interaction/Button/Button.module.css +24 -0
  25. package/src/components/interaction/Button/Button.native.mdx +31 -12
  26. package/src/components/interaction/Button/Button.native.spec.tsx +20 -0
  27. package/src/components/interaction/Button/Button.native.stories.tsx +110 -9
  28. package/src/components/interaction/Button/Button.native.tsx +13 -4
  29. package/src/components/interaction/Button/Button.spec.tsx +16 -0
  30. package/src/components/interaction/Button/Button.stories.tsx +134 -16
  31. package/src/components/interaction/Button/Button.tsx +4 -0
  32. package/src/components/layout/Grid/Grid.mdx +244 -0
  33. package/src/components/layout/Grid/Grid.module.css +59 -0
  34. package/src/components/layout/Grid/Grid.spec.tsx +401 -0
  35. package/src/components/layout/Grid/Grid.stories.tsx +160 -0
  36. package/src/components/layout/Grid/Grid.tsx +85 -0
  37. package/src/components/layout/Grid/GridItem.tsx +150 -0
  38. package/src/components/layout/Grid/autoSpan.ts +77 -0
  39. package/src/components/layout/Grid/index.ts +4 -0
  40. package/src/components/layout/Grid/masonry.ts +134 -0
  41. package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +46 -14
  42. package/src/index.ts +2 -0
  43. package/src/web-components/index.ts +4 -0
  44. package/src/web-components/interaction/BdsButton.mdx +46 -14
  45. package/src/web-components/interaction/BdsButton.stories.tsx +171 -19
  46. package/src/web-components/interaction/bds-button.spec.ts +35 -0
  47. package/src/web-components/interaction/bds-button.ts +28 -1
  48. package/src/web-components/layout/BdsGrid.mdx +210 -0
  49. package/src/web-components/layout/BdsGrid.stories.tsx +209 -0
  50. package/src/web-components/layout/BdsGridItem.mdx +52 -0
  51. package/src/web-components/layout/BdsGridItem.stories.tsx +72 -0
  52. package/src/web-components/layout/bds-grid-item.spec.ts +102 -0
  53. package/src/web-components/layout/bds-grid-item.ts +177 -0
  54. package/src/web-components/layout/bds-grid.spec.ts +62 -0
  55. package/src/web-components/layout/bds-grid.ts +184 -0
@@ -2139,6 +2139,7 @@ function Button({
2139
2139
  iconStart,
2140
2140
  iconEnd,
2141
2141
  disabled = false,
2142
+ isIconOnly = false,
2142
2143
  onPress,
2143
2144
  accessibilityLabel,
2144
2145
  style
@@ -2151,9 +2152,11 @@ function Button({
2151
2152
  const isOutline = variant === "outline" || variant === "ghost";
2152
2153
  const containerStyle = {
2153
2154
  height,
2154
- paddingHorizontal: px,
2155
+ paddingHorizontal: isIconOnly ? import_native.spacing.xs : px,
2155
2156
  borderRadius: radius,
2156
2157
  opacity: disabled ? 0.4 : 1,
2158
+ // Icon-only: square with equal xs padding on both axes.
2159
+ ...isIconOnly && { aspectRatio: 1, paddingVertical: import_native.spacing.xs },
2157
2160
  ...variant === "default" && { backgroundColor: primaryColor },
2158
2161
  ...isOutline && {
2159
2162
  backgroundColor: "transparent",
@@ -2173,7 +2176,7 @@ function Button({
2173
2176
  style: ({ pressed }) => [styles24.pressable, containerStyle, pressed && { opacity: 0.75 }, style],
2174
2177
  children: [
2175
2178
  iconStart && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native27.View, { importantForAccessibility: "no-hide-descendants", accessibilityElementsHidden: true, children: iconStart }),
2176
- children && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native27.Text, { style: [styles24.label, { fontSize, color: textColor }], children }),
2179
+ children && (isIconOnly ? children : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native27.Text, { style: [styles24.label, { fontSize, color: textColor }], children })),
2177
2180
  iconEnd && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native27.View, { importantForAccessibility: "no-hide-descendants", children: iconEnd })
2178
2181
  ]
2179
2182
  }
@@ -293,11 +293,13 @@ interface ButtonProps {
293
293
  iconStart?: ReactNode;
294
294
  iconEnd?: ReactNode;
295
295
  disabled?: boolean;
296
+ /** Forces a 1:1 aspect ratio with minimal padding for icon-only buttons. Requires `accessibilityLabel`. */
297
+ isIconOnly?: boolean;
296
298
  onPress?: () => void;
297
299
  accessibilityLabel?: string;
298
300
  style?: StyleProp<ViewStyle>;
299
301
  }
300
- declare function Button({ children, variant, size, iconStart, iconEnd, disabled, onPress, accessibilityLabel, style, }: Readonly<ButtonProps>): react_jsx_runtime.JSX.Element;
302
+ declare function Button({ children, variant, size, iconStart, iconEnd, disabled, isIconOnly, onPress, accessibilityLabel, style, }: Readonly<ButtonProps>): react_jsx_runtime.JSX.Element;
301
303
 
302
304
  interface RatingProps {
303
305
  value: number;
@@ -293,11 +293,13 @@ interface ButtonProps {
293
293
  iconStart?: ReactNode;
294
294
  iconEnd?: ReactNode;
295
295
  disabled?: boolean;
296
+ /** Forces a 1:1 aspect ratio with minimal padding for icon-only buttons. Requires `accessibilityLabel`. */
297
+ isIconOnly?: boolean;
296
298
  onPress?: () => void;
297
299
  accessibilityLabel?: string;
298
300
  style?: StyleProp<ViewStyle>;
299
301
  }
300
- declare function Button({ children, variant, size, iconStart, iconEnd, disabled, onPress, accessibilityLabel, style, }: Readonly<ButtonProps>): react_jsx_runtime.JSX.Element;
302
+ declare function Button({ children, variant, size, iconStart, iconEnd, disabled, isIconOnly, onPress, accessibilityLabel, style, }: Readonly<ButtonProps>): react_jsx_runtime.JSX.Element;
301
303
 
302
304
  interface RatingProps {
303
305
  value: number;
@@ -2087,6 +2087,7 @@ function Button({
2087
2087
  iconStart,
2088
2088
  iconEnd,
2089
2089
  disabled = false,
2090
+ isIconOnly = false,
2090
2091
  onPress,
2091
2092
  accessibilityLabel,
2092
2093
  style
@@ -2099,9 +2100,11 @@ function Button({
2099
2100
  const isOutline = variant === "outline" || variant === "ghost";
2100
2101
  const containerStyle = {
2101
2102
  height,
2102
- paddingHorizontal: px,
2103
+ paddingHorizontal: isIconOnly ? spacing.xs : px,
2103
2104
  borderRadius: radius,
2104
2105
  opacity: disabled ? 0.4 : 1,
2106
+ // Icon-only: square with equal xs padding on both axes.
2107
+ ...isIconOnly && { aspectRatio: 1, paddingVertical: spacing.xs },
2105
2108
  ...variant === "default" && { backgroundColor: primaryColor },
2106
2109
  ...isOutline && {
2107
2110
  backgroundColor: "transparent",
@@ -2121,7 +2124,7 @@ function Button({
2121
2124
  style: ({ pressed }) => [styles24.pressable, containerStyle, pressed && { opacity: 0.75 }, style],
2122
2125
  children: [
2123
2126
  iconStart && /* @__PURE__ */ jsx27(View22, { importantForAccessibility: "no-hide-descendants", accessibilityElementsHidden: true, children: iconStart }),
2124
- children && /* @__PURE__ */ jsx27(Text20, { style: [styles24.label, { fontSize, color: textColor }], children }),
2127
+ children && (isIconOnly ? children : /* @__PURE__ */ jsx27(Text20, { style: [styles24.label, { fontSize, color: textColor }], children })),
2125
2128
  iconEnd && /* @__PURE__ */ jsx27(View22, { importantForAccessibility: "no-hide-descendants", children: iconEnd })
2126
2129
  ]
2127
2130
  }
@@ -90,6 +90,23 @@ var BdsButton = class extends i2 {
90
90
  animation: pulse 3s infinite;
91
91
  }
92
92
 
93
+ /* Icon-only: square (1:1), equal xs padding on both axes — composes with
94
+ any variant/size. box-sizing: border-box keeps the outer box at
95
+ --button_height; without it, all: unset reverts to content-box and
96
+ padding-block would stretch the button past its declared height.
97
+ Slotted child fills the content area (button height minus the two
98
+ padding sides) so SVG icons scale with button size. */
99
+ .button.is-icon-only {
100
+ aspect-ratio: 1;
101
+ box-sizing: border-box;
102
+ padding: var(--bds-space_xs);
103
+ }
104
+
105
+ :host([is-icon-only]) ::slotted(*) {
106
+ inline-size: 100%;
107
+ block-size: 100%;
108
+ }
109
+
93
110
  /* SVG icon colour */
94
111
  .button svg {
95
112
  --icon__stroke: currentcolor;
@@ -117,6 +134,12 @@ var BdsButton = class extends i2 {
117
134
  transition: var(--bds-animation_transition);
118
135
  }
119
136
 
137
+ /* Icon-only: zero out icon slot margins so icon centres */
138
+ .button.is-icon-only .icon-start.has-content,
139
+ .button.is-icon-only .icon-end.has-content {
140
+ margin-inline: 0;
141
+ }
142
+
120
143
  /* Hover icon animation */
121
144
  @media (hover: hover) and (pointer: fine) {
122
145
  .button:hover .icon-start svg,
@@ -164,6 +187,7 @@ var BdsButton = class extends i2 {
164
187
  target: { type: String },
165
188
  rel: { type: String },
166
189
  hasPulse: { type: Boolean, attribute: "has-pulse", reflect: true },
190
+ isIconOnly: { type: Boolean, attribute: "is-icon-only", reflect: true },
167
191
  type: { type: String },
168
192
  ariaLabel: { type: String, attribute: "aria-label" },
169
193
  // Internal state
@@ -176,6 +200,7 @@ var BdsButton = class extends i2 {
176
200
  this.size = "medium";
177
201
  this.disabled = false;
178
202
  this.hasPulse = false;
203
+ this.isIconOnly = false;
179
204
  this.type = "button";
180
205
  this.ariaLabel = null;
181
206
  this._iconStartFilled = false;
@@ -191,7 +216,7 @@ var BdsButton = class extends i2 {
191
216
  }
192
217
  }
193
218
  get _classes() {
194
- return `button ${this.variant} ${this.size}${this.hasPulse ? " has-pulse" : ""}`;
219
+ return `button ${this.variant} ${this.size}${this.hasPulse ? " has-pulse" : ""}${this.isIconOnly ? " is-icon-only" : ""}`;
195
220
  }
196
221
  _handleAnchorClick(e) {
197
222
  if (this.disabled) {
@@ -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 };
@@ -69,7 +69,7 @@ import {
69
69
  } from "./chunk-UFFCKVZE.js";
70
70
  import {
71
71
  BdsButton
72
- } from "./chunk-3REOIRDW.js";
72
+ } from "./chunk-N3TN6WCH.js";
73
73
  import {
74
74
  BdsCollapsible
75
75
  } from "./chunk-6I2DBFQ7.js";
@@ -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,
@@ -14,6 +14,7 @@ type ButtonSize = 'small' | 'medium' | 'large';
14
14
  * target — forwarded to <a> when href is set
15
15
  * rel — forwarded to <a> when href is set
16
16
  * has-pulse — boolean, enables the pulse ring animation
17
+ * is-icon-only — boolean, forces a 1:1 aspect ratio with minimal xs padding (requires aria-label)
17
18
  * type — "button" (default) | "submit" | "reset"
18
19
  *
19
20
  * Slots:
@@ -59,6 +60,11 @@ declare class BdsButton extends LitElement {
59
60
  attribute: string;
60
61
  reflect: boolean;
61
62
  };
63
+ isIconOnly: {
64
+ type: BooleanConstructor;
65
+ attribute: string;
66
+ reflect: boolean;
67
+ };
62
68
  type: {
63
69
  type: StringConstructor;
64
70
  };
@@ -80,6 +86,7 @@ declare class BdsButton extends LitElement {
80
86
  target: string | undefined;
81
87
  rel: string | undefined;
82
88
  hasPulse: boolean;
89
+ isIconOnly: boolean;
83
90
  type: 'button' | 'submit' | 'reset';
84
91
  ariaLabel: string | null;
85
92
  private _iconStartFilled;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BdsButton
3
- } from "../chunk-3REOIRDW.js";
3
+ } from "../chunk-N3TN6WCH.js";
4
4
  import "../chunk-LGEZYFUU.js";
5
5
  export {
6
6
  BdsButton
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-components",
3
- "version": "2.0.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",