@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Joggr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import node_path from "node:path";
|
|
4
|
+
function zpressPlugin() {
|
|
5
|
+
const componentsDir = node_path.resolve(import.meta.dirname, 'theme', 'components');
|
|
6
|
+
return {
|
|
7
|
+
name: 'zpress',
|
|
8
|
+
globalUIComponents: [
|
|
9
|
+
node_path.resolve(componentsDir, 'nav', 'branch-tag.tsx')
|
|
10
|
+
]
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function loadGenerated(contentDir, name, fallback) {
|
|
14
|
+
const p = node_path.resolve(contentDir, '.generated', name);
|
|
15
|
+
if (!existsSync(p)) {
|
|
16
|
+
process.stderr.write(`[zpress] Generated file not found: ${name} — run "zpress sync" first\n`);
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
function detectGitBranch() {
|
|
22
|
+
try {
|
|
23
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
stdio: 'pipe'
|
|
26
|
+
}).trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function createRspressConfig(options) {
|
|
32
|
+
const { config, paths } = options;
|
|
33
|
+
const sidebar = loadGenerated(paths.contentDir, 'sidebar.json', {});
|
|
34
|
+
const nav = loadGenerated(paths.contentDir, 'nav.json', []);
|
|
35
|
+
const workspaces = loadGenerated(paths.contentDir, 'workspaces.json', []);
|
|
36
|
+
const gitBranch = detectGitBranch();
|
|
37
|
+
return {
|
|
38
|
+
root: paths.contentDir,
|
|
39
|
+
outDir: paths.distDir,
|
|
40
|
+
llms: true,
|
|
41
|
+
title: config.title ?? 'zpress',
|
|
42
|
+
description: config.description ?? 'Documentation',
|
|
43
|
+
logo: '/logo.svg',
|
|
44
|
+
logoText: '',
|
|
45
|
+
themeDir: node_path.resolve(import.meta.dirname, 'theme'),
|
|
46
|
+
plugins: [
|
|
47
|
+
zpressPlugin()
|
|
48
|
+
],
|
|
49
|
+
builderConfig: {
|
|
50
|
+
resolve: {
|
|
51
|
+
alias: {
|
|
52
|
+
'@zpress/ui/theme': node_path.resolve(import.meta.dirname, 'theme', 'index.tsx')
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
source: {
|
|
56
|
+
define: {
|
|
57
|
+
__ZPRESS_GIT_BRANCH__: JSON.stringify(gitBranch)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
output: {
|
|
61
|
+
distPath: {
|
|
62
|
+
root: paths.distDir
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
themeConfig: {
|
|
67
|
+
sidebar,
|
|
68
|
+
nav,
|
|
69
|
+
darkMode: true,
|
|
70
|
+
search: true,
|
|
71
|
+
workspaces
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export { createRspressConfig, zpressPlugin };
|
|
@@ -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
|
+
}
|