@flamingo-stack/openframe-frontend-core 0.0.305 → 0.0.306-snapshot.20260622221324
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/dist/{chunk-DVVQY4VI.cjs → chunk-2YYAKVL7.cjs} +12 -12
- package/dist/{chunk-DVVQY4VI.cjs.map → chunk-2YYAKVL7.cjs.map} +1 -1
- package/dist/{chunk-AGJPN5RS.js → chunk-46KRPHHL.js} +2 -2
- package/dist/{chunk-TFEXHBJE.cjs → chunk-4RHOLPFU.cjs} +14 -14
- package/dist/{chunk-TFEXHBJE.cjs.map → chunk-4RHOLPFU.cjs.map} +1 -1
- package/dist/{chunk-SR6ZSDOP.cjs → chunk-4RI7S6ZD.cjs} +9 -9
- package/dist/{chunk-SR6ZSDOP.cjs.map → chunk-4RI7S6ZD.cjs.map} +1 -1
- package/dist/{chunk-EEVOKQGR.js → chunk-4YV5U7GA.js} +48 -28
- package/dist/chunk-4YV5U7GA.js.map +1 -0
- package/dist/{chunk-KCID66IW.js → chunk-5OFBD6EQ.js} +2 -2
- package/dist/{chunk-3NDFB23M.cjs → chunk-6TRTIHGW.cjs} +27 -2
- package/dist/chunk-6TRTIHGW.cjs.map +1 -0
- package/dist/{chunk-UN7RFOZI.cjs → chunk-AIHM4TT7.cjs} +5 -5
- package/dist/{chunk-UN7RFOZI.cjs.map → chunk-AIHM4TT7.cjs.map} +1 -1
- package/dist/{chunk-TKGSUJZF.cjs → chunk-CKFHYXSJ.cjs} +7 -7
- package/dist/{chunk-TKGSUJZF.cjs.map → chunk-CKFHYXSJ.cjs.map} +1 -1
- package/dist/{chunk-QHOX4JWP.js → chunk-E7FIV5LH.js} +2 -2
- package/dist/{chunk-UEGSCIAW.js → chunk-ER4CMF47.js} +26 -1
- package/dist/chunk-ER4CMF47.js.map +1 -0
- package/dist/{chunk-L3JKIAQX.cjs → chunk-J3YKVLQ5.cjs} +26 -26
- package/dist/{chunk-L3JKIAQX.cjs.map → chunk-J3YKVLQ5.cjs.map} +1 -1
- package/dist/{chunk-NG6YEOHW.js → chunk-N3YPIZBH.js} +2 -2
- package/dist/{chunk-VP4VKYR2.cjs → chunk-PLFQJ5E7.cjs} +37 -37
- package/dist/{chunk-VP4VKYR2.cjs.map → chunk-PLFQJ5E7.cjs.map} +1 -1
- package/dist/{chunk-KKSZQLGC.js → chunk-QPTJOLAP.js} +2 -2
- package/dist/{chunk-GC4CNBTS.js → chunk-QWMYOUGP.js} +2 -2
- package/dist/{chunk-7QHSLXTB.js → chunk-WSEK6W4B.js} +2 -2
- package/dist/{chunk-OYAALXG6.cjs → chunk-XXFATUTH.cjs} +70 -50
- package/dist/chunk-XXFATUTH.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/docs/use-document-tree.d.ts.map +1 -1
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/features/seo-editor-preview.d.ts.map +1 -1
- package/dist/components/index.cjs +172 -172
- package/dist/components/index.js +8 -8
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +23 -23
- package/dist/components/onboarding-guides/index.js +3 -3
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +60 -60
- package/dist/components/tickets/index.js +3 -3
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/dist/utils/index.cjs +4 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/seo-title.d.ts +15 -0
- package/dist/utils/seo-title.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/docs/use-document-tree.ts +73 -28
- package/src/components/features/seo-editor-preview.tsx +24 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/seo-title.ts +14 -0
- package/dist/chunk-3NDFB23M.cjs.map +0 -1
- package/dist/chunk-EEVOKQGR.js.map +0 -1
- package/dist/chunk-OYAALXG6.cjs.map +0 -1
- package/dist/chunk-UEGSCIAW.js.map +0 -1
- /package/dist/{chunk-AGJPN5RS.js.map → chunk-46KRPHHL.js.map} +0 -0
- /package/dist/{chunk-KCID66IW.js.map → chunk-5OFBD6EQ.js.map} +0 -0
- /package/dist/{chunk-QHOX4JWP.js.map → chunk-E7FIV5LH.js.map} +0 -0
- /package/dist/{chunk-NG6YEOHW.js.map → chunk-N3YPIZBH.js.map} +0 -0
- /package/dist/{chunk-KKSZQLGC.js.map → chunk-QPTJOLAP.js.map} +0 -0
- /package/dist/{chunk-GC4CNBTS.js.map → chunk-QWMYOUGP.js.map} +0 -0
- /package/dist/{chunk-7QHSLXTB.js.map → chunk-WSEK6W4B.js.map} +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the `seo_title` length budget.
|
|
3
|
+
*
|
|
4
|
+
* The `seo_title` is rendered as the page `<title>` VERBATIM — no brand suffix is
|
|
5
|
+
* appended to it (the `| <Brand>` suffix is kept only on the default/actual-title
|
|
6
|
+
* fallback). So this is the full rendered-title budget; search engines truncate
|
|
7
|
+
* page titles around 60 chars.
|
|
8
|
+
*
|
|
9
|
+
* This number lives in exactly ONE place. Consumers:
|
|
10
|
+
* - `SEOEditorPreview` — input `maxLength` + the too-long alert.
|
|
11
|
+
* - the hub — imports it (via `@flamingo-stack/openframe-frontend-core/utils`)
|
|
12
|
+
* for the AI generation prompt guidance and the DB CHECK value.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SEO_TITLE_MAX_LENGTH = 60;
|
|
15
|
+
//# sourceMappingURL=seo-title.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-title.d.ts","sourceRoot":"","sources":["../../src/utils/seo-title.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -22,6 +22,29 @@ function scrollToContent() {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* First displayable document inside a folder (depth-first): a direct non-Mermaid
|
|
27
|
+
* file, else the first doc found in a subfolder. Used so a folder WITHOUT a
|
|
28
|
+
* README shows its first child's content (mirroring how a README folder shows
|
|
29
|
+
* its README) instead of a blank panel or a redundant in-page listing — the
|
|
30
|
+
* sidebar tree is the directory browser. Returns null for a folder with no docs.
|
|
31
|
+
*/
|
|
32
|
+
function findFirstDocPath(folder: DocNode): string | null {
|
|
33
|
+
const children = folder.children ?? []
|
|
34
|
+
for (const child of children) {
|
|
35
|
+
if (child.type === 'file' && !child.path.toLowerCase().endsWith('.mmd')) {
|
|
36
|
+
return child.path
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const child of children) {
|
|
40
|
+
if (child.type === 'folder') {
|
|
41
|
+
const nested = findFirstDocPath(child)
|
|
42
|
+
if (nested) return nested
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
export interface UseDocumentTreeConfig {
|
|
26
49
|
/** API endpoint for fetching the document tree structure */
|
|
27
50
|
structureEndpoint: string
|
|
@@ -155,12 +178,24 @@ export function useDocumentTree(
|
|
|
155
178
|
const node = findDocNodeByPath(selectedPath, structure)
|
|
156
179
|
|
|
157
180
|
if (node && node.type === 'folder' && !node.hasReadme) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
181
|
+
// No-README folder has no body of its own — show its FIRST child doc
|
|
182
|
+
// (mirrors a README folder showing its README). selectedPath stays the
|
|
183
|
+
// folder, so the sidebar keeps it highlighted/expanded; the sidebar is
|
|
184
|
+
// the directory browser, so we render NO separate in-page listing.
|
|
185
|
+
const firstDocPath = findFirstDocPath(node)
|
|
186
|
+
if (!firstDocPath) {
|
|
187
|
+
// Genuinely empty folder — clear to the empty state.
|
|
188
|
+
lastFetchedPath.current = null
|
|
189
|
+
setError(null)
|
|
190
|
+
setContent(null)
|
|
191
|
+
setIsLoadingContent(false)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
pathToFetch = firstDocPath
|
|
195
|
+
} else if (node && node.type === 'folder' && node.hasReadme) {
|
|
163
196
|
pathToFetch = `${selectedPath}/${folderIndexFile}`
|
|
197
|
+
} else {
|
|
198
|
+
pathToFetch = selectedPath
|
|
164
199
|
}
|
|
165
200
|
}
|
|
166
201
|
|
|
@@ -256,6 +291,25 @@ export function useDocumentTree(
|
|
|
256
291
|
setContent(null)
|
|
257
292
|
return
|
|
258
293
|
}
|
|
294
|
+
// No-README FOLDER → stay silent: the content effect resolves it to
|
|
295
|
+
// the folder's first child, so the folder-path 404 from the direct-
|
|
296
|
+
// load speculative fetch is expected, not an error to flash.
|
|
297
|
+
const probe = findDocNodeByPath(
|
|
298
|
+
stripFolderIndexFromPath(path, folderIndexFile),
|
|
299
|
+
structure,
|
|
300
|
+
)
|
|
301
|
+
const probeIsNoReadmeFolder =
|
|
302
|
+
!!probe && probe.type === 'folder' && !probe.hasReadme
|
|
303
|
+
// Before the structure has loaded to classify the path (`structure`
|
|
304
|
+
// is [] in the speculative call's closure), only silence FOLDER-LIKE
|
|
305
|
+
// paths (no `.md`) — a genuinely missing `*.md` leaf must still error.
|
|
306
|
+
const preStructureFolderLike =
|
|
307
|
+
structure.length === 0 && !path.endsWith('.md')
|
|
308
|
+
if (probeIsNoReadmeFolder || preStructureFolderLike) {
|
|
309
|
+
setError(null)
|
|
310
|
+
setContent(null)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
259
313
|
setError(result.error || 'Documentation file not found')
|
|
260
314
|
setContent(null)
|
|
261
315
|
return
|
|
@@ -310,41 +364,32 @@ export function useDocumentTree(
|
|
|
310
364
|
// narrower `NavigationNode` row shape) pass their own node back without the
|
|
311
365
|
// cross-type `as` cast. Both DocNode and NavigationNode satisfy this.
|
|
312
366
|
const selectNode = useCallback((node: Pick<DocNode, 'id' | 'path' | 'type' | 'hasReadme'>) => {
|
|
367
|
+
// Expansion only: clicking a folder toggles its own subtree; clicking a file
|
|
368
|
+
// reveals its ancestor chain.
|
|
313
369
|
if (node.type === 'folder') {
|
|
314
370
|
setExpandedNodes(prev => {
|
|
315
371
|
if (prev.has(node.id)) {
|
|
316
372
|
const ancestorIds = getDocAncestorNodeIds(node.path)
|
|
317
|
-
ancestorIds.pop()
|
|
373
|
+
ancestorIds.pop() // collapse self, keep ancestors open
|
|
318
374
|
return new Set(ancestorIds)
|
|
319
|
-
} else {
|
|
320
|
-
return new Set(getDocAncestorNodeIds(node.path))
|
|
321
375
|
}
|
|
376
|
+
return new Set(getDocAncestorNodeIds(node.path))
|
|
322
377
|
})
|
|
323
|
-
|
|
324
|
-
if (node.hasReadme) {
|
|
325
|
-
lastFetchedPath.current = null
|
|
326
|
-
setSelectedPath(node.path)
|
|
327
|
-
window.history.pushState({}, '', `${normalizedBaseRoute}/${node.path}`)
|
|
328
|
-
setTimeout(() => {
|
|
329
|
-
scrollToContent()
|
|
330
|
-
}, 150)
|
|
331
|
-
} else {
|
|
332
|
-
setSelectedPath(node.path)
|
|
333
|
-
}
|
|
334
378
|
} else {
|
|
335
379
|
const lastSlash = node.path.lastIndexOf('/')
|
|
336
380
|
if (lastSlash > 0) {
|
|
337
|
-
|
|
338
|
-
setExpandedNodes(new Set(getDocAncestorNodeIds(parentPath)))
|
|
381
|
+
setExpandedNodes(new Set(getDocAncestorNodeIds(node.path.substring(0, lastSlash))))
|
|
339
382
|
}
|
|
340
|
-
|
|
341
|
-
lastFetchedPath.current = null
|
|
342
|
-
setSelectedPath(node.path)
|
|
343
|
-
window.history.pushState({}, '', `${normalizedBaseRoute}/${node.path}`)
|
|
344
|
-
setTimeout(() => {
|
|
345
|
-
scrollToContent()
|
|
346
|
-
}, 150)
|
|
347
383
|
}
|
|
384
|
+
|
|
385
|
+
// Every node is a navigable destination — a file shows its body, a README
|
|
386
|
+
// folder its README, a no-README folder its first child doc (the content
|
|
387
|
+
// effect resolves which). So selection + URL + scroll are identical for all
|
|
388
|
+
// node types; no per-type special-casing.
|
|
389
|
+
lastFetchedPath.current = null
|
|
390
|
+
setSelectedPath(node.path)
|
|
391
|
+
window.history.pushState({}, '', `${normalizedBaseRoute}/${node.path}`)
|
|
392
|
+
setTimeout(scrollToContent, 150)
|
|
348
393
|
}, [normalizedBaseRoute])
|
|
349
394
|
|
|
350
395
|
const navigateToDoc = useCallback((path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean }) => {
|
|
@@ -6,6 +6,9 @@ import { ConfidenceBadge } from '../features';
|
|
|
6
6
|
import { Globe, ExternalLink, Upload, X, Loader2, Sparkles } from 'lucide-react';
|
|
7
7
|
import { cn } from '../../utils';
|
|
8
8
|
import Image from '../../embed-shims/next-image';
|
|
9
|
+
// SSOT for the field cap (server-safe constant). The seo_title renders as the
|
|
10
|
+
// page <title> verbatim (no brand suffix), so this is the full ~60-char budget.
|
|
11
|
+
import { SEO_TITLE_MAX_LENGTH } from '../../utils/seo-title';
|
|
9
12
|
|
|
10
13
|
export interface SEOEditorPreviewProps {
|
|
11
14
|
// SEO fields - must be strings (not undefined)
|
|
@@ -68,6 +71,10 @@ export function SEOEditorPreview({
|
|
|
68
71
|
const [isUploading, setIsUploading] = useState(false);
|
|
69
72
|
const [fileInputRef, setFileInputRef] = useState<HTMLInputElement | null>(null);
|
|
70
73
|
|
|
74
|
+
// SEO title length state — alerts the editor when the title would render too long.
|
|
75
|
+
const seoTitleLength = (seoTitle || '').length;
|
|
76
|
+
const seoTitleTooLong = seoTitleLength > SEO_TITLE_MAX_LENGTH;
|
|
77
|
+
|
|
71
78
|
// Use fallback values if OG fields are empty
|
|
72
79
|
const displayTitle = seoTitle.trim() || title || 'Untitled';
|
|
73
80
|
const displayDescription = seoDescription.trim() || summary || 'No description';
|
|
@@ -124,9 +131,26 @@ export function SEOEditorPreview({
|
|
|
124
131
|
value={seoTitle || ''}
|
|
125
132
|
onChange={(e) => onSeoTitleChange(e.target.value)}
|
|
126
133
|
disabled={disabled}
|
|
134
|
+
maxLength={SEO_TITLE_MAX_LENGTH}
|
|
135
|
+
invalid={seoTitleTooLong}
|
|
127
136
|
placeholder="Enter SEO meta title..."
|
|
128
137
|
className="bg-ods-bg border-ods-border text-ods-text-primary"
|
|
129
138
|
/>
|
|
139
|
+
<div className="flex items-center justify-between gap-2">
|
|
140
|
+
<span className="text-[11px] text-ods-error font-['DM_Sans']">
|
|
141
|
+
{seoTitleTooLong
|
|
142
|
+
? `Too long — search engines may truncate this title (keep it ≤ ${SEO_TITLE_MAX_LENGTH})`
|
|
143
|
+
: ''}
|
|
144
|
+
</span>
|
|
145
|
+
<span
|
|
146
|
+
className={cn(
|
|
147
|
+
"text-[11px] font-['DM_Sans'] tabular-nums shrink-0",
|
|
148
|
+
seoTitleTooLong ? 'text-ods-error font-semibold' : 'text-ods-text-secondary'
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
{seoTitleLength}/{SEO_TITLE_MAX_LENGTH}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
130
154
|
{!seoTitle && title && (
|
|
131
155
|
<p className="text-[11px] text-ods-accent font-['DM_Sans']">
|
|
132
156
|
Auto-populated from title
|
package/src/utils/index.ts
CHANGED
|
@@ -26,6 +26,9 @@ export {
|
|
|
26
26
|
} from './ods-color-utils'
|
|
27
27
|
export { delay, generateRandomString, truncateString, deepClone, getSlackCommunityJoinUrl, serializeJsonLd } from './common'
|
|
28
28
|
export { getBaseUrl } from '../utils/cn'
|
|
29
|
+
// SEO title length budget — server-safe constant (SSOT). Consumed by the hub
|
|
30
|
+
// (prompt guidance + DB check value) and by SEOEditorPreview (input maxLength).
|
|
31
|
+
export { SEO_TITLE_MAX_LENGTH } from './seo-title'
|
|
29
32
|
|
|
30
33
|
export * from './platform-config'
|
|
31
34
|
export * from './os-platforms'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the `seo_title` length budget.
|
|
3
|
+
*
|
|
4
|
+
* The `seo_title` is rendered as the page `<title>` VERBATIM — no brand suffix is
|
|
5
|
+
* appended to it (the `| <Brand>` suffix is kept only on the default/actual-title
|
|
6
|
+
* fallback). So this is the full rendered-title budget; search engines truncate
|
|
7
|
+
* page titles around 60 chars.
|
|
8
|
+
*
|
|
9
|
+
* This number lives in exactly ONE place. Consumers:
|
|
10
|
+
* - `SEOEditorPreview` — input `maxLength` + the too-long alert.
|
|
11
|
+
* - the hub — imports it (via `@flamingo-stack/openframe-frontend-core/utils`)
|
|
12
|
+
* for the AI generation prompt guidance and the DB CHECK value.
|
|
13
|
+
*/
|
|
14
|
+
export const SEO_TITLE_MAX_LENGTH = 60;
|