Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/electron-backend-e2e/src/electron-test-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ export async function saveSettings(page: Page): Promise<void> {

export async function goToDashboard(page: Page): Promise<void> {
const dashboardLink = page
.locator('a.brand[href$="/workspace/dashboard"]')
.getByRole('link', { name: 'Dashboard', exact: true })
.first();

await expect(dashboardLink).toBeVisible();
Expand Down
13 changes: 9 additions & 4 deletions apps/electron-backend-e2e/src/settings.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,15 @@ test.describe('Electron Settings', () => {
exact: true,
})
).toHaveCount(0);
await expect(secondLaunch.mainWindow.locator('a.brand')).toHaveAttribute(
'href',
/\/workspace\/sources$/
);
await expect(
secondLaunch.mainWindow.getByRole('link', {
name: 'Sources',
exact: true,
})
).toHaveClass(/is-active/);
await expect(
secondLaunch.mainWindow.locator('button.rail-toggle')
).toBeVisible();
} finally {
await closeElectronApp(secondLaunch);
}
Expand Down
145 changes: 145 additions & 0 deletions apps/electron-backend-e2e/src/smoke.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,151 @@ test.describe('Electron App Smoke Test', () => {
}
});

test('@critical @electron expands and collapses the labelled navigation rail', async ({
dataDir,
}) => {
const app = await launchElectronApp(dataDir);

try {
const rail = app.mainWindow.locator('.app-rail');
const toggle = app.mainWindow.getByRole('button', {
name: 'Expand navigation',
});

await expect(
app.mainWindow.getByRole('link', {
name: 'Dashboard',
exact: true,
})
).toHaveCount(1);
await expect(rail.locator('.rail-toggle-icon')).toHaveText(
'arrow_drop_down'
);
await expect(rail.locator('.rail-brand')).toHaveCount(0);

await toggle.click();

await expect(
app.mainWindow.getByRole('button', {
name: 'Collapse navigation',
})
).toBeVisible();
await expect(rail.locator('.rail-toggle-icon')).toHaveText(
'arrow_right'
);
await expect(rail.locator('.rail-brand-name')).toHaveText(
'IPTVnator'
);
await expect(
rail.locator('.portal-rail-link-label', {
hasText: 'Dashboard',
})
).toBeVisible();
expect(
await app.mainWindow
.getByRole('link', {
name: 'Dashboard',
exact: true,
})
.evaluate((link) => getComputedStyle(link, '::after').right)
).toBe('3px');

const collapseToggle = app.mainWindow.getByRole('button', {
name: 'Collapse navigation',
});
const toggleBackground = await collapseToggle.evaluate(
(button) => getComputedStyle(button).backgroundColor
);
await collapseToggle.hover();
await expect(collapseToggle.locator('.rail-toggle-icon')).toHaveCSS(
'transform',
'none'
);
await expect(collapseToggle).toHaveCSS(
'background-color',
toggleBackground
);

const sourcesLink = app.mainWindow.getByRole('link', {
name: 'Sources',
exact: true,
});
const selectionBackground = await sourcesLink.evaluate(() => {
const probe = document.createElement('div');
probe.style.background = 'var(--app-selection-surface)';
document.body.append(probe);
const background = getComputedStyle(probe).backgroundColor;
probe.remove();
return background;
});
await sourcesLink.hover();
await expect(sourcesLink).toHaveCSS(
'background-color',
selectionBackground
);
await expect
.poll(() =>
sourcesLink.locator('mat-icon').evaluate((icon) => {
const transform = getComputedStyle(icon).transform;
return transform === 'none'
? 1
: new DOMMatrixReadOnly(transform).a;
})
)
.toBeCloseTo(2.1, 2);
await expect
.poll(() =>
sourcesLink
.locator('mat-icon')
.boundingBox()
.then((box) => box?.width ?? Number.POSITIVE_INFINITY)
)
.toBeLessThanOrEqual(44);
await expect(
app.mainWindow
.getByRole('link', {
name: 'Dashboard',
exact: true,
})
.locator('mat-icon')
).toHaveCSS('transform', 'none');
await expect
.poll(() =>
sourcesLink
.locator('.portal-rail-link-label')
.evaluate((label) => {
const transform = getComputedStyle(label).transform;
return transform === 'none'
? 1
: new DOMMatrixReadOnly(transform).a;
})
)
.toBe(1);

await app.mainWindow
.getByRole('button', { name: 'Collapse navigation' })
.click();

await expect(rail.locator('.rail-toggle-icon')).toHaveText(
'arrow_drop_down'
);
await expect(rail.locator('.rail-brand')).toHaveCount(0);
await sourcesLink.hover();
await expect
.poll(() =>
sourcesLink.locator('mat-icon').evaluate((icon) => {
const transform = getComputedStyle(icon).transform;
return transform === 'none'
? 1
: new DOMMatrixReadOnly(transform).a;
})
)
.toBeCloseTo(2.1, 2);
} finally {
await closeElectronApp(app);
}
});

test('@critical @electron app should render workspace content', async ({ dataDir }) => {
const app = await launchElectronApp(dataDir);

Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "إظهار القائمة",
"COLLAPSE_NAVIGATION": "إخفاء القائمة",
"RAIL_DASHBOARD": "لوحة التحكم",
"OPEN_DASHBOARD": "فتح لوحة التحكم",
"RAIL_SOURCES": "المصادر",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/ary.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "وسّع التنقل",
"COLLAPSE_NAVIGATION": "طوي التنقل",
"RAIL_DASHBOARD": "لوحة التحكم",
"OPEN_DASHBOARD": "حل لوحة التحكم",
"RAIL_SOURCES": "المصادر",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/by.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Разгарнуць навігацыю",
"COLLAPSE_NAVIGATION": "Згарнуць навігацыю",
"RAIL_DASHBOARD": "Панэль",
"OPEN_DASHBOARD": "Адкрыць панэль",
"RAIL_SOURCES": "Крыніцы",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Navigation ausklappen",
"COLLAPSE_NAVIGATION": "Navigation einklappen",
"RAIL_DASHBOARD": "Dashboard",
"OPEN_DASHBOARD": "Dashboard öffnen",
"RAIL_SOURCES": "Quellen",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Ανάπτυξη πλοήγησης",
"COLLAPSE_NAVIGATION": "Σύμπτυξη πλοήγησης",
"RAIL_DASHBOARD": "Πίνακας ελέγχου",
"OPEN_DASHBOARD": "Άνοιγμα πίνακα ελέγχου",
"RAIL_SOURCES": "Πηγές",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Expand navigation",
"COLLAPSE_NAVIGATION": "Collapse navigation",
"RAIL_DASHBOARD": "Dashboard",
"OPEN_DASHBOARD": "Open dashboard",
"RAIL_SOURCES": "Sources",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Ampliar navegación",
"COLLAPSE_NAVIGATION": "Contraer navegación",
"RAIL_DASHBOARD": "Panel",
"OPEN_DASHBOARD": "Abrir panel",
"RAIL_SOURCES": "Fuentes",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Développer la navigation",
"COLLAPSE_NAVIGATION": "Réduire la navigation",
"RAIL_DASHBOARD": "Tableau de bord",
"OPEN_DASHBOARD": "Ouvrir le tableau de bord",
"RAIL_SOURCES": "Sources",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Espandi navigazione",
"COLLAPSE_NAVIGATION": "Comprimi navigazione",
"RAIL_DASHBOARD": "Dashboard",
"OPEN_DASHBOARD": "Apri dashboard",
"RAIL_SOURCES": "Sorgenti",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "ナビゲーションを展開",
"COLLAPSE_NAVIGATION": "ナビゲーションを折りたたむ",
"RAIL_DASHBOARD": "ダッシュボード",
"OPEN_DASHBOARD": "ダッシュボードを開く",
"RAIL_SOURCES": "ソース",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "탐색 펼치기",
"COLLAPSE_NAVIGATION": "탐색 접기",
"RAIL_DASHBOARD": "대시보드",
"OPEN_DASHBOARD": "대시보드 열기",
"RAIL_SOURCES": "소스",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Navigatie uitklappen",
"COLLAPSE_NAVIGATION": "Navigatie inklappen",
"RAIL_DASHBOARD": "Dashboard",
"OPEN_DASHBOARD": "Open dashboard",
"RAIL_SOURCES": "Bronnen",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Rozwiń nawigację",
"COLLAPSE_NAVIGATION": "Zwiń nawigację",
"RAIL_DASHBOARD": "Pulpit",
"OPEN_DASHBOARD": "Otwórz pulpit",
"RAIL_SOURCES": "Źródła",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Expandir navegação",
"COLLAPSE_NAVIGATION": "Recolher navegação",
"RAIL_DASHBOARD": "Dashboard",
"OPEN_DASHBOARD": "Abrir dashboard",
"RAIL_SOURCES": "Fontes",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Развернуть навигацию",
"COLLAPSE_NAVIGATION": "Свернуть навигацию",
"RAIL_DASHBOARD": "Панель",
"OPEN_DASHBOARD": "Открыть панель",
"RAIL_SOURCES": "Источники",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "Gezinmeyi genişlet",
"COLLAPSE_NAVIGATION": "Gezinmeyi daralt",
"RAIL_DASHBOARD": "Pano",
"OPEN_DASHBOARD": "Panoyu aç",
"RAIL_SOURCES": "Kaynaklar",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "展开导航",
"COLLAPSE_NAVIGATION": "收起导航",
"RAIL_DASHBOARD": "仪表盘",
"OPEN_DASHBOARD": "打开仪表盘",
"RAIL_SOURCES": "源",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/assets/i18n/zhtw.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
},
"SHELL": {
"BRAND_ALT": "IPTVnator",
"EXPAND_NAVIGATION": "展開導覽",
"COLLAPSE_NAVIGATION": "收合導覽",
"RAIL_DASHBOARD": "儀表板",
"OPEN_DASHBOARD": "開啟儀表板",
"RAIL_SOURCES": "來源",
Expand Down
20 changes: 20 additions & 0 deletions docs/architecture/workspace-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,26 @@ Rail navigation is also shell-owned:
3. On dashboard, sources, settings, global search, global favorites, and global
recent, the shell falls back to the currently selected playlist so provider
navigation remains available even outside a provider route.
4. The top disclosure button is not a route. It shows a downward triangle in
the default 60px icon rail and a right-pointing triangle when expanded, so
the Dashboard destination appears exactly once in the actual link list.
5. The expanded rail shows the IPTVnator logo/name plus translated labels for
every workspace, provider, and settings entry. Language direction controls
the rail's `dir` attribute; logical start alignment therefore follows LTR
and RTL translations without maintaining separate layouts.
6. Expanded width is content-driven with lower and upper bounds. Navigation
links own the scrollable area while Settings remains visible in the fixed
footer.
7. At the compact shell breakpoint the rail remains horizontal and icon-only;
direct attempts to set the expanded model are rejected as well as button
clicks.
8. The rail keeps the standard theme-token surface and selection styling.
Hovered entries use the same blue selection surface as the settings
navigation. Fine-pointer devices magnify only the hovered icon to 2.1, so a
20px icon remains inside its 44px menu slot in both rail states. Neighboring
icons, labels, and the disclosure triangle are not transformed;
reduced-motion mode disables the remaining transform. Interactive controls
remain explicit Electron `no-drag` regions.

Command palette behavior is shell-owned but view-extensible:

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
<nav class="rail-links">
<nav class="rail-links" [class.is-expanded]="expanded()">
@for (link of links(); track link.path.join('/')) {
<a
class="portal-rail-link nav-item"
[routerLink]="link.path"
[routerLinkActive]="activeClass()"
[routerLinkActiveOptions]="{ exact: link.exact ?? false }"
[matTooltip]="link.tooltip"
[matTooltipDisabled]="expanded()"
[matTooltipPosition]="'right'"
[attr.aria-label]="link.tooltip"
[attr.aria-current]="isActive(link) ? 'page' : null"
[class.active]="activeClass() === 'active' && isActive(link)"
[class.is-active]="
activeClass() === 'is-active' && isActive(link)
"
[class.is-active]="activeClass() === 'is-active' && isActive(link)"
>
<mat-icon>{{ resolveIcon(link) }}</mat-icon>
@if (expanded()) {
<span class="portal-rail-link-label">
{{ link.tooltip }}
</span>
}
</a>
}
</nav>
Loading
Loading