@nocobase/app-template-default 1.0.0-beta.58 → 1.0.0-beta.59

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 (65) hide show
  1. package/AGENTS.md +26 -23
  2. package/README.MD +7 -57
  3. package/client/agent-annotations-host.ts +8 -2
  4. package/client/app.ts +0 -2
  5. package/client/components/brand-loading-mark.tsx +46 -0
  6. package/client/components/brand-mark-geometry.ts +7 -0
  7. package/client/components/brand-spinner.tsx +22 -0
  8. package/client/components/brand-watermark.tsx +32 -0
  9. package/client/components/loading.tsx +4 -3
  10. package/client/components/route-overlay.tsx +15 -4
  11. package/client/components/status-badge.tsx +29 -0
  12. package/client/components/ui/alert-dialog.tsx +186 -0
  13. package/client/components/ui/card.tsx +102 -0
  14. package/client/components/ui/dialog.tsx +2 -2
  15. package/client/components/ui/radio-group.tsx +35 -0
  16. package/client/components/ui/spinner.tsx +2 -20
  17. package/client/components/ui/toast.tsx +3 -2
  18. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +3 -3
  19. package/client/extensions/nocobase-inbox/inbox-item.tsx +4 -3
  20. package/client/layouts/app-layout.tsx +8 -28
  21. package/client/layouts/components/app-sidebar.tsx +4 -4
  22. package/client/layouts/components/header-actions.tsx +3 -36
  23. package/client/layouts/index.ts +0 -1
  24. package/client/lib/toaster.ts +1 -1
  25. package/client/locales/en-US.ts +31 -17
  26. package/client/locales/zh-CN.ts +24 -14
  27. package/client/pages/auth/login.tsx +1 -0
  28. package/client/pages/auth/shared.tsx +9 -9
  29. package/client/pages/home.tsx +80 -7
  30. package/client/plugins.ts +2 -2
  31. package/client/routes.ts +4 -10
  32. package/client/routing/app-router.tsx +5 -55
  33. package/client/routing/route-navigation.ts +1 -1
  34. package/client/styles.css +244 -0
  35. package/client/theme/theme-settings.tsx +22 -20
  36. package/client/theme/themes/compact.css +34 -16
  37. package/client/theme/themes/default.css +34 -16
  38. package/eslint.config.js +0 -13
  39. package/index.html +48 -16
  40. package/package.json +22 -26
  41. package/tests/components/app-layout-routes.test.tsx +1 -1
  42. package/tests/components/inbox.test.tsx +0 -19
  43. package/tests/components/layout-i18n.test.tsx +5 -92
  44. package/tests/components/primitive-labels.test.tsx +61 -1
  45. package/tests/fixtures/theme-tokens.html +1 -1
  46. package/tests/fixtures/theme-tokens.tsx +7 -0
  47. package/tests/logic/app-server.test.ts +0 -15
  48. package/tests/logic/client-plugin-registry.test.ts +0 -21
  49. package/tests/logic/client-routes.test.ts +2 -6
  50. package/tests/logic/client-shell.test.tsx +11 -107
  51. package/tests/logic/client-theme.test.tsx +17 -1
  52. package/tests/logic/config.test.ts +0 -1
  53. package/tests/logic/plugin-commands.test.ts +1 -1
  54. package/tsconfig.json +1 -4
  55. package/tsconfig.server.json +1 -6
  56. package/vitest.config.ts +1 -8
  57. package/client/components/inbox-badge.ts +0 -49
  58. package/client/components/inbox-button.tsx +0 -132
  59. package/client/components/inbox-header-button.tsx +0 -25
  60. package/client/layouts/components/surface-empty.tsx +0 -35
  61. package/client/layouts/settings-layout.tsx +0 -172
  62. package/server/config/workflow.ts +0 -24
  63. package/tests/logic/client-settings.test.tsx +0 -841
  64. package/tests/logic/dev-routes-production-build.test.ts +0 -168
  65. package/tests/logic/plugin-page-authz.test.ts +0 -53
@@ -59,17 +59,15 @@ const enUS = {
59
59
  'auth.marketingTitleSecond': 'NocoBase keeps it',
60
60
  'auth.marketingTitleThird': 'reliable.',
61
61
  'auth.marketingDescription':
62
- 'Give AI a flexible frontend framework to shape each experience, while NocoBase secures the data, permissions, workflows and governance underneath.',
62
+ 'Give AI a flexible frontend framework to shape each experience, while NocoBase secures the data, permissions and governance underneath.',
63
63
  'auth.frontend': 'AI-native frontend',
64
64
  'auth.frontendDescription':
65
65
  'Compose interfaces freely on a flexible framework.',
66
66
  'auth.foundation': 'NocoBase foundation',
67
- 'auth.foundationDescription':
68
- 'Reliable data, access control, workflows and governance.',
67
+ 'auth.foundationDescription': 'Reliable data, access control and governance.',
69
68
  'auth.marketingFooter': 'Freedom above. Confidence below.',
70
69
  'status.loading': 'Loading',
71
70
  'status.loadingPage': 'Loading page',
72
- 'status.loadingSettings': 'Loading settings',
73
71
  'status.denied': 'Access denied',
74
72
  'status.pageFailed': 'Unable to load page',
75
73
  'status.retry': 'Retry',
@@ -83,22 +81,38 @@ const enUS = {
83
81
  buildFreely: 'AI builds freely.',
84
82
  reliability: '<brand>NocoBase</brand> keeps it reliable.',
85
83
  },
86
- surface: {
87
- backToApp: 'Back to app',
88
- loading: 'Loading {{title}}',
89
- navigation: '{{title}} navigation',
90
- page: '{{title}} page',
91
- },
92
- settings: {
93
- title: 'Settings',
94
- emptyTitle: 'No settings available',
95
- emptyDescription:
96
- 'No enabled plugin contributes a settings page you have access to.',
97
- },
98
84
  home: {
85
+ platform: 'NocoBase · AI-native application platform',
86
+ pages: {
87
+ title: 'Build your pages',
88
+ description: 'Turn your ideas into interfaces that fit your work.',
89
+ examples:
90
+ 'From tables and forms to dashboards, shape each page around your business.',
91
+ },
92
+ data: {
93
+ title: 'Organize your data',
94
+ description: 'Give your business a clear, connected data model.',
95
+ examples:
96
+ 'Define records, fields and relationships so your information works together.',
97
+ },
98
+ workflows: {
99
+ title: 'Connect your workflows',
100
+ description: 'Keep work moving with consistent business processes.',
101
+ examples:
102
+ 'Connect approvals, notifications and automated actions to everyday work.',
103
+ },
104
+ startTitle: 'Start with one requirement',
105
+ startDescription:
106
+ 'Tell your AI Agent what you want to achieve and who will use it.',
107
+ examplePrompt:
108
+ '“Build a customer management app for our sales team. We need a customer list, contact details and follow-up records, with a clear overview of each opportunity.”',
109
+ startHint:
110
+ 'Begin with a small, useful flow. Refine the pages, data and rules as your needs become clearer.',
111
+ foundation:
112
+ 'AI builds freely. NocoBase keeps data, permissions and workflows reliable.',
99
113
  title: 'Start building your application',
100
114
  description:
101
- 'Describe what you need to your AI Agent, then build pages, data models, and business workflows.',
115
+ 'Describe what you need to your AI Agent, then build pages, data models, and business processes.',
102
116
  },
103
117
 
104
118
  appearance: {
@@ -56,15 +56,14 @@ const zhCN: AppResource = {
56
56
  'auth.marketingTitleSecond': 'NocoBase 保障',
57
57
  'auth.marketingTitleThird': '可靠运行。',
58
58
  'auth.marketingDescription':
59
- '让 AI 在灵活的前端框架上构建体验,由 NocoBase 保障底层数据、权限、工作流与治理。',
59
+ '让 AI 在灵活的前端框架上构建体验,由 NocoBase 保障底层数据、权限与治理。',
60
60
  'auth.frontend': 'AI 原生前端',
61
61
  'auth.frontendDescription': '在灵活的框架上自由构建界面。',
62
62
  'auth.foundation': 'NocoBase 基础能力',
63
- 'auth.foundationDescription': '可靠的数据、访问控制、工作流与治理。',
63
+ 'auth.foundationDescription': '可靠的数据、访问控制与治理。',
64
64
  'auth.marketingFooter': '自由构建,可靠支撑。',
65
65
  'status.loading': '加载中',
66
66
  'status.loadingPage': '正在加载页面',
67
- 'status.loadingSettings': '正在加载设置',
68
67
  'status.denied': '无权访问',
69
68
  'status.pageFailed': '无法加载页面',
70
69
  'status.retry': '重试',
@@ -78,18 +77,29 @@ const zhCN: AppResource = {
78
77
  buildFreely: 'AI 自由构建。',
79
78
  reliability: '<brand>NocoBase</brand> 保障可靠。',
80
79
  },
81
- surface: {
82
- backToApp: '返回应用',
83
- loading: '正在加载{{title}}',
84
- navigation: '{{title}}导航',
85
- page: '{{title}}页面',
86
- },
87
- settings: {
88
- title: '设置',
89
- emptyTitle: '暂无可用设置',
90
- emptyDescription: '没有已启用的插件提供你有权访问的设置页面。',
91
- },
92
80
  home: {
81
+ platform: 'NocoBase · AI 原生应用平台',
82
+ pages: {
83
+ title: '构建业务页面',
84
+ description: '让想法变成适合你的工作界面。',
85
+ examples: '从表格、表单到仪表盘,围绕业务搭建每一个页面。',
86
+ },
87
+ data: {
88
+ title: '组织业务数据',
89
+ description: '建立清晰、相互关联的数据模型。',
90
+ examples: '定义记录、字段与关联,让分散的信息连成整体。',
91
+ },
92
+ workflows: {
93
+ title: '串联业务流程',
94
+ description: '用一致的流程,让工作顺畅推进。',
95
+ examples: '把审批、通知和自动化操作融入日常业务。',
96
+ },
97
+ startTitle: '从一个需求开始',
98
+ startDescription: '告诉 AI 助手,你想解决什么问题、谁会使用这个应用。',
99
+ examplePrompt:
100
+ '“为销售团队搭建一个客户管理应用,包含客户列表、联系人信息和跟进记录,并能清楚查看每个商机的进展。”',
101
+ startHint: '先完成一个实用的小流程,再随着需求逐步完善页面、数据和规则。',
102
+ foundation: 'AI 自由构建,NocoBase 为数据、权限和流程提供可靠底座。',
93
103
  title: '开始构建你的应用',
94
104
  description: '向 AI 助手描述你的需求,逐步构建页面、数据模型和业务流程。',
95
105
  },
@@ -32,6 +32,7 @@ export default function LoginPage(): ReactElement {
32
32
  {
33
33
  content: (
34
34
  <PasswordLoginForm
35
+ className='[&_[data-slot=input]]:h-12 [&_[data-slot=input-group]]:h-12 [&_button[type=submit]]:h-12'
35
36
  error={login.error?.message}
36
37
  footer={
37
38
  signUpAvailable ? (
@@ -49,7 +49,7 @@ export function AuthPage({
49
49
  );
50
50
  }
51
51
 
52
- // The brand panel is deliberately dark in both color modes, so it uses fixed neutrals rather than theme tokens.
52
+ // The brand panel stays dark in both color modes; its dedicated palette belongs to the theme presets.
53
53
  function BrandPanel(): ReactElement {
54
54
  const { t } = useTranslation();
55
55
  const features = [
@@ -68,10 +68,10 @@ function BrandPanel(): ReactElement {
68
68
  ];
69
69
 
70
70
  return (
71
- <div className='relative grid h-full place-items-center overflow-hidden bg-neutral-950 p-12 text-white'>
72
- <div className='pointer-events-none absolute inset-0 [background-image:linear-gradient(currentColor_1px,transparent_1px),linear-gradient(90deg,currentColor_1px,transparent_1px)] [background-size:48px_48px] opacity-[0.08]' />
71
+ <div className='relative grid h-full place-items-center overflow-hidden bg-(--auth-brand-background) p-12 text-white'>
72
+ <div className='pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,color-mix(in_srgb,var(--primary)_22%,transparent),transparent_65%)]' />
73
73
  <div className='relative w-full max-w-xl'>
74
- <p className='text-xs font-semibold tracking-[0.16em] text-white/55 uppercase'>
74
+ <p className='text-xs font-semibold tracking-[0.16em] text-(--auth-brand-highlight) uppercase'>
75
75
  {t('auth.platform')}
76
76
  </p>
77
77
  <h2 className='mt-4 text-5xl leading-[1.05] font-semibold tracking-[-0.045em]'>
@@ -81,26 +81,26 @@ function BrandPanel(): ReactElement {
81
81
  <br />
82
82
  {t('auth.marketingTitleThird')}
83
83
  </h2>
84
- <p className='mt-5 text-sm leading-6 text-white/60'>
84
+ <p className='mt-5 text-sm leading-6 text-(--auth-brand-muted)'>
85
85
  {t('auth.marketingDescription')}
86
86
  </p>
87
- <div className='mt-8 overflow-hidden rounded-2xl bg-white text-neutral-950 shadow-2xl'>
87
+ <div className='mt-8 overflow-hidden rounded-2xl border border-white/10 bg-white/5 text-white shadow-lg backdrop-blur-sm'>
88
88
  <div className='space-y-5 p-6'>
89
89
  {features.map(({ description, icon: Icon, id, title }) => (
90
90
  <div className='flex gap-4' key={id}>
91
- <span className='grid size-11 shrink-0 place-items-center rounded-xl bg-neutral-100 text-neutral-700'>
91
+ <span className='grid size-11 shrink-0 place-items-center rounded-xl bg-(--auth-brand-highlight)/10 text-(--auth-brand-highlight)'>
92
92
  <Icon aria-hidden='true' className='size-5' />
93
93
  </span>
94
94
  <div>
95
95
  <p className='font-semibold'>{title}</p>
96
- <p className='mt-1 text-sm leading-6 text-neutral-500'>
96
+ <p className='mt-1 text-sm leading-6 text-(--auth-brand-muted)'>
97
97
  {description}
98
98
  </p>
99
99
  </div>
100
100
  </div>
101
101
  ))}
102
102
  </div>
103
- <div className='flex items-center gap-3 bg-neutral-100 px-6 py-4 text-sm font-medium text-neutral-600'>
103
+ <div className='flex items-center gap-3 border-t border-white/10 bg-white/3 px-6 py-4 text-sm font-medium text-(--auth-brand-muted)'>
104
104
  <Blocks aria-hidden='true' className='size-4' />
105
105
  <span>{t('auth.marketingFooter')}</span>
106
106
  </div>
@@ -1,16 +1,89 @@
1
1
  import { useTranslation } from '@nocobase/i18n/client';
2
2
  import type { ReactElement } from 'react';
3
+ import {
4
+ Database,
5
+ MessageSquareText,
6
+ PanelsTopLeft,
7
+ ShieldCheck,
8
+ Sparkles,
9
+ Workflow,
10
+ } from 'lucide-react';
11
+ import { PageContainer } from '#components/page-container';
12
+ import { PageHeader } from '#components/page-header';
13
+ import {
14
+ Card,
15
+ CardContent,
16
+ CardDescription,
17
+ CardHeader,
18
+ CardTitle,
19
+ } from '#components/ui/card';
3
20
 
4
21
  export default function HomePage(): ReactElement {
5
22
  const { t } = useTranslation();
6
23
  return (
7
- <section className='mx-auto grid min-h-[calc(100svh-4rem)] w-full max-w-5xl place-items-center px-6 py-10'>
8
- <div className='max-w-xl space-y-6 text-center'>
9
- <h1 className='font-heading text-3xl font-semibold tracking-tight'>
10
- {t('home.title')}
11
- </h1>
12
- <p className='text-muted-foreground'>{t('home.description')}</p>
24
+ <PageContainer className='mx-auto max-w-5xl'>
25
+ <div className='flex flex-col gap-4'>
26
+ <div className='flex items-center gap-2 text-sm font-medium text-primary'>
27
+ <Sparkles aria-hidden='true' className='size-4' />
28
+ {t('home.platform')}
29
+ </div>
30
+ <PageHeader
31
+ title={t('home.title')}
32
+ description={t('home.description')}
33
+ />
13
34
  </div>
14
- </section>
35
+ <div className='grid gap-4 md:grid-cols-3'>
36
+ {(
37
+ [
38
+ ['pages', PanelsTopLeft],
39
+ ['data', Database],
40
+ ['workflows', Workflow],
41
+ ] as const
42
+ ).map(([feature, Icon]) => (
43
+ <Card key={feature}>
44
+ <CardHeader className='gap-3'>
45
+ <div className='flex size-10 items-center justify-center rounded-lg bg-primary/5 text-primary'>
46
+ <Icon aria-hidden='true' className='size-4' />
47
+ </div>
48
+ <CardTitle>
49
+ <h2>{t(`home.${feature}.title`)}</h2>
50
+ </CardTitle>
51
+ <CardDescription>
52
+ {t(`home.${feature}.description`)}
53
+ </CardDescription>
54
+ </CardHeader>
55
+ <CardContent>
56
+ <p className='text-sm leading-6 text-muted-foreground'>
57
+ {t(`home.${feature}.examples`)}
58
+ </p>
59
+ </CardContent>
60
+ </Card>
61
+ ))}
62
+ </div>
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle>
66
+ <h2 className='flex items-center gap-2'>
67
+ <MessageSquareText
68
+ aria-hidden='true'
69
+ className='size-4 text-primary'
70
+ />
71
+ {t('home.startTitle')}
72
+ </h2>
73
+ </CardTitle>
74
+ <CardDescription>{t('home.startDescription')}</CardDescription>
75
+ </CardHeader>
76
+ <CardContent className='flex flex-col gap-4'>
77
+ <blockquote className='rounded-lg border border-primary/10 bg-primary/5 p-4 text-sm leading-7'>
78
+ {t('home.examplePrompt')}
79
+ </blockquote>
80
+ <p className='text-sm text-muted-foreground'>{t('home.startHint')}</p>
81
+ </CardContent>
82
+ </Card>
83
+ <p className='flex items-start gap-2 text-sm leading-6 text-muted-foreground'>
84
+ <ShieldCheck aria-hidden='true' className='mt-1 size-4 shrink-0' />
85
+ {t('home.foundation')}
86
+ </p>
87
+ </PageContainer>
15
88
  );
16
89
  }
package/client/plugins.ts CHANGED
@@ -25,8 +25,8 @@ const clientPlugins: AppClientPlugins = defineClientPlugins([
25
25
  sharingRules(),
26
26
  restrictionRules(),
27
27
  databaseExplorer(),
28
- users({ mount: 'settings', path: '/users' }),
29
- apiKeys({ path: '/api-keys' }),
28
+ users(),
29
+ apiKeys(),
30
30
  i18n(),
31
31
  notificationInApp(),
32
32
  notification(),
package/client/routes.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Home } from 'lucide-react';
2
2
  import {
3
3
  defineAppRoutes,
4
- defineSettingsRoutes,
5
4
  type AppClientRouteContribution,
6
5
  } from '@nocobase/app-client/plugins';
7
6
 
@@ -17,9 +16,9 @@ const appRoutes: AppClientRouteContribution = defineAppRoutes([
17
16
  path: '/',
18
17
  },
19
18
  {
20
- // The header's inbox button is where a user looks for messages, so this page is reached from there. Declaring no
21
- // navigation keeps a second menu entry from pointing at the one destination the button already owns. The plugin's
22
- // API answers each person's own messages only, so the page needs no authorization of its own.
19
+ // Declares no navigation, so the application decides where its entry goes (for example a sidebar item with an
20
+ // unread badge). The plugin's API answers each person's own messages only, so the page needs no authorization of
21
+ // its own.
23
22
  authz: 'skip',
24
23
  auth: 'required',
25
24
  componentLoader: () => import('./pages/inbox.js'),
@@ -65,11 +64,6 @@ const appRoutes: AppClientRouteContribution = defineAppRoutes([
65
64
  },
66
65
  ]);
67
66
 
68
- const settingsRoutes: AppClientRouteContribution = defineSettingsRoutes([]);
69
-
70
- const routes: readonly AppClientRouteContribution[] = [
71
- appRoutes,
72
- settingsRoutes,
73
- ];
67
+ const routes: readonly AppClientRouteContribution[] = [appRoutes];
74
68
 
75
69
  export default routes;
@@ -1,52 +1,29 @@
1
- import { useTranslation } from '@nocobase/i18n/client';
2
1
  import {
3
2
  GuestAuthentication,
4
3
  RequiredAuthentication,
5
4
  } from '@nocobase/app-plugin-authentication/client';
6
5
  import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
7
- import { lazy, Suspense, useMemo, type ReactElement } from 'react';
6
+ import { useMemo, type ReactElement } from 'react';
8
7
  import { Navigate, Outlet, Route, Routes } from 'react-router';
9
8
 
10
- import { Loading } from '#components/loading';
11
9
  import { EMPTY_ARRAY } from '#lib/constants';
12
10
 
13
11
  import { AppLayout } from '../layouts/app-layout.js';
14
12
  import { renderRouteTree } from './route-tree.js';
15
13
  import { StandalonePageLayout } from './standalone-page-layout.js';
16
14
 
17
- // The settings centre brings its own chrome and navigation, none of which the application needs until someone opens
18
- // it. Loading it lazily keeps it out of the entry chunk, the same way every page it hosts stays out.
19
- const SettingsLayout = lazy(async () => ({
20
- default: (await import('../layouts/settings-layout.js')).SettingsLayout,
21
- }));
22
-
23
15
  export interface AppRouterProps {
24
- readonly settingsRouteTree: readonly AppClientRegisteredRoute[];
25
- readonly devRouteTree: readonly AppClientRegisteredRoute[];
26
16
  readonly clientRoutes: readonly AppClientRegisteredRoute[];
27
17
  }
28
18
 
29
- export function AppRouter(inputProps: AppRouterProps): ReactElement {
30
- const { t } = useTranslation();
31
- const { settingsRouteTree, devRouteTree, clientRoutes } = inputProps;
32
-
33
- const settingsRoutes = useMemo(
34
- () =>
35
- filterRouteTree(
36
- clientRoutes,
37
- (route) =>
38
- route.auth === 'required' && route.path.startsWith('/settings/'),
39
- ),
40
- [clientRoutes],
41
- );
19
+ export function AppRouter({ clientRoutes }: AppRouterProps): ReactElement {
42
20
  const routeGroups = useMemo(
43
21
  () => ({
44
22
  guest: clientRoutes.filter((route) => route.auth === 'guest'),
45
23
  optional: clientRoutes.filter((route) => route.auth === 'optional'),
46
24
  required: filterRouteTree(
47
25
  clientRoutes,
48
- (route) =>
49
- route.auth === 'required' && !route.path.startsWith('/settings/'),
26
+ (route) => route.auth === 'required',
50
27
  ),
51
28
  }),
52
29
  [clientRoutes],
@@ -61,36 +38,9 @@ export function AppRouter(inputProps: AppRouterProps): ReactElement {
61
38
  </RequiredAuthentication>
62
39
  }
63
40
  >
64
- <Route
65
- element={
66
- <AppLayout routes={routeGroups.required} devRoutes={devRouteTree} />
67
- }
68
- >
41
+ <Route element={<AppLayout routes={routeGroups.required} />}>
69
42
  {renderRouteTree(routeGroups.required)}
70
- {/* Pages plugins declare with `defineDevRoutes()` keep their `/dev/...` paths inside the application shell.
71
- A production build resolves no dev routes, so nothing is rendered here. */}
72
- {renderRouteTree(devRouteTree)}
73
43
  </Route>
74
- <Route
75
- path='/settings/*'
76
- element={
77
- <Suspense
78
- fallback={
79
- <Loading
80
- className='min-h-svh'
81
- label={t('status.loadingSettings', {
82
- defaultValue: 'Loading settings',
83
- })}
84
- />
85
- }
86
- >
87
- <SettingsLayout
88
- routeTree={settingsRouteTree}
89
- routes={settingsRoutes}
90
- />
91
- </Suspense>
92
- }
93
- />
94
44
  </Route>
95
45
 
96
46
  <Route
@@ -114,7 +64,7 @@ export function AppRouter(inputProps: AppRouterProps): ReactElement {
114
64
  );
115
65
  }
116
66
 
117
- /** Pure groups can span surfaces; a page and its descendants always share their shell. */
67
+ /** Pure groups can span shells; a page and its descendants always share their shell. */
118
68
  function filterRouteTree(
119
69
  routes: readonly AppClientRegisteredRoute[],
120
70
  predicate: (route: AppClientRegisteredRoute) => boolean,
@@ -58,7 +58,7 @@ export function selectedNavigationId(
58
58
  return selected ? routeKey(selected) : undefined;
59
59
  }
60
60
 
61
- export function navigationPages(
61
+ function navigationPages(
62
62
  items: readonly RouteNavigationItem[],
63
63
  ): AppClientRegisteredRoute[] {
64
64
  return items.flatMap(({ route, children }) => [