@flamingo-stack/openframe-frontend-core 0.0.292 → 0.0.293
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-6FHO73AP.js → chunk-26PKDALD.js} +7 -79
- package/dist/chunk-26PKDALD.js.map +1 -0
- package/dist/{chunk-B2U6INNO.js → chunk-2U2M2TG2.js} +4 -4
- package/dist/{chunk-OXOTKEYY.cjs → chunk-4W7NYJ3B.cjs} +23 -23
- package/dist/{chunk-OXOTKEYY.cjs.map → chunk-4W7NYJ3B.cjs.map} +1 -1
- package/dist/{chunk-KBKZYJRI.cjs → chunk-5E2HOSSH.cjs} +66 -19
- package/dist/chunk-5E2HOSSH.cjs.map +1 -0
- package/dist/{chunk-PZZGDS5I.cjs → chunk-6G2INVGG.cjs} +24 -22
- package/dist/chunk-6G2INVGG.cjs.map +1 -0
- package/dist/{chunk-CUQH4SHH.js → chunk-6GCI7JOE.js} +2 -2
- package/dist/{chunk-N6ZM5PYZ.js → chunk-7RIYT7ZH.js} +49 -2
- package/dist/chunk-7RIYT7ZH.js.map +1 -0
- package/dist/{chunk-E2YXRSDG.js → chunk-BRNHX6C6.js} +15 -13
- package/dist/chunk-BRNHX6C6.js.map +1 -0
- package/dist/{chunk-5FK7X3EE.js → chunk-DOMJSNXW.js} +8 -7
- package/dist/chunk-DOMJSNXW.js.map +1 -0
- package/dist/{chunk-VK4B6UGU.js → chunk-E4XABBSU.js} +16 -8
- package/dist/{chunk-VK4B6UGU.js.map → chunk-E4XABBSU.js.map} +1 -1
- package/dist/{chunk-DUIWR7RQ.js → chunk-EJXHZX2E.js} +3 -3
- package/dist/{chunk-5PELVUFT.cjs → chunk-EYEW6PTA.cjs} +44 -36
- package/dist/chunk-EYEW6PTA.cjs.map +1 -0
- package/dist/{chunk-HTYUZXQP.js → chunk-FJDPUPXC.js} +5 -5
- package/dist/{chunk-SLP4KXP6.js → chunk-FQJK446R.js} +8 -2
- package/dist/chunk-FQJK446R.js.map +1 -0
- package/dist/{chunk-JC5RN7ZS.cjs → chunk-FT4FCV7L.cjs} +6 -6
- package/dist/{chunk-JC5RN7ZS.cjs.map → chunk-FT4FCV7L.cjs.map} +1 -1
- package/dist/{chunk-ZHNL2IPK.cjs → chunk-J54Z3OCR.cjs} +8 -2
- package/dist/chunk-J54Z3OCR.cjs.map +1 -0
- package/dist/{chunk-2NJ44RTT.cjs → chunk-JSOMFVEV.cjs} +30 -30
- package/dist/{chunk-2NJ44RTT.cjs.map → chunk-JSOMFVEV.cjs.map} +1 -1
- package/dist/{chunk-Z6BK4XHH.cjs → chunk-KXCRGTRN.cjs} +10 -82
- package/dist/chunk-KXCRGTRN.cjs.map +1 -0
- package/dist/{chunk-5KD3S25X.cjs → chunk-LFGGF7OT.cjs} +139 -2
- package/dist/chunk-LFGGF7OT.cjs.map +1 -0
- package/dist/{chunk-N45M3TK3.js → chunk-NSPOYUBH.js} +2 -2
- package/dist/{chunk-TYZEMPPH.js → chunk-OQ6X7ZOC.js} +138 -1
- package/dist/chunk-OQ6X7ZOC.js.map +1 -0
- package/dist/{chunk-MDLWEJAV.cjs → chunk-RJL6PIOK.cjs} +454 -453
- package/dist/chunk-RJL6PIOK.cjs.map +1 -0
- package/dist/{chunk-IXDTNQF4.js → chunk-SOJCR63T.js} +4 -4
- package/dist/{chunk-5R5OODNE.cjs → chunk-TYMUKFP2.cjs} +40 -40
- package/dist/{chunk-5R5OODNE.cjs.map → chunk-TYMUKFP2.cjs.map} +1 -1
- package/dist/{chunk-CDJOKNCS.cjs → chunk-VTY7S2QG.cjs} +25 -19
- package/dist/chunk-VTY7S2QG.cjs.map +1 -0
- package/dist/{chunk-FFP2A77V.cjs → chunk-X3TSMCKX.cjs} +12 -12
- package/dist/{chunk-FFP2A77V.cjs.map → chunk-X3TSMCKX.cjs.map} +1 -1
- package/dist/{chunk-C667P6LZ.js → chunk-YICTMMXP.js} +13 -7
- package/dist/{chunk-C667P6LZ.js.map → chunk-YICTMMXP.js.map} +1 -1
- package/dist/{chunk-2BMVBPC7.cjs → chunk-YIGPRLQY.cjs} +9 -9
- package/dist/{chunk-2BMVBPC7.cjs.map → chunk-YIGPRLQY.cjs.map} +1 -1
- package/dist/components/chat/entity-cards/roadmap-card.d.ts +7 -1
- package/dist/components/chat/entity-cards/roadmap-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +7 -7
- package/dist/components/chat/index.js +6 -6
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/docs/use-document-tree.d.ts.map +1 -1
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/embeds/index.js +7 -7
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +8 -8
- package/dist/components/faq/index.js +7 -7
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/features/index.js +6 -6
- package/dist/components/index.cjs +214 -193
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +50 -29
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +24 -24
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +8 -8
- package/dist/components/related-content/index.js +7 -7
- package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -1
- package/dist/components/shared/delivery/delivery-row.d.ts +8 -1
- package/dist/components/shared/delivery/delivery-row.d.ts.map +1 -1
- package/dist/components/shared/delivery/delivery-table.d.ts.map +1 -1
- package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -1
- package/dist/components/shared/roadmap/roadmap-view.d.ts.map +1 -1
- package/dist/components/tickets/help-center-card.d.ts +7 -1
- package/dist/components/tickets/help-center-card.d.ts.map +1 -1
- package/dist/components/tickets/help-center-list.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +82 -73
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +24 -15
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/tickets/ticket-center.d.ts.map +1 -1
- package/dist/components/tickets/ticket-row.d.ts +6 -1
- package/dist/components/tickets/ticket-row.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +7 -7
- package/dist/components/ui/index.js +6 -6
- package/dist/hooks/index.cjs +5 -3
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +4 -2
- package/dist/hooks/use-scroll-to-hash.d.ts +17 -0
- package/dist/hooks/use-scroll-to-hash.d.ts.map +1 -0
- package/dist/index.cjs +19 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -7
- package/dist/utils/dev-sections/dev-section-param-keys.d.ts +10 -0
- package/dist/utils/dev-sections/dev-section-param-keys.d.ts.map +1 -1
- package/dist/utils/index.cjs +71 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +67 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/same-page-hash-nav.d.ts +37 -0
- package/dist/utils/same-page-hash-nav.d.ts.map +1 -0
- package/dist/utils/source-icons.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/roadmap-card.tsx +8 -1
- package/src/components/docs/use-document-tree.ts +45 -3
- package/src/components/faq/faq-section.tsx +22 -9
- package/src/components/shared/delivery/delivery-lists.tsx +9 -0
- package/src/components/shared/delivery/delivery-row.tsx +15 -2
- package/src/components/shared/delivery/delivery-table.tsx +7 -1
- package/src/components/shared/roadmap/roadmap-grid.tsx +7 -0
- package/src/components/shared/roadmap/roadmap-view.tsx +11 -0
- package/src/components/tickets/help-center-card.tsx +9 -17
- package/src/components/tickets/help-center-list.tsx +13 -0
- package/src/components/tickets/ticket-center.tsx +2 -0
- package/src/components/tickets/ticket-row.tsx +7 -1
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-scroll-to-hash.ts +74 -0
- package/src/utils/.source-icons.md +1 -1
- package/src/utils/dev-sections/dev-section-param-keys.ts +14 -0
- package/src/utils/index.ts +17 -1
- package/src/utils/same-page-hash-nav.ts +115 -0
- package/src/utils/source-icons.ts +7 -1
- package/dist/chunk-5FK7X3EE.js.map +0 -1
- package/dist/chunk-5KD3S25X.cjs.map +0 -1
- package/dist/chunk-5PELVUFT.cjs.map +0 -1
- package/dist/chunk-6FHO73AP.js.map +0 -1
- package/dist/chunk-CDJOKNCS.cjs.map +0 -1
- package/dist/chunk-E2YXRSDG.js.map +0 -1
- package/dist/chunk-KBKZYJRI.cjs.map +0 -1
- package/dist/chunk-MDLWEJAV.cjs.map +0 -1
- package/dist/chunk-N6ZM5PYZ.js.map +0 -1
- package/dist/chunk-PZZGDS5I.cjs.map +0 -1
- package/dist/chunk-SLP4KXP6.js.map +0 -1
- package/dist/chunk-TYZEMPPH.js.map +0 -1
- package/dist/chunk-Z6BK4XHH.cjs.map +0 -1
- package/dist/chunk-ZHNL2IPK.cjs.map +0 -1
- /package/dist/{chunk-B2U6INNO.js.map → chunk-2U2M2TG2.js.map} +0 -0
- /package/dist/{chunk-CUQH4SHH.js.map → chunk-6GCI7JOE.js.map} +0 -0
- /package/dist/{chunk-DUIWR7RQ.js.map → chunk-EJXHZX2E.js.map} +0 -0
- /package/dist/{chunk-HTYUZXQP.js.map → chunk-FJDPUPXC.js.map} +0 -0
- /package/dist/{chunk-N45M3TK3.js.map → chunk-NSPOYUBH.js.map} +0 -0
- /package/dist/{chunk-IXDTNQF4.js.map → chunk-SOJCR63T.js.map} +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Pages with a section-nav STRIP on top of the global hub header
|
|
2
|
+
* (dev-center roadmap/delivery/tickets, FAQ category-pill nav).
|
|
3
|
+
* Anchor lands BELOW both layers. */
|
|
4
|
+
export declare const STICKY_HEADER_OFFSET_PX = 96;
|
|
5
|
+
/** Pages with only the global hub header (docs, blog, vendor detail).
|
|
6
|
+
* Anchor lands BELOW the header bar. */
|
|
7
|
+
export declare const HUB_HEADER_OFFSET_PX = 80;
|
|
8
|
+
/**
|
|
9
|
+
* Take only the FIRST hash segment from a fragment that may contain extra
|
|
10
|
+
* `#` characters. `'' → ''`, `'#a' → '#a'`, `'#a#b' → '#a'`.
|
|
11
|
+
*
|
|
12
|
+
* No real DOM id contains `#`, so a multi-fragment hash is always a bug at
|
|
13
|
+
* the composer site; `navigateSamePageHash` + `useScrollToHash` both call
|
|
14
|
+
* this so URL bar and `getElementById` stay in sync.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalizeHashFragment(hash: string): string;
|
|
17
|
+
export interface NavigateSamePageHashOptions {
|
|
18
|
+
/** Pixels to subtract for sticky chrome. */
|
|
19
|
+
headerOffset?: number;
|
|
20
|
+
/** `'push'` (default) — new history entry; `'replace'` — overwrite
|
|
21
|
+
* current entry (use for TOC-style in-page navigators). */
|
|
22
|
+
history?: 'push' | 'replace';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Same-page hash navigation primitive: pushState + synthetic `hashchange`
|
|
26
|
+
* + anchoring-proof smooth scroll. Replaces `router.push` for hash CTAs
|
|
27
|
+
* (Next.js suppresses smooth-scroll during navigation; `router.push` on
|
|
28
|
+
* an exact-URL match is a no-op). Returns `true` when the helper claimed
|
|
29
|
+
* the nav (same pathname + search); `false` for cross-page targets so
|
|
30
|
+
* callers fall through to `router.push`.
|
|
31
|
+
*
|
|
32
|
+
* `target` accepts an origin-stripped path (`/x#anchor`) or a bare hash
|
|
33
|
+
* (`#anchor`); bare-hash callers don't need to reconstruct `pathname +
|
|
34
|
+
* search` themselves.
|
|
35
|
+
*/
|
|
36
|
+
export declare function navigateSamePageHash(target: string, options?: NavigateSamePageHashOptions): boolean;
|
|
37
|
+
//# sourceMappingURL=same-page-hash-nav.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"same-page-hash-nav.d.ts","sourceRoot":"","sources":["../../src/utils/same-page-hash-nav.ts"],"names":[],"mappings":"AAEA;;sCAEsC;AACtC,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC;yCACyC;AACzC,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED,MAAM,WAAW,2BAA2B;IAC1C,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;gEAC4D;IAC5D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAkET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-icons.d.ts","sourceRoot":"","sources":["../../src/utils/source-icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA2CpD,CAAA;AAED;;6DAE6D;AAC7D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGxF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"source-icons.d.ts","sourceRoot":"","sources":["../../src/utils/source-icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA2CpD,CAAA;AAED;;6DAE6D;AAC7D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGxF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAiDzD,CAAA;AAED;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA2CpE,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEjF"}
|
package/package.json
CHANGED
|
@@ -100,6 +100,12 @@ export interface RoadmapCardProps {
|
|
|
100
100
|
cardType?: 'roadmap_item' | 'delivery_item' | 'internal_task'
|
|
101
101
|
size?: CardSize
|
|
102
102
|
className?: string
|
|
103
|
+
/** DOM `id` applied to the card's outer element. `RoadmapGrid` sets
|
|
104
|
+
* `roadmap-<external_id>` so chat-card deep-links
|
|
105
|
+
* (`?search=<id>#roadmap-<id>`) have a target for `useScrollToHash`
|
|
106
|
+
* to scroll to. `scroll-mt-24` on the outer element keeps the card
|
|
107
|
+
* BELOW the sticky chrome. */
|
|
108
|
+
id?: string
|
|
103
109
|
// Default-branch vote controls (ignored in `sm`):
|
|
104
110
|
userVote?: VoteType | null
|
|
105
111
|
onVote?: (voteType: 'up' | 'down') => void
|
|
@@ -116,6 +122,7 @@ export function RoadmapCard({
|
|
|
116
122
|
size = 'default',
|
|
117
123
|
cardType = 'roadmap_item',
|
|
118
124
|
className,
|
|
125
|
+
id,
|
|
119
126
|
userVote,
|
|
120
127
|
onVote,
|
|
121
128
|
isVoting = false,
|
|
@@ -233,7 +240,7 @@ export function RoadmapCard({
|
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
return (
|
|
236
|
-
<div className={`bg-ods-card border border-ods-border rounded-[6px] p-[24px] flex flex-col gap-[16px] hover:border-ods-accent transition-all h-full ${className ?? ''}`}>
|
|
243
|
+
<div id={id} className={`bg-ods-card border border-ods-border rounded-[6px] p-[24px] flex flex-col gap-[16px] hover:border-ods-accent transition-all h-full scroll-mt-24 ${className ?? ''}`}>
|
|
237
244
|
<div className="flex gap-[16px] items-center w-full">
|
|
238
245
|
<div className="w-16 h-16 rounded-lg flex items-center justify-center flex-shrink-0 bg-ods-bg border border-ods-border">
|
|
239
246
|
{iconSrc ? (
|
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
} from '../../utils/doc-tree-nav'
|
|
11
11
|
import { useDocNavigation } from './doc-navigation-context'
|
|
12
12
|
import { scrollElementIntoView } from '../../utils/scroll-into-view'
|
|
13
|
+
import { navigateSamePageHash, HUB_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav'
|
|
13
14
|
|
|
14
15
|
function scrollToContent() {
|
|
15
16
|
const article = document.querySelector('article') as HTMLElement | null
|
|
16
17
|
if (article) {
|
|
17
|
-
scrollElementIntoView(article, { headerOffset:
|
|
18
|
+
scrollElementIntoView(article, { headerOffset: HUB_HEADER_OFFSET_PX })
|
|
18
19
|
} else {
|
|
19
20
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
20
21
|
}
|
|
@@ -329,12 +330,52 @@ export function useDocumentTree(
|
|
|
329
330
|
const anchor = hashIndex !== -1 ? path.substring(hashIndex) : ''
|
|
330
331
|
const cleanPath = path.replace(/\/$/, '').split('#')[0]
|
|
331
332
|
|
|
333
|
+
// Same-doc-different-anchor shortcut. Content is already mounted, so we
|
|
334
|
+
// don't need the 300ms "wait-for-fetch" bandaid — the canonical helper
|
|
335
|
+
// owns pushState + synthetic `hashchange` (so any in-doc TOC / accordion
|
|
336
|
+
// bound to the URL hash re-renders) + the anchoring-proof tween in one
|
|
337
|
+
// sync call. `headerOffset: HUB_HEADER_OFFSET_PX` matches the cross-doc path below so
|
|
338
|
+
// anchors land BELOW the docs sticky header on every same-doc internal
|
|
339
|
+
// link click. Cross-doc nav (different cleanPath) falls through to the
|
|
340
|
+
// existing fetch-then-scroll path below.
|
|
341
|
+
//
|
|
342
|
+
// We pass the BARE-hash form to the helper rather than reconstructing
|
|
343
|
+
// a full `${normalizedBaseRoute}/${cleanPath}${anchor}` path: the
|
|
344
|
+
// helper's pathname check compares against `window.location.pathname`,
|
|
345
|
+
// which carries the FOLDER-INDEX-STRIPPED form (`/docs/foo` for
|
|
346
|
+
// `foo/README.md`, `/docs` for the root index). Handing it `cleanPath`
|
|
347
|
+
// — the raw resolved path — produces e.g. `/docs/foo/README.md` and
|
|
348
|
+
// the compare fails → helper returns false → silent dead-click. The
|
|
349
|
+
// bare-hash form sidesteps that entirely: the helper reconstructs
|
|
350
|
+
// `pathname + search + hash` from `window.location`, so the compare
|
|
351
|
+
// is trivially equal. Covers bare `#anchor` links (resolve to
|
|
352
|
+
// `cleanPath=''`) AND folder-index links (`foo/README.md` resolving
|
|
353
|
+
// to the current `/docs/foo`).
|
|
354
|
+
// Bare-hash internal links (`[Click](#section)`) come in as
|
|
355
|
+
// `path === '#section'`, so `cleanPath` becomes `''` and the naive
|
|
356
|
+
// strip-then-compare misses the same-doc shortcut on every NON-root
|
|
357
|
+
// doc (selectedPath is e.g. `'foo/bar'`, not `''`). For that case the
|
|
358
|
+
// current doc IS the same-doc target by definition — short-circuit
|
|
359
|
+
// pathForSelection to the current selection so the shortcut fires.
|
|
360
|
+
const pathForSelection =
|
|
361
|
+
anchor && options?.fromInternalLink && cleanPath === ''
|
|
362
|
+
? selectedPathRef.current
|
|
363
|
+
: stripFolderIndexFromPath(cleanPath, folderIndexFile)
|
|
364
|
+
if (
|
|
365
|
+
anchor &&
|
|
366
|
+
options?.fromInternalLink &&
|
|
367
|
+
pathForSelection === selectedPathRef.current
|
|
368
|
+
) {
|
|
369
|
+
navigateSamePageHash(anchor, { headerOffset: HUB_HEADER_OFFSET_PX })
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
332
373
|
const scrollAfterNav = () => {
|
|
333
374
|
if (anchor) {
|
|
334
375
|
setTimeout(() => {
|
|
335
376
|
const el = document.getElementById(anchor.substring(1))
|
|
336
377
|
if (el) {
|
|
337
|
-
scrollElementIntoView(el, { headerOffset:
|
|
378
|
+
scrollElementIntoView(el, { headerOffset: HUB_HEADER_OFFSET_PX })
|
|
338
379
|
} else {
|
|
339
380
|
scrollToContent()
|
|
340
381
|
}
|
|
@@ -370,7 +411,8 @@ export function useDocumentTree(
|
|
|
370
411
|
return
|
|
371
412
|
}
|
|
372
413
|
|
|
373
|
-
|
|
414
|
+
// `pathForSelection` was already computed above (inside the
|
|
415
|
+
// same-doc-anchor shortcut check); reuse it here for cross-doc nav.
|
|
374
416
|
const urlPath = pathForSelection
|
|
375
417
|
|
|
376
418
|
lastFetchedPath.current = null
|
|
@@ -7,6 +7,7 @@ import { useSelfFetch } from '../../hooks/use-self-fetch'
|
|
|
7
7
|
import { buildSuggestionUrl } from '../../utils/suggestion-url'
|
|
8
8
|
import { serializeJsonLd } from '../../utils/common'
|
|
9
9
|
import { scrollElementIntoView } from '../../utils/scroll-into-view'
|
|
10
|
+
import { navigateSamePageHash, STICKY_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav'
|
|
10
11
|
import { faqSectionSlug, faqItemAnchor, parseFaqHash, type FaqHashTarget } from '../../utils/faq-anchor'
|
|
11
12
|
import { cn } from '../../utils/cn'
|
|
12
13
|
import { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'
|
|
@@ -97,9 +98,6 @@ function groupFaqsBySection(faqs: Faq[]): FaqGroup[] {
|
|
|
97
98
|
return groups
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
/** The standard hub sticky-header height — same offset `useNavLink`'s hash
|
|
101
|
-
* scroll uses, so a category jump lands below the header, not under it. */
|
|
102
|
-
const FAQ_NAV_HEADER_OFFSET = 96
|
|
103
101
|
|
|
104
102
|
/** Map key for the uncategorized bucket — `group.slug` is null for it, so
|
|
105
103
|
* every per-group map (default-open ids, accordion keys) uses this sentinel
|
|
@@ -160,7 +158,7 @@ function GroupedFaqList({
|
|
|
160
158
|
}
|
|
161
159
|
if (bestId) setActiveSlug(bestId)
|
|
162
160
|
},
|
|
163
|
-
{ rootMargin: `-${
|
|
161
|
+
{ rootMargin: `-${STICKY_HEADER_OFFSET_PX}px 0px -55% 0px`, threshold: 0 },
|
|
164
162
|
)
|
|
165
163
|
for (const group of navGroups) {
|
|
166
164
|
const el = group.slug ? document.getElementById(group.slug) : null
|
|
@@ -215,18 +213,33 @@ function GroupedFaqList({
|
|
|
215
213
|
const elId =
|
|
216
214
|
hashTarget.kind === 'item' ? faqItemAnchor(hashTarget.rawId) : hashTarget.slug
|
|
217
215
|
const el = document.getElementById(elId)
|
|
218
|
-
if (el) scrollElementIntoView(el, { headerOffset:
|
|
216
|
+
if (el) scrollElementIntoView(el, { headerOffset: STICKY_HEADER_OFFSET_PX })
|
|
219
217
|
if (hashTarget.kind === 'section') setActiveSlug(hashTarget.slug)
|
|
220
218
|
}, [hashTarget])
|
|
221
219
|
|
|
220
|
+
// Category pill click. `navigateSamePageHash` owns the entire transition:
|
|
221
|
+
// replaceState → synthetic `hashchange` → `scrollElementIntoView` tween
|
|
222
|
+
// with `STICKY_HEADER_OFFSET_PX` so the section heading lands BELOW the
|
|
223
|
+
// sticky category nav on the FIRST tween (covers the same-target
|
|
224
|
+
// re-click case, where the `hashTarget` effect at L214 is a no-op
|
|
225
|
+
// because the state reference is equal). For DIFFERENT-target clicks
|
|
226
|
+
// the helper's synthetic `hashchange` re-fires that effect, which
|
|
227
|
+
// re-scrolls with the same offset and cancels this tween (singleton)
|
|
228
|
+
// — both paths land at the same position. The effect is still
|
|
229
|
+
// required for back/forward + direct URL edits, where the helper
|
|
230
|
+
// isn't in the call chain.
|
|
231
|
+
//
|
|
232
|
+
// `history: 'replace'` matches the pre-helper behavior: category pills
|
|
233
|
+
// are a TOC, not a navigation step, so the Back button leaves the
|
|
234
|
+
// FAQ page in one step regardless of how many categories the user
|
|
235
|
+
// clicked through.
|
|
222
236
|
const handleJump = useCallback(
|
|
223
237
|
(e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {
|
|
224
238
|
e.preventDefault()
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
239
|
+
navigateSamePageHash('#' + slug, {
|
|
240
|
+
headerOffset: STICKY_HEADER_OFFSET_PX,
|
|
241
|
+
history: 'replace',
|
|
228
242
|
})
|
|
229
|
-
if (typeof history !== 'undefined') history.replaceState(null, '', `#${slug}`)
|
|
230
243
|
},
|
|
231
244
|
[],
|
|
232
245
|
)
|
|
@@ -35,7 +35,9 @@ import { DeliveryTable } from './delivery-table';
|
|
|
35
35
|
import { EmptyState } from '../../empty-state';
|
|
36
36
|
import { LoadError } from '../../ui/error-state';
|
|
37
37
|
import { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys';
|
|
38
|
+
import { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav';
|
|
38
39
|
import { contentFetch } from '../../../utils/embed-content-fetch';
|
|
40
|
+
import { useScrollToHash } from '../../../hooks/use-scroll-to-hash';
|
|
39
41
|
|
|
40
42
|
const DEFAULT_COMPLETED_ENDPOINT = '/api/delivery/completed';
|
|
41
43
|
const DEFAULT_IN_PROGRESS_ENDPOINT = '/api/delivery/in-progress';
|
|
@@ -129,6 +131,13 @@ export function DeliveryLists({
|
|
|
129
131
|
const filteredCompleted = data?.completed || [];
|
|
130
132
|
const filteredInProgress = data?.inProgress || [];
|
|
131
133
|
|
|
134
|
+
// Deep-link hash dispatch — `?search=<id>#delivery-<id>` from a chat
|
|
135
|
+
// card or a linked-delivery card on a ticket. Shared hook owns the
|
|
136
|
+
// poll-until-mount + hashchange-listener wiring (same instance used
|
|
137
|
+
// by RoadmapView). 96 matches the sticky-header offset every
|
|
138
|
+
// hash-scroll surface in the app uses.
|
|
139
|
+
useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX });
|
|
140
|
+
|
|
132
141
|
const showCompleted = true;
|
|
133
142
|
const showInProgress = true;
|
|
134
143
|
|
|
@@ -60,6 +60,13 @@ export interface DeliveryRowProps {
|
|
|
60
60
|
/** Small uppercase caption rendered above the title. Used by the
|
|
61
61
|
* linked-delivery card variant ("LINKED DELIVERY"). */
|
|
62
62
|
caption?: string
|
|
63
|
+
/** DOM `id` applied to the row's outer element. `DeliveryTable`
|
|
64
|
+
* always sets `delivery-<external_id>` so chat-card deep-links
|
|
65
|
+
* (`?search=<id>#delivery-<id>`) and the ticket linked-card path
|
|
66
|
+
* both have a target for `useScrollToHash` to scroll to. Always
|
|
67
|
+
* paired with `scroll-mt-24` on the outer element so the row lands
|
|
68
|
+
* BELOW the sticky chrome after the scroll. */
|
|
69
|
+
id?: string
|
|
63
70
|
className?: string
|
|
64
71
|
}
|
|
65
72
|
|
|
@@ -67,6 +74,7 @@ export function DeliveryRow({
|
|
|
67
74
|
item,
|
|
68
75
|
href,
|
|
69
76
|
caption,
|
|
77
|
+
id,
|
|
70
78
|
className,
|
|
71
79
|
}: DeliveryRowProps) {
|
|
72
80
|
const taskType = item.taskType as keyof typeof TASK_TYPE_LABELS
|
|
@@ -121,6 +129,11 @@ export function DeliveryRow({
|
|
|
121
129
|
|
|
122
130
|
const baseClass = cn(
|
|
123
131
|
'block p-[12px] md:p-[16px] no-underline text-inherit transition-colors duration-150',
|
|
132
|
+
// `scroll-mt-24` is paid for whether `id` is set or not (it's a
|
|
133
|
+
// single Tailwind utility, no runtime cost). Keeping it
|
|
134
|
+
// unconditional means a future caller adding `id` doesn't also
|
|
135
|
+
// have to remember to ask for the offset.
|
|
136
|
+
'scroll-mt-24',
|
|
124
137
|
href && 'hover:bg-ods-bg-hover cursor-pointer',
|
|
125
138
|
className,
|
|
126
139
|
)
|
|
@@ -133,11 +146,11 @@ export function DeliveryRow({
|
|
|
133
146
|
// losing TanStack-Query state on back, leaving /tickets stuck on
|
|
134
147
|
// its skeleton.
|
|
135
148
|
return (
|
|
136
|
-
<Link href={href} className={baseClass} prefetch={false}>
|
|
149
|
+
<Link href={href} id={id} className={baseClass} prefetch={false}>
|
|
137
150
|
{inner}
|
|
138
151
|
</Link>
|
|
139
152
|
)
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
return <div className={baseClass}>{inner}</div>
|
|
155
|
+
return <div id={id} className={baseClass}>{inner}</div>
|
|
143
156
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { DeliveryRow } from './delivery-row';
|
|
21
21
|
import type { DeliveryItem } from '../../../types/delivery';
|
|
22
|
+
import { devSectionAnchorId } from '../../../utils/dev-sections/dev-section-param-keys';
|
|
22
23
|
|
|
23
24
|
interface DeliveryTableProps {
|
|
24
25
|
items: DeliveryItem[];
|
|
@@ -95,11 +96,16 @@ export function DeliveryTable({ items, isLoading = false }: DeliveryTableProps)
|
|
|
95
96
|
<div className="bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full">
|
|
96
97
|
<div className="w-full">
|
|
97
98
|
{items.map((item) => (
|
|
99
|
+
// DOM id lives on DeliveryRow's own outer element (no wrapper
|
|
100
|
+
// div). Anchor mirrors `buildDevSectionUrl('delivery', <id>)`
|
|
101
|
+
// → `#delivery-<external_id>`; `useScrollToHash` in
|
|
102
|
+
// `delivery-lists.tsx` finds the row by id and scrolls. The
|
|
103
|
+
// outer wrapper here ONLY exists for the row separators.
|
|
98
104
|
<div
|
|
99
105
|
key={item.id}
|
|
100
106
|
className="border-b border-ods-border last:border-b-0"
|
|
101
107
|
>
|
|
102
|
-
<DeliveryRow item={item} />
|
|
108
|
+
<DeliveryRow item={item} id={devSectionAnchorId('delivery', item.id)} />
|
|
103
109
|
</div>
|
|
104
110
|
))}
|
|
105
111
|
</div>
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
AccordionContent,
|
|
34
34
|
} from '../../ui';
|
|
35
35
|
import { cn } from '../../../utils/cn';
|
|
36
|
+
import { devSectionAnchorId } from '../../../utils/dev-sections/dev-section-param-keys';
|
|
36
37
|
import { contentFetch } from '../../../utils/embed-content-fetch';
|
|
37
38
|
import type { RoadmapItem } from '../../chat/types/entities/roadmap-item';
|
|
38
39
|
|
|
@@ -134,9 +135,15 @@ function RoadmapGridSingle({
|
|
|
134
135
|
return (
|
|
135
136
|
<div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>
|
|
136
137
|
{items.map((item) => (
|
|
138
|
+
// DOM id + sticky-header scroll offset live ON RoadmapCard's own
|
|
139
|
+
// outer element (no wrapper div). Anchor mirrors
|
|
140
|
+
// `buildDevSectionUrl('roadmap', <id>)` → `#roadmap-<external_id>`;
|
|
141
|
+
// `useScrollToHash` in `roadmap-view.tsx` finds the card by id
|
|
142
|
+
// and scrolls.
|
|
137
143
|
<RoadmapCard
|
|
138
144
|
key={item.id}
|
|
139
145
|
item={item}
|
|
146
|
+
id={devSectionAnchorId('roadmap', item.id)}
|
|
140
147
|
userVote={getVote(item.id)}
|
|
141
148
|
onVote={(voteType) => onVote(item.id, voteType)}
|
|
142
149
|
isVoting={votingTasks.has(item.id)}
|
|
@@ -15,11 +15,13 @@ import { useMemo } from 'react'
|
|
|
15
15
|
import { useSearchParams } from '../../../embed-shims'
|
|
16
16
|
import { LoadError } from '../../ui/error-state'
|
|
17
17
|
import { useSelfFetch } from '../../../hooks/use-self-fetch'
|
|
18
|
+
import { useScrollToHash } from '../../../hooks/use-scroll-to-hash'
|
|
18
19
|
import type { RoadmapItem } from '../../chat/types/entities/roadmap-item'
|
|
19
20
|
import { RoadmapGrid } from './roadmap-grid'
|
|
20
21
|
import { RoadmapGridSkeleton } from './roadmap-grid-skeleton'
|
|
21
22
|
import type { UseRoadmapVotingOptions } from './use-roadmap-voting'
|
|
22
23
|
import { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys'
|
|
24
|
+
import { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav'
|
|
23
25
|
|
|
24
26
|
const DEFAULT_ENDPOINT = '/api/roadmap'
|
|
25
27
|
// Defaults sourced from the ONE param-key registry the chrome (OPENFRAME_DEV_SECTIONS) also
|
|
@@ -77,6 +79,15 @@ export function RoadmapView({
|
|
|
77
79
|
)
|
|
78
80
|
const items = data?.items ?? []
|
|
79
81
|
|
|
82
|
+
// Deep-link hash dispatch — `?search=<id>#roadmap-<id>` from a chat card.
|
|
83
|
+
// Shared hook owns the poll-until-mount + hashchange-listener wiring
|
|
84
|
+
// (same instance used by DeliveryLists). The rAF poll inside the hook
|
|
85
|
+
// handles the Radix `<AccordionItem>` lazy-unmount: on first paint
|
|
86
|
+
// every quarter is collapsed; an effect in `roadmap-grid.tsx` expands
|
|
87
|
+
// them when `hasActiveFilters` is true (chat URL carries `?search=<id>`).
|
|
88
|
+
// The card mounts one tick after `data` lands; the hook waits.
|
|
89
|
+
useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX })
|
|
90
|
+
|
|
80
91
|
if (error) {
|
|
81
92
|
return <LoadError message="Failed to load roadmap." onRetry={reload} />
|
|
82
93
|
}
|
|
@@ -19,6 +19,7 @@ import { useCallback, useEffect, useRef } from 'react'
|
|
|
19
19
|
import { StatusBadge, type StatusBadgeProps } from '../ui'
|
|
20
20
|
import { formatRelativeTime } from '../../utils/date-utils'
|
|
21
21
|
import { scrollElementIntoView } from '../../utils/scroll-into-view'
|
|
22
|
+
import { STICKY_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav'
|
|
22
23
|
import { getStatusColorScheme } from '../chat/utils/agent-status-message'
|
|
23
24
|
import { DevCardRowContent } from '../shared/dev-section/dev-card-row'
|
|
24
25
|
import {
|
|
@@ -28,23 +29,6 @@ import {
|
|
|
28
29
|
import type { AnyTicket } from './types'
|
|
29
30
|
import { isOptimistic } from './types'
|
|
30
31
|
|
|
31
|
-
/** Sticky page-chrome offset, applied two ways from this ONE constant:
|
|
32
|
-
*
|
|
33
|
-
* 1. As `scrollMarginTop` inline style on the wrapper — so any
|
|
34
|
-
* anchor-driven or `scrollIntoView()`-driven scroll (browser
|
|
35
|
-
* `#hash` navigation, Tab-focus into the card) lands BELOW the
|
|
36
|
-
* sticky header.
|
|
37
|
-
* 2. As `headerOffset` passed to `scrollElementIntoView(...)` — for
|
|
38
|
-
* the click-to-expand `window.scrollTo` path, which pre-computes
|
|
39
|
-
* its target pixel and ignores CSS `scroll-margin-top`.
|
|
40
|
-
*
|
|
41
|
-
* Single source of truth: change 96 here and BOTH paths follow. The
|
|
42
|
-
* previous code combined a `scroll-mt-24` (=96px) Tailwind class
|
|
43
|
-
* with this constant — two declarations, one comment binding them,
|
|
44
|
-
* drift hazard. Now there's nothing to keep in sync.
|
|
45
|
-
*/
|
|
46
|
-
const STICKY_HEADER_OFFSET_PX = 96
|
|
47
|
-
|
|
48
32
|
export interface HelpCenterCardProps {
|
|
49
33
|
ticket: AnyTicket
|
|
50
34
|
expanded: boolean
|
|
@@ -59,6 +43,12 @@ export interface HelpCenterCardProps {
|
|
|
59
43
|
* (`HelpCenterList`) reads via `actions.replyErrorFor(external_id)`. */
|
|
60
44
|
replyError?: TicketDetailDrawerProps['replyError']
|
|
61
45
|
onClearReplyError?: TicketDetailDrawerProps['onClearReplyError']
|
|
46
|
+
/** DOM `id` applied to the row's outer element. Parent (`HelpCenterList`)
|
|
47
|
+
* sets `ticket-<external_id>` so `useScrollToHash` can deep-link from
|
|
48
|
+
* a chat card's `?ticket=<id>#ticket-<id>` URL. The sticky-header
|
|
49
|
+
* offset is already baked in via `STICKY_HEADER_OFFSET_PX` so the row
|
|
50
|
+
* lands BELOW the chrome regardless of whether `id` is set. */
|
|
51
|
+
id?: string
|
|
62
52
|
}
|
|
63
53
|
|
|
64
54
|
export function HelpCenterCard({
|
|
@@ -73,6 +63,7 @@ export function HelpCenterCard({
|
|
|
73
63
|
onActionCollapsed,
|
|
74
64
|
replyError,
|
|
75
65
|
onClearReplyError,
|
|
66
|
+
id,
|
|
76
67
|
}: HelpCenterCardProps) {
|
|
77
68
|
const optimistic = isOptimistic(ticket)
|
|
78
69
|
const rawStatus = (ticket.status ?? 'OPEN').toUpperCase()
|
|
@@ -149,6 +140,7 @@ export function HelpCenterCard({
|
|
|
149
140
|
return (
|
|
150
141
|
<div
|
|
151
142
|
ref={rowRef}
|
|
143
|
+
id={id}
|
|
152
144
|
style={{ scrollMarginTop: STICKY_HEADER_OFFSET_PX }}
|
|
153
145
|
className={`border-b border-ods-border last:border-b-0 ${optimistic ? 'opacity-60' : ''}`}
|
|
154
146
|
aria-busy={optimistic || undefined}
|
|
@@ -35,6 +35,9 @@ import { DevSectionPage } from '../shared/dev-section'
|
|
|
35
35
|
import { DevCardRowSkeletonList } from '../shared/dev-section/dev-card-row'
|
|
36
36
|
import { UnifiedPagination } from '../unified-pagination'
|
|
37
37
|
import { useChatIdentity } from '../chat/hooks/use-chat-identity'
|
|
38
|
+
import { useScrollToHash } from '../../hooks/use-scroll-to-hash'
|
|
39
|
+
import { STICKY_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav'
|
|
40
|
+
import { devSectionAnchorId } from '../../utils/dev-sections/dev-section-param-keys'
|
|
38
41
|
import { toast as defaultToast } from '../../hooks/use-toast'
|
|
39
42
|
import { useTicketsList } from './hooks/use-tickets-list'
|
|
40
43
|
import { useTicketActions } from './hooks/use-ticket-actions'
|
|
@@ -264,6 +267,15 @@ function HelpCenterListAuthed({
|
|
|
264
267
|
)
|
|
265
268
|
|
|
266
269
|
const merged: AnyTicket[] = [...optimisticTickets, ...tickets]
|
|
270
|
+
|
|
271
|
+
// Deep-link hash dispatch — `/tickets#ticket-<external_id>` from a
|
|
272
|
+
// chat card (or any other in-app link). The `?ticket=<external_id>`
|
|
273
|
+
// query param keeps owning drawer auto-open; this hook owns the
|
|
274
|
+
// scroll-to-row independently. Both can fire on the same URL
|
|
275
|
+
// (`/tickets?ticket=X#ticket-X`) — drawer opens AND row scrolls into
|
|
276
|
+
// view. Shared `useScrollToHash` polls until the row mounts (handles
|
|
277
|
+
// the SWR fetch race), uses the canonical `scrollElementIntoView` tween.
|
|
278
|
+
useScrollToHash(tickets, { headerOffset: STICKY_HEADER_OFFSET_PX })
|
|
267
279
|
const hasActiveFilters = search !== '' || (status !== '' && status !== 'all')
|
|
268
280
|
const hasResults = merged.length > 0
|
|
269
281
|
|
|
@@ -343,6 +355,7 @@ function HelpCenterListAuthed({
|
|
|
343
355
|
{merged.map((ticket) => (
|
|
344
356
|
<HelpCenterCard
|
|
345
357
|
key={ticket.id}
|
|
358
|
+
id={devSectionAnchorId('ticket', ticket.external_id)}
|
|
346
359
|
ticket={ticket}
|
|
347
360
|
expanded={expandedTicketId === ticket.id}
|
|
348
361
|
onToggle={toggleRow}
|
|
@@ -25,6 +25,7 @@ import { RefreshCw } from 'lucide-react'
|
|
|
25
25
|
import { useChatIdentity } from '../chat/hooks/use-chat-identity'
|
|
26
26
|
import { toast as defaultToast } from '../../hooks/use-toast'
|
|
27
27
|
import { formatRelativeTime } from '../../utils/date-utils'
|
|
28
|
+
import { devSectionAnchorId } from '../../utils/dev-sections/dev-section-param-keys'
|
|
28
29
|
import { TicketOpenForm } from './ticket-open-form'
|
|
29
30
|
import { TicketRow } from './ticket-row'
|
|
30
31
|
import { useTicketsList } from './hooks/use-tickets-list'
|
|
@@ -168,6 +169,7 @@ function TicketCenterAuthed({
|
|
|
168
169
|
{merged.map((ticket) => (
|
|
169
170
|
<TicketRow
|
|
170
171
|
key={ticket.id}
|
|
172
|
+
id={devSectionAnchorId('ticket', ticket.external_id)}
|
|
171
173
|
ticket={ticket}
|
|
172
174
|
expanded={expandedTicketId === ticket.id}
|
|
173
175
|
onToggle={toggleRow}
|
|
@@ -48,6 +48,11 @@ export interface TicketRowProps {
|
|
|
48
48
|
/** Called after a successful close/reopen so the parent can collapse
|
|
49
49
|
* the drawer (status flipped — current action set is now stale). */
|
|
50
50
|
onActionCollapsed: TicketDetailDrawerProps['onActionCollapsed']
|
|
51
|
+
/** DOM `id` applied to the row's outer element. Parents that surface
|
|
52
|
+
* a deep-link target set `ticket-<external_id>` so `useScrollToHash`
|
|
53
|
+
* resolves it. `scroll-mt-24` is already baked into the outer
|
|
54
|
+
* element regardless. */
|
|
55
|
+
id?: string
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export function TicketRow({
|
|
@@ -60,6 +65,7 @@ export function TicketRow({
|
|
|
60
65
|
onClose,
|
|
61
66
|
onReopen,
|
|
62
67
|
onActionCollapsed,
|
|
68
|
+
id,
|
|
63
69
|
}: TicketRowProps) {
|
|
64
70
|
// Optimistic placeholders have no drawer — the real id hasn't
|
|
65
71
|
// arrived yet, so action targets would be undefined.
|
|
@@ -123,7 +129,7 @@ export function TicketRow({
|
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
return (
|
|
126
|
-
<div ref={rowRef} className="scroll-mt-24">
|
|
132
|
+
<div ref={rowRef} id={id} className="scroll-mt-24">
|
|
127
133
|
<Collapsible
|
|
128
134
|
open={expanded && !optimistic}
|
|
129
135
|
className="border-b border-ods-border last:border-b-0"
|
package/src/hooks/index.ts
CHANGED
|
@@ -28,6 +28,11 @@ export * from './use-access-code-integration'
|
|
|
28
28
|
// OG placeholder URL builder hook (requires host-supplied URL builder)
|
|
29
29
|
export * from './use-og-placeholder'
|
|
30
30
|
|
|
31
|
+
// Deep-link "scroll to URL hash" after data loads. Pairs with URL
|
|
32
|
+
// composers that emit `?<filter>=<id>#<prefix>-<id>` — the filter
|
|
33
|
+
// narrows the list, the hash scrolls the matching DOM id.
|
|
34
|
+
export * from './use-scroll-to-hash'
|
|
35
|
+
|
|
31
36
|
// Invisible bot-protection client primitive (honeypot ref + submit-timing).
|
|
32
37
|
// Pairs with the server-safe decision fn in `utils/humanity-signals`.
|
|
33
38
|
export * from './use-humanity-signals'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import { scrollElementIntoView } from '../utils/scroll-into-view'
|
|
5
|
+
import { normalizeHashFragment } from '../utils/same-page-hash-nav'
|
|
6
|
+
|
|
7
|
+
/** ~1s at 60fps — long enough to outlast Radix accordion expand + SWR
|
|
8
|
+
* mount, short enough that a missed anchor doesn't hang the page. */
|
|
9
|
+
const MAX_POLL_FRAMES = 60
|
|
10
|
+
|
|
11
|
+
export interface UseScrollToHashOptions {
|
|
12
|
+
/** Pixels to subtract for sticky chrome. */
|
|
13
|
+
headerOffset?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Scroll the page to `window.location.hash` once `readyDep` resolves
|
|
18
|
+
* to a truthy value. Polls via rAF for ~1s so lazy-mounted rows (Radix
|
|
19
|
+
* accordion, SWR fetch) have time to render. Re-runs on `readyDep`
|
|
20
|
+
* reference change AND on `hashchange` (browser back/forward + the
|
|
21
|
+
* synthetic event `navigateSamePageHash` dispatches).
|
|
22
|
+
*
|
|
23
|
+
* Skipped when `readyDep == null || readyDep === false`. Default
|
|
24
|
+
* `true` makes the hook run on mount for pages whose target is in the
|
|
25
|
+
* initial SSR render.
|
|
26
|
+
*/
|
|
27
|
+
export function useScrollToHash(
|
|
28
|
+
readyDep: unknown = true,
|
|
29
|
+
options?: UseScrollToHashOptions,
|
|
30
|
+
): void {
|
|
31
|
+
const headerOffset = options?.headerOffset ?? 0
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (typeof window === 'undefined') return
|
|
34
|
+
if (readyDep === null || readyDep === false) return
|
|
35
|
+
let rafId: number | null = null
|
|
36
|
+
const cancelPoll = () => {
|
|
37
|
+
if (rafId !== null) {
|
|
38
|
+
cancelAnimationFrame(rafId)
|
|
39
|
+
rafId = null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const tryScrollToHash = () => {
|
|
43
|
+
// `normalizeHashFragment` heals a malformed multi-fragment hash
|
|
44
|
+
// so `getElementById` resolves on deep-link entries that bypass
|
|
45
|
+
// `navigateSamePageHash`'s own normalize.
|
|
46
|
+
const hash = normalizeHashFragment(window.location.hash).slice(1)
|
|
47
|
+
if (!hash) return
|
|
48
|
+
// Cancel any in-flight poll from a prior invocation so two
|
|
49
|
+
// concurrent ticks can't both call scrollElementIntoView.
|
|
50
|
+
cancelPoll()
|
|
51
|
+
let frames = 0
|
|
52
|
+
const tick = () => {
|
|
53
|
+
const el = document.getElementById(hash)
|
|
54
|
+
if (el) {
|
|
55
|
+
rafId = null
|
|
56
|
+
scrollElementIntoView(el, { headerOffset })
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (frames++ < MAX_POLL_FRAMES) {
|
|
60
|
+
rafId = requestAnimationFrame(tick)
|
|
61
|
+
} else {
|
|
62
|
+
rafId = null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
tick()
|
|
66
|
+
}
|
|
67
|
+
tryScrollToHash()
|
|
68
|
+
window.addEventListener('hashchange', tryScrollToHash)
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener('hashchange', tryScrollToHash)
|
|
71
|
+
cancelPoll()
|
|
72
|
+
}
|
|
73
|
+
}, [readyDep, headerOffset])
|
|
74
|
+
}
|
|
@@ -27,7 +27,7 @@ const iconName = getSourceIconName('github-commits')
|
|
|
27
27
|
|
|
28
28
|
// Get display label for a chip strip
|
|
29
29
|
const label = getSourceLabel('hubspot-tickets-anon')
|
|
30
|
-
// → '
|
|
30
|
+
// → 'Tickets'
|
|
31
31
|
|
|
32
32
|
// Map a documentType emitted by the LLM to its table ID
|
|
33
33
|
const tableId = defaultTableIdForDocumentType('slack_message')
|
|
@@ -19,3 +19,17 @@ export const DEV_SECTION_PARAM_KEYS = {
|
|
|
19
19
|
/** Delivery (bug-fix / enhancement) task-type filter. */
|
|
20
20
|
deliveryTaskType: 'task_type',
|
|
21
21
|
} as const
|
|
22
|
+
|
|
23
|
+
/** Section keys that participate in `<section>-<id>` anchor IDs.
|
|
24
|
+
* The URL composer (`appendSearchAndHash` in hub `dev-section-url.ts`)
|
|
25
|
+
* and the row components (`DeliveryRow`, `RoadmapCard`, `HelpCenterCard`)
|
|
26
|
+
* both call `devSectionAnchorId` so the DOM `id` and the URL hash stay
|
|
27
|
+
* in lockstep — adding a new section means adding the literal here
|
|
28
|
+
* ONCE, not at every render site. */
|
|
29
|
+
export type DevSectionAnchorKind = 'roadmap' | 'delivery' | 'ticket'
|
|
30
|
+
|
|
31
|
+
/** Compose the canonical `<section>-<id>` anchor id used by the dev-center
|
|
32
|
+
* rows + URL composer. */
|
|
33
|
+
export function devSectionAnchorId(section: DevSectionAnchorKind, id: string): string {
|
|
34
|
+
return `${section}-${id}`
|
|
35
|
+
}
|