@@ -42,8 +42,8 @@ export function Aside({ className, children, ...restProps }: HeaderProps): React
);
}
-export function Main({ className, children, fullWidth, ...restProps }: MainProps): ReactNode {
- const rootClassName = classNames(styles.main, fullWidth && styles.mainFullWidth, className);
+export function Main({ className, children, ...restProps }: MainProps): ReactNode {
+ const rootClassName = classNames(styles.main, className);
return (
@@ -51,3 +51,11 @@ export function Main({ className, children, fullWidth, ...restProps }: MainProps
);
}
+
+/** Set of UI components to define page layout. */
+export const Layout = {
+ Root,
+ Header,
+ Main,
+ Aside,
+};
diff --git a/src/runtime-showcase/components/link/link.tsx b/src/runtime-showcase/components/link/link.tsx
index adee952..144a807 100644
--- a/src/runtime-showcase/components/link/link.tsx
+++ b/src/runtime-showcase/components/link/link.tsx
@@ -5,8 +5,8 @@
* - routing by router from context if applicable.
*/
import type { AnchorHTMLAttributes, MouseEvent, ReactNode } from 'react';
+import { useNavigate } from '@krutoo/utils/react';
import classNames from 'classnames';
-import { useNavigate } from '../../shared/router-react';
import styles from './link.m.css';
export interface LinkProps extends AnchorHTMLAttributes
{
diff --git a/src/runtime-showcase/components/menu-modal/menu-modal.tsx b/src/runtime-showcase/components/menu-modal/menu-modal.tsx
index 7e9ef0d..f64260c 100644
--- a/src/runtime-showcase/components/menu-modal/menu-modal.tsx
+++ b/src/runtime-showcase/components/menu-modal/menu-modal.tsx
@@ -1,7 +1,7 @@
import { type ReactNode, useContext, useEffect, useState } from 'react';
+import { useLocation, useNavigate } from '@krutoo/utils/react';
import { ShowcaseContext, useMenuItems, useStorySearchResult } from '../../context/showcase';
import { CrossSVG } from '../../icons';
-import { useLocation, useNavigate } from '../../shared/router-react';
import { Input } from '../input';
import { Menu, MenuItem, MenuItemTitle } from '../menu';
import styles from './menu-modal.m.css';
@@ -80,7 +80,7 @@ export function MenuModal({ open, onClose }: MenuModalProps): ReactNode {
onItemClick={(event, data) => {
if (data.type === 'story' && !data.menuHidden) {
event.preventDefault();
- navigate(data.story.pathname);
+ navigate(routing.getStoryShowcaseUrl(data.story));
onClose?.();
return;
}
diff --git a/src/runtime-showcase/components/menu/menu.tsx b/src/runtime-showcase/components/menu/menu.tsx
index 421799b..122ff89 100644
--- a/src/runtime-showcase/components/menu/menu.tsx
+++ b/src/runtime-showcase/components/menu/menu.tsx
@@ -9,9 +9,9 @@ import {
useSyncExternalStore,
} from 'react';
import { useIsomorphicLayoutEffect } from '@krutoo/utils/react';
+import { type Store, createStore } from '@krutoo/utils/store';
import classNames from 'classnames';
import { ChevronRightSVG } from '../../icons';
-import { type NanoStore, createNanoStore } from '../../shared/nano-store';
import styles from './menu.m.css';
export interface MenuProps {
@@ -26,12 +26,12 @@ export interface MenuProps {
}
interface MenuItemContextValue {
- store: NanoStore<{ open: boolean }>;
+ store: Store<{ open: boolean }>;
controlled: boolean;
}
const MenuItemContext = createContext({
- store: createNanoStore<{ open: boolean }>({ open: true }),
+ store: createStore<{ open: boolean }>({ open: true }),
controlled: false,
});
@@ -110,15 +110,15 @@ export function MenuItem({
defaultOpen?: boolean;
}): ReactNode {
const [store] = useState(() =>
- createNanoStore<{ open: boolean }>({
+ createStore<{ open: boolean }>({
open: openProp ?? defaultOpen ?? true,
}),
);
- const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
+ const state = useSyncExternalStore(store.subscribe, store.get, store.get);
useIsomorphicLayoutEffect(() => {
if (openProp !== undefined && openProp !== state.open) {
- store.setState({ open: openProp });
+ store.set({ open: openProp });
}
}, [store, state.open, openProp]);
@@ -160,7 +160,7 @@ export function MenuItemTitle({
}
if (!controlled) {
- store.setState({ open: !store.getState().open });
+ store.set({ open: !store.get().open });
}
}}
{...restProps}
@@ -176,7 +176,7 @@ export function MenuItemBody({
...restProps
}: HTMLAttributes): ReactNode {
const { store } = useContext(MenuItemContext);
- const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
+ const state = useSyncExternalStore(store.subscribe, store.get, store.get);
if (!state.open) {
return null;
diff --git a/src/runtime-showcase/components/standalone-provider/standalone-provider.tsx b/src/runtime-showcase/components/standalone-provider/standalone-provider.tsx
index 112e59f..c8111a9 100644
--- a/src/runtime-showcase/components/standalone-provider/standalone-provider.tsx
+++ b/src/runtime-showcase/components/standalone-provider/standalone-provider.tsx
@@ -1,10 +1,10 @@
-import { type ReactNode, useEffect, useMemo, useState } from 'react';
+import { type ReactNode, useMemo, useState } from 'react';
import { isObject } from '@krutoo/utils';
+import { RouterContext } from '@krutoo/utils/react';
+import { BrowserRouter } from '@krutoo/utils/router';
import { RoutingContext, extendRouting } from '../../context/routing';
import { ShowcaseContext, type ShowcaseContextValue } from '../../context/showcase';
-import { useInitial } from '../../shared/hooks';
-import { BrowserRouter } from '../../shared/router';
-import { RouterContext } from '../../shared/router-react';
+import { useMountEffect } from '../../shared/hooks';
import type { ShowcaseRouting, StandaloneAppConfig } from '../../types';
import { findFirstMenuItem, getMenuItems } from '../../utils/menu';
import { QueryRouting } from '../../utils/routing';
@@ -24,6 +24,7 @@ export function StandaloneProvider(props: StandaloneProviderProps): ReactNode {
defineStoryUrl,
} = props;
+ // @todo store
const [menuOpen, toggleMenu] = useState(false);
const router = useMemo(() => givenRouter ?? new BrowserRouter(), [givenRouter]);
@@ -88,20 +89,20 @@ export function StandaloneProvider(props: StandaloneProviderProps): ReactNode {
toggleMenu,
};
- const initialRouter = useInitial(router);
- const initialRouting = useInitial(context.config.routing);
- const initialStories = useInitial(context.config.stories);
- const initialDefaultStory = useInitial(defaultStory);
+ // @todo remove this step when provided?
+ useMountEffect(() => {
+ return router.connect();
+ });
- useEffect(() => {
- const disconnect = initialRouter.connect();
- const currentStoryPathname = initialRouting.getStoryPathname(initialRouter.getLocation());
+ // navigate to story by location or to default story
+ useMountEffect(() => {
+ const currentStoryPathname = routing.getStoryPathname(router.getLocation());
const navigateToDefault = () => {
- const story = initialStories.find(item => item.pathname === initialDefaultStory.pathname);
+ const story = stories.find(item => item.pathname === defaultStory.pathname);
if (story) {
- initialRouter.navigate(initialRouting.getStoryShowcaseUrl(story));
+ router.navigate(routing.getStoryShowcaseUrl(story));
}
};
@@ -110,21 +111,13 @@ export function StandaloneProvider(props: StandaloneProviderProps): ReactNode {
}
if (currentStoryPathname === '/') {
- const story = initialStories.find(item => item.pathname === currentStoryPathname);
+ const story = stories.find(item => item.pathname === currentStoryPathname);
if (!story) {
navigateToDefault();
}
}
-
- return disconnect;
- }, [
- // stable:
- initialRouter,
- initialStories,
- initialRouting,
- initialDefaultStory,
- ]);
+ });
return (
diff --git a/src/runtime-showcase/context/color-schemes.ts b/src/runtime-showcase/context/color-schemes.ts
index f8b5bc0..8922229 100644
--- a/src/runtime-showcase/context/color-schemes.ts
+++ b/src/runtime-showcase/context/color-schemes.ts
@@ -1,11 +1,14 @@
-import { createContext } from 'react';
+import { type Context, createContext } from 'react';
export interface ColorSchemesContextValue {
colorScheme: undefined | 'light' | 'dark';
onColorSchemeToggle(): void;
}
-export const ColorSchemesContext = createContext({
- colorScheme: undefined,
- onColorSchemeToggle: () => {},
-});
+export const ColorSchemesContext: Context =
+ createContext({
+ colorScheme: undefined,
+ onColorSchemeToggle: () => {},
+ });
+
+ColorSchemesContext.displayName = 'ColorSchemesContext';
diff --git a/src/runtime-showcase/context/component-registry.ts b/src/runtime-showcase/context/component-registry.ts
new file mode 100644
index 0000000..21ad71f
--- /dev/null
+++ b/src/runtime-showcase/context/component-registry.ts
@@ -0,0 +1,33 @@
+import { type ComponentType, type Context, createContext } from 'react';
+import { AppAside } from '../components/app-aside';
+import { AppHeader } from '../components/app-header';
+import { AppMain } from '../components/app-main';
+import { AppModals } from '../components/app-modals';
+import { HeaderLinks } from '../components/header-links';
+import { Logo } from '../components/logo';
+
+export interface ShowcaseComponentMap {
+ Aside: ComponentType;
+ Header: ComponentType;
+ HeaderLinks: ComponentType;
+ HeaderLogo: ComponentType;
+ Main: ComponentType;
+ Modals: ComponentType;
+}
+
+export const DefaultComponents: ShowcaseComponentMap = {
+ Aside: AppAside,
+ Header: AppHeader,
+ HeaderLinks,
+ HeaderLogo: Logo,
+ Main: AppMain,
+ Modals: AppModals,
+};
+
+/**
+ * Context for injecting UI component implementation.
+ */
+export const ComponentRegistryContext: Context =
+ createContext(DefaultComponents);
+
+ComponentRegistryContext.displayName = 'ComponentRegistryContext';
diff --git a/src/runtime-showcase/context/routing.ts b/src/runtime-showcase/context/routing.ts
index a81fa86..f89cb54 100644
--- a/src/runtime-showcase/context/routing.ts
+++ b/src/runtime-showcase/context/routing.ts
@@ -1,4 +1,4 @@
-import { createContext } from 'react';
+import { type Context, createContext } from 'react';
import type { StoryModule } from '#core';
import type { ShowcaseRouting } from '../types';
import { QueryRouting } from '../utils/routing';
@@ -27,6 +27,7 @@ export function extendRouting(
};
}
-export const RoutingContext = createContext(
- extendRouting(new QueryRouting(), []),
-);
+export const RoutingContext: Context =
+ createContext(extendRouting(new QueryRouting(), []));
+
+RoutingContext.displayName = 'RoutingContext';
diff --git a/src/runtime-showcase/context/showcase.ts b/src/runtime-showcase/context/showcase.ts
index 3fbf894..8c5eb58 100644
--- a/src/runtime-showcase/context/showcase.ts
+++ b/src/runtime-showcase/context/showcase.ts
@@ -1,7 +1,7 @@
import { type Context, createContext, useContext, useMemo } from 'react';
+import { useLocation } from '@krutoo/utils/react';
import type { StoryModule } from '#core';
import { StoryService } from '#runtime';
-import { useLocation } from '../shared/router-react';
import type { ShowcaseRouting } from '../types';
import { type AnyMenuNode, getMenuItems } from '../utils/menu';
diff --git a/src/runtime-showcase/icons/burger-menu.tsx b/src/runtime-showcase/icons/burger-menu.tsx
index 5cb167e..3ec1507 100644
--- a/src/runtime-showcase/icons/burger-menu.tsx
+++ b/src/runtime-showcase/icons/burger-menu.tsx
@@ -1,14 +1,15 @@
-import { forwardRef } from 'react';
+import { type ComponentType, type SVGProps, forwardRef } from 'react';
import type { IconProps } from './types';
-export const BurgerMenuSVG = forwardRef(
- function BurgerMenuSVG(props, ref) {
- return (
-
- );
- },
-);
+export const BurgerMenuSVG: ComponentType> = forwardRef<
+ SVGSVGElement,
+ IconProps
+>(function BurgerMenuSVG(props, ref) {
+ return (
+
+ );
+});
diff --git a/src/runtime-showcase/icons/chevron-right.tsx b/src/runtime-showcase/icons/chevron-right.tsx
index 2a475e5..2476593 100644
--- a/src/runtime-showcase/icons/chevron-right.tsx
+++ b/src/runtime-showcase/icons/chevron-right.tsx
@@ -1,12 +1,13 @@
-import { forwardRef } from 'react';
+import { type ComponentType, type SVGProps, forwardRef } from 'react';
import type { IconProps } from './types';
-export const ChevronRightSVG = forwardRef(
- function ChevronRight(props, ref) {
- return (
-
- );
- },
-);
+export const ChevronRightSVG: ComponentType> = forwardRef<
+ SVGSVGElement,
+ IconProps
+>(function ChevronRight(props, ref) {
+ return (
+
+ );
+});
diff --git a/src/runtime-showcase/icons/cross.tsx b/src/runtime-showcase/icons/cross.tsx
index 20841c3..2cc0e36 100644
--- a/src/runtime-showcase/icons/cross.tsx
+++ b/src/runtime-showcase/icons/cross.tsx
@@ -1,7 +1,10 @@
-import { forwardRef } from 'react';
+import { type ComponentType, type SVGProps, forwardRef } from 'react';
import type { IconProps } from './types';
-export const CrossSVG = forwardRef(function CrossSVG(props, ref) {
+export const CrossSVG: ComponentType> = forwardRef<
+ SVGSVGElement,
+ IconProps
+>(function CrossSVG(props, ref) {
return (