Add basic curtains.
This commit is contained in:
		
							parent
							
								
									4a7e77a8f3
								
							
						
					
					
						commit
						b0905b4a4f
					
				
					 9 changed files with 241 additions and 14 deletions
				
			
		| 
						 | 
				
			
			@ -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()
 | 
			
		|||
 | 
			
		||||
			<h1>KernelUI</h1>
 | 
			
		||||
 | 
			
		||||
			<h2>TODO</h2>
 | 
			
		||||
 | 
			
		||||
			<ul>
 | 
			
		||||
				<li>Errors</li>
 | 
			
		||||
				<li>Subapps</li>
 | 
			
		||||
				<li>Modals</li>
 | 
			
		||||
			</ul>
 | 
			
		||||
 | 
			
		||||
			<h2>Headings</h2>
 | 
			
		||||
 | 
			
		||||
			<h1>Demo app</h1>
 | 
			
		||||
| 
						 | 
				
			
			@ -442,6 +437,30 @@ export function DemoApp()
 | 
			
		|||
				)}
 | 
			
		||||
			</AsyncPaginate>
 | 
			
		||||
 | 
			
		||||
			<h2>Curtains & co</h2>
 | 
			
		||||
 | 
			
		||||
			<h3>Curtains</h3>
 | 
			
		||||
 | 
			
		||||
			<Card>
 | 
			
		||||
				<button onClick={() => {
 | 
			
		||||
					const curtainId = curtains.open(
 | 
			
		||||
						<button className={"close"} onClick={() => { curtains.close(curtainId); }}><X /> Close the curtain</button>
 | 
			
		||||
					);
 | 
			
		||||
				}}>Open a curtain</button>
 | 
			
		||||
			</Card>
 | 
			
		||||
 | 
			
		||||
			<h3>Subapps</h3>
 | 
			
		||||
 | 
			
		||||
			<Card>
 | 
			
		||||
				<button>Open a subapp</button>
 | 
			
		||||
			</Card>
 | 
			
		||||
 | 
			
		||||
			<h3>Modals</h3>
 | 
			
		||||
 | 
			
		||||
			<Card>
 | 
			
		||||
				<button>Open a modal</button>
 | 
			
		||||
			</Card>
 | 
			
		||||
 | 
			
		||||
		</Application>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}
 | 
			
		||||
			<RouterProvider router={router} />
 | 
			
		||||
			{footer}
 | 
			
		||||
			<Curtains>
 | 
			
		||||
				{header}
 | 
			
		||||
				<RouterProvider router={router} />
 | 
			
		||||
				{footer}
 | 
			
		||||
			</Curtains>
 | 
			
		||||
		</IconContext.Provider>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										136
									
								
								src/Components/Curtains/Curtains.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/Components/Curtains/Curtains.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,136 @@
 | 
			
		|||
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<CurtainsContextState>({
 | 
			
		||||
	// 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<Record<CurtainUuidType, React.ReactNode>>({});
 | 
			
		||||
 | 
			
		||||
	// Initialize open curtain function.
 | 
			
		||||
	const open = useRef<OpenCurtainFunction>();
 | 
			
		||||
	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<CloseCurtainFunction>();
 | 
			
		||||
	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]);
 | 
			
		||||
 | 
			
		||||
	// Show dimmed main content.
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		if (Object.entries(curtains).length > 0 && !document.body.classList.contains("dimmed"))
 | 
			
		||||
			// We should dim content and it's currently not.
 | 
			
		||||
			document.body.classList.add("dimmed");
 | 
			
		||||
		else
 | 
			
		||||
			// We shouldn't dim content.
 | 
			
		||||
			document.body.classList.remove("dimmed");
 | 
			
		||||
	}, [curtains]);
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<CurtainsContext.Provider value={contextState}>
 | 
			
		||||
			{children}
 | 
			
		||||
			<CurtainsPortal curtains={curtains} />
 | 
			
		||||
		</CurtainsContext.Provider>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Curtains portal manager.
 | 
			
		||||
 */
 | 
			
		||||
function CurtainsPortal({curtains}: {
 | 
			
		||||
	curtains: Record<CurtainUuidType, React.ReactNode>;
 | 
			
		||||
})
 | 
			
		||||
{
 | 
			
		||||
	return ReactDOM.createPortal(Object.entries(curtains).map(([uuid, curtainContent]) => (
 | 
			
		||||
		<CurtainInstance key={uuid}>
 | 
			
		||||
			{curtainContent}
 | 
			
		||||
		</CurtainInstance>
 | 
			
		||||
	)), document.body);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component of an opened curtain instance.
 | 
			
		||||
 */
 | 
			
		||||
function CurtainInstance({children}: React.PropsWithChildren<{}>)
 | 
			
		||||
{
 | 
			
		||||
	return (
 | 
			
		||||
		<div className={"curtain"}>
 | 
			
		||||
			{children}
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
 | 
			
		||||
@import "components/_button";
 | 
			
		||||
@import "components/_card";
 | 
			
		||||
@import "components/_curtains";
 | 
			
		||||
@import "components/_dates";
 | 
			
		||||
@import "components/_errors";
 | 
			
		||||
@import "components/_floating";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										46
									
								
								src/styles/components/_curtains.less
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/styles/components/_curtains.less
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue