@flamingo-stack/openframe-frontend-core 0.0.853 → 0.0.855
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/build/export-map.json +1 -1
- package/dist/{case-study-card-WP5Z234Q.cjs → case-study-card-6VAMNHLU.cjs} +8 -8
- package/dist/{chunk-2W22LY3W.cjs → chunk-2JCA7BRK.cjs} +19 -19
- package/dist/{chunk-53QPG7SJ.cjs → chunk-2MCPK6AX.cjs} +28 -28
- package/dist/{chunk-QOW2GP3V.cjs → chunk-2P72KAG5.cjs} +23 -23
- package/dist/{chunk-NMWLXHFH.cjs → chunk-3GJ75OUL.cjs} +183 -183
- package/dist/{chunk-P63CRPWI.cjs → chunk-4H3J57AN.cjs} +25 -25
- package/dist/{chunk-6GWRPW6B.cjs → chunk-5NK3FUHY.cjs} +4 -4
- package/dist/{chunk-DLTPSP5F.cjs → chunk-5TYG5JNB.cjs} +2 -2
- package/dist/{chunk-EIACEY2L.cjs → chunk-65NMKAPQ.cjs} +12 -12
- package/dist/{chunk-IGVAPIEW.cjs → chunk-6C4OR4MZ.cjs} +2 -2
- package/dist/{chunk-GG466XFQ.cjs → chunk-6DMH4GXN.cjs} +30 -5
- package/dist/{chunk-EF2FZ7GT.cjs → chunk-6VVQJU76.cjs} +54 -54
- package/dist/{chunk-G7GF4PWA.cjs → chunk-77GX573I.cjs} +9 -9
- package/dist/{chunk-7RZHSBNX.cjs → chunk-AOOHJLXS.cjs} +90 -90
- package/dist/{chunk-O3EKOXFZ.cjs → chunk-AYMY2VNB.cjs} +37 -43
- package/dist/{chunk-67CZ2Q5K.cjs → chunk-BSPIGZCC.cjs} +36 -36
- package/dist/{chunk-FW7RKWPY.cjs → chunk-D6GEMVHD.cjs} +3 -3
- package/dist/{chunk-KEV6F3QN.cjs → chunk-DPNFI5XM.cjs} +100 -100
- package/dist/{chunk-6V2M2X3M.cjs → chunk-DTZ6KASI.cjs} +6 -6
- package/dist/{chunk-SSKMGH5O.cjs → chunk-DXVGGWLG.cjs} +10 -10
- package/dist/{chunk-IO6NN3SL.cjs → chunk-EOUEUYCQ.cjs} +54 -54
- package/dist/{chunk-5RMBLOJ5.cjs → chunk-ESGE7CUX.cjs} +14 -14
- package/dist/{chunk-ETZFBB7O.cjs → chunk-EXQSIX2I.cjs} +4 -4
- package/dist/{chunk-IC4IJJB2.cjs → chunk-F33EAPD2.cjs} +124 -124
- package/dist/{chunk-COWMXUHN.cjs → chunk-GK577ZLJ.cjs} +6 -14
- package/dist/{chunk-E4MFQKPC.cjs → chunk-GZYRDLJW.cjs} +103 -103
- package/dist/{chunk-LFEURIZN.cjs → chunk-HML3JUXF.cjs} +324 -322
- package/dist/{chunk-BNHTBLAG.cjs → chunk-J3CFRGZ2.cjs} +17 -17
- package/dist/{chunk-USPUZO53.cjs → chunk-KZAQPRTA.cjs} +9 -11
- package/dist/{chunk-HTYJUMFB.cjs → chunk-LSLRJJFP.cjs} +2 -2
- package/dist/{chunk-TBGQMHCG.cjs → chunk-MINFN3IG.cjs} +8 -8
- package/dist/{chunk-KHOB3TES.cjs → chunk-MX52LZ6M.cjs} +6 -6
- package/dist/{chunk-OH5TXFRR.cjs → chunk-O4CPT3RK.cjs} +7 -7
- package/dist/{chunk-HF3V6WBJ.cjs → chunk-OTHQ2KQW.cjs} +3 -3
- package/dist/{chunk-IUDBG3HJ.cjs → chunk-PCQ2HKU3.cjs} +5 -5
- package/dist/{chunk-BNG6JOGZ.cjs → chunk-QPHXYE5P.cjs} +27 -27
- package/dist/{chunk-UO2EJGRJ.cjs → chunk-QZXCUY36.cjs} +53 -53
- package/dist/{chunk-COJG2ATY.cjs → chunk-RIJU4HZ2.cjs} +51 -55
- package/dist/{chunk-KKFDRCQF.cjs → chunk-RY7INTYU.cjs} +14 -1
- package/dist/{chunk-GI3MMBP3.cjs → chunk-SQ7YHWEP.cjs} +228 -15
- package/dist/{chunk-U767P4JU.cjs → chunk-STV6VJ3U.cjs} +21 -21
- package/dist/chunk-T54I5ZZY.cjs +62 -0
- package/dist/{chunk-UV45EPXN.cjs → chunk-TW5Y4D5I.cjs} +7 -7
- package/dist/{chunk-FX35L4Y6.cjs → chunk-XCQ5I27Z.cjs} +3 -3
- package/dist/{chunk-AZEO7HBB.cjs → chunk-Y44EF4JW.cjs} +2 -2
- package/dist/{chunk-W7OFAROY.cjs → chunk-YSXVRTGT.cjs} +9 -12
- package/dist/{chunk-GI6ECCWB.cjs → chunk-ZUGJLCYP.cjs} +1272 -856
- package/dist/{chunk-KNN2GCMW.cjs → chunk-ZYAVIRQV.cjs} +25 -25
- package/dist/{cloud-tenants-QUL2XPAS.cjs → cloud-tenants-2XFIDDGU.cjs} +28 -29
- package/dist/components/case-studies/index.cjs +44 -45
- package/dist/components/chat/chat-message-row.d.ts +1 -1
- package/dist/components/chat/chat-message-row.d.ts.map +1 -1
- package/dist/components/chat/chat-message-row.js +4 -4
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts +7 -13
- package/dist/components/chat/entity-cards/use-cover-image-fallback.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/use-cover-image-fallback.js +2 -8
- package/dist/components/chat/hooks/use-chat-attachment-image-gallery.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-attachment-image-gallery.js +8 -4
- package/dist/components/chat/index.cjs +22 -23
- package/dist/components/contact/index.cjs +29 -30
- package/dist/components/docs/index.cjs +32 -33
- package/dist/components/embeds/index.cjs +29 -30
- package/dist/components/faq/index.cjs +30 -31
- package/dist/components/features/cloud-tenants/index.d.ts +7 -5
- package/dist/components/features/cloud-tenants/index.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/index.js +8 -1
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts +26 -0
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.d.ts.map +1 -0
- package/dist/components/features/cloud-tenants/single-customer-tenants-table-view.js +176 -0
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts +4 -2
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-access-cell.js +3 -2
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts +12 -5
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-customer-cell.js +38 -7
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts +7 -0
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.d.ts.map +1 -0
- package/dist/components/features/cloud-tenants/tenant-licenses-cell.js +17 -0
- package/dist/components/features/cloud-tenants/tenant-presentation.d.ts +19 -0
- package/dist/components/features/cloud-tenants/tenant-presentation.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-presentation.js +24 -3
- package/dist/components/features/cloud-tenants/tenant-row.d.ts +35 -8
- package/dist/components/features/cloud-tenants/tenant-row.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts +9 -3
- package/dist/components/features/cloud-tenants/tenants-table-columns.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-columns.js +5 -3
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts +27 -6
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-view.js +75 -17
- package/dist/components/features/devices/device-directory.d.ts +18 -0
- package/dist/components/features/devices/device-directory.d.ts.map +1 -0
- package/dist/components/features/devices/device-directory.js +35 -0
- package/dist/components/features/devices/devices-filter-toolbar.d.ts +3 -1
- package/dist/components/features/devices/devices-filter-toolbar.d.ts.map +1 -1
- package/dist/components/features/devices/devices-filter-toolbar.js +2 -1
- package/dist/components/features/devices/devices-panel-view.d.ts +3 -1
- package/dist/components/features/devices/devices-panel-view.d.ts.map +1 -1
- package/dist/components/features/devices/devices-panel-view.js +11 -2
- package/dist/components/features/devices/devices-table-columns.d.ts +17 -3
- package/dist/components/features/devices/devices-table-columns.d.ts.map +1 -1
- package/dist/components/features/devices/devices-table-columns.js +80 -10
- package/dist/components/features/devices/index.d.ts +1 -0
- package/dist/components/features/devices/index.d.ts.map +1 -1
- package/dist/components/features/devices/index.js +8 -0
- package/dist/components/features/devices/types.d.ts +14 -0
- package/dist/components/features/devices/types.d.ts.map +1 -1
- package/dist/components/features/devices/use-tag-filter-modal.d.ts +1 -0
- package/dist/components/features/devices/use-tag-filter-modal.d.ts.map +1 -1
- package/dist/components/features/devices/use-tag-filter-modal.js +13 -8
- package/dist/components/features/index.cjs +42 -29
- package/dist/components/help-center-pages/index.cjs +134 -135
- package/dist/components/index.cjs +126 -113
- package/dist/components/meeting-scheduler/index.cjs +102 -103
- package/dist/components/navigation/app-layout.js +2 -2
- package/dist/components/navigation/index.cjs +31 -32
- package/dist/components/onboarding-guides/index.cjs +31 -32
- package/dist/components/product-demo/index.cjs +39 -40
- package/dist/components/related-content/index.cjs +16 -17
- package/dist/components/tickets/index.cjs +35 -36
- package/dist/components/ui/date-picker.d.ts +2 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/date-picker.js +19 -6
- package/dist/components/ui/entity-image.d.ts +1 -2
- package/dist/components/ui/entity-image.d.ts.map +1 -1
- package/dist/components/ui/entity-image.js +4 -8
- package/dist/components/ui/index.cjs +28 -29
- package/dist/components/ui/markdown/base-components.d.ts +0 -13
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/markdown/base-components.js +7 -7
- package/dist/components/ui/square-avatar.d.ts.map +1 -1
- package/dist/components/ui/square-avatar.js +3 -8
- package/dist/{devices-LNB4CYPP.cjs → devices-O3DYGIZB.cjs} +29 -30
- package/dist/embed-shims/index.cjs +3 -2
- package/dist/embed-shims/next-image.cjs +3 -2
- package/dist/embed-shims/next-image.d.ts +2 -0
- package/dist/embed-shims/next-image.d.ts.map +1 -1
- package/dist/embed-shims/next-image.js +14 -4
- package/dist/{embeddable-chat-SJEEJI4N.cjs → embeddable-chat-QD7NFD2H.cjs} +100 -101
- package/dist/hooks/index.cjs +27 -16
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/ui/use-content-breakpoint.d.ts +8 -0
- package/dist/hooks/ui/use-content-breakpoint.d.ts.map +1 -1
- package/dist/hooks/ui/use-content-breakpoint.js +17 -3
- package/dist/hooks/ui/use-media-query.d.ts +1 -0
- package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
- package/dist/hooks/ui/use-media-query.js +3 -1
- package/dist/hooks/use-authed-asset-src.d.ts +77 -2
- package/dist/hooks/use-authed-asset-src.d.ts.map +1 -1
- package/dist/hooks/use-authed-asset-src.js +185 -73
- package/dist/hooks/use-authed-image-src.d.ts +11 -4
- package/dist/hooks/use-authed-image-src.d.ts.map +1 -1
- package/dist/hooks/use-authed-image-src.js +6 -2
- package/dist/hooks/use-image-fallback.d.ts +16 -0
- package/dist/hooks/use-image-fallback.d.ts.map +1 -0
- package/dist/hooks/use-image-fallback.js +14 -0
- package/dist/index.cjs +58 -36
- package/dist/{knowledge-UTWKB6XG.cjs → knowledge-K7AFNZNO.cjs} +29 -30
- package/dist/{logs-PMXIWMBQ.cjs → logs-MZ3BR2FR.cjs} +35 -36
- package/dist/{policies-6DCMFDL7.cjs → policies-XVKQ2W7S.cjs} +32 -33
- package/dist/{remote-session-SG44CNOK.cjs → remote-session-VOZJXDM4.cjs} +13 -14
- package/dist/{scripts-2G7LXW7Z.cjs → scripts-IBPMBS6G.cjs} +35 -36
- package/dist/{software-update-MYLKHDHC.cjs → software-update-PXTANMAO.cjs} +32 -33
- package/dist/{tickets-board-SQY2Y6QV.cjs → tickets-board-POYZS7BW.cjs} +16 -17
- package/dist/utils/app-origins.d.ts +3 -6
- package/dist/utils/app-origins.d.ts.map +1 -1
- package/dist/utils/app-origins.js +8 -1
- package/dist/utils/embed-authed-fetch.d.ts +46 -26
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/embed-authed-fetch.js +24 -8
- package/dist/utils/index.cjs +4 -1
- package/dist/utils/presentation-for.d.ts +6 -0
- package/dist/utils/presentation-for.d.ts.map +1 -0
- package/dist/utils/presentation-for.js +7 -0
- package/dist/{video-file-player-XMOB234V.cjs → video-file-player-RIOT24NT.cjs} +3 -4
- package/package.json +1 -1
- package/src/components/chat/chat-message-row.tsx +8 -7
- package/src/components/chat/entity-cards/use-cover-image-fallback.ts +8 -28
- package/src/components/chat/hooks/use-chat-attachment-image-gallery.tsx +19 -8
- package/src/components/features/cloud-tenants/index.ts +8 -3
- package/src/components/features/cloud-tenants/single-customer-tenants-table-view.tsx +234 -0
- package/src/components/features/cloud-tenants/tenant-access-cell.tsx +12 -5
- package/src/components/features/cloud-tenants/tenant-customer-cell.tsx +48 -12
- package/src/components/features/cloud-tenants/tenant-licenses-cell.tsx +28 -0
- package/src/components/features/cloud-tenants/tenant-presentation.ts +46 -4
- package/src/components/features/cloud-tenants/tenant-row.ts +38 -8
- package/src/components/features/cloud-tenants/tenants-table-columns.ts +8 -5
- package/src/components/features/cloud-tenants/tenants-table-view.tsx +123 -28
- package/src/components/features/devices/device-directory.ts +55 -0
- package/src/components/features/devices/devices-filter-toolbar.tsx +4 -1
- package/src/components/features/devices/devices-panel-view.tsx +12 -2
- package/src/components/features/devices/devices-table-columns.tsx +123 -8
- package/src/components/features/devices/index.ts +7 -0
- package/src/components/features/devices/types.ts +14 -0
- package/src/components/features/devices/use-tag-filter-modal.ts +32 -14
- package/src/components/navigation/app-layout.tsx +4 -4
- package/src/components/ui/date-picker.tsx +21 -3
- package/src/components/ui/entity-image.tsx +4 -12
- package/src/components/ui/markdown/base-components.tsx +15 -20
- package/src/components/ui/square-avatar.tsx +5 -8
- package/src/embed-shims/next-image.tsx +34 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/ui/use-content-breakpoint.ts +21 -2
- package/src/hooks/ui/use-media-query.ts +1 -0
- package/src/hooks/use-authed-asset-src.ts +323 -107
- package/src/hooks/use-authed-image-src.ts +15 -5
- package/src/hooks/use-image-fallback.ts +31 -0
- package/src/utils/app-origins.ts +12 -7
- package/src/utils/embed-authed-fetch.ts +71 -32
- package/src/utils/presentation-for.ts +10 -0
- package/dist/chunk-UAI4VMEF.cjs +0 -46
- package/dist/chunk-YSDB6HGQ.cjs +0 -102
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useContentLgUp, useContentMdUp, useContentXlUp } from '../../../hooks/ui/use-content-breakpoint';
|
|
5
5
|
import type { DeviceFilters } from './types';
|
|
6
6
|
|
|
7
7
|
function isCompleteTag(tag: string): boolean {
|
|
@@ -19,7 +19,13 @@ interface FilterOption {
|
|
|
19
19
|
interface UseTagFilterModalParams {
|
|
20
20
|
tags: string[];
|
|
21
21
|
deviceFilters: DeviceFilters | null;
|
|
22
|
-
columns: Array<{
|
|
22
|
+
columns: Array<{
|
|
23
|
+
key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
filterable?: boolean;
|
|
26
|
+
filterOptions?: FilterOption[];
|
|
27
|
+
inWindowLgToXl?: boolean;
|
|
28
|
+
}>;
|
|
23
29
|
/** `useApiParams`' setter, each device list param is a list of ids. */
|
|
24
30
|
setParams: (params: Record<string, string[]>) => void;
|
|
25
31
|
/**
|
|
@@ -39,7 +45,11 @@ export function useTagFilterModal({
|
|
|
39
45
|
setParams,
|
|
40
46
|
onApplied = scrollMainToTop,
|
|
41
47
|
}: UseTagFilterModalParams) {
|
|
42
|
-
|
|
48
|
+
// The content area's step, not the window's: the table header follows it, and a docked side panel narrows it while
|
|
49
|
+
// the window stays wide.
|
|
50
|
+
const isMdUp = useContentMdUp();
|
|
51
|
+
const lgUp = useContentLgUp();
|
|
52
|
+
const xlUp = useContentXlUp();
|
|
43
53
|
const [isOpen, setIsOpen] = useState(false);
|
|
44
54
|
|
|
45
55
|
// FilterModal calls onFilterChange first, then onTagsChange.
|
|
@@ -49,16 +59,19 @@ export function useTagFilterModal({
|
|
|
49
59
|
const open = useCallback(() => setIsOpen(true), []);
|
|
50
60
|
const close = useCallback(() => setIsOpen(false), []);
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
// Below md the table draws no header, so every column filters here; from lg until xl only the columns whose header
|
|
63
|
+
// has no funnel there do.
|
|
64
|
+
const filterGroups = useMemo(
|
|
65
|
+
() =>
|
|
66
|
+
columns
|
|
67
|
+
.filter(column => column.filterable && (!isMdUp || (lgUp && xlUp === false && column.inWindowLgToXl)))
|
|
68
|
+
.map(column => ({
|
|
69
|
+
id: column.key,
|
|
70
|
+
title: column.label,
|
|
71
|
+
options: column.filterOptions || [],
|
|
72
|
+
})),
|
|
73
|
+
[columns, isMdUp, lgUp, xlUp],
|
|
74
|
+
);
|
|
62
75
|
|
|
63
76
|
const tagFilterKeys = useMemo(() => {
|
|
64
77
|
if (!deviceFilters?.tagKeys) return [];
|
|
@@ -79,6 +92,9 @@ export function useTagFilterModal({
|
|
|
79
92
|
pendingFiltersRef.current = columnFilters;
|
|
80
93
|
}, []);
|
|
81
94
|
|
|
95
|
+
// Only a host that draws the directory columns has the USER and SOURCES groups: another's list params stay untouched
|
|
96
|
+
const hasDirectoryGroups = useMemo(() => columns.some(column => column.key === 'user'), [columns]);
|
|
97
|
+
|
|
82
98
|
// Step 2: combine with buffered filters and flush (called second by FilterModal)
|
|
83
99
|
const handleTagsChange = useCallback(
|
|
84
100
|
(newTags: string[]) => {
|
|
@@ -87,12 +103,14 @@ export function useTagFilterModal({
|
|
|
87
103
|
statuses: cf.status || [],
|
|
88
104
|
osTypes: cf.os || [],
|
|
89
105
|
organizationIds: cf.organization || [],
|
|
106
|
+
// FilterModal drops a group with nothing ticked, so a missing group clears its param
|
|
107
|
+
...(hasDirectoryGroups && { userLinks: cf.user || [], directorySources: cf.sources || [] }),
|
|
90
108
|
tags: newTags.filter(isCompleteTag),
|
|
91
109
|
});
|
|
92
110
|
pendingFiltersRef.current = {};
|
|
93
111
|
onApplied();
|
|
94
112
|
},
|
|
95
|
-
[setParams, onApplied],
|
|
113
|
+
[setParams, onApplied, hasDirectoryGroups],
|
|
96
114
|
);
|
|
97
115
|
|
|
98
116
|
return {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import type { CSSProperties, ReactNode } from 'react';
|
|
5
|
-
import { ContentAreaWidthContext,
|
|
5
|
+
import { ContentAreaWidthContext, contentStepFor } from '../../hooks/ui/use-content-breakpoint';
|
|
6
6
|
import { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';
|
|
7
7
|
import type { NavigationSidebarConfig } from '../../types/navigation';
|
|
8
8
|
import { cn } from '../../utils';
|
|
@@ -203,14 +203,14 @@ export function AppLayout({
|
|
|
203
203
|
|
|
204
204
|
useIsomorphicLayoutEffect(() => {
|
|
205
205
|
if (!adaptive || !mainElement) return undefined;
|
|
206
|
-
// Every reader asks for
|
|
207
|
-
// crossing re-renders the layout: kept at the old width, the state bails out.
|
|
206
|
+
// Every reader asks for a step (`useContentBreakpoint`, `useContentXlUp`), so
|
|
207
|
+
// only a crossing re-renders the layout: kept at the old width, the state bails out.
|
|
208
208
|
// Per pixel, the uncompiled layout and every breakpoint consumer re-rendered on
|
|
209
209
|
// each frame of a sidebar toggle, a window drag or a side-panel resize.
|
|
210
210
|
const update = () => {
|
|
211
211
|
const width = mainElement.clientWidth;
|
|
212
212
|
setContentWidth(previous =>
|
|
213
|
-
previous !== null &&
|
|
213
|
+
previous !== null && contentStepFor(previous) === contentStepFor(width) ? previous : width,
|
|
214
214
|
);
|
|
215
215
|
};
|
|
216
216
|
update();
|
|
@@ -42,6 +42,8 @@ export interface DatePickerBaseProps {
|
|
|
42
42
|
error?: string;
|
|
43
43
|
/** When true, renders red error border */
|
|
44
44
|
invalid?: boolean;
|
|
45
|
+
/** The trigger button's id: a label drawn outside the picker names it with `htmlFor`. */
|
|
46
|
+
id?: string;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface SingleDatePickerProps extends DatePickerBaseProps {
|
|
@@ -69,6 +71,10 @@ export type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;
|
|
|
69
71
|
const defaultFormatDate = (date: Date): string =>
|
|
70
72
|
formatDateWithTimezone(date, VIEWER_TIMEZONE, 'numeric', { viewerLocale: true });
|
|
71
73
|
|
|
74
|
+
// A label tied to the trigger (`id`) becomes the button's whole accessible name, so the date it shows would go unread:
|
|
75
|
+
// the trigger is described by its value instead.
|
|
76
|
+
const triggerValueId = (id: string | undefined) => (id ? `${id}-value` : undefined);
|
|
77
|
+
|
|
72
78
|
const formatDateRange = (range: DateRange | undefined, formatFn: (date: Date) => string): string => {
|
|
73
79
|
if (!range?.from) return '';
|
|
74
80
|
if (!range.to) return formatFn(range.from);
|
|
@@ -667,6 +673,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
667
673
|
label,
|
|
668
674
|
error,
|
|
669
675
|
invalid = false,
|
|
676
|
+
id,
|
|
670
677
|
} = props;
|
|
671
678
|
|
|
672
679
|
const [open, setOpen] = useState(false);
|
|
@@ -701,6 +708,8 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
701
708
|
<Button
|
|
702
709
|
variant="field"
|
|
703
710
|
type="button"
|
|
711
|
+
id={id}
|
|
712
|
+
aria-describedby={triggerValueId(id)}
|
|
704
713
|
disabled={disabled}
|
|
705
714
|
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
706
715
|
// (and scrolls to) the first field that failed validation.
|
|
@@ -709,6 +718,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
709
718
|
>
|
|
710
719
|
<Calendar className="size-6 shrink-0 text-ods-text-secondary" />
|
|
711
720
|
<span
|
|
721
|
+
id={triggerValueId(id)}
|
|
712
722
|
className={cn(
|
|
713
723
|
'flex-1 truncate text-left',
|
|
714
724
|
disabled ? 'text-ods-text-disabled' : displayValue ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
@@ -749,7 +759,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
749
759
|
);
|
|
750
760
|
|
|
751
761
|
return (
|
|
752
|
-
<FieldWrapper label={label} error={error} errorSlot={'error' in props}>
|
|
762
|
+
<FieldWrapper label={label} htmlFor={id} error={error} errorSlot={'error' in props}>
|
|
753
763
|
{picker}
|
|
754
764
|
</FieldWrapper>
|
|
755
765
|
);
|
|
@@ -799,6 +809,7 @@ export function DatePickerInput(props: DatePickerInputProps) {
|
|
|
799
809
|
label,
|
|
800
810
|
error,
|
|
801
811
|
invalid = false,
|
|
812
|
+
id,
|
|
802
813
|
} = props;
|
|
803
814
|
const [open, setOpen] = useState(false);
|
|
804
815
|
const portalContainer = usePortalContainer();
|
|
@@ -883,6 +894,8 @@ export function DatePickerInput(props: DatePickerInputProps) {
|
|
|
883
894
|
<Button
|
|
884
895
|
variant="field"
|
|
885
896
|
type="button"
|
|
897
|
+
id={id}
|
|
898
|
+
aria-describedby={triggerValueId(id)}
|
|
886
899
|
disabled={disabled}
|
|
887
900
|
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
888
901
|
// (and scrolls to) the first field that failed validation.
|
|
@@ -891,6 +904,7 @@ export function DatePickerInput(props: DatePickerInputProps) {
|
|
|
891
904
|
>
|
|
892
905
|
<Calendar className="size-6 shrink-0 text-ods-text-secondary" />
|
|
893
906
|
<span
|
|
907
|
+
id={triggerValueId(id)}
|
|
894
908
|
className={cn(
|
|
895
909
|
'flex-1 truncate text-left',
|
|
896
910
|
disabled
|
|
@@ -984,7 +998,7 @@ export function DatePickerInput(props: DatePickerInputProps) {
|
|
|
984
998
|
);
|
|
985
999
|
|
|
986
1000
|
return (
|
|
987
|
-
<FieldWrapper label={label} error={error} errorSlot={'error' in props} className={className}>
|
|
1001
|
+
<FieldWrapper label={label} htmlFor={id} error={error} errorSlot={'error' in props} className={className}>
|
|
988
1002
|
{content}
|
|
989
1003
|
</FieldWrapper>
|
|
990
1004
|
);
|
|
@@ -1050,6 +1064,7 @@ export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
|
1050
1064
|
label,
|
|
1051
1065
|
error,
|
|
1052
1066
|
invalid = false,
|
|
1067
|
+
id,
|
|
1053
1068
|
} = props;
|
|
1054
1069
|
const [open, setOpen] = useState(false);
|
|
1055
1070
|
const portalContainer = usePortalContainer();
|
|
@@ -1112,6 +1127,8 @@ export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
|
1112
1127
|
<Button
|
|
1113
1128
|
variant="field"
|
|
1114
1129
|
type="button"
|
|
1130
|
+
id={id}
|
|
1131
|
+
aria-describedby={triggerValueId(id)}
|
|
1115
1132
|
disabled={disabled}
|
|
1116
1133
|
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
1117
1134
|
// (and scrolls to) the first field that failed validation.
|
|
@@ -1120,6 +1137,7 @@ export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
|
1120
1137
|
>
|
|
1121
1138
|
<Calendar className="size-6 shrink-0 text-ods-text-secondary" />
|
|
1122
1139
|
<span
|
|
1140
|
+
id={triggerValueId(id)}
|
|
1123
1141
|
className={cn(
|
|
1124
1142
|
'flex-1 truncate text-left',
|
|
1125
1143
|
disabled
|
|
@@ -1181,7 +1199,7 @@ export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
|
1181
1199
|
);
|
|
1182
1200
|
|
|
1183
1201
|
return (
|
|
1184
|
-
<FieldWrapper label={label} error={error} errorSlot={'error' in props} className={className}>
|
|
1202
|
+
<FieldWrapper label={label} htmlFor={id} error={error} errorSlot={'error' in props} className={className}>
|
|
1185
1203
|
{content}
|
|
1186
1204
|
</FieldWrapper>
|
|
1187
1205
|
);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
4
3
|
import Image from '../../embed-shims/next-image';
|
|
5
4
|
import { useAuthedImageSrc } from '../../hooks/use-authed-image-src';
|
|
5
|
+
import { useImageFallback } from '../../hooks/use-image-fallback';
|
|
6
6
|
import { TILE_SIZE } from '../../styles/component-sizes';
|
|
7
7
|
import { IMAGE_FIXED_PX } from '../../styles/image-sizes';
|
|
8
8
|
import { cn } from '../../utils/cn';
|
|
@@ -37,20 +37,12 @@ export interface EntityImageProps {
|
|
|
37
37
|
export function EntityImage({ src, alt, fallbackText, sizeClassName, className }: EntityImageProps) {
|
|
38
38
|
// In a loading scope the image is its skeleton, and it is not fetched.
|
|
39
39
|
const held = useLoadingScope();
|
|
40
|
-
const
|
|
41
|
-
const resolvedSrc = authedSrc;
|
|
42
|
-
// Which src failed, rather than a bare "failed" flag reset from an effect on
|
|
43
|
-
// every src change. The reset is then implicit — a new src simply is not the
|
|
44
|
-
// one that failed — so the fallback initials never survive into the render
|
|
45
|
-
// that swapped the image, and a new src costs no second render pass.
|
|
46
|
-
const [failedSrc, setFailedSrc] = React.useState<string | null>(null);
|
|
47
|
-
const imageFailed = resolvedSrc !== undefined && failedSrc === resolvedSrc;
|
|
40
|
+
const { src: resolvedSrc, onError } = useImageFallback(useAuthedImageSrc(held ? undefined : src));
|
|
48
41
|
|
|
49
42
|
if (held) return <EntityImageSkeleton sizeClassName={sizeClassName} className={className} />;
|
|
50
|
-
const showFallback = imageFailed || !resolvedSrc;
|
|
51
43
|
const initials = personInitials(fallbackText ?? alt);
|
|
52
44
|
|
|
53
|
-
if (
|
|
45
|
+
if (!resolvedSrc) {
|
|
54
46
|
return (
|
|
55
47
|
<div
|
|
56
48
|
aria-label={alt}
|
|
@@ -70,7 +62,7 @@ export function EntityImage({ src, alt, fallbackText, sizeClassName, className }
|
|
|
70
62
|
<Image
|
|
71
63
|
src={resolvedSrc}
|
|
72
64
|
alt={alt ?? ''}
|
|
73
|
-
onError={
|
|
65
|
+
onError={onError}
|
|
74
66
|
className={cn(
|
|
75
67
|
'shrink-0 border border-ods-border object-contain',
|
|
76
68
|
ENTITY_IMAGE_RADIUS_CLASS,
|
|
@@ -54,19 +54,6 @@ export interface StandardLeafRenderers {
|
|
|
54
54
|
* the rich composition's `img` / `video` overrides so the empty-`![]()`
|
|
55
55
|
* guard has exactly one definition.
|
|
56
56
|
*/
|
|
57
|
-
/**
|
|
58
|
-
* Inline content image (blog / docs / chat attachments). A standalone
|
|
59
|
-
* component — NOT inline JSX in the `img` renderer — because it must call
|
|
60
|
-
* `useAuthedImageSrc`: in bearer-mode native shells (`capacitor://`,
|
|
61
|
-
* `tauri://`) a gateway-hosted image can't load through a plain `<img>` (no
|
|
62
|
-
* Authorization header on native asset loads), so the hook swaps in an authed
|
|
63
|
-
* blob URL; everywhere else it returns `src` untouched. While the blob fetch
|
|
64
|
-
* is in flight `resolvedSrc` is null and the component renders nothing — same
|
|
65
|
-
* as the no-src case. Ported from #1548 into the unified engine (the original
|
|
66
|
-
* lived in the now-deleted `ui/simple-markdown-renderer.tsx`). Sizing matches
|
|
67
|
-
* the engine's cap: 400x400 intrinsic, `w-auto h-auto max-h-[400px]` drives
|
|
68
|
-
* the actual rendered box; click-to-expand surfaces provide full resolution.
|
|
69
|
-
*/
|
|
70
57
|
/**
|
|
71
58
|
* The page title, a section heading and a paragraph of rendered markdown (before the text size): read by the
|
|
72
59
|
* renderer and by every skeleton that stands in for a markdown body.
|
|
@@ -76,16 +63,23 @@ export const MARKDOWN_H2_CLASS =
|
|
|
76
63
|
'mb-4 mt-8 border-b border-ods-border pb-2 font-sans font-semibold text-ods-text-primary';
|
|
77
64
|
export const MARKDOWN_PARAGRAPH_CLASS = 'mb-4 leading-relaxed text-ods-text-primary first:mt-0 last:mb-0';
|
|
78
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Inline content image (blog / docs / chat attachments). The boxed `Image` resolves and authenticates it
|
|
68
|
+
* (`useAuthedAsset`); this component reads the same store only to draw nothing while there is nothing to draw (the
|
|
69
|
+
* authed fetch in flight or failed, as with no src), where `Image` would draw its box or the failing URL. Ported from
|
|
70
|
+
* #1548 into the unified engine (the original lived in the now-deleted `ui/simple-markdown-renderer.tsx`). Sizing
|
|
71
|
+
* matches the engine's cap: 400x400 intrinsic, `w-auto h-auto max-h-[400px]` drives the actual rendered box;
|
|
72
|
+
* click-to-expand surfaces provide full resolution.
|
|
73
|
+
*/
|
|
79
74
|
const MarkdownContentImage: React.FC<{ src: string; alt?: string }> = ({ src, alt }) => {
|
|
80
75
|
// In a loading scope the image is a box and nothing is fetched: the authed fetch is skipped, and the `Image` shim
|
|
81
76
|
// draws its skeleton box (`HeldImage`) without reading `src`. `src` is passed only so the box is drawn at all.
|
|
82
77
|
const inScope = useLoadingScope();
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
if (!resolvedSrc) return null;
|
|
78
|
+
const drawable = useAuthedImageSrc(inScope ? undefined : src);
|
|
79
|
+
if (!inScope && !drawable) return null;
|
|
86
80
|
return (
|
|
87
81
|
<Image
|
|
88
|
-
src={
|
|
82
|
+
src={src}
|
|
89
83
|
alt={alt ?? 'No image available'}
|
|
90
84
|
width={400}
|
|
91
85
|
height={400}
|
|
@@ -363,9 +357,10 @@ export function buildBaseComponents({
|
|
|
363
357
|
|
|
364
358
|
// --- images ---
|
|
365
359
|
// Inline content image renderer (blog, docs, chat attachments) —
|
|
366
|
-
// delegates to `MarkdownContentImage` (above), which
|
|
367
|
-
//
|
|
368
|
-
// expand for full resolution
|
|
360
|
+
// delegates to `MarkdownContentImage` (above), which draws nothing until
|
|
361
|
+
// there is a picture to draw and applies the 400x400 cap with click-to-
|
|
362
|
+
// expand for full resolution; the boxed `Image` it renders resolves and
|
|
363
|
+
// authenticates it.
|
|
369
364
|
//
|
|
370
365
|
// A third-party `src` never reaches here unless the composition set
|
|
371
366
|
// `allowRemoteMedia`: `rehypeStripUnsafe` moves it to `data-blocked-src`,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { type HTMLAttributes, memo, forwardRef } from 'react';
|
|
4
4
|
import Image from '../../embed-shims/next-image';
|
|
5
5
|
import { useAuthedImageSrc } from '../../hooks/use-authed-image-src';
|
|
6
|
+
import { useImageFallback } from '../../hooks/use-image-fallback';
|
|
6
7
|
import { AVATAR_SIZE, AVATAR_SIZE_PX, type AvatarSize } from '../../styles/component-sizes';
|
|
7
8
|
import { cn } from '../../utils/cn';
|
|
8
9
|
import { personInitials } from '../../utils/format';
|
|
@@ -66,7 +67,9 @@ const SquareAvatar = memo(
|
|
|
66
67
|
// is not fetched.
|
|
67
68
|
const inScope = useLoadingScope();
|
|
68
69
|
const held = inScope || !!loading;
|
|
69
|
-
|
|
70
|
+
// The initials fallback used to be written into the DOM from `onError`, and the hidden picture stayed hidden
|
|
71
|
+
// for the avatar's lifetime, even once a src that loads arrived.
|
|
72
|
+
const { src: resolvedSrc, onError } = useImageFallback(useAuthedImageSrc(held ? undefined : src));
|
|
70
73
|
if (held) {
|
|
71
74
|
return (
|
|
72
75
|
<SquareAvatarSkeleton
|
|
@@ -78,7 +81,6 @@ const SquareAvatar = memo(
|
|
|
78
81
|
/>
|
|
79
82
|
);
|
|
80
83
|
}
|
|
81
|
-
const resolvedSrc = authedSrc;
|
|
82
84
|
const sizeClasses = AVATAR_SIZE;
|
|
83
85
|
const sizePxBySize = AVATAR_SIZE_PX;
|
|
84
86
|
|
|
@@ -137,12 +139,7 @@ const SquareAvatar = memo(
|
|
|
137
139
|
alt={alt || ''}
|
|
138
140
|
width={sizePx ?? sizePxBySize[size]}
|
|
139
141
|
height={sizePx ?? sizePxBySize[size]}
|
|
140
|
-
onError={
|
|
141
|
-
const img = e.currentTarget;
|
|
142
|
-
img.style.display = 'none';
|
|
143
|
-
const el = img.previousElementSibling as HTMLElement;
|
|
144
|
-
if (el) el.classList.remove('hidden');
|
|
145
|
-
}}
|
|
142
|
+
onError={onError}
|
|
146
143
|
/>
|
|
147
144
|
)}
|
|
148
145
|
</div>
|
|
@@ -24,6 +24,8 @@ import { forwardRef, type ComponentType, type CSSProperties, type ImgHTMLAttribu
|
|
|
24
24
|
|
|
25
25
|
import { UnifiedSkeleton } from '../components/loading/unified-skeleton';
|
|
26
26
|
import { useLoadingScope } from '../components/ui/loading-scope';
|
|
27
|
+
import type { AuthedAsset } from '../hooks/use-authed-asset-src';
|
|
28
|
+
import { useAuthedImage } from '../hooks/use-authed-image-src';
|
|
27
29
|
import { cn } from '../utils/cn';
|
|
28
30
|
import { fetchPriorityProp } from '../utils/fetch-priority';
|
|
29
31
|
|
|
@@ -49,6 +51,8 @@ type ImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'placeholder
|
|
|
49
51
|
optimizedWidth?: number;
|
|
50
52
|
onLoadingComplete?: (img: HTMLImageElement) => void;
|
|
51
53
|
loader?: unknown;
|
|
54
|
+
/** The picture's address when `src` is the blob fetched from it (set by the shim; see `drawnAsset`). */
|
|
55
|
+
'data-asset-src'?: string;
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
/** What the shim renders once a host has registered `next/image`. */
|
|
@@ -122,12 +126,22 @@ function HeldImage({ className, style, width, height, fill }: ImageProps) {
|
|
|
122
126
|
const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({ optimizedWidth, ...given }, ref) {
|
|
123
127
|
// In a loading scope the picture is a box of its own size, and it is not fetched.
|
|
124
128
|
const held = useLoadingScope();
|
|
125
|
-
if (held) return <HeldImage {...given} />;
|
|
126
129
|
// A picture asked for at a width is the host resizer's URL, drawn as it comes.
|
|
127
|
-
const
|
|
130
|
+
const sized: ImageProps =
|
|
128
131
|
optimizedWidth && resizer && typeof given.src === 'string'
|
|
129
132
|
? { ...given, src: resizer(given.src, optimizedWidth), unoptimized: true }
|
|
130
133
|
: given;
|
|
134
|
+
// Every picture is drawn from where it lives and with the credential it needs: the host's `resolveAssetUrl`
|
|
135
|
+
// sends a backend path to its gateway, and a bearer-auth host's picture is fetched with the bearer and drawn
|
|
136
|
+
// from a blob (`useAuthedAsset`). Cookie-auth web and public pictures pass through unchanged.
|
|
137
|
+
const asset = useAuthedImage(!held && typeof sized.src === 'string' ? sized.src : undefined);
|
|
138
|
+
// Waiting for the credential or for the fetch: the picture's box, with nothing requested.
|
|
139
|
+
if (held || asset.status === 'pending') return <HeldImage {...given} />;
|
|
140
|
+
const props: ImageProps = typeof sized.src !== 'string' ? sized : drawnAsset(sized, asset);
|
|
141
|
+
// The element that failed is not the one a retry draws into: a caller that hid it in `onError` would keep it
|
|
142
|
+
// hidden. Only that transition remounts: a native in-place src swap keeps the old picture until the new one loads
|
|
143
|
+
// (a bearer swap draws the box while the new blob is fetched).
|
|
144
|
+
const key = asset.status === 'failed' ? 'failed' : undefined;
|
|
131
145
|
// `priority` means "this is the picture the page is waiting for", so its
|
|
132
146
|
// request is ranked high on EVERY host. Stated here, once: since Next 16
|
|
133
147
|
// `priority` only preloads and no longer ranks the request, and the plain
|
|
@@ -139,7 +153,7 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
|
|
|
139
153
|
// (priority, placeholder, sizes, loader…) reaches the real component intact.
|
|
140
154
|
if (impl) {
|
|
141
155
|
const Real = impl;
|
|
142
|
-
return <Real ref={ref} {...props} fetchPriority={fetchPriority} />;
|
|
156
|
+
return <Real key={key} ref={ref} {...props} fetchPriority={fetchPriority} />;
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
// Fallback path — plain <img>. Drops Next-only props and reduces
|
|
@@ -183,6 +197,7 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
|
|
|
183
197
|
: style;
|
|
184
198
|
return (
|
|
185
199
|
<img
|
|
200
|
+
key={key}
|
|
186
201
|
ref={ref}
|
|
187
202
|
src={srcStr}
|
|
188
203
|
alt={alt ?? ''}
|
|
@@ -197,4 +212,20 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function NextImageShim({
|
|
|
197
212
|
);
|
|
198
213
|
});
|
|
199
214
|
|
|
215
|
+
/**
|
|
216
|
+
* The props drawn for a string `src` once `useAuthedAsset` answered:
|
|
217
|
+
*
|
|
218
|
+
* - `native` / `ready`: its `src` (the URL, or the fetched blob). A blob goes to no optimizer.
|
|
219
|
+
* - `failed`: the URL itself, so the element fails the way a plain `<img>` would and the caller's `onError`
|
|
220
|
+
* fallback runs. It used to stay blank, with nothing to tell the caller.
|
|
221
|
+
*
|
|
222
|
+
* `data-asset-src` keeps the URL on the element when the drawn `src` is a blob, for code that reads a picture's
|
|
223
|
+
* address off the DOM (the chat's click-to-expand gallery).
|
|
224
|
+
*/
|
|
225
|
+
function drawnAsset(props: ImageProps, asset: AuthedAsset): ImageProps {
|
|
226
|
+
const src = asset.status === 'failed' ? asset.url : asset.src;
|
|
227
|
+
if (asset.status !== 'ready') return { ...props, src };
|
|
228
|
+
return { ...props, src, unoptimized: true, 'data-asset-src': asset.url };
|
|
229
|
+
}
|
|
230
|
+
|
|
200
231
|
export default Image;
|
package/src/hooks/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './use-batch-images';
|
|
|
12
12
|
export * from './use-authenticated-image';
|
|
13
13
|
export * from './use-authed-asset-src';
|
|
14
14
|
export * from './use-authed-image-src';
|
|
15
|
+
export * from './use-image-fallback';
|
|
15
16
|
|
|
16
17
|
// URL State Management hooks
|
|
17
18
|
export * from './state';
|
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import { createContext, createElement, type ReactNode, useContext } from 'react';
|
|
4
4
|
import { LAYOUT_STEPS } from '../../styles/layout-steps';
|
|
5
|
-
import { useLgUp, useMdUp } from './use-media-query';
|
|
5
|
+
import { breakpoints, useLgUp, useMdUp, useMediaQuery } from './use-media-query';
|
|
6
6
|
|
|
7
7
|
export type ContentBreakpoint = 'mobile' | 'tablet' | 'desktop';
|
|
8
8
|
|
|
9
9
|
/** Content-area steps, in px of the area's own width. */
|
|
10
|
-
export const CONTENT_BREAKPOINTS = {
|
|
10
|
+
export const CONTENT_BREAKPOINTS = {
|
|
11
|
+
md: LAYOUT_STEPS.md.content,
|
|
12
|
+
lg: LAYOUT_STEPS.lg.content,
|
|
13
|
+
xl: LAYOUT_STEPS.xl.content,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* How many content steps a width has passed, `xl` included: `AppLayout` publishes a new width only when this changes,
|
|
18
|
+
* so every reader below sees each crossing it asks about (`useContentBreakpoint`, `useContentXlUp`).
|
|
19
|
+
*/
|
|
20
|
+
export function contentStepFor(width: number): number {
|
|
21
|
+
return [CONTENT_BREAKPOINTS.md, CONTENT_BREAKPOINTS.lg, CONTENT_BREAKPOINTS.xl].filter(step => width >= step).length;
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
/**
|
|
13
25
|
* Width of the enclosing content area (`AppLayout` with a side panel or a composer
|
|
@@ -58,3 +70,10 @@ export function useContentLgUp(): boolean | undefined {
|
|
|
58
70
|
const breakpoint = useContentBreakpoint();
|
|
59
71
|
return breakpoint === undefined ? undefined : breakpoint === 'desktop';
|
|
60
72
|
}
|
|
73
|
+
|
|
74
|
+
/** The `content-xl:` step in JS: the content area's width from `xl`, the viewport's outside one. */
|
|
75
|
+
export function useContentXlUp(): boolean | undefined {
|
|
76
|
+
const width = useContext(ContentAreaWidthContext);
|
|
77
|
+
const viewportXlUp = useMediaQuery(breakpoints.xl);
|
|
78
|
+
return width === null ? viewportXlUp : width >= CONTENT_BREAKPOINTS.xl;
|
|
79
|
+
}
|
|
@@ -40,6 +40,7 @@ export function useMediaQuery(query: string): boolean | undefined {
|
|
|
40
40
|
export const breakpoints = {
|
|
41
41
|
md: `(min-width: ${LAYOUT_STEPS.md.viewport}px)`, // Tablet
|
|
42
42
|
lg: `(min-width: ${LAYOUT_STEPS.lg.viewport}px)`, // Desktop
|
|
43
|
+
xl: `(min-width: ${LAYOUT_STEPS.xl.viewport}px)`, // Wide desktop
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
/** @deprecated Use useMdUp instead */
|