@flamingo-stack/openframe-frontend-core 0.0.847 → 0.0.848
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-N2A24FDF.cjs → chunk-2V6FCVUL.cjs} +955 -932
- package/dist/{chunk-CNMXP77I.cjs → chunk-36DUCHI7.cjs} +4 -4
- package/dist/{chunk-J2IXYCEH.cjs → chunk-3W4ASAXN.cjs} +15 -15
- package/dist/{chunk-6LDL3UZE.cjs → chunk-4YBS4HIO.cjs} +20 -20
- package/dist/{chunk-QK2A334V.cjs → chunk-5HOWR4TG.cjs} +12 -12
- package/dist/{chunk-2FUTZKLB.cjs → chunk-74Z34I2C.cjs} +6 -6
- package/dist/{chunk-PV3NKXWS.cjs → chunk-7DW3LZ52.cjs} +57 -57
- package/dist/{chunk-ZTO7KNYV.cjs → chunk-7LE4HWHA.cjs} +35 -35
- package/dist/{chunk-URP6GYTD.cjs → chunk-7YJQTQPV.cjs} +56 -56
- package/dist/{chunk-4R4HAFXI.cjs → chunk-ALRYVZ3E.cjs} +6 -6
- package/dist/{chunk-57JYB3E2.cjs → chunk-C3WZXOZJ.cjs} +3 -3
- package/dist/{chunk-SSSI2RBV.cjs → chunk-FNQWL4W4.cjs} +14 -14
- package/dist/{chunk-J2ZSGOUC.cjs → chunk-GASFYPGH.cjs} +27 -27
- 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-HBDLJUI3.cjs → chunk-I3YRYS23.cjs} +5 -5
- package/dist/{chunk-HPXET3LH.cjs → chunk-IIUZE73M.cjs} +27 -27
- package/dist/{chunk-WCK5T3UD.cjs → chunk-IKOL6NJY.cjs} +101 -69
- package/dist/{chunk-DGQDGCQH.cjs → chunk-INA2FX2N.cjs} +2 -2
- package/dist/{chunk-PQDCSZ3W.cjs → chunk-IUZVOQXP.cjs} +85 -85
- package/dist/{chunk-BIOSHDXQ.cjs → chunk-MDLT6D46.cjs} +61 -61
- package/dist/{chunk-DEEQSVLX.cjs → chunk-MWYEROAP.cjs} +90 -90
- package/dist/{chunk-D5DMWXF2.cjs → chunk-NYT6IBIX.cjs} +4 -4
- package/dist/{chunk-TBOWFTEN.cjs → chunk-O4BXHCMK.cjs} +22 -22
- package/dist/{chunk-YC52LSPP.cjs → chunk-OCJXOSIH.cjs} +40 -40
- package/dist/{chunk-3TOWCFIK.cjs → chunk-PMUCEUFQ.cjs} +13 -1
- package/dist/{chunk-BZZE4KIV.cjs → chunk-QUSCSYHJ.cjs} +135 -135
- package/dist/{chunk-744JXTDP.cjs → chunk-SA2NNWIJ.cjs} +23 -23
- package/dist/{chunk-BHOVF4KP.cjs → chunk-SOSIXRQ3.cjs} +2 -2
- package/dist/{chunk-PZNWIJRZ.cjs → chunk-TIVPOVIM.cjs} +2 -2
- package/dist/{chunk-FLW4WULE.cjs → chunk-U7UBYJEZ.cjs} +17 -61
- package/dist/{chunk-Y2OEC7UW.cjs → chunk-VW4446JR.cjs} +40 -40
- package/dist/{chunk-NFNSPLGC.cjs → chunk-W7OFAROY.cjs} +14 -9
- package/dist/{chunk-RAJ5FYFL.cjs → chunk-Y2ZWWIUA.cjs} +14 -14
- package/dist/{chunk-MPVOPSU6.cjs → chunk-Y4BG7SPX.cjs} +96 -96
- package/dist/{chunk-SIFRIUPW.cjs → chunk-YAW7CHLV.cjs} +340 -354
- package/dist/{chunk-ABL5OBSF.cjs → chunk-YXKAHQYS.cjs} +294 -249
- package/dist/{chunk-SWF6TNZ3.cjs → chunk-ZGEWT3SF.cjs} +2 -2
- package/dist/{chunk-5SZ4ILU3.cjs → chunk-ZZDFMUEU.cjs} +3 -3
- package/dist/{cloud-tenants-XEUUMABT.cjs → cloud-tenants-YAZHLPOQ.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 +4 -7
- package/dist/components/chat/index.cjs +15 -15
- package/dist/components/contact/index.cjs +22 -22
- package/dist/components/docs/index.cjs +25 -25
- package/dist/components/embeds/index.cjs +22 -22
- 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/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-IZROCLFK.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-ETW6S22E.cjs} +94 -94
- package/dist/hooks/index.cjs +6 -6
- package/dist/index.cjs +30 -32
- package/dist/{knowledge-SQVPQSKM.cjs → knowledge-4IRWLTNW.cjs} +22 -22
- package/dist/{logs-M5DFGUBC.cjs → logs-EFDGY5IO.cjs} +28 -28
- package/dist/{policies-T3VQEJUD.cjs → policies-3KEXZ5BP.cjs} +25 -25
- package/dist/{remote-session-EE6477O3.cjs → remote-session-NT4NG7EF.cjs} +10 -10
- package/dist/{scripts-JFRFJKPH.cjs → scripts-PCGTZLZL.cjs} +28 -28
- package/dist/{software-update-E7W3AAIY.cjs → software-update-YCDNVFJE.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/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/index.cjs +5 -11
- 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 +7 -10
- 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/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/utils/design-doc-readiness.ts +22 -23
|
@@ -48,11 +48,7 @@ import {
|
|
|
48
48
|
} from '../../../types/trust-center';
|
|
49
49
|
import { isAppOriginUrl } from '../../../utils/app-origins';
|
|
50
50
|
import { formatDateShort } from '../../../utils/date-formatters';
|
|
51
|
-
import {
|
|
52
|
-
DESIGN_DOC_READINESS_DISPLAY,
|
|
53
|
-
designDocReadiness,
|
|
54
|
-
formatCompletionLabel,
|
|
55
|
-
} from '../../../utils/design-doc-readiness';
|
|
51
|
+
import { designDocStatusDisplay, formatCompletionLabel } from '../../../utils/design-doc-readiness';
|
|
56
52
|
import { faqItemAnchor } from '../../../utils/faq-anchor';
|
|
57
53
|
import { formatDateUTC as formatDate } from '../../../utils/format';
|
|
58
54
|
import { programMetaFormatters, programMetaLine } from '../../../utils/program-instant';
|
|
@@ -88,6 +84,7 @@ import type { LoadedProductReleaseCardProps } from '../../shared/product-release
|
|
|
88
84
|
import { ProductReleaseCard } from '../../shared/product-release/product-release-card';
|
|
89
85
|
import type { ActionsMenuGroup } from '../../ui/actions-menu';
|
|
90
86
|
import { ExternalMediaNotice } from '../../ui/markdown/external-media-notice';
|
|
87
|
+
import { tagVariantForBadgeColor } from '../../ui/tag';
|
|
91
88
|
import { VendorIcon } from '../../vendor-icon';
|
|
92
89
|
import { useChatPanel } from '../chat-panel-context';
|
|
93
90
|
import type { ChatRef } from '../chat-ref.types';
|
|
@@ -1046,8 +1043,8 @@ function CustomerInterviewChatCard({
|
|
|
1046
1043
|
}
|
|
1047
1044
|
|
|
1048
1045
|
/**
|
|
1049
|
-
* Design doc → the doc glyph, its
|
|
1050
|
-
*
|
|
1046
|
+
* Design doc → the doc glyph, its status as the pill (the status a person set, else its readiness, both vocabulary rows the hub resolves onto the
|
|
1047
|
+
* doc: `designDocStatusDisplay`) and "DRI · updated · n/m signed off" under the title. The hub's card route attaches the doc's list row as
|
|
1051
1048
|
* `item.doc`; a row without it (an older hub) renders the ref's own title and preview.
|
|
1052
1049
|
*/
|
|
1053
1050
|
function DesignDocChatCard({
|
|
@@ -1063,7 +1060,7 @@ function DesignDocChatCard({
|
|
|
1063
1060
|
}) {
|
|
1064
1061
|
const displayRef = fetchedItemDisplayRef(item, chatRef);
|
|
1065
1062
|
const doc = item?.doc;
|
|
1066
|
-
const
|
|
1063
|
+
const status = doc ? designDocStatusDisplay(doc) : null;
|
|
1067
1064
|
return (
|
|
1068
1065
|
<MingoInfoCard
|
|
1069
1066
|
title={doc?.title ?? displayRef.title}
|
|
@@ -1074,8 +1071,8 @@ function DesignDocChatCard({
|
|
|
1074
1071
|
}
|
|
1075
1072
|
icon={<FileContentIcon size={24} />}
|
|
1076
1073
|
status={
|
|
1077
|
-
|
|
1078
|
-
? { label:
|
|
1074
|
+
status
|
|
1075
|
+
? { label: status.label, variant: tagVariantForBadgeColor(status.color_scheme) }
|
|
1079
1076
|
: { label: 'Design doc', variant: 'grey' }
|
|
1080
1077
|
}
|
|
1081
1078
|
anchorProps={buildAnchorProps(displayRef.url, isNewTab, displayRef.targetPlatform)}
|
|
@@ -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}>
|
|
@@ -35,7 +35,11 @@ import type { PluggableList } from 'unified';
|
|
|
35
35
|
import type { ResolveLinkResult } from '../../../types/doc-source';
|
|
36
36
|
import { readAppOrigins, SERVER_APP_ORIGINS, subscribeAppOrigins } from '../../../utils/app-origins';
|
|
37
37
|
import { cn } from '../../../utils/cn';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
TextLineSkeleton,
|
|
40
|
+
TextLinesByBreakpointSkeleton,
|
|
41
|
+
type TextLinesByBreakpoint,
|
|
42
|
+
} from '../../loading/unified-skeleton';
|
|
39
43
|
import { LoadingItems, useLoadingScope } from '../loading-scope';
|
|
40
44
|
import { buildBaseComponents, MARKDOWN_H1_CLASS, MARKDOWN_H2_CLASS, MARKDOWN_PARAGRAPH_CLASS } from './base-components';
|
|
41
45
|
import { MarkdownMediaOriginsContext } from './external-media-notice';
|
|
@@ -193,8 +197,8 @@ export interface MarkdownEngineProps {
|
|
|
193
197
|
* sections, each a heading and its paragraphs. Unset, one paragraph of `skeletonLines` lines.
|
|
194
198
|
*/
|
|
195
199
|
skeletonSections?: number;
|
|
196
|
-
/** Lines of the one paragraph a loading body draws when it names no sections. Default 3. */
|
|
197
|
-
skeletonLines?:
|
|
200
|
+
/** Lines of the one paragraph a loading body draws when it names no sections, per width. Default 3 at every width. */
|
|
201
|
+
skeletonLines?: TextLinesByBreakpoint;
|
|
198
202
|
/** A loading body opens with its own `# ` title (a legal document's): its title row is drawn first. */
|
|
199
203
|
skeletonTitle?: boolean;
|
|
200
204
|
/** The content is still being read: the body draws its skeleton (the `skeleton*` shape). A loading scope does the
|
|
@@ -203,16 +207,19 @@ export interface MarkdownEngineProps {
|
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
/** Lines a loading body draws when its caller names no shape. */
|
|
206
|
-
export const MARKDOWN_SKELETON_LINES = 3;
|
|
210
|
+
export const MARKDOWN_SKELETON_LINES: TextLinesByBreakpoint = { phone: 3, md: 3, lg: 3 };
|
|
207
211
|
/** Paragraphs under each heading of a loading body, and their lines. */
|
|
208
|
-
const SKELETON_SECTION_PARAGRAPHS
|
|
212
|
+
const SKELETON_SECTION_PARAGRAPHS: readonly TextLinesByBreakpoint[] = [
|
|
213
|
+
{ phone: 3, md: 3, lg: 3 },
|
|
214
|
+
{ phone: 2, md: 2, lg: 2 },
|
|
215
|
+
];
|
|
209
216
|
/**
|
|
210
|
-
* Lines a typical body's opening line set takes,
|
|
217
|
+
* Lines a typical body's opening line set takes, per width: a document's
|
|
211
218
|
* `# ` title wraps on a phone, and the opening paragraph every post and legal document starts with runs a few lines.
|
|
212
219
|
* Typical lengths, never one record's: a longer record growing past them is a settle below its own lines.
|
|
213
220
|
*/
|
|
214
|
-
const SKELETON_TITLE_LINES = { phone: 2,
|
|
215
|
-
const SKELETON_LEAD_LINES = { phone: 5,
|
|
221
|
+
const SKELETON_TITLE_LINES: TextLinesByBreakpoint = { phone: 2, md: 1, lg: 1 };
|
|
222
|
+
const SKELETON_LEAD_LINES: TextLinesByBreakpoint = { phone: 5, md: 3, lg: 3 };
|
|
216
223
|
/** The renderer's own frame: read by the engine and by its skeleton. */
|
|
217
224
|
/** The renderer's root class, on the rendered document and on every skeleton of one: it names a markdown body. */
|
|
218
225
|
export const MARKDOWN_RENDERER_CLASS = 'simple-markdown-renderer';
|
|
@@ -467,31 +474,15 @@ export const MarkdownEngine = memo(function MarkdownEngineGate(props: MarkdownEn
|
|
|
467
474
|
return <MarkdownEngineImpl {...props} />;
|
|
468
475
|
});
|
|
469
476
|
|
|
470
|
-
/** A paragraph of `lines`
|
|
471
|
-
function ParagraphBar({ lines, className }: { lines:
|
|
477
|
+
/** A paragraph of its `lines` per width at the body's own paragraph type. */
|
|
478
|
+
function ParagraphBar({ lines, className }: { lines: TextLinesByBreakpoint; className: string }) {
|
|
472
479
|
return (
|
|
473
480
|
<div className={className}>
|
|
474
|
-
<
|
|
481
|
+
<TextLinesByBreakpointSkeleton lines={lines} />
|
|
475
482
|
</div>
|
|
476
483
|
);
|
|
477
484
|
}
|
|
478
485
|
|
|
479
|
-
/** Lines of a text run per breakpoint: on a phone, and from `content-md`. */
|
|
480
|
-
interface SkeletonLinesByBreakpoint {
|
|
481
|
-
phone: number;
|
|
482
|
-
wide: number;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/** A text run of the lines it takes on a phone and from `content-md` (the content area's own steps). */
|
|
486
|
-
function LinesByBreakpoint({ lines, as = 'div' }: { lines: SkeletonLinesByBreakpoint; as?: 'div' | 'span' }) {
|
|
487
|
-
return (
|
|
488
|
-
<>
|
|
489
|
-
<TextLineSkeleton type="inherit" as={as} lines={lines.phone} className="w-full content-md:hidden" />
|
|
490
|
-
<TextLineSkeleton type="inherit" as={as} lines={lines.wide} className="hidden w-full content-md:flex" />
|
|
491
|
-
</>
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
486
|
/**
|
|
496
487
|
* A markdown body while it is read: the renderer's own frame and type. With `sections`, the body's opening paragraph
|
|
497
488
|
* (a typical length per breakpoint), then each section a REAL `<h2>` (the renderer's heading classes) holding a bar,
|
|
@@ -506,7 +497,7 @@ export function MarkdownEngineSkeleton({
|
|
|
506
497
|
className = '',
|
|
507
498
|
}: {
|
|
508
499
|
sections?: number;
|
|
509
|
-
lines?:
|
|
500
|
+
lines?: TextLinesByBreakpoint;
|
|
510
501
|
textSize?: TextSizeConfig;
|
|
511
502
|
/** The body opens with its own `# ` title (a legal document's, demoted to an `h2`): its H1 row is drawn first. */
|
|
512
503
|
title?: boolean;
|
|
@@ -522,12 +513,12 @@ export function MarkdownEngineSkeleton({
|
|
|
522
513
|
<div className={ARTICLE_CLASS}>
|
|
523
514
|
{title && (
|
|
524
515
|
<h2 className={cn(MARKDOWN_H1_CLASS, textSizes.h1)}>
|
|
525
|
-
<
|
|
516
|
+
<TextLinesByBreakpointSkeleton lines={SKELETON_TITLE_LINES} as="span" />
|
|
526
517
|
</h2>
|
|
527
518
|
)}
|
|
528
519
|
{sections ? (
|
|
529
520
|
<div className={paragraphClass}>
|
|
530
|
-
<
|
|
521
|
+
<TextLinesByBreakpointSkeleton lines={SKELETON_LEAD_LINES} />
|
|
531
522
|
</div>
|
|
532
523
|
) : null}
|
|
533
524
|
{sections ? (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
3
4
|
import { personFirstName } from '../../utils/format';
|
|
4
5
|
import { TextLineSkeleton } from '../loading/unified-skeleton';
|
|
5
6
|
import { AvatarStack } from './avatar-stack';
|
|
@@ -48,6 +49,11 @@ export interface PersonCellProps {
|
|
|
48
49
|
size?: 'sm' | 'lg';
|
|
49
50
|
/** The person is still being read: the cell draws its skeleton at its size. A loading scope does the same. */
|
|
50
51
|
loading?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* `2` (default): the name and its second line. `1`: the name alone, one line, with the second line in a tooltip:
|
|
54
|
+
* for a table whose rows are one line in every column but the first.
|
|
55
|
+
*/
|
|
56
|
+
lines?: 1 | 2;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
const PERSON_CELL_SIZE = {
|
|
@@ -67,29 +73,35 @@ export function PersonCell({
|
|
|
67
73
|
actionTitle,
|
|
68
74
|
size = 'sm',
|
|
69
75
|
loading,
|
|
76
|
+
lines = 2,
|
|
70
77
|
}: PersonCellProps) {
|
|
71
78
|
// In a page's loading state (or while the host reads the person) the cell is its skeleton.
|
|
72
|
-
if (useLoadingScope() || loading) return <PersonCellSkeleton size={size} />;
|
|
79
|
+
if (useLoadingScope() || loading) return <PersonCellSkeleton size={size} lines={lines} />;
|
|
73
80
|
const displayName = name || fallbackName;
|
|
74
81
|
const scale = PERSON_CELL_SIZE[size];
|
|
82
|
+
const nameLine = (
|
|
83
|
+
<TruncateText
|
|
84
|
+
variant={scale.primary}
|
|
85
|
+
className={onClick ? 'text-ods-accent group-hover/person-cell:underline' : undefined}
|
|
86
|
+
>
|
|
87
|
+
{displayName}
|
|
88
|
+
</TruncateText>
|
|
89
|
+
);
|
|
75
90
|
const row = (
|
|
76
91
|
<div className={`group/person-cell ${PERSON_ROW_CLASS} ${scale.gap}`}>
|
|
77
92
|
<SquareAvatar size={scale.avatar} src={avatarUrl ?? undefined} alt={displayName} fallback={displayName} />
|
|
78
93
|
<div className="min-w-0 flex-1">
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<TruncateText variant="h6" tone="secondary">
|
|
86
|
-
{secondary === undefined ? ' ' : secondary || '·'}
|
|
87
|
-
</TruncateText>
|
|
94
|
+
{nameLine}
|
|
95
|
+
{lines === 2 ? (
|
|
96
|
+
<TruncateText variant="h6" tone="secondary">
|
|
97
|
+
{secondary === undefined ? ' ' : secondary || '·'}
|
|
98
|
+
</TruncateText>
|
|
99
|
+
) : null}
|
|
88
100
|
</div>
|
|
89
101
|
</div>
|
|
90
102
|
);
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
// A clickable cell is a button in both layouts, so `onClick` and `actionTitle` are never dropped by `lines`.
|
|
104
|
+
const cell = onClick ? (
|
|
93
105
|
<Button
|
|
94
106
|
variant="transparent"
|
|
95
107
|
onClick={onClick}
|
|
@@ -100,18 +112,34 @@ export function PersonCell({
|
|
|
100
112
|
>
|
|
101
113
|
{row}
|
|
102
114
|
</Button>
|
|
115
|
+
) : (
|
|
116
|
+
row
|
|
117
|
+
);
|
|
118
|
+
// One line: the second line is not drawn, so it is the cell's tooltip.
|
|
119
|
+
return lines === 1 && secondary ? (
|
|
120
|
+
<FloatingTooltip content={`${displayName} · ${secondary}`} triggerClassName="min-w-0 max-w-full">
|
|
121
|
+
{cell}
|
|
122
|
+
</FloatingTooltip>
|
|
123
|
+
) : (
|
|
124
|
+
cell
|
|
103
125
|
);
|
|
104
126
|
}
|
|
105
127
|
|
|
106
128
|
/** The cell while it loads: the SAME avatar box, gap and two lines (the name's type step, then the caption's). */
|
|
107
|
-
function PersonCellSkeleton({
|
|
129
|
+
function PersonCellSkeleton({
|
|
130
|
+
size = 'sm',
|
|
131
|
+
lines = 2,
|
|
132
|
+
}: {
|
|
133
|
+
size?: PersonCellProps['size'];
|
|
134
|
+
lines?: PersonCellProps['lines'];
|
|
135
|
+
}) {
|
|
108
136
|
const scale = PERSON_CELL_SIZE[size];
|
|
109
137
|
return (
|
|
110
138
|
<div className={`${PERSON_ROW_CLASS} ${scale.gap}`} aria-hidden>
|
|
111
139
|
<SquareAvatar loading size={scale.avatar} />
|
|
112
140
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
113
141
|
<TextLineSkeleton type={scale.primary} className="w-3/4" />
|
|
114
|
-
<TextLineSkeleton type="h6" className="w-1/2" />
|
|
142
|
+
{lines === 2 ? <TextLineSkeleton type="h6" className="w-1/2" /> : null}
|
|
115
143
|
</div>
|
|
116
144
|
</div>
|
|
117
145
|
);
|
|
@@ -130,14 +158,16 @@ export interface PeopleCellProps {
|
|
|
130
158
|
people: PeopleCellPerson[];
|
|
131
159
|
/** Shown when the list is empty — the same slot `PersonCell` fills. */
|
|
132
160
|
fallbackName?: string;
|
|
161
|
+
/** `1`: one line for one person or several (see `PersonCellProps.lines`). */
|
|
162
|
+
lines?: 1 | 2;
|
|
133
163
|
}
|
|
134
164
|
|
|
135
165
|
/** Positions the multi-person stack reserves: two faces, or one face and "+N". */
|
|
136
166
|
const PEOPLE_CELL_STACK_SLOTS = 2;
|
|
137
167
|
|
|
138
|
-
export function PeopleCell({ people, fallbackName = 'Unassigned' }: PeopleCellProps) {
|
|
168
|
+
export function PeopleCell({ people, fallbackName = 'Unassigned', lines = 2 }: PeopleCellProps) {
|
|
139
169
|
// In a page's loading state the people are read: the cell is a person cell's skeleton.
|
|
140
|
-
if (useLoadingScope()) return <PersonCellSkeleton />;
|
|
170
|
+
if (useLoadingScope()) return <PersonCellSkeleton lines={lines} />;
|
|
141
171
|
if (people.length <= 1) {
|
|
142
172
|
const [person] = people;
|
|
143
173
|
return (
|
|
@@ -146,6 +176,7 @@ export function PeopleCell({ people, fallbackName = 'Unassigned' }: PeopleCellPr
|
|
|
146
176
|
avatarUrl={person?.avatarUrl}
|
|
147
177
|
secondary={person ? person.secondary : undefined}
|
|
148
178
|
fallbackName={fallbackName}
|
|
179
|
+
lines={lines}
|
|
149
180
|
/>
|
|
150
181
|
);
|
|
151
182
|
}
|
|
@@ -163,7 +194,12 @@ export function PeopleCell({ people, fallbackName = 'Unassigned' }: PeopleCellPr
|
|
|
163
194
|
people={people.map(p => ({ key: p.key, name: p.name || 'Unknown', avatarUrl: p.avatarUrl }))}
|
|
164
195
|
/>
|
|
165
196
|
</div>
|
|
166
|
-
<p
|
|
197
|
+
<p
|
|
198
|
+
className={cn(
|
|
199
|
+
'min-w-0 flex-1 break-words text-ods-text-primary text-h6',
|
|
200
|
+
lines === 1 ? 'line-clamp-1' : 'line-clamp-2',
|
|
201
|
+
)}
|
|
202
|
+
>
|
|
167
203
|
<span aria-hidden>{people.map(p => personFirstName(p.name, 'Unknown')).join(' & ')}</span>
|
|
168
204
|
<span className="sr-only">{fullNames}</span>
|
|
169
205
|
</p>
|
|
@@ -27,6 +27,12 @@ type ProgressBarProps = {
|
|
|
27
27
|
height?: number; // desktop height (px)
|
|
28
28
|
mobileHeight?: number; // mobile height (px)
|
|
29
29
|
inverted?: boolean; // if true, high values are good (green), low values are bad (red)
|
|
30
|
+
/**
|
|
31
|
+
* `threshold` (default): the fill takes the success, warning or error colour by the thresholds. `neutral`: the fill
|
|
32
|
+
* is the secondary text colour at every value, for a count that is progress, not a health reading (a sign-off
|
|
33
|
+
* count), so a partial bar never reads as an error.
|
|
34
|
+
*/
|
|
35
|
+
tone?: 'threshold' | 'neutral';
|
|
30
36
|
/** The value is still being read: the bar draws its `ProgressBarSkeleton` at its own height. A loading scope does
|
|
31
37
|
* the same. */
|
|
32
38
|
loading?: boolean;
|
|
@@ -42,6 +48,7 @@ export const ProgressBar: React.FC<ProgressBarProps> = ({
|
|
|
42
48
|
height = PROGRESS_BAR_HEIGHT.desktop,
|
|
43
49
|
mobileHeight = PROGRESS_BAR_HEIGHT.mobile,
|
|
44
50
|
inverted = false,
|
|
51
|
+
tone = 'threshold',
|
|
45
52
|
loading: loadingProp,
|
|
46
53
|
}) => {
|
|
47
54
|
// ONE switch: in a page's loading state (or while the host reads the value) the bar is its skeleton.
|
|
@@ -76,6 +83,7 @@ export const ProgressBar: React.FC<ProgressBarProps> = ({
|
|
|
76
83
|
|
|
77
84
|
// Pick color based on thresholds using ODS design tokens
|
|
78
85
|
const getColor = () => {
|
|
86
|
+
if (tone === 'neutral') return 'var(--color-text-secondary)';
|
|
79
87
|
if (inverted) {
|
|
80
88
|
// Inverted: high values = good (green), low values = bad (red)
|
|
81
89
|
// For battery health: 100% = green, <30% = red
|
|
@@ -201,6 +201,20 @@ function isEmptyTagLabel(label: React.ReactNode): boolean {
|
|
|
201
201
|
return label == null || label === false || (typeof label === 'string' && label.trim() === '');
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/** The tones a badge colour keeps when it is drawn as a tag. */
|
|
205
|
+
const BADGE_COLOR_TAG_TONES = ['success', 'warning', 'error'] as const;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* THE tag variant for a badge colour that is data (a `StatusBadge` colour scheme a vocabulary row stores): the three
|
|
209
|
+
* semantic tones carry over, every other colour reads grey. Every card that draws such a colour as a tag reads it
|
|
210
|
+
* here, so a status is the same tone in a badge and in a chat card.
|
|
211
|
+
*/
|
|
212
|
+
export function tagVariantForBadgeColor(
|
|
213
|
+
color: string | null | undefined,
|
|
214
|
+
): (typeof BADGE_COLOR_TAG_TONES)[number] | 'grey' {
|
|
215
|
+
return BADGE_COLOR_TAG_TONES.find(tone => tone === color) ?? 'grey';
|
|
216
|
+
}
|
|
217
|
+
|
|
204
218
|
export { Tag, tagVariants };
|
|
205
219
|
|
|
206
220
|
/**
|