From 750b7efdc47de664ac9d557585842b72d0e6da69 Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Sun, 14 Jul 2024 14:21:51 +0200 Subject: [PATCH] Add basic curtains. --- demo/DemoApp.tsx | 37 ++++++-- package.json | 4 +- src/Application/Kernel.tsx | 9 +- src/Components/Curtains/Curtains.tsx | 133 +++++++++++++++++++++++++++ src/styles/_colors.less | 2 + src/styles/_components.less | 1 + src/styles/components/_button.less | 2 +- src/styles/components/_curtains.less | 46 +++++++++ yarn.lock | 18 ++++ 9 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 src/Components/Curtains/Curtains.tsx create mode 100644 src/styles/components/_curtains.less diff --git a/demo/DemoApp.tsx b/demo/DemoApp.tsx index 1f4b34e..737497e 100644 --- a/demo/DemoApp.tsx +++ b/demo/DemoApp.tsx @@ -2,7 +2,7 @@ import React, {useState} from "react"; import "../index"; import {Checkbox} from "../src/Components/Forms/Checkbox"; import { Radio } from "../src/Components/Forms/Radio"; -import {AirTrafficControl, Basket, FloppyDisk, House, TrashSimple, XCircle} from "@phosphor-icons/react"; +import {AirTrafficControl, Basket, FloppyDisk, House, TrashSimple, X, XCircle} from "@phosphor-icons/react"; import {Card} from "../src/Components/Card"; import {PasswordInput} from "../src/Components/Forms/PasswordInput"; import {RequiredField} from "../src/Components/Forms/RequiredField"; @@ -25,9 +25,12 @@ import {Outlet} from "react-router-dom"; import {ToggleSwitch} from "../src/Components/Forms/ToggleSwitch"; import {Step, Steps} from "../src/Components/Steps/Steps"; import {AsyncPaginate, AutoPaginate, Paginate} from "../src/Components/Pagination/Paginate"; +import {useCurtains} from "../src/Components/Curtains/Curtains"; export function DemoApp() { + const curtains = useCurtains(); + const [datetime, setDatetime] = useState(null); const [selected, setSelected] = useState(null); @@ -63,14 +66,6 @@ export function DemoApp()

KernelUI

-

TODO

- - -

Headings

Demo app

@@ -442,6 +437,30 @@ export function DemoApp() )} +

Curtains & co

+ +

Curtains

+ + + + ); + }}>Open a curtain + + +

Subapps

+ + + + + +

Modals

+ + + + + ); } diff --git a/package.json b/package.json index b0d221c..83b8a73 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "@phosphor-icons/react": "^2.1.5", "react": "^18.3.1", "react-merge-refs": "^2.1.1", - "react-router-dom": "^6.24.1" + "react-router-dom": "^6.24.1", + "uuid": "^10.0.0" }, "devDependencies": { "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/uuid": "^10", "@vitejs/plugin-react": "^4.3.0", "less": "^4.2.0", "react-dom": "^18.3.1", diff --git a/src/Application/Kernel.tsx b/src/Application/Kernel.tsx index 7aef8b0..c2c9aab 100644 --- a/src/Application/Kernel.tsx +++ b/src/Application/Kernel.tsx @@ -1,6 +1,7 @@ import { IconContext } from "@phosphor-icons/react"; import React from "react"; import {createBrowserRouter, RouterProvider} from "react-router-dom"; +import {Curtains} from "../Components/Curtains/Curtains"; /** * Main Kernel UI app component which initializes everything. @@ -16,9 +17,11 @@ export function Kernel({header, footer, router}: { size: 16, weight: "bold", }}> - {header} - - {footer} + + {header} + + {footer} + ); } diff --git a/src/Components/Curtains/Curtains.tsx b/src/Components/Curtains/Curtains.tsx new file mode 100644 index 0000000..c8b1939 --- /dev/null +++ b/src/Components/Curtains/Curtains.tsx @@ -0,0 +1,133 @@ +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from "react"; +import ReactDOM from "react-dom"; +import {v4 as uuidv4} from "uuid"; + +/** + * Curtain UUID type. + */ +export type CurtainUuidType = string; + +/** + * The function that opens a curtain. + */ +export type OpenCurtainFunction = (content: React.ReactNode) => CurtainUuidType; + +/** + * The function that closes a curtain. + */ +export type CloseCurtainFunction = (uuid: CurtainUuidType) => void; + +/** + * Interface of curtains state. + */ +export interface CurtainsContextState +{ + /** + * Open a new curtain. + * @param content The curtain content. + * @return UUID of the curtain. + */ + open: OpenCurtainFunction; + + /** + * Close the given curtain. + * @param uuid UUID of the curtain to close. + */ + close: CloseCurtainFunction; +} + +const CurtainsContext = React.createContext({ + // Empty functions. + open() { return ""; }, + close() {}, +}); + +/** + * Hook to interact with curtains (open or close). + */ +export function useCurtains(): CurtainsContextState +{ + return useContext(CurtainsContext); +} + +/** + * Page curtains system. + */ +export function Curtains({children}: React.PropsWithChildren<{}>) +{ + // Curtains state. + const [curtains, setCurtains] = useState>({}); + + // Initialize open curtain function. + const open = useRef(); + open.current = useCallback((content: React.ReactNode) => { + // Generate a new curtain UUID for the new curtain to open. + const curtainUuid = uuidv4(); + + // Add the curtain to open to the list of curtains, with the generated UUID. + setCurtains({ + ...curtains, + [curtainUuid]: content, + }); + + // Return the curtain UUID. + return curtainUuid; + }, [curtains, setCurtains]); + + // Initialize close curtain function. + const close = useRef(); + close.current = useCallback((uuid: CurtainUuidType) => { + // Copy the curtains list. + const newCurtains = {...curtains}; + // Remove the given curtain from the list. + delete newCurtains[uuid]; + // Set the new curtains list. + setCurtains(newCurtains); + }, [curtains, setCurtains]); + + // Initialize context state from action functions. + const contextState = useMemo(() => ({ + open: (content: React.ReactNode) => open.current(content), + close: (uuid: CurtainUuidType) => close.current(uuid), + }), [open, close]); + + useEffect(() => { + if (Object.entries(curtains).length > 0 && !document.body.classList.contains("dimmed")) + document.body.classList.add("dimmed"); + else + document.body.classList.remove("dimmed"); + }, [curtains]); + + return ( + + {children} + + + ); +} + +/** + * Curtains portal manager. + */ +function CurtainsPortal({curtains}: { + curtains: Record; +}) +{ + return ReactDOM.createPortal(Object.entries(curtains).map(([uuid, curtainContent]) => ( + + {curtainContent} + + )), document.body); +} + +/** + * Component of an opened curtain instance. + */ +function CurtainInstance({children}: React.PropsWithChildren<{}>) +{ + return ( +
+ {children} +
+ ); +} diff --git a/src/styles/_colors.less b/src/styles/_colors.less index b93d2e4..2e45dc1 100644 --- a/src/styles/_colors.less +++ b/src/styles/_colors.less @@ -15,6 +15,8 @@ @foreground-darkest: #000000; --foreground-darkest: @foreground-darkest; @foreground-shadow: fade(@foreground, 50%); --foreground-shadow: @foreground-shadow; + @curtain-dim: fade(@foreground, 50%); --curtain-dim: @curtain-dim; + @curtain-inset: fade(@foreground, 20%); --curtain-inset: @curtain-inset; diff --git a/src/styles/_components.less b/src/styles/_components.less index acadead..6ea1588 100644 --- a/src/styles/_components.less +++ b/src/styles/_components.less @@ -1,6 +1,7 @@ @import "components/_button"; @import "components/_card"; +@import "components/_curtains"; @import "components/_dates"; @import "components/_errors"; @import "components/_floating"; diff --git a/src/styles/components/_button.less b/src/styles/components/_button.less index ec79708..11bed29 100644 --- a/src/styles/components/_button.less +++ b/src/styles/components/_button.less @@ -125,7 +125,7 @@ a.button, button, input[type="submit"], input[type="reset"] } } - &.red, &.delete, &.remove, &.no, &.negative, &.bad + &.red, &.delete, &.remove, &.close, &.no, &.negative, &.bad { border-color: var(--red-darker); background: var(--red); diff --git a/src/styles/components/_curtains.less b/src/styles/components/_curtains.less new file mode 100644 index 0000000..72d75be --- /dev/null +++ b/src/styles/components/_curtains.less @@ -0,0 +1,46 @@ +body > .curtain +{ // Position the curtain on top of everything and dim the background. + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + + box-shadow: inset 0 0 5em 0 var(--curtain-inset); + background: var(--curtain-dim); + + // Show an animation when entering screen. + animation: curtain-in 0.4s ease-in; + transform-origin: center; + + z-index: 1000; // On top of main content. +} + +body.dimmed +{ // Disable scroll and blur the content when the body is dimmed. + overflow: hidden; + + > *:not(.curtain) + { + transition: filter 0.4s ease-in; + + filter: blur(0.2em); + } +} + +@keyframes curtain-in +{ // Screen enter animation. + from + { + transform: scale(1.2); + filter: blur(0.5em); + opacity: 0; + } + + to + { + transform: scale(1); + filter: blur(0); + opacity: 1; + } +} diff --git a/yarn.lock b/yarn.lock index 2842f13..b842322 100644 --- a/yarn.lock +++ b/yarn.lock @@ -923,6 +923,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^10": + version: 10.0.0 + resolution: "@types/uuid@npm:10.0.0" + checksum: 10c0/9a1404bf287164481cb9b97f6bb638f78f955be57c40c6513b7655160beb29df6f84c915aaf4089a1559c216557dc4d2f79b48d978742d3ae10b937420ddac60 + languageName: node + linkType: hard + "@vitejs/plugin-react@npm:^4.3.0": version: 4.3.0 resolution: "@vitejs/plugin-react@npm:4.3.0" @@ -1828,6 +1835,7 @@ __metadata: "@phosphor-icons/react": "npm:^2.1.5" "@types/react": "npm:^18.3.3" "@types/react-dom": "npm:^18.3.0" + "@types/uuid": "npm:^10" "@vitejs/plugin-react": "npm:^4.3.0" less: "npm:^4.2.0" react: "npm:^18.3.1" @@ -1835,6 +1843,7 @@ __metadata: react-merge-refs: "npm:^2.1.1" react-router-dom: "npm:^6.24.1" typescript: "npm:^5.4.5" + uuid: "npm:^10.0.0" vite: "npm:^5.2.11" vite-plugin-dts: "npm:^3.9.1" languageName: unknown @@ -2821,6 +2830,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" + bin: + uuid: dist/bin/uuid + checksum: 10c0/eab18c27fe4ab9fb9709a5d5f40119b45f2ec8314f8d4cf12ce27e4c6f4ffa4a6321dc7db6c515068fa373c075b49691ba969f0010bf37f44c37ca40cd6bf7fe + languageName: node + linkType: hard + "validator@npm:^13.7.0": version: 13.12.0 resolution: "validator@npm:13.12.0"