style(ui): refactor component styling and remove hand-drawn aesthetic

Refactor the frontend styling to use consistent design tokens and remove the hand-drawn/rotated aesthetic in favor of a cleaner, more standard UI.

- Replace hardcoded colors and border radii with CSS variables (e.g., `--default-border-color`, `--border-radius-lg`).
- Remove `transform: rotate(...)` and manual `box-shadow` offsets from various components (Sidebar, Dashboard, TemplatePicker, etc.).
- Update `Dashboard` to use a standard `ProgressBar` instead of a hand-drawn SVG chart.
- Standardize font families to use `--ui-font`.
- Clean up `TemplatePicker` logic to properly handle element grouping.
- Remove stale test result files and update `.last-run.json`.
This commit is contained in:
Tomas Dvorak
2026-05-02 12:50:56 +02:00
parent 462a70933d
commit b79c214ad2
20 changed files with 215 additions and 1177 deletions
+5 -7
View File
@@ -10,15 +10,14 @@
/* Excalidraw Context Menu Styling Overrides */ /* Excalidraw Context Menu Styling Overrides */
:global(.excalidraw .context-menu) { :global(.excalidraw .context-menu) {
background: var(--island-bg-color) !important; background: var(--island-bg-color) !important;
border: 2px solid var(--color-gray-85) !important; border: 1px solid var(--default-border-color) !important;
border-radius: 2px !important; border-radius: var(--border-radius-lg) !important;
box-shadow: 4px 4px 0 var(--color-gray-85) !important; box-shadow: var(--shadow-island-stronger) !important;
transform: rotate(-0.2deg) !important;
padding: 2px !important; padding: 2px !important;
} }
:global(.excalidraw .context-menu-item) { :global(.excalidraw .context-menu-item) {
border-radius: 2px !important; border-radius: var(--border-radius-md) !important;
color: var(--color-gray-85) !important; color: var(--color-gray-85) !important;
font-weight: 500 !important; font-weight: 500 !important;
padding: 6px 12px !important; padding: 6px 12px !important;
@@ -27,11 +26,10 @@
:global(.excalidraw .context-menu-item:hover) { :global(.excalidraw .context-menu-item:hover) {
background: var(--color-primary-light) !important; background: var(--color-primary-light) !important;
color: var(--color-primary-darkest) !important; color: var(--color-primary-darkest) !important;
transform: translateX(1px) !important;
} }
:global(.excalidraw .context-menu-item-separator) { :global(.excalidraw .context-menu-item-separator) {
border-top: 2px solid var(--color-gray-30) !important; border-top: 1px solid var(--default-border-color) !important;
margin: 2px 4px !important; margin: 2px 4px !important;
} }
@@ -8,7 +8,7 @@
.sidebar { .sidebar {
width: var(--sidebar-width); width: var(--sidebar-width);
background: var(--island-bg-color); background: var(--island-bg-color);
border-right: 2px solid var(--color-gray-85); border-right: 1px solid var(--default-border-color);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: var(--space-4); padding: var(--space-4);
@@ -18,15 +18,6 @@
bottom: 0; bottom: 0;
z-index: 100; z-index: 100;
transition: transform var(--duration-normal) var(--ease-out); transition: transform var(--duration-normal) var(--ease-out);
box-shadow: 3px 0 0 var(--color-gray-85);
background-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 23px,
var(--color-gray-20) 23px,
var(--color-gray-20) 24px
);
@media (max-width: 768px) { @media (max-width: 768px) {
transform: translateX(-100%); transform: translateX(-100%);
@@ -81,17 +72,16 @@
} }
.logoImg { .logoImg {
width: 28px; width: auto;
height: 28px; height: 28px;
flex-shrink: 0; flex-shrink: 0;
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.1));
} }
.logoMark { .logoMark {
width: 32px; width: 32px;
height: 32px; height: 32px;
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 9px; border-radius: var(--border-radius-md);
color: var(--color-gray-85); color: var(--color-gray-85);
background: var(--color-primary-light); background: var(--color-primary-light);
display: inline-flex; display: inline-flex;
@@ -99,7 +89,6 @@
justify-content: center; justify-content: center;
font-size: var(--text-lg); font-size: var(--text-lg);
font-weight: 800; font-weight: 800;
transform: rotate(-4deg);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -141,25 +130,22 @@
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
color: var(--color-gray-70); color: var(--color-gray-70);
text-decoration: none; text-decoration: none;
border: 2px solid transparent; border: 1px solid transparent;
border-radius: 2px; border-radius: var(--border-radius-lg);
transition: all var(--duration-fast) var(--ease-out); transition: all var(--duration-fast) var(--ease-out);
font-weight: 500; font-weight: 500;
&:hover { &:hover {
background: var(--color-surface-low); background: var(--color-surface-low);
color: var(--color-on-surface); color: var(--color-on-surface);
border-color: var(--color-gray-30); border-color: var(--default-border-color);
transform: rotate(-0.5deg);
} }
&.active { &.active {
background: var(--color-surface-primary-container); background: var(--color-surface-primary-container);
color: var(--color-primary-darkest); color: var(--color-primary-darkest);
font-weight: 600; font-weight: 600;
border-color: var(--color-gray-85); border-color: var(--color-primary);
box-shadow: 2px 2px 0 var(--color-gray-85);
transform: rotate(-0.3deg);
} }
} }
@@ -236,8 +222,7 @@
.header { .header {
height: var(--header-height); height: var(--header-height);
background: var(--island-bg-color); background: var(--island-bg-color);
border-bottom: 2px solid var(--color-gray-85); border-bottom: 1px solid var(--default-border-color);
box-shadow: 0 3px 0 var(--color-gray-85);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -297,11 +282,11 @@
.iconButton { .iconButton {
position: relative; position: relative;
background: none; background: none;
border: 2px solid transparent; border: 1px solid transparent;
color: var(--color-gray-60); color: var(--color-gray-60);
cursor: pointer; cursor: pointer;
padding: var(--space-2); padding: var(--space-2);
border-radius: 2px; border-radius: var(--border-radius-lg);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -310,9 +295,7 @@
&:hover { &:hover {
color: var(--color-on-surface); color: var(--color-on-surface);
background: var(--color-surface-low); background: var(--color-surface-low);
border-color: var(--color-gray-30); border-color: var(--default-border-color);
box-shadow: 2px 2px 0 var(--color-gray-85);
transform: rotate(-1deg);
} }
} }
@@ -396,8 +379,8 @@
.nameModal { .nameModal {
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: var(--modal-shadow); box-shadow: var(--modal-shadow);
padding: var(--space-5); padding: var(--space-5);
width: 360px; width: 360px;
@@ -483,14 +466,13 @@
top: calc(100% + var(--space-2)); top: calc(100% + var(--space-2));
right: 100px; right: 100px;
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 5px 5px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
width: 320px; width: 320px;
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
z-index: 100; z-index: 100;
transform: rotate(-0.2deg);
} }
.notifHeader { .notifHeader {
@@ -498,14 +480,14 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
border-bottom: 2px solid var(--color-gray-85); border-bottom: 1px solid var(--default-border-color);
} }
.notifTitle { .notifTitle {
font-weight: 600; font-weight: 600;
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--color-gray-85); color: var(--color-gray-85);
font-family: 'Georgia', serif; font-family: var(--ui-font);
} }
.notifMarkAll { .notifMarkAll {
+1 -2
View File
@@ -41,10 +41,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ open, onClose }) => {
src="https://plus.excalidraw.com/images/logo.svg" src="https://plus.excalidraw.com/images/logo.svg"
alt="Excalidraw" alt="Excalidraw"
className={styles.logoImg} className={styles.logoImg}
width={28} width={120}
height={28} height={28}
/> />
<span className={styles.logoText}>Excalidraw</span>
</div> </div>
{onClose && ( {onClose && (
<button <button
@@ -13,15 +13,14 @@
.modal { .modal {
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 8px 8px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
width: 100%; width: 100%;
max-width: 720px; max-width: 720px;
max-height: 80vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
padding: var(--space-6); padding: var(--space-6);
transform: rotate(-0.1deg);
} }
.header { .header {
@@ -42,17 +41,16 @@
.closeBtn { .closeBtn {
background: none; background: none;
border: 2px solid transparent; border: 1px solid transparent;
cursor: pointer; cursor: pointer;
color: var(--color-gray-60); color: var(--color-gray-60);
padding: var(--space-2); padding: var(--space-2);
border-radius: 2px; border-radius: var(--border-radius-lg);
&:hover { &:hover {
border-color: var(--color-gray-85); border-color: var(--default-border-color);
color: var(--color-gray-90); color: var(--color-gray-90);
box-shadow: 2px 2px 0 var(--color-gray-85); background: var(--color-surface-low);
transform: rotate(-1deg);
} }
} }
@@ -69,21 +67,20 @@
text-align: center; text-align: center;
padding: var(--space-6) var(--space-4); padding: var(--space-6) var(--space-4);
cursor: pointer; cursor: pointer;
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
background: var(--island-bg-color); background: var(--island-bg-color);
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
transition: all var(--duration-fast); transition: all var(--duration-fast);
&:hover { &:hover {
border-color: var(--color-primary); border-color: var(--color-primary);
transform: translateY(-2px) rotate(-0.3deg); transform: translateY(-2px);
box-shadow: 4px 4px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
} }
&:active { &:active {
transform: translateY(0) rotate(0); transform: translateY(0);
box-shadow: 1px 1px 0 var(--color-gray-85);
} }
} }
@@ -21,7 +21,7 @@ interface TemplateOption {
elements: RawElement[]; elements: RawElement[];
} }
function makeHandDrawnRect(x: number, y: number, w: number, h: number, text?: string, groupId?: string) { function makeHandDrawnRect(x: number, y: number, w: number, h: number, groupId?: string) {
return { return {
id: `el-${Math.random().toString(36).slice(2)}`, id: `el-${Math.random().toString(36).slice(2)}`,
type: 'rectangle', type: 'rectangle',
@@ -41,14 +41,14 @@ function makeHandDrawnRect(x: number, y: number, w: number, h: number, text?: st
version: 2, version: 2,
versionNonce: Math.floor(Math.random() * 100000), versionNonce: Math.floor(Math.random() * 100000),
isDeleted: false, isDeleted: false,
boundElements: text ? [{ id: `txt-${Math.random().toString(36).slice(2)}`, type: 'text' }] : [], boundElements: [],
updated: Date.now(), updated: Date.now(),
link: null, link: null,
locked: false, locked: false,
}; };
} }
function makeText(x: number, y: number, text: string, fontSize = 20) { function makeText(x: number, y: number, text: string, fontSize = 20, groupId?: string) {
return { return {
id: `txt-${Math.random().toString(36).slice(2)}`, id: `txt-${Math.random().toString(36).slice(2)}`,
type: 'text', type: 'text',
@@ -61,7 +61,7 @@ function makeText(x: number, y: number, text: string, fontSize = 20) {
strokeStyle: 'solid', strokeStyle: 'solid',
roughness: 1, roughness: 1,
opacity: 100, opacity: 100,
groupIds: [], groupIds: groupId ? [groupId] : [],
frameId: null, frameId: null,
roundness: null, roundness: null,
seed: Math.floor(Math.random() * 10000), seed: Math.floor(Math.random() * 10000),
@@ -77,7 +77,7 @@ function makeText(x: number, y: number, text: string, fontSize = 20) {
fontFamily: 1, fontFamily: 1,
textAlign: 'left', textAlign: 'left',
verticalAlign: 'top', verticalAlign: 'top',
baseline: 18, baseline: Math.round(fontSize * 0.7),
containerId: null, containerId: null,
originalText: text, originalText: text,
lineHeight: 1.25, lineHeight: 1.25,
@@ -208,14 +208,14 @@ export const BUILTIN_TEMPLATES: Record<PickedTemplate, RawElement[]> = {
makeText(285, 120, 'Doing', 20), makeText(285, 120, 'Doing', 20),
makeText(495, 120, 'Done', 20), makeText(495, 120, 'Done', 20),
// Card 1 - grouped // Card 1 - grouped
makeHandDrawnRect(70, 170, 140, 70, undefined, 'card1'), makeHandDrawnRect(70, 170, 140, 70, 'card1'),
makeText(85, 190, 'User research', 16), makeText(85, 190, 'User research', 16, 'card1'),
// Card 2 - grouped // Card 2 - grouped
makeHandDrawnRect(280, 170, 140, 70, undefined, 'card2'), makeHandDrawnRect(280, 170, 140, 70, 'card2'),
makeText(295, 190, 'Sketch flow', 16), makeText(295, 190, 'Sketch flow', 16, 'card2'),
// Card 3 - grouped // Card 3 - grouped
makeHandDrawnRect(490, 170, 140, 70, undefined, 'card3'), makeHandDrawnRect(490, 170, 140, 70, 'card3'),
makeText(505, 190, 'Project brief', 16), makeText(505, 190, 'Project brief', 16, 'card3'),
// Add card buttons per column // Add card buttons per column
makeAddButton(110, 380, '+', 'kanban-add-backlog'), makeAddButton(110, 380, '+', 'kanban-add-backlog'),
makeAddButton(320, 380, '+', 'kanban-add-doing'), makeAddButton(320, 380, '+', 'kanban-add-doing'),
@@ -374,13 +374,13 @@ export const BUILTIN_TEMPLATES: Record<PickedTemplate, RawElement[]> = {
makeText(50, 30, 'Project Timeline', 30), makeText(50, 30, 'Project Timeline', 30),
makeHandDrawnRect(50, 90, 600, 4), makeHandDrawnRect(50, 90, 600, 4),
// Milestones // Milestones
makeHandDrawnRect(80, 70, 20, 44, undefined, 'milestone-1'), makeHandDrawnRect(80, 70, 20, 44, 'milestone-1'),
makeText(60, 125, 'Q1 Kickoff', 14), makeText(60, 125, 'Q1 Kickoff', 14),
makeHandDrawnRect(220, 70, 20, 44, undefined, 'milestone-2'), makeHandDrawnRect(220, 70, 20, 44, 'milestone-2'),
makeText(200, 125, 'Design', 14), makeText(200, 125, 'Design', 14),
makeHandDrawnRect(360, 70, 20, 44, undefined, 'milestone-3'), makeHandDrawnRect(360, 70, 20, 44, 'milestone-3'),
makeText(340, 125, 'Build', 14), makeText(340, 125, 'Build', 14),
makeHandDrawnRect(500, 70, 20, 44, undefined, 'milestone-4'), makeHandDrawnRect(500, 70, 20, 44, 'milestone-4'),
makeText(480, 125, 'Launch', 14), makeText(480, 125, 'Launch', 14),
// Tasks below timeline // Tasks below timeline
makeHandDrawnRect(50, 170, 130, 50), makeHandDrawnRect(50, 170, 130, 50),
@@ -13,17 +13,15 @@
gap: var(--space-6); gap: var(--space-6);
padding: var(--space-5) var(--space-6); padding: var(--space-5) var(--space-6);
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 4px 4px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
transform: rotate(-0.3deg);
h1 { h1 {
font-size: var(--text-3xl); font-size: var(--text-3xl);
font-weight: 700; font-weight: 700;
color: var(--color-gray-85); color: var(--color-gray-85);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
font-family: 'Georgia', serif; font-family: var(--ui-font);
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
} }
@@ -84,21 +82,14 @@
} }
.statCardWrapper { .statCardWrapper {
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
transform: rotate(0.15deg); transition: box-shadow 0.15s ease;
transition: transform 0.15s ease, box-shadow 0.15s ease;
&:hover { &:hover {
transform: rotate(0) translate(-1px, -1px); box-shadow: var(--shadow-island-stronger);
box-shadow: 5px 5px 0 var(--color-gray-85);
} }
&:nth-child(2) { transform: rotate(-0.1deg); }
&:nth-child(3) { transform: rotate(0.25deg); }
&:nth-child(4) { transform: rotate(-0.2deg); }
&:nth-child(5) { transform: rotate(0.05deg); }
} }
.statCard { .statCard {
@@ -121,21 +112,12 @@
.statIcon { .statIcon {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: var(--border-radius-lg);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--color-primary-light); background: var(--color-primary-light);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
box-shadow: 2px 2px 0 var(--color-gray-85);
transform: rotate(-2deg);
}
.handChart {
width: 80px;
height: 40px;
flex-shrink: 0;
transform: rotate(1deg);
} }
.sparkline { .sparkline {
@@ -148,7 +130,7 @@
font-size: var(--text-3xl); font-size: var(--text-3xl);
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
font-family: 'Georgia', serif; font-family: var(--ui-font);
} }
.statLabel { .statLabel {
@@ -157,7 +139,7 @@
margin-top: var(--space-1); margin-top: var(--space-1);
} }
.chartBarWrap { .progressBarWrap {
position: relative; position: relative;
width: 100%; width: 100%;
height: 6px; height: 6px;
@@ -166,18 +148,17 @@
overflow: hidden; overflow: hidden;
} }
.chartBarBg { .progressBarBg {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: var(--color-gray-20); background: var(--color-gray-20);
border-radius: var(--border-radius-full); border-radius: var(--border-radius-full);
} }
.chartBar { .progressBarFill {
position: absolute; position: absolute;
inset: 0; inset: 0;
border-radius: var(--border-radius-full); border-radius: var(--border-radius-full);
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-darkest));
transition: width 0.4s var(--ease-out); transition: width 0.4s var(--ease-out);
} }
@@ -230,22 +211,21 @@
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-3) var(--space-2); padding: var(--space-3) var(--space-2);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
background: var(--island-bg-color); background: var(--island-bg-color);
&:hover { &:hover {
border-color: var(--color-primary); border-color: var(--color-primary);
background: var(--color-surface-low); background: var(--color-surface-low);
transform: translateX(2px) rotate(-0.3deg); transform: translateX(2px);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
} }
&:last-child { &:last-child {
border-bottom: 2px solid var(--color-gray-30);
margin-bottom: 0; margin-bottom: 0;
} }
} }
@@ -253,12 +233,12 @@
.drawingThumb { .drawingThumb {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 2px; border-radius: var(--border-radius-lg);
overflow: hidden; overflow: hidden;
background: var(--color-surface-low); background: var(--color-surface-low);
flex-shrink: 0; flex-shrink: 0;
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
img { img {
width: 100%; width: 100%;
@@ -359,7 +339,7 @@
.activityAvatar { .activityAvatar {
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 2px; border-radius: var(--border-radius-lg);
background: var(--color-primary); background: var(--color-primary);
color: white; color: white;
display: flex; display: flex;
@@ -368,8 +348,8 @@
font-size: var(--text-xs); font-size: var(--text-xs);
font-weight: 600; font-weight: 600;
flex-shrink: 0; flex-shrink: 0;
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
} }
.activityInfo { .activityInfo {
@@ -399,12 +379,11 @@
.modal { .modal {
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 5px 5px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
width: 420px; width: 420px;
max-width: 90vw; max-width: 90vw;
transform: rotate(-0.3deg);
} }
.modalHeader { .modalHeader {
@@ -412,12 +391,12 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: var(--space-4) var(--space-5); padding: var(--space-4) var(--space-5);
border-bottom: 2px solid var(--color-gray-85); border-bottom: 1px solid var(--default-border-color);
h3 { h3 {
margin: 0; margin: 0;
font-size: var(--text-lg); font-size: var(--text-lg);
color: var(--color-gray-85); color: var(--color-gray-85);
font-family: 'Georgia', serif; font-family: var(--ui-font);
} }
} }
@@ -444,15 +423,15 @@
.modalInput { .modalInput {
width: 100%; width: 100%;
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
background: var(--input-bg-color); background: var(--input-bg-color);
color: var(--color-on-surface); color: var(--color-on-surface);
font-size: var(--text-sm); font-size: var(--text-sm);
outline: none; outline: none;
&:focus { &:focus {
border-color: var(--color-primary); border-color: var(--color-primary);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
} }
} }
@@ -465,25 +444,25 @@
.modalBtnSecondary { .modalBtnSecondary {
padding: var(--space-2) var(--space-4); padding: var(--space-2) var(--space-4);
border-radius: 2px; border-radius: var(--border-radius-lg);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
background: transparent; background: transparent;
color: var(--color-gray-70); color: var(--color-gray-70);
font-size: var(--text-sm); font-size: var(--text-sm);
cursor: pointer; cursor: pointer;
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
&:hover { background: var(--color-surface-low); transform: rotate(-0.5deg); } &:hover { background: var(--color-surface-low); }
} }
.modalBtnPrimary { .modalBtnPrimary {
padding: var(--space-2) var(--space-4); padding: var(--space-2) var(--space-4);
border-radius: 2px; border-radius: var(--border-radius-lg);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
background: var(--color-primary); background: var(--color-primary);
color: white; color: white;
font-size: var(--text-sm); font-size: var(--text-sm);
cursor: pointer; cursor: pointer;
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
&:hover { background: var(--color-primary-darker); transform: rotate(-0.5deg); } &:hover { background: var(--color-primary-darker); }
&:disabled { opacity: 0.6; cursor: not-allowed; } &:disabled { opacity: 0.6; cursor: not-allowed; }
} }
+10 -91
View File
@@ -9,51 +9,13 @@ import styles from './Dashboard.module.scss';
const ACTIVITY_LIMIT = 5; const ACTIVITY_LIMIT = 5;
const HandDrawnChart: React.FC<{ value: number; max: number; color?: string }> = ({ value, max, color = '#6965db' }) => { const ProgressBar: React.FC<{ value: number; max: number; color?: string }> = ({ value, max, color = '#6965db' }) => {
const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0; const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0;
const w = 120;
const h = 60;
const pad = 6;
const barW = ((w - pad * 2) * pct) / 100;
const roughness = 1.2;
const r = () => (Math.random() - 0.5) * roughness;
return ( return (
<svg className={styles.handChart} viewBox={`0 0 ${w} ${h}`} aria-hidden="true"> <div className={styles.progressBarWrap} aria-hidden="true">
<path <div className={styles.progressBarBg} />
d={`M${pad + r()},${pad + r()} L${w - pad + r()},${pad + r()} L${w - pad + r()},${h - pad + r()} L${pad + r()},${h - pad + r()} Z`} <div className={styles.progressBarFill} style={{ width: `${pct}%`, background: color }} />
fill="none" </div>
stroke="var(--color-gray-40)"
strokeWidth="1"
strokeLinecap="round"
/>
{pct > 0 && (
<path
d={`M${pad + r()},${h - pad + r()} L${pad + r()},${pad + r()} L${pad + barW + r()},${pad + r()} L${pad + barW + r()},${h - pad + r()} Z`}
fill={color}
stroke={color}
strokeWidth="1"
opacity="0.35"
strokeLinecap="round"
/>
)}
<path
d={`M${pad + r()},${h - pad + r()} L${pad + barW + r()},${h - pad + r()}`}
fill="none"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
/>
<path
d={`M${pad + r()},${pad + r()} L${pad + barW + r()},${pad + r()}`}
fill="none"
stroke={color}
strokeWidth="1"
strokeLinecap="round"
opacity="0.5"
/>
</svg>
); );
}; };
@@ -69,7 +31,7 @@ const MiniSparkline: React.FC<{ data: number[]; color?: string }> = ({ data, col
const points = data.map((v, i) => { const points = data.map((v, i) => {
const x = i * stepX; const x = i * stepX;
const y = h - ((v - min) / range) * (h - 4) - 2; const y = h - ((v - min) / range) * (h - 4) - 2;
return `${x + (Math.random() - 0.5) * 0.8},${y + (Math.random() - 0.5) * 0.8}`; return `${x},${y}`;
}).join(' '); }).join(' ');
return ( return (
@@ -81,13 +43,8 @@ const MiniSparkline: React.FC<{ data: number[]; color?: string }> = ({ data, col
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
points={points} points={points}
opacity="0.7" opacity="0.5"
/> />
{data.map((v, i) => {
const x = i * stepX;
const y = h - ((v - min) / range) * (h - 4) - 2;
return <circle key={i} cx={x} cy={y} r="2" fill={color} opacity="0.5" />;
})}
</svg> </svg>
); );
}; };
@@ -98,8 +55,6 @@ export const Dashboard: React.FC = () => {
const { recentDrawings, setRecentDrawings, activity, setActivity } = useDrawingStore(); const { recentDrawings, setRecentDrawings, activity, setActivity } = useDrawingStore();
const { user } = useAuthStore(); const { user } = useAuthStore();
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [showNameModal, setShowNameModal] = useState(false);
const [newDrawingName, setNewDrawingName] = useState('');
const [statsData, setStatsData] = useState({ const [statsData, setStatsData] = useState({
teams: 0, teams: 0,
members: 0, members: 0,
@@ -130,18 +85,11 @@ export const Dashboard: React.FC = () => {
loadData(); loadData();
}, [setRecentDrawings, setActivity]); }, [setRecentDrawings, setActivity]);
const handleCreateDrawing = () => { const handleCreateDrawing = async () => {
setNewDrawingName('');
setShowNameModal(true);
};
const confirmCreateDrawing = async () => {
const title = newDrawingName.trim() || 'Untitled Drawing';
setIsCreating(true); setIsCreating(true);
setShowNameModal(false);
try { try {
const newDrawing = await api.drawings.create({ const newDrawing = await api.drawings.create({
title, title: 'Untitled Drawing',
visibility: 'team', visibility: 'team',
}); });
setRecentDrawings([newDrawing, ...recentDrawings]); setRecentDrawings([newDrawing, ...recentDrawings]);
@@ -231,7 +179,7 @@ export const Dashboard: React.FC = () => {
<div className={styles.statIcon} style={{ color: stat.color, borderColor: stat.color }}> <div className={styles.statIcon} style={{ color: stat.color, borderColor: stat.color }}>
<stat.icon size={22} /> <stat.icon size={22} />
</div> </div>
<HandDrawnChart value={stat.chartValue} max={stat.max} color={stat.color} /> <ProgressBar value={stat.chartValue} max={stat.max} color={stat.color} />
</div> </div>
<div className={styles.statValue} style={{ color: stat.color }}>{stat.value}</div> <div className={styles.statValue} style={{ color: stat.color }}>{stat.value}</div>
<div className={styles.statLabel}>{stat.label}</div> <div className={styles.statLabel}>{stat.label}</div>
@@ -342,35 +290,6 @@ export const Dashboard: React.FC = () => {
</div> </div>
</div> </div>
{showNameModal && (
<div className={styles.modalOverlay} role="dialog" aria-modal="true" aria-labelledby="new-drawing-title" onClick={(e) => { if (e.target === e.currentTarget) setShowNameModal(false); }}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h3 id="new-drawing-title">New Drawing</h3>
<button className={styles.modalClose} onClick={() => setShowNameModal(false)} aria-label="Close">&times;</button>
</div>
<div className={styles.modalBody}>
<label htmlFor="drawing-name">Name</label>
<input
id="drawing-name"
type="text"
autoFocus
placeholder="Untitled Drawing"
value={newDrawingName}
onChange={(e) => setNewDrawingName(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') confirmCreateDrawing(); if (e.key === 'Escape') setShowNameModal(false); }}
className={styles.modalInput}
/>
</div>
<div className={styles.modalFooter}>
<button className={styles.modalBtnSecondary} onClick={() => setShowNameModal(false)}>Cancel</button>
<button className={styles.modalBtnPrimary} onClick={confirmCreateDrawing} disabled={isCreating}>
{isCreating ? <Loader2 size={16} className={styles.spinner} /> : 'Create'}
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };
+6 -7
View File
@@ -407,18 +407,17 @@
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
padding: var(--space-2) var(--space-4); padding: var(--space-2) var(--space-4);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
transform: rotate(-0.3deg);
} }
.presentationLabel { .presentationLabel {
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--color-gray-70); color: var(--color-gray-70);
font-weight: 500; font-weight: 500;
font-family: 'Georgia', serif; font-family: var(--ui-font);
} }
.modalOverlay { .modalOverlay {
@@ -433,8 +432,8 @@
.modal { .modal {
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: var(--modal-shadow); box-shadow: var(--modal-shadow);
width: 420px; width: 420px;
max-width: 90vw; max-width: 90vw;
+11
View File
@@ -34,8 +34,15 @@ interface EditorState {
function prepareElementsForImport(sourceElements: LooseElement[], offsetX: number, offsetY: number): LooseElement[] { function prepareElementsForImport(sourceElements: LooseElement[], offsetX: number, offsetY: number): LooseElement[] {
if (!sourceElements || !sourceElements.length) return []; if (!sourceElements || !sourceElements.length) return [];
const idMap = new Map<string, string>(); const idMap = new Map<string, string>();
const groupIdMap = new Map<string, string>();
sourceElements.forEach((el) => { sourceElements.forEach((el) => {
idMap.set(el.id as string, `${el.type}-${Math.random().toString(36).slice(2, 9)}`); idMap.set(el.id as string, `${el.type}-${Math.random().toString(36).slice(2, 9)}`);
const gids = ((el as { groupIds?: string[] }).groupIds) || [];
gids.forEach((gid) => {
if (!groupIdMap.has(gid)) {
groupIdMap.set(gid, `group-${Math.random().toString(36).slice(2, 9)}`);
}
});
}); });
return sourceElements.map((el) => { return sourceElements.map((el) => {
const newEl: LooseElement = { ...el }; const newEl: LooseElement = { ...el };
@@ -55,6 +62,10 @@ function prepareElementsForImport(sourceElements: LooseElement[], offsetX: numbe
if (newEl.containerId && idMap.has(newEl.containerId as string)) { if (newEl.containerId && idMap.has(newEl.containerId as string)) {
newEl.containerId = idMap.get(newEl.containerId as string); newEl.containerId = idMap.get(newEl.containerId as string);
} }
const gids = (newEl as { groupIds?: string[] }).groupIds;
if (gids && gids.length) {
(newEl as { groupIds?: string[] }).groupIds = gids.map((gid) => groupIdMap.get(gid) || gid);
}
return newEl; return newEl;
}); });
} }
@@ -18,10 +18,9 @@
flex-wrap: wrap; flex-wrap: wrap;
padding: var(--space-5); padding: var(--space-5);
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 4px 4px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
transform: rotate(0.2deg);
@media (max-width: 640px) { @media (max-width: 640px) {
flex-direction: column; flex-direction: column;
@@ -115,9 +114,9 @@
width: 240px; width: 240px;
flex-shrink: 0; flex-shrink: 0;
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
padding: var(--space-3); padding: var(--space-3);
align-self: flex-start; align-self: flex-start;
@@ -140,12 +139,12 @@
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
border-radius: 2px; border-radius: var(--border-radius-lg);
color: var(--color-gray-70); color: var(--color-gray-70);
cursor: pointer; cursor: pointer;
transition: all var(--duration-fast) var(--ease-out); transition: all var(--duration-fast) var(--ease-out);
background: none; background: none;
border: 2px solid transparent; border: 1px solid transparent;
width: 100%; width: 100%;
text-align: left; text-align: left;
font-size: var(--text-sm); font-size: var(--text-sm);
@@ -153,17 +152,14 @@
&:hover { &:hover {
background: var(--color-surface-low); background: var(--color-surface-low);
color: var(--color-on-surface); color: var(--color-on-surface);
border-color: var(--color-gray-30); border-color: var(--default-border-color);
transform: rotate(-0.3deg);
} }
&.folderActive { &.folderActive {
background: var(--color-surface-primary-container); background: var(--color-surface-primary-container);
color: var(--color-primary-darkest); color: var(--color-primary-darkest);
font-weight: 600; font-weight: 600;
border-color: var(--color-gray-85); border-color: var(--color-primary);
box-shadow: 2px 2px 0 var(--color-gray-85);
transform: rotate(-0.2deg);
} }
svg { svg {
@@ -228,15 +224,13 @@
.drawingCard { .drawingCard {
position: relative; position: relative;
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
transform: rotate(0.1deg); transition: box-shadow 0.15s ease;
transition: transform 0.15s ease, box-shadow 0.15s ease;
&:hover { &:hover {
transform: rotate(0) translate(-1px, -1px); box-shadow: var(--shadow-island-stronger);
box-shadow: 5px 5px 0 var(--color-gray-85);
} }
} }
@@ -312,9 +306,9 @@
top: calc(100% + var(--space-1)); top: calc(100% + var(--space-1));
right: 0; right: 0;
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
min-width: 160px; min-width: 160px;
z-index: 10; z-index: 10;
display: flex; display: flex;
@@ -328,7 +322,7 @@
text-align: left; text-align: left;
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
cursor: pointer; cursor: pointer;
border-radius: var(--border-radius-sm); border-radius: var(--border-radius-md);
color: var(--color-on-surface); color: var(--color-on-surface);
font-size: var(--text-sm); font-size: var(--text-sm);
@@ -371,58 +365,55 @@
flex-wrap: wrap; flex-wrap: wrap;
padding: var(--space-3); padding: var(--space-3);
background: var(--color-surface-low); background: var(--color-surface-low);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
} }
.newProjectInput { .newProjectInput {
flex: 1; flex: 1;
min-width: 120px; min-width: 120px;
background: var(--input-bg-color); background: var(--input-bg-color);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
color: var(--text-primary-color); color: var(--text-primary-color);
font-size: var(--text-sm);
&:focus { &:focus {
outline: none; outline: none;
border-color: var(--color-primary); border-color: var(--color-primary);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
} }
} }
.newProjectBtn { .newProjectBtn {
background: var(--color-primary); background: var(--color-primary);
color: #fff; color: #fff;
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
cursor: pointer; cursor: pointer;
font-size: var(--text-sm); font-size: var(--text-sm);
font-weight: 500; font-weight: 500;
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
&:hover { &:hover {
background: var(--color-primary-darkest); background: var(--color-primary-darkest);
transform: rotate(-0.5deg);
} }
} }
.newProjectBtnCancel { .newProjectBtnCancel {
background: none; background: none;
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
cursor: pointer; cursor: pointer;
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--color-on-surface); color: var(--color-on-surface);
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
&:hover { &:hover {
background: var(--color-surface-low); background: var(--color-surface-low);
transform: rotate(-0.5deg);
} }
} }
@@ -466,12 +457,11 @@
.modal { .modal {
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 5px 5px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
width: 420px; width: 420px;
max-width: 90vw; max-width: 90vw;
transform: rotate(-0.3deg);
} }
.modalHeader { .modalHeader {
@@ -479,13 +469,13 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: var(--space-4) var(--space-5); padding: var(--space-4) var(--space-5);
border-bottom: 2px solid var(--color-gray-85); border-bottom: 1px solid var(--default-border-color);
h3 { h3 {
margin: 0; margin: 0;
font-size: var(--text-lg); font-size: var(--text-lg);
color: var(--color-gray-85); color: var(--color-gray-85);
font-family: 'Georgia', serif; font-family: var(--ui-font);
} }
} }
@@ -514,8 +504,8 @@
.modalInput { .modalInput {
width: 100%; width: 100%;
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
background: var(--input-bg-color); background: var(--input-bg-color);
color: var(--color-on-surface); color: var(--color-on-surface);
font-size: var(--text-sm); font-size: var(--text-sm);
@@ -523,7 +513,7 @@
&:focus { &:focus {
border-color: var(--color-primary); border-color: var(--color-primary);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
} }
} }
@@ -536,27 +526,27 @@
.modalBtnSecondary { .modalBtnSecondary {
padding: var(--space-2) var(--space-4); padding: var(--space-2) var(--space-4);
border-radius: 2px; border-radius: var(--border-radius-lg);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
background: transparent; background: transparent;
color: var(--color-gray-70); color: var(--color-gray-70);
font-size: var(--text-sm); font-size: var(--text-sm);
cursor: pointer; cursor: pointer;
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
&:hover { background: var(--color-surface-low); transform: rotate(-0.5deg); } &:hover { background: var(--color-surface-low); }
} }
.modalBtnPrimary { .modalBtnPrimary {
padding: var(--space-2) var(--space-4); padding: var(--space-2) var(--space-4);
border-radius: 2px; border-radius: var(--border-radius-lg);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
background: var(--color-primary); background: var(--color-primary);
color: white; color: white;
font-size: var(--text-sm); font-size: var(--text-sm);
cursor: pointer; cursor: pointer;
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
&:hover { background: var(--color-primary-darker); transform: rotate(-0.5deg); } &:hover { background: var(--color-primary-darker); }
&:disabled { opacity: 0.6; cursor: not-allowed; } &:disabled { opacity: 0.6; cursor: not-allowed; }
} }
@@ -9,17 +9,16 @@
margin-bottom: var(--space-8); margin-bottom: var(--space-8);
padding: var(--space-5); padding: var(--space-5);
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 4px 4px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
transform: rotate(0.1deg);
h1 { h1 {
font-size: var(--text-3xl); font-size: var(--text-3xl);
font-weight: 700; font-weight: 700;
color: var(--color-gray-85); color: var(--color-gray-85);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
font-family: 'Georgia', serif; font-family: var(--ui-font);
} }
} }
@@ -45,10 +44,10 @@
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
border-radius: 2px; border-radius: var(--border-radius-lg);
color: var(--color-gray-70); color: var(--color-gray-70);
background: none; background: none;
border: 2px solid transparent; border: 1px solid transparent;
cursor: pointer; cursor: pointer;
font-size: var(--text-sm); font-size: var(--text-sm);
transition: all var(--duration-fast) var(--ease-out); transition: all var(--duration-fast) var(--ease-out);
@@ -57,17 +56,14 @@
&:hover { &:hover {
background: var(--color-surface-low); background: var(--color-surface-low);
color: var(--color-on-surface); color: var(--color-on-surface);
border-color: var(--color-gray-30); border-color: var(--default-border-color);
transform: rotate(-0.2deg);
} }
&.active { &.active {
background: var(--color-surface-primary-container); background: var(--color-surface-primary-container);
color: var(--color-primary-darkest); color: var(--color-primary-darkest);
font-weight: 600; font-weight: 600;
border-color: var(--color-gray-85); border-color: var(--color-primary);
box-shadow: 2px 2px 0 var(--color-gray-85);
transform: rotate(-0.1deg);
} }
} }
@@ -96,8 +92,8 @@
font-size: var(--text-2xl); font-size: var(--text-2xl);
font-weight: 700; font-weight: 700;
overflow: hidden; overflow: hidden;
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
img { img {
width: 100%; width: 100%;
@@ -151,26 +147,24 @@
.themeOption { .themeOption {
padding: var(--space-2) var(--space-4); padding: var(--space-2) var(--space-4);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
background: var(--island-bg-color); background: var(--island-bg-color);
color: var(--color-gray-70); color: var(--color-gray-70);
font-size: var(--text-sm); font-size: var(--text-sm);
cursor: pointer; cursor: pointer;
transition: all var(--duration-fast) var(--ease-out); transition: all var(--duration-fast) var(--ease-out);
box-shadow: 2px 2px 0 var(--color-gray-30); box-shadow: var(--shadow-island);
&:hover { &:hover {
border-color: var(--color-primary); border-color: var(--color-primary);
color: var(--color-primary); color: var(--color-primary);
transform: translate(-1px, -1px); box-shadow: var(--shadow-island-stronger);
box-shadow: 3px 3px 0 var(--color-primary);
} }
&.active { &.active {
background: var(--color-primary); background: var(--color-primary);
border-color: var(--color-gray-85); border-color: var(--color-primary);
color: white; color: white;
box-shadow: 2px 2px 0 var(--color-gray-85);
} }
} }
+18 -22
View File
@@ -9,17 +9,16 @@
margin-bottom: var(--space-8); margin-bottom: var(--space-8);
padding: var(--space-5); padding: var(--space-5);
background: var(--island-bg-color); background: var(--island-bg-color);
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 4px 4px 0 var(--color-gray-85); box-shadow: var(--shadow-island-stronger);
transform: rotate(-0.2deg);
h1 { h1 {
font-size: var(--text-3xl); font-size: var(--text-3xl);
font-weight: 700; font-weight: 700;
color: var(--color-gray-85); color: var(--color-gray-85);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
font-family: 'Georgia', serif; font-family: var(--ui-font);
} }
} }
@@ -68,21 +67,20 @@
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-3) var(--space-2); padding: var(--space-3) var(--space-2);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
transition: all 0.15s ease; transition: all 0.15s ease;
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
background: var(--island-bg-color); background: var(--island-bg-color);
&:hover { &:hover {
border-color: var(--color-primary); border-color: var(--color-primary);
background: var(--color-surface-low); background: var(--color-surface-low);
transform: translateX(2px) rotate(-0.2deg); box-shadow: var(--shadow-island-stronger);
box-shadow: 3px 3px 0 var(--color-gray-85);
} }
&:last-child { &:last-child {
border-bottom: 2px solid var(--color-gray-30); border-bottom: 1px solid var(--default-border-color);
margin-bottom: 0; margin-bottom: 0;
} }
} }
@@ -98,8 +96,8 @@
justify-content: center; justify-content: center;
font-weight: 700; font-weight: 700;
flex-shrink: 0; flex-shrink: 0;
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
box-shadow: 2px 2px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
} }
.memberInfo { .memberInfo {
@@ -122,13 +120,12 @@
gap: var(--space-1); gap: var(--space-1);
padding: var(--space-1) var(--space-3); padding: var(--space-1) var(--space-3);
background: var(--color-surface-low); background: var(--color-surface-low);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
font-size: var(--text-xs); font-size: var(--text-xs);
font-weight: 500; font-weight: 500;
color: var(--color-gray-70); color: var(--color-gray-70);
text-transform: capitalize; text-transform: capitalize;
box-shadow: 1px 1px 0 var(--color-gray-85);
} }
.inviteForm { .inviteForm {
@@ -139,15 +136,15 @@
.inviteInput { .inviteInput {
padding: var(--space-3); padding: var(--space-3);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
font-size: var(--text-sm); font-size: var(--text-sm);
background: var(--input-bg-color); background: var(--input-bg-color);
&:focus { &:focus {
outline: none; outline: none;
border-color: var(--color-primary); border-color: var(--color-primary);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
} }
} }
@@ -187,12 +184,11 @@
.roleSelect { .roleSelect {
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
border: 2px solid var(--color-gray-30); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
font-size: var(--text-sm); font-size: var(--text-sm);
background: var(--input-bg-color); background: var(--input-bg-color);
cursor: pointer; cursor: pointer;
box-shadow: 1px 1px 0 var(--color-gray-85);
} }
.error { .error {
@@ -78,15 +78,13 @@
.templateCard { .templateCard {
overflow: hidden; overflow: hidden;
border: 2px solid var(--color-gray-85); border: 1px solid var(--default-border-color);
border-radius: 2px; border-radius: var(--border-radius-lg);
box-shadow: 3px 3px 0 var(--color-gray-85); box-shadow: var(--shadow-island);
transform: rotate(0.1deg); transition: box-shadow 0.15s ease;
transition: transform 0.15s ease, box-shadow 0.15s ease;
&:hover { &:hover {
transform: rotate(0) translate(-1px, -1px); box-shadow: var(--shadow-island-stronger);
box-shadow: 5px 5px 0 var(--color-gray-85);
} }
} }
+7 -7
View File
@@ -122,17 +122,17 @@ a {
// ============================================ // ============================================
.excalidraw { .excalidraw {
--border-radius-md: 2px; --border-radius-md: var(--border-radius-lg);
.context-menu { .context-menu {
border: 2px solid var(--color-gray-85) !important; border: 1px solid var(--default-border-color) !important;
border-radius: 2px !important; border-radius: var(--border-radius-lg) !important;
box-shadow: 3px 3px 0 var(--color-gray-85) !important; box-shadow: var(--shadow-island) !important;
} }
.library-menu-items-container { .library-menu-items-container {
border: 2px solid var(--color-gray-85) !important; border: 1px solid var(--default-border-color) !important;
border-radius: 2px !important; border-radius: var(--border-radius-lg) !important;
box-shadow: 3px 3px 0 var(--color-gray-85) !important; box-shadow: var(--shadow-island) !important;
} }
} }
+1 -8
View File
@@ -1,11 +1,4 @@
{ {
"status": "failed", "status": "failed",
"failedTests": [ "failedTests": []
"c31ff144dc4fee3acd0a-bec551c658216ec9862a",
"c31ff144dc4fee3acd0a-f87315abf5d197970540",
"c31ff144dc4fee3acd0a-fc5e81ebcffdb7687b8e",
"c31ff144dc4fee3acd0a-989f5dcca4211fe0b2e4",
"c31ff144dc4fee3acd0a-ac5aa3cfe7537125a151",
"c31ff144dc4fee3acd0a-7f990aaafdc09c3794e8"
]
} }
@@ -1,162 +0,0 @@
# Test info
- Name: dashboard >> shows stats cards
- Location: /home/tdvorak/Desktop/PROG+HTML/Excalidraw/frontend/e2e/app.spec.ts:45:3
# Error details
```
Error: Error reading storage state from playwright/.auth/state.json:
ENOENT: no such file or directory, open 'playwright/.auth/state.json'
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | const BASE = 'http://localhost:3456';
4 |
5 | // Auth: first-run signup, blocked signup, login
6 | test.describe.serial('auth flow', () => {
7 | test.use({ storageState: { cookies: [], origins: [] } });
8 |
9 | test('redirects to signup when no users exist', async ({ page }) => {
10 | await page.goto(BASE + '/');
11 | await expect(page).toHaveURL(/\/signup$/);
12 | await expect(page.getByRole('heading', { name: 'Create account' })).toBeVisible();
13 | });
14 |
15 | test('first user can signup', async ({ page }) => {
16 | await page.goto(BASE + '/signup');
17 | await page.getByLabel('Full Name').fill('E2E User');
18 | await page.getByLabel('Email').fill('e2e@test.com');
19 | await page.getByLabel('Password').fill('e2e-password-123');
20 | await page.getByRole('button', { name: 'Create Account' }).click();
21 | await expect(page).toHaveURL(BASE + '/');
22 | await expect(page.getByText(/Welcome back/)).toBeVisible();
23 | await page.context().storageState({ path: 'playwright/.auth/state.json' });
24 | });
25 |
26 | test('blocks second signup when users exist', async ({ page }) => {
27 | await page.goto(BASE + '/signup');
28 | await expect(page).toHaveURL(/\/login$/);
29 | });
30 |
31 | test('existing user can login', async ({ page }) => {
32 | await page.goto(BASE + '/login');
33 | await page.getByLabel('Email').fill('e2e@test.com');
34 | await page.getByLabel('Password').fill('e2e-password-123');
35 | await page.getByRole('button', { name: 'Sign In' }).click();
36 | await expect(page).toHaveURL(BASE + '/');
37 | await expect(page.getByText(/Welcome back/)).toBeVisible();
38 | });
39 | });
40 |
41 | // Dashboard: quick actions and stats
42 | test.describe.serial('dashboard', () => {
43 | test.use({ storageState: 'playwright/.auth/state.json' });
44 |
> 45 | test('shows stats cards', async ({ page }) => {
| ^ Error: Error reading storage state from playwright/.auth/state.json:
46 | await page.goto(BASE + '/');
47 | await expect(page.getByText('Drawings')).toBeVisible();
48 | await expect(page.getByText('Projects')).toBeVisible();
49 | await expect(page.getByText('Teams')).toBeVisible();
50 | });
51 |
52 | test('quick action: New Project navigates to files', async ({ page }) => {
53 | await page.goto(BASE + '/');
54 | await page.getByRole('button', { name: 'New Project' }).click();
55 | await expect(page).toHaveURL(/\/files/);
56 | await expect(page.getByRole('navigation', { name: 'Project tree' })).toBeVisible();
57 | await expect(page.getByText('All Projects')).toBeVisible();
58 | });
59 |
60 | test('quick action: Invite navigates to team', async ({ page }) => {
61 | await page.goto(BASE + '/');
62 | await page.getByRole('button', { name: 'Invite' }).click();
63 | await expect(page).toHaveURL(/\/team/);
64 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
65 | });
66 |
67 | test('quick action: Library navigates to marketplace', async ({ page }) => {
68 | await page.goto(BASE + '/');
69 | await page.getByRole('button', { name: 'Library' }).click();
70 | await expect(page).toHaveURL(/\/library/);
71 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
72 | });
73 |
74 | test('New Drawing opens template picker', async ({ page }) => {
75 | await page.goto(BASE + '/');
76 | await page.getByRole('button', { name: 'New Drawing' }).click();
77 | await expect(page.getByRole('dialog')).toBeVisible();
78 | await expect(page.getByRole('heading', { name: 'Choose a Template' })).toBeVisible();
79 | await expect(page.getByRole('button', { name: 'Blank Canvas' })).toBeVisible();
80 | await expect(page.getByRole('button', { name: 'To-Do List' })).toBeVisible();
81 | await expect(page.getByRole('button', { name: 'Checklist' })).toBeVisible();
82 | await expect(page.getByRole('button', { name: 'Bullet List' })).toBeVisible();
83 | await expect(page.getByRole('button', { name: 'Flow Chart' })).toBeVisible();
84 | });
85 | });
86 |
87 | // Projects / FileBrowser
88 | test.describe.serial('projects', () => {
89 | test.use({ storageState: 'playwright/.auth/state.json' });
90 |
91 | test('shows Projects label in sidebar and breadcrumb', async ({ page }) => {
92 | await page.goto(BASE + '/files');
93 | await expect(page.getByRole('navigation', { name: 'Main navigation' }).getByText('Projects')).toBeVisible();
94 | await expect(page.getByText('All Projects')).toBeVisible();
95 | });
96 |
97 | test('can create a drawing from file browser', async ({ page }) => {
98 | await page.goto(BASE + '/files');
99 | await page.getByRole('button', { name: 'Create new drawing' }).click();
100 | await expect(page.getByRole('dialog')).toBeVisible();
101 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
102 | await expect(page).toHaveURL(/\/drawing\//);
103 | await expect(page.getByText('Loading Excalidraw')).toBeVisible();
104 | });
105 | });
106 |
107 | // Editor / Canvas
108 | test.describe.serial('editor', () => {
109 | test.use({ storageState: 'playwright/.auth/state.json' });
110 |
111 | test('creates drawing with To-Do template', async ({ page }) => {
112 | await page.goto(BASE + '/');
113 | await page.getByRole('button', { name: 'New Drawing' }).click();
114 | await page.getByRole('button', { name: 'To-Do List' }).click();
115 | await expect(page).toHaveURL(/\/drawing\//);
116 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
117 | });
118 |
119 | test('editor shows save controls and back button', async ({ page }) => {
120 | await page.goto(BASE + '/');
121 | await page.getByRole('button', { name: 'New Drawing' }).click();
122 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
123 | await expect(page).toHaveURL(/\/drawing\//);
124 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
125 | await expect(page.getByRole('button', { name: /Back/i })).toBeVisible();
126 | });
127 | });
128 |
129 | // Library Marketplace
130 | test.describe.serial('library', () => {
131 | test.use({ storageState: 'playwright/.auth/state.json' });
132 |
133 | test('loads marketplace with search and categories', async ({ page }) => {
134 | await page.goto(BASE + '/library');
135 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
136 | await expect(page.getByPlaceholder('Search libraries...')).toBeVisible();
137 | await expect(page.getByRole('button', { name: 'All' }).first()).toBeVisible();
138 | await expect(page.getByRole('button', { name: 'Open External' })).toBeVisible();
139 | });
140 |
141 | test('search filters libraries', async ({ page }) => {
142 | await page.goto(BASE + '/library');
143 | await page.getByPlaceholder('Search libraries...').fill('zzzznonexistent');
144 | await expect(page.getByText('No libraries found')).toBeVisible();
145 | });
```
@@ -1,177 +0,0 @@
# Test info
- Name: editor >> creates drawing with To-Do template
- Location: /home/tdvorak/Desktop/PROG+HTML/Excalidraw/frontend/e2e/app.spec.ts:111:3
# Error details
```
Error: Error reading storage state from playwright/.auth/state.json:
ENOENT: no such file or directory, open 'playwright/.auth/state.json'
```
# Test source
```ts
11 | await expect(page).toHaveURL(/\/signup$/);
12 | await expect(page.getByRole('heading', { name: 'Create account' })).toBeVisible();
13 | });
14 |
15 | test('first user can signup', async ({ page }) => {
16 | await page.goto(BASE + '/signup');
17 | await page.getByLabel('Full Name').fill('E2E User');
18 | await page.getByLabel('Email').fill('e2e@test.com');
19 | await page.getByLabel('Password').fill('e2e-password-123');
20 | await page.getByRole('button', { name: 'Create Account' }).click();
21 | await expect(page).toHaveURL(BASE + '/');
22 | await expect(page.getByText(/Welcome back/)).toBeVisible();
23 | await page.context().storageState({ path: 'playwright/.auth/state.json' });
24 | });
25 |
26 | test('blocks second signup when users exist', async ({ page }) => {
27 | await page.goto(BASE + '/signup');
28 | await expect(page).toHaveURL(/\/login$/);
29 | });
30 |
31 | test('existing user can login', async ({ page }) => {
32 | await page.goto(BASE + '/login');
33 | await page.getByLabel('Email').fill('e2e@test.com');
34 | await page.getByLabel('Password').fill('e2e-password-123');
35 | await page.getByRole('button', { name: 'Sign In' }).click();
36 | await expect(page).toHaveURL(BASE + '/');
37 | await expect(page.getByText(/Welcome back/)).toBeVisible();
38 | });
39 | });
40 |
41 | // Dashboard: quick actions and stats
42 | test.describe.serial('dashboard', () => {
43 | test.use({ storageState: 'playwright/.auth/state.json' });
44 |
45 | test('shows stats cards', async ({ page }) => {
46 | await page.goto(BASE + '/');
47 | await expect(page.getByText('Drawings')).toBeVisible();
48 | await expect(page.getByText('Projects')).toBeVisible();
49 | await expect(page.getByText('Teams')).toBeVisible();
50 | });
51 |
52 | test('quick action: New Project navigates to files', async ({ page }) => {
53 | await page.goto(BASE + '/');
54 | await page.getByRole('button', { name: 'New Project' }).click();
55 | await expect(page).toHaveURL(/\/files/);
56 | await expect(page.getByRole('navigation', { name: 'Project tree' })).toBeVisible();
57 | await expect(page.getByText('All Projects')).toBeVisible();
58 | });
59 |
60 | test('quick action: Invite navigates to team', async ({ page }) => {
61 | await page.goto(BASE + '/');
62 | await page.getByRole('button', { name: 'Invite' }).click();
63 | await expect(page).toHaveURL(/\/team/);
64 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
65 | });
66 |
67 | test('quick action: Library navigates to marketplace', async ({ page }) => {
68 | await page.goto(BASE + '/');
69 | await page.getByRole('button', { name: 'Library' }).click();
70 | await expect(page).toHaveURL(/\/library/);
71 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
72 | });
73 |
74 | test('New Drawing opens template picker', async ({ page }) => {
75 | await page.goto(BASE + '/');
76 | await page.getByRole('button', { name: 'New Drawing' }).click();
77 | await expect(page.getByRole('dialog')).toBeVisible();
78 | await expect(page.getByRole('heading', { name: 'Choose a Template' })).toBeVisible();
79 | await expect(page.getByRole('button', { name: 'Blank Canvas' })).toBeVisible();
80 | await expect(page.getByRole('button', { name: 'To-Do List' })).toBeVisible();
81 | await expect(page.getByRole('button', { name: 'Checklist' })).toBeVisible();
82 | await expect(page.getByRole('button', { name: 'Bullet List' })).toBeVisible();
83 | await expect(page.getByRole('button', { name: 'Flow Chart' })).toBeVisible();
84 | });
85 | });
86 |
87 | // Projects / FileBrowser
88 | test.describe.serial('projects', () => {
89 | test.use({ storageState: 'playwright/.auth/state.json' });
90 |
91 | test('shows Projects label in sidebar and breadcrumb', async ({ page }) => {
92 | await page.goto(BASE + '/files');
93 | await expect(page.getByRole('navigation', { name: 'Main navigation' }).getByText('Projects')).toBeVisible();
94 | await expect(page.getByText('All Projects')).toBeVisible();
95 | });
96 |
97 | test('can create a drawing from file browser', async ({ page }) => {
98 | await page.goto(BASE + '/files');
99 | await page.getByRole('button', { name: 'Create new drawing' }).click();
100 | await expect(page.getByRole('dialog')).toBeVisible();
101 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
102 | await expect(page).toHaveURL(/\/drawing\//);
103 | await expect(page.getByText('Loading Excalidraw')).toBeVisible();
104 | });
105 | });
106 |
107 | // Editor / Canvas
108 | test.describe.serial('editor', () => {
109 | test.use({ storageState: 'playwright/.auth/state.json' });
110 |
> 111 | test('creates drawing with To-Do template', async ({ page }) => {
| ^ Error: Error reading storage state from playwright/.auth/state.json:
112 | await page.goto(BASE + '/');
113 | await page.getByRole('button', { name: 'New Drawing' }).click();
114 | await page.getByRole('button', { name: 'To-Do List' }).click();
115 | await expect(page).toHaveURL(/\/drawing\//);
116 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
117 | });
118 |
119 | test('editor shows save controls and back button', async ({ page }) => {
120 | await page.goto(BASE + '/');
121 | await page.getByRole('button', { name: 'New Drawing' }).click();
122 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
123 | await expect(page).toHaveURL(/\/drawing\//);
124 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
125 | await expect(page.getByRole('button', { name: /Back/i })).toBeVisible();
126 | });
127 | });
128 |
129 | // Library Marketplace
130 | test.describe.serial('library', () => {
131 | test.use({ storageState: 'playwright/.auth/state.json' });
132 |
133 | test('loads marketplace with search and categories', async ({ page }) => {
134 | await page.goto(BASE + '/library');
135 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
136 | await expect(page.getByPlaceholder('Search libraries...')).toBeVisible();
137 | await expect(page.getByRole('button', { name: 'All' }).first()).toBeVisible();
138 | await expect(page.getByRole('button', { name: 'Open External' })).toBeVisible();
139 | });
140 |
141 | test('search filters libraries', async ({ page }) => {
142 | await page.goto(BASE + '/library');
143 | await page.getByPlaceholder('Search libraries...').fill('zzzznonexistent');
144 | await expect(page.getByText('No libraries found')).toBeVisible();
145 | });
146 | });
147 |
148 | // Team / Invites
149 | test.describe.serial('team', () => {
150 | test.use({ storageState: 'playwright/.auth/state.json' });
151 |
152 | test('shows owner in members list', async ({ page }) => {
153 | await page.goto(BASE + '/team');
154 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
155 | await expect(page.getByText('E2E User')).toBeVisible();
156 | await expect(page.getByText('owner')).toBeVisible();
157 | });
158 |
159 | test('can send team invite', async ({ page }) => {
160 | await page.goto(BASE + '/team');
161 | await page.getByLabel('Email address').fill('invited@test.com');
162 | await page.locator('select').selectOption('editor');
163 | await page.getByRole('button', { name: 'Send Invite' }).click();
164 | await expect(page.getByText('Invite sent!')).toBeVisible();
165 | await expect(page.getByText('Pending Invites')).toBeVisible();
166 | await expect(page.getByText('invited@test.com')).toBeVisible();
167 | await expect(page.getByText('editor').first()).toBeVisible();
168 | });
169 | });
170 |
```
@@ -1,155 +0,0 @@
# Test info
- Name: library >> loads marketplace with search and categories
- Location: /home/tdvorak/Desktop/PROG+HTML/Excalidraw/frontend/e2e/app.spec.ts:133:3
# Error details
```
Error: Error reading storage state from playwright/.auth/state.json:
ENOENT: no such file or directory, open 'playwright/.auth/state.json'
```
# Test source
```ts
33 | await page.getByLabel('Email').fill('e2e@test.com');
34 | await page.getByLabel('Password').fill('e2e-password-123');
35 | await page.getByRole('button', { name: 'Sign In' }).click();
36 | await expect(page).toHaveURL(BASE + '/');
37 | await expect(page.getByText(/Welcome back/)).toBeVisible();
38 | });
39 | });
40 |
41 | // Dashboard: quick actions and stats
42 | test.describe.serial('dashboard', () => {
43 | test.use({ storageState: 'playwright/.auth/state.json' });
44 |
45 | test('shows stats cards', async ({ page }) => {
46 | await page.goto(BASE + '/');
47 | await expect(page.getByText('Drawings')).toBeVisible();
48 | await expect(page.getByText('Projects')).toBeVisible();
49 | await expect(page.getByText('Teams')).toBeVisible();
50 | });
51 |
52 | test('quick action: New Project navigates to files', async ({ page }) => {
53 | await page.goto(BASE + '/');
54 | await page.getByRole('button', { name: 'New Project' }).click();
55 | await expect(page).toHaveURL(/\/files/);
56 | await expect(page.getByRole('navigation', { name: 'Project tree' })).toBeVisible();
57 | await expect(page.getByText('All Projects')).toBeVisible();
58 | });
59 |
60 | test('quick action: Invite navigates to team', async ({ page }) => {
61 | await page.goto(BASE + '/');
62 | await page.getByRole('button', { name: 'Invite' }).click();
63 | await expect(page).toHaveURL(/\/team/);
64 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
65 | });
66 |
67 | test('quick action: Library navigates to marketplace', async ({ page }) => {
68 | await page.goto(BASE + '/');
69 | await page.getByRole('button', { name: 'Library' }).click();
70 | await expect(page).toHaveURL(/\/library/);
71 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
72 | });
73 |
74 | test('New Drawing opens template picker', async ({ page }) => {
75 | await page.goto(BASE + '/');
76 | await page.getByRole('button', { name: 'New Drawing' }).click();
77 | await expect(page.getByRole('dialog')).toBeVisible();
78 | await expect(page.getByRole('heading', { name: 'Choose a Template' })).toBeVisible();
79 | await expect(page.getByRole('button', { name: 'Blank Canvas' })).toBeVisible();
80 | await expect(page.getByRole('button', { name: 'To-Do List' })).toBeVisible();
81 | await expect(page.getByRole('button', { name: 'Checklist' })).toBeVisible();
82 | await expect(page.getByRole('button', { name: 'Bullet List' })).toBeVisible();
83 | await expect(page.getByRole('button', { name: 'Flow Chart' })).toBeVisible();
84 | });
85 | });
86 |
87 | // Projects / FileBrowser
88 | test.describe.serial('projects', () => {
89 | test.use({ storageState: 'playwright/.auth/state.json' });
90 |
91 | test('shows Projects label in sidebar and breadcrumb', async ({ page }) => {
92 | await page.goto(BASE + '/files');
93 | await expect(page.getByRole('navigation', { name: 'Main navigation' }).getByText('Projects')).toBeVisible();
94 | await expect(page.getByText('All Projects')).toBeVisible();
95 | });
96 |
97 | test('can create a drawing from file browser', async ({ page }) => {
98 | await page.goto(BASE + '/files');
99 | await page.getByRole('button', { name: 'Create new drawing' }).click();
100 | await expect(page.getByRole('dialog')).toBeVisible();
101 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
102 | await expect(page).toHaveURL(/\/drawing\//);
103 | await expect(page.getByText('Loading Excalidraw')).toBeVisible();
104 | });
105 | });
106 |
107 | // Editor / Canvas
108 | test.describe.serial('editor', () => {
109 | test.use({ storageState: 'playwright/.auth/state.json' });
110 |
111 | test('creates drawing with To-Do template', async ({ page }) => {
112 | await page.goto(BASE + '/');
113 | await page.getByRole('button', { name: 'New Drawing' }).click();
114 | await page.getByRole('button', { name: 'To-Do List' }).click();
115 | await expect(page).toHaveURL(/\/drawing\//);
116 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
117 | });
118 |
119 | test('editor shows save controls and back button', async ({ page }) => {
120 | await page.goto(BASE + '/');
121 | await page.getByRole('button', { name: 'New Drawing' }).click();
122 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
123 | await expect(page).toHaveURL(/\/drawing\//);
124 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
125 | await expect(page.getByRole('button', { name: /Back/i })).toBeVisible();
126 | });
127 | });
128 |
129 | // Library Marketplace
130 | test.describe.serial('library', () => {
131 | test.use({ storageState: 'playwright/.auth/state.json' });
132 |
> 133 | test('loads marketplace with search and categories', async ({ page }) => {
| ^ Error: Error reading storage state from playwright/.auth/state.json:
134 | await page.goto(BASE + '/library');
135 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
136 | await expect(page.getByPlaceholder('Search libraries...')).toBeVisible();
137 | await expect(page.getByRole('button', { name: 'All' }).first()).toBeVisible();
138 | await expect(page.getByRole('button', { name: 'Open External' })).toBeVisible();
139 | });
140 |
141 | test('search filters libraries', async ({ page }) => {
142 | await page.goto(BASE + '/library');
143 | await page.getByPlaceholder('Search libraries...').fill('zzzznonexistent');
144 | await expect(page.getByText('No libraries found')).toBeVisible();
145 | });
146 | });
147 |
148 | // Team / Invites
149 | test.describe.serial('team', () => {
150 | test.use({ storageState: 'playwright/.auth/state.json' });
151 |
152 | test('shows owner in members list', async ({ page }) => {
153 | await page.goto(BASE + '/team');
154 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
155 | await expect(page.getByText('E2E User')).toBeVisible();
156 | await expect(page.getByText('owner')).toBeVisible();
157 | });
158 |
159 | test('can send team invite', async ({ page }) => {
160 | await page.goto(BASE + '/team');
161 | await page.getByLabel('Email address').fill('invited@test.com');
162 | await page.locator('select').selectOption('editor');
163 | await page.getByRole('button', { name: 'Send Invite' }).click();
164 | await expect(page.getByText('Invite sent!')).toBeVisible();
165 | await expect(page.getByText('Pending Invites')).toBeVisible();
166 | await expect(page.getByText('invited@test.com')).toBeVisible();
167 | await expect(page.getByText('editor').first()).toBeVisible();
168 | });
169 | });
170 |
```
@@ -1,187 +0,0 @@
# Test info
- Name: projects >> shows Projects label in sidebar and breadcrumb
- Location: /home/tdvorak/Desktop/PROG+HTML/Excalidraw/frontend/e2e/app.spec.ts:91:3
# Error details
```
Error: Error reading storage state from playwright/.auth/state.json:
ENOENT: no such file or directory, open 'playwright/.auth/state.json'
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | const BASE = 'http://localhost:3456';
4 |
5 | // Auth: first-run signup, blocked signup, login
6 | test.describe.serial('auth flow', () => {
7 | test.use({ storageState: { cookies: [], origins: [] } });
8 |
9 | test('redirects to signup when no users exist', async ({ page }) => {
10 | await page.goto(BASE + '/');
11 | await expect(page).toHaveURL(/\/signup$/);
12 | await expect(page.getByRole('heading', { name: 'Create account' })).toBeVisible();
13 | });
14 |
15 | test('first user can signup', async ({ page }) => {
16 | await page.goto(BASE + '/signup');
17 | await page.getByLabel('Full Name').fill('E2E User');
18 | await page.getByLabel('Email').fill('e2e@test.com');
19 | await page.getByLabel('Password').fill('e2e-password-123');
20 | await page.getByRole('button', { name: 'Create Account' }).click();
21 | await expect(page).toHaveURL(BASE + '/');
22 | await expect(page.getByText(/Welcome back/)).toBeVisible();
23 | await page.context().storageState({ path: 'playwright/.auth/state.json' });
24 | });
25 |
26 | test('blocks second signup when users exist', async ({ page }) => {
27 | await page.goto(BASE + '/signup');
28 | await expect(page).toHaveURL(/\/login$/);
29 | });
30 |
31 | test('existing user can login', async ({ page }) => {
32 | await page.goto(BASE + '/login');
33 | await page.getByLabel('Email').fill('e2e@test.com');
34 | await page.getByLabel('Password').fill('e2e-password-123');
35 | await page.getByRole('button', { name: 'Sign In' }).click();
36 | await expect(page).toHaveURL(BASE + '/');
37 | await expect(page.getByText(/Welcome back/)).toBeVisible();
38 | });
39 | });
40 |
41 | // Dashboard: quick actions and stats
42 | test.describe.serial('dashboard', () => {
43 | test.use({ storageState: 'playwright/.auth/state.json' });
44 |
45 | test('shows stats cards', async ({ page }) => {
46 | await page.goto(BASE + '/');
47 | await expect(page.getByText('Drawings')).toBeVisible();
48 | await expect(page.getByText('Projects')).toBeVisible();
49 | await expect(page.getByText('Teams')).toBeVisible();
50 | });
51 |
52 | test('quick action: New Project navigates to files', async ({ page }) => {
53 | await page.goto(BASE + '/');
54 | await page.getByRole('button', { name: 'New Project' }).click();
55 | await expect(page).toHaveURL(/\/files/);
56 | await expect(page.getByRole('navigation', { name: 'Project tree' })).toBeVisible();
57 | await expect(page.getByText('All Projects')).toBeVisible();
58 | });
59 |
60 | test('quick action: Invite navigates to team', async ({ page }) => {
61 | await page.goto(BASE + '/');
62 | await page.getByRole('button', { name: 'Invite' }).click();
63 | await expect(page).toHaveURL(/\/team/);
64 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
65 | });
66 |
67 | test('quick action: Library navigates to marketplace', async ({ page }) => {
68 | await page.goto(BASE + '/');
69 | await page.getByRole('button', { name: 'Library' }).click();
70 | await expect(page).toHaveURL(/\/library/);
71 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
72 | });
73 |
74 | test('New Drawing opens template picker', async ({ page }) => {
75 | await page.goto(BASE + '/');
76 | await page.getByRole('button', { name: 'New Drawing' }).click();
77 | await expect(page.getByRole('dialog')).toBeVisible();
78 | await expect(page.getByRole('heading', { name: 'Choose a Template' })).toBeVisible();
79 | await expect(page.getByRole('button', { name: 'Blank Canvas' })).toBeVisible();
80 | await expect(page.getByRole('button', { name: 'To-Do List' })).toBeVisible();
81 | await expect(page.getByRole('button', { name: 'Checklist' })).toBeVisible();
82 | await expect(page.getByRole('button', { name: 'Bullet List' })).toBeVisible();
83 | await expect(page.getByRole('button', { name: 'Flow Chart' })).toBeVisible();
84 | });
85 | });
86 |
87 | // Projects / FileBrowser
88 | test.describe.serial('projects', () => {
89 | test.use({ storageState: 'playwright/.auth/state.json' });
90 |
> 91 | test('shows Projects label in sidebar and breadcrumb', async ({ page }) => {
| ^ Error: Error reading storage state from playwright/.auth/state.json:
92 | await page.goto(BASE + '/files');
93 | await expect(page.getByRole('navigation', { name: 'Main navigation' }).getByText('Projects')).toBeVisible();
94 | await expect(page.getByText('All Projects')).toBeVisible();
95 | });
96 |
97 | test('can create a drawing from file browser', async ({ page }) => {
98 | await page.goto(BASE + '/files');
99 | await page.getByRole('button', { name: 'Create new drawing' }).click();
100 | await expect(page.getByRole('dialog')).toBeVisible();
101 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
102 | await expect(page).toHaveURL(/\/drawing\//);
103 | await expect(page.getByText('Loading Excalidraw')).toBeVisible();
104 | });
105 | });
106 |
107 | // Editor / Canvas
108 | test.describe.serial('editor', () => {
109 | test.use({ storageState: 'playwright/.auth/state.json' });
110 |
111 | test('creates drawing with To-Do template', async ({ page }) => {
112 | await page.goto(BASE + '/');
113 | await page.getByRole('button', { name: 'New Drawing' }).click();
114 | await page.getByRole('button', { name: 'To-Do List' }).click();
115 | await expect(page).toHaveURL(/\/drawing\//);
116 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
117 | });
118 |
119 | test('editor shows save controls and back button', async ({ page }) => {
120 | await page.goto(BASE + '/');
121 | await page.getByRole('button', { name: 'New Drawing' }).click();
122 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
123 | await expect(page).toHaveURL(/\/drawing\//);
124 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
125 | await expect(page.getByRole('button', { name: /Back/i })).toBeVisible();
126 | });
127 | });
128 |
129 | // Library Marketplace
130 | test.describe.serial('library', () => {
131 | test.use({ storageState: 'playwright/.auth/state.json' });
132 |
133 | test('loads marketplace with search and categories', async ({ page }) => {
134 | await page.goto(BASE + '/library');
135 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
136 | await expect(page.getByPlaceholder('Search libraries...')).toBeVisible();
137 | await expect(page.getByRole('button', { name: 'All' }).first()).toBeVisible();
138 | await expect(page.getByRole('button', { name: 'Open External' })).toBeVisible();
139 | });
140 |
141 | test('search filters libraries', async ({ page }) => {
142 | await page.goto(BASE + '/library');
143 | await page.getByPlaceholder('Search libraries...').fill('zzzznonexistent');
144 | await expect(page.getByText('No libraries found')).toBeVisible();
145 | });
146 | });
147 |
148 | // Team / Invites
149 | test.describe.serial('team', () => {
150 | test.use({ storageState: 'playwright/.auth/state.json' });
151 |
152 | test('shows owner in members list', async ({ page }) => {
153 | await page.goto(BASE + '/team');
154 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
155 | await expect(page.getByText('E2E User')).toBeVisible();
156 | await expect(page.getByText('owner')).toBeVisible();
157 | });
158 |
159 | test('can send team invite', async ({ page }) => {
160 | await page.goto(BASE + '/team');
161 | await page.getByLabel('Email address').fill('invited@test.com');
162 | await page.locator('select').selectOption('editor');
163 | await page.getByRole('button', { name: 'Send Invite' }).click();
164 | await expect(page.getByText('Invite sent!')).toBeVisible();
165 | await expect(page.getByText('Pending Invites')).toBeVisible();
166 | await expect(page.getByText('invited@test.com')).toBeVisible();
167 | await expect(page.getByText('editor').first()).toBeVisible();
168 | });
169 | });
170 |
```
@@ -1,136 +0,0 @@
# Test info
- Name: team >> shows owner in members list
- Location: /home/tdvorak/Desktop/PROG+HTML/Excalidraw/frontend/e2e/app.spec.ts:152:3
# Error details
```
Error: Error reading storage state from playwright/.auth/state.json:
ENOENT: no such file or directory, open 'playwright/.auth/state.json'
```
# Test source
```ts
52 | test('quick action: New Project navigates to files', async ({ page }) => {
53 | await page.goto(BASE + '/');
54 | await page.getByRole('button', { name: 'New Project' }).click();
55 | await expect(page).toHaveURL(/\/files/);
56 | await expect(page.getByRole('navigation', { name: 'Project tree' })).toBeVisible();
57 | await expect(page.getByText('All Projects')).toBeVisible();
58 | });
59 |
60 | test('quick action: Invite navigates to team', async ({ page }) => {
61 | await page.goto(BASE + '/');
62 | await page.getByRole('button', { name: 'Invite' }).click();
63 | await expect(page).toHaveURL(/\/team/);
64 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
65 | });
66 |
67 | test('quick action: Library navigates to marketplace', async ({ page }) => {
68 | await page.goto(BASE + '/');
69 | await page.getByRole('button', { name: 'Library' }).click();
70 | await expect(page).toHaveURL(/\/library/);
71 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
72 | });
73 |
74 | test('New Drawing opens template picker', async ({ page }) => {
75 | await page.goto(BASE + '/');
76 | await page.getByRole('button', { name: 'New Drawing' }).click();
77 | await expect(page.getByRole('dialog')).toBeVisible();
78 | await expect(page.getByRole('heading', { name: 'Choose a Template' })).toBeVisible();
79 | await expect(page.getByRole('button', { name: 'Blank Canvas' })).toBeVisible();
80 | await expect(page.getByRole('button', { name: 'To-Do List' })).toBeVisible();
81 | await expect(page.getByRole('button', { name: 'Checklist' })).toBeVisible();
82 | await expect(page.getByRole('button', { name: 'Bullet List' })).toBeVisible();
83 | await expect(page.getByRole('button', { name: 'Flow Chart' })).toBeVisible();
84 | });
85 | });
86 |
87 | // Projects / FileBrowser
88 | test.describe.serial('projects', () => {
89 | test.use({ storageState: 'playwright/.auth/state.json' });
90 |
91 | test('shows Projects label in sidebar and breadcrumb', async ({ page }) => {
92 | await page.goto(BASE + '/files');
93 | await expect(page.getByRole('navigation', { name: 'Main navigation' }).getByText('Projects')).toBeVisible();
94 | await expect(page.getByText('All Projects')).toBeVisible();
95 | });
96 |
97 | test('can create a drawing from file browser', async ({ page }) => {
98 | await page.goto(BASE + '/files');
99 | await page.getByRole('button', { name: 'Create new drawing' }).click();
100 | await expect(page.getByRole('dialog')).toBeVisible();
101 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
102 | await expect(page).toHaveURL(/\/drawing\//);
103 | await expect(page.getByText('Loading Excalidraw')).toBeVisible();
104 | });
105 | });
106 |
107 | // Editor / Canvas
108 | test.describe.serial('editor', () => {
109 | test.use({ storageState: 'playwright/.auth/state.json' });
110 |
111 | test('creates drawing with To-Do template', async ({ page }) => {
112 | await page.goto(BASE + '/');
113 | await page.getByRole('button', { name: 'New Drawing' }).click();
114 | await page.getByRole('button', { name: 'To-Do List' }).click();
115 | await expect(page).toHaveURL(/\/drawing\//);
116 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
117 | });
118 |
119 | test('editor shows save controls and back button', async ({ page }) => {
120 | await page.goto(BASE + '/');
121 | await page.getByRole('button', { name: 'New Drawing' }).click();
122 | await page.getByRole('button', { name: 'Blank Canvas' }).click();
123 | await expect(page).toHaveURL(/\/drawing\//);
124 | await expect(page.getByRole('button', { name: /Save Now/i })).toBeVisible({ timeout: 10000 });
125 | await expect(page.getByRole('button', { name: /Back/i })).toBeVisible();
126 | });
127 | });
128 |
129 | // Library Marketplace
130 | test.describe.serial('library', () => {
131 | test.use({ storageState: 'playwright/.auth/state.json' });
132 |
133 | test('loads marketplace with search and categories', async ({ page }) => {
134 | await page.goto(BASE + '/library');
135 | await expect(page.getByRole('heading', { name: 'Library Marketplace' })).toBeVisible();
136 | await expect(page.getByPlaceholder('Search libraries...')).toBeVisible();
137 | await expect(page.getByRole('button', { name: 'All' }).first()).toBeVisible();
138 | await expect(page.getByRole('button', { name: 'Open External' })).toBeVisible();
139 | });
140 |
141 | test('search filters libraries', async ({ page }) => {
142 | await page.goto(BASE + '/library');
143 | await page.getByPlaceholder('Search libraries...').fill('zzzznonexistent');
144 | await expect(page.getByText('No libraries found')).toBeVisible();
145 | });
146 | });
147 |
148 | // Team / Invites
149 | test.describe.serial('team', () => {
150 | test.use({ storageState: 'playwright/.auth/state.json' });
151 |
> 152 | test('shows owner in members list', async ({ page }) => {
| ^ Error: Error reading storage state from playwright/.auth/state.json:
153 | await page.goto(BASE + '/team');
154 | await expect(page.getByRole('heading', { name: 'Team Settings' })).toBeVisible();
155 | await expect(page.getByText('E2E User')).toBeVisible();
156 | await expect(page.getByText('owner')).toBeVisible();
157 | });
158 |
159 | test('can send team invite', async ({ page }) => {
160 | await page.goto(BASE + '/team');
161 | await page.getByLabel('Email address').fill('invited@test.com');
162 | await page.locator('select').selectOption('editor');
163 | await page.getByRole('button', { name: 'Send Invite' }).click();
164 | await expect(page.getByText('Invite sent!')).toBeVisible();
165 | await expect(page.getByText('Pending Invites')).toBeVisible();
166 | await expect(page.getByText('invited@test.com')).toBeVisible();
167 | await expect(page.getByText('editor').first()).toBeVisible();
168 | });
169 | });
170 |
```