@owlmeans/create-app 0.1.18-rc.7 → 0.1.18

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.
Files changed (119) hide show
  1. package/README.md +49 -5
  2. package/THIRD-PARTY-NOTICES.txt +24 -0
  3. package/build/args/types.d.ts +9 -0
  4. package/build/args/types.d.ts.map +1 -0
  5. package/build/args/types.js +2 -0
  6. package/build/args/types.js.map +1 -0
  7. package/build/args.d.ts +3 -20
  8. package/build/args.d.ts.map +1 -1
  9. package/build/args.js +121 -88
  10. package/build/args.js.map +1 -1
  11. package/build/bin.js +3 -3
  12. package/build/bin.js.map +1 -1
  13. package/build/consts.d.ts +6 -0
  14. package/build/consts.d.ts.map +1 -0
  15. package/build/consts.js +6 -0
  16. package/build/consts.js.map +1 -0
  17. package/build/consts.local.d.ts +12 -0
  18. package/build/consts.local.d.ts.map +1 -0
  19. package/build/consts.local.js +49 -0
  20. package/build/consts.local.js.map +1 -0
  21. package/build/index.d.ts +11 -4
  22. package/build/index.d.ts.map +1 -1
  23. package/build/index.js +5 -2
  24. package/build/index.js.map +1 -1
  25. package/build/naming/types.d.ts +14 -0
  26. package/build/naming/types.d.ts.map +1 -0
  27. package/build/naming/types.js +2 -0
  28. package/build/naming/types.js.map +1 -0
  29. package/build/naming.d.ts +4 -0
  30. package/build/naming.d.ts.map +1 -0
  31. package/build/naming.js +18 -0
  32. package/build/naming.js.map +1 -0
  33. package/build/run.d.ts +1 -1
  34. package/build/run.d.ts.map +1 -1
  35. package/build/run.js +23 -17
  36. package/build/run.js.map +1 -1
  37. package/build/scaffold.d.ts +8 -0
  38. package/build/scaffold.d.ts.map +1 -0
  39. package/build/scaffold.js +19 -0
  40. package/build/scaffold.js.map +1 -0
  41. package/build/template/types.d.ts +16 -0
  42. package/build/template/types.d.ts.map +1 -0
  43. package/build/template/types.js +2 -0
  44. package/build/template/types.js.map +1 -0
  45. package/build/template.d.ts +3 -16
  46. package/build/template.d.ts.map +1 -1
  47. package/build/template.js +69 -52
  48. package/build/template.js.map +1 -1
  49. package/build/types.d.ts +61 -0
  50. package/build/types.d.ts.map +1 -0
  51. package/build/types.js +2 -0
  52. package/build/types.js.map +1 -0
  53. package/package.json +8 -6
  54. package/template/AGENTS.md +61 -2
  55. package/template/README.bare.md +70 -0
  56. package/template/README.md +9 -60
  57. package/template/_agents/scripts/link-skills.sh +327 -15
  58. package/template/_agents/skills/agent-memory/SKILL.md +12 -11
  59. package/template/_agents/skills/getting-started/SKILL.md +38 -79
  60. package/template/_agents/skills/logging/SKILL.md +41 -0
  61. package/template/_agents/skills/memory-promotion/SKILL.md +10 -4
  62. package/template/_agents/skills/memory-recompact/SKILL.md +15 -14
  63. package/template/_agents/skills/owlmeans-code-structure/SKILL.md +188 -0
  64. package/template/_agents/skills/owlmeans-code-structure/parts/code-structure-reference.md +125 -0
  65. package/template/_agents/skills/reuse-code/SKILL.md +56 -10
  66. package/template/_agents/skills/self-education/SKILL.md +25 -5
  67. package/template/_agents/skills/skill-authoring/SKILL.md +132 -14
  68. package/template/_bare.json +24 -0
  69. package/template/_gitignore +8 -0
  70. package/template/bunfig.toml +2 -0
  71. package/template/package.json +3 -2
  72. package/template/sources/api/package.json +5 -4
  73. package/template/sources/api/src/app/session/add.ts +6 -17
  74. package/template/sources/api/src/app/session/list.ts +6 -12
  75. package/template/sources/api/src/app/session/remove.ts +6 -15
  76. package/template/sources/api/src/config.ts +7 -0
  77. package/template/sources/api/src/context.bare.ts +11 -0
  78. package/template/sources/api/src/context.ts +0 -4
  79. package/template/sources/api/src/entrypoints.bare.ts +4 -0
  80. package/template/sources/api/src/entrypoints.ts +13 -0
  81. package/template/sources/api/src/index.ts +8 -2
  82. package/template/sources/api/src/models/session/types.ts +14 -0
  83. package/template/sources/api/src/models/session.ts +43 -0
  84. package/template/sources/api/src/types.bare.ts +5 -0
  85. package/template/sources/common/package.json +8 -8
  86. package/template/sources/common/src/config.ts +2 -1
  87. package/template/sources/common/src/consts.bare.ts +8 -0
  88. package/template/sources/common/src/consts.ts +0 -13
  89. package/template/sources/common/src/entrypoints.bare.ts +14 -0
  90. package/template/sources/common/src/entrypoints.ts +60 -0
  91. package/template/sources/common/src/index.bare.ts +3 -0
  92. package/template/sources/common/src/index.ts +1 -1
  93. package/template/sources/common/src/schemas.ts +7 -7
  94. package/template/sources/web/THIRD-PARTY-NOTICES.txt +24 -0
  95. package/template/sources/web/index.html +3 -1
  96. package/template/sources/web/package.json +33 -31
  97. package/template/sources/web/src/components/ui/dropdown-menu.tsx +257 -0
  98. package/template/sources/web/src/components/ui/navigation-menu.tsx +169 -0
  99. package/template/sources/web/src/config.ts +8 -0
  100. package/template/sources/web/src/consts.ts +15 -0
  101. package/template/sources/web/src/context.bare.ts +13 -0
  102. package/template/sources/web/src/context.ts +1 -7
  103. package/template/sources/web/src/entrypoints.bare.ts +13 -0
  104. package/template/sources/web/src/entrypoints.ts +16 -0
  105. package/template/sources/web/src/index.css +7 -0
  106. package/template/sources/web/src/index.tsx +2 -2
  107. package/template/sources/web/src/layout/main.tsx +17 -19
  108. package/template/sources/web/src/nav.bare.ts +24 -0
  109. package/template/sources/web/src/nav.ts +23 -0
  110. package/template/sources/web/src/screens/about.tsx +19 -0
  111. package/template/sources/web/src/screens/home.bare.tsx +17 -0
  112. package/template/sources/web/src/screens/session.tsx +14 -16
  113. package/template/sources/web/src/vite-env.d.ts +8 -0
  114. package/template/sources/web/vite.config.ts +4 -2
  115. package/template/CLAUDE.md +0 -13
  116. package/template/sources/api/src/modules.ts +0 -11
  117. package/template/sources/common/src/modules.ts +0 -26
  118. package/template/sources/web/src/components/nav/main.tsx +0 -15
  119. package/template/sources/web/src/modules.ts +0 -20
@@ -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,15 @@
1
1
  import { config } from '@owlmeans/web-panel'
2
2
  import { APP_WEB, commonConfig } from '__APP_SLUG__-common'
3
3
  import type { Config } from './types.js'
4
+ // The module augmentation that gives the config its `log` field — a side-effect import only.
5
+ import type {} from '@owlmeans/log'
4
6
 
5
7
  const cfg: Config = config(APP_WEB, commonConfig as Config)
6
8
 
9
+ // The log level is decided at BUILD time (the logger is configured before the first render):
10
+ // `debug` under `vite`, `info` in a production build, unless `sources/web/.env` sets
11
+ // VITE_LOG_LEVEL; VITE_LOG_DEBUG lists scopes forced to debug (`*` for all).
12
+ const env = import.meta.env
13
+ cfg.log = { level: env.VITE_LOG_LEVEL || (env.PROD ? 'info' : 'debug'), debug: env.VITE_LOG_DEBUG ?? '' }
14
+
7
15
  export default cfg
@@ -0,0 +1,15 @@
1
+ import { type PanelNavLink, HOME } from '@owlmeans/web-panel'
2
+
3
+ /** The alias the session items are stored under. Screens address the store by it, never by path. */
4
+ export const SESSION_STATE = 'session-items'
5
+
6
+ /** The `localStorage` key that keeps this browser's session id across reloads. */
7
+ export const SESSION_ID_KEY = '__APP_SLUG__-sid'
8
+
9
+ // The platform/owner credit — "Powered by OwlMeans" and the copyright the platform delivers — is
10
+ // rendered by the shell itself (`NavLayout`'s `Footer`) and is never a footer link: an app link
11
+ // list is places IN the app, and the credit is not one of those.
12
+ export const footerLinks: PanelNavLink[] = [
13
+ { href: '/THIRD-PARTY-NOTICES.txt', label: 'Third-party notices' },
14
+ { alias: HOME, label: '__APP_NAME__' },
15
+ ]
@@ -0,0 +1,13 @@
1
+ import { makeContext as makeBasicContext, useContext as useBasicContext } from '@owlmeans/web-panel'
2
+ import type { Config, Context } from './types.js'
3
+
4
+ export const useContext = (): Context => useBasicContext<Config, Context>()
5
+
6
+ /**
7
+ * The framework's client store lives ON the context, so a screen, a guard and a service all
8
+ * reach the same records — which is what separates it from a store held beside the app in a
9
+ * module of its own. `appendStateResource<C, T>(context, ALIAS)` from '@owlmeans/state'
10
+ * (already a dependency) registers one; screens then address it by that alias, never by path.
11
+ */
12
+ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T =>
13
+ makeBasicContext<C, T>(cfg)
@@ -1,9 +1,7 @@
1
1
  import { makeContext as makeBasicContext, useContext as useBasicContext } from '@owlmeans/web-panel'
2
2
  import { appendStateResource } from '@owlmeans/state'
3
3
  import type { Config, Context } from './types.js'
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'
4
+ import { SESSION_STATE } from './consts.js'
7
5
 
8
6
  export const useContext = (): Context => useBasicContext<Config, Context>()
9
7
 
@@ -15,9 +13,5 @@ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T =
15
13
  // the app in a module of its own.
16
14
  appendStateResource<C, T>(context, SESSION_STATE)
17
15
 
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
16
  return context
23
17
  }
@@ -0,0 +1,13 @@
1
+ import { bindAll, bindScreen, entrypoints as baseEntrypoints, handler } from '@owlmeans/web-panel'
2
+ import { appProtocols } from '__APP_SLUG__-common'
3
+ import { MainLayout } from './layout/main.js'
4
+ import { HomeScreen } from './screens/home.js'
5
+
6
+ /** Local browser bindings for shared API and screen protocols. */
7
+ export const appBindings = [
8
+ ...baseEntrypoints,
9
+ ...bindAll(appProtocols.api),
10
+ // BASE renders the shared layout; HOME is its default child.
11
+ bindScreen(appProtocols.web.base, handler(MainLayout)),
12
+ bindScreen(appProtocols.web.home, handler(HomeScreen)),
13
+ ]
@@ -0,0 +1,16 @@
1
+ import { bindAll, bindScreen, entrypoints as baseEntrypoints, handler } from '@owlmeans/web-panel'
2
+ import { appProtocols } from '__APP_SLUG__-common'
3
+ import { MainLayout } from './layout/main.js'
4
+ import { AboutScreen } from './screens/about.js'
5
+ import { HomeScreen } from './screens/home.js'
6
+ import { SessionScreen } from './screens/session.js'
7
+
8
+ /** Local browser bindings for shared API and screen protocols. */
9
+ export const appBindings = [
10
+ ...baseEntrypoints,
11
+ ...bindAll(appProtocols.api),
12
+ bindScreen(appProtocols.web.base, handler(MainLayout)),
13
+ bindScreen(appProtocols.web.home, handler(HomeScreen)),
14
+ bindScreen(appProtocols.web.session, handler(SessionScreen)),
15
+ bindScreen(appProtocols.web.about, handler(AboutScreen)),
16
+ ]
@@ -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/src";
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
@@ -2,12 +2,12 @@ import './index.css'
2
2
  import { APP_API, APP_WEB } from '__APP_SLUG__-common'
3
3
  import config from './config.js'
4
4
  import { makeContext } from './context.js'
5
- import { appModules } from './modules.js'
5
+ import { appBindings } from './entrypoints.js'
6
6
  import { render } from './render.js'
7
7
  import type { Config, Context } from './types.js'
8
8
 
9
9
  const context = makeContext<Config, Context>(config)
10
- context.registerEntrypoints(appModules)
10
+ context.registerEntrypoints(appBindings)
11
11
 
12
12
  context.serviceRoute(APP_WEB, true)
13
13
  context.serviceRoute(APP_API, true)
@@ -1,21 +1,19 @@
1
1
  import type { FC, PropsWithChildren } from 'react'
2
- import { HOME, useNavigate } from '@owlmeans/web-panel'
3
- import { MainNavigation } from '@/components/nav/main'
2
+ import { NavLayout } from '@owlmeans/web-panel'
3
+ import { navConfig } from '@/nav'
4
+ import { footerLinks } from '@/consts'
4
5
 
5
- export const MainLayout: FC<PropsWithChildren> = ({ children }) => {
6
- const nav = useNavigate()
7
-
8
- return (
9
- <div className="min-h-screen bg-background text-foreground">
10
- <header className="border-b">
11
- <div className="mx-auto flex h-16 max-w-4xl items-center justify-between px-4">
12
- <a onClick={nav.press(HOME)} className="cursor-pointer text-lg font-semibold">
13
- __APP_NAME__
14
- </a>
15
- <MainNavigation />
16
- </div>
17
- </header>
18
- <main className="mx-auto max-w-4xl px-4 py-8">{children}</main>
19
- </div>
20
- )
21
- }
6
+ /**
7
+ * The application shell. `NavLayout` renders the header with the section menu, the side menu
8
+ * of the active section (hidden when that section has a single screen), the content, and the
9
+ * footer — the screen arrives as `children`.
10
+ *
11
+ * The page width comes from the shell, which applies it to the header, the content and the
12
+ * footer alike; set `containerClassName` to change it for all three at once, never a width on
13
+ * the content alone.
14
+ */
15
+ export const MainLayout: FC<PropsWithChildren> = ({ children }) => (
16
+ <NavLayout nav={navConfig} title="__APP_NAME__" footer={footerLinks}>
17
+ {children}
18
+ </NavLayout>
19
+ )
@@ -0,0 +1,24 @@
1
+ import { HOME } from '@owlmeans/web-panel'
2
+ import type { PanelNavConfig, PanelNavLink } from '@owlmeans/web-panel'
3
+
4
+ /**
5
+ * The application's navigation, as data.
6
+ *
7
+ * Sections are the top menu; their items are the side menu of whichever section is active.
8
+ * A section holding ONE item renders no side menu at all — which is why the single-screen
9
+ * shell below shows none. Labels here are literal; drop them to fall back on the panel i18n
10
+ * keys (`nav.<section>` / `modules.<alias>`) instead.
11
+ */
12
+ export const navConfig: PanelNavConfig = {
13
+ sections: [
14
+ { name: 'home', label: 'Home', items: [{ alias: HOME, label: 'Overview' }] },
15
+ ],
16
+ }
17
+
18
+ // The platform/owner credit — "Powered by OwlMeans" and the copyright the platform delivers — is
19
+ // rendered by the shell itself (`NavLayout`'s `Footer`) and is never a footer link: an app link
20
+ // list is places IN the app, and the credit is not one of those.
21
+ export const footerLinks: PanelNavLink[] = [
22
+ { href: '/THIRD-PARTY-NOTICES.txt', label: 'Third-party notices' },
23
+ { alias: HOME, label: '__APP_NAME__' },
24
+ ]
@@ -0,0 +1,23 @@
1
+ import { HOME, type PanelNavConfig } from '@owlmeans/web-panel'
2
+ import { web } from '__APP_SLUG__-common'
3
+
4
+ /**
5
+ * The application's navigation, as data.
6
+ *
7
+ * Sections are the top menu; their items are the side menu of whichever section is active.
8
+ * A section holding ONE item renders no side menu at all — `home` below shows that, `demo`
9
+ * shows the two-level case. Labels here are literal; drop them to fall back on the panel
10
+ * i18n keys (`nav.<section>` / `modules.<alias>`) instead.
11
+ */
12
+ export const navConfig: PanelNavConfig = {
13
+ sections: [
14
+ { name: 'home', label: 'Home', items: [{ alias: HOME, label: 'Overview' }] },
15
+ {
16
+ name: 'demo', label: 'Demo', items: [
17
+ { alias: web.session.alias, label: 'Session' },
18
+ { alias: web.about.alias, label: 'About' },
19
+ ]
20
+ },
21
+ ],
22
+ }
23
+
@@ -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
+ )
@@ -0,0 +1,17 @@
1
+ import type { FC } from 'react'
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
3
+
4
+ export const HomeScreen: FC = () => (
5
+ <Card>
6
+ <CardHeader>
7
+ <CardTitle>__APP_NAME__</CardTitle>
8
+ <CardDescription>__APP_DESCRIPTION__</CardDescription>
9
+ </CardHeader>
10
+ <CardContent>
11
+ <p className="text-sm text-muted-foreground">
12
+ Screens live in <code>src/screens</code>, the menu that reaches them in
13
+ {' '}<code>src/nav.ts</code>, and the routes they hang off in <code>src/entrypoints.ts</code>.
14
+ </p>
15
+ </CardContent>
16
+ </Card>
17
+ )
@@ -1,19 +1,17 @@
1
1
  import { useEffect, useState, type FC } from 'react'
2
- import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
3
2
  import { useStoreList } from '@owlmeans/client'
4
3
  import { session, type SessionItem } from '__APP_SLUG__-common'
5
- import { SESSION_STATE, useContext } from '../context.js'
4
+ import { useContext } from '../context.js'
5
+ import { SESSION_ID_KEY, SESSION_STATE } from '../consts.js'
6
6
  import { Button } from '@/components/ui/button'
7
7
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
8
8
  import { Input } from '@/components/ui/input'
9
9
 
10
- const SID_KEY = '__APP_SLUG__-sid'
11
-
12
10
  const sessionId = (): string => {
13
- let sid = localStorage.getItem(SID_KEY)
11
+ let sid = localStorage.getItem(SESSION_ID_KEY)
14
12
  if (sid == null) {
15
13
  sid = crypto.randomUUID()
16
- localStorage.setItem(SID_KEY, sid)
14
+ localStorage.setItem(SESSION_ID_KEY, sid)
17
15
  }
18
16
  return sid
19
17
  }
@@ -33,15 +31,15 @@ export const SessionScreen: FC = () => {
33
31
  */
34
32
  const items = useStoreList<SessionItem>({ query: {}, resource: SESSION_STATE })
35
33
 
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.
34
+ // The server is the source of truth; the store is what the screen reads. Fetch once, install
35
+ // what came back, and let the subscription render it. `replace` rather than a save per item:
36
+ // the endpoint answers with the session's whole set, so an item removed on another tab has to
37
+ // leave the store too, and one write wakes the subscribers once instead of once per record.
38
38
  const load = async () => {
39
- const [data] = await ctx
40
- .entrypoint<ClientEntrypoint<SessionItem[]>>(session.list)
39
+ const data = await ctx
40
+ .entrypoint(session.list)
41
41
  .call({ params: { sid } })
42
- for (const item of data ?? []) {
43
- await store.save(item)
44
- }
42
+ await store.replace(data ?? [])
45
43
  }
46
44
 
47
45
  useEffect(() => { void load() }, [])
@@ -50,8 +48,8 @@ export const SessionScreen: FC = () => {
50
48
  if (text.trim() === '') return
51
49
  setBusy(true)
52
50
  try {
53
- const [item] = await ctx
54
- .entrypoint<ClientEntrypoint<SessionItem>>(session.add)
51
+ const item = await ctx
52
+ .entrypoint(session.add)
55
53
  .call({ params: { sid }, body: { text } })
56
54
  setText('')
57
55
  if (item != null) {
@@ -64,7 +62,7 @@ export const SessionScreen: FC = () => {
64
62
 
65
63
  const remove = async (id: string) => {
66
64
  await ctx
67
- .entrypoint<ClientEntrypoint<{ removed: boolean }>>(session.remove)
65
+ .entrypoint(session.remove)
68
66
  .call({ params: { sid, id } })
69
67
  await store.delete(id)
70
68
  }
@@ -0,0 +1,8 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ /** `debug` | `info` | `warn` | `error` | `silent`. Default: `debug` under `vite`, `info` in a build. */
5
+ readonly VITE_LOG_LEVEL?: string
6
+ /** Scopes forced to debug whatever the level is, e.g. `session,i18n` or `*`. */
7
+ readonly VITE_LOG_DEBUG?: string
8
+ }
@@ -1,6 +1,6 @@
1
+ import { createThirdPartyNoticesPlugin } from '@owlmeans/dep-config/notices'
1
2
  import { defineConfig } from 'vite'
2
3
  import react from '@vitejs/plugin-react'
3
- import tsconfigPaths from 'vite-tsconfig-paths'
4
4
  import tailwindcss from '@tailwindcss/vite'
5
5
  import path from 'path'
6
6
 
@@ -14,13 +14,15 @@ const owlMeansSingletons = [
14
14
  ]
15
15
 
16
16
  export default defineConfig({
17
- plugins: [react(), tsconfigPaths(), tailwindcss()],
17
+ plugins: [createThirdPartyNoticesPlugin({ root: import.meta.dirname, additionalPackages: ['tailwindcss'] }), react(), tailwindcss()],
18
18
  server: {
19
19
  port: 3001,
20
20
  host: '0.0.0.0',
21
21
  strictPort: true,
22
22
  },
23
23
  resolve: {
24
+ // Vite resolves the `paths` of tsconfig.json itself since 8.
25
+ tsconfigPaths: true,
24
26
  alias: [
25
27
  { find: '@', replacement: path.resolve(__dirname, 'src') },
26
28
  ],
@@ -1,13 +0,0 @@
1
- @AGENTS.md
2
-
3
- ## Claude Code bridge
4
-
5
- All guidance lives in `AGENTS.md` (imported above) and in skills under
6
- `.agents/skills/<name>/SKILL.md` — the single canonical location shared with Copilot
7
- and Codex. `.claude/skills/` holds only generated per-skill symlinks (gitignored
8
- except `.gitkeep`); the committed `SessionStart` hook runs
9
- `sh .agents/scripts/link-skills.sh` to (re)create them each session.
10
-
11
- - Never author files under `.claude/skills/` — write skills in `.agents/skills/`.
12
- - After creating, renaming, or deleting a skill, re-run
13
- `sh .agents/scripts/link-skills.sh` so this session picks it up.
@@ -1,11 +0,0 @@
1
- import { elevate, modules } from '@owlmeans/server-app'
2
- import { session, sessionModules } from '__APP_SLUG__-common'
3
- import * as handlers from './app/session/index.js'
4
-
5
- // Attach handler implementations to the shared entrypoint declarations.
6
- elevate(sessionModules, session.base)
7
- elevate(sessionModules, session.list, handlers.list)
8
- elevate(sessionModules, session.add, handlers.add)
9
- elevate(sessionModules, session.remove, handlers.remove)
10
-
11
- export const appModules = [...modules, ...sessionModules]
@@ -1,26 +0,0 @@
1
- import { body, entrypoint, filter, params } from '@owlmeans/entrypoint'
2
- import { route, RouteMethod } from '@owlmeans/route'
3
- import { session } from './consts.js'
4
- import { AddItemSchema, ItemParamsSchema, SessionParamsSchema } from './schemas.js'
5
- import type { AddItemPayload, ItemParams, SessionParams } from './types.js'
6
-
7
- /**
8
- * Shared entrypoint declarations. The api elevates these with handlers; the web
9
- * elevates them with screen components and calls them. Routes resolve under the
10
- * api service `base` (`/api`), so e.g. `session.list` → `GET /api/session/:sid/items`.
11
- */
12
- export const sessionModules = [
13
- entrypoint(route(session.base, '/session')),
14
- entrypoint(
15
- route(session.list, '/:sid/items', { parent: session.base, method: RouteMethod.GET }),
16
- filter(params<SessionParams>(SessionParamsSchema)),
17
- ),
18
- entrypoint(
19
- route(session.add, '/:sid/items', { parent: session.base, method: RouteMethod.POST }),
20
- filter(params<SessionParams>(SessionParamsSchema, body<AddItemPayload>(AddItemSchema))),
21
- ),
22
- entrypoint(
23
- route(session.remove, '/:sid/items/:id', { parent: session.base, method: RouteMethod.DELETE }),
24
- filter(params<ItemParams>(ItemParamsSchema)),
25
- ),
26
- ]
@@ -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
- }
@@ -1,20 +0,0 @@
1
- import { BASE, elevate, entrypoint, frontend, handler, HOME, modules as baseModules, route } from '@owlmeans/web-panel'
2
- import { session, sessionModules, web } from '__APP_SLUG__-common'
3
- import { MainLayout } from './layout/main.js'
4
- import { HomeScreen } from './screens/home.js'
5
- import { SessionScreen } from './screens/session.js'
6
-
7
- const modules = [...baseModules, ...sessionModules]
8
-
9
- // Backend entrypoints — elevated without a component so the client can call them.
10
- elevate(modules, session.base)
11
- elevate(modules, session.list)
12
- elevate(modules, session.add)
13
- elevate(modules, session.remove)
14
-
15
- // Frontend layout + screens. BASE renders the shared layout; HOME is its default child.
16
- modules.push(entrypoint(route(BASE, '/', frontend()), handler(MainLayout)))
17
- modules.push(entrypoint(route(HOME, '/', frontend({ default: true, parent: BASE })), handler(HomeScreen)))
18
- modules.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
19
-
20
- export const appModules = modules