@human-synthesis/norns 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@human-synthesis/norns",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Norns — SvelteKit with Civet, Pug, and the .n / .civet / .c file extensions",
5
5
  "license": "MIT",
6
6
  "author": "Daniel Teodoroiu (https://humansynthesis.ai)",
@@ -68,6 +68,12 @@ function parseUnitExpr(exprSrc, states) {
68
68
  return withStateLiterals(parseExpr(exprSrc), states ?? new Set());
69
69
  }
70
70
 
71
+ /** `dealBoard` → `Deal Board`, `leads` → `Leads`. */
72
+ export function humanizeName(name) {
73
+ const spaced = String(name).replaceAll('_', ' ').replaceAll('-', ' ').replace(/([a-z0-9])([A-Z])/g, '$1 $2');
74
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
75
+ }
76
+
71
77
  function whereCall(exprSrc, { entity, ownerField, states }) {
72
78
  const ast = JSON.stringify(parseUnitExpr(exprSrc, states));
73
79
  const owner = ownerField ? `, ownerField: ${JSON.stringify(ownerField)}` : '';
@@ -764,6 +770,8 @@ export function emitModulePages(moduleName, moduleSpec, specs) {
764
770
  );
765
771
  } else {
766
772
  pug.push(`section.norns-page`);
773
+ const titleFrom = ['index', 'home', 'main', 'page'].includes(name) ? moduleName : name;
774
+ pug.push(`\th1.norns-page-title ${humanizeName(titleFrom)}`);
767
775
  for (const c of components) {
768
776
  pug.push(`\t${c.tag}(${c.props.join(' ')})`);
769
777
  }
@@ -25,6 +25,7 @@ import {
25
25
  componentKey,
26
26
  endpointsEmitter,
27
27
  jobsEmitter,
28
+ humanizeName,
28
29
  pagesEmitter,
29
30
  policiesEmitter,
30
31
  queriesEmitter,
@@ -686,24 +687,57 @@ export function liveRouteFile() {
686
687
  * then the generated token-override sheet (after, so `app.settings.tokens`
687
688
  * wins over library defaults pulled in via app.css).
688
689
  *
690
+ * When the specs declare statically-routed Pages, the layout is an admin
691
+ * shell: sidebar nav (one link per Page, active state via aria-current)
692
+ * around the content. Styled by the `.norns-*` atoms in norns-ui; override
693
+ * through `app.settings.tokens` or `src/app.css`.
694
+ *
695
+ * @param {{ app?: *, modules: Record<string, *> }} specs
689
696
  * @param {boolean} hasAppCss
690
697
  * @param {boolean} [hasTokens]
691
698
  * @returns {{ path: string, text: string }}
692
699
  */
693
- export function layoutFile(hasAppCss, hasTokens = false) {
700
+ export function layoutFile(specs, hasAppCss, hasTokens = false) {
701
+ const nav = [];
702
+ for (const [moduleName, spec] of Object.entries(specs?.modules ?? {})) {
703
+ for (const [name, p] of Object.entries(spec?.pages ?? {})) {
704
+ const route = p?.route;
705
+ if (typeof route !== 'string' || route.includes('[') || route.includes(':')) continue;
706
+ // Generic page names label as their module: companies.Page.index → "Companies".
707
+ const label = ['index', 'home', 'main', 'page'].includes(name) ? moduleName : name;
708
+ nav.push({ href: route, label: humanizeName(label) });
709
+ }
710
+ }
711
+ nav.sort((a, b) => a.href.localeCompare(b.href));
712
+ const brand = humanizeName(specs?.app?.name ?? 'App');
713
+
714
+ const script = [
715
+ '<script>',
716
+ ...(hasAppCss ? ["\timport '$custom/app.css';"] : []),
717
+ ...(hasTokens ? ["\timport './tokens.css';"] : []),
718
+ ...(nav.length ? ["\timport { page } from '$app/state';"] : []),
719
+ '\tlet { children } = $props();',
720
+ ...(nav.length ? [`\tconst nav = ${JSON.stringify(nav)};`] : []),
721
+ '</script>'
722
+ ];
723
+ const body = nav.length
724
+ ? [
725
+ '<div class="norns-shell">',
726
+ '\t<aside class="norns-sidebar">',
727
+ `\t\t<div class="norns-brand">${brand}</div>`,
728
+ '\t\t<nav class="norns-nav">',
729
+ '\t\t\t{#each nav as item (item.href)}',
730
+ "\t\t\t\t<a href={item.href} aria-current={page.url.pathname === item.href ? 'page' : undefined}>{item.label}</a>",
731
+ '\t\t\t{/each}',
732
+ '\t\t</nav>',
733
+ '\t</aside>',
734
+ '\t<main class="norns-main">{@render children()}</main>',
735
+ '</div>'
736
+ ]
737
+ : ['{@render children()}'];
694
738
  return {
695
739
  path: 'routes/+layout.svelte',
696
- text: [
697
- '<!-- GENERATED by `norns generate` — do not edit. -->',
698
- '<script>',
699
- ...(hasAppCss ? ["\timport '$custom/app.css';"] : []),
700
- ...(hasTokens ? ["\timport './tokens.css';"] : []),
701
- '\tlet { children } = $props();',
702
- '</script>',
703
- '',
704
- '{@render children()}',
705
- ''
706
- ].join('\n')
740
+ text: ['<!-- GENERATED by `norns generate` — do not edit. -->', ...script, '', ...body, ''].join('\n')
707
741
  };
708
742
  }
709
743
 
@@ -808,7 +842,7 @@ export function generateApp(dir, opts = {}) {
808
842
 
809
843
  // App-level: a root layout so generated routes render inside a shell.
810
844
  if (pending.length > 0 || !existsSync(join(outRoot, 'routes', '+layout.svelte'))) {
811
- pending.push(layoutFile(existsSync(join(appRoot, 'src', 'app.css')), Boolean(overrides)));
845
+ pending.push(layoutFile(specs, existsSync(join(appRoot, 'src', 'app.css')), Boolean(overrides)));
812
846
  }
813
847
 
814
848
  const failures = selfCheck(pending);