Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53c40a61ba |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -3,6 +3,22 @@
|
|||||||
All notable changes to the independently versioned Toolbox SDK packages are
|
All notable changes to the independently versioned Toolbox SDK packages are
|
||||||
recorded here. The packages currently share one release version.
|
recorded here. The packages currently share one release version.
|
||||||
|
|
||||||
|
## 0.2.2 — 2026-07-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `ToolboxPersonalizePanel` for the portal-style personalization heading,
|
||||||
|
storage warning, appearance picker, and portal extension content.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The invariant top row now fixes the Toolbox brand/home at the far left and
|
||||||
|
Help, source, Apps, and Personalize at the far right in that order while the
|
||||||
|
text identity remains geometrically centered.
|
||||||
|
- Default app personalization now matches the central portal's panel styling;
|
||||||
|
portal-only catalogue counts and import/export actions remain extensible
|
||||||
|
content.
|
||||||
|
|
||||||
## 0.2.1 — 2026-07-23
|
## 0.2.1 — 2026-07-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -5,7 +5,7 @@ tools. An app always works by itself. When it receives a trusted same-origin
|
|||||||
catalog URL, the same app gains a toolbox home link and an app switcher without
|
catalog URL, the same app gains a toolbox home link and an app switcher without
|
||||||
becoming coupled to a portal or client-side router.
|
becoming coupled to a portal or client-side router.
|
||||||
|
|
||||||
Version `0.2.1` contains three publish-ready packages:
|
Version `0.2.2` contains three publish-ready packages:
|
||||||
|
|
||||||
- `@add-ideas/toolbox-contract` — types, strict v1 runtime parsing, context
|
- `@add-ideas/toolbox-contract` — types, strict v1 runtime parsing, context
|
||||||
discovery/loading, resolved URLs, and contextual link helpers.
|
discovery/loading, resolved URLs, and contextual link helpers.
|
||||||
@@ -167,11 +167,11 @@ import "@add-ideas/toolbox-shell-react/styles.css";
|
|||||||
</AppShell>;
|
</AppShell>;
|
||||||
```
|
```
|
||||||
|
|
||||||
The shell's fixed top row renders Personalize, Apps, source, and Help controls
|
The shell's fixed top row renders the Toolbox brand/home at the far left, a
|
||||||
in that order, a geometrically centered text identity, and the Toolbox
|
geometrically centered text identity, and Help, source, Apps, and Personalize
|
||||||
brand/home at the far right. Personalize and Apps have labeled desktop controls
|
controls at the far right in that order. Personalize and Apps have labeled
|
||||||
and retain their accessible names when compact. The mutually exclusive popovers
|
desktop controls and retain their accessible names when compact. The mutually
|
||||||
close on Escape, outside interaction, and link selection.
|
exclusive popovers close on Escape, outside interaction, and link selection.
|
||||||
|
|
||||||
`AppShell` moves version, derived privacy facts, manifest actions, and legacy
|
`AppShell` moves version, derived privacy facts, manifest actions, and legacy
|
||||||
`appActions` into its standard footer, keeping the top row stable in connected
|
`appActions` into its standard footer, keeping the top row stable in connected
|
||||||
@@ -183,7 +183,9 @@ optional observability.
|
|||||||
|
|
||||||
For a custom portal-style header, `ToolboxHeader` accepts `personalizeContent`
|
For a custom portal-style header, `ToolboxHeader` accepts `personalizeContent`
|
||||||
for content hosted inside the shared controlled popover and `brandIconUrl` for
|
for content hosted inside the shared controlled popover and `brandIconUrl` for
|
||||||
the Toolbox mark. `onPersonalize` remains available for callback-only
|
the Toolbox mark. `ToolboxPersonalizePanel` provides the shared portal-style
|
||||||
|
heading, storage warning, and appearance picker, with a child slot for
|
||||||
|
portal-specific controls. `onPersonalize` remains available for callback-only
|
||||||
integrations. The deprecated `iconUrl`, `leadingActions`, and `metadata` props
|
integrations. The deprecated `iconUrl`, `leadingActions`, and `metadata` props
|
||||||
are retained for source compatibility; the centered app icon is intentionally no
|
are retained for source compatibility; the centered app icon is intentionally no
|
||||||
longer rendered.
|
longer rendered.
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@add-ideas/toolbox-sdk-workspace",
|
"name": "@add-ideas/toolbox-sdk-workspace",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@add-ideas/toolbox-sdk-workspace",
|
"name": "@add-ideas/toolbox-sdk-workspace",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -3946,7 +3946,7 @@
|
|||||||
},
|
},
|
||||||
"packages/contract": {
|
"packages/contract": {
|
||||||
"name": "@add-ideas/toolbox-contract",
|
"name": "@add-ideas/toolbox-contract",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
@@ -3954,10 +3954,10 @@
|
|||||||
},
|
},
|
||||||
"packages/shell-react": {
|
"packages/shell-react": {
|
||||||
"name": "@add-ideas/toolbox-shell-react",
|
"name": "@add-ideas/toolbox-shell-react",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.1"
|
"@add-ideas/toolbox-contract": "0.2.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18 <20",
|
"react": ">=18 <20",
|
||||||
@@ -3966,10 +3966,10 @@
|
|||||||
},
|
},
|
||||||
"packages/testkit": {
|
"packages/testkit": {
|
||||||
"name": "@add-ideas/toolbox-testkit",
|
"name": "@add-ideas/toolbox-testkit",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.1"
|
"@add-ideas/toolbox-contract": "0.2.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"toolbox-check": "dist/cli.js"
|
"toolbox-check": "dist/cli.js"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@add-ideas/toolbox-sdk-workspace",
|
"name": "@add-ideas/toolbox-sdk-workspace",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A small, framework-neutral toolbox contract with a React application shell.",
|
"description": "A small, framework-neutral toolbox contract with a React application shell.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@add-ideas/toolbox-contract",
|
"name": "@add-ideas/toolbox-contract",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"description": "Runtime-validated manifests, catalogs, discovery, and URL helpers for toolbox applications.",
|
"description": "Runtime-validated manifests, catalogs, discovery, and URL helpers for toolbox applications.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -13,14 +13,17 @@ import "@add-ideas/toolbox-shell-react/styles.css";
|
|||||||
standalone mode if context is absent, unavailable, cross-origin, or invalid. Its
|
standalone mode if context is absent, unavailable, cross-origin, or invalid. Its
|
||||||
app switcher is exposed as the accessible navigation landmark
|
app switcher is exposed as the accessible navigation landmark
|
||||||
`Toolbox applications` and all destinations use ordinary document navigation.
|
`Toolbox applications` and all destinations use ordinary document navigation.
|
||||||
The fixed top row exposes Personalize, Apps, Gitea source, and Help in that
|
The fixed top row exposes a Toolbox home/brand block at the far left, a centered
|
||||||
order, a centered title/subtitle, and a Toolbox home/brand block. Personalize
|
title/subtitle, and Help, Gitea source, Apps, and Personalize at the far right
|
||||||
offers light/dark/system mode by default. Version, privacy, and application
|
in that order. Personalize uses the portal-style panel and offers
|
||||||
actions render in the standard footer so connected and standalone headers keep
|
light/dark/system mode by default. Version, privacy, and application actions
|
||||||
the same geometry.
|
render in the standard footer so connected and standalone headers keep the same
|
||||||
|
geometry.
|
||||||
|
|
||||||
`ToolboxHeader` is exported separately for custom shells. Its
|
`ToolboxHeader` is exported separately for custom shells. Its
|
||||||
`personalizeContent` prop puts custom preferences inside the shared controlled
|
`personalizeContent` prop puts custom preferences inside the shared controlled
|
||||||
popover, while `brandIconUrl` supplies a portal favicon; an inline Toolbox cube
|
popover, while `brandIconUrl` supplies a portal favicon; an inline Toolbox cube
|
||||||
is the fallback. Personalize and Apps popovers are mutually exclusive and close
|
is the fallback. `ToolboxPersonalizePanel` shares the portal heading, optional
|
||||||
on Escape, outside interaction, or link selection.
|
storage warning, and appearance picker with custom preference content.
|
||||||
|
Personalize and Apps popovers are mutually exclusive and close on Escape,
|
||||||
|
outside interaction, or link selection.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@add-ideas/toolbox-shell-react",
|
"name": "@add-ideas/toolbox-shell-react",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"description": "A lightweight React application shell for toolbox-compatible browser applications.",
|
"description": "A lightweight React application shell for toolbox-compatible browser applications.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"react-dom": ">=18 <20"
|
"react-dom": ">=18 <20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.1"
|
"@add-ideas/toolbox-contract": "0.2.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');fs.copyFileSync('src/styles.css','dist/styles.css');fs.copyFileSync('../../LICENSE','LICENSE')\"",
|
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');fs.copyFileSync('src/styles.css','dist/styles.css');fs.copyFileSync('../../LICENSE','LICENSE')\"",
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ export interface ToolboxHeaderProps {
|
|||||||
className?: string | undefined;
|
className?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolboxPersonalizePanelProps {
|
||||||
|
theme: ToolboxThemeMode;
|
||||||
|
onThemeChange: (theme: ToolboxThemeMode) => void;
|
||||||
|
storageAvailable?: boolean | undefined;
|
||||||
|
children?: ReactNode | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
type OpenPopover = "personalize" | "apps" | null;
|
type OpenPopover = "personalize" | "apps" | null;
|
||||||
|
|
||||||
function HelpIcon() {
|
function HelpIcon() {
|
||||||
@@ -117,6 +124,57 @@ function ToolboxBrandMark({ iconUrl }: { iconUrl?: string | undefined }) {
|
|||||||
|
|
||||||
const THEMES: readonly ToolboxThemeMode[] = ["system", "light", "dark"];
|
const THEMES: readonly ToolboxThemeMode[] = ["system", "light", "dark"];
|
||||||
|
|
||||||
|
export function ToolboxPersonalizePanel({
|
||||||
|
theme,
|
||||||
|
onThemeChange,
|
||||||
|
storageAvailable,
|
||||||
|
children,
|
||||||
|
}: ToolboxPersonalizePanelProps) {
|
||||||
|
const headingId = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="toolbox-shell__preferences-content"
|
||||||
|
aria-labelledby={headingId}
|
||||||
|
>
|
||||||
|
<div className="toolbox-shell__preferences-heading">
|
||||||
|
<p className="toolbox-shell__preferences-kicker">On this device</p>
|
||||||
|
<h2 id={headingId}>Personalize your toolbox</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{storageAvailable === false ? (
|
||||||
|
<p className="toolbox-shell__preferences-warning" role="status">
|
||||||
|
Browser storage is unavailable. Changes will last only until this page
|
||||||
|
closes.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<fieldset className="toolbox-shell__theme-choice">
|
||||||
|
<legend>Appearance</legend>
|
||||||
|
<div className="toolbox-shell__theme-options">
|
||||||
|
{THEMES.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={theme === option}
|
||||||
|
onClick={() => onThemeChange(option)}
|
||||||
|
>
|
||||||
|
{option.slice(0, 1).toUpperCase() + option.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{children ?? (
|
||||||
|
<p className="toolbox-shell__preferences-copy">
|
||||||
|
The Toolbox stores your appearance choice in this browser and does not
|
||||||
|
transmit it.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ToolboxHeader({
|
export function ToolboxHeader({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
@@ -219,11 +277,89 @@ export function ToolboxHeader({
|
|||||||
data-toolbox-theme={theme}
|
data-toolbox-theme={theme}
|
||||||
>
|
>
|
||||||
<div className="toolbox-shell__bar">
|
<div className="toolbox-shell__bar">
|
||||||
|
{homeHref ? (
|
||||||
|
<a
|
||||||
|
className="toolbox-shell__brand"
|
||||||
|
href={homeHref}
|
||||||
|
aria-label={homeLabel}
|
||||||
|
onClick={() => setOpenPopover(null)}
|
||||||
|
>
|
||||||
|
{brandContent}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="toolbox-shell__brand">{brandContent}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="toolbox-shell__identity">
|
||||||
|
<h1 className="toolbox-shell__name">{title}</h1>
|
||||||
|
{subtitle ? (
|
||||||
|
<span className="toolbox-shell__description">{subtitle}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="toolbox-shell__controls"
|
className="toolbox-shell__controls"
|
||||||
role="group"
|
role="group"
|
||||||
aria-label="Toolbox controls"
|
aria-label="Toolbox controls"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
className="toolbox-shell__icon-button"
|
||||||
|
type="button"
|
||||||
|
aria-label={helpAction?.label ?? "Help"}
|
||||||
|
title={
|
||||||
|
helpAction?.title ??
|
||||||
|
helpAction?.label ??
|
||||||
|
(helpAction ? "Help" : "Help unavailable")
|
||||||
|
}
|
||||||
|
disabled={!helpAction}
|
||||||
|
onClick={() => {
|
||||||
|
setOpenPopover(null);
|
||||||
|
helpAction?.onClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HelpIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{sourceHref ? (
|
||||||
|
<a
|
||||||
|
className="toolbox-shell__icon-button toolbox-shell__source"
|
||||||
|
href={sourceHref}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label={sourceLabel}
|
||||||
|
title={sourceLabel}
|
||||||
|
onClick={() => setOpenPopover(null)}
|
||||||
|
>
|
||||||
|
<GiteaIcon />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="toolbox-shell__icon-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Source unavailable"
|
||||||
|
title="Source unavailable"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<GiteaIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
ref={appsButtonRef}
|
||||||
|
className="toolbox-shell__menu-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Apps"
|
||||||
|
title="Apps"
|
||||||
|
aria-expanded={visiblePopover === "apps"}
|
||||||
|
aria-controls={visiblePopover === "apps" ? appsId : undefined}
|
||||||
|
onClick={() =>
|
||||||
|
setOpenPopover((current) => (current === "apps" ? null : "apps"))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AppsIcon />
|
||||||
|
<span>Apps</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
ref={personalizeButtonRef}
|
ref={personalizeButtonRef}
|
||||||
className="toolbox-shell__menu-button"
|
className="toolbox-shell__menu-button"
|
||||||
@@ -260,64 +396,6 @@ export function ToolboxHeader({
|
|||||||
<span>Personalize</span>
|
<span>Personalize</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
ref={appsButtonRef}
|
|
||||||
className="toolbox-shell__menu-button"
|
|
||||||
type="button"
|
|
||||||
aria-label="Apps"
|
|
||||||
title="Apps"
|
|
||||||
aria-expanded={visiblePopover === "apps"}
|
|
||||||
aria-controls={visiblePopover === "apps" ? appsId : undefined}
|
|
||||||
onClick={() =>
|
|
||||||
setOpenPopover((current) => (current === "apps" ? null : "apps"))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AppsIcon />
|
|
||||||
<span>Apps</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{sourceHref ? (
|
|
||||||
<a
|
|
||||||
className="toolbox-shell__icon-button toolbox-shell__source"
|
|
||||||
href={sourceHref}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
aria-label={sourceLabel}
|
|
||||||
title={sourceLabel}
|
|
||||||
onClick={() => setOpenPopover(null)}
|
|
||||||
>
|
|
||||||
<GiteaIcon />
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="toolbox-shell__icon-button"
|
|
||||||
type="button"
|
|
||||||
aria-label="Source unavailable"
|
|
||||||
title="Source unavailable"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<GiteaIcon />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="toolbox-shell__icon-button"
|
|
||||||
type="button"
|
|
||||||
aria-label={helpAction?.label ?? "Help"}
|
|
||||||
title={
|
|
||||||
helpAction?.title ??
|
|
||||||
helpAction?.label ??
|
|
||||||
(helpAction ? "Help" : "Help unavailable")
|
|
||||||
}
|
|
||||||
disabled={!helpAction}
|
|
||||||
onClick={() => {
|
|
||||||
setOpenPopover(null);
|
|
||||||
helpAction?.onClick();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HelpIcon />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{visiblePopover === "personalize" ? (
|
{visiblePopover === "personalize" ? (
|
||||||
<section
|
<section
|
||||||
ref={popoverRef}
|
ref={popoverRef}
|
||||||
@@ -331,22 +409,10 @@ export function ToolboxHeader({
|
|||||||
{customPersonalizeContent ? (
|
{customPersonalizeContent ? (
|
||||||
personalizeContent
|
personalizeContent
|
||||||
) : (
|
) : (
|
||||||
<>
|
<ToolboxPersonalizePanel
|
||||||
<strong>Appearance</strong>
|
theme={theme}
|
||||||
<div className="toolbox-shell__theme-options">
|
onThemeChange={(option) => onThemeChange?.(option)}
|
||||||
{THEMES.map((option) => (
|
/>
|
||||||
<button
|
|
||||||
key={option}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={theme === option}
|
|
||||||
onClick={() => onThemeChange?.(option)}
|
|
||||||
>
|
|
||||||
{option.slice(0, 1).toUpperCase() + option.slice(1)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<small>Saved only in this browser.</small>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -387,26 +453,6 @@ export function ToolboxHeader({
|
|||||||
</nav>
|
</nav>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="toolbox-shell__identity">
|
|
||||||
<h1 className="toolbox-shell__name">{title}</h1>
|
|
||||||
{subtitle ? (
|
|
||||||
<span className="toolbox-shell__description">{subtitle}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{homeHref ? (
|
|
||||||
<a
|
|
||||||
className="toolbox-shell__brand"
|
|
||||||
href={homeHref}
|
|
||||||
aria-label={homeLabel}
|
|
||||||
onClick={() => setOpenPopover(null)}
|
|
||||||
>
|
|
||||||
{brandContent}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<div className="toolbox-shell__brand">{brandContent}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{leadingActions || metadata ? (
|
{leadingActions || metadata ? (
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
export { AppShell } from "./AppShell.js";
|
export { AppShell } from "./AppShell.js";
|
||||||
export type { AppShellProps } from "./AppShell.js";
|
export type { AppShellProps } from "./AppShell.js";
|
||||||
export { ToolboxHeader } from "./ToolboxHeader.js";
|
export { ToolboxHeader, ToolboxPersonalizePanel } from "./ToolboxHeader.js";
|
||||||
export type {
|
export type {
|
||||||
ToolboxHeaderAction,
|
ToolboxHeaderAction,
|
||||||
ToolboxHeaderApp,
|
ToolboxHeaderApp,
|
||||||
ToolboxHeaderProps,
|
ToolboxHeaderProps,
|
||||||
|
ToolboxPersonalizePanelProps,
|
||||||
} from "./ToolboxHeader.js";
|
} from "./ToolboxHeader.js";
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
--toolbox-accent-soft: #e9edff;
|
--toolbox-accent-soft: #e9edff;
|
||||||
--toolbox-accent-contrast: #ffffff;
|
--toolbox-accent-contrast: #ffffff;
|
||||||
--toolbox-focus: #63cdbc;
|
--toolbox-focus: #63cdbc;
|
||||||
|
--toolbox-danger: #b4233a;
|
||||||
--toolbox-radius: 0.7rem;
|
--toolbox-radius: 0.7rem;
|
||||||
--toolbox-shadow: 0 10px 34px rgb(24 34 68 / 9%);
|
--toolbox-shadow: 0 10px 34px rgb(24 34 68 / 9%);
|
||||||
}
|
}
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
--toolbox-accent-soft: #252f55;
|
--toolbox-accent-soft: #252f55;
|
||||||
--toolbox-accent-contrast: #11182a;
|
--toolbox-accent-contrast: #11182a;
|
||||||
--toolbox-focus: #66cdbd;
|
--toolbox-focus: #66cdbd;
|
||||||
|
--toolbox-danger: #ff8fa3;
|
||||||
--toolbox-shadow: 0 14px 42px rgb(0 0 0 / 24%);
|
--toolbox-shadow: 0 14px 42px rgb(0 0 0 / 24%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@
|
|||||||
--toolbox-accent-soft: #252f55;
|
--toolbox-accent-soft: #252f55;
|
||||||
--toolbox-accent-contrast: #11182a;
|
--toolbox-accent-contrast: #11182a;
|
||||||
--toolbox-focus: #66cdbd;
|
--toolbox-focus: #66cdbd;
|
||||||
|
--toolbox-danger: #ff8fa3;
|
||||||
--toolbox-shadow: 0 14px 42px rgb(0 0 0 / 24%);
|
--toolbox-shadow: 0 14px 42px rgb(0 0 0 / 24%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +109,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
justify-self: start;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbox-shell__header a {
|
.toolbox-shell__header a {
|
||||||
@@ -233,7 +236,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-self: end;
|
justify-self: start;
|
||||||
gap: 0.58rem;
|
gap: 0.58rem;
|
||||||
padding: 0.28rem 0.35rem;
|
padding: 0.28rem 0.35rem;
|
||||||
border-radius: var(--toolbox-radius);
|
border-radius: var(--toolbox-radius);
|
||||||
@@ -278,7 +281,8 @@ a.toolbox-shell__brand:hover {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: calc(100% + 0.7rem);
|
top: calc(100% + 0.7rem);
|
||||||
left: 0;
|
right: 0;
|
||||||
|
left: auto;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-width: 13rem;
|
min-width: 13rem;
|
||||||
max-width: min(22rem, calc(100vw - 1.5rem));
|
max-width: min(22rem, calc(100vw - 1.5rem));
|
||||||
@@ -292,7 +296,12 @@ a.toolbox-shell__brand:hover {
|
|||||||
color: var(--toolbox-text);
|
color: var(--toolbox-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbox-shell__popover strong {
|
.toolbox-shell__personalize-popover {
|
||||||
|
width: min(26rem, calc(100vw - 1.5rem));
|
||||||
|
max-width: min(26rem, calc(100vw - 1.5rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__popover > strong {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.35rem 0.4rem 0.5rem;
|
padding: 0.35rem 0.4rem 0.5rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
@@ -300,8 +309,8 @@ a.toolbox-shell__brand:hover {
|
|||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbox-shell__popover small,
|
.toolbox-shell__popover > small,
|
||||||
.toolbox-shell__popover p {
|
.toolbox-shell__switcher > p {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.55rem 0.4rem 0.25rem;
|
padding: 0.55rem 0.4rem 0.25rem;
|
||||||
@@ -310,17 +319,59 @@ a.toolbox-shell__brand:hover {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__preferences-content {
|
||||||
|
padding: 0.35rem;
|
||||||
|
color: var(--toolbox-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__preferences-heading {
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__preferences-content h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--toolbox-text);
|
||||||
|
font-family: var(--toolbox-font);
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__preferences-content .toolbox-shell__preferences-kicker {
|
||||||
|
margin: 0 0 0.3rem;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--toolbox-accent);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__theme-choice {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__theme-choice legend {
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
color: var(--toolbox-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbox-shell__theme-options {
|
.toolbox-shell__theme-options {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 0.25rem;
|
gap: 0.45rem;
|
||||||
padding: 0.25rem;
|
padding: 0.35rem;
|
||||||
border-radius: 0.65rem;
|
border-radius: 0.75rem;
|
||||||
background: var(--toolbox-surface-soft);
|
background: var(--toolbox-surface-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbox-shell__theme-options button {
|
.toolbox-shell__theme-options button {
|
||||||
padding: 0.5rem 0.45rem;
|
padding: 0.62rem;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -333,8 +384,30 @@ a.toolbox-shell__brand:hover {
|
|||||||
.toolbox-shell__theme-options button[aria-pressed="true"] {
|
.toolbox-shell__theme-options button[aria-pressed="true"] {
|
||||||
background: var(--toolbox-surface);
|
background: var(--toolbox-surface);
|
||||||
color: var(--toolbox-accent);
|
color: var(--toolbox-accent);
|
||||||
box-shadow: 0 1px 5px rgb(24 34 68 / 12%);
|
box-shadow: 0 2px 8px rgb(20 30 60 / 9%);
|
||||||
font-weight: 750;
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__preferences-content .toolbox-shell__preferences-copy {
|
||||||
|
margin: 0.9rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--toolbox-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox-shell__preferences-content .toolbox-shell__preferences-warning {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
padding: 0.7rem 0.8rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--toolbox-danger) 8%,
|
||||||
|
var(--toolbox-surface)
|
||||||
|
);
|
||||||
|
color: var(--toolbox-danger);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbox-shell__switcher ul {
|
.toolbox-shell__switcher ul {
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
} from "@add-ideas/toolbox-contract";
|
} from "@add-ideas/toolbox-contract";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { AppShell, ToolboxHeader } from "../src/index.js";
|
import {
|
||||||
|
AppShell,
|
||||||
|
ToolboxHeader,
|
||||||
|
ToolboxPersonalizePanel,
|
||||||
|
} from "../src/index.js";
|
||||||
|
|
||||||
const currentApp: ToolboxAppManifest = {
|
const currentApp: ToolboxAppManifest = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
@@ -124,10 +128,20 @@ describe("AppShell", () => {
|
|||||||
element.getAttribute("aria-label"),
|
element.getAttribute("aria-label"),
|
||||||
),
|
),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
"Personalize",
|
|
||||||
"Apps",
|
|
||||||
"Source for PDF Workbench on Gitea",
|
|
||||||
"Help",
|
"Help",
|
||||||
|
"Source for PDF Workbench on Gitea",
|
||||||
|
"Apps",
|
||||||
|
"Personalize",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
Array.from(
|
||||||
|
document.querySelector(".toolbox-shell__bar")?.children ?? [],
|
||||||
|
(element) => element.className,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
"toolbox-shell__brand",
|
||||||
|
"toolbox-shell__identity",
|
||||||
|
"toolbox-shell__controls",
|
||||||
]);
|
]);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Apps" }));
|
fireEvent.click(screen.getByRole("button", { name: "Apps" }));
|
||||||
expect(
|
expect(
|
||||||
@@ -278,6 +292,21 @@ describe("AppShell", () => {
|
|||||||
const shell = document.querySelector(".toolbox-shell");
|
const shell = document.querySelector(".toolbox-shell");
|
||||||
expect(shell).toHaveAttribute("data-toolbox-theme", "dark");
|
expect(shell).toHaveAttribute("data-toolbox-theme", "dark");
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Personalize" }));
|
fireEvent.click(screen.getByRole("button", { name: "Personalize" }));
|
||||||
|
expect(
|
||||||
|
screen.getByRole("heading", { name: "Personalize your toolbox" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("On this device")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("group", { name: "Appearance" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Dark" })).toHaveAttribute(
|
||||||
|
"aria-pressed",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("Pinned")).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: "Export JSON" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Light" }));
|
fireEvent.click(screen.getByRole("button", { name: "Light" }));
|
||||||
expect(shell).toHaveAttribute("data-toolbox-theme", "light");
|
expect(shell).toHaveAttribute("data-toolbox-theme", "light");
|
||||||
expect(
|
expect(
|
||||||
@@ -285,6 +314,29 @@ describe("AppShell", () => {
|
|||||||
).toMatchObject({ theme: "light" });
|
).toMatchObject({ theme: "light" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shares the portal-style personalization frame and extension slot", () => {
|
||||||
|
const onThemeChange = vi.fn();
|
||||||
|
render(
|
||||||
|
<ToolboxPersonalizePanel
|
||||||
|
theme="system"
|
||||||
|
onThemeChange={onThemeChange}
|
||||||
|
storageAvailable={false}
|
||||||
|
>
|
||||||
|
<p>Portal preference actions</p>
|
||||||
|
</ToolboxPersonalizePanel>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole("heading", { name: "Personalize your toolbox" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("status")).toHaveTextContent(
|
||||||
|
"Browser storage is unavailable",
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Portal preference actions")).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Dark" }));
|
||||||
|
expect(onThemeChange).toHaveBeenCalledWith("dark");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves manifest-relative header links without rendering an app icon", async () => {
|
it("resolves manifest-relative header links without rendering an app icon", async () => {
|
||||||
render(
|
render(
|
||||||
<AppShell
|
<AppShell
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@add-ideas/toolbox-testkit",
|
"name": "@add-ideas/toolbox-testkit",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"description": "Manifest, asset, and nested-deployment smoke checks for built toolbox applications.",
|
"description": "Manifest, asset, and nested-deployment smoke checks for built toolbox applications.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.1"
|
"@add-ideas/toolbox-contract": "0.2.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');fs.chmodSync('dist/cli.js',0o755);fs.copyFileSync('../../LICENSE','LICENSE')\"",
|
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');fs.chmodSync('dist/cli.js',0o755);fs.copyFileSync('../../LICENSE','LICENSE')\"",
|
||||||
|
|||||||
Reference in New Issue
Block a user