@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.
- package/.turbo/turbo-build.log +89 -0
- package/README.md +206 -0
- package/dist/index.css +154 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +395 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +357 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logo-X7RC63NT.svg +9 -0
- package/package.json +44 -0
- package/src/components/Breadcrumbs/Breadcrumbs.module.css +22 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +49 -0
- package/src/components/Breadcrumbs/Breadcrumbs.types.ts +4 -0
- package/src/components/Breadcrumbs/index.ts +2 -0
- package/src/components/GlobalFooter/GlobalFooter.module.css +28 -0
- package/src/components/GlobalFooter/GlobalFooter.tsx +23 -0
- package/src/components/GlobalFooter/GlobalFooter.types.ts +3 -0
- package/src/components/GlobalFooter/index.ts +2 -0
- package/src/components/GlobalHeader/GlobalHeader.module.css +70 -0
- package/src/components/GlobalHeader/GlobalHeader.tsx +38 -0
- package/src/components/GlobalHeader/GlobalHeader.types.ts +4 -0
- package/src/components/GlobalHeader/index.ts +2 -0
- package/src/components/MenuButton/MenuButton.module.css +41 -0
- package/src/components/MenuButton/MenuButton.tsx +57 -0
- package/src/components/MenuButton/MenuButton.types.ts +3 -0
- package/src/components/MenuButton/index.ts +2 -0
- package/src/components/RootLayout/RootLayout.module.css +9 -0
- package/src/components/RootLayout/RootLayout.tsx +39 -0
- package/src/components/RootLayout/RootLayout.types.ts +5 -0
- package/src/components/RootLayout/index.ts +2 -0
- package/src/index.ts +6 -0
- package/src/logo.svg +9 -0
- package/src/routes.ts +211 -0
- package/src/store/globalState.ts +40 -0
- package/src/types/css.d.ts +8 -0
- package/src/utils/routeRegistry.ts +92 -0
- package/tsconfig.json +20 -0
- 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,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,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,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,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
|
+
}
|