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
-
-
- - Errors
- - Subapps
- - Modals
-
-
Headings
Demo app
@@ -442,6 +437,30 @@ export function DemoApp()
)}
+ Curtains & co
+
+ Curtains
+
+
+
+
+
+ Subapps
+
+
+ Open a subapp
+
+
+ Modals
+
+
+ Open a modal
+
+
);
}
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..cf6ba8f
--- /dev/null
+++ b/src/Components/Curtains/Curtains.tsx
@@ -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({
+ // 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]);
+
+ // 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 (
+
+ {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"