@zpress/ui 0.1.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/LICENSE +21 -0
- package/dist/index.mjs +75 -0
- package/dist/theme/components/home/feature-card.css +89 -0
- package/dist/theme/components/home/feature-card.tsx +75 -0
- package/dist/theme/components/home/feature.tsx +53 -0
- package/dist/theme/components/home/layout.tsx +36 -0
- package/dist/theme/components/home/workspaces.tsx +53 -0
- package/dist/theme/components/nav/branch-tag.css +54 -0
- package/dist/theme/components/nav/branch-tag.tsx +66 -0
- package/dist/theme/components/shared/card.tsx +27 -0
- package/dist/theme/components/shared/icon.tsx +27 -0
- package/dist/theme/components/shared/section-card.tsx +42 -0
- package/dist/theme/components/shared/section-grid.tsx +12 -0
- package/dist/theme/components/shared/tech-icon-table.tsx +68 -0
- package/dist/theme/components/shared/tech-tag.tsx +28 -0
- package/dist/theme/components/workspaces/card.css +141 -0
- package/dist/theme/components/workspaces/card.tsx +116 -0
- package/dist/theme/components/workspaces/grid.tsx +40 -0
- package/dist/theme/css.d.ts +1 -0
- package/dist/theme/fonts/GeistMono-Variable.woff2 +0 -0
- package/dist/theme/fonts/GeistPixel-Square.woff2 +0 -0
- package/dist/theme/fonts/GeistSans-Variable.woff2 +0 -0
- package/dist/theme/hooks/use-zpress.ts +57 -0
- package/dist/theme/icons/index.ts +2 -0
- package/dist/theme/icons/tech-map.ts +221 -0
- package/dist/theme/index.tsx +46 -0
- package/dist/theme/styles/overrides/details.css +61 -0
- package/dist/theme/styles/overrides/fonts.css +27 -0
- package/dist/theme/styles/overrides/home-card.css +125 -0
- package/dist/theme/styles/overrides/home.css +55 -0
- package/dist/theme/styles/overrides/rspress.css +108 -0
- package/dist/theme/styles/overrides/scrollbar.css +25 -0
- package/dist/theme/styles/overrides/section-card.css +115 -0
- package/dist/theme/styles/overrides/sidebar.css +9 -0
- package/dist/theme/styles/overrides/tokens.css +48 -0
- package/package.json +64 -0
- package/src/theme/components/home/feature-card.css +89 -0
- package/src/theme/components/home/feature-card.tsx +75 -0
- package/src/theme/components/home/feature.tsx +53 -0
- package/src/theme/components/home/layout.tsx +36 -0
- package/src/theme/components/home/workspaces.tsx +53 -0
- package/src/theme/components/nav/branch-tag.css +54 -0
- package/src/theme/components/nav/branch-tag.tsx +66 -0
- package/src/theme/components/shared/card.tsx +27 -0
- package/src/theme/components/shared/icon.tsx +27 -0
- package/src/theme/components/shared/section-card.tsx +42 -0
- package/src/theme/components/shared/section-grid.tsx +12 -0
- package/src/theme/components/shared/tech-icon-table.tsx +68 -0
- package/src/theme/components/shared/tech-tag.tsx +28 -0
- package/src/theme/components/workspaces/card.css +141 -0
- package/src/theme/components/workspaces/card.tsx +116 -0
- package/src/theme/components/workspaces/grid.tsx +40 -0
- package/src/theme/css.d.ts +1 -0
- package/src/theme/fonts/GeistMono-Variable.woff2 +0 -0
- package/src/theme/fonts/GeistPixel-Square.woff2 +0 -0
- package/src/theme/fonts/GeistSans-Variable.woff2 +0 -0
- package/src/theme/hooks/use-zpress.ts +57 -0
- package/src/theme/icons/index.ts +2 -0
- package/src/theme/icons/tech-map.ts +221 -0
- package/src/theme/index.tsx +46 -0
- package/src/theme/styles/overrides/details.css +61 -0
- package/src/theme/styles/overrides/fonts.css +27 -0
- package/src/theme/styles/overrides/home-card.css +125 -0
- package/src/theme/styles/overrides/home.css +55 -0
- package/src/theme/styles/overrides/rspress.css +108 -0
- package/src/theme/styles/overrides/scrollbar.css +25 -0
- package/src/theme/styles/overrides/section-card.css +115 -0
- package/src/theme/styles/overrides/sidebar.css +9 -0
- package/src/theme/styles/overrides/tokens.css +48 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Card — layout and typography (base card styles from home-card.css).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* ── Feature grid ───────────────────────────────────────────── */
|
|
6
|
+
.feature-grid {
|
|
7
|
+
--feature-grid-gap: 12px;
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
gap: var(--feature-grid-gap);
|
|
11
|
+
max-width: 1152px;
|
|
12
|
+
margin: 0 auto;
|
|
13
|
+
padding: 0 0 64px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ── Grid item + span system ────────────────────────────────── */
|
|
17
|
+
.feature-card__item {
|
|
18
|
+
width: 100%;
|
|
19
|
+
position: relative;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.feature-card__item-wrapper {
|
|
23
|
+
position: relative;
|
|
24
|
+
height: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (min-width: 640px) {
|
|
28
|
+
.feature-card__item--span-2,
|
|
29
|
+
.feature-card__item--span-4,
|
|
30
|
+
.feature-card__item--span-6 {
|
|
31
|
+
width: calc(50% - var(--feature-grid-gap));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@media (min-width: 768px) {
|
|
36
|
+
.feature-card__item--span-2,
|
|
37
|
+
.feature-card__item--span-4 {
|
|
38
|
+
width: calc(50% - var(--feature-grid-gap));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.feature-card__item--span-3,
|
|
42
|
+
.feature-card__item--span-6 {
|
|
43
|
+
width: calc((100% - 2 * var(--feature-grid-gap)) / 3);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@media (min-width: 960px) {
|
|
48
|
+
.feature-card__item--span-3 {
|
|
49
|
+
width: calc((100% - 3 * var(--feature-grid-gap)) / 4);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.feature-card__item--span-4 {
|
|
53
|
+
width: calc((100% - 2 * var(--feature-grid-gap)) / 3);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.feature-card__item--span-6 {
|
|
57
|
+
width: calc(50% - var(--feature-grid-gap));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Feature card layout ───────────────────────────────────── */
|
|
62
|
+
.feature-card {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: 10px;
|
|
66
|
+
height: 100%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Row 1: icon + title */
|
|
70
|
+
.feature-header {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 12px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.feature-title {
|
|
77
|
+
font-family: 'Geist Pixel Square', var(--rp-font-family-base, sans-serif);
|
|
78
|
+
font-size: 18px;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
color: var(--rp-c-text-1, var(--zp-c-text-1, #fbfbfb));
|
|
81
|
+
line-height: 1.4;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Row 2: description */
|
|
85
|
+
.feature-desc {
|
|
86
|
+
font-size: 16px;
|
|
87
|
+
color: var(--rp-c-text-2, var(--zp-c-text-2, rgba(251, 251, 251, 0.72)));
|
|
88
|
+
line-height: 1.5;
|
|
89
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { match, P } from 'ts-pattern'
|
|
3
|
+
|
|
4
|
+
import './feature-card.css'
|
|
5
|
+
import { Card } from '../shared/card'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Icon color variants matching the design system.
|
|
9
|
+
*/
|
|
10
|
+
export type IconColor = 'purple' | 'blue' | 'green' | 'amber' | 'red' | 'slate' | 'cyan' | 'pink'
|
|
11
|
+
|
|
12
|
+
export interface FeatureCardProps {
|
|
13
|
+
readonly title: string
|
|
14
|
+
readonly description: string
|
|
15
|
+
readonly href?: string
|
|
16
|
+
readonly icon?: React.ReactNode
|
|
17
|
+
readonly iconColor?: IconColor
|
|
18
|
+
readonly span?: 2 | 3 | 4 | 6
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Frontmatter-shaped data for consumers to map from.
|
|
23
|
+
*/
|
|
24
|
+
export interface FeatureItem {
|
|
25
|
+
readonly title: string
|
|
26
|
+
readonly details: string
|
|
27
|
+
readonly link?: string
|
|
28
|
+
readonly icon?: string
|
|
29
|
+
readonly iconColor?: IconColor
|
|
30
|
+
readonly span?: 2 | 3 | 4 | 6
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Feature card for landing pages — matches the workspace/section card design.
|
|
35
|
+
* Renders as `<a>` when `href` is provided, `<div>` otherwise.
|
|
36
|
+
*/
|
|
37
|
+
export function FeatureCard({
|
|
38
|
+
title,
|
|
39
|
+
description,
|
|
40
|
+
href,
|
|
41
|
+
icon,
|
|
42
|
+
iconColor = 'purple',
|
|
43
|
+
span = 4,
|
|
44
|
+
}: FeatureCardProps): React.ReactElement {
|
|
45
|
+
const iconEl = match(icon)
|
|
46
|
+
.with(P.nonNullable, (ic) => (
|
|
47
|
+
<span className={`home-card-icon home-card-icon--${iconColor}`}>{ic}</span>
|
|
48
|
+
))
|
|
49
|
+
.otherwise(() => null)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={`feature-card__item feature-card__item--span-${span}`}>
|
|
53
|
+
<div className="feature-card__item-wrapper">
|
|
54
|
+
<Card href={href} className="feature-card">
|
|
55
|
+
<div className="feature-header">
|
|
56
|
+
{iconEl}
|
|
57
|
+
<span className="feature-title">{title}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<span className="feature-desc">{description}</span>
|
|
60
|
+
</Card>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface FeatureGridProps {
|
|
67
|
+
readonly children: React.ReactNode
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Flex-wrap layout container for feature cards.
|
|
72
|
+
*/
|
|
73
|
+
export function FeatureGrid({ children }: FeatureGridProps): React.ReactElement {
|
|
74
|
+
return <div className="feature-grid">{children}</div>
|
|
75
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useFrontmatter } from '@rspress/core/runtime'
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
import { match, P } from 'ts-pattern'
|
|
4
|
+
|
|
5
|
+
import { Icon } from '../shared/icon'
|
|
6
|
+
import { FeatureCard, FeatureGrid } from './feature-card'
|
|
7
|
+
import type { FeatureItem } from './feature-card'
|
|
8
|
+
|
|
9
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Render a single feature as a FeatureCard element.
|
|
13
|
+
* Accepts the array index from `.map()` to guarantee unique keys.
|
|
14
|
+
*/
|
|
15
|
+
function renderFeature(feature: FeatureItem, index: number): React.ReactElement {
|
|
16
|
+
const iconEl = match(feature.icon)
|
|
17
|
+
.with(P.nonNullable, (iconId) => <Icon icon={iconId} />)
|
|
18
|
+
.otherwise(() => null)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<FeatureCard
|
|
22
|
+
key={`${feature.title}-${index}`}
|
|
23
|
+
title={feature.title}
|
|
24
|
+
description={feature.details}
|
|
25
|
+
href={feature.link}
|
|
26
|
+
icon={iconEl}
|
|
27
|
+
iconColor={feature.iconColor}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Component ────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom HomeFeature override for zpress.
|
|
36
|
+
* Uses useFrontmatter() hook to read features and renders with FeatureCard/FeatureGrid styling.
|
|
37
|
+
*/
|
|
38
|
+
export function HomeFeature(): React.ReactElement | null {
|
|
39
|
+
const { frontmatter } = useFrontmatter()
|
|
40
|
+
// Rspress types frontmatter as its own FrontMatterMeta shape which does not
|
|
41
|
+
// include zpress-specific `features`. The double cast is necessary because
|
|
42
|
+
// no shared Zod schema exists for frontmatter validation at runtime.
|
|
43
|
+
const features = (frontmatter as Record<string, unknown>).features as
|
|
44
|
+
| readonly FeatureItem[]
|
|
45
|
+
| undefined
|
|
46
|
+
|
|
47
|
+
return match(features)
|
|
48
|
+
.with(
|
|
49
|
+
P.when((f): f is readonly FeatureItem[] => Array.isArray(f) && f.length > 0),
|
|
50
|
+
(items) => <FeatureGrid>{items.map(renderFeature)}</FeatureGrid>
|
|
51
|
+
)
|
|
52
|
+
.otherwise(() => null)
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HomeLayout as OriginalHomeLayout } from '@rspress/core/theme-original'
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
|
|
4
|
+
import { HomeWorkspaces } from './workspaces'
|
|
5
|
+
|
|
6
|
+
// ── Types ────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
interface HomeLayoutProps {
|
|
9
|
+
readonly beforeHero?: React.ReactNode
|
|
10
|
+
readonly afterHero?: React.ReactNode
|
|
11
|
+
readonly beforeFeatures?: React.ReactNode
|
|
12
|
+
readonly afterFeatures?: React.ReactNode
|
|
13
|
+
readonly beforeHeroActions?: React.ReactNode
|
|
14
|
+
readonly afterHeroActions?: React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── Component ────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Custom HomeLayout override for zpress.
|
|
21
|
+
* Wraps the original Rspress HomeLayout and injects HomeWorkspaces
|
|
22
|
+
* into the afterFeatures slot.
|
|
23
|
+
*/
|
|
24
|
+
export function HomeLayout(props: HomeLayoutProps): React.ReactElement {
|
|
25
|
+
return (
|
|
26
|
+
<OriginalHomeLayout
|
|
27
|
+
{...props}
|
|
28
|
+
afterFeatures={
|
|
29
|
+
<>
|
|
30
|
+
{props.afterFeatures}
|
|
31
|
+
<HomeWorkspaces />
|
|
32
|
+
</>
|
|
33
|
+
}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { match, P } from 'ts-pattern'
|
|
3
|
+
|
|
4
|
+
import { useZpress } from '../../hooks/use-zpress'
|
|
5
|
+
import type { WorkspaceGroupData } from '../../hooks/use-zpress'
|
|
6
|
+
import { WorkspaceCard } from '../workspaces/card'
|
|
7
|
+
import { WorkspaceGrid } from '../workspaces/grid'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Smart orchestrator that reads workspace data from themeConfig
|
|
11
|
+
* and renders workspace groups with the correct card component per type.
|
|
12
|
+
*/
|
|
13
|
+
export function HomeWorkspaces(): React.ReactElement | null {
|
|
14
|
+
const { workspaces } = useZpress()
|
|
15
|
+
|
|
16
|
+
return match(workspaces)
|
|
17
|
+
.with(
|
|
18
|
+
P.when((w): w is readonly WorkspaceGroupData[] => Array.isArray(w) && w.length > 0),
|
|
19
|
+
(groups) => (
|
|
20
|
+
<div className="workspace-section">
|
|
21
|
+
<hr className="home-card-divider" />
|
|
22
|
+
{groups.map(renderGroup)}
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
.otherwise(() => null)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render a single workspace group.
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
function renderGroup(group: WorkspaceGroupData): React.ReactElement {
|
|
36
|
+
return (
|
|
37
|
+
<WorkspaceGrid key={group.heading} heading={group.heading} description={group.description}>
|
|
38
|
+
{group.cards.map((card, i) => (
|
|
39
|
+
<WorkspaceCard
|
|
40
|
+
key={`${card.text}-${i}`}
|
|
41
|
+
text={card.text}
|
|
42
|
+
href={card.href}
|
|
43
|
+
icon={card.icon}
|
|
44
|
+
iconColor={card.iconColor}
|
|
45
|
+
scope={card.scope}
|
|
46
|
+
description={card.description}
|
|
47
|
+
tags={card.tags}
|
|
48
|
+
badge={card.badge}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
</WorkspaceGrid>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branch tag pill — git branch badge positioned in the nav bar.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.branch-tag {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 5px;
|
|
9
|
+
margin-left: 8px;
|
|
10
|
+
padding: 3px 10px;
|
|
11
|
+
border-radius: 20px;
|
|
12
|
+
background: var(--zp-c-bg-soft);
|
|
13
|
+
border: 1px solid var(--zp-c-divider);
|
|
14
|
+
color: var(--zp-c-text-3);
|
|
15
|
+
font-family: var(--zp-font-family-mono);
|
|
16
|
+
font-size: 11px;
|
|
17
|
+
font-weight: 600;
|
|
18
|
+
line-height: 1;
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
max-width: 180px;
|
|
21
|
+
text-decoration: none !important;
|
|
22
|
+
transition:
|
|
23
|
+
border-color 0.2s,
|
|
24
|
+
color 0.2s;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.branch-tag:hover {
|
|
28
|
+
border-color: var(--zp-c-brand-1);
|
|
29
|
+
color: var(--zp-c-brand-1);
|
|
30
|
+
text-decoration: none !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.branch-tag:focus-visible {
|
|
34
|
+
outline: 2px solid var(--rp-c-brand);
|
|
35
|
+
outline-offset: 2px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.branch-tag svg {
|
|
39
|
+
width: 14px;
|
|
40
|
+
height: 14px;
|
|
41
|
+
flex-shrink: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.branch-tag-text {
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
text-overflow: ellipsis;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ── Responsive ──────────────────────────────────────────── */
|
|
50
|
+
@media (max-width: 768px) {
|
|
51
|
+
.branch-tag-text {
|
|
52
|
+
display: none;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import './branch-tag.css'
|
|
4
|
+
import { Icon } from '../shared/icon.tsx'
|
|
5
|
+
|
|
6
|
+
declare const __ZPRESS_GIT_BRANCH__: string | undefined
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the current git branch name from the build-time global,
|
|
10
|
+
* returning an empty string when undefined.
|
|
11
|
+
*/
|
|
12
|
+
function resolveBranch(): string {
|
|
13
|
+
if (__ZPRESS_GIT_BRANCH__ !== undefined) {
|
|
14
|
+
return __ZPRESS_GIT_BRANCH__
|
|
15
|
+
}
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Git branch tag — pill-shaped badge positioned in the nav bar.
|
|
21
|
+
* Hidden when on `main` (production). Uses the pixelarticons:git-branch icon.
|
|
22
|
+
*/
|
|
23
|
+
export function BranchTag(): React.ReactElement | null {
|
|
24
|
+
const branch = resolveBranch()
|
|
25
|
+
const rootRef = useRef<HTMLAnchorElement>(null)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!branch || branch === 'main' || !rootRef.current) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const node = rootRef.current
|
|
33
|
+
|
|
34
|
+
// Relocate into the search container so it sits beside the search button.
|
|
35
|
+
// No Rspress API exists for injecting into the nav bar, so direct DOM
|
|
36
|
+
// manipulation is the only option for this placement.
|
|
37
|
+
const searchContainer = document.querySelector('.rspress-nav-search')
|
|
38
|
+
if (searchContainer) {
|
|
39
|
+
searchContainer.append(node)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
node.remove()
|
|
44
|
+
}
|
|
45
|
+
}, [branch])
|
|
46
|
+
|
|
47
|
+
if (!branch || branch === 'main') {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<a
|
|
53
|
+
className="branch-tag"
|
|
54
|
+
ref={rootRef}
|
|
55
|
+
href={`https://github.com/joggrdocs/zpress/tree/${branch}`}
|
|
56
|
+
target="_blank"
|
|
57
|
+
rel="noopener noreferrer"
|
|
58
|
+
title={`Branch: ${branch}`}
|
|
59
|
+
>
|
|
60
|
+
<Icon icon="pixelarticons:git-branch" width={14} height={14} />
|
|
61
|
+
<span className="branch-tag-text">{branch}</span>
|
|
62
|
+
</a>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { BranchTag as default }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { match, P } from 'ts-pattern'
|
|
3
|
+
|
|
4
|
+
// ── Types ────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface CardProps {
|
|
7
|
+
readonly href?: string
|
|
8
|
+
readonly className?: string
|
|
9
|
+
readonly children: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── Component ────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shared base card handling link-vs-div rendering.
|
|
16
|
+
* Renders `<a>` with `home-card--clickable` when `href` is provided,
|
|
17
|
+
* plain `<div>` otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export function Card({ href, className, children }: CardProps): React.ReactElement {
|
|
20
|
+
return match(href)
|
|
21
|
+
.with(P.nonNullable, (h) => (
|
|
22
|
+
<a className={`home-card home-card--clickable ${className ?? ''}`} href={h}>
|
|
23
|
+
{children}
|
|
24
|
+
</a>
|
|
25
|
+
))
|
|
26
|
+
.otherwise(() => <div className={`home-card ${className ?? ''}`}>{children}</div>)
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import catppuccin from '@iconify-json/catppuccin/icons.json' with { type: 'json' }
|
|
2
|
+
import devicon from '@iconify-json/devicon/icons.json' with { type: 'json' }
|
|
3
|
+
import logos from '@iconify-json/logos/icons.json' with { type: 'json' }
|
|
4
|
+
import materialIconTheme from '@iconify-json/material-icon-theme/icons.json' with { type: 'json' }
|
|
5
|
+
import mdi from '@iconify-json/mdi/icons.json' with { type: 'json' }
|
|
6
|
+
import pixelarticons from '@iconify-json/pixelarticons/icons.json' with { type: 'json' }
|
|
7
|
+
import simpleIcons from '@iconify-json/simple-icons/icons.json' with { type: 'json' }
|
|
8
|
+
import skillIcons from '@iconify-json/skill-icons/icons.json' with { type: 'json' }
|
|
9
|
+
import vscodeIcons from '@iconify-json/vscode-icons/icons.json' with { type: 'json' }
|
|
10
|
+
import { addCollection, Icon } from '@iconify/react'
|
|
11
|
+
|
|
12
|
+
// Register all icon collections for offline Iconify resolution
|
|
13
|
+
function cast(v: unknown): Parameters<typeof addCollection>[0] {
|
|
14
|
+
return v as Parameters<typeof addCollection>[0]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const pixelarticonsLoaded = addCollection(cast(pixelarticons))
|
|
18
|
+
export const deviconLoaded = addCollection(cast(devicon))
|
|
19
|
+
export const mdiLoaded = addCollection(cast(mdi))
|
|
20
|
+
export const simpleIconsLoaded = addCollection(cast(simpleIcons))
|
|
21
|
+
export const skillIconsLoaded = addCollection(cast(skillIcons))
|
|
22
|
+
export const catppuccinLoaded = addCollection(cast(catppuccin))
|
|
23
|
+
export const logosLoaded = addCollection(cast(logos))
|
|
24
|
+
export const vscodeIconsLoaded = addCollection(cast(vscodeIcons))
|
|
25
|
+
export const materialIconThemeLoaded = addCollection(cast(materialIconTheme))
|
|
26
|
+
|
|
27
|
+
export { Icon }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { match, P } from 'ts-pattern'
|
|
3
|
+
|
|
4
|
+
import type { IconColor } from '../home/feature-card'
|
|
5
|
+
import { Card } from './card'
|
|
6
|
+
import { Icon } from './icon'
|
|
7
|
+
|
|
8
|
+
export interface SectionCardProps {
|
|
9
|
+
readonly href: string
|
|
10
|
+
readonly title: string
|
|
11
|
+
readonly description?: string
|
|
12
|
+
readonly icon?: string
|
|
13
|
+
readonly iconColor?: IconColor
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Section card — simple icon + title + description link card
|
|
18
|
+
* used on auto-generated section landing pages.
|
|
19
|
+
*/
|
|
20
|
+
export function SectionCard({
|
|
21
|
+
href,
|
|
22
|
+
title,
|
|
23
|
+
description,
|
|
24
|
+
icon = 'pixelarticons:file',
|
|
25
|
+
iconColor = 'purple',
|
|
26
|
+
}: SectionCardProps): React.ReactElement {
|
|
27
|
+
const descEl = match(description)
|
|
28
|
+
.with(P.nonNullable, (d) => <span className="section-desc">{d}</span>)
|
|
29
|
+
.otherwise(() => null)
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Card href={href} className="section-card">
|
|
33
|
+
<span className={`section-icon section-icon--${iconColor}`}>
|
|
34
|
+
<Icon icon={icon} />
|
|
35
|
+
</span>
|
|
36
|
+
<div className="section-info">
|
|
37
|
+
<span className="section-title">{title}</span>
|
|
38
|
+
{descEl}
|
|
39
|
+
</div>
|
|
40
|
+
</Card>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
3
|
+
export interface SectionGridProps {
|
|
4
|
+
readonly children: React.ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Grid container for section cards on landing pages.
|
|
9
|
+
*/
|
|
10
|
+
export function SectionGrid({ children }: SectionGridProps): React.ReactElement {
|
|
11
|
+
return <div className="section-grid">{children}</div>
|
|
12
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Icon } from './icon'
|
|
4
|
+
|
|
5
|
+
export interface TechIconEntry {
|
|
6
|
+
readonly tag: string
|
|
7
|
+
readonly icon: string
|
|
8
|
+
readonly label: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TechIconTableProps {
|
|
12
|
+
readonly entries: readonly TechIconEntry[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const tableStyle: React.CSSProperties = {
|
|
16
|
+
width: '100%',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const iconCellStyle: React.CSSProperties = {
|
|
20
|
+
width: 48,
|
|
21
|
+
textAlign: 'center' as const,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const iconWrapperStyle: React.CSSProperties = {
|
|
25
|
+
display: 'inline-flex',
|
|
26
|
+
alignItems: 'center',
|
|
27
|
+
justifyContent: 'center',
|
|
28
|
+
width: 36,
|
|
29
|
+
height: 36,
|
|
30
|
+
background: 'var(--zp-c-bg-icon)',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Renders a table of technology icons with tag, label, and identifier columns.
|
|
35
|
+
* Used in generated icon reference docs.
|
|
36
|
+
*/
|
|
37
|
+
export function TechIconTable({ entries }: TechIconTableProps): React.ReactElement {
|
|
38
|
+
return (
|
|
39
|
+
<table style={tableStyle}>
|
|
40
|
+
<thead>
|
|
41
|
+
<tr>
|
|
42
|
+
<th style={iconCellStyle} />
|
|
43
|
+
<th>Tag</th>
|
|
44
|
+
<th>Label</th>
|
|
45
|
+
<th>Identifier</th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
<tbody>
|
|
49
|
+
{entries.map((entry) => (
|
|
50
|
+
<tr key={entry.tag}>
|
|
51
|
+
<td style={iconCellStyle}>
|
|
52
|
+
<div style={iconWrapperStyle}>
|
|
53
|
+
<Icon icon={entry.icon} width={20} height={20} />
|
|
54
|
+
</div>
|
|
55
|
+
</td>
|
|
56
|
+
<td>
|
|
57
|
+
<code>{entry.tag}</code>
|
|
58
|
+
</td>
|
|
59
|
+
<td>{entry.label}</td>
|
|
60
|
+
<td>
|
|
61
|
+
<code>{entry.icon}</code>
|
|
62
|
+
</td>
|
|
63
|
+
</tr>
|
|
64
|
+
))}
|
|
65
|
+
</tbody>
|
|
66
|
+
</table>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { match, P } from 'ts-pattern'
|
|
3
|
+
|
|
4
|
+
import { TECH_ICONS } from '../../icons/index.ts'
|
|
5
|
+
import { Icon } from './icon'
|
|
6
|
+
|
|
7
|
+
export interface TechTagProps {
|
|
8
|
+
readonly name: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Technology tag — resolves a tech name to icon + label using the tech map.
|
|
13
|
+
* Falls back to raw name string if not in map.
|
|
14
|
+
*/
|
|
15
|
+
export function TechTag({ name }: TechTagProps): React.ReactElement {
|
|
16
|
+
const entry = (TECH_ICONS as Record<string, { readonly icon: string; readonly label: string }>)[
|
|
17
|
+
name
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
return match(entry)
|
|
21
|
+
.with(P.nonNullable, (e) => (
|
|
22
|
+
<span className="workspace-tag">
|
|
23
|
+
<Icon icon={e.icon} />
|
|
24
|
+
{` ${e.label}`}
|
|
25
|
+
</span>
|
|
26
|
+
))
|
|
27
|
+
.otherwise(() => <span className="workspace-tag">{name}</span>)
|
|
28
|
+
}
|