fse-theme-default 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,8 @@ A child theme overrides any file under `src/` by creating the same path in
33
33
  its own `src/` — the parent's pages then use the child's version:
34
34
 
35
35
  - `src/layouts/Layout.astro` — the whole shell,
36
- - `src/components/SidebarLinks.astro` — app-specific sidebar entries (empty here),
36
+ - `src/components/SidebarLinks.astro` — app-specific sidebar entries (renders
37
+ the `sidebar-links` slot; overriding it replaces every contribution),
37
38
  - `src/components/Logo.astro` — the logo in the sidebar, mobile header and
38
39
  auth pages (or replace just `src/assets/logo.svg`),
39
40
  - `src/components/Head.astro` — extra `<head>` tags on every page (favicon,
@@ -41,7 +42,23 @@ its own `src/` — the parent's pages then use the child's version:
41
42
  - `src/styles/global.css` — the Tailwind root (import `src/styles/theme.css`
42
43
  and override tokens after it).
43
44
 
44
- `@parent/...` imports the parent's original file (e.g. to wrap it).
45
+ `@parent/...` imports the parent's original file (e.g. to wrap it);
46
+ `@theme/...` the most specific theme's version of a file.
47
+
48
+ ### Slots
49
+
50
+ Where several child themes and modules add to one place, the theme has a
51
+ slot: every `src/slots/<slot>/<Name>.astro` of the child theme and of the
52
+ app's modules is rendered there (this theme's first, modules next, the
53
+ child last; a same-named file of a more specific layer replaces the other):
54
+
55
+ - `sidebar-links` — sidebar entries between "Home" and the model links,
56
+ - `settings-sections` — sections of the settings page (data through
57
+ `page_context("settings", ...)`),
58
+ - `head` — extra `<head>` tags,
59
+ - `dashboard` — widgets below the home page's model cards,
60
+ - `record-panels` — panels below a generated record form (check
61
+ `meta.table`).
45
62
 
46
63
  ## Development
47
64
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fse-theme-default",
3
- "version": "0.3.0",
4
- "description": "Default theme for full_stack_engine apps — a complete Astro project: app shell, design system, auth pages, emails and the generic metadata-driven CRUD pages. Extend it with a child theme.",
3
+ "version": "0.5.0",
4
+ "description": "Default theme for full_stack_engine apps \u2014 a complete Astro project: app shell, design system, auth pages, emails and the generic metadata-driven CRUD pages. Extend it with a child theme.",
5
5
  "type": "module",
6
6
  "license": "MIT OR Apache-2.0",
7
7
  "repository": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "peerDependencies": {
27
27
  "astro": ">=7.0.0",
28
- "fse-ssr": ">=0.2.0"
28
+ "fse-ssr": ">=0.3.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@astrojs/check": "^0.9.9",
@@ -2,8 +2,12 @@
2
2
  /**
3
3
  * Extension point: extra <head> tags for every page (favicon, fonts, meta
4
4
  * tags). A child theme overrides this file; its version replaces this one,
5
- * so it should link its own favicon too.
5
+ * so it should link its own favicon too. Tags that only add (a module's
6
+ * stylesheet, a meta tag) go into the `head` slot instead:
7
+ * `src/slots/head/*.astro`.
6
8
  */
9
+ import Slot from "fse-ssr/Slot.astro";
7
10
  ---
8
11
 
9
12
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
13
+ <Slot name="head" />
@@ -2,16 +2,21 @@
2
2
  /**
3
3
  * Extension point: the logo shown in the sidebar, the mobile header and the
4
4
  * auth pages. A child theme either replaces src/assets/logo.svg or
5
- * overrides this file (e.g. separate light and dark images).
5
+ * overrides this file (e.g. separate light and dark images, other sizes per
6
+ * `variant`).
6
7
  */
7
8
  import { Image } from "astro:assets";
8
9
  import logo from "../assets/logo.svg";
9
10
 
10
11
  interface Props {
12
+ /** Where it is drawn; picks the default size. */
13
+ variant?: "sidebar" | "mobile" | "auth";
14
+ /** Replaces the variant's size classes entirely. */
11
15
  class?: string;
12
16
  }
13
17
 
14
- const { class: className = "h-9 w-auto" } = Astro.props;
18
+ const sizes = { sidebar: "h-9 w-auto", mobile: "h-8 w-auto", auth: "h-12 w-auto" };
19
+ const { variant = "sidebar", class: className } = Astro.props;
15
20
  ---
16
21
 
17
- <Image src={logo} alt="Logo" class={className} />
22
+ <Image src={logo} alt="Logo" class={className ?? sizes[variant]} />
@@ -0,0 +1,48 @@
1
+ ---
2
+ /**
3
+ * The named 18px icons sidebar entries use. A model picks one with
4
+ * `#[model(nav(icon = "heart"))]` (or `public_nav(...)`); the framework sends
5
+ * `table` when it names none. An unknown name draws nothing — add it here
6
+ * (or in a child theme's override of this file).
7
+ *
8
+ * home, grid, calendar, heart, run, user, users, settings, table, list,
9
+ * box, cart, file, chart, tag, mail, map-pin, star
10
+ */
11
+ import type { SsrValue } from "fse-ssr/ssr";
12
+
13
+ interface Props {
14
+ name: string | SsrValue<string>;
15
+ }
16
+
17
+ const { name } = Astro.props;
18
+ const a = {
19
+ xmlns: "http://www.w3.org/2000/svg",
20
+ width: "18",
21
+ height: "18",
22
+ viewBox: "0 0 24 24",
23
+ fill: "none",
24
+ stroke: "currentColor",
25
+ "stroke-width": "1.5",
26
+ "stroke-linecap": "round",
27
+ "stroke-linejoin": "round",
28
+ } as const;
29
+ ---
30
+
31
+ {name === "home" && (<svg {...a}><path d="M3 10.5 12 3l9 7.5"></path><path d="M5 9.5V21h14V9.5"></path><path d="M10 21v-6h4v6"></path></svg>)}
32
+ {name === "grid" && (<svg {...a}><rect width="7.5" height="7.5" x="2.75" y="2.75" rx="1.9"></rect><rect width="7.5" height="7.5" x="13.75" y="2.75" rx="1.9"></rect><rect width="7.5" height="7.5" x="13.75" y="13.75" rx="1.9"></rect><rect width="7.5" height="7.5" x="2.75" y="13.75" rx="1.9"></rect></svg>)}
33
+ {name === "calendar" && (<svg {...a}><rect width="18" height="18" x="3" y="4" rx="2" ry="2"></rect><path d="M16 2v4M8 2v4m-5 4h18"></path></svg>)}
34
+ {name === "heart" && (<svg {...a}><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>)}
35
+ {name === "run" && (<svg {...a}><path d="M13 4a1 1 0 1 0 2 0 1 1 0 0 0-2 0"></path><path d="m7 21 3-6 2 2 3-4 3 8"></path><path d="m5 14 2-5 5 1 2-4"></path></svg>)}
36
+ {name === "user" && (<svg {...a}><circle cx="12" cy="7" r="4"></circle><path d="M5 21v-2a7 7 0 0 1 14 0v2"></path></svg>)}
37
+ {name === "users" && (<svg {...a}><circle cx="9" cy="7" r="4"></circle><path d="M3 21v-2a6 6 0 0 1 12 0v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75M21 21v-2a6 6 0 0 0-4-5.66"></path></svg>)}
38
+ {name === "settings" && (<svg {...a}><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.26.6.85 1 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>)}
39
+ {name === "table" && (<svg {...a}><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M3 9h18M9 21V9"></path></svg>)}
40
+ {name === "list" && (<svg {...a}><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"></path></svg>)}
41
+ {name === "box" && (<svg {...a}><path d="M21 8 12 3 3 8v8l9 5 9-5z"></path><path d="m3 8 9 5 9-5M12 13v8"></path></svg>)}
42
+ {name === "cart" && (<svg {...a}><circle cx="9" cy="20" r="1"></circle><circle cx="18" cy="20" r="1"></circle><path d="M2 3h3l2.7 12.4a2 2 0 0 0 2 1.6h7.6a2 2 0 0 0 2-1.6L21 7H6"></path></svg>)}
43
+ {name === "file" && (<svg {...a}><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><path d="M14 3v6h6M8 13h8M8 17h5"></path></svg>)}
44
+ {name === "chart" && (<svg {...a}><path d="M3 3v18h18"></path><path d="M7 15v2M12 10v7M17 6v11"></path></svg>)}
45
+ {name === "tag" && (<svg {...a}><path d="M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3 13V3h10l7.6 7.6a2 2 0 0 1 0 2.8z"></path><circle cx="7.5" cy="7.5" r="1.5"></circle></svg>)}
46
+ {name === "mail" && (<svg {...a}><rect width="18" height="14" x="3" y="5" rx="2"></rect><path d="m3 7 9 6 9-6"></path></svg>)}
47
+ {name === "map-pin" && (<svg {...a}><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>)}
48
+ {name === "star" && (<svg {...a}><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"></path></svg>)}
@@ -0,0 +1,32 @@
1
+ ---
2
+ /**
3
+ * Extension point: the app's own sections of the settings page, rendered
4
+ * between "account security" and "delete account". The default shows the
5
+ * account's name, followed by the `settings-sections` slot: every
6
+ * `src/slots/settings-sections/*.astro` of a child theme or a module (a
7
+ * profile, preferences, ...). Their data comes through
8
+ * `page_context("settings", ...)` (app or module), and POST handlers
9
+ * re-render the page with `auth_module::settings_context`. Overriding this
10
+ * file replaces the default and every contribution.
11
+ */
12
+ import Slot from "fse-ssr/Slot.astro";
13
+ import { ssr } from "fse-ssr/ssr";
14
+ import type { SettingsPage } from "../types/pages";
15
+
16
+ const { t, first_name, last_name } = ssr<SettingsPage>();
17
+ ---
18
+
19
+ {(first_name || last_name) && (
20
+ <div class="py-6">
21
+ <h3 class="text-base font-semibold text-secondary-900">
22
+ {t.settings.name}
23
+ </h3>
24
+ <div class="mt-3 px-4 py-3 rounded-lg bg-secondary-50 border border-secondary-100">
25
+ <span class="text-sm font-medium text-secondary-900">
26
+ {first_name} {last_name}
27
+ </span>
28
+ </div>
29
+ </div>
30
+ )}
31
+
32
+ <Slot name="settings-sections" />
@@ -1,16 +1,20 @@
1
1
  ---
2
2
  /**
3
3
  * The app shell's navigation. Everything is data-driven: the framework
4
- * injects `nav` (the admin page of every model the user may read) and
5
- * `user`; app-specific links come from the SidebarLinks extension point.
4
+ * injects `nav_groups` (the models' sidebar entries — label, icon and order
5
+ * come from `#[model(nav(...), public_nav(...))]` and the app's
6
+ * translations — grouped by module, each module's under its title) and
7
+ * `user`; links that aren't a model come from the SidebarLinks extension
8
+ * point.
6
9
  */
7
10
  import { ssr } from "fse-ssr/ssr";
8
11
  import Logo from "./Logo.astro";
12
+ import NavIcon from "./NavIcon.astro";
9
13
  import NavLink from "./NavLink.astro";
10
14
  import SidebarLinks from "./SidebarLinks.astro";
11
15
  import type { ShellContext } from "../types";
12
16
 
13
- const { t, nav, user, lang_prefix } = ssr<ShellContext>();
17
+ const { t, nav_groups, user, lang_prefix } = ssr<ShellContext>();
14
18
  ---
15
19
 
16
20
  <div
@@ -33,7 +37,7 @@ const { t, nav, user, lang_prefix } = ssr<ShellContext>();
33
37
  class="sidebar-full-logo flex items-center gap-2 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-primary-500 rounded-md"
34
38
  href={`${lang_prefix}/`}
35
39
  >
36
- <Logo class="h-9 w-auto" />
40
+ <Logo variant="sidebar" />
37
41
  </a>
38
42
 
39
43
  <button
@@ -69,12 +73,19 @@ const { t, nav, user, lang_prefix } = ssr<ShellContext>();
69
73
 
70
74
  <SidebarLinks />
71
75
 
72
- {nav.map((item) => (
73
- <NavLink href={`${lang_prefix}${item.href}`} label={t.models[item.table].title ?? item.table}>
74
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
75
- <rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M3 9h18M9 21V9"></path>
76
- </svg>
77
- </NavLink>
76
+ {nav_groups.map((group) => (
77
+ <div class="space-y-0.5">
78
+ {group.label && (
79
+ <p class="sidebar-label px-3 pt-4 pb-1 text-xs font-semibold uppercase tracking-wide text-secondary-400 truncate">
80
+ {group.label}
81
+ </p>
82
+ )}
83
+ {group.entries.map((item) => (
84
+ <NavLink href={`${lang_prefix}${item.href}`} label={item.label}>
85
+ <NavIcon name={item.icon} />
86
+ </NavLink>
87
+ ))}
88
+ </div>
78
89
  ))}
79
90
  </nav>
80
91
 
@@ -1,10 +1,17 @@
1
1
  ---
2
2
  /**
3
3
  * Extension point: app-specific sidebar entries, rendered between "Home"
4
- * and the generated model links. Empty in the default theme — a child theme
5
- * overrides this file (same path under its src/) to add its own links:
4
+ * and the generated model links. Filled through the `sidebar-links` slot:
5
+ * every `src/slots/sidebar-links/*.astro` of a child theme or a module is
6
+ * rendered here, so several can add links side by side:
6
7
  *
7
- * import NavLink from "@parent/components/NavLink.astro";
8
+ * // src/slots/sidebar-links/Products.astro
9
+ * import NavLink from "@theme/components/NavLink.astro";
8
10
  * <NavLink href="/products" label={t.nav.products}>…icon…</NavLink>
11
+ *
12
+ * Overriding this file instead replaces every contribution.
9
13
  */
14
+ import Slot from "fse-ssr/Slot.astro";
10
15
  ---
16
+
17
+ <Slot name="sidebar-links" />
@@ -73,7 +73,7 @@ const { lang } = ssr();
73
73
  <line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line>
74
74
  </svg>
75
75
  </button>
76
- <a href="/"><Logo class="h-8 w-auto" /></a>
76
+ <a href="/"><Logo variant="mobile" /></a>
77
77
  </div>
78
78
  )}
79
79
  <slot />
@@ -14,7 +14,7 @@ const messages = t.messages as Record<string, string>;
14
14
  <div class="w-full max-w-sm">
15
15
  <div class="flex justify-center mb-8">
16
16
  <a href="/">
17
- <Logo class="h-12 w-auto" />
17
+ <Logo variant="auth" />
18
18
  </a>
19
19
  </div>
20
20
 
@@ -7,13 +7,19 @@
7
7
  * row) the same form renders disabled and without a save button. An
8
8
  * existing row also shows its allowed row actions and links to its nested
9
9
  * models and link tables; a nested model links back to its parent row.
10
+ * Below the form: `related` — the rows of other models (and modules)
11
+ * that point at this one through a foreign key, one panel each — then the
12
+ * `record-panels` slot: extra panels from a child theme or modules
13
+ * (`src/slots/record-panels/*.astro`, which check `meta.table` to pick the
14
+ * models they extend).
10
15
  */
16
+ import Slot from "fse-ssr/Slot.astro";
11
17
  import { ssr } from "fse-ssr/ssr";
12
18
  import type { SsrValue } from "fse-ssr/ssr";
13
19
  import Layout from "../../layouts/Layout.astro";
14
20
  import type { ModelFormPage } from "../../types";
15
21
 
16
- const { t, lang_prefix, meta, row, errors, is_new, can_edit, can_delete, actions, parent } =
22
+ const { t, lang_prefix, meta, row, errors, is_new, can_edit, can_delete, actions, parent, related } =
17
23
  ssr<ModelFormPage>();
18
24
  const title = t.models[meta.table].title ?? meta.table;
19
25
  const formErrors = t.form_errors as Record<string, SsrValue<string>>;
@@ -155,6 +161,32 @@ const inputClass =
155
161
  ))}
156
162
  </div>
157
163
  )}
164
+
165
+ {related.map((panel) => (
166
+ <section class="mt-8">
167
+ <h2 class="mb-2 text-base font-semibold text-secondary-900">
168
+ {t.models[panel.table].title ?? panel.table}
169
+ {panel.label_column && (
170
+ <span class="font-normal text-secondary-500"> · {t.models[panel.table].fields[panel.label_column] ?? panel.label_column}</span>
171
+ )}
172
+ <span class="ml-1 text-sm font-normal text-secondary-500">({panel.total})</span>
173
+ </h2>
174
+ <ul class="divide-y divide-secondary-100 rounded-lg border border-secondary-100 bg-white">
175
+ {panel.rows.map((item) => (
176
+ <li>
177
+ <a class="block px-4 py-2 text-sm text-secondary-800 hover:bg-secondary-50" href={`${lang_prefix}${panel.base_path}/${item.id}`}>
178
+ {item.title}
179
+ </a>
180
+ </li>
181
+ ))}
182
+ </ul>
183
+ {panel.more && (
184
+ <p class="mt-1 text-xs text-secondary-500">{t.model_ui.more_related ?? "Showing the first 10."}</p>
185
+ )}
186
+ </section>
187
+ ))}
188
+
189
+ <Slot name="record-panels" />
158
190
  </main>
159
191
 
160
192
  <script>
@@ -1,8 +1,11 @@
1
1
  ---
2
2
  /**
3
- * Default home page: a dashboard linking every model the user may manage.
4
- * Apps usually replace it (a child theme's own src/pages/index.astro).
3
+ * Default home page: a dashboard linking every model the user may manage,
4
+ * then the `dashboard` slot (widgets from a child theme or modules:
5
+ * `src/slots/dashboard/*.astro`). Apps usually replace it (a child theme's
6
+ * own src/pages/index.astro).
5
7
  */
8
+ import Slot from "fse-ssr/Slot.astro";
6
9
  import { ssr } from "fse-ssr/ssr";
7
10
  import Card from "../components/Card.astro";
8
11
  import Header from "../components/Header.astro";
@@ -30,12 +33,14 @@ const { t, nav, user, lang_prefix } = ssr<ShellContext>();
30
33
  {nav.map((item) => (
31
34
  <a href={`${lang_prefix}${item.href}`}>
32
35
  <Card accent="primary" hover>
33
- <h2 class="text-lg font-bold text-secondary-900">{t.models[item.table].title ?? item.table}</h2>
36
+ <h2 class="text-lg font-bold text-secondary-900">{item.label}</h2>
34
37
  <p class="text-sm text-secondary-500">{t.dashboard.manage ?? "Manage entries"}</p>
35
38
  </Card>
36
39
  </a>
37
40
  ))}
38
41
  </div>
39
42
  )}
43
+
44
+ <Slot name="dashboard" />
40
45
  </main>
41
46
  </Layout>
@@ -16,7 +16,7 @@ const messages = t.messages as Record<string, string>;
16
16
  <!-- Logo -->
17
17
  <div class="flex justify-center mb-8">
18
18
  <a href="/">
19
- <Logo class="h-12 w-auto" />
19
+ <Logo variant="auth" />
20
20
  </a>
21
21
  </div>
22
22
 
@@ -14,7 +14,7 @@ const errors = t.errors as Record<string, string>;
14
14
  <!-- Logo -->
15
15
  <div class="flex justify-center mb-8">
16
16
  <a href="/">
17
- <Logo class="h-12 w-auto" />
17
+ <Logo variant="auth" />
18
18
  </a>
19
19
  </div>
20
20
 
@@ -5,13 +5,12 @@ import Card from "../components/Card.astro";
5
5
  import Header from "../components/Header.astro";
6
6
  import Input from "../components/Input.astro";
7
7
  import Modal from "../components/Modal.astro";
8
+ import SettingsSections from "../components/SettingsSections.astro";
8
9
  import type { SettingsPage } from "../types/pages";
9
10
 
10
11
  const {
11
12
  t,
12
13
  current_email,
13
- first_name,
14
- last_name,
15
14
  success,
16
15
  error,
17
16
  email_success,
@@ -29,20 +28,6 @@ const errors = t.errors as Record<string, string>;
29
28
  <Card>
30
29
  <div class="flex flex-col divide-y divide-secondary-100">
31
30
 
32
- <!-- Name -->
33
- {(first_name || last_name) && (
34
- <div class="pb-6">
35
- <h3 class="text-base font-semibold text-secondary-900">
36
- {t.settings.name}
37
- </h3>
38
- <div class="mt-3 px-4 py-3 rounded-lg bg-secondary-50 border border-secondary-100">
39
- <span class="text-sm font-medium text-secondary-900">
40
- {first_name} {last_name}
41
- </span>
42
- </div>
43
- </div>
44
- )}
45
-
46
31
  <!-- Email Setting -->
47
32
  <div class="py-6 first:pt-0">
48
33
  <div class="flex items-start justify-between mb-3 flex-wrap gap-2">
@@ -89,6 +74,9 @@ const errors = t.errors as Record<string, string>;
89
74
  )}
90
75
  </div>
91
76
 
77
+ <!-- The app's sections (default: the account's name) -->
78
+ <SettingsSections />
79
+
92
80
  <!-- Danger Zone -->
93
81
  <div class="pt-6">
94
82
  <div class="flex items-start justify-between flex-wrap gap-3">
@@ -56,7 +56,7 @@ const roleLabels = t.roles as Record<string, string>;
56
56
  <!-- Actions -->
57
57
  <div class="flex sm:justify-end pt-5 border-t border-secondary-100">
58
58
  <button type="submit" class="btn-primary w-full sm:w-auto">
59
- <span>{t.common.save}</span>
59
+ <span>{t.users.save}</span>
60
60
  </button>
61
61
  </div>
62
62
  </form>
package/src/types.ts CHANGED
@@ -116,6 +116,22 @@ export interface ModelFormPage {
116
116
  actions: string[];
117
117
  /** The parent row, for a nested model (else null). */
118
118
  parent: ParentContext | null;
119
+ /** Rows of other models (and modules) pointing at this one. */
120
+ related: RelatedPanel[];
121
+ }
122
+
123
+ /**
124
+ * One model's rows that point at a record through a foreign key — the
125
+ * first ten, with `more` when there are others.
126
+ */
127
+ export interface RelatedPanel {
128
+ table: string;
129
+ base_path: string;
130
+ /** Set when the model points here through several columns. */
131
+ label_column: string | null;
132
+ total: number;
133
+ more: boolean;
134
+ rows: { id: number; title: string }[];
119
135
  }
120
136
 
121
137
  /** `fse/links` — the rows of another model linked to one row. */
@@ -136,6 +152,25 @@ export interface ModelDetailPage {
136
152
  export interface NavItem {
137
153
  table: string;
138
154
  href: string;
155
+ /** Already translated (`t.models.<table>.nav` / `.public_nav` / `.title`). */
156
+ label: string;
157
+ /** A `NavIcon` name. */
158
+ icon: string;
159
+ /** The public list's entry rather than the admin page. */
160
+ public: boolean;
161
+ /** The cargo package defining the model (the app's or a module's). */
162
+ krate: string;
163
+ }
164
+
165
+ /**
166
+ * `nav` grouped by origin: the app's own models first (`module` and
167
+ * `label` null), then one group per enabled module, labelled
168
+ * `t.modules.<package>.title`.
169
+ */
170
+ export interface NavGroup {
171
+ module: string | null;
172
+ label: string | null;
173
+ entries: NavItem[];
139
174
  }
140
175
 
141
176
  /** The signed-in user as injected by the framework (absent when signed out). */
@@ -149,6 +184,7 @@ export interface SessionUser {
149
184
  /** Context keys every page receives, whatever renders it. */
150
185
  export interface ShellContext {
151
186
  nav: NavItem[];
187
+ nav_groups: NavGroup[];
152
188
  user?: SessionUser;
153
189
  lang_prefix: string;
154
190
  }
package/theme.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "fse-theme-default",
3
- "version": "0.2.1",
3
+ "version": "0.5.0",
4
4
  "description": "Default full_stack_engine theme: app shell, design system, auth pages, emails and the generic model CRUD pages."
5
5
  }