@flamingo-stack/openframe-frontend-core 0.0.847 → 0.0.849
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-DJOHIHYU.cjs → case-study-card-X2HKPKT7.cjs} +5 -5
- package/dist/{chunk-CNMXP77I.cjs → chunk-36DUCHI7.cjs} +4 -4
- package/dist/{chunk-MPVOPSU6.cjs → chunk-36XWCVSM.cjs} +96 -96
- package/dist/{chunk-J2IXYCEH.cjs → chunk-3W4ASAXN.cjs} +15 -15
- package/dist/{chunk-6LDL3UZE.cjs → chunk-4YBS4HIO.cjs} +20 -20
- package/dist/{chunk-ABL5OBSF.cjs → chunk-533QK4R6.cjs} +296 -250
- package/dist/{chunk-QK2A334V.cjs → chunk-5CBI2ZYV.cjs} +12 -12
- package/dist/{chunk-D5DMWXF2.cjs → chunk-5PIR77V5.cjs} +4 -4
- package/dist/{chunk-HPXET3LH.cjs → chunk-6AJYGXXS.cjs} +27 -27
- package/dist/{chunk-4R4HAFXI.cjs → chunk-ALRYVZ3E.cjs} +6 -6
- package/dist/{chunk-FLW4WULE.cjs → chunk-BU2PSRAS.cjs} +17 -61
- package/dist/{chunk-HBDLJUI3.cjs → chunk-C3P7IUTU.cjs} +39 -14
- package/dist/{chunk-SSSI2RBV.cjs → chunk-FNQWL4W4.cjs} +14 -14
- package/dist/{chunk-TBOWFTEN.cjs → chunk-FRG724DU.cjs} +22 -22
- package/dist/{chunk-SIFRIUPW.cjs → chunk-FXLROQUT.cjs} +345 -356
- package/dist/{chunk-3V5JWG27.cjs → chunk-GG466XFQ.cjs} +2 -2
- package/dist/{chunk-XCVH5BCZ.cjs → chunk-GRCOEICK.cjs} +6 -6
- package/dist/{chunk-5GBLXY5F.cjs → chunk-HTYJUMFB.cjs} +7 -7
- package/dist/{chunk-URP6GYTD.cjs → chunk-IG6ROHPS.cjs} +56 -56
- package/dist/{chunk-DGQDGCQH.cjs → chunk-IHY7E4EX.cjs} +2 -2
- package/dist/{chunk-WCK5T3UD.cjs → chunk-IKOL6NJY.cjs} +101 -69
- package/dist/{chunk-RAJ5FYFL.cjs → chunk-IN3LA2FU.cjs} +14 -14
- package/dist/{chunk-ZTO7KNYV.cjs → chunk-KXBZCUDW.cjs} +35 -35
- package/dist/{chunk-57JYB3E2.cjs → chunk-MC3L525K.cjs} +3 -3
- package/dist/{chunk-BIOSHDXQ.cjs → chunk-MDLT6D46.cjs} +61 -61
- package/dist/{chunk-3TOWCFIK.cjs → chunk-PMUCEUFQ.cjs} +13 -1
- package/dist/{chunk-PV3NKXWS.cjs → chunk-PV2XQJ2R.cjs} +99 -178
- package/dist/{chunk-J2ZSGOUC.cjs → chunk-R5JPZ6TY.cjs} +27 -27
- package/dist/{chunk-744JXTDP.cjs → chunk-SA2NNWIJ.cjs} +23 -23
- package/dist/{chunk-DEEQSVLX.cjs → chunk-SHQY4GOE.cjs} +90 -90
- package/dist/{chunk-BHOVF4KP.cjs → chunk-SOSIXRQ3.cjs} +2 -2
- package/dist/{chunk-Y2OEC7UW.cjs → chunk-TEKN6TJ5.cjs} +40 -40
- package/dist/{chunk-PZNWIJRZ.cjs → chunk-TIVPOVIM.cjs} +2 -2
- package/dist/{chunk-BZZE4KIV.cjs → chunk-TX4NTH22.cjs} +155 -136
- package/dist/{chunk-NFNSPLGC.cjs → chunk-W7OFAROY.cjs} +14 -9
- package/dist/{chunk-N2A24FDF.cjs → chunk-YLETC7LP.cjs} +994 -953
- package/dist/{chunk-YC52LSPP.cjs → chunk-YYHQY3K6.cjs} +40 -40
- package/dist/{chunk-2FUTZKLB.cjs → chunk-Z5UCDEKZ.cjs} +6 -6
- package/dist/{chunk-SWF6TNZ3.cjs → chunk-ZGEWT3SF.cjs} +2 -2
- package/dist/{chunk-PQDCSZ3W.cjs → chunk-ZY34R37A.cjs} +85 -85
- package/dist/{chunk-5SZ4ILU3.cjs → chunk-ZZDFMUEU.cjs} +3 -3
- package/dist/{cloud-tenants-XEUUMABT.cjs → cloud-tenants-HKUUEUAY.cjs} +21 -21
- package/dist/components/case-studies/index.cjs +36 -36
- package/dist/components/chat/chat-composer.d.ts.map +1 -1
- package/dist/components/chat/chat-composer.js +4 -3
- package/dist/components/chat/chat-container.d.ts.map +1 -1
- package/dist/components/chat/chat-container.js +2 -2
- package/dist/components/chat/entity-cards/design-doc-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/design-doc-card.js +5 -17
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.js +5 -7
- package/dist/components/chat/index.cjs +15 -15
- package/dist/components/contact/index.cjs +22 -22
- package/dist/components/docs/doc-viewer.d.ts +7 -7
- package/dist/components/docs/doc-viewer.d.ts.map +1 -1
- package/dist/components/docs/doc-viewer.js +29 -16
- package/dist/components/docs/docs-hub-page.d.ts +1 -1
- package/dist/components/docs/docs-hub-page.d.ts.map +1 -1
- package/dist/components/docs/docs-hub-page.js +4 -11
- package/dist/components/docs/index.cjs +25 -25
- package/dist/components/docs/use-document-tree.d.ts.map +1 -1
- package/dist/components/docs/use-document-tree.js +24 -24
- package/dist/components/embeds/claude-embed.d.ts +2 -2
- package/dist/components/embeds/claude-embed.d.ts.map +1 -1
- package/dist/components/embeds/claude-embed.js +3 -3
- package/dist/components/embeds/embed-iframe.d.ts +5 -3
- package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
- package/dist/components/embeds/embed-iframe.js +4 -3
- package/dist/components/embeds/embed-viewer-frame.d.ts +12 -9
- package/dist/components/embeds/embed-viewer-frame.d.ts.map +1 -1
- package/dist/components/embeds/embed-viewer-frame.js +21 -7
- package/dist/components/embeds/figma-embed.d.ts +4 -2
- package/dist/components/embeds/figma-embed.d.ts.map +1 -1
- package/dist/components/embeds/figma-embed.js +4 -3
- package/dist/components/embeds/file-download-card.d.ts +3 -1
- package/dist/components/embeds/file-download-card.d.ts.map +1 -1
- package/dist/components/embeds/file-download-card.js +18 -3
- package/dist/components/embeds/google-sheets-viewer.d.ts +3 -1
- package/dist/components/embeds/google-sheets-viewer.d.ts.map +1 -1
- package/dist/components/embeds/google-sheets-viewer.js +7 -4
- package/dist/components/embeds/index.cjs +22 -22
- package/dist/components/embeds/pdf-viewer.d.ts +3 -1
- package/dist/components/embeds/pdf-viewer.d.ts.map +1 -1
- package/dist/components/embeds/pdf-viewer.js +6 -2
- package/dist/components/faq/index.cjs +23 -23
- package/dist/components/features/index.cjs +21 -21
- package/dist/components/features/status-filter-component.d.ts +7 -1
- package/dist/components/features/status-filter-component.d.ts.map +1 -1
- package/dist/components/features/status-filter-component.js +9 -3
- package/dist/components/help-center-pages/index.cjs +140 -140
- package/dist/components/index.cjs +137 -135
- package/dist/components/layout/back-button.d.ts +7 -1
- package/dist/components/layout/back-button.d.ts.map +1 -1
- package/dist/components/layout/back-button.js +34 -2
- package/dist/components/loading/unified-skeleton.d.ts +6 -1
- package/dist/components/loading/unified-skeleton.d.ts.map +1 -1
- package/dist/components/loading/unified-skeleton.js +13 -1
- package/dist/components/meeting-scheduler/index.cjs +119 -119
- package/dist/components/navigation/index.cjs +24 -24
- package/dist/components/onboarding-guides/index.cjs +24 -24
- package/dist/components/product-demo/index.cjs +33 -33
- package/dist/components/related-content/index.cjs +11 -11
- package/dist/components/tickets/index.cjs +28 -28
- package/dist/components/ui/file-manager/file-manager-table.d.ts.map +1 -1
- package/dist/components/ui/file-manager/file-manager-table.js +59 -50
- package/dist/components/ui/file-manager/index.cjs +71 -62
- package/dist/components/ui/index.cjs +23 -21
- package/dist/components/ui/markdown/engine.d.ts +5 -4
- package/dist/components/ui/markdown/engine.d.ts.map +1 -1
- package/dist/components/ui/markdown/engine.js +15 -15
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -1
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.js +7 -3
- package/dist/components/ui/people-cell.d.ts +9 -2
- package/dist/components/ui/people-cell.d.ts.map +1 -1
- package/dist/components/ui/people-cell.js +39 -23
- package/dist/components/ui/progress-bar.d.ts +6 -0
- package/dist/components/ui/progress-bar.d.ts.map +1 -1
- package/dist/components/ui/progress-bar.js +2 -0
- package/dist/components/ui/tag.d.ts +8 -0
- package/dist/components/ui/tag.d.ts.map +1 -1
- package/dist/components/ui/tag.js +5 -0
- package/dist/{devices-7YDIHVVP.cjs → devices-P7M4Z7VP.cjs} +22 -22
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/next-image.cjs +3 -3
- package/dist/{embeddable-chat-Z3S37SSM.cjs → embeddable-chat-322NDFP5.cjs} +94 -94
- package/dist/hooks/index.cjs +6 -6
- package/dist/index.cjs +32 -30
- package/dist/{knowledge-SQVPQSKM.cjs → knowledge-TC7JLOWI.cjs} +22 -22
- package/dist/{logs-M5DFGUBC.cjs → logs-GPOYCP4B.cjs} +28 -28
- package/dist/{policies-T3VQEJUD.cjs → policies-ZUHOUMDD.cjs} +25 -25
- package/dist/{remote-session-EE6477O3.cjs → remote-session-NT4NG7EF.cjs} +10 -10
- package/dist/{scripts-JFRFJKPH.cjs → scripts-FOEVDGTH.cjs} +28 -28
- package/dist/{software-update-E7W3AAIY.cjs → software-update-XGTMNFST.cjs} +25 -25
- package/dist/{tickets-board-44HO3LR4.cjs → tickets-board-7FLGQEHF.cjs} +10 -10
- package/dist/types/design-doc.d.ts +45 -8
- package/dist/types/design-doc.d.ts.map +1 -1
- package/dist/types/doc-source.d.ts +2 -2
- package/dist/types/doc-source.d.ts.map +1 -1
- package/dist/utils/design-doc-readiness.d.ts +17 -15
- package/dist/utils/design-doc-readiness.d.ts.map +1 -1
- package/dist/utils/design-doc-readiness.js +5 -11
- package/dist/utils/doc-tree-nav.d.ts +7 -0
- package/dist/utils/doc-tree-nav.d.ts.map +1 -1
- package/dist/utils/doc-tree-nav.js +19 -0
- package/dist/utils/index.cjs +30 -13
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/list-url.js +2 -1
- package/dist/utils/source-grouping.d.ts +2 -2
- package/dist/utils/source-grouping.d.ts.map +1 -1
- package/dist/utils/source-grouping.js +1 -1
- package/dist/utils/source-icons.d.ts.map +1 -1
- package/dist/utils/source-icons.js +3 -0
- package/package.json +1 -1
- package/src/components/chat/chat-composer.tsx +10 -3
- package/src/components/chat/chat-container.tsx +6 -4
- package/src/components/chat/entity-cards/design-doc-card.tsx +12 -17
- package/src/components/chat/entity-cards/dispatch.tsx +10 -10
- package/src/components/docs/doc-viewer.tsx +56 -35
- package/src/components/docs/docs-hub-page.tsx +13 -37
- package/src/components/docs/use-document-tree.ts +33 -23
- package/src/components/embeds/claude-embed.tsx +4 -4
- package/src/components/embeds/embed-iframe.tsx +8 -5
- package/src/components/embeds/embed-viewer-frame.tsx +35 -15
- package/src/components/embeds/figma-embed.tsx +9 -4
- package/src/components/embeds/file-download-card.tsx +28 -4
- package/src/components/embeds/google-sheets-viewer.tsx +9 -3
- package/src/components/embeds/pdf-viewer.tsx +9 -2
- package/src/components/features/status-filter-component.tsx +15 -0
- package/src/components/layout/back-button.tsx +54 -2
- package/src/components/loading/unified-skeleton.tsx +18 -2
- package/src/components/ui/file-manager/file-manager-table.tsx +8 -3
- package/src/components/ui/markdown/engine.tsx +21 -30
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +4 -0
- package/src/components/ui/people-cell.tsx +53 -17
- package/src/components/ui/progress-bar.tsx +8 -0
- package/src/components/ui/tag.tsx +14 -0
- package/src/types/design-doc.ts +59 -10
- package/src/types/doc-source.ts +2 -2
- package/src/utils/design-doc-readiness.ts +22 -23
- package/src/utils/doc-tree-nav.ts +27 -0
- package/src/utils/list-url.ts +2 -1
- package/src/utils/source-grouping.ts +2 -2
- package/src/utils/source-icons.ts +3 -0
- package/dist/components/docs/skeletons.d.ts +0 -25
- package/dist/components/docs/skeletons.d.ts.map +0 -1
- package/dist/components/docs/skeletons.js +0 -83
- package/src/components/docs/skeletons.tsx +0 -131
|
@@ -19,7 +19,7 @@ export interface ClaudeEmbedProps {
|
|
|
19
19
|
/** iframe height (CSS value), matching `FigmaEmbed`'s prop. */
|
|
20
20
|
height?: string;
|
|
21
21
|
/** iframe loading strategy. Defaults to `lazy`, as `FigmaEmbed` does. */
|
|
22
|
-
|
|
22
|
+
iframeLoading?: 'eager' | 'lazy';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const KIND_HEADING: Record<ClaudeEmbedKind, string> = {
|
|
@@ -51,7 +51,7 @@ const KIND_HEADING: Record<ClaudeEmbedKind, string> = {
|
|
|
51
51
|
* a Claude Design url (no embed route), an artifact whose author has not
|
|
52
52
|
* allow-listed this host, or an http host (mixed content).
|
|
53
53
|
*/
|
|
54
|
-
export function ClaudeEmbed({ url, kind = 'artifact', title, height,
|
|
54
|
+
export function ClaudeEmbed({ url, kind = 'artifact', title, height, iframeLoading = 'lazy' }: ClaudeEmbedProps) {
|
|
55
55
|
// Mirror detection lives in `useClaudeMirrorSrc` (derive from
|
|
56
56
|
// EndpointsRuntime + 1-byte ranged probe + background revalidation) — this
|
|
57
57
|
// component stays presentational.
|
|
@@ -70,7 +70,7 @@ export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = '
|
|
|
70
70
|
icon={<ClaudeIcon className="h-5 w-5 shrink-0" />}
|
|
71
71
|
title={title?.trim() || KIND_HEADING[kind]}
|
|
72
72
|
titleVariant="h6"
|
|
73
|
-
|
|
73
|
+
resolving={isProbing}
|
|
74
74
|
actions={
|
|
75
75
|
<Button
|
|
76
76
|
variant="outline"
|
|
@@ -89,7 +89,7 @@ export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = '
|
|
|
89
89
|
// (unlike Figma's player) — the shell provides the toggle for both
|
|
90
90
|
// the claude.ai `/embed` route and a mirror.
|
|
91
91
|
fullscreenControl
|
|
92
|
-
|
|
92
|
+
iframeLoading={iframeLoading}
|
|
93
93
|
height={height}
|
|
94
94
|
// Same frame contract Figma gets — an artifact is a real app: it copies
|
|
95
95
|
// to the clipboard and can go fullscreen.
|
|
@@ -43,8 +43,10 @@ export interface EmbedIframeProps {
|
|
|
43
43
|
allow?: string;
|
|
44
44
|
/** iframe `referrerPolicy` attribute */
|
|
45
45
|
referrerPolicy?: React.IframeHTMLAttributes<HTMLIFrameElement>['referrerPolicy'];
|
|
46
|
-
/** iframe `loading` attribute */
|
|
47
|
-
|
|
46
|
+
/** iframe `loading` attribute (eager or lazy). */
|
|
47
|
+
iframeLoading?: 'eager' | 'lazy';
|
|
48
|
+
/** The frame's source is not known yet: the frame draws its loading form. A loading scope does the same. */
|
|
49
|
+
loading?: boolean;
|
|
48
50
|
/** iframe `allowFullScreen` attribute */
|
|
49
51
|
allowFullScreen?: boolean;
|
|
50
52
|
/**
|
|
@@ -74,7 +76,8 @@ export function EmbedIframe({
|
|
|
74
76
|
height,
|
|
75
77
|
allow,
|
|
76
78
|
referrerPolicy,
|
|
77
|
-
|
|
79
|
+
iframeLoading,
|
|
80
|
+
loading = false,
|
|
78
81
|
allowFullScreen,
|
|
79
82
|
sandbox,
|
|
80
83
|
}: EmbedIframeProps) {
|
|
@@ -87,7 +90,7 @@ export function EmbedIframe({
|
|
|
87
90
|
const isLoaded = loadedSrc === src;
|
|
88
91
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
89
92
|
const handleLoad = useCallback(() => setLoadedSrc(src), [src]);
|
|
90
|
-
const held = useLoadingScope();
|
|
93
|
+
const held = useLoadingScope() || loading;
|
|
91
94
|
|
|
92
95
|
useEffect(() => {
|
|
93
96
|
const iframe = iframeRef.current;
|
|
@@ -123,7 +126,7 @@ export function EmbedIframe({
|
|
|
123
126
|
onLoad={handleLoad}
|
|
124
127
|
allow={allow}
|
|
125
128
|
referrerPolicy={referrerPolicy}
|
|
126
|
-
loading={
|
|
129
|
+
loading={iframeLoading}
|
|
127
130
|
allowFullScreen={allow?.includes('fullscreen') ? undefined : allowFullScreen}
|
|
128
131
|
sandbox={sandbox}
|
|
129
132
|
/>
|
|
@@ -5,6 +5,7 @@ import type React from 'react';
|
|
|
5
5
|
import { useEffect, useRef, useState } from 'react';
|
|
6
6
|
import { cn } from '../../utils/cn';
|
|
7
7
|
import { FullscreenSwitchController } from '../../utils/fullscreen-switch';
|
|
8
|
+
import { InteractiveSkeleton, TextLineSkeleton } from '../loading/unified-skeleton';
|
|
8
9
|
import { Button } from '../ui/button/button';
|
|
9
10
|
import { useLoadingScope } from '../ui/loading-scope';
|
|
10
11
|
import { EmbedIframe, EmbedLoadingSkeleton } from './embed-iframe';
|
|
@@ -40,21 +41,22 @@ export interface EmbedViewerFrameProps {
|
|
|
40
41
|
/** Rendered verbatim in the header row — the caller owns its own container/state. */
|
|
41
42
|
actions?: React.ReactNode;
|
|
42
43
|
/** Iframe source; falsy renders the inline empty state (or the loading
|
|
43
|
-
* state when `
|
|
44
|
+
* state when `loading` or `resolving`) under the header. */
|
|
44
45
|
src: string | null | undefined;
|
|
45
|
-
/**
|
|
46
|
-
* loading skeleton
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
|
|
46
|
+
/** The document is being read (the standard `loading` prop; a loading scope does the same): the whole frame is its
|
|
47
|
+
* loading form, a skeleton title, its buttons' loading forms and the frame's loading box. */
|
|
48
|
+
loading?: boolean;
|
|
49
|
+
/** The document is known (its title shows) but the viewer is still finding where its frame points (ClaudeEmbed
|
|
50
|
+
* probing for a self-hosted mirror): only the body is its loading box. */
|
|
51
|
+
resolving?: boolean;
|
|
51
52
|
/** Large icon for the inline empty state (the viewers pass `w-16 h-16`). */
|
|
52
53
|
emptyIcon?: React.ReactNode;
|
|
53
54
|
emptyMessage?: string;
|
|
54
55
|
height?: string;
|
|
55
56
|
allow?: string;
|
|
56
57
|
allowFullScreen?: boolean;
|
|
57
|
-
loading
|
|
58
|
+
/** iframe loading strategy (eager or lazy). */
|
|
59
|
+
iframeLoading?: 'eager' | 'lazy';
|
|
58
60
|
/** iframe `sandbox` — see `EmbedIframe`. Set it for user-authored contents. */
|
|
59
61
|
sandbox?: string;
|
|
60
62
|
/** Wrapper classes; defaults to the sheets/pdf `space-y-4`. */
|
|
@@ -66,6 +68,8 @@ export interface EmbedViewerFrameProps {
|
|
|
66
68
|
* button (the Figma player) and some cannot (a self-hosted Claude
|
|
67
69
|
* artifact mirror) — the shell provides the affordance uniformly. */
|
|
68
70
|
fullscreenControl?: boolean;
|
|
71
|
+
/** How many buttons `actions` draws (PDF: Preview and Download): the header's loading form draws as many. Default 1. */
|
|
72
|
+
loadingActionCount?: number;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
export function EmbedViewerFrame({
|
|
@@ -74,19 +78,23 @@ export function EmbedViewerFrame({
|
|
|
74
78
|
titleVariant = 'h3',
|
|
75
79
|
actions,
|
|
76
80
|
src,
|
|
77
|
-
|
|
81
|
+
loading: loadingGiven = false,
|
|
82
|
+
resolving = false,
|
|
78
83
|
emptyIcon,
|
|
79
84
|
emptyMessage = 'Content not available',
|
|
80
85
|
height,
|
|
81
86
|
allow,
|
|
82
87
|
allowFullScreen,
|
|
83
|
-
|
|
88
|
+
iframeLoading,
|
|
84
89
|
sandbox,
|
|
85
90
|
className,
|
|
86
91
|
fullscreenControl,
|
|
92
|
+
loadingActionCount = 1,
|
|
87
93
|
}: EmbedViewerFrameProps) {
|
|
88
|
-
// ONE switch: a page's loading scope, or the
|
|
89
|
-
|
|
94
|
+
// ONE switch for the whole frame (header included): a page's loading scope, or the standard `loading` prop. The body
|
|
95
|
+
// alone also waits while the viewer resolves where its frame points.
|
|
96
|
+
const loadingForm = useLoadingScope() || loadingGiven;
|
|
97
|
+
const isLoading = loadingForm || resolving;
|
|
90
98
|
const frameRef = useRef<HTMLDivElement>(null);
|
|
91
99
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
92
100
|
|
|
@@ -127,7 +135,11 @@ export function EmbedViewerFrame({
|
|
|
127
135
|
<div className="flex flex-col gap-3 content-sm:flex-row content-sm:items-center content-sm:justify-between">
|
|
128
136
|
<div className="flex min-w-0 items-center gap-2">
|
|
129
137
|
{icon}
|
|
130
|
-
{
|
|
138
|
+
{loadingForm ? (
|
|
139
|
+
// The document is being read: its name is not known yet. (A viewer still resolving its frame, with a
|
|
140
|
+
// known title, keeps it.)
|
|
141
|
+
<TextLineSkeleton type={titleVariant} className="w-48 max-w-full" />
|
|
142
|
+
) : titleVariant === 'h3' ? (
|
|
131
143
|
<h2 className="truncate text-ods-text-primary text-h3">{title}</h2>
|
|
132
144
|
) : (
|
|
133
145
|
<span className="truncate font-semibold text-ods-text-primary text-h6">{title}</span>
|
|
@@ -140,7 +152,15 @@ export function EmbedViewerFrame({
|
|
|
140
152
|
fullscreen toggle, `actions` renders EXACTLY as before — the
|
|
141
153
|
wrapper exists only when the toggle joins it, so pre-existing
|
|
142
154
|
viewers (figma/pdf/sheets) are byte-identical. */}
|
|
143
|
-
{
|
|
155
|
+
{loadingForm ? (
|
|
156
|
+
// The actions point at the document being read: each its button's loading form, in the actions' row.
|
|
157
|
+
<div className="flex w-full items-center gap-2 content-sm:w-auto">
|
|
158
|
+
{Array.from({ length: loadingActionCount }, (_, i) => (
|
|
159
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: loading buttons never reorder
|
|
160
|
+
<InteractiveSkeleton.Button key={i} size="small-legacy" className="flex-1 content-sm:flex-initial" />
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
) : fullscreenControl && src ? (
|
|
144
164
|
<div className="flex flex-col gap-[var(--spacing-system-xsf)] content-sm:flex-row content-sm:items-center">
|
|
145
165
|
{actions}
|
|
146
166
|
<Button
|
|
@@ -169,7 +189,7 @@ export function EmbedViewerFrame({
|
|
|
169
189
|
height={isFullscreen ? 'calc(100vh - 96px)' : height}
|
|
170
190
|
allow={allow}
|
|
171
191
|
allowFullScreen={allowFullScreen}
|
|
172
|
-
|
|
192
|
+
iframeLoading={iframeLoading}
|
|
173
193
|
sandbox={sandbox}
|
|
174
194
|
/>
|
|
175
195
|
) : isLoading ? (
|
|
@@ -19,7 +19,9 @@ export interface FigmaEmbedProps {
|
|
|
19
19
|
*/
|
|
20
20
|
height?: string;
|
|
21
21
|
/** iframe loading strategy. Defaults to `"lazy"`; the data-room viewer passes `"eager"`. */
|
|
22
|
-
|
|
22
|
+
iframeLoading?: 'eager' | 'lazy';
|
|
23
|
+
/** The document is being read: the embed draws its loading form. A loading scope does the same. */
|
|
24
|
+
loading?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
type SlidesView = 'present' | 'browse';
|
|
@@ -42,7 +44,9 @@ function SlidesViewToggle({ view, onChange }: { view: SlidesView; onChange: (v:
|
|
|
42
44
|
if (v && v !== view) onChange(v as SlidesView);
|
|
43
45
|
}}
|
|
44
46
|
aria-label="Figma slides view mode"
|
|
45
|
-
|
|
47
|
+
// The whole control is one button tall (40px, border and padding included), so the header row is the same height
|
|
48
|
+
// with or without it, loading and loaded alike.
|
|
49
|
+
className="flex h-10 shrink-0 items-center gap-0.5 rounded-lg border border-ods-border bg-ods-card p-0.5"
|
|
46
50
|
>
|
|
47
51
|
{options.map(({ key, label, Icon }) => {
|
|
48
52
|
const active = view === key;
|
|
@@ -51,7 +55,7 @@ function SlidesViewToggle({ view, onChange }: { view: SlidesView; onChange: (v:
|
|
|
51
55
|
key={key}
|
|
52
56
|
value={key}
|
|
53
57
|
aria-label={label}
|
|
54
|
-
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1 transition-colors text-h6 ${
|
|
58
|
+
className={`flex h-full items-center gap-1.5 rounded-md px-2.5 py-1 transition-colors text-h6 ${
|
|
55
59
|
active
|
|
56
60
|
? 'bg-ods-accent text-ods-text-on-accent'
|
|
57
61
|
: 'text-ods-text-secondary hover:bg-ods-bg-hover hover:text-ods-text-primary'
|
|
@@ -80,7 +84,7 @@ function SlidesViewToggle({ view, onChange }: { view: SlidesView; onChange: (v:
|
|
|
80
84
|
* plain ReactNode) — flipping it recomputes `embedSrc`, and the frame's
|
|
81
85
|
* `EmbedIframe` remounts on the new src exactly as before the extraction.
|
|
82
86
|
*/
|
|
83
|
-
export function FigmaEmbed({ url, title, height,
|
|
87
|
+
export function FigmaEmbed({ url, title, height, iframeLoading = 'lazy', loading = false }: FigmaEmbedProps) {
|
|
84
88
|
const [view, setView] = useState<SlidesView>('present');
|
|
85
89
|
const isSlides = url ? isFigmaSlidesUrl(url) : false;
|
|
86
90
|
const embedSrc = url ? toFigmaEmbedUrl(url, { slidesView: view }) : null;
|
|
@@ -124,6 +128,7 @@ export function FigmaEmbed({ url, title, height, loading = 'lazy' }: FigmaEmbedP
|
|
|
124
128
|
}
|
|
125
129
|
src={embedSrc}
|
|
126
130
|
allow="clipboard-write; clipboard-read; fullscreen"
|
|
131
|
+
iframeLoading={iframeLoading}
|
|
127
132
|
loading={loading}
|
|
128
133
|
height={height}
|
|
129
134
|
allowFullScreen
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { FileText, Download } from 'lucide-react';
|
|
2
2
|
import { formatFileSize } from '../../utils';
|
|
3
|
+
import { InteractiveSkeleton, TextLineSkeleton } from '../loading/unified-skeleton';
|
|
3
4
|
import { Button } from '../ui';
|
|
5
|
+
import { useLoadingScope } from '../ui/loading-scope';
|
|
4
6
|
|
|
5
7
|
export interface FileDownloadCardProps {
|
|
6
8
|
fileName?: string;
|
|
7
9
|
mimeType?: string;
|
|
8
10
|
fileSize?: number;
|
|
9
11
|
fileUrl?: string;
|
|
12
|
+
/** The file is being read: the card draws its loading form. A loading scope does the same. */
|
|
13
|
+
loading?: boolean;
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
/** The card's boxes, shared by the card and its loading form. */
|
|
17
|
+
const FILE_CARD_OUTER_CLASS = 'flex flex-col items-center justify-center py-16';
|
|
18
|
+
const FILE_CARD_CLASS = 'w-full max-w-md space-y-4 rounded-xl border border-ods-border bg-ods-card p-8 text-center';
|
|
19
|
+
const FILE_ICON_CLASS = 'mx-auto h-16 w-16 text-ods-text-secondary';
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* Generic downloadable-file card for the `file` document type. Used by
|
|
14
23
|
* `<DocsHubPage>`'s default `documentTypeRenderers.file`. Embedders can
|
|
@@ -18,11 +27,26 @@ export interface FileDownloadCardProps {
|
|
|
18
27
|
* renders the filename + type + size so the user knows what they were
|
|
19
28
|
* about to download).
|
|
20
29
|
*/
|
|
21
|
-
export function FileDownloadCard({ fileName, mimeType, fileSize, fileUrl }: FileDownloadCardProps) {
|
|
30
|
+
export function FileDownloadCard({ fileName, mimeType, fileSize, fileUrl, loading = false }: FileDownloadCardProps) {
|
|
31
|
+
// In a loading scope the file is being read: the card draws its own loading form, in its own boxes.
|
|
32
|
+
if (useLoadingScope() || loading) {
|
|
33
|
+
return (
|
|
34
|
+
<div className={FILE_CARD_OUTER_CLASS}>
|
|
35
|
+
<div className={FILE_CARD_CLASS}>
|
|
36
|
+
<FileText className={FILE_ICON_CLASS} />
|
|
37
|
+
<div>
|
|
38
|
+
<TextLineSkeleton type="h3" className="mx-auto w-2/3" />
|
|
39
|
+
<TextLineSkeleton type="h6" className="mx-auto mt-2 w-1/2" />
|
|
40
|
+
</div>
|
|
41
|
+
<InteractiveSkeleton.Button className="mx-auto" />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
22
46
|
return (
|
|
23
|
-
<div className=
|
|
24
|
-
<div className=
|
|
25
|
-
<FileText className=
|
|
47
|
+
<div className={FILE_CARD_OUTER_CLASS}>
|
|
48
|
+
<div className={FILE_CARD_CLASS}>
|
|
49
|
+
<FileText className={FILE_ICON_CLASS} />
|
|
26
50
|
<div>
|
|
27
51
|
<h3 className="text-ods-text-primary text-h3">{fileName || 'File'}</h3>
|
|
28
52
|
<div className="mt-2 flex items-center justify-center gap-3 text-ods-text-secondary text-h6">
|
|
@@ -4,20 +4,25 @@ import { ExternalLink } from 'lucide-react';
|
|
|
4
4
|
import { toGoogleSheetsEmbedUrl, toGoogleSheetsOriginalUrl } from '../../utils/embed-url-converters';
|
|
5
5
|
import { GoogleSheetsIcon } from '../icons-v2-generated';
|
|
6
6
|
import { Button } from '../ui';
|
|
7
|
+
import { useLoadingScope } from '../ui/loading-scope';
|
|
7
8
|
import { EmbedViewerFrame } from './embed-viewer-frame';
|
|
8
9
|
|
|
9
10
|
export interface GoogleSheetsViewerProps {
|
|
10
11
|
externalUrl: string;
|
|
11
12
|
fileName?: string;
|
|
12
13
|
height?: string;
|
|
14
|
+
/** The document is being read: the viewer draws its loading form. A loading scope does the same. */
|
|
15
|
+
loading?: boolean;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
export function GoogleSheetsViewer({ externalUrl, fileName, height }: GoogleSheetsViewerProps) {
|
|
18
|
+
export function GoogleSheetsViewer({ externalUrl, fileName, height, loading = false }: GoogleSheetsViewerProps) {
|
|
16
19
|
const displayName = fileName || 'Google Sheet';
|
|
20
|
+
// In a loading scope the document is being read: the frame draws its loading form, never "not configured".
|
|
21
|
+
const isLoading = useLoadingScope() || loading;
|
|
17
22
|
|
|
18
23
|
// Historical shape: no URL at all → standalone empty state with NO header
|
|
19
24
|
// (the frame's empty body keeps the header — that's the figma behavior).
|
|
20
|
-
if (!externalUrl) {
|
|
25
|
+
if (!externalUrl && !isLoading) {
|
|
21
26
|
return (
|
|
22
27
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
23
28
|
<GoogleSheetsIcon className="mb-4 h-16 w-16 text-ods-text-secondary" />
|
|
@@ -43,8 +48,9 @@ export function GoogleSheetsViewer({ externalUrl, fileName, height }: GoogleShee
|
|
|
43
48
|
Open in Google Sheets
|
|
44
49
|
</Button>
|
|
45
50
|
}
|
|
46
|
-
src={toGoogleSheetsEmbedUrl(externalUrl)}
|
|
51
|
+
src={externalUrl ? toGoogleSheetsEmbedUrl(externalUrl) : null}
|
|
47
52
|
height={height}
|
|
53
|
+
loading={loading}
|
|
48
54
|
/>
|
|
49
55
|
);
|
|
50
56
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Download, Eye } from 'lucide-react';
|
|
4
4
|
import { AdobePdfIcon } from '../icons-v2-generated';
|
|
5
5
|
import { Button } from '../ui';
|
|
6
|
+
import { useLoadingScope } from '../ui/loading-scope';
|
|
6
7
|
import { EmbedViewerFrame } from './embed-viewer-frame';
|
|
7
8
|
|
|
8
9
|
export interface PdfViewerProps {
|
|
@@ -11,13 +12,17 @@ export interface PdfViewerProps {
|
|
|
11
12
|
onPreview?: () => void;
|
|
12
13
|
onDownload?: () => void;
|
|
13
14
|
height?: string;
|
|
15
|
+
/** The document is being read: the viewer draws its loading form. A loading scope does the same. */
|
|
16
|
+
loading?: boolean;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export function PdfViewer({ src, fileName, onPreview, onDownload, height }: PdfViewerProps) {
|
|
19
|
+
export function PdfViewer({ src, fileName, onPreview, onDownload, height, loading = false }: PdfViewerProps) {
|
|
17
20
|
const displayName = fileName || 'PDF Document';
|
|
21
|
+
// In a loading scope the document is being read: the frame draws its loading form, never "not available".
|
|
22
|
+
const isLoading = useLoadingScope() || loading;
|
|
18
23
|
|
|
19
24
|
// Historical shape: no src at all → standalone empty state with NO header.
|
|
20
|
-
if (!src) {
|
|
25
|
+
if (!src && !isLoading) {
|
|
21
26
|
return (
|
|
22
27
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
23
28
|
<AdobePdfIcon className="mb-4 h-16 w-16 text-ods-text-secondary" />
|
|
@@ -57,6 +62,8 @@ export function PdfViewer({ src, fileName, onPreview, onDownload, height }: PdfV
|
|
|
57
62
|
</div>
|
|
58
63
|
}
|
|
59
64
|
src={src}
|
|
65
|
+
loadingActionCount={2}
|
|
66
|
+
loading={loading}
|
|
60
67
|
height={height}
|
|
61
68
|
/>
|
|
62
69
|
);
|
|
@@ -43,6 +43,12 @@ export interface StatusFilterComponentProps {
|
|
|
43
43
|
showAll?: boolean;
|
|
44
44
|
/** The options are read on the client and have not arrived: the row draws its skeleton, as in a loading scope. */
|
|
45
45
|
loading?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The options' labels are known but their counts are not ("Active" now, "Active (33)" once read): while loading,
|
|
48
|
+
* each option is drawn as its real button with a bar where the count goes, so the row keeps its width and, on a
|
|
49
|
+
* narrow screen, wraps onto the same lines as the loaded row.
|
|
50
|
+
*/
|
|
51
|
+
countsPending?: boolean;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
/** The row's frame, label and controls, read by the row and by its skeleton, so the skeleton is the row's size. */
|
|
@@ -74,6 +80,7 @@ export function StatusFilterComponent({
|
|
|
74
80
|
label = 'Status',
|
|
75
81
|
showAll = true,
|
|
76
82
|
loading = false,
|
|
83
|
+
countsPending = false,
|
|
77
84
|
}: StatusFilterComponentProps) {
|
|
78
85
|
// Filter out 'all' from options since we render it separately
|
|
79
86
|
const filteredOptions = statusOptions.filter(option => option.value !== 'all');
|
|
@@ -84,6 +91,7 @@ export function StatusFilterComponent({
|
|
|
84
91
|
label={label}
|
|
85
92
|
// Options the page states itself are its own words: drawn for real. Read ones arrive empty: boxes.
|
|
86
93
|
options={filteredOptions.length > 0 ? filteredOptions.map(option => option.label) : undefined}
|
|
94
|
+
countsPending={countsPending}
|
|
87
95
|
showAll={showAll}
|
|
88
96
|
showCount={showCount}
|
|
89
97
|
className={className}
|
|
@@ -142,6 +150,7 @@ function StatusFilterComponentSkeleton({
|
|
|
142
150
|
label = 'Status',
|
|
143
151
|
options,
|
|
144
152
|
pills = STATUS_FILTER_SKELETON_PILLS,
|
|
153
|
+
countsPending = false,
|
|
145
154
|
showAll = true,
|
|
146
155
|
showCount = false,
|
|
147
156
|
className,
|
|
@@ -150,6 +159,8 @@ function StatusFilterComponentSkeleton({
|
|
|
150
159
|
/** Option labels the page states itself: drawn as real buttons (in place of `pills` boxes). */
|
|
151
160
|
options?: string[];
|
|
152
161
|
pills?: number;
|
|
162
|
+
/** Each option's count is still being read: a bar the width of " (00)" follows its label. */
|
|
163
|
+
countsPending?: boolean;
|
|
153
164
|
showAll?: boolean;
|
|
154
165
|
showCount?: boolean;
|
|
155
166
|
className?: string;
|
|
@@ -182,6 +193,10 @@ function StatusFilterComponentSkeleton({
|
|
|
182
193
|
tabIndex={-1}
|
|
183
194
|
>
|
|
184
195
|
{option}
|
|
196
|
+
{/* With the button's gap before it, a 2ch bar is the width of the " (00)" the count adds. */}
|
|
197
|
+
{countsPending && (
|
|
198
|
+
<TextLineSkeleton type="inherit" as="span" className="inline-block w-[2ch] align-top" />
|
|
199
|
+
)}
|
|
185
200
|
</Button>
|
|
186
201
|
))
|
|
187
202
|
: Array.from({ length: pills }, (_, index) => (
|
|
@@ -14,6 +14,28 @@ export interface BackButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLBut
|
|
|
14
14
|
* prefetch, right-click); without it, a button that runs `onClick`.
|
|
15
15
|
*/
|
|
16
16
|
href?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Go back the way the browser's own back button does: when this tab has an earlier page of the same site, the
|
|
19
|
+
* click returns to it (with the address it had: its filters, page and sort), and `href` is only where a visitor
|
|
20
|
+
* who arrived from outside (a shared link, a new tab) goes. Needs `href`.
|
|
21
|
+
*/
|
|
22
|
+
historyBack?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The earlier page of this tab, when it is a page of the same site (the Navigation API, else the referrer). */
|
|
26
|
+
function hasSameSiteHistory(): boolean {
|
|
27
|
+
if (typeof window === 'undefined') return false;
|
|
28
|
+
const nav = (window as Window & { navigation?: { canGoBack?: boolean; currentEntry?: { index: number } | null } })
|
|
29
|
+
.navigation;
|
|
30
|
+
// The Navigation API lists only this tab's same-origin entries, so an earlier one is a page of this site.
|
|
31
|
+
if (nav && typeof nav.canGoBack === 'boolean') return nav.canGoBack && (nav.currentEntry?.index ?? 0) > 0;
|
|
32
|
+
try {
|
|
33
|
+
return (
|
|
34
|
+
window.history.length > 1 && !!document.referrer && new URL(document.referrer).origin === window.location.origin
|
|
35
|
+
);
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
17
39
|
}
|
|
18
40
|
|
|
19
41
|
/** Its place in the page: the start of the column, above the title. The box is the `link` button's at `default` size. */
|
|
@@ -30,7 +52,14 @@ export const BACK_BUTTON_SKELETON_CLASS = 'h-11 w-40 rounded-md md:h-12';
|
|
|
30
52
|
* Every detail page renders this one (through `PageLayout`'s `backButton` or
|
|
31
53
|
* directly), never a hand-built link.
|
|
32
54
|
*/
|
|
33
|
-
export function BackButton({
|
|
55
|
+
export function BackButton({
|
|
56
|
+
label = 'Back',
|
|
57
|
+
className,
|
|
58
|
+
type = 'button',
|
|
59
|
+
href,
|
|
60
|
+
historyBack = false,
|
|
61
|
+
...props
|
|
62
|
+
}: BackButtonProps) {
|
|
34
63
|
const content = (
|
|
35
64
|
<>
|
|
36
65
|
<Chevron02LeftIcon className="size-6 shrink-0" />
|
|
@@ -54,7 +83,30 @@ export function BackButton({ label = 'Back', className, type = 'button', href, .
|
|
|
54
83
|
} = props;
|
|
55
84
|
return (
|
|
56
85
|
<Button asChild variant="link" noPaddingX className={cn(BACK_BUTTON_LAYOUT_CLASS, className)}>
|
|
57
|
-
<Link
|
|
86
|
+
<Link
|
|
87
|
+
{...(shared as unknown as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
|
|
88
|
+
href={href}
|
|
89
|
+
onClick={
|
|
90
|
+
historyBack
|
|
91
|
+
? (event: React.MouseEvent<HTMLAnchorElement>) => {
|
|
92
|
+
(shared.onClick as unknown as React.MouseEventHandler<HTMLAnchorElement> | undefined)?.(event);
|
|
93
|
+
// A modified click (new tab, new window) keeps the link's own behaviour.
|
|
94
|
+
if (
|
|
95
|
+
event.defaultPrevented ||
|
|
96
|
+
event.button !== 0 ||
|
|
97
|
+
event.metaKey ||
|
|
98
|
+
event.ctrlKey ||
|
|
99
|
+
event.shiftKey ||
|
|
100
|
+
event.altKey
|
|
101
|
+
)
|
|
102
|
+
return;
|
|
103
|
+
if (!hasSameSiteHistory()) return;
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
window.history.back();
|
|
106
|
+
}
|
|
107
|
+
: (shared.onClick as unknown as React.MouseEventHandler<HTMLAnchorElement> | undefined)
|
|
108
|
+
}
|
|
109
|
+
>
|
|
58
110
|
{content}
|
|
59
111
|
</Link>
|
|
60
112
|
</Button>
|
|
@@ -179,11 +179,16 @@ export function TextLineSkeleton({ type, lines = 1, as = 'div', width, className
|
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* THE lines a text run takes per width: on a phone, from `content-md`, from `content-lg`, and from `content-xl` where
|
|
184
|
+
* a wide screen wraps it differently (`lg` when not given). Every loading text whose length changes with the width
|
|
185
|
+
* states its lines in this one shape.
|
|
186
|
+
*/
|
|
183
187
|
export interface TextLinesByBreakpoint {
|
|
184
188
|
phone: number;
|
|
185
189
|
md: number;
|
|
186
190
|
lg: number;
|
|
191
|
+
xl?: number;
|
|
187
192
|
}
|
|
188
193
|
|
|
189
194
|
/**
|
|
@@ -215,8 +220,19 @@ export function TextLinesByBreakpointSkeleton({
|
|
|
215
220
|
type={type}
|
|
216
221
|
as={as}
|
|
217
222
|
lines={lines.lg}
|
|
218
|
-
className={cn(
|
|
223
|
+
className={cn(
|
|
224
|
+
lines.xl === undefined ? 'hidden w-full content-lg:flex' : 'hidden w-full content-lg:flex content-xl:hidden',
|
|
225
|
+
className,
|
|
226
|
+
)}
|
|
219
227
|
/>
|
|
228
|
+
{lines.xl === undefined ? null : (
|
|
229
|
+
<TextLineSkeleton
|
|
230
|
+
type={type}
|
|
231
|
+
as={as}
|
|
232
|
+
lines={lines.xl}
|
|
233
|
+
className={cn('hidden w-full content-xl:flex', className)}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
220
236
|
</>
|
|
221
237
|
);
|
|
222
238
|
}
|
|
@@ -57,10 +57,15 @@ export function FileManagerTable({
|
|
|
57
57
|
// The height its parent gives it, both forms: the rows scroll inside, under the header. Not
|
|
58
58
|
// the window's: whatever is below the parent (a docked composer, a page's padding) would
|
|
59
59
|
// hide the last rows under it. A parent of no definite height lets the table grow with its rows.
|
|
60
|
-
|
|
60
|
+
// `overflow-hidden` rounds the header and the rows at the border's inner curve: a child rounded
|
|
61
|
+
// to the frame's own radius sits 1px inside it and leaves a sliver of the frame's fill at each
|
|
62
|
+
// corner. Nothing floats out of the table (the row menu is portalled).
|
|
63
|
+
<div
|
|
64
|
+
className={cn('flex h-full flex-col overflow-hidden rounded-lg border border-ods-border bg-ods-bg', className)}
|
|
65
|
+
>
|
|
61
66
|
{/* ONE header in both forms: the column labels are copy, and while the folder is read the select-all box is the
|
|
62
67
|
real checkbox, inert. */}
|
|
63
|
-
<div className="flex h-12 items-center
|
|
68
|
+
<div className="flex h-12 items-center border-b border-ods-border bg-ods-bg-surface px-4">
|
|
64
69
|
{showCheckboxes && (
|
|
65
70
|
<div
|
|
66
71
|
className={FILE_MANAGER_CHECKBOX_CELL_CLASS}
|
|
@@ -84,7 +89,7 @@ export function FileManagerTable({
|
|
|
84
89
|
<div className={FILE_MANAGER_ACTIONS_CELL_BOX_CLASS}>{/* Space for action buttons */}</div>
|
|
85
90
|
</div>
|
|
86
91
|
|
|
87
|
-
<div className="flex-1 divide-y divide-ods-border overflow-auto
|
|
92
|
+
<div className="flex-1 divide-y divide-ods-border overflow-auto">
|
|
88
93
|
{loading ? (
|
|
89
94
|
// A folder listing has no page size: a typical folder's rows, each the row's own loading form.
|
|
90
95
|
<LoadingItems count={FILE_MANAGER_SKELETON_ROWS}>
|