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.
|
|
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
|
-
|
|
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@
|
|
203
|
-
style={{ flex: 1, minWidth: 0, fontSize: '16px', padding: '0
|
|
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 =
|
|
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
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
<
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
);
|