2 Commits

19 changed files with 359 additions and 158 deletions

2
.npmrc
View File

@@ -1 +1 @@
@add-ideas:registry=https://git.add-ideas.de/api/packages/zemion/npm/ @add-ideas:registry=https://git.add-ideas.de/api/packages/lotobo/npm/

View File

@@ -3,6 +3,30 @@
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.3 — 2026-07-27
### Changed
- Moved the canonical repository, schema identifiers, examples, and source
metadata to the `lotobo` (LocalToolBox) Gitea organization.
- Moved publication of all three packages to the `lotobo` Gitea npm registry.
## 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

View File

@@ -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.3` 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.
@@ -36,7 +36,7 @@ Deploy `toolbox-app.json` beside the application entry. The canonical schema is
```json ```json
{ {
"$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json", "$schema": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
"schemaVersion": 1, "schemaVersion": 1,
"id": "de.add-ideas.pdf-tools", "id": "de.add-ideas.pdf-tools",
"name": "PDF Workbench", "name": "PDF Workbench",
@@ -63,7 +63,7 @@ Deploy `toolbox-app.json` beside the application entry. The canonical schema is
"telemetry": false "telemetry": false
}, },
"source": { "source": {
"repository": "https://git.add-ideas.de/zemion/pdf-tools", "repository": "https://git.add-ideas.de/lotobo/pdf-tools",
"license": "AGPL-3.0-only" "license": "AGPL-3.0-only"
} }
} }
@@ -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.

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json", "$schema": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
"schemaVersion": 1, "schemaVersion": 1,
"id": "de.add-ideas.example-tool", "id": "de.add-ideas.example-tool",
"name": "Example Tool", "name": "Example Tool",
@@ -26,7 +26,7 @@
"telemetry": false "telemetry": false
}, },
"source": { "source": {
"repository": "https://git.add-ideas.de/zemion/toolbox-sdk", "repository": "https://git.add-ideas.de/lotobo/toolbox-sdk",
"license": "Apache-2.0" "license": "Apache-2.0"
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json", "$schema": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json",
"schemaVersion": 1, "schemaVersion": 1,
"id": "de.add-ideas.example-toolbox", "id": "de.add-ideas.example-toolbox",
"name": "Example Toolbox", "name": "Example Toolbox",

View File

@@ -27,7 +27,7 @@ export const toolboxApp = defineToolboxApp({
telemetry: false, telemetry: false,
}, },
source: { source: {
repository: "https://git.add-ideas.de/zemion/toolbox-sdk", repository: "https://git.add-ideas.de/lotobo/toolbox-sdk",
license: "Apache-2.0", license: "Apache-2.0",
}, },
}); });

14
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@add-ideas/toolbox-sdk-workspace", "name": "@add-ideas/toolbox-sdk-workspace",
"version": "0.2.1", "version": "0.2.3",
"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.3",
"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.3",
"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.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@add-ideas/toolbox-contract": "0.2.1" "@add-ideas/toolbox-contract": "0.2.3"
}, },
"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.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@add-ideas/toolbox-contract": "0.2.1" "@add-ideas/toolbox-contract": "0.2.3"
}, },
"bin": { "bin": {
"toolbox-check": "dist/cli.js" "toolbox-check": "dist/cli.js"

View File

@@ -1,12 +1,12 @@
{ {
"name": "@add-ideas/toolbox-sdk-workspace", "name": "@add-ideas/toolbox-sdk-workspace",
"version": "0.2.1", "version": "0.2.3",
"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",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://git.add-ideas.de/zemion/toolbox-sdk.git" "url": "git+https://git.add-ideas.de/lotobo/toolbox-sdk.git"
}, },
"type": "module", "type": "module",
"workspaces": [ "workspaces": [

View File

@@ -17,7 +17,7 @@ The package also owns the versioned same-origin browser preference contract. Use
`readToolboxPreferences()` and `writeToolboxPreferences()` to share pinned apps, `readToolboxPreferences()` and `writeToolboxPreferences()` to share pinned apps,
ordering, visibility, and light/dark/system mode with the Toolbox portal. ordering, visibility, and light/dark/system mode with the Toolbox portal.
See the workspace [README](https://git.add-ideas.de/zemion/toolbox-sdk#readme) See the workspace [README](https://git.add-ideas.de/lotobo/toolbox-sdk#readme)
for the v1 document formats and full API. for the v1 document formats and full API.
The canonical schemas are also published at The canonical schemas are also published at

View File

@@ -1,14 +1,14 @@
{ {
"name": "@add-ideas/toolbox-contract", "name": "@add-ideas/toolbox-contract",
"version": "0.2.1", "version": "0.2.3",
"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": {
"type": "git", "type": "git",
"url": "git+https://git.add-ideas.de/zemion/toolbox-sdk.git", "url": "git+https://git.add-ideas.de/lotobo/toolbox-sdk.git",
"directory": "packages/contract" "directory": "packages/contract"
}, },
"homepage": "https://git.add-ideas.de/zemion/toolbox-sdk", "homepage": "https://git.add-ideas.de/lotobo/toolbox-sdk",
"keywords": [ "keywords": [
"toolbox", "toolbox",
"manifest", "manifest",
@@ -43,6 +43,6 @@
}, },
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.add-ideas.de/api/packages/zemion/npm/" "registry": "https://git.add-ideas.de/api/packages/lotobo/npm/"
} }
} }

View File

@@ -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.

View File

@@ -1,14 +1,14 @@
{ {
"name": "@add-ideas/toolbox-shell-react", "name": "@add-ideas/toolbox-shell-react",
"version": "0.2.1", "version": "0.2.3",
"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": {
"type": "git", "type": "git",
"url": "git+https://git.add-ideas.de/zemion/toolbox-sdk.git", "url": "git+https://git.add-ideas.de/lotobo/toolbox-sdk.git",
"directory": "packages/shell-react" "directory": "packages/shell-react"
}, },
"homepage": "https://git.add-ideas.de/zemion/toolbox-sdk", "homepage": "https://git.add-ideas.de/lotobo/toolbox-sdk",
"keywords": [ "keywords": [
"toolbox", "toolbox",
"react", "react",
@@ -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.3"
}, },
"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')\"",
@@ -48,6 +48,6 @@
}, },
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.add-ideas.de/api/packages/zemion/npm/" "registry": "https://git.add-ideas.de/api/packages/lotobo/npm/"
} }
} }

View File

@@ -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 ? (

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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

View File

@@ -1,14 +1,14 @@
{ {
"name": "@add-ideas/toolbox-testkit", "name": "@add-ideas/toolbox-testkit",
"version": "0.2.1", "version": "0.2.3",
"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": {
"type": "git", "type": "git",
"url": "git+https://git.add-ideas.de/zemion/toolbox-sdk.git", "url": "git+https://git.add-ideas.de/lotobo/toolbox-sdk.git",
"directory": "packages/testkit" "directory": "packages/testkit"
}, },
"homepage": "https://git.add-ideas.de/zemion/toolbox-sdk", "homepage": "https://git.add-ideas.de/lotobo/toolbox-sdk",
"keywords": [ "keywords": [
"toolbox", "toolbox",
"manifest", "manifest",
@@ -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.3"
}, },
"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')\"",
@@ -47,6 +47,6 @@
}, },
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://git.add-ideas.de/api/packages/zemion/npm/" "registry": "https://git.add-ideas.de/api/packages/lotobo/npm/"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json", "$id": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json",
"title": "Toolbox application manifest v1", "title": "Toolbox application manifest v1",
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json", "$id": "https://git.add-ideas.de/lotobo/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json",
"title": "Toolbox catalog v1", "title": "Toolbox catalog v1",
"type": "object", "type": "object",
"required": ["schemaVersion", "id", "name", "home", "theme", "apps"], "required": ["schemaVersion", "id", "name", "home", "theme", "apps"],