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 CHANGED
@@ -1,7 +1,11 @@
1
1
  # create-ngmd
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/create-ngmd.svg)](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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ngmd",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Scaffold a NgMd Angular docs site. Run via `pnpm create ngmd@latest` / `npm create ngmd@latest` / `yarn create ngmd` / `bun create ngmd`.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -49,6 +49,7 @@
49
49
  "marked-highlight": "^2.2.1",
50
50
  "marked-mangle": "^1.1.10",
51
51
  "marked-shiki": "^1.2.1",
52
+ "motion": "^12.40.0",
52
53
  "postcss": "^8.5.3",
53
54
  "prismjs": "^1.29.0",
54
55
  "rxjs": "~7.8.0",
@@ -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="https://github.com"
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 chrome shown under every docs route: previous/next sibling
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 chrome (sidebar, breadcrumb, TOC)
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 chrome component in one go:
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 chrome 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.
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
- const KEYWORD_RE = /^\*([A-Z][a-zA-Z0-9]+)\b/;
26
- const HINT_RE = /\*[A-Z]/;
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 {
@@ -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 *));