diff --git a/demo/DemoApp.tsx b/demo/DemoApp.tsx index 0e06b57..e90abc4 100644 --- a/demo/DemoApp.tsx +++ b/demo/DemoApp.tsx @@ -2,7 +2,7 @@ import React, {useState} from "react"; import "../index"; import {Checkbox} from "../src/Components/Forms/Checkbox"; import { Radio } from "../src/Components/Forms/Radio"; -import {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 {PasswordInput} from "../src/Components/Forms/PasswordInput"; 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 {Submenu} from "../src/Components/Menus/Submenu"; 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() { @@ -27,10 +30,10 @@ export function DemoApp() const [selected, setSelected] = useState(null); return ( -
+ - Home - Test + Home + Test Test 1 @@ -44,12 +47,12 @@ export function DemoApp() First last choice Another last choice - }>Submenu in submenu + }> Submenu in submenu - }>Submenu + }> Submenu }> - Submenu + Submenu @@ -58,12 +61,8 @@ export function DemoApp()

TODO

+ +

App selectors

+ + + + + Home + + + + Test link + + + + Test 3 + + + + + ); } diff --git a/demo/NavTest.tsx b/demo/NavTest.tsx new file mode 100644 index 0000000..1afa84e --- /dev/null +++ b/demo/NavTest.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import {Card} from "../src/Components/Card"; + +/** + * Navigation test component. + */ +export function NavTest() +{ + return ( + + This is a navigation test. + + ) +} diff --git a/demo/demo.tsx b/demo/demo.tsx index 1d3361d..8ce8eb1 100644 --- a/demo/demo.tsx +++ b/demo/demo.tsx @@ -1,11 +1,32 @@ 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"; + +// Router initialization. +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + path: "test", + element: , + } + ], + } +]) document.addEventListener("DOMContentLoaded", () => { const demoApp = document.getElementById("demo-app"); const root = createRoot(demoApp); - root.render(); + root.render( + Footer test. + + } />); }); diff --git a/package.json b/package.json index 5d82e6e..b0d221c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "@fontsource-variable/source-serif-4": "^5.0.19", "@phosphor-icons/react": "^2.1.5", "react": "^18.3.1", - "react-merge-refs": "^2.1.1" + "react-merge-refs": "^2.1.1", + "react-router-dom": "^6.24.1" }, "devDependencies": { "@types/react": "^18.3.3", diff --git a/src/Application/Application.tsx b/src/Application/Application.tsx new file mode 100644 index 0000000..3676892 --- /dev/null +++ b/src/Application/Application.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +/** + * Main Kernel UI application. + */ +export function Application({children}: React.PropsWithChildren<{}>) +{ + return ( +
+ {children} +
+ ); +} diff --git a/src/Application/Kernel.tsx b/src/Application/Kernel.tsx new file mode 100644 index 0000000..7aef8b0 --- /dev/null +++ b/src/Application/Kernel.tsx @@ -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; +}) +{ + return ( + + {header} + + {footer} + + ); +} diff --git a/src/Components/Dates/Datepicker.tsx b/src/Components/Dates/Datepicker.tsx index edf6ceb..188882e 100644 --- a/src/Components/Dates/Datepicker.tsx +++ b/src/Components/Dates/Datepicker.tsx @@ -46,7 +46,7 @@ export function Datepicker({date, onDateSelected, locale, className, ...divProps onDateSelected(newDate); }, [date, onDateSelected]) }> - + @@ -57,7 +57,7 @@ export function Datepicker({date, onDateSelected, locale, className, ...divProps onDateSelected(newDate); }, [date, onDateSelected]) }> - + diff --git a/src/Components/Forms/Checkbox.tsx b/src/Components/Forms/Checkbox.tsx index a3fd756..3fd9fc6 100644 --- a/src/Components/Forms/Checkbox.tsx +++ b/src/Components/Forms/Checkbox.tsx @@ -7,7 +7,7 @@ export function Checkbox({children, className, type, ...inputProps}: React.Props return ( ); diff --git a/src/Components/Forms/PasswordInput.tsx b/src/Components/Forms/PasswordInput.tsx index 1cd5197..4b57ee3 100644 --- a/src/Components/Forms/PasswordInput.tsx +++ b/src/Components/Forms/PasswordInput.tsx @@ -15,7 +15,7 @@ export function PasswordInput({children, className, type, ...props}: React.Props setShowPassword(!showPassword); }}> { - showPassword ? : + showPassword ? : } diff --git a/src/Components/Forms/Radio.tsx b/src/Components/Forms/Radio.tsx index 9e24aa0..ea64198 100644 --- a/src/Components/Forms/Radio.tsx +++ b/src/Components/Forms/Radio.tsx @@ -7,7 +7,7 @@ export function Radio({children, className, type, ...inputProps}: React.PropsWit return ( ); diff --git a/src/Components/Menus/AppsMenu.tsx b/src/Components/Menus/AppsMenu.tsx new file mode 100644 index 0000000..e4a1db2 --- /dev/null +++ b/src/Components/Menus/AppsMenu.tsx @@ -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) +{ + return ( + + {(value) => ( + + + + )} + + ); +} + +/** + * Component of an app item in apps menu. + */ +export function AppItem({className, children, ...props}: React.HTMLAttributes) +{ + return ( +
  • + + {children} + +
  • + ); +} + +/** + * Component of an app link in apps menu. + */ +export function AppLink({className, children, ...props}: NavLinkProps & React.HTMLAttributes) +{ + return ( +
  • + + {children} + +
  • + ); +} diff --git a/src/Components/Menus/MainMenuItem.tsx b/src/Components/Menus/MainMenuItem.tsx index ef4c202..278b569 100644 --- a/src/Components/Menus/MainMenuItem.tsx +++ b/src/Components/Menus/MainMenuItem.tsx @@ -1,7 +1,11 @@ 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, { }>>; @@ -17,6 +21,24 @@ export const MainMenuItem = React.forwardRef, { +}>>; + +/** + * A main menu item link. + */ +export const MainMenuLink = React.forwardRef(function MainMenuLink({children, ...props}: MainMenuLinkProperties, ref) +{ + return ( +
  • + {children} +
  • + ); +}); + /** * A main menu item that open a submenu. */ diff --git a/src/Components/Menus/SubmenuItem.tsx b/src/Components/Menus/SubmenuItem.tsx index 1a5dccb..0b750fa 100644 --- a/src/Components/Menus/SubmenuItem.tsx +++ b/src/Components/Menus/SubmenuItem.tsx @@ -1,7 +1,11 @@ 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, { }>>; @@ -15,6 +19,22 @@ export const SubmenuItem = React.forwardRef, { +}>>; + +/** + * A submenu item link. + */ +export const SubmenuLink = React.forwardRef(function SubmenuLink({className, children, ...props}: SubmenuLinkProperties, ref) +{ + return ( + {children} + ); +}); + /** * A submenu item that open a submenu. */ diff --git a/src/Components/Select/OptionsSuggestions.tsx b/src/Components/Select/OptionsSuggestions.tsx index dbaea7b..6e6a06d 100644 --- a/src/Components/Select/OptionsSuggestions.tsx +++ b/src/Components/Select/OptionsSuggestions.tsx @@ -126,7 +126,7 @@ export function OptionsSuggestions({options onClick={() => { onSelected(key, option); }}> {(renderOption ?? defaultRenderOption)(option)} - + )); } diff --git a/src/Components/Select/Select.tsx b/src/Components/Select/Select.tsx index e3852a2..1261cfc 100644 --- a/src/Components/Select/Select.tsx +++ b/src/Components/Select/Select.tsx @@ -205,17 +205,17 @@ export function Select( {...props} /> - +
      { // Showing each selected value. selectedOptions.map(([optionKey, option]) => (
    • - +
      {(renderOption ?? defaultRenderOption)(option)}
    • )) diff --git a/src/styles/_colors.less b/src/styles/_colors.less index c4653e5..b93d2e4 100644 --- a/src/styles/_colors.less +++ b/src/styles/_colors.less @@ -85,4 +85,5 @@ @menu-hover: rgba(255, 255, 255, 0.15); --menu-hover: @menu-hover; + @menu-active: rgba(0, 0, 0, 0.125); --menu-active: @menu-active; } diff --git a/src/styles/components/_button.less b/src/styles/components/_button.less index d6178bd..335d8dd 100644 --- a/src/styles/components/_button.less +++ b/src/styles/components/_button.less @@ -152,13 +152,13 @@ a.button, button, input[type="submit"], input[type="reset"] } svg - { // Icon style.* + { // Icon style. display: inline-block; margin-top: -0.2em; margin-right: 0.2em; vertical-align: middle; } - > svg:last-of-type + &.icon-only svg { margin-right: 0.05em; } diff --git a/src/styles/components/_menus.less b/src/styles/components/_menus.less index 3bc0861..a1c45f2 100644 --- a/src/styles/components/_menus.less +++ b/src/styles/components/_menus.less @@ -1,2 +1,3 @@ +@import "menus/_apps-menu"; @import "menus/_main-menu"; @import "menus/_submenu"; diff --git a/src/styles/components/menus/_apps-menu.less b/src/styles/components/menus/_apps-menu.less new file mode 100644 index 0000000..851f42c --- /dev/null +++ b/src/styles/components/menus/_apps-menu.less @@ -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; + } + } +} diff --git a/src/styles/components/menus/_main-menu.less b/src/styles/components/menus/_main-menu.less index ffe1217..f49e922 100644 --- a/src/styles/components/menus/_main-menu.less +++ b/src/styles/components/menus/_main-menu.less @@ -1,8 +1,13 @@ nav.main.menu { + position: sticky; + top: 0; + background: var(--primary-gradient); color: var(--background); + z-index: 2; + .floating { justify-content: flex-start; @@ -47,6 +52,19 @@ nav.main.menu { 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); } + + &.active + { + background: var(--menu-active); + } } } } diff --git a/src/styles/components/menus/_submenu.less b/src/styles/components/menus/_submenu.less index 166aee1..d002b13 100644 --- a/src/styles/components/menus/_submenu.less +++ b/src/styles/components/menus/_submenu.less @@ -12,7 +12,7 @@ justify-content: flex-end; } - .submenu + > .submenu { display: flex; flex-direction: column; @@ -32,6 +32,14 @@ { background: var(--background-darker); } + + svg + { // Icon style. + display: inline-block; + margin-top: -0.2em; + margin-right: 0.2em; + vertical-align: middle; + } } } } diff --git a/yarn.lock b/yarn.lock index edf1fe6..2842f13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -651,6 +651,13 @@ __metadata: languageName: node 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": version: 5.1.0 resolution: "@rollup/pluginutils@npm:5.1.0" @@ -1826,6 +1833,7 @@ __metadata: react: "npm:^18.3.1" react-dom: "npm:^18.3.1" react-merge-refs: "npm:^2.1.1" + react-router-dom: "npm:^6.24.1" typescript: "npm:^5.4.5" vite: "npm:^5.2.11" vite-plugin-dts: "npm:^3.9.1" @@ -2313,6 +2321,30 @@ __metadata: languageName: node 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": version: 18.3.1 resolution: "react@npm:18.3.1"