@@ -1188,7 +1189,7 @@ function StatusBox({ icon: Icon, title, detail, tone }) {
function ClientOsNotice({ agentCommand, canShowCommand, isWindows, onCopyCommand, osLabel }) {
return (
-
+
{isWindows ? 'Remote Ubuntu endpoint needed' : 'Local Agent requires Linux'}
@@ -1200,7 +1201,7 @@ function ClientOsNotice({ agentCommand, canShowCommand, isWindows, onCopyCommand
{canShowCommand && (
)}
@@ -1268,11 +1269,11 @@ function TaskPanel({ task, onClear, onRefresh }) {
{task.status || 'queued'}
{canClear && (
)}
@@ -1305,7 +1306,7 @@ function TaskPanel({ task, onClear, onRefresh }) {
{components.map((component) => (
@@ -1351,7 +1352,7 @@ function ServiceChecks({ checks = [], status = 'not-checked', showSource = false
return (
-
+
{message || 'Service status unavailable'}
@@ -1364,8 +1365,8 @@ function ServiceChecks({ checks = [], status = 'not-checked', showSource = false
{check.healthy
- ?
- :
}
+ ?
+ :
}
{check.serviceName}
@@ -1412,9 +1413,9 @@ function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPa
{status.message || endpoint || '127.0.0.1:5010'}
- {needsUpdate ?
: health ?
:
}
+ {needsUpdate ?
: health ?
:
}
@@ -1454,18 +1455,18 @@ function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPa
- {systemInfo?.kernel || 'kernel -'}
- {systemInfo?.diskFree || 'disk -'}
+ {systemInfo?.kernel || 'kernel -'}
+ {systemInfo?.diskFree || 'disk -'}
{showAgentActions && (
{needsUpdate && latestAgentPackage?.downloadUrl && (
-
+
Latest .deb
)}
@@ -1488,7 +1489,7 @@ function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
{app?.appName || 'App detail'}
{app?.appCode || app?.appId || status.message || packageBaseUrl}
- {status.state === 'loading' ?
:
}
+ {status.state === 'loading' ?
:
}
{app ? (
@@ -1510,7 +1511,7 @@ function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
-
+
Components
{status.state === 'danger' && (
@@ -1533,7 +1534,7 @@ function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
{app.installed && installedServiceStatus !== 'not-checked' && (
-
+
Service health
{serviceCheckSummary(installedServiceStatus, installedServiceChecks)}
@@ -1559,7 +1560,7 @@ function Toast({ toast }) {
const Icon = tone === 'danger' ? AlertCircle : tone === 'success' ? CheckCircle2 : Activity;
return (
-
+
{toast.message}
);
diff --git a/web-client/src/styles.css b/web-client/src/styles.css
index bf62d2e..ff382ab 100644
--- a/web-client/src/styles.css
+++ b/web-client/src/styles.css
@@ -1,28 +1,93 @@
:root {
- --primary: #3755c3;
- --primary-dim: #2848b7;
- --primary-container: #dde1ff;
- --on-primary: #f8f7ff;
- --background: #f7f9fb;
- --surface-lowest: #ffffff;
- --surface-low: #f0f4f7;
- --surface: #e8eff3;
- --surface-high: #e1e9ee;
- --on-surface: #2a3439;
- --on-surface-variant: #566166;
- --outline-variant: #a9b4b9;
- --danger: #b42318;
- --danger-bg: #fee4e2;
- --success: #067647;
- --success-bg: #dcfae6;
- --warning: #b54708;
- --warning-bg: #fef0c7;
- --info: #175cd3;
- --info-bg: #d1e9ff;
- --muted-bg: #e2e8f0;
- --radius: 8px;
- --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
- --shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.18);
+ color-scheme: light;
+
+ /* Phenikaa-X universal tokens — design-language.md v0.4.4 */
+ --blue-50: #eaedf5;
+ --blue-100: #c5cce0;
+ --blue-200: #8c9ac4;
+ --blue-300: #6e7ca8;
+ --blue-500: #223771;
+ --blue-700: #1a2a57;
+ --blue-900: #1e2243;
+ --orange-100: #fcd9c4;
+ --orange-300: #f58220;
+ --orange-500: #f26522;
+ --orange-700: #c44e14;
+ --neutral-0: #ffffff;
+ --neutral-50: #f7f8fb;
+ --neutral-100: #eeedf6;
+ --neutral-200: #dddfea;
+ --neutral-300: #c5c8d8;
+ --neutral-400: #9fa3b8;
+ --neutral-450: #8c90a8;
+ --neutral-500: #757a91;
+ --neutral-600: #565b70;
+ --neutral-700: #3c4054;
+ --neutral-800: #272a3a;
+ --neutral-900: #161824;
+
+ --surface-base: var(--neutral-0);
+ --surface-raised: var(--neutral-50);
+ --surface-brand-tint: var(--neutral-100);
+ --text-primary: var(--neutral-900);
+ --text-secondary: var(--neutral-600);
+ --text-tertiary: var(--neutral-500);
+ --border-subtle: var(--neutral-300);
+ --border-strong: var(--neutral-450);
+ --brand-primary: var(--blue-500);
+ --brand-primary-strong: var(--blue-700);
+ --brand-accent: var(--orange-500);
+ --brand-accent-strong: var(--orange-700);
+
+ --semantic-success: #2e7d32;
+ --semantic-success-bg: #e8f5e9;
+ --semantic-warning: #9a5b00;
+ --semantic-warning-icon: #cb6119;
+ --semantic-warning-bg: #fff4db;
+ --semantic-error: #c62828;
+ --semantic-error-bg: #ffebee;
+ --semantic-info: #1565c0;
+ --semantic-info-bg: #e7f1fc;
+
+ --space-xxs: 4px;
+ --space-xs: 8px;
+ --space-sm: 12px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+ --space-2xl: 48px;
+ --radius-sm: 2px;
+ --radius-md: 4px;
+ --radius-lg: 8px;
+ --elevation-1: 0 1px 3px rgba(22, 24, 36, 0.12);
+ --elevation-2: 0 4px 12px rgba(22, 24, 36, 0.18);
+
+ /* Compatibility aliases used by the existing components. */
+ --primary: var(--brand-primary);
+ --primary-dim: var(--brand-primary-strong);
+ --primary-container: var(--blue-50);
+ --on-primary: var(--neutral-0);
+ --background: var(--surface-raised);
+ --surface-lowest: var(--surface-base);
+ --surface-low: var(--surface-raised);
+ --surface: var(--surface-brand-tint);
+ --surface-high: var(--neutral-200);
+ --on-surface: var(--text-primary);
+ --on-surface-variant: var(--text-secondary);
+ --outline: var(--border-strong);
+ --outline-variant: var(--border-subtle);
+ --danger: var(--semantic-error);
+ --danger-bg: var(--semantic-error-bg);
+ --success: var(--semantic-success);
+ --success-bg: var(--semantic-success-bg);
+ --warning: var(--semantic-warning);
+ --warning-bg: var(--semantic-warning-bg);
+ --info: var(--semantic-info);
+ --info-bg: var(--semantic-info-bg);
+ --muted-bg: var(--neutral-200);
+ --radius: var(--radius-md);
+ --shadow-sm: var(--elevation-1);
+ --shadow-lg: var(--elevation-2);
}
* {
@@ -40,8 +105,9 @@ body {
margin: 0;
background: var(--background);
color: var(--on-surface);
- font-family: "Inter", Arial, sans-serif;
- font-size: 14px;
+ font-family: "Montserrat Variable", Montserrat, "Segoe UI", sans-serif;
+ font-size: 15px;
+ line-height: 1.5;
overflow: hidden;
}
@@ -75,7 +141,7 @@ p {
h1,
h2,
.brand-copy strong {
- font-family: "Manrope", Arial, sans-serif;
+ font-family: "Montserrat Variable", Montserrat, "Segoe UI", sans-serif;
}
code {
@@ -1446,3 +1512,290 @@ tbody tr.selected-row td.action-col {
}
}
+
+/* --------------------------------------------------------------------------
+ Phenikaa-X application theme
+ Keeps the existing component contract while applying the Layer-1 language.
+ -------------------------------------------------------------------------- */
+
+::selection {
+ background: var(--orange-100);
+ color: var(--text-primary);
+}
+
+:focus-visible {
+ outline: 2px solid var(--blue-500);
+ outline-offset: 2px;
+}
+
+.sidebar {
+ background: var(--blue-900);
+ border-color: var(--neutral-700);
+}
+
+.brand-block {
+ border-bottom: 1px solid rgba(197, 204, 224, 0.18);
+ min-height: 72px;
+ padding: var(--space-md);
+}
+
+.brand-mark {
+ background: var(--brand-accent);
+ border-radius: var(--radius-md);
+ box-shadow: none;
+ color: var(--neutral-0);
+ height: 40px;
+ width: 40px;
+}
+
+.brand-copy strong,
+.sidebar .status-box strong,
+.sidebar .endpoint-summary-row strong {
+ color: var(--neutral-0);
+}
+
+.brand-copy strong {
+ font-size: 15px;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+}
+
+.brand-copy span,
+.sidebar .nav-label,
+.sidebar .endpoint-summary-row span,
+.sidebar .status-box div > span {
+ color: var(--blue-100);
+}
+
+.brand-copy span,
+.nav-label {
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+}
+
+.sidebar .status-box,
+.sidebar .endpoint-summary {
+ background: rgba(255, 255, 255, 0.055);
+ border-color: rgba(197, 204, 224, 0.2);
+}
+
+.sidebar .status-box:hover,
+.sidebar .endpoint-summary:hover {
+ background: rgba(255, 255, 255, 0.085);
+ border-color: rgba(197, 204, 224, 0.35);
+}
+
+.sidebar .btn-secondary {
+ background: transparent;
+ border-color: var(--blue-300);
+ color: var(--neutral-0);
+}
+
+.sidebar .btn-secondary:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: var(--blue-100);
+}
+
+.topbar {
+ background: rgba(255, 255, 255, 0.96);
+ border-color: var(--border-subtle);
+ min-height: 64px;
+}
+
+.topbar-title span {
+ color: var(--text-tertiary);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+}
+
+.topbar-title strong {
+ color: var(--text-primary);
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.page {
+ padding: var(--space-lg);
+}
+
+.page h1 {
+ color: var(--text-primary);
+ font-size: 25px;
+ font-weight: 700;
+ letter-spacing: -0.025em;
+ line-height: 1.2;
+}
+
+.panel,
+.table-panel,
+.dialog-panel,
+.offline-banner,
+.agent-status-button {
+ border-color: var(--border-subtle);
+ border-radius: var(--radius-md);
+ box-shadow: var(--elevation-1);
+}
+
+.panel,
+.table-panel,
+.dialog-panel {
+ background: var(--surface-base);
+}
+
+.panel-header,
+.dialog-header,
+.dialog-actions,
+.table-wrap thead,
+.page-filters {
+ border-color: var(--border-subtle);
+}
+
+.btn,
+.icon-button,
+input,
+select,
+textarea {
+ border-radius: var(--radius-md);
+}
+
+.btn {
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.btn-primary {
+ background: var(--brand-primary);
+ color: var(--neutral-0);
+}
+
+.btn-primary:hover {
+ background: var(--brand-primary-strong);
+}
+
+.btn-warning {
+ background: var(--brand-accent);
+ color: var(--neutral-900);
+}
+
+.btn-warning:hover {
+ background: var(--brand-accent-strong);
+ color: var(--neutral-0);
+}
+
+.btn-secondary,
+.icon-button {
+ background: var(--surface-base);
+ border-color: var(--border-subtle);
+ color: var(--text-secondary);
+}
+
+.btn-secondary:hover,
+.icon-button:hover,
+.icon-button.subtle:hover {
+ background: var(--blue-50);
+ border-color: var(--blue-200);
+ color: var(--brand-primary);
+}
+
+.settings-field input,
+.filter-field input,
+.filter-field select,
+input,
+select,
+textarea {
+ background: var(--surface-base);
+ border-color: var(--border-strong);
+ color: var(--text-primary);
+}
+
+.settings-field input:focus,
+.filter-field input:focus,
+.filter-field select:focus,
+input:focus,
+select:focus,
+textarea:focus {
+ border-color: var(--brand-primary);
+ box-shadow: 0 0 0 3px rgba(34, 55, 113, 0.16);
+}
+
+table {
+ color: var(--text-primary);
+}
+
+thead {
+ background: var(--surface-brand-tint);
+}
+
+th {
+ color: var(--blue-700);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.045em;
+}
+
+td {
+ border-color: var(--neutral-200);
+ font-size: 13px;
+}
+
+tbody tr:hover,
+.selected-row {
+ background: var(--blue-50);
+}
+
+.badge {
+ border-radius: var(--radius-sm);
+ font-weight: 600;
+}
+
+.badge-success {
+ background: var(--semantic-success-bg);
+ color: var(--semantic-success);
+}
+
+.badge-danger {
+ background: var(--semantic-error-bg);
+ color: var(--semantic-error);
+}
+
+.badge-warning {
+ background: var(--semantic-warning-bg);
+ color: var(--semantic-warning);
+}
+
+.badge-info,
+.badge-primary {
+ background: var(--semantic-info-bg);
+ color: var(--semantic-info);
+}
+
+code,
+.task-log-list {
+ background: var(--blue-900);
+ color: var(--neutral-100);
+}
+
+@media (max-width: 980px) {
+ .sidebar {
+ border-bottom-color: var(--neutral-700);
+ }
+}
+
+@media (max-width: 680px) {
+ .page {
+ padding: var(--space-md);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ }
+}
diff --git a/web-server/package-lock.json b/web-server/package-lock.json
index ba167b4..1ab7a62 100644
--- a/web-server/package-lock.json
+++ b/web-server/package-lock.json
@@ -8,6 +8,8 @@
"name": "robot-installer-web-server",
"version": "1.0.0",
"dependencies": {
+ "@fluentui/svg-icons": "^1.1.333",
+ "@fontsource-variable/montserrat": "^5.3.0",
"dotenv": "^17.4.2",
"ejs": "^3.1.10",
"express": "^4.19.2",
@@ -276,6 +278,21 @@
"node": ">=20"
}
},
+ "node_modules/@fluentui/svg-icons": {
+ "version": "1.1.333",
+ "resolved": "https://registry.npmjs.org/@fluentui/svg-icons/-/svg-icons-1.1.333.tgz",
+ "integrity": "sha512-uSFMDQq25/sVgHvD9KhP+c1rZrHWqIA0OZkofu9Ii3Q5b72DqLgqOreABHbPD67U5XfsFc4iPlZzWEl1849Obg==",
+ "license": "MIT"
+ },
+ "node_modules/@fontsource-variable/montserrat": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/montserrat/-/montserrat-5.3.0.tgz",
+ "integrity": "sha512-7PaZoxaxrWLAyrhO46v65An9LhUhfkTExWLhfbywYZCnZEgg/W1rEHnlNmZKjNZ3nJTVYyicqxlJt10z/26yTA==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@js-joda/core": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz",
diff --git a/web-server/package.json b/web-server/package.json
index 4be4d2e..5bfc802 100644
--- a/web-server/package.json
+++ b/web-server/package.json
@@ -9,6 +9,8 @@
"dev": "node server.js"
},
"dependencies": {
+ "@fluentui/svg-icons": "^1.1.333",
+ "@fontsource-variable/montserrat": "^5.3.0",
"dotenv": "^17.4.2",
"ejs": "^3.1.10",
"express": "^4.19.2",
diff --git a/web-server/public/css/styles.css b/web-server/public/css/styles.css
index 6d32f8c..2291e26 100644
--- a/web-server/public/css/styles.css
+++ b/web-server/public/css/styles.css
@@ -1,28 +1,92 @@
:root {
- --primary: #3755c3;
- --primary-dim: #2848b7;
- --primary-container: #dde1ff;
- --on-primary: #f8f7ff;
- --background: #f7f9fb;
- --surface-lowest: #ffffff;
- --surface-low: #f0f4f7;
- --surface: #e8eff3;
- --surface-high: #e1e9ee;
- --on-surface: #2a3439;
- --on-surface-variant: #566166;
- --outline: #717c82;
- --outline-variant: #a9b4b9;
- --danger: #b42318;
- --danger-bg: #fee4e2;
- --success: #067647;
- --success-bg: #dcfae6;
- --warning: #b54708;
- --warning-bg: #fef0c7;
- --info: #175cd3;
- --info-bg: #d1e9ff;
- --radius: 8px;
- --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
- --shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.18);
+ color-scheme: light;
+
+ /* Phenikaa-X universal tokens — design-language.md v0.4.4 */
+ --blue-50: #eaedf5;
+ --blue-100: #c5cce0;
+ --blue-200: #8c9ac4;
+ --blue-300: #6e7ca8;
+ --blue-500: #223771;
+ --blue-700: #1a2a57;
+ --blue-900: #1e2243;
+ --orange-100: #fcd9c4;
+ --orange-300: #f58220;
+ --orange-500: #f26522;
+ --orange-700: #c44e14;
+ --neutral-0: #ffffff;
+ --neutral-50: #f7f8fb;
+ --neutral-100: #eeedf6;
+ --neutral-200: #dddfea;
+ --neutral-300: #c5c8d8;
+ --neutral-400: #9fa3b8;
+ --neutral-450: #8c90a8;
+ --neutral-500: #757a91;
+ --neutral-600: #565b70;
+ --neutral-700: #3c4054;
+ --neutral-800: #272a3a;
+ --neutral-900: #161824;
+
+ --surface-base: var(--neutral-0);
+ --surface-raised: var(--neutral-50);
+ --surface-brand-tint: var(--neutral-100);
+ --text-primary: var(--neutral-900);
+ --text-secondary: var(--neutral-600);
+ --text-tertiary: var(--neutral-500);
+ --border-subtle: var(--neutral-300);
+ --border-strong: var(--neutral-450);
+ --brand-primary: var(--blue-500);
+ --brand-primary-strong: var(--blue-700);
+ --brand-accent: var(--orange-500);
+ --brand-accent-strong: var(--orange-700);
+
+ --semantic-success: #2e7d32;
+ --semantic-success-bg: #e8f5e9;
+ --semantic-warning: #9a5b00;
+ --semantic-warning-icon: #cb6119;
+ --semantic-warning-bg: #fff4db;
+ --semantic-error: #c62828;
+ --semantic-error-bg: #ffebee;
+ --semantic-info: #1565c0;
+ --semantic-info-bg: #e7f1fc;
+
+ --space-xxs: 4px;
+ --space-xs: 8px;
+ --space-sm: 12px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+ --space-2xl: 48px;
+ --radius-sm: 2px;
+ --radius-md: 4px;
+ --radius-lg: 8px;
+ --elevation-1: 0 1px 3px rgba(22, 24, 36, 0.12);
+ --elevation-2: 0 4px 12px rgba(22, 24, 36, 0.18);
+
+ /* Compatibility aliases used by the existing components. */
+ --primary: var(--brand-primary);
+ --primary-dim: var(--brand-primary-strong);
+ --primary-container: var(--blue-50);
+ --on-primary: var(--neutral-0);
+ --background: var(--surface-raised);
+ --surface-lowest: var(--surface-base);
+ --surface-low: var(--surface-raised);
+ --surface: var(--surface-brand-tint);
+ --surface-high: var(--neutral-200);
+ --on-surface: var(--text-primary);
+ --on-surface-variant: var(--text-secondary);
+ --outline: var(--border-strong);
+ --outline-variant: var(--border-subtle);
+ --danger: var(--semantic-error);
+ --danger-bg: var(--semantic-error-bg);
+ --success: var(--semantic-success);
+ --success-bg: var(--semantic-success-bg);
+ --warning: var(--semantic-warning);
+ --warning-bg: var(--semantic-warning-bg);
+ --info: var(--semantic-info);
+ --info-bg: var(--semantic-info-bg);
+ --radius: var(--radius-md);
+ --shadow-sm: var(--elevation-1);
+ --shadow-lg: var(--elevation-2);
}
* {
@@ -39,8 +103,9 @@ body {
margin: 0;
background: var(--background);
color: var(--on-surface);
- font-family: "Inter", Arial, sans-serif;
- font-size: 14px;
+ font-family: "Montserrat Variable", Montserrat, "Segoe UI", sans-serif;
+ font-size: 15px;
+ line-height: 1.5;
overflow: hidden;
}
@@ -48,7 +113,7 @@ h1,
h2,
h3,
.brand-copy strong {
- font-family: "Manrope", Arial, sans-serif;
+ font-family: "Montserrat Variable", Montserrat, "Segoe UI", sans-serif;
}
h1,
@@ -79,17 +144,13 @@ button:disabled {
opacity: 0.5;
}
-.material-symbols-outlined {
- direction: ltr;
+.fluent-icon {
+ background: currentColor;
display: inline-flex;
- font-family: "Material Symbols Outlined";
- font-size: 1.25rem;
- font-style: normal;
- font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 20;
- letter-spacing: 0;
- line-height: 1;
- text-transform: none;
- white-space: nowrap;
+ flex: 0 0 auto;
+ height: 1.25rem;
+ mask: var(--fluent-icon-url) center / contain no-repeat;
+ width: 1.25rem;
}
.app-shell {
@@ -304,7 +365,7 @@ button:disabled {
.notification-panel-header strong {
color: #111827;
- font-family: "Manrope", Arial, sans-serif;
+ font-family: "Montserrat Variable", Montserrat, "Segoe UI", sans-serif;
font-size: 15px;
}
@@ -394,8 +455,10 @@ button:disabled {
width: 34px;
}
-.notification-item-icon .material-symbols-outlined {
- font-size: 19px;
+.notification-item-icon .material-symbols-outlined,
+.notification-item-icon .fluent-icon {
+ height: 19px;
+ width: 19px;
}
.notification-item.tone-success .notification-item-icon {
@@ -468,9 +531,11 @@ button:disabled {
text-align: center;
}
-.notification-empty .material-symbols-outlined {
+.notification-empty .material-symbols-outlined,
+.notification-empty .fluent-icon {
color: #94a3b8;
- font-size: 32px;
+ height: 32px;
+ width: 32px;
}
.notification-empty strong {
@@ -705,7 +770,7 @@ button:disabled {
.metric-card strong {
color: #111827;
- font-family: "Manrope", Arial, sans-serif;
+ font-family: "Montserrat Variable", Montserrat, "Segoe UI", sans-serif;
font-size: 28px;
font-weight: 800;
}
@@ -1208,9 +1273,11 @@ tbody tr:hover td.action-col {
text-align: center;
}
-.file-dropzone-content > .material-symbols-outlined {
+.file-dropzone-content > .material-symbols-outlined,
+.file-dropzone-content > .fluent-icon {
color: var(--primary);
- font-size: 34px;
+ height: 34px;
+ width: 34px;
}
.file-dropzone-content strong {
@@ -1237,7 +1304,8 @@ tbody tr:hover td.action-col {
padding: 10px;
}
-.file-preview > .material-symbols-outlined {
+.file-preview > .material-symbols-outlined,
+.file-preview > .fluent-icon {
align-items: center;
background: var(--primary-container);
border-radius: var(--radius);
@@ -1798,3 +1866,323 @@ tbody tr:hover td.action-col {
width: 100%;
}
}
+
+/* --------------------------------------------------------------------------
+ Phenikaa-X application theme
+ Keeps the existing EJS/JavaScript contract while applying Layer-1 tokens.
+ -------------------------------------------------------------------------- */
+
+::selection {
+ background: var(--orange-100);
+ color: var(--text-primary);
+}
+
+:focus-visible {
+ outline: 2px solid var(--blue-500);
+ outline-offset: 2px;
+}
+
+.sidebar {
+ background: var(--blue-900);
+ border-color: var(--neutral-700);
+}
+
+.brand-block {
+ border-bottom: 1px solid rgba(197, 204, 224, 0.18);
+ min-height: 72px;
+ padding: var(--space-md);
+}
+
+.brand-mark {
+ background: var(--brand-accent);
+ border-radius: var(--radius-md);
+ box-shadow: none;
+ color: var(--neutral-0);
+ height: 40px;
+ width: 40px;
+}
+
+.brand-copy strong {
+ color: var(--neutral-0);
+ font-size: 15px;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+}
+
+.brand-copy span,
+.sidebar .nav-label,
+.sidebar-copyright {
+ color: var(--blue-100);
+}
+
+.brand-copy span,
+.nav-label {
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+}
+
+.nav-section {
+ gap: var(--space-xxs);
+ padding: var(--space-sm);
+}
+
+.nav-item {
+ border-left-width: 4px;
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
+ color: var(--blue-100);
+ font-size: 13px;
+ font-weight: 500;
+ gap: var(--space-sm);
+ min-height: 44px;
+}
+
+.nav-item:hover {
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--neutral-0);
+}
+
+.nav-item.active {
+ background: rgba(255, 255, 255, 0.12);
+ border-left-color: var(--brand-accent);
+ color: var(--neutral-0);
+ font-weight: 600;
+}
+
+.topbar {
+ background: rgba(255, 255, 255, 0.96);
+ border-color: var(--border-subtle);
+ min-height: 64px;
+}
+
+.page {
+ padding: var(--space-lg);
+}
+
+.page-header h1,
+.auth-heading h1 {
+ color: var(--text-primary);
+ font-size: 25px;
+ font-weight: 700;
+ letter-spacing: -0.025em;
+ line-height: 1.2;
+}
+
+.panel,
+.table-panel,
+.metric-card,
+.modal-content,
+.notification-panel,
+.empty-state,
+.auth-panel {
+ background: var(--surface-base);
+ border-color: var(--border-subtle);
+ border-radius: var(--radius-md);
+ box-shadow: var(--elevation-1);
+}
+
+.modal-content,
+.notification-panel,
+.auth-panel {
+ box-shadow: var(--elevation-2);
+}
+
+.panel-header,
+.modal-header,
+.modal-actions,
+.page-filters,
+thead,
+td {
+ border-color: var(--border-subtle);
+}
+
+.btn,
+.icon-button,
+input,
+select,
+textarea {
+ border-radius: var(--radius-md);
+}
+
+.btn {
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.btn-primary {
+ background: var(--brand-primary);
+ color: var(--neutral-0);
+}
+
+.btn-primary:hover {
+ background: var(--brand-primary-strong);
+}
+
+.btn-warning {
+ background: var(--brand-accent);
+ color: var(--neutral-900);
+}
+
+.btn-warning:hover {
+ background: var(--brand-accent-strong);
+ color: var(--neutral-0);
+}
+
+.btn-secondary,
+.icon-button {
+ background: var(--surface-base);
+ border-color: var(--border-subtle);
+ color: var(--text-secondary);
+}
+
+.btn-secondary:hover,
+.icon-button:hover,
+.icon-button.subtle:hover {
+ background: var(--blue-50);
+ border-color: var(--blue-200);
+ color: var(--brand-primary);
+}
+
+.form-field input,
+.form-field select,
+.form-field textarea,
+.filter-field input,
+.filter-field select,
+input,
+select,
+textarea {
+ background: var(--surface-base);
+ border-color: var(--border-strong);
+ color: var(--text-primary);
+}
+
+.form-field input:focus,
+.form-field select:focus,
+.form-field textarea:focus,
+.filter-field input:focus,
+.filter-field select:focus,
+input:focus,
+select:focus,
+textarea:focus {
+ border-color: var(--brand-primary);
+ box-shadow: 0 0 0 3px rgba(34, 55, 113, 0.16);
+}
+
+thead {
+ background: var(--surface-brand-tint);
+}
+
+th {
+ color: var(--blue-700);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.045em;
+}
+
+td {
+ font-size: 13px;
+}
+
+tbody tr:hover {
+ background: var(--blue-50);
+}
+
+.badge {
+ border-radius: var(--radius-sm);
+ font-weight: 600;
+}
+
+.badge-success {
+ background: var(--semantic-success-bg);
+ color: var(--semantic-success);
+}
+
+.badge-danger {
+ background: var(--semantic-error-bg);
+ color: var(--semantic-error);
+}
+
+.badge-warning {
+ background: var(--semantic-warning-bg);
+ color: var(--semantic-warning);
+}
+
+.badge-info,
+.badge-primary {
+ background: var(--semantic-info-bg);
+ color: var(--semantic-info);
+}
+
+.empty-state > .fluent-icon {
+ color: var(--brand-primary);
+ height: 42px;
+ width: 42px;
+}
+
+.error-state > .fluent-icon {
+ color: var(--semantic-error);
+}
+
+.auth-shell {
+ background:
+ linear-gradient(135deg, rgba(30, 34, 67, 0.98), rgba(34, 55, 113, 0.93) 42%, rgba(247, 248, 251, 0.98) 42.1%),
+ var(--surface-raised);
+}
+
+.auth-panel {
+ border-top: 4px solid var(--brand-accent);
+ gap: var(--space-lg);
+ padding: var(--space-lg);
+}
+
+.auth-brand .brand-copy strong {
+ color: var(--text-primary);
+}
+
+.auth-brand .brand-copy span {
+ color: var(--text-tertiary);
+}
+
+.auth-confirm-icon {
+ background: var(--blue-50);
+ border-radius: var(--radius-md);
+ color: var(--brand-primary);
+}
+
+.auth-confirm-icon .fluent-icon {
+ height: 28px;
+ width: 28px;
+}
+
+.toast {
+ background: var(--blue-900);
+ border-radius: var(--radius-md);
+ box-shadow: var(--elevation-2);
+}
+
+@media (max-width: 900px) {
+ .sidebar {
+ box-shadow: var(--elevation-2);
+ }
+}
+
+@media (max-width: 560px) {
+ .page {
+ padding: var(--space-md);
+ }
+
+ .auth-page {
+ padding: var(--space-md);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ }
+}
diff --git a/web-server/public/js/app.js b/web-server/public/js/app.js
index b644ad6..b2134f4 100644
--- a/web-server/public/js/app.js
+++ b/web-server/public/js/app.js
@@ -2,6 +2,64 @@
const body = document.body;
const menuButton = document.getElementById('mobileMenuBtn');
const sidebarBackdrop = document.getElementById('sidebarBackdrop');
+ const FLUENT_ICON_FILES = Object.freeze({
+ add: 'add_20_regular.svg',
+ add_box: 'box_24_regular.svg',
+ apps: 'apps_24_regular.svg',
+ archive: 'archive_24_regular.svg',
+ attach_file: 'attach_20_regular.svg',
+ campaign: 'megaphone_20_regular.svg',
+ check_circle: 'checkmark_circle_20_filled.svg',
+ close: 'dismiss_20_regular.svg',
+ dashboard: 'grid_24_regular.svg',
+ database_off: 'database_warning_20_regular.svg',
+ delete: 'delete_20_regular.svg',
+ deployed_code: 'cube_24_regular.svg',
+ download: 'arrow_download_20_regular.svg',
+ draft: 'document_24_regular.svg',
+ edit: 'edit_20_regular.svg',
+ error: 'error_circle_20_filled.svg',
+ forward_to_inbox: 'mail_arrow_forward_20_regular.svg',
+ group: 'people_24_regular.svg',
+ inventory_2: 'box_24_regular.svg',
+ link_off: 'link_dismiss_20_regular.svg',
+ login: 'arrow_enter_20_regular.svg',
+ logout: 'sign_out_20_regular.svg',
+ mark_email_unread: 'mail_unread_24_regular.svg',
+ memory: 'memory_16_regular.svg',
+ menu: 'navigation_20_regular.svg',
+ notifications: 'alert_20_regular.svg',
+ notifications_none: 'alert_off_20_regular.svg',
+ person_add: 'person_add_20_regular.svg',
+ precision_manufacturing: 'bot_24_filled.svg',
+ save: 'save_20_regular.svg',
+ search_off: 'search_24_regular.svg',
+ send: 'send_20_regular.svg',
+ stars: 'star_20_filled.svg',
+ swap_horiz: 'arrow_swap_20_regular.svg',
+ sync: 'arrow_sync_20_regular.svg',
+ upgrade: 'arrow_up_20_regular.svg',
+ upload: 'arrow_upload_20_regular.svg',
+ upload_file: 'document_arrow_up_24_regular.svg',
+ visibility: 'eye_20_regular.svg',
+ warning: 'warning_20_filled.svg'
+ });
+
+ function applyFluentIcon(element, iconName) {
+ if (!element) return;
+ const fileName = FLUENT_ICON_FILES[iconName] || 'question_20_regular.svg';
+ element.classList.remove('material-symbols-outlined');
+ element.classList.add('fluent-icon');
+ element.style.setProperty('--fluent-icon-url', `url("/vendor/fluent-icons/${fileName}")`);
+ element.setAttribute('aria-hidden', 'true');
+ element.textContent = '';
+ }
+
+ function hydrateFluentIcons(root) {
+ root.querySelectorAll('.material-symbols-outlined').forEach((element) => {
+ applyFluentIcon(element, element.textContent.trim());
+ });
+ }
function initNotiflix() {
if (!window.Notiflix) return;
@@ -11,8 +69,8 @@
position: 'right-top',
distance: '16px',
timeout: 2600,
- borderRadius: '8px',
- fontFamily: 'Inter, sans-serif',
+ borderRadius: '4px',
+ fontFamily: 'Montserrat Variable, Montserrat, sans-serif',
fontSize: '13px',
messageMaxLength: 180,
clickToClose: true,
@@ -21,35 +79,35 @@
useIcon: true,
zindex: 5000,
success: {
- background: '#067647',
+ background: '#2e7d32',
textColor: '#ffffff'
},
failure: {
- background: '#b42318',
+ background: '#c62828',
textColor: '#ffffff'
},
warning: {
- background: '#b54708',
+ background: '#9a5b00',
textColor: '#ffffff'
},
info: {
- background: '#3755c3',
+ background: '#223771',
textColor: '#ffffff'
}
});
window.Notiflix.Confirm.init({
width: '360px',
- borderRadius: '8px',
- fontFamily: 'Inter, sans-serif',
- titleColor: '#111827',
+ borderRadius: '4px',
+ fontFamily: 'Montserrat Variable, Montserrat, sans-serif',
+ titleColor: '#161824',
titleFontSize: '16px',
- messageColor: '#475569',
+ messageColor: '#565b70',
messageFontSize: '13px',
- okButtonBackground: '#3755c3',
+ okButtonBackground: '#223771',
okButtonColor: '#ffffff',
- cancelButtonBackground: '#e2e8f0',
- cancelButtonColor: '#334155',
+ cancelButtonBackground: '#eeedf6',
+ cancelButtonColor: '#3c4054',
backOverlayColor: 'rgba(15, 23, 42, 0.42)',
zindex: 5001,
cssAnimationStyle: 'zoom'
@@ -701,9 +759,7 @@
const icon = document.createElement('span');
icon.className = 'notification-item-icon';
const iconGlyph = document.createElement('span');
- iconGlyph.className = 'material-symbols-outlined';
- iconGlyph.setAttribute('aria-hidden', 'true');
- iconGlyph.textContent = getNotificationIcon(notification);
+ applyFluentIcon(iconGlyph, getNotificationIcon(notification));
icon.appendChild(iconGlyph);
const copy = document.createElement('span');
@@ -926,6 +982,7 @@
});
}
+ hydrateFluentIcons(document);
initNotiflix();
initFileDropzones();
initRegistrationUniqueChecks();
diff --git a/web-server/server.js b/web-server/server.js
index 589eb8c..6755253 100644
--- a/web-server/server.js
+++ b/web-server/server.js
@@ -127,6 +127,8 @@ app.use(express.urlencoded({ extended: true }));
app.use(express.json({ limit: '32kb' }));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/vendor/notiflix', express.static(path.join(__dirname, 'node_modules/notiflix/dist')));
+app.use('/vendor/montserrat', express.static(path.join(__dirname, 'node_modules/@fontsource-variable/montserrat')));
+app.use('/vendor/fluent-icons', express.static(path.join(__dirname, 'node_modules/@fluentui/svg-icons/icons')));
app.use(applyPublicApiCors);
app.get('/packages/agent/latest.deb', asyncRoute(async (req, res) => {
const arch = normalizeAgentArch(req.query.arch);
diff --git a/web-server/views/auth.ejs b/web-server/views/auth.ejs
index eddbcf2..0e288cc 100644
--- a/web-server/views/auth.ejs
+++ b/web-server/views/auth.ejs
@@ -4,10 +4,7 @@
<%= title %> | Robot Installer
-
-
-
-
+
diff --git a/web-server/views/confirm-email-sent.ejs b/web-server/views/confirm-email-sent.ejs
index 69f553a..9317c52 100644
--- a/web-server/views/confirm-email-sent.ejs
+++ b/web-server/views/confirm-email-sent.ejs
@@ -4,10 +4,7 @@
<%= title %> | Robot Installer
-
-
-
-
+
diff --git a/web-server/views/partials/page-start.ejs b/web-server/views/partials/page-start.ejs
index 6245b56..f806690 100644
--- a/web-server/views/partials/page-start.ejs
+++ b/web-server/views/partials/page-start.ejs
@@ -4,10 +4,7 @@
<%= title %> | Robot Installer
-
-
-
-
+