Skip to content

Commit db977d4

Browse files
committed
Add responsive rendering
1 parent 2083ae2 commit db977d4

File tree

8 files changed

+116
-51
lines changed

8 files changed

+116
-51
lines changed

packages/noya-component/src/renderResolvedNode.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import {
1616
svgToDataUri,
1717
} from 'noya-component';
1818
import {
19+
BreakpointKey,
1920
extractTailwindClassesByBreakpoint,
2021
extractTailwindClassesByTheme,
22+
matchBreakpoint,
2123
parametersToTailwindStyle,
2224
tailwindColors,
2325
} from 'noya-tailwind';
@@ -52,6 +54,7 @@ export function getImageFromProp(
5254
}
5355

5456
export function renderResolvedNode({
57+
containerWidth,
5558
contentEditable,
5659
disableTabNavigation,
5760
includeDataProps,
@@ -60,6 +63,7 @@ export function renderResolvedNode({
6063
system,
6164
theme,
6265
}: {
66+
containerWidth?: number;
6367
contentEditable: boolean;
6468
disableTabNavigation: boolean;
6569
includeDataProps: boolean;
@@ -140,9 +144,15 @@ export function renderResolvedNode({
140144
dsConfig.colorMode ?? 'light',
141145
);
142146

147+
let breakpoint: BreakpointKey = 'md';
148+
149+
if (containerWidth) {
150+
breakpoint = matchBreakpoint(containerWidth);
151+
}
152+
143153
// Keep classNames starting with sm: and md:, but remove the prefixes.
144154
// Remove any classNames starting with lg:, xl:, and 2xl:.
145-
classNames = extractTailwindClassesByBreakpoint(classNames, 'md');
155+
classNames = extractTailwindClassesByBreakpoint(classNames, breakpoint);
146156

147157
const style = parametersToTailwindStyle(classNames);
148158

packages/noya-tailwind/src/tailwind.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,24 @@ export const filterTailwindClassesByLastInGroup = memoize(
187187
},
188188
);
189189

190-
const breakpoints = ['sm', 'md', 'lg', 'xl', '2xl'] as const;
191-
type BreakpointKey = (typeof breakpoints)[number];
190+
export const breakpoints = [
191+
'sm' as const,
192+
'md' as const,
193+
'lg' as const,
194+
'xl' as const,
195+
'2xl' as const,
196+
];
197+
198+
export type BreakpointKey = (typeof breakpoints)[number];
199+
200+
export function matchBreakpoint(width: number): BreakpointKey {
201+
if (width < 640) return 'sm';
202+
if (width < 768) return 'md';
203+
if (width < 1024) return 'lg';
204+
if (width < 1280) return 'xl';
205+
if (width < 1536) return '2xl';
206+
return '2xl';
207+
}
192208

193209
export const extractTailwindClassesByBreakpoint = (
194210
classes: string[],
@@ -244,7 +260,7 @@ export const extractTailwindClassesByTheme = (
244260

245261
function getValue(className: string): string | undefined {
246262
let value =
247-
/-((\d+)((\/|\.)\d+)?|(sm|md|lg|xl|2xl|3xl|full|none|auto|screen))$/.exec(
263+
/-((\d+)((\/|\.)\d+)?|(sm|md|lg|\d?xl|full|none|auto|screen))$/.exec(
248264
className,
249265
)?.[1];
250266

packages/noya-utils/src/__tests__/cartesianProduct.test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,22 @@ test('product 3x3', () => {
8686
],
8787
);
8888
});
89+
90+
test('array of arrays', () => {
91+
expect(
92+
cartesianProduct(
93+
[
94+
[1, 2],
95+
[3, 4],
96+
],
97+
['sm', 'md', 'lg'],
98+
),
99+
).toEqual([
100+
[[1, 2], 'sm'],
101+
[[1, 2], 'md'],
102+
[[1, 2], 'lg'],
103+
[[3, 4], 'sm'],
104+
[[3, 4], 'md'],
105+
[[3, 4], 'lg'],
106+
]);
107+
});

packages/noya-utils/src/cartesianProduct.ts

+10-22
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,20 @@ export function cartesianProduct<T>(...arrays: T[][]): T[][];
1414
* Cartesian product of input arrays.
1515
*/
1616
export function cartesianProduct(...arrays: unknown[][]): unknown[][] {
17-
// Product of array lengths up to the current index
18-
// We leave the extra [1] in the front of the array since we use it later
19-
const lengths = arrays
20-
.map((array) => array.length)
21-
.reduce((result, value) => [...result, result[result.length - 1] * value], [
22-
1,
23-
]);
17+
let result: unknown[][] = [[]];
2418

25-
const resultLength = lengths[lengths.length - 1];
26-
const result = new Array<unknown[]>(resultLength);
19+
// Iterate over each array, and for each element in the array, add it to each
20+
// existing combination. The result will grow exponentially with each array.
21+
for (const array of arrays) {
22+
const tempResult: unknown[][] = [];
2723

28-
for (let arrayIndex = 0; arrayIndex < arrays.length; arrayIndex++) {
29-
const array = arrays[arrayIndex];
30-
31-
for (let index = 0; index < resultLength; index++) {
32-
const repeat = lengths[lengths.length - 2 - arrayIndex];
33-
34-
const wrappedIndex = Math.floor(index / repeat) % array.length;
35-
const value = array[wrappedIndex];
36-
37-
if (arrayIndex === 0) {
38-
result[index] = [value];
39-
} else {
40-
result[index].push(value);
24+
for (const existingCombo of result) {
25+
for (const element of array) {
26+
tempResult.push([...existingCombo, element]);
4127
}
4228
}
29+
30+
result = tempResult;
4331
}
4432

4533
return result;

packages/site/src/dseditor/ControlledFrame.tsx

+23-10
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1+
import { Size } from 'noya-geometry';
12
import { assignRef } from 'noya-react-utils';
23
import React, {
3-
HTMLProps,
44
forwardRef,
55
memo,
66
useCallback,
77
useEffect,
8+
useMemo,
89
useRef,
910
useState,
1011
} from 'react';
1112

12-
interface Props extends HTMLProps<HTMLIFrameElement> {
13+
interface Props {
1314
onReady?: () => void;
15+
onResize?: (size: Size) => void;
1416
title: string;
1517
}
1618

1719
export const ControlledFrame = memo(
1820
forwardRef(function ControlledFrame(
19-
{ onReady, ...props }: Props,
21+
{ onReady, onResize, title }: Props,
2022
forwardedRef: React.ForwardedRef<HTMLIFrameElement>,
2123
) {
2224
const ref = useRef<HTMLIFrameElement | null>(null);
@@ -34,6 +36,10 @@ export const ControlledFrame = memo(
3436
ok = true;
3537
break;
3638
}
39+
case 'resize': {
40+
onResize?.(event.data.size);
41+
break;
42+
}
3743
case 'keydown': {
3844
const customEvent = new KeyboardEvent('keydown', {
3945
key: event.data.command.key,
@@ -61,7 +67,7 @@ export const ControlledFrame = memo(
6167
return () => {
6268
window.removeEventListener('message', listener);
6369
};
64-
}, [id, onReady]);
70+
}, [id, onReady, onResize]);
6571

6672
const handleRef = useCallback(
6773
(value: HTMLIFrameElement | null) => {
@@ -71,16 +77,14 @@ export const ControlledFrame = memo(
7177
[forwardedRef],
7278
);
7379

80+
const style = useMemo(() => ({ width: '100%', height: '100%' }), []);
81+
7482
return (
7583
<iframe
7684
ref={handleRef}
7785
tabIndex={-1}
78-
style={{
79-
width: '100%',
80-
height: '100%',
81-
}}
82-
{...props}
83-
title={props.title}
86+
title={title}
87+
style={style}
8488
// Ensure html5 doctype for proper styling
8589
srcDoc={`<!DOCTYPE html>
8690
<head>
@@ -139,6 +143,15 @@ export const ControlledFrame = memo(
139143
document.addEventListener('DOMContentLoaded', callback);
140144
}
141145
146+
// Handle window resize events
147+
window.addEventListener('resize', function(event) {
148+
parent.postMessage({
149+
id,
150+
type: 'resize',
151+
size: { width: window.innerWidth, height: window.innerHeight }
152+
}, '*');
153+
});
154+
142155
// Propagate keyboard shortcuts (keydown events) to the parent window
143156
window.addEventListener('keydown', function(event) {
144157
// Check if Cmd (for Mac) or Ctrl (for other OS) is pressed

packages/site/src/dseditor/DSRenderer.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
} from '@noya-design-system/protocol';
99
import { DSConfig } from 'noya-api';
1010
import { Stack } from 'noya-designsystem';
11+
import { Size } from 'noya-geometry';
1112
import { loadDesignSystem } from 'noya-module-loader';
12-
import { useStableCallback } from 'noya-react-utils';
13+
import { useDeepState, useStableCallback } from 'noya-react-utils';
1314
import { tailwindColors } from 'noya-tailwind';
1415
import React, {
1516
forwardRef,
@@ -48,6 +49,7 @@ export type DSRenderProps = {
4849
theme: any;
4950
primary: string;
5051
iframe: HTMLIFrameElement;
52+
size: Size;
5153
};
5254

5355
export interface IDSRenderer {
@@ -94,6 +96,7 @@ export const DSRenderer = forwardRef(function DSRenderer(
9496
let [system, setSystem] = React.useState<
9597
DesignSystemDefinition | undefined
9698
>();
99+
const [iframeSize, setIframeSize] = useDeepState<Size | undefined>();
97100

98101
const handleReady = useCallback(() => {
99102
setReady(true);
@@ -174,6 +177,10 @@ export const DSRenderer = forwardRef(function DSRenderer(
174177
theme,
175178
primary: config.colors.primary,
176179
iframe,
180+
size: iframeSize ?? {
181+
width: iframe.clientWidth,
182+
height: iframe.clientHeight,
183+
},
177184
});
178185

179186
const withProvider = Provider ? (
@@ -206,6 +213,7 @@ export const DSRenderer = forwardRef(function DSRenderer(
206213
sync,
207214
config.colors.primary,
208215
onContentDidChange,
216+
iframeSize,
209217
]);
210218

211219
useEffect(() => {
@@ -272,6 +280,7 @@ export const DSRenderer = forwardRef(function DSRenderer(
272280
ref={ref}
273281
title="Design System Preview"
274282
onReady={handleReady}
283+
onResize={setIframeSize}
275284
/>
276285
{!system && <Loading>Loading design system...</Loading>}
277286
</Stack.V>

packages/site/src/dseditor/completionItems.tsx

+23-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CompletionItem } from 'noya-designsystem';
22
import { StarIcon } from 'noya-icons';
3-
import { allClassNames } from 'noya-tailwind';
4-
import { range } from 'noya-utils';
3+
import { allClassNames, breakpoints } from 'noya-tailwind';
4+
import { cartesianProduct, range } from 'noya-utils';
55
import React from 'react';
66
import { HashtagIcon } from '../ayon/components/inspector/HashtagIcon';
77
import { PRIMITIVE_ELEMENT_MAP, primitiveElements } from './primitiveElements';
@@ -15,23 +15,32 @@ const primaryStyles = [
1515
...colorScale.map((value) => `border-primary-${value}`),
1616
];
1717

18-
export const styleItems = allClassNames
19-
.map(
18+
const baseStyleItems = [
19+
...allClassNames.map(
2020
(item): CompletionItem => ({
2121
name: item,
2222
id: item,
2323
icon: <HashtagIcon item={item} />,
2424
}),
25-
)
26-
.concat(
27-
primaryStyles.map(
28-
(item): CompletionItem => ({
29-
name: item,
30-
id: item,
31-
icon: <HashtagIcon item={item} />,
32-
}),
33-
),
34-
);
25+
),
26+
...primaryStyles.map(
27+
(item): CompletionItem => ({
28+
name: item,
29+
id: item,
30+
icon: <HashtagIcon item={item} />,
31+
}),
32+
),
33+
];
34+
35+
const breakpointStyleItems = cartesianProduct(baseStyleItems, breakpoints).map(
36+
([item, breakpoint]): CompletionItem => ({
37+
name: `${breakpoint}:${item.name}`,
38+
id: `${breakpoint}:${item.id}`,
39+
icon: item.icon,
40+
}),
41+
);
42+
43+
export const styleItems = [...baseStyleItems, ...breakpointStyleItems];
3544

3645
export const primitiveElementStyleItems = Object.fromEntries(
3746
Object.entries(PRIMITIVE_ELEMENT_MAP).map(([id, metadata]) => [

packages/site/src/dseditor/renderDSPreview.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function renderDSPreview({
2929
// const system = isThumbnail ? ThumbnailDesignSystem : props.system;
3030

3131
const content = renderResolvedNode({
32+
containerWidth: props.size.width,
3233
contentEditable: true,
3334
disableTabNavigation: false,
3435
includeDataProps: true,

0 commit comments

Comments
 (0)