@rizom/site-rizom 0.2.0-alpha.142
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/dist/index.d.ts +135 -0
- package/dist/index.js +31089 -0
- package/dist/index.js.map +259 -0
- package/package.json +54 -0
- package/src/content.ts +9 -0
- package/src/contracts.ts +157 -0
- package/src/create-site.ts +108 -0
- package/src/index.ts +63 -0
- package/src/runtime/base-site.ts +13 -0
- package/src/runtime/boot/boot.boot.d.ts +2 -0
- package/src/runtime/boot/boot.boot.js +83 -0
- package/src/runtime/canvases/constellation.canvas.d.ts +2 -0
- package/src/runtime/canvases/constellation.canvas.js +307 -0
- package/src/runtime/canvases/prelude.canvas.d.ts +2 -0
- package/src/runtime/canvases/prelude.canvas.js +193 -0
- package/src/runtime/canvases/products.canvas.d.ts +2 -0
- package/src/runtime/canvases/roots.canvas.d.ts +2 -0
- package/src/runtime/canvases/roots.canvas.js +358 -0
- package/src/runtime/canvases/tree.canvas.d.ts +2 -0
- package/src/runtime/canvases/tree.canvas.js +544 -0
- package/src/runtime/default-layout.tsx +10 -0
- package/src/runtime/index.ts +8 -0
- package/src/runtime/plugin.ts +147 -0
- package/src/ui/Badge.tsx +14 -0
- package/src/ui/Button.tsx +56 -0
- package/src/ui/Divider.tsx +12 -0
- package/src/ui/Footer.tsx +76 -0
- package/src/ui/Header.tsx +45 -0
- package/src/ui/Section.tsx +22 -0
- package/src/ui/SideNav.tsx +22 -0
- package/src/ui/cn.ts +1 -0
- package/src/ui/external-link.ts +8 -0
- package/src/ui/frame.tsx +28 -0
- package/src/ui/highlighted-text.tsx +45 -0
- package/src/ui/index.ts +21 -0
- package/src/ui/site-info-links.ts +24 -0
- package/src/ui/types.ts +6 -0
package/src/ui/Badge.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JSX, ComponentChildren } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
export interface BadgeProps {
|
|
5
|
+
children?: ComponentChildren;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BASE =
|
|
10
|
+
"inline-flex items-center px-5 py-2 border border-accent text-accent rounded-[20px] font-label text-label-md font-semibold tracking-[0.09375em] uppercase";
|
|
11
|
+
|
|
12
|
+
export const Badge = ({ children, className }: BadgeProps): JSX.Element => (
|
|
13
|
+
<span className={cn(BASE, className)}>{children}</span>
|
|
14
|
+
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { JSX, ComponentChildren } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
export type ButtonVariant = "primary" | "primary-strong" | "secondary";
|
|
5
|
+
export type ButtonSize = "md" | "lg";
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps {
|
|
8
|
+
href: string;
|
|
9
|
+
variant?: ButtonVariant;
|
|
10
|
+
size?: ButtonSize;
|
|
11
|
+
block?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
children?: ComponentChildren;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const BASE =
|
|
17
|
+
"inline-flex items-center justify-center cursor-pointer border border-solid transition-all [gap:var(--rizom-btn-gap)] [border-radius:var(--rizom-btn-radius)] [font-family:var(--rizom-btn-font-family)] [font-style:var(--rizom-btn-font-style)] [letter-spacing:var(--rizom-btn-letter-spacing)] [text-transform:var(--rizom-btn-text-transform)]";
|
|
18
|
+
const VARIANT: Record<ButtonVariant, string> = {
|
|
19
|
+
primary:
|
|
20
|
+
"[font-weight:var(--rizom-btn-primary-font-weight)] [color:var(--rizom-btn-primary-color)] [background:var(--rizom-btn-primary-bg)] [border-color:var(--rizom-btn-primary-border-color)] [border-width:var(--rizom-btn-primary-border-width)] [box-shadow:var(--rizom-btn-primary-shadow)] hover:[color:var(--rizom-btn-primary-hover-color)] hover:[background:var(--rizom-btn-primary-hover-bg)] hover:[border-color:var(--rizom-btn-primary-hover-border-color)] hover:[border-width:var(--rizom-btn-primary-hover-border-width)] hover:[box-shadow:var(--rizom-btn-primary-hover-shadow)] hover:[transform:var(--rizom-btn-primary-hover-transform)]",
|
|
21
|
+
"primary-strong":
|
|
22
|
+
"duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] [font-weight:var(--rizom-btn-primary-strong-font-weight)] [color:var(--rizom-btn-primary-strong-color)] [background:var(--rizom-btn-primary-strong-bg)] [border-color:var(--rizom-btn-primary-strong-border-color)] [border-width:var(--rizom-btn-primary-strong-border-width)] [box-shadow:var(--rizom-btn-primary-strong-shadow)] hover:[color:var(--rizom-btn-primary-strong-hover-color)] hover:[background:var(--rizom-btn-primary-strong-hover-bg)] hover:[border-color:var(--rizom-btn-primary-strong-hover-border-color)] hover:[border-width:var(--rizom-btn-primary-strong-hover-border-width)] hover:[box-shadow:var(--rizom-btn-primary-strong-hover-shadow)] hover:[transform:var(--rizom-btn-primary-strong-hover-transform)]",
|
|
23
|
+
secondary:
|
|
24
|
+
"[font-weight:var(--rizom-btn-secondary-font-weight)] [color:var(--rizom-btn-secondary-color)] [background:var(--rizom-btn-secondary-bg)] [border-color:var(--rizom-btn-secondary-border-color)] [border-width:var(--rizom-btn-secondary-border-width)] [box-shadow:var(--rizom-btn-secondary-shadow)] hover:[color:var(--rizom-btn-secondary-hover-color)] hover:[background:var(--rizom-btn-secondary-hover-bg)] hover:[border-color:var(--rizom-btn-secondary-hover-border-color)] hover:[border-width:var(--rizom-btn-secondary-hover-border-width)] hover:[box-shadow:var(--rizom-btn-secondary-hover-shadow)] hover:[transform:var(--rizom-btn-secondary-hover-transform)]",
|
|
25
|
+
};
|
|
26
|
+
const SIZE: Record<ButtonSize, string> = {
|
|
27
|
+
md: "text-base [padding:var(--rizom-btn-md-padding)]",
|
|
28
|
+
lg: "text-body-md md:text-body-lg [padding:var(--rizom-btn-lg-padding-mobile)] md:[padding:var(--rizom-btn-lg-padding-desktop)]",
|
|
29
|
+
};
|
|
30
|
+
const BLOCK = "w-full md:w-auto";
|
|
31
|
+
|
|
32
|
+
export const Button = ({
|
|
33
|
+
href,
|
|
34
|
+
variant = "primary",
|
|
35
|
+
size = "md",
|
|
36
|
+
block = false,
|
|
37
|
+
className,
|
|
38
|
+
children,
|
|
39
|
+
}: ButtonProps): JSX.Element => (
|
|
40
|
+
<a
|
|
41
|
+
href={href}
|
|
42
|
+
className={cn(
|
|
43
|
+
"rizom-btn",
|
|
44
|
+
`rizom-btn-${variant}`,
|
|
45
|
+
`rizom-btn-${size}`,
|
|
46
|
+
block && "rizom-btn-block",
|
|
47
|
+
BASE,
|
|
48
|
+
VARIANT[variant],
|
|
49
|
+
SIZE[size],
|
|
50
|
+
block && BLOCK,
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</a>
|
|
56
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { JSX } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
export interface DividerProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const BASE = "w-[60px] h-px bg-[var(--color-divider)] mx-auto";
|
|
9
|
+
|
|
10
|
+
export const Divider = ({ className }: DividerProps): JSX.Element => (
|
|
11
|
+
<div className={cn(BASE, className)} />
|
|
12
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { JSX } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
import { externalLinkProps } from "./external-link";
|
|
4
|
+
import type { RizomBrandSuffix, RizomFooterTagline, RizomLink } from "./types";
|
|
5
|
+
import { GUTTER } from "./Section";
|
|
6
|
+
|
|
7
|
+
const LINK_CLS =
|
|
8
|
+
"text-label-md text-theme-light hover:text-theme transition-colors";
|
|
9
|
+
|
|
10
|
+
const TOGGLE_CLS =
|
|
11
|
+
"bg-transparent border border-theme-light rounded-md px-2.5 py-1.5 cursor-pointer text-theme-light text-label-md font-body transition-colors hover:text-theme hover:border-theme";
|
|
12
|
+
|
|
13
|
+
interface FooterProps {
|
|
14
|
+
brandSuffix: RizomBrandSuffix;
|
|
15
|
+
metaLabel: string;
|
|
16
|
+
tagline?: RizomFooterTagline;
|
|
17
|
+
links: RizomLink[];
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Footer = ({
|
|
22
|
+
brandSuffix,
|
|
23
|
+
metaLabel,
|
|
24
|
+
tagline,
|
|
25
|
+
links,
|
|
26
|
+
className,
|
|
27
|
+
}: FooterProps): JSX.Element => (
|
|
28
|
+
<footer
|
|
29
|
+
className={cn(
|
|
30
|
+
`flex flex-col gap-4 ${GUTTER} py-8 md:flex-row md:items-center md:justify-between md:gap-6 md:py-6 border-t border-theme-light text-center md:text-left`,
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<div className="flex flex-col items-center gap-1.5 md:items-start max-w-[560px]">
|
|
35
|
+
<div className="flex flex-col items-center gap-1.5 md:flex-row md:items-center md:gap-3">
|
|
36
|
+
<span className="font-nav text-[15px]">
|
|
37
|
+
<span className="font-bold">rizom</span>
|
|
38
|
+
<span className="font-bold text-accent">.</span>
|
|
39
|
+
<span className="text-theme-muted">{brandSuffix}</span>
|
|
40
|
+
</span>
|
|
41
|
+
<span className="text-label-md text-theme-light">{metaLabel}</span>
|
|
42
|
+
</div>
|
|
43
|
+
{tagline ? (
|
|
44
|
+
<p className="text-label-md leading-[1.6] text-theme-light">
|
|
45
|
+
{tagline.prefix ?? ""}
|
|
46
|
+
<a
|
|
47
|
+
href={tagline.link.href}
|
|
48
|
+
className="text-accent hover:opacity-75 transition-opacity"
|
|
49
|
+
>
|
|
50
|
+
{tagline.link.label}
|
|
51
|
+
</a>
|
|
52
|
+
{tagline.suffix ?? ""}
|
|
53
|
+
</p>
|
|
54
|
+
) : null}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex flex-wrap items-center justify-center gap-4 md:justify-end md:gap-6">
|
|
57
|
+
{links.map((link) => (
|
|
58
|
+
<a
|
|
59
|
+
key={link.href + link.label}
|
|
60
|
+
href={link.href}
|
|
61
|
+
{...externalLinkProps(link)}
|
|
62
|
+
className={LINK_CLS}
|
|
63
|
+
>
|
|
64
|
+
{link.label}
|
|
65
|
+
</a>
|
|
66
|
+
))}
|
|
67
|
+
<button
|
|
68
|
+
id="themeToggle"
|
|
69
|
+
aria-label="Toggle light mode"
|
|
70
|
+
className={TOGGLE_CLS}
|
|
71
|
+
>
|
|
72
|
+
☀ Light
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
</footer>
|
|
76
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { JSX } from "preact";
|
|
2
|
+
import { externalLinkProps } from "./external-link";
|
|
3
|
+
import type { RizomBrandSuffix, RizomLink } from "./types";
|
|
4
|
+
|
|
5
|
+
const LINK_CLS =
|
|
6
|
+
"hidden md:inline-block font-body text-[15px] text-theme-muted hover:text-theme transition-colors relative py-1 after:content-[''] after:absolute after:left-0 after:bottom-0 after:h-px after:w-0 after:bg-accent after:transition-all after:duration-300 hover:after:w-full";
|
|
7
|
+
|
|
8
|
+
interface HeaderProps {
|
|
9
|
+
brandSuffix: RizomBrandSuffix;
|
|
10
|
+
navLinks: RizomLink[];
|
|
11
|
+
primaryCta: RizomLink;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Header = ({
|
|
15
|
+
brandSuffix,
|
|
16
|
+
navLinks,
|
|
17
|
+
primaryCta,
|
|
18
|
+
}: HeaderProps): JSX.Element => (
|
|
19
|
+
<nav className="fixed top-0 left-0 right-0 z-[100] flex items-center justify-between bg-nav-fade px-6 py-4 backdrop-blur-[8px] md:px-10 md:py-5 xl:px-20">
|
|
20
|
+
<div className="flex items-center gap-0 font-nav text-[20px]">
|
|
21
|
+
<span className="font-bold text-theme">rizom</span>
|
|
22
|
+
<span className="font-bold text-accent">.</span>
|
|
23
|
+
<span className="text-theme-muted">{brandSuffix}</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div className="flex items-center gap-3 md:gap-8">
|
|
26
|
+
{navLinks.map((link) => (
|
|
27
|
+
<a
|
|
28
|
+
key={link.href}
|
|
29
|
+
href={link.href}
|
|
30
|
+
{...externalLinkProps(link)}
|
|
31
|
+
className={LINK_CLS}
|
|
32
|
+
>
|
|
33
|
+
{link.label}
|
|
34
|
+
</a>
|
|
35
|
+
))}
|
|
36
|
+
<a
|
|
37
|
+
href={primaryCta.href}
|
|
38
|
+
{...externalLinkProps(primaryCta)}
|
|
39
|
+
className="font-body text-[13px] font-semibold text-theme border border-theme rounded-[8px] px-4 py-2 transition-colors hover:border-accent hover:text-accent md:px-6 md:py-2.5 md:text-[15px]"
|
|
40
|
+
>
|
|
41
|
+
{primaryCta.label}
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
</nav>
|
|
45
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { JSX, ComponentChildren } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
export interface SectionProps {
|
|
5
|
+
id?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
children?: ComponentChildren;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GUTTER = "px-6 md:px-10 xl:px-20";
|
|
11
|
+
|
|
12
|
+
const BASE = `${GUTTER} relative z-[1]`;
|
|
13
|
+
|
|
14
|
+
export const Section = ({
|
|
15
|
+
id,
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
}: SectionProps): JSX.Element => (
|
|
19
|
+
<section id={id} className={cn(BASE, className)}>
|
|
20
|
+
{children}
|
|
21
|
+
</section>
|
|
22
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { JSX } from "preact";
|
|
2
|
+
import type { RizomSideNavItem } from "./types";
|
|
3
|
+
|
|
4
|
+
interface SideNavProps {
|
|
5
|
+
items: RizomSideNavItem[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const SideNav = ({ items }: SideNavProps): JSX.Element => (
|
|
9
|
+
<aside
|
|
10
|
+
aria-hidden="true"
|
|
11
|
+
className="fixed right-8 top-1/2 z-[90] hidden -translate-y-1/2 flex-col gap-[18px] px-2 py-4 xl:flex"
|
|
12
|
+
>
|
|
13
|
+
{items.map((dot) => (
|
|
14
|
+
<a
|
|
15
|
+
key={dot.href}
|
|
16
|
+
href={dot.href}
|
|
17
|
+
className="side-nav-dot"
|
|
18
|
+
data-label={dot.label}
|
|
19
|
+
/>
|
|
20
|
+
))}
|
|
21
|
+
</aside>
|
|
22
|
+
);
|
package/src/ui/cn.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from "@rizom/ui";
|
package/src/ui/frame.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JSX, ComponentChildren } from "preact";
|
|
2
|
+
|
|
3
|
+
export type { RizomLayoutProps } from "@rizom/site";
|
|
4
|
+
|
|
5
|
+
export interface RizomFrameProps {
|
|
6
|
+
children?: ComponentChildren;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shared Rizom page frame.
|
|
11
|
+
*
|
|
12
|
+
* Owns only the full-page canvas background and the centered page
|
|
13
|
+
* container. Wrapper sites own their actual chrome/layout composition.
|
|
14
|
+
*/
|
|
15
|
+
export const RizomFrame = ({ children }: RizomFrameProps): JSX.Element => (
|
|
16
|
+
<>
|
|
17
|
+
<div
|
|
18
|
+
id="bgCanvasWrap"
|
|
19
|
+
className="rizom-frame-canvas-wrap fixed top-0 left-0 w-full h-full pointer-events-none"
|
|
20
|
+
>
|
|
21
|
+
<canvas id="heroCanvas" className="w-full h-full block" />
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div className="max-w-[1440px] mx-auto relative overflow-x-clip">
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
</>
|
|
28
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Fragment, type JSX } from "preact";
|
|
2
|
+
|
|
3
|
+
const EMPHASIS_PATTERN = /(\*[^*]+\*)/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render a content string with two lightweight markup conventions:
|
|
7
|
+
*
|
|
8
|
+
* - `\n` in the string produces a `<br />` (for multi-line headlines)
|
|
9
|
+
* - Any `*...*` run is wrapped in a `<span>` carrying `highlightClass`
|
|
10
|
+
*
|
|
11
|
+
* Authors write the phrase as one natural string in markdown content;
|
|
12
|
+
* the renderer does the structural split. Highlight styling is passed
|
|
13
|
+
* per-brand (rizom.ai uses accent+midline, rizom.work uses
|
|
14
|
+
* italic+accent) so each brand owns its own emphasis treatment while
|
|
15
|
+
* the parsing/structure is shared.
|
|
16
|
+
*/
|
|
17
|
+
export function renderHighlightedText(
|
|
18
|
+
text: string,
|
|
19
|
+
highlightClass: string,
|
|
20
|
+
): JSX.Element {
|
|
21
|
+
const lines = text.split("\n");
|
|
22
|
+
return (
|
|
23
|
+
<Fragment>
|
|
24
|
+
{lines.map((line, lineIdx) => (
|
|
25
|
+
<Fragment key={lineIdx}>
|
|
26
|
+
{lineIdx > 0 && <br />}
|
|
27
|
+
{line.split(EMPHASIS_PATTERN).map((part, partIdx) => {
|
|
28
|
+
if (
|
|
29
|
+
part.length >= 3 &&
|
|
30
|
+
part.startsWith("*") &&
|
|
31
|
+
part.endsWith("*")
|
|
32
|
+
) {
|
|
33
|
+
return (
|
|
34
|
+
<span key={partIdx} className={highlightClass}>
|
|
35
|
+
{part.slice(1, -1)}
|
|
36
|
+
</span>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return <Fragment key={partIdx}>{part}</Fragment>;
|
|
40
|
+
})}
|
|
41
|
+
</Fragment>
|
|
42
|
+
))}
|
|
43
|
+
</Fragment>
|
|
44
|
+
);
|
|
45
|
+
}
|
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { RizomFrame } from "./frame";
|
|
2
|
+
export type { RizomFrameProps, RizomLayoutProps } from "./frame";
|
|
3
|
+
export { Header } from "./Header";
|
|
4
|
+
export { Footer } from "./Footer";
|
|
5
|
+
export { SideNav } from "./SideNav";
|
|
6
|
+
export { Section, GUTTER } from "./Section";
|
|
7
|
+
export type { SectionProps } from "./Section";
|
|
8
|
+
export { Button } from "./Button";
|
|
9
|
+
export type { ButtonProps, ButtonSize, ButtonVariant } from "./Button";
|
|
10
|
+
export { Badge } from "./Badge";
|
|
11
|
+
export type { BadgeProps } from "./Badge";
|
|
12
|
+
export { Divider } from "./Divider";
|
|
13
|
+
export type { DividerProps } from "./Divider";
|
|
14
|
+
export { renderHighlightedText } from "./highlighted-text";
|
|
15
|
+
export { socialLinksToRizomLinks } from "./site-info-links";
|
|
16
|
+
export type {
|
|
17
|
+
RizomBrandSuffix,
|
|
18
|
+
RizomFooterTagline,
|
|
19
|
+
RizomLink,
|
|
20
|
+
RizomSideNavItem,
|
|
21
|
+
} from "./types";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SiteLayoutInfo } from "@rizom/site";
|
|
2
|
+
import type { RizomLink } from "./types";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_LABELS: Record<string, string> = {
|
|
5
|
+
github: "GitHub",
|
|
6
|
+
linkedin: "LinkedIn",
|
|
7
|
+
instagram: "Instagram",
|
|
8
|
+
email: "Email",
|
|
9
|
+
website: "Website",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function socialLinksToRizomLinks(
|
|
13
|
+
siteInfo: Pick<SiteLayoutInfo, "socialLinks">,
|
|
14
|
+
allowedPlatforms?: string[],
|
|
15
|
+
): RizomLink[] {
|
|
16
|
+
const allowed = allowedPlatforms ? new Set(allowedPlatforms) : undefined;
|
|
17
|
+
|
|
18
|
+
return (siteInfo.socialLinks ?? [])
|
|
19
|
+
.filter((link) => (allowed ? allowed.has(link.platform) : true))
|
|
20
|
+
.map((link) => ({
|
|
21
|
+
href: link.url,
|
|
22
|
+
label: link.label ?? DEFAULT_LABELS[link.platform] ?? link.platform,
|
|
23
|
+
}));
|
|
24
|
+
}
|