@a-type/ui 1.0.13 → 1.1.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 (42) hide show
  1. package/dist/cjs/components/card/Card.d.ts +7 -6
  2. package/dist/cjs/components/card/Card.js +10 -2
  3. package/dist/cjs/components/card/Card.js.map +1 -1
  4. package/dist/cjs/components/card/Card.stories.d.ts +1 -0
  5. package/dist/cjs/components/card/Card.stories.js +12 -1
  6. package/dist/cjs/components/card/Card.stories.js.map +1 -1
  7. package/dist/cjs/components/dialog/Dialog.js +2 -2
  8. package/dist/cjs/components/dialog/Dialog.js.map +1 -1
  9. package/dist/cjs/components/masonry/masonry.d.ts +8 -0
  10. package/dist/cjs/components/masonry/masonry.js +96 -0
  11. package/dist/cjs/components/masonry/masonry.js.map +1 -0
  12. package/dist/cjs/components/masonry/masonry.stories.d.ts +15 -0
  13. package/dist/cjs/components/masonry/masonry.stories.js +25 -0
  14. package/dist/cjs/components/masonry/masonry.stories.js.map +1 -0
  15. package/dist/cjs/components/masonry.d.ts +1 -0
  16. package/dist/cjs/components/masonry.js +19 -0
  17. package/dist/cjs/components/masonry.js.map +1 -0
  18. package/dist/css/main.css +2 -2
  19. package/dist/esm/components/card/Card.d.ts +7 -6
  20. package/dist/esm/components/card/Card.js +8 -1
  21. package/dist/esm/components/card/Card.js.map +1 -1
  22. package/dist/esm/components/card/Card.stories.d.ts +1 -0
  23. package/dist/esm/components/card/Card.stories.js +12 -1
  24. package/dist/esm/components/card/Card.stories.js.map +1 -1
  25. package/dist/esm/components/dialog/Dialog.js +2 -2
  26. package/dist/esm/components/dialog/Dialog.js.map +1 -1
  27. package/dist/esm/components/masonry/masonry.d.ts +8 -0
  28. package/dist/esm/components/masonry/masonry.js +92 -0
  29. package/dist/esm/components/masonry/masonry.js.map +1 -0
  30. package/dist/esm/components/masonry/masonry.stories.d.ts +15 -0
  31. package/dist/esm/components/masonry/masonry.stories.js +22 -0
  32. package/dist/esm/components/masonry/masonry.stories.js.map +1 -0
  33. package/dist/esm/components/masonry.d.ts +1 -0
  34. package/dist/esm/components/masonry.js +3 -0
  35. package/dist/esm/components/masonry.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/components/card/Card.stories.tsx +37 -0
  38. package/src/components/card/Card.tsx +20 -7
  39. package/src/components/dialog/Dialog.tsx +2 -2
  40. package/src/components/masonry/masonry.stories.tsx +30 -0
  41. package/src/components/masonry/masonry.tsx +136 -0
  42. package/src/components/masonry.ts +1 -0
@@ -0,0 +1,92 @@
1
+ // @unocss-include
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ class MasonryLayout {
5
+ constructor(config) {
6
+ this.config = config;
7
+ this.containerResizeObserver = null;
8
+ this.containerMutationObserver = null;
9
+ this.container = null;
10
+ this.columns = 0;
11
+ this.attach = (container) => {
12
+ var _a, _b;
13
+ (_a = this.containerResizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
14
+ (_b = this.containerMutationObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
15
+ this.container = container;
16
+ this.containerResizeObserver = new ResizeObserver(this.handleContainerResize);
17
+ this.containerMutationObserver = new MutationObserver(this.handleContainerMutation);
18
+ this.containerResizeObserver.observe(container);
19
+ this.containerMutationObserver.observe(container, { childList: true });
20
+ container.style.setProperty('position', 'relative');
21
+ container.style.setProperty('overflow', 'hidden');
22
+ this.updateFromContainerSize(container.offsetWidth);
23
+ this.relayout();
24
+ return () => {
25
+ var _a, _b;
26
+ (_a = this.containerResizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
27
+ (_b = this.containerMutationObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
28
+ container.style.removeProperty('position');
29
+ container.style.removeProperty('overflow');
30
+ this.container = null;
31
+ };
32
+ };
33
+ this.handleContainerResize = (entries) => {
34
+ const containerWidth = entries[0].contentRect.width;
35
+ this.updateFromContainerSize(containerWidth);
36
+ };
37
+ this.updateFromContainerSize = (containerWidth) => {
38
+ if (typeof this.config.columns === 'function') {
39
+ const newValue = this.config.columns(containerWidth);
40
+ if (newValue !== this.columns) {
41
+ this.columns = newValue;
42
+ this.relayout();
43
+ }
44
+ }
45
+ };
46
+ this.handleContainerMutation = (entries) => {
47
+ // TODO: why is this timeout necessary?
48
+ setTimeout(this.relayout, 100);
49
+ };
50
+ this.relayout = () => {
51
+ if (!this.container) {
52
+ return;
53
+ }
54
+ console.log('relayout');
55
+ const tracks = new Array(this.columns).fill(0);
56
+ const gap = this.config.gap;
57
+ // percentage-based width and x position so that items automatically
58
+ // layout correctly when the container is resized (as long as columns
59
+ // are the same)
60
+ const pixelColumnWidthMinusGap = (this.container.offsetWidth - gap * (this.columns - 1)) / this.columns;
61
+ const columnPercentageWidth = (pixelColumnWidthMinusGap / this.container.offsetWidth) * 100;
62
+ const gapPercentageWidth = (gap / this.container.offsetWidth) * 100;
63
+ const children = Array.from(this.container.children);
64
+ children.forEach((child) => {
65
+ const trackIndex = tracks.indexOf(Math.min(...tracks));
66
+ const x = (columnPercentageWidth + gapPercentageWidth) * trackIndex;
67
+ const y = tracks[trackIndex];
68
+ const width = columnPercentageWidth;
69
+ child.style.setProperty('position', 'absolute');
70
+ child.style.setProperty('width', `${width}%`);
71
+ child.style.setProperty('left', `${x}%`);
72
+ child.style.setProperty('top', `${y}px`);
73
+ child.classList.add('transition-all');
74
+ tracks[trackIndex] += child.offsetHeight + gap;
75
+ });
76
+ this.container.style.setProperty('height', `${Math.max(...tracks) - gap}px`);
77
+ };
78
+ this.columns =
79
+ typeof config.columns === 'function' ? config.columns(0) : config.columns;
80
+ }
81
+ }
82
+ export function Masonry({ className, children, columns = 3, gap = 16, }) {
83
+ const [layout] = useState(() => new MasonryLayout({ columns, gap }));
84
+ const ref = useRef(null);
85
+ useEffect(() => {
86
+ if (ref.current) {
87
+ return layout.attach(ref.current);
88
+ }
89
+ }, [layout, ref]);
90
+ return (_jsx("div", Object.assign({ ref: ref, className: className }, { children: children })));
91
+ }
92
+ //# sourceMappingURL=masonry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"masonry.js","sourceRoot":"","sources":["../../../../src/components/masonry/masonry.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { StoryObj } from '@storybook/react';
2
+ import { Masonry } from './masonry.js';
3
+ declare const meta: {
4
+ title: string;
5
+ component: typeof Masonry;
6
+ argTypes: {};
7
+ parameters: {
8
+ controls: {
9
+ expanded: boolean;
10
+ };
11
+ };
12
+ };
13
+ export default meta;
14
+ type Story = StoryObj<typeof Masonry>;
15
+ export declare const Default: Story;
@@ -0,0 +1,22 @@
1
+ // @unocss-include
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Masonry } from './masonry.js';
4
+ const meta = {
5
+ title: 'Masonry',
6
+ component: Masonry,
7
+ argTypes: {},
8
+ parameters: {
9
+ controls: { expanded: true },
10
+ },
11
+ };
12
+ export default meta;
13
+ const sizes = Array.from({ length: 100 }, (_, i) => {
14
+ const size = 100 + Math.floor(Math.random() * 100);
15
+ return (_jsx("div", Object.assign({ className: "bg-gray-5 rounded-lg", style: { height: size } }, { children: size }), i));
16
+ });
17
+ export const Default = {
18
+ render(props) {
19
+ return _jsx(Masonry, Object.assign({}, props, { children: sizes }));
20
+ },
21
+ };
22
+ //# sourceMappingURL=masonry.stories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"masonry.stories.js","sourceRoot":"","sources":["../../../../src/components/masonry/masonry.stories.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,IAAI,GAAG;IACZ,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,OAAO;IAClB,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;KAC5B;CAC8B,CAAC;AAEjC,eAAe,IAAI,CAAC;AAIpB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;IAClD,MAAM,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;IACnD,OAAO,CACN,4BAAa,SAAS,EAAC,sBAAsB,EAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,gBACnE,IAAI,KADI,CAAC,CAEL,CACN,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,OAAO,GAAU;IAC7B,MAAM,CAAC,KAAK;QACX,OAAO,KAAC,OAAO,oBAAK,KAAK,cAAG,KAAK,IAAW,CAAC;IAC9C,CAAC;CACD,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './masonry/masonry.js';
@@ -0,0 +1,3 @@
1
+ // @unocss-include
2
+ export * from './masonry/masonry.js';
3
+ //# sourceMappingURL=masonry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"masonry.js","sourceRoot":"","sources":["../../../src/components/masonry.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-type/ui",
3
- "version": "1.0.13",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "/dist",
@@ -1,5 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import {
3
+ Card,
3
4
  CardActions,
4
5
  CardContent,
5
6
  CardFooter,
@@ -11,6 +12,7 @@ import {
11
12
  } from './Card.js';
12
13
  import { Button } from '../button.js';
13
14
  import { Icon } from '../icon.js';
15
+ import { useState } from 'react';
14
16
 
15
17
  const meta = {
16
18
  title: 'Card',
@@ -153,3 +155,38 @@ export const AsChildNonInteractive: Story = {
153
155
  </CardRoot>
154
156
  ),
155
157
  };
158
+
159
+ export const Grid: Story = {
160
+ render: () => {
161
+ const [sizes, setSizes] = useState(() =>
162
+ Array.from(
163
+ { length: 40 },
164
+ (_, i) => 50 + Math.floor(Math.random() * 300),
165
+ ),
166
+ );
167
+ const remove = (index: number) =>
168
+ setSizes((v) => v.filter((_, i) => i !== index));
169
+ return (
170
+ <Card.Grid>
171
+ {sizes.map((size, i) => (
172
+ <GridCard key={i} size={size} remove={() => remove(i)} />
173
+ ))}
174
+ </Card.Grid>
175
+ );
176
+ },
177
+ };
178
+
179
+ function GridCard({ size, remove }: { size: number; remove: () => void }) {
180
+ return (
181
+ <CardRoot style={{ height: size }}>
182
+ <CardMain>
183
+ <CardTitle>{size}</CardTitle>
184
+ </CardMain>
185
+ <CardActions>
186
+ <Button size="small" onClick={remove}>
187
+ Delete
188
+ </Button>
189
+ </CardActions>
190
+ </CardRoot>
191
+ );
192
+ }
@@ -1,8 +1,9 @@
1
1
  import { HTMLProps, MouseEvent, ReactNode, forwardRef } from 'react';
2
- import { withClassName } from '../../hooks.js';
2
+ import { useStableCallback, withClassName } from '../../hooks.js';
3
3
  import { Slot } from '@radix-ui/react-slot';
4
- import classNames from 'clsx';
4
+ import classNames, { clsx } from 'clsx';
5
5
  import { SlotDiv } from '../utility/SlotDiv.js';
6
+ import { Masonry, MasonryProps } from '../masonry/masonry.js';
6
7
 
7
8
  export const CardRoot = withClassName(
8
9
  'div',
@@ -74,11 +75,23 @@ export const CardMenu = withClassName(
74
75
  'layer-components:(mr-0 ml-auto my-auto flex flex-row gap-1 items-center bg-overlay rounded-full p-0)',
75
76
  );
76
77
 
77
- export const CardGrid = withClassName(
78
- 'div',
79
- 'layer-components:(grid grid-cols-[1fr] [grid-auto-rows:auto] gap-4 p-0 m-0)',
80
- 'layer-components:md:(grid-cols-[repeat(2,1fr)] [grid-auto-rows:1fr] items-end)',
81
- );
78
+ export const cardGridColumns = {
79
+ default: (size: number) => (size < 480 ? 1 : size < 800 ? 2 : 3),
80
+ small: (size: number) =>
81
+ size < 320 ? 1 : size < 480 ? 2 : size < 800 ? 3 : 4,
82
+ };
83
+ export const CardGrid = ({
84
+ children,
85
+ className,
86
+ columns = cardGridColumns.default,
87
+ gap,
88
+ }: MasonryProps) => {
89
+ return (
90
+ <Masonry className={className} columns={columns} gap={gap}>
91
+ {children}
92
+ </Masonry>
93
+ );
94
+ };
82
95
 
83
96
  export const Card = Object.assign(CardRoot, {
84
97
  Main: CardMain,
@@ -227,10 +227,10 @@ export const DialogDescription = StyledDescription;
227
227
  export const DialogClose = forwardRef<
228
228
  HTMLButtonElement,
229
229
  DialogPrimitive.DialogCloseProps
230
- >(function DialogClose({ asChild = true, children, ...props }, ref) {
230
+ >(function DialogClose({ asChild, children, ...props }, ref) {
231
231
  return (
232
232
  <DialogPrimitive.DialogClose asChild ref={ref} {...props}>
233
- {asChild ? children : <Button>{children ?? 'Close'}</Button>}
233
+ {asChild === true ? children : <Button>{children ?? 'Close'}</Button>}
234
234
  </DialogPrimitive.DialogClose>
235
235
  );
236
236
  });
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Masonry } from './masonry.js';
3
+
4
+ const meta = {
5
+ title: 'Masonry',
6
+ component: Masonry,
7
+ argTypes: {},
8
+ parameters: {
9
+ controls: { expanded: true },
10
+ },
11
+ } satisfies Meta<typeof Masonry>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof Masonry>;
16
+
17
+ const sizes = Array.from({ length: 100 }, (_, i) => {
18
+ const size = 100 + Math.floor(Math.random() * 100);
19
+ return (
20
+ <div key={i} className="bg-gray-5 rounded-lg" style={{ height: size }}>
21
+ {size}
22
+ </div>
23
+ );
24
+ });
25
+
26
+ export const Default: Story = {
27
+ render(props) {
28
+ return <Masonry {...props}>{sizes}</Masonry>;
29
+ },
30
+ };
@@ -0,0 +1,136 @@
1
+ import { ReactNode, useEffect, useRef, useState } from 'react';
2
+
3
+ interface MasonryLayoutConfig {
4
+ columns: number | ((containerWidth: number) => number);
5
+ gap: number;
6
+ }
7
+
8
+ class MasonryLayout {
9
+ private containerResizeObserver: ResizeObserver | null = null;
10
+ private containerMutationObserver: MutationObserver | null = null;
11
+
12
+ private container: HTMLElement | null = null;
13
+
14
+ private columns: number = 0;
15
+
16
+ constructor(private config: MasonryLayoutConfig) {
17
+ this.columns =
18
+ typeof config.columns === 'function' ? config.columns(0) : config.columns;
19
+ }
20
+
21
+ attach = (container: HTMLElement) => {
22
+ this.containerResizeObserver?.disconnect();
23
+ this.containerMutationObserver?.disconnect();
24
+
25
+ this.container = container;
26
+
27
+ this.containerResizeObserver = new ResizeObserver(
28
+ this.handleContainerResize,
29
+ );
30
+ this.containerMutationObserver = new MutationObserver(
31
+ this.handleContainerMutation,
32
+ );
33
+ this.containerResizeObserver.observe(container);
34
+ this.containerMutationObserver.observe(container, { childList: true });
35
+
36
+ container.style.setProperty('position', 'relative');
37
+ container.style.setProperty('overflow', 'hidden');
38
+
39
+ this.updateFromContainerSize(container.offsetWidth);
40
+
41
+ this.relayout();
42
+
43
+ return () => {
44
+ this.containerResizeObserver?.disconnect();
45
+ this.containerMutationObserver?.disconnect();
46
+ container.style.removeProperty('position');
47
+ container.style.removeProperty('overflow');
48
+ this.container = null;
49
+ };
50
+ };
51
+
52
+ private handleContainerResize = (entries: ResizeObserverEntry[]) => {
53
+ const containerWidth = entries[0].contentRect.width;
54
+ this.updateFromContainerSize(containerWidth);
55
+ };
56
+
57
+ private updateFromContainerSize = (containerWidth: number) => {
58
+ if (typeof this.config.columns === 'function') {
59
+ const newValue = this.config.columns(containerWidth);
60
+ if (newValue !== this.columns) {
61
+ this.columns = newValue;
62
+ this.relayout();
63
+ }
64
+ }
65
+ };
66
+
67
+ private handleContainerMutation = (entries: MutationRecord[]) => {
68
+ // TODO: why is this timeout necessary?
69
+ setTimeout(this.relayout, 100);
70
+ };
71
+
72
+ private relayout = () => {
73
+ if (!this.container) {
74
+ return;
75
+ }
76
+
77
+ console.log('relayout');
78
+
79
+ const tracks = new Array(this.columns).fill(0);
80
+ const gap = this.config.gap;
81
+ // percentage-based width and x position so that items automatically
82
+ // layout correctly when the container is resized (as long as columns
83
+ // are the same)
84
+ const pixelColumnWidthMinusGap =
85
+ (this.container.offsetWidth - gap * (this.columns - 1)) / this.columns;
86
+ const columnPercentageWidth =
87
+ (pixelColumnWidthMinusGap / this.container.offsetWidth) * 100;
88
+ const gapPercentageWidth = (gap / this.container.offsetWidth) * 100;
89
+
90
+ const children = Array.from(this.container.children) as HTMLElement[];
91
+ children.forEach((child) => {
92
+ const trackIndex = tracks.indexOf(Math.min(...tracks));
93
+ const x = (columnPercentageWidth + gapPercentageWidth) * trackIndex;
94
+ const y = tracks[trackIndex];
95
+ const width = columnPercentageWidth;
96
+ child.style.setProperty('position', 'absolute');
97
+ child.style.setProperty('width', `${width}%`);
98
+ child.style.setProperty('left', `${x}%`);
99
+ child.style.setProperty('top', `${y}px`);
100
+ child.classList.add('transition-all');
101
+ tracks[trackIndex] += child.offsetHeight + gap;
102
+ });
103
+ this.container.style.setProperty(
104
+ 'height',
105
+ `${Math.max(...tracks) - gap}px`,
106
+ );
107
+ };
108
+ }
109
+
110
+ export interface MasonryProps {
111
+ children: ReactNode;
112
+ className?: string;
113
+ columns?: number | ((containerWidth: number) => number);
114
+ gap?: number;
115
+ }
116
+
117
+ export function Masonry({
118
+ className,
119
+ children,
120
+ columns = 3,
121
+ gap = 16,
122
+ }: MasonryProps) {
123
+ const [layout] = useState(() => new MasonryLayout({ columns, gap }));
124
+ const ref = useRef<HTMLDivElement>(null);
125
+ useEffect(() => {
126
+ if (ref.current) {
127
+ return layout.attach(ref.current);
128
+ }
129
+ }, [layout, ref]);
130
+
131
+ return (
132
+ <div ref={ref} className={className}>
133
+ {children}
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1 @@
1
+ export * from './masonry/masonry.js';