feat: add shared Toolbox header and themes
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Added
|
||||
|
||||
- Added the unified Toolbox top bar with a centered app title, standard Help and Gitea source controls, Personalize, and Apps switching.
|
||||
- Added persistent System, Light, and Dark appearance choices shared with the Toolbox Portal.
|
||||
- Added a real dark CodeMirror configuration with dark editor surfaces, gutters, active lines, and syntax highlighting.
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded Toolbox Contract, React Shell, and Testkit dependencies to 0.2.0.
|
||||
- Replaced the app-specific Help and Source actions with the shared shell controls and manifest repository fallback.
|
||||
- Converted workbench controls, cards, diagnostics, dialogs, tables, and editor chrome to semantic theme variables.
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Added
|
||||
|
||||
@@ -6,7 +6,7 @@ file dialogs where available. XSLT is executable code, however: a trusted
|
||||
stylesheet may access browser or network resources, so processing is not
|
||||
described as guaranteed local-only.
|
||||
|
||||
The application is independently deployable and also implements Toolbox Contract v1. When launched with a same-origin `?toolbox=<catalog URL>` context, the shared application shell adds toolbox navigation and an application switcher; without that context, the same build runs standalone.
|
||||
The application is independently deployable and also implements Toolbox Contract v1. The shared top bar keeps the app title centered and provides Help, its Gitea source link, persistent System/Light/Dark personalization, and app selection. When launched with a same-origin `?toolbox=<catalog URL>` context, the Apps menu includes the catalogue tools; without that context, the same build runs standalone.
|
||||
|
||||
## Features in this MVP
|
||||
|
||||
@@ -17,6 +17,7 @@ The application is independently deployable and also implements Toolbox Contract
|
||||
- CodeMirror editors with:
|
||||
- line numbers
|
||||
- XML syntax highlighting
|
||||
- theme-aware light and dark editor, gutter, and highlighting palettes
|
||||
- editor undo/redo
|
||||
- cut/copy/paste helpers
|
||||
- find support via CodeMirror shortcuts
|
||||
@@ -36,7 +37,7 @@ The application is independently deployable and also implements Toolbox Contract
|
||||
- Confirmation dialog before destructive overwrites
|
||||
- LocalStorage persistence
|
||||
- Approximate “explain transformation” table after a run
|
||||
- Shared toolbox shell with standalone fallback, application switching, and the XSLT-specific Help action
|
||||
- Shared toolbox shell with standalone fallback, unified Help/source controls, personalization, and application switching
|
||||
- Relocatable static output for deployment at root or nested paths
|
||||
|
||||
## SaxonJS note
|
||||
@@ -101,6 +102,10 @@ Toolbox mode is contextual rather than a separate build:
|
||||
|
||||
An unavailable or invalid catalogue falls back to standalone operation.
|
||||
|
||||
## Appearance
|
||||
|
||||
Use **Personalize** in the shared top bar to select System, Light, or Dark. The choice is stored only in this browser using the same preference record as the Toolbox Portal, so it follows you between current Toolbox apps on the same origin. System mode follows the operating-system color preference. CodeMirror is reconfigured when the effective palette changes, including its editor background, gutters, active line, and XML syntax colors.
|
||||
|
||||
## Release archive
|
||||
|
||||
After a successful build, create the deterministic application ZIP and SHA-256 sidecar with:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Corresponding source
|
||||
|
||||
The corresponding application source for XSLT tools 0.3.2 is available at:
|
||||
The corresponding application source for XSLT tools 0.4.0 is available at:
|
||||
|
||||
<https://git.add-ideas.de/zemion/xslt-tools/src/tag/v0.3.2>
|
||||
<https://git.add-ideas.de/zemion/xslt-tools/src/tag/v0.4.0>
|
||||
|
||||
The XSLT tools application source is licensed under AGPL-3.0-only. This release
|
||||
also contains third-party components under separate terms. See
|
||||
|
||||
@@ -6,6 +6,9 @@ from the XSLT tools application's AGPL-3.0-only licence.
|
||||
- The vendored SaxonJS browser distribution is proprietary software available
|
||||
under Saxonica's free-of-charge terms. Its complete supplied licence is at
|
||||
`LICENSES/SaxonJS-LICENSE.txt` in release archives.
|
||||
- The CodeMirror packages, including the One Dark editor theme, are distributed
|
||||
under the MIT licence; their installed licence files are included in release
|
||||
archives.
|
||||
- Runtime npm dependencies retain their own licences. Release archives include
|
||||
the installed licence and notice files for every production dependency below
|
||||
`LICENSES/npm/`, including the Apache-2.0 Toolbox Contract and React shell
|
||||
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"name": "xsl-tools",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "xsl-tools",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "^0.1.0",
|
||||
"@add-ideas/toolbox-shell-react": "^0.1.0",
|
||||
"@add-ideas/toolbox-contract": "0.2.0",
|
||||
"@add-ideas/toolbox-shell-react": "0.2.0",
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lint": "^6.8.5",
|
||||
"@codemirror/search": "^6.5.10",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "6.1.3",
|
||||
"@codemirror/view": "^6.38.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -23,7 +24,7 @@
|
||||
"react-dom": "^19.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@add-ideas/toolbox-testkit": "^0.1.0",
|
||||
"@add-ideas/toolbox-testkit": "0.2.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -45,18 +46,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@add-ideas/toolbox-contract": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-contract/-/0.1.1/toolbox-contract-0.1.1.tgz",
|
||||
"integrity": "sha512-0q2whA5Y4SqG4PNNleXYR8nGbT3UYsZ1x+6ZWVmxuc/WjMxmujhD7ilIISmFzmnNdbyEyv/Zpr+U0fnHqGODqw==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.0/toolbox-contract-0.2.0.tgz",
|
||||
"integrity": "sha512-up+wY+ft1L7TbXrflcTeTOwEdcFwHBWFlGFj8skQvcLy5z2mDBpd6/7PVQKDvDTkiODu2npviHm6AFglFw+17Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@add-ideas/toolbox-shell-react": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.1.1/toolbox-shell-react-0.1.1.tgz",
|
||||
"integrity": "sha512-Di2mu1BoJrWJg4/v5jDExROXDpvLXorlUgqcsBQV8ea/LklKoBFTAfgTkk/23HZ2BzixyMIjrhl9kD507s+iPQ==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.2.0/toolbox-shell-react-0.2.0.tgz",
|
||||
"integrity": "sha512-qYR+OBXX8rfB+BYCsUbpiCzo2xK12SIPWVNXHre158Da5uT6usr8rRvp5qa5u2oQJ50PNzlXyXMyTeRp0Vz5wg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "0.1.1"
|
||||
"@add-ideas/toolbox-contract": "0.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18 <20",
|
||||
@@ -64,13 +65,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@add-ideas/toolbox-testkit": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-testkit/-/0.1.1/toolbox-testkit-0.1.1.tgz",
|
||||
"integrity": "sha512-o+H7LfM7ipdgmYz18S0lLuN6JsXUR+8ubAbcjTWTk8rSqNfp1JyeoRHOPbJVEHXrQwIFe/OB86UzRpex2gnKUQ==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.0/toolbox-testkit-0.2.0.tgz",
|
||||
"integrity": "sha512-+BSiWgS0F3vpjPgty9B4UtfdZTRbo97ZElb2BSb0ry57fS3cE7w7pfJW1Mzk/gV1/JChspw3JBY+4ZJDBhkiFw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "0.1.1"
|
||||
"@add-ideas/toolbox-contract": "0.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"toolbox-check": "dist/cli.js"
|
||||
@@ -473,6 +474,18 @@
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xsl-tools",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -28,13 +28,14 @@
|
||||
"clean": "rm -rf dist node_modules/.vite coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "^0.1.0",
|
||||
"@add-ideas/toolbox-shell-react": "^0.1.0",
|
||||
"@add-ideas/toolbox-contract": "0.2.0",
|
||||
"@add-ideas/toolbox-shell-react": "0.2.0",
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lint": "^6.8.5",
|
||||
"@codemirror/search": "^6.5.10",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "6.1.3",
|
||||
"@codemirror/view": "^6.38.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -42,7 +43,7 @@
|
||||
"react-dom": "^19.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@add-ideas/toolbox-testkit": "^0.1.0",
|
||||
"@add-ideas/toolbox-testkit": "0.2.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
||||
@@ -3,19 +3,12 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "de.add-ideas.xslt-tools",
|
||||
"name": "XSLT tools",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"description": "Browser-based XML and XSLT transformations with executable-stylesheet trust controls.",
|
||||
"entry": "./",
|
||||
"icon": "./favicon.svg",
|
||||
"categories": ["documents", "xml", "xslt"],
|
||||
"tags": ["transform", "validate", "format", "explain"],
|
||||
"actions": [
|
||||
{
|
||||
"id": "source",
|
||||
"label": "Source",
|
||||
"url": "https://git.add-ideas.de/zemion/xslt-tools/src/tag/v0.3.2"
|
||||
}
|
||||
],
|
||||
"assets": ["./vendor/saxon/SaxonJS2.js", "./vendor/saxon/LICENSE.txt"],
|
||||
"integration": {
|
||||
"contextVersion": 1,
|
||||
|
||||
17
src/App.tsx
17
src/App.tsx
@@ -536,18 +536,11 @@ const App: React.FC = () => {
|
||||
<AppShell
|
||||
app={toolboxApp}
|
||||
className="xslt-app-shell"
|
||||
appActions={
|
||||
<button
|
||||
type="button"
|
||||
className="app-help-button"
|
||||
onClick={handleOpenHelp}
|
||||
aria-haspopup="dialog"
|
||||
title="Open help and keyboard shortcuts (F1)"
|
||||
aria-label="Open help"
|
||||
>
|
||||
<i className="bi bi-question-circle" aria-hidden="true" /> Help
|
||||
</button>
|
||||
}
|
||||
helpAction={{
|
||||
onClick: handleOpenHelp,
|
||||
label: 'Help',
|
||||
title: 'Open help and keyboard shortcuts (F1)',
|
||||
}}
|
||||
>
|
||||
<div className="workspace-grid">
|
||||
<EditorPanel
|
||||
|
||||
@@ -9,14 +9,24 @@ const HelpDialog: React.FC<HelpDialogProps> = ({ open, onClose }) => {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="help-dialog-backdrop" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="help-dialog-backdrop"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="xslt-help-title"
|
||||
>
|
||||
<div className="help-dialog-panel">
|
||||
<div className="help-dialog-header">
|
||||
<div>
|
||||
<h2>XSLT tools help</h2>
|
||||
<h2 id="xslt-help-title">XSLT tools help</h2>
|
||||
<p>Browser XML/XSLT transformation workbench.</p>
|
||||
</div>
|
||||
<button type="button" className="icon-button" onClick={onClose}>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={onClose}
|
||||
aria-label="Close help"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { Compartment, EditorState, type Extension } from '@codemirror/state';
|
||||
import { EditorView, keymap, placeholder } from '@codemirror/view';
|
||||
import {
|
||||
defaultKeymap,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@codemirror/commands';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import type { CodeMirrorEditorHandle } from './editorTypes';
|
||||
|
||||
interface CodeMirrorEditorProps {
|
||||
@@ -28,7 +29,7 @@ interface CodeMirrorEditorProps {
|
||||
placeholderText?: string;
|
||||
}
|
||||
|
||||
const editorTheme = EditorView.theme({
|
||||
const editorLayoutTheme = EditorView.theme({
|
||||
'&': {
|
||||
minHeight: '100%',
|
||||
height: '100%',
|
||||
@@ -41,11 +42,61 @@ const editorTheme = EditorView.theme({
|
||||
'.cm-content': {
|
||||
minHeight: '100%',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
borderRight: '1px solid #e5e7eb',
|
||||
},
|
||||
});
|
||||
|
||||
const lightEditorTheme = EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
backgroundColor: 'var(--app-editor-background)',
|
||||
color: 'var(--app-editor-text)',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'var(--app-editor-gutter)',
|
||||
color: 'var(--app-muted)',
|
||||
borderRight: '1px solid var(--app-border-soft)',
|
||||
},
|
||||
'.cm-activeLine, .cm-activeLineGutter': {
|
||||
backgroundColor: 'var(--app-editor-active-line)',
|
||||
},
|
||||
},
|
||||
{ dark: false }
|
||||
);
|
||||
|
||||
const darkEditorTheme: Extension = [
|
||||
oneDark,
|
||||
EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
backgroundColor: 'var(--app-editor-background)',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'var(--app-editor-gutter)',
|
||||
borderRight: '1px solid var(--app-border-soft)',
|
||||
},
|
||||
'.cm-activeLine, .cm-activeLineGutter': {
|
||||
backgroundColor: 'var(--app-editor-active-line)',
|
||||
},
|
||||
},
|
||||
{ dark: true }
|
||||
),
|
||||
];
|
||||
|
||||
function prefersDarkEditor(container: HTMLElement): boolean {
|
||||
const themeRoot = container.closest<HTMLElement>('[data-toolbox-theme]');
|
||||
const theme = themeRoot?.dataset.toolboxTheme;
|
||||
|
||||
if (theme === 'dark') return true;
|
||||
if (theme === 'light') return false;
|
||||
|
||||
return (
|
||||
globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false
|
||||
);
|
||||
}
|
||||
|
||||
function editorColorTheme(dark: boolean): Extension {
|
||||
return dark ? darkEditorTheme : lightEditorTheme;
|
||||
}
|
||||
|
||||
const CodeMirrorEditor = forwardRef<
|
||||
CodeMirrorEditorHandle,
|
||||
CodeMirrorEditorProps
|
||||
@@ -73,6 +124,11 @@ const CodeMirrorEditor = forwardRef<
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const themeCompartment = new Compartment();
|
||||
let dark = prefersDarkEditor(container);
|
||||
container.dataset.editorTheme = dark ? 'dark' : 'light';
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: value,
|
||||
extensions: [
|
||||
@@ -87,7 +143,8 @@ const CodeMirrorEditor = forwardRef<
|
||||
]),
|
||||
EditorState.readOnly.of(readOnly),
|
||||
EditorView.lineWrapping,
|
||||
editorTheme,
|
||||
editorLayoutTheme,
|
||||
themeCompartment.of(editorColorTheme(dark)),
|
||||
placeholder(placeholderText ?? ''),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && !syncingExternalValueRef.current) {
|
||||
@@ -101,10 +158,37 @@ const CodeMirrorEditor = forwardRef<
|
||||
],
|
||||
});
|
||||
|
||||
const view = new EditorView({ state, parent: containerRef.current });
|
||||
const view = new EditorView({ state, parent: container });
|
||||
viewRef.current = view;
|
||||
|
||||
const applyColorTheme = () => {
|
||||
const nextDark = prefersDarkEditor(container);
|
||||
container.dataset.editorTheme = nextDark ? 'dark' : 'light';
|
||||
if (nextDark === dark) return;
|
||||
|
||||
dark = nextDark;
|
||||
view.dispatch({
|
||||
effects: themeCompartment.reconfigure(editorColorTheme(nextDark)),
|
||||
});
|
||||
};
|
||||
|
||||
const themeRoot = container.closest<HTMLElement>('[data-toolbox-theme]');
|
||||
const observer = themeRoot
|
||||
? new MutationObserver(applyColorTheme)
|
||||
: undefined;
|
||||
observer?.observe(themeRoot as HTMLElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-toolbox-theme'],
|
||||
});
|
||||
|
||||
const colorScheme = globalThis.matchMedia?.(
|
||||
'(prefers-color-scheme: dark)'
|
||||
);
|
||||
colorScheme?.addEventListener('change', applyColorTheme);
|
||||
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
colorScheme?.removeEventListener('change', applyColorTheme);
|
||||
view.destroy();
|
||||
viewRef.current = null;
|
||||
};
|
||||
|
||||
289
src/styles.css
289
src/styles.css
@@ -5,21 +5,19 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
background: #f6f7fb;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 1180px;
|
||||
background: #f3f4f6;
|
||||
background: #f6f7fb;
|
||||
}
|
||||
|
||||
button,
|
||||
@@ -36,14 +34,112 @@ kbd {
|
||||
}
|
||||
|
||||
.xslt-app-shell {
|
||||
color-scheme: light;
|
||||
--toolbox-background: #f3f4f6;
|
||||
--toolbox-surface: #ffffff;
|
||||
--toolbox-text: #111827;
|
||||
--toolbox-muted: #6b7280;
|
||||
--toolbox-border: #d1d5db;
|
||||
--toolbox-accent: #2563eb;
|
||||
--toolbox-accent-contrast: #ffffff;
|
||||
--app-surface: var(--toolbox-surface);
|
||||
--app-surface-soft: var(--toolbox-surface-soft);
|
||||
--app-text: var(--toolbox-text);
|
||||
--app-text-secondary: #374151;
|
||||
--app-muted: var(--toolbox-muted);
|
||||
--app-border: var(--toolbox-border);
|
||||
--app-border-soft: #e5e7eb;
|
||||
--app-control: #e5e7eb;
|
||||
--app-control-hover: #d1d5db;
|
||||
--app-primary: #2563eb;
|
||||
--app-primary-contrast: #ffffff;
|
||||
--app-danger: #dc2626;
|
||||
--app-danger-surface: #fff1f2;
|
||||
--app-danger-text: #991b1b;
|
||||
--app-danger-ink: #881337;
|
||||
--app-danger-border: #fecaca;
|
||||
--app-danger-border-strong: #fca5a5;
|
||||
--app-success-surface: #f0fdf4;
|
||||
--app-success-text: #14532d;
|
||||
--app-success-border: #86efac;
|
||||
--app-warning-surface: #fffbeb;
|
||||
--app-warning-text: #92400e;
|
||||
--app-info-surface: #eff6ff;
|
||||
--app-info-text: #1e40af;
|
||||
--app-focus: #bfdbfe;
|
||||
--app-accent-border: #c7d2fe;
|
||||
--app-backdrop: rgb(15 23 42 / 60%);
|
||||
--app-card-shadow: 0 1px 3px rgb(15 23 42 / 12%);
|
||||
--app-float-shadow: 0 12px 30px rgb(15 23 42 / 22%);
|
||||
--app-dialog-shadow: 0 24px 60px rgb(15 23 42 / 40%);
|
||||
--app-small-shadow: 0 1px 2px rgb(15 23 42 / 25%);
|
||||
--app-focus-ring: rgb(37 99 235 / 22%);
|
||||
--app-editor-background: #ffffff;
|
||||
--app-editor-text: #111827;
|
||||
--app-editor-gutter: #f9fafb;
|
||||
--app-editor-active-line: rgb(37 99 235 / 8%);
|
||||
}
|
||||
|
||||
[data-toolbox-theme='dark'].xslt-app-shell {
|
||||
--app-text-secondary: #c8d1e4;
|
||||
--app-border-soft: #303a50;
|
||||
--app-control: #293247;
|
||||
--app-control-hover: #37435c;
|
||||
--app-primary: #a9b7ff;
|
||||
--app-primary-contrast: #11182a;
|
||||
--app-danger: #fb7185;
|
||||
--app-danger-surface: #3b1d27;
|
||||
--app-danger-text: #fecdd3;
|
||||
--app-danger-ink: #fecdd3;
|
||||
--app-danger-border: #7f3344;
|
||||
--app-danger-border-strong: #a84457;
|
||||
--app-success-surface: #123424;
|
||||
--app-success-text: #bbf7d0;
|
||||
--app-success-border: #23734a;
|
||||
--app-warning-surface: #3a290e;
|
||||
--app-warning-text: #fde68a;
|
||||
--app-info-surface: #172554;
|
||||
--app-info-text: #bfdbfe;
|
||||
--app-focus: #7dd3fc;
|
||||
--app-accent-border: #4c5c91;
|
||||
--app-backdrop: rgb(0 0 0 / 72%);
|
||||
--app-card-shadow: 0 1px 4px rgb(0 0 0 / 32%);
|
||||
--app-float-shadow: 0 14px 34px rgb(0 0 0 / 46%);
|
||||
--app-dialog-shadow: 0 24px 64px rgb(0 0 0 / 58%);
|
||||
--app-small-shadow: 0 1px 2px rgb(0 0 0 / 45%);
|
||||
--app-focus-ring: rgb(125 211 252 / 24%);
|
||||
--app-editor-background: #20252e;
|
||||
--app-editor-text: #abb2bf;
|
||||
--app-editor-gutter: #171b22;
|
||||
--app-editor-active-line: rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[data-toolbox-theme='system'].xslt-app-shell {
|
||||
--app-text-secondary: #c8d1e4;
|
||||
--app-border-soft: #303a50;
|
||||
--app-control: #293247;
|
||||
--app-control-hover: #37435c;
|
||||
--app-primary: #a9b7ff;
|
||||
--app-primary-contrast: #11182a;
|
||||
--app-danger: #fb7185;
|
||||
--app-danger-surface: #3b1d27;
|
||||
--app-danger-text: #fecdd3;
|
||||
--app-danger-ink: #fecdd3;
|
||||
--app-danger-border: #7f3344;
|
||||
--app-danger-border-strong: #a84457;
|
||||
--app-success-surface: #123424;
|
||||
--app-success-text: #bbf7d0;
|
||||
--app-success-border: #23734a;
|
||||
--app-warning-surface: #3a290e;
|
||||
--app-warning-text: #fde68a;
|
||||
--app-info-surface: #172554;
|
||||
--app-info-text: #bfdbfe;
|
||||
--app-focus: #7dd3fc;
|
||||
--app-accent-border: #4c5c91;
|
||||
--app-backdrop: rgb(0 0 0 / 72%);
|
||||
--app-card-shadow: 0 1px 4px rgb(0 0 0 / 32%);
|
||||
--app-float-shadow: 0 14px 34px rgb(0 0 0 / 46%);
|
||||
--app-dialog-shadow: 0 24px 64px rgb(0 0 0 / 58%);
|
||||
--app-small-shadow: 0 1px 2px rgb(0 0 0 / 45%);
|
||||
--app-focus-ring: rgb(125 211 252 / 24%);
|
||||
--app-editor-background: #20252e;
|
||||
--app-editor-text: #abb2bf;
|
||||
--app-editor-gutter: #171b22;
|
||||
--app-editor-active-line: rgb(255 255 255 / 6%);
|
||||
}
|
||||
}
|
||||
|
||||
.xslt-app-shell > .toolbox-shell__main {
|
||||
@@ -52,29 +148,11 @@ kbd {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.app-help-button {
|
||||
border: 1px solid var(--toolbox-border);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--toolbox-text);
|
||||
padding: 0.35rem 0.65rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.app-help-button:hover,
|
||||
.app-help-button:focus-visible {
|
||||
background: var(--toolbox-background);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);
|
||||
box-shadow: var(--app-card-shadow);
|
||||
}
|
||||
|
||||
.button,
|
||||
@@ -102,32 +180,32 @@ kbd {
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
background: var(--app-primary);
|
||||
color: var(--app-primary-contrast);
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: #e5e7eb;
|
||||
color: #111827;
|
||||
background: var(--app-control);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
background: #dc2626;
|
||||
color: #ffffff;
|
||||
background: var(--app-danger);
|
||||
color: var(--app-primary-contrast);
|
||||
}
|
||||
|
||||
.button.danger-outline {
|
||||
border: 1px solid #fecaca;
|
||||
background: #fff1f2;
|
||||
color: #991b1b;
|
||||
border: 1px solid var(--app-danger-border);
|
||||
background: var(--app-danger-surface);
|
||||
color: var(--app-danger-text);
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
border-radius: 999px;
|
||||
background: #e5e7eb;
|
||||
color: #111827;
|
||||
background: var(--app-control);
|
||||
color: var(--app-text);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
@@ -155,7 +233,7 @@ kbd {
|
||||
|
||||
.toolbar-options {
|
||||
align-items: center;
|
||||
color: #374151;
|
||||
color: var(--app-text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -168,15 +246,15 @@ kbd {
|
||||
}
|
||||
|
||||
.stylesheet-trust-status.is-untrusted {
|
||||
border-color: #fca5a5;
|
||||
background: #fff1f2;
|
||||
color: #881337;
|
||||
border-color: var(--app-danger-border-strong);
|
||||
background: var(--app-danger-surface);
|
||||
color: var(--app-danger-ink);
|
||||
}
|
||||
|
||||
.stylesheet-trust-status.is-trusted {
|
||||
border-color: #86efac;
|
||||
background: #f0fdf4;
|
||||
color: #14532d;
|
||||
border-color: var(--app-success-border);
|
||||
background: var(--app-success-surface);
|
||||
color: var(--app-success-text);
|
||||
}
|
||||
|
||||
.toggle-option {
|
||||
@@ -199,7 +277,7 @@ kbd {
|
||||
width: 2.35rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 999px;
|
||||
background: #d1d5db;
|
||||
background: var(--app-control-hover);
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
@@ -213,13 +291,13 @@ kbd {
|
||||
width: 0.91rem;
|
||||
height: 0.91rem;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.25);
|
||||
background: var(--app-surface);
|
||||
box-shadow: var(--app-small-shadow);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.toggle-option input:checked + .toggle-slider {
|
||||
background: #2563eb;
|
||||
background: var(--app-primary);
|
||||
}
|
||||
|
||||
.toggle-option input:checked + .toggle-slider::before {
|
||||
@@ -227,12 +305,12 @@ kbd {
|
||||
}
|
||||
|
||||
.toggle-option input:focus-visible + .toggle-slider {
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.22);
|
||||
box-shadow: 0 0 0 3px var(--app-focus-ring);
|
||||
}
|
||||
|
||||
.toolbar-meta {
|
||||
justify-content: space-between;
|
||||
color: #6b7280;
|
||||
color: var(--app-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -241,24 +319,25 @@ kbd {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
color: var(--app-text-secondary);
|
||||
}
|
||||
|
||||
.toolbar-meta select {
|
||||
min-width: 18rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 0.5rem;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
border-left: 4px solid #2563eb;
|
||||
border-left: 4px solid var(--app-primary);
|
||||
border-radius: 0.65rem;
|
||||
background: #eff6ff;
|
||||
color: #1e3a8a;
|
||||
background: var(--app-info-surface);
|
||||
color: var(--app-info-text);
|
||||
padding: 0.65rem 0.8rem;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||
box-shadow: var(--app-card-shadow);
|
||||
}
|
||||
|
||||
.runtime-diagnostics {
|
||||
@@ -298,15 +377,15 @@ kbd {
|
||||
}
|
||||
|
||||
.editor-panel small {
|
||||
color: #6b7280;
|
||||
color: var(--app-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.diagnostics-badge {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 999px;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
background: var(--app-surface-soft);
|
||||
color: var(--app-text-secondary);
|
||||
padding: 0.3rem 0.55rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
@@ -318,9 +397,9 @@ kbd {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border: 1px solid var(--app-border-soft);
|
||||
border-radius: 0.55rem;
|
||||
background: #f9fafb;
|
||||
background: var(--app-surface-soft);
|
||||
}
|
||||
|
||||
.editor-tool-button {
|
||||
@@ -328,7 +407,7 @@ kbd {
|
||||
height: 1.9rem;
|
||||
border-radius: 0.4rem;
|
||||
background: transparent;
|
||||
color: #374151;
|
||||
color: var(--app-text-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -342,7 +421,7 @@ kbd {
|
||||
|
||||
.editor-tool-button:hover:not(:disabled),
|
||||
.editor-tool-button:focus-visible {
|
||||
background: #e5e7eb;
|
||||
background: var(--app-control);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -351,14 +430,14 @@ kbd {
|
||||
align-self: stretch;
|
||||
min-height: 1.45rem;
|
||||
margin: 0 0.2rem;
|
||||
background: #d1d5db;
|
||||
background: var(--app-border);
|
||||
}
|
||||
|
||||
.mini-button,
|
||||
.snippet-button {
|
||||
border-radius: 0.4rem;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
background: var(--app-surface-soft);
|
||||
color: var(--app-text-secondary);
|
||||
padding: 0.28rem 0.48rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
@@ -366,14 +445,14 @@ kbd {
|
||||
|
||||
.mini-button:hover,
|
||||
.snippet-button:hover {
|
||||
background: #e5e7eb;
|
||||
background: var(--app-control-hover);
|
||||
}
|
||||
|
||||
.editor-host {
|
||||
overflow: hidden;
|
||||
border: 1px solid #d1d5db;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 0.65rem;
|
||||
background: #ffffff;
|
||||
background: var(--app-editor-background);
|
||||
}
|
||||
|
||||
.editor-host .cm-editor {
|
||||
@@ -382,7 +461,7 @@ kbd {
|
||||
}
|
||||
|
||||
.editor-host .cm-focused {
|
||||
outline: 2px solid #bfdbfe;
|
||||
outline: 2px solid var(--app-focus);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@@ -412,15 +491,15 @@ kbd {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.35rem;
|
||||
padding: 0.55rem;
|
||||
border: 1px solid #c7d2fe;
|
||||
border: 1px solid var(--app-accent-border);
|
||||
border-radius: 0.65rem;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.22);
|
||||
background: var(--app-surface);
|
||||
box-shadow: var(--app-float-shadow);
|
||||
}
|
||||
|
||||
.diagnostics-empty {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
color: var(--app-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
@@ -448,18 +527,18 @@ kbd {
|
||||
}
|
||||
|
||||
.diagnostic-error {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
background: var(--app-danger-surface);
|
||||
color: var(--app-danger-text);
|
||||
}
|
||||
|
||||
.diagnostic-warning {
|
||||
background: #fffbeb;
|
||||
color: #92400e;
|
||||
background: var(--app-warning-surface);
|
||||
color: var(--app-warning-text);
|
||||
}
|
||||
|
||||
.diagnostic-info {
|
||||
background: #eff6ff;
|
||||
color: #1e40af;
|
||||
background: var(--app-info-surface);
|
||||
color: var(--app-info-text);
|
||||
}
|
||||
|
||||
.trace-panel {
|
||||
@@ -481,18 +560,18 @@ kbd {
|
||||
|
||||
.trace-header p {
|
||||
margin: 0.2rem 0 0;
|
||||
color: #6b7280;
|
||||
color: var(--app-muted);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.trace-header small {
|
||||
color: #6b7280;
|
||||
color: var(--app-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.trace-table-wrap {
|
||||
overflow: auto;
|
||||
border: 1px solid #e5e7eb;
|
||||
border: 1px solid var(--app-border-soft);
|
||||
border-radius: 0.65rem;
|
||||
}
|
||||
|
||||
@@ -505,14 +584,14 @@ kbd {
|
||||
.trace-table th,
|
||||
.trace-table td {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--app-border-soft);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.trace-table th {
|
||||
background: #f9fafb;
|
||||
color: #374151;
|
||||
background: var(--app-surface-soft);
|
||||
color: var(--app-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
@@ -523,7 +602,7 @@ kbd {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 70;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
background: var(--app-backdrop);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -532,9 +611,9 @@ kbd {
|
||||
|
||||
.action-dialog-panel {
|
||||
width: min(440px, 100%);
|
||||
background: #ffffff;
|
||||
background: var(--app-surface);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.35);
|
||||
box-shadow: var(--app-dialog-shadow);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -556,7 +635,7 @@ kbd {
|
||||
}
|
||||
|
||||
.action-dialog-body {
|
||||
color: #4b5563;
|
||||
color: var(--app-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -580,9 +659,9 @@ kbd {
|
||||
width: min(900px, 100%);
|
||||
max-height: min(88vh, 760px);
|
||||
overflow: auto;
|
||||
background: #ffffff;
|
||||
background: var(--app-surface);
|
||||
border-radius: 0.9rem;
|
||||
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.4);
|
||||
box-shadow: var(--app-dialog-shadow);
|
||||
}
|
||||
|
||||
.help-dialog-header {
|
||||
@@ -590,18 +669,18 @@ kbd {
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
padding: 1rem;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: var(--app-surface);
|
||||
border-bottom: 1px solid var(--app-border-soft);
|
||||
}
|
||||
|
||||
.help-dialog-header p {
|
||||
margin: 0.2rem 0 0;
|
||||
color: #6b7280;
|
||||
color: var(--app-muted);
|
||||
}
|
||||
|
||||
.help-dialog-content {
|
||||
padding: 1rem;
|
||||
color: #374151;
|
||||
color: var(--app-text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -614,10 +693,10 @@ kbd {
|
||||
}
|
||||
|
||||
kbd {
|
||||
border: 1px solid #d1d5db;
|
||||
border: 1px solid var(--app-border);
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 0.3rem;
|
||||
background: #f9fafb;
|
||||
background: var(--app-surface-soft);
|
||||
padding: 0.05rem 0.25rem;
|
||||
font-size: 0.82em;
|
||||
}
|
||||
|
||||
@@ -14,13 +14,6 @@ export const toolboxAppDefinition = {
|
||||
icon: './favicon.svg',
|
||||
categories: ['documents', 'xml', 'xslt'],
|
||||
tags: ['transform', 'validate', 'format', 'explain'],
|
||||
actions: [
|
||||
{
|
||||
id: 'source',
|
||||
label: 'Source',
|
||||
url: 'https://git.add-ideas.de/zemion/xslt-tools/src/tag/v0.3.2',
|
||||
},
|
||||
],
|
||||
assets: ['./vendor/saxon/SaxonJS2.js', './vendor/saxon/LICENSE.txt'],
|
||||
integration: {
|
||||
contextVersion: 1,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const APP_VERSION = '0.3.2';
|
||||
export const APP_VERSION = '0.4.0';
|
||||
|
||||
48
tests/editorTheme.test.tsx
Normal file
48
tests/editorTheme.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { cleanup, render, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import CodeMirrorEditor from '../src/editor/CodeMirrorEditor';
|
||||
|
||||
const originalGetClientRects = Range.prototype.getClientRects;
|
||||
|
||||
beforeAll(() => {
|
||||
Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Range.prototype.getClientRects = originalGetClientRects;
|
||||
});
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('CodeMirror themes', () => {
|
||||
it('reconfigures the editor when the Toolbox theme changes', async () => {
|
||||
const onChange = vi.fn();
|
||||
const { container, rerender } = render(
|
||||
<div data-toolbox-theme="dark">
|
||||
<CodeMirrorEditor value="<root/>" onChange={onChange} ariaLabel="XML" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const editorHost = container.querySelector<HTMLElement>('.editor-host');
|
||||
expect(editorHost?.dataset.editorTheme).toBe('dark');
|
||||
expect(container.querySelector('.cm-editor')).toBeTruthy();
|
||||
|
||||
rerender(
|
||||
<div data-toolbox-theme="light">
|
||||
<CodeMirrorEditor value="<root/>" onChange={onChange} ariaLabel="XML" />
|
||||
</div>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editorHost?.dataset.editorTheme).toBe('light');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -70,6 +70,11 @@ describe('release packaging', () => {
|
||||
)
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
names.some((name) =>
|
||||
/^LICENSES\/npm\/codemirror--theme-one-dark-[^/]+\/LICENSE$/.test(name)
|
||||
)
|
||||
).toBe(true);
|
||||
expect(names.some((name) => /\/react-[^/]+\/LICENSE$/.test(name))).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AppShell } from '@add-ideas/toolbox-shell-react';
|
||||
import type {
|
||||
ToolboxAppManifest,
|
||||
@@ -7,6 +13,7 @@ import type {
|
||||
} from '@add-ideas/toolbox-contract';
|
||||
import { toolboxApp } from '../src/toolboxApp';
|
||||
|
||||
beforeEach(() => localStorage.clear());
|
||||
afterEach(cleanup);
|
||||
|
||||
const pdfApp: ToolboxAppManifest = {
|
||||
@@ -46,25 +53,51 @@ const catalog: ToolboxCatalog = {
|
||||
};
|
||||
|
||||
describe('toolbox shell integration', () => {
|
||||
it('keeps the app usable standalone with its XSLT Help action', () => {
|
||||
it('shows the standard app controls when running standalone', () => {
|
||||
const onHelp = vi.fn();
|
||||
render(
|
||||
<AppShell
|
||||
app={toolboxApp}
|
||||
contextOptions={{ location: 'https://example.test/deep/nested/app/' }}
|
||||
appActions={<button type="button">Help</button>}
|
||||
helpAction={{ onClick: onHelp, label: 'Help' }}
|
||||
>
|
||||
<p>XSLT workbench</p>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
expect(screen.getByText('XSLT workbench')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: 'Help' })).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Help' }));
|
||||
expect(onHelp).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'Source' }).getAttribute('href')
|
||||
).toBe('https://git.add-ideas.de/zemion/xslt-tools/src/tag/v0.3.2');
|
||||
screen
|
||||
.getByRole('link', { name: 'Source for XSLT tools on Gitea' })
|
||||
.getAttribute('href')
|
||||
).toBe('https://git.add-ideas.de/zemion/xslt-tools');
|
||||
expect(screen.getByText('Personalize')).toBeTruthy();
|
||||
expect(screen.getByText('Apps')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('persists explicit light, dark, and system theme choices', () => {
|
||||
const { container } = render(
|
||||
<AppShell app={toolboxApp}>
|
||||
<p>XSLT workbench</p>
|
||||
</AppShell>
|
||||
);
|
||||
const shell = container.firstElementChild;
|
||||
|
||||
expect(shell?.getAttribute('data-toolbox-theme')).toBe('system');
|
||||
fireEvent.click(screen.getByText('Personalize').closest('summary')!);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dark' }));
|
||||
expect(shell?.getAttribute('data-toolbox-theme')).toBe('dark');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Light' }));
|
||||
expect(shell?.getAttribute('data-toolbox-theme')).toBe('light');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'System' }));
|
||||
expect(shell?.getAttribute('data-toolbox-theme')).toBe('system');
|
||||
expect(
|
||||
screen.queryByRole('navigation', { name: 'Toolbox applications' })
|
||||
).toBeNull();
|
||||
localStorage.getItem('@add-ideas/toolbox-portal:v1:preferences')
|
||||
).toContain('"theme":"system"');
|
||||
});
|
||||
|
||||
it('loads a same-origin catalogue and carries context through switcher links', async () => {
|
||||
@@ -81,6 +114,7 @@ describe('toolbox shell integration', () => {
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Apps').closest('summary')!);
|
||||
const navigation = await screen.findByRole('navigation', {
|
||||
name: 'Toolbox applications',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user