General application & routing setup, apps menu, links in menus, icons in menus, visual improvements.

This commit is contained in:
Madeorsk 2024-07-07 18:37:34 +02:00
parent 97cf06bc7f
commit 3305f09d32
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
22 changed files with 333 additions and 32 deletions

View file

@ -2,7 +2,7 @@ import React, {useState} from "react";
import "../index"; import "../index";
import {Checkbox} from "../src/Components/Forms/Checkbox"; import {Checkbox} from "../src/Components/Forms/Checkbox";
import { Radio } from "../src/Components/Forms/Radio"; import { Radio } from "../src/Components/Forms/Radio";
import {FloppyDisk, TrashSimple, XCircle} from "@phosphor-icons/react"; import {AirTrafficControl, Basket, FloppyDisk, House, TrashSimple, XCircle} from "@phosphor-icons/react";
import {Card} from "../src/Components/Card"; import {Card} from "../src/Components/Card";
import {PasswordInput} from "../src/Components/Forms/PasswordInput"; import {PasswordInput} from "../src/Components/Forms/PasswordInput";
import {RequiredField} from "../src/Components/Forms/RequiredField"; import {RequiredField} from "../src/Components/Forms/RequiredField";
@ -18,7 +18,10 @@ import {MainMenu} from "../src/Components/Menus/MainMenu";
import {SubmenuFloat} from "../src/Components/Menus/SubmenuFloat"; import {SubmenuFloat} from "../src/Components/Menus/SubmenuFloat";
import {Submenu} from "../src/Components/Menus/Submenu"; import {Submenu} from "../src/Components/Menus/Submenu";
import {SubmenuItem, SubmenuItemSubmenu} from "../src/Components/Menus/SubmenuItem"; import {SubmenuItem, SubmenuItemSubmenu} from "../src/Components/Menus/SubmenuItem";
import {MainMenuItem, MainMenuItemSubmenu} from "../src/Components/Menus/MainMenuItem"; 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";
export function DemoApp() export function DemoApp()
{ {
@ -27,10 +30,10 @@ export function DemoApp()
const [selected, setSelected] = useState(null); const [selected, setSelected] = useState(null);
return ( return (
<main className={"app"}> <Application>
<MainMenu> <MainMenu>
<MainMenuItem href={"/"}>Home</MainMenuItem> <MainMenuLink to={"/"}><House /> Home</MainMenuLink>
<MainMenuItem href={"#"}>Test</MainMenuItem> <MainMenuLink to={"/test"}><AirTrafficControl /> Test</MainMenuLink>
<MainMenuItemSubmenu submenu={ <MainMenuItemSubmenu submenu={
<Submenu> <Submenu>
<SubmenuItem>Test 1</SubmenuItem> <SubmenuItem>Test 1</SubmenuItem>
@ -44,12 +47,12 @@ export function DemoApp()
<SubmenuItem href={"#first-last-choice"}>First last choice</SubmenuItem> <SubmenuItem href={"#first-last-choice"}>First last choice</SubmenuItem>
<SubmenuItem>Another last choice</SubmenuItem> <SubmenuItem>Another last choice</SubmenuItem>
</Submenu> </Submenu>
}>Submenu in submenu</SubmenuItemSubmenu> }><Basket /> Submenu in submenu</SubmenuItemSubmenu>
</Submenu> </Submenu>
}>Submenu</SubmenuItemSubmenu> }><Basket /> Submenu</SubmenuItemSubmenu>
</Submenu> </Submenu>
}> }>
Submenu <Basket /> Submenu
</MainMenuItemSubmenu> </MainMenuItemSubmenu>
</MainMenu> </MainMenu>
@ -58,12 +61,8 @@ export function DemoApp()
<h2>TODO</h2> <h2>TODO</h2>
<ul> <ul>
<li>Dropdown menus</li>
<li>Main menu</li>
<li>Tabs / Apps selectors</li>
<li>App steps</li> <li>App steps</li>
<li>Pagination</li> <li>Pagination</li>
<li>Apps</li>
<li>Global states</li> <li>Global states</li>
<li>Async</li> <li>Async</li>
<li>Subapps</li> <li>Subapps</li>
@ -89,9 +88,9 @@ export function DemoApp()
<button type={"button"}>A cool button</button> <button type={"button"}>A cool button</button>
<a className={"button"} href={"#"}>A link button</a> <a className={"button"} href={"#"}>A link button</a>
<button type={"button"} className={"flat"}>A flat button</button> <button type={"button"} className={"flat"}>A flat button</button>
<button type={"button"} className={"validation"}><FloppyDisk weight={"bold"}/> A validation button</button> <button type={"button"} className={"validation"}><FloppyDisk /> A validation button</button>
<button type={"button"} className={"cancel"}><XCircle weight={"bold"}/> A cancellation button</button> <button type={"button"} className={"cancel"}><XCircle /> A cancellation button</button>
<button type={"button"} className={"delete"}><TrashSimple weight={"bold"}/> A deletion button</button> <button type={"button"} className={"delete"}><TrashSimple /> A deletion button</button>
<h2>Forms</h2> <h2>Forms</h2>
@ -303,15 +302,34 @@ export function DemoApp()
<SubmenuItemSubmenu submenu={ <SubmenuItemSubmenu submenu={
<Submenu> <Submenu>
<SubmenuItem>Test A</SubmenuItem> <SubmenuItem>Test A</SubmenuItem>
<SubmenuItem>Test B</SubmenuItem> <SubmenuItem><AirTrafficControl /> Test B</SubmenuItem>
</Submenu> </Submenu>
}> }>
Submenu <Basket /> Submenu
</SubmenuItemSubmenu> </SubmenuItemSubmenu>
</Submenu> </Submenu>
} floatingOptions={{ placement: "right-start" }}> } floatingOptions={{ placement: "right-start" }}>
<button>Submenu on a button</button> <button>Submenu on a button</button>
</SubmenuFloat> </SubmenuFloat>
</main>
<h2>App selectors</h2>
<AppsMenu>
<AppLink to={"/"}>
<House />
Home
</AppLink>
<AppLink to={"/test"}>
<AirTrafficControl />
Test link
</AppLink>
<AppItem>
<Basket />
Test 3
</AppItem>
</AppsMenu>
<Outlet />
</Application>
); );
} }

14
demo/NavTest.tsx Normal file
View file

@ -0,0 +1,14 @@
import React from "react";
import {Card} from "../src/Components/Card";
/**
* Navigation test component.
*/
export function NavTest()
{
return (
<Card>
This is a navigation test.
</Card>
)
}

View file

@ -1,11 +1,32 @@
import React from "react"; import React from "react";
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";
import {DemoApp} from "./DemoApp"; import {DemoApp} from "./DemoApp";
import {createBrowserRouter} from "react-router-dom";
import {Kernel} from "../src/Application/Kernel";
import {NavTest} from "./NavTest";
// Router initialization.
const router = createBrowserRouter([
{
path: "/",
element: <DemoApp />,
children: [
{
path: "test",
element: <NavTest />,
}
],
}
])
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const demoApp = document.getElementById("demo-app"); const demoApp = document.getElementById("demo-app");
const root = createRoot(demoApp); const root = createRoot(demoApp);
root.render(<DemoApp />); root.render(<Kernel router={router} footer={
<footer>
Footer test.
</footer>
} />);
}); });

View file

@ -17,7 +17,8 @@
"@fontsource-variable/source-serif-4": "^5.0.19", "@fontsource-variable/source-serif-4": "^5.0.19",
"@phosphor-icons/react": "^2.1.5", "@phosphor-icons/react": "^2.1.5",
"react": "^18.3.1", "react": "^18.3.1",
"react-merge-refs": "^2.1.1" "react-merge-refs": "^2.1.1",
"react-router-dom": "^6.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.3", "@types/react": "^18.3.3",

View file

@ -0,0 +1,13 @@
import React from "react";
/**
* Main Kernel UI application.
*/
export function Application({children}: React.PropsWithChildren<{}>)
{
return (
<main className={"app"}>
{children}
</main>
);
}

View file

@ -0,0 +1,24 @@
import { IconContext } from "@phosphor-icons/react";
import React from "react";
import {createBrowserRouter, RouterProvider} from "react-router-dom";
/**
* 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 (
<IconContext.Provider value={{
size: 16,
weight: "bold",
}}>
{header}
<RouterProvider router={router} />
{footer}
</IconContext.Provider>
);
}

View file

@ -46,7 +46,7 @@ export function Datepicker({date, onDateSelected, locale, className, ...divProps
onDateSelected(newDate); onDateSelected(newDate);
}, [date, onDateSelected]) }, [date, onDateSelected])
}> }>
<CaretLeft weight={"bold"}/> <CaretLeft />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content={nextMonthName}> <Tooltip content={nextMonthName}>
@ -57,7 +57,7 @@ export function Datepicker({date, onDateSelected, locale, className, ...divProps
onDateSelected(newDate); onDateSelected(newDate);
}, [date, onDateSelected]) }, [date, onDateSelected])
}> }>
<CaretRight weight={"bold"}/> <CaretRight />
</button> </button>
</Tooltip> </Tooltip>
</div> </div>

View file

@ -7,7 +7,7 @@ export function Checkbox({children, className, type, ...inputProps}: React.Props
return ( return (
<label className={classes("box", className)}> <label className={classes("box", className)}>
<input type={"checkbox"} {...inputProps} /> <input type={"checkbox"} {...inputProps} />
<a className={"button"} tabIndex={-1}><Check weight={"bold"} /></a> <a className={"button"} tabIndex={-1}><Check /></a>
{children} {children}
</label> </label>
); );

View file

@ -15,7 +15,7 @@ export function PasswordInput({children, className, type, ...props}: React.Props
setShowPassword(!showPassword); setShowPassword(!showPassword);
}}> }}>
{ {
showPassword ? <EyeSlash weight={"bold"} /> : <Eye weight={"bold"} /> showPassword ? <EyeSlash /> : <Eye />
} }
</a> </a>
</div> </div>

View file

@ -7,7 +7,7 @@ export function Radio({children, className, type, ...inputProps}: React.PropsWit
return ( return (
<label className={classes("box", className)}> <label className={classes("box", className)}>
<input type={"radio"} {...inputProps} /> <input type={"radio"} {...inputProps} />
<a className={"button"} tabIndex={-1}><Check weight={"bold"} /></a> <a className={"button"} tabIndex={-1}><Check /></a>
{children} {children}
</label> </label>
); );

View file

@ -0,0 +1,54 @@
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,
})}>
<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>
);
}

View file

@ -1,7 +1,11 @@
import React from "react"; import React from "react";
import {classes, Modify} from "../../Utils"; import {classes, Modify} from "../../Utils";
import {SubmenuFloat} from "./SubmenuFloat"; import {SubmenuFloat} from "./SubmenuFloat";
import {NavLink, NavLinkProps} from "react-router-dom";
/**
* Main menu item properties.
*/
export type MainMenuItemProperties = React.PropsWithChildren<Modify<React.AnchorHTMLAttributes<HTMLAnchorElement>, { export type MainMenuItemProperties = React.PropsWithChildren<Modify<React.AnchorHTMLAttributes<HTMLAnchorElement>, {
}>>; }>>;
@ -17,6 +21,24 @@ export const MainMenuItem = React.forwardRef<HTMLAnchorElement, MainMenuItemProp
); );
}); });
/**
* 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. * A main menu item that open a submenu.
*/ */

View file

@ -1,7 +1,11 @@
import React from "react"; import React from "react";
import {classes, Modify} from "../../Utils"; import {classes, Modify} from "../../Utils";
import {SubmenuFloat} from "./SubmenuFloat"; import {SubmenuFloat} from "./SubmenuFloat";
import {NavLink, NavLinkProps} from "react-router-dom";
/**
* Submenu item properties.
*/
export type SubmenuItemProperties = React.PropsWithChildren<Modify<React.AnchorHTMLAttributes<HTMLAnchorElement>, { export type SubmenuItemProperties = React.PropsWithChildren<Modify<React.AnchorHTMLAttributes<HTMLAnchorElement>, {
}>>; }>>;
@ -15,6 +19,22 @@ export const SubmenuItem = React.forwardRef<HTMLAnchorElement, SubmenuItemProper
); );
}); });
/**
* 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. * A submenu item that open a submenu.
*/ */

View file

@ -126,7 +126,7 @@ export function OptionsSuggestions<OptionKey extends keyof any, Option>({options
onClick={() => { onSelected(key, option); }}> onClick={() => { onSelected(key, option); }}>
{(renderOption ?? defaultRenderOption)(option)} {(renderOption ?? defaultRenderOption)(option)}
<span className={"selected"}><Check weight={"bold"} /></span> <span className={"selected"}><Check /></span>
</a> </a>
)); ));
} }

View file

@ -205,17 +205,17 @@ export function Select<OptionKey extends keyof any, Option>(
{...props} /> {...props} />
</Suggestible> </Suggestible>
<a className={"button"} tabIndex={-1}><CaretDown weight={"bold"}/></a> <a className={"button"} tabIndex={-1}><CaretDown /></a>
</div> </div>
<ul className={"selected"}> <ul className={"selected"}>
{ // Showing each selected value. { // Showing each selected value.
selectedOptions.map(([optionKey, option]) => ( selectedOptions.map(([optionKey, option]) => (
<li key={String(optionKey)}> <li key={String(optionKey)}>
<Check weight={"bold"}/> <Check />
<div className={"option"}>{(renderOption ?? defaultRenderOption)(option)}</div> <div className={"option"}>{(renderOption ?? defaultRenderOption)(option)}</div>
<button className={"remove flat"} type={"button"} onClick={() => handleDeselectedOption(optionKey)}> <button className={"remove flat"} type={"button"} onClick={() => handleDeselectedOption(optionKey)}>
<X weight={"bold"}/> <X />
</button> </button>
</li> </li>
)) ))

View file

@ -85,4 +85,5 @@
@menu-hover: rgba(255, 255, 255, 0.15); --menu-hover: @menu-hover; @menu-hover: rgba(255, 255, 255, 0.15); --menu-hover: @menu-hover;
@menu-active: rgba(0, 0, 0, 0.125); --menu-active: @menu-active;
} }

View file

@ -152,13 +152,13 @@ a.button, button, input[type="submit"], input[type="reset"]
} }
svg svg
{ // Icon style.* { // Icon style.
display: inline-block; display: inline-block;
margin-top: -0.2em; margin-top: -0.2em;
margin-right: 0.2em; margin-right: 0.2em;
vertical-align: middle; vertical-align: middle;
} }
> svg:last-of-type &.icon-only svg
{ {
margin-right: 0.05em; margin-right: 0.05em;
} }

View file

@ -1,2 +1,3 @@
@import "menus/_apps-menu";
@import "menus/_main-menu"; @import "menus/_main-menu";
@import "menus/_submenu"; @import "menus/_submenu";

View file

@ -0,0 +1,49 @@
nav.apps.menu
{
> ul
{
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
gap: 0.66em;
margin: auto;
padding: 0;
list-style: none;
> li
{
padding: 0;
}
}
a.app
{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: auto;
padding: 1em;
min-width: 10em;
font-size: 1.1em;
svg
{
display: block;
margin: 0 auto 0.5em auto;
color: var(--primary);
}
&.active
{
outline: solid 2px var(--primary);
outline-offset: 2px;
}
}
}

View file

@ -1,8 +1,13 @@
nav.main.menu nav.main.menu
{ {
position: sticky;
top: 0;
background: var(--primary-gradient); background: var(--primary-gradient);
color: var(--background); color: var(--background);
z-index: 2;
.floating .floating
{ {
justify-content: flex-start; justify-content: flex-start;
@ -47,6 +52,19 @@ nav.main.menu
{ {
background: var(--menu-hover); background: var(--menu-hover);
} }
&.active
{
background: var(--menu-active);
}
svg
{ // Icon style.
display: inline-block;
margin-top: -0.2em;
margin-right: 0.2em;
vertical-align: middle;
}
} }
} }
} }
@ -68,6 +86,11 @@ nav.main.menu
{ {
background: var(--menu-hover); background: var(--menu-hover);
} }
&.active
{
background: var(--menu-active);
}
} }
} }
} }

View file

@ -12,7 +12,7 @@
justify-content: flex-end; justify-content: flex-end;
} }
.submenu > .submenu
{ {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -32,6 +32,14 @@
{ {
background: var(--background-darker); background: var(--background-darker);
} }
svg
{ // Icon style.
display: inline-block;
margin-top: -0.2em;
margin-right: 0.2em;
vertical-align: middle;
}
} }
} }
} }

View file

@ -651,6 +651,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@remix-run/router@npm:1.17.1":
version: 1.17.1
resolution: "@remix-run/router@npm:1.17.1"
checksum: 10c0/bee1631feb03975b64e1c7b574da432a05095dda2ff0f164c737e4952841a58d7b9861de87bd13a977fd970c74dcf8c558fc2d26c6ec01a9ae9041b1b4430869
languageName: node
linkType: hard
"@rollup/pluginutils@npm:^5.1.0": "@rollup/pluginutils@npm:^5.1.0":
version: 5.1.0 version: 5.1.0
resolution: "@rollup/pluginutils@npm:5.1.0" resolution: "@rollup/pluginutils@npm:5.1.0"
@ -1826,6 +1833,7 @@ __metadata:
react: "npm:^18.3.1" react: "npm:^18.3.1"
react-dom: "npm:^18.3.1" react-dom: "npm:^18.3.1"
react-merge-refs: "npm:^2.1.1" react-merge-refs: "npm:^2.1.1"
react-router-dom: "npm:^6.24.1"
typescript: "npm:^5.4.5" typescript: "npm:^5.4.5"
vite: "npm:^5.2.11" vite: "npm:^5.2.11"
vite-plugin-dts: "npm:^3.9.1" vite-plugin-dts: "npm:^3.9.1"
@ -2313,6 +2321,30 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-router-dom@npm:^6.24.1":
version: 6.24.1
resolution: "react-router-dom@npm:6.24.1"
dependencies:
"@remix-run/router": "npm:1.17.1"
react-router: "npm:6.24.1"
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: 10c0/458c6c539304984c47b0ad8d5d5b1f8859cc0845e47591d530cb4fcb13498f70a89b42bc4daeea55d57cfa08408b453bcf601cabb2c987f554cdcac13805caa8
languageName: node
linkType: hard
"react-router@npm:6.24.1":
version: 6.24.1
resolution: "react-router@npm:6.24.1"
dependencies:
"@remix-run/router": "npm:1.17.1"
peerDependencies:
react: ">=16.8"
checksum: 10c0/f50c78ca52c5154ab933c17708125e8bf71ccf2072993a80302526a0a23db9ceac6e36d5c891d62ccd16f13e60cd1b6533a2036523d1b09e0148ac49e34b2e83
languageName: node
linkType: hard
"react@npm:^18.3.1": "react@npm:^18.3.1":
version: 18.3.1 version: 18.3.1
resolution: "react@npm:18.3.1" resolution: "react@npm:18.3.1"