New setup for V2.
This commit is contained in:
parent
989f1f04d2
commit
af7f024af9
120 changed files with 2445 additions and 7179 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -21,5 +21,7 @@ node_modules/
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
# Library
|
# Library
|
||||||
|
|
||||||
lib/
|
lib/
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
|
|
3
.prettierrc
Normal file
3
.prettierrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"useTabs": true
|
||||||
|
}
|
16
.storybook/main.ts
Normal file
16
.storybook/main.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
"stories": [
|
||||||
|
"../src/**/*.mdx",
|
||||||
|
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@storybook/addon-docs"
|
||||||
|
],
|
||||||
|
"framework": {
|
||||||
|
"name": "@storybook/react-vite",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default config;
|
14
.storybook/preview.ts
Normal file
14
.storybook/preview.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Preview } from '@storybook/react-vite'
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
|
@ -1,17 +0,0 @@
|
||||||
steps:
|
|
||||||
- name: build_library
|
|
||||||
image: node:alpine
|
|
||||||
volumes:
|
|
||||||
- /tmp/woodpecker/cache/uikernel/core/node_modules:/woodpecker/src/code.zeptotech.net/UIKernel/Core/node_modules
|
|
||||||
- /tmp/woodpecker/cache/uikernel/core/.yarn/cache:/woodpecker/src/code.zeptotech.net/UIKernel/Core/.yarn/cache
|
|
||||||
secrets:
|
|
||||||
- FORGE_TOKEN
|
|
||||||
commands:
|
|
||||||
- corepack enable
|
|
||||||
- yarn install
|
|
||||||
- yarn build
|
|
||||||
- ./.woodpecker/yarn_auth.sh
|
|
||||||
- yarn npm publish
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
ref: refs/tags/v*
|
|
|
@ -1,6 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "npmRegistries:
|
|
||||||
//code.zeptotech.net/api/packages/UIKernel/npm/:
|
|
||||||
npmAlwaysAuth: true
|
|
||||||
npmAuthToken: \"$FORGE_TOKEN\"" >> ./.yarnrc.yml
|
|
635
demo/DemoApp.tsx
635
demo/DemoApp.tsx
|
@ -1,635 +0,0 @@
|
||||||
import React, {useState} from "react";
|
|
||||||
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 {Card} from "../src/Components/Card";
|
|
||||||
import {PasswordInput} from "../src/Components/Forms/PasswordInput";
|
|
||||||
import {RequiredField} from "../src/Components/Forms/RequiredField";
|
|
||||||
import {Float} from "../src/Components/Floating/Float";
|
|
||||||
import {Tooltip} from "../src/Components/Floating/Tooltip";
|
|
||||||
import {DatepickerInput} from "../src/Components/Forms/DatepickerInput";
|
|
||||||
import {TimepickerInput} from "../src/Components/Forms/TimepickerInput";
|
|
||||||
import {Select} from "../src/Components/Select/Select";
|
|
||||||
import {SpinningLoader} from "../src/Components/Loaders/SpinningLoader";
|
|
||||||
import {ListLoader} from "../src/Components/Loaders/ListLoader";
|
|
||||||
import {GenericLoader} from "../src/Components/Loaders/GenericLoader";
|
|
||||||
import {MainMenu} from "../src/Components/Menus/MainMenu";
|
|
||||||
import {SubmenuFloat} from "../src/Components/Menus/SubmenuFloat";
|
|
||||||
import {Submenu} from "../src/Components/Menus/Submenu";
|
|
||||||
import {SubmenuItem, SubmenuItemSubmenu} from "../src/Components/Menus/SubmenuItem";
|
|
||||||
import {MainMenuItemSubmenu, MainMenuLink} from "../src/Components/Menus/MainMenuItem";
|
|
||||||
import {AppItem, AppLink, AppsMenu} from "../src/Components/Menus/AppsMenu";
|
|
||||||
import {Application} from "../src/Application/Application";
|
|
||||||
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 {useCallableCurtain, useCurtains} from "../src/Components/Curtains/Curtains";
|
|
||||||
import {Subapp, useCallableSubapp, useSubapps} from "../src/Components/Subapps/Subapps";
|
|
||||||
import {DemoSubapp} from "./DemoSubapp";
|
|
||||||
import {DemoCurtain} from "./DemoCurtain";
|
|
||||||
import {DemoModal} from "./DemoModal";
|
|
||||||
import {useCallableModal} from "../src/Components/Modals/Modals";
|
|
||||||
import {ModalType} from "../src/Components/Modals/ModalsTypes";
|
|
||||||
import {Buttons} from "../src/Components/Buttons/Buttons";
|
|
||||||
import {useNotify} from "../src/Components/Notifications/Notifications";
|
|
||||||
import {Notification, NotificationType} from "../src/Components/Notifications/Notification";
|
|
||||||
import {Box} from "../src/Components/Box";
|
|
||||||
import {Await, useAsync} from "../src/Async";
|
|
||||||
import {NotifyErrorsBoundary} from "../src/Components/Errors/NotifyErrorsBoundary";
|
|
||||||
import {DemoFailingComponent, DemoResetComponent} from "./DemoFailingComponent";
|
|
||||||
import {Tip} from "../src/Components/Tips/Tip";
|
|
||||||
import {useKernelContext} from "../src/KernelGlobalContext";
|
|
||||||
import {KernelContext} from "./DemoKernelContext";
|
|
||||||
|
|
||||||
export function DemoApp()
|
|
||||||
{
|
|
||||||
const curtains = useCurtains();
|
|
||||||
const subapps = useSubapps();
|
|
||||||
|
|
||||||
// Easy curtain.
|
|
||||||
const easyCurtain = useCallableCurtain(<DemoCurtain />);
|
|
||||||
|
|
||||||
// Easy subapp.
|
|
||||||
const easySubapp = useCallableSubapp(<DemoSubapp />);
|
|
||||||
|
|
||||||
// Easy modal.
|
|
||||||
const easyModal = useCallableModal((type: ModalType = ModalType.NONE) => <DemoModal type={type} />);
|
|
||||||
|
|
||||||
const notify = useNotify();
|
|
||||||
|
|
||||||
const [datetime, setDatetime] = useState(null);
|
|
||||||
|
|
||||||
const [selected, setSelected] = useState(null);
|
|
||||||
const [anotherSelected, setAnotherSelected] = useState(null);
|
|
||||||
|
|
||||||
const [page, setPage] = useState(11);
|
|
||||||
|
|
||||||
const [asyncChange, setAsyncChange] = useState(0);
|
|
||||||
const [anotherChange, setAnotherChange] = useState(0);
|
|
||||||
const [asyncData] = useAsync<string>(() => new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve("async data (" + Math.random() + ")");
|
|
||||||
}, 2000);
|
|
||||||
}), [asyncChange]);
|
|
||||||
|
|
||||||
const [failingComponentsCount, setFailingComponentsCount] = useState(0);
|
|
||||||
|
|
||||||
const [kernelContext, setKernelContext] = useKernelContext(KernelContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Application>
|
|
||||||
<MainMenu>
|
|
||||||
<MainMenuLink to={"/"}><House /> Home</MainMenuLink>
|
|
||||||
<MainMenuLink to={"/test"}><AirTrafficControl /> Test</MainMenuLink>
|
|
||||||
<MainMenuItemSubmenu submenu={
|
|
||||||
<Submenu>
|
|
||||||
<SubmenuItem>Test 1</SubmenuItem>
|
|
||||||
<SubmenuItem>Test 2</SubmenuItem>
|
|
||||||
<SubmenuItemSubmenu submenu={
|
|
||||||
<Submenu>
|
|
||||||
<SubmenuItem>Test A</SubmenuItem>
|
|
||||||
<SubmenuItem>Test B</SubmenuItem>
|
|
||||||
<SubmenuItemSubmenu submenu={
|
|
||||||
<Submenu>
|
|
||||||
<SubmenuItem href={"#first-last-choice"}>First last choice</SubmenuItem>
|
|
||||||
<SubmenuItem>Another last choice</SubmenuItem>
|
|
||||||
</Submenu>
|
|
||||||
}><Basket /> Submenu in submenu</SubmenuItemSubmenu>
|
|
||||||
</Submenu>
|
|
||||||
}><Basket /> Submenu</SubmenuItemSubmenu>
|
|
||||||
</Submenu>
|
|
||||||
}>
|
|
||||||
<Basket /> Submenu
|
|
||||||
</MainMenuItemSubmenu>
|
|
||||||
</MainMenu>
|
|
||||||
|
|
||||||
<h1>KernelUI</h1>
|
|
||||||
|
|
||||||
<h2>Headings</h2>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<h1>Demo app</h1>
|
|
||||||
|
|
||||||
<h2>Second title</h2>
|
|
||||||
|
|
||||||
<h3>Third title</h3>
|
|
||||||
|
|
||||||
<h4>Fourth title</h4>
|
|
||||||
|
|
||||||
<h5>Fifth title</h5>
|
|
||||||
|
|
||||||
<h6>Sixth title</h6>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<h2>Buttons</h2>
|
|
||||||
|
|
||||||
<button type={"button"}>A cool button</button>
|
|
||||||
<a className={"button"} href={"#"}>A link button</a>
|
|
||||||
<button type={"button"} className={"flat"}>A flat button</button>
|
|
||||||
<button type={"button"} className={"validation"}><FloppyDisk /> A validation button</button>
|
|
||||||
<button type={"button"} className={"cancel"}><XCircle /> A cancellation button</button>
|
|
||||||
<button type={"button"} className={"delete"}><TrashSimple /> A deletion button</button>
|
|
||||||
|
|
||||||
<h2>Forms</h2>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<label>
|
|
||||||
Text label <RequiredField/>
|
|
||||||
<input type={"text"} placeholder={"Normal demo text"} required={true}/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Textarea label <RequiredField/>
|
|
||||||
<textarea placeholder={"A normal textarea."} required={true}></textarea>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<PasswordInput>
|
|
||||||
Test password
|
|
||||||
</PasswordInput>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Disabled input
|
|
||||||
<input type={"text"} name={"disabled"} value={"fixed value"} disabled={true} />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<DatepickerInput value={datetime} onChange={setDatetime}>
|
|
||||||
Date test
|
|
||||||
</DatepickerInput>
|
|
||||||
|
|
||||||
<TimepickerInput value={datetime} onChange={setDatetime}>
|
|
||||||
Time test
|
|
||||||
</TimepickerInput>
|
|
||||||
|
|
||||||
<p>Currently selected datetime: <strong>{datetime ? datetime.toISOString() : "none"}</strong></p>
|
|
||||||
|
|
||||||
<Checkbox>Checkbox demo</Checkbox>
|
|
||||||
<ToggleSwitch>Toggle switch demo</ToggleSwitch>
|
|
||||||
<Radio name={"radio-test"}>Radio box test</Radio>
|
|
||||||
<Radio name={"radio-test"}>Radio box test</Radio>
|
|
||||||
|
|
||||||
<Tip>
|
|
||||||
A tip component, very useful in forms which require some explanations.
|
|
||||||
</Tip>
|
|
||||||
|
|
||||||
<Select options={{
|
|
||||||
"a": "AAAAAA",
|
|
||||||
"b": "BBBBBB",
|
|
||||||
"c": "CCCCCC",
|
|
||||||
"d": "DDDDDD",
|
|
||||||
"e": "EEEEEE",
|
|
||||||
"f": "FFFFFF",
|
|
||||||
"g": "GGGGGG",
|
|
||||||
"h": "HHHHHH",
|
|
||||||
"i": "IIIIII",
|
|
||||||
"j": "JJJJJJ",
|
|
||||||
"k": "KKKKKK",
|
|
||||||
"l": "LLLLLL",
|
|
||||||
"m": "MMMMMM",
|
|
||||||
"n": "NNNNNN",
|
|
||||||
}} value={selected} onChange={setSelected} selectibleMaxCount={3} placeholder={"Simple test"}>
|
|
||||||
Simple select test
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select value={anotherSelected} onChange={setAnotherSelected} options={{
|
|
||||||
"test": "test",
|
|
||||||
"foo": "foo",
|
|
||||||
"bar": "bar",
|
|
||||||
}} required={true}>
|
|
||||||
At least one selected element
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Buttons placement={"center"}>
|
|
||||||
<button>Validation test</button>
|
|
||||||
</Buttons>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>HTML</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<a href={"#"}>Link test</a>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<strong>Lorem ipsum</strong> dolor <code>sit amet</code>, <em>consectetur</em> adipiscing elit <a
|
|
||||||
href={"https://aleph.land"} target={"_blank"}>aleph</a>. Donec accumsan
|
|
||||||
pulvinar felis, vitae eleifend augue lacinia tempus. Integer nec iaculis ante. Duis a quam urna. Nullam
|
|
||||||
tincidunt rutrum felis, a efficitur enim facilisis sit amet. Quisque dictum semper sagittis. Maecenas in orci
|
|
||||||
hendrerit, tempor nunc non, tempus mi. Praesent blandit varius rutrum. Nullam quis mauris eros. Vestibulum
|
|
||||||
commodo libero sed pellentesque pharetra. Donec eget fringilla ante. Aliquam id leo massa. Duis dictum nunc ut
|
|
||||||
dolor iaculis malesuada. Nulla elementum justo a sem eleifend finibus. Phasellus bibendum elit nibh, at tempor
|
|
||||||
odio efficitur id.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Steps</h2>
|
|
||||||
|
|
||||||
<div className={"steps-counter"}>
|
|
||||||
<h3 className={"step"}>Step one</h3>
|
|
||||||
<h3 className={"step"}>Step two</h3>
|
|
||||||
<h3 className={"step"}>Step three</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Lists</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>One</li>
|
|
||||||
<li>Two</li>
|
|
||||||
<li>Three</li>
|
|
||||||
<li>Four</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>One</li>
|
|
||||||
<li>Two</li>
|
|
||||||
<li>Three</li>
|
|
||||||
<li>Four</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h2>Tables</h2>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Column 1</th>
|
|
||||||
<th>Column 2</th>
|
|
||||||
<th colSpan={2}>Column 3</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>A</th>
|
|
||||||
<th>B</th>
|
|
||||||
<th>C</th>
|
|
||||||
<th>D</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Lorem</td>
|
|
||||||
<td>Ipsum</td>
|
|
||||||
<td>Dolor</td>
|
|
||||||
<td>Amet</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>Foo</td>
|
|
||||||
<td>Bar</td>
|
|
||||||
<td>Baz</td>
|
|
||||||
<td>John</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>Alice</td>
|
|
||||||
<td>Bob</td>
|
|
||||||
<td></td>
|
|
||||||
<td>Jack</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h2>Cards</h2>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<h3>Card title</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Donec lobortis quam sapien, et efficitur dolor laoreet ut. Ut pretium, lacus at bibendum rutrum, nibh nibh
|
|
||||||
scelerisque nisi, nec semper dolor turpis sed tortor. Nulla massa sapien, accumsan id vestibulum et, elementum
|
|
||||||
eget metus. Morbi quis bibendum purus. Nunc at fermentum tortor. Quisque viverra diam in sem auctor blandit.
|
|
||||||
Vestibulum dignissim bibendum nunc, non tristique quam sollicitudin eu. Ut feugiat lectus tellus, tempus
|
|
||||||
viverra sapien aliquet vel. Morbi ac est mauris. Praesent facilisis ut tellus at cursus. Aenean placerat nulla
|
|
||||||
non mi vulputate hendrerit. Praesent fermentum dui eu gravida pharetra. Quisque rhoncus, magna non congue
|
|
||||||
egestas, leo lorem malesuada felis, eu imperdiet orci magna ultrices est.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Buttons>
|
|
||||||
<button type={"button"}>Button position ?</button>
|
|
||||||
<button type={"button"}>Button position ?</button>
|
|
||||||
</Buttons>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<p>Another small card</p>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h2>Popovers</h2>
|
|
||||||
|
|
||||||
<Float mode={"hover"} content={"Do you see me?"}>
|
|
||||||
<button type={"button"}>Hover me!</button>
|
|
||||||
</Float>
|
|
||||||
|
|
||||||
<Float mode={"focus"} content={<>I am <strong>focused</strong></>}>
|
|
||||||
<button>Focus me!</button>
|
|
||||||
</Float>
|
|
||||||
|
|
||||||
<Float mode={"click"} content={(
|
|
||||||
<div>
|
|
||||||
You can add complex (clickable) content in me.
|
|
||||||
<button type={"button"}>OK</button>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
<button>Click me!</button>
|
|
||||||
</Float>
|
|
||||||
|
|
||||||
<Float mode={"always"} content={"I am always shown."} floatingOptions={{placement: "top"}}>
|
|
||||||
<button>Why always me?</button>
|
|
||||||
</Float>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Float mode={"managed"} content={(show, hide) => (<button onClick={hide}>I can hide the popover!</button>)}>
|
|
||||||
{(show, hide) => (
|
|
||||||
<button type={"button"} onClick={show}>Customized behavior</button>
|
|
||||||
)}
|
|
||||||
</Float>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Tooltips</h2>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
This is a very simple <Tooltip content={"I am a beautiful simple tooltip."}><a>tooltip</a></Tooltip>.
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h2>Loaders</h2>
|
|
||||||
|
|
||||||
<h3>Simple loaders</h3>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<SpinningLoader inline={true} />
|
|
||||||
|
|
||||||
<ListLoader/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h3>Generic loader</h3>
|
|
||||||
<GenericLoader>
|
|
||||||
<Card>
|
|
||||||
Sample content.
|
|
||||||
</Card>
|
|
||||||
</GenericLoader>
|
|
||||||
|
|
||||||
<h2>Menus</h2>
|
|
||||||
|
|
||||||
<SubmenuFloat submenu={
|
|
||||||
<Submenu>
|
|
||||||
<SubmenuItem>Test 1</SubmenuItem>
|
|
||||||
<SubmenuItem>Test 2</SubmenuItem>
|
|
||||||
<SubmenuItemSubmenu submenu={
|
|
||||||
<Submenu>
|
|
||||||
<SubmenuItem>Test A</SubmenuItem>
|
|
||||||
<SubmenuItem><AirTrafficControl /> Test B</SubmenuItem>
|
|
||||||
</Submenu>
|
|
||||||
}>
|
|
||||||
<Basket /> Submenu
|
|
||||||
</SubmenuItemSubmenu>
|
|
||||||
</Submenu>
|
|
||||||
} floatingOptions={{placement: "right-start"}}>
|
|
||||||
<button>Submenu on a button</button>
|
|
||||||
</SubmenuFloat>
|
|
||||||
|
|
||||||
<h2>App selectors</h2>
|
|
||||||
|
|
||||||
<AppsMenu>
|
|
||||||
<AppLink to={"/"}>
|
|
||||||
<House />
|
|
||||||
Home
|
|
||||||
</AppLink>
|
|
||||||
<AppLink to={"/test"}>
|
|
||||||
<AirTrafficControl />
|
|
||||||
Test link
|
|
||||||
</AppLink>
|
|
||||||
<AppItem>
|
|
||||||
<Basket />
|
|
||||||
Test 3
|
|
||||||
</AppItem>
|
|
||||||
</AppsMenu>
|
|
||||||
|
|
||||||
<Outlet />
|
|
||||||
|
|
||||||
<h2>App steps</h2>
|
|
||||||
|
|
||||||
<h3>Basic</h3>
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step stepKey={"abc"}>
|
|
||||||
<Card>
|
|
||||||
<h4>First step</h4>
|
|
||||||
|
|
||||||
ABC STEP
|
|
||||||
</Card>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step stepKey={"def"} stepTitle={"Title"}>
|
|
||||||
<Card>
|
|
||||||
<h4>Big content</h4>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam id dignissim ligula, ut tempus sem. Sed
|
|
||||||
faucibus tincidunt ante vel iaculis. Duis hendrerit, orci eu gravida interdum, nulla lacus congue augue,
|
|
||||||
nec efficitur diam dui sollicitudin eros. Donec lacus lectus, aliquam nec feugiat non, gravida ac est.
|
|
||||||
Suspendisse feugiat justo quis dui vehicula, sed auctor est mollis. Sed hendrerit nisi non lectus lacinia,
|
|
||||||
id posuere dolor dignissim. Quisque commodo mi sit amet quam tincidunt auctor. Ut sit amet scelerisque
|
|
||||||
sem. Nulla rhoncus, orci vitae cursus ullamcorper, ex odio rhoncus enim, et blandit elit libero quis est.
|
|
||||||
Suspendisse lectus nunc, gravida sit amet vulputate eget, porta ac odio.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Mauris egestas bibendum facilisis. Maecenas accumsan lorem arcu, ut faucibus dui euismod et. Nulla et
|
|
||||||
dignissim est, interdum luctus diam. Aliquam condimentum ex augue, id porttitor enim vestibulum quis.
|
|
||||||
Vivamus sed convallis leo. Duis finibus, ipsum sed condimentum viverra, ipsum sapien congue nunc, sed
|
|
||||||
fermentum metus ante quis ligula. Fusce eleifend ante in leo molestie, at suscipit metus cursus. Class
|
|
||||||
aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed laoreet justo ac
|
|
||||||
lacus porta, sed ultricies est mattis. Integer finibus purus metus, quis posuere risus suscipit suscipit.
|
|
||||||
Nulla facilisi. Duis tincidunt vitae enim eu sagittis.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Donec rutrum tellus vitae vehicula tempor. Sed porta tempus leo, vel aliquam risus scelerisque nec.
|
|
||||||
Pellentesque diam nibh, ultrices in rhoncus ut, rhoncus et ligula. Duis pellentesque diam purus, ut
|
|
||||||
scelerisque turpis condimentum sit amet. Sed sit amet efficitur tortor, vitae aliquet quam. Nullam
|
|
||||||
placerat dui eu sapien condimentum, placerat convallis sapien imperdiet. Suspendisse vitae laoreet ex.
|
|
||||||
Etiam quis rhoncus ante. Ut egestas eget ipsum ultrices tempus. Vivamus non odio non nisl aliquet rhoncus.
|
|
||||||
Ut et nisl placerat, interdum turpis a, condimentum mauris. In laoreet lobortis justo. Maecenas vehicula
|
|
||||||
magna non libero posuere rutrum. Praesent eget lectus feugiat dui pellentesque vehicula a sed felis.
|
|
||||||
Curabitur nunc orci, vehicula non gravida sed, suscipit sed diam. Nam semper, dui eu volutpat vulputate,
|
|
||||||
metus mauris congue lectus, ultrices sollicitudin eros felis ac erat.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Proin et rhoncus purus. Etiam nulla libero, dictum sed quam lacinia, consequat euismod ipsum. Donec quis
|
|
||||||
tristique metus. Cras vitae pretium massa. Etiam laoreet, eros in rhoncus ultrices, nibh nibh bibendum
|
|
||||||
diam, nec ultricies mi ante non ex. Pellentesque eget mattis dolor, eget pulvinar lorem. Nullam ante
|
|
||||||
dolor, ultricies et malesuada in, efficitur sit amet diam. Duis ligula augue, vestibulum sit amet ligula
|
|
||||||
ac, vestibulum tincidunt eros. Nullam commodo euismod vulputate. Morbi varius accumsan diam eu
|
|
||||||
pellentesque. Nunc vehicula pretium risus dapibus cursus. Mauris sit amet est at ipsum scelerisque
|
|
||||||
lobortis. Aenean eget quam sit amet arcu mattis interdum in at neque.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Mauris efficitur, enim pellentesque maximus faucibus, nibh enim gravida urna, quis dignissim turpis velit
|
|
||||||
quis nibh. Mauris eget vestibulum tellus. Duis mollis, ante in egestas lacinia, felis massa rutrum ante,
|
|
||||||
vel placerat lectus neque in purus. Fusce sodales nunc vel ligula mollis tincidunt. Sed ac viverra ligula.
|
|
||||||
Vestibulum ut velit sit amet ipsum cursus posuere in nec lacus. Praesent vel odio pellentesque,
|
|
||||||
ullamcorper metus in, accumsan dolor. Donec vel mi ultrices, interdum arcu vel, pellentesque nunc.
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step stepKey={"ghi"}>
|
|
||||||
<Card>
|
|
||||||
<h4>Third step</h4>
|
|
||||||
|
|
||||||
GHI STEP
|
|
||||||
</Card>
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
<h2>Pagination</h2>
|
|
||||||
|
|
||||||
<h3>Normal pagination</h3>
|
|
||||||
|
|
||||||
<Paginate onChange={setPage} count={72} page={page}>
|
|
||||||
<Card>Page {page}</Card>
|
|
||||||
</Paginate>
|
|
||||||
|
|
||||||
<h3>Auto pagination</h3>
|
|
||||||
|
|
||||||
<AutoPaginate count={55}>
|
|
||||||
{(page) => (
|
|
||||||
<Card>Page {page}</Card>
|
|
||||||
)}
|
|
||||||
</AutoPaginate>
|
|
||||||
|
|
||||||
<h3>Async pagination</h3>
|
|
||||||
|
|
||||||
<AsyncPaginate count={async () => { return 72; }} getData={async () => (["a", Math.random(), "c"])}>
|
|
||||||
{(data) => (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
data.map((value, index) => (
|
|
||||||
<div key={index}>{value}</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AsyncPaginate>
|
|
||||||
|
|
||||||
<h2>Curtains & co</h2>
|
|
||||||
|
|
||||||
<h3>Curtains</h3>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<button onClick={() => {
|
|
||||||
curtains.open(<DemoCurtain />);
|
|
||||||
}}>Open a curtain</button>
|
|
||||||
|
|
||||||
<button onClick={easyCurtain}>Easy with callable curtain</button>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h3>Subapps</h3>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<button onClick={() => {
|
|
||||||
subapps.open(
|
|
||||||
<Subapp title={"Title test"}>
|
|
||||||
<p>A test content.</p>
|
|
||||||
<Buttons placement={"center"}>
|
|
||||||
<Float mode={"click"} content={"Test content."}>
|
|
||||||
<button>A button with floating content</button>
|
|
||||||
</Float>
|
|
||||||
</Buttons>
|
|
||||||
</Subapp>
|
|
||||||
)
|
|
||||||
}}>Open a subapp
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onClick={() => { subapps.open(<DemoSubapp />) }}>
|
|
||||||
Complex subapp with component
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onClick={easySubapp}>
|
|
||||||
Easy with callable subapp
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onClick={() => {
|
|
||||||
subapps.open(
|
|
||||||
<Subapp title={"Very long subapp"}>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
<Card><p>A test content.</p></Card>
|
|
||||||
</Subapp>
|
|
||||||
)
|
|
||||||
}}>A long subapp
|
|
||||||
</button>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h3>Modals</h3>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<button onClick={() => easyModal(ModalType.INFO)}>Open an info modal</button>
|
|
||||||
<button className={"warning"} onClick={() => easyModal(ModalType.WARNING)}>Open a warning modal</button>
|
|
||||||
<button className={"flat"} onClick={() => easyModal()}>Open a simple modal</button>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h2>Notifications</h2>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<button onClick={() => { notify(<Notification type={NotificationType.INFO}>Test notification</Notification>); }}>Information notification</button>
|
|
||||||
<button className={"success"} onClick={() => { notify(<Notification type={NotificationType.SUCCESS}>Test notification</Notification>); }}>Success notification</button>
|
|
||||||
<button className={"warning"} onClick={() => { notify(<Notification type={NotificationType.WARNING}>Test notification</Notification>); }}>Warning notification</button>
|
|
||||||
<button className={"error"} onClick={() => { notify(<Notification type={NotificationType.ERROR}>Test notification</Notification>); }}>Error notification</button>
|
|
||||||
<button className={"flat"} onClick={() => { notify(<Notification>Test notification</Notification>); }}>Generic notification</button>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h2>Async</h2>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
Async data test:
|
|
||||||
<Await async={asyncData} fallback={<SpinningLoader />}>
|
|
||||||
{(data) => (
|
|
||||||
<p>Data: {data}</p>
|
|
||||||
)}
|
|
||||||
</Await>
|
|
||||||
<button type={"button"} onClick={() => setAsyncChange(asyncChange + 1)}>Change async deps</button>
|
|
||||||
<button type={"button"} onClick={() => setAnotherChange(anotherChange + 1)}>Change something else</button>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h2>Error boundaries</h2>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<button type={"button"} className={"error"}
|
|
||||||
onClick={() => setFailingComponentsCount(failingComponentsCount + 1)}>Do something dangerous</button>
|
|
||||||
<NotifyErrorsBoundary fallback={<DemoResetComponent />}>
|
|
||||||
{
|
|
||||||
[...Array(failingComponentsCount)].map(() => <DemoFailingComponent />)
|
|
||||||
}
|
|
||||||
</NotifyErrorsBoundary>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<h2>
|
|
||||||
Global states
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<label>
|
|
||||||
Kernel context data
|
|
||||||
<input type={"text"} name={"kernel-context-data"}
|
|
||||||
value={kernelContext}
|
|
||||||
onChange={(event) => setKernelContext(event.currentTarget.value)} />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Buttons placement={"center"}>
|
|
||||||
<button type={"button"} onClick={easySubapp}>Open demo subapp to see if it works</button>
|
|
||||||
</Buttons>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</Application>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {useCurtain} from "../src/Components/Curtains/CurtainInstance";
|
|
||||||
import {X} from "@phosphor-icons/react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Demo curtain component.
|
|
||||||
*/
|
|
||||||
export function DemoCurtain()
|
|
||||||
{
|
|
||||||
const {close} = useCurtain();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className={"close"} onClick={close}><X/> Close the curtain</button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {useErrorBoundary} from "react-error-boundary";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple demo failing component.
|
|
||||||
*/
|
|
||||||
export function DemoFailingComponent()
|
|
||||||
{
|
|
||||||
throw new Error("Proudly thrown error.");
|
|
||||||
return (<p>I will never be shown...</p>);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple demo reset component.
|
|
||||||
*/
|
|
||||||
export function DemoResetComponent()
|
|
||||||
{
|
|
||||||
// Get error boundary.
|
|
||||||
const errorBoundary = useErrorBoundary();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button type={"button"} onClick={() => {
|
|
||||||
errorBoundary.resetBoundary();
|
|
||||||
}}>Reset to try again!</button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import {createKernelContext} from "../src/KernelGlobalContext";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create kernel context.
|
|
||||||
*/
|
|
||||||
export const KernelContext = createKernelContext("");
|
|
|
@ -1,19 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Modal, useModal} from "../src/Components/Modals/Modals";
|
|
||||||
import {ModalType} from "../src/Components/Modals/ModalsTypes";
|
|
||||||
import {Buttons} from "../src/Components/Buttons/Buttons";
|
|
||||||
|
|
||||||
export function DemoModal({type}: { type: ModalType; })
|
|
||||||
{
|
|
||||||
const {close} = useModal();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal type={type} title={"Modal title"}>
|
|
||||||
Modal test content
|
|
||||||
|
|
||||||
<Buttons>
|
|
||||||
<button onClick={close}>OK</button>
|
|
||||||
</Buttons>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Subapp, useSubapp} from "../src/Components/Subapps/Subapps";
|
|
||||||
import {Card} from "../src/Components/Card";
|
|
||||||
import {useKernelContext} from "../src/KernelGlobalContext";
|
|
||||||
import {KernelContext} from "./DemoKernelContext";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A demo Subapp component.
|
|
||||||
*/
|
|
||||||
export function DemoSubapp()
|
|
||||||
{
|
|
||||||
// Get subapp close function.
|
|
||||||
const {uuid, close} = useSubapp();
|
|
||||||
|
|
||||||
// Get kernel context data.
|
|
||||||
const [kernelContext] = useKernelContext(KernelContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Subapp title={"My complex subapp"}>
|
|
||||||
<Card>
|
|
||||||
<p>This is a complex subapp.</p>
|
|
||||||
|
|
||||||
<p>UUID : <code>{uuid}</code></p>
|
|
||||||
|
|
||||||
{kernelContext && <p><strong>Kernel context data</strong>: <code>{kernelContext}</code></p>}
|
|
||||||
|
|
||||||
<button onClick={close}>Close the subapp</button>
|
|
||||||
</Card>
|
|
||||||
</Subapp>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Card} from "../src/Components/Card";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigation test component.
|
|
||||||
*/
|
|
||||||
export function NavTest()
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
This is a navigation test.
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import "../index";
|
|
||||||
import React from "react";
|
|
||||||
import {createRoot} from "react-dom/client";
|
|
||||||
import {DemoApp} from "./DemoApp";
|
|
||||||
import {createBrowserRouter} from "react-router-dom";
|
|
||||||
import {Kernel} from "../src/Application/Kernel";
|
|
||||||
import {NavTest} from "./NavTest";
|
|
||||||
import {Avocado} from "@phosphor-icons/react";
|
|
||||||
import {ApplicationError} from "../src/Application/ApplicationError";
|
|
||||||
|
|
||||||
// Router initialization.
|
|
||||||
const router = createBrowserRouter([
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
element: <DemoApp />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "test",
|
|
||||||
element: <NavTest />,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
errorElement: <ApplicationError />,
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const demoApp = document.getElementById("demo-app");
|
|
||||||
|
|
||||||
const root = createRoot(demoApp);
|
|
||||||
|
|
||||||
root.render(<Kernel router={router} footer={
|
|
||||||
<footer>
|
|
||||||
<Avocado weight={"duotone"} size={32} />
|
|
||||||
<div>Kernel</div>
|
|
||||||
</footer>
|
|
||||||
} />);
|
|
||||||
});
|
|
26
eslint.config.js
Normal file
26
eslint.config.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||||
|
import storybook from "eslint-plugin-storybook";
|
||||||
|
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], storybook.configs["flat/recommended"]);
|
|
@ -1,9 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>UIKernel - Demo</title>
|
|
||||||
<script type="module" src="demo/demo.tsx"></script>
|
|
||||||
</head>
|
|
||||||
<body id="demo-app">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
53
index.ts
53
index.ts
|
@ -1,53 +0,0 @@
|
||||||
import "./src/styles/main.less";
|
|
||||||
|
|
||||||
export * from "./src/Application/Application";
|
|
||||||
export * from "./src/Application/ApplicationError";
|
|
||||||
export * from "./src/Application/Kernel";
|
|
||||||
|
|
||||||
export * from "./src/Components/Box";
|
|
||||||
export * from "./src/Components/Card";
|
|
||||||
export * from "./src/Components/Buttons/Buttons";
|
|
||||||
export * from "./src/Components/Curtains/Curtains";
|
|
||||||
export {useCurtain} from "./src/Components/Curtains/CurtainInstance";
|
|
||||||
export type {CurtainContextState} from "./src/Components/Curtains/CurtainInstance";
|
|
||||||
export * from "./src/Components/Dates/Calendar";
|
|
||||||
export * from "./src/Components/Dates/Datepicker";
|
|
||||||
export * from "./src/Components/Errors/NotifyErrorsBoundary";
|
|
||||||
export * from "./src/Components/Floating/Float";
|
|
||||||
export * from "./src/Components/Floating/Tooltip";
|
|
||||||
export * from "./src/Components/Forms/Checkbox";
|
|
||||||
export * from "./src/Components/Forms/CustomValidationRule";
|
|
||||||
export * from "./src/Components/Forms/DatepickerInput";
|
|
||||||
export * from "./src/Components/Forms/PasswordInput";
|
|
||||||
export * from "./src/Components/Forms/Radio";
|
|
||||||
export * from "./src/Components/Forms/RequiredField";
|
|
||||||
export * from "./src/Components/Forms/TimepickerInput";
|
|
||||||
export * from "./src/Components/Forms/ToggleSwitch";
|
|
||||||
export * from "./src/Components/Loaders/GenericLoader";
|
|
||||||
export * from "./src/Components/Loaders/ListLoader";
|
|
||||||
export * from "./src/Components/Loaders/SpinningLoader";
|
|
||||||
export * from "./src/Components/Menus/AppsMenu";
|
|
||||||
export * from "./src/Components/Menus/MainMenu";
|
|
||||||
export * from "./src/Components/Menus/MainMenuItem";
|
|
||||||
export * from "./src/Components/Menus/Submenu";
|
|
||||||
export * from "./src/Components/Menus/SubmenuFloat";
|
|
||||||
export * from "./src/Components/Menus/SubmenuItem";
|
|
||||||
export * from "./src/Components/Modals/Modals";
|
|
||||||
export * from "./src/Components/Modals/ModalsTypes";
|
|
||||||
export * from "./src/Components/Notifications/Notification";
|
|
||||||
export * from "./src/Components/Notifications/Notifications";
|
|
||||||
export * from "./src/Components/Pagination/Paginate";
|
|
||||||
export * from "./src/Components/Pagination/Pagination";
|
|
||||||
export * from "./src/Components/Select/OptionsSuggestions";
|
|
||||||
export * from "./src/Components/Select/Select";
|
|
||||||
export * from "./src/Components/Select/SimpleSuggestions";
|
|
||||||
export * from "./src/Components/Select/Suggestible";
|
|
||||||
export * from "./src/Components/Steps/Steps";
|
|
||||||
export * from "./src/Components/Steps/StepsContext";
|
|
||||||
export * from "./src/Components/Subapps/Subapps";
|
|
||||||
export * from "./src/Components/Tips/Tip";
|
|
||||||
|
|
||||||
export * from "./src/Async";
|
|
||||||
export * from "./src/GlobalState";
|
|
||||||
export * from "./src/KernelGlobalContext";
|
|
||||||
export * from "./src/Utils";
|
|
57
package.json
57
package.json
|
@ -4,47 +4,46 @@
|
||||||
"description": "Kernel UI Core.",
|
"description": "Kernel UI Core.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build"
|
"build": "tsc && vite build",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"storybook:build": "storybook build"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"source": "index.ts",
|
"source": "index.ts",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
"lib/**/*"
|
"lib/**/*",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"@kernelui:registry": "https://code.zeptotech.net/api/packages/UIKernel/npm/"
|
"@kernelui:registry": "https://code.zeptotech.net/api/packages/UIKernel/npm/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/react": "^0.26.17",
|
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.0.21",
|
|
||||||
"@fontsource-variable/manrope": "^5.0.20",
|
|
||||||
"@fontsource-variable/source-serif-4": "^5.0.19",
|
|
||||||
"react-error-boundary": "^4.0.13",
|
|
||||||
"react-merge-refs": "^2.1.1",
|
|
||||||
"uuid": "^10.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@types/node": "^22.7.4",
|
"@storybook/addon-docs": "^9.1.5",
|
||||||
"@types/react": "^18.3.12",
|
"@storybook/react-vite": "^9.1.5",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/node": "^24.3.1",
|
||||||
"@types/uuid": "^10",
|
"@types/react": "^19.1.12",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@types/react-dom": "^19.1.9",
|
||||||
"less": "^4.2.0",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"react": "^18.3.1",
|
"eslint": "^9.35.0",
|
||||||
"react-dom": "^18.3.1",
|
"eslint-plugin-storybook": "^9.1.5",
|
||||||
"react-router-dom": "^7.0.1",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.6.2",
|
"react": "^19.1.1",
|
||||||
"vite": "^6.0.1",
|
"react-dom": "^19.1.1",
|
||||||
"vite-plugin-dts": "^4.3.0"
|
"react-router-dom": "^7.8.2",
|
||||||
|
"storybook": "^9.1.5",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vite": "^7.1.5",
|
||||||
|
"vite-plugin-dts": "^4.5.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"react": "^18.3.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.0.1"
|
"react-router-dom": "^7.8.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.5.3"
|
"packageManager": "yarn@4.9.4"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {ApplicationError, ApplicationErrorBoundary} from "./ApplicationError";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main Kernel UI application.
|
|
||||||
*/
|
|
||||||
export function Application({errorElement, children}: React.PropsWithChildren<{
|
|
||||||
errorElement?: React.ReactNode;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<ApplicationErrorBoundary errorElement={errorElement ?? <ApplicationError />}>
|
|
||||||
<main className={"app"}>
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</ApplicationErrorBoundary>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
import React, {useContext, useState} from "react";
|
|
||||||
import {ArrowsClockwise, Bug, BugDroid} from "@phosphor-icons/react";
|
|
||||||
import {useRouteError} from "react-router-dom";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Application error context.
|
|
||||||
*/
|
|
||||||
const ApplicationErrorContext = React.createContext<Error>(undefined);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current error from context or router.
|
|
||||||
*/
|
|
||||||
export function useApplicationError(): Error
|
|
||||||
{
|
|
||||||
// Get error from context or router.
|
|
||||||
const error = useContext(ApplicationErrorContext);
|
|
||||||
const routeError = useRouteError() as Error;
|
|
||||||
return error ?? routeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Application error component.
|
|
||||||
*/
|
|
||||||
export function ApplicationError({children}: {
|
|
||||||
children?: (error: Error) => React.ReactElement;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Get error from context.
|
|
||||||
const error = useApplicationError();
|
|
||||||
|
|
||||||
// Show details state.
|
|
||||||
const [showDetails, setShowDetails] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className={"error"}>
|
|
||||||
{children ? children(error) : (
|
|
||||||
<>
|
|
||||||
<Bug size={64} weight={"duotone"} />
|
|
||||||
<h1>Error</h1>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h2>{error.name}</h2>
|
|
||||||
|
|
||||||
<p>An unexpected error happened and the application was forced to quit.</p>
|
|
||||||
|
|
||||||
<pre className={"error"}>{error.message}</pre>
|
|
||||||
|
|
||||||
<button onClick={() => window.location.reload()}><ArrowsClockwise size={20} /> Restart application</button>
|
|
||||||
|
|
||||||
<div className={"details"}>
|
|
||||||
<button onClick={() => setShowDetails(!showDetails)}><BugDroid size={18} /> {showDetails ? "Hide" : "Show"} details</button>
|
|
||||||
|
|
||||||
{ // Show details if required.
|
|
||||||
showDetails && (
|
|
||||||
<pre>
|
|
||||||
{error.stack}
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error boundary component for the application.
|
|
||||||
*/
|
|
||||||
export class ApplicationErrorBoundary extends React.Component<React.PropsWithChildren<{
|
|
||||||
errorElement: React.ReactNode;
|
|
||||||
}>, {
|
|
||||||
error: Error;
|
|
||||||
}>
|
|
||||||
{
|
|
||||||
static getDerivedStateFromError(error: Error)
|
|
||||||
{
|
|
||||||
return { error: error };
|
|
||||||
}
|
|
||||||
|
|
||||||
render()
|
|
||||||
{
|
|
||||||
if (this.state?.error)
|
|
||||||
// An error happened, showing the application error content.
|
|
||||||
return (
|
|
||||||
<ApplicationErrorContext.Provider value={this.state?.error}>
|
|
||||||
{this.props.errorElement}
|
|
||||||
</ApplicationErrorContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { IconContext } from "@phosphor-icons/react";
|
|
||||||
import React from "react";
|
|
||||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
|
||||||
import {CurtainsProvider} from "../Components/Curtains/Curtains";
|
|
||||||
import {NotificationsProvider} from "../Components/Notifications/Notifications";
|
|
||||||
import {KernelGlobalContextProvider} from "../KernelGlobalContext";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main Kernel UI app component which initializes everything.
|
|
||||||
*/
|
|
||||||
export function Kernel({header, footer, router}: {
|
|
||||||
header?: React.ReactNode;
|
|
||||||
footer?: React.ReactNode;
|
|
||||||
router: ReturnType<typeof createBrowserRouter>;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<KernelGlobalContextProvider>
|
|
||||||
<IconContext.Provider value={{
|
|
||||||
size: "1em",
|
|
||||||
weight: "bold",
|
|
||||||
}}>
|
|
||||||
<NotificationsProvider>
|
|
||||||
<CurtainsProvider>
|
|
||||||
{header}
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
{footer}
|
|
||||||
</CurtainsProvider>
|
|
||||||
</NotificationsProvider>
|
|
||||||
</IconContext.Provider>
|
|
||||||
</KernelGlobalContextProvider>
|
|
||||||
);
|
|
||||||
}
|
|
158
src/Async.tsx
158
src/Async.tsx
|
@ -1,158 +0,0 @@
|
||||||
import React, {useEffect, useMemo, useState} from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A type that can be returned by a promise or as is.
|
|
||||||
*/
|
|
||||||
export type Promisable<T> = T|Promise<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous data state.
|
|
||||||
*/
|
|
||||||
interface AsyncState<T>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Determine if we are waiting for the promise result or not.
|
|
||||||
*/
|
|
||||||
pending: boolean;
|
|
||||||
/**
|
|
||||||
* The promise which is retrieved (or has retrieved) data.
|
|
||||||
*/
|
|
||||||
promise: Promisable<T>;
|
|
||||||
/**
|
|
||||||
* Error thrown by the promise.
|
|
||||||
*/
|
|
||||||
error: Error;
|
|
||||||
/**
|
|
||||||
* The promise result.
|
|
||||||
*/
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A promise production function.
|
|
||||||
*/
|
|
||||||
export type PromiseFn<T> = () => Promise<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React hook for promise result retrieval.
|
|
||||||
* @param promise The promise or a function which produces a promise.
|
|
||||||
* @param deps When one of the `deps` change, it will wait for the promise again.
|
|
||||||
*/
|
|
||||||
export function useAsync<T>(promise: Promisable<T>|PromiseFn<T>, deps: any[] = []): [AsyncState<T>, React.Dispatch<T>]
|
|
||||||
{
|
|
||||||
// Get the actual promise from the function if there is one.
|
|
||||||
promise = useMemo(() => {
|
|
||||||
if ((promise as PromiseFn<T>)?.call)
|
|
||||||
return (promise as PromiseFn<T>)();
|
|
||||||
else if (promise instanceof Promise)
|
|
||||||
return Promise.race([promise as Promise<T>]);
|
|
||||||
else
|
|
||||||
return promise;
|
|
||||||
}, deps);
|
|
||||||
|
|
||||||
// The async state.
|
|
||||||
const [state, setState] = useState<AsyncState<T>>({
|
|
||||||
pending: promise instanceof Promise,
|
|
||||||
promise: promise as Promisable<T>,
|
|
||||||
error: undefined,
|
|
||||||
data: promise instanceof Promise ? undefined : promise as T,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Partial update of an async state.
|
|
||||||
* @param stateUpdate A partial update object.
|
|
||||||
*/
|
|
||||||
const updateState = (stateUpdate: Partial<AsyncState<T>>) => {
|
|
||||||
// Copy the original state and apply the partial state update.
|
|
||||||
setState(Object.assign({}, state, stateUpdate));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reconfigure the promise when any deps have changed.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!(promise instanceof Promise))
|
|
||||||
{ // If it's not a promise, there is nothing to wait for.
|
|
||||||
updateState({
|
|
||||||
pending: false,
|
|
||||||
promise: promise as Promisable<T>,
|
|
||||||
error: undefined,
|
|
||||||
data: promise as T,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.then((result) => {
|
|
||||||
// When there is a result, disable pending state and set retrieved data, without error.
|
|
||||||
updateState({
|
|
||||||
pending: false,
|
|
||||||
error: undefined,
|
|
||||||
data: result,
|
|
||||||
})
|
|
||||||
}).catch((error) => {
|
|
||||||
// An error happened, disable pending state, reset data, and set the error.
|
|
||||||
updateState({
|
|
||||||
pending: false,
|
|
||||||
error: error,
|
|
||||||
data: undefined,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// Promise is ready: reset the state to pending with the configured promise, without data and error.
|
|
||||||
updateState({
|
|
||||||
pending: true,
|
|
||||||
promise: promise,
|
|
||||||
error: undefined,
|
|
||||||
data: undefined,
|
|
||||||
});
|
|
||||||
}, deps);
|
|
||||||
|
|
||||||
// Return the current async state and a dispatch data function.
|
|
||||||
return [state, (data: T) => {
|
|
||||||
updateState({data: data});
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for the promise to be fulfilled to render the children.
|
|
||||||
* @param async The async state.
|
|
||||||
* @param children Renderer function of the children, takes promised data as argument.
|
|
||||||
* @param fallback Content shown when the promise is not fulfilled yet.
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function Await<T>({ async, children, fallback }: {
|
|
||||||
async: AsyncState<T>;
|
|
||||||
children: (async: T) => React.ReactElement;
|
|
||||||
fallback?: React.ReactElement;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Still waiting for the promised data, showing fallback content.
|
|
||||||
if (async.pending) return fallback ?? <></>;
|
|
||||||
// An error happened, throwing it.
|
|
||||||
if (async.error) throw async.error;
|
|
||||||
|
|
||||||
// Promise is fulfilled, rendering the children with result data.
|
|
||||||
return children(async.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Easy async component with fallback and fulfilled children render.
|
|
||||||
* @param promise The promise.
|
|
||||||
* @param children Renderer function of the children, takes promised data as argument.
|
|
||||||
* @param fallback Content shown when the promise is not fulfilled yet.
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function Async<T>({promise, fallback, children}: {
|
|
||||||
promise: Promisable<T>|PromiseFn<T>;
|
|
||||||
children: (async: T) => React.ReactElement;
|
|
||||||
fallback?: React.ReactElement;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Get async data from given promise.
|
|
||||||
const [async] = useAsync(promise, [promise]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
// Wait for promised data.
|
|
||||||
<Await async={async} fallback={fallback ?? undefined}>
|
|
||||||
{children}
|
|
||||||
</Await>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React, {PropsWithChildren} from "react";
|
|
||||||
import {classes} from "../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content box generic component.
|
|
||||||
*/
|
|
||||||
export function Box({children, className, ...props}: PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>): React.ReactElement
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<div className={classes("box", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
export function Buttons({placement, children}: React.PropsWithChildren<{
|
|
||||||
placement?: "right"|"left"|"center";
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
// Default placement: right.
|
|
||||||
placement = placement ?? "right";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes("buttons", placement)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React, {PropsWithChildren} from "react";
|
|
||||||
import {classes} from "../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content card component.
|
|
||||||
*/
|
|
||||||
export function Card({children, className, ...props}: PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>): React.ReactElement
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<div className={classes("card", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
import React, {useCallback, useContext, useMemo, useRef} from "react";
|
|
||||||
import {CurtainUuidType, useCurtains} from "./Curtains";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current curtain data and functions.
|
|
||||||
*/
|
|
||||||
export interface CurtainContextState
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Curtain UUID.
|
|
||||||
*/
|
|
||||||
uuid: CurtainUuidType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the curtain.
|
|
||||||
*/
|
|
||||||
close: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the curtain is closed (while transitioning out).
|
|
||||||
*/
|
|
||||||
closed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current curtain context.
|
|
||||||
*/
|
|
||||||
const CurtainContext = React.createContext<CurtainContextState>({
|
|
||||||
// Empty values.
|
|
||||||
uuid: "",
|
|
||||||
close: () => {},
|
|
||||||
closed: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to access current curtain data and functions.
|
|
||||||
*/
|
|
||||||
export function useCurtain(): CurtainContextState
|
|
||||||
{
|
|
||||||
return useContext(CurtainContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component of an opened curtain instance.
|
|
||||||
*/
|
|
||||||
export function CurtainInstance({uuid, children}: React.PropsWithChildren<{
|
|
||||||
/**
|
|
||||||
* Curtain UUID.
|
|
||||||
*/
|
|
||||||
uuid: string;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
// Get close curtain function.
|
|
||||||
const {close, isClosed} = useCurtains();
|
|
||||||
|
|
||||||
// Initialize close curtain function.
|
|
||||||
const closeCurtain = useRef<() => void>();
|
|
||||||
closeCurtain.current = useCallback(() => {
|
|
||||||
// Close the current curtain.
|
|
||||||
close(uuid);
|
|
||||||
}, [uuid, close]);
|
|
||||||
|
|
||||||
// Initialize context state from action functions.
|
|
||||||
const contextState = useMemo(() => ({
|
|
||||||
uuid: uuid,
|
|
||||||
close: () => closeCurtain.current(),
|
|
||||||
closed: isClosed(uuid),
|
|
||||||
}), [uuid, closeCurtain, isClosed]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CurtainContext.Provider value={contextState}>
|
|
||||||
<div className={classes("curtain", isClosed(uuid) ? "closed" : undefined)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CurtainContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
|
||||||
import {CurtainInstance} from "./CurtainInstance";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The function that checks if a curtain is closed (while transitioning out) or not.
|
|
||||||
*/
|
|
||||||
export type IsCurtainClosedFunction = (uuid: CurtainUuidType) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given curtain is closed or not.
|
|
||||||
* @param uuid UUID of the curtain to check.
|
|
||||||
*/
|
|
||||||
isClosed: IsCurtainClosedFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CurtainsContext = React.createContext<CurtainsContextState>({
|
|
||||||
// Empty functions.
|
|
||||||
open() { return ""; },
|
|
||||||
close() {},
|
|
||||||
isClosed() { return false; },
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to interact with curtains (open or close).
|
|
||||||
*/
|
|
||||||
export function useCurtains(): CurtainsContextState
|
|
||||||
{
|
|
||||||
return useContext(CurtainsContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A generic callable curtain element.
|
|
||||||
*/
|
|
||||||
export type CallableCurtainElement = (...args: any) => React.ReactNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A callable curtain function with typed parameters.
|
|
||||||
*/
|
|
||||||
export type CallableCurtain<F extends CallableCurtainElement> = (...args: Parameters<F>) => CurtainUuidType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callable curtain function generator.
|
|
||||||
* @param curtains The curtains context state.
|
|
||||||
* @param curtainElement The curtain element to open when called.
|
|
||||||
*/
|
|
||||||
export function callableCurtain<F extends CallableCurtainElement>(curtains: CurtainsContextState, curtainElement: React.ReactNode|F): CallableCurtain<F>
|
|
||||||
{
|
|
||||||
if (typeof curtainElement == "function")
|
|
||||||
// It's a callable curtain element, the callable curtain should be called with the same parameters.
|
|
||||||
return (...args: Parameters<F>) => curtains.open(curtainElement(...args));
|
|
||||||
else
|
|
||||||
// It's a simple element, just open it.
|
|
||||||
return () => curtains.open(curtainElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to create a simple curtain.
|
|
||||||
* @param curtainElement Content of the curtain to open.
|
|
||||||
*/
|
|
||||||
export function useCallableCurtain<F extends CallableCurtainElement>(curtainElement: React.ReactNode|F): CallableCurtain<F>
|
|
||||||
{
|
|
||||||
// Get curtains context state.
|
|
||||||
const curtains = useCurtains();
|
|
||||||
// Create and keep the curtain callable in memory.
|
|
||||||
return useCallback(callableCurtain(curtains, curtainElement), [curtains, curtainElement]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page curtains provider.
|
|
||||||
*/
|
|
||||||
export function CurtainsProvider({children}: React.PropsWithChildren<{}>)
|
|
||||||
{
|
|
||||||
// Curtains state.
|
|
||||||
const [curtains, setCurtains] = useState<Record<CurtainUuidType, React.ReactNode>>({});
|
|
||||||
// Keeping track of closed curtains that are still on (while transitioning out).
|
|
||||||
const [closedCurtains, setClosedCurtains] = useState<Record<CurtainUuidType, boolean>>({});
|
|
||||||
|
|
||||||
// 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 remove curtain function.
|
|
||||||
const remove = useRef<CloseCurtainFunction>();
|
|
||||||
remove.current = useCallback((uuid: CurtainUuidType) => {
|
|
||||||
// Copy the curtains list.
|
|
||||||
const newCurtains = {...curtains};
|
|
||||||
const newClosedCurtains = {...closedCurtains};
|
|
||||||
// Remove the given curtain from the list.
|
|
||||||
delete newCurtains[uuid];
|
|
||||||
delete newClosedCurtains[uuid];
|
|
||||||
// Set the new curtains list.
|
|
||||||
setCurtains(newCurtains);
|
|
||||||
setClosedCurtains(newClosedCurtains);
|
|
||||||
}, [curtains, setCurtains, closedCurtains, setClosedCurtains]);
|
|
||||||
|
|
||||||
// Initialize close curtain function with animation.
|
|
||||||
const close = useRef<CloseCurtainFunction>();
|
|
||||||
close.current = useCallback((uuid) => {
|
|
||||||
// Add the given curtain UUID to the list of closed curtains.
|
|
||||||
setClosedCurtains({
|
|
||||||
...closedCurtains,
|
|
||||||
[uuid]: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove the curtain 1s later.
|
|
||||||
window.setTimeout(() => {
|
|
||||||
// Remove the curtain.
|
|
||||||
remove.current(uuid);
|
|
||||||
}, 1000);
|
|
||||||
}, [remove, closedCurtains, setClosedCurtains]);
|
|
||||||
|
|
||||||
// Initialize isClosed curtain function.
|
|
||||||
const isClosed = useRef<IsCurtainClosedFunction>();
|
|
||||||
isClosed.current = useCallback((uuid) => (!!closedCurtains?.[uuid]), [closedCurtains]);
|
|
||||||
|
|
||||||
// Initialize context state from action functions.
|
|
||||||
const contextState = useMemo(() => ({
|
|
||||||
open: (content: React.ReactNode) => open.current(content),
|
|
||||||
close: (uuid: CurtainUuidType) => close.current(uuid),
|
|
||||||
isClosed: (uuid: CurtainUuidType) => isClosed.current(uuid),
|
|
||||||
}), [open, close, isClosed]);
|
|
||||||
|
|
||||||
// Show dimmed main content.
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
Object.keys(curtains).filter((curtainId) => !closedCurtains[curtainId]).length > 0
|
|
||||||
) // We should dim content if there is at least one open curtain.
|
|
||||||
{ // Only dim if it's not already dimmed.
|
|
||||||
if (!document.body.classList.contains("dimmed")) document.body.classList.add("dimmed");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
// We shouldn't dim content.
|
|
||||||
document.body.classList.remove("dimmed");
|
|
||||||
}, [curtains, closedCurtains]);
|
|
||||||
|
|
||||||
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} uuid={uuid}>
|
|
||||||
{curtainContent}
|
|
||||||
</CurtainInstance>
|
|
||||||
)), document.body);
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
import React, {useMemo} from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calendar component.
|
|
||||||
*/
|
|
||||||
export function Calendar({date, onDateSelected, locale, className, ...tableProps}: {
|
|
||||||
date: Date;
|
|
||||||
onDateSelected: (date: Date) => void;
|
|
||||||
locale?: string;
|
|
||||||
} & React.TableHTMLAttributes<HTMLTableElement>): React.ReactElement
|
|
||||||
{
|
|
||||||
locale = useMemo(() => (locale ?? "fr"), [locale]);
|
|
||||||
|
|
||||||
const currentMonthHeader = useMemo(() => (
|
|
||||||
<tr>
|
|
||||||
{ // For each day of the week, showing its name.
|
|
||||||
[1, 2, 3, 4, 5, 6, 7].map(day => {
|
|
||||||
// Getting a date with the right day of the week.
|
|
||||||
const dayOfWeek = new Date(1970, 0, 4 + day);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<th key={day}>
|
|
||||||
{(new Intl.DateTimeFormat(locale, {weekday: "short"})).format(dayOfWeek)}
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</tr>
|
|
||||||
), [date]);
|
|
||||||
|
|
||||||
const currentMonthTable = useMemo(() => {
|
|
||||||
// Initialize weeks.
|
|
||||||
const weeksRows = [];
|
|
||||||
let currentWeek = [];
|
|
||||||
|
|
||||||
// Get start date of the calendar.
|
|
||||||
let currentDate = new Date(date);
|
|
||||||
currentDate.setDate(1); // First day of the month.
|
|
||||||
currentDate.setDate(currentDate.getDate() - (currentDate.getDay() - 1 + 7) % 7); // Searching the start of the first week of the current month.
|
|
||||||
|
|
||||||
// Get last day of the calendar.
|
|
||||||
const lastDate = new Date(date);
|
|
||||||
lastDate.setMonth(lastDate.getMonth() + 1, 0); // Get the last day of the month.
|
|
||||||
lastDate.setDate(lastDate.getDate() + (7 - lastDate.getDay())); // Searching the end of the last week of the current month.
|
|
||||||
|
|
||||||
|
|
||||||
while (currentDate.getTime() <= lastDate.getTime() || weeksRows.length < 6)
|
|
||||||
{ // While the current date is before or is the last day of the current view,
|
|
||||||
// adding the current day to the current week.
|
|
||||||
currentWeek.push(
|
|
||||||
<Day key={`${currentDate.getFullYear()}-${currentDate.getMonth()}-${currentDate.getDate()}`}
|
|
||||||
date={new Date(currentDate)}
|
|
||||||
faded={date.getMonth() != currentDate.getMonth()}
|
|
||||||
selected={date.getTime() == currentDate.getTime()}
|
|
||||||
onClick={onDateSelected} />
|
|
||||||
);
|
|
||||||
|
|
||||||
// We're on sunday, adding the current week and creating a new one.
|
|
||||||
if (currentDate.getDay() == 0)
|
|
||||||
{ // The current week is ended, adding it and creating a new one.
|
|
||||||
weeksRows.push(
|
|
||||||
<tr key={`${currentDate.getFullYear()}-${currentDate.getMonth()}-${currentDate.getDate()}`}>
|
|
||||||
{currentWeek}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
currentWeek = []; // Reset the current week to a new one.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the next day.
|
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return generated weeks rows.
|
|
||||||
return weeksRows;
|
|
||||||
}, [date, onDateSelected]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table className={classes("calendar", className)} {...tableProps}>
|
|
||||||
<thead>
|
|
||||||
{currentMonthHeader}
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{currentMonthTable}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calendar day component.
|
|
||||||
*/
|
|
||||||
function Day({date, onClick, faded, selected}: {
|
|
||||||
date: Date;
|
|
||||||
onClick?: (date: Date) => void;
|
|
||||||
faded: boolean;
|
|
||||||
selected: boolean;
|
|
||||||
}): React.ReactElement
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<td key={`${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`}>
|
|
||||||
<a className={`day${faded ? " faded" : ""}${selected ? " selected" : ""}`}
|
|
||||||
onClick={() => onClick?.(date)}>
|
|
||||||
{date.getDate()}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import React, {useCallback, useMemo} from "react";
|
|
||||||
import {CaretLeft, CaretRight} from "@phosphor-icons/react";
|
|
||||||
import {Tooltip} from "../Floating/Tooltip";
|
|
||||||
import {Calendar} from "./Calendar";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Datepicker component.
|
|
||||||
*/
|
|
||||||
export function Datepicker({date, onDateSelected, locale, className, ...divProps}: {
|
|
||||||
date: Date;
|
|
||||||
onDateSelected: (date: Date) => void;
|
|
||||||
locale?: string;
|
|
||||||
} & React.HTMLAttributes<HTMLDivElement>): React.ReactElement
|
|
||||||
{
|
|
||||||
locale = useMemo(() => (locale ?? "fr"), [locale]);
|
|
||||||
|
|
||||||
// Get previous month name.
|
|
||||||
const previousMonthName = useMemo(() => {
|
|
||||||
// Copy the current date and get back one month earlier.
|
|
||||||
const previousMonthDate = new Date(date);
|
|
||||||
previousMonthDate.setMonth(previousMonthDate.getMonth() - 1);
|
|
||||||
return (new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" })).format(previousMonthDate);
|
|
||||||
}, [date]);
|
|
||||||
// Get next month name.
|
|
||||||
const nextMonthName = useMemo(() => {
|
|
||||||
// Copy the current date and go to one month later.
|
|
||||||
const nextMonthDate = new Date(date);
|
|
||||||
nextMonthDate.setMonth(nextMonthDate.getMonth() + 1);
|
|
||||||
return (new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" })).format(nextMonthDate);
|
|
||||||
}, [date]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes("datepicker", className)} {...divProps}>
|
|
||||||
<div className={"year-month"}>
|
|
||||||
{(new Intl.DateTimeFormat(locale, {month: "long", year: "numeric"})).format(date)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Calendar date={date} onDateSelected={onDateSelected} />
|
|
||||||
|
|
||||||
<Tooltip content={previousMonthName}>
|
|
||||||
<button type={"button"} className={"previous-month"} onClick={
|
|
||||||
useCallback((event) => {
|
|
||||||
const newDate = new Date(date);
|
|
||||||
newDate.setMonth(newDate.getMonth() - 1);
|
|
||||||
onDateSelected(newDate);
|
|
||||||
}, [date, onDateSelected])
|
|
||||||
}>
|
|
||||||
<CaretLeft />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content={nextMonthName}>
|
|
||||||
<button type={"button"} className={"next-month"} onClick={
|
|
||||||
useCallback((event) => {
|
|
||||||
const newDate = new Date(date);
|
|
||||||
newDate.setMonth(newDate.getMonth() + 1);
|
|
||||||
onDateSelected(newDate);
|
|
||||||
}, [date, onDateSelected])
|
|
||||||
}>
|
|
||||||
<CaretRight />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import React, {useCallback, useMemo} from "react";
|
|
||||||
import {ErrorBoundary, ErrorBoundaryProps} from "react-error-boundary";
|
|
||||||
import {useNotify} from "../Notifications/Notifications";
|
|
||||||
import {Notification, NotificationType} from "../Notifications/Notification";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A React error boundary that show errors in notifications.
|
|
||||||
*/
|
|
||||||
export function NotifyErrorsBoundary({children, onError, fallback, fallbackRender, FallbackComponent, ...props}: React.PropsWithChildren<Partial<ErrorBoundaryProps>>)
|
|
||||||
{
|
|
||||||
// Get notification function.
|
|
||||||
const notify = useNotify();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error handling function.
|
|
||||||
*/
|
|
||||||
const handleError = useCallback((error: Error, info: React.ErrorInfo) => {
|
|
||||||
// Show a notification about the error.
|
|
||||||
notify(<Notification type={NotificationType.ERROR}>Unexpected error: {error.message}</Notification>);
|
|
||||||
// Then call defined onError, if there is one.
|
|
||||||
onError?.(error, info);
|
|
||||||
}, [onError]);
|
|
||||||
|
|
||||||
// Define default fallback component.
|
|
||||||
const defaultFallback = useMemo(() => <></>, []);
|
|
||||||
|
|
||||||
if (!fallback && !fallbackRender && !FallbackComponent)
|
|
||||||
// Set default fallback if nothing is set.
|
|
||||||
fallback = defaultFallback;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorBoundary onError={handleError} fallback={fallback} {...props}>
|
|
||||||
{children}
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,146 +0,0 @@
|
||||||
import React, {useCallback, useMemo, useState} from "react";
|
|
||||||
import {Card} from "../Card";
|
|
||||||
import {
|
|
||||||
flip,
|
|
||||||
shift,
|
|
||||||
useClick, useDismiss,
|
|
||||||
useFloating, UseFloatingOptions,
|
|
||||||
useFocus,
|
|
||||||
useHover,
|
|
||||||
useInteractions, useRole, useTransitionStyles
|
|
||||||
} from "@floating-ui/react";
|
|
||||||
import {mergeRefs} from "react-merge-refs";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fully managed floating content function.
|
|
||||||
*/
|
|
||||||
export type Managed<T = React.ReactElement|React.ReactNode> = (show: () => void, hide: () => void) => T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allowed floating modes.
|
|
||||||
*/
|
|
||||||
export type FloatingMode = "always"|"click"|"hover"|"focus"|"managed";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Role of a floating element.
|
|
||||||
*/
|
|
||||||
export type FloatRole = "tooltip" | "dialog" | "alertdialog" | "menu" | "listbox" | "grid" | "tree" | "select" | "label" | "combobox";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of the element on which the floating element is based.
|
|
||||||
*/
|
|
||||||
export type FloatChild = (React.ReactElement & React.ClassAttributes<HTMLElement>)|Managed<(React.ReactElement & React.ClassAttributes<HTMLElement>)>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Properties of the Float component.
|
|
||||||
*/
|
|
||||||
export interface FloatProperties
|
|
||||||
{
|
|
||||||
children: FloatChild;
|
|
||||||
content?: React.ReactNode|Managed<React.ReactNode>;
|
|
||||||
className?: string;
|
|
||||||
mode?: FloatingMode;
|
|
||||||
dismissible?: boolean;
|
|
||||||
role?: FloatRole;
|
|
||||||
floatingOptions?: UseFloatingOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A component to show something floating next to an element.
|
|
||||||
*/
|
|
||||||
export const Float = React.forwardRef(({children, content, className, mode, dismissible, role, floatingOptions}: FloatProperties, ref): React.ReactElement => {
|
|
||||||
// By default, use "always" mode.
|
|
||||||
if (!mode) mode = "always";
|
|
||||||
if (dismissible === undefined && (mode != "always" && mode != "managed")) dismissible = true;
|
|
||||||
|
|
||||||
// Followed show status.
|
|
||||||
const [shown, setShown] = useState(false);
|
|
||||||
|
|
||||||
// Create show / hide functions.
|
|
||||||
const show = useCallback(() => setShown(true), [setShown]);
|
|
||||||
const hide = useCallback(() => setShown(false), [setShown]);
|
|
||||||
|
|
||||||
// If show mode is "always", always show the floating part after render.
|
|
||||||
if (mode == "always")
|
|
||||||
{
|
|
||||||
setTimeout(() => {
|
|
||||||
setShown(true);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Floating initialization.
|
|
||||||
const { refs, floatingStyles, context, placement } = useFloating(
|
|
||||||
useMemo(() => (Object.assign({
|
|
||||||
open: shown,
|
|
||||||
onOpenChange: setShown,
|
|
||||||
middleware: [shift(), flip()],
|
|
||||||
} as UseFloatingOptions, floatingOptions)), [floatingOptions, shown, setShown])
|
|
||||||
);
|
|
||||||
|
|
||||||
// Interactions initialization.
|
|
||||||
const hover = useHover(context, useMemo(() => ({ enabled: mode == "hover" }), [mode]));
|
|
||||||
const focus = useFocus(context, useMemo(() => ({ enabled: mode == "focus", visibleOnly: false }), [mode]));
|
|
||||||
const click = useClick(context, useMemo(() => ({ enabled: mode == "click" }), [mode]));
|
|
||||||
const dismiss = useDismiss(context, useMemo(() => ({ enabled: !!dismissible && mode != "click" }), [dismissible]));
|
|
||||||
const roleProps = useRole(context, {
|
|
||||||
role: role,
|
|
||||||
enabled: !!role,
|
|
||||||
});
|
|
||||||
const {getReferenceProps, getFloatingProps} = useInteractions([roleProps, dismiss, hover, focus, click]);
|
|
||||||
|
|
||||||
// Transition configuration.
|
|
||||||
const {isMounted, styles: transitionStyles} = useTransitionStyles(context, {
|
|
||||||
duration: 200,
|
|
||||||
common: ({side}) => ({
|
|
||||||
transformOrigin: {
|
|
||||||
top: "bottom",
|
|
||||||
bottom: "top",
|
|
||||||
left: "right",
|
|
||||||
right: "left",
|
|
||||||
}[side],
|
|
||||||
}),
|
|
||||||
initial: {
|
|
||||||
transform: "scale(0.66)",
|
|
||||||
opacity: "0",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change the child element to use the reference and the interactions properties.
|
|
||||||
const referencedChild = useMemo(() => {
|
|
||||||
// Render the children if a managed floating function is passed.
|
|
||||||
const child = typeof children == "function" ? children(show, hide) : children;
|
|
||||||
|
|
||||||
return React.cloneElement(child,
|
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
// Pass references.
|
|
||||||
ref: mergeRefs([ref, refs.setReference, child?.ref]),
|
|
||||||
},
|
|
||||||
// Get interaction properties.
|
|
||||||
getReferenceProps(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, [children, show, hide, ref, refs.setReference, getReferenceProps]);
|
|
||||||
|
|
||||||
// Update floating content.
|
|
||||||
const floatingContent = useMemo(() => (
|
|
||||||
// Render the children if a managed floating function is passed.
|
|
||||||
typeof content == "function" ? content(show, hide) : content
|
|
||||||
), [shown, show, hide, content]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{referencedChild}
|
|
||||||
|
|
||||||
{ // Showing floating element if the state says to do so.
|
|
||||||
isMounted &&
|
|
||||||
<div ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} className={"floating"} data-placement={placement}>
|
|
||||||
<Card style={transitionStyles} className={classes("floating", className)}>
|
|
||||||
{floatingContent}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Float} from "./Float";
|
|
||||||
|
|
||||||
export function Tooltip({children, content}: {
|
|
||||||
children: React.ReactElement;
|
|
||||||
content: React.ReactNode;
|
|
||||||
}): React.ReactElement
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<Float mode={"hover"} content={content} className={"tooltip"} role={"tooltip"} floatingOptions={{ placement: "top" }}>
|
|
||||||
{children}
|
|
||||||
</Float>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Check} from "@phosphor-icons/react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
export function Checkbox({children, className, type, ...inputProps}: React.PropsWithChildren<React.InputHTMLAttributes<HTMLInputElement>>): React.ReactElement
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<label className={classes("box", className)}>
|
|
||||||
<input type={"checkbox"} {...inputProps} />
|
|
||||||
<a className={"button"} tabIndex={-1}><Check /></a>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import React, {useEffect, useRef} from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom validation rule in a form.
|
|
||||||
*/
|
|
||||||
export function CustomValidationRule({valid, errorMessage}: {
|
|
||||||
valid: boolean;
|
|
||||||
errorMessage: string;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// HTML virtual input ref.
|
|
||||||
const ref = useRef<HTMLInputElement>();
|
|
||||||
|
|
||||||
// When the validation is invalid, set a custom error message, set the custom error message.
|
|
||||||
useEffect(() => {
|
|
||||||
ref.current.setCustomValidity(valid ? "" : errorMessage);
|
|
||||||
}, [ref, valid, errorMessage]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input ref={ref}
|
|
||||||
className={"virtual"} type={"text"} required={true}
|
|
||||||
value={valid ? "true" : ""} onChange={() => {}} />
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
|
||||||
import {classes, formatDate, Modify} from "../../Utils";
|
|
||||||
import {Float} from "../Floating/Float";
|
|
||||||
import {Datepicker} from "../Dates/Datepicker";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A form input for a date with a datepicker.
|
|
||||||
*/
|
|
||||||
export function DatepickerInput(
|
|
||||||
{
|
|
||||||
children, className,
|
|
||||||
value, onChange,
|
|
||||||
// Properties to pass down.
|
|
||||||
onKeyUp, onBlur,
|
|
||||||
// Already set properties.
|
|
||||||
type, placeholder,
|
|
||||||
...props}: React.PropsWithChildren<Modify<React.InputHTMLAttributes<HTMLInputElement>, {
|
|
||||||
/**
|
|
||||||
* The current date value.
|
|
||||||
*/
|
|
||||||
value?: Date|null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when picked date is changed.
|
|
||||||
* @param newDate The new date.
|
|
||||||
*/
|
|
||||||
onChange: (newDate: Date) => void;
|
|
||||||
|
|
||||||
// Already set properties.
|
|
||||||
type?: never;
|
|
||||||
placeholder?: never;
|
|
||||||
}>>): React.ReactElement
|
|
||||||
{
|
|
||||||
// Date text state.
|
|
||||||
const [dateText, setDateText] = useState("");
|
|
||||||
|
|
||||||
// Update date text when date value has changed.
|
|
||||||
useEffect(() => {
|
|
||||||
if (value && value instanceof Date && !isNaN(value.getTime()))
|
|
||||||
setDateText(formatDate(value));
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
// Check if date is valid.
|
|
||||||
const invalidDate = useMemo(() => !(value && value instanceof Date && !isNaN(value.getTime())) && dateText.length > 0, [value, dateText]);
|
|
||||||
const dateValue = useMemo(() => invalidDate ? new Date() : (value ?? new Date()), [invalidDate, value]);
|
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit a new date from its raw text.
|
|
||||||
*/
|
|
||||||
const submitDate = useCallback((customDateText?: string) => {
|
|
||||||
// Get the current date text status.
|
|
||||||
const dateTextMatch = (customDateText ?? dateText).match(/^([0-9]?[0-9])\/([0-9]?[0-9])\/([0-9]?[0-9]?[0-9]?[0-9])$/);
|
|
||||||
|
|
||||||
if (dateTextMatch)
|
|
||||||
{
|
|
||||||
// Parse day.
|
|
||||||
let day = dateTextMatch[1];
|
|
||||||
if (day.length < 2) day = "0" + day;
|
|
||||||
if (parseInt(day) <= 0) day = "01";
|
|
||||||
|
|
||||||
// Parse month.
|
|
||||||
let month = dateTextMatch[2];
|
|
||||||
if (month.length < 2) month = "0" + month;
|
|
||||||
if (parseInt(month) <= 0) month = "01";
|
|
||||||
|
|
||||||
// Parse year.
|
|
||||||
let year = parseInt(dateTextMatch[3]);
|
|
||||||
if (year < 100)
|
|
||||||
{
|
|
||||||
year += 1900;
|
|
||||||
if ((new Date()).getFullYear() - year > 96) year += 100;
|
|
||||||
}
|
|
||||||
if (year < 1000) year += 1000;
|
|
||||||
|
|
||||||
// Try to build the structurally valid date.
|
|
||||||
const date = new Date(year + "-" + month + "-" + day);
|
|
||||||
|
|
||||||
if (!isNaN(date.getTime()))
|
|
||||||
{ // Date is valid, checking that it uses the right month (to fix the behavior when we go change 31/03 to 31/02, we want 28/02 or 29/02 and not 01/03).
|
|
||||||
if ((date.getMonth() + 1) > parseInt(month))
|
|
||||||
{ // Current date month is not valid, we're getting back to it.
|
|
||||||
date.setDate(date.getDate() - 1);
|
|
||||||
}
|
|
||||||
if ((date.getMonth() + 1) < parseInt(month))
|
|
||||||
{ // Current date month is not valid, we're getting back to it.
|
|
||||||
date.setDate(date.getDate() + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to keep original hours.
|
|
||||||
date.setHours(value?.getHours() ?? 0, value?.getMinutes() ?? 0, value?.getSeconds() ?? 0, value?.getMilliseconds() ?? 0);
|
|
||||||
|
|
||||||
// Set the structurally valid date.
|
|
||||||
onChange?.(date);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
// No structurally valid date, removing it.
|
|
||||||
onChange?.(null);
|
|
||||||
}, [dateText, onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className={classes("datepicker-input", invalidDate ? "error" : null, className)}
|
|
||||||
// Keeping focus on the input when something else in the label takes it.
|
|
||||||
onFocus={useCallback(() => inputRef.current?.focus(), [inputRef])}>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<Float mode={"focus"} className={"datepicker"} content={
|
|
||||||
<Datepicker date={dateValue} onDateSelected={onChange} />
|
|
||||||
}>
|
|
||||||
<input type={"text"} ref={inputRef} placeholder={"DD/MM/AAAA"} value={dateText}
|
|
||||||
onChange={useCallback((event) => {
|
|
||||||
let dateText = event.currentTarget.value;
|
|
||||||
|
|
||||||
// Add first '/' if missing.
|
|
||||||
if (dateText.match(/^[0-9]{2}[^\/]*$/))
|
|
||||||
dateText = dateText.substring(0, 2) + "/" + dateText.substring(2);
|
|
||||||
|
|
||||||
// Add second '/' if missing.
|
|
||||||
if (dateText.match(/^[0-9]{2}\/[0-9]{2}[^\/]*$/))
|
|
||||||
dateText = dateText.substring(0, 5) + "/" + dateText.substring(5);
|
|
||||||
|
|
||||||
// Set the new date text.
|
|
||||||
setDateText(dateText);
|
|
||||||
|
|
||||||
// If a full date has been entered, reading it.
|
|
||||||
const fullDateMatch = dateText.match(/^([0-9]{2})\/([0-9]{2})\/([0-9]{4})$/);
|
|
||||||
if (fullDateMatch)
|
|
||||||
{ // We have a structurally valid date, submitting it.
|
|
||||||
submitDate(dateText);
|
|
||||||
}
|
|
||||||
}, [submitDate])}
|
|
||||||
onKeyUp={useCallback((event) => {
|
|
||||||
if (event.key == "Enter")
|
|
||||||
// Submit date when enter is pressed.
|
|
||||||
submitDate();
|
|
||||||
return onKeyUp?.(event);
|
|
||||||
}, [submitDate, onKeyUp])}
|
|
||||||
onBlur={useCallback((event) => {
|
|
||||||
// Submit date when leaving form input.
|
|
||||||
submitDate();
|
|
||||||
return onBlur?.(event);
|
|
||||||
}, [submitDate, onBlur])}
|
|
||||||
{...props} />
|
|
||||||
</Float>
|
|
||||||
|
|
||||||
{ // The date is invalid, showing a subtext to say so.
|
|
||||||
invalidDate && (
|
|
||||||
<span className={"error subtext"}>Invalid date.</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import React, {useState} from "react";
|
|
||||||
import {Eye, EyeSlash} from "@phosphor-icons/react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
export function PasswordInput({children, className, type, ...props}: React.PropsWithChildren<React.InputHTMLAttributes<HTMLInputElement>>): React.ReactElement
|
|
||||||
{
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className={classes("password", className)}>
|
|
||||||
{children}
|
|
||||||
<div>
|
|
||||||
<input type={showPassword ? "text" : "password"} {...props} />
|
|
||||||
<a className={"button"} tabIndex={-1} onClick={() => {
|
|
||||||
setShowPassword(!showPassword);
|
|
||||||
}}>
|
|
||||||
{
|
|
||||||
showPassword ? <EyeSlash /> : <Eye />
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Check} from "@phosphor-icons/react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
export function Radio({children, className, type, ...inputProps}: React.PropsWithChildren<React.InputHTMLAttributes<HTMLInputElement>>): React.ReactElement
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<label className={classes("box", className)}>
|
|
||||||
<input type={"radio"} {...inputProps} />
|
|
||||||
<a className={"button"} tabIndex={-1}><Check /></a>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
export function RequiredField({className, ...props}: React.HTMLAttributes<HTMLSpanElement>): React.ReactElement
|
|
||||||
{
|
|
||||||
return <span className={classes("required", className)} {...props}></span>;
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
|
||||||
import {classes, formatTime, Modify} from "../../Utils";
|
|
||||||
|
|
||||||
export function TimepickerInput(
|
|
||||||
{
|
|
||||||
children, className,
|
|
||||||
value, onChange,
|
|
||||||
// Properties to pass down.
|
|
||||||
onKeyUp, onBlur,
|
|
||||||
// Already set properties.
|
|
||||||
type, placeholder,
|
|
||||||
...props
|
|
||||||
}: React.PropsWithChildren<Modify<React.InputHTMLAttributes<HTMLInputElement>, {
|
|
||||||
value?: Date|null;
|
|
||||||
|
|
||||||
onChange: (newDateTime: Date) => void;
|
|
||||||
|
|
||||||
// Already set properties.
|
|
||||||
type?: never;
|
|
||||||
placeholder?: never;
|
|
||||||
}>>): React.ReactElement
|
|
||||||
{
|
|
||||||
// Time text state.
|
|
||||||
const [timeText, setTimeText] = useState("");
|
|
||||||
|
|
||||||
// Update time text when datetime value has changed.
|
|
||||||
useEffect(() => {
|
|
||||||
if (value && value instanceof Date && !isNaN(value.getTime()))
|
|
||||||
setTimeText(formatTime(value));
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
// Check if time is valid.
|
|
||||||
const invalidTime = useMemo(() => !(value && value instanceof Date && !isNaN(value.getTime())) && timeText.length > 0, [value, timeText]);
|
|
||||||
const timeValue = useMemo(() => invalidTime ? new Date() : (value ?? new Date()), [invalidTime, value]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit a new time from its raw text.
|
|
||||||
*/
|
|
||||||
const submitTime = useCallback((customTimeText?: string) => {
|
|
||||||
// Get the current date text status.
|
|
||||||
const timeTextMatch = (customTimeText ?? timeText).match(/^([0-2]?[0-9]):([0-5]?[0-9])$/);
|
|
||||||
|
|
||||||
if (timeTextMatch)
|
|
||||||
{
|
|
||||||
// Parse hours.
|
|
||||||
let rawHours = timeTextMatch[1];
|
|
||||||
if (rawHours.length < 2) rawHours = "0" + rawHours;
|
|
||||||
let hours = parseInt(rawHours);
|
|
||||||
if (isNaN(hours)) hours = 0;
|
|
||||||
|
|
||||||
// Parse minutes.
|
|
||||||
let rawMinutes = timeTextMatch[2];
|
|
||||||
if (rawMinutes.length < 2) rawMinutes = "0" + rawMinutes;
|
|
||||||
let minutes = parseInt(rawMinutes);
|
|
||||||
if (isNaN(minutes)) minutes = 0;
|
|
||||||
|
|
||||||
//TODO
|
|
||||||
// Parse seconds?
|
|
||||||
|
|
||||||
// Try to build the structurally valid date with this time.
|
|
||||||
//TODO
|
|
||||||
const datetime = new Date(timeValue);
|
|
||||||
datetime.setHours(hours, minutes);
|
|
||||||
// Put the date back, if it changed to another day.
|
|
||||||
datetime.setFullYear(timeValue.getFullYear(), timeValue.getMonth(), timeValue.getDate());
|
|
||||||
|
|
||||||
// Set the structurally valid date.
|
|
||||||
onChange?.(datetime);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
// No structurally valid date, removing it.
|
|
||||||
onChange?.(null);
|
|
||||||
}, [timeText, timeValue, onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className={classes("timepicker", className)}>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<input type={"text"} placeholder={"HH:MM"} value={timeText}
|
|
||||||
onChange={useCallback((event) => {
|
|
||||||
let timeText = event.currentTarget.value;
|
|
||||||
|
|
||||||
// Add first ':' if missing.
|
|
||||||
if (timeText.match(/^[0-9]{2}[^:]*$/))
|
|
||||||
timeText = timeText.substring(0, 2) + ":" + timeText.substring(2);
|
|
||||||
|
|
||||||
// Set the new time text.
|
|
||||||
setTimeText(timeText);
|
|
||||||
|
|
||||||
// If a full time has been entered, reading it.
|
|
||||||
const fullTimeMatch = timeText.match(/^([0-2][0-9]):([0-5][0-9])$/);
|
|
||||||
if (fullTimeMatch)
|
|
||||||
{ // We have a structurally valid time, submitting it.
|
|
||||||
submitTime(timeText);
|
|
||||||
}
|
|
||||||
}, [submitTime])}
|
|
||||||
onKeyUp={useCallback((event) => {
|
|
||||||
if (event.key == "Enter")
|
|
||||||
// Submit time when enter is pressed.
|
|
||||||
submitTime();
|
|
||||||
return onKeyUp?.(event);
|
|
||||||
}, [submitTime, onKeyUp])}
|
|
||||||
onBlur={useCallback((event) => {
|
|
||||||
// Submit time when leaving form input.
|
|
||||||
submitTime();
|
|
||||||
return onBlur?.(event);
|
|
||||||
}, [submitTime, onBlur])}
|
|
||||||
{...props} />
|
|
||||||
|
|
||||||
{ // The date is invalid, showing a subtext to say so.
|
|
||||||
invalidTime && (
|
|
||||||
<span className={"error subtext"}>Invalid time.</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Check} from "@phosphor-icons/react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
export function ToggleSwitch({children, className, type, ...inputProps}: React.PropsWithChildren<React.InputHTMLAttributes<HTMLInputElement>>): React.ReactElement
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<label className={classes("toggleswitch", className)}>
|
|
||||||
<input type={"checkbox"} {...inputProps} />
|
|
||||||
<a className={"button"} tabIndex={-1}><span className={"switch"}></span></a>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export function GenericLoader({children}: React.PropsWithChildren<{}>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<div className={"generic loader"}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import React, {useCallback} from "react";
|
|
||||||
|
|
||||||
export function ListLoader({count, itemContent}: {
|
|
||||||
/**
|
|
||||||
* Sample items count.
|
|
||||||
* 3 by default.
|
|
||||||
*/
|
|
||||||
count?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sample items content or content generator function.
|
|
||||||
*/
|
|
||||||
itemContent?: React.ReactNode|((key: number) => React.ReactNode);
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Sample items count. 3 by default.
|
|
||||||
count = count === undefined ? 3 : count;
|
|
||||||
|
|
||||||
// Initialize the sample content generator.
|
|
||||||
const contentGenerator = useCallback((key: number) => (
|
|
||||||
typeof itemContent != "function"
|
|
||||||
// No function given, return the given content or a sample text.
|
|
||||||
? (itemContent ?? "Loading content...")
|
|
||||||
// A function have been given, just return its result.
|
|
||||||
: itemContent(key)
|
|
||||||
), [itemContent]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className={"list loader"}>
|
|
||||||
{ // Render every sample item.
|
|
||||||
[...Array(count)].map((_, key) => (
|
|
||||||
<li key={key}><div className={"content"}>{contentGenerator(key)}</div></li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
export function SpinningLoader({ inline }: { inline?: boolean; })
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<div className={classes("spinning loader", inline ? "inline" : undefined)}></div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
import {IconContext} from "@phosphor-icons/react";
|
|
||||||
import {NavLink, NavLinkProps} from "react-router-dom";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main apps menu component.
|
|
||||||
*/
|
|
||||||
export function AppsMenu({className, children, ...props}: React.HTMLAttributes<HTMLDivElement>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<IconContext.Consumer>
|
|
||||||
{(value) => (
|
|
||||||
<IconContext.Provider value={Object.assign({}, value, {
|
|
||||||
size: 40,
|
|
||||||
weight: "regular",
|
|
||||||
})}>
|
|
||||||
<nav className={classes("apps", "menu", className)} {...props}>
|
|
||||||
<ul>
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</IconContext.Provider>
|
|
||||||
)}
|
|
||||||
</IconContext.Consumer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component of an app item in apps menu.
|
|
||||||
*/
|
|
||||||
export function AppItem({className, children, ...props}: React.HTMLAttributes<HTMLAnchorElement>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<a className={classes("app", "flat button", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component of an app link in apps menu.
|
|
||||||
*/
|
|
||||||
export function AppLink({className, children, ...props}: NavLinkProps & React.HTMLAttributes<HTMLAnchorElement>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<NavLink className={classes("app", "flat button", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main component of a main menu.
|
|
||||||
*/
|
|
||||||
export function MainMenu({children, className, ...props}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<nav className={classes("main", "menu", className)} {...props}>
|
|
||||||
<ul>
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes, Modify} from "../../Utils";
|
|
||||||
import {SubmenuFloat} from "./SubmenuFloat";
|
|
||||||
import {NavLink, NavLinkProps} from "react-router-dom";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main menu item properties.
|
|
||||||
*/
|
|
||||||
export type MainMenuItemProperties = React.PropsWithChildren<Modify<React.AnchorHTMLAttributes<HTMLAnchorElement>, {
|
|
||||||
}>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main component of a main menu item.
|
|
||||||
*/
|
|
||||||
export const MainMenuItem = React.forwardRef<HTMLAnchorElement, MainMenuItemProperties>(function SubmenuItem({children, ...props}: MainMenuItemProperties, ref)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<a ref={ref} {...props}>{children}</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A main menu item link properties.
|
|
||||||
*/
|
|
||||||
export type MainMenuLinkProperties = React.PropsWithChildren<Modify<NavLinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>, {
|
|
||||||
}>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A main menu item link.
|
|
||||||
*/
|
|
||||||
export const MainMenuLink = React.forwardRef<HTMLAnchorElement, MainMenuLinkProperties>(function MainMenuLink({children, ...props}: MainMenuLinkProperties, ref)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<NavLink ref={ref} {...props}>{children}</NavLink>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A main menu item that open a submenu.
|
|
||||||
*/
|
|
||||||
export function MainMenuItemSubmenu({submenu, className, children, ...props}: Modify<MainMenuItemProperties, {
|
|
||||||
/**
|
|
||||||
* The submenu content.
|
|
||||||
*/
|
|
||||||
submenu: React.ReactNode;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<SubmenuFloat submenu={submenu} role={"menu"} floatingOptions={{ placement: "bottom-start" }}>
|
|
||||||
<MainMenuItem className={classes("submenu", className)} {...props}>{children}</MainMenuItem>
|
|
||||||
</SubmenuFloat>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main component of a submenu.
|
|
||||||
*/
|
|
||||||
export function Submenu({className, children, ...props}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<div className={classes("submenu", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Float, FloatProperties} from "../Floating/Float";
|
|
||||||
import {classes, Modify} from "../../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a submenu which opens on click on the child.
|
|
||||||
*/
|
|
||||||
export function SubmenuFloat({submenu, className, mode, dismissible, children, content, ...props}: Modify<FloatProperties, {
|
|
||||||
/**
|
|
||||||
* The submenu content.
|
|
||||||
*/
|
|
||||||
submenu: React.ReactNode;
|
|
||||||
|
|
||||||
// Ignored overridden properties.
|
|
||||||
content?: never;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<Float mode={mode ?? "click"} dismissible={dismissible ?? true}
|
|
||||||
className={classes("submenu", className)}
|
|
||||||
content={submenu} {...props}>
|
|
||||||
{children}
|
|
||||||
</Float>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes, Modify} from "../../Utils";
|
|
||||||
import {SubmenuFloat} from "./SubmenuFloat";
|
|
||||||
import {NavLink, NavLinkProps} from "react-router-dom";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submenu item properties.
|
|
||||||
*/
|
|
||||||
export type SubmenuItemProperties = React.PropsWithChildren<Modify<React.AnchorHTMLAttributes<HTMLAnchorElement>, {
|
|
||||||
}>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main component of a submenu item.
|
|
||||||
*/
|
|
||||||
export const SubmenuItem = React.forwardRef<HTMLAnchorElement, SubmenuItemProperties>(function SubmenuItem({className, children, ...props}: SubmenuItemProperties, ref)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<a ref={ref} className={classes("item", className)} {...props}>{children}</a>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A submenu item link properties.
|
|
||||||
*/
|
|
||||||
export type SubmenuLinkProperties = React.PropsWithChildren<Modify<NavLinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>, {
|
|
||||||
}>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A submenu item link.
|
|
||||||
*/
|
|
||||||
export const SubmenuLink = React.forwardRef<HTMLAnchorElement, SubmenuLinkProperties>(function SubmenuLink({className, children, ...props}: SubmenuLinkProperties, ref)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<NavLink ref={ref} className={classes("item", className)} {...props}>{children}</NavLink>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A submenu item that open a submenu.
|
|
||||||
*/
|
|
||||||
export function SubmenuItemSubmenu({submenu, className, children, ...props}: Modify<SubmenuItemProperties, {
|
|
||||||
/**
|
|
||||||
* The submenu content.
|
|
||||||
*/
|
|
||||||
submenu: React.ReactNode;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<SubmenuFloat submenu={submenu} role={"menu"} floatingOptions={{ placement: "right-start" }}>
|
|
||||||
<SubmenuItem className={classes("submenu", className)} {...props}>{children}</SubmenuItem>
|
|
||||||
</SubmenuFloat>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {X} from "@phosphor-icons/react";
|
|
||||||
import {useCurtains, useCallableCurtain} from "../Curtains/Curtains";
|
|
||||||
import {classes, Modify} from "../../Utils";
|
|
||||||
import {ModalType, ModalTypeIcon} from "./ModalsTypes";
|
|
||||||
import {useCurtain} from "../Curtains/CurtainInstance";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More natural name of useCurtains for modals.
|
|
||||||
* @see useCurtains
|
|
||||||
*/
|
|
||||||
export const useModals = useCurtains;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More natural name of useCurtain for modals.
|
|
||||||
* @see useCurtain
|
|
||||||
*/
|
|
||||||
export const useModal = useCurtain;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More natural name of useCallableCurtain for modals.
|
|
||||||
* @see useCallableCurtain
|
|
||||||
*/
|
|
||||||
export const useCallableModal = useCallableCurtain;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modal main component.
|
|
||||||
*/
|
|
||||||
export function Modal({className, title, closable, type, children, ...props}: React.PropsWithChildren<Modify<React.HTMLAttributes<HTMLDivElement>, {
|
|
||||||
/**
|
|
||||||
* Modal title.
|
|
||||||
*/
|
|
||||||
title?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can disable close button.
|
|
||||||
*/
|
|
||||||
closable?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modal type. None by default.
|
|
||||||
*/
|
|
||||||
type?: ModalType;
|
|
||||||
}>>)
|
|
||||||
{
|
|
||||||
// Modal is closable by default.
|
|
||||||
closable = closable !== undefined ? closable : true;
|
|
||||||
|
|
||||||
// Modal type is NONE by default.
|
|
||||||
type = type !== undefined ? type : ModalType.NONE;
|
|
||||||
|
|
||||||
// Modal state.
|
|
||||||
const {close} = useModal();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes("modal", type, className)} {...props}>
|
|
||||||
<header>
|
|
||||||
<h1>{ModalTypeIcon[type]} {title ?? ""}</h1>
|
|
||||||
|
|
||||||
<button className={"icon-only close"} onClick={close} disabled={!closable}><X size={20} /></button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {CheckCircle, Info, Record, Warning, WarningDiamond} from "@phosphor-icons/react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modal types enumeration.
|
|
||||||
*/
|
|
||||||
export enum ModalType
|
|
||||||
{
|
|
||||||
INFO = "info",
|
|
||||||
SUCCESS = "success",
|
|
||||||
WARNING = "warning",
|
|
||||||
ERROR = "error",
|
|
||||||
NONE = "none",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Icon for each modal type.
|
|
||||||
*/
|
|
||||||
export const ModalTypeIcon: Record<ModalType, React.ReactElement> = {
|
|
||||||
[ModalType.INFO]: <Info size={24} weight={"duotone"} />,
|
|
||||||
[ModalType.SUCCESS]: <CheckCircle size={24} weight={"duotone"} />,
|
|
||||||
[ModalType.WARNING]: <Warning size={24} weight={"duotone"} />,
|
|
||||||
[ModalType.ERROR]: <WarningDiamond size={24} weight={"duotone"} />,
|
|
||||||
[ModalType.NONE]: null,
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
import React, {useCallback, useContext} from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
import {NotificationContext, NotificationsContext} from "./Notifications";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifications types enumeration.
|
|
||||||
*/
|
|
||||||
export enum NotificationType
|
|
||||||
{
|
|
||||||
NONE = "none",
|
|
||||||
INFO = "info",
|
|
||||||
SUCCESS = "success",
|
|
||||||
WARNING = "warning",
|
|
||||||
ERROR = "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification component.
|
|
||||||
*/
|
|
||||||
export function Notification({type, children}: React.PropsWithChildren<{
|
|
||||||
type?: NotificationType;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
// Default type is NONE.
|
|
||||||
type = type ?? NotificationType.NONE;
|
|
||||||
|
|
||||||
// Get notifications context.
|
|
||||||
const {close} = useContext(NotificationsContext);
|
|
||||||
|
|
||||||
// Get current notification UUID.
|
|
||||||
const {uuid, closed} = useContext(NotificationContext);
|
|
||||||
|
|
||||||
// Initialize close notification function.
|
|
||||||
const closeNotification = useCallback(() => { close(uuid); }, [uuid]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={classes("notification", type, closed ? "closed" : undefined)} onMouseDown={closeNotification}>
|
|
||||||
{children}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,217 +0,0 @@
|
||||||
import React, {startTransition, useCallback, useContext, useEffect, useMemo, useRef, useState} from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification UUID type.
|
|
||||||
*/
|
|
||||||
export type NotificationUuid = string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification data.
|
|
||||||
*/
|
|
||||||
export interface NotificationData
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The notification content.
|
|
||||||
*/
|
|
||||||
content: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of notification emitter function.
|
|
||||||
*/
|
|
||||||
export type EmitNotificationFunction = (content: React.ReactNode) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of notification close function.
|
|
||||||
*/
|
|
||||||
export type RemoveNotificationFunction = (uuid: NotificationUuid) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of notification close function.
|
|
||||||
*/
|
|
||||||
export type CloseNotificationFunction = (uuid: NotificationUuid) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of notification closed state function.
|
|
||||||
*/
|
|
||||||
export type IsNotificationClosedFunction = (uuid: NotificationUuid) => boolean;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface of notifications state.
|
|
||||||
*/
|
|
||||||
export interface NotificationsContextState
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Notification function.
|
|
||||||
* @param content Notification content.
|
|
||||||
*/
|
|
||||||
notify: EmitNotificationFunction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close notification function.
|
|
||||||
* @param uuid UUID of notification to close.
|
|
||||||
*/
|
|
||||||
close: CloseNotificationFunction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is given notification closed?
|
|
||||||
* @param uuid UUID of notification to get closed state.
|
|
||||||
*/
|
|
||||||
isClosed: IsNotificationClosedFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotificationsContext = React.createContext<NotificationsContextState>({
|
|
||||||
notify() {},
|
|
||||||
close() {},
|
|
||||||
isClosed() { return false; },
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to emit a notification.
|
|
||||||
*/
|
|
||||||
export function useNotify(): EmitNotificationFunction
|
|
||||||
{
|
|
||||||
return useContext(NotificationsContext).notify;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifications provider.
|
|
||||||
*/
|
|
||||||
export function NotificationsProvider({children}: React.PropsWithChildren<{}>)
|
|
||||||
{
|
|
||||||
// Notifications.
|
|
||||||
const [notifications, setNotifications] = useState<Record<NotificationUuid, NotificationData>>({});
|
|
||||||
// Keeping track of closed notifications that are still on (while transitioning out).
|
|
||||||
const [closedNotifications, setClosedNotifications] = useState<Record<NotificationUuid, boolean>>()
|
|
||||||
|
|
||||||
// Initialize remove notification function.
|
|
||||||
const remove = useRef<RemoveNotificationFunction>();
|
|
||||||
remove.current = useCallback((uuid) => {
|
|
||||||
// Copy the notifications list.
|
|
||||||
const newNotifications = {...notifications};
|
|
||||||
const newClosedNotifications = {...closedNotifications};
|
|
||||||
// Remove the given notification from the list.
|
|
||||||
delete newNotifications[uuid];
|
|
||||||
delete newClosedNotifications[uuid];
|
|
||||||
// Set the new notifications list.
|
|
||||||
setNotifications(newNotifications);
|
|
||||||
setClosedNotifications(newClosedNotifications);
|
|
||||||
}, [notifications, setNotifications, closedNotifications, setClosedNotifications]);
|
|
||||||
|
|
||||||
// Initialize close notification function with animation.
|
|
||||||
const close = useRef<CloseNotificationFunction>();
|
|
||||||
close.current = useCallback((uuid) => {
|
|
||||||
// Add the given curtain UUID to the list of closed curtains.
|
|
||||||
setClosedNotifications({
|
|
||||||
...closedNotifications,
|
|
||||||
[uuid]: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove the notification 300ms later.
|
|
||||||
window.setTimeout(() => {
|
|
||||||
// Remove the curtain.
|
|
||||||
remove.current(uuid);
|
|
||||||
}, 300);
|
|
||||||
}, [remove, closedNotifications, setClosedNotifications]);
|
|
||||||
|
|
||||||
// Initialize isClosed notification function.
|
|
||||||
const isClosed = useRef<IsNotificationClosedFunction>();
|
|
||||||
isClosed.current = useCallback((uuid) => (!!closedNotifications?.[uuid]), [closedNotifications]);
|
|
||||||
|
|
||||||
// Initialize notify function.
|
|
||||||
const notify = useRef<EmitNotificationFunction>();
|
|
||||||
notify.current = useCallback((content: React.ReactNode) => {
|
|
||||||
// Generate a new notification UUID for the new notification.
|
|
||||||
const notificationUuid = uuidv4();
|
|
||||||
|
|
||||||
// Add the notification to the list of notifications, with the generated UUID.
|
|
||||||
setNotifications({
|
|
||||||
...notifications,
|
|
||||||
[notificationUuid]: {
|
|
||||||
content: content,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close notification 10s later.
|
|
||||||
setTimeout(() => {
|
|
||||||
close.current(notificationUuid);
|
|
||||||
}, 10000);
|
|
||||||
}, [notifications, setNotifications]);
|
|
||||||
|
|
||||||
// Initialize context state from action functions.
|
|
||||||
const contextState = useMemo(() => ({
|
|
||||||
notify: (content: React.ReactNode) => notify.current(content),
|
|
||||||
close: (uuid: NotificationUuid) => close.current(uuid),
|
|
||||||
isClosed: (uuid: NotificationUuid) => isClosed.current(uuid),
|
|
||||||
}), [notify, isClosed]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationsContext.Provider value={contextState}>
|
|
||||||
{children}
|
|
||||||
<NotificationsPortal notifications={notifications} />
|
|
||||||
</NotificationsContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Curtains portal manager.
|
|
||||||
*/
|
|
||||||
function NotificationsPortal({notifications}: {
|
|
||||||
notifications: Record<NotificationUuid, NotificationData>;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
return ReactDOM.createPortal((
|
|
||||||
<ul className={"notifications"}>
|
|
||||||
{ // Show notifications list.
|
|
||||||
Object.entries(notifications).map(([uuid, notificationData]) => (
|
|
||||||
<NotificationInstance key={uuid} uuid={uuid}>
|
|
||||||
{notificationData.content}
|
|
||||||
</NotificationInstance>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
), document.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A notification context.
|
|
||||||
*/
|
|
||||||
export const NotificationContext = React.createContext<{
|
|
||||||
/**
|
|
||||||
* Notification UUID.
|
|
||||||
*/
|
|
||||||
uuid: NotificationUuid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification closed state.
|
|
||||||
*/
|
|
||||||
closed: boolean;
|
|
||||||
}>(undefined);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification component.
|
|
||||||
*/
|
|
||||||
function NotificationInstance({uuid, children}: React.PropsWithChildren<{
|
|
||||||
/**
|
|
||||||
* Notification UUID.
|
|
||||||
*/
|
|
||||||
uuid: NotificationUuid;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
// Get notifications context.
|
|
||||||
const {isClosed} = useContext(NotificationsContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationContext.Provider value={{
|
|
||||||
uuid: uuid,
|
|
||||||
closed: isClosed(uuid),
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</NotificationContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
import React, {useCallback, useState} from "react";
|
|
||||||
import {Pagination} from "./Pagination";
|
|
||||||
import {SpinningLoader} from "../Loaders/SpinningLoader";
|
|
||||||
import {Await, useAsync} from "../../Async";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Paginated content component with custom page handling.
|
|
||||||
*/
|
|
||||||
export function Paginate({ page, onChange, count, children }: React.PropsWithChildren<{
|
|
||||||
/**
|
|
||||||
* The current page.
|
|
||||||
*/
|
|
||||||
page: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a new page is selected.
|
|
||||||
* @param newPage The newly selected page.
|
|
||||||
*/
|
|
||||||
onChange: (newPage: number) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pages count.
|
|
||||||
*/
|
|
||||||
count: number;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<Pagination page={page} onChange={onChange} count={count} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Paginated content component.
|
|
||||||
*/
|
|
||||||
export function AutoPaginate({ count, children }: {
|
|
||||||
/**
|
|
||||||
* Pages count.
|
|
||||||
*/
|
|
||||||
count: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the given page.
|
|
||||||
* @param page The page to show.
|
|
||||||
*/
|
|
||||||
children: (page: number) => React.ReactElement;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// The current page.
|
|
||||||
const [page, setPage] = useState<number>(1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paginate page={page} onChange={setPage} count={count}>
|
|
||||||
{children(page)}
|
|
||||||
</Paginate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous paginated content component.
|
|
||||||
*/
|
|
||||||
export function AsyncPaginate<T>({ count, getData, children }: {
|
|
||||||
/**
|
|
||||||
* Get pages count.
|
|
||||||
*/
|
|
||||||
count: () => Promise<number>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get data for the given page.
|
|
||||||
* @param page The page for which to get data.
|
|
||||||
*/
|
|
||||||
getData: (page: number) => Promise<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the current page with its retrieved data.
|
|
||||||
* @param data Data of the page to show.
|
|
||||||
* @param page The page to show.
|
|
||||||
*/
|
|
||||||
children: (data: T, page: number) => React.ReactElement;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Getting pages count.
|
|
||||||
const [asyncCount] = useAsync(count, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Await async={asyncCount} fallback={<SpinningLoader />}>
|
|
||||||
{
|
|
||||||
(count) => (
|
|
||||||
<AutoPaginate count={count}>
|
|
||||||
{(page) => <AsyncPage page={page} getData={getData} render={children} />}
|
|
||||||
</AutoPaginate>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Await>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An async page to render.
|
|
||||||
*/
|
|
||||||
export function AsyncPage<T>({page, getData, render}: {
|
|
||||||
/**
|
|
||||||
* The page number to show.
|
|
||||||
*/
|
|
||||||
page: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get data for the given page.
|
|
||||||
* @param page The page for which to get data.
|
|
||||||
*/
|
|
||||||
getData: (page: number) => Promise<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the page with its retrieved data.
|
|
||||||
* @param data Data of the page to show.
|
|
||||||
* @param page The page to show.
|
|
||||||
*/
|
|
||||||
render: (data: T, page: number) => React.ReactElement;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Store function to get page data.
|
|
||||||
const getPageData = useCallback(() => {
|
|
||||||
return getData(page);
|
|
||||||
}, [page]);
|
|
||||||
|
|
||||||
// Getting page data.
|
|
||||||
const [asyncPageData] = useAsync(getPageData, [getPageData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Await async={asyncPageData} fallback={<SpinningLoader />}>
|
|
||||||
{(pageData) => render(pageData, page)}
|
|
||||||
</Await>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
import React, {useEffect, useState} from "react";
|
|
||||||
import {CaretLeft, CaretRight} from "@phosphor-icons/react";
|
|
||||||
import {Tooltip} from "../Floating/Tooltip";
|
|
||||||
import {usePreviousValue} from "../../Utils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pagination component.
|
|
||||||
*/
|
|
||||||
export function Pagination({ page, onChange, count }: {
|
|
||||||
/**
|
|
||||||
* The current page.
|
|
||||||
*/
|
|
||||||
page: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a new page is selected.
|
|
||||||
* @param newPage The newly selected page.
|
|
||||||
*/
|
|
||||||
onChange: (newPage: number) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pages count.
|
|
||||||
*/
|
|
||||||
count: number;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Memorize previous page.
|
|
||||||
const previousPage = usePreviousValue(page);
|
|
||||||
|
|
||||||
// The input text to use, when the currently entered value is not a number.
|
|
||||||
const [inputText, setInputText] = useState(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (page != previousPage)
|
|
||||||
// If the page has changed, resetting the input text.
|
|
||||||
setInputText(undefined);
|
|
||||||
}, [previousPage, page]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className={"pages"}>
|
|
||||||
<Tooltip content={"Previous"}>
|
|
||||||
<button className={"flat icon-only"} disabled={page <= 1} onClick={() => onChange(page - 1)}>
|
|
||||||
<CaretLeft/>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{ // First page, only show when the current page isn't the first page.
|
|
||||||
page > 1 &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={() => onChange(1)}>1</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
{ // Space between first page and shown pages.
|
|
||||||
// Only show when the page isn't the first page.
|
|
||||||
page - 4 > 1 &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={page - 4 > 2 ? undefined : () => onChange(page - 4)}>{page - 4 > 2 ? "···" : page - 4}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
{ // Only show when the page isn't the first page.
|
|
||||||
page - 3 > 1 &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={() => onChange(page - 3)}>{page - 3}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
{ // Only show when the page isn't the first page.
|
|
||||||
page - 2 > 1 &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={() => onChange(page - 2)}>{page - 2}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
{ // Only show when the page isn't the first page.
|
|
||||||
page - 1 > 1 &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={() => onChange(page - 1)}>{page - 1}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
{ // Current page input.
|
|
||||||
<li className={"current"}>
|
|
||||||
<input type={"number"} step={1} min={1} max={count} value={inputText ?? page} onChange={(event) => {
|
|
||||||
// Try to get the new value, between 1 and page count.
|
|
||||||
const newValue = Math.max(Math.min(event.currentTarget.valueAsNumber, count), 1);
|
|
||||||
if (isNaN(newValue))
|
|
||||||
// Not a numeric value, keep the current input text without changing the page number.
|
|
||||||
setInputText(event.currentTarget.value);
|
|
||||||
else
|
|
||||||
{ // The value has been read successfully, changing it and resetting input text.
|
|
||||||
onChange(newValue);
|
|
||||||
setInputText(undefined);
|
|
||||||
}
|
|
||||||
}} />
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
{ // Only show when the page isn't the last page.
|
|
||||||
page + 1 < count &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={() => onChange(page + 1)}>{page + 1}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
{ // Only show when the page isn't the last page.
|
|
||||||
page + 2 < count &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={() => onChange(page + 2)}>{page + 2}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
{ // Only show when the page isn't the last page.
|
|
||||||
page + 3 < count &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={() => onChange(page + 3)}>{page + 3}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
{ // Space between shown pages and last page.
|
|
||||||
// Only show when the page isn't the last page.
|
|
||||||
page + 4 < count &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={page + 4 < (count - 1) ? undefined : () => onChange(page + 4)}>{page + 4 < (count - 1) ? "···" : page + 4}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
|
|
||||||
{ // Last page, only show when the current page isn't the last page.
|
|
||||||
page < count &&
|
|
||||||
<li>
|
|
||||||
<button className={"flat"} onClick={() => onChange(count)}>{count}</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<Tooltip content={"Next"}>
|
|
||||||
<button className={"flat icon-only"} disabled={page >= count} onClick={() => onChange(page + 1)}>
|
|
||||||
<CaretRight/>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
import React, {MutableRefObject, useCallback, useMemo, useRef, useState} from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
import {Check} from "@phosphor-icons/react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Suggestions preselected options navigation configuration.
|
|
||||||
*/
|
|
||||||
export interface SuggestionsNavigation
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Return true if the preselected options navigation is initialized.
|
|
||||||
*/
|
|
||||||
initialized(): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preselect the next option in the suggestions.
|
|
||||||
*/
|
|
||||||
next(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preselect the previous option in the suggestions.
|
|
||||||
*/
|
|
||||||
previous(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the currently preselected option.
|
|
||||||
*/
|
|
||||||
select(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to get the preselected options navigation reference.
|
|
||||||
*/
|
|
||||||
export function useSuggestionsNavigation(): MutableRefObject<SuggestionsNavigation>
|
|
||||||
{
|
|
||||||
return useRef({
|
|
||||||
initialized(): boolean
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
next(): void
|
|
||||||
{
|
|
||||||
},
|
|
||||||
|
|
||||||
previous(): void
|
|
||||||
{
|
|
||||||
},
|
|
||||||
|
|
||||||
select(): void
|
|
||||||
{
|
|
||||||
},
|
|
||||||
} as SuggestionsNavigation);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OptionsSuggestions<OptionKey extends keyof any, Option>({options, onSelected, selectedOptions, renderOption, navigator}: {
|
|
||||||
/**
|
|
||||||
* Options to suggest.
|
|
||||||
*/
|
|
||||||
options: Record<OptionKey, Option>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an option is selected.
|
|
||||||
* @param key
|
|
||||||
* @param option
|
|
||||||
*/
|
|
||||||
onSelected: (key: OptionKey, option: Option) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Already selected options that will be shown differently.
|
|
||||||
*/
|
|
||||||
selectedOptions?: Record<OptionKey, Option>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render an option.
|
|
||||||
* @param option The option to render.
|
|
||||||
*/
|
|
||||||
renderOption?: (option: Option) => React.ReactNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reference to a preselected suggestions options navigation object.
|
|
||||||
*/
|
|
||||||
navigator?: MutableRefObject<SuggestionsNavigation>;
|
|
||||||
}): React.ReactNode
|
|
||||||
{
|
|
||||||
// Initialize default option render function.
|
|
||||||
const defaultRenderOption = useCallback((option: Option) => (String(option)), []);
|
|
||||||
|
|
||||||
const optionsArray = useMemo(() => (Object.entries(options) as [OptionKey, Option][]), [options]);
|
|
||||||
|
|
||||||
const [preselectedOptionIndex, setPreselectedOptionIndex] = useState<number>(0);
|
|
||||||
|
|
||||||
const navigation = useMemo(() => ({
|
|
||||||
initialized(): boolean
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
next(): void
|
|
||||||
{
|
|
||||||
// Preselect the next option in the options array, or the first one if it's the last element.
|
|
||||||
setPreselectedOptionIndex((optionsArray.length == (preselectedOptionIndex + 1)) ? 0 : (preselectedOptionIndex + 1));
|
|
||||||
},
|
|
||||||
|
|
||||||
previous(): void
|
|
||||||
{
|
|
||||||
// Preselect the previous option in the options array, or the last one if it's the first element.
|
|
||||||
setPreselectedOptionIndex(((preselectedOptionIndex - 1) < 0) ? (optionsArray.length - 1) : (preselectedOptionIndex - 1));
|
|
||||||
},
|
|
||||||
|
|
||||||
select(): void
|
|
||||||
{
|
|
||||||
// Get the currently preselected option.
|
|
||||||
const [optionKey, option] = optionsArray[preselectedOptionIndex];
|
|
||||||
// Select the currently preselected option.
|
|
||||||
onSelected(optionKey, option);
|
|
||||||
},
|
|
||||||
}), [optionsArray, preselectedOptionIndex, setPreselectedOptionIndex]);
|
|
||||||
|
|
||||||
if (navigator)
|
|
||||||
// If navigator reference is set, assigning it.
|
|
||||||
navigator.current = navigation;
|
|
||||||
|
|
||||||
return optionsArray.map(([key, option], index) => (
|
|
||||||
<a key={String(key)} className={classes("suggestion", preselectedOptionIndex == index ? "preselected" : null, selectedOptions?.[key] ? "selected" : null)}
|
|
||||||
onClick={() => { onSelected(key, option); }}>
|
|
||||||
{(renderOption ?? defaultRenderOption)(option)}
|
|
||||||
|
|
||||||
<span className={"selected"}><Check /></span>
|
|
||||||
</a>
|
|
||||||
));
|
|
||||||
}
|
|
|
@ -1,250 +0,0 @@
|
||||||
import React, {useCallback, useMemo, useRef} from "react";
|
|
||||||
import {Suggestible} from "./Suggestible";
|
|
||||||
import {OptionsSuggestions, useSuggestionsNavigation} from "./OptionsSuggestions";
|
|
||||||
import {classes, Modify, normalizeString} from "../../Utils";
|
|
||||||
import {CaretDown, Check, X} from "@phosphor-icons/react";
|
|
||||||
import {CustomValidationRule} from "../Forms/CustomValidationRule";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic select component properties.
|
|
||||||
*/
|
|
||||||
export type SelectProperties<OptionKey extends keyof any, Option> = React.PropsWithChildren<Modify<React.InputHTMLAttributes<HTMLInputElement>, {
|
|
||||||
/**
|
|
||||||
* The currently selected option(s).
|
|
||||||
*/
|
|
||||||
value: OptionKey|OptionKey[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when new options are selected.
|
|
||||||
* @param newValue
|
|
||||||
*/
|
|
||||||
onChange: (newValue: OptionKey|OptionKey[]) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options list or a way to get it from a given search.
|
|
||||||
*/
|
|
||||||
options: Record<OptionKey, Option>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render an option.
|
|
||||||
* @param option The option to render.
|
|
||||||
*/
|
|
||||||
renderOption?: (option: Option) => React.ReactNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Option match function. Return true if the given option matches the given search query.
|
|
||||||
* @param search The search query.
|
|
||||||
* @param option The option that should match the search query.
|
|
||||||
*/
|
|
||||||
match?: (search: string, option: Option) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Min count of options to allow to select.
|
|
||||||
* 0 by default, 1 when required is true.
|
|
||||||
*/
|
|
||||||
selectibleMinCount?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Max count of options to allow to select.
|
|
||||||
* 1 by default when multiple is false, infinity when multiple is true.
|
|
||||||
*/
|
|
||||||
selectibleMaxCount?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True to allow to select an infinite count of options.
|
|
||||||
* false by default.
|
|
||||||
*/
|
|
||||||
multiple?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When true, the select loses focus when a new option is selected (by clicking or pressing Enter).
|
|
||||||
* false by default.
|
|
||||||
*/
|
|
||||||
blurOnSelect?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When true, the select loses focus when the max count of selectible options have been reached (by clicking or pressing Enter).
|
|
||||||
* true by default.
|
|
||||||
*/
|
|
||||||
blurWhenMaxCountSelected?: boolean;
|
|
||||||
|
|
||||||
// Already set properties.
|
|
||||||
type?: never;
|
|
||||||
role?: never;
|
|
||||||
}>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic select component.
|
|
||||||
*/
|
|
||||||
export function Select<OptionKey extends keyof any, Option>(
|
|
||||||
{
|
|
||||||
className,
|
|
||||||
value, onChange,
|
|
||||||
options, renderOption, match,
|
|
||||||
required, selectibleMinCount,
|
|
||||||
selectibleMaxCount, multiple,
|
|
||||||
blurOnSelect, blurWhenMaxCountSelected,
|
|
||||||
// Properties to pass down.
|
|
||||||
onKeyDown,
|
|
||||||
// Already set properties.
|
|
||||||
type, role,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: SelectProperties<OptionKey, Option>): React.ReactElement
|
|
||||||
{
|
|
||||||
const [search, setSearch] = React.useState("");
|
|
||||||
|
|
||||||
// By default, allow to select only one option.
|
|
||||||
// If `multiple` is set and `selectibleMaxCount` is not, allow an infinite count of options to select.
|
|
||||||
selectibleMaxCount = selectibleMaxCount ?? ((multiple === undefined ? false : multiple) ? Infinity : 1);
|
|
||||||
|
|
||||||
// By default, allow to select no option.
|
|
||||||
// If `required` is set, `selectibleMinCount` will be set as 1 by default.
|
|
||||||
selectibleMinCount = selectibleMinCount ?? (required ? 1 : 0);
|
|
||||||
|
|
||||||
// true by default.
|
|
||||||
blurOnSelect = blurOnSelect === undefined ? false : blurOnSelect;
|
|
||||||
blurWhenMaxCountSelected = blurWhenMaxCountSelected === undefined ? true : blurWhenMaxCountSelected;
|
|
||||||
|
|
||||||
// Initialize default option render function.
|
|
||||||
const defaultRenderOption = useCallback((option: Option) => (String(option)), []);
|
|
||||||
|
|
||||||
// Initialize default match option function.
|
|
||||||
const defaultMatchOption = useCallback((search: string, option: Option) => normalizeString(String(option)).includes(normalizeString(search)), []);
|
|
||||||
|
|
||||||
// An array of the selected options.
|
|
||||||
const selectedOptions = useMemo(() => (
|
|
||||||
// Normalize value to an array.
|
|
||||||
((!Array.isArray(value) ? [value] : value)
|
|
||||||
// Try to get the corresponding option for each given value.
|
|
||||||
.map((optionKey) => [optionKey, options?.[optionKey]]) as [OptionKey, Option][])
|
|
||||||
// Filter non-existing options.
|
|
||||||
.filter(([_, option]) => (option !== undefined))
|
|
||||||
), [value, options]);
|
|
||||||
|
|
||||||
// A reference to the main search input.
|
|
||||||
const inputRef = useRef<HTMLInputElement>();
|
|
||||||
|
|
||||||
// The suggestions' navigator.
|
|
||||||
const suggestionsNavigator = useSuggestionsNavigation();
|
|
||||||
|
|
||||||
// Get all available options, filtered using search query.
|
|
||||||
const filteredOptions = useMemo(() => (
|
|
||||||
!search || (search.length == 0)
|
|
||||||
// Nothing is searched, return all options.
|
|
||||||
? options
|
|
||||||
// Filter options using search query and matching function.
|
|
||||||
: Object.fromEntries(
|
|
||||||
(Object.entries(options) as [OptionKey, Option][]).filter(([optionKey, option]) => (
|
|
||||||
(match ?? defaultMatchOption)(search, option)
|
|
||||||
))
|
|
||||||
) as Record<OptionKey, Option>
|
|
||||||
), [options, search, match, defaultMatchOption]);
|
|
||||||
|
|
||||||
// Called when a new option is selected.
|
|
||||||
const handleSelectedOption = useCallback((selectedOption: OptionKey) => {
|
|
||||||
// Get an associative object from selected options.
|
|
||||||
const currentSelection = Object.fromEntries(selectedOptions) as Record<OptionKey, Option>;
|
|
||||||
|
|
||||||
// Initialize the new selection variable.
|
|
||||||
let newSelection: OptionKey|OptionKey[] = Object.keys(currentSelection) as OptionKey[];
|
|
||||||
|
|
||||||
if (selectibleMaxCount == 1)
|
|
||||||
// Only one possible selection = the newly selected option is the new selection.
|
|
||||||
newSelection = selectedOption;
|
|
||||||
else
|
|
||||||
{ // Multiple selections possible.
|
|
||||||
if (!currentSelection[selectedOption])
|
|
||||||
{ // The newly selected option wasn't selected, we should add it in the selection array.
|
|
||||||
// Add the newly selected option in the array.
|
|
||||||
newSelection = [...newSelection, selectedOption];
|
|
||||||
if (newSelection.length > selectibleMaxCount)
|
|
||||||
// If the array is now too big, we remove the first options to match the max count of options.
|
|
||||||
newSelection.splice(0, newSelection.length - selectibleMaxCount);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{ // The option was already selected, we should deselect it.
|
|
||||||
newSelection = newSelection.filter((key) => key != selectedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call onChange event with the new selection.
|
|
||||||
onChange(newSelection);
|
|
||||||
|
|
||||||
// Reset search query.
|
|
||||||
setSearch("");
|
|
||||||
|
|
||||||
if (
|
|
||||||
// Always blur on selection.
|
|
||||||
blurOnSelect
|
|
||||||
||
|
|
||||||
// Blur when the new selection now reach the max selectible count.
|
|
||||||
(blurWhenMaxCountSelected && ((selectibleMaxCount == 1 && !!newSelection) || (selectibleMaxCount == (newSelection as OptionKey[])?.length)))
|
|
||||||
) // Try to lose focus, as a new selection has been made and blur conditions are reached.
|
|
||||||
window.setTimeout(() => { inputRef?.current?.blur(); }, 0);
|
|
||||||
}, [selectedOptions, onChange, setSearch]);
|
|
||||||
|
|
||||||
// Called when an option is deselected.
|
|
||||||
const handleDeselectedOption = useCallback((deselectedOption: OptionKey) => {
|
|
||||||
// Call onChange event with the new selection.
|
|
||||||
onChange(
|
|
||||||
selectedOptions
|
|
||||||
// Remove deselected option if it was in the selected options array.
|
|
||||||
.filter(([optionKey]) => (optionKey != deselectedOption))
|
|
||||||
.map(([optionKey]) => optionKey)
|
|
||||||
);
|
|
||||||
}, [selectedOptions, onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className={classes("select", className)}
|
|
||||||
onFocus={useCallback(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}, [inputRef])}>
|
|
||||||
<div>
|
|
||||||
<Suggestible suggestions={
|
|
||||||
<OptionsSuggestions options={filteredOptions} renderOption={renderOption}
|
|
||||||
// Get an associative object from selected options.
|
|
||||||
selectedOptions={Object.fromEntries(selectedOptions) as Record<OptionKey, Option>}
|
|
||||||
onSelected={handleSelectedOption} navigator={suggestionsNavigator}/>
|
|
||||||
}>
|
|
||||||
<input ref={inputRef} type={"text"} role={"select"} value={search}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key == "ArrowDown")
|
|
||||||
suggestionsNavigator.current?.next();
|
|
||||||
else if (event.key == "ArrowUp")
|
|
||||||
suggestionsNavigator.current?.previous();
|
|
||||||
else if (event.key == "Enter")
|
|
||||||
suggestionsNavigator.current?.select();
|
|
||||||
|
|
||||||
return onKeyDown?.(event);
|
|
||||||
}}
|
|
||||||
onChange={(event) => setSearch(event.currentTarget.value)}
|
|
||||||
{...props} />
|
|
||||||
</Suggestible>
|
|
||||||
|
|
||||||
<a className={"button"} tabIndex={-1}><CaretDown /></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CustomValidationRule valid={selectedOptions.length >= selectibleMinCount}
|
|
||||||
errorMessage={`At least ${selectibleMinCount} option${selectibleMinCount > 1 ? "s are" : " is"} required.`} />
|
|
||||||
|
|
||||||
<ul className={"selected"}>
|
|
||||||
{ // Showing each selected value.
|
|
||||||
selectedOptions.map(([optionKey, option]) => (
|
|
||||||
<li key={String(optionKey)}>
|
|
||||||
<Check />
|
|
||||||
<div className={"option"}>{(renderOption ?? defaultRenderOption)(option)}</div>
|
|
||||||
<button className={"remove flat"} type={"button"} onMouseDown={() => handleDeselectedOption(optionKey)}>
|
|
||||||
<X />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className={"label"}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export function SimpleSuggestions(): React.ReactElement
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<a className={"suggestion"}>test</a>
|
|
||||||
<a className={"suggestion"}>another</a>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Float, FloatProperties} from "../Floating/Float";
|
|
||||||
import {classes, Modify} from "../../Utils";
|
|
||||||
|
|
||||||
export function Suggestible({className, suggestions, mode, content, role, children, ...props}: Modify<FloatProperties, {
|
|
||||||
/**
|
|
||||||
* Suggestions element.
|
|
||||||
*/
|
|
||||||
suggestions: React.ReactNode;
|
|
||||||
|
|
||||||
content?: never;
|
|
||||||
role?: never;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
// Default mode for showing suggestions is "focus".
|
|
||||||
mode = mode ?? "focus";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Float className={classes("suggestions", className)} role={"select"} dismissible={false} content={suggestions} mode={mode} {...props}>
|
|
||||||
{children}
|
|
||||||
</Float>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
import React, {useEffect} from "react";
|
|
||||||
import {
|
|
||||||
GlobalStateProvider,
|
|
||||||
useGlobalStateReducers, useGlobalStateValue,
|
|
||||||
} from "../../GlobalState";
|
|
||||||
import {usePreviousValue} from "../../Utils";
|
|
||||||
import {StepKeyType, stepsGlobalState, useCurrentStepKey, useStepsNavigator} from "./StepsContext";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main Steps component.
|
|
||||||
*/
|
|
||||||
export function Steps({children}: React.PropsWithChildren<{}>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<GlobalStateProvider globalState={stepsGlobalState}>
|
|
||||||
<div className={"steps"}>
|
|
||||||
<StepsNavigatorComponent />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GlobalStateProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Steps navigator component.
|
|
||||||
*/
|
|
||||||
export function StepsNavigatorComponent()
|
|
||||||
{
|
|
||||||
// Get the steps navigator functions.
|
|
||||||
const stepsNavigator = useStepsNavigator();
|
|
||||||
|
|
||||||
// Get the current steps state.
|
|
||||||
const stepsState = useGlobalStateValue(stepsGlobalState);
|
|
||||||
|
|
||||||
// Get the current step.
|
|
||||||
const currentStep = useCurrentStepKey();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className={"steps"}>
|
|
||||||
<ul>
|
|
||||||
{ // Showing a button for each step.
|
|
||||||
stepsState.steps.map((step, index) => (
|
|
||||||
// Rendering the current step button.
|
|
||||||
<li key={step.key} className={currentStep == step.key ? "active" : undefined}>
|
|
||||||
<button type={"button"} onClick={() => {
|
|
||||||
stepsNavigator.set(step.key);
|
|
||||||
}}>
|
|
||||||
{step.title ?? (index + 1)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component of a step.
|
|
||||||
*/
|
|
||||||
export function Step({stepKey, stepTitle, children}: React.PropsWithChildren<{
|
|
||||||
/**
|
|
||||||
* The current step unique key.
|
|
||||||
*/
|
|
||||||
stepKey: StepKeyType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The step title, to show in the navigator.
|
|
||||||
*/
|
|
||||||
stepTitle?: React.ReactNode;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
// Get the global state reducers class.
|
|
||||||
const stepsGlobalStateReducers = useGlobalStateReducers(stepsGlobalState);
|
|
||||||
|
|
||||||
// Get the previous step key.
|
|
||||||
const previousStepKey = usePreviousValue(stepKey);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Remove the previous step key, if there is one.
|
|
||||||
if (previousStepKey) stepsGlobalStateReducers.removeStep(previousStepKey);
|
|
||||||
|
|
||||||
// Register the current step key.
|
|
||||||
stepsGlobalStateReducers.registerStep(stepKey, stepTitle);
|
|
||||||
|
|
||||||
// Remove the step key when component is removed.
|
|
||||||
return () => stepsGlobalStateReducers.removeStep(stepKey);
|
|
||||||
}, [stepsGlobalStateReducers, previousStepKey, stepTitle]);
|
|
||||||
|
|
||||||
// Get the current step key.
|
|
||||||
const currentStep = useCurrentStepKey();
|
|
||||||
|
|
||||||
if (currentStep != stepKey)
|
|
||||||
// If this step is not the current one, rendering nothing.
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
// Rendering the current step.
|
|
||||||
return (
|
|
||||||
<section className={"step"}>
|
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
import React, {useMemo} from "react";
|
|
||||||
import {GlobalState, GlobalStateReducers, useGlobalStateReducers, useGlobalStateValue} from "../../GlobalState";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of step key.
|
|
||||||
*/
|
|
||||||
export type StepKeyType = string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Steps global state data type.
|
|
||||||
*/
|
|
||||||
export interface StepsGlobalStateType
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Steps list, by their order of apparition.
|
|
||||||
*/
|
|
||||||
steps: { key: StepKeyType; title?: React.ReactNode; }[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The index of the current step in the steps list.
|
|
||||||
*/
|
|
||||||
currentStepIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The steps global state modification functions.
|
|
||||||
*/
|
|
||||||
class StepsGlobalStateReducers extends GlobalStateReducers<StepsGlobalStateType>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Register a new step with its step key.
|
|
||||||
* @param newStepKey The step key to register.
|
|
||||||
*/
|
|
||||||
registerStep(newStepKey: StepKeyType, newStepTitle?: React.ReactNode): void
|
|
||||||
{
|
|
||||||
this.setState({
|
|
||||||
// Add the given step key to the steps array, ensuring that it will be there only once.
|
|
||||||
steps: [...this.state.steps.filter(({key: currentStepKey}) => currentStepKey != newStepKey), { key: newStepKey, title: newStepTitle }]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an old step with its step key.
|
|
||||||
* @param oldStepKey The step key to remove.
|
|
||||||
*/
|
|
||||||
removeStep(oldStepKey: StepKeyType): void
|
|
||||||
{
|
|
||||||
this.setState({
|
|
||||||
// Remove the given step key from the steps array.
|
|
||||||
steps: this.state.steps.filter(({key: currentStepKey}) => currentStepKey != oldStepKey)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current step from its key.
|
|
||||||
* @param stepKey The step key to set as the current one.
|
|
||||||
*/
|
|
||||||
setCurrentStep(stepKey: StepKeyType): void
|
|
||||||
{
|
|
||||||
// Get the new step index.
|
|
||||||
const stepIndex = this.state.steps.findIndex(({key: currentStep}) => currentStep == stepKey);
|
|
||||||
|
|
||||||
if (stepIndex >= 0)
|
|
||||||
{ // If the new step index has been found, setting it as the current one.
|
|
||||||
this.setState({
|
|
||||||
currentStepIndex: stepIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the next step key.
|
|
||||||
*/
|
|
||||||
getNextStep(): StepKeyType
|
|
||||||
{
|
|
||||||
return this.state.steps[this.state.currentStepIndex + 1]?.key ?? this.state.steps[0]?.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the previous step key.
|
|
||||||
*/
|
|
||||||
getPreviousStep(): StepKeyType
|
|
||||||
{
|
|
||||||
return this.state.steps[this.state.currentStepIndex - 1]?.key ?? this.state.steps[this.state.steps.length - 1]?.key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The steps global state.
|
|
||||||
*/
|
|
||||||
export const stepsGlobalState = new GlobalState<StepsGlobalStateType, StepsGlobalStateReducers>({
|
|
||||||
steps: [],
|
|
||||||
currentStepIndex: 0,
|
|
||||||
}, new StepsGlobalStateReducers);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook of the current step key in a steps component.
|
|
||||||
*/
|
|
||||||
export function useCurrentStepKey(): StepKeyType
|
|
||||||
{
|
|
||||||
// Get the current steps global state value.
|
|
||||||
const stepsState = useGlobalStateValue(stepsGlobalState);
|
|
||||||
|
|
||||||
// Get the current step from the global state value.
|
|
||||||
return stepsState.steps.length > 0
|
|
||||||
? ( // If there are steps, getting the current one.
|
|
||||||
stepsState.currentStepIndex < stepsState.steps.length
|
|
||||||
? ( // If the index is correctly defined, trying to get the corresponding state key
|
|
||||||
stepsState.currentStepIndex >= 0 ? (stepsState.steps[stepsState.currentStepIndex]?.key) : stepsState.steps[0]?.key
|
|
||||||
)
|
|
||||||
// The current index is too high, taking the last step as the current one.
|
|
||||||
: stepsState.steps[stepsState.steps.length - 1]?.key
|
|
||||||
)
|
|
||||||
// There are no steps, returning none.
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Steps navigator
|
|
||||||
*/
|
|
||||||
export interface StepsNavigator
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Go to the next step.
|
|
||||||
*/
|
|
||||||
next(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return to the previous step.
|
|
||||||
*/
|
|
||||||
previous(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to the given step.
|
|
||||||
* @param stepKey The step key to join.
|
|
||||||
*/
|
|
||||||
set(stepKey: StepKeyType): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook of the steps' navigator.
|
|
||||||
*/
|
|
||||||
export function useStepsNavigator(): StepsNavigator
|
|
||||||
{
|
|
||||||
// Get the steps reducers instance.
|
|
||||||
const stepsReducers = useGlobalStateReducers(stepsGlobalState);
|
|
||||||
|
|
||||||
// Create the steps navigator object.
|
|
||||||
return useMemo(() => ({
|
|
||||||
next()
|
|
||||||
{
|
|
||||||
stepsReducers.setCurrentStep(stepsReducers.getNextStep());
|
|
||||||
},
|
|
||||||
|
|
||||||
previous()
|
|
||||||
{
|
|
||||||
stepsReducers.setCurrentStep(stepsReducers.getPreviousStep());
|
|
||||||
},
|
|
||||||
|
|
||||||
set(stepKey: StepKeyType)
|
|
||||||
{
|
|
||||||
stepsReducers.setCurrentStep(stepKey);
|
|
||||||
},
|
|
||||||
}), [stepsReducers]);
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {X} from "@phosphor-icons/react";
|
|
||||||
import {useCallableCurtain, useCurtains} from "../Curtains/Curtains";
|
|
||||||
import {classes, Modify} from "../../Utils";
|
|
||||||
import {useCurtain} from "../Curtains/CurtainInstance";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More natural name of useCurtains for subapps.
|
|
||||||
* @see useCurtains
|
|
||||||
*/
|
|
||||||
export const useSubapps = useCurtains;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More natural name of useCurtain for subapps.
|
|
||||||
* @see useCurtain
|
|
||||||
*/
|
|
||||||
export const useSubapp = useCurtain;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More natural name of useCallableCurtain for subapps.
|
|
||||||
* @see useCallableCurtain
|
|
||||||
*/
|
|
||||||
export const useCallableSubapp = useCallableCurtain;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subapp main component.
|
|
||||||
*/
|
|
||||||
export function Subapp({className, title, closable, children}: React.PropsWithChildren<Modify<React.HTMLAttributes<HTMLDivElement>, {
|
|
||||||
/**
|
|
||||||
* Subapp title.
|
|
||||||
*/
|
|
||||||
title?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can disable close button.
|
|
||||||
*/
|
|
||||||
closable?: boolean;
|
|
||||||
}>>)
|
|
||||||
{
|
|
||||||
// Subapp is closable by default.
|
|
||||||
closable = closable !== undefined ? closable : true;
|
|
||||||
|
|
||||||
// Subapp state.
|
|
||||||
const {close} = useSubapp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes("subapp", className)}>
|
|
||||||
<header>
|
|
||||||
<h1>{title ?? ""}</h1>
|
|
||||||
|
|
||||||
<button className={"icon-only close"} onClick={close} disabled={!closable}><X size={32}/></button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {classes} from "../../Utils";
|
|
||||||
import {Info} from "@phosphor-icons/react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple text tip component.
|
|
||||||
*/
|
|
||||||
export function Tip({className, children, ...props}: React.PropsWithChildren<React.HTMLAttributes<HTMLParagraphElement>>)
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<p className={classes("tip", className)} {...props}>
|
|
||||||
<Info weight={"duotone"} />
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,184 +0,0 @@
|
||||||
import React, {PropsWithChildren, useCallback, useContext, useState} from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global state modifiers functions.
|
|
||||||
*/
|
|
||||||
export class GlobalStateReducers<S>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The current state (readonly).
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
protected state: Readonly<S>;
|
|
||||||
/**
|
|
||||||
* State update system.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private stateUpdater: React.Dispatch<S>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign changes to the current state.
|
|
||||||
* @param stateUpdate Changes to apply to the current state.
|
|
||||||
*/
|
|
||||||
setState(stateUpdate: Partial<S>)
|
|
||||||
{
|
|
||||||
// Apply update object to the current state.
|
|
||||||
this.state = Object.assign({}, this.state, stateUpdate);
|
|
||||||
// Update the changed state.
|
|
||||||
this.stateUpdater(this.state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reducers definition interface for easy type definition.
|
|
||||||
*/
|
|
||||||
interface ReducerHack<S>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The current state (readonly).
|
|
||||||
*/
|
|
||||||
state: Readonly<S>;
|
|
||||||
/**
|
|
||||||
* State update system.
|
|
||||||
*/
|
|
||||||
stateUpdater: React.Dispatch<S>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main global state class.
|
|
||||||
*/
|
|
||||||
export class GlobalState<S, R extends GlobalStateReducers<S>>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Global state context.
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
protected context: React.Context<S>;
|
|
||||||
/**
|
|
||||||
* State update system, used in the reducers class.
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
protected stateUpdater: React.Dispatch<S>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new global state.
|
|
||||||
* @param defaultValue The default value of the global state.
|
|
||||||
* @param reducers Reducers class.
|
|
||||||
*/
|
|
||||||
constructor(protected defaultValue: S, protected reducers: R)
|
|
||||||
{
|
|
||||||
// Create a new context for the global state.
|
|
||||||
this.context = React.createContext<S>(defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the context for the global state.
|
|
||||||
*/
|
|
||||||
getContext(): React.Context<S>
|
|
||||||
{ return this.context; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the default value of the global state.
|
|
||||||
*/
|
|
||||||
getDefaultValue(): S
|
|
||||||
{ return this.defaultValue; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign the state update system, used in the reducers class.
|
|
||||||
* @param updater State update system instance.
|
|
||||||
*/
|
|
||||||
setStateUpdater(updater: React.Dispatch<S>): void
|
|
||||||
{
|
|
||||||
this.stateUpdater = updater;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the configured state reducers class for the current state.
|
|
||||||
* @param state The state on which the reducers should apply.
|
|
||||||
*/
|
|
||||||
getStateReducers(state: S): R
|
|
||||||
{
|
|
||||||
// Assign the given state.
|
|
||||||
(this.reducers as unknown as ReducerHack<S>).state = state;
|
|
||||||
// Assign the current state updater.
|
|
||||||
(this.reducers as unknown as ReducerHack<S>).stateUpdater = this.stateUpdater;
|
|
||||||
return this.reducers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global state Provider hook.
|
|
||||||
* @param globalState Global state for which to get the global state Provider.
|
|
||||||
*/
|
|
||||||
export function useGlobalStateProvider<S, R extends GlobalStateReducers<S>>(globalState: GlobalState<S, R>): React.FunctionComponent<PropsWithChildren<{}>>
|
|
||||||
{
|
|
||||||
// Get the provider from the given global state context.
|
|
||||||
const Provider = globalState.getContext().Provider;
|
|
||||||
|
|
||||||
// Create the global state provider component.
|
|
||||||
return useCallback((props: PropsWithChildren<{}>) => {
|
|
||||||
// Get the current state of the global state.
|
|
||||||
const [state, setState] = useState(globalState.getDefaultValue());
|
|
||||||
// Pass the state update system to the global state.
|
|
||||||
globalState.setStateUpdater(setState);
|
|
||||||
|
|
||||||
// Use the context provider and render children.
|
|
||||||
return (
|
|
||||||
<Provider value={state}>
|
|
||||||
{props.children}
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}, [Provider]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global state Provider component
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export function GlobalStateProvider<S, R extends GlobalStateReducers<S>>({globalState, children}: React.PropsWithChildren<{
|
|
||||||
/**
|
|
||||||
* Global state for which to get the global state Provider.
|
|
||||||
*/
|
|
||||||
globalState: GlobalState<S, R>;
|
|
||||||
}>)
|
|
||||||
{
|
|
||||||
// Get the current global state provider.
|
|
||||||
const Provider = useGlobalStateProvider(globalState);
|
|
||||||
|
|
||||||
return (
|
|
||||||
// Render the children inside the provider.
|
|
||||||
<Provider>{children}</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the global state and its reducers.
|
|
||||||
* @param globalState The global state to get.
|
|
||||||
*/
|
|
||||||
export function useGlobalState<S, R extends GlobalStateReducers<S>>(globalState: GlobalState<S, R>): [S, R]
|
|
||||||
{
|
|
||||||
// Get the global state data (from its context state).
|
|
||||||
const ctx = useContext(globalState.getContext());
|
|
||||||
// Return the context state (global state data), and the global state reducers for this current state.
|
|
||||||
return [ctx, globalState.getStateReducers(ctx)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the reducers of the global state.
|
|
||||||
* @param globalState The global state for which to get the reducers.
|
|
||||||
*/
|
|
||||||
export function useGlobalStateReducers<S, R extends GlobalStateReducers<S>>(globalState: GlobalState<S, R>): R
|
|
||||||
{
|
|
||||||
// Return the global state reducers for the current global state.
|
|
||||||
return globalState.getStateReducers(useContext(globalState.getContext()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the global state data.
|
|
||||||
* @param globalState The global state for which to get the data.
|
|
||||||
*/
|
|
||||||
export function useGlobalStateValue<S, R extends GlobalStateReducers<S>>(globalState: GlobalState<S, R>): S
|
|
||||||
{
|
|
||||||
// Return the global state data (from its context state).
|
|
||||||
return useContext(globalState.getContext());
|
|
||||||
}
|
|
|
@ -1,133 +0,0 @@
|
||||||
import React, {useCallback, useContext, useMemo, useState} from "react";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create Kernel context.
|
|
||||||
* @param defaultValue Kernel context default value.
|
|
||||||
*/
|
|
||||||
export function createKernelContext<T>(defaultValue: T): KernelContext<T>
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
uuid: uuidv4(),
|
|
||||||
defaultValue: defaultValue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kernel context definition object.
|
|
||||||
*/
|
|
||||||
export interface KernelContext<T>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Kernel context UUID.
|
|
||||||
*/
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kernel context default value.
|
|
||||||
*/
|
|
||||||
defaultValue: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kernel context setter function type.
|
|
||||||
*/
|
|
||||||
export type KernelContextSetter<IdentifierType extends string|symbol|number = string, ValueType = any> = (identifier: IdentifierType, value: ValueType) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kernel contexts type.
|
|
||||||
*/
|
|
||||||
export type KernelContexts = Record<string, any>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kernel context dispatcher function type.
|
|
||||||
*/
|
|
||||||
export type KernelContextDispatcher<ValueType = any> = (value: ValueType) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kernel global context data.
|
|
||||||
*/
|
|
||||||
export interface KernelGlobalContextData
|
|
||||||
{
|
|
||||||
contexts: KernelContexts;
|
|
||||||
setContext: KernelContextSetter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React Kernel global context.
|
|
||||||
*/
|
|
||||||
export const KernelGlobalContext = React.createContext<KernelGlobalContextData>({
|
|
||||||
contexts: {},
|
|
||||||
setContext: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kernel global context provider.
|
|
||||||
*/
|
|
||||||
export function KernelGlobalContextProvider({children}: React.PropsWithChildren<{}>)
|
|
||||||
{
|
|
||||||
// Kernel contexts initialization.
|
|
||||||
const [kernelContexts, setKernelContexts] = useState<KernelContexts>({});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change kernel contexts.
|
|
||||||
*/
|
|
||||||
const changeKernelContexts = useCallback((kernelContextsUpdate: Partial<KernelContexts>) => {
|
|
||||||
setKernelContexts({
|
|
||||||
...kernelContexts,
|
|
||||||
...kernelContextsUpdate,
|
|
||||||
});
|
|
||||||
}, [kernelContexts, setKernelContexts]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kernel context setter function.
|
|
||||||
*/
|
|
||||||
const kernelContextSetter = useCallback((identifier: string, data: any) => (
|
|
||||||
changeKernelContexts({
|
|
||||||
[identifier]: data,
|
|
||||||
})
|
|
||||||
), [changeKernelContexts]);
|
|
||||||
|
|
||||||
// Initialize kernel global context value.
|
|
||||||
const kernelContextValue = useMemo<KernelGlobalContextData>(() => ({
|
|
||||||
contexts: kernelContexts,
|
|
||||||
setContext: kernelContextSetter,
|
|
||||||
}), [kernelContexts, kernelContextSetter]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<KernelGlobalContext.Provider value={kernelContextValue}>
|
|
||||||
{children}
|
|
||||||
</KernelGlobalContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get kernel global context data.
|
|
||||||
*/
|
|
||||||
function useKernelGlobalContext(): KernelGlobalContextData
|
|
||||||
{
|
|
||||||
return useContext(KernelGlobalContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize or get kernel context with given identifier.
|
|
||||||
* @param context Context object.
|
|
||||||
*/
|
|
||||||
export function useKernelContext<T>(context: KernelContext<T>): [T, KernelContextDispatcher<T>]
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get kernel global context.
|
|
||||||
*/
|
|
||||||
const kernelGlobalContext = useKernelGlobalContext();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to set kernel context.
|
|
||||||
*/
|
|
||||||
const setContext = useCallback(
|
|
||||||
(newValue: T) => kernelGlobalContext.setContext(context.uuid, newValue),
|
|
||||||
[kernelGlobalContext.setContext],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return kernel context and its dispatcher.
|
|
||||||
return [kernelGlobalContext.contexts?.[context.uuid] ?? context.defaultValue, setContext];
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
import React, {DependencyList, FormEvent, useCallback, useEffect, useRef} from "react";
|
|
||||||
|
|
||||||
export type Modify<T, R> = Omit<T, keyof R> & R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge multiple class names to one full class name.
|
|
||||||
* @param className Class names.
|
|
||||||
*/
|
|
||||||
export function classes(...className: (string|null|undefined|false)[]): string
|
|
||||||
{
|
|
||||||
return className.filter((className) => !!className).join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDate(date: Date): string
|
|
||||||
{
|
|
||||||
return ((date.getDate() < 10 ? "0" : "") + date.getDate()) + "/" + (((date.getMonth() + 1) < 10 ? "0" : "") + (date.getMonth() + 1)) + "/" + date.getFullYear();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatTime(date: Date): string
|
|
||||||
{
|
|
||||||
return (date.getHours() < 10 ? "0" : "") + date.getHours() + ":" + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a given string for searching.
|
|
||||||
* @param str The string to normalize.
|
|
||||||
*/
|
|
||||||
export function normalizeString(str: string): string
|
|
||||||
{
|
|
||||||
return str
|
|
||||||
? str.toLowerCase?.().normalize?.("NFD")
|
|
||||||
.replace?.(/[\u0300-\u036f]/g, "")
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the previous value of a given value.
|
|
||||||
* @param currentValue The current value.
|
|
||||||
*/
|
|
||||||
export function usePreviousValue<T>(currentValue: T): T
|
|
||||||
{
|
|
||||||
// Get the reference to the previous value.
|
|
||||||
const ref = useRef<T>();
|
|
||||||
|
|
||||||
// If the value has changed, saving it in the reference after rendering.
|
|
||||||
useEffect(() => {
|
|
||||||
ref.current = currentValue;
|
|
||||||
}, [currentValue]);
|
|
||||||
|
|
||||||
// Get the previous value.
|
|
||||||
return ref?.current ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a callback for a form submit function that fully overrides default form behavior.
|
|
||||||
* @param submitFunction The function to call on form submit.
|
|
||||||
* @param dependencies Submit function dependencies.
|
|
||||||
*/
|
|
||||||
export function useFormSubmit(submitFunction: () => void, dependencies: DependencyList = []): React.FormEventHandler<HTMLFormElement>
|
|
||||||
{
|
|
||||||
// Use a memoized callback.
|
|
||||||
return useCallback((event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
// Prevent default behavior, then call submit function.
|
|
||||||
event.preventDefault();
|
|
||||||
submitFunction();
|
|
||||||
return false;
|
|
||||||
}, [submitFunction, ...dependencies]);
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
:root
|
|
||||||
{
|
|
||||||
@background-lightest: #FFFFFF; --background-lightest: @background-lightest;
|
|
||||||
@background-lighter: #F7F7F7; --background-lighter: @background-lighter;
|
|
||||||
@background: #EFEFEF; --background: @background;
|
|
||||||
@background-darker: #E7E7E7; --background-darker: @background-darker;
|
|
||||||
@background-darkest: #D9D9D9; --background-darkest: @background-darkest;
|
|
||||||
|
|
||||||
@neutral: #909090; --neutral: @neutral;
|
|
||||||
|
|
||||||
@foreground-lightest: #1F1F1F; --foreground-lightest: @foreground-lightest;
|
|
||||||
@foreground-lighter: #171717; --foreground-lighter: @foreground-lighter;
|
|
||||||
@foreground: #0F0F0F; --foreground: @foreground;
|
|
||||||
@foreground-darker: #080808; --foreground-darker: @foreground-darker;
|
|
||||||
@foreground-darkest: #000000; --foreground-darkest: @foreground-darkest;
|
|
||||||
|
|
||||||
@foreground-shadow: fade(@foreground, 33%); --foreground-shadow: @foreground-shadow;
|
|
||||||
@curtain-dim: fade(@foreground, 50%); --curtain-dim: @curtain-dim;
|
|
||||||
@curtain-inset: fade(@foreground, 20%); --curtain-inset: @curtain-inset;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@green-lighter: #22BD12; --green-lighter: @green-lighter;
|
|
||||||
@green: #1DA90F; --green: @green;
|
|
||||||
@green-darker: #159308; --green-darker: @green-darker;
|
|
||||||
@green-background: #A9FFA0; --green-background: @green-background;
|
|
||||||
@green-background-darker: #86F37E; --green-background-darker: @green-background-darker;
|
|
||||||
|
|
||||||
@red-lighter: #E32424; --red-lighter: @red-lighter;
|
|
||||||
@red: #D01212; --red: @red;
|
|
||||||
@red-darker: #AF0707; --red-darker: @red-darker;
|
|
||||||
@red-background: #FFBABA; --red-background: @red-background;
|
|
||||||
@red-background-darker: #FFA6A6; --red-background-darker: @red-background-darker;
|
|
||||||
|
|
||||||
@blue-lighter: #378AFF; --blue-lighter: @blue-lighter;
|
|
||||||
@blue: #0D6DEE; --blue: @blue;
|
|
||||||
@blue-darker: #0657C5; --blue-darker: @blue-darker;
|
|
||||||
@blue-background: #9DC8FF; --blue-background: @blue-background;
|
|
||||||
@blue-background-darker: #7EA9E1; --blue-background-darker: @blue-background-darker;
|
|
||||||
@blue-gradient: linear-gradient(33deg, @blue 0%, @blue-lighter 100%), @blue; --blue-gradient: @blue-gradient;
|
|
||||||
|
|
||||||
@orange-lighter: #E77220; --orange-lighter: @orange-lighter;
|
|
||||||
@orange: #D06112; --orange: @orange;
|
|
||||||
@orange-darker: #BB5308; --orange-darker: @orange-darker;
|
|
||||||
@orange-background: #FFC599; --orange-background: @orange-background;
|
|
||||||
@orange-background-darker: #FFB47D; --orange-background-darker: @orange-background-darker;
|
|
||||||
|
|
||||||
@pink-lighter: #EF56DF; --pink-lighter: @pink-lighter;
|
|
||||||
@pink: #CE3EBF; --pink: @pink;
|
|
||||||
@pink-darker: #B927AB; --pink-darker: @pink-darker;
|
|
||||||
@pink-background: #FFABF4; --pink-background: @pink-background;
|
|
||||||
@pink-background-darker: #EF8CE1; --pink-background-darker: @pink-background-darker;
|
|
||||||
|
|
||||||
@purple-lighter: #9752FF; --purple-lighter: @purple-lighter;
|
|
||||||
@purple: #7D2AFF; --purple: @purple;
|
|
||||||
@purple-darker: #6610EE; --purple-darker: @purple-darker;
|
|
||||||
@purple-background: #C8A5FF; --purple-background: @purple-background;
|
|
||||||
@purple-background-darker: #B489F6; --purple-background-darker: @purple-background-darker;
|
|
||||||
|
|
||||||
@yellow-lighter: #F8DF3D; --yellow-lighter: @yellow-lighter;
|
|
||||||
@yellow: #EACD0D; --yellow: @yellow;
|
|
||||||
@yellow-darker: #D3B803; --yellow-darker: @yellow-darker;
|
|
||||||
@yellow-background: #FFF195; --yellow-background: @yellow-background;
|
|
||||||
@yellow-background-darker: #ECDB71; --yellow-background-darker: @yellow-background-darker;
|
|
||||||
|
|
||||||
@brown-lighter: #5E2617; --brown-lighter: @brown-lighter;
|
|
||||||
@brown: #4B190C; --brown: @brown;
|
|
||||||
@brown-darker: #3B1105; --brown-darker: @brown-darker;
|
|
||||||
@brown-background: #B6968F; --brown-background: @brown-background;
|
|
||||||
@brown-background-darker: #A97E74; --brown-background-darker: @brown-background-darker;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@primary-lighter: @blue-lighter; --primary-lighter: @primary-lighter;
|
|
||||||
@primary: @blue; --primary: @primary;
|
|
||||||
@primary-darker: @blue-darker; --primary-darker: @primary-darker;
|
|
||||||
@primary-background: @blue-background; --primary-background: @primary-background;
|
|
||||||
@primary-background-darker: @blue-background-darker; --primary-background-darker: @primary-background-darker;
|
|
||||||
@primary-gradient: @blue-gradient; --primary-gradient: @primary-gradient;
|
|
||||||
|
|
||||||
@secondary-lighter: @orange-lighter; --secondary-lighter: @secondary-lighter;
|
|
||||||
@secondary: @orange; --secondary: @secondary;
|
|
||||||
@secondary-darker: @orange-darker; --secondary-darker: @secondary-darker;
|
|
||||||
@secondary-background: @orange-background; --secondary-background: @secondary-background;
|
|
||||||
@secondary-background-darker: @orange-background-darker; --secondary-background-darker: @secondary-background-darker;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@menu-hover: rgba(255, 255, 255, 0.15); --menu-hover: @menu-hover;
|
|
||||||
@menu-active: rgba(0, 0, 0, 0.125); --menu-active: @menu-active;
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
html, body
|
|
||||||
{
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body, input, button, textarea
|
|
||||||
{
|
|
||||||
font-size: 1em;
|
|
||||||
font-family: var(--sans-serif-fonts);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, code
|
|
||||||
{
|
|
||||||
font-family: var(--monospace-fonts);
|
|
||||||
}
|
|
||||||
|
|
||||||
body
|
|
||||||
{
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
p
|
|
||||||
{
|
|
||||||
margin: 0.75em auto;
|
|
||||||
line-height: 1.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtual
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
|
|
||||||
@import "components/_box";
|
|
||||||
@import "components/_button";
|
|
||||||
@import "components/_card";
|
|
||||||
@import "components/_curtains";
|
|
||||||
@import "components/_dates";
|
|
||||||
@import "components/_errors";
|
|
||||||
@import "components/_floating";
|
|
||||||
@import "components/_form";
|
|
||||||
@import "components/_headings";
|
|
||||||
@import "components/_link";
|
|
||||||
@import "components/_list";
|
|
||||||
@import "components/_loaders";
|
|
||||||
@import "components/_menus";
|
|
||||||
@import "components/_modal";
|
|
||||||
@import "components/_notifications";
|
|
||||||
@import "components/_pagination";
|
|
||||||
@import "components/_select";
|
|
||||||
@import "components/_steps";
|
|
||||||
@import "components/_steps-counter";
|
|
||||||
@import "components/_subapp";
|
|
||||||
@import "components/_table";
|
|
||||||
@import "components/_tip";
|
|
|
@ -1,14 +0,0 @@
|
||||||
@import "@fontsource-variable/manrope";
|
|
||||||
@import "@fontsource-variable/source-serif-4";
|
|
||||||
@import "@fontsource-variable/jetbrains-mono";
|
|
||||||
|
|
||||||
:root
|
|
||||||
{
|
|
||||||
@sans-serif-fonts: "Manrope Variable", sans-serif;
|
|
||||||
@serif-fonts: "Source Serif 4 Variable", serif;
|
|
||||||
@monospace-fonts: "Jetbrains Mono Variable", monospace;
|
|
||||||
|
|
||||||
--sans-serif-fonts: @sans-serif-fonts;
|
|
||||||
--serif-fonts: @serif-fonts;
|
|
||||||
--monospace-fonts: @monospace-fonts;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
footer
|
|
||||||
{
|
|
||||||
margin: 4em auto;
|
|
||||||
color: var(--foreground-lightest);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
div.box
|
|
||||||
{
|
|
||||||
margin: 0.5em auto;
|
|
||||||
width: 50em;
|
|
||||||
max-width: 95%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6
|
|
||||||
{
|
|
||||||
margin: 0.33em 0 0.5em 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,198 +0,0 @@
|
||||||
a.button, button, input[type="submit"], input[type="reset"]
|
|
||||||
{
|
|
||||||
transition: background 0.2s ease, outline 0.2s ease, transform 0.1s ease;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
margin: 0.2em 0.33em;
|
|
||||||
padding: 0.45em 0.66em;
|
|
||||||
width: fit-content;
|
|
||||||
border-radius: 0.25em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
box-shadow: 0 0 0.3em 0 var(--foreground-shadow);
|
|
||||||
outline: solid 2px transparent;
|
|
||||||
outline-offset: 2px;
|
|
||||||
border: solid var(--primary-darker) thin;
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--background);
|
|
||||||
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
vertical-align: middle;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
transform: scale(1);
|
|
||||||
transform-origin: center;
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--primary-lighter);
|
|
||||||
}
|
|
||||||
&:focus
|
|
||||||
{
|
|
||||||
outline: solid 2px var(--primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active
|
|
||||||
{
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled
|
|
||||||
{
|
|
||||||
opacity: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.flat
|
|
||||||
{
|
|
||||||
box-shadow: 0 0 0 0 transparent;
|
|
||||||
border-color: var(--background-darkest);
|
|
||||||
background: var(--background-lighter);
|
|
||||||
color: var(--foreground);
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--background-lightest);
|
|
||||||
}
|
|
||||||
&:focus
|
|
||||||
{
|
|
||||||
outline-color: var(--neutral);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.green, &.validation, &.ok, &.save, &.positive, &.good, &.success
|
|
||||||
{
|
|
||||||
border-color: var(--green-darker);
|
|
||||||
background: var(--green);
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--green-lighter);
|
|
||||||
}
|
|
||||||
&:focus
|
|
||||||
{
|
|
||||||
outline-color: var(--green);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.flat
|
|
||||||
{
|
|
||||||
border-color: var(--green-background);
|
|
||||||
background: var(--green-background);
|
|
||||||
color: var(--green);
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--green-background-darker);
|
|
||||||
}
|
|
||||||
&:focus
|
|
||||||
{
|
|
||||||
outline-color: var(--green-background-darker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.orange, &.cancel, &.back, &.return, &.warning
|
|
||||||
{
|
|
||||||
border-color: var(--orange-darker);
|
|
||||||
background: var(--orange);
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--orange-lighter);
|
|
||||||
}
|
|
||||||
&:focus
|
|
||||||
{
|
|
||||||
outline-color: var(--orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.flat
|
|
||||||
{
|
|
||||||
border-color: var(--orange-background);
|
|
||||||
background: var(--orange-background);
|
|
||||||
color: var(--orange);
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--orange-background-darker);
|
|
||||||
}
|
|
||||||
&:focus
|
|
||||||
{
|
|
||||||
outline-color: var(--orange-background-darker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.red, &.delete, &.remove, &.close, &.no, &.negative, &.bad, &.error
|
|
||||||
{
|
|
||||||
border-color: var(--red-darker);
|
|
||||||
background: var(--red);
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--red-lighter);
|
|
||||||
}
|
|
||||||
&:focus
|
|
||||||
{
|
|
||||||
outline-color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.flat
|
|
||||||
{
|
|
||||||
border-color: var(--red-background);
|
|
||||||
background: var(--red-background);
|
|
||||||
color: var(--red);
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--red-background-darker);
|
|
||||||
}
|
|
||||||
&:focus
|
|
||||||
{
|
|
||||||
outline-color: var(--red-background-darker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg
|
|
||||||
{ // Icon style.
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: -0.2em;
|
|
||||||
margin-right: 0.2em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
&.icon-only svg
|
|
||||||
{
|
|
||||||
margin-right: 0.05em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div.buttons
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5em;
|
|
||||||
|
|
||||||
margin: 0.25em auto;
|
|
||||||
|
|
||||||
&.right
|
|
||||||
{
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
&.left
|
|
||||||
{
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
&.center
|
|
||||||
{
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button, .button
|
|
||||||
{
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
.card
|
|
||||||
{
|
|
||||||
margin: 0.5em auto;
|
|
||||||
padding: 1.4em;
|
|
||||||
width: 50em;
|
|
||||||
max-width: 92%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 0.25em;
|
|
||||||
|
|
||||||
border: solid var(--background-darkest) thin;
|
|
||||||
background: var(--background-lighter);
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6
|
|
||||||
{
|
|
||||||
margin: 0.33em 0 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> p
|
|
||||||
{
|
|
||||||
&:first-child
|
|
||||||
{ margin-top: auto; }
|
|
||||||
&:last-child
|
|
||||||
{ margin-bottom: auto; }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.floating
|
|
||||||
{
|
|
||||||
width: 20em;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
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.
|
|
||||||
|
|
||||||
&.closed
|
|
||||||
{ // Added when the curtain is closing and will soon be removed from DOM.
|
|
||||||
transition: transform 0.4s ease-out, filter 0.4s ease-out, opacity 0.4s ease-out;
|
|
||||||
|
|
||||||
transform: scale(1.2);
|
|
||||||
filter: blur(0.5em);
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dimmed
|
|
||||||
{ // Disable scroll and blur the content when the body is dimmed.
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> *:not(.curtain):not(.notifications)
|
|
||||||
{
|
|
||||||
transition: filter 0.4s ease-in;
|
|
||||||
|
|
||||||
filter: blur(0.25em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
|
|
||||||
@import "dates/_calendar";
|
|
||||||
@import "dates/_datepicker";
|
|
|
@ -1,60 +0,0 @@
|
||||||
main.error
|
|
||||||
{
|
|
||||||
font-size: 1.2em;
|
|
||||||
|
|
||||||
> svg
|
|
||||||
{
|
|
||||||
display: block;
|
|
||||||
margin: 1em auto;
|
|
||||||
color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
hr
|
|
||||||
{
|
|
||||||
margin: 1em auto;
|
|
||||||
|
|
||||||
width: 20em;
|
|
||||||
max-width: 50%;
|
|
||||||
height: 0.25em;
|
|
||||||
border-radius: 0.1em;
|
|
||||||
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
background: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6
|
|
||||||
{
|
|
||||||
margin: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
p, pre
|
|
||||||
{
|
|
||||||
margin: 1em auto;
|
|
||||||
width: 40em;
|
|
||||||
max-width: 95%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button
|
|
||||||
{
|
|
||||||
display: block;
|
|
||||||
margin: 1em auto;
|
|
||||||
padding: 0.66em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details
|
|
||||||
{
|
|
||||||
font-size: 0.8em;
|
|
||||||
|
|
||||||
button
|
|
||||||
{}
|
|
||||||
|
|
||||||
pre
|
|
||||||
{
|
|
||||||
width: auto;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
@import "floating/_floating";
|
|
||||||
@import "floating/_tooltip";
|
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
@import "forms/_box";
|
|
||||||
@import "forms/_datepicker-input";
|
|
||||||
@import "forms/_input";
|
|
||||||
@import "forms/_label";
|
|
||||||
@import "forms/_password-input";
|
|
|
@ -1,49 +0,0 @@
|
||||||
h1, h2, h3, h4, h5, h6
|
|
||||||
{
|
|
||||||
margin: 0.8em auto;
|
|
||||||
|
|
||||||
&.center, &.main
|
|
||||||
{ text-align: center; }
|
|
||||||
|
|
||||||
> svg
|
|
||||||
{
|
|
||||||
position: relative;
|
|
||||||
bottom: -0.15em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1
|
|
||||||
{
|
|
||||||
font-size: 2.5em;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2
|
|
||||||
{
|
|
||||||
font-size: 2em;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3
|
|
||||||
{
|
|
||||||
font-size: 1.7em;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4
|
|
||||||
{
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5
|
|
||||||
{
|
|
||||||
font-size: 1.3em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
h6
|
|
||||||
{
|
|
||||||
font-size: 1.15em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
a
|
|
||||||
{
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
ul
|
|
||||||
{
|
|
||||||
list-style: "–";
|
|
||||||
padding: 0 2em;
|
|
||||||
|
|
||||||
> li
|
|
||||||
{
|
|
||||||
padding: 0 0 0 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol
|
|
||||||
{
|
|
||||||
margin: 1em auto;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
@import "loaders/_animated-background";
|
|
||||||
|
|
||||||
@import "loaders/_generic";
|
|
||||||
@import "loaders/_list";
|
|
||||||
@import "loaders/_spinning";
|
|
|
@ -1,3 +0,0 @@
|
||||||
@import "menus/_apps-menu";
|
|
||||||
@import "menus/_main-menu";
|
|
||||||
@import "menus/_submenu";
|
|
|
@ -1,99 +0,0 @@
|
||||||
.curtain > .modal
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
width: 45em; max-width: 95%;
|
|
||||||
height: fit-content; max-height: 95%;
|
|
||||||
border-radius: 0.25em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
background: var(--background);
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
> header
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> h1
|
|
||||||
{
|
|
||||||
flex: 1;
|
|
||||||
margin: auto;
|
|
||||||
padding: 1em 1em;
|
|
||||||
|
|
||||||
font-size: 1.33em;
|
|
||||||
font-weight: 650;
|
|
||||||
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
svg
|
|
||||||
{
|
|
||||||
margin-top: -0.2em;
|
|
||||||
margin-right: 0.1em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .close
|
|
||||||
{
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
margin: auto 1em;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 2em;
|
|
||||||
|
|
||||||
box-shadow: 0 0 0 0 transparent;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--foreground-lightest);
|
|
||||||
|
|
||||||
opacity: 0.6;
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled
|
|
||||||
{
|
|
||||||
opacity: 0.33;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> main
|
|
||||||
{
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.info
|
|
||||||
{
|
|
||||||
> header > h1 { color: var(--primary); }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success
|
|
||||||
{
|
|
||||||
> header > h1 { color: var(--green); }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warning
|
|
||||||
{
|
|
||||||
> header > h1 { color: var(--orange); }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error
|
|
||||||
{
|
|
||||||
> header > h1 { color: var(--red); }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
body > ul.notifications
|
|
||||||
{ // Notifications list.
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
margin: 0 1em 1em;
|
|
||||||
padding: 0;
|
|
||||||
width: 20em;
|
|
||||||
max-width: 66%;
|
|
||||||
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
z-index: 2000;
|
|
||||||
|
|
||||||
> li.notification
|
|
||||||
{ // A single notification.
|
|
||||||
margin: 1em auto;
|
|
||||||
padding: 1em;
|
|
||||||
|
|
||||||
border-radius: 0.25em;
|
|
||||||
|
|
||||||
box-shadow: 0 0 0.5em 0 var(--foreground-shadow);
|
|
||||||
background: var(--background);
|
|
||||||
|
|
||||||
// Show an animation when entering screen.
|
|
||||||
animation: notification-in 0.3s ease-in;
|
|
||||||
transform-origin: center;
|
|
||||||
|
|
||||||
&.closed
|
|
||||||
{ // Added when the notification is closing and will soon be removed from DOM.
|
|
||||||
transition: transform 0.3s ease-out, filter 0.3s ease-out, opacity 0.3s ease-out;
|
|
||||||
|
|
||||||
transform: scale(1.15);
|
|
||||||
filter: blur(0.25em);
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.info
|
|
||||||
{
|
|
||||||
border: solid var(--primary-darker) thin;
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success
|
|
||||||
{
|
|
||||||
border: solid var(--green-darker) thin;
|
|
||||||
background: var(--green);
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warning
|
|
||||||
{
|
|
||||||
border: solid var(--orange-darker) thin;
|
|
||||||
background: var(--orange);
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error
|
|
||||||
{
|
|
||||||
border: solid var(--red-darker) thin;
|
|
||||||
background: var(--red);
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes notification-in
|
|
||||||
{ // Screen enter animation.
|
|
||||||
from
|
|
||||||
{
|
|
||||||
transform: scale(1.15);
|
|
||||||
filter: blur(0.25em);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to
|
|
||||||
{
|
|
||||||
transform: scale(1);
|
|
||||||
filter: blur(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
@import "pagination/_nav";
|
|
|
@ -1,3 +0,0 @@
|
||||||
@import "select/_input";
|
|
||||||
@import "select/_selected";
|
|
||||||
@import "select/_suggestions";
|
|
|
@ -1,24 +0,0 @@
|
||||||
.steps-counter
|
|
||||||
{
|
|
||||||
counter-reset: steps-count 0;
|
|
||||||
|
|
||||||
.step
|
|
||||||
{
|
|
||||||
&::before
|
|
||||||
{
|
|
||||||
content: counter(steps-count);
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 0.2em;
|
|
||||||
|
|
||||||
color: var(--primary);
|
|
||||||
|
|
||||||
font-family: var(--monospace-fonts);
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: 700;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
counter-increment: steps-count;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
div.steps
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
> nav.steps
|
|
||||||
{
|
|
||||||
position: sticky;
|
|
||||||
top: 1em;
|
|
||||||
bottom: 1em;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
margin: auto 0.5em;
|
|
||||||
|
|
||||||
> ul
|
|
||||||
{
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
> li
|
|
||||||
{
|
|
||||||
margin: auto;
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> button
|
|
||||||
{
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
box-shadow: 0 0 0 0 transparent;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--foreground-lightest);
|
|
||||||
|
|
||||||
&:focus::before
|
|
||||||
{
|
|
||||||
outline-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before
|
|
||||||
{
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
content: "";
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
margin: auto auto 0.4em auto;
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
border-radius: 2em;
|
|
||||||
|
|
||||||
box-shadow: 0 0 0 0 transparent;
|
|
||||||
outline: solid 2px transparent;
|
|
||||||
outline-offset: 2px;
|
|
||||||
border: solid var(--background-darkest) thin;
|
|
||||||
background: var(--background-lightest);
|
|
||||||
|
|
||||||
transform: scale(0.75);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active
|
|
||||||
{
|
|
||||||
> button
|
|
||||||
{
|
|
||||||
&::before
|
|
||||||
{
|
|
||||||
box-shadow: 0 0 0.3em 0 var(--foreground-shadow);
|
|
||||||
border-color: var(--primary-darker);
|
|
||||||
background: var(--primary);
|
|
||||||
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> :not(nav)
|
|
||||||
{
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
|
|
||||||
@subapp-margin: 1.5em;
|
|
||||||
|
|
||||||
.curtain > .subapp
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
top: @subapp-margin;
|
|
||||||
left: @subapp-margin;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: calc(100% - (2 * @subapp-margin));
|
|
||||||
height: calc(100% - (2 * @subapp-margin));
|
|
||||||
border-radius: 0.25em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
background: var(--background);
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
> header
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> h1
|
|
||||||
{
|
|
||||||
flex: 1;
|
|
||||||
margin: auto;
|
|
||||||
padding: 1em 1em;
|
|
||||||
|
|
||||||
font-size: 1.66em;
|
|
||||||
font-weight: 650;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .close
|
|
||||||
{
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
|
|
||||||
width: 3em;
|
|
||||||
height: 3em;
|
|
||||||
margin: auto 1em;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 2em;
|
|
||||||
|
|
||||||
box-shadow: 0 0 0 0 transparent;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--foreground-lightest);
|
|
||||||
|
|
||||||
opacity: 0.6;
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled
|
|
||||||
{
|
|
||||||
opacity: 0.33;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> main
|
|
||||||
{
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
padding: 1em;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
table
|
|
||||||
{
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
border-collapse: collapse;
|
|
||||||
|
|
||||||
background: var(--background-lighter);
|
|
||||||
//border: solid var(--background-darkest) thin;
|
|
||||||
border-radius: 0.25em;
|
|
||||||
|
|
||||||
thead
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody
|
|
||||||
{
|
|
||||||
tr
|
|
||||||
{
|
|
||||||
&:nth-child(even)
|
|
||||||
{
|
|
||||||
background: var(--background-lightest);
|
|
||||||
}
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--background-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child { border-bottom: none; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr
|
|
||||||
{
|
|
||||||
transition: background 0.1s ease;
|
|
||||||
border-bottom: solid var(--background-darkest) thin;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td
|
|
||||||
{
|
|
||||||
padding: 0.4em 0.6em;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
p.tip
|
|
||||||
{
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
width: 30em;
|
|
||||||
padding: 0.3em 0.5em 0.3em 3em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
border: solid var(--background-darkest) thin;
|
|
||||||
color: var(--foreground-lightest);
|
|
||||||
|
|
||||||
svg
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
top: 0.3em;
|
|
||||||
left: 0.5em;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
color: var(--primary);
|
|
||||||
font-size: 1.5em;
|
|
||||||
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
table.calendar
|
|
||||||
{
|
|
||||||
thead th
|
|
||||||
{
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody > tr
|
|
||||||
{
|
|
||||||
border-bottom: none;
|
|
||||||
|
|
||||||
&:nth-child(even) { background: transparent; }
|
|
||||||
&:hover { background: transparent; }
|
|
||||||
|
|
||||||
> td
|
|
||||||
{
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.day
|
|
||||||
{
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 2.2em;
|
|
||||||
height: 2.2em;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
color: var(--foreground);
|
|
||||||
|
|
||||||
font-weight: 400;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: var(--background-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected
|
|
||||||
{
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.faded
|
|
||||||
{
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
.datepicker
|
|
||||||
{
|
|
||||||
position: relative;
|
|
||||||
padding: 0 1em;
|
|
||||||
background: var(--background-lighter);
|
|
||||||
font-size: 0.9em;
|
|
||||||
|
|
||||||
.year-month
|
|
||||||
{
|
|
||||||
font-size: 1.1em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button
|
|
||||||
{
|
|
||||||
&.previous-month, &.next-month
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
padding: 0;
|
|
||||||
width: 2em;
|
|
||||||
height: 2em;
|
|
||||||
|
|
||||||
svg
|
|
||||||
{ margin: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.previous-month
|
|
||||||
{
|
|
||||||
left: -1.1em;
|
|
||||||
}
|
|
||||||
&.next-month
|
|
||||||
{
|
|
||||||
right: -1.1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
:not(.card).floating
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
align-self: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
> .card.floating
|
|
||||||
{
|
|
||||||
margin: 0.4em;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
.card.floating.tooltip
|
|
||||||
{
|
|
||||||
width: unset;
|
|
||||||
max-width: 20em;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
border-color: var(--foreground-darker);
|
|
||||||
background: var(--foreground-lightest);
|
|
||||||
color: var(--background);
|
|
||||||
|
|
||||||
font-size: 0.9em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
label.box, label.toggleswitch
|
|
||||||
{
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
&::before
|
|
||||||
{
|
|
||||||
content: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
> input
|
|
||||||
{
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> input[type="radio"] + .button
|
|
||||||
{
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .button
|
|
||||||
{
|
|
||||||
margin: 0 0.6em 0.12em 0;
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
color: var(--background-darkest);
|
|
||||||
|
|
||||||
> svg
|
|
||||||
{
|
|
||||||
display: block;
|
|
||||||
margin: 0.22em auto 0;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within > .button
|
|
||||||
{
|
|
||||||
outline-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
> input:not(:checked) + .button
|
|
||||||
{
|
|
||||||
background: var(--background-lighter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
label.toggleswitch
|
|
||||||
{
|
|
||||||
> .button
|
|
||||||
{
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 2.5em;
|
|
||||||
|
|
||||||
> .switch
|
|
||||||
{
|
|
||||||
transition: margin-left 0.25s ease, box-shadow 0.25s ease;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
margin: auto 0.25em;
|
|
||||||
width: 0.9em;
|
|
||||||
height: 0.8em;
|
|
||||||
border-radius: 0.25em;
|
|
||||||
box-shadow: 0 0 0 0 var(--foreground-shadow);
|
|
||||||
border: solid var(--background-darkest) thin;
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> input:checked + .button > .switch
|
|
||||||
{
|
|
||||||
margin-left: 1.2em;
|
|
||||||
border-color: transparent;
|
|
||||||
box-shadow: 0 0 0.4em 0 var(--foreground-shadow);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
label.datepicker-input
|
|
||||||
{
|
|
||||||
.card.datepicker
|
|
||||||
{
|
|
||||||
padding: 0.2em;
|
|
||||||
width: fit-content;
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue