@flamingo-stack/openframe-frontend-core 0.0.402 → 0.0.404-snapshot.20260711053526
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-NHFQ2LCC.js → chunk-3XCNFTFV.js} +3 -3
- package/dist/{chunk-F63VMX4S.js → chunk-5XZEY34X.js} +3 -3
- package/dist/{chunk-EDOLCPBS.cjs → chunk-6OQ7YOX5.cjs} +33 -33
- package/dist/{chunk-EDOLCPBS.cjs.map → chunk-6OQ7YOX5.cjs.map} +1 -1
- package/dist/{chunk-A4RDD2PS.cjs → chunk-ACJ4CM6H.cjs} +93 -93
- package/dist/{chunk-A4RDD2PS.cjs.map → chunk-ACJ4CM6H.cjs.map} +1 -1
- package/dist/{chunk-WM72XFOD.js → chunk-BK7QCB5J.js} +6 -6
- package/dist/{chunk-ONAWSVFK.cjs → chunk-BS6O4HLJ.cjs} +40 -40
- package/dist/{chunk-ONAWSVFK.cjs.map → chunk-BS6O4HLJ.cjs.map} +1 -1
- package/dist/{chunk-GA5AN24T.cjs → chunk-BX3R44MR.cjs} +2 -2
- package/dist/chunk-BX3R44MR.cjs.map +1 -0
- package/dist/{chunk-J4XR6YYZ.cjs → chunk-CCIBZ7TO.cjs} +10 -6
- package/dist/chunk-CCIBZ7TO.cjs.map +1 -0
- package/dist/{chunk-DYV47UA6.cjs → chunk-DHIYBC5Z.cjs} +32 -32
- package/dist/{chunk-DYV47UA6.cjs.map → chunk-DHIYBC5Z.cjs.map} +1 -1
- package/dist/{chunk-DH6HTYOV.js → chunk-E6YHDT3D.js} +6 -2
- package/dist/chunk-E6YHDT3D.js.map +1 -0
- package/dist/{chunk-CWLKGOU4.cjs → chunk-ELLTTMPE.cjs} +5 -5
- package/dist/{chunk-CWLKGOU4.cjs.map → chunk-ELLTTMPE.cjs.map} +1 -1
- package/dist/{chunk-6VAJDGZK.js → chunk-F3YLM5PO.js} +3 -3
- package/dist/{chunk-MEU2JEXL.cjs → chunk-FFYEDIAG.cjs} +511 -447
- package/dist/chunk-FFYEDIAG.cjs.map +1 -0
- package/dist/{chunk-D3HPV6YE.cjs → chunk-FOJRR647.cjs} +73 -73
- package/dist/{chunk-D3HPV6YE.cjs.map → chunk-FOJRR647.cjs.map} +1 -1
- package/dist/{chunk-6FIVVZO2.js → chunk-GT7AJXKH.js} +2 -2
- package/dist/{chunk-RZIFF62P.js → chunk-ICJRSNBM.js} +73 -9
- package/dist/chunk-ICJRSNBM.js.map +1 -0
- package/dist/{chunk-32K5CRL2.cjs → chunk-LREN3G7R.cjs} +12 -12
- package/dist/{chunk-32K5CRL2.cjs.map → chunk-LREN3G7R.cjs.map} +1 -1
- package/dist/{chunk-HU5HWEQ5.cjs → chunk-M6BGJVDY.cjs} +19 -19
- package/dist/{chunk-HU5HWEQ5.cjs.map → chunk-M6BGJVDY.cjs.map} +1 -1
- package/dist/{chunk-MTBFE3JF.js → chunk-PNJ5LMVK.js} +16 -2
- package/dist/chunk-PNJ5LMVK.js.map +1 -0
- package/dist/{chunk-VMJTNXOH.js → chunk-Q2KKEE6W.js} +6 -6
- package/dist/{chunk-Q3AO2QWK.cjs → chunk-QAFRLO5N.cjs} +18 -4
- package/dist/chunk-QAFRLO5N.cjs.map +1 -0
- package/dist/{chunk-O5QU5KAC.js → chunk-QYTGHN2T.js} +24 -21
- package/dist/chunk-QYTGHN2T.js.map +1 -0
- package/dist/{chunk-COGPHD7D.js → chunk-RC35ITSR.js} +6 -6
- package/dist/{chunk-3TCCW7B4.js → chunk-RIGB7LP7.js} +4 -4
- package/dist/{chunk-YPNI74ZP.js → chunk-UIAQXIA6.js} +5 -5
- package/dist/{chunk-LAFLYVZ4.js → chunk-UN2BJ7XW.js} +3 -3
- package/dist/{chunk-YIJOSOSA.cjs → chunk-V4MOB3AD.cjs} +12 -12
- package/dist/{chunk-YIJOSOSA.cjs.map → chunk-V4MOB3AD.cjs.map} +1 -1
- package/dist/{chunk-JXSWDXUA.cjs → chunk-VFWEFR3Q.cjs} +40 -40
- package/dist/{chunk-JXSWDXUA.cjs.map → chunk-VFWEFR3Q.cjs.map} +1 -1
- package/dist/{chunk-OPCGDJU7.cjs → chunk-WEZIVLCO.cjs} +37 -34
- package/dist/chunk-WEZIVLCO.cjs.map +1 -0
- package/dist/{chunk-HGKWTKW7.js → chunk-WJRPMULP.js} +2 -2
- package/dist/chunk-WJRPMULP.js.map +1 -0
- package/dist/components/announcement-bar.d.ts +27 -1
- package/dist/components/announcement-bar.d.ts.map +1 -1
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/faq-accordion.d.ts.map +1 -1
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +274 -299
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +110 -135
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +4 -4
- package/dist/components/ui/index.js +3 -3
- package/dist/hooks/api-hooks.d.ts +0 -4
- package/dist/hooks/api-hooks.d.ts.map +1 -1
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/use-self-fetch.d.ts +7 -0
- package/dist/hooks/use-self-fetch.d.ts.map +1 -1
- package/dist/index.cjs +20 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +19 -3
- package/dist/types/announcement.cjs +5 -0
- package/dist/types/announcement.cjs.map +1 -1
- package/dist/types/announcement.d.ts +37 -7
- package/dist/types/announcement.d.ts.map +1 -1
- package/dist/types/announcement.js +5 -1
- package/dist/types/announcement.js.map +1 -1
- package/dist/types/index.cjs +5 -0
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +5 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/announcement-storage.d.ts +39 -7
- package/dist/utils/announcement-storage.d.ts.map +1 -1
- package/dist/utils/app-config.d.ts.map +1 -1
- package/dist/utils/color-analysis.d.ts +13 -0
- package/dist/utils/color-analysis.d.ts.map +1 -1
- package/dist/utils/index.cjs +58 -1
- 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 +52 -2
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/.announcement-bar.md +32 -30
- package/src/components/announcement-bar.tsx +227 -182
- package/src/components/faq/faq-section.tsx +3 -3
- package/src/components/faq-accordion.tsx +17 -20
- package/src/components/features/video.tsx +26 -6
- package/src/components/ui/button/button.tsx +1 -1
- package/src/hooks/.api-hooks.md +1 -4
- package/src/hooks/api-hooks.ts +0 -7
- package/src/hooks/use-self-fetch.ts +29 -1
- package/src/stories/AnnouncementBar.stories.tsx +188 -152
- package/src/types/.announcement.md +2 -2
- package/src/types/announcement.ts +37 -7
- package/src/utils/.announcement-storage.md +19 -24
- package/src/utils/announcement-storage.ts +74 -28
- package/src/utils/app-config.ts +10 -1
- package/src/utils/color-analysis.ts +16 -1
- package/src/utils/index.ts +3 -0
- package/dist/chunk-DH6HTYOV.js.map +0 -1
- package/dist/chunk-GA5AN24T.cjs.map +0 -1
- package/dist/chunk-HGKWTKW7.js.map +0 -1
- package/dist/chunk-J4XR6YYZ.cjs.map +0 -1
- package/dist/chunk-MEU2JEXL.cjs.map +0 -1
- package/dist/chunk-MTBFE3JF.js.map +0 -1
- package/dist/chunk-O5QU5KAC.js.map +0 -1
- package/dist/chunk-OPCGDJU7.cjs.map +0 -1
- package/dist/chunk-Q3AO2QWK.cjs.map +0 -1
- package/dist/chunk-RZIFF62P.js.map +0 -1
- /package/dist/{chunk-NHFQ2LCC.js.map → chunk-3XCNFTFV.js.map} +0 -0
- /package/dist/{chunk-F63VMX4S.js.map → chunk-5XZEY34X.js.map} +0 -0
- /package/dist/{chunk-WM72XFOD.js.map → chunk-BK7QCB5J.js.map} +0 -0
- /package/dist/{chunk-6VAJDGZK.js.map → chunk-F3YLM5PO.js.map} +0 -0
- /package/dist/{chunk-6FIVVZO2.js.map → chunk-GT7AJXKH.js.map} +0 -0
- /package/dist/{chunk-VMJTNXOH.js.map → chunk-Q2KKEE6W.js.map} +0 -0
- /package/dist/{chunk-COGPHD7D.js.map → chunk-RC35ITSR.js.map} +0 -0
- /package/dist/{chunk-3TCCW7B4.js.map → chunk-RIGB7LP7.js.map} +0 -0
- /package/dist/{chunk-YPNI74ZP.js.map → chunk-UIAQXIA6.js.map} +0 -0
- /package/dist/{chunk-LAFLYVZ4.js.map → chunk-UN2BJ7XW.js.map} +0 -0
|
@@ -708,6 +708,15 @@ interface YouTubeFacadeInnerProps {
|
|
|
708
708
|
|
|
709
709
|
const YT_NOCOOKIE_ORIGIN = 'https://www.youtube-nocookie.com';
|
|
710
710
|
|
|
711
|
+
// Poster thumbnail tiers, best → always-present. YouTube serves each size at a
|
|
712
|
+
// fixed path on i.ytimg.com. `maxresdefault` is 1280×720 (crisp) but is only
|
|
713
|
+
// generated for uploads whose source was ≥720p, so it can 404; `mqdefault` is
|
|
714
|
+
// 320×180 and ALWAYS exists. BOTH are 16:9 — unlike the letterboxed 4:3 `hq`/`sd`
|
|
715
|
+
// sizes — so an `object-cover` frame never shows black bars regardless of tier.
|
|
716
|
+
// The facade starts at `maxresdefault` and drops to `mqdefault` on load error,
|
|
717
|
+
// so low-res-only videos render exactly as they did before this change.
|
|
718
|
+
const YT_POSTER_TIERS = ['maxresdefault', 'mqdefault'] as const;
|
|
719
|
+
|
|
711
720
|
// YouTube IFrame Player API state codes — documented integers.
|
|
712
721
|
// https://developers.google.com/youtube/iframe_api_reference#Playback_status
|
|
713
722
|
const YT_STATE_ENDED = 0;
|
|
@@ -757,7 +766,7 @@ function YouTubeFacadeInner({
|
|
|
757
766
|
// SSR-safe: the URL is rebuilt client-side in the same useMemo on
|
|
758
767
|
// hydration when `window` becomes available; the first SSR pass emits
|
|
759
768
|
// the URL without `origin` (no jsapi traffic yet — no iframe mounted).
|
|
760
|
-
const
|
|
769
|
+
const embedUrl = useMemo(() => {
|
|
761
770
|
const params = new URLSearchParams({
|
|
762
771
|
autoplay: '1',
|
|
763
772
|
rel: '0',
|
|
@@ -775,13 +784,23 @@ function YouTubeFacadeInner({
|
|
|
775
784
|
params.set('cc_load_policy', '0');
|
|
776
785
|
params.set('disablekb', '1');
|
|
777
786
|
}
|
|
778
|
-
return {
|
|
779
|
-
embedUrl: `${YT_NOCOOKIE_ORIGIN}/embed/${videoId}?${params.toString()}`,
|
|
780
|
-
posterJpg: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,
|
|
781
|
-
posterWebp: `https://i.ytimg.com/vi_webp/${videoId}/mqdefault.webp`,
|
|
782
|
-
};
|
|
787
|
+
return `${YT_NOCOOKIE_ORIGIN}/embed/${videoId}?${params.toString()}`;
|
|
783
788
|
}, [videoId, minimalControls]);
|
|
784
789
|
|
|
790
|
+
// Poster starts at the highest tier and steps down on load error (see
|
|
791
|
+
// `YT_POSTER_TIERS`). Reset to the top tier whenever the video changes so a
|
|
792
|
+
// new id gets a fresh shot at its `maxresdefault`.
|
|
793
|
+
const [posterTier, setPosterTier] = useState(0);
|
|
794
|
+
useEffect(() => {
|
|
795
|
+
setPosterTier(0);
|
|
796
|
+
}, [videoId]);
|
|
797
|
+
const posterQuality = YT_POSTER_TIERS[posterTier];
|
|
798
|
+
const posterJpg = `https://i.ytimg.com/vi/${videoId}/${posterQuality}.jpg`;
|
|
799
|
+
const posterWebp = `https://i.ytimg.com/vi_webp/${videoId}/${posterQuality}.webp`;
|
|
800
|
+
// On a 404 (e.g. no `maxresdefault`) drop one tier; clamp at the last so a
|
|
801
|
+
// genuinely missing thumbnail can't loop.
|
|
802
|
+
const handlePosterError = () => setPosterTier((tier) => Math.min(tier + 1, YT_POSTER_TIERS.length - 1));
|
|
803
|
+
|
|
785
804
|
// ---------------------------------------------------------------------------
|
|
786
805
|
// YouTube control-fade accelerator (user-locked target: ~1s).
|
|
787
806
|
//
|
|
@@ -894,6 +913,7 @@ function YouTubeFacadeInner({
|
|
|
894
913
|
src={posterJpg}
|
|
895
914
|
alt={title}
|
|
896
915
|
loading="lazy"
|
|
916
|
+
onError={handlePosterError}
|
|
897
917
|
// React 18 wants lowercase (`fetchpriority` DOM attribute);
|
|
898
918
|
// React 19 wants camelCase (`fetchPriority` prop). Detect at
|
|
899
919
|
// module load and spread the right shape so both runtimes
|
|
@@ -29,7 +29,7 @@ const buttonVariants = cva(
|
|
|
29
29
|
size: {
|
|
30
30
|
default: "py-[var(--spacing-system-sf)] px-[var(--spacing-system-m)] text-h3 md:h-12 h-10",
|
|
31
31
|
small: "p-[var(--spacing-system-xs)] text-h5 h-6 md:h-8",
|
|
32
|
-
"small-legacy": "py-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] h-10 text-[14px] font-bold", // Temporary alias for "small"
|
|
32
|
+
"small-legacy": "py-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] h-10 text-[14px] font-bold", // Temporary alias for "small" — deprecated; grep size="small-legacy" (lib + hub) and migrate the remaining consumers before removal
|
|
33
33
|
icon: "p-[var(--spacing-system-sf)] h-11 w-11 md:h-12 md:w-12 [&_svg]:h-4 [&_svg]:w-4 md:[&_svg]:h-6 md:[&_svg]:w-6",
|
|
34
34
|
// Quiet 32px icon target with a 16px glyph, fixed across breakpoints
|
|
35
35
|
// (Carbon ghost sm / Primer medium / shadcn icon-sm all pin 32px;
|
package/src/hooks/.api-hooks.md
CHANGED
|
@@ -5,18 +5,16 @@ This file provides stub React hooks for API data fetching, offering consistent i
|
|
|
5
5
|
|
|
6
6
|
- **`useCategories()`** - Hook for fetching and managing category data with loading/error states
|
|
7
7
|
- **`useVendors()`** - Hook for fetching and managing vendor data with loading/error states
|
|
8
|
-
- **`useAnnouncements()`** - Hook for fetching announcement data with loading state
|
|
9
8
|
- **`useSubcategoryCountByCategory()`** - Hook that returns empty subcategory count data
|
|
10
9
|
|
|
11
10
|
## Usage Example
|
|
12
11
|
|
|
13
12
|
```typescript
|
|
14
|
-
import { useCategories, useVendors
|
|
13
|
+
import { useCategories, useVendors } from './api-hooks';
|
|
15
14
|
|
|
16
15
|
function MyComponent() {
|
|
17
16
|
const { categories, loading: categoriesLoading, error: categoriesError } = useCategories();
|
|
18
17
|
const { vendors, loading: vendorsLoading, error: vendorsError } = useVendors();
|
|
19
|
-
const { announcements, loading: announcementsLoading } = useAnnouncements();
|
|
20
18
|
|
|
21
19
|
if (categoriesLoading || vendorsLoading) {
|
|
22
20
|
return <div>Loading...</div>;
|
|
@@ -30,7 +28,6 @@ function MyComponent() {
|
|
|
30
28
|
<div>
|
|
31
29
|
<h2>Categories: {categories.length}</h2>
|
|
32
30
|
<h2>Vendors: {vendors.length}</h2>
|
|
33
|
-
<h2>Announcements: {announcements.length}</h2>
|
|
34
31
|
</div>
|
|
35
32
|
);
|
|
36
33
|
}
|
package/src/hooks/api-hooks.ts
CHANGED
|
@@ -17,13 +17,6 @@ export function useVendors() {
|
|
|
17
17
|
return { vendors, loading, error };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function useAnnouncements() {
|
|
21
|
-
const [announcements, setAnnouncements] = useState([]);
|
|
22
|
-
const [loading, setLoading] = useState(false);
|
|
23
|
-
|
|
24
|
-
return { announcements, loading };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
20
|
export function useSubcategoryCountByCategory() {
|
|
28
21
|
return { data: {}, loading: false };
|
|
29
22
|
}
|
|
@@ -31,19 +31,30 @@ export interface UseSelfFetchResult<T> {
|
|
|
31
31
|
* - a `cancelled` guard ensures an in-flight response from a STALE `url`
|
|
32
32
|
* (e.g. a fast pagination / section toggle) can't overwrite a newer one.
|
|
33
33
|
* - `reload()` bumps an internal key to retry after an error.
|
|
34
|
+
* - `revalidateOnVisibleAfterMs` (opt-in) → re-fetch when the tab becomes
|
|
35
|
+
* visible AND the held data is older than the threshold. Event-driven
|
|
36
|
+
* freshness with ZERO background requests — the replacement for
|
|
37
|
+
* setInterval polling. Data age counts from the last completed fetch, or
|
|
38
|
+
* from mount when `initialData` seeded it (an SSR seed is milliseconds
|
|
39
|
+
* old — without that, the first refocus would always refire).
|
|
34
40
|
*
|
|
35
41
|
* Re-fetches whenever `url` changes, so callers fold all query params INTO the
|
|
36
42
|
* url string (the url IS the cache key).
|
|
37
43
|
*/
|
|
38
44
|
export function useSelfFetch<T>(
|
|
39
45
|
url: string | null,
|
|
40
|
-
options?: { initialData?: T },
|
|
46
|
+
options?: { initialData?: T; revalidateOnVisibleAfterMs?: number },
|
|
41
47
|
): UseSelfFetchResult<T> {
|
|
42
48
|
const initialData = options?.initialData
|
|
49
|
+
const revalidateOnVisibleAfterMs = options?.revalidateOnVisibleAfterMs
|
|
43
50
|
const [data, setData] = useState<T | null>(initialData ?? null)
|
|
44
51
|
const [isLoading, setIsLoading] = useState(initialData === undefined && url !== null)
|
|
45
52
|
const [error, setError] = useState(false)
|
|
46
53
|
const [reloadKey, setReloadKey] = useState(0)
|
|
54
|
+
// When the held data was obtained: seeded at mount for SSR-hydrated data,
|
|
55
|
+
// stamped after each completed fetch. Drives the visibility revalidation age
|
|
56
|
+
// check. 0 = nothing held yet (first visible event may fetch).
|
|
57
|
+
const dataAtRef = useRef<number>(initialData !== undefined ? Date.now() : 0)
|
|
47
58
|
// The url whose data we currently hold — seeded from SSR `initialData`, then set after
|
|
48
59
|
// each completed fetch. The effect skips the fetch when this equals `url` (so server-
|
|
49
60
|
// rendered data isn't re-fetched on mount). A VALUE compare (not a one-shot flag) so the
|
|
@@ -78,6 +89,7 @@ export function useSelfFetch<T>(
|
|
|
78
89
|
if (!cancelled) {
|
|
79
90
|
setData(json)
|
|
80
91
|
dataUrlRef.current = url // remember the url we now hold data for
|
|
92
|
+
dataAtRef.current = Date.now()
|
|
81
93
|
}
|
|
82
94
|
} catch (err) {
|
|
83
95
|
// AbortError on cleanup (unmount / stale-url change / React StrictMode's dev
|
|
@@ -100,6 +112,22 @@ export function useSelfFetch<T>(
|
|
|
100
112
|
// `url` folds in every query param; `reloadKey` drives retry.
|
|
101
113
|
}, [url, reloadKey])
|
|
102
114
|
|
|
115
|
+
// Opt-in visibility-driven revalidation. Registered only when the option is
|
|
116
|
+
// set and fetching is enabled; SSR-safe (effects don't run on the server)
|
|
117
|
+
// and StrictMode-safe (idempotent listener, cleaned up per mount cycle).
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!revalidateOnVisibleAfterMs || url === null) return
|
|
120
|
+
const onVisible = () => {
|
|
121
|
+
if (document.visibilityState !== 'visible') return
|
|
122
|
+
if (Date.now() - dataAtRef.current < revalidateOnVisibleAfterMs) return
|
|
123
|
+
// Same mechanics as reload(): clear the held-url skip, bump the key.
|
|
124
|
+
dataUrlRef.current = null
|
|
125
|
+
setReloadKey((k) => k + 1)
|
|
126
|
+
}
|
|
127
|
+
document.addEventListener('visibilitychange', onVisible)
|
|
128
|
+
return () => document.removeEventListener('visibilitychange', onVisible)
|
|
129
|
+
}, [url, revalidateOnVisibleAfterMs])
|
|
130
|
+
|
|
103
131
|
return {
|
|
104
132
|
data,
|
|
105
133
|
setData,
|
|
@@ -1,36 +1,24 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
2
|
import type { Announcement } from '../types/announcement'
|
|
3
3
|
import { AnnouncementBar } from '../components/announcement-bar'
|
|
4
|
+
import { announcementDismissCookieName } from '../utils/announcement-storage'
|
|
5
|
+
import { getAppType } from '../utils/app-config'
|
|
6
|
+
import { EndpointsRuntimeContext, type EndpointsRuntime } from '../contexts/endpoints-runtime-context'
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
9
|
+
* Stories drive the bar through its real `initialAnnouncement` prop (the hub's
|
|
10
|
+
* SSR-seed path) — no fetch mock needed; with no EndpointsRuntime provider
|
|
11
|
+
* mounted the bar never fetches. The decorator clears any dismissal state left
|
|
12
|
+
* by a previous interaction so every story starts visible.
|
|
8
13
|
*/
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
})
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const originalFetch = window.fetch
|
|
20
|
-
window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
21
|
-
const url = typeof input === 'string' ? input : input.toString()
|
|
22
|
-
if (url.includes('/api/announcements/active')) {
|
|
23
|
-
return new Response(JSON.stringify({ announcement }), {
|
|
24
|
-
status: 200,
|
|
25
|
-
headers: { 'Content-Type': 'application/json' },
|
|
26
|
-
})
|
|
14
|
+
function clearDismissals() {
|
|
15
|
+
const platform = getAppType()
|
|
16
|
+
document.cookie = `${announcementDismissCookieName(platform)}=; path=/; max-age=0`
|
|
17
|
+
Object.keys(localStorage).forEach((key) => {
|
|
18
|
+
if (key.includes('-announcement-') && key.endsWith('-dismissed')) {
|
|
19
|
+
localStorage.removeItem(key)
|
|
27
20
|
}
|
|
28
|
-
|
|
29
|
-
}) as typeof window.fetch
|
|
30
|
-
|
|
31
|
-
return () => {
|
|
32
|
-
window.fetch = originalFetch
|
|
33
|
-
}
|
|
21
|
+
})
|
|
34
22
|
}
|
|
35
23
|
|
|
36
24
|
const now = new Date().toISOString()
|
|
@@ -41,7 +29,6 @@ const baseAnnouncement: Announcement = {
|
|
|
41
29
|
description: 'Check out our latest updates and improvements to the platform.',
|
|
42
30
|
background_color: '#FFD951',
|
|
43
31
|
icon_name: 'rocket',
|
|
44
|
-
platform_id: 'openframe',
|
|
45
32
|
is_active: true,
|
|
46
33
|
created_at: now,
|
|
47
34
|
updated_at: now,
|
|
@@ -55,10 +42,16 @@ const meta = {
|
|
|
55
42
|
docs: {
|
|
56
43
|
description: {
|
|
57
44
|
component:
|
|
58
|
-
'A top-of-page announcement bar
|
|
45
|
+
'A top-of-page announcement bar. The host seeds it with `initialAnnouncement` (SSR path) or lets it self-fetch via the endpoints runtime. Supports SVG/PNG icons, CTA buttons, contrast-aware text, animated dismiss, and responsive mobile layout.',
|
|
59
46
|
},
|
|
60
47
|
},
|
|
61
48
|
},
|
|
49
|
+
decorators: [
|
|
50
|
+
(Story) => {
|
|
51
|
+
clearDismissals()
|
|
52
|
+
return <Story />
|
|
53
|
+
},
|
|
54
|
+
],
|
|
62
55
|
tags: ['autodocs'],
|
|
63
56
|
} satisfies Meta<typeof AnnouncementBar>
|
|
64
57
|
|
|
@@ -69,189 +62,232 @@ type Story = StoryObj<typeof meta>
|
|
|
69
62
|
* Default announcement with an icon and description.
|
|
70
63
|
*/
|
|
71
64
|
export const Default: Story = {
|
|
72
|
-
|
|
73
|
-
(Story) => {
|
|
74
|
-
const cleanup = mockAnnouncement(baseAnnouncement)
|
|
75
|
-
return (
|
|
76
|
-
<>
|
|
77
|
-
<Story />
|
|
78
|
-
{/* cleanup on unmount is handled by storybook re-renders */}
|
|
79
|
-
</>
|
|
80
|
-
)
|
|
81
|
-
},
|
|
82
|
-
],
|
|
65
|
+
args: { initialAnnouncement: baseAnnouncement },
|
|
83
66
|
}
|
|
84
67
|
|
|
85
68
|
/**
|
|
86
69
|
* Announcement with a CTA button.
|
|
87
70
|
*/
|
|
88
71
|
export const WithCTA: Story = {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
cta_button_text_color: '#FFFFFF',
|
|
106
|
-
})
|
|
107
|
-
return <Story />
|
|
72
|
+
args: {
|
|
73
|
+
initialAnnouncement: {
|
|
74
|
+
...baseAnnouncement,
|
|
75
|
+
id: 'story-cta',
|
|
76
|
+
title: 'OpenFrame v2.0 is here!',
|
|
77
|
+
description: 'Explore the new dashboard, improved device management, and more.',
|
|
78
|
+
background_color: '#FFD951',
|
|
79
|
+
icon_name: 'star',
|
|
80
|
+
cta_enabled: true,
|
|
81
|
+
cta_text: 'Learn More',
|
|
82
|
+
cta_url: 'https://example.com',
|
|
83
|
+
cta_target: '_blank',
|
|
84
|
+
cta_icon_name: 'rocket',
|
|
85
|
+
cta_show_icon: true,
|
|
86
|
+
cta_button_background_color: '#1A1A1A',
|
|
87
|
+
cta_button_text_color: '#FFFFFF',
|
|
108
88
|
},
|
|
109
|
-
|
|
89
|
+
},
|
|
110
90
|
}
|
|
111
91
|
|
|
112
92
|
/**
|
|
113
93
|
* Announcement with a CTA button that opens in the same tab.
|
|
114
94
|
*/
|
|
115
95
|
export const WithCTASameTab: Story = {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
cta_target: '_self',
|
|
129
|
-
})
|
|
130
|
-
return <Story />
|
|
96
|
+
args: {
|
|
97
|
+
initialAnnouncement: {
|
|
98
|
+
...baseAnnouncement,
|
|
99
|
+
id: 'story-cta-same',
|
|
100
|
+
title: 'Scheduled Maintenance',
|
|
101
|
+
description: 'We will perform maintenance on Saturday from 2:00 AM to 4:00 AM UTC.',
|
|
102
|
+
background_color: '#FFE082',
|
|
103
|
+
icon_name: 'info',
|
|
104
|
+
cta_enabled: true,
|
|
105
|
+
cta_text: 'View Status',
|
|
106
|
+
cta_url: '/status',
|
|
107
|
+
cta_target: '_self',
|
|
131
108
|
},
|
|
132
|
-
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Dark background — text flips to the light shade via pickReadableTextColor.
|
|
114
|
+
*/
|
|
115
|
+
export const DarkBackground: Story = {
|
|
116
|
+
args: {
|
|
117
|
+
initialAnnouncement: {
|
|
118
|
+
...baseAnnouncement,
|
|
119
|
+
id: 'story-dark',
|
|
120
|
+
title: 'Contrast-aware text',
|
|
121
|
+
description: 'On a dark admin-chosen background the foreground goes light automatically.',
|
|
122
|
+
background_color: '#1F2937',
|
|
123
|
+
icon_name: 'megaphone',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
133
126
|
}
|
|
134
127
|
|
|
135
128
|
/**
|
|
136
129
|
* Announcement with a blue background.
|
|
137
130
|
*/
|
|
138
131
|
export const BlueBackground: Story = {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
icon_name: 'megaphone',
|
|
148
|
-
})
|
|
149
|
-
return <Story />
|
|
132
|
+
args: {
|
|
133
|
+
initialAnnouncement: {
|
|
134
|
+
...baseAnnouncement,
|
|
135
|
+
id: 'story-blue',
|
|
136
|
+
title: 'Join our Community',
|
|
137
|
+
description: 'Connect with other MSPs and share best practices.',
|
|
138
|
+
background_color: '#60A5FA',
|
|
139
|
+
icon_name: 'megaphone',
|
|
150
140
|
},
|
|
151
|
-
|
|
141
|
+
},
|
|
152
142
|
}
|
|
153
143
|
|
|
154
144
|
/**
|
|
155
145
|
* Announcement with a green background.
|
|
156
146
|
*/
|
|
157
147
|
export const GreenBackground: Story = {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
icon_name: 'bell',
|
|
167
|
-
})
|
|
168
|
-
return <Story />
|
|
148
|
+
args: {
|
|
149
|
+
initialAnnouncement: {
|
|
150
|
+
...baseAnnouncement,
|
|
151
|
+
id: 'story-green',
|
|
152
|
+
title: 'All Systems Operational',
|
|
153
|
+
description: 'Everything is running smoothly. No issues detected.',
|
|
154
|
+
background_color: '#86EFAC',
|
|
155
|
+
icon_name: 'bell',
|
|
169
156
|
},
|
|
170
|
-
|
|
157
|
+
},
|
|
171
158
|
}
|
|
172
159
|
|
|
173
160
|
/**
|
|
174
161
|
* Announcement with OpenFrame logo icon.
|
|
175
162
|
*/
|
|
176
163
|
export const WithOpenFrameLogo: Story = {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
cta_button_text_color: '#FFD951',
|
|
192
|
-
})
|
|
193
|
-
return <Story />
|
|
164
|
+
args: {
|
|
165
|
+
initialAnnouncement: {
|
|
166
|
+
...baseAnnouncement,
|
|
167
|
+
id: 'story-logo',
|
|
168
|
+
title: 'Welcome to OpenFrame',
|
|
169
|
+
description: 'Your open-source IT infrastructure management platform.',
|
|
170
|
+
background_color: '#FFD951',
|
|
171
|
+
icon_name: 'openframe-logo',
|
|
172
|
+
cta_enabled: true,
|
|
173
|
+
cta_text: 'Get Started',
|
|
174
|
+
cta_url: '/getting-started',
|
|
175
|
+
cta_target: '_self',
|
|
176
|
+
cta_button_background_color: '#000000',
|
|
177
|
+
cta_button_text_color: '#FFD951',
|
|
194
178
|
},
|
|
195
|
-
|
|
179
|
+
},
|
|
196
180
|
}
|
|
197
181
|
|
|
198
182
|
/**
|
|
199
183
|
* Announcement with a PNG icon.
|
|
200
184
|
*/
|
|
201
185
|
export const WithPNGIcon: Story = {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
icon_url: 'https://placehold.co/32x32/1a1a1a/white?text=OF',
|
|
211
|
-
})
|
|
212
|
-
return <Story />
|
|
186
|
+
args: {
|
|
187
|
+
initialAnnouncement: {
|
|
188
|
+
...baseAnnouncement,
|
|
189
|
+
id: 'story-png',
|
|
190
|
+
title: 'Partner Program Launch',
|
|
191
|
+
description: 'Become an OpenFrame certified partner and grow your business.',
|
|
192
|
+
background_color: '#C4B5FD',
|
|
193
|
+
icon_url: 'https://placehold.co/32x32/1a1a1a/white?text=OF',
|
|
213
194
|
},
|
|
214
|
-
|
|
195
|
+
},
|
|
215
196
|
}
|
|
216
197
|
|
|
217
198
|
/**
|
|
218
199
|
* Long title and description to test truncation.
|
|
219
200
|
*/
|
|
220
201
|
export const LongContent: Story = {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
description
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
cta_target: '_self',
|
|
235
|
-
})
|
|
236
|
-
return <Story />
|
|
202
|
+
args: {
|
|
203
|
+
initialAnnouncement: {
|
|
204
|
+
...baseAnnouncement,
|
|
205
|
+
id: 'story-long',
|
|
206
|
+
title: 'This is a very long announcement title that should be truncated on smaller screens to prevent layout issues',
|
|
207
|
+
description:
|
|
208
|
+
'This is an extremely long description that goes into great detail about the announcement. It should be truncated on the desktop view and hidden on mobile view to keep the bar compact and readable.',
|
|
209
|
+
background_color: '#FCA5A5',
|
|
210
|
+
icon_name: 'info',
|
|
211
|
+
cta_enabled: true,
|
|
212
|
+
cta_text: 'Read Full Announcement',
|
|
213
|
+
cta_url: '/announcements/long',
|
|
214
|
+
cta_target: '_self',
|
|
237
215
|
},
|
|
238
|
-
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* previewMode — the admin live-preview path: render-only, inert dismiss,
|
|
221
|
+
* no fetch and no storage side effects.
|
|
222
|
+
*/
|
|
223
|
+
export const PreviewMode: Story = {
|
|
224
|
+
args: {
|
|
225
|
+
initialAnnouncement: {
|
|
226
|
+
...baseAnnouncement,
|
|
227
|
+
id: 'story-preview',
|
|
228
|
+
title: 'Admin preview',
|
|
229
|
+
description: 'Exactly the live bar, but inert: dismiss does nothing.',
|
|
230
|
+
},
|
|
231
|
+
previewMode: true,
|
|
232
|
+
},
|
|
239
233
|
}
|
|
240
234
|
|
|
241
235
|
/**
|
|
242
236
|
* No active announcement — the bar renders nothing.
|
|
243
237
|
*/
|
|
244
238
|
export const NoAnnouncement: Story = {
|
|
239
|
+
args: { initialAnnouncement: null },
|
|
240
|
+
decorators: [
|
|
241
|
+
(Story) => (
|
|
242
|
+
<div>
|
|
243
|
+
<Story />
|
|
244
|
+
<p className="p-4 text-sm text-gray-500">
|
|
245
|
+
No announcement is active — the bar renders nothing above this text.
|
|
246
|
+
</p>
|
|
247
|
+
</div>
|
|
248
|
+
),
|
|
249
|
+
],
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Client-only mode (no SSR) — the react-embedding-example structure: the
|
|
254
|
+
* embedder mounts an EndpointsRuntime provider (announcementsUrl is a fixed
|
|
255
|
+
* suffix under its one content base) and drops in a PROP-LESS bar. No
|
|
256
|
+
* platform knob anywhere on the client: the proxied server resolves its own
|
|
257
|
+
* platform via currentPlatform(). The decorator mocks the fetch.
|
|
258
|
+
*/
|
|
259
|
+
const embedEndpoints: EndpointsRuntime = {
|
|
260
|
+
announcementsUrl: '/content/api/announcements/active',
|
|
261
|
+
accessCode: { validateUrl: '/content/api/validate-access-code', consumeUrl: '/content/api/consume-access-code' },
|
|
262
|
+
contactUrl: '/content/api/contact',
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const ClientOnlyEmbed: Story = {
|
|
266
|
+
args: {},
|
|
245
267
|
decorators: [
|
|
246
268
|
(Story) => {
|
|
247
|
-
|
|
269
|
+
const originalFetch = window.fetch
|
|
270
|
+
window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
271
|
+
const requestUrl = typeof input === 'string' ? input : input.toString()
|
|
272
|
+
if (requestUrl.includes('/content/api/announcements/active')) {
|
|
273
|
+
return new Response(
|
|
274
|
+
JSON.stringify({
|
|
275
|
+
announcement: {
|
|
276
|
+
...baseAnnouncement,
|
|
277
|
+
id: 'story-embed',
|
|
278
|
+
title: 'Client-only embed',
|
|
279
|
+
description: 'Fetched on mount through the EndpointsRuntime provider, no SSR.',
|
|
280
|
+
},
|
|
281
|
+
}),
|
|
282
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
return originalFetch(input, init)
|
|
286
|
+
}) as typeof window.fetch
|
|
248
287
|
return (
|
|
249
|
-
<
|
|
288
|
+
<EndpointsRuntimeContext.Provider value={embedEndpoints}>
|
|
250
289
|
<Story />
|
|
251
|
-
|
|
252
|
-
No announcement is active — the bar renders nothing above this text.
|
|
253
|
-
</p>
|
|
254
|
-
</div>
|
|
290
|
+
</EndpointsRuntimeContext.Provider>
|
|
255
291
|
)
|
|
256
292
|
},
|
|
257
293
|
],
|
|
@@ -4,7 +4,7 @@ TypeScript type definitions for the announcement system, providing comprehensive
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
6
6
|
### Core Database Models
|
|
7
|
-
- **`Announcement`** - Main announcement entity
|
|
7
|
+
- **`Announcement`** - Main announcement entity; platform targeting via hydrated `announcement_platforms?: EntityPlatformAssoc[]` (no singular platform_id/platform)
|
|
8
8
|
- **`AnnouncementAsset`** - File attachments linked to announcements (images, documents, etc.)
|
|
9
9
|
|
|
10
10
|
### API Types
|
|
@@ -30,7 +30,7 @@ const newAnnouncement: CreateAnnouncementData = {
|
|
|
30
30
|
background_color: "#FFC008",
|
|
31
31
|
icon_type: "svg",
|
|
32
32
|
icon_svg_name: "rocket",
|
|
33
|
-
|
|
33
|
+
platforms: ["openframe-platform-id"],
|
|
34
34
|
is_active: true,
|
|
35
35
|
cta_enabled: true,
|
|
36
36
|
cta_text: "Learn More",
|