@carlonicora/nextjs-jsonapi 3.25.0 → 3.26.0
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/{BlockNoteEditor-HKS6AXDA.js → BlockNoteEditor-4EIKMNY3.js} +20 -10
- package/dist/BlockNoteEditor-4EIKMNY3.js.map +1 -0
- package/dist/{BlockNoteEditor-46O3JC4Z.mjs → BlockNoteEditor-AXYVDDXM.mjs} +13 -3
- package/dist/{BlockNoteEditor-46O3JC4Z.mjs.map → BlockNoteEditor-AXYVDDXM.mjs.map} +1 -1
- package/dist/billing/index.js +335 -335
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-Y3OKEQGL.js → chunk-ABVTYQIJ.js} +578 -363
- package/dist/chunk-ABVTYQIJ.js.map +1 -0
- package/dist/{chunk-KLV6RLDE.js → chunk-GKDHYB2R.js} +2 -2
- package/dist/chunk-GKDHYB2R.js.map +1 -0
- package/dist/{chunk-XDYILJ2S.mjs → chunk-QWZAGQWJ.mjs} +546 -331
- package/dist/chunk-QWZAGQWJ.mjs.map +1 -0
- package/dist/{chunk-4Z2UONN2.mjs → chunk-U65JPKO3.mjs} +2 -2
- package/dist/chunk-U65JPKO3.mjs.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +16 -3
- package/dist/components/index.d.ts +16 -3
- package/dist/components/index.js +4 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +3 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/analytics/index.js +100 -100
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +5 -5
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/help/index.js +36 -36
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +6 -6
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.js +103 -103
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/containers/PageContainer.tsx +1 -1
- package/src/components/containers/RoundPageContainer.tsx +60 -16
- package/src/components/containers/RoundPageContainerTitle.tsx +75 -35
- package/src/components/editors/BlockNoteEditor.tsx +13 -1
- package/src/components/forms/EntityMultiSelector.tsx +4 -4
- package/src/components/forms/EntitySelector.tsx +4 -4
- package/src/components/forms/FormBlockNote.tsx +21 -1
- package/src/components/forms/FormCheckbox.tsx +6 -2
- package/src/components/forms/__tests__/useEditorDialog.test.ts +47 -0
- package/src/components/forms/useEditorDialog.ts +22 -0
- package/src/components/navigations/Breadcrumb.tsx +45 -16
- package/src/components/navigations/Header.tsx +16 -3
- package/src/components/navigations/MobileNavigationBar.tsx +12 -0
- package/src/components/navigations/__tests__/MobileNavigationBar.spec.tsx +17 -0
- package/src/components/tables/ContentListTable.tsx +52 -32
- package/src/features/analytics/components/AnalyticsAdminFilterBar.tsx +9 -6
- package/src/features/analytics/components/AnalyticsBreakdownTable.tsx +2 -1
- package/src/features/assistant/components/parts/AssistantBlockNoteComposer.tsx +1 -1
- package/src/features/assistant/components/parts/AssistantComposer.tsx +1 -1
- package/src/features/assistant-message/components/MessageItem.tsx +2 -2
- package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +23 -5
- package/src/features/auth/components/containers/AuthContainer.tsx +8 -1
- package/src/features/company/components/details/CompanyContent.tsx +3 -1
- package/src/features/company/components/details/CompanyDetails.tsx +9 -1
- package/src/features/company/components/details/TokenStatusIndicator.tsx +42 -13
- package/src/features/help/components/HelpHeader.tsx +7 -5
- package/src/features/tokenusage/components/TokenUsageAdminFilterBar.tsx +9 -6
- package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +21 -0
- package/src/features/tokenusage/lib/formatters.ts +5 -1
- package/src/shadcnui/ui/button.tsx +1 -1
- package/src/shadcnui/ui/checkbox.tsx +1 -1
- package/src/shadcnui/ui/command.tsx +4 -4
- package/src/shadcnui/ui/dialog.tsx +2 -0
- package/src/shadcnui/ui/input.tsx +1 -1
- package/src/shadcnui/ui/popover.tsx +9 -0
- package/src/shadcnui/ui/select.tsx +1 -1
- package/src/shadcnui/ui/sheet.tsx +8 -1
- package/src/shadcnui/ui/table.tsx +2 -2
- package/src/shadcnui/ui/textarea.tsx +1 -1
- package/dist/BlockNoteEditor-HKS6AXDA.js.map +0 -1
- package/dist/chunk-4Z2UONN2.mjs.map +0 -1
- package/dist/chunk-KLV6RLDE.js.map +0 -1
- package/dist/chunk-XDYILJ2S.mjs.map +0 -1
- package/dist/chunk-Y3OKEQGL.js.map +0 -1
|
@@ -15,9 +15,22 @@ type HeaderProps = {
|
|
|
15
15
|
/** Rendered before everything else, on mobile only — where the sidebar (and its logo) is off-canvas. */
|
|
16
16
|
logo?: React.ReactNode;
|
|
17
17
|
className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Drop the sidebar toggle on mobile. Set by page shells that render the mobile
|
|
20
|
+
* bottom bar, whose "Menu" slot opens the same drawer — two controls for one
|
|
21
|
+
* drawer only cost header room on a phone. Desktop always keeps the toggle.
|
|
22
|
+
*/
|
|
23
|
+
hideSidebarTriggerOnMobile?: boolean;
|
|
18
24
|
};
|
|
19
25
|
|
|
20
|
-
export function Header({
|
|
26
|
+
export function Header({
|
|
27
|
+
children,
|
|
28
|
+
mobileChildren,
|
|
29
|
+
leftContent,
|
|
30
|
+
logo,
|
|
31
|
+
className,
|
|
32
|
+
hideSidebarTriggerOnMobile,
|
|
33
|
+
}: HeaderProps) {
|
|
21
34
|
const { breadcrumbs } = useSharedContext();
|
|
22
35
|
const rootLabel = useHeaderRootLabel();
|
|
23
36
|
const isRootHidden = useIsBreadcrumbRootHidden();
|
|
@@ -42,9 +55,9 @@ export function Header({ children, mobileChildren, leftContent, logo, className
|
|
|
42
55
|
>
|
|
43
56
|
<div className="bg-sidebar flex h-12 w-full flex-row items-center justify-between ps-2 pe-4">
|
|
44
57
|
{isMobile && logo && <div className="flex shrink-0 flex-row items-center pe-1">{logo}</div>}
|
|
45
|
-
{sidebar && <SidebarTrigger aria-label="Toggle sidebar" id="sidebar-trigger" />}
|
|
58
|
+
{sidebar && !(isMobile && hideSidebarTriggerOnMobile) && <SidebarTrigger aria-label="Toggle sidebar" id="sidebar-trigger" />}
|
|
46
59
|
{leftContent}
|
|
47
|
-
<div className="flex w-full flex-row items-center justify-start">
|
|
60
|
+
<div className="flex w-full flex-row items-center justify-start max-md:min-w-0">
|
|
48
61
|
<BreadcrumbNavigation items={breadcrumbs} rootLabel={rootLabel ?? undefined} showRoot={!isRootHidden} />
|
|
49
62
|
</div>
|
|
50
63
|
{isMobile
|
|
@@ -84,6 +84,18 @@ export function MobileNavigationBar({ className }: { className?: string } = {})
|
|
|
84
84
|
</>
|
|
85
85
|
);
|
|
86
86
|
|
|
87
|
+
// A slot with neither href nor onClick is self-contained: its `render`
|
|
88
|
+
// brings its own control (e.g. an avatar that opens a user menu), so it
|
|
89
|
+
// is not wrapped in a Link/button — that would nest one interactive
|
|
90
|
+
// element inside another.
|
|
91
|
+
if (!item.href && !item.onClick) {
|
|
92
|
+
return (
|
|
93
|
+
<div key={item.key} data-testid={`mobile-nav-${item.key}`} className={cn(slotClass, tone)}>
|
|
94
|
+
<SlotContent item={item} />
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
return item.href ? (
|
|
88
100
|
<Link
|
|
89
101
|
key={item.key}
|
|
@@ -109,6 +109,23 @@ describe("MobileNavigationBar", () => {
|
|
|
109
109
|
expect(screen.getByTestId("avatar-stub")).toBeInTheDocument();
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
it("renders a slot with neither href nor onClick as-is, without wrapping it in a link or button", () => {
|
|
113
|
+
renderBar([
|
|
114
|
+
{
|
|
115
|
+
key: "profile",
|
|
116
|
+
label: "Account",
|
|
117
|
+
render: (
|
|
118
|
+
<button type="button" data-testid="menu-trigger">
|
|
119
|
+
Account
|
|
120
|
+
</button>
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
const trigger = screen.getByTestId("menu-trigger");
|
|
125
|
+
expect(trigger.parentElement?.closest("button, a")).toBeNull();
|
|
126
|
+
expect(screen.getAllByRole("button")).toHaveLength(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
112
129
|
// The load-bearing case: a bare startsWith would make href="/" match everything.
|
|
113
130
|
it("marks only the exactly-matching slot active when one href is '/'", () => {
|
|
114
131
|
pathnameRef.current = "/conversations";
|
|
@@ -10,6 +10,7 @@ import React, { ReactNode, memo, useMemo, useState } from "react";
|
|
|
10
10
|
import { DataListRetriever, TableContent, useTableGenerator } from "../../hooks";
|
|
11
11
|
import { ModuleWithPermissions } from "../../permissions";
|
|
12
12
|
import { Button, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "../../shadcnui";
|
|
13
|
+
import { useIsMobile } from "../../utils/use-mobile";
|
|
13
14
|
import { MicroLabel } from "../typography";
|
|
14
15
|
import { ContentTableSearch } from "./ContentTableSearch";
|
|
15
16
|
|
|
@@ -110,6 +111,7 @@ function renderCell(cellDef: unknown, context: unknown): ReactNode {
|
|
|
110
111
|
export const ContentListTable = memo(function ContentListTable(props: ContentListTableProps) {
|
|
111
112
|
const { data, fields, checkedIds, toggleId, allowSearch, filters: _filters, fullWidth, onRowClick } = props;
|
|
112
113
|
const t = useTranslations();
|
|
114
|
+
const isMobile = useIsMobile();
|
|
113
115
|
const [expanded, setExpanded] = useState<ExpandedState>(
|
|
114
116
|
props.defaultExpanded === true ? true : typeof props.defaultExpanded === "object" ? props.defaultExpanded : {},
|
|
115
117
|
);
|
|
@@ -212,13 +214,60 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
212
214
|
// 12px title is the mismatch the opt-in exists to avoid.
|
|
213
215
|
const titleIsLarge = (props.titleSize ?? (fullWidth ? `lg` : `sm`)) === `lg`;
|
|
214
216
|
|
|
217
|
+
const titleLabel = (
|
|
218
|
+
<div
|
|
219
|
+
className={cn(
|
|
220
|
+
"text-muted-foreground flex items-center gap-x-2 font-light whitespace-nowrap",
|
|
221
|
+
titleIsLarge ? `text-lg` : `text-sm`,
|
|
222
|
+
)}
|
|
223
|
+
>
|
|
224
|
+
{props.titleActions}
|
|
225
|
+
{props.tableGeneratorType.icon && (
|
|
226
|
+
<props.tableGeneratorType.icon className={cn(`text-primary`, titleIsLarge ? `h-6 w-6` : `h-4 w-4`)} />
|
|
227
|
+
)}
|
|
228
|
+
{props.title}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const hasTitleControls = !!(props.functions || props.filters || allowSearch);
|
|
233
|
+
const titleControls = (
|
|
234
|
+
<>
|
|
235
|
+
{props.filters}
|
|
236
|
+
{props.functions}
|
|
237
|
+
{/* `allowSearch !== false`, not `allowSearch`: the search box has
|
|
238
|
+
always rendered here whenever `functions`/`filters` were set, and
|
|
239
|
+
most callers rely on that without passing the flag. Gating on the
|
|
240
|
+
flag alone (as the sibling ContentListGrid does) would silently
|
|
241
|
+
remove search from every such list. This only lets a caller whose
|
|
242
|
+
retriever ignores `search` — e.g. notifications, whose repository
|
|
243
|
+
has no search param and no fulltext index — opt out of a control
|
|
244
|
+
that would do nothing. */}
|
|
245
|
+
{allowSearch !== false && <ContentTableSearch data={data} />}
|
|
246
|
+
</>
|
|
247
|
+
);
|
|
248
|
+
|
|
215
249
|
return (
|
|
216
250
|
<div data-help="list.table" className="flex w-full flex-col">
|
|
217
251
|
{/* <div className="overflow-clip rounded-md border"> */}
|
|
218
252
|
<div className={cn(`overflow-clip`, fullWidth ? `` : `rounded-md border`)}>
|
|
253
|
+
{/* On a phone the title bar sits above the table instead of inside its
|
|
254
|
+
header: inside, it is as wide as the table, so on a table wider than
|
|
255
|
+
the screen "Create New", search and filters land off-screen. Above it,
|
|
256
|
+
it wraps within the card while the table scrolls sideways on its own. */}
|
|
257
|
+
{props.title && isMobile && (
|
|
258
|
+
<div
|
|
259
|
+
data-testid="content-list-table-title-bar"
|
|
260
|
+
className="bg-card text-primary flex flex-wrap items-center gap-2 border-b p-4 font-bold"
|
|
261
|
+
>
|
|
262
|
+
<div className="shrink-0">{titleLabel}</div>
|
|
263
|
+
{hasTitleControls && (
|
|
264
|
+
<div className="ms-auto flex flex-wrap items-center justify-end gap-2">{titleControls}</div>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
219
268
|
<Table>
|
|
220
269
|
<TableHeader className="bg-muted font-semibold">
|
|
221
|
-
{props.title && (
|
|
270
|
+
{props.title && !isMobile && (
|
|
222
271
|
<TableRow>
|
|
223
272
|
<TableHead
|
|
224
273
|
className="bg-card rounded-t-lg text-primary p-4 text-start font-bold"
|
|
@@ -226,37 +275,8 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
226
275
|
>
|
|
227
276
|
<div className="flex w-full items-center justify-between gap-x-2">
|
|
228
277
|
{/* <div className="w-full">{fullWidth ? `` : props.title}</div> */}
|
|
229
|
-
<div className="w-full">
|
|
230
|
-
|
|
231
|
-
className={cn(
|
|
232
|
-
"text-muted-foreground flex items-center gap-x-2 font-light whitespace-nowrap",
|
|
233
|
-
titleIsLarge ? `text-lg` : `text-sm`,
|
|
234
|
-
)}
|
|
235
|
-
>
|
|
236
|
-
{props.titleActions}
|
|
237
|
-
{props.tableGeneratorType.icon && (
|
|
238
|
-
<props.tableGeneratorType.icon
|
|
239
|
-
className={cn(`text-primary`, titleIsLarge ? `h-6 w-6` : `h-4 w-4`)}
|
|
240
|
-
/>
|
|
241
|
-
)}
|
|
242
|
-
{props.title}
|
|
243
|
-
</div>
|
|
244
|
-
</div>
|
|
245
|
-
{(props.functions || props.filters || allowSearch) && (
|
|
246
|
-
<>
|
|
247
|
-
{props.filters}
|
|
248
|
-
{props.functions}
|
|
249
|
-
{/* `allowSearch !== false`, not `allowSearch`: the search box has
|
|
250
|
-
always rendered here whenever `functions`/`filters` were set, and
|
|
251
|
-
most callers rely on that without passing the flag. Gating on the
|
|
252
|
-
flag alone (as the sibling ContentListGrid does) would silently
|
|
253
|
-
remove search from every such list. This only lets a caller whose
|
|
254
|
-
retriever ignores `search` — e.g. notifications, whose repository
|
|
255
|
-
has no search param and no fulltext index — opt out of a control
|
|
256
|
-
that would do nothing. */}
|
|
257
|
-
{allowSearch !== false && <ContentTableSearch data={data} />}
|
|
258
|
-
</>
|
|
259
|
-
)}
|
|
278
|
+
<div className="w-full">{titleLabel}</div>
|
|
279
|
+
{hasTitleControls && titleControls}
|
|
260
280
|
</div>
|
|
261
281
|
</TableHead>
|
|
262
282
|
</TableRow>
|
|
@@ -48,12 +48,15 @@ export function AnalyticsAdminFilterBar({ section, granularity, onChange }: Prop
|
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<div className="flex flex-wrap items-center gap-2">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
{/* The date button is a fixed 300px; on a phone it takes the full row instead. */}
|
|
52
|
+
<div className="max-md:w-full max-md:[&_#date]:w-full">
|
|
53
|
+
<DateRangeSelector
|
|
54
|
+
onDateChange={(range) => {
|
|
55
|
+
if (!range?.from || !range?.to) return;
|
|
56
|
+
onChange({ from: range.from.toISOString(), to: range.to.toISOString() });
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
57
60
|
|
|
58
61
|
<Segmented
|
|
59
62
|
ariaLabel={t("analytics.admin.filter.section")}
|
|
@@ -48,7 +48,8 @@ export function AnalyticsBreakdownTable({ breakdowns }: Props) {
|
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<Tabs defaultValue={ANALYTICS_DIMENSIONS[0]}>
|
|
51
|
-
|
|
51
|
+
{/* Six tabs are wider than a phone-width card: scroll them sideways there. */}
|
|
52
|
+
<TabsList className="max-md:w-full max-md:justify-start max-md:overflow-x-auto">
|
|
52
53
|
{ANALYTICS_DIMENSIONS.map((dimension) => (
|
|
53
54
|
<TabsTrigger key={dimension} value={dimension}>
|
|
54
55
|
{labels[dimension]}
|
|
@@ -129,7 +129,7 @@ export function AssistantBlockNoteComposer({
|
|
|
129
129
|
<ArrowUp className="me-1 h-4 w-4" /> {t("features.assistant.send")}
|
|
130
130
|
</Button>
|
|
131
131
|
</div>
|
|
132
|
-
<div className="text-muted-foreground text-end text-xs">{t("features.assistant.keyboard_hint")}</div>
|
|
132
|
+
<div className="text-muted-foreground text-end text-xs max-md:hidden">{t("features.assistant.keyboard_hint")}</div>
|
|
133
133
|
</div>
|
|
134
134
|
);
|
|
135
135
|
}
|
|
@@ -92,7 +92,7 @@ export function AssistantComposer({
|
|
|
92
92
|
{sendButton}
|
|
93
93
|
</div>
|
|
94
94
|
)}
|
|
95
|
-
<div className="text-muted-foreground text-end text-xs">{t("features.assistant.keyboard_hint")}</div>
|
|
95
|
+
<div className="text-muted-foreground text-end text-xs max-md:hidden">{t("features.assistant.keyboard_hint")}</div>
|
|
96
96
|
</div>
|
|
97
97
|
);
|
|
98
98
|
}
|
|
@@ -82,7 +82,7 @@ export function MessageItem({
|
|
|
82
82
|
renders invisible here, so force the on-primary colour and lean on
|
|
83
83
|
weight for the affordance (links are never underlined in this UI).
|
|
84
84
|
*/}
|
|
85
|
-
<div className="bg-primary text-primary-foreground max-w-[72%] rounded-2xl rounded-ee-sm px-3.5 py-2 text-sm [&_a]:text-primary-foreground [&_a]:font-semibold [&_p]:m-0">
|
|
85
|
+
<div className="bg-primary text-primary-foreground max-w-[72%] max-md:max-w-[92%] rounded-2xl rounded-ee-sm px-3.5 py-2 text-sm [&_a]:text-primary-foreground [&_a]:font-semibold [&_p]:m-0">
|
|
86
86
|
<ReactMarkdown remarkPlugins={[remarkGfm]} urlTransform={mentionUrlTransform} components={markdownComponents}>
|
|
87
87
|
{message.content}
|
|
88
88
|
</ReactMarkdown>
|
|
@@ -103,7 +103,7 @@ export function MessageItem({
|
|
|
103
103
|
const isApprovalRequest = message.messageType === "approval-request" && !!renderApprovalAction;
|
|
104
104
|
|
|
105
105
|
return (
|
|
106
|
-
<div className="flex min-w-0 max-w-[78%] flex-col gap-1.5">
|
|
106
|
+
<div className="flex min-w-0 max-w-[78%] max-md:max-w-[92%] flex-col gap-1.5">
|
|
107
107
|
<div className="text-muted-foreground flex items-center gap-2 ps-1 text-xs">
|
|
108
108
|
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-gradient-to-br from-blue-400 to-violet-500 text-white">
|
|
109
109
|
<Sparkles className="h-2.5 w-2.5" />
|
|
@@ -9,6 +9,8 @@ import type { ApiDataInterface } from "../../../../../core";
|
|
|
9
9
|
import type { ChunkInterface, ChunkRelationshipMeta } from "../../../../chunk/data/ChunkInterface";
|
|
10
10
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table";
|
|
11
11
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../../shadcnui/ui/tooltip";
|
|
12
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../../../../../shadcnui/ui/popover";
|
|
13
|
+
import { useIsMobile } from "../../../../../utils/use-mobile";
|
|
12
14
|
import { cn } from "@/lib/utils";
|
|
13
15
|
import { RelevanceMeter } from "../RelevanceMeter";
|
|
14
16
|
import { useEntityLabel } from "./useEntityLabel";
|
|
@@ -26,6 +28,8 @@ interface Props {
|
|
|
26
28
|
export function CitationsTab({ citations, sources }: Props) {
|
|
27
29
|
const t = useTranslations();
|
|
28
30
|
const entityLabel = useEntityLabel();
|
|
31
|
+
// A tooltip never opens on touch, so on a phone the reason is a tap popover.
|
|
32
|
+
const isMobile = useIsMobile();
|
|
29
33
|
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
30
34
|
if (citations.length === 0) return null;
|
|
31
35
|
|
|
@@ -45,7 +49,7 @@ export function CitationsTab({ citations, sources }: Props) {
|
|
|
45
49
|
<TableHeader>
|
|
46
50
|
<TableRow>
|
|
47
51
|
<TableHead>{t("features.assistant.message.sources.source")}</TableHead>
|
|
48
|
-
<TableHead className="w-28 text-center">{t("features.assistant.message.sources.relevance")}</TableHead>
|
|
52
|
+
<TableHead className="w-28 text-center max-md:w-20">{t("features.assistant.message.sources.relevance")}</TableHead>
|
|
49
53
|
</TableRow>
|
|
50
54
|
</TableHeader>
|
|
51
55
|
<TableBody>
|
|
@@ -76,9 +80,23 @@ export function CitationsTab({ citations, sources }: Props) {
|
|
|
76
80
|
aria-expanded={isOpen}
|
|
77
81
|
className="flex w-full cursor-pointer items-center justify-start gap-x-2"
|
|
78
82
|
>
|
|
79
|
-
<ChevronDown
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
<ChevronDown
|
|
84
|
+
className={cn("h-4 w-4 transition-transform max-md:shrink-0", !isOpen && "-rotate-90")}
|
|
85
|
+
/>
|
|
86
|
+
<span className="font-semibold max-md:min-w-0 max-md:truncate">{sourceName}</span>
|
|
87
|
+
{chunk.reason && isMobile && (
|
|
88
|
+
<Popover>
|
|
89
|
+
<PopoverTrigger
|
|
90
|
+
aria-label={chunk.reason}
|
|
91
|
+
onClick={(e) => e.stopPropagation()}
|
|
92
|
+
className="text-muted-foreground -m-2 inline-flex shrink-0 p-2"
|
|
93
|
+
>
|
|
94
|
+
<HelpCircle className="h-3.5 w-3.5" />
|
|
95
|
+
</PopoverTrigger>
|
|
96
|
+
<PopoverContent className="max-w-64 text-xs">{chunk.reason}</PopoverContent>
|
|
97
|
+
</Popover>
|
|
98
|
+
)}
|
|
99
|
+
{chunk.reason && !isMobile && (
|
|
82
100
|
<Tooltip>
|
|
83
101
|
<TooltipTrigger className="text-muted-foreground inline-flex">
|
|
84
102
|
<HelpCircle className="h-3.5 w-3.5" />
|
|
@@ -89,7 +107,7 @@ export function CitationsTab({ citations, sources }: Props) {
|
|
|
89
107
|
</div>
|
|
90
108
|
</TableCell>
|
|
91
109
|
<TableCell className="text-center">
|
|
92
|
-
<RelevanceMeter value={chunk.relevance ?? 0} />
|
|
110
|
+
<RelevanceMeter value={chunk.relevance ?? 0} className="max-md:w-16" />
|
|
93
111
|
</TableCell>
|
|
94
112
|
</TableRow>
|
|
95
113
|
{isOpen && (
|
|
@@ -28,5 +28,12 @@ function InnerAuthContainer() {
|
|
|
28
28
|
</div>
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// Phone: 40px buttons and inputs on the auth forms (login, register, reset…)
|
|
32
|
+
// so they are comfortable to tap. Input-group buttons (show password) carry
|
|
33
|
+
// data-size and keep their own size. md+ is unchanged.
|
|
34
|
+
return (
|
|
35
|
+
<Card className="w-full max-w-md max-md:[&_[data-slot=button]:not([data-size])]:h-10 max-md:[&_[data-slot=input-group-control]]:h-full max-md:[&_[data-slot=input-group]]:h-10 max-md:[&_[data-slot=input]]:h-10">
|
|
36
|
+
{activeComponent}
|
|
37
|
+
</Card>
|
|
38
|
+
);
|
|
32
39
|
}
|
|
@@ -7,6 +7,7 @@ import { ReactNode } from "react";
|
|
|
7
7
|
import { AttributeElement } from "../../../../components";
|
|
8
8
|
import { SectionHeader, MicroLabel } from "../../../../components/typography";
|
|
9
9
|
import { FiscalDataDisplay } from "../../../../components/fiscal/FiscalDataDisplay";
|
|
10
|
+
import { cn } from "../../../../utils";
|
|
10
11
|
import { CompanyInterface } from "../../data";
|
|
11
12
|
|
|
12
13
|
type CompanyContentProps = {
|
|
@@ -34,7 +35,8 @@ export function CompanyContent({ company, actions }: CompanyContentProps) {
|
|
|
34
35
|
return (
|
|
35
36
|
<div className="flex flex-col gap-y-8">
|
|
36
37
|
{/* Title Row */}
|
|
37
|
-
|
|
38
|
+
{/* Without actions the row only repeats the name the page header shows; hide it on a phone. */}
|
|
39
|
+
<div className={cn("flex w-full items-center justify-between", !actions && "max-md:hidden")}>
|
|
38
40
|
<SectionHeader level={2}>{company.name}</SectionHeader>
|
|
39
41
|
{actions && <div className="flex items-center gap-x-2">{actions}</div>}
|
|
40
42
|
</div>
|
|
@@ -17,7 +17,15 @@ export function CompanyDetails() {
|
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<div className="flex w-full flex-col gap-y-2">
|
|
20
|
-
|
|
20
|
+
{/* On a phone the page header already shows the type, the name and the
|
|
21
|
+
commands, so this inner copy is hidden there. */}
|
|
22
|
+
<ContentTitle
|
|
23
|
+
module={Modules.Company}
|
|
24
|
+
type={title.type}
|
|
25
|
+
element={title.element}
|
|
26
|
+
functions={title.functions}
|
|
27
|
+
className="max-md:hidden"
|
|
28
|
+
/>
|
|
21
29
|
<TokenStatusIndicator size="md" />
|
|
22
30
|
<CompanyContent company={company} />
|
|
23
31
|
</div>
|
|
@@ -12,6 +12,12 @@ interface TokenStatusIndicatorProps {
|
|
|
12
12
|
className?: string;
|
|
13
13
|
size?: "sm" | "md" | "lg";
|
|
14
14
|
showExtraPages?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Render the indicator as a link to this URL instead of a hover tooltip.
|
|
17
|
+
* For touch surfaces (the mobile header), where a hover tooltip never opens:
|
|
18
|
+
* the link carries a 40px tap area and leads to the usage page.
|
|
19
|
+
*/
|
|
20
|
+
href?: string;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
/**
|
|
@@ -36,7 +42,12 @@ const asWholeCredits = (value: number): string => Math.trunc(value).toLocaleStri
|
|
|
36
42
|
* - BatteryLow: 5-25% available
|
|
37
43
|
* - Battery (empty): <5% available
|
|
38
44
|
*/
|
|
39
|
-
export function TokenStatusIndicator({
|
|
45
|
+
export function TokenStatusIndicator({
|
|
46
|
+
className,
|
|
47
|
+
size = "md",
|
|
48
|
+
showExtraPages = true,
|
|
49
|
+
href,
|
|
50
|
+
}: TokenStatusIndicatorProps) {
|
|
40
51
|
const { company } = useCurrentUserContext();
|
|
41
52
|
const t = useTranslations();
|
|
42
53
|
|
|
@@ -131,6 +142,35 @@ export function TokenStatusIndicator({ className, size = "md", showExtraPages =
|
|
|
131
142
|
</div>
|
|
132
143
|
);
|
|
133
144
|
|
|
145
|
+
const badge = (
|
|
146
|
+
<>
|
|
147
|
+
{getBatteryIcon()}
|
|
148
|
+
<span className={cn(textSize, "text-muted-foreground font-medium leading-none")}>
|
|
149
|
+
{asWholeCredits(availableMonthlyCredits)}
|
|
150
|
+
</span>
|
|
151
|
+
{showExtraPages && availableExtraCredits > 0 && (
|
|
152
|
+
<span className="inline-flex items-center gap-0.5">
|
|
153
|
+
<PlusCircle className={cn(smallIconSize, "text-blue-500")} />
|
|
154
|
+
<span className={cn(textSize, "text-blue-500 font-medium leading-none")}>
|
|
155
|
+
{asWholeCredits(availableExtraCredits)}
|
|
156
|
+
</span>
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (href) {
|
|
163
|
+
return (
|
|
164
|
+
<Link
|
|
165
|
+
href={href}
|
|
166
|
+
className={cn("inline-flex min-h-10 min-w-10 items-center justify-center gap-1.5 px-1", className)}
|
|
167
|
+
aria-label={t("billing.tokens.status", { defaultValue: "Page Status" })}
|
|
168
|
+
>
|
|
169
|
+
{badge}
|
|
170
|
+
</Link>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
134
174
|
return (
|
|
135
175
|
<Tooltip>
|
|
136
176
|
{/* The layout classes live on the trigger itself, which renders the
|
|
@@ -142,18 +182,7 @@ export function TokenStatusIndicator({ className, size = "md", showExtraPages =
|
|
|
142
182
|
className={cn("inline-flex items-center gap-1.5 cursor-default", className)}
|
|
143
183
|
aria-label={t("billing.tokens.status", { defaultValue: "Page Status" })}
|
|
144
184
|
>
|
|
145
|
-
{
|
|
146
|
-
<span className={cn(textSize, "text-muted-foreground font-medium leading-none")}>
|
|
147
|
-
{asWholeCredits(availableMonthlyCredits)}
|
|
148
|
-
</span>
|
|
149
|
-
{showExtraPages && availableExtraCredits > 0 && (
|
|
150
|
-
<span className="inline-flex items-center gap-0.5">
|
|
151
|
-
<PlusCircle className={cn(smallIconSize, "text-blue-500")} />
|
|
152
|
-
<span className={cn(textSize, "text-blue-500 font-medium leading-none")}>
|
|
153
|
-
{asWholeCredits(availableExtraCredits)}
|
|
154
|
-
</span>
|
|
155
|
-
</span>
|
|
156
|
-
)}
|
|
185
|
+
{badge}
|
|
157
186
|
</TooltipTrigger>
|
|
158
187
|
<TooltipContent side="top" className="max-w-xs">
|
|
159
188
|
{tooltipContent}
|
|
@@ -16,14 +16,16 @@ export function HelpHeader() {
|
|
|
16
16
|
const appHref = brand?.appHref ?? "/";
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<header className="border-border bg-background flex items-center justify-between border-b px-4 py-3">
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
<header className="border-border bg-background flex items-center justify-between border-b px-4 py-3 max-md:gap-2">
|
|
20
|
+
{/* Phone: the title shrinks and the "· Help & Documentation" suffix
|
|
21
|
+
truncates with an ellipsis instead of wrapping onto a second line. */}
|
|
22
|
+
<Link href="/help" className="flex items-center gap-2 max-md:min-w-0">
|
|
23
|
+
{logo ? <Image src={logo} alt={label} width={28} height={28} className="max-md:shrink-0" /> : null}
|
|
24
|
+
<span className="text-base font-semibold max-md:truncate">
|
|
23
25
|
{label} · {t("help.footerLink")}
|
|
24
26
|
</span>
|
|
25
27
|
</Link>
|
|
26
|
-
<div className="flex items-center gap-2">
|
|
28
|
+
<div className="flex items-center gap-2 max-md:shrink-0">
|
|
27
29
|
<HelpAskAi />
|
|
28
30
|
{/* Styled Links, not <Button render={<Link/>} nativeButton={false}>. Base UI's
|
|
29
31
|
Button primitive warns when rendered as a non-button, and nativeButton={false}
|
|
@@ -41,12 +41,15 @@ export function TokenUsageAdminFilterBar({ granularity, companyId, metric, compa
|
|
|
41
41
|
|
|
42
42
|
return (
|
|
43
43
|
<div className="flex flex-wrap items-center gap-2">
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
{/* The date button is a fixed 300px; on a phone it takes the full row instead. */}
|
|
45
|
+
<div className="max-md:w-full max-md:[&_#date]:w-full">
|
|
46
|
+
<DateRangeSelector
|
|
47
|
+
onDateChange={(range) => {
|
|
48
|
+
if (!range?.from || !range?.to) return;
|
|
49
|
+
onChange({ from: range.from.toISOString(), to: range.to.toISOString() });
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
50
53
|
|
|
51
54
|
<Segmented
|
|
52
55
|
ariaLabel={t("token_usage.admin.granularity.label")}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createUsageFormatters } from "../formatters";
|
|
3
|
+
|
|
4
|
+
describe("createUsageFormatters.compact", () => {
|
|
5
|
+
const { compact } = createUsageFormatters("en-US", "EUR");
|
|
6
|
+
|
|
7
|
+
it("keeps sub-unit axis ticks distinct", () => {
|
|
8
|
+
const ticks = [0, 0.05, 0.1, 0.15, 0.2, 0.25];
|
|
9
|
+
const labels = ticks.map(compact);
|
|
10
|
+
expect(labels).toEqual(["0", "0.05", "0.1", "0.15", "0.2", "0.25"]);
|
|
11
|
+
expect(new Set(labels).size).toBe(ticks.length);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("keeps thousands ticks distinct and exact", () => {
|
|
15
|
+
expect([0, 250, 500, 750, 1000, 1250].map(compact)).toEqual(["0", "250", "500", "750", "1K", "1.25K"]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("still abbreviates large values", () => {
|
|
19
|
+
expect(compact(1500000)).toBe("1.5M");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -50,7 +50,11 @@ export function createUsageFormatters(locale: string, currency: string): UsageFo
|
|
|
50
50
|
|
|
51
51
|
const dayFormat = new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", timeZone: "UTC" });
|
|
52
52
|
const monthFormat = new Intl.DateTimeFormat(locale, { month: "short", year: "numeric", timeZone: "UTC" });
|
|
53
|
-
|
|
53
|
+
// Axis ticks step by 1, 2, 2.5 or 5 times a power of ten, so a tick never needs
|
|
54
|
+
// more than three significant digits. A fixed one-decimal cap rounded a 0.05
|
|
55
|
+
// step into repeated labels (0.1, 0.1, 0.2, 0.2); significant digits keep every
|
|
56
|
+
// tick distinct at any scale (0.05, 0.15, 1.25K).
|
|
57
|
+
const compactFormat = new Intl.NumberFormat(locale, { notation: "compact", maximumSignificantDigits: 3 });
|
|
54
58
|
const collator = new Intl.Collator(locale);
|
|
55
59
|
|
|
56
60
|
return {
|
|
@@ -21,7 +21,7 @@ const buttonVariants = cva(
|
|
|
21
21
|
},
|
|
22
22
|
size: {
|
|
23
23
|
default:
|
|
24
|
-
"h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
24
|
+
"h-7 max-md:h-9 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
25
25
|
xs: "h-5 gap-1 rounded-sm px-2 text-xs/relaxed has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-2.5",
|
|
26
26
|
sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
27
27
|
lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 [&_svg:not([class*='size-'])]:size-4",
|
|
@@ -10,7 +10,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
|
10
10
|
<CheckboxPrimitive.Root
|
|
11
11
|
data-slot="checkbox"
|
|
12
12
|
className={cn(
|
|
13
|
-
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-[2px] aria-invalid:ring-[2px] peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 cursor-pointer data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
13
|
+
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-[2px] aria-invalid:ring-[2px] peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 max-md:after:-inset-y-3 cursor-pointer data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
14
14
|
className,
|
|
15
15
|
)}
|
|
16
16
|
{...props}
|
|
@@ -21,7 +21,7 @@ function Command({ className, ...props }: React.ComponentProps<typeof CommandPri
|
|
|
21
21
|
<CommandPrimitive
|
|
22
22
|
data-slot="command"
|
|
23
23
|
className={cn(
|
|
24
|
-
"bg-popover text-popover-foreground rounded-xl p-1 flex size-full flex-col overflow-hidden",
|
|
24
|
+
"bg-popover text-popover-foreground rounded-xl p-1 flex size-full flex-col overflow-hidden max-md:min-h-0",
|
|
25
25
|
"[[data-slot=dialog-content]_&]:bg-transparent [[data-slot=dialog-content]_&]:text-foreground",
|
|
26
26
|
className,
|
|
27
27
|
)}
|
|
@@ -75,11 +75,11 @@ function CommandDialog({
|
|
|
75
75
|
function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
76
76
|
return (
|
|
77
77
|
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
|
78
|
-
<InputGroup className="bg-input/20 dark:bg-input/30 h-8!">
|
|
78
|
+
<InputGroup className="bg-input/20 dark:bg-input/30 h-8! max-md:h-10!">
|
|
79
79
|
<CommandPrimitive.Input
|
|
80
80
|
data-slot="command-input"
|
|
81
81
|
className={cn(
|
|
82
|
-
"w-full text-xs/relaxed outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
82
|
+
"w-full text-xs/relaxed max-md:text-base outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
|
83
83
|
className,
|
|
84
84
|
)}
|
|
85
85
|
{...props}
|
|
@@ -96,7 +96,7 @@ function CommandList({ className, ...props }: React.ComponentProps<typeof Comman
|
|
|
96
96
|
return (
|
|
97
97
|
<CommandPrimitive.List
|
|
98
98
|
data-slot="command-list"
|
|
99
|
-
className={cn("no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto", className)}
|
|
99
|
+
className={cn("no-scrollbar max-h-72 max-md:min-h-0 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto", className)}
|
|
100
100
|
{...props}
|
|
101
101
|
/>
|
|
102
102
|
);
|
|
@@ -52,6 +52,8 @@ function DialogContent({
|
|
|
52
52
|
className={cn(
|
|
53
53
|
// rtl-ok: symmetric centering (`left-1/2` + `-translate-x-1/2`) — identical in LTR and RTL
|
|
54
54
|
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid gap-4 rounded-xl p-4 text-xs/relaxed ring-1 duration-100 fixed top-1/2 left-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 outline-none",
|
|
55
|
+
// Phone: never wider or taller than the screen; the dialog scrolls inside.
|
|
56
|
+
"max-md:max-h-[calc(100svh-2rem)] max-md:max-w-[calc(100vw-2rem)] max-md:overflow-y-auto",
|
|
55
57
|
className,
|
|
56
58
|
)}
|
|
57
59
|
{...props}
|
|
@@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
|
9
9
|
type={type}
|
|
10
10
|
data-slot="input"
|
|
11
11
|
className={cn(
|
|
12
|
-
"bg-input/20 dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 rounded-md border px-2 py-0.5 text-
|
|
12
|
+
"bg-input/20 dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 max-md:h-10 rounded-md border px-2 py-0.5 text-base transition-colors file:h-6 file:text-xs/relaxed file:font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
13
13
|
className,
|
|
14
14
|
)}
|
|
15
15
|
{...props}
|