lupine.press 1.0.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/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # lupine.press
2
+
3
+ `lupine.press` is a lightweight, high-performance documentation site framework built on top of `lupine.web`. It provides a complete solution for rendering Markdown-based documentation websites with a responsive layout, sidebar navigation, and theming support.
4
+
5
+ It is designed to work seamlessly with the `lupine` ecosystem, powering documentation sites like the official LupineJS documentation.
6
+
7
+ ## Features
8
+
9
+ - **Responsive Layout**: Built-in `PressFrame` provides a standard documentation layout with a header, responsive sidebar, and content area.
10
+ - **Markdown Rendering**: Optimized for rendering content generated from Markdown files, including syntax highlighting and standard typography.
11
+ - **Sidebar Navigation**: Automatically generates a multi-level sidebar based on your configuration.
12
+ - **Theming**: Built-in support for multiple themes (e.g., light/dark mode) via `lupine.components` theme system.
13
+ - **Routing**: explicit integration with `PageRouter` for handling client-side navigation.
14
+ - **多语言支持**:自动扫描多语言目录的 markdown 文件,多语言显示切换。
15
+
16
+ ## Usage Guide
17
+
18
+ To use `lupine.press`, you typically set up a `lupine.web` application and configure it to use `PressPage` as the main route handler.
19
+
20
+ ### 1. Prerequisites
21
+
22
+ Ensure you have `lupine.web` and `lupine.components` installed in your project.
23
+
24
+ ### 2. Basic Setup
25
+
26
+ In your application entry point (e.g., `src/index.tsx`), you need to bind the necessary configurations and set up the router.
27
+
28
+ ```typescript
29
+ import { bindRouter, PageRouter, bindTheme, bindLang, setDefaultPageTitle } from 'lupine.components';
30
+ import { bindPressData, PressPage, pressThemes } from 'lupine.press';
31
+ import { markdownConfig } from './markdown-config'; // Your generated markdown data
32
+
33
+ // 1. Initialize core settings
34
+ bindLang('en', {}); // Set default language
35
+ bindTheme('light', pressThemes); // Bind themes (includes specific styles for press)
36
+ setDefaultPageTitle('My Documentation');
37
+
38
+ // 2. Bind documentation data
39
+ // markdownConfig is a dictionary containing HTML content and metadata generated from markdown files.
40
+ bindPressData(markdownConfig);
41
+
42
+ // 3. Configure Router
43
+ const pageRouter = new PageRouter();
44
+ // Route all requests to PressPage, which handles looking up content in markdownConfig
45
+ pageRouter.use('*', PressPage);
46
+
47
+ // 4. Start the application
48
+ bindRouter(pageRouter);
49
+ ```
50
+
51
+ ### 3. Data Structure (`markdownConfig`)
52
+
53
+ The `bindPressData` function expects a configuration object where keys are route paths (e.g., `/guide/started`) and values contain the content and metadata.
54
+
55
+ Typically, this data is generated at build time from your Markdown files.
56
+
57
+ ```typescript
58
+ export const markdownConfig = {
59
+ '/en/guide/started': {
60
+ html: '<h1>Getting Started</h1><p>...</p>', // Pre-rendered HTML content
61
+ data: {
62
+ title: 'Getting Started',
63
+ sidebar: [
64
+ // Sidebar configuration for this page context
65
+ { type: 'group', text: 'Guide', level: 0 },
66
+ { type: 'link', text: 'Installation', link: '/en/guide/install', level: 1 },
67
+ ],
68
+ },
69
+ headings: [{ level: 2, text: 'Prerequisites', id: 'prerequisites' }],
70
+ },
71
+ // ... other pages
72
+ };
73
+ ```
74
+
75
+ ## Architecture
76
+
77
+ - **`PressFrame`**: The main layout component. It handles the specific CSS and structure for a documentation site, ensuring the sidebar and content area scroll independently.
78
+ - **`PressPage`**: The "controller" component. It looks up the current URL in the bound `markdownConfig`, retrieves the corresponding HTML and metadata, and renders the `PressFrame` with the correct sidebar and content.
79
+ - **`pressLoad`**: A navigation utility to handle link clicks within the documentation, ensuring smooth client-side transitions.
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "lupine.press",
3
+ "version": "1.0.0",
4
+ "description": "A modern documentation generator for lupine.js",
5
+ "main": "src/index.ts",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "note": "echo 'build is not needed as the typescript code is supposed to be referred directly from other projects'",
9
+ "npm-publish": "npm publish --access public"
10
+ },
11
+ "dependencies": {
12
+ "lupine.web": "^1.0.0",
13
+ "lupine.components": "^1.0.0",
14
+ "lupine.api": "^1.0.0",
15
+ "marked": "^15.0.0",
16
+ "gray-matter": "^4.0.3"
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ export * from './lang-switcher';
2
+ export * from './press-header';
3
+ export * from './press-heading';
4
+ export * from './press-home';
5
+ export * from './press-layout';
6
+ export * from '../services/press-load';
7
+ export * from './press-sidemenu';
@@ -0,0 +1,38 @@
1
+ import { initializePage } from 'lupine.web';
2
+ import { PopupMenu } from 'lupine.components';
3
+ import langIcon from '../styles/lang.svg';
4
+ import { Svg } from 'lupine.components';
5
+
6
+ export const LangSwitcher = (props: { className?: string; currentLang: string; langs: any[] }) => {
7
+ const langs = props.langs || [];
8
+ const currentLabel = langs.find((l) => l.id === props.currentLang)?.text || 'Language';
9
+
10
+ const handleSelected = (text: string) => {
11
+ const lang = langs.find((l) => l.text === text);
12
+ if (lang && lang.id !== props.currentLang) {
13
+ let newPath = window.location.pathname;
14
+ const langIds = langs.map((l) => l.id).join('|');
15
+ const langRegex = new RegExp(`^/(${langIds})(\\/|$)`);
16
+
17
+ if (langRegex.test(newPath)) {
18
+ newPath = newPath.replace(langRegex, `/${lang.id}$2`);
19
+ } else {
20
+ newPath = `/${lang.id}${newPath === '/' ? '/' : newPath}`;
21
+ }
22
+ // window.location.href = newPath;
23
+ initializePage(newPath);
24
+ }
25
+ };
26
+
27
+ return (
28
+ <div class={['lang-switcher', props.className].join(' ')}>
29
+ <PopupMenu
30
+ list={langs.map((l) => l.text)}
31
+ defaultValue={currentLabel}
32
+ icon={<Svg>{langIcon}</Svg>}
33
+ handleSelected={handleSelected}
34
+ align='right'
35
+ />
36
+ </div>
37
+ );
38
+ };
@@ -0,0 +1,146 @@
1
+ import { CssProps, MediaQueryRange, MenuItemProps, PopupMenu, Svg, VNode } from 'lupine.components';
2
+ import { pressLoad } from '../services/press-load';
3
+ import { LayoutHome } from './press-home';
4
+ import { PageHeading } from './press-heading';
5
+ import menuIcon from '../styles/menu.svg';
6
+
7
+ export const PressContent = (props: {
8
+ children: VNode<any>;
9
+ isHome: boolean;
10
+ sidebar: any[];
11
+ headings: any[];
12
+ data: any;
13
+ }) => {
14
+ const css: CssProps = {
15
+ display: 'flex',
16
+ flex: 1,
17
+ maxWidth: '100vw',
18
+ margin: '0 auto',
19
+ width: '100%',
20
+ '.press-content': {
21
+ flex: 1,
22
+ padding: props.isHome ? '0' : '2rem 4rem',
23
+ width: '100%',
24
+ // maxWidth: isHome ? '100%' : '800px',
25
+ margin: props.isHome ? '0' : '0 auto',
26
+ minWidth: 0,
27
+ },
28
+ '.page-heading-container': {
29
+ width: '240px',
30
+ minWidth: '240px',
31
+ padding: '2rem 1rem',
32
+ position: 'sticky',
33
+ top: '64px',
34
+ maxHeight: 'calc(100vh - 64px)',
35
+ overflowY: 'auto',
36
+ alignSelf: 'flex-start', // Prevent stretching to full height
37
+ display: props.isHome || props.headings.length === 0 ? 'none' : 'block',
38
+ },
39
+ '.markdown-body': {
40
+ lineHeight: 1.6,
41
+ },
42
+ '.press-mobile-toc': {
43
+ display: 'none',
44
+ // border: '1px solid var(--press-border-color)',
45
+ borderRadius: '6px',
46
+ alignItems: 'center',
47
+ padding: '4px',
48
+ position: 'fixed',
49
+ top: '74px',
50
+ right: '7px',
51
+ zIndex: 90,
52
+ fontSize: '0.9rem',
53
+ cursor: 'pointer',
54
+ textTransform: 'uppercase',
55
+ backgroundColor: 'var(--primary-bg-color)',
56
+ },
57
+ '.press-mobile-sidebar': {
58
+ display: 'none',
59
+ borderRadius: '6px',
60
+ alignItems: 'center',
61
+ padding: '4px',
62
+ position: 'fixed',
63
+ top: '74px',
64
+ left: '7px',
65
+ zIndex: 90,
66
+ fontSize: '0.9rem',
67
+ cursor: 'pointer',
68
+ // can't put up level, otherwise it will override parent's same level selector
69
+ [MediaQueryRange.TabletBelow]: {
70
+ display: 'flex',
71
+ },
72
+ },
73
+ [MediaQueryRange.MobileBelow]: {
74
+ '.page-heading-container': {
75
+ display: 'none',
76
+ },
77
+ '.press-mobile-toc': { display: 'flex' },
78
+ },
79
+ };
80
+ return (
81
+ <main css={css}>
82
+ {!props.isHome && props.sidebar.length > 0 && (
83
+ <div class='press-mobile-sidebar'>
84
+ <PopupMenu
85
+ list={props.sidebar.map(
86
+ (item: any) =>
87
+ ({
88
+ text: item.text,
89
+ id: item.link || '',
90
+ url: item.link || '',
91
+ indent: item.level,
92
+ visible: item.type ? true : false, // Validating it's processed item
93
+ disabled: item.type === 'group',
94
+ bold: item.type === 'group',
95
+ } as MenuItemProps)
96
+ )}
97
+ defaultValue='Menu'
98
+ tips=''
99
+ width='max-content'
100
+ maxHeight='400px'
101
+ align='left'
102
+ handleSelected={(text: string, item: any) => {
103
+ if (item && item.url) pressLoad(item.url);
104
+ }}
105
+ noUpdateLabel={true}
106
+ icon={<Svg>{menuIcon}</Svg>}
107
+ ></PopupMenu>
108
+ </div>
109
+ )}
110
+
111
+ <main class='press-content'>
112
+ {props.isHome ? <LayoutHome data={props.data} /> : <article class='markdown-body'>{props.children}</article>}
113
+ </main>
114
+ <aside class='page-heading-container'>
115
+ <PageHeading headings={props.headings} />
116
+ </aside>
117
+ {!props.isHome && props.headings.length > 0 && (
118
+ <div class='press-mobile-toc'>
119
+ <PopupMenu
120
+ list={props.headings.map(
121
+ (h) =>
122
+ ({
123
+ text: h.text,
124
+ id: h.id,
125
+ indent: h.level - 2,
126
+ } as MenuItemProps)
127
+ )}
128
+ defaultValue='On this page'
129
+ tips=''
130
+ width='max-content'
131
+ maxHeight='300px'
132
+ align='right'
133
+ handleSelected={(text: string) => {
134
+ const cleanText = text.trim();
135
+ const heading = props.headings.find((h) => h.text === cleanText);
136
+ if (heading) {
137
+ document.getElementById(heading.id)?.scrollIntoView(true);
138
+ }
139
+ }}
140
+ noUpdateLabel={true}
141
+ ></PopupMenu>
142
+ </div>
143
+ )}
144
+ </main>
145
+ );
146
+ };
@@ -0,0 +1,80 @@
1
+ import { CssProps, Svg, ThemeSelector } from 'lupine.components';
2
+ import { LangSwitcher } from './lang-switcher';
3
+ import githubIcon from '../styles/github.svg';
4
+ import themeIcon from '../styles/theme.svg';
5
+
6
+ export type PageHeaderProps = {
7
+ title: string;
8
+ nav: any[];
9
+ langs: any[];
10
+ currentLang: string;
11
+ github?: {
12
+ url: string;
13
+ title: string;
14
+ };
15
+ };
16
+ export const PageHeader = (props: PageHeaderProps) => {
17
+ const css: CssProps = {
18
+ width: '100%',
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ padding: '0.75rem 2rem',
22
+ justifyContent: 'space-between',
23
+ '.press-navbar-left': {
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ '.title': {
27
+ fontWeight: 'bold',
28
+ fontSize: '1.2rem',
29
+ marginRight: '1rem',
30
+ },
31
+ '.nav': {
32
+ display: 'flex',
33
+ gap: '1rem',
34
+ },
35
+ },
36
+ '.press-navbar-right': {
37
+ display: 'flex',
38
+ alignItems: 'center',
39
+ gap: '1.25rem',
40
+ '.navbar-item': {
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ textDecoration: 'none',
44
+ transition: 'color 0.2s',
45
+ '&:hover': { color: 'var(--press-brand-color)' },
46
+ },
47
+ },
48
+ };
49
+ return (
50
+ <header css={css} class='press-navbar'>
51
+ <div class='press-navbar-left'>
52
+ <div class='title'>
53
+ <a href='/'>{props.title}</a>
54
+ </div>
55
+ <nav class='nav'>
56
+ {props.nav.map((item: any) => (
57
+ <a href={item.link}>{item.text}</a>
58
+ ))}
59
+ </nav>
60
+ </div>
61
+ <div class='press-navbar-right'>
62
+ {props.langs.length > 1 && (
63
+ <LangSwitcher className='navbar-item' currentLang={props.currentLang} langs={props.langs || []} />
64
+ )}
65
+ <ThemeSelector className='navbar-item' icon={<Svg>{themeIcon}</Svg>} noUpdateLabel={true} />
66
+ {props.github && props.github.url && (
67
+ <a
68
+ href={props.github.url}
69
+ target='_blank'
70
+ rel='noopener noreferrer'
71
+ class='navbar-item'
72
+ title={props.github.title}
73
+ >
74
+ <Svg>{githubIcon}</Svg>
75
+ </a>
76
+ )}
77
+ </div>
78
+ </header>
79
+ );
80
+ };
@@ -0,0 +1,46 @@
1
+ import { CssProps } from 'lupine.components';
2
+
3
+ export const PageHeading = (props: { headings: any[] }) => {
4
+ if (props.headings.length === 0) return null;
5
+
6
+ const css: CssProps = {
7
+ '&-title': {
8
+ fontWeight: 'bold',
9
+ fontSize: '0.8rem',
10
+ textTransform: 'uppercase',
11
+ letterSpacing: '0.05em',
12
+ color: 'var(--primary-color)',
13
+ marginBottom: '0.8rem',
14
+ },
15
+ '&-list': {
16
+ listStyle: 'none',
17
+ padding: 0,
18
+ margin: 0,
19
+ borderLeft: '1px solid var(--press-border-color)',
20
+ },
21
+ '&-item': {
22
+ padding: '0.2rem 0 0.2rem 1rem',
23
+ fontSize: '0.85rem',
24
+ '&.level-3': { paddingLeft: '2rem' },
25
+ a: {
26
+ display: 'block',
27
+ transition: 'color 0.2s',
28
+ whiteSpace: 'nowrap',
29
+ overflow: 'hidden',
30
+ textOverflow: 'ellipsis',
31
+ },
32
+ },
33
+ };
34
+ return (
35
+ <div css={css}>
36
+ <div class='&-title'>On this page</div>
37
+ <ul class='&-list'>
38
+ {props.headings.map((h) => (
39
+ <li class={`&-item level-${h.level}`}>
40
+ <a href={`#${h.id}`}>{h.text}</a>
41
+ </li>
42
+ ))}
43
+ </ul>
44
+ </div>
45
+ );
46
+ };
@@ -0,0 +1,109 @@
1
+ import { CssProps } from 'lupine.components';
2
+
3
+ export const LayoutHome = (props: { data: any }) => {
4
+ const { hero, features } = props.data || {};
5
+ const css: CssProps = {
6
+ '.&-hero': {
7
+ padding: '64px 32px',
8
+ textAlign: 'center',
9
+ display: 'flex',
10
+ flexDirection: 'column',
11
+ alignItems: 'center',
12
+ },
13
+ '.&-hero-name': {
14
+ fontSize: '56px',
15
+ lineHeight: '64px',
16
+ fontWeight: 'bold',
17
+ background: 'linear-gradient(135deg, var(--press-brand-color) 30%, #4facfe 100%)',
18
+ '-webkit-background-clip': 'text',
19
+ '-webkit-text-fill-color': 'transparent',
20
+ },
21
+ '.&-hero-text': {
22
+ fontSize: '56px',
23
+ lineHeight: '64px',
24
+ fontWeight: 'bold',
25
+ marginTop: '8px',
26
+ },
27
+ '.&-hero-tagline': {
28
+ fontSize: '24px',
29
+ lineHeight: '36px',
30
+ color: 'var(--secondary-color)',
31
+ marginTop: '24px',
32
+ maxWidth: '576px',
33
+ },
34
+ '.&-hero-actions': {
35
+ display: 'flex',
36
+ gap: '12px',
37
+ marginTop: '48px',
38
+ },
39
+ '.&-button': {
40
+ display: 'inline-block',
41
+ padding: '0 20px',
42
+ lineHeight: '38px',
43
+ borderRadius: '20px',
44
+ fontWeight: '600',
45
+ textDecoration: 'none',
46
+ fontSize: '14px',
47
+ },
48
+ '.&-button.brand': {
49
+ backgroundColor: 'var(--press-brand-color)',
50
+ color: '#fff',
51
+ },
52
+ '.&-button.alt': {
53
+ backgroundColor: 'var(--secondary-bg-color)',
54
+ color: 'var(--primary-color)',
55
+ border: '1px solid var(--press-border-color)',
56
+ },
57
+ '.&-features': {
58
+ display: 'grid',
59
+ gridTemplateColumns: 'repeat(auto-fit, minmax(256px, 1fr))',
60
+ gap: '24px',
61
+ padding: '48px 32px',
62
+ maxWidth: '1152px',
63
+ margin: '0 auto',
64
+ },
65
+ '.&-feature-card': {
66
+ backgroundColor: 'var(--secondary-bg-color)',
67
+ padding: '24px',
68
+ borderRadius: '12px',
69
+ border: '1px solid var(--press-border-color)',
70
+ },
71
+ '.&-feature-title': {
72
+ fontSize: '20px',
73
+ fontWeight: 'bold',
74
+ marginBottom: '8px',
75
+ },
76
+ '.&-feature-details': {
77
+ fontSize: '14px',
78
+ color: 'var(--secondary-color)',
79
+ lineHeight: '22px',
80
+ },
81
+ };
82
+
83
+ return (
84
+ <div css={css}>
85
+ <section class='&-hero'>
86
+ <h1 class='&-hero-name'>{hero.name}</h1>
87
+ <p class='&-hero-text'>{hero.text}</p>
88
+ <p class='&-hero-tagline'>{hero.tagline}</p>
89
+ <div class='&-hero-actions'>
90
+ {hero.actions?.map((action: any) => (
91
+ <a href={action.link} class={`&-button ${action.theme}`}>
92
+ {action.text}
93
+ </a>
94
+ ))}
95
+ </div>
96
+ </section>
97
+ {features && (
98
+ <section class='&-features'>
99
+ {features.map((feature: any) => (
100
+ <div class='&-feature-card'>
101
+ <h2 class='&-feature-title'>{feature.title}</h2>
102
+ <p class='&-feature-details'>{feature.details}</p>
103
+ </div>
104
+ ))}
105
+ </section>
106
+ )}
107
+ </div>
108
+ );
109
+ };
@@ -0,0 +1,46 @@
1
+ import { getCurrentLang } from 'lupine.web';
2
+ import { PressFrame } from '../frames/press-frame';
3
+ import { PageHeader } from './press-header';
4
+ import { PressSidemenu } from './press-sidemenu';
5
+ import { PressContent } from './press-content';
6
+
7
+ export const PressLayout = (props: {
8
+ children: any;
9
+ title: string;
10
+ nav: any[];
11
+ sidebar: any[];
12
+ lang: string;
13
+ langs?: any[];
14
+ data?: any;
15
+ headings?: any[];
16
+ sidemenuWidth?: string;
17
+ github?: { url: string; title: string };
18
+ }) => {
19
+ const isHome = props.data?.layout === 'home';
20
+ const headings = props.headings || [];
21
+ const currentLang = props.lang || getCurrentLang().langName;
22
+
23
+ const sidebar = props.sidebar || [];
24
+ const content = (
25
+ <PressContent sidebar={sidebar} isHome={isHome} headings={headings} data={props.data}>
26
+ {props.children}
27
+ </PressContent>
28
+ );
29
+ return (
30
+ <PressFrame
31
+ header={
32
+ <PageHeader
33
+ title={props.title}
34
+ nav={props.nav}
35
+ langs={props.langs || []}
36
+ currentLang={currentLang}
37
+ github={props.github}
38
+ />
39
+ }
40
+ sidemenu={<PressSidemenu sidebar={sidebar} />}
41
+ content={content}
42
+ hideSidemenu={isHome}
43
+ sidemenuWidth={props.sidemenuWidth}
44
+ />
45
+ );
46
+ };
@@ -0,0 +1,74 @@
1
+ import { CssProps } from 'lupine.components';
2
+ import { pressLoad } from '../services/press-load';
3
+
4
+ export const PressSidemenu = (props: { sidebar: any[] }) => {
5
+ const css: CssProps = {
6
+ width: '100%',
7
+ padding: '0 8px 8px',
8
+ height: 'max-content',
9
+ // overflowY: 'auto',
10
+ '&-item': {
11
+ marginBottom: '0.3rem',
12
+ display: 'block',
13
+ color: 'var(--text-color)',
14
+ textDecoration: 'none',
15
+ '&:hover': {
16
+ color: 'var(--primary-color)',
17
+ },
18
+ },
19
+ '&-group-title': {
20
+ fontWeight: 'bold',
21
+ marginTop: '0.5rem',
22
+ marginBottom: '0.5rem',
23
+ fontSize: '15px',
24
+ // color: 'var(--secondary-color)',
25
+ '&.group-level-0': {
26
+ marginTop: '1.5rem',
27
+ fontSize: '19px',
28
+ },
29
+ '&.group-level-1': {
30
+ marginTop: '0.75rem',
31
+ fontSize: '17px',
32
+ },
33
+ },
34
+ '&-active': {
35
+ color: 'var(--primary-color)',
36
+ fontWeight: 'bold',
37
+ },
38
+ };
39
+
40
+ // Expecting props.sidebar to be already flattened by parent
41
+ const flatList = props.sidebar || [];
42
+ const basePadding = 1; // rem
43
+
44
+ return (
45
+ <aside css={css}>
46
+ {flatList.map((item, index) => {
47
+ const style = { paddingLeft: `${item.level * basePadding}rem` };
48
+
49
+ if (item.type === 'group') {
50
+ return (
51
+ <div class={'&-group-title' + (' group-level-' + item.level)} style={style} key={index}>
52
+ {item.text}
53
+ </div>
54
+ );
55
+ } else {
56
+ return (
57
+ <a
58
+ class='&-item'
59
+ style={style}
60
+ href='javascript:void(0)'
61
+ onClick={() => {
62
+ pressLoad(item.link);
63
+ return false;
64
+ }}
65
+ key={index}
66
+ >
67
+ {item.text}
68
+ </a>
69
+ );
70
+ }
71
+ })}
72
+ </aside>
73
+ );
74
+ };
@@ -0,0 +1 @@
1
+ export * from './press-frame';