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 "../index";
|
||||||
import {Checkbox} from "../src/Components/Forms/Checkbox";
|
import {Checkbox} from "../src/Components/Forms/Checkbox";
|
||||||
import { Radio } from "../src/Components/Forms/Radio";
|
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 {Card} from "../src/Components/Card";
|
||||||
import {PasswordInput} from "../src/Components/Forms/PasswordInput";
|
import {PasswordInput} from "../src/Components/Forms/PasswordInput";
|
||||||
import {RequiredField} from "../src/Components/Forms/RequiredField";
|
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 {ToggleSwitch} from "../src/Components/Forms/ToggleSwitch";
|
||||||
import {Step, Steps} from "../src/Components/Steps/Steps";
|
import {Step, Steps} from "../src/Components/Steps/Steps";
|
||||||
import {AsyncPaginate, AutoPaginate, Paginate} from "../src/Components/Pagination/Paginate";
|
import {AsyncPaginate, AutoPaginate, Paginate} from "../src/Components/Pagination/Paginate";
|
||||||
|
import {useCurtains} from "../src/Components/Curtains/Curtains";
|
||||||
|
|
||||||
export function DemoApp()
|
export function DemoApp()
|
||||||
{
|
{
|
||||||
|
const curtains = useCurtains();
|
||||||
|
|
||||||
const [datetime, setDatetime] = useState(null);
|
const [datetime, setDatetime] = useState(null);
|
||||||
|
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
|
@ -63,14 +66,6 @@ export function DemoApp()
|
||||||
|
|
||||||
<h1>KernelUI</h1>
|
<h1>KernelUI</h1>
|
||||||
|
|
||||||
<h2>TODO</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Errors</li>
|
|
||||||
<li>Subapps</li>
|
|
||||||
<li>Modals</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Headings</h2>
|
<h2>Headings</h2>
|
||||||
|
|
||||||
<h1>Demo app</h1>
|
<h1>Demo app</h1>
|
||||||
|
@ -442,6 +437,30 @@ export function DemoApp()
|
||||||
)}
|
)}
|
||||||
</AsyncPaginate>
|
</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>
|
</Application>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,13 @@
|
||||||
"@phosphor-icons/react": "^2.1.5",
|
"@phosphor-icons/react": "^2.1.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-merge-refs": "^2.1.1",
|
"react-merge-refs": "^2.1.1",
|
||||||
"react-router-dom": "^6.24.1"
|
"react-router-dom": "^6.24.1",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/uuid": "^10",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { IconContext } from "@phosphor-icons/react";
|
import { IconContext } from "@phosphor-icons/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
||||||
|
import {Curtains} from "../Components/Curtains/Curtains";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Kernel UI app component which initializes everything.
|
* Main Kernel UI app component which initializes everything.
|
||||||
|
@ -16,9 +17,11 @@ export function Kernel({header, footer, router}: {
|
||||||
size: 16,
|
size: 16,
|
||||||
weight: "bold",
|
weight: "bold",
|
||||||
}}>
|
}}>
|
||||||
|
<Curtains>
|
||||||
{header}
|
{header}
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
{footer}
|
{footer}
|
||||||
|
</Curtains>
|
||||||
</IconContext.Provider>
|
</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-darkest: #000000; --foreground-darkest: @foreground-darkest;
|
||||||
|
|
||||||
@foreground-shadow: fade(@foreground, 50%); --foreground-shadow: @foreground-shadow;
|
@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/_button";
|
||||||
@import "components/_card";
|
@import "components/_card";
|
||||||
|
@import "components/_curtains";
|
||||||
@import "components/_dates";
|
@import "components/_dates";
|
||||||
@import "components/_errors";
|
@import "components/_errors";
|
||||||
@import "components/_floating";
|
@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);
|
border-color: var(--red-darker);
|
||||||
background: var(--red);
|
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
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@vitejs/plugin-react@npm:^4.3.0":
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
resolution: "@vitejs/plugin-react@npm:4.3.0"
|
resolution: "@vitejs/plugin-react@npm:4.3.0"
|
||||||
|
@ -1828,6 +1835,7 @@ __metadata:
|
||||||
"@phosphor-icons/react": "npm:^2.1.5"
|
"@phosphor-icons/react": "npm:^2.1.5"
|
||||||
"@types/react": "npm:^18.3.3"
|
"@types/react": "npm:^18.3.3"
|
||||||
"@types/react-dom": "npm:^18.3.0"
|
"@types/react-dom": "npm:^18.3.0"
|
||||||
|
"@types/uuid": "npm:^10"
|
||||||
"@vitejs/plugin-react": "npm:^4.3.0"
|
"@vitejs/plugin-react": "npm:^4.3.0"
|
||||||
less: "npm:^4.2.0"
|
less: "npm:^4.2.0"
|
||||||
react: "npm:^18.3.1"
|
react: "npm:^18.3.1"
|
||||||
|
@ -1835,6 +1843,7 @@ __metadata:
|
||||||
react-merge-refs: "npm:^2.1.1"
|
react-merge-refs: "npm:^2.1.1"
|
||||||
react-router-dom: "npm:^6.24.1"
|
react-router-dom: "npm:^6.24.1"
|
||||||
typescript: "npm:^5.4.5"
|
typescript: "npm:^5.4.5"
|
||||||
|
uuid: "npm:^10.0.0"
|
||||||
vite: "npm:^5.2.11"
|
vite: "npm:^5.2.11"
|
||||||
vite-plugin-dts: "npm:^3.9.1"
|
vite-plugin-dts: "npm:^3.9.1"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
@ -2821,6 +2830,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"validator@npm:^13.7.0":
|
||||||
version: 13.12.0
|
version: 13.12.0
|
||||||
resolution: "validator@npm:13.12.0"
|
resolution: "validator@npm:13.12.0"
|
||||||
|
|
Loading…
Reference in a new issue