Skip to content

Commit

Permalink
feat(UIPatternBlock): implement UI pattern block (#676)
Browse files Browse the repository at this point in the history
* feat(UIPatternBlock): implement UI pattern block
  • Loading branch information
fulopdaniel authored Oct 19, 2023
1 parent 0ad1012 commit 7f8c7f2
Show file tree
Hide file tree
Showing 29 changed files with 2,021 additions and 73 deletions.
7 changes: 6 additions & 1 deletion packages/ui-pattern-block/manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{
"appId": "clmyyhwdf00010nuqq138mw3s"
"appId": "clncxpkhl00014muq6fm3u7i0",
"searchFields": [
{ "type": "fondueRte", "settingId": "title" },
{ "type": "fondueRte", "settingId": "description" }
],
"i18nFields": ["title", "description"]
}
5 changes: 4 additions & 1 deletion packages/ui-pattern-block/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@babel/core": "^7.22.17",
"@frontify/eslint-config-react": "0.16.1",
"@frontify/frontify-cli": "^5.3.17",
"@types/lodash": "^4.14.199",
"@types/lodash-es": "^4.17.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
Expand All @@ -32,12 +33,14 @@
"typescript": "^5.2.2"
},
"dependencies": {
"@codesandbox/sandpack-react": "^2.7.1",
"@codesandbox/sandpack-react": "^2.9.0",
"@codesandbox/sandpack-themes": "^2.0.21",
"@frontify/app-bridge": "^3.0.0-beta.98",
"@frontify/fondue": "12.0.0-beta.341",
"@frontify/guideline-blocks-settings": "^0.29.10",
"@frontify/guideline-blocks-shared": "workspace:*",
"@mantine/core": "^7.1.2",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0"
Expand Down
387 changes: 387 additions & 0 deletions packages/ui-pattern-block/src/UIPatternBlock.spec.ct.tsx

Large diffs are not rendered by default.

256 changes: 245 additions & 11 deletions packages/ui-pattern-block/src/UIPatternBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,259 @@
/* (c) Copyright Frontify Ltd., all rights reserved. */

import { ReactElement } from 'react';
import { useBlockSettings } from '@frontify/app-bridge';
import { BlockProps } from '@frontify/guideline-blocks-settings';
import { Sandpack, SandpackPredefinedTemplate } from '@codesandbox/sandpack-react';
import { ReactElement, useMemo, useRef, useState } from 'react';
import { useBlockSettings, useEditorState } from '@frontify/app-bridge';
import { BlockProps, getBackgroundColorStyles, getBorderStyles } from '@frontify/guideline-blocks-settings';
import { SandpackLayout, SandpackPreview, SandpackPreviewRef, SandpackProvider } from '@codesandbox/sandpack-react';

import 'tailwindcss/tailwind.css';
import '@frontify/guideline-blocks-settings/styles';

import { type Settings, sandpackThemeValues } from './types';
import { Alignment, Height, type Settings, sandpackThemeValues } from './types';
import {
DEFAULT_BLOCK_SETTINGS,
EDITOR_CLASSES,
centeringCss,
getBackgroundCss,
getCssToInject,
getDefaultFilesOfTemplate,
getHeightStyle,
getPaddingStyle,
getParsedDependencies,
getRadiusValue,
getScriptToInject,
initialActiveFile,
} from './helpers';
import { useDebounce } from './hooks';
import { Captions, CodeEditor, ExternalDependencies, NPMDependencies, ResponsivePreview } from './components';

export const UIPatternBlock = ({ appBridge }: BlockProps): ReactElement => {
const [blockSettings] = useBlockSettings<Settings>(appBridge);
const { sandpackTemplate, sandpackTheme } = blockSettings;
const [blockSettings, setBlockSettings] = useBlockSettings<Settings>(appBridge);
const {
sandpackTemplate,
sandpackTheme,
files,
alignment,
dependencies: blockDependencies,
paddingChoice,
paddingCustom,
hasCustomPadding,
heightChoice,
customHeightValue,
isCustomHeight,
showResetButton,
showSandboxLink,
showResponsivePreview,
showCode,
isCodeEditable,
showNpmDependencies,
showExternalDependencies,
shouldCollapseCodeByDefault,
shouldCollapseDependenciesByDefault,
backgroundColor,
hasBackground,
borderColor,
borderStyle,
borderWidth,
hasBorder,
hasRadius,
radiusChoice,
radiusValue,
description,
title,
} = { ...DEFAULT_BLOCK_SETTINGS, ...blockSettings };

const { debounce } = useDebounce();
const isEditing = useEditorState(appBridge);
const [isResponsivePreviewOpen, setIsResponsivePreviewOpen] = useState(false);
const [dependencies, setDependencies] = useState(blockDependencies);
const [resetFiles, setResetFiles] = useState(false);
const [hasCodeChanges, setHasCodeChanges] = useState(false);
const previewRef = useRef<SandpackPreviewRef>(null);

const cssToInject = useMemo(() => {
const alignmentCss = alignment === Alignment.Center ? centeringCss : '';
const backgroundCss = hasBackground ? getBackgroundCss(backgroundColor) : '';
const hasAutoHeight = !isCustomHeight && heightChoice === Height.Auto;
return getScriptToInject(getCssToInject(hasAutoHeight, alignmentCss, backgroundCss));
}, [hasBackground, backgroundColor, alignment, isCustomHeight, heightChoice]);

const npmDependencies = dependencies?.[sandpackTemplate]?.npm ?? '';
const externalDependencies = dependencies?.[sandpackTemplate]?.external ?? '';

// Remount sandpack provider if any of these change
const sandpackRestartInitiators = [
heightChoice,
customHeightValue,
cssToInject,
npmDependencies,
externalDependencies,
resetFiles,
];

const onResetFiles = () => {
setResetFiles(!resetFiles);
setHasCodeChanges(false);
};

const templateFiles = useMemo(
() => ({ ...getDefaultFilesOfTemplate(sandpackTemplate), ...files?.[sandpackTemplate] }),
// eslint-disable-next-line react-hooks/exhaustive-deps
sandpackRestartInitiators,
);

const onResetRun = () => {
previewRef.current?.getClient()?.dispatch({ type: 'refresh' });
};

const onCodeChange = (filename: string, code: string) => {
if (!isEditing) {
const existingFiles = { ...getDefaultFilesOfTemplate(sandpackTemplate), ...files?.[sandpackTemplate] };
if (existingFiles[filename] !== code) {
setHasCodeChanges(true);
}
return;
}
setHasCodeChanges(false);
debounce(() =>
setBlockSettings({
files: {
...blockSettings.files,
[sandpackTemplate]: {
...blockSettings.files?.[sandpackTemplate],
[filename]: code,
},
},
}),
);
};

const onDependenciesChanged = (newDependencies: string, source: 'npm' | 'external') => {
if (isEditing) {
setBlockSettings({
dependencies: {
...blockSettings.dependencies,
[sandpackTemplate]: {
...blockSettings.dependencies?.[sandpackTemplate],
[source]: newDependencies,
},
},
});
}
setDependencies({
...dependencies,
[sandpackTemplate]: {
...dependencies?.[sandpackTemplate],
[source]: newDependencies,
},
});
};

const parsedExternalDependencies = useMemo(
() => getParsedDependencies(externalDependencies, []),
[externalDependencies],
);
const parsedNpmDependencies = useMemo(
() => ({ dependencies: getParsedDependencies(npmDependencies, {}) }),
[npmDependencies],
);

const borderRadius = getRadiusValue(hasRadius, radiusValue, radiusChoice);

return (
<div className="ui-pattern-block">
<Sandpack
template={sandpackTemplate as SandpackPredefinedTemplate}
theme={sandpackThemeValues[sandpackTheme]}
<div key={sandpackTemplate} data-test-id="ui-pattern-block" className="ui-pattern-block">
<Captions
appBridge={appBridge}
title={title}
description={description}
onTitleChange={(newValue) => setBlockSettings({ title: newValue })}
onDescriptionChange={(newValue) => setBlockSettings({ description: newValue })}
isEditing={isEditing}
/>
<div
data-test-id="ui-pattern-block-wrapper"
style={{
...(hasBorder && getBorderStyles(borderStyle, borderWidth, borderColor)),
borderRadius,
}}
className="tw-rounded tw-bg-white"
>
<SandpackProvider
files={templateFiles}
template={sandpackTemplate}
customSetup={parsedNpmDependencies}
theme={sandpackThemeValues[sandpackTheme]}
key={sandpackRestartInitiators.map((i) => i.toString()).join('')}
options={{
classes: EDITOR_CLASSES,
activeFile: initialActiveFile[sandpackTemplate],
externalResources: [cssToInject, ...parsedExternalDependencies],
}}
>
<SandpackLayout
style={{ borderTopRightRadius: borderRadius, borderTopLeftRadius: borderRadius }}
className="tw-flex tw-flex-col"
>
{isResponsivePreviewOpen && (
<ResponsivePreview onClose={() => setIsResponsivePreviewOpen(false)} />
)}
<SandpackPreview
ref={previewRef}
style={{
height: getHeightStyle(isCustomHeight, customHeightValue, heightChoice),
padding: getPaddingStyle(hasCustomPadding, paddingCustom, paddingChoice),
...(hasBackground && {
...getBackgroundColorStyles(backgroundColor),
backgroundImage: 'none',
}),
borderTopRightRadius: borderRadius,
borderTopLeftRadius: borderRadius,
}}
showRefreshButton={false}
showOpenInCodeSandbox={false}
className="tw-rounded-none tw-shadow-none tw-ml-0 tw-bg-white"
/>
{(isEditing || showCode) && (
<CodeEditor
key={`editor_${isEditing.toString()}`}
isCodeEditable={isEditing || isCodeEditable}
showResetButton={showResetButton}
showSandboxLink={showSandboxLink}
showResponsivePreview={showResponsivePreview}
onResponsivePreviewOpen={() => setIsResponsivePreviewOpen(true)}
onCodeChange={onCodeChange}
onResetFilesToDefault={onResetFiles}
onResetRun={onResetRun}
hasCodeChanges={!isEditing && hasCodeChanges}
template={sandpackTemplate}
shouldCollapseCodeByDefault={!isEditing && shouldCollapseCodeByDefault}
/>
)}
</SandpackLayout>
</SandpackProvider>

{(isEditing || showNpmDependencies) && (
<NPMDependencies
key={`npm_${isEditing.toString()}`}
npmDependencies={npmDependencies}
shouldCollapseByDefault={!isEditing && shouldCollapseDependenciesByDefault}
onNpmDependenciesChanged={(newDependencies) => onDependenciesChanged(newDependencies, 'npm')}
borderRadius={showExternalDependencies ? undefined : borderRadius}
readOnly={!isEditing}
/>
)}
{(isEditing || showExternalDependencies) && (
<ExternalDependencies
key={`external_${isEditing.toString()}`}
externalDependencies={externalDependencies}
shouldCollapseByDefault={!isEditing && shouldCollapseDependenciesByDefault}
onExternalDependenciesChanged={(newDependencies) =>
onDependenciesChanged(newDependencies, 'external')
}
borderRadius={borderRadius}
readOnly={!isEditing}
/>
)}
</div>
</div>
);
};
55 changes: 55 additions & 0 deletions packages/ui-pattern-block/src/components/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* (c) Copyright Frontify Ltd., all rights reserved. */

import { FOCUS_VISIBLE_STYLE, IconCaretDown12 } from '@frontify/fondue';
import { joinClassNames } from '@frontify/guideline-blocks-settings';
import { PropsWithChildren, ReactElement } from 'react';

interface Props {
label: string;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
borderRadius?: number;
}

export const Accordion = ({
label,
children,
isOpen,
setIsOpen,
borderRadius,
}: PropsWithChildren<Props>): ReactElement => {
return (
<div data-test-id="dependency-accordion" className="tw-border-b tw-border-b-line last:tw-border-b-0">
<button
aria-expanded={isOpen}
className={joinClassNames([
'tw-relative focus:tw-z-20 tw-text-s tw-gap-2 tw-w-[calc(100%-32px)] tw-text-text-weak tw-box-content tw-bg-white tw-h-10 tw-px-4 tw-flex tw-items-center',
isOpen && 'tw-border-b tw-border-b-line',
FOCUS_VISIBLE_STYLE,
])}
style={{
borderBottomLeftRadius: !isOpen ? borderRadius : undefined,
borderBottomRightRadius: !isOpen ? borderRadius : undefined,
}}
onClick={() => setIsOpen(!isOpen)}
>
{label}
<div className={joinClassNames([isOpen ? 'tw-rotate-180' : ''])}>
<IconCaretDown12 />
</div>
</button>
{isOpen && (
<div
style={{
borderBottomLeftRadius: borderRadius,
borderBottomRightRadius: borderRadius,
}}
data-test-id="dependency-accordion-children"
className="tw-overflow-hidden"
>
{children}
</div>
)}
</div>
);
};
Loading

0 comments on commit 7f8c7f2

Please sign in to comment.