diff --git a/src/interfaces/api/handlers/grant_handler.rs b/src/interfaces/api/handlers/grant_handler.rs index 77ab00ee..bdd49756 100644 --- a/src/interfaces/api/handlers/grant_handler.rs +++ b/src/interfaces/api/handlers/grant_handler.rs @@ -676,12 +676,13 @@ pub async fn list_my_shares( file_idx += 1; match result { Ok(file_dto) => { + // Caller is the granter — they had share-access to the + // resource, so the containing hierarchy is already known + // to them. Keep `path` (unlike list_shared_with_me). items.push(OutgoingResourceItemDto { resource_type: ResourceTypeDto::File, first_shared_at: summary.first_shared_at, - resource: ResourceContentDto::File( - file_dto.clone().without_hierarchy_info(), - ), + resource: ResourceContentDto::File(file_dto.clone()), grants, }); } @@ -708,9 +709,7 @@ pub async fn list_my_shares( items.push(OutgoingResourceItemDto { resource_type: ResourceTypeDto::Folder, first_shared_at: summary.first_shared_at, - resource: ResourceContentDto::Folder( - folder_dto.clone().without_hierarchy_info(), - ), + resource: ResourceContentDto::Folder(folder_dto.clone()), grants, }); } diff --git a/static/admin.html b/static/admin.html index fa4bb92b..bc82c574 100644 --- a/static/admin.html +++ b/static/admin.html @@ -3,6 +3,10 @@ + + OxiCloud — Admin Panel diff --git a/static/css/base/variables.css b/static/css/base/variables.css index cb953e83..78496da7 100644 --- a/static/css/base/variables.css +++ b/static/css/base/variables.css @@ -1,49 +1,72 @@ +/* + * OxiCloud design tokens. + * + * Single source of truth for light + dark colours. Each token whose value + * differs between modes uses `light-dark(LIGHT, DARK)`, which the browser + * resolves against the page's `color-scheme`. + * + * Mode switching: + * • `` in declares + * both schemes are supported. + * • `:root { color-scheme: light dark }` (default) lets the UA follow the + * OS preference (`prefers-color-scheme`). + * • `html[data-color-scheme="light"]` / `…="dark"` force a specific mode. + * `theme-init.js` sets the attribute from localStorage. + * + * Fallback: browsers that don't support `light-dark()` (Chrome < 123 / + * Safari < 17.5 / Firefox < 120) hit a `@supports not (...)` block in + * `themes/dark.css` that still applies the old `[data-theme="dark"]` overrides. + */ + :root { + /* Default: follow the OS preference. Overridden by html[data-color-scheme]. */ + color-scheme: light dark; + --sidebar-width: 250px; /* Backgrounds */ - --color-bg-page: #f5f7fa; - --color-bg-surface: #ffffff; - --color-bg-input: #f9fafb; - --color-bg-hover: #f8fafc; - --color-bg-muted: #f0f3f7; - --color-bg-subtle: #f8f9fa; - --color-bg-alt: #f7fafc; - --color-bg-input-alt: #edf2f7; - --color-bg-empty: #f0f0f0; + --color-bg-page: light-dark(#f5f7fa, #0f172a); + --color-bg-surface: light-dark(#ffffff, #1e293b); + --color-bg-input: light-dark(#f9fafb, #0f172a); + --color-bg-hover: light-dark(#f8fafc, #334155); + --color-bg-muted: light-dark(#f0f3f7, #1a2540); + --color-bg-subtle: light-dark(#f8f9fa, #162032); + --color-bg-alt: light-dark(#f7fafc, #0f172a); + --color-bg-input-alt: light-dark(#edf2f7, #253045); + --color-bg-empty: light-dark(#f0f0f0, #253045); /* Borders */ - --color-border: #e2e8f0; - --color-border-light: #f1f5f9; - --color-border-medium: #cbd5e0; - --color-border-faint: #e0e6ed; - --color-border-subtle: #e0e5e8; - --color-border-xfaint: #f0f0f0; - --color-border-ddd: #ddd; + --color-border: light-dark(#e2e8f0, #334155); + --color-border-light: light-dark(#f1f5f9, #334155); + --color-border-medium: light-dark(#cbd5e0, #475569); + --color-border-faint: light-dark(#e0e6ed, #2a3650); + --color-border-subtle: light-dark(#e0e5e8, #2a3650); + --color-border-xfaint: light-dark(#f0f0f0, #1e293b); + --color-border-ddd: light-dark(#ddd, #334155); /* Text */ - --color-text: #2d3748; - --color-text-heading: #1e293b; - --color-text-muted: #718096; - --color-text-faint: #94a3b8; - --color-text-secondary: #4a5568; - --color-text-subtle: #64748b; - --color-text-dark: #334155; - --color-text-placeholder: #a0aec0; - --color-text-gray: #6c757d; - --color-text-medium: #666; - --color-text-faint2: #888; - --color-text-light: #999; - --color-text-black: #333; - --color-text-dim: #555; + --color-text: light-dark(#2d3748, #e2e8f0); + --color-text-heading: light-dark(#1e293b, #f1f5f9); + --color-text-muted: light-dark(#718096, #94a3b8); + --color-text-faint: light-dark(#94a3b8, #7a8a9f); + --color-text-secondary: light-dark(#4a5568, #cbd5e1); + --color-text-subtle: light-dark(#64748b, #94a3b8); + --color-text-dark: light-dark(#334155, #cbd5e1); + --color-text-placeholder: light-dark(#a0aec0, #64748b); + --color-text-gray: light-dark(#6c757d, #94a3b8); + --color-text-medium: light-dark(#666, #94a3b8); + --color-text-faint2: light-dark(#888, #64748b); + --color-text-light: light-dark(#999, #64748b); + --color-text-black: light-dark(#333, #e2e8f0); + --color-text-dim: light-dark(#555, #94a3b8); - /* Accent */ + /* Accent (orange) — mostly mode-agnostic. */ --color-accent: #ff5e3a; - --color-accent-hover: #e04520; + --color-accent-hover: light-dark(#e04520, #ff7a5c); --color-accent-gradient: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%); --color-accent-shadow: rgba(255, 94, 58, 0.3); - --color-accent-ring: rgba(255, 94, 58, 0.1); - --color-accent-tint: #fff5f3; + --color-accent-ring: light-dark(rgba(255, 94, 58, 0.1), rgba(255, 94, 58, 0.15)); + --color-accent-tint: light-dark(#fff5f3, #2a1a15); --color-accent-mid: #ff8a5c; --color-accent-shadow-lg: rgba(255, 94, 58, 0.4); --color-accent-bg: rgba(255, 94, 58, 0.06); @@ -54,15 +77,15 @@ --color-accent-ring-xs: rgba(255, 94, 58, 0.05); /* Feedback */ - --color-error-bg: #fee2e2; - --color-error-text: #b91c1c; - --color-success-bg: #dcfce7; - --color-success-text: #15803d; + --color-error-bg: light-dark(#fee2e2, #3b1111); + --color-error-text: light-dark(#b91c1c, #fca5a5); + --color-success-bg: light-dark(#dcfce7, #052e16); + --color-success-text: light-dark(#15803d, #86efac); --color-success-border: #48bb78; --color-success-alt: #34c759; - --color-success-bg-alt: #e6f4ea; + --color-success-bg-alt: light-dark(#e6f4ea, #0a2015); --color-success-text-alt: #0d904f; - --color-success-bg-green: #e8f5e9; + --color-success-bg-green: light-dark(#e8f5e9, #0a2015); --color-success-text-green: #4eaa25; /* Dangerous actions */ @@ -72,48 +95,48 @@ --color-danger-alt: #ef4444; --color-danger-ring: rgba(239, 68, 68, 0.3); --color-danger-ring-lg: rgba(239, 68, 68, 0.4); - --color-danger-light-bg: #fef2f2; - --color-danger-lighter: #fef2f2; + --color-danger-light-bg: light-dark(#fef2f2, #2a0c0c); + --color-danger-lighter: light-dark(#fef2f2, #2a0c0c); --color-danger-text-alt: #e53e3e; --color-danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); /* Warning/feedback */ - --color-warning-bg: #ffeaa7; - --color-warning-bg-dark: #fdcb6e; + --color-warning-bg: light-dark(#ffeaa7, #3d2e00); + --color-warning-bg-dark: light-dark(#fdcb6e, #5a4200); --color-warning-text: #ffc107; --color-warning-border: #ffc107; --color-warning-ring: rgba(255, 193, 7, 0.1); --color-warning-shadow: rgba(255, 193, 7, 0.5); - --color-warning-orange-bg: #fff3e0; + --color-warning-orange-bg: light-dark(#fff3e0, #2a1c10); --color-warning-orange-border: #f57c00; - --color-warning-orange-text: #f57c00; - --color-warning-bg-light: #fef3c7; - --color-warning-text-amber: #f59e0b; + --color-warning-orange-text: light-dark(#f57c00, #fb923c); + --color-warning-bg-light: light-dark(#fef3c7, #2a2410); + --color-warning-text-amber: light-dark(#f59e0b, #fbbf24); --color-warning-bg-orange: #fef3e2; --color-warning-text-orange: #d04423; /* Info */ - --color-info-bg: #ebf5fb; + --color-info-bg: light-dark(#ebf5fb, #0c1e35); --color-info-border: #2b6cb0; --color-info-text: #2b6cb0; - --color-info-bg-alt: #e0ecff; + --color-info-bg-alt: light-dark(#e0ecff, #0c1e35); --color-info-text-alt: #3171d8; - --color-info-surface: #e0e7ff; + --color-info-surface: light-dark(#e0e7ff, #0c1e35); --color-info-blue: #3b82f6; - /* Shadows */ - --color-shadow: rgba(0, 0, 0, 0.1); - --color-shadow-lg: rgba(0, 0, 0, 0.12); + /* Shadows — slightly stronger in dark mode for parity. */ + --color-shadow: light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3)); + --color-shadow-lg: light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.3)); --color-shadow-xs: rgba(0, 0, 0, 0.05); --color-shadow-sm: rgba(0, 0, 0, 0.08); - --color-shadow-md: rgba(0, 0, 0, 0.15); + --color-shadow-md: light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.3)); --color-shadow-xl: rgba(0, 0, 0, 0.2); --color-shadow-2xl: rgba(0, 0, 0, 0.25); --color-shadow-3xl: rgba(0, 0, 0, 0.3); --color-shadow-4xl: rgba(0, 0, 0, 0.4); --color-shadow-5xl: rgba(0, 0, 0, 0.5); - /* Overlays */ + /* Overlays — same in both modes; they sit on top of arbitrary content. */ --color-overlay: rgba(0, 0, 0, 0.5); --color-overlay-light: rgba(0, 0, 0, 0.45); --color-overlay-heavy: rgba(0, 0, 0, 0.85); @@ -129,15 +152,15 @@ /* Items */ --color-item: var(--color-bg-surface); --color-item-hover: var(--color-bg-hover); - --color-item-active: #f8d2ae; - --color-item-selected: #fff8f6; - --color-item-hover-accent: #fff0ec; + --color-item-active: light-dark(#f8d2ae, #5a5047); + --color-item-selected: light-dark(#fff8f6, #39281a); + --color-item-hover-accent: light-dark(#fff0ec, #3d342c); --color-item-hover-blue: #f0f8ff; --color-item-hover-sky: #e0f2fe; - /* Sidebar — always dark */ - --color-sidebar-bg-from: #2a3042; - --color-sidebar-bg-to: #232838; + /* Sidebar — already dark-leaning in both modes; dark mode goes deeper. */ + --color-sidebar-bg-from: light-dark(#2a3042, #0f172a); + --color-sidebar-bg-to: light-dark(#232838, #0c1322); --color-sidebar-text: rgba(255, 255, 255, 0.65); --color-sidebar-text-hover: rgba(255, 255, 255, 0.9); --color-sidebar-text-active: #ffffff; @@ -236,7 +259,7 @@ --color-multiselect-danger-text-active: #ffffff; /* Notification */ - --color-notification-bg: #ffffff; + --color-notification-bg: light-dark(#ffffff, #1e293b); --color-notification-badge: #ff3b30; --color-notification-success: #34c759; --color-notification-error: #ff3b30; @@ -252,7 +275,7 @@ --color-lightbox-text-muted: rgba(255, 255, 255, 0.7); /* Purple/special */ - --color-purple-bg: #f3e8ff; + --color-purple-bg: light-dark(#f3e8ff, #1a0a33); --color-purple-border: #7c3aed; --color-purple-text: #7c3aed; --color-purple-bg-gradient: linear-gradient(135deg, #e0e7ff 0%, #ede9fe 50%, #fce7f3 100%); @@ -275,14 +298,14 @@ /* Content area */ --color-content-muted: #888; - --color-content-bg-warn: #ffeaa7; - --color-content-bg-warn-dark: #fdcb6e; + --color-content-bg-warn: light-dark(#ffeaa7, #3d2e00); + --color-content-bg-warn-dark: light-dark(#fdcb6e, #5a4200); --color-content-debug-bg: red; --color-content-debug-text: white; /* User menu */ - --color-user-menu-header-bg: linear-gradient(135deg, #fef5f3 0%, #fdf2f8 100%); - --color-user-menu-header-border: #fce7e1; + --color-user-menu-header-bg: light-dark(linear-gradient(135deg, #fef5f3 0%, #fdf2f8 100%), linear-gradient(135deg, #1a2332 0%, #1e2940 100%)); + --color-user-menu-header-border: light-dark(#fce7e1, #3a2520); /* Share dialog */ --color-share-link-text: #1565c0; @@ -291,8 +314,8 @@ --color-share-owner-text: #757575; /* Primary (style.css) */ - --color-primary: #2563eb; - --color-primary-hover: #1d4ed8; + --color-primary: light-dark(#2563eb, #60a5fa); + --color-primary-hover: light-dark(#1d4ed8, #6fa8ee); /* Recent view */ --color-recent-muted: #6c757d; @@ -367,13 +390,13 @@ --color-storage-fill-orange: linear-gradient(90deg, #d97706, #f59e0b); --color-storage-fill-red: linear-gradient(90deg, #dc2626, #ef4444); - /* Error text (dark shade) */ - --color-error-text-dark: #991b1b; + /* Error text (dark shade) — light-mode is dark red, dark-mode is light red. */ + --color-error-text-dark: light-dark(#991b1b, #f87171); /* Stat warning border */ --color-stat-warn-border: #fbbf24; - /* Status badge — light mode (success/emerald) */ + /* Status badge — success/emerald */ --color-badge-success-bg: #ecfdf5; --color-badge-success-bg-medium: #d1fae5; --color-badge-success-text: #065f46; @@ -384,32 +407,33 @@ --color-badge-green-bg: #ecfdf5; --color-badge-green-text: #065f46; - /* Status badge — light mode (orange/coral) */ - --color-badge-orange-bg: #fff5f3; - --color-badge-orange-text: #ff5e3a; + /* Status badge — orange/coral (used by role-chip & user-vignette) */ + --color-badge-orange-bg: light-dark(#fff5f3, #2a1814); + --color-badge-orange-text: light-dark(#ff5e3a, #ff8a65); - /* Status badge — light mode (error/red) */ + /* Status badge — error/red */ --color-badge-error-border: #fecaca; - /* Status badge — light mode (warning/amber) */ + /* Status badge — warning/amber */ --color-badge-warning-text: #92400e; --color-badge-warning-border: #fde68a; --color-badge-amber-bg: #fef3c7; --color-badge-amber-text: #f59e0b; - /* Status badge — light mode (indigo/purple) */ + /* Status badge — indigo/purple */ --color-badge-indigo-bg: #ede9fe; --color-badge-indigo-text: #6d28d9; - /* Status badge — light mode (blue) */ - --color-badge-blue-bg: #eff6ff; - --color-badge-blue-text: #1e40af; + /* Status badge — blue (used by role-chip & user-vignette) */ + --color-badge-blue-bg: light-dark(#eff6ff, #0c2d48); + --color-badge-blue-text: light-dark(#1e40af, #93c5fd); --color-badge-blue-border: #bfdbfe; - /* Status badge — light mode (gray/disabled) */ + /* Status badge — gray/disabled */ --color-badge-gray: #d1d5db; - /* FIXME: Status badge — dark mode */ + /* FIXME: legacy dark-mode badge tokens — kept for now; can be folded into + * the main badges once their last consumers migrate. */ --color-badge-success-dark-bg: #052e16; --color-badge-success-dark-border: #065f46; --color-badge-error-dark-bg: #3b1111; @@ -439,3 +463,13 @@ --color-video-play: white; --color-video-play-shadow: black; } + +/* Explicit user choice overrides the OS preference. `theme-init.js` writes + * the attribute from localStorage on render-blocking startup. */ +html[data-color-scheme="light"] { + color-scheme: light; +} + +html[data-color-scheme="dark"] { + color-scheme: dark; +} diff --git a/static/css/components/itemTooltip.css b/static/css/components/itemTooltip.css index eaf3b5d8..1248d773 100644 --- a/static/css/components/itemTooltip.css +++ b/static/css/components/itemTooltip.css @@ -1,29 +1,19 @@ /* ── Item tooltip — "technical sheet" ─────────────────────────────────────── */ /* */ -/* Overlays the search bar area in the top-bar while hovering a file item. */ -/* Spans from the sidebar right-edge to just before .user-controls. */ +/* Absolutely overlays `.search-slot` in the top-bar. The search bar stays in */ +/* place underneath; only the tooltip's opacity is animated, so there is no */ +/* second element to keep in sync and no display:none flicker between rapid */ +/* hover transitions. RTL just works (inset:0). */ /* */ -/* 3-column CSS grid: [icon] [label] [value] */ -/* All values start at the same x position regardless of label width. */ +/* 3-column CSS grid: [icon] [label] [value] */ +/* All values start at the same x position regardless of label width. */ .path-tooltip { - position: fixed; - - /* Fill most of the top-bar (topbar height 70px, 8px margin top/bottom) */ - top: 8px; - - /* Desktop: starts at sidebar right edge + top-bar padding */ - left: calc(var(--sidebar-width) + 30px); - - /* - * Right edge stops just before .user-controls: - * 30px (top-bar right padding) + ~90px (notif bell + gap + avatar btn) + 4px gap - * Using left + right instead of width so the browser computes it dynamically. - */ - right: 124px; - - /* High enough to sit above the top-bar content */ - z-index: 200; + /* Cover the search slot, sit above it (later in DOM order). */ + position: absolute; + inset: 0; + /* Opaque background so the search bar underneath is fully masked. */ + background-color: var(--color-bg-surface); display: grid; grid-template-columns: 1em max-content 1fr; @@ -31,20 +21,24 @@ row-gap: 6px; align-items: center; - /* Fill the top-bar height minus an 8px margin on each side */ - height: 54px; - padding: 0 16px; - border-radius: 10px; - border: 2px solid var(--color-border-medium); - - background-color: var(--color-bg-input); color: var(--color-text-muted); font-size: 0.75rem; pointer-events: none; - /* avoid blinking when pointer moves between items */ - transition: display 0.2s allow-discrete; + /* Show instantly. The hide direction (.hidden) overrides this with a + 1s delay so the tooltip lingers after the mouse leaves an item; + rapid item-to-item moves cancel the pending fade before it fires. */ + opacity: 1; + transition: opacity 0.2s ease 0s; +} + +/* Hidden state — override the global `.hidden { display:none !important }` + so we can fade with opacity instead of snapping to display:none. */ +.path-tooltip.hidden { + display: grid !important; + opacity: 0; + transition-delay: 1s; } /* Icon column */ @@ -85,23 +79,6 @@ font-style: italic; } -/* ── Narrow screens (sidebar hidden) ────────────────────────────────────── */ -/* On mobile the sidebar slides off-screen; align after the #sidebar-toggle */ -/* button (≈44px) that takes its place in the top-bar. */ - -@media (max-width: 768px) { - .path-tooltip { - /* 16px topbar padding + ~44px sidebar-toggle + 8px gap */ - left: 68px; - - /* - * Right edge stops before search-toggle-btn + user-controls: - * ~40px (search-toggle) + 12px (gap) + ~90px (user-controls) + 16px (padding) - */ - right: 158px; - } -} - /* ── Vignette inside tooltip ─────────────────────────────────────────────── */ /* The vignette name hard-codes 14px; scale it down to match the tooltip. */ diff --git a/static/css/components/userMenu.css b/static/css/components/userMenu.css index 1a7ede6f..68f559c6 100644 --- a/static/css/components/userMenu.css +++ b/static/css/components/userMenu.css @@ -182,34 +182,58 @@ color: var(--color-text-subtle); } -.user-menu-item .theme-toggle-pill { +/* The appearance row is a plain container, not a button — the inner + * segmented control captures the clicks. Match the height of other + * .user-menu-item rows so the row reads as part of the same list. */ +.user-menu-item--theme { + cursor: default; +} + +.user-menu-item--theme:hover { + background: transparent; +} + +/* Light / Like OS / Dark — three-option pill, active option highlighted + * with the accent colour. Sits at the right edge of the row. */ +.theme-segmented { margin-left: auto; - width: 40px; + display: inline-flex; + background: var(--color-bg-muted); + border: 1px solid var(--color-border); + border-radius: 999px; + padding: 2px; + gap: 2px; +} + +.theme-segmented__opt { + width: 28px; height: 22px; - background: var(--color-border); - border-radius: 11px; - position: relative; - transition: background 0.3s ease; + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--color-text-subtle); + border-radius: 999px; + cursor: pointer; + font-size: 11px; + padding: 0; + transition: + background 0.15s ease, + color 0.15s ease; } -.user-menu-item .theme-toggle-pill.active { +.theme-segmented__opt:hover { + color: var(--color-text); +} + +.theme-segmented__opt--active { background: var(--color-accent); + color: var(--color-danger-text); } -.user-menu-item .theme-toggle-knob { - width: 18px; - height: 18px; - background: var(--color-danger-text); - border-radius: 50%; - position: absolute; - top: 2px; - left: 2px; - transition: transform 0.3s ease; - box-shadow: 0 1px 4px var(--color-shadow-md); -} - -.user-menu-item .theme-toggle-pill.active .theme-toggle-knob { - transform: translateX(18px); +.theme-segmented__opt--active:hover { + color: var(--color-danger-text); } .user-menu-admin { diff --git a/static/css/layout/topbar.css b/static/css/layout/topbar.css index d0a392ea..d0f27eda 100644 --- a/static/css/layout/topbar.css +++ b/static/css/layout/topbar.css @@ -86,11 +86,14 @@ color: var(--color-text-heading); } -.search-container { +/* Slot wrapping .search-container + #path-tooltip — it owns the flex slot + so the tooltip can absolutely overlay the search bar via inset:0 instead + of viewport-fixed positioning. */ +.search-slot { flex-grow: 1; max-width: 600px; - position: relative; margin-right: 20px; + position: relative; display: flex; align-items: center; @@ -100,6 +103,13 @@ } } +.search-container { + flex-grow: 1; + position: relative; + display: flex; + align-items: center; +} + .search-container input { width: 100%; padding: 12px 50px 12px 44px; @@ -204,8 +214,8 @@ justify-content: center; } - /* Hide full search bar on mobile by default */ - .search-container { + /* Hide full search slot on mobile by default */ + .search-slot { display: none; } @@ -224,7 +234,7 @@ justify-content: center; } - .top-bar--search-active .search-container { + .top-bar--search-active .search-slot { display: flex; flex-grow: 1; max-width: none; diff --git a/static/css/themes/dark.css b/static/css/themes/dark.css index 5f7b1df3..98b49b8d 100644 --- a/static/css/themes/dark.css +++ b/static/css/themes/dark.css @@ -1,122 +1,151 @@ -[data-theme="dark"] { - /* Backgrounds */ - --color-bg-page: #0f172a; - --color-bg-surface: #1e293b; - --color-bg-input: #0f172a; - --color-bg-hover: #334155; - --color-bg-muted: #1a2540; - --color-bg-subtle: #162032; - --color-bg-alt: #0f172a; - --color-bg-input-alt: #253045; - --color-bg-empty: #253045; +/* + * Legacy dark-mode fallback for browsers that don't support `light-dark()` + * (Chrome < 123 / Safari < 17.5 / Firefox < 120, all from early 2024). + * + * Modern browsers skip this entire stylesheet at parse time thanks to the + * `@supports not (color: light-dark(white, black))` gate — the merged tokens + * in `base/variables.css` handle dark mode declaratively via the + * `color-scheme` CSS property. + * + * On capable browsers this file becomes dead code; once the supported + * baseline raises beyond the cut-off, it can be deleted. + * + * The selector matches BOTH attribute names so the fallback works during + * the transitional window (when some users may still have `data-theme` + * stamped from cached app state): + * • [data-theme="dark"] — legacy attribute, never set by new code + * • html[data-color-scheme="dark"] — new attribute set by theme-init.js + */ - /* Borders */ - --color-border: #334155; - --color-border-light: #334155; - --color-border-medium: #475569; - --color-border-faint: #2a3650; - --color-border-subtle: #2a3650; - --color-border-xfaint: #1e293b; - --color-border-ddd: #334155; +@supports not (color: light-dark(white, black)) { + [data-theme="dark"], + html[data-color-scheme="dark"] { + /* Backgrounds */ + --color-bg-page: #0f172a; + --color-bg-surface: #1e293b; + --color-bg-input: #0f172a; + --color-bg-hover: #334155; + --color-bg-muted: #1a2540; + --color-bg-subtle: #162032; + --color-bg-alt: #0f172a; + --color-bg-input-alt: #253045; + --color-bg-empty: #253045; - /* Text */ - --color-text: #e2e8f0; - --color-text-heading: #f1f5f9; - --color-text-muted: #94a3b8; - --color-text-faint: #7a8a9f; - --color-text-secondary: #cbd5e1; - --color-text-subtle: #94a3b8; - --color-text-dark: #cbd5e1; - --color-text-placeholder: #64748b; - --color-text-gray: #94a3b8; - --color-text-medium: #94a3b8; - --color-text-faint2: #64748b; - --color-text-light: #64748b; - --color-text-black: #e2e8f0; - --color-text-dim: #94a3b8; + /* Borders */ + --color-border: #334155; + --color-border-light: #334155; + --color-border-medium: #475569; + --color-border-faint: #2a3650; + --color-border-subtle: #2a3650; + --color-border-xfaint: #1e293b; + --color-border-ddd: #334155; - /* Accent */ - --color-accent: #ff5e3a; - --color-accent-hover: #ff7a5c; - --color-accent-gradient: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%); - --color-accent-shadow: rgba(255, 94, 58, 0.3); - --color-accent-ring: rgba(255, 94, 58, 0.15); - --color-accent-tint: #2a1a15; + /* Text */ + --color-text: #e2e8f0; + --color-text-heading: #f1f5f9; + --color-text-muted: #94a3b8; + --color-text-faint: #7a8a9f; + --color-text-secondary: #cbd5e1; + --color-text-subtle: #94a3b8; + --color-text-dark: #cbd5e1; + --color-text-placeholder: #64748b; + --color-text-gray: #94a3b8; + --color-text-medium: #94a3b8; + --color-text-faint2: #64748b; + --color-text-light: #64748b; + --color-text-black: #e2e8f0; + --color-text-dim: #94a3b8; - /* Feedback */ - --color-error-bg: #3b1111; - --color-error-text: #fca5a5; - --color-success-bg: #052e16; - --color-success-text: #86efac; + /* Accent */ + --color-accent: #ff5e3a; + --color-accent-hover: #ff7a5c; + --color-accent-gradient: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%); + --color-accent-shadow: rgba(255, 94, 58, 0.3); + --color-accent-ring: rgba(255, 94, 58, 0.15); + --color-accent-tint: #2a1a15; - /* Shadows (stronger on dark mode) */ - --color-shadow: rgba(0, 0, 0, 0.3); - --color-shadow-lg: rgba(0, 0, 0, 0.3); - --color-shadow-md: rgba(0, 0, 0, 0.3); + /* Feedback */ + --color-error-bg: #3b1111; + --color-error-text: #fca5a5; + --color-success-bg: #052e16; + --color-success-text: #86efac; - /* Additional dark overrides */ - --color-warning-bg: #3d2e00; - --color-warning-bg-dark: #5a4200; - --color-notification-bg: #1e293b; - --color-user-menu-header-bg: linear-gradient(135deg, #1a2332 0%, #1e2940 100%); - --color-user-menu-header-border: #3a2520; - --color-info-bg: #0c1e35; - --color-info-bg-alt: #0c1e35; - --color-info-surface: #0c1e35; - --color-danger-light-bg: #2a0c0c; - --color-danger-lighter: #2a0c0c; - --color-error-text-dark: #f87171; + /* Shadows (stronger on dark mode) */ + --color-shadow: rgba(0, 0, 0, 0.3); + --color-shadow-lg: rgba(0, 0, 0, 0.3); + --color-shadow-md: rgba(0, 0, 0, 0.3); - /* Pastel chip backgrounds need dark equivalents — otherwise the light - * cream / white-blue / pink-cream backdrops glow against dark surfaces - * and the saturated text colours become unreadable on the now-dark fill. - * Same pattern: dark tinted background + lighter pastel text. */ - --color-badge-orange-bg: #2a1814; - --color-badge-orange-text: #ff8a65; - --color-badge-blue-bg: #0c2d48; - --color-badge-blue-text: #93c5fd; - --color-warning-bg-light: #2a2410; - --color-warning-text-amber: #fbbf24; - --color-warning-orange-bg: #2a1c10; - --color-warning-orange-text: #fb923c; - --color-purple-bg: #1a0a33; - --color-success-bg-alt: #0a2015; - --color-success-bg-green: #0a2015; - --color-content-bg-warn: #3d2e00; - --color-content-bg-warn-dark: #5a4200; + /* Additional dark overrides */ + --color-warning-bg: #3d2e00; + --color-warning-bg-dark: #5a4200; + --color-notification-bg: #1e293b; + --color-user-menu-header-bg: linear-gradient(135deg, #1a2332 0%, #1e2940 100%); + --color-user-menu-header-border: #3a2520; + --color-info-bg: #0c1e35; + --color-info-bg-alt: #0c1e35; + --color-info-surface: #0c1e35; + --color-danger-light-bg: #2a0c0c; + --color-danger-lighter: #2a0c0c; + --color-error-text-dark: #f87171; - /* Sidebar */ - --color-sidebar-bg-from: #0f172a; - --color-sidebar-bg-to: #0c1322; + /* Pastel chip backgrounds need dark equivalents — otherwise the light + * cream / white-blue / pink-cream backdrops glow against dark surfaces + * and the saturated text colours become unreadable on the now-dark fill. + * Same pattern: dark tinted background + lighter pastel text. */ + --color-badge-orange-bg: #2a1814; + --color-badge-orange-text: #ff8a65; + --color-badge-blue-bg: #0c2d48; + --color-badge-blue-text: #93c5fd; + --color-warning-bg-light: #2a2410; + --color-warning-text-amber: #fbbf24; + --color-warning-orange-bg: #2a1c10; + --color-warning-orange-text: #fb923c; + --color-purple-bg: #1a0a33; + --color-success-bg-alt: #0a2015; + --color-success-bg-green: #0a2015; + --color-content-bg-warn: #3d2e00; + --color-content-bg-warn-dark: #5a4200; - /* Items */ - --color-item-active: #5a5047; - --color-item-selected: #39281a; - --color-item-hover-accent: #3d342c; + /* Sidebar */ + --color-sidebar-bg-from: #0f172a; + --color-sidebar-bg-to: #0c1322; - /* Primary */ - /* TODO: rename into primary-blue ? */ - --color-primary: #60a5fa; - --color-primary-hover: #6fa8ee; -} - -/* ------------ */ - -[data-theme="dark"] select { - background-color: #1e293b; - border-color: #334155; - color: #e2e8f0; -} - -[data-theme="dark"] select:hover { - border-color: #475569; -} - -[data-theme="dark"] select:focus { - border-color: #ff5e3a; - background-color: #1e293b; -} - -[data-theme="dark"] .smd-expiry-date-input::-webkit-calendar-picker-indicator { - filter: invert(1); + /* Items */ + --color-item-active: #5a5047; + --color-item-selected: #39281a; + --color-item-hover-accent: #3d342c; + + /* Primary */ + /* TODO: rename into primary-blue ? */ + --color-primary: #60a5fa; + --color-primary-hover: #6fa8ee; + } + + /* ------------ */ + /* Native form controls need explicit dark styling on browsers without + * `color-scheme` — on capable browsers the UA does this automatically + * once `color-scheme: dark` is in effect. */ + + [data-theme="dark"] select, + html[data-color-scheme="dark"] select { + background-color: #1e293b; + border-color: #334155; + color: #e2e8f0; + } + + [data-theme="dark"] select:hover, + html[data-color-scheme="dark"] select:hover { + border-color: #475569; + } + + [data-theme="dark"] select:focus, + html[data-color-scheme="dark"] select:focus { + border-color: #ff5e3a; + background-color: #1e293b; + } + + [data-theme="dark"] .smd-expiry-date-input::-webkit-calendar-picker-indicator, + html[data-color-scheme="dark"] .smd-expiry-date-input::-webkit-calendar-picker-indicator { + filter: invert(1); + } } diff --git a/static/index.html b/static/index.html index 9a74a6e9..08a3a772 100644 --- a/static/index.html +++ b/static/index.html @@ -3,6 +3,10 @@ + + OxiCloud @@ -132,12 +136,17 @@ -
- - - + +
+
+ + + +
+
@@ -195,13 +204,30 @@
- + +
- +
`; + // Rebuild menu options — call i18n.t() at call time so labels are + // localised. Each def supplies its own icon. A section opts into an + // ungrouped/sorted entry by including a def with key=''. + menu.innerHTML = ''; for (const def of defs) { - if (def.key === '') continue; - menu.insertAdjacentHTML('beforeend', ``); + const iconClass = def.icon ?? 'fas fa-layer-group'; + menu.insertAdjacentHTML( + 'beforeend', + `` + ); } // One stable document-level handler to close the menu on outside clicks. @@ -314,9 +315,10 @@ function setupActionsBarDelegation() { btn.classList.add('active'); document.getElementById('group-by-menu')?.classList.add('hidden'); const groupByBtn = document.getElementById('group-by-btn'); - groupByBtn?.classList.toggle('active', key !== ''); + // Always active — there's no neutral "None" state anymore. + groupByBtn?.classList.add('active'); const lbl = groupByBtn?.querySelector('.group-by-label'); - if (lbl) lbl.textContent = key !== '' ? (btn.textContent ?? '') : ''; + if (lbl) lbl.innerHTML = btn.innerHTML; // clone icon + label markup // Changing order-by dimension resets direction to ascending _groupByView?.setDirection(false); document.getElementById('sort-dir-btn')?.classList.remove('active'); diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index 9ec60235..09eaab5b 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -417,6 +417,8 @@ function switchToTrashSection() { setActionsBarMode('trash'); setGroupByView(trashView); syncGroupByMenu(trashView.groupByDefs); + const trashPrefs = viewPrefs.load('trash'); + applyGroupByMenuState(trashPrefs.groupBy || 'remainingDays', trashPrefs.reversed); //reset files view + remove any error ui.resetFilesList(); diff --git a/static/js/app/userMenu.js b/static/js/app/userMenu.js index 0234d135..a345ec3b 100644 --- a/static/js/app/userMenu.js +++ b/static/js/app/userMenu.js @@ -6,14 +6,13 @@ import { createUserVignette } from '../components/userVignette.js'; import { getCsrfHeaders } from '../core/csrf.js'; import { formatFileSize, formatQuotaSize } from '../core/formatters.js'; import { i18n } from '../core/i18n.js'; -import { ui } from './ui.js'; function setupUserMenu() { const wrapper = document.getElementById('user-menu-wrapper'); const avatarBtn = document.getElementById('user-avatar-btn'); const menu = document.getElementById('user-menu'); const logoutBtn = document.getElementById('user-menu-logout'); - const themeBtn = document.getElementById('user-menu-theme'); + const themeSegmented = document.getElementById('user-menu-theme-segmented'); const aboutBtn = document.getElementById('user-menu-about'); const adminBtn = document.getElementById('user-menu-admin'); const adminDivider = document.getElementById('user-menu-admin-divider'); @@ -65,53 +64,49 @@ function setupUserMenu() { }); } - if (themeBtn) { - const pill = document.getElementById('theme-toggle-pill'); - - // Sync pill UI with current theme state - function syncThemePill() { - const isDark = localStorage.getItem('oxicloud_theme') === 'dark'; - if (pill) { - if (isDark) { - pill.classList.add('active'); - } else { - pill.classList.remove('active'); - } - } - // Ensure document theme matches localStorage - if (isDark) { - document.documentElement.setAttribute('data-theme', 'dark'); - } else { - document.documentElement.removeAttribute('data-theme'); - } + if (themeSegmented) { + /** + * Read the current selection. Returns `'auto'` when no explicit choice + * is stored — that's the default and means "follow OS preference". + * @returns {'light' | 'dark' | 'auto'} + */ + function currentMode() { + const saved = localStorage.getItem('oxicloud_theme'); + return saved === 'light' || saved === 'dark' ? saved : 'auto'; } - // Initialize pill state on load - syncThemePill(); - - themeBtn.addEventListener('click', (e) => { - e.stopPropagation(); - // Toggle theme based on current state, not pill state - const currentIsDark = localStorage.getItem('oxicloud_theme') === 'dark'; - const newIsDark = !currentIsDark; - - localStorage.setItem('oxicloud_theme', newIsDark ? 'dark' : 'light'); - - if (newIsDark) { - document.documentElement.setAttribute('data-theme', 'dark'); + /** + * Apply a mode: persist it, stamp the html attribute (or remove it + * for 'auto' so CSS `color-scheme: light dark` resolves via OS), and + * highlight the matching button. + * @param {'light' | 'dark' | 'auto'} mode + */ + function applyMode(mode) { + if (mode === 'auto') { + localStorage.setItem('oxicloud_theme', 'auto'); + document.documentElement.removeAttribute('data-color-scheme'); } else { - document.documentElement.removeAttribute('data-theme'); + localStorage.setItem('oxicloud_theme', mode); + document.documentElement.setAttribute('data-color-scheme', mode); } + themeSegmented.querySelectorAll('.theme-segmented__opt').forEach((b) => { + const el = /** @type {HTMLElement} */ (b); + const isActive = el.dataset.mode === mode; + el.classList.toggle('theme-segmented__opt--active', isActive); + el.setAttribute('aria-checked', String(isActive)); + }); + } - if (pill) { - if (newIsDark) { - pill.classList.add('active'); - } else { - pill.classList.remove('active'); - } - } + // Initial highlight (theme-init.js already stamped the attribute). + applyMode(currentMode()); - ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled'); + themeSegmented.addEventListener('click', (e) => { + const btn = /** @type {HTMLElement | null} */ (/** @type {HTMLElement} */ (e.target).closest('.theme-segmented__opt')); + if (!btn) return; + e.stopPropagation(); + const mode = /** @type {'light' | 'dark' | 'auto'} */ (btn.dataset.mode); + if (mode !== 'light' && mode !== 'dark' && mode !== 'auto') return; + applyMode(mode); }); } diff --git a/static/js/components/mySharesList.js b/static/js/components/mySharesList.js index 5c04e202..9dcaadc4 100644 --- a/static/js/components/mySharesList.js +++ b/static/js/components/mySharesList.js @@ -167,6 +167,8 @@ class MySharesList { _buildResourceLaneHeader(item) { const row = document.createElement('div'); row.className = 'ms-resource-row'; + if (item.resource.path) row.dataset.path = item.resource.path; + if (item.resource.owner_id) row.dataset.ownerId = item.resource.owner_id; row.appendChild(buildResourceIcon(item.resource, item.resource_type)); @@ -229,6 +231,13 @@ class MySharesList { if (_expiryState(grant.expires_at ?? null) === 'expired') { row.classList.add('ms-grant-row--expired'); } + // In sharedWith mode each grant row represents a (resource → subject) + // pair, so stamp the resource hierarchy info for the hover tooltip. + // In items mode the row represents a subject — no resource attrs. + if (viewMode === 'sharedWith') { + if (item.resource.path) row.dataset.path = item.resource.path; + if (item.resource.owner_id) row.dataset.ownerId = item.resource.owner_id; + } row.appendChild(this._buildIdentity(grant, item, viewMode)); row.appendChild(this._buildRolePill(grant.role)); diff --git a/static/js/core/groupBySync.js b/static/js/core/groupBySync.js index 47877a67..1b26e75d 100644 --- a/static/js/core/groupBySync.js +++ b/static/js/core/groupBySync.js @@ -22,7 +22,8 @@ * * No-op when the menu elements are not in the DOM (e.g. before initApp). * - * @param {string} groupBy Active group-by key, or `''` for "None". + * @param {string} groupBy Active group-by key, e.g. `''` for the "Name" + * / flat-sort option a section opts into. * @param {boolean} reversed Whether sort direction is reversed. */ function applyGroupByMenuState(groupBy, reversed) { @@ -32,18 +33,14 @@ function applyGroupByMenuState(groupBy, reversed) { btn.classList.toggle('active', (btn.dataset.groupBy ?? '') === groupBy); } - // Group-by button: active class + label text. + // Group-by button always reflects the active option (icon + label). const groupByBtn = document.getElementById('group-by-btn'); - groupByBtn?.classList.toggle('active', groupBy !== ''); + groupByBtn?.classList.add('active'); const lbl = groupByBtn?.querySelector('.group-by-label'); if (lbl) { - if (groupBy === '') { - lbl.textContent = ''; - } else { - const activeOpt = /** @type {HTMLElement|null} */ (document.querySelector(`.group-by-option[data-group-by="${CSS.escape(groupBy)}"]`)); - lbl.textContent = activeOpt?.textContent ?? ''; - } + const activeOpt = /** @type {HTMLElement|null} */ (document.querySelector(`.group-by-option[data-group-by="${CSS.escape(groupBy)}"]`)); + lbl.innerHTML = activeOpt?.innerHTML ?? ''; } // Sort-direction button: active = reversed. diff --git a/static/js/core/icons.js b/static/js/core/icons.js index 2ec374be..ae120282 100644 --- a/static/js/core/icons.js +++ b/static/js/core/icons.js @@ -17,18 +17,6 @@ /** @type {Record>} */ const OxiIcons = { - 'arrow-left': [ - 448, - 'M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z' - ], - 'arrows-alt': [ - 512, - 'M278.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l32 0 0 96-96 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6l0-32 96 0 0 96-32 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-32 0 0-96 96 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 32-96 0 0-96 32 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64z' - ], - 'arrow-up': [ - 512, - 'M214.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 109.3 160 480c0 17.7 14.3 32 32 32s32-14.3 32-32l0-370.7 105.4 105.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z' - ], 'arrow-down': [ 512, 'M169.4 502.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 402.7 224 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 370.7-105.4-105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z' @@ -41,6 +29,26 @@ const OxiIcons = { 576, 'M246.6 374.6l-96 96c-12.5 12.5-32.8 12.5-45.3 0l-96-96c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L96 370.7 96 64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 306.7 41.4-41.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3zM320 480c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0zm0-128c-17.7 0-32-14.3-32-32s14.3-32 32-32l96 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-96 0zm0-128c-17.7 0-32-14.3-32-32s14.3-32 32-32l160 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-160 0zm0-128c-17.7 0-32-14.3-32-32s14.3-32 32-32l224 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L320 96z' ], + 'arrow-left': [ + 448, + 'M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z' + ], + 'arrow-up': [ + 512, + 'M214.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 109.3 160 480c0 17.7 14.3 32 32 32s32-14.3 32-32l0-370.7 105.4 105.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z' + ], + 'arrow-up-a-z': [ + 576, + 'M183.6 42.4C177.5 35.8 169 32 160 32s-17.5 3.8-23.6 10.4l-88 96c-11.9 13-11.1 33.3 2 45.2s33.3 11.1 45.2-2L128 146.3 128 448c0 17.7 14.3 32 32 32s32-14.3 32-32l0-301.7 32.4 35.4c11.9 13 32.2 13.9 45.2 2s13.9-32.2 2-45.2l-88-96zM320 320c0 17.7 14.3 32 32 32l50.7 0-73.4 73.4c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l128 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-50.7 0 73.4-73.4c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-128 0c-17.7 0-32 14.3-32 32zM416 32c-12.1 0-23.2 6.8-28.6 17.7l-64 128-16 32c-7.9 15.8-1.5 35 14.3 42.9s35 1.5 42.9-14.3l7.2-14.3 88.4 0 7.2 14.3c7.9 15.8 27.1 22.2 42.9 14.3s22.2-27.1 14.3-42.9l-16-32-64-128C439.2 38.8 428.1 32 416 32zM395.8 176L416 135.6 436.2 176l-40.4 0z' + ], + 'arrows-alt': [ + 512, + 'M278.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l32 0 0 96-96 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6l0-32 96 0 0 96-32 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-32 0 0-96 96 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 32-96 0 0-96 32 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64z' + ], + backward: [ + 512, + 'M204.3 43.1C215.9 32 233 28.9 247.7 35.2S272 56 272 72l0 136.3 172.3-165.1C455.9 32 473 28.9 487.7 35.2S512 56 512 72l0 368c0 16-9.6 30.5-24.3 36.8s-31.8 3.2-43.4-7.9L272 303.7 272 440c0 16-9.6 30.5-24.3 36.8s-31.8 3.2-43.4-7.9l-192-184C4.5 277.3 0 266.9 0 256s4.5-21.3 12.3-28.9l192-184z' + ], ban: [ 512, 'M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM159.3 388.7L388.7 159.3c4.6-4.6 11.5-5.9 17.4-3.5c14.5 6 26.4 15.3 35.1 27c3.8 5.2 3.2 12.3-1.2 16.8L210.2 428.4c-4.4 4.4-11.6 5-16.8 1.2c-11.7-8.7-21-20.6-27-35.1c-2.5-5.9-1.1-12.8 3.5-17.4z' @@ -69,6 +77,10 @@ const OxiIcons = { 512, 'M120 0c13.3 0 24 10.7 24 24l0 40 160 0 0-40c0-13.3 10.7-24 24-24s24 10.7 24 24l0 40 32 0c35.3 0 64 28.7 64 64l0 288c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 128C0 92.7 28.7 64 64 64l32 0 0-40c0-13.3 10.7-24 24-24zm0 112l-56 0c-8.8 0-16 7.2-16 16l0 48 352 0 0-48c0-8.8-7.2-16-16-16l-264 0zM48 224l0 192c0 8.8 7.2 16 16 16l320 0c8.8 0 16-7.2 16-16l0-192-352 0z' ], + camera: [ + 512, + 'M149.1 64.8L138.7 96 64 96C28.7 96 0 124.7 0 160L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-256c0-35.3-28.7-64-64-64l-74.7 0-10.4-31.2C356.4 45.2 338.1 32 317.4 32L194.6 32c-20.7 0-39 13.2-45.5 32.8zM256 192a96 96 0 1 1 0 192 96 96 0 1 1 0-192z' + ], 'caret-down': [ 320, 'M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z' @@ -125,23 +137,30 @@ const OxiIcons = { 640, 'M308.5 135.3c7.1-6.3 9.9-16.2 6.2-25c-2.3-5.3-4.8-10.5-7.6-15.5L304 89.4c-3-5-6.3-9.9-9.8-14.6c-5.7-7.6-15.7-10.1-24.7-7.1l-28.2 9.3c-10.7-8.8-23-16-36.2-20.9L199 27.1c-1.9-9.3-9.1-16.7-18.5-17.8C173.9 8.4 167.2 8 160.4 8l-.7 0c-6.8 0-13.5 .4-20.1 1.2c-9.4 1.1-16.6 8.6-18.5 17.8L115 56.1c-13.3 5-25.5 12.1-36.2 20.9L50.5 67.8c-9-3-19-.5-24.7 7.1c-3.5 4.7-6.8 9.6-9.9 14.6l-3 5.3c-2.8 5-5.3 10.2-7.6 15.6c-3.7 8.7-.9 18.6 6.2 25l22.2 19.8C32.6 161.9 32 168.9 32 176s.6 14.1 1.7 20.9L11.5 216.7c-7.1 6.3-9.9 16.2-6.2 25c2.3 5.3 4.8 10.5 7.6 15.6l3 5.2c3 5.1 6.3 9.9 9.9 14.6c5.7 7.6 15.7 10.1 24.7 7.1l28.2-9.3c10.7 8.8 23 16 36.2 20.9l6.1 29.1c1.9 9.3 9.1 16.7 18.5 17.8c6.7 .8 13.5 1.2 20.4 1.2s13.7-.4 20.4-1.2c9.4-1.1 16.6-8.6 18.5-17.8l6.1-29.1c13.3-5 25.5-12.1 36.2-20.9l28.2 9.3c9 3 19 .5 24.7-7.1c3.5-4.7 6.8-9.5 9.8-14.6l3.1-5.4c2.8-5 5.3-10.2 7.6-15.5c3.7-8.7 .9-18.6-6.2-25l-22.2-19.8c1.1-6.8 1.7-13.8 1.7-20.9s-.6-14.1-1.7-20.9l22.2-19.8zM112 176a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM504.7 500.5c6.3 7.1 16.2 9.9 25 6.2c5.3-2.3 10.5-4.8 15.5-7.6l5.4-3.1c5-3 9.9-6.3 14.6-9.8c7.6-5.7 10.1-15.7 7.1-24.7l-9.3-28.2c8.8-10.7 16-23 20.9-36.2l29.1-6.1c9.3-1.9 16.7-9.1 17.8-18.5c.8-6.7 1.2-13.5 1.2-20.4s-.4-13.7-1.2-20.4c-1.1-9.4-8.6-16.6-17.8-18.5L583.9 307c-5-13.3-12.1-25.5-20.9-36.2l9.3-28.2c3-9 .5-19-7.1-24.7c-4.7-3.5-9.6-6.8-14.6-9.9l-5.3-3c-5-2.8-10.2-5.3-15.6-7.6c-8.7-3.7-18.6-.9-25 6.2l-19.8 22.2c-6.8-1.1-13.8-1.7-20.9-1.7s-14.1 .6-20.9 1.7l-19.8-22.2c-6.3-7.1-16.2-9.9-25-6.2c-5.3 2.3-10.5 4.8-15.6 7.6l-5.2 3c-5.1 3-9.9 6.3-14.6 9.9c-7.6 5.7-10.1 15.7-7.1 24.7l9.3 28.2c-8.8 10.7-16 23-20.9 36.2L315.1 313c-9.3 1.9-16.7 9.1-17.8 18.5c-.8 6.7-1.2 13.5-1.2 20.4s.4 13.7 1.2 20.4c1.1 9.4 8.6 16.6 17.8 18.5l29.1 6.1c5 13.3 12.1 25.5 20.9 36.2l-9.3 28.2c-3 9-.5 19 7.1 24.7c4.7 3.5 9.5 6.8 14.6 9.8l5.4 3.1c5 2.8 10.2 5.3 15.5 7.6c8.7 3.7 18.6 .9 25-6.2l19.8-22.2c6.8 1.1 13.8 1.7 20.9 1.7s14.1-.6 20.9-1.7l19.8 22.2zM464 304a48 48 0 1 1 0 96 48 48 0 1 1 0-96z' ], - crown: [ - 576, - 'M309 106c11.4-7 19-19.7 19-34c0-22.1-17.9-40-40-40s-40 17.9-40 40c0 14.4 7.6 27 19 34L209.7 220.6c-9.1 18.2-32.7 23.4-48.6 10.7L72 160c5-6.7 8-15 8-24c0-22.1-17.9-40-40-40S0 113.9 0 136s17.9 40 40 40c.2 0 .5 0 .7 0L86.4 427.4c5.5 30.4 32 52.6 63 52.6l277.2 0c30.9 0 57.4-22.1 63-52.6L535.3 176c.2 0 .5 0 .7 0c22.1 0 40-17.9 40-40s-17.9-40-40-40s-40 17.9-40 40c0 9 3 17.3 8 24l-89.1 71.3c-15.9 12.7-39.5 7.5-48.6-10.7L309 106z' + 'compact-disc': [ + 512, + 'M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zM88 256H56c0-105.9 86.1-192 192-192v32c-88.2 0-160 71.8-160 160zm160 96c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96zm0-128c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32z' ], copy: [ 448, 'M208 0L332.1 0c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1 33.9L448 336c0 26.5-21.5 48-48 48l-192 0c-26.5 0-48-21.5-48-48l0-288c0-26.5 21.5-48 48-48zM48 128l80 0 0 64-64 0 0 256 192 0 0-32 64 0 0 48c0 26.5-21.5 48-48 48L48 512c-26.5 0-48-21.5-48-48L0 176c0-26.5 21.5-48 48-48z' ], + crown: [ + 576, + 'M309 106c11.4-7 19-19.7 19-34c0-22.1-17.9-40-40-40s-40 17.9-40 40c0 14.4 7.6 27 19 34L209.7 220.6c-9.1 18.2-32.7 23.4-48.6 10.7L72 160c5-6.7 8-15 8-24c0-22.1-17.9-40-40-40S0 113.9 0 136s17.9 40 40 40c.2 0 .5 0 .7 0L86.4 427.4c5.5 30.4 32 52.6 63 52.6l277.2 0c30.9 0 57.4-22.1 63-52.6L535.3 176c.2 0 .5 0 .7 0c22.1 0 40-17.9 40-40s-17.9-40-40-40s-40 17.9-40 40c0 9 3 17.3 8 24l-89.1 71.3c-15.9 12.7-39.5 7.5-48.6-10.7L309 106z' + ], database: [ 448, 'M448 80l0 48c0 44.2-100.3 80-224 80S0 172.2 0 128L0 80C0 35.8 100.3 0 224 0S448 35.8 448 80zM393.2 214.7c20.8-7.4 39.9-16.9 54.8-28.6L448 288c0 44.2-100.3 80-224 80S0 332.2 0 288L0 186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3zM0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6l0 85.9c0 44.2-100.3 80-224 80S0 476.2 0 432l0-85.9z' ], + desktop: [ + 576, + 'M64 0C28.7 0 0 28.7 0 64L0 352c0 35.3 28.7 64 64 64l176 0-10.7 32L160 448c-17.7 0-32 14.3-32 32s14.3 32 32 32l256 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-69.3 0L336 416l176 0c35.3 0 64-28.7 64-64l0-288c0-35.3-28.7-64-64-64L64 0zM512 64l0 224L64 288 64 64l448 0z' + ], download: [ 512, 'M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 242.7-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7 288 32zM64 352c-35.3 0-64 28.7-64 64l0 32c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-32c0-35.3-28.7-64-64-64l-101.5 0-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352 64 352zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z' ], - 'ellipsis-v': [128, 'M64 360a56 56 0 1 0 0 112 56 56 0 1 0 0-112zm0-160a56 56 0 1 0 0 112 56 56 0 1 0 0-112zM120 96A56 56 0 1 0 8 96a56 56 0 1 0 112 0z'], envelope: [ 512, 'M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z' @@ -158,6 +177,10 @@ const OxiIcons = { 448, 'M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z' ], + 'external-link-alt': [ + 512, + 'M290.4 19.8C295.4 7.8 307.1 0 320 0L480 0c17.7 0 32 14.3 32 32l0 160c0 12.9-7.8 24.6-19.8 29.6s-25.7 2.2-34.9-6.9L400 157.3 246.6 310.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L354.7 112 297.4 54.6c-9.2-9.2-11.9-22.9-6.9-34.9zM0 176c0-44.2 35.8-80 80-80l80 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-80 0c-8.8 0-16 7.2-16 16l0 256c0 8.8 7.2 16 16 16l256 0c8.8 0 16-7.2 16-16l0-80c0-17.7 14.3-32 32-32s32 14.3 32 32l0 80c0 44.2-35.8 80-80 80L80 512c-44.2 0-80-35.8-80-80L0 176z' + ], eye: [ 576, 'M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z' @@ -218,6 +241,10 @@ const OxiIcons = { 512, 'M512 416c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96C0 60.7 28.7 32 64 32l128 0c20.1 0 39.1 9.5 51.2 25.6l19.2 25.6c6 8.1 15.5 12.8 25.6 12.8l160 0c35.3 0 64 28.7 64 64l0 256zM232 376c0 13.3 10.7 24 24 24s24-10.7 24-24l0-64 64 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-64 0 0-64c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 64-64 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l64 0 0 64z' ], + forward: [ + 512, + 'M371.7 43.1C360.1 32 343 28.9 328.3 35.2S304 56 304 72l0 136.3-172.3-165.1C120.1 32 103 28.9 88.3 35.2S64 56 64 72l0 368c0 16 9.6 30.5 24.3 36.8s31.8 3.2 43.4-7.9L304 303.7 304 440c0 16 9.6 30.5 24.3 36.8s31.8 3.2 43.4-7.9l192-184c7.9-7.5 12.3-18 12.3-28.9s-4.5-21.3-12.3-28.9l-192-184z' + ], github: [ 496, 'M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z' @@ -226,6 +253,10 @@ const OxiIcons = { 512, 'M352 256c0 22.2-1.2 43.6-3.3 64l-185.3 0c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64l185.3 0c2.2 20.4 3.3 41.8 3.3 64zm28.8-64l123.1 0c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64l-123.1 0c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32l-116.7 0c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0l-176.6 0c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0L18.6 160C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192l123.1 0c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64L8.1 320C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6l176.6 0c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352l116.7 0zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6l116.7 0z' ], + 'grip-vertical': [ + 320, + 'M128 40c0-22.1-17.9-40-40-40L40 0C17.9 0 0 17.9 0 40L0 88c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zm0 192c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zM0 424l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM320 40c0-22.1-17.9-40-40-40L232 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zM192 232l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM320 424c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48z' + ], hdd: [ 512, 'M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 184.4c-17-15.2-39.4-24.4-64-24.4L64 256c-24.6 0-47 9.2-64 24.4L0 96zM64 288l384 0c35.3 0 64 28.7 64 64l0 64c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64l0-64c0-35.3 28.7-64 64-64zM320 416a32 32 0 1 0 0-64 32 32 0 1 0 0 64zm128-32a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z' @@ -262,6 +293,10 @@ const OxiIcons = { 512, 'M232.5 5.2c14.9-6.9 32.1-6.9 47 0l218.6 101c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L13.9 149.8C5.4 145.8 0 137.3 0 128s5.4-17.9 13.9-21.8L232.5 5.2zM48.1 218.4l164.3 75.9c27.7 12.8 59.6 12.8 87.3 0l164.3-75.9 34.1 15.8c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L13.9 277.8C5.4 273.8 0 265.3 0 256s5.4-17.9 13.9-21.8l34.1-15.8zM13.9 362.2l34.1-15.8 164.3 75.9c27.7 12.8 59.6 12.8 87.3 0l164.3-75.9 34.1 15.8c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L13.9 405.8C5.4 401.8 0 393.3 0 384s5.4-17.9 13.9-21.8z' ], + 'level-up-alt': [ + 384, + 'M169.4 9.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9S332.9 192 320 192l-64 0 0 160c0 88.4-71.6 160-160 160l-64 0c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l64 0c17.7 0 32-14.3 32-32l0-160-64 0c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128z' + ], link: [ 576, 'M419.5 96c-16.6 0-32.7 4.5-46.8 12.7-15.8-16-34.2-29.4-54.5-39.5 28.2-24 64.1-37.2 101.3-37.2 86.4 0 156.5 70 156.5 156.5 0 41.5-16.5 81.3-45.8 110.6l-71.1 71.1c-29.3 29.3-69.1 45.8-110.6 45.8-86.4 0-156.5-70-156.5-156.5 0-1.5 0-3 .1-4.5 .5-17.7 15.2-31.6 32.9-31.1s31.6 15.2 31.1 32.9c0 .9 0 1.8 0 2.6 0 51.1 41.4 92.5 92.5 92.5 24.5 0 48-9.7 65.4-27.1l71.1-71.1c17.3-17.3 27.1-40.9 27.1-65.4 0-51.1-41.4-92.5-92.5-92.5zM275.2 173.3c-1.9-.8-3.8-1.9-5.5-3.1-12.6-6.5-27-10.2-42.1-10.2-24.5 0-48 9.7-65.4 27.1L91.1 258.2c-17.3 17.3-27.1 40.9-27.1 65.4 0 51.1 41.4 92.5 92.5 92.5 16.5 0 32.6-4.4 46.7-12.6 15.8 16 34.2 29.4 54.6 39.5-28.2 23.9-64 37.2-101.3 37.2-86.4 0-156.5-70-156.5-156.5 0-41.5 16.5-81.3 45.8-110.6l71.1-71.1c29.3-29.3 69.1-45.8 110.6-45.8 86.6 0 156.5 70.6 156.5 156.9 0 1.3 0 2.6 0 3.9-.4 17.7-15.1 31.6-32.8 31.2s-31.6-15.1-31.2-32.8c0-.8 0-1.5 0-2.3 0-33.7-18-63.3-44.8-79.6z' @@ -282,6 +317,10 @@ const OxiIcons = { 384, 'M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z' ], + music: [ + 512, + 'M468 7c7.6 6.1 12 15.3 12 25l0 304c0 44.2-43 80-96 80s-96-35.8-96-80 43-80 96-80c11.2 0 22 1.6 32 4.6l0-116.7-224 49.8 0 206.3c0 44.2-43 80-96 80s-96-35.8-96-80 43-80 96-80c11.2 0 22 1.6 32 4.6L128 96c0-15 10.4-28 25.1-31.2l288-64c9.5-2.1 19.4 .2 27 6.3z' + ], oxiexport: [ 576, 'M384.5 24l0 72-64 0c-79.5 0-144 64.5-144 144 0 93.4 82.8 134.8 100.6 142.6 2.2 1 4.6 1.4 7.1 1.4l2.5 0c9.8 0 17.8-8 17.8-17.8 0-8.3-5.9-15.5-12.8-20.3-8.9-6.2-19.2-18.2-19.2-40.5 0-45 36.5-81.5 81.5-81.5l30.5 0 0 72c0 9.7 5.8 18.5 14.8 22.2s19.3 1.7 26.2-5.2l136-136c9.4-9.4 9.4-24.6 0-33.9L425.5 7c-6.9-6.9-17.2-8.9-26.2-5.2S384.5 14.3 384.5 24zm-272 72c-44.2 0-80 35.8-80 80l0 256c0 44.2 35.8 80 80 80l256 0c44.2 0 80-35.8 80-80l0-32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 32c0 8.8-7.2 16-16 16l-256 0c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l16 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-16 0z' @@ -290,50 +329,34 @@ const OxiIcons = { 576, 'm 360.55,24 v 72 h 64 c 79.5,0 144,64.5 144,144 0,93.4 -82.8,134.8 -100.6,142.6 -2.2,1 -4.6,1.4 -7.1,1.4 h -2.5 c -9.8,0 -17.8,-8 -17.8,-17.8 0,-8.3 5.9,-15.5 12.8,-20.3 8.9,-6.2 19.2,-18.2 19.2,-40.5 0,-45 -36.5,-81.5 -81.5,-81.5 h -30.5 v 72 c 0,9.7 -5.8,18.5 -14.8,22.2 -9,3.7 -19.3,1.7 -26.2,-5.2 l -136,-136 c -9.4,-9.4 -9.4,-24.6 0,-33.9 l 136,-136 c 6.9,-6.9 17.2,-8.9 26.2,-5.2 9,3.7 14.8,12.5 14.8,22.2 z M 112.5,96 c -44.2,0 -80,35.8 -80,80 v 256 c 0,44.2 35.8,80 80,80 h 256 c 44.2,0 80,-35.8 80,-80 v -32 c 0,-17.7 -14.3,-32 -32,-32 -17.7,0 -32,14.3 -32,32 v 32 c 0,8.8 -7.2,16 -16,16 h -256 c -8.8,0 -16,-7.2 -16,-16 V 176 c 0,-8.8 7.2,-16 16,-16 h 16 c 17.7,0 32,-14.3 32,-32 0,-17.7 -14.3,-32 -32,-32 z' ], - pen: [ - 512, - 'M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z' - ], - play: [ - 384, - 'M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80L0 432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z' - ], pause: [ 384, 'M48 32C21.5 32 0 53.5 0 80L0 432c0 26.5 21.5 48 48 48l64 0c26.5 0 48-21.5 48-48l0-352c0-26.5-21.5-48-48-48L48 32zm224 0c-26.5 0-48 21.5-48 48l0 352c0 26.5 21.5 48 48 48l64 0c26.5 0 48-21.5 48-48l0-352c0-26.5-21.5-48-48-48l-64 0z' ], + pen: [ + 512, + 'M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z' + ], 'pencil-alt': [ 512, 'M36.4 353.2c4.1-14.6 11.8-27.9 22.6-38.7l181.2-181.2 33.9-33.9c16.6 16.6 51.3 51.3 104 104l33.9 33.9-33.9 33.9-181.2 181.2c-10.7 10.7-24.1 18.5-38.7 22.6L30.4 510.6c-8.3 2.3-17.3 0-23.4-6.2S-1.4 489.3 .9 481L36.4 353.2zm55.6-3.7c-4.4 4.7-7.6 10.4-9.3 16.6l-24.1 86.9 86.9-24.1c6.4-1.8 12.2-5.1 17-9.7L91.9 349.5zm354-146.1c-16.6-16.6-51.3-51.3-104-104L308 65.5C334.5 39 349.4 24.1 352.9 20.6 366.4 7 384.8-.6 404-.6S441.6 7 455.1 20.6l35.7 35.7C504.4 69.9 512 88.3 512 107.4s-7.6 37.6-21.2 51.1c-3.5 3.5-18.4 18.4-44.9 44.9z' ], - shuffle: [ - 512, - 'M403.8 34.4c12-5 25.7-2.2 34.9 6.9l64 64c6 6 9.4 14.1 9.4 22.6s-3.4 16.6-9.4 22.6l-64 64c-9.2 9.2-22.9 11.9-34.9 6.9S384 204.9 384 192l0-32-32 0c-10.1 0-19.6 4.7-25.6 12.8l-32.4 43.2-40-53.3 21.2-28.3C293.3 110.2 321.8 96 352 96l32 0 0-32c0-12.9 7.8-24.6 19.8-29.6zM154 296l40 53.3-21.2 28.3C154.7 401.8 126.2 416 96 416l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l64 0c10.1 0 19.6-4.7 25.6-12.8L154 296zM438.6 470.6c-9.2 9.2-22.9 11.9-34.9 6.9S384 460.9 384 448l0-32-32 0c-30.2 0-58.7-14.2-76.8-38.4L121.6 172.8c-6-8.1-15.5-12.8-25.6-12.8l-64 0c-17.7 0-32-14.3-32-32S14.3 96 32 96l64 0c30.2 0 58.7 14.2 76.8 38.4L326.4 339.2c6 8.1 15.5 12.8 25.6 12.8l32 0 0-32c0-12.9 7.8-24.6 19.8-29.6s25.7-2.2 34.9 6.9l64 64c6 6 9.4 14.1 9.4 22.6s-3.4 16.6-9.4 22.6l-64 64z' + play: [ + 384, + 'M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80L0 432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z' ], - repeat: [ - 512, - 'M470.6 118.6c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S352 19.1 352 32l0 32-160 0C86 64 0 150 0 256 0 273.7 14.3 288 32 288s32-14.3 32-32c0-70.7 57.3-128 128-128l160 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64zM41.4 393.4c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9S160 492.9 160 480l0-32 160 0c106 0 192-86 192-192 0-17.7-14.3-32-32-32s-32 14.3-32 32c0 70.7-57.3 128-128 128l-160 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64z' - ], - forward: [ - 512, - 'M371.7 43.1C360.1 32 343 28.9 328.3 35.2S304 56 304 72l0 136.3-172.3-165.1C120.1 32 103 28.9 88.3 35.2S64 56 64 72l0 368c0 16 9.6 30.5 24.3 36.8s31.8 3.2 43.4-7.9L304 303.7 304 440c0 16 9.6 30.5 24.3 36.8s31.8 3.2 43.4-7.9l192-184c7.9-7.5 12.3-18 12.3-28.9s-4.5-21.3-12.3-28.9l-192-184z' - ], - backward: [ - 512, - 'M204.3 43.1C215.9 32 233 28.9 247.7 35.2S272 56 272 72l0 136.3 172.3-165.1C455.9 32 473 28.9 487.7 35.2S512 56 512 72l0 368c0 16-9.6 30.5-24.3 36.8s-31.8 3.2-43.4-7.9L272 303.7 272 440c0 16-9.6 30.5-24.3 36.8s-31.8 3.2-43.4-7.9l-192-184C4.5 277.3 0 266.9 0 256s4.5-21.3 12.3-28.9l192-184z' - ], - volume: [ - 512, - 'M48 352l48 0 134.1 119.2c6.4 5.7 14.6 8.8 23.1 8.8 19.2 0 34.8-15.6 34.8-34.8l0-378.4c0-19.2-15.6-34.8-34.8-34.8-8.5 0-16.7 3.1-23.1 8.8L96 160 48 160c-26.5 0-48 21.5-48 48l0 96c0 26.5 21.5 48 48 48zM441.1 107c-10.3-8.4-25.4-6.8-33.8 3.5s-6.8 25.4 3.5 33.8C443.3 170.7 464 210.9 464 256s-20.7 85.3-53.2 111.8c-10.3 8.4-11.8 23.5-3.5 33.8s23.5 11.8 33.8 3.5c43.2-35.2 70.9-88.9 70.9-149s-27.7-113.8-70.9-149zm-60.5 74.5c-10.3-8.4-25.4-6.8-33.8 3.5s-6.8 25.4 3.5 33.8C361.1 227.6 368 241 368 256s-6.9 28.4-17.7 37.3c-10.3 8.4-11.8 23.5-3.5 33.8s23.5 11.8 33.8 3.5C402.1 312.9 416 286.1 416 256s-13.9-56.9-35.5-74.5z' - ], - 'compact-disc': [ - 512, - 'M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zM88 256H56c0-105.9 86.1-192 192-192v32c-88.2 0-160 71.8-160 160zm160 96c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96zm0-128c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32z' + plus: [ + 448, + 'M256 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 160-160 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l160 0 0 160c0 17.7 14.3 32 32 32s32-14.3 32-32l0-160 160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-160 0 0-160z' ], 'question-circle': [ 512, 'M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3l58.3 0c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24l0-13.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1l-58.3 0c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z' ], + repeat: [ + 512, + 'M470.6 118.6c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S352 19.1 352 32l0 32-160 0C86 64 0 150 0 256 0 273.7 14.3 288 32 288s32-14.3 32-32c0-70.7 57.3-128 128-128l160 0 0 32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64zM41.4 393.4c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9S160 492.9 160 480l0-32 160 0c106 0 192-86 192-192 0-17.7-14.3-32-32-32s-32 14.3-32 32c0 70.7-57.3 128-128 128l-160 0 0-32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64z' + ], save: [ 448, 'M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-242.7c0-17-6.7-33.3-18.7-45.3L352 50.7C340 38.7 323.7 32 306.7 32L64 32zm0 96c0-17.7 14.3-32 32-32l192 0c17.7 0 32 14.3 32 32l0 64c0 17.7-14.3 32-32 32L96 224c-17.7 0-32-14.3-32-32l0-64zM224 288a64 64 0 1 1 0 128 64 64 0 1 1 0-128z' @@ -362,6 +385,10 @@ const OxiIcons = { 512, 'M256 0c4.6 0 9.2 1 13.4 2.9L457.7 82.8c22 9.3 38.4 31 38.3 57.2c-.5 99.2-41.3 280.7-213.6 363.2c-16.7 8-36.1 8-52.8 0C57.3 420.7 16.5 239.2 16 140c-.1-26.2 16.3-47.9 38.3-57.2L242.7 2.9C246.8 1 251.4 0 256 0zm0 66.8l0 378.1C394 378 431.1 230.1 432 141.4L256 66.8s0 0 0 0z' ], + shuffle: [ + 512, + 'M403.8 34.4c12-5 25.7-2.2 34.9 6.9l64 64c6 6 9.4 14.1 9.4 22.6s-3.4 16.6-9.4 22.6l-64 64c-9.2 9.2-22.9 11.9-34.9 6.9S384 204.9 384 192l0-32-32 0c-10.1 0-19.6 4.7-25.6 12.8l-32.4 43.2-40-53.3 21.2-28.3C293.3 110.2 321.8 96 352 96l32 0 0-32c0-12.9 7.8-24.6 19.8-29.6zM154 296l40 53.3-21.2 28.3C154.7 401.8 126.2 416 96 416l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l64 0c10.1 0 19.6-4.7 25.6-12.8L154 296zM438.6 470.6c-9.2 9.2-22.9 11.9-34.9 6.9S384 460.9 384 448l0-32-32 0c-30.2 0-58.7-14.2-76.8-38.4L121.6 172.8c-6-8.1-15.5-12.8-25.6-12.8l-64 0c-17.7 0-32-14.3-32-32S14.3 96 32 96l64 0c30.2 0 58.7 14.2 76.8 38.4L326.4 339.2c6 8.1 15.5 12.8 25.6 12.8l32 0 0-32c0-12.9 7.8-24.6 19.8-29.6s25.7-2.2 34.9 6.9l64 64c6 6 9.4 14.1 9.4 22.6s-3.4 16.6-9.4 22.6l-64 64z' + ], 'sign-in-alt': [ 512, 'M217.9 105.9L340.7 228.7c7.2 7.2 11.3 17.1 11.3 27.3s-4.1 20.1-11.3 27.3L217.9 406.1c-6.4 6.4-15 9.9-24 9.9c-18.7 0-33.9-15.2-33.9-33.9l0-62.1L32 320c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l128 0 0-62.1c0-18.7 15.2-33.9 33.9-33.9c9 0 17.6 3.6 24 9.9zM352 416l64 0c17.7 0 32-14.3 32-32l0-256c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l64 0c53 0 96 43 96 96l0 256c0 53-43 96-96 96l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32z' @@ -386,6 +413,10 @@ const OxiIcons = { 576, 'M287.9 0c9.2 0 17.6 5.2 21.6 13.5l68.6 141.3 153.2 22.6c9 1.3 16.5 7.6 19.3 16.3s.5 18.1-5.9 24.5L439.6 319.9l24.6 145.7c1.5 9-2.2 18.1-9.7 23.5s-17.3 6-25.3 1.7l-137-73.2L155.2 490.8c-8 4.3-17.8 3.7-25.3-1.7s-11.2-14.5-9.7-23.5l24.6-145.7L39.6 218.2c-6.4-6.4-8.7-15.9-5.9-24.5s10.3-14.9 19.3-16.3l153.2-22.6L274.3 13.5C278.3 5.2 286.7 0 295.9 0h-8zm0 79L235.4 187.2c-3.5 7.1-10.2 12.1-18.1 13.3L99 218.9l85.8 85.1c5.5 5.5 8.1 13.3 6.8 21L171.3 444.7l111.5-59.5c7-3.7 15.3-3.7 22.3 0l111.5 59.5-20.3-119.7c-1.3-7.7 1.2-15.5 6.8-21l85.8-85.1-118.3-17.4c-7.8-1.2-14.6-6.1-18.1-13.3L287.9 79z' ], + sun: [ + 512, + 'M375.7 19.7c-1.5-8-6.9-14.7-14.4-17.8s-16.1-2.2-22.8 2.4L256 61 173.5 4.2c-6.7-4.6-15.3-5.5-22.8-2.4s-12.9 9.8-14.4 17.8l-18.1 98.5L19.7 136.3c-8 1.5-14.7 6.9-17.8 14.4s-2.2 16.1 2.4 22.8L61 256 4.2 338.5c-4.6 6.7-5.5 15.3-2.4 22.8s9.8 13 17.8 14.4l98.5 18.1 18.1 98.5c1.5 8 6.9 14.7 14.4 17.8s16.1 2.2 22.8-2.4L256 451l82.5 56.8c6.7 4.6 15.3 5.5 22.8 2.4s12.9-9.8 14.4-17.8l18.1-98.5 98.5-18.1c8-1.5 14.7-6.9 17.8-14.4s2.2-16.1-2.4-22.8L451 256l56.8-82.5c4.6-6.7 5.5-15.3 2.4-22.8s-9.8-12.9-17.8-14.4l-98.5-18.1L375.7 19.7zM269.6 110l65.6-45.2 14.4 78.3c1.8 9.8 9.5 17.5 19.3 19.3l78.3 14.4L402 242.4c-5.7 8.2-5.7 19 0 27.2l45.2 65.6-78.3 14.4c-9.8 1.8-17.5 9.5-19.3 19.3l-14.4 78.3L269.6 402c-8.2-5.7-19-5.7-27.2 0l-65.6 45.2-14.4-78.3c-1.8-9.8-9.5-17.5-19.3-19.3L64.8 335.2 110 269.6c5.7-8.2 5.7-19 0-27.2L64.8 176.8l78.3-14.4c9.8-1.8 17.5-9.5 19.3-19.3l14.4-78.3L242.4 110c8.2 5.7 19 5.7 27.2 0zM256 368a112 112 0 1 0 0-224 112 112 0 1 0 0 224zM192 256a64 64 0 1 1 128 0 64 64 0 1 1 -128 0z' + ], terminal: [ 576, 'M9.4 86.6C-3.1 74.1-3.1 53.9 9.4 41.4s32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L178.7 256 9.4 86.6zM256 416l288 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-288 0c-17.7 0-32-14.3-32-32s14.3-32 32-32z' @@ -438,29 +469,9 @@ const OxiIcons = { 512, 'M342.6 9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l9.4 9.4L28.1 342.6C10.1 360.6 0 385 0 410.5L0 416c0 53 43 96 96 96l5.5 0c25.5 0 49.9-10.1 67.9-28.1L448 205.3l9.4 9.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-32-32-96-96-32-32zM205.3 256L352 109.3 402.7 160l-96 96-101.5 0z' ], - camera: [ + volume: [ 512, - 'M149.1 64.8L138.7 96 64 96C28.7 96 0 124.7 0 160L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-256c0-35.3-28.7-64-64-64l-74.7 0-10.4-31.2C356.4 45.2 338.1 32 317.4 32L194.6 32c-20.7 0-39 13.2-45.5 32.8zM256 192a96 96 0 1 1 0 192 96 96 0 1 1 0-192z' - ], - 'external-link-alt': [ - 512, - 'M290.4 19.8C295.4 7.8 307.1 0 320 0L480 0c17.7 0 32 14.3 32 32l0 160c0 12.9-7.8 24.6-19.8 29.6s-25.7 2.2-34.9-6.9L400 157.3 246.6 310.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L354.7 112 297.4 54.6c-9.2-9.2-11.9-22.9-6.9-34.9zM0 176c0-44.2 35.8-80 80-80l80 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-80 0c-8.8 0-16 7.2-16 16l0 256c0 8.8 7.2 16 16 16l256 0c8.8 0 16-7.2 16-16l0-80c0-17.7 14.3-32 32-32s32 14.3 32 32l0 80c0 44.2-35.8 80-80 80L80 512c-44.2 0-80-35.8-80-80L0 176z' - ], - 'grip-vertical': [ - 320, - 'M128 40c0-22.1-17.9-40-40-40L40 0C17.9 0 0 17.9 0 40L0 88c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zm0 192c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zM0 424l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM320 40c0-22.1-17.9-40-40-40L232 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zM192 232l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM320 424c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48z' - ], - 'level-up-alt': [ - 384, - 'M169.4 9.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9S332.9 192 320 192l-64 0 0 160c0 88.4-71.6 160-160 160l-64 0c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l64 0c17.7 0 32-14.3 32-32l0-160-64 0c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128z' - ], - music: [ - 512, - 'M468 7c7.6 6.1 12 15.3 12 25l0 304c0 44.2-43 80-96 80s-96-35.8-96-80 43-80 96-80c11.2 0 22 1.6 32 4.6l0-116.7-224 49.8 0 206.3c0 44.2-43 80-96 80s-96-35.8-96-80 43-80 96-80c11.2 0 22 1.6 32 4.6L128 96c0-15 10.4-28 25.1-31.2l288-64c9.5-2.1 19.4 .2 27 6.3z' - ], - plus: [ - 448, - 'M256 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 160-160 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l160 0 0 160c0 17.7 14.3 32 32 32s32-14.3 32-32l0-160 160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-160 0 0-160z' + 'M48 352l48 0 134.1 119.2c6.4 5.7 14.6 8.8 23.1 8.8 19.2 0 34.8-15.6 34.8-34.8l0-378.4c0-19.2-15.6-34.8-34.8-34.8-8.5 0-16.7 3.1-23.1 8.8L96 160 48 160c-26.5 0-48 21.5-48 48l0 96c0 26.5 21.5 48 48 48zM441.1 107c-10.3-8.4-25.4-6.8-33.8 3.5s-6.8 25.4 3.5 33.8C443.3 170.7 464 210.9 464 256s-20.7 85.3-53.2 111.8c-10.3 8.4-11.8 23.5-3.5 33.8s23.5 11.8 33.8 3.5c43.2-35.2 70.9-88.9 70.9-149s-27.7-113.8-70.9-149zm-60.5 74.5c-10.3-8.4-25.4-6.8-33.8 3.5s-6.8 25.4 3.5 33.8C361.1 227.6 368 241 368 256s-6.9 28.4-17.7 37.3c-10.3 8.4-11.8 23.5-3.5 33.8s23.5 11.8 33.8 3.5C402.1 312.9 416 286.1 416 256s-13.9-56.9-35.5-74.5z' ], 'volume-up': [ 640, diff --git a/static/js/core/theme-init.js b/static/js/core/theme-init.js index a3dbda3a..51ceb3fd 100644 --- a/static/js/core/theme-init.js +++ b/static/js/core/theme-init.js @@ -1,3 +1,21 @@ -// Apply saved theme immediately (render-blocking) to prevent FOUC. -// This file MUST be loaded without "defer" or "async". -if (localStorage.getItem('oxicloud_theme') === 'dark') document.documentElement.setAttribute('data-theme', 'dark'); +// Apply saved colour-scheme choice immediately (render-blocking) to prevent FOUC. +// This file MUST be loaded WITHOUT "defer" or "async" so it runs before any paint. +// +// localStorage values: +// 'light' / 'dark' → force that mode (stamp html[data-color-scheme]). +// 'auto' / absent → follow the OS preference (no attribute → CSS default +// `color-scheme: light dark` resolves via prefers-color-scheme). +// +// Backward compat: existing users have 'light' / 'dark' stored from the old +// binary toggle; both still apply correctly. New 'auto' value is introduced +// by the segmented control in user-menu. + +(() => { + var saved = localStorage.getItem('oxicloud_theme'); + var html = document.documentElement; + if (saved === 'light' || saved === 'dark') { + html.setAttribute('data-color-scheme', saved); + } else { + html.removeAttribute('data-color-scheme'); + } +})(); diff --git a/static/js/features/itemTooltip.js b/static/js/features/itemTooltip.js index 3848b55a..8dd3e6ff 100644 --- a/static/js/features/itemTooltip.js +++ b/static/js/features/itemTooltip.js @@ -27,21 +27,19 @@ import { i18n } from '../core/i18n.js'; import { systemUsers } from '../model/systemUsers.js'; // ── Tooltip DOM ─────────────────────────────────────────────────────────────── +// +// The tooltip element is a static node inside `.search-slot` (see index.html) +// and is absolutely positioned over `.search-container`. Show/hide is a +// simple `.hidden` toggle on the tooltip itself — the search bar underneath +// is never touched, so there's no two-element swap to keep in sync. -/** @returns {HTMLElement} */ -function _getOrCreateTooltip() { - let el = document.getElementById('path-tooltip'); - if (!el) { - el = document.createElement('div'); - el.id = 'path-tooltip'; - el.className = 'path-tooltip hidden'; - document.body.appendChild(el); - } - return el; +/** @returns {HTMLElement | null} */ +function _getTooltip() { + return document.getElementById('path-tooltip'); } function _hide() { - const el = document.getElementById('path-tooltip'); + const el = _getTooltip(); if (el) el.classList.add('hidden'); } @@ -98,7 +96,8 @@ function _onEnter(e) { // Nothing to show — don't display an all-? tooltip. if (!ownerId && !path) return; - const tooltip = _getOrCreateTooltip(); + const tooltip = _getTooltip(); + if (!tooltip) return; // Clear previous content. while (tooltip.firstChild) tooltip.removeChild(tooltip.firstChild); @@ -140,14 +139,19 @@ const _registry = new WeakMap(); // ── Public API ──────────────────────────────────────────────────────────────── +/** Containers known to expose `data-path`/`data-owner-id` for tooltip use. + * Add new opt-in row classes here when other views want the tooltip — each + * one must stamp the dataset attributes itself. */ +const _TOOLTIP_SELECTOR = '.file-item, .ms-resource-row, .ms-grant-row'; + /** - * Attach tooltip listeners to every `.file-item` inside `container`. - * Items with neither `data-owner-id` nor `data-path` will not trigger the + * Attach tooltip listeners to every tooltip-capable row inside `container`. + * Rows with neither `data-owner-id` nor `data-path` will not trigger the * tooltip. Safe to call repeatedly — already-wired elements are skipped. * @param {HTMLElement} container */ function init(container) { - for (const item of container.querySelectorAll('.file-item')) { + for (const item of container.querySelectorAll(_TOOLTIP_SELECTOR)) { const el = /** @type {HTMLElement} */ (item); if (_registry.has(el)) continue; // already wired @@ -161,12 +165,12 @@ function init(container) { } /** - * Remove tooltip listeners from all `.file-item` elements inside `container` + * Remove tooltip listeners from all tooltip-capable rows inside `container` * and hide any visible tooltip. * @param {HTMLElement} container */ function destroy(container) { - for (const item of container.querySelectorAll('.file-item')) { + for (const item of container.querySelectorAll(_TOOLTIP_SELECTOR)) { const el = /** @type {HTMLElement} */ (item); const h = _registry.get(el); if (h) { diff --git a/static/js/views/favorites/favoritesView.js b/static/js/views/favorites/favoritesView.js index c721e92a..a9bee282 100644 --- a/static/js/views/favorites/favoritesView.js +++ b/static/js/views/favorites/favoritesView.js @@ -31,8 +31,8 @@ import { systemUsers } from '../../model/systemUsers.js'; /** @import {FavoritesResourceItem, FileItem, FolderItem, ResourceTypeEnum} from '../../core/types.js' */ /** - * @typedef {{ key: string, label: string, orderBy: string, - * keyFn: (item: FileItem|FolderItem) => string|null, + * @typedef {{ key: string, label: string, icon?: string, orderBy: string, + * keyFn?: (item: FileItem|FolderItem) => string|null, * labelFn?: (key: string) => string, * headerNodeFn?: (key: string) => HTMLElement }} GroupByDef */ @@ -44,11 +44,21 @@ import { systemUsers } from '../../model/systemUsers.js'; * @type {GroupByDef[]} */ const GROUP_BY_DEFS = [ + { + key: '', + get label() { + return i18n.t('files.name', 'Name'); + }, + icon: 'fas fa-arrow-up-a-z', + orderBy: 'name' + // no keyFn → flat list. + }, { key: 'owner', get label() { return i18n.t('groupby.owner', 'Owner'); }, + icon: 'fas fa-layer-group', orderBy: 'owner', // keyFn groups by UUID — stable, avoids collisions on identical display names. keyFn: (item) => { @@ -63,6 +73,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.type', 'Type'); }, + icon: 'fas fa-layer-group', orderBy: 'type', // keyFn: folders → 'Folder' swimlane; files → their `category` field. keyFn: (item) => ('mime_type' in item ? /** @type {Record} */ (/** @type {unknown} */ (item)).category || 'other' : 'Folder'), @@ -92,6 +103,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.size', 'Size'); }, + icon: 'fas fa-layer-group', orderBy: 'size', // Folders have no size — sizeBucket(-1) returns the "Folders" label. keyFn: (item) => { @@ -105,6 +117,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.favoriteDate', 'Favorite date'); }, + icon: 'fas fa-layer-group', orderBy: 'favorited_at', // sort_date is stored as unix seconds in _mapItems(). keyFn: (item) => { @@ -117,6 +130,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.modifiedAt', 'Modified date'); }, + icon: 'fas fa-layer-group', orderBy: 'modified_at', // modified_at is a unix seconds timestamp on FileItem/FolderItem. keyFn: (item) => { diff --git a/static/js/views/myShares/mySharesView.js b/static/js/views/myShares/mySharesView.js index 257d9c8d..b5d1ccfd 100644 --- a/static/js/views/myShares/mySharesView.js +++ b/static/js/views/myShares/mySharesView.js @@ -22,7 +22,7 @@ import { grants } from '../../model/grants.js'; /** @import {FileItem, FolderItem} from '../../core/types.js' */ /** - * @typedef {{ key: string, label: string, orderBy: string }} GroupByDef + * @typedef {{ key: string, label: string, icon?: string, orderBy: string }} GroupByDef * @typedef {'items'|'sharedWith'} ViewMode */ @@ -42,6 +42,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.byFiles', 'By files'); }, + icon: 'fas fa-layer-group', orderBy: 'type' }, { @@ -49,6 +50,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.sharedWith', 'Shared with'); }, + icon: 'fas fa-layer-group', orderBy: 'subject' } ]; diff --git a/static/js/views/recent/recentView.js b/static/js/views/recent/recentView.js index a758bb3a..b68a8ca2 100644 --- a/static/js/views/recent/recentView.js +++ b/static/js/views/recent/recentView.js @@ -32,8 +32,8 @@ import { systemUsers } from '../../model/systemUsers.js'; /** @import {FileItem, FolderItem, ResourceTypeEnum} from '../../core/types.js' */ /** - * @typedef {{ key: string, label: string, orderBy: string, - * keyFn: (item: FileItem|FolderItem) => string|null, + * @typedef {{ key: string, label: string, icon?: string, orderBy: string, + * keyFn?: (item: FileItem|FolderItem) => string|null, * labelFn?: (key: string) => string, * headerNodeFn?: (key: string) => HTMLElement }} GroupByDef */ @@ -54,11 +54,21 @@ import { systemUsers } from '../../model/systemUsers.js'; * @type {GroupByDef[]} */ const GROUP_BY_DEFS = [ + { + key: '', + get label() { + return i18n.t('files.name', 'Name'); + }, + icon: 'fas fa-arrow-up-a-z', + orderBy: 'name' + // no keyFn → flat list. + }, { key: 'owner', get label() { return i18n.t('groupby.owner', 'Owner'); }, + icon: 'fas fa-layer-group', orderBy: 'owner', keyFn: (item) => { const r = /** @type {Record} */ (/** @type {unknown} */ (item)); @@ -72,6 +82,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.type', 'Type'); }, + icon: 'fas fa-layer-group', orderBy: 'type', keyFn: (item) => ('mime_type' in item ? /** @type {Record} */ (/** @type {unknown} */ (item)).category || 'other' : 'Folder'), labelFn: (key) => { @@ -100,6 +111,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.size', 'Size'); }, + icon: 'fas fa-layer-group', orderBy: 'size', keyFn: (item) => { if (!('mime_type' in item)) return sizeBucket(-1); @@ -112,6 +124,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.accessedAt', 'Accessed date'); }, + icon: 'fas fa-layer-group', orderBy: 'accessed_at', // sort_date is unix seconds set in _mapItems(); keyFn returns the bucket label. keyFn: (item) => { @@ -124,6 +137,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.modifiedAt', 'Modified date'); }, + icon: 'fas fa-layer-group', orderBy: 'modified_at', keyFn: (item) => { const r = /** @type {Record} */ (/** @type {unknown} */ (item)); diff --git a/static/js/views/sharedWithMe/sharedWithMeView.js b/static/js/views/sharedWithMe/sharedWithMeView.js index d8323b87..34008993 100644 --- a/static/js/views/sharedWithMe/sharedWithMeView.js +++ b/static/js/views/sharedWithMe/sharedWithMeView.js @@ -25,8 +25,8 @@ import { systemUsers } from '../../model/systemUsers.js'; /** @import {SharedWithMeItem, FileItem, FolderItem, ResourceTypeEnum} from '../../core/types.js' */ /** - * @typedef {{ key: string, label: string, orderBy: string, - * keyFn: (item: FileItem|FolderItem) => string|null, + * @typedef {{ key: string, label: string, icon?: string, orderBy: string, + * keyFn?: (item: FileItem|FolderItem) => string|null, * labelFn?: (key: string) => string, * headerNodeFn?: (key: string) => HTMLElement }} GroupByDef */ @@ -44,6 +44,15 @@ import { systemUsers } from '../../model/systemUsers.js'; * @type {GroupByDef[]} */ const GROUP_BY_DEFS = [ + { + key: '', + get label() { + return i18n.t('files.name', 'Name'); + }, + icon: 'fas fa-arrow-up-a-z', + orderBy: 'name' + // no keyFn → flat list. + }, { key: 'owner', // label is accessed via syncGroupByMenu → read at section-switch time, @@ -51,6 +60,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.owner', 'Owner'); }, + icon: 'fas fa-layer-group', orderBy: 'granted_by', // keyFn groups by UUID — stable and unique, avoids collisions between // users with the same display name. @@ -67,6 +77,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.type', 'Type'); }, + icon: 'fas fa-layer-group', orderBy: 'type', // keyFn: folders get their own swimlane; files use the pre-computed // `category` field from the DTO (e.g. 'Image', 'Video', 'Audio' …). @@ -99,6 +110,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.size', 'Size'); }, + icon: 'fas fa-layer-group', orderBy: 'size', // keyFn: the key IS the bucket label returned by sizeBucket(), so no // separate labelFn is needed (same pattern as shareDate). @@ -115,6 +127,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.shareDate', 'Share date'); }, + icon: 'fas fa-layer-group', orderBy: 'granted_at', // keyFn returns the human-readable bucket label; the label IS the key // because consecutive items with the same bucket should be in one group. diff --git a/static/js/views/trash/trashView.js b/static/js/views/trash/trashView.js index ad09e579..a4b0f79a 100644 --- a/static/js/views/trash/trashView.js +++ b/static/js/views/trash/trashView.js @@ -28,8 +28,8 @@ import { fetchTrashPage } from '../../model/trashModel.js'; /** @import {FileItem, FolderItem, ResourceTypeEnum, TrashResourceItem} from '../../core/types.js' */ /** - * @typedef {{ key: string, label: string, orderBy: string, reverseDefault?: boolean, - * keyFn: (item: FileItem|FolderItem) => string|null, + * @typedef {{ key: string, label: string, icon?: string, orderBy: string, reverseDefault?: boolean, + * keyFn?: (item: FileItem|FolderItem) => string|null, * labelFn?: (key: string) => string, * headerNodeFn?: (key: string) => HTMLElement }} GroupByDef */ @@ -49,11 +49,22 @@ import { fetchTrashPage } from '../../model/trashModel.js'; * @type {GroupByDef[]} */ const GROUP_BY_DEFS = [ + { + key: '', + get label() { + return i18n.t('files.name', 'Name'); + }, + icon: 'fas fa-arrow-up-a-z', + orderBy: 'name', + reverseDefault: false + // no keyFn → ResourceListComponent renders a flat list (server pins folders first). + }, { key: 'remainingDays', get label() { return i18n.t('trash.groupby.remaining_days', 'Remaining days'); }, + icon: 'fas fa-layer-group', orderBy: 'deletion_date', reverseDefault: false, keyFn: (item) => { @@ -66,6 +77,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.type', 'Type'); }, + icon: 'fas fa-layer-group', orderBy: 'type', reverseDefault: false, keyFn: (item) => ('mime_type' in item ? /** @type {Record} */ (/** @type {unknown} */ (item)).category || 'other' : 'Folder'), @@ -95,6 +107,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('groupby.size', 'Size'); }, + icon: 'fas fa-layer-group', orderBy: 'size', reverseDefault: false, keyFn: (item) => { @@ -108,6 +121,7 @@ const GROUP_BY_DEFS = [ get label() { return i18n.t('trash.groupby.trashed_time', 'Trashed time'); }, + icon: 'fas fa-layer-group', orderBy: 'trashed_at', reverseDefault: false, keyFn: (item) => { diff --git a/static/locales/ar.json b/static/locales/ar.json index 940e18d7..085e9bfd 100644 --- a/static/locales/ar.json +++ b/static/locales/ar.json @@ -126,7 +126,12 @@ "about_description": "منصة تخزين سحابي مبنية بـ Rust و Clean Architecture. سريعة وآمنة وخاصة.", "admin_panel": "لوحة الإدارة", "profile": "ملفي الشخصي", - "role_user": "مستخدم" + "role_user": "مستخدم", + "theme": { + "light": "فاتح", + "dark": "داكن", + "auto": "مثل النظام" + } }, "share": { "dialogTitle": "رابط المشاركة", @@ -341,10 +346,29 @@ "empty_state": "سلة المهملات فارغة", "original_location": "الموقع الأصلي", "deleted_date": "تاريخ الحذف", + "remaining": "المتبقي", "actions": "الإجراءات", "restore": "استعادة", "delete_permanently": "حذف نهائياً", - "empty_confirm": "هل أنت متأكد أنك تريد تفريغ سلة المهملات؟ سيتم حذف جميع العناصر نهائياً." + "empty_confirm": "هل أنت متأكد أنك تريد تفريغ سلة المهملات؟ سيتم حذف جميع العناصر نهائياً.", + "groupby": { + "remaining_days": "الأيام المتبقية", + "trashed_time": "وقت الحذف" + } + }, + "daysRemaining": { + "expired": "منتهية الصلاحية", + "today": "اليوم", + "tomorrow": "غدًا", + "inDays": "{{count}} يوم" + }, + "expiryChip": { + "never": "لا تنتهي الصلاحية", + "expired": "منتهية الصلاحية", + "today": "تنتهي الصلاحية اليوم", + "tomorrow": "تنتهي الصلاحية غدًا", + "inDays": "تنتهي الصلاحية خلال {{count}} يوم", + "onDate": "تنتهي الصلاحية في {{date}}" }, "auth": { "login_title": "تسجيل الدخول", diff --git a/static/locales/de.json b/static/locales/de.json index 24d1a2c7..25da51dc 100644 --- a/static/locales/de.json +++ b/static/locales/de.json @@ -126,7 +126,12 @@ "about_description": "Cloud-Speicherplattform mit Rust und Clean Architecture. Schnell, sicher und privat.", "admin_panel": "Admin-Panel", "profile": "Mein Profil", - "role_user": "Benutzer" + "role_user": "Benutzer", + "theme": { + "light": "Hell", + "dark": "Dunkel", + "auto": "Wie System" + } }, "share": { "dialogTitle": "Link teilen", @@ -341,10 +346,29 @@ "empty_state": "Der Papierkorb ist leer", "original_location": "Ursprünglicher Speicherort", "deleted_date": "Löschdatum", + "remaining": "Verbleibend", "actions": "Aktionen", "restore": "Wiederherstellen", "delete_permanently": "Endgültig löschen", - "empty_confirm": "Sind Sie sicher, dass Sie den Papierkorb leeren möchten? Alle Elemente werden endgültig gelöscht." + "empty_confirm": "Sind Sie sicher, dass Sie den Papierkorb leeren möchten? Alle Elemente werden endgültig gelöscht.", + "groupby": { + "remaining_days": "Verbleibende Tage", + "trashed_time": "Löschzeit" + } + }, + "daysRemaining": { + "expired": "Abgelaufen", + "today": "Heute", + "tomorrow": "Morgen", + "inDays": "{{count}} Tage" + }, + "expiryChip": { + "never": "Läuft nie ab", + "expired": "Abgelaufen", + "today": "Läuft heute ab", + "tomorrow": "Läuft morgen ab", + "inDays": "Läuft in {{count}} Tagen ab", + "onDate": "Läuft am {{date}} ab" }, "auth": { "login_title": "Anmelden", diff --git a/static/locales/en.json b/static/locales/en.json index 220fcfc5..b897107a 100644 --- a/static/locales/en.json +++ b/static/locales/en.json @@ -126,7 +126,12 @@ "about_description": "Cloud storage platform built with Rust & Clean Architecture. Fast, secure, and private.", "admin_panel": "Admin Panel", "profile": "My Profile", - "role_user": "User" + "role_user": "User", + "theme": { + "light": "Light", + "dark": "Dark", + "auto": "Like OS" + } }, "share": { "dialogTitle": "Share Link", diff --git a/static/locales/es.json b/static/locales/es.json index a0deed14..153a1061 100644 --- a/static/locales/es.json +++ b/static/locales/es.json @@ -245,7 +245,12 @@ "about_description": "Plataforma de almacenamiento en la nube creada con Rust y Arquitectura Limpia. Rápida, segura y privada.", "admin_panel": "Panel de administración", "profile": "Mi perfil", - "role_user": "Usuario" + "role_user": "Usuario", + "theme": { + "light": "Claro", + "dark": "Oscuro", + "auto": "Como el sistema" + } }, "files": { "name": "Nombre", @@ -341,10 +346,29 @@ "empty_state": "La papelera está vacía", "original_location": "Ubicación original", "deleted_date": "Fecha de eliminación", + "remaining": "Restante", "actions": "Acciones", "restore": "Restaurar", "delete_permanently": "Eliminar permanentemente", - "empty_confirm": "¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos." + "empty_confirm": "¿Estás seguro de que quieres vaciar la papelera? Esta acción eliminará permanentemente todos los elementos.", + "groupby": { + "remaining_days": "Días restantes", + "trashed_time": "Fecha de eliminación" + } + }, + "daysRemaining": { + "expired": "Caducado", + "today": "Hoy", + "tomorrow": "Mañana", + "inDays": "{{count}} días" + }, + "expiryChip": { + "never": "Nunca caduca", + "expired": "Caducado", + "today": "Caduca hoy", + "tomorrow": "Caduca mañana", + "inDays": "Caduca en {{count}} días", + "onDate": "Caduca el {{date}}" }, "auth": { "login_title": "Iniciar sesión", diff --git a/static/locales/fa.json b/static/locales/fa.json index e4ffcce5..b5040586 100644 --- a/static/locales/fa.json +++ b/static/locales/fa.json @@ -126,7 +126,12 @@ "about_description": "پلتفرم ذخیره‌سازی ابری ساخته شده با Rust و معماری تمیز. سریع، امن و خصوصی.", "admin_panel": "پنل مدیریت", "profile": "نمایه من", - "role_user": "کاربر" + "role_user": "کاربر", + "theme": { + "light": "روشن", + "dark": "تاریک", + "auto": "مانند سیستم" + } }, "share": { "dialogTitle": "پیوند هم‌رسانی", @@ -341,10 +346,29 @@ "empty_state": "سطل زباله خالی است", "original_location": "محل اصلی", "deleted_date": "تاریخ حذف", + "remaining": "باقی‌مانده", "actions": "عملیات", "restore": "بازیابی", "delete_permanently": "حذف دائمی", - "empty_confirm": "آیا مطمئن هستید که می‌خواهید سطل زباله را خالی کنید؟ این کار همهٔ موارد را به‌طور دائمی حذف خواهد کرد." + "empty_confirm": "آیا مطمئن هستید که می‌خواهید سطل زباله را خالی کنید؟ این کار همهٔ موارد را به‌طور دائمی حذف خواهد کرد.", + "groupby": { + "remaining_days": "روزهای باقی‌مانده", + "trashed_time": "زمان حذف" + } + }, + "daysRemaining": { + "expired": "منقضی شده", + "today": "امروز", + "tomorrow": "فردا", + "inDays": "{{count}} روز" + }, + "expiryChip": { + "never": "هرگز منقضی نمی‌شود", + "expired": "منقضی شده", + "today": "امروز منقضی می‌شود", + "tomorrow": "فردا منقضی می‌شود", + "inDays": "در {{count}} روز منقضی می‌شود", + "onDate": "در {{date}} منقضی می‌شود" }, "auth": { "login_title": "ورود", diff --git a/static/locales/fr.json b/static/locales/fr.json index b410050f..98fc7920 100644 --- a/static/locales/fr.json +++ b/static/locales/fr.json @@ -126,7 +126,12 @@ "about_description": "Plateforme de stockage cloud construite avec Rust et Architecture Propre. Rapide, sécurisée et privée.", "admin_panel": "Panneau d'administration", "profile": "Mon profil", - "role_user": "Utilisateur" + "role_user": "Utilisateur", + "theme": { + "light": "Clair", + "dark": "Sombre", + "auto": "Comme le système" + } }, "share": { "dialogTitle": "Lien de partage", @@ -341,10 +346,29 @@ "empty_state": "La corbeille est vide", "original_location": "Emplacement d'origine", "deleted_date": "Date de suppression", + "remaining": "Restant", "actions": "Actions", "restore": "Restaurer", "delete_permanently": "Supprimer définitivement", - "empty_confirm": "Êtes-vous sûr de vouloir vider la corbeille ? Tous les éléments seront définitivement supprimés." + "empty_confirm": "Êtes-vous sûr de vouloir vider la corbeille ? Tous les éléments seront définitivement supprimés.", + "groupby": { + "remaining_days": "Jours restants", + "trashed_time": "Date de suppression" + } + }, + "daysRemaining": { + "expired": "Expiré", + "today": "Aujourd'hui", + "tomorrow": "Demain", + "inDays": "{{count}} jours" + }, + "expiryChip": { + "never": "N'expire jamais", + "expired": "Expiré", + "today": "Expire aujourd'hui", + "tomorrow": "Expire demain", + "inDays": "Expire dans {{count}} jours", + "onDate": "Expire le {{date}}" }, "auth": { "login_title": "Se connecter", diff --git a/static/locales/hi.json b/static/locales/hi.json index 011dbec5..679f54d1 100644 --- a/static/locales/hi.json +++ b/static/locales/hi.json @@ -126,7 +126,12 @@ "about_description": "Rust और Clean Architecture से बना क्लाउड स्टोरेज प्लेटफ़ॉर्म। तेज़, सुरक्षित और निजी।", "admin_panel": "एडमिन पैनल", "profile": "मेरी प्रोफ़ाइल", - "role_user": "उपयोगकर्ता" + "role_user": "उपयोगकर्ता", + "theme": { + "light": "हल्का", + "dark": "गहरा", + "auto": "सिस्टम जैसा" + } }, "share": { "dialogTitle": "शेयर लिंक", @@ -341,10 +346,29 @@ "empty_state": "रद्दी खाली है", "original_location": "मूल स्थान", "deleted_date": "हटाने की तिथि", + "remaining": "शेष", "actions": "कार्य", "restore": "पुनर्स्थापित करें", "delete_permanently": "स्थायी रूप से हटाएँ", - "empty_confirm": "क्या आप वाकई रद्दी खाली करना चाहते हैं? यह सभी आइटम स्थायी रूप से हटा देगा।" + "empty_confirm": "क्या आप वाकई रद्दी खाली करना चाहते हैं? यह सभी आइटम स्थायी रूप से हटा देगा।", + "groupby": { + "remaining_days": "शेष दिन", + "trashed_time": "हटाने का समय" + } + }, + "daysRemaining": { + "expired": "समाप्त", + "today": "आज", + "tomorrow": "कल", + "inDays": "{{count}} दिन" + }, + "expiryChip": { + "never": "कभी समाप्त नहीं होता", + "expired": "समाप्त", + "today": "आज समाप्त होता है", + "tomorrow": "कल समाप्त होता है", + "inDays": "{{count}} दिनों में समाप्त होता है", + "onDate": "{{date}} को समाप्त होता है" }, "auth": { "login_title": "साइन इन", diff --git a/static/locales/it.json b/static/locales/it.json index 0977c1f3..eb53fcd5 100644 --- a/static/locales/it.json +++ b/static/locales/it.json @@ -126,7 +126,12 @@ "about_description": "Piattaforma di archiviazione cloud realizzata con Rust & Architettura Pulita. Veloce, sicura e privata.", "admin_panel": "Pannello di amministrazione", "profile": "Il mio profilo", - "role_user": "Utente" + "role_user": "Utente", + "theme": { + "light": "Chiaro", + "dark": "Scuro", + "auto": "Come il sistema" + } }, "share": { "dialogTitle": "Link di condivisione", @@ -341,10 +346,29 @@ "empty_state": "Il cestino è vuoto", "original_location": "Posizione originale", "deleted_date": "Data di eliminazione", + "remaining": "Rimanente", "actions": "Azioni", "restore": "Ripristina", "delete_permanently": "Elimina definitivamente", - "empty_confirm": "Sei sicuro di voler svuotare il cestino? Questa operazione eliminerà definitivamente tutti gli elementi." + "empty_confirm": "Sei sicuro di voler svuotare il cestino? Questa operazione eliminerà definitivamente tutti gli elementi.", + "groupby": { + "remaining_days": "Giorni rimanenti", + "trashed_time": "Data di eliminazione" + } + }, + "daysRemaining": { + "expired": "Scaduto", + "today": "Oggi", + "tomorrow": "Domani", + "inDays": "{{count}} giorni" + }, + "expiryChip": { + "never": "Non scade mai", + "expired": "Scaduto", + "today": "Scade oggi", + "tomorrow": "Scade domani", + "inDays": "Scade tra {{count}} giorni", + "onDate": "Scade il {{date}}" }, "auth": { "login_title": "Accedi", diff --git a/static/locales/ja.json b/static/locales/ja.json index 17b3d1fc..e97f54cb 100644 --- a/static/locales/ja.json +++ b/static/locales/ja.json @@ -126,7 +126,12 @@ "about_description": "RustとClean Architectureで構築されたクラウドストレージプラットフォーム。高速・安全・プライベート。", "admin_panel": "管理パネル", "profile": "マイプロフィール", - "role_user": "ユーザー" + "role_user": "ユーザー", + "theme": { + "light": "ライト", + "dark": "ダーク", + "auto": "システムに合わせる" + } }, "share": { "dialogTitle": "共有リンク", @@ -341,10 +346,29 @@ "empty_state": "ゴミ箱は空です", "original_location": "元の場所", "deleted_date": "削除日", + "remaining": "残り", "actions": "操作", "restore": "復元", "delete_permanently": "完全に削除", - "empty_confirm": "ゴミ箱を空にしますか?すべてのアイテムが完全に削除されます。" + "empty_confirm": "ゴミ箱を空にしますか?すべてのアイテムが完全に削除されます。", + "groupby": { + "remaining_days": "残り日数", + "trashed_time": "削除日時" + } + }, + "daysRemaining": { + "expired": "期限切れ", + "today": "今日", + "tomorrow": "明日", + "inDays": "{{count}}日" + }, + "expiryChip": { + "never": "期限なし", + "expired": "期限切れ", + "today": "今日で期限切れ", + "tomorrow": "明日で期限切れ", + "inDays": "{{count}}日後に期限切れ", + "onDate": "{{date}}に期限切れ" }, "auth": { "login_title": "サインイン", diff --git a/static/locales/ko.json b/static/locales/ko.json index 131b5805..6b5a7159 100644 --- a/static/locales/ko.json +++ b/static/locales/ko.json @@ -126,7 +126,12 @@ "about_description": "Rust와 Clean Architecture로 구축된 클라우드 스토리지 플랫폼. 빠르고, 안전하고, 프라이빗합니다.", "admin_panel": "관리자 패널", "profile": "내 프로필", - "role_user": "사용자" + "role_user": "사용자", + "theme": { + "light": "라이트", + "dark": "다크", + "auto": "시스템과 동일" + } }, "share": { "dialogTitle": "공유 링크", @@ -341,10 +346,29 @@ "empty_state": "휴지통이 비어 있습니다", "original_location": "원래 위치", "deleted_date": "삭제일", + "remaining": "남음", "actions": "작업", "restore": "복원", "delete_permanently": "영구 삭제", - "empty_confirm": "휴지통을 비우시겠습니까? 모든 항목이 영구적으로 삭제됩니다." + "empty_confirm": "휴지통을 비우시겠습니까? 모든 항목이 영구적으로 삭제됩니다.", + "groupby": { + "remaining_days": "남은 일수", + "trashed_time": "삭제 시간" + } + }, + "daysRemaining": { + "expired": "만료됨", + "today": "오늘", + "tomorrow": "내일", + "inDays": "{{count}}일" + }, + "expiryChip": { + "never": "만료되지 않음", + "expired": "만료됨", + "today": "오늘 만료", + "tomorrow": "내일 만료", + "inDays": "{{count}}일 후 만료", + "onDate": "{{date}}에 만료" }, "auth": { "login_title": "로그인", diff --git a/static/locales/nl.json b/static/locales/nl.json index 397bc034..34000d55 100644 --- a/static/locales/nl.json +++ b/static/locales/nl.json @@ -126,7 +126,12 @@ "about_description": "Cloudopslagplatform gebouwd met Rust & Clean Architecture. Snel, veilig en privé.", "admin_panel": "Beheerpaneel", "profile": "Mijn profiel", - "role_user": "Gebruiker" + "role_user": "Gebruiker", + "theme": { + "light": "Licht", + "dark": "Donker", + "auto": "Zoals systeem" + } }, "share": { "dialogTitle": "Deellink", @@ -341,10 +346,29 @@ "empty_state": "Prullenbak is leeg", "original_location": "Oorspronkelijke locatie", "deleted_date": "Verwijderdatum", + "remaining": "Resterend", "actions": "Acties", "restore": "Herstellen", "delete_permanently": "Permanent verwijderen", - "empty_confirm": "Weet je zeker dat je de prullenbak wilt legen? Dit verwijdert alle items permanent." + "empty_confirm": "Weet je zeker dat je de prullenbak wilt legen? Dit verwijdert alle items permanent.", + "groupby": { + "remaining_days": "Resterende dagen", + "trashed_time": "Verwijderd op" + } + }, + "daysRemaining": { + "expired": "Verlopen", + "today": "Vandaag", + "tomorrow": "Morgen", + "inDays": "{{count}} dagen" + }, + "expiryChip": { + "never": "Verloopt nooit", + "expired": "Verlopen", + "today": "Verloopt vandaag", + "tomorrow": "Verloopt morgen", + "inDays": "Verloopt over {{count}} dagen", + "onDate": "Verloopt op {{date}}" }, "auth": { "login_title": "Inloggen", diff --git a/static/locales/pl.json b/static/locales/pl.json index da2a023e..edfcca80 100644 --- a/static/locales/pl.json +++ b/static/locales/pl.json @@ -126,7 +126,12 @@ "about_description": "Platforma pamięci masowej w chmurze zbudowana w oparciu o Rust & Clean Architecture. Szybka, bezpieczna i prywatna.", "admin_panel": "Panel administratora", "profile": "Mój profil", - "role_user": "Użytkownik" + "role_user": "Użytkownik", + "theme": { + "light": "Jasny", + "dark": "Ciemny", + "auto": "Jak system" + } }, "share": { "dialogTitle": "Link udostępniania", @@ -341,10 +346,29 @@ "empty_state": "Kosz jest pusty", "original_location": "Pierwotna lokalizacja", "deleted_date": "Data usunięcia", + "remaining": "Pozostało", "actions": "Akcje", "restore": "Przywróć", "delete_permanently": "Usuń trwale", - "empty_confirm": "Czy na pewno chcesz opróżnić kosz? Wszystkie elementy zostaną trwale usunięte." + "empty_confirm": "Czy na pewno chcesz opróżnić kosz? Wszystkie elementy zostaną trwale usunięte.", + "groupby": { + "remaining_days": "Pozostałe dni", + "trashed_time": "Czas usunięcia" + } + }, + "daysRemaining": { + "expired": "Wygasł", + "today": "Dziś", + "tomorrow": "Jutro", + "inDays": "{{count}} dni" + }, + "expiryChip": { + "never": "Nigdy nie wygasa", + "expired": "Wygasł", + "today": "Wygasa dziś", + "tomorrow": "Wygasa jutro", + "inDays": "Wygasa za {{count}} dni", + "onDate": "Wygasa {{date}}" }, "auth": { "login_title": "Zaloguj się", diff --git a/static/locales/pt.json b/static/locales/pt.json index 9835d164..1a5599f0 100644 --- a/static/locales/pt.json +++ b/static/locales/pt.json @@ -126,7 +126,12 @@ "about_description": "Plataforma de armazenamento em nuvem construída com Rust e Arquitetura Limpa. Rápida, segura e privada.", "admin_panel": "Painel de administração", "profile": "Meu perfil", - "role_user": "Usuário" + "role_user": "Usuário", + "theme": { + "light": "Claro", + "dark": "Escuro", + "auto": "Como o sistema" + } }, "share": { "dialogTitle": "Link de compartilhamento", @@ -341,10 +346,29 @@ "empty_state": "A lixeira está vazia", "original_location": "Local original", "deleted_date": "Data de exclusão", + "remaining": "Restante", "actions": "Ações", "restore": "Restaurar", "delete_permanently": "Excluir permanentemente", - "empty_confirm": "Tem certeza de que deseja esvaziar a lixeira? Todos os itens serão excluídos permanentemente." + "empty_confirm": "Tem certeza de que deseja esvaziar a lixeira? Todos os itens serão excluídos permanentemente.", + "groupby": { + "remaining_days": "Dias restantes", + "trashed_time": "Data de exclusão" + } + }, + "daysRemaining": { + "expired": "Expirado", + "today": "Hoje", + "tomorrow": "Amanhã", + "inDays": "{{count}} dias" + }, + "expiryChip": { + "never": "Nunca expira", + "expired": "Expirado", + "today": "Expira hoje", + "tomorrow": "Expira amanhã", + "inDays": "Expira em {{count}} dias", + "onDate": "Expira em {{date}}" }, "auth": { "login_title": "Entrar", diff --git a/static/locales/ru.json b/static/locales/ru.json index f470d2cc..ef3316d1 100644 --- a/static/locales/ru.json +++ b/static/locales/ru.json @@ -126,7 +126,12 @@ "about_description": "Платформа облачного хранения на Rust с чистой архитектурой. Быстрая, безопасная и конфиденциальная.", "admin_panel": "Панель администратора", "profile": "Мой профиль", - "role_user": "Пользователь" + "role_user": "Пользователь", + "theme": { + "light": "Светлая", + "dark": "Тёмная", + "auto": "Как в системе" + } }, "share": { "dialogTitle": "Ссылка для обмена", @@ -341,10 +346,29 @@ "empty_state": "Корзина пуста", "original_location": "Исходное расположение", "deleted_date": "Дата удаления", + "remaining": "Осталось", "actions": "Действия", "restore": "Восстановить", "delete_permanently": "Удалить навсегда", - "empty_confirm": "Вы уверены, что хотите очистить корзину? Все элементы будут удалены навсегда." + "empty_confirm": "Вы уверены, что хотите очистить корзину? Все элементы будут удалены навсегда.", + "groupby": { + "remaining_days": "Осталось дней", + "trashed_time": "Время удаления" + } + }, + "daysRemaining": { + "expired": "Истёк", + "today": "Сегодня", + "tomorrow": "Завтра", + "inDays": "{{count}} дн." + }, + "expiryChip": { + "never": "Никогда не истекает", + "expired": "Истёк", + "today": "Истекает сегодня", + "tomorrow": "Истекает завтра", + "inDays": "Истекает через {{count}} дн.", + "onDate": "Истекает {{date}}" }, "auth": { "login_title": "Вход", diff --git a/static/locales/zh-TW.json b/static/locales/zh-TW.json index d6bd270e..23439867 100644 --- a/static/locales/zh-TW.json +++ b/static/locales/zh-TW.json @@ -126,7 +126,12 @@ "about_description": "基於 Rust 和整潔架構構建的雲端儲存平臺。快速、安全、私密。", "admin_panel": "管理面板", "profile": "我的資料", - "role_user": "使用者" + "role_user": "使用者", + "theme": { + "light": "淺色", + "dark": "深色", + "auto": "跟隨系統" + } }, "share": { "dialogTitle": "共享連結", @@ -341,10 +346,29 @@ "empty_state": "回收站為空", "original_location": "原始位置", "deleted_date": "刪除日期", + "remaining": "剩餘", "actions": "操作", "restore": "恢復", "delete_permanently": "永久刪除", - "empty_confirm": "你確定要清空回收站嗎?這將永久刪除所有專案。" + "empty_confirm": "你確定要清空回收站嗎?這將永久刪除所有專案。", + "groupby": { + "remaining_days": "剩餘天數", + "trashed_time": "刪除時間" + } + }, + "daysRemaining": { + "expired": "已過期", + "today": "今天", + "tomorrow": "明天", + "inDays": "{{count}} 天" + }, + "expiryChip": { + "never": "永不過期", + "expired": "已過期", + "today": "今天到期", + "tomorrow": "明天到期", + "inDays": "{{count}} 天後到期", + "onDate": "於 {{date}} 到期" }, "auth": { "login_title": "登入", diff --git a/static/locales/zh.json b/static/locales/zh.json index 6f04a4a2..f841cba4 100644 --- a/static/locales/zh.json +++ b/static/locales/zh.json @@ -126,7 +126,12 @@ "about_description": "基于 Rust 和整洁架构构建的云存储平台。快速、安全、私密。", "admin_panel": "管理面板", "profile": "我的资料", - "role_user": "用户" + "role_user": "用户", + "theme": { + "light": "浅色", + "dark": "深色", + "auto": "跟随系统" + } }, "share": { "dialogTitle": "共享链接", @@ -341,10 +346,29 @@ "empty_state": "回收站为空", "original_location": "原始位置", "deleted_date": "删除日期", + "remaining": "剩余", "actions": "操作", "restore": "恢复", "delete_permanently": "永久删除", - "empty_confirm": "你确定要清空回收站吗?这将永久删除所有项目。" + "empty_confirm": "你确定要清空回收站吗?这将永久删除所有项目。", + "groupby": { + "remaining_days": "剩余天数", + "trashed_time": "删除时间" + } + }, + "daysRemaining": { + "expired": "已过期", + "today": "今天", + "tomorrow": "明天", + "inDays": "{{count}} 天" + }, + "expiryChip": { + "never": "永不过期", + "expired": "已过期", + "today": "今天到期", + "tomorrow": "明天到期", + "inDays": "{{count}} 天后到期", + "onDate": "于 {{date}} 到期" }, "auth": { "login_title": "登录", diff --git a/static/login.html b/static/login.html index af2bfc15..c8a9032a 100644 --- a/static/login.html +++ b/static/login.html @@ -3,6 +3,10 @@ + + OxiCloud - Login diff --git a/static/profile.html b/static/profile.html index 9d1ea1ea..7a984390 100644 --- a/static/profile.html +++ b/static/profile.html @@ -3,6 +3,10 @@ + + OxiCloud — My Profile diff --git a/static/share.html b/static/share.html index 77574699..140d2b45 100644 --- a/static/share.html +++ b/static/share.html @@ -3,6 +3,10 @@ + + OxiCloud — Shared diff --git a/tests/e2e/scenarios/01-home-and-login.spec.ts b/tests/e2e/scenarios/01-home-and-login.spec.ts index 6ae2c3c7..c7014c70 100644 --- a/tests/e2e/scenarios/01-home-and-login.spec.ts +++ b/tests/e2e/scenarios/01-home-and-login.spec.ts @@ -59,12 +59,15 @@ test.describe('authenticated as admin', () => { test('theme can be changed to dark', async ({ page }) => { await page.locator('#user-avatar-btn').click(); await expect(page.locator('#user-menu')).toBeVisible(); - await page.locator('#theme-toggle-pill').click(); - // html element must carry data-theme="dark" - await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + // The appearance row is now a 3-option segmented control + // (Light / Like OS / Dark). Each option carries a `data-mode` attribute. + await page.locator('.theme-segmented__opt[data-mode="dark"]').click(); - // localStorage must persist the choice + // html element must carry data-color-scheme="dark" (new attribute). + await expect(page.locator('html')).toHaveAttribute('data-color-scheme', 'dark'); + + // localStorage must persist the choice. const theme = await page.evaluate(() => localStorage.getItem('oxicloud_theme')); expect(theme).toBe('dark'); @@ -75,9 +78,9 @@ test.describe('authenticated as admin', () => { mask: [page.locator('.storage-bar'), page.locator('.storage-info') ] }); - // Toggle back to light - await page.locator('#theme-toggle-pill').click(); - await expect(page.locator('html')).not.toHaveAttribute('data-theme', 'dark'); + // Switch back to light via the Light option. + await page.locator('.theme-segmented__opt[data-mode="light"]').click(); + await expect(page.locator('html')).toHaveAttribute('data-color-scheme', 'light'); const themeAfter = await page.evaluate(() => localStorage.getItem('oxicloud_theme')); expect(themeAfter).toBe('light');