@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.
- package/AGENTS.md +2 -1
- package/README.md +42 -1
- package/dist/client.cjs +305 -60
- package/dist/client.css +577 -523
- package/dist/client.d.cts +42 -2
- package/dist/client.d.ts +42 -2
- package/dist/client.js +311 -60
- package/dist/index.cjs +305 -60
- package/dist/index.css +577 -523
- package/dist/index.d.cts +42 -2
- package/dist/index.d.ts +42 -2
- package/dist/index.js +311 -60
- package/dist/native/index.cjs +5 -2
- package/dist/native/index.d.cts +3 -1
- package/dist/native/index.d.ts +3 -1
- package/dist/native/index.js +5 -2
- package/dist/web-components/{chunk-3REOIRDW.js → chunk-N3TN6WCH.js} +26 -1
- package/dist/web-components/index.d.ts +129 -1
- package/dist/web-components/index.js +312 -1
- package/dist/web-components/interaction/bds-button.d.ts +7 -0
- package/dist/web-components/interaction/bds-button.js +1 -1
- package/package.json +1 -1
- package/src/components/interaction/Button/Button.mdx +81 -36
- package/src/components/interaction/Button/Button.module.css +24 -0
- package/src/components/interaction/Button/Button.native.mdx +31 -12
- package/src/components/interaction/Button/Button.native.spec.tsx +20 -0
- package/src/components/interaction/Button/Button.native.stories.tsx +110 -9
- package/src/components/interaction/Button/Button.native.tsx +13 -4
- package/src/components/interaction/Button/Button.spec.tsx +16 -0
- package/src/components/interaction/Button/Button.stories.tsx +134 -16
- package/src/components/interaction/Button/Button.tsx +4 -0
- package/src/components/layout/Grid/Grid.mdx +244 -0
- package/src/components/layout/Grid/Grid.module.css +59 -0
- package/src/components/layout/Grid/Grid.spec.tsx +401 -0
- package/src/components/layout/Grid/Grid.stories.tsx +160 -0
- package/src/components/layout/Grid/Grid.tsx +85 -0
- package/src/components/layout/Grid/GridItem.tsx +150 -0
- package/src/components/layout/Grid/autoSpan.ts +77 -0
- package/src/components/layout/Grid/index.ts +4 -0
- package/src/components/layout/Grid/masonry.ts +134 -0
- package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +46 -14
- package/src/index.ts +2 -0
- package/src/web-components/index.ts +4 -0
- package/src/web-components/interaction/BdsButton.mdx +46 -14
- package/src/web-components/interaction/BdsButton.stories.tsx +171 -19
- package/src/web-components/interaction/bds-button.spec.ts +35 -0
- package/src/web-components/interaction/bds-button.ts +28 -1
- package/src/web-components/layout/BdsGrid.mdx +210 -0
- package/src/web-components/layout/BdsGrid.stories.tsx +209 -0
- package/src/web-components/layout/BdsGridItem.mdx +52 -0
- package/src/web-components/layout/BdsGridItem.stories.tsx +72 -0
- package/src/web-components/layout/bds-grid-item.spec.ts +102 -0
- package/src/web-components/layout/bds-grid-item.ts +177 -0
- package/src/web-components/layout/bds-grid.spec.ts +62 -0
- package/src/web-components/layout/bds-grid.ts +184 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ElementType,
|
|
3
|
+
ComponentPropsWithoutRef,
|
|
4
|
+
CSSProperties,
|
|
5
|
+
useContext,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import css from './Grid.module.css';
|
|
11
|
+
import { cn } from '@boostdev/design-system-foundation';
|
|
12
|
+
import type { WithClassName } from '../../../types';
|
|
13
|
+
import { GridContext } from './Grid';
|
|
14
|
+
import {
|
|
15
|
+
aspectToColumnSpan,
|
|
16
|
+
findMediaChild,
|
|
17
|
+
mediaReadyEvent,
|
|
18
|
+
readMediaAspect,
|
|
19
|
+
type AutoSpanResult,
|
|
20
|
+
} from './autoSpan';
|
|
21
|
+
|
|
22
|
+
export type GridItemSpanValue =
|
|
23
|
+
| 'full'
|
|
24
|
+
| 'three-quarters'
|
|
25
|
+
| 'two-thirds'
|
|
26
|
+
| 'half'
|
|
27
|
+
| 'one-third'
|
|
28
|
+
| 'one-quarter'
|
|
29
|
+
| 'auto'
|
|
30
|
+
| number;
|
|
31
|
+
|
|
32
|
+
export const GridItemSpan = {
|
|
33
|
+
full: 'full',
|
|
34
|
+
threeQuarters: 'three-quarters',
|
|
35
|
+
twoThirds: 'two-thirds',
|
|
36
|
+
half: 'half',
|
|
37
|
+
oneThird: 'one-third',
|
|
38
|
+
oneQuarter: 'one-quarter',
|
|
39
|
+
auto: 'auto',
|
|
40
|
+
} as const satisfies Record<string, GridItemSpanValue>;
|
|
41
|
+
|
|
42
|
+
const SPAN_VAR: Record<Exclude<GridItemSpanValue, number | 'auto'>, string> = {
|
|
43
|
+
full: 'var(--bds-grid_span-100)',
|
|
44
|
+
'three-quarters': 'span var(--bds-grid_columns-75)',
|
|
45
|
+
'two-thirds': 'span var(--bds-grid_columns-66)',
|
|
46
|
+
half: 'span var(--bds-grid_columns-50)',
|
|
47
|
+
'one-third': 'span var(--bds-grid_columns-33)',
|
|
48
|
+
'one-quarter': 'span var(--bds-grid_columns-25)',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function resolveSpan(value: Exclude<GridItemSpanValue, 'auto'>): string {
|
|
52
|
+
if (typeof value === 'number') return `span ${value}`;
|
|
53
|
+
return SPAN_VAR[value];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type GridItemOwnProps = WithClassName & {
|
|
57
|
+
columnSpan?: GridItemSpanValue;
|
|
58
|
+
rowSpan?: number;
|
|
59
|
+
startColumn?: number;
|
|
60
|
+
endColumn?: number;
|
|
61
|
+
as?: ElementType;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type GridItemProps<C extends ElementType = 'div'> = GridItemOwnProps &
|
|
65
|
+
Omit<ComponentPropsWithoutRef<C>, keyof GridItemOwnProps>;
|
|
66
|
+
|
|
67
|
+
export function GridItem<C extends ElementType = 'div'>({
|
|
68
|
+
children,
|
|
69
|
+
className,
|
|
70
|
+
columnSpan,
|
|
71
|
+
rowSpan,
|
|
72
|
+
startColumn,
|
|
73
|
+
endColumn,
|
|
74
|
+
as,
|
|
75
|
+
style,
|
|
76
|
+
...rest
|
|
77
|
+
}: GridItemProps<C>) {
|
|
78
|
+
const { autoSpanMedia } = useContext(GridContext);
|
|
79
|
+
const effectiveSpan: GridItemSpanValue =
|
|
80
|
+
columnSpan ?? (autoSpanMedia ? 'auto' : 'full');
|
|
81
|
+
|
|
82
|
+
const ref = useRef<HTMLElement | null>(null);
|
|
83
|
+
const [autoSpan, setAutoSpan] = useState<AutoSpanResult>('one-quarter');
|
|
84
|
+
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
if (effectiveSpan !== 'auto') return;
|
|
87
|
+
const el = ref.current;
|
|
88
|
+
if (!el) return;
|
|
89
|
+
|
|
90
|
+
let cancelled = false;
|
|
91
|
+
let cleanup: (() => void) | undefined;
|
|
92
|
+
|
|
93
|
+
const resolve = () => {
|
|
94
|
+
if (cancelled) return;
|
|
95
|
+
const media = findMediaChild(el);
|
|
96
|
+
if (!media) {
|
|
97
|
+
setAutoSpan('one-quarter');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const aspect = readMediaAspect(media);
|
|
101
|
+
if (aspect !== null) {
|
|
102
|
+
setAutoSpan(aspectToColumnSpan(aspect));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const event = mediaReadyEvent(media);
|
|
106
|
+
if (!event) {
|
|
107
|
+
setAutoSpan('one-quarter');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const handler = () => {
|
|
111
|
+
const a = readMediaAspect(media);
|
|
112
|
+
if (a !== null) setAutoSpan(aspectToColumnSpan(a));
|
|
113
|
+
};
|
|
114
|
+
media.addEventListener(event, handler, { once: true });
|
|
115
|
+
cleanup = () => media.removeEventListener(event, handler);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
resolve();
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
cleanup?.();
|
|
123
|
+
};
|
|
124
|
+
}, [effectiveSpan, children]);
|
|
125
|
+
|
|
126
|
+
const hasExplicitRange = startColumn !== undefined || endColumn !== undefined;
|
|
127
|
+
|
|
128
|
+
const gridColumn = hasExplicitRange
|
|
129
|
+
? `${startColumn ?? 'auto'} / ${endColumn ?? 'auto'}`
|
|
130
|
+
: resolveSpan(effectiveSpan === 'auto' ? autoSpan : effectiveSpan);
|
|
131
|
+
|
|
132
|
+
const composedStyle: CSSProperties = {
|
|
133
|
+
...(style as CSSProperties),
|
|
134
|
+
gridColumn,
|
|
135
|
+
...(rowSpan !== undefined ? { gridRow: `span ${rowSpan}` } : {}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const Component = as ?? 'div';
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Component
|
|
142
|
+
ref={ref as never}
|
|
143
|
+
{...rest}
|
|
144
|
+
className={cn(css.item, className)}
|
|
145
|
+
style={composedStyle}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</Component>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `columnSpan="auto"` support — derives a column span from the intrinsic aspect
|
|
3
|
+
* ratio of the first `<img>` or `<video>` child of a GridItem.
|
|
4
|
+
*
|
|
5
|
+
* Framework-agnostic: pure functions, used by both the React `GridItem` and the
|
|
6
|
+
* `bds-grid-item` web component. No layout reflow — reads `naturalWidth/Height`
|
|
7
|
+
* on images and `videoWidth/videoHeight` on videos, both of which are cached
|
|
8
|
+
* DOM properties that resolve once the media's metadata is available.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type AutoSpanResult =
|
|
12
|
+
| 'full'
|
|
13
|
+
| 'three-quarters'
|
|
14
|
+
| 'half'
|
|
15
|
+
| 'one-quarter';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Map a width-to-height ratio to a named column span.
|
|
19
|
+
*
|
|
20
|
+
* Ratios were chosen for photography conventions:
|
|
21
|
+
* • 3:2 / 16:9 landscape → `half` (1.5 < r ≤ 2.2)
|
|
22
|
+
* • 21:9 / 2.35:1 / 2.4:1 / 2.5:1 → `three-quarters` (2.2 < r ≤ 3.5)
|
|
23
|
+
* • 32:9 / hard pano → `full` (r > 3.5)
|
|
24
|
+
* • Portrait, square, or unknown → `one-quarter` (r ≤ 1.5)
|
|
25
|
+
*
|
|
26
|
+
* Returns `one-quarter` for invalid/non-positive ratios so layout stays stable
|
|
27
|
+
* when metadata isn't yet resolved.
|
|
28
|
+
*/
|
|
29
|
+
export function aspectToColumnSpan(ratio: number): AutoSpanResult {
|
|
30
|
+
if (!Number.isFinite(ratio) || ratio <= 0) return 'one-quarter';
|
|
31
|
+
if (ratio > 3.5) return 'full';
|
|
32
|
+
if (ratio > 2.2) return 'three-quarters';
|
|
33
|
+
if (ratio > 1.5) return 'half';
|
|
34
|
+
return 'one-quarter';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find the first `<img>` or `<video>` descendant of `root`. Returns `null`
|
|
39
|
+
* if none. Only descends the light DOM; shadow content is opaque by design.
|
|
40
|
+
*/
|
|
41
|
+
export function findMediaChild(
|
|
42
|
+
root: HTMLElement,
|
|
43
|
+
): HTMLImageElement | HTMLVideoElement | null {
|
|
44
|
+
return root.querySelector('img, video');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read the intrinsic aspect ratio (width / height) of an image or video.
|
|
49
|
+
* Returns `null` if the intrinsic dimensions haven't resolved yet — consumers
|
|
50
|
+
* should subscribe to `load` (images) or `loadedmetadata` (video) and retry.
|
|
51
|
+
*/
|
|
52
|
+
export function readMediaAspect(
|
|
53
|
+
media: HTMLImageElement | HTMLVideoElement,
|
|
54
|
+
): number | null {
|
|
55
|
+
if (media instanceof HTMLImageElement) {
|
|
56
|
+
if (media.naturalWidth > 0 && media.naturalHeight > 0) {
|
|
57
|
+
return media.naturalWidth / media.naturalHeight;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (media.videoWidth > 0 && media.videoHeight > 0) {
|
|
62
|
+
return media.videoWidth / media.videoHeight;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the name of the event the given media element fires when its
|
|
69
|
+
* intrinsic dimensions become available.
|
|
70
|
+
*/
|
|
71
|
+
export function mediaReadyEvent(
|
|
72
|
+
media: HTMLImageElement | HTMLVideoElement,
|
|
73
|
+
): 'load' | 'loadedmetadata' | null {
|
|
74
|
+
if (media instanceof HTMLImageElement) return media.complete ? null : 'load';
|
|
75
|
+
if (media.readyState < 1 /* HAVE_METADATA */) return 'loadedmetadata';
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { RefObject, useLayoutEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Masonry layout polyfill based on CSS Grid Layout Module Level 3 (a.k.a. "grid-lanes").
|
|
5
|
+
* https://www.w3.org/TR/css-grid-3/ · https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Masonry_layout
|
|
6
|
+
*
|
|
7
|
+
* Implementation strategy — row-span packing:
|
|
8
|
+
* 1. Override the container to `grid-auto-rows: 1px` + `row-gap: 0`. The
|
|
9
|
+
* author-intended row gap is baked into each item's span instead of
|
|
10
|
+
* being applied between rows — that way the ceil() rounding can only
|
|
11
|
+
* overshoot by <1px per item, so visual gaps stay uniform.
|
|
12
|
+
* 2. Set `grid-auto-flow: dense` so items backfill into the earliest empty
|
|
13
|
+
* slot — this is what produces the masonry effect when items have
|
|
14
|
+
* variable heights.
|
|
15
|
+
* 3. For each child: measure its natural height H, then set
|
|
16
|
+
* `grid-row-end: span ceil(H + targetGap)`. The last `targetGap` pixels
|
|
17
|
+
* of each item's cell are empty, which renders as the visual gap below
|
|
18
|
+
* the item.
|
|
19
|
+
*
|
|
20
|
+
* Items stay in normal document flow — no `position: absolute`. CSS Grid handles
|
|
21
|
+
* column-track sizing (including the foundation's responsive `--bds-grid_*` tokens),
|
|
22
|
+
* column spans, and definite placements; the polyfill only supplies the row-span.
|
|
23
|
+
* Column gap is left untouched; only row gap is subsumed into spans.
|
|
24
|
+
*
|
|
25
|
+
* When the browser supports the native `display: grid-lanes` or
|
|
26
|
+
* `grid-template-rows: masonry` value, the polyfill yields to CSS.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const ROW_UNIT_PX = 1;
|
|
30
|
+
|
|
31
|
+
export function supportsNativeMasonry(): boolean {
|
|
32
|
+
if (typeof CSS === 'undefined' || typeof CSS.supports !== 'function') return false;
|
|
33
|
+
return (
|
|
34
|
+
CSS.supports('display', 'grid-lanes') ||
|
|
35
|
+
CSS.supports('grid-template-rows', 'masonry')
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function applyMasonryLayout(container: HTMLElement, rawChildren: HTMLElement[]): void {
|
|
40
|
+
// We override row-gap to 0, so reading it back would always give 0 on re-runs.
|
|
41
|
+
// Read column-gap instead — `.grid` sets both via the `gap` shorthand and the
|
|
42
|
+
// polyfill never touches column-gap. (If a consumer sets asymmetric gaps, the
|
|
43
|
+
// row spacing falls back to the column value — a reasonable compromise.)
|
|
44
|
+
const cs = window.getComputedStyle(container);
|
|
45
|
+
const targetGap = parseFloat(cs.columnGap) || 0;
|
|
46
|
+
|
|
47
|
+
container.style.gridAutoRows = `${ROW_UNIT_PX}px`;
|
|
48
|
+
container.style.rowGap = '0px';
|
|
49
|
+
container.style.gridAutoFlow = 'dense';
|
|
50
|
+
|
|
51
|
+
const children = rawChildren.filter(
|
|
52
|
+
(el) => el.nodeType === 1 && window.getComputedStyle(el).display !== 'none',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Pass 1 — clear prior row-span overrides so items measure at their natural height.
|
|
56
|
+
for (const child of children) {
|
|
57
|
+
child.style.gridRow = '';
|
|
58
|
+
child.style.gridRowStart = '';
|
|
59
|
+
child.style.gridRowEnd = '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pass 2 — read all heights in a tight loop. The first offsetHeight read forces one
|
|
63
|
+
// reflow; subsequent reads are cached until the next style write. This avoids the
|
|
64
|
+
// O(N) forced-reflow cost of a read-then-write loop.
|
|
65
|
+
const heights = children.map((child) => child.offsetHeight);
|
|
66
|
+
|
|
67
|
+
// Pass 3 — batch all row-span writes. No reads in between, so the browser can
|
|
68
|
+
// coalesce style invalidation and do layout once for the whole batch.
|
|
69
|
+
for (let i = 0; i < children.length; i++) {
|
|
70
|
+
const span = Math.max(1, Math.ceil(heights[i] + targetGap));
|
|
71
|
+
children[i].style.gridRowEnd = `span ${span}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function clearMasonryLayout(container: HTMLElement, rawChildren: HTMLElement[]): void {
|
|
76
|
+
container.style.gridAutoRows = '';
|
|
77
|
+
container.style.gridAutoFlow = '';
|
|
78
|
+
container.style.rowGap = '';
|
|
79
|
+
for (const child of rawChildren) {
|
|
80
|
+
if (child.nodeType !== 1) continue;
|
|
81
|
+
child.style.gridRow = '';
|
|
82
|
+
child.style.gridRowStart = '';
|
|
83
|
+
child.style.gridRowEnd = '';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* React hook that runs the polyfill against `ref.current` and re-runs on
|
|
89
|
+
* size / mutation changes. No-op when `enabled` is false or when the browser
|
|
90
|
+
* natively supports masonry.
|
|
91
|
+
*/
|
|
92
|
+
export function useMasonry(ref: RefObject<HTMLElement | null>, enabled: boolean): void {
|
|
93
|
+
useLayoutEffect(() => {
|
|
94
|
+
const container = ref.current;
|
|
95
|
+
if (!container || !enabled) return;
|
|
96
|
+
if (supportsNativeMasonry()) return;
|
|
97
|
+
|
|
98
|
+
let frame = 0;
|
|
99
|
+
const schedule = () => {
|
|
100
|
+
cancelAnimationFrame(frame);
|
|
101
|
+
frame = requestAnimationFrame(() => {
|
|
102
|
+
applyMasonryLayout(container, Array.from(container.children) as HTMLElement[]);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
schedule();
|
|
107
|
+
|
|
108
|
+
const ro =
|
|
109
|
+
typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null;
|
|
110
|
+
if (ro) {
|
|
111
|
+
// Observe each child's size, not the container — re-running when the container
|
|
112
|
+
// resizes causes a feedback loop (we set grid-auto-rows, which changes container
|
|
113
|
+
// block-size, which fires the observer). Child-size changes are what we actually
|
|
114
|
+
// need to respond to.
|
|
115
|
+
for (const child of Array.from(container.children)) ro.observe(child);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const mo =
|
|
119
|
+
typeof MutationObserver !== 'undefined'
|
|
120
|
+
? new MutationObserver(() => {
|
|
121
|
+
if (ro) for (const child of Array.from(container.children)) ro.observe(child);
|
|
122
|
+
schedule();
|
|
123
|
+
})
|
|
124
|
+
: null;
|
|
125
|
+
mo?.observe(container, { childList: true });
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
cancelAnimationFrame(frame);
|
|
129
|
+
ro?.disconnect();
|
|
130
|
+
mo?.disconnect();
|
|
131
|
+
clearMasonryLayout(container, Array.from(container.children) as HTMLElement[]);
|
|
132
|
+
};
|
|
133
|
+
}, [ref, enabled]);
|
|
134
|
+
}
|
|
@@ -1,21 +1,53 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import { IconWrapper } from './IconWrapper';
|
|
2
|
+
import { IconWrapper, type IconWrapperProps } from './IconWrapper';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const CheckIcon = (
|
|
5
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" aria-hidden>
|
|
6
|
+
<path d="M12 0a12 12 0 1 0 12 12A12 12 0 0 0 12 0Zm6.93 8.2-6.85 9.29a1 1 0 0 1-1.43.19l-4.89-3.91a1 1 0 0 1-.15-1.41A1 1 0 0 1 7 12.21l4.08 3.26L17.32 7a1 1 0 0 1 1.39-.21 1 1 0 0 1 .22 1.41Z"/>
|
|
7
|
+
</svg>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const StarIcon = (
|
|
11
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em" aria-hidden>
|
|
12
|
+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
13
|
+
</svg>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const HeartIcon = (
|
|
17
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em" aria-hidden>
|
|
18
|
+
<path d="M12 21s-7-4.35-7-10a4.5 4.5 0 0 1 8-2.8A4.5 4.5 0 0 1 19 11c0 5.65-7 10-7 10z" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const iconMap = {
|
|
23
|
+
check: CheckIcon,
|
|
24
|
+
star: StarIcon,
|
|
25
|
+
heart: HeartIcon,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type IconKey = keyof typeof iconMap;
|
|
29
|
+
|
|
30
|
+
type IconWrapperStoryArgs = Omit<IconWrapperProps, 'children'> & {
|
|
31
|
+
children?: IconKey;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const meta: Meta<IconWrapperStoryArgs> = {
|
|
5
35
|
title: 'React/Layout/IconWrapper',
|
|
6
36
|
component: IconWrapper,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
children: (
|
|
15
|
-
<svg viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em">
|
|
16
|
-
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
17
|
-
</svg>
|
|
18
|
-
),
|
|
37
|
+
argTypes: {
|
|
38
|
+
children: {
|
|
39
|
+
control: 'select',
|
|
40
|
+
options: Object.keys(iconMap) as IconKey[],
|
|
41
|
+
mapping: iconMap,
|
|
42
|
+
description: 'The icon to render inside the circular wrapper.',
|
|
43
|
+
},
|
|
19
44
|
},
|
|
45
|
+
args: { children: 'star' },
|
|
20
46
|
};
|
|
21
47
|
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<IconWrapperStoryArgs>;
|
|
50
|
+
|
|
51
|
+
export const Default: Story = {};
|
|
52
|
+
export const Check: Story = { args: { children: 'check' } };
|
|
53
|
+
export const Heart: Story = { args: { children: 'heart' } };
|
package/src/index.ts
CHANGED
|
@@ -67,6 +67,8 @@ export { Textarea } from './components/interaction/form/Textarea';
|
|
|
67
67
|
export { ButtonContainer, ButtonGroup } from './components/layout/ButtonGroup';
|
|
68
68
|
export type { ButtonContainerProps, ButtonGroupProps } from './components/layout/ButtonGroup';
|
|
69
69
|
export { Card } from './components/layout/Card';
|
|
70
|
+
export { Grid, GridItem, GridItemSpan } from './components/layout/Grid';
|
|
71
|
+
export type { GridVariant, GridItemSpanValue } from './components/layout/Grid';
|
|
70
72
|
export { SectionHeader } from './components/layout/SectionHeader';
|
|
71
73
|
export { IconWrapper } from './components/layout/IconWrapper';
|
|
72
74
|
export type { IconWrapperProps } from './components/layout/IconWrapper';
|
|
@@ -58,6 +58,10 @@ export { BdsDescriptionList } from './ui/bds-description-list';
|
|
|
58
58
|
export { BdsBreadcrumb } from './ui/bds-breadcrumb';
|
|
59
59
|
export { BdsLink } from './ui/bds-link';
|
|
60
60
|
|
|
61
|
+
// Phase 8 — layout primitives
|
|
62
|
+
export { BdsGrid } from './layout/bds-grid';
|
|
63
|
+
export { BdsGridItem } from './layout/bds-grid-item';
|
|
64
|
+
|
|
61
65
|
// Phase 8 — complete parity with React library
|
|
62
66
|
export { BdsButtonGroup } from './ui/bds-button-group';
|
|
63
67
|
export { BdsTable } from './ui/bds-table';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
1
|
+
import { Meta, Canvas, ArgTypes, Controls } from '@storybook/blocks';
|
|
2
2
|
import * as Stories from './BdsButton.stories';
|
|
3
3
|
|
|
4
4
|
<Meta of={Stories} />
|
|
@@ -24,26 +24,54 @@ Or in HTML without a bundler:
|
|
|
24
24
|
</script>
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## Playground
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
<
|
|
29
|
+
<Canvas of={Stories.Playground} />
|
|
30
|
+
<Controls of={Stories.Playground} />
|
|
31
|
+
|
|
32
|
+
## Variants
|
|
31
33
|
|
|
32
|
-
### Default
|
|
33
34
|
<Canvas of={Stories.Default} />
|
|
35
|
+
<Canvas of={Stories.Outline} />
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
<Canvas of={Stories.Ghost} />
|
|
37
|
+
## Sizes
|
|
37
38
|
|
|
38
|
-
### Sizes
|
|
39
39
|
<Canvas of={Stories.Small} />
|
|
40
40
|
<Canvas of={Stories.Medium} />
|
|
41
41
|
<Canvas of={Stories.Large} />
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
## Icons
|
|
44
|
+
|
|
45
|
+
Slot an SVG (or any element) into `icon-start` or `icon-end`:
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<bds-button>
|
|
49
|
+
<svg slot="icon-start">…</svg>
|
|
50
|
+
Save
|
|
51
|
+
</bds-button>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
<Canvas of={Stories.WithIconStart} />
|
|
55
|
+
<Canvas of={Stories.WithIconEnd} />
|
|
56
|
+
|
|
57
|
+
### Icon-only (1:1 aspect ratio)
|
|
58
|
+
|
|
59
|
+
Set the `is-icon-only` attribute to render a square button with equal `var(--bds-space_xs)` padding on both axes. Composable with any `variant`. Always pair with `aria-label` — there's no visible label text for assistive tech to read.
|
|
60
|
+
|
|
61
|
+
<Canvas of={Stories.IconOnly} />
|
|
62
|
+
<Canvas of={Stories.IconOnlyOutline} />
|
|
63
|
+
<Canvas of={Stories.AllIconOnly} />
|
|
64
|
+
|
|
65
|
+
## All variants & sizes
|
|
66
|
+
|
|
67
|
+
<Canvas of={Stories.AllVariants} />
|
|
68
|
+
|
|
69
|
+
## Pulse (CTA emphasis)
|
|
70
|
+
|
|
44
71
|
<Canvas of={Stories.WithPulse} />
|
|
45
72
|
|
|
46
|
-
|
|
73
|
+
## Disabled
|
|
74
|
+
|
|
47
75
|
<Canvas of={Stories.Disabled} />
|
|
48
76
|
|
|
49
77
|
## Props
|
|
@@ -54,14 +82,15 @@ Or in HTML without a bundler:
|
|
|
54
82
|
|
|
55
83
|
| Attribute | Type | Default | Description |
|
|
56
84
|
|-----------|------|---------|-------------|
|
|
57
|
-
| `variant` | `"default" \| "
|
|
58
|
-
| `size` | `"small" \| "medium" \| "large"` | `"medium"` | Height and padding
|
|
85
|
+
| `variant` | `"default" \| "outline"` | `"default"` | Visual style — filled or transparent outlined (`"ghost"` is a deprecated alias for `"outline"`) |
|
|
86
|
+
| `size` | `"small" \| "medium" \| "large"` | `"medium"` | Height and horizontal padding |
|
|
59
87
|
| `disabled` | boolean | `false` | Prevents interaction, sets `aria-disabled` |
|
|
60
88
|
| `href` | string | — | Renders as `<a>` when set |
|
|
61
89
|
| `target` | string | — | Forwarded to `<a>` when `href` is set |
|
|
62
90
|
| `rel` | string | — | Forwarded to `<a>` when `href` is set |
|
|
63
91
|
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | Native button type |
|
|
64
92
|
| `has-pulse` | boolean | `false` | Adds animated ring for CTA emphasis |
|
|
93
|
+
| `is-icon-only` | boolean | `false` | 1:1 aspect ratio with equal `xs` padding on both axes. Requires `aria-label`. |
|
|
65
94
|
| `aria-label` | string | — | Accessible label (required for icon-only buttons) |
|
|
66
95
|
|
|
67
96
|
## Slots
|
|
@@ -76,7 +105,7 @@ Or in HTML without a bundler:
|
|
|
76
105
|
|
|
77
106
|
| Property | Description |
|
|
78
107
|
|----------|-------------|
|
|
79
|
-
| `--button_color` | Identity colour — fills the default variant, outlines the
|
|
108
|
+
| `--button_color` | Identity colour — fills the default variant, outlines the outline variant |
|
|
80
109
|
| `--button_on-color` | Text/icon colour on the button surface. Pair with `--button_color` |
|
|
81
110
|
|
|
82
111
|
## Invoker Commands API
|
|
@@ -94,8 +123,11 @@ Or in HTML without a bundler:
|
|
|
94
123
|
|
|
95
124
|
```html
|
|
96
125
|
<bds-button variant="default" size="medium">Save</bds-button>
|
|
97
|
-
<bds-button variant="
|
|
126
|
+
<bds-button variant="outline">Cancel</bds-button>
|
|
98
127
|
<bds-button href="/dashboard">Go to dashboard</bds-button>
|
|
128
|
+
<bds-button is-icon-only aria-label="Confirm">
|
|
129
|
+
<svg viewBox="0 0 24 24">…</svg>
|
|
130
|
+
</bds-button>
|
|
99
131
|
```
|
|
100
132
|
|
|
101
133
|
## Accessibility
|