create-ngmd 0.0.1 → 0.0.2
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 +4 -0
- package/package.json +1 -1
- package/template/package.json +1 -0
- package/template/src/app/app.ts +4 -1
- package/template/src/app/components/page-footer.ts +1 -1
- package/template/src/app/components/toc.ts +29 -1
- package/template/src/app/layout-mode.service.ts +2 -2
- package/template/src/app/ui/index.ts +1 -1
- package/template/src/content/welcome.md +1 -1
- package/template/src/marked-extensions/ngmd-keywords.ts +5 -2
- package/template/src/styles.css +44 -0
package/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# create-ngmd
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/create-ngmd)
|
|
4
|
+
|
|
3
5
|
Scaffold a new [NgMd](https://github.com/erkamyaman/ngmd) docs project.
|
|
4
6
|
|
|
7
|
+
Live demo: [ngmd.netlify.app](https://ngmd.netlify.app)
|
|
8
|
+
|
|
5
9
|
```bash
|
|
6
10
|
pnpm create ngmd@latest my-docs
|
|
7
11
|
# or
|
package/package.json
CHANGED
package/template/package.json
CHANGED
package/template/src/app/app.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from 'lucide-angular';
|
|
15
15
|
import { ThemeService } from './theme';
|
|
16
16
|
import { LayoutMode } from './layout-mode.service';
|
|
17
|
+
import siteConfig from '../ngmd.config';
|
|
17
18
|
import { CommandPalette } from './components/command-palette';
|
|
18
19
|
import { Sidebar } from './components/sidebar';
|
|
19
20
|
import { Breadcrumb } from './components/breadcrumb';
|
|
@@ -114,7 +115,7 @@ import { MediaEnhancer } from './components/media-enhancer';
|
|
|
114
115
|
</button>
|
|
115
116
|
<span class="h-4 w-px bg-zinc-300/60 dark:bg-zinc-700/60"></span>
|
|
116
117
|
<a
|
|
117
|
-
href="
|
|
118
|
+
[href]="githubUrl"
|
|
118
119
|
target="_blank"
|
|
119
120
|
rel="noopener noreferrer"
|
|
120
121
|
class="rounded p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-900"
|
|
@@ -224,6 +225,8 @@ export class App implements OnInit {
|
|
|
224
225
|
readonly moonIcon = Moon;
|
|
225
226
|
readonly autoIcon = SunMoon;
|
|
226
227
|
|
|
228
|
+
readonly githubUrl = siteConfig.site.githubUrl;
|
|
229
|
+
|
|
227
230
|
readonly drawerOpen = signal(false);
|
|
228
231
|
|
|
229
232
|
private readonly url = toSignal(
|
|
@@ -7,7 +7,7 @@ import { pageMeta } from 'virtual:ngmd/page-meta';
|
|
|
7
7
|
import { navItems } from '../../ngmd.config';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Bottom-of-page
|
|
10
|
+
* Bottom-of-page frame shown under every docs route: previous/next sibling
|
|
11
11
|
* pages derived from `ngmd.config.ts`, an "Edit on GitHub" link, and the
|
|
12
12
|
* page's last-updated date (commit cs from `git log`, baked at build time
|
|
13
13
|
* via the page-meta vite plugin).
|
|
@@ -70,6 +70,12 @@ export class Toc implements AfterViewInit {
|
|
|
70
70
|
const el = document.getElementById(id);
|
|
71
71
|
if (el) {
|
|
72
72
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
73
|
+
// Force-activate the clicked id. The IntersectionObserver uses a
|
|
74
|
+
// `rootMargin: '0px 0px -70% 0px'` so only the top 30% of viewport
|
|
75
|
+
// counts as "in view"; the LAST heading can't reach that region if
|
|
76
|
+
// there isn't enough content below it, leaving scroll-spy stuck on
|
|
77
|
+
// an earlier heading. Setting active directly here bypasses that.
|
|
78
|
+
this.active.set(id);
|
|
73
79
|
// index.html has <base href="/">, so a relative `#frag` resolves to
|
|
74
80
|
// `/#frag` and strips the path. Pass the full path explicitly.
|
|
75
81
|
history.replaceState(
|
|
@@ -82,9 +88,13 @@ export class Toc implements AfterViewInit {
|
|
|
82
88
|
|
|
83
89
|
private scanWithRetry(attempt = 0): void {
|
|
84
90
|
if (typeof document === 'undefined' || attempt > 20) return;
|
|
91
|
+
// Prefer the markdown wrappers for content-driven pages; fall back to
|
|
92
|
+
// `main article` for TS-driven pages (components.page.ts, etc.) that
|
|
93
|
+
// render Angular templates directly without analog-markdown.
|
|
85
94
|
const content =
|
|
86
95
|
document.querySelector('main analog-markdown') ??
|
|
87
|
-
document.querySelector('main analog-markdown-route')
|
|
96
|
+
document.querySelector('main analog-markdown-route') ??
|
|
97
|
+
document.querySelector('main article');
|
|
88
98
|
if (!content || content.querySelectorAll('h2, h3').length === 0) {
|
|
89
99
|
setTimeout(() => this.scanWithRetry(attempt + 1), 50);
|
|
90
100
|
return;
|
|
@@ -127,5 +137,23 @@ export class Toc implements AfterViewInit {
|
|
|
127
137
|
{ rootMargin: '0px 0px -70% 0px', threshold: 0 },
|
|
128
138
|
);
|
|
129
139
|
nodes.forEach((node) => this.observer!.observe(node));
|
|
140
|
+
|
|
141
|
+
// Bottom-of-page guard: when the user scrolls within ~100px of the
|
|
142
|
+
// bottom of the document, force-activate the last heading. The
|
|
143
|
+
// IntersectionObserver alone can't reach this state because the last
|
|
144
|
+
// heading never enters the top 30% of the viewport if there's not
|
|
145
|
+
// enough content below it.
|
|
146
|
+
const last = nodes[nodes.length - 1];
|
|
147
|
+
const onScroll = () => {
|
|
148
|
+
const scrolled = window.innerHeight + window.scrollY;
|
|
149
|
+
const fullHeight = document.documentElement.scrollHeight;
|
|
150
|
+
if (scrolled >= fullHeight - 100) {
|
|
151
|
+
this.active.set(last.id);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
155
|
+
this.destroyRef.onDestroy(() =>
|
|
156
|
+
window.removeEventListener('scroll', onScroll),
|
|
157
|
+
);
|
|
130
158
|
}
|
|
131
159
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Injectable, signal } from '@angular/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Pages that want to render without the docs
|
|
5
|
-
* can flip this signal in their constructor. The app shell reads it.
|
|
4
|
+
* Pages that want to render without the docs site frame (sidebar, breadcrumb,
|
|
5
|
+
* TOC) can flip this signal in their constructor. The app shell reads it.
|
|
6
6
|
*/
|
|
7
7
|
@Injectable({ providedIn: 'root' })
|
|
8
8
|
export class LayoutMode {
|
|
@@ -21,7 +21,7 @@ import { NgmdTab, NgmdTabs } from './tabs';
|
|
|
21
21
|
import { NgmdVideo } from './video';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Spread into a page's `imports` to get every
|
|
24
|
+
* Spread into a page's `imports` to get every authoring component in one go:
|
|
25
25
|
* `imports: [...NgmdUi]`. For lighter pages, import only what you use.
|
|
26
26
|
*
|
|
27
27
|
* Not declared `as const`: Angular's standalone-component compiler needs
|
|
@@ -16,4 +16,4 @@ Edit the `nav` array in `src/ngmd.config.ts`. Sidebar, command palette, breadcru
|
|
|
16
16
|
|
|
17
17
|
## Authoring components
|
|
18
18
|
|
|
19
|
-
NgMd ships a small
|
|
19
|
+
NgMd ships a small authoring component library under `src/app/ui/`: callouts, alerts, cards, tabs (Spartan brain), pill rows, workflows, hero, and a code block with shiki highlighting. Compose them in `.page.ts` around your markdown.
|
|
@@ -22,8 +22,11 @@ interface NgmdKeywordToken extends Tokens.Generic {
|
|
|
22
22
|
url: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// `(?!\*)` after the leading `*` prevents matching the second `*` of a
|
|
26
|
+
// `**bold**` pair. `(?!\*)` after the keyword prevents matching the inside
|
|
27
|
+
// of `**Keyword**` (which would leave one stray `*` and one stray `**`).
|
|
28
|
+
const KEYWORD_RE = /^\*(?!\*)([A-Z][a-zA-Z0-9]+)\b(?!\*)/;
|
|
29
|
+
const HINT_RE = /\*(?!\*)[A-Z]/;
|
|
27
30
|
const warned = new Set<string>();
|
|
28
31
|
|
|
29
32
|
function lookup(keyword: string): string | undefined {
|
package/template/src/styles.css
CHANGED
|
@@ -8,6 +8,50 @@
|
|
|
8
8
|
animation-duration: 150ms;
|
|
9
9
|
animation-timing-function: ease;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/* Hero title animation. The gradient span slowly shifts horizontally so the
|
|
13
|
+
* fuchsia core appears to pulse through the text. Whole h1 fades up on
|
|
14
|
+
* first paint. Honours prefers-reduced-motion. */
|
|
15
|
+
@keyframes ngmd-hero-fade-in {
|
|
16
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
17
|
+
to { opacity: 1; transform: translateY(0); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@keyframes ngmd-hero-gradient-flow {
|
|
21
|
+
from { background-position: 200% 50%; }
|
|
22
|
+
to { background-position: -100% 50%; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ngmd-hero-fade {
|
|
26
|
+
animation: ngmd-hero-fade-in 600ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Hide hero words and the gradient line at first paint so SSR HTML doesn't
|
|
30
|
+
* flash before motion takes over. Motion sets opacity:1 + translateY(0)
|
|
31
|
+
* via inline styles, which override these defaults. */
|
|
32
|
+
.ngmd-hero-anim {
|
|
33
|
+
opacity: 0;
|
|
34
|
+
transform: translateY(0.5em);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@media (prefers-reduced-motion: reduce) {
|
|
38
|
+
.ngmd-hero-anim {
|
|
39
|
+
opacity: 1;
|
|
40
|
+
transform: none;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ngmd-hero-gradient {
|
|
45
|
+
background-size: 300% auto;
|
|
46
|
+
animation: ngmd-hero-gradient-flow 5s linear infinite;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@media (prefers-reduced-motion: reduce) {
|
|
50
|
+
.ngmd-hero-fade,
|
|
51
|
+
.ngmd-hero-gradient {
|
|
52
|
+
animation: none;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
11
55
|
@plugin '@tailwindcss/typography';
|
|
12
56
|
|
|
13
57
|
@variant dark (&:where(.dark, .dark *));
|