@owlmeans/create-app 0.1.18-rc.8 → 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 (116) 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 -14
  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 -32
  97. package/template/sources/web/src/components/ui/dropdown-menu.tsx +257 -0
  98. package/template/sources/web/src/config.ts +8 -0
  99. package/template/sources/web/src/consts.ts +15 -0
  100. package/template/sources/web/src/context.bare.ts +13 -0
  101. package/template/sources/web/src/context.ts +1 -7
  102. package/template/sources/web/src/entrypoints.bare.ts +13 -0
  103. package/template/sources/web/src/entrypoints.ts +16 -0
  104. package/template/sources/web/src/index.css +1 -1
  105. package/template/sources/web/src/index.tsx +2 -2
  106. package/template/sources/web/src/layout/main.tsx +7 -2
  107. package/template/sources/web/src/nav.bare.ts +24 -0
  108. package/template/sources/web/src/nav.ts +3 -8
  109. package/template/sources/web/src/screens/home.bare.tsx +17 -0
  110. package/template/sources/web/src/screens/session.tsx +14 -16
  111. package/template/sources/web/src/vite-env.d.ts +8 -0
  112. package/template/sources/web/vite.config.ts +4 -2
  113. package/template/CLAUDE.md +0 -13
  114. package/template/sources/api/src/modules.ts +0 -11
  115. package/template/sources/common/src/modules.ts +0 -26
  116. package/template/sources/web/src/modules.ts +0 -22
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import type { SessionItem } from '__APP_SLUG__-common'
3
+ import { SESSION_ITEMS } from '../consts.js'
4
+ import type { Context } from '../types.js'
5
+ import type { SessionModel } from './session/types.js'
6
+
7
+ /**
8
+ * The session domain, bound to the context that holds its resource and to the session it serves.
9
+ * Built where it is used — `makeSessionModel(context, sid).list()` — never stored.
10
+ */
11
+ export const makeSessionModel = (context: Context, sid: string): SessionModel => {
12
+ const resource = context.getStaticResource<SessionItem>(SESSION_ITEMS)
13
+
14
+ const list: SessionModel['list'] = async () => {
15
+ // The resource answers the whole question — this session's items, newest first.
16
+ const { items } = await resource.list(
17
+ { sessionId: sid },
18
+ { sort: [{ field: 'createdAt', order: 'desc' }] }
19
+ )
20
+
21
+ return items
22
+ }
23
+
24
+ const add: SessionModel['add'] = async text => await resource.create({
25
+ id: randomUUID(),
26
+ sessionId: sid,
27
+ text,
28
+ createdAt: new Date().toISOString(),
29
+ })
30
+
31
+ const remove: SessionModel['remove'] = async id => {
32
+ const existing = await resource.load(id)
33
+ // Only remove the item if it belongs to this session.
34
+ if (existing == null || existing.sessionId !== sid) {
35
+ return { removed: false }
36
+ }
37
+
38
+ await resource.delete(id)
39
+ return { removed: true }
40
+ }
41
+
42
+ return { list, add, remove }
43
+ }
@@ -0,0 +1,5 @@
1
+ import type { AppConfig, AppContext } from '@owlmeans/server-app'
2
+
3
+ export interface Config extends AppConfig {}
4
+
5
+ export interface Context<C extends Config = Config> extends AppContext<C> {}
@@ -19,16 +19,16 @@
19
19
  "typecheck": "tsc -b"
20
20
  },
21
21
  "dependencies": {
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
- "ajv": "^8.17.1"
22
+ "@owlmeans/config": "^0.1.18",
23
+ "@owlmeans/entrypoint": "^0.1.18",
24
+ "@owlmeans/error": "^0.1.18",
25
+ "@owlmeans/resource": "^0.1.18",
26
+ "@owlmeans/route": "^0.1.18",
27
+ "ajv": "^8.20.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@owlmeans/dep-config": "^0.1.18-rc.0",
31
- "@types/node": "^24.10.1",
30
+ "@owlmeans/dep-config": "^0.1.18",
31
+ "@types/node": "^26.6.4",
32
32
  "typescript": "^7.0.2"
33
33
  }
34
34
  }
@@ -18,7 +18,8 @@ service({
18
18
  base: 'api',
19
19
  }, cfg)
20
20
 
21
- cfg.debug = { all: true }
21
+ // No log level here: this file runs in Bun AND in the browser, so each runtime's own config.ts
22
+ // sets `cfg.log` from its environment (api: LOG_LEVEL / LOG_DEBUG, web: VITE_LOG_LEVEL / VITE_LOG_DEBUG).
22
23
  cfg.alias = APP
23
24
  // Local dev serves the API over plain HTTP. Without this the web client builds https:// URLs
24
25
  // and every call fails. In production put the API behind TLS and remove this line.
@@ -0,0 +1,8 @@
1
+ /** Service aliases shared between web and api. */
2
+ export const APP = '__APP_SLUG__'
3
+ export const APP_WEB = '__APP_SLUG__-web'
4
+ export const APP_API = '__APP_SLUG__-api'
5
+
6
+ /** Local development ports. */
7
+ export const WEB_PORT = 3001
8
+ export const API_PORT = 3000
@@ -6,17 +6,3 @@ export const APP_API = '__APP_SLUG__-api'
6
6
  /** Local development ports. */
7
7
  export const WEB_PORT = 3001
8
8
  export const API_PORT = 3000
9
-
10
- /** Backend (API) entrypoint identifiers. */
11
- export const session = {
12
- base: '__APP_SLUG__:api:session',
13
- list: '__APP_SLUG__:api:session:list',
14
- add: '__APP_SLUG__:api:session:add',
15
- remove: '__APP_SLUG__:api:session:remove',
16
- }
17
-
18
- /** Frontend (web) entrypoint identifiers. */
19
- export const web = {
20
- session: '__APP_SLUG__:web:session',
21
- about: '__APP_SLUG__:web:about',
22
- }
@@ -0,0 +1,14 @@
1
+ import { openProtocol } from '@owlmeans/entrypoint'
2
+ import { BASE, HOME } from '@owlmeans/context'
3
+ import { frontend, route } from '@owlmeans/route'
4
+
5
+ const webBase = openProtocol(route(BASE, '/', frontend()))
6
+
7
+ /** The shared immutable protocol tree. Add API declarations below `api` as the shell grows. */
8
+ export const appProtocols = {
9
+ api: {},
10
+ web: {
11
+ base: webBase,
12
+ home: openProtocol(route(HOME, '/', frontend({ default: true, parent: webBase }))),
13
+ },
14
+ }
@@ -0,0 +1,60 @@
1
+ import { contract, openProtocol, protocol, typed } from '@owlmeans/entrypoint'
2
+ import { route, RouteMethod, frontend } from '@owlmeans/route'
3
+ import { AddItemSchema, ItemParamsSchema, SessionParamsSchema } from './schemas.js'
4
+ import type { SessionItem } from './types.js'
5
+
6
+ const aliases = {
7
+ session: {
8
+ base: '__APP_SLUG__:api:session',
9
+ list: '__APP_SLUG__:api:session:list',
10
+ add: '__APP_SLUG__:api:session:add',
11
+ remove: '__APP_SLUG__:api:session:remove',
12
+ },
13
+ web: {
14
+ base: '__APP_SLUG__:web:base',
15
+ home: '__APP_SLUG__:web:home',
16
+ session: '__APP_SLUG__:web:session',
17
+ about: '__APP_SLUG__:web:about',
18
+ },
19
+ }
20
+
21
+ const sessionBase = protocol(route(aliases.session.base, '/session'), contract())
22
+ const webBase = openProtocol(route(aliases.web.base, '/', frontend()))
23
+
24
+ /**
25
+ * Shared entrypoint declarations. The api materializes these with handlers; the web
26
+ * materializes them with screen components and calls them. Routes resolve under the
27
+ * api service `base` (`/api`), so e.g. `session.list` → `GET /api/session/:sid/items`.
28
+ */
29
+ export const session = {
30
+ base: sessionBase,
31
+ list: protocol(
32
+ route(aliases.session.list, '/:sid/items', { parent: sessionBase, method: RouteMethod.GET }),
33
+ contract.request({ params: SessionParamsSchema }, typed<SessionItem[]>()),
34
+ ),
35
+ add: protocol(
36
+ route(aliases.session.add, '/:sid/items', { parent: sessionBase, method: RouteMethod.POST }),
37
+ contract.request({
38
+ params: SessionParamsSchema,
39
+ body: AddItemSchema,
40
+ }, typed<SessionItem>()),
41
+ ),
42
+ remove: protocol(
43
+ route(aliases.session.remove, '/:sid/items/:id', { parent: sessionBase, method: RouteMethod.DELETE }),
44
+ contract.request({ params: ItemParamsSchema }, typed<{ removed: boolean }>()),
45
+ ),
46
+ }
47
+
48
+ /** Frontend routes are protocol declarations too; their renderer is bound by the web project. */
49
+ export const web = {
50
+ base: webBase,
51
+ home: openProtocol(route(aliases.web.home, '/', frontend({ default: true, parent: webBase }))),
52
+ session: openProtocol(route(aliases.web.session, '/session', frontend({ parent: webBase }))),
53
+ about: openProtocol(route(aliases.web.about, '/about', frontend({ parent: webBase }))),
54
+ }
55
+
56
+ /** The shared immutable protocol tree. Runtime packages bind their own local handlers and screens. */
57
+ export const appProtocols = Object.freeze({
58
+ api: Object.freeze({ session }),
59
+ web: Object.freeze(web),
60
+ })
@@ -0,0 +1,3 @@
1
+ export * from './consts.js'
2
+ export * from './config.js'
3
+ export * from './entrypoints.js'
@@ -2,4 +2,4 @@ export * from './consts.js'
2
2
  export * from './types.js'
3
3
  export * from './schemas.js'
4
4
  export * from './config.js'
5
- export * from './modules.js'
5
+ export * from './entrypoints.js'
@@ -1,25 +1,25 @@
1
- import type { JSONSchemaType } from 'ajv'
1
+ import { schema } from '@owlmeans/entrypoint'
2
2
  import type { AddItemPayload, ItemParams, SessionParams } from './types.js'
3
3
 
4
- export const AddItemSchema: JSONSchemaType<AddItemPayload> = {
4
+ export const AddItemSchema = schema<AddItemPayload>({
5
5
  type: 'object',
6
6
  properties: {
7
7
  text: { type: 'string', minLength: 1, maxLength: 280 },
8
8
  },
9
9
  required: ['text'],
10
10
  additionalProperties: false,
11
- }
11
+ })
12
12
 
13
- export const SessionParamsSchema: JSONSchemaType<SessionParams> = {
13
+ export const SessionParamsSchema = schema<SessionParams>({
14
14
  type: 'object',
15
15
  properties: {
16
16
  sid: { type: 'string', minLength: 1 },
17
17
  },
18
18
  required: ['sid'],
19
19
  additionalProperties: false,
20
- }
20
+ })
21
21
 
22
- export const ItemParamsSchema: JSONSchemaType<ItemParams> = {
22
+ export const ItemParamsSchema = schema<ItemParams>({
23
23
  type: 'object',
24
24
  properties: {
25
25
  sid: { type: 'string', minLength: 1 },
@@ -27,4 +27,4 @@ export const ItemParamsSchema: JSONSchemaType<ItemParams> = {
27
27
  },
28
28
  required: ['sid', 'id'],
29
29
  additionalProperties: false,
30
- }
30
+ })
@@ -0,0 +1,24 @@
1
+ shadcn/ui — copied UI components
2
+ Source: https://github.com/shadcn-ui/ui
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2023 shadcn
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
@@ -1,9 +1,11 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="__APP_LANG__">
3
3
 
4
4
  <head>
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta name="application-name" content="__APP_NAME__" />
8
+ <meta name="description" content="__APP_DESCRIPTION__" />
7
9
  <title>__APP_NAME__</title>
8
10
  </head>
9
11
 
@@ -11,41 +11,42 @@
11
11
  "preview": "vite preview"
12
12
  },
13
13
  "dependencies": {
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
- "@radix-ui/react-label": "^2.1.0",
25
- "@radix-ui/react-navigation-menu": "^1.2.14",
26
- "@radix-ui/react-progress": "^1.1.0",
27
- "@radix-ui/react-slot": "^1.1.0",
14
+ "@owlmeans/client": "^0.1.18",
15
+ "@owlmeans/client-config": "^0.1.18",
16
+ "@owlmeans/client-context": "^0.1.18",
17
+ "@owlmeans/client-entrypoint": "^0.1.18",
18
+ "@owlmeans/client-i18n": "^0.1.18",
19
+ "@owlmeans/entrypoint": "^0.1.18",
20
+ "@owlmeans/log": "^0.1.18",
21
+ "@owlmeans/route": "^0.1.18",
22
+ "@owlmeans/state": "^0.1.18",
23
+ "@owlmeans/web-client": "^0.1.18",
24
+ "@owlmeans/web-panel": "^0.1.18",
25
+ "@radix-ui/react-dropdown-menu": "^2.1.24",
26
+ "@radix-ui/react-label": "^2.1.15",
27
+ "@radix-ui/react-navigation-menu": "^1.2.22",
28
+ "@radix-ui/react-progress": "^1.1.16",
29
+ "@radix-ui/react-slot": "^1.3.3",
28
30
  "__APP_SLUG__-common": "workspace:^",
29
- "ajv": "^8.17.1",
30
- "class-variance-authority": "^0.7.0",
31
- "clsx": "^2.1.0",
32
- "i18next": "^26.2.0",
33
- "lucide-react": "^1.31.0",
34
- "react": "^19.2.8",
35
- "react-dom": "^19.2.8",
36
- "react-hook-form": "^7.62.0",
37
- "react-i18next": "^17.0.11",
38
- "tailwind-merge": "^3.6.0",
39
- "tailwindcss": "^4.1.13"
31
+ "ajv": "^8.20.0",
32
+ "class-variance-authority": "^0.7.1",
33
+ "clsx": "^2.1.1",
34
+ "i18next": "^26.4.2",
35
+ "lucide-react": "^1.52.0",
36
+ "react": "^19.3.0",
37
+ "react-dom": "^19.3.0",
38
+ "react-hook-form": "^7.89.0",
39
+ "react-i18next": "^17.0.15",
40
+ "tailwind-merge": "^3.7.0",
41
+ "tailwindcss": "^4.3.3"
40
42
  },
41
43
  "devDependencies": {
42
- "@owlmeans/dep-config": "^0.1.18-rc.0",
43
- "@tailwindcss/vite": "^4.1.13",
44
- "@types/react": "^19.2.17",
45
- "@types/react-dom": "^19.2.3",
46
- "@vitejs/plugin-react": "^6.0.3",
44
+ "@owlmeans/dep-config": "^0.1.18",
45
+ "@tailwindcss/vite": "^4.3.3",
46
+ "@types/react": "^19.3.0",
47
+ "@types/react-dom": "^19.3.0",
48
+ "@vitejs/plugin-react": "^6.1.1",
47
49
  "typescript": "^7.0.2",
48
- "vite": "^8.1.2",
49
- "vite-tsconfig-paths": "^5.1.4"
50
+ "vite": "^8.3.2"
50
51
  }
51
52
  }
@@ -0,0 +1,257 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function DropdownMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
13
+ }
14
+
15
+ function DropdownMenuPortal({
16
+ ...props
17
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18
+ return (
19
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20
+ )
21
+ }
22
+
23
+ function DropdownMenuTrigger({
24
+ ...props
25
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26
+ return (
27
+ <DropdownMenuPrimitive.Trigger
28
+ data-slot="dropdown-menu-trigger"
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function DropdownMenuContent({
35
+ className,
36
+ sideOffset = 4,
37
+ ...props
38
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
39
+ return (
40
+ <DropdownMenuPrimitive.Portal>
41
+ <DropdownMenuPrimitive.Content
42
+ data-slot="dropdown-menu-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </DropdownMenuPrimitive.Portal>
51
+ )
52
+ }
53
+
54
+ function DropdownMenuGroup({
55
+ ...props
56
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
57
+ return (
58
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
59
+ )
60
+ }
61
+
62
+ function DropdownMenuItem({
63
+ className,
64
+ inset,
65
+ variant = "default",
66
+ ...props
67
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
68
+ inset?: boolean
69
+ variant?: "default" | "destructive"
70
+ }) {
71
+ return (
72
+ <DropdownMenuPrimitive.Item
73
+ data-slot="dropdown-menu-item"
74
+ data-inset={inset}
75
+ data-variant={variant}
76
+ className={cn(
77
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ )
83
+ }
84
+
85
+ function DropdownMenuCheckboxItem({
86
+ className,
87
+ children,
88
+ checked,
89
+ ...props
90
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
91
+ return (
92
+ <DropdownMenuPrimitive.CheckboxItem
93
+ data-slot="dropdown-menu-checkbox-item"
94
+ className={cn(
95
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
96
+ className
97
+ )}
98
+ checked={checked}
99
+ {...props}
100
+ >
101
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
102
+ <DropdownMenuPrimitive.ItemIndicator>
103
+ <CheckIcon className="size-4" />
104
+ </DropdownMenuPrimitive.ItemIndicator>
105
+ </span>
106
+ {children}
107
+ </DropdownMenuPrimitive.CheckboxItem>
108
+ )
109
+ }
110
+
111
+ function DropdownMenuRadioGroup({
112
+ ...props
113
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
114
+ return (
115
+ <DropdownMenuPrimitive.RadioGroup
116
+ data-slot="dropdown-menu-radio-group"
117
+ {...props}
118
+ />
119
+ )
120
+ }
121
+
122
+ function DropdownMenuRadioItem({
123
+ className,
124
+ children,
125
+ ...props
126
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
127
+ return (
128
+ <DropdownMenuPrimitive.RadioItem
129
+ data-slot="dropdown-menu-radio-item"
130
+ className={cn(
131
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
132
+ className
133
+ )}
134
+ {...props}
135
+ >
136
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
137
+ <DropdownMenuPrimitive.ItemIndicator>
138
+ <CircleIcon className="size-2 fill-current" />
139
+ </DropdownMenuPrimitive.ItemIndicator>
140
+ </span>
141
+ {children}
142
+ </DropdownMenuPrimitive.RadioItem>
143
+ )
144
+ }
145
+
146
+ function DropdownMenuLabel({
147
+ className,
148
+ inset,
149
+ ...props
150
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
151
+ inset?: boolean
152
+ }) {
153
+ return (
154
+ <DropdownMenuPrimitive.Label
155
+ data-slot="dropdown-menu-label"
156
+ data-inset={inset}
157
+ className={cn(
158
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
159
+ className
160
+ )}
161
+ {...props}
162
+ />
163
+ )
164
+ }
165
+
166
+ function DropdownMenuSeparator({
167
+ className,
168
+ ...props
169
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
170
+ return (
171
+ <DropdownMenuPrimitive.Separator
172
+ data-slot="dropdown-menu-separator"
173
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
174
+ {...props}
175
+ />
176
+ )
177
+ }
178
+
179
+ function DropdownMenuShortcut({
180
+ className,
181
+ ...props
182
+ }: React.ComponentProps<"span">) {
183
+ return (
184
+ <span
185
+ data-slot="dropdown-menu-shortcut"
186
+ className={cn(
187
+ "text-muted-foreground ml-auto text-xs tracking-widest",
188
+ className
189
+ )}
190
+ {...props}
191
+ />
192
+ )
193
+ }
194
+
195
+ function DropdownMenuSub({
196
+ ...props
197
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
198
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
199
+ }
200
+
201
+ function DropdownMenuSubTrigger({
202
+ className,
203
+ inset,
204
+ children,
205
+ ...props
206
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
207
+ inset?: boolean
208
+ }) {
209
+ return (
210
+ <DropdownMenuPrimitive.SubTrigger
211
+ data-slot="dropdown-menu-sub-trigger"
212
+ data-inset={inset}
213
+ className={cn(
214
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
215
+ className
216
+ )}
217
+ {...props}
218
+ >
219
+ {children}
220
+ <ChevronRightIcon className="ml-auto size-4" />
221
+ </DropdownMenuPrimitive.SubTrigger>
222
+ )
223
+ }
224
+
225
+ function DropdownMenuSubContent({
226
+ className,
227
+ ...props
228
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
229
+ return (
230
+ <DropdownMenuPrimitive.SubContent
231
+ data-slot="dropdown-menu-sub-content"
232
+ className={cn(
233
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
234
+ className
235
+ )}
236
+ {...props}
237
+ />
238
+ )
239
+ }
240
+
241
+ export {
242
+ DropdownMenu,
243
+ DropdownMenuPortal,
244
+ DropdownMenuTrigger,
245
+ DropdownMenuContent,
246
+ DropdownMenuGroup,
247
+ DropdownMenuLabel,
248
+ DropdownMenuItem,
249
+ DropdownMenuCheckboxItem,
250
+ DropdownMenuRadioGroup,
251
+ DropdownMenuRadioItem,
252
+ DropdownMenuSeparator,
253
+ DropdownMenuShortcut,
254
+ DropdownMenuSub,
255
+ DropdownMenuSubTrigger,
256
+ DropdownMenuSubContent,
257
+ }
@@ -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)