jamdesk 1.1.145 → 1.1.147

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.145",
3
+ "version": "1.1.147",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -12,11 +12,21 @@ export interface NativeSubscribeFormProps {
12
12
  className?: string;
13
13
  }
14
14
 
15
+ // Shared control height for the email field + Subscribe button so they line up
16
+ // exactly. The input must carry 16px text (iOS no-zoom rule) which otherwise
17
+ // makes it taller than the smaller-font button; pinning both to one height with
18
+ // border-box collapses the mismatch. 2rem (32px) stays compact yet clears the
19
+ // WCAG 24px minimum target.
20
+ const CONTROL_HEIGHT = '2rem';
21
+
15
22
  // Solid primary button (the collapsed trigger and the form's submit share it).
16
23
  // Kept compact — the iOS 16px-min rule is for inputs, not buttons. Radius tracks
17
24
  // the theme's button token (--radius-md) so it matches the docs' own buttons.
25
+ // inline-flex centers the label inside the fixed CONTROL_HEIGHT box.
18
26
  const PRIMARY_BUTTON: CSSProperties = {
19
- fontSize: '0.875rem', padding: '0.4rem 0.85rem', border: 0,
27
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
28
+ height: CONTROL_HEIGHT, boxSizing: 'border-box',
29
+ fontSize: '0.8125rem', padding: '0 0.85rem', border: 0,
20
30
  borderRadius: 'var(--radius-md, 0.375rem)', cursor: 'pointer',
21
31
  background: 'var(--color-primary, #2563eb)', color: '#fff',
22
32
  };
@@ -189,6 +199,11 @@ export function NativeSubscribeForm({ provider, title, description, collapsed, c
189
199
  <Shell className={className}>
190
200
  {titleEl}
191
201
  {description && <p className="mb-3 text-sm text-theme-text-secondary">{description}</p>}
202
+ {/* The field keeps 16px text (iOS no-zoom), but the placeholder reads a
203
+ touch smaller — inline styles can't reach ::placeholder, so scope it by
204
+ the stable jd-emailsubscribe hook. Smaller ::placeholder font doesn't
205
+ re-trigger iOS zoom (that keys off the input's own font-size). */}
206
+ <style>{`.jd-emailsubscribe input[type="email"]::placeholder{font-size:0.875rem}`}</style>
192
207
  <form onSubmit={onSubmit} style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center', maxWidth: '28rem' }}>
193
208
  {/* Honeypot: a deliberately odd name (NOT website/url/email, which password
194
209
  managers autofill — a filled honeypot silently drops a real user). Bots
@@ -196,11 +211,12 @@ export function NativeSubscribeForm({ provider, title, description, collapsed, c
196
211
  <input type="text" name="jd_hp" tabIndex={-1} autoComplete="off" aria-hidden="true"
197
212
  defaultValue="" style={{ position: 'absolute', left: '-5000px' }} />
198
213
  {/* font-size:16px avoids iOS auto-zoom (monorepo UI rule). primary-bg field
199
- pops against the card's secondary-bg surface; radius tracks the theme. */}
214
+ pops against the card's secondary-bg surface; radius tracks the theme.
215
+ height+border-box matches the Subscribe button exactly. */}
200
216
  <input type="email" name="email" required value={email}
201
217
  onChange={(e) => { setEmail(e.target.value); if (status === 'error') setStatus('idle'); }}
202
- disabled={status === 'submitting'} placeholder="you@example.com" aria-label="Email address"
203
- style={{ flex: 1, minWidth: 0, fontSize: '16px', padding: '0.4rem 0.75rem', border: 'var(--border-width, 1px) solid var(--color-border, #d4d4d8)', borderRadius: 'var(--radius-md, 0.375rem)', background: 'var(--color-bg-primary, #fff)', color: 'var(--color-text-primary, #18181b)' }} />
218
+ disabled={status === 'submitting'} placeholder="you@email.com" aria-label="Email address"
219
+ style={{ flex: 1, minWidth: 0, height: CONTROL_HEIGHT, boxSizing: 'border-box', fontSize: '16px', padding: '0 0.7rem', border: 'var(--border-width, 1px) solid var(--color-border, #d4d4d8)', borderRadius: 'var(--radius-md, 0.375rem)', background: 'var(--color-bg-primary, #fff)', color: 'var(--color-text-primary, #18181b)' }} />
204
220
  <button type="submit" disabled={status === 'submitting'} style={PRIMARY_BUTTON}>
205
221
  {status === 'submitting' ? 'Subscribing…' : 'Subscribe'}
206
222
  </button>
@@ -1,4 +1,7 @@
1
+ 'use client';
2
+
1
3
  import { generateSlug } from '@/lib/heading-extractor';
4
+ import { useUpdateSlug } from './UpdateSlugContext';
2
5
 
3
6
  interface UpdateProps {
4
7
  label?: string;
@@ -12,6 +15,11 @@ interface UpdateProps {
12
15
  * Generates a URL-friendly slug from a label string.
13
16
  * Used to create anchor IDs for Update components.
14
17
  * Uses shared generateSlug to stay in sync with TOC and link validation.
18
+ *
19
+ * Stateless: two Updates with the same label produce the same slug here. The
20
+ * Update component itself resolves duplicate-label collisions via useUpdateSlug
21
+ * (page-scoped dedup); this export stays for the no-provider fallback, the RSS
22
+ * feed's parallel anchor algorithm, and link validation.
15
23
  */
16
24
  export function generateUpdateId(label?: string): string | undefined {
17
25
  if (!label) return undefined;
@@ -21,9 +29,13 @@ export function generateUpdateId(label?: string): string | undefined {
21
29
  /**
22
30
  * Update component - for changelog/whatsnew entries.
23
31
  * Creates timeline-style entries with automatic anchor links and TOC integration.
32
+ *
33
+ * Anchor id comes from useUpdateSlug so duplicate labels on one page (e.g. two
34
+ * "June 2026" entries) get distinct, TOC-aligned ids (`june-2026`,
35
+ * `june-2026-1`) instead of colliding on a single `#june-2026`.
24
36
  */
25
37
  export function Update({ label, description, tags, date, children }: UpdateProps) {
26
- const id = generateUpdateId(label);
38
+ const id = useUpdateSlug(label);
27
39
 
28
40
  return (
29
41
  <div
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useRef, ReactNode } from 'react';
4
+ import { generateSlug } from '@/lib/heading-extractor';
5
+
6
+ export interface UpdateSlugEntry {
7
+ label: string;
8
+ slug: string;
9
+ }
10
+
11
+ interface UpdateSlugContextValue {
12
+ // Per-label counter — advances each time useUpdateSlug is called for that label.
13
+ counts: Map<string, number>;
14
+ entries: UpdateSlugEntry[];
15
+ }
16
+
17
+ const UpdateSlugCtx = createContext<UpdateSlugContextValue | null>(null);
18
+
19
+ export function UpdateSlugProvider({
20
+ entries,
21
+ children,
22
+ }: {
23
+ entries: UpdateSlugEntry[];
24
+ children: ReactNode;
25
+ }) {
26
+ // useRef so the same Map persists across renders. We rely on the same
27
+ // contract React's own useId rests on: source-order rendering during SSR
28
+ // and during client hydration produces matching ids.
29
+ const ref = useRef<UpdateSlugContextValue | null>(null);
30
+ if (ref.current === null || ref.current.entries !== entries) {
31
+ ref.current = { counts: new Map(), entries };
32
+ }
33
+ return <UpdateSlugCtx.Provider value={ref.current}>{children}</UpdateSlugCtx.Provider>;
34
+ }
35
+
36
+ /**
37
+ * Resolve the unique slug for an <Update> with this label. Each call advances
38
+ * the per-label counter, so two Updates with the same label (e.g. two
39
+ * "June 2026" changelog entries) get sequential slugs from the provider's
40
+ * `entries` list (`june-2026`, `june-2026-1`) — matching the page-scoped
41
+ * github-slugger ids that extractHeadings (and the TOC) already compute. Falls
42
+ * back to a stateless `generateSlug(label)` if no provider is mounted
43
+ * (preview/storybook/etc), which preserves the pre-dedup single-Update behavior.
44
+ */
45
+ export function useUpdateSlug(label: string | undefined): string | undefined {
46
+ const ctx = useContext(UpdateSlugCtx);
47
+ if (!label) return undefined;
48
+ if (!ctx) return generateSlug(label) || undefined;
49
+
50
+ const idx = ctx.counts.get(label) ?? 0;
51
+ ctx.counts.set(label, idx + 1);
52
+
53
+ let seen = 0;
54
+ for (const entry of ctx.entries) {
55
+ if (entry.label !== label) continue;
56
+ if (seen === idx) return entry.slug;
57
+ seen += 1;
58
+ }
59
+ return generateSlug(label) || undefined;
60
+ }
@@ -15,6 +15,8 @@ export interface HeadingInfo {
15
15
  line: number; // 1-indexed
16
16
  /** When the entry is a <Step> inside a <Steps> block, this is its 1-based index within the block. */
17
17
  stepNumber?: number;
18
+ /** True when the entry came from an <Update label="..."> anchor (vs a markdown heading). */
19
+ isUpdate?: boolean;
18
20
  }
19
21
 
20
22
  /**
@@ -44,7 +46,13 @@ export function generateSlug(text: string): string {
44
46
 
45
47
  const HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
46
48
  const FENCE_REGEX = /^(`{3,}|~{3,})/;
47
- const UPDATE_LABEL_REGEX = /<Update\s+label=["']([^"']+)["']/;
49
+ // Global + matchAll (like STEP_TITLE_REGEX below) so multiple inline <Update>
50
+ // tags on one line each get an anchor, and `[^>]*` so `label=` need not be the
51
+ // first attribute (e.g. `<Update date=".." label="..">`). Alternation (not a
52
+ // character class) keeps an apostrophe inside a double-quoted label from
53
+ // terminating the capture. MUST only be used with matchAll — direct test/exec
54
+ // would share lastIndex across calls and miscount.
55
+ const UPDATE_LABEL_REGEX = /<Update\s+[^>]*label=(?:"([^"]+)"|'([^']+)')/g;
48
56
  const STEPS_OPEN_REGEX = /<Steps(\s|>)/;
49
57
  const STEPS_CLOSE_REGEX = /<\/Steps>/;
50
58
  // Global flag — we iterate with matchAll so authors who inline multiple
@@ -114,13 +122,11 @@ export function extractHeadings(content: string): HeadingInfo[] {
114
122
  }
115
123
  }
116
124
 
117
- const updateMatch = line.match(UPDATE_LABEL_REGEX);
118
- if (updateMatch) {
119
- const text = updateMatch[1];
120
- if (generateSlug(text)) {
121
- const id = slugger.slug(text);
122
- headings.push({ id, text, level: 2, line: i + 1 });
123
- }
125
+ for (const match of line.matchAll(UPDATE_LABEL_REGEX)) {
126
+ const text = match[1] ?? match[2];
127
+ if (!generateSlug(text)) continue;
128
+ const id = slugger.slug(text);
129
+ headings.push({ id, text, level: 2, line: i + 1, isUpdate: true });
124
130
  }
125
131
 
126
132
  if (inStepsBlock) {
@@ -56,6 +56,7 @@ import { recmaGuardExpressions } from '@/lib/recma-guard-expressions';
56
56
  import { extractInlineComponents } from '@/lib/process-mdx-with-exports';
57
57
  import { extractHeadings } from '@/lib/heading-extractor';
58
58
  import { StepSlugProvider, type StepSlugEntry } from '@/components/mdx/StepSlugContext';
59
+ import { UpdateSlugProvider, type UpdateSlugEntry } from '@/components/mdx/UpdateSlugContext';
59
60
  import { buildEndpointFromMdx } from '@/lib/build-endpoint-from-mdx';
60
61
  import { mdxSecurityOptions } from '@/lib/mdx-security-options';
61
62
  import { getContentDir } from '@/lib/docs';
@@ -365,9 +366,16 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
365
366
  logger.warn(`[MDX] preprocess failed for ${pagePath}: ${preprocessError}`);
366
367
  }
367
368
 
368
- const stepEntries: StepSlugEntry[] = extractHeadings(content)
369
+ // Both lists are derived from the SAME page-scoped slugger pass (extractHeadings),
370
+ // so Step and Update ids share one dedup namespace and never collide with each
371
+ // other or with markdown heading ids.
372
+ const pageHeadings = extractHeadings(content);
373
+ const stepEntries: StepSlugEntry[] = pageHeadings
369
374
  .filter(h => typeof h.stepNumber === 'number')
370
375
  .map(h => ({ title: h.text, slug: h.id }));
376
+ const updateEntries: UpdateSlugEntry[] = pageHeadings
377
+ .filter(h => h.isUpdate)
378
+ .map(h => ({ label: h.text, slug: h.id }));
371
379
 
372
380
  // [render-timing] proves the snippet/inline parallel win. With Promise.all,
373
381
  // wall-clock should be ~max(R2_snippets, CPU_inline). Compare against the
@@ -754,17 +762,19 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
754
762
  ) : (
755
763
  <MdxRenderBoundary fallback={<MdxErrorBlock label="page content" />}>
756
764
  <StepSlugProvider entries={stepEntries}>
757
- <MDXRemote
758
- source={effectiveSource}
759
- components={ComponentsWithUnknownFallback}
760
- options={{
761
- ...mdxSecurityOptions,
762
- mdxOptions: {
763
- ...getCommonMdxOptions(config, highlighter),
764
- recmaPlugins: [recmaCompoundComponents, recmaGuardExpressions],
765
- },
766
- }}
767
- />
765
+ <UpdateSlugProvider entries={updateEntries}>
766
+ <MDXRemote
767
+ source={effectiveSource}
768
+ components={ComponentsWithUnknownFallback}
769
+ options={{
770
+ ...mdxSecurityOptions,
771
+ mdxOptions: {
772
+ ...getCommonMdxOptions(config, highlighter),
773
+ recmaPlugins: [recmaCompoundComponents, recmaGuardExpressions],
774
+ },
775
+ }}
776
+ />
777
+ </UpdateSlugProvider>
768
778
  </StepSlugProvider>
769
779
  </MdxRenderBoundary>
770
780
  )}
@@ -784,17 +794,19 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
784
794
  ) : (
785
795
  <MdxRenderBoundary fallback={<MdxErrorBlock label="page content" />}>
786
796
  <StepSlugProvider entries={stepEntries}>
787
- <MDXRemote
788
- source={content}
789
- components={ComponentsWithUnknownFallback}
790
- options={{
791
- ...mdxSecurityOptions,
792
- mdxOptions: {
793
- ...getCommonMdxOptions(config, highlighter),
794
- recmaPlugins: [recmaCompoundComponents, recmaGuardExpressions],
795
- },
796
- }}
797
- />
797
+ <UpdateSlugProvider entries={updateEntries}>
798
+ <MDXRemote
799
+ source={content}
800
+ components={ComponentsWithUnknownFallback}
801
+ options={{
802
+ ...mdxSecurityOptions,
803
+ mdxOptions: {
804
+ ...getCommonMdxOptions(config, highlighter),
805
+ recmaPlugins: [recmaCompoundComponents, recmaGuardExpressions],
806
+ },
807
+ }}
808
+ />
809
+ </UpdateSlugProvider>
798
810
  </StepSlugProvider>
799
811
  </MdxRenderBoundary>
800
812
  );