-
CipherBox
-
Zero-knowledge encrypted cloud storage
+
+
CIPHERBOX
+
zero-knowledge encrypted storage
- Your files, encrypted on your device. We never see your data.
+ your files, encrypted on your device. we never see your data.
diff --git a/apps/web/src/styles/breadcrumbs.css b/apps/web/src/styles/breadcrumbs.css
index eab3dbe4e..f09653093 100644
--- a/apps/web/src/styles/breadcrumbs.css
+++ b/apps/web/src/styles/breadcrumbs.css
@@ -1,8 +1,8 @@
/**
- * Breadcrumb Navigation Styles
+ * Breadcrumb Navigation Styles - Terminal Aesthetic
*
- * Per CONTEXT.md: Simple back navigation with current folder name
- * Structure supports future dropdown-per-segment enhancement
+ * Per design: Terminal-style path display like ~/storage/documents/projects
+ * Shows current folder path with back navigation
*/
/* ==========================================================================
@@ -12,10 +12,18 @@
.breadcrumbs {
display: flex;
align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 0;
- font-size: 1rem;
+ gap: var(--spacing-xs);
+ padding: var(--spacing-xs) 0;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
min-height: 2.5rem;
+ color: var(--color-text-primary);
+}
+
+/* Terminal-style path prefix */
+.breadcrumbs::before {
+ content: "~/";
+ color: var(--color-text-secondary);
}
/* ==========================================================================
@@ -29,40 +37,58 @@
width: 2rem;
height: 2rem;
padding: 0;
- background: transparent;
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 4px;
- color: rgba(255, 255, 255, 0.8);
+ background-color: transparent;
+ border: var(--border-thickness) solid var(--color-border);
+ border-radius: 0;
+ color: var(--color-text-primary);
cursor: pointer;
- transition: background-color 0.15s ease, border-color 0.15s ease;
+ transition: background-color 0.15s ease, box-shadow 0.15s ease;
}
.breadcrumbs-back:hover {
- background: rgba(255, 255, 255, 0.1);
- border-color: rgba(255, 255, 255, 0.3);
+ background-color: var(--color-green-darker);
+ box-shadow: var(--glow-green);
}
.breadcrumbs-back:focus {
- outline: 2px solid rgba(102, 126, 234, 0.6);
+ outline: 2px solid var(--color-green-primary);
outline-offset: 2px;
}
.breadcrumbs-back:active {
- background: rgba(255, 255, 255, 0.15);
+ background-color: var(--color-green-darker);
}
.breadcrumbs-back-icon {
- font-size: 1rem;
+ font-size: var(--font-size-sm);
line-height: 1;
}
+/* ==========================================================================
+ Path Segments
+ ========================================================================== */
+
+.breadcrumbs-separator {
+ color: var(--color-text-secondary);
+}
+
+.breadcrumbs-segment {
+ color: var(--color-text-secondary);
+ transition: color 0.15s ease;
+}
+
+.breadcrumbs-segment:hover {
+ color: var(--color-text-primary);
+ cursor: pointer;
+}
+
/* ==========================================================================
Current Folder Name
========================================================================== */
.breadcrumbs-current {
- font-weight: 600;
- color: rgba(255, 255, 255, 0.95);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -75,11 +101,11 @@
@media (max-width: 768px) {
.breadcrumbs {
- padding: 0.5rem;
+ padding: var(--spacing-xs);
}
.breadcrumbs-current {
max-width: 200px;
- font-size: 0.9375rem;
+ font-size: var(--font-size-sm);
}
}
diff --git a/apps/web/src/styles/context-menu.css b/apps/web/src/styles/context-menu.css
index bf01fa005..053ead78a 100644
--- a/apps/web/src/styles/context-menu.css
+++ b/apps/web/src/styles/context-menu.css
@@ -1,49 +1,39 @@
-/* Context Menu Container */
+/**
+ * Context Menu Styles - Terminal Aesthetic
+ *
+ * Right-click menu for file/folder actions
+ * Green border, black background, monospace font
+ */
+
+/* ==========================================================================
+ Context Menu Container
+ ========================================================================== */
+
.context-menu {
position: absolute;
z-index: 1100;
- min-width: 160px;
- padding: 4px 0;
+ min-width: 180px;
+ padding: var(--spacing-xs) 0;
overflow: hidden;
- background-color: var(--context-menu-bg, #ffffff);
- border: 1px solid var(--context-menu-border, #e5e7eb);
- border-radius: 6px;
- box-shadow:
- 0 4px 6px -1px rgba(0, 0, 0, 0.1),
- 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ background-color: var(--color-background);
+ border: var(--border-thickness) solid var(--color-border);
+ border-radius: 0;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5), var(--glow-green);
}
-/* Dark mode support */
-@media (prefers-color-scheme: dark) {
- .context-menu {
- --context-menu-bg: #1f2937;
- --context-menu-border: #374151;
- --context-menu-text: #f9fafb;
- --context-menu-hover: #374151;
- --context-menu-destructive: #ef4444;
- --context-menu-destructive-hover: rgba(239, 68, 68, 0.1);
- }
-}
-
-/* Light mode defaults */
-:root {
- --context-menu-bg: #ffffff;
- --context-menu-border: #e5e7eb;
- --context-menu-text: #374151;
- --context-menu-hover: #f3f4f6;
- --context-menu-destructive: #dc2626;
- --context-menu-destructive-hover: rgba(220, 38, 38, 0.1);
-}
+/* ==========================================================================
+ Menu Item
+ ========================================================================== */
-/* Menu Item */
.context-menu-item {
display: flex;
align-items: center;
- gap: 8px;
+ gap: var(--spacing-xs);
width: 100%;
- padding: 8px 12px;
- font-size: 0.875rem;
- color: var(--context-menu-text);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-primary);
text-align: left;
cursor: pointer;
background: transparent;
@@ -53,37 +43,51 @@
.context-menu-item:hover,
.context-menu-item:focus {
- background-color: var(--context-menu-hover);
+ background-color: var(--color-green-darker);
outline: none;
}
.context-menu-item:focus-visible {
- outline: 2px solid #3b82f6;
+ outline: 2px solid var(--color-green-primary);
outline-offset: -2px;
}
-/* Menu Item Icon */
+/* ==========================================================================
+ Menu Item Icon
+ ========================================================================== */
+
.context-menu-item-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
- font-size: 0.875rem;
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
}
-/* Destructive Action (Delete) */
+/* ==========================================================================
+ Destructive Action (Delete)
+ ========================================================================== */
+
.context-menu-item--destructive {
- color: var(--context-menu-destructive);
+ color: var(--color-error);
+}
+
+.context-menu-item--destructive .context-menu-item-icon {
+ color: var(--color-error);
}
.context-menu-item--destructive:hover,
.context-menu-item--destructive:focus {
- background-color: var(--context-menu-destructive-hover);
+ background-color: var(--color-error-dim);
}
-/* Divider */
+/* ==========================================================================
+ Divider
+ ========================================================================== */
+
.context-menu-divider {
height: 1px;
- margin: 4px 0;
- background-color: var(--context-menu-border);
+ margin: var(--spacing-xs) 0;
+ background-color: var(--color-border-dim);
}
diff --git a/apps/web/src/styles/dialogs.css b/apps/web/src/styles/dialogs.css
index bc66e60b0..249fe4ed5 100644
--- a/apps/web/src/styles/dialogs.css
+++ b/apps/web/src/styles/dialogs.css
@@ -1,166 +1,185 @@
-/* Dialog Content */
+/**
+ * Dialog Styles - Terminal Aesthetic
+ *
+ * Dialog content for rename, delete confirmation, etc.
+ * Green accents, monospace font, dark only
+ */
+
+/* ==========================================================================
+ Dialog Content
+ ========================================================================== */
+
.dialog-content {
display: flex;
flex-direction: column;
- gap: 1rem;
+ gap: var(--spacing-md);
}
-/* Dialog Message */
+/* ==========================================================================
+ Dialog Message
+ ========================================================================== */
+
.dialog-message {
margin: 0;
- font-size: 0.9375rem;
- line-height: 1.5;
- color: var(--dialog-text, #4b5563);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ line-height: 1.6;
+ color: var(--color-text-primary);
}
-/* Dialog Field */
+/* ==========================================================================
+ Dialog Field
+ ========================================================================== */
+
.dialog-field {
display: flex;
flex-direction: column;
- gap: 0.375rem;
+ gap: var(--spacing-xs);
}
-/* Dialog Label */
+/* ==========================================================================
+ Dialog Label
+ ========================================================================== */
+
.dialog-label {
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--dialog-text, #374151);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-semibold);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--color-text-secondary);
}
-/* Dialog Input */
+/* ==========================================================================
+ Dialog Input
+ ========================================================================== */
+
.dialog-input {
width: 100%;
- padding: 0.625rem 0.75rem;
- font-size: 0.9375rem;
+ padding: var(--spacing-xs) var(--spacing-sm);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
line-height: 1.5;
- color: var(--dialog-text, #111827);
- background-color: var(--dialog-input-bg, #ffffff);
- border: 1px solid var(--dialog-input-border, #d1d5db);
- border-radius: 6px;
+ color: var(--color-text-primary);
+ background-color: var(--color-background);
+ border: var(--border-thickness) solid var(--color-border);
+ border-radius: 0;
outline: none;
- transition:
- border-color 0.15s ease,
- box-shadow 0.15s ease;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.dialog-input:focus {
- border-color: var(--dialog-input-focus, #3b82f6);
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
+ border-color: var(--color-green-primary);
+ box-shadow: var(--glow-green);
}
.dialog-input--error {
- border-color: var(--dialog-error-color, #dc2626);
+ border-color: var(--color-error);
}
.dialog-input--error:focus {
- border-color: var(--dialog-error-color, #dc2626);
- box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
+ border-color: var(--color-error);
+ box-shadow: 0 0 10px rgba(239, 68, 68, 0.4);
}
.dialog-input:disabled {
- opacity: 0.6;
+ opacity: 0.5;
cursor: not-allowed;
}
-/* Dialog Error */
+.dialog-input::placeholder {
+ color: var(--color-text-secondary);
+}
+
+/* ==========================================================================
+ Dialog Error
+ ========================================================================== */
+
.dialog-error {
- font-size: 0.8125rem;
- color: var(--dialog-error-color, #dc2626);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ color: var(--color-error);
}
-/* Dialog Actions */
+/* ==========================================================================
+ Dialog Actions
+ ========================================================================== */
+
.dialog-actions {
display: flex;
- gap: 0.75rem;
+ gap: var(--spacing-sm);
justify-content: flex-end;
- margin-top: 0.5rem;
+ margin-top: var(--spacing-xs);
}
-/* Dialog Button Base */
+/* ==========================================================================
+ Dialog Button Base
+ ========================================================================== */
+
.dialog-button {
display: inline-flex;
align-items: center;
justify-content: center;
- padding: 0.5rem 1rem;
- font-size: 0.875rem;
- font-weight: 500;
+ padding: var(--spacing-xs) var(--spacing-md);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-normal);
line-height: 1.5;
border: none;
- border-radius: 6px;
+ border-radius: 0;
cursor: pointer;
- transition:
- background-color 0.15s ease,
- opacity 0.15s ease;
+ transition: background-color 0.15s ease, box-shadow 0.15s ease;
}
.dialog-button:disabled {
- opacity: 0.6;
+ opacity: 0.5;
cursor: not-allowed;
}
.dialog-button:focus-visible {
- outline: 2px solid #3b82f6;
+ outline: 2px solid var(--color-green-primary);
outline-offset: 2px;
}
-/* Secondary Button */
+/* ==========================================================================
+ Secondary Button (Cancel)
+ ========================================================================== */
+
.dialog-button--secondary {
- color: var(--dialog-text, #374151);
- background-color: var(--dialog-button-secondary-bg, #f3f4f6);
+ color: var(--color-text-primary);
+ background-color: transparent;
+ border: var(--border-thickness) solid var(--color-border);
}
.dialog-button--secondary:hover:not(:disabled) {
- background-color: var(--dialog-button-secondary-hover, #e5e7eb);
+ background-color: var(--color-green-darker);
+ box-shadow: var(--glow-green);
}
-/* Primary Button */
+/* ==========================================================================
+ Primary Button (Confirm)
+ ========================================================================== */
+
.dialog-button--primary {
- color: #ffffff;
- background-color: var(--dialog-button-primary-bg, #3b82f6);
+ color: var(--color-black);
+ background-color: var(--color-green-primary);
+ font-weight: var(--font-weight-semibold);
}
.dialog-button--primary:hover:not(:disabled) {
- background-color: var(--dialog-button-primary-hover, #2563eb);
+ box-shadow: var(--glow-green);
}
-/* Destructive Button */
+/* ==========================================================================
+ Destructive Button (Delete)
+ ========================================================================== */
+
.dialog-button--destructive {
- color: #ffffff;
- background-color: var(--dialog-button-destructive-bg, #dc2626);
+ color: var(--color-black);
+ background-color: var(--color-error);
+ font-weight: var(--font-weight-semibold);
}
.dialog-button--destructive:hover:not(:disabled) {
- background-color: var(--dialog-button-destructive-hover, #b91c1c);
-}
-
-/* Dark mode support */
-@media (prefers-color-scheme: dark) {
- :root {
- --dialog-text: #f9fafb;
- --dialog-input-bg: #374151;
- --dialog-input-border: #4b5563;
- --dialog-input-focus: #60a5fa;
- --dialog-error-color: #f87171;
- --dialog-button-secondary-bg: #374151;
- --dialog-button-secondary-hover: #4b5563;
- --dialog-button-primary-bg: #3b82f6;
- --dialog-button-primary-hover: #2563eb;
- --dialog-button-destructive-bg: #dc2626;
- --dialog-button-destructive-hover: #b91c1c;
- }
-}
-
-/* Light mode defaults */
-:root {
- --dialog-text: #374151;
- --dialog-input-bg: #ffffff;
- --dialog-input-border: #d1d5db;
- --dialog-input-focus: #3b82f6;
- --dialog-error-color: #dc2626;
- --dialog-button-secondary-bg: #f3f4f6;
- --dialog-button-secondary-hover: #e5e7eb;
- --dialog-button-primary-bg: #3b82f6;
- --dialog-button-primary-hover: #2563eb;
- --dialog-button-destructive-bg: #dc2626;
- --dialog-button-destructive-hover: #b91c1c;
+ box-shadow: 0 0 10px rgba(239, 68, 68, 0.6);
}
diff --git a/apps/web/src/styles/file-browser.css b/apps/web/src/styles/file-browser.css
index 2448a80f1..3adf1bbaa 100644
--- a/apps/web/src/styles/file-browser.css
+++ b/apps/web/src/styles/file-browser.css
@@ -1,5 +1,5 @@
/**
- * File Browser Component Styles
+ * File Browser Component Styles - Terminal Aesthetic
*
* Styling for the file browser UI including:
* - Main layout (sidebar + content)
@@ -15,32 +15,73 @@
.file-browser {
display: flex;
flex: 1;
- min-height: 0; /* Allow shrinking in flex container */
+ min-height: 0;
+ background-color: var(--color-background);
}
.file-browser-sidebar {
width: 250px;
min-width: 200px;
max-width: 350px;
- border-right: 1px solid rgba(255, 255, 255, 0.1);
+ border-right: var(--border-thickness) solid var(--color-border);
overflow-y: auto;
- background: rgba(0, 0, 0, 0.2);
+ background-color: var(--color-background);
}
.file-browser-main {
flex: 1;
- min-width: 0; /* Allow shrinking in flex container */
+ min-width: 0;
overflow-y: auto;
- padding: 1rem;
+ padding: var(--spacing-md);
+ background-color: var(--color-background);
}
.file-browser-toolbar {
display: flex;
align-items: center;
- gap: 1rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- margin-bottom: 1rem;
+ gap: var(--spacing-md);
+ padding-bottom: var(--spacing-md);
+ border-bottom: var(--border-thickness) solid var(--color-border-dim);
+ margin-bottom: var(--spacing-md);
+}
+
+/* Toolbar buttons - Terminal flag style */
+.file-browser-toolbar button,
+.toolbar-button {
+ padding: var(--spacing-xs) var(--spacing-md);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-normal);
+ border-radius: 0;
+ cursor: pointer;
+ transition: box-shadow 0.15s ease;
+}
+
+/* Primary action button (--upload) */
+.toolbar-button--primary,
+.file-browser-toolbar .btn-primary {
+ background-color: var(--color-green-primary);
+ color: var(--color-black);
+ border: none;
+ font-weight: var(--font-weight-semibold);
+}
+
+.toolbar-button--primary:hover,
+.file-browser-toolbar .btn-primary:hover {
+ box-shadow: var(--glow-green);
+}
+
+/* Secondary action buttons (--new-dir, --refresh) */
+.toolbar-button--secondary,
+.file-browser-toolbar .btn-secondary {
+ background-color: transparent;
+ color: var(--color-text-primary);
+ border: var(--border-thickness) solid var(--color-border);
+}
+
+.toolbar-button--secondary:hover,
+.file-browser-toolbar .btn-secondary:hover {
+ box-shadow: var(--glow-green);
}
.file-browser-loading {
@@ -48,11 +89,12 @@
align-items: center;
justify-content: center;
height: 200px;
- color: rgba(255, 255, 255, 0.5);
+ color: var(--color-text-secondary);
+ font-family: var(--font-family-mono);
}
.file-browser-loading-spinner {
- font-size: 0.875rem;
+ font-size: var(--font-size-sm);
}
.file-browser-actions {
@@ -102,30 +144,31 @@
}
.folder-tree-header {
- padding: 1rem;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ padding: var(--spacing-md);
+ border-bottom: var(--border-thickness) solid var(--color-border-dim);
}
.folder-tree-title {
margin: 0;
- font-size: 0.875rem;
- font-weight: 600;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-semibold);
text-transform: uppercase;
- letter-spacing: 0.05em;
- color: rgba(255, 255, 255, 0.6);
+ letter-spacing: 0.1em;
+ color: var(--color-text-secondary);
}
.folder-tree-content {
flex: 1;
overflow-y: auto;
- padding: 0.5rem 0;
+ padding: var(--spacing-xs) 0;
}
.folder-tree-placeholder {
- padding: 1rem;
- color: rgba(255, 255, 255, 0.4);
- font-style: italic;
- font-size: 0.875rem;
+ padding: var(--spacing-md);
+ color: var(--color-text-secondary);
+ font-style: normal;
+ font-size: var(--font-size-sm);
}
/* Folder Tree Node */
@@ -136,37 +179,40 @@
.folder-tree-item {
display: flex;
align-items: center;
- gap: 0.25rem;
- padding: 0.5rem 0.75rem;
+ gap: var(--spacing-xs);
+ padding: var(--spacing-xs) var(--spacing-sm);
cursor: pointer;
transition: background-color 0.15s ease;
- border-radius: 4px;
- margin: 0 0.5rem;
+ border-radius: 0;
+ margin: 0 var(--spacing-xs);
+ border-left: 2px solid transparent;
}
.folder-tree-item:hover {
- background: rgba(255, 255, 255, 0.1);
+ background-color: var(--color-green-darker);
+ border-left-color: var(--color-green-dim);
}
.folder-tree-item--active {
- background: rgba(102, 126, 234, 0.3);
+ background-color: var(--color-green-darker);
+ border-left-color: var(--color-green-primary);
}
.folder-tree-item--active:hover {
- background: rgba(102, 126, 234, 0.4);
+ background-color: var(--color-green-darker);
}
.folder-tree-item--drag-over {
- background: rgba(102, 126, 234, 0.5);
- outline: 2px dashed rgba(102, 126, 234, 0.8);
- outline-offset: -2px;
+ background-color: var(--color-green-darker);
+ border-left-color: var(--color-green-primary);
+ box-shadow: inset 0 0 0 1px var(--color-green-dim);
}
.folder-tree-toggle {
width: 16px;
flex-shrink: 0;
- font-size: 0.625rem;
- color: rgba(255, 255, 255, 0.5);
+ font-size: 10px;
+ color: var(--color-text-secondary);
cursor: pointer;
}
@@ -175,8 +221,9 @@
}
.folder-tree-icon {
- font-size: 1rem;
+ font-size: var(--font-size-sm);
flex-shrink: 0;
+ color: var(--color-text-primary);
}
.folder-tree-name {
@@ -184,19 +231,17 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- font-size: 0.875rem;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-primary);
}
.folder-tree-loading {
- color: rgba(255, 255, 255, 0.4);
- font-size: 0.75rem;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
margin-left: auto;
}
-.folder-tree-children {
- /* Child indentation handled inline via paddingLeft */
-}
-
/* ==========================================================================
File List
========================================================================== */
@@ -204,84 +249,110 @@
.file-list {
display: flex;
flex-direction: column;
+ border: var(--border-thickness) solid var(--color-border);
}
.file-list-header {
display: grid;
- grid-template-columns: 1fr 100px 150px;
- gap: 1rem;
- padding: 0.75rem 1rem;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- font-size: 0.75rem;
- font-weight: 600;
+ grid-template-columns: 1fr 120px 120px 180px;
+ gap: var(--spacing-md);
+ padding: var(--spacing-sm) var(--spacing-md);
+ border-bottom: var(--border-thickness) solid var(--color-border);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-semibold);
text-transform: uppercase;
- letter-spacing: 0.05em;
- color: rgba(255, 255, 255, 0.5);
-}
-
-.file-list-header-name,
-.file-list-header-size,
-.file-list-header-date {
- /* Column styles */
+ letter-spacing: 0.1em;
+ color: var(--color-text-primary);
+ background-color: var(--color-background);
}
.file-list-body {
- /* Container for items */
+ /* Container for file items */
}
-/* File List Item */
+/* File List Item - Desktop uses grid, Mobile uses 2-row stacked layout */
.file-list-item {
display: grid;
- grid-template-columns: 1fr 100px 150px;
- gap: 1rem;
- padding: 0.75rem 1rem;
+ grid-template-columns: 1fr 120px 120px 180px;
+ grid-template-areas: "name size type date";
+ gap: var(--spacing-md);
+ padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
transition: background-color 0.15s ease;
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+ border-bottom: var(--border-thickness) solid var(--color-border-dim);
+ font-family: var(--font-family-mono);
+}
+
+.file-list-item:last-child {
+ border-bottom: none;
}
.file-list-item:hover {
- background: rgba(255, 255, 255, 0.05);
+ background-color: var(--color-green-darker);
}
.file-list-item--selected {
- background: rgba(102, 126, 234, 0.2);
+ background-color: var(--color-green-darker);
+ border-left: 2px solid var(--color-green-primary);
}
.file-list-item--selected:hover {
- background: rgba(102, 126, 234, 0.3);
+ background-color: var(--color-green-darker);
}
-.file-list-item-name {
+/* Row wrappers - on desktop, these act as containers within grid */
+.file-list-item-row-top {
+ grid-area: name;
display: flex;
align-items: center;
- gap: 0.5rem;
- min-width: 0; /* Allow text truncation */
+ gap: 6px;
+ min-width: 0;
+}
+
+.file-list-item-row-bottom {
+ display: contents; /* Children participate directly in parent grid on desktop */
}
.file-list-item-icon {
- font-size: 1.25rem;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-semibold);
flex-shrink: 0;
+ color: var(--color-text-primary);
}
-.file-list-item-text {
+.file-list-item-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
}
.file-list-item-size {
+ grid-area: size;
display: flex;
align-items: center;
- color: rgba(255, 255, 255, 0.6);
- font-size: 0.875rem;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+}
+
+.file-list-item-type {
+ grid-area: type;
+ display: flex;
+ align-items: center;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+ text-transform: lowercase;
}
.file-list-item-date {
+ grid-area: date;
display: flex;
align-items: center;
- color: rgba(255, 255, 255, 0.6);
- font-size: 0.875rem;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
}
/* ==========================================================================
@@ -293,43 +364,46 @@
align-items: center;
justify-content: center;
min-height: 300px;
- border: 2px dashed rgba(255, 255, 255, 0.2);
- border-radius: 12px;
- margin: 2rem;
+ border: var(--border-thickness) dashed var(--color-border);
+ border-radius: 0;
+ margin: var(--spacing-lg);
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.empty-state:hover {
- border-color: rgba(102, 126, 234, 0.5);
- background: rgba(102, 126, 234, 0.05);
+ border-color: var(--color-green-primary);
+ background-color: var(--color-green-darker);
}
.empty-state-content {
text-align: center;
- padding: 2rem;
+ padding: var(--spacing-lg);
}
.empty-state-icon {
- font-size: 3rem;
+ font-size: 2rem;
display: block;
- margin-bottom: 1rem;
+ margin-bottom: var(--spacing-md);
+ color: var(--color-text-secondary);
}
.empty-state-text {
- font-size: 1.125rem;
- margin: 0 0 0.5rem 0;
- color: rgba(255, 255, 255, 0.8);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ margin: 0 0 var(--spacing-xs) 0;
+ color: var(--color-text-primary);
}
.empty-state-hint {
- font-size: 0.875rem;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
margin: 0;
- color: rgba(255, 255, 255, 0.4);
+ color: var(--color-text-secondary);
}
.empty-state-upload {
- margin-top: 1.5rem;
+ margin-top: var(--spacing-md);
}
/* Note: Mobile responsive styles are in responsive.css */
diff --git a/apps/web/src/styles/modal.css b/apps/web/src/styles/modal.css
index 416a5ffcd..462c9bcdb 100644
--- a/apps/web/src/styles/modal.css
+++ b/apps/web/src/styles/modal.css
@@ -1,4 +1,14 @@
-/* Modal Backdrop */
+/**
+ * Modal Styles - Terminal Aesthetic
+ *
+ * Modal overlay and container for dialogs
+ * Green border, black background, terminal feel
+ */
+
+/* ==========================================================================
+ Modal Backdrop
+ ========================================================================== */
+
.modal-backdrop {
position: fixed;
inset: 0;
@@ -6,49 +16,53 @@
display: flex;
align-items: center;
justify-content: center;
- background-color: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(2px);
+ background-color: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(4px);
}
-/* Modal Container */
+/* ==========================================================================
+ Modal Container
+ ========================================================================== */
+
.modal-container {
position: relative;
width: 100%;
max-width: 500px;
max-height: 90vh;
- margin: 1rem;
+ margin: var(--spacing-md);
overflow: hidden;
- background-color: var(--modal-bg, #ffffff);
- border-radius: 8px;
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+ background-color: var(--color-background);
+ border: var(--border-thickness) solid var(--color-border);
+ border-radius: 0;
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), var(--glow-green);
}
-/* Dark mode support */
-@media (prefers-color-scheme: dark) {
- .modal-container {
- --modal-bg: #1f2937;
- --modal-text: #f9fafb;
- --modal-border: #374151;
- }
-}
+/* ==========================================================================
+ Modal Header
+ ========================================================================== */
-/* Modal Header */
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
- padding: 1rem 1.5rem;
- border-bottom: 1px solid var(--modal-border, #e5e7eb);
+ padding: var(--spacing-md);
+ border-bottom: var(--border-thickness) solid var(--color-border-dim);
}
.modal-title {
margin: 0;
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--modal-text, #111827);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-semibold);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--color-text-primary);
}
-/* Modal Close Button */
+/* ==========================================================================
+ Modal Close Button
+ ========================================================================== */
+
.modal-close {
display: flex;
align-items: center;
@@ -57,30 +71,34 @@
height: 2rem;
padding: 0;
margin-left: auto;
- font-size: 1.5rem;
- font-weight: 400;
+ font-size: 1.25rem;
+ font-weight: var(--font-weight-normal);
line-height: 1;
- color: var(--modal-text, #6b7280);
+ color: var(--color-text-secondary);
cursor: pointer;
background: transparent;
- border: none;
- border-radius: 4px;
- transition: background-color 0.15s ease;
+ border: var(--border-thickness) solid var(--color-border);
+ border-radius: 0;
+ transition: background-color 0.15s ease, color 0.15s ease;
}
.modal-close:hover {
- background-color: rgba(0, 0, 0, 0.1);
+ background-color: var(--color-green-darker);
+ color: var(--color-text-primary);
}
.modal-close:focus {
- outline: 2px solid #3b82f6;
+ outline: 2px solid var(--color-green-primary);
outline-offset: 2px;
}
-/* Modal Body */
+/* ==========================================================================
+ Modal Body
+ ========================================================================== */
+
.modal-body {
- padding: 1.5rem;
+ padding: var(--spacing-md);
overflow-y: auto;
max-height: calc(90vh - 80px);
- color: var(--modal-text, #374151);
+ color: var(--color-text-primary);
}
diff --git a/apps/web/src/styles/responsive.css b/apps/web/src/styles/responsive.css
index a46f9225d..bd9511ed2 100644
--- a/apps/web/src/styles/responsive.css
+++ b/apps/web/src/styles/responsive.css
@@ -1,5 +1,5 @@
/**
- * Responsive Styles
+ * Responsive Styles - Terminal Aesthetic
*
* Mobile-first responsive design for the file browser.
* Breakpoint: 768px (tablet/desktop boundary)
@@ -23,22 +23,22 @@
height: 2.5rem;
padding: 0;
background: transparent;
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 6px;
- color: rgba(255, 255, 255, 0.9);
+ border: var(--border-thickness) solid var(--color-border);
+ border-radius: 0;
+ color: var(--color-text-primary);
font-size: 1.25rem;
cursor: pointer;
- transition: background-color 0.15s ease, border-color 0.15s ease;
+ transition: background-color 0.15s ease, box-shadow 0.15s ease;
flex-shrink: 0;
}
.file-browser-toggle:hover {
- background: rgba(255, 255, 255, 0.1);
- border-color: rgba(255, 255, 255, 0.3);
+ background-color: var(--color-green-darker);
+ box-shadow: var(--glow-green);
}
.file-browser-toggle:focus {
- outline: 2px solid rgba(102, 126, 234, 0.6);
+ outline: 2px solid var(--color-green-primary);
outline-offset: 2px;
}
@@ -53,7 +53,7 @@
left: 0;
right: 0;
bottom: 0;
- background: rgba(0, 0, 0, 0.5);
+ background: rgba(0, 0, 0, 0.8);
z-index: 99;
animation: fadeIn 0.2s ease;
}
@@ -70,8 +70,8 @@
/* Sidebar close button (mobile) */
.file-browser-sidebar-close {
position: absolute;
- top: 0.75rem;
- right: 0.75rem;
+ top: var(--spacing-sm);
+ right: var(--spacing-sm);
display: flex;
align-items: center;
justify-content: center;
@@ -79,16 +79,18 @@
height: 2rem;
padding: 0;
background: transparent;
- border: none;
- color: rgba(255, 255, 255, 0.6);
+ border: var(--border-thickness) solid var(--color-border);
+ border-radius: 0;
+ color: var(--color-text-secondary);
font-size: 1.25rem;
cursor: pointer;
- transition: color 0.15s ease;
+ transition: color 0.15s ease, background-color 0.15s ease;
z-index: 1;
}
.file-browser-sidebar-close:hover {
- color: rgba(255, 255, 255, 0.9);
+ color: var(--color-text-primary);
+ background-color: var(--color-green-darker);
}
/* ==========================================================================
@@ -107,8 +109,9 @@
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 100;
- background: #1a1a2e;
- box-shadow: 4px 0 20px rgba(0, 0, 0, 0.3);
+ background-color: var(--color-background);
+ border-right: var(--border-thickness) solid var(--color-border);
+ box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
}
.file-browser-sidebar--open {
@@ -126,53 +129,99 @@
/* Main content: full width */
.file-browser-main {
- padding: 0.75rem;
+ padding: var(--spacing-sm);
}
/* Toolbar: add gap for hamburger */
.file-browser-toolbar {
- gap: 0.5rem;
+ gap: var(--spacing-xs);
flex-wrap: wrap;
}
- /* File list: hide date column, reduce size column */
- .file-list-header,
+ /* File list: hide header on mobile (2-row item layout) */
+ .file-list-header {
+ display: none;
+ }
+
+ /* File list items: 2-row stacked layout per Pencil design */
.file-list-item {
- grid-template-columns: 1fr 80px;
- gap: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 10px 12px;
+ min-height: auto;
}
- .file-list-header-date,
- .file-list-item-date {
- display: none;
+ /* Row 1: Icon + Name (left aligned) */
+ .file-list-item-row-top {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 100%;
}
- /* File list items: touch-friendly sizing */
- .file-list-item {
- padding: 0.875rem 1rem;
- min-height: 3rem;
+ .file-list-item-icon {
+ font-size: 10px;
+ font-weight: 600;
+ }
+
+ .file-list-item-name {
+ font-size: 11px;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ /* Row 2: Modified (left) | Size (right) - space-between */
+ .file-list-item-row-bottom {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ }
+
+ .file-list-item-date,
+ .file-list-item-size {
+ font-size: 9px;
+ color: var(--color-text-secondary);
+ }
+
+ /* Hide TYPE column on mobile (shown in row layout via icon) */
+ .file-list-item-type {
+ display: none;
}
/* Empty state: smaller margins */
.empty-state {
- margin: 1rem;
+ margin: var(--spacing-md);
min-height: 200px;
}
/* Context menu: touch-friendly */
.context-menu {
- min-width: 180px;
+ min-width: 200px;
}
.context-menu-item {
- padding: 0.875rem 1rem;
- font-size: 1rem;
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-size: var(--font-size-sm);
}
/* Dialogs: full width on small screens */
- .modal-content {
+ .modal-container {
max-width: calc(100vw - 2rem);
- margin: 1rem;
+ margin: var(--spacing-md);
+ }
+
+ /* Login: adjust spacing */
+ .login-container {
+ padding: var(--spacing-md);
+ }
+
+ .login-container h1 {
+ font-size: 1.5rem;
}
}
@@ -214,10 +263,10 @@
/* Visual feedback for touch */
@media (pointer: coarse) {
.file-list-item:active {
- background: rgba(255, 255, 255, 0.1);
+ background-color: var(--color-green-darker);
}
.folder-tree-item:active {
- background: rgba(255, 255, 255, 0.15);
+ background-color: var(--color-green-darker);
}
}
diff --git a/apps/web/src/styles/upload.css b/apps/web/src/styles/upload.css
index ed0019dcd..87d17c415 100644
--- a/apps/web/src/styles/upload.css
+++ b/apps/web/src/styles/upload.css
@@ -1,4 +1,13 @@
-/* Upload Zone Styles */
+/**
+ * Upload Styles - Terminal Aesthetic
+ *
+ * Upload zone (drag-drop) and upload modal progress
+ * Green accents, terminal feel
+ */
+
+/* ==========================================================================
+ Upload Zone
+ ========================================================================== */
.upload-zone-wrapper {
width: 100%;
@@ -9,31 +18,30 @@
align-items: center;
justify-content: center;
min-height: 120px;
- padding: 2rem;
- border: 2px dashed var(--upload-border, #d1d5db);
- border-radius: 8px;
- background-color: var(--upload-bg, #f9fafb);
+ padding: var(--spacing-lg);
+ border: var(--border-thickness) dashed var(--color-border);
+ border-radius: 0;
+ background-color: var(--color-background);
cursor: pointer;
- transition:
- border-color 0.15s ease,
- background-color 0.15s ease;
+ transition: border-color 0.15s ease, background-color 0.15s ease;
}
.upload-zone:hover {
- border-color: var(--upload-border-hover, #9ca3af);
- background-color: var(--upload-bg-hover, #f3f4f6);
+ border-color: var(--color-green-primary);
+ background-color: var(--color-green-darker);
}
.upload-zone:focus-within {
- outline: 2px solid #3b82f6;
+ outline: 2px solid var(--color-green-primary);
outline-offset: 2px;
}
/* Active drop state */
.upload-zone-active {
- border-color: var(--upload-border-active, #3b82f6);
+ border-color: var(--color-green-primary);
border-style: solid;
- background-color: var(--upload-bg-active, #eff6ff);
+ background-color: var(--color-green-darker);
+ box-shadow: var(--glow-green);
}
/* Uploading state */
@@ -47,20 +55,22 @@
display: flex;
flex-direction: column;
align-items: center;
- gap: 0.5rem;
+ gap: var(--spacing-xs);
text-align: center;
- color: var(--upload-text, #6b7280);
+ color: var(--color-text-secondary);
}
.upload-zone-icon {
- font-size: 2.5rem;
+ font-size: 2rem;
line-height: 1;
- opacity: 0.6;
+ color: var(--color-text-secondary);
}
.upload-zone-text {
margin: 0;
- font-size: 0.875rem;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-primary);
}
/* Error message */
@@ -68,20 +78,21 @@
display: flex;
align-items: center;
justify-content: space-between;
- gap: 0.5rem;
- margin-top: 0.5rem;
- padding: 0.75rem 1rem;
- font-size: 0.875rem;
- color: var(--upload-error-text, #991b1b);
- background-color: var(--upload-error-bg, #fef2f2);
- border: 1px solid var(--upload-error-border, #fecaca);
- border-radius: 4px;
+ gap: var(--spacing-xs);
+ margin-top: var(--spacing-xs);
+ padding: var(--spacing-sm);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ color: var(--color-error);
+ background-color: var(--color-error-dim);
+ border: var(--border-thickness) solid var(--color-error);
+ border-radius: 0;
}
.upload-zone-error-dismiss {
padding: 0;
- font-size: 1.25rem;
- font-weight: 400;
+ font-size: 1rem;
+ font-weight: var(--font-weight-normal);
line-height: 1;
color: inherit;
cursor: pointer;
@@ -94,51 +105,33 @@
opacity: 1;
}
-/* Dark mode support */
-@media (prefers-color-scheme: dark) {
- .upload-zone {
- --upload-border: #4b5563;
- --upload-bg: #1f2937;
- --upload-border-hover: #6b7280;
- --upload-bg-hover: #374151;
- --upload-border-active: #60a5fa;
- --upload-bg-active: #1e3a5f;
- --upload-text: #9ca3af;
- }
-
- .upload-zone-error {
- --upload-error-text: #fca5a5;
- --upload-error-bg: #450a0a;
- --upload-error-border: #7f1d1d;
- }
-}
-
-/* ============================================
- Upload Modal Styles
- ============================================ */
+/* ==========================================================================
+ Upload Modal
+ ========================================================================== */
.upload-modal-content {
min-width: 400px;
}
.upload-modal-progress {
- margin-bottom: 1rem;
+ margin-bottom: var(--spacing-md);
}
.upload-modal-overall {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 0.5rem;
- font-size: 0.875rem;
- color: var(--modal-text, #6b7280);
+ margin-bottom: var(--spacing-xs);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
}
/* Upload item list */
.upload-item-list {
display: flex;
flex-direction: column;
- gap: 0.75rem;
+ gap: var(--spacing-sm);
max-height: 300px;
overflow-y: auto;
}
@@ -147,31 +140,32 @@
.upload-item {
display: flex;
flex-direction: column;
- gap: 0.5rem;
- padding: 0.75rem;
- background-color: var(--upload-item-bg, #f9fafb);
- border-radius: 4px;
+ gap: var(--spacing-xs);
+ padding: var(--spacing-sm);
+ background-color: var(--color-green-darker);
+ border: var(--border-thickness) solid var(--color-border-dim);
+ border-radius: 0;
}
.upload-item-header {
display: flex;
align-items: center;
justify-content: space-between;
- gap: 0.5rem;
+ gap: var(--spacing-xs);
}
.upload-item-info {
display: flex;
align-items: center;
- gap: 0.5rem;
+ gap: var(--spacing-xs);
flex: 1;
- min-width: 0; /* Allow text truncation */
+ min-width: 0;
}
.upload-item-icon {
flex-shrink: 0;
- font-size: 1rem;
- opacity: 0.6;
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
}
.upload-item-name {
@@ -179,132 +173,119 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- font-size: 0.875rem;
- color: var(--modal-text, #374151);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-primary);
}
.upload-item-actions {
display: flex;
- gap: 0.25rem;
+ gap: var(--spacing-xs);
}
.upload-item-cancel,
.upload-item-retry {
- padding: 0.25rem 0.5rem;
- font-size: 0.75rem;
+ padding: var(--spacing-xs);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
cursor: pointer;
border: none;
- border-radius: 4px;
+ border-radius: 0;
transition: background-color 0.15s ease;
}
.upload-item-cancel {
- color: var(--cancel-text, #6b7280);
- background-color: var(--cancel-bg, #e5e7eb);
+ color: var(--color-text-primary);
+ background-color: transparent;
+ border: var(--border-thickness) solid var(--color-border);
}
.upload-item-cancel:hover {
- background-color: var(--cancel-bg-hover, #d1d5db);
+ background-color: var(--color-green-darker);
}
.upload-item-retry {
- color: var(--retry-text, #1d4ed8);
- background-color: var(--retry-bg, #dbeafe);
+ color: var(--color-black);
+ background-color: var(--color-green-primary);
}
.upload-item-retry:hover {
- background-color: var(--retry-bg-hover, #bfdbfe);
+ box-shadow: var(--glow-green);
}
/* Progress bar */
.upload-item-progress-bar {
height: 4px;
- background-color: var(--progress-bg, #e5e7eb);
- border-radius: 2px;
+ background-color: var(--color-border-dim);
+ border-radius: 0;
overflow: hidden;
}
.upload-item-progress-fill {
height: 100%;
- background-color: var(--progress-fill, #3b82f6);
+ background-color: var(--color-green-primary);
transition: width 0.2s ease;
}
.upload-item-progress-fill[data-status='complete'] {
- background-color: var(--progress-complete, #22c55e);
+ background-color: var(--color-green-primary);
}
.upload-item-progress-fill[data-status='error'] {
- background-color: var(--progress-error, #ef4444);
+ background-color: var(--color-error);
}
/* Status text */
.upload-item-status {
- font-size: 0.75rem;
- color: var(--status-text, #6b7280);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
}
.upload-item-status[data-status='complete'] {
- color: var(--status-complete, #16a34a);
+ color: var(--color-green-primary);
}
.upload-item-status[data-status='error'] {
- color: var(--status-error, #dc2626);
+ color: var(--color-error);
}
/* Modal actions */
.upload-modal-actions {
display: flex;
justify-content: flex-end;
- gap: 0.5rem;
- margin-top: 1rem;
- padding-top: 1rem;
- border-top: 1px solid var(--modal-border, #e5e7eb);
+ gap: var(--spacing-xs);
+ margin-top: var(--spacing-md);
+ padding-top: var(--spacing-md);
+ border-top: var(--border-thickness) solid var(--color-border-dim);
}
.upload-modal-btn {
- padding: 0.5rem 1rem;
- font-size: 0.875rem;
+ padding: var(--spacing-xs) var(--spacing-md);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
cursor: pointer;
border: none;
- border-radius: 4px;
- transition: background-color 0.15s ease;
+ border-radius: 0;
+ transition: background-color 0.15s ease, box-shadow 0.15s ease;
}
.upload-modal-btn-cancel {
- color: var(--modal-text, #374151);
- background-color: var(--btn-cancel-bg, #e5e7eb);
+ color: var(--color-text-primary);
+ background-color: transparent;
+ border: var(--border-thickness) solid var(--color-border);
}
.upload-modal-btn-cancel:hover {
- background-color: var(--btn-cancel-bg-hover, #d1d5db);
+ background-color: var(--color-green-darker);
}
.upload-modal-btn-close {
- color: white;
- background-color: var(--btn-close-bg, #3b82f6);
+ color: var(--color-black);
+ background-color: var(--color-green-primary);
+ font-weight: var(--font-weight-semibold);
}
.upload-modal-btn-close:hover {
- background-color: var(--btn-close-bg-hover, #2563eb);
-}
-
-/* Dark mode for upload modal */
-@media (prefers-color-scheme: dark) {
- .upload-item {
- --upload-item-bg: #374151;
- }
-
- .upload-item-progress-bar {
- --progress-bg: #4b5563;
- }
-
- .upload-modal-actions {
- --modal-border: #4b5563;
- }
-
- .upload-modal-btn-cancel {
- --btn-cancel-bg: #4b5563;
- --btn-cancel-bg-hover: #6b7280;
- }
+ box-shadow: var(--glow-green);
}
diff --git a/designs/cipher-box-design.pen b/designs/cipher-box-design.pen
index 19558cdf1..2606e2bff 100644
--- a/designs/cipher-box-design.pen
+++ b/designs/cipher-box-design.pen
@@ -1661,16 +1661,6 @@
"fontWeight": "normal"
}
]
- },
- {
- "type": "text",
- "id": "UuUso",
- "name": "mRow2Size",
- "fill": "#006644",
- "content": "4.2 GB",
- "fontFamily": "JetBrains Mono",
- "fontSize": 10,
- "fontWeight": "normal"
}
]
},
@@ -1680,23 +1670,24 @@
"name": "mRow2Bottom",
"width": "fill_container",
"gap": 12,
+ "justifyContent": "space_between",
"children": [
{
"type": "text",
- "id": "Oczq6",
- "name": "mRow2Type",
+ "id": "fISJ6",
+ "name": "mRow2Modified",
"fill": "#006644",
- "content": "dir",
+ "content": "2026-01-20 14:32",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
},
{
"type": "text",
- "id": "fISJ6",
- "name": "mRow2Modified",
+ "id": "bBDIP",
+ "name": "mRow2Size",
"fill": "#006644",
- "content": "2026-01-20 14:32",
+ "content": "4.2 GB",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
@@ -1760,16 +1751,6 @@
"fontWeight": "normal"
}
]
- },
- {
- "type": "text",
- "id": "quPft",
- "name": "mRow3Size",
- "fill": "#006644",
- "content": "1.8 GB",
- "fontFamily": "JetBrains Mono",
- "fontSize": 10,
- "fontWeight": "normal"
}
]
},
@@ -1779,23 +1760,24 @@
"name": "mRow3Bottom",
"width": "fill_container",
"gap": 12,
+ "justifyContent": "space_between",
"children": [
{
"type": "text",
- "id": "7wWjA",
- "name": "mRow3Type",
+ "id": "4OQUu",
+ "name": "mRow3Modified",
"fill": "#006644",
- "content": "dir",
+ "content": "2026-01-22 09:15",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
},
{
"type": "text",
- "id": "4OQUu",
- "name": "mRow3Modified",
+ "id": "s2zqk",
+ "name": "mRow3Size",
"fill": "#006644",
- "content": "2026-01-22 09:15",
+ "content": "1.8 GB",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
@@ -1859,16 +1841,6 @@
"fontWeight": "normal"
}
]
- },
- {
- "type": "text",
- "id": "Lhljr",
- "name": "mRow4Size",
- "fill": "#006644",
- "content": "7.5 GB",
- "fontFamily": "JetBrains Mono",
- "fontSize": 10,
- "fontWeight": "normal"
}
]
},
@@ -1878,23 +1850,24 @@
"name": "mRow4Bottom",
"width": "fill_container",
"gap": 12,
+ "justifyContent": "space_between",
"children": [
{
"type": "text",
- "id": "CLjWS",
- "name": "mRow4Type",
+ "id": "Yx41C",
+ "name": "mRow4Modified",
"fill": "#006644",
- "content": "dir",
+ "content": "2026-01-23 11:45",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
},
{
"type": "text",
- "id": "Yx41C",
- "name": "mRow4Modified",
+ "id": "rRWMs",
+ "name": "mRow4Size",
"fill": "#006644",
- "content": "2026-01-23 11:45",
+ "content": "7.5 GB",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
@@ -1958,16 +1931,6 @@
"fontWeight": "normal"
}
]
- },
- {
- "type": "text",
- "id": "85UJI",
- "name": "mRow5Size",
- "fill": "#006644",
- "content": "2.4 MB",
- "fontFamily": "JetBrains Mono",
- "fontSize": 10,
- "fontWeight": "normal"
}
]
},
@@ -1977,23 +1940,24 @@
"name": "mRow5Bottom",
"width": "fill_container",
"gap": 12,
+ "justifyContent": "space_between",
"children": [
{
"type": "text",
- "id": "2rycB",
- "name": "mRow5Type",
+ "id": "jcJiO",
+ "name": "mRow5Modified",
"fill": "#006644",
- "content": "pdf",
+ "content": "2026-01-21 16:20",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
},
{
"type": "text",
- "id": "jcJiO",
- "name": "mRow5Modified",
+ "id": "vJoZq",
+ "name": "mRow5Size",
"fill": "#006644",
- "content": "2026-01-21 16:20",
+ "content": "2.4 MB",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
@@ -2057,16 +2021,6 @@
"fontWeight": "normal"
}
]
- },
- {
- "type": "text",
- "id": "qkRp0",
- "name": "mRow6Size",
- "fill": "#006644",
- "content": "856 KB",
- "fontFamily": "JetBrains Mono",
- "fontSize": 10,
- "fontWeight": "normal"
}
]
},
@@ -2076,23 +2030,24 @@
"name": "mRow6Bottom",
"width": "fill_container",
"gap": 12,
+ "justifyContent": "space_between",
"children": [
{
"type": "text",
- "id": "q9JhN",
- "name": "mRow6Type",
+ "id": "P6ZHl",
+ "name": "mRow6Modified",
"fill": "#006644",
- "content": "docx",
+ "content": "2026-01-22 13:08",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
},
{
"type": "text",
- "id": "P6ZHl",
- "name": "mRow6Modified",
+ "id": "MUupx",
+ "name": "mRow6Size",
"fill": "#006644",
- "content": "2026-01-22 13:08",
+ "content": "856 KB",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
@@ -2156,16 +2111,6 @@
"fontWeight": "normal"
}
]
- },
- {
- "type": "text",
- "id": "XLqMB",
- "name": "mRow7Size",
- "fill": "#006644",
- "content": "14.2 MB",
- "fontFamily": "JetBrains Mono",
- "fontSize": 10,
- "fontWeight": "normal"
}
]
},
@@ -2175,23 +2120,24 @@
"name": "mRow7Bottom",
"width": "fill_container",
"gap": 12,
+ "justifyContent": "space_between",
"children": [
{
"type": "text",
- "id": "OPhfh",
- "name": "mRow7Type",
+ "id": "QnS6X",
+ "name": "mRow7Modified",
"fill": "#006644",
- "content": "csv",
+ "content": "2026-01-19 08:42",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
},
{
"type": "text",
- "id": "QnS6X",
- "name": "mRow7Modified",
+ "id": "gSbTn",
+ "name": "mRow7Size",
"fill": "#006644",
- "content": "2026-01-19 08:42",
+ "content": "14.2 MB",
"fontFamily": "JetBrains Mono",
"fontSize": 9,
"fontWeight": "normal"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index abb0d2b39..0334fccb7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -326,6 +326,25 @@ importers:
specifier: ^5.9.3
version: 5.9.3
+ tools/mock-ipns-routing:
+ dependencies:
+ fastify:
+ specifier: ^5.0.0
+ version: 5.7.1
+ pino-pretty:
+ specifier: ^13.1.3
+ version: 13.1.3
+ devDependencies:
+ '@types/node':
+ specifier: ^22.19.7
+ version: 22.19.7
+ tsx:
+ specifier: ^4.0.0
+ version: 4.21.0
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+
packages:
'@adraffy/ens-normalize@1.10.1':
resolution:
@@ -1417,6 +1436,42 @@ packages:
}
engines: { node: '>=18.0.0', npm: '>=9.0.0' }
+ '@fastify/ajv-compiler@4.0.5':
+ resolution:
+ {
+ integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==,
+ }
+
+ '@fastify/error@4.2.0':
+ resolution:
+ {
+ integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==,
+ }
+
+ '@fastify/fast-json-stringify-compiler@5.0.3':
+ resolution:
+ {
+ integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==,
+ }
+
+ '@fastify/forwarded@3.0.1':
+ resolution:
+ {
+ integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==,
+ }
+
+ '@fastify/merge-json-schemas@0.2.1':
+ resolution:
+ {
+ integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==,
+ }
+
+ '@fastify/proxy-addr@5.1.0':
+ resolution:
+ {
+ integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==,
+ }
+
'@floating-ui/core@1.7.3':
resolution:
{
@@ -2584,6 +2639,12 @@ packages:
}
engines: { node: '>=10' }
+ '@pinojs/redact@0.4.0':
+ resolution:
+ {
+ integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==,
+ }
+
'@pkgjs/parseargs@0.11.0':
resolution:
{
@@ -4295,6 +4356,12 @@ packages:
}
engines: { node: '>=6.5' }
+ abstract-logging@2.0.1:
+ resolution:
+ {
+ integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==,
+ }
+
accepts@2.0.0:
resolution:
{
@@ -4648,6 +4715,12 @@ packages:
}
engines: { node: '>= 0.4' }
+ avvio@9.1.0:
+ resolution:
+ {
+ integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==,
+ }
+
axios@1.13.2:
resolution:
{
@@ -5597,6 +5670,12 @@ packages:
}
engines: { node: '>=0.11' }
+ dateformat@4.6.3:
+ resolution:
+ {
+ integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==,
+ }
+
dayjs@1.11.19:
resolution:
{
@@ -5718,6 +5797,13 @@ packages:
}
engines: { node: '>= 0.6.0' }
+ dequal@2.0.3:
+ resolution:
+ {
+ integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==,
+ }
+ engines: { node: '>=6' }
+
des.js@1.1.0:
resolution:
{
@@ -6355,6 +6441,18 @@ packages:
}
engines: { node: '> 0.1.90' }
+ fast-copy@4.0.2:
+ resolution:
+ {
+ integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==,
+ }
+
+ fast-decode-uri-component@1.0.1:
+ resolution:
+ {
+ integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==,
+ }
+
fast-deep-equal@3.1.3:
resolution:
{
@@ -6386,6 +6484,12 @@ packages:
integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==,
}
+ fast-json-stringify@6.2.0:
+ resolution:
+ {
+ integrity: sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw==,
+ }
+
fast-levenshtein@2.0.6:
resolution:
{
@@ -6398,6 +6502,12 @@ packages:
integrity: sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==,
}
+ fast-querystring@1.1.2:
+ resolution:
+ {
+ integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==,
+ }
+
fast-safe-stringify@2.1.1:
resolution:
{
@@ -6416,6 +6526,12 @@ packages:
integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==,
}
+ fastify@5.7.1:
+ resolution:
+ {
+ integrity: sha512-ZW7S4fxlZhE+tYWVokFzjh+i56R+buYKNGhrVl6DtN8sxkyMEzpJnzvO8A/ZZrsg5w6X37u6I4EOQikYS5DXpA==,
+ }
+
fastq@1.20.1:
resolution:
{
@@ -6481,6 +6597,13 @@ packages:
}
engines: { node: '>= 18.0.0' }
+ find-my-way@9.4.0:
+ resolution:
+ {
+ integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==,
+ }
+ engines: { node: '>=20' }
+
find-up@4.1.0:
resolution:
{
@@ -6709,6 +6832,12 @@ packages:
}
engines: { node: '>= 0.4' }
+ get-tsconfig@4.13.0:
+ resolution:
+ {
+ integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==,
+ }
+
glob-parent@5.1.2:
resolution:
{
@@ -6879,6 +7008,12 @@ packages:
}
engines: { node: '>= 0.4' }
+ help-me@5.0.0:
+ resolution:
+ {
+ integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==,
+ }
+
hmac-drbg@1.0.1:
resolution:
{
@@ -7074,6 +7209,13 @@ packages:
}
engines: { node: '>= 0.10' }
+ ipaddr.js@2.3.0:
+ resolution:
+ {
+ integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==,
+ }
+ engines: { node: '>= 10' }
+
ipns@10.1.3:
resolution:
{
@@ -7760,6 +7902,12 @@ packages:
integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==,
}
+ json-schema-ref-resolver@3.0.0:
+ resolution:
+ {
+ integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==,
+ }
+
json-schema-traverse@0.4.1:
resolution:
{
@@ -7909,6 +8057,12 @@ packages:
integrity: sha512-v/Ip8k8eYdp7bINpzqDh46V/PaQ8sK+qi97nMQgjZzFlb166YFqlR/HVI+MzsI9JqcyyVWCOipmmretiaSyQyw==,
}
+ light-my-request@6.6.0:
+ resolution:
+ {
+ integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==,
+ }
+
lilconfig@3.1.3:
resolution:
{
@@ -9129,6 +9283,19 @@ packages:
integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==,
}
+ pino-abstract-transport@3.0.0:
+ resolution:
+ {
+ integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==,
+ }
+
+ pino-pretty@13.1.3:
+ resolution:
+ {
+ integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==,
+ }
+ hasBin: true
+
pino-std-serializers@7.1.0:
resolution:
{
@@ -9142,6 +9309,13 @@ packages:
}
hasBin: true
+ pino@10.3.0:
+ resolution:
+ {
+ integrity: sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA==,
+ }
+ hasBin: true
+
pirates@4.0.7:
resolution:
{
@@ -9304,6 +9478,12 @@ packages:
integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==,
}
+ process-warning@4.0.1:
+ resolution:
+ {
+ integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==,
+ }
+
process-warning@5.0.0:
resolution:
{
@@ -9653,6 +9833,12 @@ packages:
}
engines: { node: '>=8' }
+ resolve-pkg-maps@1.0.0:
+ resolution:
+ {
+ integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==,
+ }
+
resolve.exports@2.0.3:
resolution:
{
@@ -9682,6 +9868,13 @@ packages:
}
engines: { node: '>=18' }
+ ret@0.5.0:
+ resolution:
+ {
+ integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==,
+ }
+ engines: { node: '>=10' }
+
reusify@1.1.0:
resolution:
{
@@ -9817,6 +10010,12 @@ packages:
}
engines: { node: '>= 0.4' }
+ safe-regex2@5.0.0:
+ resolution:
+ {
+ integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==,
+ }
+
safe-stable-stringify@1.1.1:
resolution:
{
@@ -9856,6 +10055,12 @@ packages:
}
engines: { node: '>= 10.13.0' }
+ secure-json-parse@4.1.0:
+ resolution:
+ {
+ integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==,
+ }
+
semver@6.3.1:
resolution:
{
@@ -10343,6 +10548,13 @@ packages:
}
engines: { node: '>=8' }
+ strip-json-comments@5.0.3:
+ resolution:
+ {
+ integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==,
+ }
+ engines: { node: '>=14.16' }
+
strip-literal@3.1.0:
resolution:
{
@@ -10525,6 +10737,13 @@ packages:
integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==,
}
+ thread-stream@4.0.0:
+ resolution:
+ {
+ integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==,
+ }
+ engines: { node: '>=20' }
+
timers-browserify@2.0.12:
resolution:
{
@@ -10606,6 +10825,13 @@ packages:
}
engines: { node: '>=8.0' }
+ toad-cache@3.7.0:
+ resolution:
+ {
+ integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==,
+ }
+ engines: { node: '>=12' }
+
toggle-selection@1.0.6:
resolution:
{
@@ -10775,6 +11001,14 @@ packages:
typescript:
optional: true
+ tsx@4.21.0:
+ resolution:
+ {
+ integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==,
+ }
+ engines: { node: '>=18.0.0' }
+ hasBin: true
+
tty-browserify@0.0.1:
resolution:
{
@@ -12367,6 +12601,29 @@ snapshots:
'@faker-js/faker@9.9.0': {}
+ '@fastify/ajv-compiler@4.0.5':
+ dependencies:
+ ajv: 8.17.1
+ ajv-formats: 3.0.1(ajv@8.17.1)
+ fast-uri: 3.1.0
+
+ '@fastify/error@4.2.0': {}
+
+ '@fastify/fast-json-stringify-compiler@5.0.3':
+ dependencies:
+ fast-json-stringify: 6.2.0
+
+ '@fastify/forwarded@3.0.1': {}
+
+ '@fastify/merge-json-schemas@0.2.1':
+ dependencies:
+ dequal: 2.0.3
+
+ '@fastify/proxy-addr@5.1.0':
+ dependencies:
+ '@fastify/forwarded': 3.0.1
+ ipaddr.js: 2.3.0
+
'@floating-ui/core@1.7.3':
dependencies:
'@floating-ui/utils': 0.2.10
@@ -13397,6 +13654,8 @@ snapshots:
'@phc/format@1.0.0': {}
+ '@pinojs/redact@0.4.0': {}
+
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -15000,6 +15259,8 @@ snapshots:
dependencies:
event-target-shim: 5.0.1
+ abstract-logging@2.0.1: {}
+
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
@@ -15185,6 +15446,11 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
+ avvio@9.1.0:
+ dependencies:
+ '@fastify/error': 4.2.0
+ fastq: 1.20.1
+
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
@@ -15793,6 +16059,8 @@ snapshots:
dependencies:
'@babel/runtime': 7.28.6
+ dateformat@4.6.3: {}
+
dayjs@1.11.19: {}
debug@4.3.4:
@@ -15839,6 +16107,8 @@ snapshots:
dependency-graph@0.11.0: {}
+ dequal@2.0.3: {}
+
des.js@1.1.0:
dependencies:
inherits: 2.0.4
@@ -16370,6 +16640,10 @@ snapshots:
eyes@0.1.8: {}
+ fast-copy@4.0.2: {}
+
+ fast-decode-uri-component@1.0.1: {}
+
fast-deep-equal@3.1.3: {}
fast-diff@1.3.0: {}
@@ -16386,16 +16660,47 @@ snapshots:
fast-json-stable-stringify@2.1.0: {}
+ fast-json-stringify@6.2.0:
+ dependencies:
+ '@fastify/merge-json-schemas': 0.2.1
+ ajv: 8.17.1
+ ajv-formats: 3.0.1(ajv@8.17.1)
+ fast-uri: 3.1.0
+ json-schema-ref-resolver: 3.0.0
+ rfdc: 1.4.1
+
fast-levenshtein@2.0.6: {}
fast-memoize@2.5.2: {}
+ fast-querystring@1.1.2:
+ dependencies:
+ fast-decode-uri-component: 1.0.1
+
fast-safe-stringify@2.1.1: {}
fast-stable-stringify@1.0.0: {}
fast-uri@3.1.0: {}
+ fastify@5.7.1:
+ dependencies:
+ '@fastify/ajv-compiler': 4.0.5
+ '@fastify/error': 4.2.0
+ '@fastify/fast-json-stringify-compiler': 5.0.3
+ '@fastify/proxy-addr': 5.1.0
+ abstract-logging: 2.0.1
+ avvio: 9.1.0
+ fast-json-stringify: 6.2.0
+ find-my-way: 9.4.0
+ light-my-request: 6.6.0
+ pino: 10.3.0
+ process-warning: 5.0.0
+ rfdc: 1.4.1
+ secure-json-parse: 4.1.0
+ semver: 7.7.3
+ toad-cache: 3.7.0
+
fastq@1.20.1:
dependencies:
reusify: 1.1.0
@@ -16442,6 +16747,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ find-my-way@9.4.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-querystring: 1.1.2
+ safe-regex2: 5.0.0
+
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
@@ -16584,6 +16895,10 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
+ get-tsconfig@4.13.0:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -16702,6 +17017,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ help-me@5.0.0: {}
+
hmac-drbg@1.0.1:
dependencies:
hash.js: 1.1.7
@@ -16801,6 +17118,8 @@ snapshots:
ipaddr.js@1.9.1: {}
+ ipaddr.js@2.3.0: {}
+
ipns@10.1.3:
dependencies:
'@libp2p/crypto': 5.1.13
@@ -17395,6 +17714,10 @@ snapshots:
json-rpc-random-id@1.0.1: {}
+ json-schema-ref-resolver@3.0.0:
+ dependencies:
+ dequal: 2.0.3
+
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
@@ -17478,6 +17801,12 @@ snapshots:
libphonenumber-js@1.12.34: {}
+ light-my-request@6.6.0:
+ dependencies:
+ cookie: 1.1.1
+ process-warning: 4.0.1
+ set-cookie-parser: 2.7.2
+
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
@@ -18192,6 +18521,26 @@ snapshots:
dependencies:
split2: 4.2.0
+ pino-abstract-transport@3.0.0:
+ dependencies:
+ split2: 4.2.0
+
+ pino-pretty@13.1.3:
+ dependencies:
+ colorette: 2.0.20
+ dateformat: 4.6.3
+ fast-copy: 4.0.2
+ fast-safe-stringify: 2.1.1
+ help-me: 5.0.0
+ joycon: 3.1.1
+ minimist: 1.2.8
+ on-exit-leak-free: 2.1.2
+ pino-abstract-transport: 3.0.0
+ pump: 3.0.3
+ secure-json-parse: 4.1.0
+ sonic-boom: 4.2.0
+ strip-json-comments: 5.0.3
+
pino-std-serializers@7.1.0: {}
pino@10.0.0:
@@ -18208,6 +18557,20 @@ snapshots:
sonic-boom: 4.2.0
thread-stream: 3.1.0
+ pino@10.3.0:
+ dependencies:
+ '@pinojs/redact': 0.4.0
+ atomic-sleep: 1.0.0
+ on-exit-leak-free: 2.1.2
+ pino-abstract-transport: 3.0.0
+ pino-std-serializers: 7.1.0
+ process-warning: 5.0.0
+ quick-format-unescaped: 4.0.4
+ real-require: 0.2.0
+ safe-stable-stringify: 2.5.0
+ sonic-boom: 4.2.0
+ thread-stream: 4.0.0
+
pirates@4.0.7: {}
pkg-dir@4.2.0:
@@ -18280,6 +18643,8 @@ snapshots:
process-nextick-args@2.0.1: {}
+ process-warning@4.0.1: {}
+
process-warning@5.0.0: {}
process@0.11.10: {}
@@ -18484,6 +18849,8 @@ snapshots:
resolve-from@5.0.0: {}
+ resolve-pkg-maps@1.0.0: {}
+
resolve.exports@2.0.3: {}
resolve@1.22.11:
@@ -18502,6 +18869,8 @@ snapshots:
onetime: 7.0.0
signal-exit: 4.1.0
+ ret@0.5.0: {}
+
reusify@1.1.0: {}
rfdc@1.4.1: {}
@@ -18644,6 +19013,10 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
+ safe-regex2@5.0.0:
+ dependencies:
+ ret: 0.5.0
+
safe-stable-stringify@1.1.1: {}
safe-stable-stringify@2.5.0: {}
@@ -18667,6 +19040,8 @@ snapshots:
ajv-formats: 2.1.1(ajv@8.17.1)
ajv-keywords: 5.1.0(ajv@8.17.1)
+ secure-json-parse@4.1.0: {}
+
semver@6.3.1: {}
semver@7.7.3: {}
@@ -18977,6 +19352,8 @@ snapshots:
strip-json-comments@3.1.1: {}
+ strip-json-comments@5.0.3: {}
+
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
@@ -19105,6 +19482,10 @@ snapshots:
dependencies:
real-require: 0.2.0
+ thread-stream@4.0.0:
+ dependencies:
+ real-require: 0.2.0
+
timers-browserify@2.0.12:
dependencies:
setimmediate: 1.0.5
@@ -19146,6 +19527,8 @@ snapshots:
dependencies:
is-number: 7.0.0
+ toad-cache@3.7.0: {}
+
toggle-selection@1.0.6: {}
toidentifier@1.0.1: {}
@@ -19257,6 +19640,13 @@ snapshots:
- tsx
- yaml
+ tsx@4.21.0:
+ dependencies:
+ esbuild: 0.27.2
+ get-tsconfig: 4.13.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
tty-browserify@0.0.1: {}
type-check@0.4.0:
diff --git a/tests/e2e/archive/README.md b/tests/e2e/archive/README.md
deleted file mode 100644
index 8583acfce..000000000
--- a/tests/e2e/archive/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Archived E2E Tests
-
-These test files have been archived as part of the simplified E2E test approach.
-
-## Why Archived?
-
-The original tests used a storage-state-based approach that required:
-
-1. Global setup to perform Web3Auth login and save session state
-2. Each test file loading the saved storage state
-3. Complex fixture handling for session restoration
-
-This approach had issues with:
-
-- Web3Auth session expiry causing flaky tests
-- Storage state becoming stale between test runs
-- Complex CI setup with state file management
-
-## New Approach
-
-The new `tests/full-workflow.spec.ts` uses a single sequential test session that:
-
-1. Logs in once via Web3Auth at the start
-2. Performs all file/folder operations in sequence
-3. Cleans up and logs out at the end
-
-This eliminates session expiry issues and reduces complexity.
-
-## Restoring Archived Tests
-
-If you need to restore these tests:
-
-1. Move the test files back to `tests/e2e/tests/`
-2. Restore `fixtures/auth.fixture.ts` and `fixtures/index.ts`
-3. Restore `global-setup.ts`
-4. Update `playwright.config.ts` to use globalSetup and storageState
-
-Note: The archived tests may need updates to work with the current codebase.
diff --git a/tests/e2e/archive/tests/auth/login.spec.ts b/tests/e2e/archive/tests/auth/login.spec.ts
deleted file mode 100644
index 92c91ef52..000000000
--- a/tests/e2e/archive/tests/auth/login.spec.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { test, expect } from '@playwright/test';
-import {
- waitForWeb3AuthModal,
- loginViaEmail,
- TEST_CREDENTIALS,
-} from '../../utils/web3auth-helpers';
-
-/**
- * Login flow E2E tests.
- * Tests the complete authentication journey via Web3Auth.
- *
- * These tests need fresh browser context without any stored authentication,
- * so we override the storageState from playwright.config.ts
- */
-test.describe('Login Flow', () => {
- // Don't use stored authentication for login flow tests - start fresh
- test.use({ storageState: { cookies: [], origins: [] } });
- test('shows login page for unauthenticated users', async ({ page }) => {
- // Navigate to root
- await page.goto('/');
-
- // Verify login page is displayed
- await expect(page.locator('h1:has-text("CipherBox")')).toBeVisible();
- await expect(page.locator('text=Zero-knowledge encrypted cloud storage')).toBeVisible();
- await expect(page.locator('button:has-text("Sign In")')).toBeVisible();
-
- // Verify user is not authenticated (no logout button)
- await expect(page.locator('button:has-text("Logout")')).not.toBeVisible();
- });
-
- test('clicking login opens Web3Auth modal', async ({ page }) => {
- // Navigate to login page
- await page.goto('/');
-
- // Click the Sign In button
- await page.click('button:has-text("Sign In")');
-
- // Wait for Web3Auth modal to appear
- try {
- const { locator: modal } = await waitForWeb3AuthModal(page);
-
- // Verify modal content is visible - look for email login option
- const emailButton = modal.locator(
- 'button:has-text("Continue with Email"), button:has-text("Email")'
- );
- await expect(emailButton.first()).toBeVisible();
-
- // Close modal by pressing Escape or clicking outside
- await page.keyboard.press('Escape');
- } catch (error) {
- // If Web3Auth modal doesn't appear, it might be a configuration issue
- console.error('Web3Auth modal did not appear:', error);
- throw error;
- }
- });
-
- // This test uses Web3Auth test credentials with static OTP for automated login
- test('redirects to dashboard after successful login', async ({ page }) => {
- // Skip if test credentials are not configured
- test.skip(
- !TEST_CREDENTIALS.email || !TEST_CREDENTIALS.otp,
- 'Web3Auth test credentials not configured (WEB3AUTH_TEST_EMAIL, WEB3AUTH_TEST_OTP)'
- );
-
- // Perform full login flow with test credentials
- await page.goto('/');
- await loginViaEmail(page, TEST_CREDENTIALS.email, TEST_CREDENTIALS.otp);
-
- // Verify we're on the dashboard
- await expect(page).toHaveURL(/.*dashboard/);
- await expect(page.locator('button:has-text("Logout")')).toBeVisible();
- });
-
- test('redirects unauthenticated users from protected routes to login', async ({ page }) => {
- // Try to navigate directly to dashboard without authentication
- await page.goto('/dashboard');
-
- // Should be redirected to login page
- // Dashboard checks authentication and redirects if not authenticated
- await page.waitForURL(/^(?!.*dashboard).*$/); // Wait for URL to not contain 'dashboard'
-
- // Verify we're back on login page
- await expect(page.locator('button:has-text("Sign In")')).toBeVisible();
- await expect(page.locator('text=Zero-knowledge encrypted cloud storage')).toBeVisible();
- });
-
- test('redirects unauthenticated users from settings to login', async ({ page }) => {
- // Try to navigate to settings without authentication
- await page.goto('/settings');
-
- // Should be redirected to login page
- await page.waitForURL(/^(?!.*settings).*$/);
-
- // Verify we're on login page
- await expect(page.locator('button:has-text("Sign In")')).toBeVisible();
- });
-});
diff --git a/tests/e2e/archive/tests/auth/session.spec.ts b/tests/e2e/archive/tests/auth/session.spec.ts
deleted file mode 100644
index e284bfcbe..000000000
--- a/tests/e2e/archive/tests/auth/session.spec.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-
-/**
- * Session persistence E2E tests.
- * Tests that authenticated session persists across page reloads and navigation.
- */
-authenticatedTest.describe('Session Persistence', () => {
- authenticatedTest('session persists after page reload', async ({ authenticatedPage }) => {
- // Navigate to dashboard
- await authenticatedPage.goto('/dashboard');
-
- // Verify we're authenticated
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
- await expect(authenticatedPage.locator('.user-info')).toBeVisible();
-
- // Reload the page
- await authenticatedPage.reload();
-
- // Wait for page to fully load
- await authenticatedPage.waitForLoadState('networkidle');
-
- // Verify we're still on dashboard (not redirected to login)
- await expect(authenticatedPage).toHaveURL(/.*dashboard/);
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
- await expect(authenticatedPage.locator('.user-info')).toBeVisible();
- });
-
- authenticatedTest(
- 'session persists when navigating between pages',
- async ({ authenticatedPage }) => {
- // Start on dashboard
- await authenticatedPage.goto('/dashboard');
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
-
- // Navigate to settings
- await authenticatedPage.goto('/settings');
- await expect(authenticatedPage).toHaveURL(/.*settings/);
- // Settings page doesn't have Logout button, verify we see the "Back to Dashboard" button
- await expect(authenticatedPage.locator('button:has-text("Back to Dashboard")')).toBeVisible();
-
- // Navigate back to dashboard
- await authenticatedPage.goto('/dashboard');
- await expect(authenticatedPage).toHaveURL(/.*dashboard/);
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
-
- // Verify session remained valid throughout navigation
- await expect(authenticatedPage.locator('.user-info')).toBeVisible();
- }
- );
-
- authenticatedTest('expired session redirects to login', async ({ authenticatedPage }) => {
- // Navigate to dashboard
- await authenticatedPage.goto('/dashboard');
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
-
- // Clear all cookies to simulate expired session
- await authenticatedPage.context().clearCookies();
-
- // Also clear localStorage and sessionStorage
- await authenticatedPage.evaluate(() => {
- localStorage.clear();
- sessionStorage.clear();
- });
-
- // Reload page
- await authenticatedPage.reload();
- await authenticatedPage.waitForLoadState('networkidle');
-
- // Should be redirected to login page
- await authenticatedPage.waitForURL(/^(?!.*dashboard).*$/, { timeout: 10000 });
- await expect(authenticatedPage.locator('button:has-text("Sign In")')).toBeVisible({
- timeout: 10000,
- });
- });
-
- authenticatedTest(
- 'browser back/forward maintains authentication',
- async ({ authenticatedPage }) => {
- // Start on dashboard
- await authenticatedPage.goto('/dashboard');
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
-
- // Navigate to settings
- await authenticatedPage.goto('/settings');
- await expect(authenticatedPage).toHaveURL(/.*settings/);
-
- // Use browser back button
- await authenticatedPage.goBack();
- await expect(authenticatedPage).toHaveURL(/.*dashboard/);
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
-
- // Use browser forward button
- await authenticatedPage.goForward();
- await expect(authenticatedPage).toHaveURL(/.*settings/);
- // Settings page has "Back to Dashboard" button instead of Logout
- await expect(authenticatedPage.locator('button:has-text("Back to Dashboard")')).toBeVisible();
- }
- );
-
- authenticatedTest(
- 'multiple tabs share authentication state',
- async ({ authenticatedPage, context }) => {
- // Navigate to dashboard in first tab
- await authenticatedPage.goto('/dashboard');
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
-
- // Verify cookies are accessible (they're shared across tabs in same context)
- const cookies = await context.cookies();
- const refreshTokenCookie = cookies.find((c) => c.name === 'refresh_token');
- expect(refreshTokenCookie).toBeDefined();
-
- // Get the current cached token from the first tab to pass to new tab
- const cachedToken = await authenticatedPage.evaluate(() => {
- return sessionStorage.getItem('__e2e_cached_token__');
- });
-
- // Open a new tab and inject the cached access token
- // This simulates what would happen if the user opened a new tab
- // (cookies are shared, but sessionStorage is not)
- const newPage = await context.newPage();
-
- // Use addInitScript with explicit token injection
- await newPage.addInitScript(`
- sessionStorage.setItem('__e2e_test_mode__', 'true');
- sessionStorage.setItem('__e2e_cached_token__', '${cachedToken || ''}');
- `);
-
- await newPage.goto('/dashboard');
-
- // New tab should also be authenticated (using cached token)
- await expect(newPage).toHaveURL(/.*dashboard/);
- await expect(newPage.locator('button:has-text("Logout")')).toBeVisible({ timeout: 20000 });
-
- // Cleanup
- await newPage.close();
- }
- );
-});
diff --git a/tests/e2e/archive/tests/auth/z-logout.spec.ts b/tests/e2e/archive/tests/auth/z-logout.spec.ts
deleted file mode 100644
index ee3fbc9dd..000000000
--- a/tests/e2e/archive/tests/auth/z-logout.spec.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-
-/**
- * Logout flow E2E tests.
- * Tests logout functionality and session clearing.
- *
- * IMPORTANT: These tests perform logout which revokes all user tokens.
- * Tests are structured to handle this:
- * - Test 1 (non-destructive): Just checks logout button is visible
- * - Test 2 (comprehensive logout): Single test that verifies all logout behaviors
- *
- * This structure ensures each test can authenticate successfully without
- * being affected by token revocation from previous tests.
- */
-authenticatedTest.describe('Logout Flow', () => {
- authenticatedTest(
- 'logout button is visible when authenticated',
- async ({ authenticatedPage }) => {
- // Navigate to dashboard (should already be authenticated)
- await authenticatedPage.goto('/dashboard');
-
- // Verify logout button is visible
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
-
- // Verify user info is displayed (authenticated state)
- await expect(authenticatedPage.locator('.user-info')).toBeVisible();
- }
- );
-
- /**
- * Comprehensive logout test that verifies all logout behaviors in a single test.
- * This avoids issues with token revocation breaking subsequent tests.
- */
- authenticatedTest(
- 'logout clears session, redirects to login, and clears localStorage',
- async ({ authenticatedPage }) => {
- // Start on dashboard
- await authenticatedPage.goto('/dashboard');
-
- // Verify we're authenticated
- await expect(authenticatedPage.locator('button:has-text("Logout")')).toBeVisible();
-
- // Check localStorage before logout (should have some auth-related items)
- const localStorageBefore = await authenticatedPage.evaluate(() => {
- return {
- hasAuthToken: localStorage.getItem('authToken') !== null,
- hasWeb3AuthSession: Object.keys(localStorage).some((key) => key.includes('web3auth')),
- itemCount: localStorage.length,
- };
- });
-
- // We expect some items in localStorage when authenticated
- expect(localStorageBefore.itemCount).toBeGreaterThan(0);
-
- // Click logout button
- await authenticatedPage.click('button:has-text("Logout")');
-
- // Wait for redirect to login page
- await authenticatedPage.waitForURL(/^(?!.*dashboard).*$/);
-
- // Wait for loading to complete (E2E session restoration may take time)
- // Then verify we're on login page with Sign In button
- await expect(authenticatedPage.locator('button:has-text("Sign In")')).toBeVisible({
- timeout: 15000,
- });
- await expect(authenticatedPage.locator('button:has-text("Logout")')).not.toBeVisible();
-
- // Verify localStorage was cleared
- const localStorageAfter = await authenticatedPage.evaluate(() => {
- return {
- hasAuthToken: localStorage.getItem('authToken') !== null,
- hasWeb3AuthSession: Object.keys(localStorage).some((key) => key.includes('web3auth')),
- allItems: Object.keys(localStorage),
- };
- });
-
- // Sensitive auth data should be cleared
- expect(localStorageAfter.hasAuthToken).toBe(false);
-
- // Log remaining items for debugging (should be empty or only non-sensitive data)
- if (localStorageAfter.allItems.length > 0) {
- console.log('Remaining localStorage items:', localStorageAfter.allItems);
- }
-
- // Try to navigate to dashboard again (should redirect to login)
- await authenticatedPage.goto('/dashboard');
-
- // Should remain on login page (not dashboard) since we're logged out
- await authenticatedPage.waitForURL(/^(?!.*dashboard).*$/);
- await expect(authenticatedPage.locator('button:has-text("Sign In")')).toBeVisible({
- timeout: 15000,
- });
- }
- );
-});
diff --git a/tests/e2e/archive/tests/files/delete.spec.ts b/tests/e2e/archive/tests/files/delete.spec.ts
deleted file mode 100644
index 81d63cc7a..000000000
--- a/tests/e2e/archive/tests/files/delete.spec.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-import { UploadZonePage } from '../../page-objects/file-browser/upload-zone.page';
-import { FileListPage } from '../../page-objects/file-browser/file-list.page';
-import { ContextMenuPage } from '../../page-objects/file-browser/context-menu.page';
-import { ConfirmDialogPage } from '../../page-objects/dialogs/confirm-dialog.page';
-import { createTestTextFile, cleanupTestFiles } from '../../utils/test-files';
-
-/**
- * E2E tests for file delete functionality.
- * Tests delete via context menu with confirmation dialog.
- */
-authenticatedTest.describe('File Delete', () => {
- authenticatedTest.afterEach(async () => {
- // Clean up test files after each test
- cleanupTestFiles();
- });
-
- authenticatedTest('can delete file via context menu', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const confirmDialog = new ConfirmDialogPage(authenticatedPage);
-
- // Create and upload test file
- const testFile = createTestTextFile('delete-me.txt', 'Test content');
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(testFile.name, { timeout: 5000 });
-
- // Open context menu and click delete
- await fileList.rightClickItem(testFile.name);
- await contextMenu.waitForOpen({ timeout: 2000 });
- await contextMenu.clickDelete();
-
- // Wait for confirmation dialog
- await confirmDialog.waitForOpen({ timeout: 2000 });
-
- // Confirm deletion
- await confirmDialog.clickConfirm();
- await confirmDialog.waitForClose({ timeout: 5000 });
-
- // Verify file is removed from list
- await fileList.waitForItemToDisappear(testFile.name, { timeout: 5000 });
- await expect(fileList.getItem(testFile.name)).not.toBeVisible();
- });
-
- authenticatedTest('delete confirmation shows file name', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const confirmDialog = new ConfirmDialogPage(authenticatedPage);
-
- // Create and upload test file
- const fileName = 'important-file.txt';
- const testFile = createTestTextFile(fileName, 'Test content');
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(fileName, { timeout: 5000 });
-
- // Open delete confirmation
- await fileList.rightClickItem(fileName);
- await contextMenu.waitForOpen({ timeout: 2000 });
- await contextMenu.clickDelete();
- await confirmDialog.waitForOpen({ timeout: 2000 });
-
- // Verify dialog shows file name in message
- const message = await confirmDialog.getMessage();
- expect(message).toContain(fileName);
-
- // Cancel without deleting
- await confirmDialog.clickCancel();
- await confirmDialog.waitForClose({ timeout: 2000 });
- });
-
- authenticatedTest('can cancel delete', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const confirmDialog = new ConfirmDialogPage(authenticatedPage);
-
- // Create and upload test file
- const fileName = 'keep-me.txt';
- const testFile = createTestTextFile(fileName, 'Test content');
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(fileName, { timeout: 5000 });
-
- // Open delete confirmation
- await fileList.rightClickItem(fileName);
- await contextMenu.waitForOpen({ timeout: 2000 });
- await contextMenu.clickDelete();
- await confirmDialog.waitForOpen({ timeout: 2000 });
-
- // Cancel deletion
- await confirmDialog.clickCancel();
- await confirmDialog.waitForClose({ timeout: 2000 });
-
- // Verify file is still in list
- await expect(fileList.getFileItem(fileName)).toBeVisible();
-
- // Verify file count hasn't changed (at least 1 file)
- const itemCount = await fileList.getItemCount();
- expect(itemCount).toBeGreaterThanOrEqual(1);
- });
-});
diff --git a/tests/e2e/archive/tests/files/download.spec.ts b/tests/e2e/archive/tests/files/download.spec.ts
deleted file mode 100644
index 75975c17e..000000000
--- a/tests/e2e/archive/tests/files/download.spec.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { expect } from '@playwright/test';
-import { readFileSync } from 'fs';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-import { UploadZonePage } from '../../page-objects/file-browser/upload-zone.page';
-import { FileListPage } from '../../page-objects/file-browser/file-list.page';
-import { ContextMenuPage } from '../../page-objects/file-browser/context-menu.page';
-import { createTestTextFile, cleanupTestFiles } from '../../utils/test-files';
-
-/**
- * E2E tests for file download functionality.
- * Tests download via context menu and verifies downloaded content.
- */
-authenticatedTest.describe('File Download', () => {
- authenticatedTest.afterEach(async () => {
- // Clean up test files after each test
- cleanupTestFiles();
- });
-
- authenticatedTest('can download file via context menu', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
-
- // Create and upload test file
- const testFile = createTestTextFile('download-test.txt', 'Download test content');
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(testFile.name, { timeout: 5000 });
-
- // Set up download promise before triggering download
- const downloadPromise = authenticatedPage.waitForEvent('download');
-
- // Right-click file and select download
- await fileList.rightClickItem(testFile.name);
- await contextMenu.waitForOpen({ timeout: 2000 });
- await contextMenu.clickDownload();
-
- // Wait for download to start
- const download = await downloadPromise;
-
- // Verify download started with correct filename
- expect(download.suggestedFilename()).toBe(testFile.name);
- });
-
- authenticatedTest('downloaded file has correct content', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
-
- // Create test file with specific content
- const fileContent = 'This is the exact content we expect to download.\nLine 2 of content.';
- const testFile = createTestTextFile('content-test.txt', fileContent);
-
- // Upload file
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(testFile.name, { timeout: 5000 });
-
- // Set up download promise
- const downloadPromise = authenticatedPage.waitForEvent('download');
-
- // Trigger download
- await fileList.rightClickItem(testFile.name);
- await contextMenu.waitForOpen({ timeout: 2000 });
- await contextMenu.clickDownload();
-
- // Wait for download and save to temporary location
- const download = await downloadPromise;
- const downloadPath = await download.path();
-
- // Verify downloaded file content matches original
- if (downloadPath) {
- const downloadedContent = readFileSync(downloadPath, 'utf-8');
- expect(downloadedContent).toBe(fileContent);
- } else {
- throw new Error('Download path is null - download may have failed');
- }
- });
-});
diff --git a/tests/e2e/archive/tests/files/rename.spec.ts b/tests/e2e/archive/tests/files/rename.spec.ts
deleted file mode 100644
index 5d5533326..000000000
--- a/tests/e2e/archive/tests/files/rename.spec.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-import { UploadZonePage } from '../../page-objects/file-browser/upload-zone.page';
-import { FileListPage } from '../../page-objects/file-browser/file-list.page';
-import { ContextMenuPage } from '../../page-objects/file-browser/context-menu.page';
-import { RenameDialogPage } from '../../page-objects/dialogs/rename-dialog.page';
-import { createTestTextFile, cleanupTestFiles } from '../../utils/test-files';
-
-/**
- * E2E tests for file rename functionality.
- * Tests rename via context menu and rename dialog.
- */
-authenticatedTest.describe('File Rename', () => {
- authenticatedTest.afterEach(async () => {
- // Clean up test files after each test
- cleanupTestFiles();
- });
-
- authenticatedTest('can rename file via context menu', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const renameDialog = new RenameDialogPage(authenticatedPage);
-
- // Create and upload test file
- const testFile = createTestTextFile('old-name.txt', 'Test content');
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(testFile.name, { timeout: 5000 });
-
- // Open context menu and click rename
- await fileList.rightClickItem(testFile.name);
- await contextMenu.waitForOpen({ timeout: 2000 });
- await contextMenu.clickRename();
-
- // Wait for rename dialog
- await renameDialog.waitForOpen({ timeout: 2000 });
-
- // Enter new name and save
- const newName = 'new-name.txt';
- await renameDialog.clearAndEnterName(newName);
- await renameDialog.clickSave();
-
- // Wait for dialog to close
- await renameDialog.waitForClose({ timeout: 5000 });
-
- // Verify old name is gone and new name appears
- await fileList.waitForItemToDisappear(testFile.name, { timeout: 5000 });
- await fileList.waitForItemToAppear(newName, { timeout: 5000 });
- await expect(fileList.getFileItem(newName)).toBeVisible();
- });
-
- authenticatedTest('rename dialog shows current name', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const renameDialog = new RenameDialogPage(authenticatedPage);
-
- // Create and upload test file
- const fileName = 'current-name.txt';
- const testFile = createTestTextFile(fileName, 'Test content');
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(fileName, { timeout: 5000 });
-
- // Open rename dialog
- await fileList.rightClickItem(fileName);
- await contextMenu.waitForOpen({ timeout: 2000 });
- await contextMenu.clickRename();
- await renameDialog.waitForOpen({ timeout: 2000 });
-
- // Verify current name is pre-filled in input
- const currentName = await renameDialog.getCurrentName();
- expect(currentName).toBe(fileName);
-
- // Close dialog without saving
- await renameDialog.clickCancel();
- await renameDialog.waitForClose({ timeout: 2000 });
- });
-
- authenticatedTest('can cancel rename', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const renameDialog = new RenameDialogPage(authenticatedPage);
-
- // Create and upload test file
- const originalName = 'original.txt';
- const testFile = createTestTextFile(originalName, 'Test content');
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(originalName, { timeout: 5000 });
-
- // Open rename dialog
- await fileList.rightClickItem(originalName);
- await contextMenu.waitForOpen({ timeout: 2000 });
- await contextMenu.clickRename();
- await renameDialog.waitForOpen({ timeout: 2000 });
-
- // Enter new name but cancel
- await renameDialog.clearAndEnterName('cancelled-name.txt');
- await renameDialog.clickCancel();
- await renameDialog.waitForClose({ timeout: 2000 });
-
- // Verify original name is still there
- await expect(fileList.getFileItem(originalName)).toBeVisible();
-
- // Verify new name was not created
- const visibleNames = await fileList.getVisibleItemNames();
- expect(visibleNames).not.toContain('cancelled-name.txt');
- });
-});
diff --git a/tests/e2e/archive/tests/files/upload.spec.ts b/tests/e2e/archive/tests/files/upload.spec.ts
deleted file mode 100644
index b8c0e024b..000000000
--- a/tests/e2e/archive/tests/files/upload.spec.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-import { UploadZonePage } from '../../page-objects/file-browser/upload-zone.page';
-import { FileListPage } from '../../page-objects/file-browser/file-list.page';
-import { createTestTextFile, cleanupTestFiles } from '../../utils/test-files';
-
-/**
- * E2E tests for file upload functionality.
- * Tests upload via drag-drop zone and verifies files appear in file list.
- */
-authenticatedTest.describe('File Upload', () => {
- authenticatedTest.afterEach(async () => {
- // Clean up test files after each test
- cleanupTestFiles();
- });
-
- authenticatedTest('can upload a text file', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
-
- // Create test file
- const testFile = createTestTextFile('test-upload.txt', 'Upload test content');
-
- // Upload file
- await uploadZone.uploadFile(testFile.path);
-
- // Wait for upload to complete
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
-
- // Verify file appears in file list
- await fileList.waitForItemToAppear(testFile.name, { timeout: 5000 });
- await expect(fileList.getFileItem(testFile.name)).toBeVisible();
- });
-
- authenticatedTest('uploaded file shows correct name', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
-
- // Create test file with specific name
- const fileName = 'my-document.txt';
- const testFile = createTestTextFile(fileName, 'Document content');
-
- // Upload file
- await uploadZone.uploadFile(testFile.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
-
- // Verify exact file name matches
- await fileList.waitForItemToAppear(fileName, { timeout: 5000 });
- const visibleNames = await fileList.getVisibleItemNames();
- expect(visibleNames).toContain(fileName);
- });
-
- authenticatedTest('can upload multiple files sequentially', async ({ authenticatedPage }) => {
- const uploadZone = new UploadZonePage(authenticatedPage);
- const fileList = new FileListPage(authenticatedPage);
-
- // Create multiple test files
- const file1 = createTestTextFile('file1.txt', 'Content 1');
- const file2 = createTestTextFile('file2.txt', 'Content 2');
- const file3 = createTestTextFile('file3.txt', 'Content 3');
-
- // Upload files one by one
- await uploadZone.uploadFile(file1.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(file1.name, { timeout: 5000 });
-
- await uploadZone.uploadFile(file2.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(file2.name, { timeout: 5000 });
-
- await uploadZone.uploadFile(file3.path);
- await uploadZone.waitForUploadComplete({ timeout: 10000 });
- await fileList.waitForItemToAppear(file3.name, { timeout: 5000 });
-
- // Verify all files are visible
- await expect(fileList.getFileItem(file1.name)).toBeVisible();
- await expect(fileList.getFileItem(file2.name)).toBeVisible();
- await expect(fileList.getFileItem(file3.name)).toBeVisible();
-
- // Verify file count
- const itemCount = await fileList.getItemCount();
- expect(itemCount).toBeGreaterThanOrEqual(3);
- });
-});
diff --git a/tests/e2e/archive/tests/folders/create.spec.ts b/tests/e2e/archive/tests/folders/create.spec.ts
deleted file mode 100644
index 868481bee..000000000
--- a/tests/e2e/archive/tests/folders/create.spec.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-import { FileListPage, FolderTreePage } from '../../page-objects/file-browser';
-
-/**
- * Folder creation E2E tests.
- * Tests creating folders via UI and verifying they appear correctly.
- */
-authenticatedTest.describe('Folder Creation', () => {
- authenticatedTest('can create a new folder', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const folderTree = new FolderTreePage(authenticatedPage);
-
- // Navigate to dashboard
- await authenticatedPage.goto('/dashboard');
-
- // Wait for file browser to load
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create a new folder via "New Folder" button or keyboard shortcut
- // Assuming there's a "New Folder" button in the UI
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await expect(newFolderButton).toBeVisible();
- await newFolderButton.click();
-
- // Dialog should appear for folder name input
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await expect(folderNameInput).toBeVisible();
-
- // Enter folder name
- const folderName = `Test Folder ${Date.now()}`;
- await folderNameInput.fill(folderName);
-
- // Submit the form
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- // Verify folder appears in file list
- await fileList.waitForItemToAppear(folderName);
- await expect(fileList.getFolderItem(folderName)).toBeVisible();
-
- // Verify folder appears in tree
- await folderTree.waitForFolderToAppear(folderName);
- await expect(folderTree.getFolder(folderName)).toBeVisible();
- });
-
- authenticatedTest('new folder appears in tree and list', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const folderTree = new FolderTreePage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Get initial counts
- const initialListCount = await fileList.getItemCount();
- const initialTreeFolders = await folderTree.getVisibleFolderNames();
-
- // Create folder
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- const folderName = `Verified Folder ${Date.now()}`;
- await folderNameInput.fill(folderName);
-
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- // Wait for folder to appear
- await fileList.waitForItemToAppear(folderName);
-
- // Verify count increased
- const newListCount = await fileList.getItemCount();
- expect(newListCount).toBe(initialListCount + 1);
-
- // Verify folder is in visible names
- const visibleNames = await fileList.getVisibleItemNames();
- expect(visibleNames).toContain(folderName);
-
- // Verify folder appears in tree
- await folderTree.waitForFolderToAppear(folderName);
- const newTreeFolders = await folderTree.getVisibleFolderNames();
- expect(newTreeFolders).toContain(folderName);
- expect(newTreeFolders.length).toBe(initialTreeFolders.length + 1);
- });
-
- authenticatedTest('can create nested folders', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const folderTree = new FolderTreePage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create parent folder
- const parentName = `Parent ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- let folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(parentName);
-
- let createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(parentName);
-
- // Navigate into parent folder
- await fileList.doubleClickFolder(parentName);
-
- // Wait for navigation
- await authenticatedPage.waitForTimeout(500);
-
- // Create child folder
- const childName = `Child ${Date.now()}`;
- await newFolderButton.click();
-
- folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(childName);
-
- createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- // Verify child folder appears in list
- await fileList.waitForItemToAppear(childName);
- await expect(fileList.getFolderItem(childName)).toBeVisible();
-
- // Expand parent in tree to see child
- await folderTree.expandFolder(parentName);
-
- // Verify child appears in tree under parent
- await folderTree.waitForFolderToAppear(childName);
- await expect(folderTree.getFolder(childName)).toBeVisible();
- });
-});
diff --git a/tests/e2e/archive/tests/folders/delete.spec.ts b/tests/e2e/archive/tests/folders/delete.spec.ts
deleted file mode 100644
index d85cac7f7..000000000
--- a/tests/e2e/archive/tests/folders/delete.spec.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-import { FileListPage, FolderTreePage, ContextMenuPage } from '../../page-objects/file-browser';
-import { ConfirmDialogPage } from '../../page-objects/dialogs';
-
-/**
- * Folder deletion E2E tests.
- * Tests deleting folders via context menu and confirming warnings.
- */
-authenticatedTest.describe('Folder Delete', () => {
- authenticatedTest('delete empty folder', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const folderTree = new FolderTreePage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const confirmDialog = new ConfirmDialogPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create a folder to delete
- const folderName = `Empty Folder ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(folderName);
-
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(folderName);
-
- // Right-click to delete
- await fileList.rightClickItem(folderName);
- await contextMenu.waitForOpen();
- await contextMenu.clickDelete();
-
- // Confirm dialog should appear
- await confirmDialog.waitForOpen();
-
- const title = await confirmDialog.getTitle();
- expect(title).toContain('Delete');
-
- // Confirm deletion
- await confirmDialog.clickConfirm();
- await confirmDialog.waitForClose();
-
- // Verify folder is removed from list
- await fileList.waitForItemToDisappear(folderName);
- await expect(fileList.getItem(folderName)).not.toBeVisible();
-
- // Verify folder is removed from tree
- await expect(folderTree.getFolder(folderName)).not.toBeVisible();
- });
-
- authenticatedTest('delete folder with contents shows warning', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const confirmDialog = new ConfirmDialogPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create parent folder
- const parentName = `Parent With Content ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- let folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(parentName);
-
- let createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(parentName);
-
- // Navigate into parent
- await fileList.doubleClickFolder(parentName);
- await authenticatedPage.waitForTimeout(500);
-
- // Create child folder
- const childName = `Child ${Date.now()}`;
- await newFolderButton.click();
-
- folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(childName);
-
- createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(childName);
-
- // Navigate back to root
- const homeButton = authenticatedPage.locator('.breadcrumb-item').first();
- await homeButton.click();
- await authenticatedPage.waitForTimeout(500);
-
- // Try to delete parent folder
- await fileList.rightClickItem(parentName);
- await contextMenu.waitForOpen();
- await contextMenu.clickDelete();
-
- // Confirm dialog should appear with warning about contents
- await confirmDialog.waitForOpen();
-
- const message = await confirmDialog.getMessage();
- // Message should warn about deleting folder with contents
- expect(message.toLowerCase()).toMatch(/content|subfolder|file/);
-
- // Confirm deletion
- await confirmDialog.clickConfirm();
- await confirmDialog.waitForClose();
-
- // Verify parent and all contents are deleted
- await fileList.waitForItemToDisappear(parentName);
- await expect(fileList.getItem(parentName)).not.toBeVisible();
- });
-
- authenticatedTest('can cancel folder delete', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const folderTree = new FolderTreePage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const confirmDialog = new ConfirmDialogPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create folder
- const folderName = `Folder To Keep ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(folderName);
-
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(folderName);
-
- // Start delete but cancel
- await fileList.rightClickItem(folderName);
- await contextMenu.waitForOpen();
- await contextMenu.clickDelete();
- await confirmDialog.waitForOpen();
-
- // Cancel deletion
- await confirmDialog.clickCancel();
- await confirmDialog.waitForClose();
-
- // Verify folder still exists in list
- await expect(fileList.getFolderItem(folderName)).toBeVisible();
-
- // Verify folder still exists in tree
- await expect(folderTree.getFolder(folderName)).toBeVisible();
- });
-});
diff --git a/tests/e2e/archive/tests/folders/navigation.spec.ts b/tests/e2e/archive/tests/folders/navigation.spec.ts
deleted file mode 100644
index 33b1d4e08..000000000
--- a/tests/e2e/archive/tests/folders/navigation.spec.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-import { FileListPage, FolderTreePage, BreadcrumbsPage } from '../../page-objects/file-browser';
-
-/**
- * Folder navigation E2E tests.
- * Tests navigating between folders via double-click, tree, and breadcrumbs.
- */
-authenticatedTest.describe('Folder Navigation', () => {
- authenticatedTest('double-click folder navigates into it', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const breadcrumbs = new BreadcrumbsPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create a test folder
- const folderName = `Nav Test Folder ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(folderName);
-
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(folderName);
-
- // Get initial breadcrumb count (should be at root)
- const initialBreadcrumbCount = await breadcrumbs.getBreadcrumbCount();
-
- // Double-click to navigate into folder
- await fileList.doubleClickFolder(folderName);
-
- // Wait for navigation
- await authenticatedPage.waitForTimeout(500);
-
- // Verify breadcrumbs updated
- const newBreadcrumbCount = await breadcrumbs.getBreadcrumbCount();
- expect(newBreadcrumbCount).toBe(initialBreadcrumbCount + 1);
-
- // Verify current breadcrumb shows folder name
- const currentBreadcrumb = await breadcrumbs.getCurrentBreadcrumb();
- expect(currentBreadcrumb).toContain(folderName);
-
- // Verify URL or state changed (depends on implementation)
- // File list should be empty (new folder has no contents)
- const itemCount = await fileList.getItemCount();
- expect(itemCount).toBe(0);
- });
-
- authenticatedTest('clicking folder in tree navigates', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const folderTree = new FolderTreePage(authenticatedPage);
- const breadcrumbs = new BreadcrumbsPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create test folder
- const folderName = `Tree Nav Folder ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(folderName);
-
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(folderName);
- await folderTree.waitForFolderToAppear(folderName);
-
- // Click folder in tree
- await folderTree.clickFolder(folderName);
-
- // Wait for navigation
- await authenticatedPage.waitForTimeout(500);
-
- // Verify folder is selected in tree
- const isSelected = await folderTree.isFolderSelected(folderName);
- expect(isSelected).toBe(true);
-
- // Verify breadcrumbs show navigation
- const currentBreadcrumb = await breadcrumbs.getCurrentBreadcrumb();
- expect(currentBreadcrumb).toContain(folderName);
-
- // Verify file list is empty (new folder)
- const itemCount = await fileList.getItemCount();
- expect(itemCount).toBe(0);
- });
-
- authenticatedTest('breadcrumb click navigates to parent', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const breadcrumbs = new BreadcrumbsPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create parent folder
- const parentName = `Parent ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- let folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(parentName);
-
- let createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(parentName);
-
- // Navigate into parent
- await fileList.doubleClickFolder(parentName);
- await authenticatedPage.waitForTimeout(500);
-
- // Create child folder
- const childName = `Child ${Date.now()}`;
- await newFolderButton.click();
-
- folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(childName);
-
- createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(childName);
-
- // Navigate into child
- await fileList.doubleClickFolder(childName);
- await authenticatedPage.waitForTimeout(500);
-
- // Verify we're in child (breadcrumbs show parent > child)
- const pathInChild = await breadcrumbs.getCurrentPath();
- expect(pathInChild.length).toBeGreaterThan(2);
-
- // Click parent breadcrumb to navigate back
- await breadcrumbs.clickBreadcrumb(parentName);
- await authenticatedPage.waitForTimeout(500);
-
- // Verify we're back in parent
- const currentBreadcrumb = await breadcrumbs.getCurrentBreadcrumb();
- expect(currentBreadcrumb).toContain(parentName);
-
- // Verify child folder is visible in list
- await expect(fileList.getFolderItem(childName)).toBeVisible();
- });
-
- authenticatedTest('home breadcrumb navigates to root', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const breadcrumbs = new BreadcrumbsPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create nested structure
- const folderName = `Deep Folder ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(folderName);
-
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(folderName);
-
- // Navigate into folder
- await fileList.doubleClickFolder(folderName);
- await authenticatedPage.waitForTimeout(500);
-
- // Verify we're not at root
- const breadcrumbCount = await breadcrumbs.getBreadcrumbCount();
- expect(breadcrumbCount).toBeGreaterThan(1);
-
- // Click home breadcrumb
- await breadcrumbs.clickHome();
- await authenticatedPage.waitForTimeout(500);
-
- // Verify we're back at root
- const newBreadcrumbCount = await breadcrumbs.getBreadcrumbCount();
- expect(newBreadcrumbCount).toBe(1);
-
- // Verify folder we created is visible in root
- await expect(fileList.getFolderItem(folderName)).toBeVisible();
- });
-});
diff --git a/tests/e2e/archive/tests/folders/rename.spec.ts b/tests/e2e/archive/tests/folders/rename.spec.ts
deleted file mode 100644
index a4359e926..000000000
--- a/tests/e2e/archive/tests/folders/rename.spec.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import { expect } from '@playwright/test';
-import { authenticatedTest } from '../../fixtures/auth.fixture';
-import { FileListPage, FolderTreePage, ContextMenuPage } from '../../page-objects/file-browser';
-import { RenameDialogPage } from '../../page-objects/dialogs';
-
-/**
- * Folder rename E2E tests.
- * Tests renaming folders via context menu and verifying changes.
- */
-authenticatedTest.describe('Folder Rename', () => {
- authenticatedTest('can rename folder via context menu', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const folderTree = new FolderTreePage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const renameDialog = new RenameDialogPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create a test folder first
- const originalName = `Folder To Rename ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(originalName);
-
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(originalName);
-
- // Right-click the folder to open context menu
- await fileList.rightClickItem(originalName);
- await contextMenu.waitForOpen();
-
- // Click rename option
- await contextMenu.clickRename();
- await renameDialog.waitForOpen();
-
- // Verify dialog shows correct title
- const title = await renameDialog.getTitle();
- expect(title).toContain('Rename');
-
- // Enter new name
- const newName = `Renamed Folder ${Date.now()}`;
- await renameDialog.clearAndEnterName(newName);
- await renameDialog.clickSave();
-
- // Wait for dialog to close
- await renameDialog.waitForClose();
-
- // Verify old name is gone
- await fileList.waitForItemToDisappear(originalName);
- await expect(fileList.getItem(originalName)).not.toBeVisible();
-
- // Verify new name appears in file list
- await fileList.waitForItemToAppear(newName);
- await expect(fileList.getFolderItem(newName)).toBeVisible();
-
- // Verify new name appears in tree
- await folderTree.waitForFolderToAppear(newName);
- await expect(folderTree.getFolder(newName)).toBeVisible();
- });
-
- authenticatedTest('renamed folder maintains contents', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const renameDialog = new RenameDialogPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create parent folder
- const parentName = `Parent With Child ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- let folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(parentName);
-
- let createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(parentName);
-
- // Navigate into parent
- await fileList.doubleClickFolder(parentName);
- await authenticatedPage.waitForTimeout(500);
-
- // Create child folder
- const childName = `Child ${Date.now()}`;
- await newFolderButton.click();
-
- folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(childName);
-
- createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(childName);
-
- // Navigate back to root
- const homeButton = authenticatedPage.locator('.breadcrumb-item').first();
- await homeButton.click();
- await authenticatedPage.waitForTimeout(500);
-
- // Rename parent folder
- await fileList.rightClickItem(parentName);
- await contextMenu.waitForOpen();
- await contextMenu.clickRename();
- await renameDialog.waitForOpen();
-
- const newParentName = `Renamed Parent ${Date.now()}`;
- await renameDialog.clearAndEnterName(newParentName);
- await renameDialog.clickSave();
- await renameDialog.waitForClose();
-
- // Wait for rename to complete
- await fileList.waitForItemToAppear(newParentName);
-
- // Navigate into renamed parent
- await fileList.doubleClickFolder(newParentName);
- await authenticatedPage.waitForTimeout(500);
-
- // Verify child still exists
- await expect(fileList.getFolderItem(childName)).toBeVisible();
- });
-
- authenticatedTest('can cancel folder rename', async ({ authenticatedPage }) => {
- const fileList = new FileListPage(authenticatedPage);
- const contextMenu = new ContextMenuPage(authenticatedPage);
- const renameDialog = new RenameDialogPage(authenticatedPage);
-
- await authenticatedPage.goto('/dashboard');
- await fileList.fileListContainer().waitFor({ state: 'visible' });
-
- // Create test folder
- const folderName = `Folder To Not Rename ${Date.now()}`;
- const newFolderButton = authenticatedPage.locator('button:has-text("New Folder")');
- await newFolderButton.click();
-
- const folderNameInput = authenticatedPage.locator('input[placeholder*="folder" i]');
- await folderNameInput.fill(folderName);
-
- const createButton = authenticatedPage.locator('button[type="submit"]:has-text("Create")');
- await createButton.click();
-
- await fileList.waitForItemToAppear(folderName);
-
- // Open rename dialog
- await fileList.rightClickItem(folderName);
- await contextMenu.waitForOpen();
- await contextMenu.clickRename();
- await renameDialog.waitForOpen();
-
- // Enter new name but cancel
- await renameDialog.clearAndEnterName('This Should Not Apply');
- await renameDialog.clickCancel();
- await renameDialog.waitForClose();
-
- // Verify original name still exists
- await expect(fileList.getFolderItem(folderName)).toBeVisible();
-
- // Verify new name was not applied
- await expect(fileList.getItem('This Should Not Apply')).not.toBeVisible();
- });
-});
diff --git a/tests/e2e/archive/tests/smoke.spec.ts b/tests/e2e/archive/tests/smoke.spec.ts
deleted file mode 100644
index bc5558cf4..000000000
--- a/tests/e2e/archive/tests/smoke.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { test, expect } from '../fixtures';
-
-/**
- * Smoke test to verify the E2E testing infrastructure is working correctly.
- * Tests basic page load and interaction capabilities.
- */
-test.describe('Smoke Test', () => {
- test('homepage loads and shows login button', async ({ page, loginPage }) => {
- // Navigate to the homepage
- await loginPage.goto();
-
- // Verify the page loads successfully
- await expect(page).toHaveURL('/');
-
- // Verify the page title contains expected text
- await expect(page).toHaveTitle(/CipherBox|Cipher/i);
-
- // Verify the login button is visible
- const loginButton = page.getByRole('button', { name: /login|sign in/i });
- await expect(loginButton).toBeVisible();
- });
-});
diff --git a/tests/e2e/page-objects/dashboard.page.ts b/tests/e2e/page-objects/dashboard.page.ts
index bc7714754..f930546ca 100644
--- a/tests/e2e/page-objects/dashboard.page.ts
+++ b/tests/e2e/page-objects/dashboard.page.ts
@@ -29,9 +29,10 @@ export class DashboardPage extends BasePage {
/**
* Locator for the logout button
+ * Uses class selector for stability
*/
get logoutButton() {
- return this.page.getByRole('button', { name: /logout|sign out/i });
+ return this.page.locator('button.logout-link');
}
/**
diff --git a/tests/e2e/page-objects/file-browser/file-list.page.ts b/tests/e2e/page-objects/file-browser/file-list.page.ts
index 296aa630b..63971fe36 100644
--- a/tests/e2e/page-objects/file-browser/file-list.page.ts
+++ b/tests/e2e/page-objects/file-browser/file-list.page.ts
@@ -29,7 +29,7 @@ export class FileListPage {
*/
getItem(name: string): Locator {
return this.page.locator('.file-list-item', { hasText: name }).filter({
- has: this.page.locator('.file-list-item-text', { hasText: name }),
+ has: this.page.locator('.file-list-item-name', { hasText: name }),
});
}
@@ -139,7 +139,7 @@ export class FileListPage {
const names: string[] = [];
for (let i = 0; i < count; i++) {
- const nameText = await items.nth(i).locator('.file-list-item-text').textContent();
+ const nameText = await items.nth(i).locator('.file-list-item-name').textContent();
if (nameText) {
names.push(nameText);
}
diff --git a/tests/e2e/page-objects/login.page.ts b/tests/e2e/page-objects/login.page.ts
index eec6c9797..b786c04c9 100644
--- a/tests/e2e/page-objects/login.page.ts
+++ b/tests/e2e/page-objects/login.page.ts
@@ -7,9 +7,10 @@ import { BasePage } from './base.page';
export class LoginPage extends BasePage {
/**
* Locator for the login button on the homepage
+ * Uses class selector for stability (text varies: [CONNECT] or connecting...)
*/
private get loginButton() {
- return this.page.getByRole('button', { name: /login|sign in/i });
+ return this.page.locator('button.login-button');
}
/**
diff --git a/tests/e2e/tests/full-workflow.spec.ts b/tests/e2e/tests/full-workflow.spec.ts
index a4c0b6411..811f5461e 100644
--- a/tests/e2e/tests/full-workflow.spec.ts
+++ b/tests/e2e/tests/full-workflow.spec.ts
@@ -289,6 +289,10 @@ test.describe.serial('Full Workflow', () => {
test('3.1 Upload files to root level (3 files)', async () => {
await navigateToRoot();
+ // Wait for file list to stabilize after navigation
+ // Ensure the workspace folder is visible (created in test 2.1)
+ await fileList.waitForItemToAppear(workspaceFolder, { timeout: 10000 });
+
for (const file of rootFiles) {
await uploadFile(file.name, file.content);
expect(await fileList.isItemVisible(file.name)).toBe(true);
@@ -587,8 +591,8 @@ test.describe.serial('Full Workflow', () => {
await logoutButton.click();
await expect(page).toHaveURL(/localhost:\d+\/?$/);
- await expect(
- page.getByRole('button', { name: /sign in|login|continue with google/i })
- ).toBeVisible({ timeout: 10000 });
+ await expect(page.getByRole('button', { name: /\[CONNECT\]|sign in|login/i })).toBeVisible({
+ timeout: 10000,
+ });
});
});
diff --git a/tests/e2e/utils/web3auth-helpers.ts b/tests/e2e/utils/web3auth-helpers.ts
index 2d15a344f..5ddc64b5d 100644
--- a/tests/e2e/utils/web3auth-helpers.ts
+++ b/tests/e2e/utils/web3auth-helpers.ts
@@ -58,8 +58,8 @@ export async function waitForWeb3AuthModal(
export async function loginViaEmail(page: Page, email: string, otp?: string): Promise
{
const otpCode = otp || TEST_CREDENTIALS.otp;
- // Click the Sign In button on the login page
- await page.click('button:has-text("Sign In")');
+ // Click the [CONNECT] button on the login page
+ await page.click('button.login-button');
// Wait for Web3Auth modal to appear
await waitForWeb3AuthModal(page);
@@ -151,5 +151,5 @@ export async function getStorageState(page: Page): Promise {
*/
export async function waitForAuthentication(page: Page): Promise {
// Wait for logout button to appear (indicator of authenticated state)
- await page.waitForSelector('button:has-text("Logout")', { timeout: 10000 });
+ await page.waitForSelector('button.logout-link', { timeout: 10000 });
}
diff --git a/tools/mock-ipns-routing/package.json b/tools/mock-ipns-routing/package.json
index 4e105cb20..307e3c74a 100644
--- a/tools/mock-ipns-routing/package.json
+++ b/tools/mock-ipns-routing/package.json
@@ -14,7 +14,7 @@
"pino-pretty": "^13.1.3"
},
"devDependencies": {
- "@types/node": "^22.0.0",
+ "@types/node": "^22.19.7",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}