@owlmeans/create-app 0.1.18-rc.6 → 0.1.18-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/template/sources/api/package.json +3 -3
- package/template/sources/api/src/context.ts +4 -0
- package/template/sources/common/package.json +6 -6
- package/template/sources/common/src/consts.ts +1 -0
- package/template/sources/web/package.json +13 -11
- package/template/sources/web/src/components/ui/navigation-menu.tsx +169 -0
- package/template/sources/web/src/context.ts +18 -2
- package/template/sources/web/src/index.css +7 -0
- package/template/sources/web/src/layout/main.tsx +12 -19
- package/template/sources/web/src/modules.ts +2 -0
- package/template/sources/web/src/nav.ts +28 -0
- package/template/sources/web/src/screens/about.tsx +19 -0
- package/template/sources/web/src/screens/session.tsx +24 -9
- package/template/sources/web/src/components/nav/main.tsx +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/create-app",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Scaffold a minimal fullstack OwlMeans Common app — common + api + web workspaces, shadcn UI navigation/layout, no auth, and a session-scoped in-memory resource. Deploys agent skills via @owlmeans/agent-skills by default.",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"template"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@owlmeans/agent-skills": "^0.1.18-rc.
|
|
32
|
+
"@owlmeans/agent-skills": "^0.1.18-rc.7"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@owlmeans/dep-config": "workspace:*",
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
"typecheck": "tsc -b"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@owlmeans/server-app": "^0.1.
|
|
15
|
-
"@owlmeans/static-resource": "^0.1.
|
|
14
|
+
"@owlmeans/server-app": "^0.1.18-rc.0",
|
|
15
|
+
"@owlmeans/static-resource": "^0.1.18-rc.0",
|
|
16
16
|
"__APP_SLUG__-common": "workspace:^"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@owlmeans/dep-config": "^0.1.
|
|
19
|
+
"@owlmeans/dep-config": "^0.1.18-rc.0",
|
|
20
20
|
"@types/node": "^24.10.1",
|
|
21
21
|
"typescript": "^7.0.2"
|
|
22
22
|
}
|
|
@@ -10,5 +10,9 @@ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T =
|
|
|
10
10
|
// data lives in process memory and is cleared when the api restarts.
|
|
11
11
|
appendStaticResource<C, T>(context, SESSION_ITEMS)
|
|
12
12
|
|
|
13
|
+
// A child context has to inherit THIS factory, not the layer's — otherwise a derived context
|
|
14
|
+
// is built without the resource registered above and every lookup on it throws.
|
|
15
|
+
context.makeContext = makeContext as typeof context.makeContext
|
|
16
|
+
|
|
13
17
|
return context
|
|
14
18
|
}
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
"typecheck": "tsc -b"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@owlmeans/config": "^0.1.
|
|
23
|
-
"@owlmeans/entrypoint": "^0.1.
|
|
24
|
-
"@owlmeans/error": "^0.1.
|
|
25
|
-
"@owlmeans/resource": "^0.1.
|
|
26
|
-
"@owlmeans/route": "^0.1.
|
|
22
|
+
"@owlmeans/config": "^0.1.18-rc.0",
|
|
23
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.0",
|
|
24
|
+
"@owlmeans/error": "^0.1.18-rc.0",
|
|
25
|
+
"@owlmeans/resource": "^0.1.18-rc.0",
|
|
26
|
+
"@owlmeans/route": "^0.1.18-rc.0",
|
|
27
27
|
"ajv": "^8.17.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@owlmeans/dep-config": "^0.1.
|
|
30
|
+
"@owlmeans/dep-config": "^0.1.18-rc.0",
|
|
31
31
|
"@types/node": "^24.10.1",
|
|
32
32
|
"typescript": "^7.0.2"
|
|
33
33
|
}
|
|
@@ -11,19 +11,21 @@
|
|
|
11
11
|
"preview": "vite preview"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@owlmeans/client": "^0.1.
|
|
15
|
-
"@owlmeans/client-config": "^0.1.
|
|
16
|
-
"@owlmeans/client-context": "^0.1.
|
|
17
|
-
"@owlmeans/client-entrypoint": "^0.1.
|
|
18
|
-
"@owlmeans/client-i18n": "^0.1.
|
|
19
|
-
"@owlmeans/entrypoint": "^0.1.
|
|
20
|
-
"@owlmeans/route": "^0.1.
|
|
21
|
-
"@owlmeans/
|
|
22
|
-
"@owlmeans/web-
|
|
23
|
-
"
|
|
14
|
+
"@owlmeans/client": "^0.1.18-rc.0",
|
|
15
|
+
"@owlmeans/client-config": "^0.1.18-rc.0",
|
|
16
|
+
"@owlmeans/client-context": "^0.1.18-rc.0",
|
|
17
|
+
"@owlmeans/client-entrypoint": "^0.1.18-rc.0",
|
|
18
|
+
"@owlmeans/client-i18n": "^0.1.18-rc.0",
|
|
19
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.0",
|
|
20
|
+
"@owlmeans/route": "^0.1.18-rc.0",
|
|
21
|
+
"@owlmeans/state": "^0.1.18-rc.0",
|
|
22
|
+
"@owlmeans/web-client": "^0.1.18-rc.0",
|
|
23
|
+
"@owlmeans/web-panel": "^0.1.18-rc.0",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.0",
|
|
25
|
+
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
25
26
|
"@radix-ui/react-progress": "^1.1.0",
|
|
26
27
|
"@radix-ui/react-slot": "^1.1.0",
|
|
28
|
+
"__APP_SLUG__-common": "workspace:^",
|
|
27
29
|
"ajv": "^8.17.1",
|
|
28
30
|
"class-variance-authority": "^0.7.0",
|
|
29
31
|
"clsx": "^2.1.0",
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
"tailwindcss": "^4.1.13"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"@owlmeans/dep-config": "^0.1.
|
|
42
|
+
"@owlmeans/dep-config": "^0.1.18-rc.0",
|
|
41
43
|
"@tailwindcss/vite": "^4.1.13",
|
|
42
44
|
"@types/react": "^19.2.17",
|
|
43
45
|
"@types/react-dom": "^19.2.3",
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// shadcn navigation-menu — sourced from shadcn/ui (new-york) 2026-08-24
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
|
4
|
+
import { cva } from "class-variance-authority"
|
|
5
|
+
import { ChevronDownIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
function NavigationMenu({
|
|
10
|
+
className,
|
|
11
|
+
children,
|
|
12
|
+
viewport = true,
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
|
15
|
+
viewport?: boolean
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<NavigationMenuPrimitive.Root
|
|
19
|
+
data-slot="navigation-menu"
|
|
20
|
+
data-viewport={viewport}
|
|
21
|
+
className={cn(
|
|
22
|
+
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
{viewport && <NavigationMenuViewport />}
|
|
29
|
+
</NavigationMenuPrimitive.Root>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function NavigationMenuList({
|
|
34
|
+
className,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
|
37
|
+
return (
|
|
38
|
+
<NavigationMenuPrimitive.List
|
|
39
|
+
data-slot="navigation-menu-list"
|
|
40
|
+
className={cn(
|
|
41
|
+
"group flex flex-1 list-none items-center justify-center gap-1",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function NavigationMenuItem({
|
|
50
|
+
className,
|
|
51
|
+
...props
|
|
52
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
|
53
|
+
return (
|
|
54
|
+
<NavigationMenuPrimitive.Item
|
|
55
|
+
data-slot="navigation-menu-item"
|
|
56
|
+
className={cn("relative", className)}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const navigationMenuTriggerStyle = cva(
|
|
63
|
+
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
function NavigationMenuTrigger({
|
|
67
|
+
className,
|
|
68
|
+
children,
|
|
69
|
+
...props
|
|
70
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
|
71
|
+
return (
|
|
72
|
+
<NavigationMenuPrimitive.Trigger
|
|
73
|
+
data-slot="navigation-menu-trigger"
|
|
74
|
+
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{children}{" "}
|
|
78
|
+
<ChevronDownIcon
|
|
79
|
+
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
/>
|
|
82
|
+
</NavigationMenuPrimitive.Trigger>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function NavigationMenuContent({
|
|
87
|
+
className,
|
|
88
|
+
...props
|
|
89
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
|
90
|
+
return (
|
|
91
|
+
<NavigationMenuPrimitive.Content
|
|
92
|
+
data-slot="navigation-menu-content"
|
|
93
|
+
className={cn(
|
|
94
|
+
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
|
95
|
+
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
|
96
|
+
className
|
|
97
|
+
)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function NavigationMenuViewport({
|
|
104
|
+
className,
|
|
105
|
+
...props
|
|
106
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
"absolute top-full left-0 isolate z-50 flex justify-center"
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
<NavigationMenuPrimitive.Viewport
|
|
114
|
+
data-slot="navigation-menu-viewport"
|
|
115
|
+
className={cn(
|
|
116
|
+
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
|
117
|
+
className
|
|
118
|
+
)}
|
|
119
|
+
{...props}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function NavigationMenuLink({
|
|
126
|
+
className,
|
|
127
|
+
...props
|
|
128
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
|
129
|
+
return (
|
|
130
|
+
<NavigationMenuPrimitive.Link
|
|
131
|
+
data-slot="navigation-menu-link"
|
|
132
|
+
className={cn(
|
|
133
|
+
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
|
134
|
+
className
|
|
135
|
+
)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function NavigationMenuIndicator({
|
|
142
|
+
className,
|
|
143
|
+
...props
|
|
144
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
|
145
|
+
return (
|
|
146
|
+
<NavigationMenuPrimitive.Indicator
|
|
147
|
+
data-slot="navigation-menu-indicator"
|
|
148
|
+
className={cn(
|
|
149
|
+
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
|
150
|
+
className
|
|
151
|
+
)}
|
|
152
|
+
{...props}
|
|
153
|
+
>
|
|
154
|
+
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
|
155
|
+
</NavigationMenuPrimitive.Indicator>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
NavigationMenu,
|
|
161
|
+
NavigationMenuList,
|
|
162
|
+
NavigationMenuItem,
|
|
163
|
+
NavigationMenuContent,
|
|
164
|
+
NavigationMenuTrigger,
|
|
165
|
+
NavigationMenuLink,
|
|
166
|
+
NavigationMenuIndicator,
|
|
167
|
+
NavigationMenuViewport,
|
|
168
|
+
navigationMenuTriggerStyle,
|
|
169
|
+
}
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { makeContext as makeBasicContext, useContext as useBasicContext } from '@owlmeans/web-panel'
|
|
2
|
+
import { appendStateResource } from '@owlmeans/state'
|
|
2
3
|
import type { Config, Context } from './types.js'
|
|
3
4
|
|
|
5
|
+
/** The alias the session items are stored under. Screens address the store by it, never by path. */
|
|
6
|
+
export const SESSION_STATE = 'session-items'
|
|
7
|
+
|
|
4
8
|
export const useContext = (): Context => useBasicContext<Config, Context>()
|
|
5
9
|
|
|
6
|
-
export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T =>
|
|
7
|
-
makeBasicContext<C, T>(cfg)
|
|
10
|
+
export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T => {
|
|
11
|
+
const context = makeBasicContext<C, T>(cfg)
|
|
12
|
+
|
|
13
|
+
// The framework's client store. A state resource lives ON the context, so a screen, a guard and
|
|
14
|
+
// a service all reach the same records — which is what separates it from a store held beside
|
|
15
|
+
// the app in a module of its own.
|
|
16
|
+
appendStateResource<C, T>(context, SESSION_STATE)
|
|
17
|
+
|
|
18
|
+
// A child context has to inherit THIS factory, not the layer's. Without the line, a derived
|
|
19
|
+
// context is built without the resource registered above and every lookup on it throws.
|
|
20
|
+
context.makeContext = makeContext as typeof context.makeContext
|
|
21
|
+
|
|
22
|
+
return context
|
|
23
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
|
+
/*
|
|
4
|
+
* Tailwind scans this app's sources for class names, and node_modules is excluded from that
|
|
5
|
+
* scan by default — so the classes that exist ONLY inside @owlmeans/web-panel components
|
|
6
|
+
* (the navigation shell, the footer) would never reach the stylesheet. This line adds them.
|
|
7
|
+
*/
|
|
8
|
+
@source "../../../node_modules/@owlmeans/web-panel/build";
|
|
9
|
+
|
|
3
10
|
/*
|
|
4
11
|
* shadcn / Tailwind v4 design tokens (new-york, neutral). The app owns these — they
|
|
5
12
|
* replace what @owlmeans/owl-theme would otherwise provide. The shadcn primitives in
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import type { FC, PropsWithChildren } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { NavLayout } from '@owlmeans/web-panel'
|
|
3
|
+
import { footerLinks, navConfig } from '@/nav'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<MainNavigation />
|
|
16
|
-
</div>
|
|
17
|
-
</header>
|
|
18
|
-
<main className="mx-auto max-w-4xl px-4 py-8">{children}</main>
|
|
19
|
-
</div>
|
|
20
|
-
)
|
|
21
|
-
}
|
|
5
|
+
/**
|
|
6
|
+
* The application shell. `NavLayout` renders the header with the section menu, the side menu
|
|
7
|
+
* of the active section (hidden when that section has a single screen), the content, and the
|
|
8
|
+
* footer — the screen arrives as `children`.
|
|
9
|
+
*/
|
|
10
|
+
export const MainLayout: FC<PropsWithChildren> = ({ children }) => (
|
|
11
|
+
<NavLayout nav={navConfig} title="__APP_NAME__" footer={footerLinks} contentClassName="mx-auto w-full max-w-4xl">
|
|
12
|
+
{children}
|
|
13
|
+
</NavLayout>
|
|
14
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BASE, elevate, entrypoint, frontend, handler, HOME, modules as baseModules, route } from '@owlmeans/web-panel'
|
|
2
2
|
import { session, sessionModules, web } from '__APP_SLUG__-common'
|
|
3
3
|
import { MainLayout } from './layout/main.js'
|
|
4
|
+
import { AboutScreen } from './screens/about.js'
|
|
4
5
|
import { HomeScreen } from './screens/home.js'
|
|
5
6
|
import { SessionScreen } from './screens/session.js'
|
|
6
7
|
|
|
@@ -16,5 +17,6 @@ elevate(modules, session.remove)
|
|
|
16
17
|
modules.push(entrypoint(route(BASE, '/', frontend()), handler(MainLayout)))
|
|
17
18
|
modules.push(entrypoint(route(HOME, '/', frontend({ default: true, parent: BASE })), handler(HomeScreen)))
|
|
18
19
|
modules.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
|
|
20
|
+
modules.push(entrypoint(route(web.about, '/about', frontend({ parent: BASE })), handler(AboutScreen)))
|
|
19
21
|
|
|
20
22
|
export const appModules = modules
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HOME } from '@owlmeans/web-panel'
|
|
2
|
+
import type { PanelNavConfig, PanelNavLink } from '@owlmeans/web-panel'
|
|
3
|
+
import { web } from '__APP_SLUG__-common'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The application's navigation, as data.
|
|
7
|
+
*
|
|
8
|
+
* Sections are the top menu; their items are the side menu of whichever section is active.
|
|
9
|
+
* A section holding ONE item renders no side menu at all — `home` below shows that, `demo`
|
|
10
|
+
* shows the two-level case. Labels here are literal; drop them to fall back on the panel
|
|
11
|
+
* i18n keys (`nav.<section>` / `modules.<alias>`) instead.
|
|
12
|
+
*/
|
|
13
|
+
export const navConfig: PanelNavConfig = {
|
|
14
|
+
sections: [
|
|
15
|
+
{ name: 'home', label: 'Home', items: [{ alias: HOME, label: 'Overview' }] },
|
|
16
|
+
{
|
|
17
|
+
name: 'demo', label: 'Demo', items: [
|
|
18
|
+
{ alias: web.session, label: 'Session' },
|
|
19
|
+
{ alias: web.about, label: 'About' },
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const footerLinks: PanelNavLink[] = [
|
|
26
|
+
{ alias: HOME, label: '__APP_NAME__' },
|
|
27
|
+
{ href: 'https://owlmeans.com', label: 'OwlMeans', open: true },
|
|
28
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FC } from 'react'
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
3
|
+
|
|
4
|
+
export const AboutScreen: FC = () => (
|
|
5
|
+
<Card>
|
|
6
|
+
<CardHeader>
|
|
7
|
+
<CardTitle>About __APP_NAME__</CardTitle>
|
|
8
|
+
<CardDescription>
|
|
9
|
+
The second screen of the Demo section — which is why that section shows a side menu
|
|
10
|
+
and the single-screen Home section does not.
|
|
11
|
+
</CardDescription>
|
|
12
|
+
</CardHeader>
|
|
13
|
+
<CardContent>
|
|
14
|
+
<p className="text-sm text-muted-foreground">
|
|
15
|
+
Built with OwlMeans Common. Navigation lives in <code>src/nav.ts</code>.
|
|
16
|
+
</p>
|
|
17
|
+
</CardContent>
|
|
18
|
+
</Card>
|
|
19
|
+
)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useEffect, useState, type FC } from 'react'
|
|
2
2
|
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
|
|
3
|
+
import { useStoreList } from '@owlmeans/client'
|
|
3
4
|
import { session, type SessionItem } from '__APP_SLUG__-common'
|
|
4
|
-
import { useContext } from '../context.js'
|
|
5
|
+
import { SESSION_STATE, useContext } from '../context.js'
|
|
5
6
|
import { Button } from '@/components/ui/button'
|
|
6
7
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
7
8
|
import { Input } from '@/components/ui/input'
|
|
@@ -20,15 +21,27 @@ const sessionId = (): string => {
|
|
|
20
21
|
export const SessionScreen: FC = () => {
|
|
21
22
|
const ctx = useContext()
|
|
22
23
|
const sid = sessionId()
|
|
23
|
-
const [items, setItems] = useState<SessionItem[]>([])
|
|
24
24
|
const [text, setText] = useState('')
|
|
25
25
|
const [busy, setBusy] = useState(false)
|
|
26
26
|
|
|
27
|
+
const store = ctx.getStateResource<SessionItem>(SESSION_STATE)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The list is a live subscription to the state resource, not a piece of component state.
|
|
31
|
+
* Anything that writes a record — this screen or another one — re-renders it, so adding and
|
|
32
|
+
* removing never re-fetch to stay in step. `{}` matches every record; a real filter goes here.
|
|
33
|
+
*/
|
|
34
|
+
const items = useStoreList<SessionItem>({ query: {}, resource: SESSION_STATE })
|
|
35
|
+
|
|
36
|
+
// The server is the source of truth; the store is what the screen reads. Fetch once, write
|
|
37
|
+
// what came back into the store, and let the subscription render it.
|
|
27
38
|
const load = async () => {
|
|
28
39
|
const [data] = await ctx
|
|
29
40
|
.entrypoint<ClientEntrypoint<SessionItem[]>>(session.list)
|
|
30
41
|
.call({ params: { sid } })
|
|
31
|
-
|
|
42
|
+
for (const item of data ?? []) {
|
|
43
|
+
await store.save(item)
|
|
44
|
+
}
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
useEffect(() => { void load() }, [])
|
|
@@ -37,11 +50,13 @@ export const SessionScreen: FC = () => {
|
|
|
37
50
|
if (text.trim() === '') return
|
|
38
51
|
setBusy(true)
|
|
39
52
|
try {
|
|
40
|
-
await ctx
|
|
53
|
+
const [item] = await ctx
|
|
41
54
|
.entrypoint<ClientEntrypoint<SessionItem>>(session.add)
|
|
42
55
|
.call({ params: { sid }, body: { text } })
|
|
43
56
|
setText('')
|
|
44
|
-
|
|
57
|
+
if (item != null) {
|
|
58
|
+
await store.save(item)
|
|
59
|
+
}
|
|
45
60
|
} finally {
|
|
46
61
|
setBusy(false)
|
|
47
62
|
}
|
|
@@ -51,7 +66,7 @@ export const SessionScreen: FC = () => {
|
|
|
51
66
|
await ctx
|
|
52
67
|
.entrypoint<ClientEntrypoint<{ removed: boolean }>>(session.remove)
|
|
53
68
|
.call({ params: { sid, id } })
|
|
54
|
-
await
|
|
69
|
+
await store.delete(id)
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
return (
|
|
@@ -75,11 +90,11 @@ export const SessionScreen: FC = () => {
|
|
|
75
90
|
<ul className="flex flex-col gap-2">
|
|
76
91
|
{items.map(item => (
|
|
77
92
|
<li
|
|
78
|
-
key={item.id}
|
|
93
|
+
key={item.record.id}
|
|
79
94
|
className="flex items-center justify-between rounded-md border px-3 py-2"
|
|
80
95
|
>
|
|
81
|
-
<span>{item.text}</span>
|
|
82
|
-
<Button variant="ghost" size="sm" onClick={() => void remove(item.id)}>
|
|
96
|
+
<span>{item.record.text}</span>
|
|
97
|
+
<Button variant="ghost" size="sm" onClick={() => void remove(item.record.id)}>
|
|
83
98
|
Remove
|
|
84
99
|
</Button>
|
|
85
100
|
</li>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { FC } from 'react'
|
|
2
|
-
import { HOME, useNavigate } from '@owlmeans/web-panel'
|
|
3
|
-
import { web } from '__APP_SLUG__-common'
|
|
4
|
-
import { Button } from '@/components/ui/button'
|
|
5
|
-
|
|
6
|
-
export const MainNavigation: FC = () => {
|
|
7
|
-
const nav = useNavigate()
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<nav className="flex items-center gap-1">
|
|
11
|
-
<Button variant="ghost" size="sm" onClick={nav.press(HOME)}>Home</Button>
|
|
12
|
-
<Button variant="ghost" size="sm" onClick={nav.press(web.session)}>Session</Button>
|
|
13
|
-
</nav>
|
|
14
|
-
)
|
|
15
|
-
}
|