Compare commits

...

7 Commits

Author SHA1 Message Date
hsiegeln
4b873194c9 fix(ci): add --allow-same-version to npm version in publish workflow
All checks were successful
Build & Publish / publish (push) Successful in 51s
The tag push job fails with "Version not changed" when package.json
already has the correct version from the bump commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:05:08 +01:00
hsiegeln
5f1b039056 chore: bump version to 0.1.6
Some checks failed
Build & Publish / publish (push) Failing after 50s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:01:53 +01:00
hsiegeln
095abe1751 feat: self-portaling DetailPanel via AppShell portal target
Some checks failed
Build & Publish / publish (push) Failing after 50s
DetailPanel now uses createPortal to render itself into
#cameleer-detail-panel-root, a div that AppShell places as a
direct sibling of .main in the top-level flex row. This means
pages can render <DetailPanel> anywhere in their JSX and it
automatically appears at the correct layout position.

AppShell's detail prop is deprecated and ignored — the portal
handles positioning automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:58:06 +01:00
hsiegeln
e8859e53ce chore: sync package-lock.json
All checks were successful
Build & Publish / publish (push) Successful in 49s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:12:21 +01:00
hsiegeln
021f6c7811 chore: bump version to 0.1.3
Some checks failed
Build & Publish / publish (push) Failing after 6s
2026-03-24 18:04:51 +01:00
hsiegeln
c18ba7d085 fix: exclude e2e tests from CI vitest run
Some checks failed
Build & Publish / publish (push) Failing after 7s
Playwright e2e tests need a browser and can't run in the CI container.
Exclude e2e/ directory from vitest so only unit tests run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:01:26 +01:00
hsiegeln
795ffef9dc feat: add auto-refresh toggle to TopBar and GlobalFilterProvider
Some checks failed
Build & Publish / publish (push) Failing after 5s
Add autoRefresh/setAutoRefresh to GlobalFilterContext, persisted in
localStorage. TopBar shows a LIVE/PAUSED toggle button with pulsing
dot indicator. Consumers can use useGlobalFilters().autoRefresh to
conditionally enable/disable polling intervals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:58:20 +01:00
8 changed files with 171 additions and 15 deletions

View File

@@ -17,7 +17,7 @@ jobs:
run: npm ci
- name: Run tests
run: npx vitest run
run: npx vitest run --exclude 'e2e/**'
- name: Build library
run: npm run build:lib
@@ -28,7 +28,7 @@ jobs:
case "$GITHUB_REF" in
refs/tags/v*)
VERSION="${GITHUB_REF_NAME#v}"
npm version "$VERSION" --no-git-tag-version
npm version "$VERSION" --no-git-tag-version --allow-same-version
TAG="latest"
;;
*)

77
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{
"name": "cameleer3",
"version": "0.0.0",
"name": "@cameleer/design-system",
"version": "0.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cameleer3",
"version": "0.0.0",
"name": "@cameleer/design-system",
"version": "0.1.6",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
@@ -24,6 +25,11 @@
"vite": "^6.0.0",
"vite-plugin-dts": "^4.5.4",
"vitest": "^3.0.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0"
}
},
"node_modules/@adobe/css-tools": {
@@ -925,6 +931,22 @@
"resolve": "~1.22.2"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -2874,6 +2896,53 @@
"pathe": "^2.0.3"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@cameleer/design-system",
"version": "0.1.0",
"version": "0.1.6",
"type": "module",
"main": "./dist/index.es.js",
"module": "./dist/index.es.js",
@@ -12,8 +12,12 @@
},
"./style.css": "./dist/style.css"
},
"files": ["dist"],
"sideEffects": ["*.css"],
"files": [
"dist"
],
"sideEffects": [
"*.css"
],
"publishConfig": {
"registry": "https://gitea.siegeln.net/api/packages/cameleer/npm/"
},
@@ -27,7 +31,8 @@
"build:lib": "vite build --config vite.lib.config.ts",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest"
"test": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"react": "^19.0.0",
@@ -40,6 +45,7 @@
"react-router-dom": "^7.0.0"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",

View File

@@ -1,4 +1,5 @@
import { useState, type ReactNode } from 'react'
import { createPortal } from 'react-dom'
import styles from './DetailPanel.module.css'
interface Tab {
@@ -22,7 +23,7 @@ export function DetailPanel({ open, onClose, title, tabs, children, actions, cla
const activeContent = tabs?.find((t) => t.value === activeTab)?.content
return (
const panel = (
<aside
className={`${styles.panel} ${open ? styles.open : ''} ${className ?? ''}`}
aria-hidden={!open}
@@ -65,4 +66,8 @@ export function DetailPanel({ open, onClose, title, tabs, children, actions, cla
)}
</aside>
)
// Portal to AppShell level if target exists, otherwise render in place
const portalTarget = document.getElementById('cameleer-detail-panel-root')
return portalTarget ? createPortal(panel, portalTarget) : panel
}

View File

@@ -4,17 +4,18 @@ import type { ReactNode } from 'react'
interface AppShellProps {
sidebar: ReactNode
children: ReactNode
/** @deprecated DetailPanel now portals itself automatically. This prop is ignored. */
detail?: ReactNode
}
export function AppShell({ sidebar, children, detail }: AppShellProps) {
export function AppShell({ sidebar, children }: AppShellProps) {
return (
<div className={styles.app}>
{sidebar}
<div className={styles.main}>
{children}
</div>
{detail}
<div id="cameleer-detail-panel-root" />
</div>
)
}

View File

@@ -81,6 +81,56 @@
flex-shrink: 0;
}
.liveToggle {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-raised);
color: var(--text-muted);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.5px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
height: 30px;
}
.liveToggle:hover {
border-color: var(--text-faint);
}
.liveToggleActive {
color: var(--success);
border-color: var(--success-border);
background: var(--success-bg);
}
.liveToggleActive:hover {
border-color: var(--success);
}
.liveDot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-muted);
flex-shrink: 0;
}
.liveToggleActive .liveDot {
background: var(--success);
animation: livePulse 2s ease-in-out infinite;
}
@keyframes livePulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.themeToggle {
background: none;
border: 1px solid var(--border);

View File

@@ -84,8 +84,18 @@ export function TopBar({
onChange={globalFilters.setTimeRange}
/>
{/* Right: theme toggle, env badge, user */}
{/* Right: auto-refresh toggle, theme toggle, env badge, user */}
<div className={styles.right}>
<button
className={`${styles.liveToggle} ${globalFilters.autoRefresh ? styles.liveToggleActive : ''}`}
onClick={() => globalFilters.setAutoRefresh(!globalFilters.autoRefresh)}
type="button"
aria-label={globalFilters.autoRefresh ? 'Disable auto-refresh' : 'Enable auto-refresh'}
title={globalFilters.autoRefresh ? 'Auto-refresh is on — click to pause' : 'Auto-refresh is paused — click to resume'}
>
<span className={styles.liveDot} />
{globalFilters.autoRefresh ? 'LIVE' : 'PAUSED'}
</button>
<button
className={styles.themeToggle}
onClick={toggleTheme}

View File

@@ -16,6 +16,8 @@ interface GlobalFilterContextValue {
toggleStatus: (status: ExchangeStatus) => void
clearStatusFilters: () => void
isInTimeRange: (timestamp: Date) => boolean
autoRefresh: boolean
setAutoRefresh: (enabled: boolean) => void
}
const GlobalFilterContext = createContext<GlobalFilterContextValue | null>(null)
@@ -27,9 +29,17 @@ function getDefaultTimeRange(): TimeRange {
return { start, end, preset: DEFAULT_PRESET }
}
function getInitialAutoRefresh(): boolean {
try {
const stored = localStorage.getItem('cameleer:auto-refresh')
return stored === null ? true : stored === 'true'
} catch { return true }
}
export function GlobalFilterProvider({ children }: { children: ReactNode }) {
const [timeRange, setTimeRangeState] = useState<TimeRange>(getDefaultTimeRange)
const [statusFilters, setStatusFilters] = useState<Set<ExchangeStatus>>(new Set())
const [autoRefresh, setAutoRefreshState] = useState<boolean>(getInitialAutoRefresh)
const setTimeRange = useCallback((range: TimeRange) => {
setTimeRangeState(range)
@@ -51,6 +61,11 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) {
setStatusFilters(new Set())
}, [])
const setAutoRefresh = useCallback((enabled: boolean) => {
setAutoRefreshState(enabled)
try { localStorage.setItem('cameleer:auto-refresh', String(enabled)) } catch {}
}, [])
const isInTimeRange = useCallback(
(timestamp: Date) => {
if (timeRange.preset) {
@@ -65,7 +80,7 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) {
return (
<GlobalFilterContext.Provider
value={{ timeRange, setTimeRange, statusFilters, toggleStatus, clearStatusFilters, isInTimeRange }}
value={{ timeRange, setTimeRange, statusFilters, toggleStatus, clearStatusFilters, isInTimeRange, autoRefresh, setAutoRefresh }}
>
{children}
</GlobalFilterContext.Provider>