@iloveagents/foundry-web-ui 0.21.0 → 0.22.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/components/app-brand.d.ts +3 -13
- package/dist/components/app-brand.js +50 -2
- package/dist/components/data-table/data-table-selection-bar.d.ts +2 -1
- package/dist/components/data-table/data-table-selection-bar.js +21 -1
- package/dist/components/data-table/data-table-toolbar.js +29 -3
- package/dist/components/sidebar.js +9 -9
- package/dist/components/theme-runtime-provider.js +144 -2
- package/dist/lib/theme-runtime.d.ts +25 -0
- package/dist/lib/theme-runtime.js +6 -0
- package/dist/styles.css +24 -0
- package/package.json +3 -3
|
@@ -6,20 +6,10 @@ export interface BrandLockup {
|
|
|
6
6
|
isIcon: boolean;
|
|
7
7
|
name: string;
|
|
8
8
|
showName: boolean;
|
|
9
|
+
/** Validated CSS height for the mark, or undefined to use the default. */
|
|
10
|
+
height?: string;
|
|
9
11
|
}
|
|
10
|
-
|
|
11
|
-
* Decide what the brand lockup renders. Exported (underscore-prefixed) so the
|
|
12
|
-
* branching is unit-testable without a DOM renderer — same approach as
|
|
13
|
-
* ``tool-fallback.tsx``.
|
|
14
|
-
*
|
|
15
|
-
* The rule worth stating: whether the name is printed depends on the *shape*
|
|
16
|
-
* of the brand image. A wordmark already spells the brand out, so pairing it
|
|
17
|
-
* with `appName` stutters ("Acme Acme"). An icon says nothing on its
|
|
18
|
-
* own, so it needs the name to complete the lockup. Hence an icon wins over a
|
|
19
|
-
* wordmark when both are set — supplying an icon is an explicit request for
|
|
20
|
-
* "<mark> Product Name", which is what a tenant whose product name differs
|
|
21
|
-
* from their company name is asking for.
|
|
22
|
-
*/
|
|
12
|
+
export declare function _safeLogoHeight(value: string | undefined): string | undefined;
|
|
23
13
|
export declare function _resolveBrandLockup(branding: ThemeBranding, mode: "light" | "dark", showName: boolean): BrandLockup;
|
|
24
14
|
export declare function AppBrand({ showName, className, iconClassName, labelClassName, }: {
|
|
25
15
|
showName?: boolean;
|
|
@@ -15,6 +15,37 @@ import { useThemeRuntime } from "./theme-runtime-provider.js";
|
|
|
15
15
|
* "<mark> Product Name", which is what a tenant whose product name differs
|
|
16
16
|
* from their company name is asking for.
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* A tenant-supplied length, or undefined.
|
|
20
|
+
*
|
|
21
|
+
* The value lands in an inline `style`, and a theme is content a tenant
|
|
22
|
+
* authors — so this allowlists a shape rather than trying to spot bad ones.
|
|
23
|
+
* Anything else (a `calc()`, a `var()`, a url, a second declaration smuggled
|
|
24
|
+
* in behind a `;`) fails the pattern and the caller falls back to the default
|
|
25
|
+
* height, which is always a safe thing to render.
|
|
26
|
+
*/
|
|
27
|
+
//: The tallest mark the header will render. A syntactically valid length is
|
|
28
|
+
//: not a SAFE one: `logoHeight: "320px"` is a plausible typo, and because the
|
|
29
|
+
//: mark is `shrink-0` it would push the sidebar and mobile-header actions off
|
|
30
|
+
//: screen rather than shrinking. Marks above this are clamped, not rejected —
|
|
31
|
+
//: a too-large value should look wrong, not vanish.
|
|
32
|
+
const MAX_LOGO_HEIGHT_REM = 4;
|
|
33
|
+
const REM_PER_UNIT = { rem: 1, em: 1, px: 1 / 16 };
|
|
34
|
+
export function _safeLogoHeight(value) {
|
|
35
|
+
if (!value)
|
|
36
|
+
return undefined;
|
|
37
|
+
// The integer part is optional: `.5rem` is a valid CSS length and a common
|
|
38
|
+
// spelling, and silently falling back to the default for it would make the
|
|
39
|
+
// contract a lie.
|
|
40
|
+
const match = /^(\d*\.?\d+)(px|rem|em)$/.exec(value.trim());
|
|
41
|
+
if (!match)
|
|
42
|
+
return undefined;
|
|
43
|
+
const [, rawNumber, unit] = match;
|
|
44
|
+
const asRem = Number(rawNumber) * REM_PER_UNIT[unit];
|
|
45
|
+
if (!Number.isFinite(asRem) || asRem <= 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
return asRem > MAX_LOGO_HEIGHT_REM ? `${MAX_LOGO_HEIGHT_REM}rem` : value.trim();
|
|
48
|
+
}
|
|
18
49
|
export function _resolveBrandLockup(branding, mode, showName) {
|
|
19
50
|
const isDark = mode === "dark";
|
|
20
51
|
const pick = (dark, light) => (isDark ? dark || light : light || dark);
|
|
@@ -26,12 +57,18 @@ export function _resolveBrandLockup(branding, mode, showName) {
|
|
|
26
57
|
isIcon: Boolean(iconUrl),
|
|
27
58
|
name: branding.appName || "Foundry UI",
|
|
28
59
|
showName: showName && (Boolean(iconUrl) || !markUrl),
|
|
60
|
+
height: _safeLogoHeight(branding.logoHeight),
|
|
29
61
|
};
|
|
30
62
|
}
|
|
31
63
|
export function AppBrand({ showName = true, className, iconClassName, labelClassName, }) {
|
|
32
64
|
const runtime = useThemeRuntime();
|
|
33
65
|
const lockup = _resolveBrandLockup(runtime.branding, runtime.mode, showName);
|
|
34
|
-
return (_jsxs("span", { className: cn("flex items-center gap-2", className), children: [lockup.markUrl ? (_jsx("img", { src: lockup.markUrl, alt: lockup.name, className: cn("
|
|
66
|
+
return (_jsxs("span", { className: cn("flex items-center gap-2", className), children: [lockup.markUrl ? (_jsx("img", { src: lockup.markUrl, alt: lockup.name, style: lockup.height ? { height: lockup.height } : undefined, className: cn("object-contain",
|
|
67
|
+
// A custom mark must be able to SHRINK. `shrink-0` is right for the
|
|
68
|
+
// 20px default, whose max-widths already fit every header; for a
|
|
69
|
+
// taller one it is the thing that pushes the collapse button out
|
|
70
|
+
// of the sidebar instead of letting the image narrow.
|
|
71
|
+
lockup.height ? "min-w-0 shrink" : "shrink-0",
|
|
35
72
|
// Both lock HEIGHT and let width follow the aspect ratio. Sizing an
|
|
36
73
|
// icon into a square box (`size-5`) silently shrinks any mark that
|
|
37
74
|
// is not 1:1 — `object-contain` letterboxes a 4:3 glyph to 20x15,
|
|
@@ -39,5 +76,16 @@ export function AppBrand({ showName = true, className, iconClassName, labelClass
|
|
|
39
76
|
// replaced. Marks are rarely square, so height is the only
|
|
40
77
|
// dimension worth fixing. The max-width is just a runaway guard:
|
|
41
78
|
// tighter for an icon, which has to leave room for the name.
|
|
42
|
-
|
|
79
|
+
// `branding.logoHeight` overrides the HEIGHT, and the width is
|
|
80
|
+
// bounded by the CONTAINER rather than by a number picked here.
|
|
81
|
+
// The 7rem cap is sized for a 20px mark and would letterbox a
|
|
82
|
+
// taller one; a bigger fixed cap is just a different wrong number
|
|
83
|
+
// — 20rem is 320px while an expanded sidebar is 248px, so a 4:1
|
|
84
|
+
// wordmark at the 4rem ceiling (256px) still overflowed it.
|
|
85
|
+
// `max-w-full` is whatever the header actually has.
|
|
86
|
+
lockup.height
|
|
87
|
+
? "w-auto max-w-full"
|
|
88
|
+
: lockup.isIcon
|
|
89
|
+
? "h-5 w-auto max-w-8"
|
|
90
|
+
: "h-5 w-auto max-w-[7rem]", iconClassName) })) : (_jsx(Layers3, { className: cn("size-5 shrink-0 text-primary", iconClassName) })), lockup.showName && _jsx("span", { className: cn("truncate", labelClassName), children: lockup.name })] }));
|
|
43
91
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* habit across the platform (and the same set the chat agent reads).
|
|
6
6
|
*/
|
|
7
7
|
import type { Row, Table } from "@tanstack/react-table";
|
|
8
|
-
import type
|
|
8
|
+
import { type ReactNode } from "react";
|
|
9
|
+
export declare const SelectionBarSingleLine: import("react").Provider<boolean>;
|
|
9
10
|
export interface DataTableSelectionBarProps<T> {
|
|
10
11
|
table: Table<T>;
|
|
11
12
|
/** Bulk actions for the selected rows. */
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
3
4
|
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
/**
|
|
6
|
+
* Whether the bar has a row to itself or shares one.
|
|
7
|
+
*
|
|
8
|
+
* The toolbar's non-stacked layout puts the bar on the SAME line as the
|
|
9
|
+
* search, in a region that scrolls sideways rather than growing — because a
|
|
10
|
+
* toolbar that gains height on the first tick shoves the table down under
|
|
11
|
+
* the cursor mid-click, which is the whole reason the bar moved into the
|
|
12
|
+
* toolbar. A bar that wraps internally defeats that just as thoroughly as
|
|
13
|
+
* one that wraps externally, and its nested actions container is a second
|
|
14
|
+
* place it can happen (review catch).
|
|
15
|
+
*
|
|
16
|
+
* The container knows the constraint and the bar owns the class names, so
|
|
17
|
+
* the policy travels between them instead of the toolbar reaching in with a
|
|
18
|
+
* descendant selector — which would apply to a host's own `selectionBar`
|
|
19
|
+
* node too, and lose to it on specificity besides.
|
|
20
|
+
*/
|
|
21
|
+
const SelectionBarSingleLineContext = createContext(false);
|
|
22
|
+
export const SelectionBarSingleLine = SelectionBarSingleLineContext.Provider;
|
|
4
23
|
export function DataTableSelectionBar({ table, children, label = "row", plural, className, }) {
|
|
5
24
|
// Every selected row, not only the ones the current filter shows: a
|
|
6
25
|
// selection the user cannot see is still a selection, and hiding the bar
|
|
7
26
|
// would take away the only way to clear it.
|
|
8
27
|
const rows = table.getSelectedRowModel().rows;
|
|
28
|
+
const singleLine = useContext(SelectionBarSingleLineContext);
|
|
9
29
|
if (rows.length === 0)
|
|
10
30
|
return null;
|
|
11
|
-
return (_jsxs("div", { "data-selection-bar": "", role: "status", className: cn("flex min-h-8
|
|
31
|
+
return (_jsxs("div", { "data-selection-bar": "", role: "status", className: cn("flex min-h-8 items-center gap-2 rounded-xl bg-primary/5 px-3 text-sm", singleLine ? "flex-nowrap whitespace-nowrap" : "flex-wrap", className), children: [_jsxs("span", { className: "font-medium", children: [rows.length, " ", rows.length === 1 ? label : (plural ?? `${label}s`), " selected"] }), _jsx("div", { "data-selection-actions": "", className: cn("flex items-center gap-2", singleLine ? "flex-nowrap" : "flex-wrap"), children: typeof children === "function" ? children(rows) : children }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm",
|
|
12
32
|
// `true` = the blank state, not the caller's `initialState`, which
|
|
13
33
|
// would make "Clear" resurrect rows.
|
|
14
34
|
onClick: () => table.resetRowSelection(true), className: "ml-auto h-8 rounded-lg px-2.5", children: ["Clear", _jsx(X, { className: "size-4" })] })] }));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Maximize2, Minimize2, Plus, Search, SlidersHorizontal, X } from "lucide-react";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Button, Input, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
5
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
|
|
6
6
|
import { DataTableFacetedFilter } from "./data-table-faceted-filter.js";
|
|
7
|
+
import { SelectionBarSingleLine } from "./data-table-selection-bar.js";
|
|
7
8
|
import { useDataTableFrame } from "./data-table-frame.js";
|
|
8
9
|
import { DataTableViewOptions } from "./data-table-view-options.js";
|
|
9
10
|
import { columnLabel, normalizeFacetValue } from "./facets.js";
|
|
@@ -75,7 +76,32 @@ export function DataTableToolbar({ table, facets = [], layout = "auto", collapsi
|
|
|
75
76
|
const filtersToggle = canCollapse ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", "data-filters-toggle": "", "aria-expanded": showFilters, onClick: () => setFiltersOpen((current) => !current), className: cn("h-8 rounded-xl border-border/45 bg-background/65 shadow-none hover:bg-muted/26", activeFilters > 0 && "border-primary/30"), children: [_jsx(SlidersHorizontal, { className: "size-4" }), "Filters", activeFilters > 0 ? (_jsxs("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal tabular-nums", children: [_jsx("span", { "aria-hidden": "true", children: activeFilters }), _jsxs("span", { className: "sr-only", children: [activeFilters, " active"] })] })) : null] })) : null;
|
|
76
77
|
const rightGroup = actions || viewOptions || focusButton || filtersToggle ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [filtersToggle, actions, viewOptions ? _jsx(DataTableViewOptions, { table: table }) : null, focusButton] })) : null;
|
|
77
78
|
if (!stacked) {
|
|
78
|
-
return (
|
|
79
|
+
return (
|
|
80
|
+
// A stable hook for tests and hosts, rather than leaving them to
|
|
81
|
+
// match on Tailwind classes — a selector that silently stops
|
|
82
|
+
// matching is how an assertion starts passing for the wrong reason
|
|
83
|
+
// (review catch).
|
|
84
|
+
_jsx("div", { "data-slot": "data-table-toolbar", className: cn("flex flex-col gap-2", className), children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsxs("div", { "data-toolbar-lead": showSelectionBar ? "selection" : "filters", className: cn("flex min-w-0 flex-1 items-center gap-2",
|
|
85
|
+
// Facets genuinely want rows: six of them in a narrow pane
|
|
86
|
+
// should stack rather than scroll off the side. A selection
|
|
87
|
+
// bar is the opposite — one row by definition. Left wrapping,
|
|
88
|
+
// it drops below the search as soon as the search alone is
|
|
89
|
+
// wider than the space, which grows the toolbar again and is
|
|
90
|
+
// the jump this whole change exists to remove (review catch).
|
|
91
|
+
showSelectionBar ? "flex-nowrap" : "flex-wrap"), children: [searchNode, showSelectionBar ? (
|
|
92
|
+
// The bar shares this row with a search that has a width
|
|
93
|
+
// floor and a right group that does not shrink, so in a
|
|
94
|
+
// narrow pane it gets squeezed. It scrolls rather than
|
|
95
|
+
// wrapping inside itself — the row stays exactly one high.
|
|
96
|
+
//
|
|
97
|
+
// And the scrollbar itself stays out of layout. A classic,
|
|
98
|
+
// space-consuming one — Windows and Linux by default, macOS
|
|
99
|
+
// set to "always show" — appears only once the bar overflows,
|
|
100
|
+
// which is only once something is ticked, so its height lands
|
|
101
|
+
// on the toolbar as the same jump by another route (review
|
|
102
|
+
// catch). `[data-selection-row]` in `styles.css` keeps it out
|
|
103
|
+
// of layout; hiding the widget does not hide the scrolling.
|
|
104
|
+
_jsx("div", { "data-selection-row": "", className: "min-w-0 flex-1 overflow-x-auto", children: _jsx(SelectionBarSingleLine, { value: true, children: selectionBar }) })) : (_jsxs(_Fragment, { children: [facetNodes, addFilterNode, children, resetNode] }))] }), rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }) }));
|
|
79
105
|
}
|
|
80
|
-
return (_jsxs("div", { className: cn("flex flex-col gap-2", className), children: [_jsxs("div", { className: "flex items-center gap-2", children: [searchNode, rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }), showSelectionBar ? (_jsx("div", { "data-selection-row": "", children: selectionBar })) : showFilters ? (_jsxs("div", { "data-filter-row": "", className: "flex flex-wrap items-center gap-2", children: [facetNodes, addFilterNode, children, resetNode] })) : null] }));
|
|
106
|
+
return (_jsxs("div", { "data-slot": "data-table-toolbar", className: cn("flex flex-col gap-2", className), children: [_jsxs("div", { className: "flex items-center gap-2", children: [searchNode, rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }), showSelectionBar ? (_jsx("div", { "data-selection-row": "", children: selectionBar })) : showFilters ? (_jsxs("div", { "data-filter-row": "", className: "flex flex-wrap items-center gap-2", children: [facetNodes, addFilterNode, children, resetNode] })) : null] }));
|
|
81
107
|
}
|
|
@@ -211,7 +211,7 @@ function statusIconClass(tone) {
|
|
|
211
211
|
return "text-primary";
|
|
212
212
|
case "neutral":
|
|
213
213
|
default:
|
|
214
|
-
return "text-sidebar-
|
|
214
|
+
return "text-sidebar-icon";
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
function NavStatusIcon({ statusIcon }) {
|
|
@@ -481,7 +481,7 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
|
|
|
481
481
|
? "text-sidebar-foreground/45 cursor-default"
|
|
482
482
|
: isActive
|
|
483
483
|
? ACTIVE_NAV_TEXT_CLASS
|
|
484
|
-
: "text-sidebar-foreground/80"), children: [_jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-
|
|
484
|
+
: "text-sidebar-foreground/80"), children: [_jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
485
485
|
? ACTIVE_NAV_BADGE_CLASS
|
|
486
486
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
|
|
487
487
|
}
|
|
@@ -492,7 +492,7 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
|
|
|
492
492
|
!isDisabled &&
|
|
493
493
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
494
494
|
!isDisabled &&
|
|
495
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-
|
|
495
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-icon") }), _jsx("span", { className: cn("truncate", isActive && !isDisabled && ACTIVE_NAV_TEXT_CLASS), children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] }));
|
|
496
496
|
}
|
|
497
497
|
function SubNavItems({ items, parentPath, depth = 1, }) {
|
|
498
498
|
const navigate = useNavigate();
|
|
@@ -586,7 +586,7 @@ function NestedFolderItem({ item, depth }) {
|
|
|
586
586
|
? ACTIVE_NAV_ICON_CLASS
|
|
587
587
|
: isAncestorActive
|
|
588
588
|
? "text-sidebar-foreground/80"
|
|
589
|
-
: "text-sidebar-
|
|
589
|
+
: "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
590
590
|
? ACTIVE_NAV_BADGE_CLASS
|
|
591
591
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }), isExpanded && item.children && (_jsxs("div", { children: [item.children.map((child) => {
|
|
592
592
|
const hasNestedChildren = child.children && child.children.length > 0;
|
|
@@ -660,7 +660,7 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
660
660
|
? ACTIVE_NAV_ICON_CLASS
|
|
661
661
|
: ancestor
|
|
662
662
|
? "text-sidebar-foreground/80"
|
|
663
|
-
: "text-sidebar-
|
|
663
|
+
: "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", selected
|
|
664
664
|
? ACTIVE_NAV_BADGE_CLASS
|
|
665
665
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
|
|
666
666
|
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), children: [inner(isActive, isAncestorActive), hasChildren && isExpanded && (_jsxs(_Fragment, { children: [_jsx(SubNavItems, { items: item.children, parentPath: item.to }), item.dnd?.canDrop() && _jsx(ContainerDropZone, { dnd: item.dnd })] }))] }));
|
|
@@ -679,7 +679,7 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
679
679
|
"rounded-xl border border-transparent pr-3 py-2 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70", isActive && cn(ACTIVE_NAV_ROW_CLASS, ACTIVE_NAV_TEXT_CLASS), isDragOver &&
|
|
680
680
|
!isFileDragOver &&
|
|
681
681
|
"bg-sidebar-selected ring-2 ring-inset ring-sidebar-ring/50", isFileDragOver &&
|
|
682
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: inTree, label: item.label, isActive: isActive, depth: 0 }), _jsx(item.icon, { className: cn("size-4 shrink-0", isActive ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-
|
|
682
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: inTree, label: item.label, isActive: isActive, depth: 0 }), _jsx(item.icon, { className: cn("size-4 shrink-0", isActive ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
683
683
|
? ACTIVE_NAV_BADGE_CLASS
|
|
684
684
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] })) }));
|
|
685
685
|
}
|
|
@@ -743,7 +743,7 @@ function SidebarResizeHandle() {
|
|
|
743
743
|
// group is now the source of truth for "you're in a chat right now".
|
|
744
744
|
// Kept as a comment so future spelunkers know it was intentional.
|
|
745
745
|
function CollapsedNavShortcut({ to, label, icon: Icon, isActive, }) {
|
|
746
|
-
return (_jsx(NavLink, { to: to, children: _jsx(TooltipIconButton, { tooltip: label, side: "right", className: cn(isActive && "border border-transparent bg-sidebar-selected text-sidebar-foreground"), children: _jsx(Icon, { className: cn("size-4", isActive
|
|
746
|
+
return (_jsx(NavLink, { to: to, children: _jsx(TooltipIconButton, { tooltip: label, side: "right", className: cn(isActive && "border border-transparent bg-sidebar-selected text-sidebar-foreground"), children: _jsx(Icon, { className: cn("size-4", isActive ? "text-primary" : "text-sidebar-icon") }) }) }, to));
|
|
747
747
|
}
|
|
748
748
|
// ---------------------------------------------------------------------------
|
|
749
749
|
// Collapsible nav-group state — persisted per-label in localStorage so the
|
|
@@ -946,7 +946,7 @@ function SidebarContent({ collapsed }) {
|
|
|
946
946
|
e.preventDefault();
|
|
947
947
|
}, []);
|
|
948
948
|
if (collapsed) {
|
|
949
|
-
return (_jsxs("div", { className: "flex flex-col items-center gap-1 py-3 flex-1", children: [_jsx(TooltipIconButton, { tooltip: "Expand sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeftOpen, { className: "size-5" }) }), _jsx(TooltipIconButton, { tooltip: "New Thread", side: "right", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4" }) }), _jsx("div", { className: "mt-4 flex w-full flex-col items-stretch gap-1", children: navConfig
|
|
949
|
+
return (_jsxs("div", { className: "flex flex-col items-center gap-1 py-3 flex-1", children: [_jsx(TooltipIconButton, { tooltip: "Expand sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeftOpen, { className: "size-5" }) }), _jsx(TooltipIconButton, { tooltip: "New Thread", side: "right", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-sidebar-icon" }) }), _jsx("div", { className: "mt-4 flex w-full flex-col items-stretch gap-1", children: navConfig
|
|
950
950
|
.map((group, groupIndex) => {
|
|
951
951
|
// Modules with a non-flat collapsed rail (e.g. Spaces'
|
|
952
952
|
// "header + active workspace") supply `collapsedRail`; the
|
|
@@ -980,7 +980,7 @@ function SidebarContent({ collapsed }) {
|
|
|
980
980
|
// column so icons stay aligned with the rail.
|
|
981
981
|
_jsx("div", { className: cn("flex flex-col items-center gap-1", visibleIndex > 0 && "mt-2 pt-2 border-t border-sidebar-border/40"), children: items.map(({ to, label, icon, isActive }) => (_jsx(CollapsedNavShortcut, { to: to, label: label, icon: icon, isActive: isActive }, to))) }, `${group.label || ""}-${groupIndex}`))) })] }));
|
|
982
982
|
}
|
|
983
|
-
return (_jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsx(NavLink, { to: "/", className: "flex items-center gap-2 hover:opacity-80 transition-opacity", children: _jsx(AppBrand, { iconClassName: "text-primary", labelClassName: "truncate text-sm font-semibold text-sidebar-foreground" }) }), _jsx(TooltipIconButton, { tooltip: "Collapse sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeft, { className: "size-5" }) })] }), _jsx("div", { className: "px-1 mb-0.5", children: _jsxs("button", { type: "button", onClick: handleNewConversation, className: cn("flex items-center gap-2 w-full", "rounded-xl px-3 py-2 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70"), children: [_jsx(SquarePen, { className: "size-4 shrink-0 text-sidebar-
|
|
983
|
+
return (_jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsx(NavLink, { to: "/", className: "flex items-center gap-2 hover:opacity-80 transition-opacity", children: _jsx(AppBrand, { iconClassName: "text-primary", labelClassName: "truncate text-sm font-semibold text-sidebar-foreground" }) }), _jsx(TooltipIconButton, { tooltip: "Collapse sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeft, { className: "size-5" }) })] }), _jsx("div", { className: "px-1 mb-0.5", children: _jsxs("button", { type: "button", onClick: handleNewConversation, className: cn("flex items-center gap-2 w-full", "rounded-xl px-3 py-2 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70"), children: [_jsx(SquarePen, { className: "size-4 shrink-0 text-sidebar-icon" }), "New Thread"] }) }), _jsxs("nav", { className: "flex-1 overflow-y-auto px-1 pb-16", onDragOverCapture: preventNativeFileDrop, onDropCapture: preventNativeFileDrop, children: [navConfig.map((group) => (_jsx(NavGroupSection, { group: group }, group.label))), showNavSkeleton && (_jsx("div", { className: "space-y-1 px-1 pt-2", "aria-hidden": "true", children: Array.from({ length: 6 }).map((_, index) => (_jsx("div", { className: "h-7 mx-1 rounded-lg animate-pulse bg-sidebar-accent/40" }, index))) }))] }), _jsx(SidebarFooterActions, {})] }));
|
|
984
984
|
}
|
|
985
985
|
function SidebarFooterActions() {
|
|
986
986
|
const footerActions = useNavStore((s) => s.footerActions);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { resolveThemeRuntime, } from "../lib/theme-runtime.js";
|
|
4
4
|
const ThemeRuntimeContext = createContext(null);
|
|
5
|
+
/** True inside a ThemeScope — the outermost one owns the document. */
|
|
6
|
+
const ThemeScopeNestedContext = createContext(false);
|
|
5
7
|
export function ThemeRuntimeProvider({ layers, mode, children, }) {
|
|
6
8
|
const fallback = useRef(resolveThemeRuntime([], "system"));
|
|
7
9
|
const [systemPrefersDark, setSystemPrefersDark] = useState(() => typeof window !== "undefined"
|
|
@@ -33,9 +35,149 @@ export function ThemeRuntimeProvider({ layers, mode, children, }) {
|
|
|
33
35
|
export function useThemeRuntime() {
|
|
34
36
|
return useContext(ThemeRuntimeContext) ?? resolveThemeRuntime([], "system");
|
|
35
37
|
}
|
|
38
|
+
const rootThemeStack = [];
|
|
39
|
+
const rootThemeBase = new Map();
|
|
40
|
+
// What this module last put on the element, per name. Anything else there
|
|
41
|
+
// now came from outside, and outside wins — see `applyTopRootTheme`. Value
|
|
42
|
+
// AND priority: a host re-declaring the same value as `!important` has said
|
|
43
|
+
// something new about that name, and comparing values alone would read it as
|
|
44
|
+
// our own write and quietly drop the flag (review catch).
|
|
45
|
+
const rootThemeWritten = new Map();
|
|
46
|
+
function currentlyOurs(root, name) {
|
|
47
|
+
const written = rootThemeWritten.get(name);
|
|
48
|
+
if (written === undefined)
|
|
49
|
+
return true;
|
|
50
|
+
return (root.style.getPropertyValue(name) === written.value
|
|
51
|
+
&& root.style.getPropertyPriority(name) === written.priority);
|
|
52
|
+
}
|
|
53
|
+
function rememberWrite(root, name) {
|
|
54
|
+
// Read back rather than storing what we passed: the browser normalises the
|
|
55
|
+
// value, and a comparison against the un-normalised string would read
|
|
56
|
+
// every one of our own writes as somebody else's.
|
|
57
|
+
rootThemeWritten.set(name, {
|
|
58
|
+
value: root.style.getPropertyValue(name),
|
|
59
|
+
priority: root.style.getPropertyPriority(name),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function applyTopRootTheme(root) {
|
|
63
|
+
const top = rootThemeStack[rootThemeStack.length - 1];
|
|
64
|
+
for (const [name, base] of rootThemeBase) {
|
|
65
|
+
if (!currentlyOurs(root, name)) {
|
|
66
|
+
// Someone outside this module wrote this name after we did — a host
|
|
67
|
+
// theme manager, a design-mode preview, a test. Their write is newer
|
|
68
|
+
// than anything we have to say about it, so we neither overwrite it
|
|
69
|
+
// now nor restore over it later: the name is theirs from here
|
|
70
|
+
// (review catch).
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const next = top?.vars[name];
|
|
74
|
+
if (next !== undefined) {
|
|
75
|
+
root.style.setProperty(name, String(next));
|
|
76
|
+
}
|
|
77
|
+
else if (base.value) {
|
|
78
|
+
// Value AND priority: a host token declared `!important` keeps its
|
|
79
|
+
// flag, or anything else can start overriding a theme it owns.
|
|
80
|
+
root.style.setProperty(name, base.value, base.priority);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
root.style.removeProperty(name);
|
|
84
|
+
rootThemeWritten.delete(name);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
rememberWrite(root, name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Publish `vars` on the document for `id`, claiming a place in the stack the
|
|
92
|
+
* first time and KEEPING it afterwards.
|
|
93
|
+
*
|
|
94
|
+
* Keeping it is the point: a scope whose theme merely changes — a rerender
|
|
95
|
+
* that hands back a new `variables` object, even a semantically identical
|
|
96
|
+
* one from an inline `layers` array — must not jump over a scope that
|
|
97
|
+
* mounted after it. Re-registering on every dependency change would make
|
|
98
|
+
* the newest write win instead of the newest owner (review catch).
|
|
99
|
+
*/
|
|
100
|
+
function writeRootTheme(root, id, vars) {
|
|
101
|
+
for (const name of Object.keys(vars)) {
|
|
102
|
+
if (!rootThemeBase.has(name)) {
|
|
103
|
+
rootThemeBase.set(name, {
|
|
104
|
+
value: root.style.getPropertyValue(name),
|
|
105
|
+
priority: root.style.getPropertyPriority(name),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const existing = rootThemeStack.find((write) => write.id === id);
|
|
110
|
+
if (existing)
|
|
111
|
+
existing.vars = vars;
|
|
112
|
+
else
|
|
113
|
+
rootThemeStack.push({ id, vars });
|
|
114
|
+
applyTopRootTheme(root);
|
|
115
|
+
}
|
|
116
|
+
/** Give up `id`'s place, wherever in the stack it sits. */
|
|
117
|
+
function releaseRootTheme(root, id) {
|
|
118
|
+
const at = rootThemeStack.findIndex((write) => write.id === id);
|
|
119
|
+
if (at >= 0)
|
|
120
|
+
rootThemeStack.splice(at, 1);
|
|
121
|
+
applyTopRootTheme(root);
|
|
122
|
+
if (rootThemeStack.length === 0) {
|
|
123
|
+
rootThemeBase.clear();
|
|
124
|
+
rootThemeWritten.clear();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
36
127
|
export function ThemeScope({ children, className, style, }) {
|
|
37
128
|
const runtime = useThemeRuntime();
|
|
38
|
-
|
|
129
|
+
const nested = useContext(ThemeScopeNestedContext);
|
|
130
|
+
// Variables on this div alone reach only what renders INSIDE it. Radix
|
|
131
|
+
// portals mount into document.body, outside it, so every dropdown,
|
|
132
|
+
// popover, select and tooltip fell back to :root — the base preset —
|
|
133
|
+
// while the page around them wore the customer's theme. `dialog` papered
|
|
134
|
+
// over its own case with a nested ThemeScope; that fixes one portal and
|
|
135
|
+
// leaves the next one to rediscover the bug.
|
|
136
|
+
//
|
|
137
|
+
// The outermost scope publishes to the document element instead, so
|
|
138
|
+
// anything portalled anywhere inherits. Nested scopes keep the div only:
|
|
139
|
+
// they exist to theme a subtree differently, and must not overwrite the
|
|
140
|
+
// document with their own values.
|
|
141
|
+
//
|
|
142
|
+
// Residual, stated because it is a real limit and not an oversight: a
|
|
143
|
+
// NESTED scope's own portalled content still resolves the document's
|
|
144
|
+
// (outermost) theme, because the portal escapes the nested div and the
|
|
145
|
+
// nested scope deliberately does not publish. That is strictly better
|
|
146
|
+
// than before — such content used to get the base `:root` preset rather
|
|
147
|
+
// than any theme at all — and the remedy already exists and is used:
|
|
148
|
+
// wrap the portalled content in a `ThemeScope`, as `ui/dialog.tsx` does
|
|
149
|
+
// with `display: contents`. Context reaches through a portal, so the
|
|
150
|
+
// nested scope's variables land on the portalled subtree (review catch).
|
|
151
|
+
// BEFORE paint, not after. A portal that exists on the very first render —
|
|
152
|
+
// a default-open dialog, an overlay restored during hydration — would
|
|
153
|
+
// otherwise paint once against the base `:root` preset and then correct
|
|
154
|
+
// itself: the mismatched-theme flash this exists to remove, in miniature
|
|
155
|
+
// (review catch).
|
|
156
|
+
// The identity of THIS scope, stable for its whole life. Its place in the
|
|
157
|
+
// stack is claimed once and released once; changing its theme in between
|
|
158
|
+
// must not re-order it (review catch).
|
|
159
|
+
const idRef = useRef(undefined);
|
|
160
|
+
idRef.current ?? (idRef.current = Symbol("theme-scope"));
|
|
161
|
+
useLayoutEffect(() => {
|
|
162
|
+
if (nested || typeof document === "undefined")
|
|
163
|
+
return;
|
|
164
|
+
const root = document.documentElement;
|
|
165
|
+
const id = idRef.current;
|
|
166
|
+
return () => releaseRootTheme(root, id);
|
|
167
|
+
}, [nested]);
|
|
168
|
+
useLayoutEffect(() => {
|
|
169
|
+
if (nested || typeof document === "undefined")
|
|
170
|
+
return;
|
|
171
|
+
// `font-family` rides along as one more name: `<html>` needs it set,
|
|
172
|
+
// not just `--font-body` defined, or portalled content keeps the host's
|
|
173
|
+
// font (review catch).
|
|
174
|
+
const vars = {
|
|
175
|
+
...runtime.variables,
|
|
176
|
+
"font-family": "var(--font-body)",
|
|
177
|
+
};
|
|
178
|
+
writeRootTheme(document.documentElement, idRef.current, vars);
|
|
179
|
+
}, [nested, runtime.variables]);
|
|
180
|
+
return (_jsx(ThemeScopeNestedContext.Provider, { value: true, children: _jsx("div", { className: className, style: { ...runtime.variables, fontFamily: "var(--font-body)", ...style }, children: children }) }));
|
|
39
181
|
}
|
|
40
182
|
export function ThemeDocumentMetadata() {
|
|
41
183
|
const runtime = useThemeRuntime();
|
|
@@ -30,6 +30,16 @@ export interface ThemeSidebarSlots {
|
|
|
30
30
|
sidebarAccentForeground?: string;
|
|
31
31
|
sidebarSelected?: string;
|
|
32
32
|
sidebarSelectedForeground?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Colour of the icons in the sidebar's idle rows.
|
|
35
|
+
*
|
|
36
|
+
* Separate from `sidebarForeground` because a brand whose mark is a colour
|
|
37
|
+
* usually wants the icons in that colour and the LABELS still readable —
|
|
38
|
+
* tinting `sidebarForeground` to reach the icons drags every label with it.
|
|
39
|
+
* Unset resolves to the previous behaviour (the label colour at 60%), so no
|
|
40
|
+
* existing theme moves.
|
|
41
|
+
*/
|
|
42
|
+
sidebarIcon?: string;
|
|
33
43
|
sidebarBorder?: string;
|
|
34
44
|
sidebarRing?: string;
|
|
35
45
|
}
|
|
@@ -99,6 +109,21 @@ export interface ThemeBranding {
|
|
|
99
109
|
*/
|
|
100
110
|
iconUrl?: string;
|
|
101
111
|
darkIconUrl?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Rendered height of the mark, as a CSS length (`"2rem"`, `"32px"`).
|
|
114
|
+
* Defaults to the 20px lockup height when unset.
|
|
115
|
+
*
|
|
116
|
+
* A wordmark is not interchangeable with an icon at one fixed height. Marks
|
|
117
|
+
* that carry a tagline under the name — a common wordmark shape — turn the
|
|
118
|
+
* tagline into an unreadable smear at 20px, and the whole lockup reads as
|
|
119
|
+
* undersized next to the product name. The tenant supplying the asset is the
|
|
120
|
+
* only one who knows how much height it needs, so this is theirs to set.
|
|
121
|
+
*
|
|
122
|
+
* Validated, not interpolated blindly: the value reaches an inline style and
|
|
123
|
+
* themes are tenant-authored content, so anything that is not a plain
|
|
124
|
+
* non-negative CSS length in px/rem/em is ignored in favour of the default.
|
|
125
|
+
*/
|
|
126
|
+
logoHeight?: string;
|
|
102
127
|
}
|
|
103
128
|
export interface ThemeDefinition {
|
|
104
129
|
light?: ThemeColorSlots;
|
|
@@ -28,6 +28,7 @@ const SIDEBAR_KEYS = [
|
|
|
28
28
|
"sidebarAccentForeground",
|
|
29
29
|
"sidebarSelected",
|
|
30
30
|
"sidebarSelectedForeground",
|
|
31
|
+
"sidebarIcon",
|
|
31
32
|
"sidebarBorder",
|
|
32
33
|
"sidebarRing",
|
|
33
34
|
];
|
|
@@ -84,6 +85,7 @@ const BRANDING_KEYS = [
|
|
|
84
85
|
"darkLogoUrl",
|
|
85
86
|
"iconUrl",
|
|
86
87
|
"darkIconUrl",
|
|
88
|
+
"logoHeight",
|
|
87
89
|
];
|
|
88
90
|
function cleanObject(value, keys) {
|
|
89
91
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -188,6 +190,7 @@ function toCssVars(colors, sidebar, status, charts, typography, radius, app) {
|
|
|
188
190
|
["--sidebar-accent-foreground"]: sidebar.sidebarAccentForeground,
|
|
189
191
|
["--sidebar-selected"]: sidebar.sidebarSelected,
|
|
190
192
|
["--sidebar-selected-foreground"]: sidebar.sidebarSelectedForeground,
|
|
193
|
+
["--sidebar-icon"]: sidebar.sidebarIcon,
|
|
191
194
|
["--sidebar-border"]: sidebar.sidebarBorder,
|
|
192
195
|
["--sidebar-ring"]: sidebar.sidebarRing,
|
|
193
196
|
["--success"]: status.success,
|
|
@@ -248,6 +251,7 @@ function defaultSidebar(colors) {
|
|
|
248
251
|
sidebarAccentForeground: colors.accentForeground,
|
|
249
252
|
sidebarSelected: colors.muted,
|
|
250
253
|
sidebarSelectedForeground: colors.foreground,
|
|
254
|
+
sidebarIcon: `color-mix(in srgb, ${colors.foreground} 60%, transparent)`,
|
|
251
255
|
sidebarBorder: colors.border,
|
|
252
256
|
sidebarRing: colors.ring,
|
|
253
257
|
};
|
|
@@ -376,6 +380,7 @@ export const FOUNDRY_VIOLET_THEME = {
|
|
|
376
380
|
sidebarAccentForeground: "color-mix(in srgb, var(--foreground) 92%, var(--primary) 8%)",
|
|
377
381
|
sidebarSelected: "var(--muted)",
|
|
378
382
|
sidebarSelectedForeground: "var(--foreground)",
|
|
383
|
+
sidebarIcon: "color-mix(in srgb, var(--sidebar-foreground) 60%, transparent)",
|
|
379
384
|
sidebarBorder: "color-mix(in srgb, var(--border) 96%, var(--foreground) 4%)",
|
|
380
385
|
sidebarRing: "oklch(43% 0.24 286)",
|
|
381
386
|
},
|
|
@@ -388,6 +393,7 @@ export const FOUNDRY_VIOLET_THEME = {
|
|
|
388
393
|
sidebarAccentForeground: "color-mix(in srgb, var(--foreground) 90%, white 10%)",
|
|
389
394
|
sidebarSelected: "color-mix(in srgb, var(--muted) 78%, var(--background) 22%)",
|
|
390
395
|
sidebarSelectedForeground: "var(--foreground)",
|
|
396
|
+
sidebarIcon: "color-mix(in srgb, var(--sidebar-foreground) 60%, transparent)",
|
|
391
397
|
sidebarBorder: "color-mix(in srgb, var(--border) 90%, white 10%)",
|
|
392
398
|
sidebarRing: "oklch(0.72 0.18 286)",
|
|
393
399
|
},
|
package/dist/styles.css
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
34
34
|
--color-sidebar-selected: var(--sidebar-selected);
|
|
35
35
|
--color-sidebar-selected-foreground: var(--sidebar-selected-foreground);
|
|
36
|
+
--color-sidebar-icon: var(--sidebar-icon);
|
|
36
37
|
--color-sidebar-border: var(--sidebar-border);
|
|
37
38
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
38
39
|
--color-success: var(--success);
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
78
79
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
79
80
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
81
|
+
--sidebar-icon: color-mix(in srgb, var(--sidebar-foreground) 60%, transparent);
|
|
80
82
|
--sidebar-selected: var(--muted);
|
|
81
83
|
--sidebar-selected-foreground: var(--foreground);
|
|
82
84
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
@@ -122,6 +124,7 @@
|
|
|
122
124
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
123
125
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
124
126
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
127
|
+
--sidebar-icon: color-mix(in srgb, var(--sidebar-foreground) 60%, transparent);
|
|
125
128
|
--sidebar-selected: color-mix(in srgb, var(--muted) 78%, var(--background) 22%);
|
|
126
129
|
--sidebar-selected-foreground: var(--foreground);
|
|
127
130
|
--sidebar-border: oklch(1 0 0 / 10%);
|
|
@@ -184,6 +187,27 @@
|
|
|
184
187
|
box-shadow: 6px 0 6px -6px color-mix(in srgb, var(--foreground) 12%, transparent);
|
|
185
188
|
}
|
|
186
189
|
|
|
190
|
+
/* The opposite call from the list scroller below, for the opposite reason.
|
|
191
|
+
* The toolbar's selection row shares a line with the search and the actions,
|
|
192
|
+
* and its entire contract is that ticking a row never changes the toolbar's
|
|
193
|
+
* height. A classic, space-consuming scrollbar — Windows and Linux by
|
|
194
|
+
* default, macOS set to "always show" — would appear exactly when the bar
|
|
195
|
+
* overflows, which is exactly when something is ticked, and its height would
|
|
196
|
+
* land on the toolbar as the same jump by another route (review catch).
|
|
197
|
+
*
|
|
198
|
+
* So the widget is hidden, not the scrolling: wheel, trackpad, and tabbing
|
|
199
|
+
* to a button past the edge all still reach it. Plain CSS rather than
|
|
200
|
+
* utilities, because a class that a consumer's Tailwind never scans fails
|
|
201
|
+
* silently, and this one has no visible symptom until the platform is one we
|
|
202
|
+
* do not develop on. */
|
|
203
|
+
[data-selection-row] {
|
|
204
|
+
scrollbar-width: none;
|
|
205
|
+
}
|
|
206
|
+
[data-selection-row]::-webkit-scrollbar {
|
|
207
|
+
height: 0;
|
|
208
|
+
display: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
187
211
|
/* A list you can scroll should say so: keep its scrollbars visible rather
|
|
188
212
|
* than relying on the overlay ones macOS hides until you already scroll. */
|
|
189
213
|
[data-list-scroller] {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
|
|
6
6
|
"keywords": [
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"react-markdown": "^10.0.0",
|
|
72
72
|
"remark-gfm": "^4.0.0",
|
|
73
73
|
"tailwind-merge": "^3.5.0",
|
|
74
|
-
"@iloveagents/foundry-agent": "^0.
|
|
75
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
74
|
+
"@iloveagents/foundry-agent": "^0.22.0",
|
|
75
|
+
"@iloveagents/foundry-web-primitives": "^0.22.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@ag-ui/client": "^0.0.52",
|