@statsbygg/layout 0.0.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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +89 -0
  2. package/README.md +206 -0
  3. package/dist/index.css +154 -0
  4. package/dist/index.css.map +1 -0
  5. package/dist/index.d.mts +40 -0
  6. package/dist/index.d.ts +40 -0
  7. package/dist/index.js +395 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +357 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/logo-X7RC63NT.svg +9 -0
  12. package/package.json +44 -0
  13. package/src/components/Breadcrumbs/Breadcrumbs.module.css +22 -0
  14. package/src/components/Breadcrumbs/Breadcrumbs.tsx +49 -0
  15. package/src/components/Breadcrumbs/Breadcrumbs.types.ts +4 -0
  16. package/src/components/Breadcrumbs/index.ts +2 -0
  17. package/src/components/GlobalFooter/GlobalFooter.module.css +28 -0
  18. package/src/components/GlobalFooter/GlobalFooter.tsx +23 -0
  19. package/src/components/GlobalFooter/GlobalFooter.types.ts +3 -0
  20. package/src/components/GlobalFooter/index.ts +2 -0
  21. package/src/components/GlobalHeader/GlobalHeader.module.css +70 -0
  22. package/src/components/GlobalHeader/GlobalHeader.tsx +38 -0
  23. package/src/components/GlobalHeader/GlobalHeader.types.ts +4 -0
  24. package/src/components/GlobalHeader/index.ts +2 -0
  25. package/src/components/MenuButton/MenuButton.module.css +41 -0
  26. package/src/components/MenuButton/MenuButton.tsx +57 -0
  27. package/src/components/MenuButton/MenuButton.types.ts +3 -0
  28. package/src/components/MenuButton/index.ts +2 -0
  29. package/src/components/RootLayout/RootLayout.module.css +9 -0
  30. package/src/components/RootLayout/RootLayout.tsx +39 -0
  31. package/src/components/RootLayout/RootLayout.types.ts +5 -0
  32. package/src/components/RootLayout/index.ts +2 -0
  33. package/src/index.ts +6 -0
  34. package/src/logo.svg +9 -0
  35. package/src/routes.ts +211 -0
  36. package/src/store/globalState.ts +40 -0
  37. package/src/types/css.d.ts +8 -0
  38. package/src/utils/routeRegistry.ts +92 -0
  39. package/tsconfig.json +20 -0
  40. package/tsup.config.ts +15 -0
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@statsbygg/layout",
3
+ "version": "0.0.1",
4
+ "publishConfig": { "access": "public" },
5
+ "description": "Shared layout components for Statsbygg microfrontend architecture",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsup --watch",
19
+ "lint": "eslint src --ext .ts,.tsx",
20
+ "type-check": "tsc --noEmit"
21
+ },
22
+ "peerDependencies": {
23
+ "next": ">=14.0.0",
24
+ "react": ">=18.0.0",
25
+ "react-dom": ">=18.0.0",
26
+ "zustand": "^5.0.4"
27
+ },
28
+ "dependencies": {
29
+ "@digdir/designsystemet-react": "^1.5.1",
30
+ "@statsbygg/design-tokens": "^0.2.0",
31
+ "clsx": "^2.0.0",
32
+ "zustand": "^5.0.4",
33
+ "lucide-react": "^0.514.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20.0.0",
37
+ "@types/react": "^18.2.0",
38
+ "@types/react-dom": "^18.2.0",
39
+ "tsup": "^8.0.0",
40
+ "zustand": "^5.0.4",
41
+ "typescript": "^5.0.0"
42
+ },
43
+ "sideEffects": ["**/*.css"]
44
+ }
@@ -0,0 +1,22 @@
1
+ .breadcrumbs {
2
+ --dsc-breadcrumbs-color: var(--ds-color-text-default);
3
+ padding: 2.5rem 0 0 0;
4
+ }
5
+
6
+ .link {
7
+ text-decoration: underline;
8
+ text-underline-offset: 2px;
9
+ color: var(--ds-color-text-default);
10
+ }
11
+
12
+ .link:hover
13
+ .link:visited{
14
+ color: var(--ds-color-text-default);
15
+ }
16
+
17
+ .currentLink {
18
+ text-decoration: none;
19
+ font-weight: 500;
20
+ pointer-events: none;
21
+ color: var(--ds-color-text-default);
22
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import { Breadcrumbs } from '@digdir/designsystemet-react';
5
+ import clsx from 'clsx';
6
+ import type { BreadcrumbsProps } from './Breadcrumbs.types';
7
+ import styles from './Breadcrumbs.module.css';
8
+ import { getBreadcrumbs, getZoneRoot, transformHrefForZone } from '@/routes';
9
+
10
+ export function SbBreadcrumbs({ className, zone }: BreadcrumbsProps) {
11
+ const pathname = usePathname();
12
+ const isDev = process.env.NODE_ENV === 'development';
13
+ const prodUrl = 'https://www.statsbygg.no';
14
+ const zoneRoot = getZoneRoot(zone);
15
+
16
+ const fullPath = isDev && zoneRoot !== '/' && !pathname.startsWith(zoneRoot)
17
+ ? `${zoneRoot}${pathname}`
18
+ : pathname;
19
+
20
+ const breadcrumbs = getBreadcrumbs(zone, fullPath);
21
+
22
+ if (breadcrumbs.length <= 1) {
23
+ return null;
24
+ }
25
+
26
+
27
+ return (
28
+ <Breadcrumbs aria-label="Du er her:" className={clsx(styles.breadcrumbs, className)}>
29
+ <Breadcrumbs.List>
30
+ {breadcrumbs.map((crumb, index) => {
31
+ const isLast = index === breadcrumbs.length - 1;
32
+ const href = transformHrefForZone(crumb.href, zone, { isDev, prodUrl });
33
+
34
+ return (
35
+ <Breadcrumbs.Item key={crumb.href}>
36
+ <Breadcrumbs.Link
37
+ href={href}
38
+ aria-current={isLast ? 'page' : undefined}
39
+ className={isLast ? styles.currentLink : styles.link}
40
+ >
41
+ {crumb.label}
42
+ </Breadcrumbs.Link>
43
+ </Breadcrumbs.Item>
44
+ );
45
+ })}
46
+ </Breadcrumbs.List>
47
+ </Breadcrumbs>
48
+ );
49
+ }
@@ -0,0 +1,4 @@
1
+ export interface BreadcrumbsProps {
2
+ className?: string;
3
+ zone: string;
4
+ }
@@ -0,0 +1,2 @@
1
+ export { SbBreadcrumbs as Breadcrumbs } from './Breadcrumbs';
2
+ export type { BreadcrumbsProps } from './Breadcrumbs.types';
@@ -0,0 +1,28 @@
1
+ .footer {
2
+ background-color: var(--ds-color-neutral-surface-subtle);
3
+ border-top: 1px solid var(--ds-color-neutral-border-subtle);
4
+ margin-top: auto;
5
+ }
6
+
7
+ .container {
8
+ max-width: 1440px;
9
+ margin: 0 auto;
10
+ padding: var(--ds-spacing-6) var(--ds-spacing-4);
11
+ }
12
+
13
+ .content {
14
+ display: flex;
15
+ justify-content: space-between;
16
+ align-items: center;
17
+ gap: var(--ds-spacing-4);
18
+ flex-wrap: wrap;
19
+ }
20
+
21
+ @media (max-width: 768px) {
22
+ .content {
23
+ flex-direction: column;
24
+ align-items: flex-start;
25
+ }
26
+
27
+
28
+ }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { Paragraph } from '@digdir/designsystemet-react';
4
+ import clsx from 'clsx';
5
+ import type { GlobalFooterProps } from './GlobalFooter.types';
6
+ import styles from './GlobalFooter.module.css';
7
+
8
+ export function GlobalFooter({ className }: GlobalFooterProps) {
9
+
10
+ return (
11
+ <footer className={clsx(styles.footer, className)}>
12
+ <div className={styles.container}>
13
+ <div className={styles.content}>
14
+ <Paragraph>
15
+ Statsbygg Footer
16
+ </Paragraph>
17
+
18
+
19
+ </div>
20
+ </div>
21
+ </footer>
22
+ );
23
+ }
@@ -0,0 +1,3 @@
1
+ export interface GlobalFooterProps {
2
+ className?: string;
3
+ }
@@ -0,0 +1,2 @@
1
+ export { GlobalFooter } from './GlobalFooter';
2
+ export type { GlobalFooterProps } from './GlobalFooter.types';
@@ -0,0 +1,70 @@
1
+ .header {
2
+ background-color: var(--ds-color-accent-surface-tinted);
3
+ border-bottom: 1px solid var(--ds-color-neutral-border-subtle);
4
+ position: sticky;
5
+ top: 0;
6
+ z-index: 100;
7
+ }
8
+
9
+ .headerContainer {
10
+ max-width: 90rem;
11
+ margin: 0 auto;
12
+ padding: 0 var(--ds-size-30);
13
+ }
14
+
15
+ .topBarContainer {
16
+ display: flex;
17
+ justify-content: space-between;
18
+ align-items: center;
19
+ padding: 1.25rem 0;
20
+ }
21
+
22
+ .logo {
23
+ margin: 0;
24
+ color: var(--ds-color-neutral-text-default);
25
+ white-space: nowrap;
26
+ display: flex;
27
+ align-items: center;
28
+ min-height: inherit;
29
+ }
30
+
31
+ .actionsContainer {
32
+ display: flex;
33
+ align-items: stretch;
34
+ gap: var(--ds-size-9);
35
+ min-height: inherit;
36
+ }
37
+
38
+ .searchInput {
39
+ min-width: 12.5rem;
40
+ display: flex;
41
+ align-items: center;
42
+ }
43
+
44
+ @media (max-width: 768px) {
45
+ .container {
46
+ padding: 0 var(--ds-spacing-4);
47
+ }
48
+
49
+ .topBar {
50
+ flex-wrap: wrap;
51
+ padding: var(--ds-spacing-4) 0;
52
+ }
53
+
54
+ .actions {
55
+ order: 3;
56
+ width: 100%;
57
+ flex-direction: column;
58
+ gap: var(--ds-spacing-3);
59
+ }
60
+
61
+ .searchInput {
62
+ width: 100%;
63
+ min-width: auto;
64
+ }
65
+
66
+ .menuButton {
67
+ width: 100%;
68
+ justify-content: center;
69
+ }
70
+ }
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Link, Textfield } from '@digdir/designsystemet-react';
5
+ import clsx from 'clsx';
6
+ import { Breadcrumbs } from '../Breadcrumbs';
7
+ import { MenuButton } from '../MenuButton';
8
+ import type { GlobalHeaderProps } from './GlobalHeader.types';
9
+ import styles from './GlobalHeader.module.css';
10
+ import logo from '../../logo.svg';
11
+ import Image from 'next/image';
12
+
13
+ export function GlobalHeader({ className, zone }: GlobalHeaderProps) {
14
+ const [searchValue, setSearchValue] = useState('');
15
+
16
+ return (
17
+ <header className={clsx(styles.header, className)}>
18
+ <div className={styles.headerContainer}>
19
+ <div className={styles.topBarContainer}>
20
+ <Link href="https://www.statsbygg.no">
21
+ <Image src={logo} alt="Logo" className={styles.logo} />
22
+ </Link>
23
+ <div className={styles.actionsContainer}>
24
+ <Textfield
25
+ value={searchValue}
26
+ onChange={(e) => setSearchValue(e.target.value)}
27
+ placeholder="Søk..."
28
+ className={styles.searchInput}
29
+ aria-label="Søk"
30
+ />
31
+ <MenuButton zone={zone}/>
32
+ </div>
33
+ </div>
34
+ <Breadcrumbs zone={zone} />
35
+ </div>
36
+ </header>
37
+ );
38
+ }
@@ -0,0 +1,4 @@
1
+ export interface GlobalHeaderProps {
2
+ className?: string;
3
+ zone: string;
4
+ }
@@ -0,0 +1,2 @@
1
+ export { GlobalHeader } from './GlobalHeader';
2
+ export type { GlobalHeaderProps } from './GlobalHeader.types';
@@ -0,0 +1,41 @@
1
+ .userInfo {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--ds-spacing-1);
5
+ padding: var(--ds-spacing-2) var(--ds-spacing-3);
6
+ }
7
+
8
+ .userName {
9
+ font-size: var(--ds-font-size-sm);
10
+ font-weight: var(--ds-font-weight-medium);
11
+ color: var(--ds-color-neutral-text-default);
12
+ }
13
+
14
+ .userEmail {
15
+ font-size: var(--ds-font-size-xs);
16
+ color: var(--ds-color-neutral-text-subtle);
17
+ }
18
+
19
+ .menuButton {
20
+ background-color: var(--ds-color-neutral-base-default);
21
+ }
22
+
23
+ .devContainer {
24
+ display: flex;
25
+ gap: var(--ds-spacing-4);
26
+ padding: var(--ds-spacing-2);
27
+ }
28
+
29
+ .zoneSection {
30
+ flex: 1;
31
+ min-width: 12rem;
32
+ }
33
+
34
+ .zoneTitle {
35
+ font-weight: 600;
36
+ padding: var(--ds-spacing-2);
37
+ color: var(--ds-color-neutral-text-default);
38
+ border-bottom: 1px solid var(--ds-color-neutral-border-subtle);
39
+ margin-bottom: var(--ds-spacing-2);
40
+ text-transform: capitalize;
41
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { Dropdown } from '@digdir/designsystemet-react';
5
+ import { Menu } from 'lucide-react';
6
+ import { getZoneMenuRoutes, getAllZones, transformHrefForZone } from '@/routes';
7
+ import type { MenuButtonProps } from './MenuButton.types';
8
+ import styles from './MenuButton.module.css';
9
+
10
+ export function MenuButton({ zone }: MenuButtonProps) {
11
+ const isDev = process.env.NODE_ENV === 'development';
12
+ // TODO: Temporary here. Should come from env or something
13
+ const prodUrl = 'https://www.statsbygg.no';
14
+ const allZones = getAllZones();
15
+
16
+ return (
17
+ <Dropdown.TriggerContext>
18
+ <Dropdown.Trigger asChild className={styles.menuButton}>
19
+ <Menu size={20} aria-hidden />
20
+ Meny
21
+ </Dropdown.Trigger>
22
+ <Dropdown>
23
+ {isDev ? (
24
+ <div className={styles.devContainer}>
25
+ {allZones.map((z) => {
26
+ const routes = getZoneMenuRoutes(z);
27
+ return (
28
+ <div key={z} className={styles.zoneSection}>
29
+ <div className={styles.zoneTitle}>{z}</div>
30
+ <Dropdown.List>
31
+ {routes.map((r) => (
32
+ <Dropdown.Item key={`${r.zone}:${r.path}`}>
33
+ <Link href={transformHrefForZone(r.path, zone, { isDev, prodUrl })}>
34
+ {r.label}
35
+ </Link>
36
+ </Dropdown.Item>
37
+ ))}
38
+ </Dropdown.List>
39
+ </div>
40
+ );
41
+ })}
42
+ </div>
43
+ ) : (
44
+ <Dropdown.List>
45
+ {getZoneMenuRoutes(zone).map((r) => (
46
+ <Dropdown.Item key={`${r.zone}:${r.path}`}>
47
+ <Link href={transformHrefForZone(r.path, zone, { isDev, prodUrl })}>
48
+ {r.label}
49
+ </Link>
50
+ </Dropdown.Item>
51
+ ))}
52
+ </Dropdown.List>
53
+ )}
54
+ </Dropdown>
55
+ </Dropdown.TriggerContext>
56
+ );
57
+ }
@@ -0,0 +1,3 @@
1
+ export type MenuButtonProps = {
2
+ zone: string;
3
+ };
@@ -0,0 +1,2 @@
1
+ export { MenuButton } from './MenuButton';
2
+ export type { MenuButtonProps } from './MenuButton.types';
@@ -0,0 +1,9 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ min-height: 100vh;
5
+ }
6
+
7
+ .main {
8
+ flex: 1;
9
+ }
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import clsx from 'clsx';
5
+ import { GlobalHeader } from '../GlobalHeader';
6
+ import { GlobalFooter } from '../GlobalFooter';
7
+ import type { RootLayoutProps } from './RootLayout.types';
8
+ import styles from './RootLayout.module.css';
9
+ import { useGlobalStore } from '@/store/globalState';
10
+
11
+ export function RootLayout({
12
+ children,
13
+ zone,
14
+ className,
15
+ }: RootLayoutProps) {
16
+ const initialize = useGlobalStore((state) => state.initialize);
17
+
18
+ useEffect(() => {
19
+ try {
20
+ const maybe = initialize();
21
+ if (maybe && typeof (maybe as Promise<void>).then === 'function') {
22
+ (maybe as Promise<void>).catch((error) => {
23
+ console.error('Failed to initialize global state:', error);
24
+ });
25
+ }
26
+ } catch (error) {
27
+ console.error('Failed to initialize global state:', error);
28
+ }
29
+ }, [initialize]);
30
+
31
+
32
+ return (
33
+ <div className={clsx(styles.root, className)} data-zone={zone}>
34
+ <GlobalHeader zone={zone}/>
35
+ <main className={styles.main}>{children}</main>
36
+ <GlobalFooter />
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,5 @@
1
+ export interface RootLayoutProps {
2
+ children: React.ReactNode;
3
+ zone: string;
4
+ className?: string;
5
+ }
@@ -0,0 +1,2 @@
1
+ export { RootLayout } from './RootLayout';
2
+ export type { RootLayoutProps } from './RootLayout.types';
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+
2
+ export { RootLayout } from './components/RootLayout';
3
+ export type { RootLayoutProps } from './components/RootLayout';
4
+
5
+ export { useGlobalStore } from './store/globalState';
6
+ export type { GlobalState } from './store/globalState';