diff --git a/apps/web/src/app/app-routes.tsx b/apps/web/src/app/app-routes.tsx
index 9830f93..a810ba6 100644
--- a/apps/web/src/app/app-routes.tsx
+++ b/apps/web/src/app/app-routes.tsx
@@ -1,4 +1,4 @@
-import { UiThemeLink } from '@pubkey-ui/core'
+import { UiNotFound, UiThemeLink } from '@pubkey-ui/core'
import { lazy } from 'react'
import { Link, Navigate, RouteObject, useRoutes } from 'react-router-dom'
import { DemoFeature } from './features'
@@ -17,6 +17,7 @@ const routes: RouteObject[] = [
{ path: '/dashboard', element: },
{ path: '/demo/*', element: },
{ path: '/dev', element: },
+ { path: '*', element: },
]
export function AppRoutes() {
diff --git a/apps/web/src/app/features/demo/demo-feature-not-found.tsx b/apps/web/src/app/features/demo/demo-feature-not-found.tsx
new file mode 100644
index 0000000..8b88733
--- /dev/null
+++ b/apps/web/src/app/features/demo/demo-feature-not-found.tsx
@@ -0,0 +1,10 @@
+import { UiCardTitle, UiNotFound, UiStack } from '@pubkey-ui/core'
+
+export function DemoFeatureNotFound() {
+ return (
+
+ NotFound
+
+
+ )
+}
diff --git a/apps/web/src/app/features/demo/demo-feature.tsx b/apps/web/src/app/features/demo/demo-feature.tsx
index 31e15fe..60dd0a6 100644
--- a/apps/web/src/app/features/demo/demo-feature.tsx
+++ b/apps/web/src/app/features/demo/demo-feature.tsx
@@ -1,5 +1,5 @@
import { Grid, NavLink } from '@mantine/core'
-import { UiContainer, UiStack } from '@pubkey-ui/core'
+import { UiContainer, UiNotFound, UiStack } from '@pubkey-ui/core'
import { ReactNode } from 'react'
import { Link, Navigate, useLocation, useRoutes } from 'react-router-dom'
import { DemoFeatureAlerts } from './demo-feature-alerts'
@@ -11,6 +11,7 @@ import { DemoFeatureGroup } from './demo-feature-group'
import { DemoFeatureHeader } from './demo-feature-header'
import { DemoFeatureLogo } from './demo-feature-logo'
import { DemoFeatureMenu } from './demo-feature-menu'
+import { DemoFeatureNotFound } from './demo-feature-not-found'
import { DemoFeatureSearchInput } from './demo-feature-search-input'
import { DemoFeatureStack } from './demo-feature-stack'
import { DemoFeatureTabRoutes } from './demo-feature-tab-routes'
@@ -33,6 +34,7 @@ export function DemoFeature() {
{ path: 'header', label: 'Header', element: },
{ path: 'logo', label: 'Logo', element: },
{ path: 'menu', label: 'Menu', element: },
+ { path: 'not-found', label: 'Not Found', element: },
{ path: 'search-input', label: 'Search Input', element: },
{ path: 'stack', label: 'Stack', element: },
{ path: 'tab-routes', label: 'Tab Routes', element: },
@@ -43,6 +45,7 @@ export function DemoFeature() {
const routes = useRoutes([
{ index: true, element: },
...demos.map((demo) => ({ path: `${demo.path}/*`, element: demo.element })),
+ { path: '*', element: },
])
return (
diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts
index be93fe0..349b5bb 100644
--- a/packages/core/src/lib/index.ts
+++ b/packages/core/src/lib/index.ts
@@ -9,6 +9,7 @@ export * from './ui-header'
export * from './ui-layout'
export * from './ui-logo'
export * from './ui-menu'
+export * from './ui-not-found'
export * from './ui-search-input'
export * from './ui-stack'
export * from './ui-tab-routes'
diff --git a/packages/core/src/lib/ui-card/index.ts b/packages/core/src/lib/ui-card/index.ts
index 56f9779..2be55a7 100644
--- a/packages/core/src/lib/ui-card/index.ts
+++ b/packages/core/src/lib/ui-card/index.ts
@@ -1 +1,2 @@
export * from './ui-card'
+export * from './ui-card-title'
diff --git a/packages/core/src/lib/ui-not-found/index.ts b/packages/core/src/lib/ui-not-found/index.ts
new file mode 100644
index 0000000..0160703
--- /dev/null
+++ b/packages/core/src/lib/ui-not-found/index.ts
@@ -0,0 +1 @@
+export * from './ui-not-found'
diff --git a/packages/core/src/lib/ui-not-found/ui-not-found.module.css b/packages/core/src/lib/ui-not-found/ui-not-found.module.css
new file mode 100644
index 0000000..fc981eb
--- /dev/null
+++ b/packages/core/src/lib/ui-not-found/ui-not-found.module.css
@@ -0,0 +1,34 @@
+.root {
+ padding-top: rem(80px);
+ padding-bottom: rem(80px);
+}
+
+.label {
+ text-align: center;
+ font-weight: 900;
+ font-size: rem(38px);
+ line-height: 1;
+ margin-bottom: calc(1.5 * var(--mantine-spacing-xl));
+ color: var(--mantine-color-gray-2);
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ font-size: rem(32px);
+ }
+}
+
+.description {
+ max-width: rem(500px);
+ margin: auto;
+ margin-top: var(--mantine-spacing-xl);
+ margin-bottom: calc(1.5 * var(--mantine-spacing-xl));
+}
+
+.title {
+ text-align: center;
+ font-weight: 900;
+ font-size: rem(38px);
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ font-size: rem(32px);
+ }
+}
diff --git a/packages/core/src/lib/ui-not-found/ui-not-found.tsx b/packages/core/src/lib/ui-not-found/ui-not-found.tsx
new file mode 100644
index 0000000..720bf70
--- /dev/null
+++ b/packages/core/src/lib/ui-not-found/ui-not-found.tsx
@@ -0,0 +1,22 @@
+import { Button, Container, Group, Text, Title } from '@mantine/core'
+import { useUiTheme } from '../ui-theme'
+import classes from './ui-not-found.module.css'
+
+export function UiNotFound({ to = '/' }: { to?: string }) {
+ const { Link } = useUiTheme()
+ return (
+
+ 404
+ You have found a secret place.
+
+ Unfortunately, this is only a 404 page. You may have mistyped the address, or the page has been moved to another
+ URL.
+
+
+
+
+
+ )
+}
diff --git a/packages/generators/src/generators/component/component-generator-schema.d.ts b/packages/generators/src/generators/component/component-generator-schema.d.ts
index 939530a..d053f4d 100644
--- a/packages/generators/src/generators/component/component-generator-schema.d.ts
+++ b/packages/generators/src/generators/component/component-generator-schema.d.ts
@@ -25,6 +25,7 @@ export interface ComponentGeneratorSchema {
| 'layout'
| 'logo'
| 'menu'
+ | 'not-found'
| 'search-input'
| 'stack'
| 'tab-routes'
diff --git a/packages/generators/src/generators/component/component-generator-schema.json b/packages/generators/src/generators/component/component-generator-schema.json
index ae8ac70..513999b 100644
--- a/packages/generators/src/generators/component/component-generator-schema.json
+++ b/packages/generators/src/generators/component/component-generator-schema.json
@@ -27,6 +27,7 @@
"layout",
"logo",
"menu",
+ "not-found",
"search-input",
"stack",
"tab-routes",
diff --git a/packages/generators/src/generators/component/files/card/index.ts.template b/packages/generators/src/generators/component/files/card/index.ts.template
index 82720b8..015bf41 100644
--- a/packages/generators/src/generators/component/files/card/index.ts.template
+++ b/packages/generators/src/generators/component/files/card/index.ts.template
@@ -1 +1,2 @@
export * from './<%= prefixFileName %>-card'
+export * from './<%= prefixFileName %>-card-title'
diff --git a/packages/generators/src/generators/component/files/not-found/__prefixFileName__-not-found.module.css.template b/packages/generators/src/generators/component/files/not-found/__prefixFileName__-not-found.module.css.template
new file mode 100644
index 0000000..fc981eb
--- /dev/null
+++ b/packages/generators/src/generators/component/files/not-found/__prefixFileName__-not-found.module.css.template
@@ -0,0 +1,34 @@
+.root {
+ padding-top: rem(80px);
+ padding-bottom: rem(80px);
+}
+
+.label {
+ text-align: center;
+ font-weight: 900;
+ font-size: rem(38px);
+ line-height: 1;
+ margin-bottom: calc(1.5 * var(--mantine-spacing-xl));
+ color: var(--mantine-color-gray-2);
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ font-size: rem(32px);
+ }
+}
+
+.description {
+ max-width: rem(500px);
+ margin: auto;
+ margin-top: var(--mantine-spacing-xl);
+ margin-bottom: calc(1.5 * var(--mantine-spacing-xl));
+}
+
+.title {
+ text-align: center;
+ font-weight: 900;
+ font-size: rem(38px);
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ font-size: rem(32px);
+ }
+}
diff --git a/packages/generators/src/generators/component/files/not-found/__prefixFileName__-not-found.tsx.template b/packages/generators/src/generators/component/files/not-found/__prefixFileName__-not-found.tsx.template
new file mode 100644
index 0000000..660ab6a
--- /dev/null
+++ b/packages/generators/src/generators/component/files/not-found/__prefixFileName__-not-found.tsx.template
@@ -0,0 +1,22 @@
+import { Button, Container, Group, Text, Title } from '@mantine/core'
+import { use<%= prefix.className %>Theme } from '../<%= prefix.fileName %>-theme'
+import classes from './<%= prefix.fileName %>-not-found.module.css'
+
+export function <%= prefix.className %>NotFound({ to = '/' }: { to?: string }) {
+ const { Link } = use<%= prefix.className %>Theme()
+ return (
+
+ 404
+ You have found a secret place.
+
+ Unfortunately, this is only a 404 page. You may have mistyped the address, or the page has been moved to another
+ URL.
+
+
+
+
+
+ )
+}
diff --git a/packages/generators/src/generators/component/files/not-found/index.ts.template b/packages/generators/src/generators/component/files/not-found/index.ts.template
new file mode 100644
index 0000000..091cfdd
--- /dev/null
+++ b/packages/generators/src/generators/component/files/not-found/index.ts.template
@@ -0,0 +1 @@
+export * from './<%= prefix.fileName %>-not-found'
diff --git a/packages/generators/src/generators/components/components.ts b/packages/generators/src/generators/components/components.ts
index 49abb0c..ff5c9af 100644
--- a/packages/generators/src/generators/components/components.ts
+++ b/packages/generators/src/generators/components/components.ts
@@ -12,6 +12,7 @@ export const components: ComponentGeneratorSchema['type'][] = [
'layout',
'logo',
'menu',
+ 'not-found',
'search-input',
'stack',
'tab-routes',
diff --git a/packages/generators/src/generators/feature/files/demo/demo-feature-not-found.tsx.template b/packages/generators/src/generators/feature/files/demo/demo-feature-not-found.tsx.template
new file mode 100644
index 0000000..8b88733
--- /dev/null
+++ b/packages/generators/src/generators/feature/files/demo/demo-feature-not-found.tsx.template
@@ -0,0 +1,10 @@
+import { UiCardTitle, UiNotFound, UiStack } from '@pubkey-ui/core'
+
+export function DemoFeatureNotFound() {
+ return (
+
+ NotFound
+
+
+ )
+}
diff --git a/packages/generators/src/generators/feature/files/demo/demo-feature.tsx.template b/packages/generators/src/generators/feature/files/demo/demo-feature.tsx.template
index 018c5c6..1376cf1 100644
--- a/packages/generators/src/generators/feature/files/demo/demo-feature.tsx.template
+++ b/packages/generators/src/generators/feature/files/demo/demo-feature.tsx.template
@@ -1,5 +1,5 @@
import { Grid, NavLink } from '@mantine/core'
-import { <%= prefix.className %>Container, <%= prefix.className %>Stack } from '<%= uiImport %>'
+import { <%= prefix.className %>Container, <%= prefix.className %>NotFound, <%= prefix.className %>Stack } from '<%= uiImport %>'
import { ReactNode } from 'react'
import { Link, Navigate, useLocation, useRoutes } from 'react-router-dom'
import { DemoFeatureAlerts } from './demo-feature-alerts'
@@ -11,6 +11,7 @@ import { DemoFeatureGroup } from './demo-feature-group'
import { DemoFeatureHeader } from './demo-feature-header'
import { DemoFeatureLogo } from './demo-feature-logo'
import { DemoFeatureMenu } from './demo-feature-menu'
+import { DemoFeatureNotFound } from './demo-feature-not-found'
import { DemoFeatureSearchInput } from './demo-feature-search-input'
import { DemoFeatureStack } from './demo-feature-stack'
import { DemoFeatureTabRoutes } from './demo-feature-tab-routes'
@@ -33,6 +34,7 @@ element: ReactNode
{ path: 'header', label: 'Header', element: },
{ path: 'logo', label: 'Logo', element: },
{ path: 'menu', label: 'Menu', element: },
+{ path: 'not-found', label: 'Not Found', element: },
{ path: 'search-input', label: 'Search Input', element: },
{ path: 'stack', label: 'Stack', element: },
{ path: 'tab-routes', label: 'Tab Routes', element: },
@@ -43,6 +45,7 @@ element: ReactNode
const routes = useRoutes([
{ index: true, element: },
...demos.map((demo) => ({ path: `${demo.path}/*`, element: demo.element })),
+{ path: '*', element: <<%= prefix.className %>NotFound to="/demo" /> },
])
return (