@owlmeans/web-panel 0.1.18-rc.40 → 0.1.18-rc.41

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 (157) hide show
  1. package/README.md +391 -121
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/web-panel/SKILL.md +73 -3
  4. package/build/@/components/ui/alert-dialog.d.ts +15 -0
  5. package/build/@/components/ui/alert-dialog.d.ts.map +1 -0
  6. package/build/@/components/ui/alert-dialog.js +39 -0
  7. package/build/@/components/ui/alert-dialog.js.map +1 -0
  8. package/build/@/components/ui/dropdown-menu.d.ts +26 -0
  9. package/build/@/components/ui/dropdown-menu.d.ts.map +1 -0
  10. package/build/@/components/ui/dropdown-menu.js +51 -0
  11. package/build/@/components/ui/dropdown-menu.js.map +1 -0
  12. package/build/auth/entrypoints.d.ts +2 -2
  13. package/build/auth/entrypoints.d.ts.map +1 -1
  14. package/build/components/index.d.ts +1 -0
  15. package/build/components/index.d.ts.map +1 -1
  16. package/build/components/index.js +1 -0
  17. package/build/components/index.js.map +1 -1
  18. package/build/components/login/append.d.ts.map +1 -1
  19. package/build/components/login/append.js +5 -0
  20. package/build/components/login/append.js.map +1 -1
  21. package/build/components/login/index.d.ts +1 -0
  22. package/build/components/login/index.d.ts.map +1 -1
  23. package/build/components/login/index.js +1 -0
  24. package/build/components/login/index.js.map +1 -1
  25. package/build/components/login/notify.d.ts +18 -0
  26. package/build/components/login/notify.d.ts.map +1 -0
  27. package/build/components/login/notify.js +42 -0
  28. package/build/components/login/notify.js.map +1 -0
  29. package/build/components/menu/component.d.ts +15 -0
  30. package/build/components/menu/component.d.ts.map +1 -0
  31. package/build/components/menu/component.js +146 -0
  32. package/build/components/menu/component.js.map +1 -0
  33. package/build/components/menu/index.d.ts +3 -0
  34. package/build/components/menu/index.d.ts.map +1 -0
  35. package/build/components/menu/index.js +3 -0
  36. package/build/components/menu/index.js.map +1 -0
  37. package/build/components/menu/types.d.ts +138 -0
  38. package/build/components/menu/types.d.ts.map +1 -0
  39. package/build/components/menu/types.js +17 -0
  40. package/build/components/menu/types.js.map +1 -0
  41. package/build/components/panel-app/component.d.ts.map +1 -1
  42. package/build/components/panel-app/component.js +3 -2
  43. package/build/components/panel-app/component.js.map +1 -1
  44. package/build/components/socket/i18n/be.json +7 -0
  45. package/build/components/socket/i18n/de.json +7 -0
  46. package/build/components/socket/i18n/en.json +7 -0
  47. package/build/components/socket/i18n/es.json +7 -0
  48. package/build/components/socket/i18n/pl.json +7 -0
  49. package/build/components/socket/i18n/ru.json +7 -0
  50. package/build/components/socket/i18n/uk.json +7 -0
  51. package/build/components/socket/i18n.d.ts +2 -0
  52. package/build/components/socket/i18n.d.ts.map +1 -0
  53. package/build/components/socket/i18n.js +16 -0
  54. package/build/components/socket/i18n.js.map +1 -0
  55. package/build/components/socket/index.d.ts +3 -0
  56. package/build/components/socket/index.d.ts.map +1 -0
  57. package/build/components/socket/index.js +3 -0
  58. package/build/components/socket/index.js.map +1 -0
  59. package/build/components/socket/reload-dialog.d.ts +18 -0
  60. package/build/components/socket/reload-dialog.d.ts.map +1 -0
  61. package/build/components/socket/reload-dialog.js +26 -0
  62. package/build/components/socket/reload-dialog.js.map +1 -0
  63. package/build/consent/component.d.ts +5 -0
  64. package/build/consent/component.d.ts.map +1 -1
  65. package/build/consent/component.js +11 -25
  66. package/build/consent/component.js.map +1 -1
  67. package/build/consent/consts.d.ts +2 -0
  68. package/build/consent/consts.d.ts.map +1 -0
  69. package/build/consent/consts.js +2 -0
  70. package/build/consent/consts.js.map +1 -0
  71. package/build/consent/index.d.ts +6 -1
  72. package/build/consent/index.d.ts.map +1 -1
  73. package/build/consent/index.js +4 -0
  74. package/build/consent/index.js.map +1 -1
  75. package/build/consent/menu-widget.d.ts +13 -0
  76. package/build/consent/menu-widget.d.ts.map +1 -0
  77. package/build/consent/menu-widget.js +20 -0
  78. package/build/consent/menu-widget.js.map +1 -0
  79. package/build/consent/presence.d.ts +24 -0
  80. package/build/consent/presence.d.ts.map +1 -0
  81. package/build/consent/presence.js +34 -0
  82. package/build/consent/presence.js.map +1 -0
  83. package/build/consent/service.d.ts +18 -0
  84. package/build/consent/service.d.ts.map +1 -0
  85. package/build/consent/service.js +53 -0
  86. package/build/consent/service.js.map +1 -0
  87. package/build/consent/translate.d.ts +14 -0
  88. package/build/consent/translate.d.ts.map +1 -0
  89. package/build/consent/translate.js +28 -0
  90. package/build/consent/translate.js.map +1 -0
  91. package/build/consent/types.d.ts +19 -0
  92. package/build/consent/types.d.ts.map +1 -0
  93. package/build/consent/types.js +2 -0
  94. package/build/consent/types.js.map +1 -0
  95. package/build/context.d.ts.map +1 -1
  96. package/build/context.js +6 -0
  97. package/build/context.js.map +1 -1
  98. package/build/entrypoints.d.ts +2 -2
  99. package/build/hooks/index.d.ts +2 -0
  100. package/build/hooks/index.d.ts.map +1 -0
  101. package/build/hooks/index.js +2 -0
  102. package/build/hooks/index.js.map +1 -0
  103. package/build/hooks/use-mobile.d.ts +16 -0
  104. package/build/hooks/use-mobile.d.ts.map +1 -0
  105. package/build/hooks/use-mobile.js +30 -0
  106. package/build/hooks/use-mobile.js.map +1 -0
  107. package/build/jobs/component.d.ts +19 -0
  108. package/build/jobs/component.d.ts.map +1 -0
  109. package/build/jobs/component.js +55 -0
  110. package/build/jobs/component.js.map +1 -0
  111. package/build/jobs/index.d.ts +4 -0
  112. package/build/jobs/index.d.ts.map +1 -0
  113. package/build/jobs/index.js +3 -0
  114. package/build/jobs/index.js.map +1 -0
  115. package/build/jobs/toasts.d.ts +14 -0
  116. package/build/jobs/toasts.d.ts.map +1 -0
  117. package/build/jobs/toasts.js +39 -0
  118. package/build/jobs/toasts.js.map +1 -0
  119. package/build/jobs/types.d.ts +21 -0
  120. package/build/jobs/types.d.ts.map +1 -0
  121. package/build/jobs/types.js +2 -0
  122. package/build/jobs/types.js.map +1 -0
  123. package/build/types.d.ts +11 -1
  124. package/build/types.d.ts.map +1 -1
  125. package/build/utils.d.ts +13 -0
  126. package/build/utils.d.ts.map +1 -0
  127. package/build/utils.js +14 -0
  128. package/build/utils.js.map +1 -0
  129. package/package.json +8 -4
  130. package/src/@/components/ui/alert-dialog.tsx +156 -0
  131. package/src/components/index.ts +1 -0
  132. package/src/components/login/append.ts +6 -0
  133. package/src/components/login/index.ts +1 -0
  134. package/src/components/login/notify.ts +53 -0
  135. package/src/components/panel-app/component.tsx +8 -0
  136. package/src/components/socket/i18n/be.json +7 -0
  137. package/src/components/socket/i18n/de.json +7 -0
  138. package/src/components/socket/i18n/en.json +7 -0
  139. package/src/components/socket/i18n/es.json +7 -0
  140. package/src/components/socket/i18n/pl.json +7 -0
  141. package/src/components/socket/i18n/ru.json +7 -0
  142. package/src/components/socket/i18n/uk.json +7 -0
  143. package/src/components/socket/i18n.ts +17 -0
  144. package/src/components/socket/index.ts +2 -0
  145. package/src/components/socket/reload-dialog.tsx +45 -0
  146. package/src/consent/component.tsx +16 -29
  147. package/src/consent/consts.ts +1 -0
  148. package/src/consent/index.ts +9 -2
  149. package/src/consent/menu-widget.tsx +22 -0
  150. package/src/consent/presence.ts +39 -0
  151. package/src/consent/service.ts +75 -0
  152. package/src/consent/translate.ts +31 -0
  153. package/src/consent/types.ts +21 -0
  154. package/src/context.ts +7 -0
  155. package/src/types.ts +11 -1
  156. package/tests/harness/mount.tsx +36 -1
  157. package/tests/socket-dialog.spec.ts +69 -0
package/README.md CHANGED
@@ -1,18 +1,14 @@
1
1
  # @owlmeans/web-panel
2
2
 
3
- shadcn UI + Tailwind v4 implementation of the OwlMeans web panel layer.
4
- Wraps the headless `@owlmeans/client-panel` logic with shadcn primitives
5
- instead of Material UI.
3
+ shadcn UI + Tailwind v4 implementation of the OwlMeans web panel layer. It wraps the headless
4
+ `@owlmeans/client-panel` logic and `@owlmeans/web-client`'s context with shadcn primitives: the base
5
+ context factory, the navigation shell, forms, panels, the sign-in screen, toasts and the socket
6
+ reload prompt. New OwlMeans web apps build on it. An app that renders only its own components and
7
+ needs no panel can stay on [`@owlmeans/web-client`](../web-client). Material UI apps already on
8
+ [`@owlmeans/mui-panel`](../mui-panel) are legacy: the same surface rendered with MUI, maintained
9
+ but never started.
6
10
 
7
- ## When to use this package
8
-
9
- - New OwlMeans web apps using the package's shipped shadcn primitives and a
10
- Tailwind v4 theme.
11
- - Drop-in replacement for the previous Material-UI based `@owlmeans/web-panel`
12
- (which has been renamed to [`@owlmeans/mui-panel`](../mui-panel) for
13
- backwards-compatibility consumers).
14
-
15
- ## Install
11
+ ## Installation
16
12
 
17
13
  ```sh
18
14
  bun add @owlmeans/web-panel@^0.1.18-rc.31
@@ -21,19 +17,34 @@ bun add @owlmeans/web-panel@^0.1.18-rc.31
21
17
  Peer requirements (the consuming app provides these): `react`, `react-dom`,
22
18
  `react-hook-form`, `ajv`, `tailwindcss@^4`, `lucide-react`, `clsx`,
23
19
  `tailwind-merge`, `class-variance-authority`, plus the radix primitives
24
- listed in `peerDependencies`.
25
-
26
- ## Consumer setup package boundary
27
-
28
- The package ships its shadcn primitives and `cn` helper as private implementation files. Its build
29
- uses relative imports into that private tree, so a consumer needs neither an `@` bundler alias nor
30
- a matching copy of the primitives. The consumer supplies the declared React, Tailwind and Radix peer
31
- dependencies and may use its own unrelated shadcn setup.
32
-
33
- ### 1. Add Tailwind v4 theme tokens
34
-
35
- The components rely on the following CSS variables (defined inside
36
- `@theme` in your app's globals.css):
20
+ listed in `peerDependencies` (`alert-dialog`, `label`, `navigation-menu`, `progress`, `separator`,
21
+ `slot`). Also declare `ajv-formats` next to `ajv`: the form model imports it, but no dependency
22
+ section of this manifest lists it.
23
+
24
+ ## Concepts
25
+
26
+ - **Panel context** `makeContext(cfg)` is `@owlmeans/web-client`'s context plus
27
+ `apiConfigMiddleware`, the flow service (`context.flow()`), the socket-status service and the
28
+ default shadcn sign-in screen. The app factory calls it and appends its own mixins.
29
+ - **Package boundary** the shadcn primitives and `cn` are private files under the package's own
30
+ `build/@/` tree, imported relatively. A consumer needs no `@` alias and vendors nothing. It
31
+ supplies the peers, its Tailwind theme tokens and an `@source` line for this package.
32
+ - **Two-layer navigation** the top menu lists sections and the side menu lists the active
33
+ section's screens. A section holding a single screen renders no side menu. The model
34
+ (`usePanelNav`, `PanelNav*`) is headless in `@owlmeans/client-panel`.
35
+ - **Style slots** — `className` (page root), `headerClassName` (sticky bar), `contentClassName`
36
+ and `containerClassName` (width and padding of all three rows) are merged over their defaults
37
+ with tailwind-merge, never substituted.
38
+ - **Global overlays** — `PanelApp` renders its `children` and `SocketReloadDialog` beside the
39
+ router, so a dialog survives navigation.
40
+ - **`translate` props** — the navigation and login components take a
41
+ `(key, defaultValue) => string` resolver as a prop instead of reading an i18n provider implicitly.
42
+
43
+ ## Usage
44
+
45
+ ### Consumer setup — theme tokens and `@source`
46
+
47
+ The components paint with these CSS variables, defined inside `@theme` in the app's globals.css:
37
48
 
38
49
  - `--color-background`, `--color-foreground`
39
50
  - `--color-card`, `--color-card-foreground`
@@ -48,14 +59,13 @@ The components rely on the following CSS variables (defined inside
48
59
  - `--animate-progress-indeterminate` + the `@keyframes progress-indeterminate`
49
60
  rule for the `Progress` component's indeterminate mode
50
61
 
51
- A working set is shipped at `src/@/globals.css` (for dev/test only).
52
-
53
- ### 2. Tailwind `@source` for this package
62
+ A working set is shipped at `src/@/globals.css` (for dev/test only). Toasts additionally read
63
+ `--popover`, `--popover-foreground` and `--border`.
54
64
 
55
- Tailwind's scanner reads your CSS root plus its `@source` directives, and it
56
- excludes `node_modules`. Classes that exist only inside this package's
57
- components — the whole navigation shell and footer — therefore never reach your
58
- stylesheet unless you point Tailwind at the built package:
65
+ Tailwind's scanner reads your CSS root plus its `@source` directives, and it excludes
66
+ `node_modules`. Classes that exist only inside this package's components — the whole navigation
67
+ shell and footer — therefore never reach your stylesheet unless you point Tailwind at the
68
+ installed package's `src`:
59
69
 
60
70
  ```css
61
71
  @import "tailwindcss";
@@ -63,26 +73,124 @@ stylesheet unless you point Tailwind at the built package:
63
73
  @source "../../../node_modules/@owlmeans/web-panel/src";
64
74
  ```
65
75
 
66
- Adjust the relative depth to your own layout; the target is this package's
67
- installed `src` directory.
76
+ Adjust the relative depth to your own layout. An app using `./consent` adds a second `@source` for
77
+ `@owlmeans/web-consent`'s `src`.
78
+
79
+ ### 1. Config and context factory
80
+
81
+ ```ts
82
+ // src/config.ts
83
+ import { config } from '@owlmeans/web-panel'
84
+ import { commonConfig, MY_APP_WEB } from 'my-app-common'
85
+ import type { Config } from './types.js'
86
+
87
+ const cfg: Config = config(MY_APP_WEB, commonConfig as Config)
88
+ // Block the screen with a "reload the page" prompt once every socket has given up reconnecting.
89
+ cfg.socket = { ...cfg.socket, reloadDialog: true }
90
+
91
+ export default cfg
92
+ ```
93
+
94
+ ```ts
95
+ // src/context.ts
96
+ import { appendLoginScreen, makeContext as makePanelContext, useContext as usePanelContext } from '@owlmeans/web-panel'
97
+ import type { AppConfig, AppContext } from '@owlmeans/web-panel'
98
+ import { appendConsentWidgetService } from '@owlmeans/web-panel/consent'
99
+ import { appendStateResource } from '@owlmeans/state'
100
+ import { BrandMark } from './components/brand.js'
101
+ import { PROJECT_STATE } from './consts.js'
102
+
103
+ export interface Config extends AppConfig {}
104
+ export interface Context<C extends Config = Config> extends AppContext<C> {}
105
+
106
+ export const useContext = (): Context => usePanelContext<Config, Context>()
107
+
108
+ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T => {
109
+ const context = makePanelContext<C, T>(cfg)
110
+ // Idempotent: makeContext registered the default screen; this puts the app's mark on it.
111
+ appendLoginScreen<C, T>(context, { Logo: BrandMark })
112
+ appendConsentWidgetService<C, T>(context)
113
+ appendStateResource<C, T>(context, PROJECT_STATE)
114
+
115
+ return context
116
+ }
117
+ ```
118
+
119
+ ### 2. Entrypoints and mounting with overlays
120
+
121
+ ```ts
122
+ // src/entrypoints.ts
123
+ import { bindAll, bindScreen, entrypoints as panelEntrypoints, handler } from '@owlmeans/web-panel'
124
+ import { entrypoints as authEntrypoints } from '@owlmeans/client-auth'
125
+ import { apiProtocols, webProtocols } from 'my-app-common'
126
+ import { MainLayout } from './layout/main.js'
127
+ import { HomeScreen } from './screens/home.js'
128
+ import { CookiePolicyScreen } from './screens/legal/cookies.js'
129
+
130
+ export const appEntrypoints = [
131
+ ...authEntrypoints,
132
+ ...panelEntrypoints,
133
+ ...bindAll(apiProtocols),
134
+ bindScreen(webProtocols.base, handler(MainLayout)),
135
+ bindScreen(webProtocols.home, handler(HomeScreen)),
136
+ bindScreen(webProtocols.legal.cookies, handler(CookiePolicyScreen)),
137
+ ]
138
+ ```
139
+
140
+ `render(context, { rootClassName })` mounts `PanelApp` with the browser language detector
141
+ installed. An app with global overlays of its own mounts `PanelApp` itself:
142
+
143
+ ```tsx
144
+ // src/render.tsx
145
+ import type { FC } from 'react'
146
+ import { render as mount } from '@owlmeans/web-client'
147
+ import type { AppContext } from '@owlmeans/web-client'
148
+ import { useI18nInstance } from '@owlmeans/client-i18n/utils'
149
+ import { PanelApp } from '@owlmeans/web-panel'
150
+ import { PanelCookieConsent } from '@owlmeans/web-panel/consent'
151
+ import detector from 'i18next-browser-languagedetector'
152
+
153
+ const App: FC<{ context: AppContext }> = ({ context }) => {
154
+ useI18nInstance(context.cfg).use(detector)
155
+
156
+ // Children of PanelApp sit beside the router, so the dialog survives navigation.
157
+ return <PanelApp context={context} rootClassName="dark">
158
+ <PanelCookieConsent policyHref="/legal/cookies" />
159
+ </PanelApp>
160
+ }
161
+
162
+ export const render = (context: AppContext) => mount(<App context={context} />)
163
+ ```
164
+
165
+ ```ts
166
+ // src/index.tsx
167
+ const context = makeContext(config)
168
+ context.registerEntrypoints(appEntrypoints)
169
+ context.serviceRoute(MY_APP_WEB, true)
170
+ render(context)
171
+ ```
68
172
 
69
- ## Navigation shell
173
+ ### 3. Navigation shell, toasts and a guarded layout
70
174
 
71
- `NavLayout` is the standard application shell header, section menu, the active
72
- section's screen menu, content, footer. A layout entrypoint binds a component
73
- that renders it and nothing else; the matched screen arrives as `children`.
175
+ `NavLayout` is the standard shell: header, section menu, the active section's screen menu, content
176
+ and footer. A layout entrypoint binds a component that renders it; the matched screen arrives as
177
+ `children`. Keep the navigation as data in its own module, so screens, entrypoints and the shell
178
+ read the same aliases.
74
179
 
75
180
  ```tsx
76
- import { NavLayout, HOME } from '@owlmeans/web-panel'
181
+ import type { FC, PropsWithChildren } from 'react'
182
+ import { HOME, NavLayout, Toaster, useI18nApp } from '@owlmeans/web-panel'
77
183
  import type { PanelNavConfig, PanelNavLink } from '@owlmeans/web-panel'
184
+ import { useSelfAuth } from '@owlmeans/client-auth'
185
+ import { webProtocols } from 'my-app-common'
78
186
 
79
- const navConfig: PanelNavConfig = {
187
+ export const navConfig: PanelNavConfig = {
80
188
  sections: [
81
189
  { name: 'home', label: 'Home', items: [{ alias: HOME, label: 'Overview' }] },
82
190
  {
83
- name: 'demo', label: 'Demo', items: [
84
- { alias: web.session, label: 'Session' },
85
- { alias: web.about, label: 'About' },
191
+ name: 'projects', items: [
192
+ { alias: webProtocols.projects.alias },
193
+ { alias: webProtocols.archive.alias },
86
194
  ]
87
195
  },
88
196
  ],
@@ -90,98 +198,260 @@ const navConfig: PanelNavConfig = {
90
198
 
91
199
  const footerLinks: PanelNavLink[] = [
92
200
  { alias: HOME, label: 'My App' },
93
- { href: 'https://owlmeans.com', label: 'OwlMeans', open: true },
201
+ { href: 'https://example.com/docs', label: 'Docs', open: true },
94
202
  ]
95
203
 
96
- export const MainLayout: FC<PropsWithChildren> = ({ children }) => (
97
- <NavLayout nav={navConfig} title="My App" footer={footerLinks}>
98
- {children}
99
- </NavLayout>
100
- )
204
+ export const MainLayout: FC<PropsWithChildren> = ({ children }) => {
205
+ useSelfAuth(true)
206
+ const t = useI18nApp('menu')
207
+
208
+ return <>
209
+ <NavLayout nav={navConfig} title="My App" footer={footerLinks}
210
+ translate={(key, defaultValue) => t(key, { defaultValue })}
211
+ headerClassName="bg-secondary text-secondary-foreground"
212
+ containerClassName="max-w-[1280px]">
213
+ {children}
214
+ </NavLayout>
215
+ {/* Exactly once, in the layout. Messages come from `toast.success(...)` in `sonner`. */}
216
+ <Toaster />
217
+ </>
218
+ }
101
219
  ```
102
220
 
103
- Navigation is **two-layer**: the top menu lists sections, the side menu lists the
104
- active section's screens. A section holding a single screen renders **no side
105
- menu at all**. `NavLayout` mounts `SideNav` twice — a `hidden md:block` column
106
- beside the content and a `md:hidden` strip under the header — so the same items
107
- serve wide and narrow viewports.
221
+ Labels resolve as literal `label`, then `translate(key, humanized alias)`, then the humanized
222
+ alias. Default keys are `nav.<section>` and `modules.<alias>`.
108
223
 
109
224
  | Component | Props |
110
225
  |---|---|
111
- | `NavLayout` | `nav`, `translate?`, `title?`, `home?` (brand target; defaults to the first section's first item), `actions?`, `footer?` (`PanelNavLink[]` renders the standard footer, a node replaces it), `contentClassName?`, `className?`, `style?` |
226
+ | `NavLayout` | `nav`, `translate?`, `title?`, `home?` (brand target; defaults to the first section's first item), `actions?`, `footer?` (`PanelNavLink[]` renders the standard footer, a node replaces it), `headerClassName?`, `contentClassName?`, `containerClassName?`, `className?`, `style?` |
112
227
  | `TopNav` | `config`, `translate?`, `ariaLabel?`, `className?`, `style?` |
113
228
  | `SideNav` | the same, plus `variant?: 'side' \| 'bar'` |
114
- | `Footer` | `links?`, `translate?`, `children?`, `className?`, `style?` |
115
-
116
- The navigation model itself (`usePanelNav`, `PanelNavConfig`, `PanelNavItem`,
117
- `PanelNavSection`, `PanelNavLink`, `NavTranslate`) is headless and lives in
118
- [`@owlmeans/client-panel`](../client-panel); these components are its shadcn
119
- rendering.
120
-
121
- Two things to know when wiring it:
122
-
123
- - **Labels do not use i18n unless you pass a resolver.** `translate` is a prop
124
- (`NavTranslate`), defaulting to one that returns the fallback, because an app
125
- rendered without an i18n provider throws if a menu reads the panel i18n
126
- context. Labels resolve as literal `label` → `translate(key, humanized)` →
127
- humanized alias, with default keys `nav.<section>` and `modules.<alias>`.
128
- - **A parent route needs a `default: true` child.** A frontend entrypoint with
129
- children but no default child renders blank at its own path — give a grouping
130
- screen an index child at `'/'`.
131
-
132
- ## Breaking changes vs `@owlmeans/mui-panel`
133
-
134
- This package keeps the same public name and re-export surface as the
135
- previous MUI implementation, but the following props/types have changed:
136
-
137
- - **`styles?: SxProps` is removed.** Use the new `className?: string` and
138
- `style?: React.CSSProperties` props instead. Affected: `BlockProps`,
139
- `TextProps`, `LinkProps`, `WebFormProps`, `LayoutProps`.
140
- - **`variant` on `Text` / `Link` is now `TextVariant`**, a string-literal
141
- union: `'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'lead' | 'large' | 'small' |
142
- 'muted' | 'blockquote'`. The previous MUI `TypographyOwnProps['variant']`
143
- union no longer applies.
144
- - **`PanelAppProps.theme?: Theme` is replaced with
145
- `PanelAppProps.rootClassName?: string`.** Apply your theme via Tailwind
146
- classes / CSS variables instead of MUI's `Theme` object.
147
- - **`render(context, theme?, opts?)` signature → `render(context, opts?)`**
148
- where `opts` includes the new `rootClassName?: string`.
149
- - **`scalingToStyles()`** now returns a class-name string (composable with
150
- `cn()`), not an `SxProps` object. The `theme?: Theme` parameter is
151
- removed (Tailwind handles breakpoints declaratively).
152
- - **`useBreakPoint` / `useMapBreakpoint`** now use Tailwind's static
153
- default breakpoints (`xs/sm/md/lg/xl`) instead of MUI's `Theme.breakpoints`.
154
- Customise via Tailwind config in the consuming app.
155
- - **Component prop API for buttons** stays compatible (`'small' | 'medium'
156
- | 'large'`, `variant` strings `contained`/`outlined`/`text`/shadcn names).
157
- Internally they map to shadcn variants.
158
-
159
- Consumers that need the previous MUI behaviour should swap to
160
- [`@owlmeans/mui-panel`](../mui-panel) — same exports, MUI-rendered.
161
-
162
- ## Public exports
229
+ | `Footer` | `links?`, `translate?`, `containerClassName?`, `children?`, `className?`, `style?` |
163
230
 
164
- ```ts
165
- import {
166
- PanelApp, Layout, NavLayout, TopNav, SideNav, Footer,
167
- Form, TextInput, SubmitButton, Button, ButtonSelector,
168
- Block, Text, Link, Status, ImageUploader,
169
- scalingToStyles, useBreakPoint, useMapBreakpoint,
170
- render,
171
- } from '@owlmeans/web-panel'
172
-
173
- import { setupExternalAuthentication } from '@owlmeans/web-panel/auth'
174
- import { entrypoints } from '@owlmeans/web-panel/auth/entrypoints'
231
+ ### 4. Forms, panels and status
232
+
233
+ `Form` owns the whole model: `useForm` with an `ajvResolver` over `validation`, published through
234
+ `FormProvider` and `FormContext`, so every control below reads one form.
235
+
236
+ ```tsx
237
+ import type { FC } from 'react'
238
+ import { useCallback } from 'react'
239
+ import { toast } from 'sonner'
240
+ import { Block, BlockScaling, Button, Form, PanelContext, SubmitButton, TextInput, useFormRef } from '@owlmeans/web-panel'
241
+ import { apiProtocols, ProjectCreateSchema } from 'my-app-common'
242
+ import type { ProjectCreate } from 'my-app-common'
243
+ import { useContext } from '../context.js'
244
+
245
+ export const CreateProject: FC<{ onCancel: () => void }> = ({ onCancel }) => {
246
+ const context = useContext()
247
+ const formRef = useFormRef<ProjectCreate>()
248
+
249
+ const onSubmit = useCallback(async (data: ProjectCreate) => {
250
+ try {
251
+ await context.entrypoint(apiProtocols.project.create).call({ body: data })
252
+ toast.success('Project created')
253
+ } catch (error) {
254
+ // Plants a root error; Form shows it through Status when decorated.
255
+ formRef.current?.error(error)
256
+ }
257
+ }, [])
258
+
259
+ return <PanelContext resource="my-app" prefix="project-create">
260
+ {/* `decorate` renders a Card, the root error and a SubmitButton in the footer. */}
261
+ <Form decorate name="project" formRef={formRef} validation={ProjectCreateSchema}
262
+ horizontal={BlockScaling.Half} onSubmit={onSubmit}>
263
+ <TextInput name="name" label placeholder hint />
264
+ <TextInput name="repository" type="url" label="Repository URL" disableAutocomplete />
265
+ </Form>
266
+ <Button label="cancel" variant="text" onClick={onCancel} />
267
+ </PanelContext>
268
+ }
269
+ ```
270
+
271
+ - `TextInput` takes `label`, `placeholder` and `hint` as `string | boolean`: `true` resolves
272
+ `<name>.label` / `.placeholder` / `.hint` from the form namespace, and a string is used verbatim.
273
+ - Without `decorate`, `Form` is a bare flex column with no card, no root-error surface and no submit
274
+ button, so render your own `SubmitButton` inside it.
275
+ - `formRef` (from `useFormRef()`) is filled with `{ form, update, loader, error }`.
276
+ - `Button` maps `contained` / `outlined` / `text` onto shadcn `default` / `outline` / `ghost`, and
277
+ forwards any other shadcn variant name unchanged.
278
+
279
+ Panels and feedback:
280
+
281
+ ```tsx
282
+ import { Block, ButtonSelector, ImageUploader, Link, Status, Text } from '@owlmeans/web-panel'
283
+
284
+ <Block horizontal={BlockScaling.Wide} Actions={() => <Link module={webProtocols.projects.alias} />}>
285
+ <Text variant="h3" name="title" />
286
+ <ButtonSelector name="period" options={['day', 'week', 'month']} current={period} onSelect={setPeriod} />
287
+ <ImageUploader maxFiles={1} previewUrl={logoUrl} onDrop={files => upload(files[0])} />
288
+ {result != null && <Status ok={result.ok} error={result.error} message={result.message} />}
289
+ </Block>
175
290
  ```
176
291
 
292
+ ### 5. Socket connection state
293
+
294
+ With `cfg.socket.reloadDialog` set, `PanelApp` mounts `SocketReloadDialog`. It is a blocking
295
+ `AlertDialog` that opens when every `ws()` / `useWs()` connection has exhausted its retry budget
296
+ (`useSocketStatus() === 'lost'`), and its only action reloads the page. An app that prefers an
297
+ inline state leaves the flag off and reads the same status:
298
+
299
+ ```tsx
300
+ import type { FC } from 'react'
301
+ import { useSocketStatus } from '@owlmeans/client-socket'
302
+ import { useI18nApp } from '@owlmeans/web-panel'
303
+
304
+ export const ConnectionBadge: FC = () => {
305
+ const status = useSocketStatus() // 'online' | 'reconnecting' | 'lost'
306
+ const t = useI18nApp('connection')
307
+
308
+ return status === 'online' ? null : <span role="status" data-state={status}>{t(status)}</span>
309
+ }
310
+ ```
311
+
312
+ The dialog's strings are the lib-tier `socket` namespace (`reload.title`, `reload.description`,
313
+ `reload.action`), shipped in seven languages.
314
+
315
+ ## API
316
+
317
+ ### Context and mounting
318
+
319
+ | Symbol | Kind | Purpose |
320
+ |---|---|---|
321
+ | `makeContext<C, T>(cfg)` | function | Panel context factory — see *Concepts* |
322
+ | `useContext<C, T>()` | hook | The current panel context |
323
+ | `render(context, opts?)` | function | Mount `PanelApp` with the language detector; `opts` is `WebRenderOptions` |
324
+ | `WebRenderOptions` | type | `@owlmeans/web-client`'s `RenderOptions` plus `rootClassName` |
325
+ | `PanelApp`, `PanelAppProps` | component, type | Themed root `div`, i18n provider, app and router, overlays |
326
+ | `entrypoints` | const | `@owlmeans/web-client`'s entrypoints plus `@owlmeans/api-config-client`'s |
327
+ | `AppConfig`, `AppContext` | type | Adds `socket?: SocketClientSettings & { reloadDialog? }`, `flow()` and the socket-status append |
328
+
329
+ ### Components
330
+
331
+ | Symbol | Kind | Purpose |
332
+ |---|---|---|
333
+ | `NavLayout`, `TopNav`, `SideNav`, `Footer` | component | Navigation shell and its pieces |
334
+ | `NavLayoutProps`, `TopNavProps`, `SideNavProps`, `FooterProps` | type | Their props |
335
+ | `Layout`, `LayoutProps` | component, type | Plain content wrapper |
336
+ | `Form`, `WebFormProps` | component, type | Web form; `FormProps` plus `className` / `style` |
337
+ | `TextInput`, `TextInputProps` | component, type | `react-hook-form` controlled input |
338
+ | `Button`, `SubmitButton`, `ButtonProps`, `SubmitProps` | component, type | Action buttons; `SubmitButton` is bound to `handleSubmit` |
339
+ | `ButtonSelector`, `SelectorProps` | component, type | One button per option, the current one `contained` |
340
+ | `Block`, `Text`, `Link`, `Status` | component | Card panel with `Actions`, translated text, entrypoint link, translated `Alert` |
341
+ | `BlockProps`, `TextProps`, `LinkProps`, `StatusProps`, `StyledProps`, `TextVariant` | type | Their props |
342
+ | `ImageUploader`, `ImageUploaderProps` | component, type | Drop target with a `previewUrl` |
343
+ | `Toaster` | component | `sonner` surface themed from app tokens; follows `.dark` on the document element |
344
+ | `SocketReloadDialog` | component | Blocking reload prompt for `'lost'` sockets |
345
+ | `LoginScreen`, `LocalizedLoginScreen` | component | Identity-provider choice screen; the localized one binds `translate` to the app's resources |
346
+ | `appendLoginScreen(ctx, setup?)`, `LoginScreenSetup` | function, type | Register the screen on the login service, with `Logo` and other `LoginScreenProps` |
347
+ | `LoginTerms`, `LoginCredit`, `LoginMethodIcon` | component | The screen's terms sentence, credit line and method icons |
348
+ | `LoginTermsProps`, `LoginCreditProps` | type | Their props |
349
+ | `scalingToStyles(horizontal?, vertical?)` | function | `BlockScaling` to Tailwind width/height classes |
350
+ | `useBreakPoint()` | hook | The current Tailwind breakpoint name |
351
+ | `useMapBreakpoint(map, def?, breakpoint?)` | hook | The map entry for the current breakpoint; throws `SyntaxError` when nothing matches and no `def` is given |
352
+
353
+ ### Re-exports
354
+
355
+ | Symbol | From |
356
+ |---|---|
357
+ | everything (`usePanelNav`, `PanelNavConfig` / `PanelNavItem` / `PanelNavSection` / `PanelNavLink` / `NavTranslate`, `PanelContext`, `BlockScaling`, `useFormRef`, `FormContext`, ...) | `@owlmeans/client-panel` |
358
+ | `handler`, `useNavigate`, `useValue`, `useEntrypoint` | `@owlmeans/client` |
359
+ | `bind`, `bindAll`, `bindScreen`, `provideRequest`, `stab`; type `Module` | `@owlmeans/client-entrypoint` |
360
+ | `route`, `frontend` / `croute` | `@owlmeans/route` / `@owlmeans/client-route` |
361
+ | `config` / `service` / `addWebService` | `@owlmeans/client-context` / `@owlmeans/config` / `@owlmeans/client-config` |
362
+ | `AppType`, `HOME`, `ROOT`, `BASE`, `GUEST` | `@owlmeans/context` |
363
+ | `DISPATCHER`, `CAUTHEN_FLOW_ENTER`; type `AuthToken` | `@owlmeans/auth` |
364
+ | `DAUTH_GUARD` (`DEFAULT_ALIAS`), `bindExternalAuthentication` | `@owlmeans/client-auth` |
365
+ | `Dispatcher`, `appendWebAuthService` | `@owlmeans/web-client` |
366
+ | `composePrefix`, `useI18n`, `useI18nApp`, `useI18nLib`, `useLanguage` | `@owlmeans/client-i18n` |
367
+ | `addI18nApp`, `addI18nLib`, `SUPPORTED_LNGS` | `@owlmeans/i18n` |
368
+ | `flow`, `configureFlows` / `FLOW_PARAM`, `SERVICE_PARAM`, `useFlow` | `@owlmeans/flow` / `@owlmeans/web-flow` |
369
+
370
+ ### `@owlmeans/web-panel/auth`
371
+
372
+ The context and screens for an authentication manager app — the identity-provider side, not a
373
+ relying party.
374
+
375
+ | Symbol | Kind | Purpose |
376
+ |---|---|---|
377
+ | `makeContext`, `useContext` | function, hook | Client context with IndexedDB, router, `apiConfigMiddleware` and flow service, without the relying-party auth service |
378
+ | `AppConfig`, `AppContext` | type | With `flow()` |
379
+ | `render` | function | Same as the root `render` |
380
+ | `plugins` | registry | `@owlmeans/client-auth/manager`'s registry with the shadcn renderers assigned |
381
+ | `Ed22519BasicAuthUIPlugin`, `ReCaptchaAuthUIPlugin`, `TunnelConsumerUIPlugin` | component | Those renderers |
382
+ | `AuthenticationHOC`, `AuthenticationType`, `DISPATCHER`, `CAUTHEN`, `EntrypointOutcome`, `RouteMethod`, `FLOW_ALIAS`, type `Navigator` / `AbstractRequest` / `Request` / `ServiceRoute` / `FlowService`, and the `@owlmeans/client-panel/auth` exports | re-export | Authentication-manager vocabulary |
383
+
384
+ `@owlmeans/web-panel/auth/entrypoints` exports `entrypoints`: `@owlmeans/client-auth/manager`'s
385
+ bindings plus `@owlmeans/api-config-client`'s.
386
+
387
+ ### `@owlmeans/web-panel/consent`
388
+
389
+ | Symbol | Kind | Purpose |
390
+ |---|---|---|
391
+ | `PanelCookieConsent`, `PanelCookiePolicy` | component | `@owlmeans/web-consent` components bound to the app's language and translations |
392
+ | `PanelConsentMenuWidget` | component | The cookie-preferences row for a host menu |
393
+ | `appendConsentWidgetService(ctx, alias?)`, `createConsentWidgetService` | function | Ref-counted presence service over a state resource |
394
+ | `useConsentMenuPresence()`, `useConsentWidgetPresent()` | hook | Declare that a menu shows the row; read whether one does (hides the floating button) |
395
+ | `CONSENT_WIDGET_SERVICE`, `CONSENT_WIDGET_STATE` | const | Service alias and state alias |
396
+ | `ConsentWidgetPresenceRecord`, `ConsentWidgetService`, `ConsentWidgetServiceAppend` | type | Service types |
397
+ | `useConsent`, `useConsentCategory`, `consentStore`, `openConsent`, `isConsented`, `readConsent`, `writeConsent`, `clearConsent`, `consentBootstrapScript`, `DEFAULT_CONSENT_CATEGORIES`, `CONSENT_KEY`, `CONSENT_ESSENTIAL`, `CONSENT_ANALYTICS`, `CONSENT_MARKETING` and the consent types | re-export | `@owlmeans/web-consent` |
398
+
399
+ ### Differences from `@owlmeans/mui-panel`
400
+
401
+ The public names match the MUI implementation; these props and types differ:
402
+
403
+ - **`styles?: SxProps` is removed.** Use `className?: string` and `style?: React.CSSProperties`
404
+ (`BlockProps`, `TextProps`, `LinkProps`, `WebFormProps`, `LayoutProps`).
405
+ - **`variant` on `Text` / `Link` is `TextVariant`**: `'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'lead' |
406
+ 'large' | 'small' | 'muted' | 'blockquote'`.
407
+ - **`PanelAppProps.theme?: Theme` is replaced with `rootClassName?: string`.** Apply the theme
408
+ through Tailwind classes and CSS variables.
409
+ - **`render(context, theme?, opts?)` is now `render(context, opts?)`**, where `opts` includes
410
+ `rootClassName`.
411
+ - **`scalingToStyles()` returns a class-name string** composable with other classes, not an
412
+ `SxProps`; its `theme` parameter is gone.
413
+ - **`useBreakPoint` / `useMapBreakpoint` use Tailwind's default breakpoints**
414
+ (`xs/sm/md/lg/xl`) instead of MUI's `Theme.breakpoints`.
415
+ - **Button props stay compatible** (`'small' | 'medium' | 'large'`, `contained` / `outlined` /
416
+ `text` or shadcn variant names).
417
+
418
+ ## Common pitfalls
419
+
420
+ - **Add the `@source` line** for this package's `src` (and for `@owlmeans/web-consent` when using
421
+ `./consent`). Without it the navigation shell renders unstyled.
422
+ - **A parent frontend route needs a `default: true` child.** A grouping screen with children but no
423
+ default child renders blank at its own path.
424
+ - **Menu and login labels need a `translate` prop to be localized.** The components never read the
425
+ i18n context implicitly, so without the prop they show literal labels or humanized aliases.
426
+ - **Mount `Toaster` exactly once, in the layout.** Two surfaces render every toast twice. An app
427
+ raising its own toasts declares `sonner` at a range that resolves to the same copy.
428
+ - **Mount dialogs beside the router** (`PanelApp` children or the web-client overlay slot), never
429
+ inside a route.
430
+ - **Give a dark top bar both halves through `headerClassName`** (`bg-secondary
431
+ text-secondary-foreground`). The header is its own surface; colouring only the root leaves its
432
+ menu unreadable.
433
+ - **Name only the utility you want to move in `containerClassName`.** It merges over
434
+ `mx-auto w-full max-w-6xl px-4`, so `max-w-[1280px]` changes only the width.
435
+ - **`Link` resolves its `href` asynchronously.** Never key a test or layout on the anchor having an
436
+ `href` at first paint.
437
+ - **`reloadDialog` is off by default.** The dialog renders nothing unless `cfg.socket.reloadDialog`
438
+ is `true`.
439
+ - **Run `useConsentMenuPresence()` from the menu's always-mounted component**, never from inside a
440
+ dropdown row, which mounts only while the menu is open.
441
+ - **Never import `@/…` from package code or vendor its primitives in the app.** The `@` alias
442
+ belongs to the consumer.
443
+ - **Every override of a packaged string covers all seven languages** (`SUPPORTED_LNGS`).
444
+
177
445
  ## Related packages
178
446
 
179
- - [`@owlmeans/mui-panel`](../mui-panel) — Material UI implementation of the
180
- same surface.
181
- - [`@owlmeans/web-oidc-rp`](../web-oidc-rp) — companion OIDC/OAuth UI for
182
- this package; uses the same Tailwind theme.
183
- - [`@owlmeans/client-panel`](../client-panel) — framework-agnostic headless
184
- form/layout logic this package wraps.
447
+ - [`@owlmeans/web-client`](../web-client) — the web context and mount helpers this package builds on
448
+ - [`@owlmeans/client-panel`](../client-panel) — headless form, layout, navigation and login models
449
+ - [`@owlmeans/client-auth`](../client-auth) — auth service, login hooks and plugin registry
450
+ - [`@owlmeans/client-socket`](../client-socket) `useSocketStatus` behind the reload prompt
451
+ - [`@owlmeans/web-consent`](../web-consent) — cookie consent components behind `./consent`
452
+ - [`@owlmeans/web-oidc-rp`](../web-oidc-rp) OIDC relying party UI on the same Tailwind theme
453
+ - [`@owlmeans/web-router`](../web-router) — default OwlMeans routing plugin
454
+ - [`@owlmeans/mui-panel`](../mui-panel) — legacy Material UI implementation of the same surface
185
455
 
186
456
  <!-- owlmeans:agent-guidance:start -->
187
457
  ## Agent guidance
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-panel",
4
- "version": "0.1.18-rc.40",
5
- "generatedAt": "2026-09-12T14:18:54.798Z",
4
+ "version": "0.1.18-rc.41",
5
+ "generatedAt": "2026-09-14T19:17:35.200Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {