mirror of
https://github.com/penpot/penpot.git
synced 2026-07-31 18:36:18 +00:00
🎉 Add components library (#10675)
* 🎉 Install react-aria-components * 🎉 Create modal component in TS using react-aria-component * 🎉 Create modal ds component * 🎉 Separate header content and footer components * 🐛 Remove mf/html macros when not needed * 🎉 Solve little problems * ♻️ Format files * ♻️ Remove ModalCloseBtn * 🐛 Fix CI * ♻️ Remove unused files * 🎉 Make close button not dependant on the modal header * 🎉 Add footer with two slots * 🎉 Improvements on modal * 🐛 Fix package imports and remove login example code --------- Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
972353eccb
commit
e705b6e12c
@ -16,7 +16,7 @@
|
||||
"scripts": {
|
||||
"build:app:assets": "node ./scripts/build-app-assets.js",
|
||||
"build:fonts-preview": "node ./scripts/build-fonts-preview.js",
|
||||
"build:storybook": "pnpm run build:storybook:assets && pnpm run build:storybook:cljs && storybook build",
|
||||
"build:storybook": "(cd packages/ui && pnpm run build) && pnpm run build:storybook:assets && pnpm run build:storybook:cljs && storybook build",
|
||||
"build:storybook:assets": "node ./scripts/build-storybook-assets.js",
|
||||
"build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook",
|
||||
"build:wasm": "../render-wasm/build",
|
||||
@ -58,7 +58,6 @@
|
||||
"@penpot/text-editor": "link:text-editor",
|
||||
"@penpot/tokenscript": "link:packages/tokenscript",
|
||||
"@penpot/ua-parser": "penpot/ua-parser#1.0.0",
|
||||
"@penpot/ui": "link:packages/ui",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@storybook/addon-docs": "10.4.6",
|
||||
"@storybook/addon-themes": "10.4.6",
|
||||
@ -129,5 +128,9 @@
|
||||
"watcher": "^2.3.1",
|
||||
"workerpool": "^10.0.3",
|
||||
"xregexp": "^5.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@penpot/ui": "link:packages/ui",
|
||||
"react-aria-components": "^1.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
".": {
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./modal": {
|
||||
"import": "./dist/modal.js",
|
||||
"types": "./dist/modal.d.ts"
|
||||
},
|
||||
"./style.css": "./dist/style.css"
|
||||
},
|
||||
"scripts": {
|
||||
@ -32,6 +36,9 @@
|
||||
"storybook": "10.4.6",
|
||||
"vite-plugin-dts": "^5.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-aria-components": "^1.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19.2",
|
||||
"react-dom": ">=19.2"
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from './lib/example/Example';
|
||||
export { Modal, useModalClose } from './lib/modal/Modal';
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
.container {
|
||||
background-color: #f0f0f0;
|
||||
padding: 16px;
|
||||
border: 2px solid #000;
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import Example from './Example';
|
||||
|
||||
describe('Example', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<Example />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
import { Example } from './Example';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
const meta = {
|
||||
title: 'UI/Example',
|
||||
component: Example,
|
||||
} satisfies Meta<typeof Example>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {};
|
||||
@ -1,21 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import styles from './Example.module.css';
|
||||
|
||||
export function Example() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h1>Example!</h1>
|
||||
<div>
|
||||
<h2>Counter: {count}</h2>
|
||||
<button onClick={() => setCount(count + 1)}>Increment</button>
|
||||
<button onClick={() => setCount(count - 1)}>Decrement</button>
|
||||
<button onClick={() => setCount(0)}>Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default Example;
|
||||
113
frontend/packages/ui/src/lib/modal/Modal.module.scss
Normal file
113
frontend/packages/ui/src/lib/modal/Modal.module.scss
Normal file
@ -0,0 +1,113 @@
|
||||
@use "ds/_borders" as *;
|
||||
@use "ds/_sizes" as *;
|
||||
@use "ds/_utils" as *;
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--color-overlay-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--z-index-set);
|
||||
|
||||
&[data-entering] {
|
||||
animation: overlay-fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: overlay-fade-out 0.15s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
inline-size: 100%;
|
||||
margin: var(--sp-l);
|
||||
background: var(--color-background-primary);
|
||||
border-radius: $br-8;
|
||||
box-shadow: var(--el-shadow-dark);
|
||||
outline: none;
|
||||
max-block-size: calc(100dvh - var(--sp-xxxl) * 2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-zoom-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: modal-zoom-out 0.15s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.modalSmall {
|
||||
max-inline-size: $sz-400;
|
||||
}
|
||||
|
||||
.modalMedium {
|
||||
max-inline-size: $sz-512;
|
||||
}
|
||||
|
||||
.modalLarge {
|
||||
max-inline-size: px2rem(640);
|
||||
}
|
||||
|
||||
.modalXlarge {
|
||||
max-inline-size: px2rem(960);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: relative;
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
overflow: hidden;
|
||||
max-block-size: inherit;
|
||||
}
|
||||
|
||||
|
||||
@keyframes overlay-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes overlay-fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modal-zoom-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modal-zoom-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
66
frontend/packages/ui/src/lib/modal/Modal.tsx
Normal file
66
frontend/packages/ui/src/lib/modal/Modal.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import {
|
||||
DialogTrigger,
|
||||
ModalOverlay,
|
||||
Modal as RACModal,
|
||||
Dialog,
|
||||
} from 'react-aria-components';
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import styles from './Modal.module.scss';
|
||||
|
||||
const ModalCloseContext = createContext<(() => void) | null>(null);
|
||||
|
||||
export function useModalClose(): (() => void) | null {
|
||||
return useContext(ModalCloseContext);
|
||||
}
|
||||
|
||||
interface ModalProps {
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
children: ReactNode;
|
||||
trigger?: ReactNode;
|
||||
isDismissable?: boolean;
|
||||
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
children,
|
||||
trigger,
|
||||
isDismissable = true,
|
||||
size = 'medium',
|
||||
className,
|
||||
}: ModalProps) {
|
||||
const sizeClass =
|
||||
size === 'small'
|
||||
? styles.modalSmall
|
||||
: size === 'large'
|
||||
? styles.modalLarge
|
||||
: size === 'xlarge'
|
||||
? styles.modalXlarge
|
||||
: styles.modalMedium;
|
||||
|
||||
return (
|
||||
<DialogTrigger isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
{trigger}
|
||||
<ModalOverlay className={styles.overlay} isDismissable={isDismissable}>
|
||||
<RACModal
|
||||
className={`${styles.modal} ${sizeClass} ${className ?? ''}`}
|
||||
>
|
||||
<Dialog className={styles.dialog}>
|
||||
{({ close }) => (
|
||||
<ModalCloseContext.Provider value={close}>
|
||||
{children}
|
||||
</ModalCloseContext.Provider>
|
||||
)}
|
||||
</Dialog>
|
||||
</RACModal>
|
||||
</ModalOverlay>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
1
frontend/packages/ui/src/modal.ts
Normal file
1
frontend/packages/ui/src/modal.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Modal, useModalClose } from './lib/modal/Modal';
|
||||
@ -10,7 +10,7 @@ const copyCssPlugin = () => ({
|
||||
closeBundle: () => {
|
||||
try {
|
||||
copyFileSync(
|
||||
'dist/index.css',
|
||||
'dist/ui.css',
|
||||
'../../resources/public/css/ui.css',
|
||||
);
|
||||
} catch (e) {
|
||||
@ -21,6 +21,15 @@ const copyCssPlugin = () => ({
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: import.meta.dirname,
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
loadPaths: [
|
||||
path.resolve(import.meta.dirname, '../../src/app/main/ui'),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
@ -42,9 +51,11 @@ export default defineConfig(() => ({
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
modal: 'src/modal.ts',
|
||||
},
|
||||
name: 'ui',
|
||||
fileName: 'index',
|
||||
formats: ['es' as const],
|
||||
},
|
||||
rollupOptions: {
|
||||
|
||||
121
frontend/pnpm-lock.yaml
generated
121
frontend/pnpm-lock.yaml
generated
@ -25,6 +25,13 @@ patchedDependencies:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@penpot/ui':
|
||||
specifier: link:packages/ui
|
||||
version: link:packages/ui
|
||||
react-aria-components:
|
||||
specifier: ^1.19.0
|
||||
version: 1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
devDependencies:
|
||||
'@penpot/draft-js':
|
||||
specifier: link:packages/draft-js
|
||||
@ -47,9 +54,6 @@ importers:
|
||||
'@penpot/ua-parser':
|
||||
specifier: penpot/ua-parser#1.0.0
|
||||
version: https://codeload.github.com/penpot/ua-parser/tar.gz/90b970f39f2dc08378b975a0f01045b4ec8e89a4
|
||||
'@penpot/ui':
|
||||
specifier: link:packages/ui
|
||||
version: link:packages/ui
|
||||
'@playwright/test':
|
||||
specifier: 1.61.1
|
||||
version: 1.61.1
|
||||
@ -293,6 +297,9 @@ importers:
|
||||
react:
|
||||
specifier: '>=19.2'
|
||||
version: 19.2.7
|
||||
react-aria-components:
|
||||
specifier: ^1.19.0
|
||||
version: 1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
react-dom:
|
||||
specifier: '>=19.2'
|
||||
version: 19.2.7(react@19.2.7)
|
||||
@ -965,6 +972,15 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@internationalized/date@3.12.2':
|
||||
resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==}
|
||||
|
||||
'@internationalized/number@3.6.7':
|
||||
resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==}
|
||||
|
||||
'@internationalized/string@3.2.9':
|
||||
resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==}
|
||||
|
||||
'@joshwooding/vite-plugin-react-docgen-typescript@0.7.0':
|
||||
resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==}
|
||||
peerDependencies:
|
||||
@ -1505,6 +1521,11 @@ packages:
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@react-types/shared@3.36.0':
|
||||
resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
|
||||
'@resvg/resvg-js-android-arm-eabi@2.6.2':
|
||||
resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==}
|
||||
engines: {node: '>= 10'}
|
||||
@ -1977,6 +1998,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@swc/helpers@0.5.23':
|
||||
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -2268,6 +2292,10 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
aria-query@5.3.0:
|
||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||
|
||||
@ -2515,6 +2543,9 @@ packages:
|
||||
resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==}
|
||||
engines: {node: '>= 4.0'}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
@ -2541,6 +2572,10 @@ packages:
|
||||
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
color-convert@1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
|
||||
@ -4527,6 +4562,18 @@ packages:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
react-aria-components@1.19.0:
|
||||
resolution: {integrity: sha512-2smSS5nqJ8cGYMQezuUXveZm7eMyHCqTN6mDpylQBYLYbdF5dxCCuW1DHn1VKLe1DybSfPvX/cZtJlDmvFfn8A==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
|
||||
react-aria@3.50.0:
|
||||
resolution: {integrity: sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
|
||||
react-compiler-runtime@1.0.0:
|
||||
resolution: {integrity: sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==}
|
||||
peerDependencies:
|
||||
@ -4560,6 +4607,11 @@ packages:
|
||||
react-lifecycles-compat@3.0.4:
|
||||
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
||||
|
||||
react-stately@3.48.0:
|
||||
resolution: {integrity: sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
|
||||
react-virtualized@9.22.6:
|
||||
resolution: {integrity: sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==}
|
||||
peerDependencies:
|
||||
@ -6304,6 +6356,18 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@internationalized/date@3.12.2':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.23
|
||||
|
||||
'@internationalized/number@3.6.7':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.23
|
||||
|
||||
'@internationalized/string@3.2.9':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.23
|
||||
|
||||
'@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.1.2(@types/node@26.1.0)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.101.0))':
|
||||
dependencies:
|
||||
glob: 13.0.6
|
||||
@ -6753,6 +6817,10 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@react-types/shared@3.36.0(react@19.2.7)':
|
||||
dependencies:
|
||||
react: 19.2.7
|
||||
|
||||
'@resvg/resvg-js-android-arm-eabi@2.6.2':
|
||||
optional: true
|
||||
|
||||
@ -7107,6 +7175,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@swc/helpers@0.5.23':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
@ -7457,6 +7529,10 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
aria-query@5.3.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
@ -7767,6 +7843,8 @@ snapshots:
|
||||
dependencies:
|
||||
source-map: 0.6.1
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@ -7793,6 +7871,8 @@ snapshots:
|
||||
|
||||
clsx@1.2.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
color-convert@1.9.3:
|
||||
dependencies:
|
||||
color-name: 1.1.3
|
||||
@ -9909,6 +9989,31 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
react-aria-components@1.19.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
'@internationalized/date': 3.12.2
|
||||
'@react-types/shared': 3.36.0(react@19.2.7)
|
||||
'@swc/helpers': 0.5.23
|
||||
client-only: 0.0.1
|
||||
react: 19.2.7
|
||||
react-aria: 3.50.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
react-stately: 3.48.0(react@19.2.7)
|
||||
|
||||
react-aria@3.50.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
'@internationalized/date': 3.12.2
|
||||
'@internationalized/number': 3.6.7
|
||||
'@internationalized/string': 3.2.9
|
||||
'@react-types/shared': 3.36.0(react@19.2.7)
|
||||
'@swc/helpers': 0.5.23
|
||||
aria-hidden: 1.2.6
|
||||
clsx: 2.1.1
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
react-stately: 3.48.0(react@19.2.7)
|
||||
use-sync-external-store: 1.6.0(react@19.2.7)
|
||||
|
||||
react-compiler-runtime@1.0.0(react@19.2.7):
|
||||
dependencies:
|
||||
react: 19.2.7
|
||||
@ -9947,6 +10052,16 @@ snapshots:
|
||||
|
||||
react-lifecycles-compat@3.0.4: {}
|
||||
|
||||
react-stately@3.48.0(react@19.2.7):
|
||||
dependencies:
|
||||
'@internationalized/date': 3.12.2
|
||||
'@internationalized/number': 3.6.7
|
||||
'@internationalized/string': 3.2.9
|
||||
'@react-types/shared': 3.36.0(react@19.2.7)
|
||||
'@swc/helpers': 0.5.23
|
||||
react: 19.2.7
|
||||
use-sync-external-store: 1.6.0(react@19.2.7)
|
||||
|
||||
react-virtualized@9.22.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||
<link id="theme" href="css/main.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/ui.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
||||
{{#isDebug}}
|
||||
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
|
||||
{{/isDebug}}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<link href="./css/ds.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link href="./css/ui.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
execSync("pnpm run build", { cwd: "packages/ui", stdio: "inherit" });
|
||||
|
||||
/**
|
||||
* esbuild plugin to watch a directory recursively
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*
|
||||
token-status-list]]
|
||||
[app.main.ui.ds.layout.modal :refer [modal* modal-header* modal-content* modal-footer*]]
|
||||
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
|
||||
[app.main.ui.ds.notifications.actionable :refer [actionable*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
@ -83,6 +84,10 @@
|
||||
:MilestoneGroup milestone-group*
|
||||
:Date date*
|
||||
:PanelTitle panel-title*
|
||||
:Modal modal*
|
||||
:ModalHeader modal-header*
|
||||
:ModalContent modal-content*
|
||||
:ModalFooter modal-footer*
|
||||
|
||||
:set-default-translations
|
||||
(fn [data]
|
||||
|
||||
120
frontend/src/app/main/ui/ds/layout/modal.cljs
Normal file
120
frontend/src/app/main/ui/ds/layout/modal.cljs
Normal file
@ -0,0 +1,120 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.main.ui.ds.layout.modal
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
["@penpot/ui/modal" :as modal]
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:modal-header
|
||||
[:map
|
||||
[:title {:optional true} [:maybe :string]]
|
||||
[:class {:optional true} [:maybe :string]]])
|
||||
|
||||
(mf/defc modal-header*
|
||||
{::mf/schema schema:modal-header}
|
||||
[{:keys [title class children] :rest props}]
|
||||
(let [props (mf/spread-props props
|
||||
{:class [class (stl/css :modal-header)]})]
|
||||
[:> :div props
|
||||
(when title
|
||||
[:> heading* {:typography t/headline-medium
|
||||
:level 2
|
||||
:class (stl/css :modal-header-title)}
|
||||
title])
|
||||
children]))
|
||||
|
||||
(def ^:private schema:modal-content
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]])
|
||||
|
||||
(mf/defc modal-content*
|
||||
{::mf/schema schema:modal-content}
|
||||
[{:keys [class children] :rest props}]
|
||||
(let [props (mf/spread-props props
|
||||
{:class [class (stl/css :modal-content)]})]
|
||||
[:> :div props
|
||||
children]))
|
||||
|
||||
(def ^:private schema:modal-footer
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:start {:optional true} [:maybe :any]]
|
||||
[:end {:optional true} [:maybe :any]]
|
||||
[:variant {:optional true} [:maybe [:enum "split" "base"]]]])
|
||||
|
||||
(mf/defc modal-footer*
|
||||
{::mf/schema schema:modal-footer}
|
||||
[{:keys [class variant children start end] :rest props}]
|
||||
(let [variant (d/nilv variant "base")
|
||||
variant-class
|
||||
(case variant
|
||||
"split" (stl/css :modal-footer-split)
|
||||
"base" (stl/css :modal-footer-base)
|
||||
nil)
|
||||
props (mf/spread-props props
|
||||
{:class [class (stl/css :modal-footer) variant-class]})]
|
||||
(if (= variant "split")
|
||||
[:> :div props
|
||||
[:> :div {:class (stl/css :modal-footer-left)}
|
||||
start]
|
||||
[:> :div {:class (stl/css :modal-footer-right)}
|
||||
end]]
|
||||
[:> :div props
|
||||
children])))
|
||||
|
||||
|
||||
|
||||
(def ^:private schema:modal
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:is-open {:optional true} [:maybe :boolean]]
|
||||
[:on-open-change {:optional true} [:maybe fn?]]
|
||||
[:trigger {:optional true} [:maybe :any]]
|
||||
[:is-dismissable {:optional true} [:maybe :boolean]]
|
||||
[:size {:optional true} [:maybe [:enum "small" "medium" "large" "xlarge"]]]
|
||||
[:hide-close {:optional true} [:maybe :boolean]]
|
||||
[:show-body {:optional true} [:maybe :boolean]]])
|
||||
|
||||
(mf/defc modal-close-button*
|
||||
[]
|
||||
(let [close (modal/useModalClose)]
|
||||
(when close
|
||||
[:div {:class (stl/css :modal-close)}
|
||||
[:> icon-button* {:icon "close"
|
||||
:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click close}]])))
|
||||
|
||||
(mf/defc modal*
|
||||
{::mf/schema schema:modal}
|
||||
[{:keys [class is-open on-open-change trigger is-dismissable size hide-close show-body children] :rest props}]
|
||||
(let [hide-close (d/nilv hide-close false)
|
||||
is-dismissable (d/nilv is-dismissable true)
|
||||
size (d/nilv size "medium")
|
||||
show-body (d/nilv show-body true)
|
||||
props
|
||||
(mf/spread-props props
|
||||
{:class class
|
||||
:is-open is-open
|
||||
:on-open-change on-open-change
|
||||
:trigger trigger
|
||||
:is-dismissable is-dismissable
|
||||
:size size})]
|
||||
[:> modal/Modal props
|
||||
(when-not hide-close
|
||||
[:> modal-close-button*])
|
||||
(if show-body
|
||||
[:div {:class [class (stl/css :modal-body)]}
|
||||
children]
|
||||
children)]))
|
||||
163
frontend/src/app/main/ui/ds/layout/modal.mdx
Normal file
163
frontend/src/app/main/ui/ds/layout/modal.mdx
Normal file
@ -0,0 +1,163 @@
|
||||
{ /* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
Copyright (c) KALEIDOS INC Sucursal en España SL */ }
|
||||
import { Canvas, Meta } from "@storybook/addon-docs/blocks";
|
||||
import * as Modal from "./modal.stories";
|
||||
|
||||
<Meta title="Layout/Modal" />
|
||||
|
||||
# Modal
|
||||
|
||||
A modal interrupts the current workflow to display critical information or require a user decision. While open, interaction with the rest of the interface is blocked.
|
||||
|
||||
## Example
|
||||
|
||||
### Default
|
||||
|
||||
<Canvas of={Modal.Default} />
|
||||
|
||||
---
|
||||
|
||||
# Usage
|
||||
|
||||
The modal is composed of optional layout components.
|
||||
|
||||
```clojure
|
||||
[:> modal*
|
||||
{:is-open open?
|
||||
:on-open-change #(reset! open* %)}
|
||||
|
||||
[:> modal-header*
|
||||
{:title "Dialog title"}]
|
||||
|
||||
[:> modal-content* {}
|
||||
[:> :p {} "Content"]]
|
||||
|
||||
[:> modal-footer* {}
|
||||
[:> button* {:variant "secondary"} "Cancel"]
|
||||
[:> button* {:variant "primary"} "Save"]]]
|
||||
```
|
||||
|
||||
The header, content and footer are optional. Use only the sections your dialog requires.
|
||||
|
||||
---
|
||||
|
||||
## Footer layouts
|
||||
|
||||
### Simple
|
||||
|
||||
Use when all actions belong together.
|
||||
|
||||
```clojure
|
||||
[:> modal-footer* {}
|
||||
[:> button* {:variant "secondary"} "Cancel"]
|
||||
[:> button* {:variant "primary"} "Save"]]
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```
|
||||
. Cancel Save
|
||||
```
|
||||
|
||||
### Split actions
|
||||
|
||||
Use when a destructive or secondary action should be visually separated from the primary actions.
|
||||
|
||||
```clojure
|
||||
[:> modal-footer*
|
||||
{:variant "split"
|
||||
:start
|
||||
(mf/html
|
||||
[:> button* {:variant "destructive"} "Delete"])
|
||||
|
||||
:end
|
||||
(mf/html
|
||||
[:*
|
||||
[:> button* {:variant "secondary"} "Cancel"]
|
||||
[:> button* {:variant "primary"} "Save"]])}]
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```
|
||||
Delete Cancel Save
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Modal props
|
||||
## is-open
|
||||
|
||||
Controls whether the modal is open.
|
||||
|
||||
Type: boolean
|
||||
|
||||
## on-open-change
|
||||
|
||||
Callback invoked whenever the open state changes.
|
||||
|
||||
Type: function
|
||||
|
||||
## trigger
|
||||
|
||||
Element that opens the modal.
|
||||
|
||||
Type: React element
|
||||
|
||||
## is-dismissable
|
||||
|
||||
Whether the modal can be closed by pressing Escape or clicking the backdrop.
|
||||
|
||||
Default: true
|
||||
|
||||
## hide-close
|
||||
|
||||
Hides the default close button.
|
||||
|
||||
Default: false
|
||||
|
||||
## size
|
||||
|
||||
Controls the modal width.
|
||||
|
||||
Options
|
||||
|
||||
"small"
|
||||
"medium" (default)
|
||||
"large"
|
||||
"xlarge"
|
||||
|
||||
## class
|
||||
|
||||
Additional CSS class applied to the modal.
|
||||
|
||||
# Accessibility
|
||||
|
||||
The modal automatically provides:
|
||||
|
||||
- Accessible `dialog` semantics
|
||||
- Focus trapping
|
||||
- Focus restoration when closed
|
||||
- Keyboard support
|
||||
- Optional dismissal with **Escape** and backdrop click
|
||||
|
||||
---
|
||||
|
||||
# Best practices
|
||||
|
||||
Use a modal for:
|
||||
|
||||
- Confirming destructive actions
|
||||
- Collecting short forms
|
||||
- Displaying critical information
|
||||
- Requesting a decision before continuing
|
||||
|
||||
Avoid using a modal for:
|
||||
|
||||
- Long forms
|
||||
- Large amounts of content
|
||||
- Complex navigation
|
||||
- Non-critical information
|
||||
68
frontend/src/app/main/ui/ds/layout/modal.scss
Normal file
68
frontend/src/app/main/ui/ds/layout/modal.scss
Normal file
@ -0,0 +1,68 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "ds/mixins" as *;
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-header-title {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
inset-block-start: var(--sp-m);
|
||||
inset-inline-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include custom-scrollbar;
|
||||
|
||||
padding: var(--sp-l);
|
||||
overflow-block: auto;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: var(--sp-xxxl);
|
||||
min-block-size: 0;
|
||||
padding: var(--sp-xxxl);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-s);
|
||||
flex-shrink: 0;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.modal-footer-base {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-footer-split {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
146
frontend/src/app/main/ui/ds/layout/modal.stories.jsx
Normal file
146
frontend/src/app/main/ui/ds/layout/modal.stories.jsx
Normal file
@ -0,0 +1,146 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
import * as React from "react";
|
||||
import Components from "@target/components";
|
||||
|
||||
const { Modal, ModalHeader, ModalContent, ModalFooter, Button, IconButton } =
|
||||
Components;
|
||||
|
||||
const ModalWrapper = ({ children, ...props }) => {
|
||||
const [open, setOpen] = React.useState(props.isOpen ?? false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setOpen(props.isOpen ?? false);
|
||||
}, [props.isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>Open Modal</Button>
|
||||
<Modal {...props} isOpen={open} onOpenChange={setOpen}>
|
||||
{children}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Layout/Modal",
|
||||
component: ModalWrapper,
|
||||
args: {
|
||||
isOpen: false,
|
||||
size: "medium",
|
||||
isDismissable: true,
|
||||
children: (
|
||||
<>
|
||||
<ModalHeader title="Dialog Title" />
|
||||
<ModalContent>
|
||||
<p>This is the default modal content.</p>
|
||||
<p>You can put any React content inside.</p>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="primary">Confirm</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
),
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["small", "medium", "large", "xlarge"],
|
||||
},
|
||||
isDismissable: { control: "boolean" },
|
||||
},
|
||||
parameters: {
|
||||
controls: { exclude: ["isOpen", "onOpenChange", "children"] },
|
||||
},
|
||||
render: ({ ...args }) => <ModalWrapper {...args} />,
|
||||
};
|
||||
|
||||
export const Default = {};
|
||||
|
||||
export const WithoutTitle = {
|
||||
args: {
|
||||
hideClose: true,
|
||||
children: (
|
||||
<ModalContent>
|
||||
<p>This modal has no header or footer.</p>
|
||||
</ModalContent>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const DestructiveFooter = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<ModalHeader title="Delete item" />
|
||||
<ModalContent>
|
||||
<p>
|
||||
Are you sure you want to delete this item? This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
</ModalContent>
|
||||
<ModalFooter
|
||||
variant="split"
|
||||
start={<Button variant="destructive">Delete</Button>}
|
||||
end={
|
||||
<>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="primary">Accept</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const ScrollableContent = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<ModalHeader title="Terms and Conditions" />
|
||||
<ModalContent>
|
||||
{Array.from({ length: 20 }, (_, i) => (
|
||||
<p key={i}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat.
|
||||
</p>
|
||||
))}
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="primary">Accept</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const NonDismissable = {
|
||||
args: {
|
||||
isDismissable: false,
|
||||
children: (
|
||||
<>
|
||||
<ModalHeader title="Important" />
|
||||
<ModalContent>
|
||||
<p>
|
||||
This modal cannot be closed by clicking the backdrop or pressing
|
||||
Escape.
|
||||
</p>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="primary">Confirm</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user