@flamingo-stack/openframe-frontend-core 0.0.848 → 0.0.850
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/{chunk-MWYEROAP.cjs → chunk-2QZUTNL3.cjs} +55 -55
- package/dist/{chunk-YXKAHQYS.cjs → chunk-533QK4R6.cjs} +2 -1
- package/dist/{chunk-5HOWR4TG.cjs → chunk-5CBI2ZYV.cjs} +5 -5
- package/dist/{chunk-NYT6IBIX.cjs → chunk-5PIR77V5.cjs} +4 -4
- package/dist/{chunk-IIUZE73M.cjs → chunk-6AJYGXXS.cjs} +14 -14
- package/dist/{chunk-VW4446JR.cjs → chunk-7ICHJVAT.cjs} +16 -16
- package/dist/{chunk-U7UBYJEZ.cjs → chunk-BU2PSRAS.cjs} +5 -5
- package/dist/{chunk-IKOL6NJY.cjs → chunk-CAGNWDVJ.cjs} +11 -1
- package/dist/{chunk-O4BXHCMK.cjs → chunk-FRG724DU.cjs} +2 -2
- package/dist/{chunk-YAW7CHLV.cjs → chunk-FXLROQUT.cjs} +41 -38
- package/dist/{chunk-IUZVOQXP.cjs → chunk-GAYLAA3L.cjs} +45 -45
- package/dist/{chunk-7YJQTQPV.cjs → chunk-IG6ROHPS.cjs} +46 -46
- package/dist/{chunk-INA2FX2N.cjs → chunk-IHY7E4EX.cjs} +2 -2
- package/dist/{chunk-Y2ZWWIUA.cjs → chunk-IN3LA2FU.cjs} +2 -2
- package/dist/{chunk-74Z34I2C.cjs → chunk-IPGGSFR7.cjs} +6 -6
- package/dist/{chunk-7DW3LZ52.cjs → chunk-JWIITCVB.cjs} +94 -173
- package/dist/{chunk-OCJXOSIH.cjs → chunk-LGYPCUHO.cjs} +32 -32
- package/dist/{chunk-7LE4HWHA.cjs → chunk-OHHDX3Z4.cjs} +24 -24
- package/dist/{chunk-C3WZXOZJ.cjs → chunk-QJER2ZNA.cjs} +3 -3
- package/dist/{chunk-GASFYPGH.cjs → chunk-R5JPZ6TY.cjs} +14 -14
- package/dist/{chunk-QUSCSYHJ.cjs → chunk-TX4NTH22.cjs} +45 -26
- package/dist/{chunk-Y4BG7SPX.cjs → chunk-VRWBJL7B.cjs} +66 -66
- package/dist/{chunk-2V6FCVUL.cjs → chunk-XKOL5CNU.cjs} +449 -390
- package/dist/{chunk-I3YRYS23.cjs → chunk-ZDSC3GFX.cjs} +38 -12
- package/dist/{cloud-tenants-YAZHLPOQ.cjs → cloud-tenants-TVB3DT3J.cjs} +8 -8
- package/dist/components/case-studies/index.cjs +22 -22
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.js +1 -0
- package/dist/components/chat/index.cjs +5 -5
- package/dist/components/contact/index.cjs +9 -9
- 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 +12 -12
- 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 +14 -9
- package/dist/components/embeds/embed-viewer-frame.d.ts.map +1 -1
- package/dist/components/embeds/embed-viewer-frame.js +31 -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 +9 -9
- 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 +7 -2
- package/dist/components/faq/index.cjs +10 -10
- package/dist/components/features/index.cjs +10 -8
- package/dist/components/features/software/index.d.ts +1 -1
- package/dist/components/features/software/index.d.ts.map +1 -1
- package/dist/components/features/software/index.js +2 -0
- package/dist/components/features/software/software-action-copy.d.ts.map +1 -1
- package/dist/components/features/software/software-action-copy.js +6 -12
- package/dist/components/features/software/software-action-form.d.ts +7 -1
- package/dist/components/features/software/software-action-form.d.ts.map +1 -1
- package/dist/components/features/software/software-action-form.js +11 -3
- package/dist/components/features/software/update-settings-fields.d.ts +12 -1
- package/dist/components/features/software/update-settings-fields.d.ts.map +1 -1
- package/dist/components/features/software/update-settings-fields.js +57 -29
- package/dist/components/help-center-pages/index.cjs +87 -87
- package/dist/components/index.cjs +52 -50
- package/dist/components/meeting-scheduler/index.cjs +40 -40
- package/dist/components/navigation/index.cjs +11 -11
- package/dist/components/onboarding-guides/index.cjs +11 -11
- package/dist/components/product-demo/index.cjs +18 -18
- package/dist/components/related-content/index.cjs +4 -4
- package/dist/components/tickets/index.cjs +13 -13
- package/dist/components/ui/index.cjs +8 -8
- 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/page-actions.d.ts.map +1 -1
- package/dist/components/ui/page-actions.js +11 -1
- package/dist/{devices-IZROCLFK.cjs → devices-NGCTSXIU.cjs} +9 -9
- package/dist/{embeddable-chat-ETW6S22E.cjs → embeddable-chat-322NDFP5.cjs} +74 -74
- package/dist/index.cjs +18 -12
- package/dist/{knowledge-4IRWLTNW.cjs → knowledge-75PO5DYS.cjs} +9 -9
- package/dist/{logs-EFDGY5IO.cjs → logs-PZESOAIG.cjs} +15 -15
- package/dist/{policies-3KEXZ5BP.cjs → policies-6XNMBFWO.cjs} +12 -12
- package/dist/{remote-session-NT4NG7EF.cjs → remote-session-Q2FIBCGT.cjs} +4 -4
- package/dist/{scripts-PCGTZLZL.cjs → scripts-B3JW6TPF.cjs} +15 -15
- package/dist/{software-update-YCDNVFJE.cjs → software-update-NWP542J7.cjs} +12 -12
- package/dist/types/doc-source.d.ts +2 -2
- package/dist/types/doc-source.d.ts.map +1 -1
- 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 +25 -2
- 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/entity-cards/dispatch.tsx +3 -0
- 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 +43 -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 +12 -2
- package/src/components/features/software/index.ts +1 -0
- package/src/components/features/software/software-action-copy.ts +19 -12
- package/src/components/features/software/software-action-form.tsx +16 -3
- package/src/components/features/software/update-settings-fields.tsx +104 -32
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +4 -0
- package/src/components/ui/page-actions.tsx +19 -2
- package/src/types/doc-source.ts +2 -2
- 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,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,20 @@ 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
|
-
|
|
19
|
+
/** The header's loading form: Preview and Download, the viewer's two actions. */
|
|
20
|
+
const PDF_VIEWER_SKELETON_ACTIONS = 2;
|
|
21
|
+
|
|
22
|
+
export function PdfViewer({ src, fileName, onPreview, onDownload, height, loading = false }: PdfViewerProps) {
|
|
17
23
|
const displayName = fileName || 'PDF Document';
|
|
24
|
+
// In a loading scope the document is being read: the frame draws its loading form, never "not available".
|
|
25
|
+
const isLoading = useLoadingScope() || loading;
|
|
18
26
|
|
|
19
27
|
// Historical shape: no src at all → standalone empty state with NO header.
|
|
20
|
-
if (!src) {
|
|
28
|
+
if (!src && !isLoading) {
|
|
21
29
|
return (
|
|
22
30
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
23
31
|
<AdobePdfIcon className="mb-4 h-16 w-16 text-ods-text-secondary" />
|
|
@@ -57,6 +65,8 @@ export function PdfViewer({ src, fileName, onPreview, onDownload, height }: PdfV
|
|
|
57
65
|
</div>
|
|
58
66
|
}
|
|
59
67
|
src={src}
|
|
68
|
+
loadingActionCount={PDF_VIEWER_SKELETON_ACTIONS}
|
|
69
|
+
loading={loading}
|
|
60
70
|
height={height}
|
|
61
71
|
/>
|
|
62
72
|
);
|
|
@@ -40,6 +40,7 @@ export { type PackageSearchSlotProps, SoftwareRowFields, type SoftwareRowFieldsP
|
|
|
40
40
|
export {
|
|
41
41
|
DEFAULT_SOFTWARE_UPDATE_POLICY,
|
|
42
42
|
DEFAULT_SOFTWARE_UPDATE_SETTINGS,
|
|
43
|
+
defaultSoftwareUpdateSettings,
|
|
43
44
|
SOFTWARE_UPDATE_COUNTDOWN_OPTIONS,
|
|
44
45
|
SOFTWARE_UPDATE_DEADLINE_OPTIONS,
|
|
45
46
|
type SoftwareUpdateMode,
|
|
@@ -19,6 +19,21 @@ export interface SoftwareActionCopy {
|
|
|
19
19
|
logsHeading: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The run modes say when a device actually gets the run, not only when it is sent. Both forms submit a software bundle
|
|
24
|
+
* (`submitSoftwareBundle`), and the backend delivers it per device:
|
|
25
|
+
* - Now: each device gets an online-dispatch sentinel (`SoftwareBundleService.armOnlineDispatch`), so an online device
|
|
26
|
+
* starts at once and an offline one when it comes online.
|
|
27
|
+
* - Scheduled: the schedule is created with `offlineBehavior` RETRY_ON_RECONNECT and the reconnect window
|
|
28
|
+
* `openframe.rmm.software.bundle.schedule.reconnect-window-seconds` (604800 s, 7 days, in saas-tenant
|
|
29
|
+
* `configs/base/openframe-saas-api.yml`): a device offline at the start time gets the run when it reconnects within
|
|
30
|
+
* that window. Keep "within 7 days" in step with that key.
|
|
31
|
+
*/
|
|
32
|
+
const nowDescription = (noun: string) =>
|
|
33
|
+
`The ${noun} starts now on online devices, and on the others when they come online.`;
|
|
34
|
+
const scheduleDescription = (noun: string) =>
|
|
35
|
+
`The ${noun} runs at the scheduled time. Devices offline then get it when they come back online, within 7 days.`;
|
|
36
|
+
|
|
22
37
|
/** Everything the install and update flows word differently, on every page that shows one. */
|
|
23
38
|
export const SOFTWARE_ACTION_COPY: Record<SoftwareActionKind, SoftwareActionCopy> = {
|
|
24
39
|
INSTALL: {
|
|
@@ -27,12 +42,8 @@ export const SOFTWARE_ACTION_COPY: Record<SoftwareActionKind, SoftwareActionCopy
|
|
|
27
42
|
runLabel: 'Run Installation',
|
|
28
43
|
scheduleLabel: 'Schedule Install',
|
|
29
44
|
modes: [
|
|
30
|
-
{ value: 'now', label: 'Install Now', description: '
|
|
31
|
-
{
|
|
32
|
-
value: 'schedule',
|
|
33
|
-
label: 'Schedule Install',
|
|
34
|
-
description: 'The install runs automatically at the scheduled time.',
|
|
35
|
-
},
|
|
45
|
+
{ value: 'now', label: 'Install Now', description: nowDescription('install') },
|
|
46
|
+
{ value: 'schedule', label: 'Schedule Install', description: scheduleDescription('install') },
|
|
36
47
|
],
|
|
37
48
|
started: 'Installation started',
|
|
38
49
|
scheduled: 'Installation scheduled',
|
|
@@ -46,12 +57,8 @@ export const SOFTWARE_ACTION_COPY: Record<SoftwareActionKind, SoftwareActionCopy
|
|
|
46
57
|
runLabel: 'Run Update',
|
|
47
58
|
scheduleLabel: 'Schedule Update',
|
|
48
59
|
modes: [
|
|
49
|
-
{ value: 'now', label: 'Update Now', description: '
|
|
50
|
-
{
|
|
51
|
-
value: 'schedule',
|
|
52
|
-
label: 'Schedule Update',
|
|
53
|
-
description: 'The update runs automatically at the scheduled time.',
|
|
54
|
-
},
|
|
60
|
+
{ value: 'now', label: 'Update Now', description: nowDescription('update') },
|
|
61
|
+
{ value: 'schedule', label: 'Schedule Update', description: scheduleDescription('update') },
|
|
55
62
|
],
|
|
56
63
|
started: 'Update started',
|
|
57
64
|
scheduled: 'Update scheduled',
|
|
@@ -10,7 +10,7 @@ import { SOFTWARE_ACTION_COPY, type SoftwareActionKind } from './software-action
|
|
|
10
10
|
import { newSoftwareRow, type SoftwareRow } from './software-row';
|
|
11
11
|
import { type PackageSearchSlotProps, SoftwareRowFields } from './software-row-fields';
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
defaultSoftwareUpdateSettings,
|
|
14
14
|
SoftwareUpdateSettingsFields,
|
|
15
15
|
type SoftwareUpdateSettingsValues,
|
|
16
16
|
} from './update-settings-fields';
|
|
@@ -74,6 +74,12 @@ export interface SoftwareActionFormProps<TRef extends string = string> {
|
|
|
74
74
|
* settings yet never shows choices that go nowhere; the host that submits `updateSettings` passes true.
|
|
75
75
|
*/
|
|
76
76
|
showUpdateSettings?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* "Update Settings" offer Wait & notify and the countdowns, which need the devices to notify the user. Default false:
|
|
79
|
+
* Wait (with an optional deadline that closes the app without warning) or Force Update, opening on Force Update, and
|
|
80
|
+
* no countdown.
|
|
81
|
+
*/
|
|
82
|
+
updateUserNotifications?: boolean;
|
|
77
83
|
/** The "Device Selection" heading over the picker. Default true; a host that titles the picker itself passes false. */
|
|
78
84
|
showDeviceHeading?: boolean;
|
|
79
85
|
className?: string;
|
|
@@ -104,6 +110,7 @@ export function SoftwareActionForm<TRef extends string = string>({
|
|
|
104
110
|
addRows = true,
|
|
105
111
|
runModeLayout = 'stacked',
|
|
106
112
|
showUpdateSettings = false,
|
|
113
|
+
updateUserNotifications = false,
|
|
107
114
|
showDeviceHeading = true,
|
|
108
115
|
className = 'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',
|
|
109
116
|
}: SoftwareActionFormProps<TRef>) {
|
|
@@ -115,7 +122,7 @@ export function SoftwareActionForm<TRef extends string = string>({
|
|
|
115
122
|
const [time, setTime] = useState(initialValues.time ?? '');
|
|
116
123
|
const [timeReference, setTimeReference] = useState<TRef>(initialValues.timeReference);
|
|
117
124
|
const [updateSettings, setUpdateSettings] = useState<SoftwareUpdateSettingsValues>(
|
|
118
|
-
() => initialValues.updateSettings ??
|
|
125
|
+
() => initialValues.updateSettings ?? defaultSoftwareUpdateSettings(updateUserNotifications),
|
|
119
126
|
);
|
|
120
127
|
const withUpdateSettings = action === 'UPDATE' && showUpdateSettings;
|
|
121
128
|
|
|
@@ -215,7 +222,13 @@ export function SoftwareActionForm<TRef extends string = string>({
|
|
|
215
222
|
</>
|
|
216
223
|
)}
|
|
217
224
|
|
|
218
|
-
{withUpdateSettings &&
|
|
225
|
+
{withUpdateSettings && (
|
|
226
|
+
<SoftwareUpdateSettingsFields
|
|
227
|
+
value={updateSettings}
|
|
228
|
+
onChange={setUpdateSettings}
|
|
229
|
+
userNotifications={updateUserNotifications}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
219
232
|
|
|
220
233
|
{showDeviceHeading && (
|
|
221
234
|
<h2 className="pt-[var(--spacing-system-l)] text-ods-text-primary text-h2">Device Selection</h2>
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { InfoCircleIcon } from '../../icons-v2-generated';
|
|
4
3
|
import {
|
|
5
|
-
Alert,
|
|
6
4
|
CheckboxBlock,
|
|
7
5
|
Label,
|
|
8
6
|
RadioGroupBlock,
|
|
@@ -71,24 +69,95 @@ export const DEFAULT_SOFTWARE_UPDATE_SETTINGS: SoftwareUpdateSettingsValues = {
|
|
|
71
69
|
autoUpdatePolicy: DEFAULT_SOFTWARE_UPDATE_POLICY,
|
|
72
70
|
};
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
/** Force Update, with a 7-day deadline ready for when Wait is picked: what the form opens with without notifications. */
|
|
73
|
+
const FORCE_UPDATE_POLICY: SoftwareUpdatePolicy = {
|
|
74
|
+
...DEFAULT_SOFTWARE_UPDATE_POLICY,
|
|
75
|
+
mode: 'FORCE',
|
|
76
|
+
deadlineEnabled: false,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What the form opens with. With user notifications, the server's own defaults. Without them, Force Update: an app is
|
|
81
|
+
* never closed unannounced a week later, and nobody is promised a prompt that cannot be shown.
|
|
82
|
+
*/
|
|
83
|
+
export function defaultSoftwareUpdateSettings(userNotifications: boolean): SoftwareUpdateSettingsValues {
|
|
84
|
+
if (userNotifications) return DEFAULT_SOFTWARE_UPDATE_SETTINGS;
|
|
85
|
+
return { ...DEFAULT_SOFTWARE_UPDATE_SETTINGS, policy: FORCE_UPDATE_POLICY, autoUpdatePolicy: FORCE_UPDATE_POLICY };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The two modes and their deadline, worded by what the device does. With user notifications the user is asked and
|
|
90
|
+
* warned. Without them the agent shows nothing on the device (no prompt, no countdown on screen; there is no end-user
|
|
91
|
+
* UI anywhere yet), so the copy says only what happens:
|
|
92
|
+
* - Wait (WAIT, no deadline): the agent reports Waiting and installs once the app is closed.
|
|
93
|
+
* - A deadline (WAIT + `deadlineSeconds`): counted from the first time the agent finds the app running; at the deadline
|
|
94
|
+
* the backend sends the update again as FORCE (`armDeadline` / `driveDeadlineForce` in openframe-client-core), and
|
|
95
|
+
* withdraws that if the update finished first.
|
|
96
|
+
* - Force (FORCE): the agent waits `countdownSeconds` (clamped to 60–1800 s), asks the app to close, stops it after a
|
|
97
|
+
* 10 s grace and installs (`run.rs` / `close.rs` in openframe-client). The wait is silent, so it is not offered as a
|
|
98
|
+
* control; the copy states it, since the app is not closed at once.
|
|
99
|
+
*/
|
|
100
|
+
interface ModeCopy {
|
|
101
|
+
wait: RadioGroupBlockOption;
|
|
102
|
+
force: RadioGroupBlockOption;
|
|
103
|
+
deadline: { label: string; description: string };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** "5 minutes": the silent wait before a forced close, worded as the countdown options word it. */
|
|
107
|
+
function closeDelay(countdownSeconds: number): string {
|
|
108
|
+
const option = SOFTWARE_UPDATE_COUNTDOWN_OPTIONS.find(candidate => candidate.seconds === countdownSeconds);
|
|
109
|
+
if (option) return option.label;
|
|
110
|
+
const minutes = Math.max(1, Math.round(countdownSeconds / 60));
|
|
111
|
+
return minutes === 1 ? '1 minute' : `${minutes} minutes`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const NOTIFIED_COPY: ModeCopy = {
|
|
115
|
+
wait: {
|
|
76
116
|
value: 'WAIT',
|
|
77
117
|
label: 'Wait & notify',
|
|
78
118
|
description: 'The user gets Update now or Ask Later. If they close the app, it updates then.',
|
|
79
119
|
},
|
|
80
|
-
{
|
|
120
|
+
force: {
|
|
81
121
|
value: 'FORCE',
|
|
82
122
|
label: 'Force Update',
|
|
83
123
|
description: 'The user gets a short countdown, then the app closes and updates.',
|
|
84
124
|
},
|
|
85
|
-
|
|
125
|
+
deadline: {
|
|
126
|
+
label: 'Countdown after the deadline',
|
|
127
|
+
description: 'If the update still isn’t installed by the deadline, the user gets a countdown and then it installs.',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function silentCopy(countdownSeconds: number): ModeCopy {
|
|
132
|
+
const delay = closeDelay(countdownSeconds);
|
|
133
|
+
return {
|
|
134
|
+
wait: { value: 'WAIT', label: 'Wait', description: 'The update waits until the user closes the app.' },
|
|
135
|
+
force: {
|
|
136
|
+
value: 'FORCE',
|
|
137
|
+
label: 'Force Update',
|
|
138
|
+
description: `The app is closed without warning ${delay} later, then updated.`,
|
|
139
|
+
},
|
|
140
|
+
deadline: {
|
|
141
|
+
label: 'Close the app after a deadline',
|
|
142
|
+
description: `If the app is still open by the deadline, it is closed without warning ${delay} later, then updated.`,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
86
146
|
|
|
87
147
|
const SAME_SETTINGS_DESCRIPTION: Record<SoftwareUpdateMode, string> = {
|
|
88
148
|
WAIT: 'Auto-updates follow "If the app is running" above. Uncheck to set them separately.',
|
|
89
149
|
FORCE: 'Later releases will also be force-updated.',
|
|
90
150
|
};
|
|
91
151
|
|
|
152
|
+
/**
|
|
153
|
+
* "Keep it auto-updated", as the backend runs it (`PackageAutoUpdatePolicyService.turnOn`, `PackageAutoUpdateTrigger`):
|
|
154
|
+
* a run with it on enables a policy per device for the package, and the trigger orders each new catalog version (Brew
|
|
155
|
+
* and winget catalogs) on those devices. A run with it off leaves an earlier policy as it is: only Turn Off Auto-Update
|
|
156
|
+
* (`turnOffSoftwareAutoUpdate`) or the package leaving the device turns one off, so "off" does not mean "stop".
|
|
157
|
+
*/
|
|
158
|
+
const AUTO_UPDATE_DESCRIPTION =
|
|
159
|
+
'After this update, OpenFrame installs each new version from Winget or Brew on the selected devices. Leaving it off does not turn off auto-update that is already on there.';
|
|
160
|
+
|
|
92
161
|
const groupLabelClass = 'text-h5 text-ods-text-secondary';
|
|
93
162
|
|
|
94
163
|
function SecondsSelect({
|
|
@@ -126,17 +195,21 @@ function UpdatePolicyFields({
|
|
|
126
195
|
name,
|
|
127
196
|
label,
|
|
128
197
|
policy,
|
|
198
|
+
userNotifications,
|
|
129
199
|
onChange,
|
|
130
200
|
}: {
|
|
131
201
|
/** The radio group's name: two policies on one page must not share it. */
|
|
132
202
|
name: string;
|
|
133
203
|
label: string;
|
|
134
204
|
policy: SoftwareUpdatePolicy;
|
|
205
|
+
/** Off, the choices say what the device does with nobody told, and there is no countdown to set. */
|
|
206
|
+
userNotifications: boolean;
|
|
135
207
|
onChange: (policy: SoftwareUpdatePolicy) => void;
|
|
136
208
|
}) {
|
|
209
|
+
const copy = userNotifications ? NOTIFIED_COPY : silentCopy(policy.countdownSeconds);
|
|
137
210
|
const waits = policy.mode === 'WAIT';
|
|
138
211
|
const deadline = waits && policy.deadlineEnabled;
|
|
139
|
-
const countdown = (
|
|
212
|
+
const countdown = userNotifications && (
|
|
140
213
|
<SecondsSelect
|
|
141
214
|
label="Countdown"
|
|
142
215
|
value={policy.countdownSeconds}
|
|
@@ -154,7 +227,7 @@ function UpdatePolicyFields({
|
|
|
154
227
|
variant="grouped"
|
|
155
228
|
value={policy.mode}
|
|
156
229
|
onValueChange={mode => onChange({ ...policy, mode: mode as SoftwareUpdateMode })}
|
|
157
|
-
options={
|
|
230
|
+
options={[copy.wait, copy.force]}
|
|
158
231
|
itemClassName="py-[var(--spacing-system-sf)]"
|
|
159
232
|
/>
|
|
160
233
|
</div>
|
|
@@ -164,8 +237,8 @@ function UpdatePolicyFields({
|
|
|
164
237
|
id={`${name}-deadline`}
|
|
165
238
|
checked={policy.deadlineEnabled}
|
|
166
239
|
onCheckedChange={deadlineEnabled => onChange({ ...policy, deadlineEnabled })}
|
|
167
|
-
label=
|
|
168
|
-
description=
|
|
240
|
+
label={copy.deadline.label}
|
|
241
|
+
description={copy.deadline.description}
|
|
169
242
|
/>
|
|
170
243
|
{deadline && (
|
|
171
244
|
<div className="grid grid-cols-1 gap-[var(--spacing-system-lf)] content-md:grid-cols-3 content-md:items-start">
|
|
@@ -180,7 +253,9 @@ function UpdatePolicyFields({
|
|
|
180
253
|
)}
|
|
181
254
|
</div>
|
|
182
255
|
) : (
|
|
183
|
-
|
|
256
|
+
userNotifications && (
|
|
257
|
+
<div className="grid grid-cols-1 gap-[var(--spacing-system-lf)] content-md:grid-cols-3">{countdown}</div>
|
|
258
|
+
)
|
|
184
259
|
)}
|
|
185
260
|
</div>
|
|
186
261
|
);
|
|
@@ -189,6 +264,12 @@ function UpdatePolicyFields({
|
|
|
189
264
|
export interface SoftwareUpdateSettingsFieldsProps {
|
|
190
265
|
value: SoftwareUpdateSettingsValues;
|
|
191
266
|
onChange: (value: SoftwareUpdateSettingsValues) => void;
|
|
267
|
+
/**
|
|
268
|
+
* The devices can notify the user: Wait & notify, its countdown after the deadline and the countdowns. Default false,
|
|
269
|
+
* as today, when the client has no notifications: Wait (until the user closes the app, or a deadline closes it without
|
|
270
|
+
* warning) or Force Update, and no countdown.
|
|
271
|
+
*/
|
|
272
|
+
userNotifications?: boolean;
|
|
192
273
|
}
|
|
193
274
|
|
|
194
275
|
/**
|
|
@@ -196,7 +277,11 @@ export interface SoftwareUpdateSettingsFieldsProps {
|
|
|
196
277
|
* rules 1034:8173): what happens on a device where the app is running, and
|
|
197
278
|
* whether later releases are installed the same way.
|
|
198
279
|
*/
|
|
199
|
-
export function SoftwareUpdateSettingsFields({
|
|
280
|
+
export function SoftwareUpdateSettingsFields({
|
|
281
|
+
value,
|
|
282
|
+
onChange,
|
|
283
|
+
userNotifications = false,
|
|
284
|
+
}: SoftwareUpdateSettingsFieldsProps) {
|
|
200
285
|
return (
|
|
201
286
|
<section aria-labelledby="software-update-settings" className="flex flex-col gap-[var(--spacing-system-l)]">
|
|
202
287
|
<h2 id="software-update-settings" className="pt-[var(--spacing-system-l)] text-ods-text-primary text-h2">
|
|
@@ -207,6 +292,7 @@ export function SoftwareUpdateSettingsFields({ value, onChange }: SoftwareUpdate
|
|
|
207
292
|
name="updatePolicy"
|
|
208
293
|
label="If the app is running"
|
|
209
294
|
policy={value.policy}
|
|
295
|
+
userNotifications={userNotifications}
|
|
210
296
|
onChange={policy => onChange({ ...value, policy })}
|
|
211
297
|
/>
|
|
212
298
|
|
|
@@ -218,7 +304,7 @@ export function SoftwareUpdateSettingsFields({ value, onChange }: SoftwareUpdate
|
|
|
218
304
|
checked={value.autoUpdate}
|
|
219
305
|
onCheckedChange={autoUpdate => onChange({ ...value, autoUpdate })}
|
|
220
306
|
label="Keep it auto-updated"
|
|
221
|
-
description=
|
|
307
|
+
description={AUTO_UPDATE_DESCRIPTION}
|
|
222
308
|
/>
|
|
223
309
|
{value.autoUpdate && (
|
|
224
310
|
<>
|
|
@@ -235,6 +321,7 @@ export function SoftwareUpdateSettingsFields({ value, onChange }: SoftwareUpdate
|
|
|
235
321
|
name="autoUpdatePolicy"
|
|
236
322
|
label="For auto-updates"
|
|
237
323
|
policy={value.autoUpdatePolicy}
|
|
324
|
+
userNotifications={userNotifications}
|
|
238
325
|
onChange={autoUpdatePolicy => onChange({ ...value, autoUpdatePolicy })}
|
|
239
326
|
/>
|
|
240
327
|
</div>
|
|
@@ -244,25 +331,10 @@ export function SoftwareUpdateSettingsFields({ value, onChange }: SoftwareUpdate
|
|
|
244
331
|
</div>
|
|
245
332
|
</div>
|
|
246
333
|
|
|
247
|
-
{/*
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<Alert
|
|
252
|
-
role="note"
|
|
253
|
-
className="flex items-center gap-[var(--spacing-system-m)] rounded-[6px] border-ods-border bg-ods-card p-[var(--spacing-system-m)]"
|
|
254
|
-
>
|
|
255
|
-
<span className="shrink-0 text-ods-text-secondary">
|
|
256
|
-
<InfoCircleIcon size={24} color="currentColor" />
|
|
257
|
-
</span>
|
|
258
|
-
<div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
|
|
259
|
-
<p className="text-ods-text-primary text-h3">Apps installed by hand get reinstalled</p>
|
|
260
|
-
<p className="text-ods-text-secondary text-h6">
|
|
261
|
-
On the first update, OpenFrame reinstalls them with Winget or Brew so it can keep them updated.
|
|
262
|
-
</p>
|
|
263
|
-
</div>
|
|
264
|
-
</Alert>
|
|
265
|
-
)}
|
|
334
|
+
{/* No note that apps installed by hand get reinstalled on the first update: the agent does not do that. A copy no
|
|
335
|
+
package manager installed is left alone (brew probes it as not installed, winget as "not installed from a
|
|
336
|
+
winget source"), so it is neither updated nor reinstalled. Reinstalling it with a package manager is Adopt
|
|
337
|
+
(`adoptSoftware`), which the backend does not implement yet. */}
|
|
266
338
|
</section>
|
|
267
339
|
);
|
|
268
340
|
}
|
|
@@ -101,6 +101,7 @@ const RichMarkdownInner: React.FC<InnerProps> = ({
|
|
|
101
101
|
demoteMarkdownH1ToH2,
|
|
102
102
|
skeletonSections,
|
|
103
103
|
skeletonLines,
|
|
104
|
+
skeletonTitle,
|
|
104
105
|
}) => {
|
|
105
106
|
const { ogScraperUrl } = useRichMarkdownRuntime();
|
|
106
107
|
|
|
@@ -158,6 +159,7 @@ const RichMarkdownInner: React.FC<InnerProps> = ({
|
|
|
158
159
|
allowRemoteMedia
|
|
159
160
|
skeletonSections={skeletonSections}
|
|
160
161
|
skeletonLines={skeletonLines}
|
|
162
|
+
skeletonTitle={skeletonTitle}
|
|
161
163
|
/>
|
|
162
164
|
);
|
|
163
165
|
};
|
|
@@ -174,6 +176,7 @@ const RichMarkdownRendererImpl: React.FC<RichMarkdownRendererProps> = ({
|
|
|
174
176
|
demoteMarkdownH1ToH2 = false,
|
|
175
177
|
skeletonSections,
|
|
176
178
|
skeletonLines,
|
|
179
|
+
skeletonTitle,
|
|
177
180
|
// Runtime overrides; provider fills the defaults
|
|
178
181
|
redditProxyUrl,
|
|
179
182
|
twitterProxyUrl,
|
|
@@ -199,6 +202,7 @@ const RichMarkdownRendererImpl: React.FC<RichMarkdownRendererProps> = ({
|
|
|
199
202
|
demoteMarkdownH1ToH2={demoteMarkdownH1ToH2}
|
|
200
203
|
skeletonSections={skeletonSections}
|
|
201
204
|
skeletonLines={skeletonLines}
|
|
205
|
+
skeletonTitle={skeletonTitle}
|
|
202
206
|
/>
|
|
203
207
|
</RichMarkdownRuntimeProvider>
|
|
204
208
|
);
|
|
@@ -527,9 +527,26 @@ function CompactPrimaryActions({ actions, loading: loadingProp }: { actions: Pag
|
|
|
527
527
|
);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
const subscribeNothing = () => () => {};
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* False on the server and through hydration, true after: what lets a render use a value only the client measures and
|
|
534
|
+
* still hydrate into the server's markup.
|
|
535
|
+
*/
|
|
536
|
+
function useHydrated(): boolean {
|
|
537
|
+
return React.useSyncExternalStore(
|
|
538
|
+
subscribeNothing,
|
|
539
|
+
() => true,
|
|
540
|
+
() => false,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
530
544
|
function MobileBottomActions({ actions, loading: loadingProp }: { actions: PageActionButton[]; loading?: boolean }) {
|
|
531
545
|
const loading = useLoadingScope() || loadingProp;
|
|
532
|
-
// Over the composer dock where the layout has one.
|
|
546
|
+
// Over the composer dock where the layout has one. `AppLayout` measures the dock after it mounts, and a page hydrates
|
|
547
|
+
// after the layout, so the height is often known by the bar's first client render: drawn then, it would not match the
|
|
548
|
+
// server's markup (no offset). The bar takes it from the render after hydration instead.
|
|
549
|
+
const hydrated = useHydrated();
|
|
533
550
|
const dockHeight = useComposerDockHeight();
|
|
534
551
|
// Its height, for the chrome that stays clear of it (`useMobileActionBarHeight`).
|
|
535
552
|
const barRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -543,7 +560,7 @@ function MobileBottomActions({ actions, loading: loadingProp }: { actions: PageA
|
|
|
543
560
|
<div
|
|
544
561
|
ref={barRef}
|
|
545
562
|
data-testid="mobile-action-bar"
|
|
546
|
-
style={dockHeight > 0 ? { bottom: dockHeight } : undefined}
|
|
563
|
+
style={hydrated && dockHeight > 0 ? { bottom: dockHeight } : undefined}
|
|
547
564
|
className={cn(
|
|
548
565
|
'fixed bottom-0 left-0 right-0 z-50 md:hidden',
|
|
549
566
|
'border-t border-ods-border bg-ods-card',
|
package/src/types/doc-source.ts
CHANGED
|
@@ -75,12 +75,12 @@ export interface DocSourceDal<Client = unknown> {
|
|
|
75
75
|
* Doc-source registry id. The lib's `<DocViewer>` is generic over this union
|
|
76
76
|
* — the hub's `DOC_SOURCES` registry defines the actual ids. Listed here as a
|
|
77
77
|
* type-only union so the lib can flow the id into `DocRenderHandlers.sourceId`
|
|
78
|
-
* without callers having to cast `string`
|
|
78
|
+
* without callers having to cast `string` to the union.
|
|
79
79
|
*
|
|
80
80
|
* Adding a new source = add the id here AND register it in the hub. The lib
|
|
81
81
|
* never reaches into the registry — this is purely a type-narrowing handle.
|
|
82
82
|
*/
|
|
83
|
-
export type DocSourceId = 'openframe-docs' | 'data-room-docs';
|
|
83
|
+
export type DocSourceId = 'openframe-docs' | 'data-room-docs' | 'company-policies';
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Result returned by the resolve-link endpoint. Discriminator-style: `success`
|
|
@@ -102,3 +102,30 @@ export function getDocSourceDefaultPath(
|
|
|
102
102
|
const firstFolderWithReadme = structure.find(node => node.type === 'folder' && node.hasReadme && !!node.path);
|
|
103
103
|
return firstFolderWithReadme?.path ?? null;
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
/** The first document under a folder (its own files first, then each subfolder's, in order), never a diagram source. */
|
|
107
|
+
export function findFirstDocPath(folder: DocNode): string | null {
|
|
108
|
+
const children = folder.children ?? [];
|
|
109
|
+
for (const child of children) {
|
|
110
|
+
if (child.type === 'file' && !child.path.toLowerCase().endsWith('.mmd')) return child.path;
|
|
111
|
+
}
|
|
112
|
+
for (const child of children) {
|
|
113
|
+
if (child.type === 'folder') {
|
|
114
|
+
const nested = findFirstDocPath(child);
|
|
115
|
+
if (nested) return nested;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The document a source's root shows: its folder index when the tree has one, else its first document (the rule a
|
|
123
|
+
* folder with no README follows), else null (a tree with no document at all).
|
|
124
|
+
*/
|
|
125
|
+
export function rootDocumentPath(
|
|
126
|
+
structure: DocNode[],
|
|
127
|
+
folderIndexFile: string = DEFAULT_FOLDER_INDEX_FILE,
|
|
128
|
+
): string | null {
|
|
129
|
+
if (structure.some(node => node.type === 'file' && node.path === folderIndexFile)) return folderIndexFile;
|
|
130
|
+
return findFirstDocPath({ id: '', name: '', slug: '', path: '', type: 'folder', children: structure });
|
|
131
|
+
}
|