mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-06-03 13:52:56 +00:00
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:
@@ -10,15 +10,14 @@
|
||||
/* Excalidraw Context Menu Styling Overrides */
|
||||
:global(.excalidraw .context-menu) {
|
||||
background: var(--island-bg-color) !important;
|
||||
border: 2px solid var(--color-gray-85) !important;
|
||||
border-radius: 2px !important;
|
||||
box-shadow: 4px 4px 0 var(--color-gray-85) !important;
|
||||
transform: rotate(-0.2deg) !important;
|
||||
border: 1px solid var(--default-border-color) !important;
|
||||
border-radius: var(--border-radius-lg) !important;
|
||||
box-shadow: var(--shadow-island-stronger) !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
|
||||
:global(.excalidraw .context-menu-item) {
|
||||
border-radius: 2px !important;
|
||||
border-radius: var(--border-radius-md) !important;
|
||||
color: var(--color-gray-85) !important;
|
||||
font-weight: 500 !important;
|
||||
padding: 6px 12px !important;
|
||||
@@ -27,11 +26,10 @@
|
||||
:global(.excalidraw .context-menu-item:hover) {
|
||||
background: var(--color-primary-light) !important;
|
||||
color: var(--color-primary-darkest) !important;
|
||||
transform: translateX(1px) !important;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: var(--island-bg-color);
|
||||
border-right: 2px solid var(--color-gray-85);
|
||||
border-right: 1px solid var(--default-border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--space-4);
|
||||
@@ -18,15 +18,6 @@
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
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) {
|
||||
transform: translateX(-100%);
|
||||
@@ -81,17 +72,16 @@
|
||||
}
|
||||
|
||||
.logoImg {
|
||||
width: 28px;
|
||||
width: auto;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.logoMark {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 9px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--color-gray-85);
|
||||
background: var(--color-primary-light);
|
||||
display: inline-flex;
|
||||
@@ -99,7 +89,6 @@
|
||||
justify-content: center;
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 800;
|
||||
transform: rotate(-4deg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -141,25 +130,22 @@
|
||||
padding: var(--space-3) var(--space-4);
|
||||
color: var(--color-gray-70);
|
||||
text-decoration: none;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--border-radius-lg);
|
||||
transition: all var(--duration-fast) var(--ease-out);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-low);
|
||||
color: var(--color-on-surface);
|
||||
border-color: var(--color-gray-30);
|
||||
transform: rotate(-0.5deg);
|
||||
border-color: var(--default-border-color);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-surface-primary-container);
|
||||
color: var(--color-primary-darkest);
|
||||
font-weight: 600;
|
||||
border-color: var(--color-gray-85);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
transform: rotate(-0.3deg);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +222,7 @@
|
||||
.header {
|
||||
height: var(--header-height);
|
||||
background: var(--island-bg-color);
|
||||
border-bottom: 2px solid var(--color-gray-85);
|
||||
box-shadow: 0 3px 0 var(--color-gray-85);
|
||||
border-bottom: 1px solid var(--default-border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -297,11 +282,11 @@
|
||||
.iconButton {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: 2px solid transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--color-gray-60);
|
||||
cursor: pointer;
|
||||
padding: var(--space-2);
|
||||
border-radius: 2px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -310,9 +295,7 @@
|
||||
&:hover {
|
||||
color: var(--color-on-surface);
|
||||
background: var(--color-surface-low);
|
||||
border-color: var(--color-gray-30);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
transform: rotate(-1deg);
|
||||
border-color: var(--default-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,8 +379,8 @@
|
||||
|
||||
.nameModal {
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--modal-shadow);
|
||||
padding: var(--space-5);
|
||||
width: 360px;
|
||||
@@ -483,14 +466,13 @@
|
||||
top: calc(100% + var(--space-2));
|
||||
right: 100px;
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 5px 5px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
width: 320px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
transform: rotate(-0.2deg);
|
||||
}
|
||||
|
||||
.notifHeader {
|
||||
@@ -498,14 +480,14 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 2px solid var(--color-gray-85);
|
||||
border-bottom: 1px solid var(--default-border-color);
|
||||
}
|
||||
|
||||
.notifTitle {
|
||||
font-weight: 600;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-gray-85);
|
||||
font-family: 'Georgia', serif;
|
||||
font-family: var(--ui-font);
|
||||
}
|
||||
|
||||
.notifMarkAll {
|
||||
|
||||
@@ -41,10 +41,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ open, onClose }) => {
|
||||
src="https://plus.excalidraw.com/images/logo.svg"
|
||||
alt="Excalidraw"
|
||||
className={styles.logoImg}
|
||||
width={28}
|
||||
width={120}
|
||||
height={28}
|
||||
/>
|
||||
<span className={styles.logoText}>Excalidraw</span>
|
||||
</div>
|
||||
{onClose && (
|
||||
<button
|
||||
|
||||
@@ -13,15 +13,14 @@
|
||||
|
||||
.modal {
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 8px 8px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-6);
|
||||
transform: rotate(-0.1deg);
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -42,17 +41,16 @@
|
||||
|
||||
.closeBtn {
|
||||
background: none;
|
||||
border: 2px solid transparent;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
color: var(--color-gray-60);
|
||||
padding: var(--space-2);
|
||||
border-radius: 2px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-gray-85);
|
||||
border-color: var(--default-border-color);
|
||||
color: var(--color-gray-90);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
transform: rotate(-1deg);
|
||||
background: var(--color-surface-low);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,21 +67,20 @@
|
||||
text-align: center;
|
||||
padding: var(--space-6) var(--space-4);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: var(--island-bg-color);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
transition: all var(--duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-2px) rotate(-0.3deg);
|
||||
box-shadow: 4px 4px 0 var(--color-gray-85);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0) rotate(0);
|
||||
box-shadow: 1px 1px 0 var(--color-gray-85);
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ interface TemplateOption {
|
||||
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 {
|
||||
id: `el-${Math.random().toString(36).slice(2)}`,
|
||||
type: 'rectangle',
|
||||
@@ -41,14 +41,14 @@ function makeHandDrawnRect(x: number, y: number, w: number, h: number, text?: st
|
||||
version: 2,
|
||||
versionNonce: Math.floor(Math.random() * 100000),
|
||||
isDeleted: false,
|
||||
boundElements: text ? [{ id: `txt-${Math.random().toString(36).slice(2)}`, type: 'text' }] : [],
|
||||
boundElements: [],
|
||||
updated: Date.now(),
|
||||
link: null,
|
||||
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 {
|
||||
id: `txt-${Math.random().toString(36).slice(2)}`,
|
||||
type: 'text',
|
||||
@@ -61,7 +61,7 @@ function makeText(x: number, y: number, text: string, fontSize = 20) {
|
||||
strokeStyle: 'solid',
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
groupIds: [],
|
||||
groupIds: groupId ? [groupId] : [],
|
||||
frameId: null,
|
||||
roundness: null,
|
||||
seed: Math.floor(Math.random() * 10000),
|
||||
@@ -77,7 +77,7 @@ function makeText(x: number, y: number, text: string, fontSize = 20) {
|
||||
fontFamily: 1,
|
||||
textAlign: 'left',
|
||||
verticalAlign: 'top',
|
||||
baseline: 18,
|
||||
baseline: Math.round(fontSize * 0.7),
|
||||
containerId: null,
|
||||
originalText: text,
|
||||
lineHeight: 1.25,
|
||||
@@ -208,14 +208,14 @@ export const BUILTIN_TEMPLATES: Record<PickedTemplate, RawElement[]> = {
|
||||
makeText(285, 120, 'Doing', 20),
|
||||
makeText(495, 120, 'Done', 20),
|
||||
// Card 1 - grouped
|
||||
makeHandDrawnRect(70, 170, 140, 70, undefined, 'card1'),
|
||||
makeText(85, 190, 'User research', 16),
|
||||
makeHandDrawnRect(70, 170, 140, 70, 'card1'),
|
||||
makeText(85, 190, 'User research', 16, 'card1'),
|
||||
// Card 2 - grouped
|
||||
makeHandDrawnRect(280, 170, 140, 70, undefined, 'card2'),
|
||||
makeText(295, 190, 'Sketch flow', 16),
|
||||
makeHandDrawnRect(280, 170, 140, 70, 'card2'),
|
||||
makeText(295, 190, 'Sketch flow', 16, 'card2'),
|
||||
// Card 3 - grouped
|
||||
makeHandDrawnRect(490, 170, 140, 70, undefined, 'card3'),
|
||||
makeText(505, 190, 'Project brief', 16),
|
||||
makeHandDrawnRect(490, 170, 140, 70, 'card3'),
|
||||
makeText(505, 190, 'Project brief', 16, 'card3'),
|
||||
// Add card buttons per column
|
||||
makeAddButton(110, 380, '+', 'kanban-add-backlog'),
|
||||
makeAddButton(320, 380, '+', 'kanban-add-doing'),
|
||||
@@ -374,13 +374,13 @@ export const BUILTIN_TEMPLATES: Record<PickedTemplate, RawElement[]> = {
|
||||
makeText(50, 30, 'Project Timeline', 30),
|
||||
makeHandDrawnRect(50, 90, 600, 4),
|
||||
// Milestones
|
||||
makeHandDrawnRect(80, 70, 20, 44, undefined, 'milestone-1'),
|
||||
makeHandDrawnRect(80, 70, 20, 44, 'milestone-1'),
|
||||
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),
|
||||
makeHandDrawnRect(360, 70, 20, 44, undefined, 'milestone-3'),
|
||||
makeHandDrawnRect(360, 70, 20, 44, 'milestone-3'),
|
||||
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),
|
||||
// Tasks below timeline
|
||||
makeHandDrawnRect(50, 170, 130, 50),
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
gap: var(--space-6);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 4px 4px 0 var(--color-gray-85);
|
||||
transform: rotate(-0.3deg);
|
||||
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
h1 {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-85);
|
||||
margin-bottom: var(--space-2);
|
||||
font-family: 'Georgia', serif;
|
||||
font-family: var(--ui-font);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
@@ -84,21 +82,14 @@
|
||||
}
|
||||
|
||||
.statCardWrapper {
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
transform: rotate(0.15deg);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island);
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(0) translate(-1px, -1px);
|
||||
box-shadow: 5px 5px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
}
|
||||
|
||||
&: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 {
|
||||
@@ -121,21 +112,12 @@
|
||||
.statIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--border-radius-lg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary-light);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
|
||||
.handChart {
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
transform: rotate(1deg);
|
||||
border: 1px solid var(--default-border-color);
|
||||
}
|
||||
|
||||
.sparkline {
|
||||
@@ -148,7 +130,7 @@
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
font-family: 'Georgia', serif;
|
||||
font-family: var(--ui-font);
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
@@ -157,7 +139,7 @@
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.chartBarWrap {
|
||||
.progressBarWrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
@@ -166,18 +148,17 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chartBarBg {
|
||||
.progressBarBg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--color-gray-20);
|
||||
border-radius: var(--border-radius-full);
|
||||
}
|
||||
|
||||
.chartBar {
|
||||
.progressBarFill {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--border-radius-full);
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-darkest));
|
||||
transition: width 0.4s var(--ease-out);
|
||||
}
|
||||
|
||||
@@ -230,22 +211,21 @@
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
background: var(--island-bg-color);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-surface-low);
|
||||
transform: translateX(2px) rotate(-0.3deg);
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
transform: translateX(2px);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 2px solid var(--color-gray-30);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -253,12 +233,12 @@
|
||||
.drawingThumb {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 2px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
overflow: hidden;
|
||||
background: var(--color-surface-low);
|
||||
flex-shrink: 0;
|
||||
border: 2px solid var(--color-gray-30);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@@ -359,7 +339,7 @@
|
||||
.activityAvatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
display: flex;
|
||||
@@ -368,8 +348,8 @@
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
|
||||
.activityInfo {
|
||||
@@ -399,12 +379,11 @@
|
||||
|
||||
.modal {
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 5px 5px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
width: 420px;
|
||||
max-width: 90vw;
|
||||
transform: rotate(-0.3deg);
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
@@ -412,12 +391,12 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: 2px solid var(--color-gray-85);
|
||||
border-bottom: 1px solid var(--default-border-color);
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: var(--text-lg);
|
||||
color: var(--color-gray-85);
|
||||
font-family: 'Georgia', serif;
|
||||
font-family: var(--ui-font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,15 +423,15 @@
|
||||
.modalInput {
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: var(--input-bg-color);
|
||||
color: var(--color-on-surface);
|
||||
font-size: var(--text-sm);
|
||||
outline: none;
|
||||
&:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,25 +444,25 @@
|
||||
|
||||
.modalBtnSecondary {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: 2px;
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--default-border-color);
|
||||
background: transparent;
|
||||
color: var(--color-gray-70);
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
&:hover { background: var(--color-surface-low); transform: rotate(-0.5deg); }
|
||||
box-shadow: var(--shadow-island);
|
||||
&:hover { background: var(--color-surface-low); }
|
||||
}
|
||||
|
||||
.modalBtnPrimary {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: 2px;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--default-border-color);
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
&:hover { background: var(--color-primary-darker); transform: rotate(-0.5deg); }
|
||||
box-shadow: var(--shadow-island);
|
||||
&:hover { background: var(--color-primary-darker); }
|
||||
&:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
}
|
||||
|
||||
@@ -9,51 +9,13 @@ import styles from './Dashboard.module.scss';
|
||||
|
||||
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 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 (
|
||||
<svg className={styles.handChart} viewBox={`0 0 ${w} ${h}`} aria-hidden="true">
|
||||
<path
|
||||
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`}
|
||||
fill="none"
|
||||
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>
|
||||
<div className={styles.progressBarWrap} aria-hidden="true">
|
||||
<div className={styles.progressBarBg} />
|
||||
<div className={styles.progressBarFill} style={{ width: `${pct}%`, background: color }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,7 +31,7 @@ const MiniSparkline: React.FC<{ data: number[]; color?: string }> = ({ data, col
|
||||
const points = data.map((v, i) => {
|
||||
const x = i * stepX;
|
||||
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(' ');
|
||||
|
||||
return (
|
||||
@@ -81,13 +43,8 @@ const MiniSparkline: React.FC<{ data: number[]; color?: string }> = ({ data, col
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -98,8 +55,6 @@ export const Dashboard: React.FC = () => {
|
||||
const { recentDrawings, setRecentDrawings, activity, setActivity } = useDrawingStore();
|
||||
const { user } = useAuthStore();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [showNameModal, setShowNameModal] = useState(false);
|
||||
const [newDrawingName, setNewDrawingName] = useState('');
|
||||
const [statsData, setStatsData] = useState({
|
||||
teams: 0,
|
||||
members: 0,
|
||||
@@ -130,18 +85,11 @@ export const Dashboard: React.FC = () => {
|
||||
loadData();
|
||||
}, [setRecentDrawings, setActivity]);
|
||||
|
||||
const handleCreateDrawing = () => {
|
||||
setNewDrawingName('');
|
||||
setShowNameModal(true);
|
||||
};
|
||||
|
||||
const confirmCreateDrawing = async () => {
|
||||
const title = newDrawingName.trim() || 'Untitled Drawing';
|
||||
const handleCreateDrawing = async () => {
|
||||
setIsCreating(true);
|
||||
setShowNameModal(false);
|
||||
try {
|
||||
const newDrawing = await api.drawings.create({
|
||||
title,
|
||||
title: 'Untitled Drawing',
|
||||
visibility: 'team',
|
||||
});
|
||||
setRecentDrawings([newDrawing, ...recentDrawings]);
|
||||
@@ -231,7 +179,7 @@ export const Dashboard: React.FC = () => {
|
||||
<div className={styles.statIcon} style={{ color: stat.color, borderColor: stat.color }}>
|
||||
<stat.icon size={22} />
|
||||
</div>
|
||||
<HandDrawnChart value={stat.chartValue} max={stat.max} color={stat.color} />
|
||||
<ProgressBar value={stat.chartValue} max={stat.max} color={stat.color} />
|
||||
</div>
|
||||
<div className={styles.statValue} style={{ color: stat.color }}>{stat.value}</div>
|
||||
<div className={styles.statLabel}>{stat.label}</div>
|
||||
@@ -342,35 +290,6 @@ export const Dashboard: React.FC = () => {
|
||||
</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">×</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -407,18 +407,17 @@
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
transform: rotate(-0.3deg);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
|
||||
.presentationLabel {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-gray-70);
|
||||
font-weight: 500;
|
||||
font-family: 'Georgia', serif;
|
||||
font-family: var(--ui-font);
|
||||
}
|
||||
|
||||
.modalOverlay {
|
||||
@@ -433,8 +432,8 @@
|
||||
|
||||
.modal {
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--modal-shadow);
|
||||
width: 420px;
|
||||
max-width: 90vw;
|
||||
|
||||
@@ -34,8 +34,15 @@ interface EditorState {
|
||||
function prepareElementsForImport(sourceElements: LooseElement[], offsetX: number, offsetY: number): LooseElement[] {
|
||||
if (!sourceElements || !sourceElements.length) return [];
|
||||
const idMap = new Map<string, string>();
|
||||
const groupIdMap = new Map<string, string>();
|
||||
sourceElements.forEach((el) => {
|
||||
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) => {
|
||||
const newEl: LooseElement = { ...el };
|
||||
@@ -55,6 +62,10 @@ function prepareElementsForImport(sourceElements: LooseElement[], offsetX: numbe
|
||||
if (newEl.containerId && idMap.has(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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
flex-wrap: wrap;
|
||||
padding: var(--space-5);
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 4px 4px 0 var(--color-gray-85);
|
||||
transform: rotate(0.2deg);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
|
||||
@media (max-width: 640px) {
|
||||
flex-direction: column;
|
||||
@@ -115,9 +114,9 @@
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island);
|
||||
padding: var(--space-3);
|
||||
align-self: flex-start;
|
||||
|
||||
@@ -140,12 +139,12 @@
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: 2px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--color-gray-70);
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-fast) var(--ease-out);
|
||||
background: none;
|
||||
border: 2px solid transparent;
|
||||
border: 1px solid transparent;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: var(--text-sm);
|
||||
@@ -153,17 +152,14 @@
|
||||
&:hover {
|
||||
background: var(--color-surface-low);
|
||||
color: var(--color-on-surface);
|
||||
border-color: var(--color-gray-30);
|
||||
transform: rotate(-0.3deg);
|
||||
border-color: var(--default-border-color);
|
||||
}
|
||||
|
||||
&.folderActive {
|
||||
background: var(--color-surface-primary-container);
|
||||
color: var(--color-primary-darkest);
|
||||
font-weight: 600;
|
||||
border-color: var(--color-gray-85);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
transform: rotate(-0.2deg);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -228,15 +224,13 @@
|
||||
|
||||
.drawingCard {
|
||||
position: relative;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
transform: rotate(0.1deg);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island);
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(0) translate(-1px, -1px);
|
||||
box-shadow: 5px 5px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,9 +306,9 @@
|
||||
top: calc(100% + var(--space-1));
|
||||
right: 0;
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island);
|
||||
min-width: 160px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
@@ -328,7 +322,7 @@
|
||||
text-align: left;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-sm);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--color-on-surface);
|
||||
font-size: var(--text-sm);
|
||||
|
||||
@@ -371,58 +365,55 @@
|
||||
flex-wrap: wrap;
|
||||
padding: var(--space-3);
|
||||
background: var(--color-surface-low);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
|
||||
.newProjectInput {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
background: var(--input-bg-color);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
color: var(--text-primary-color);
|
||||
font-size: var(--text-sm);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
}
|
||||
|
||||
.newProjectBtn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary-darkest);
|
||||
transform: rotate(-0.5deg);
|
||||
}
|
||||
}
|
||||
|
||||
.newProjectBtnCancel {
|
||||
background: none;
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-on-surface);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-low);
|
||||
transform: rotate(-0.5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,12 +457,11 @@
|
||||
|
||||
.modal {
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 5px 5px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
width: 420px;
|
||||
max-width: 90vw;
|
||||
transform: rotate(-0.3deg);
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
@@ -479,13 +469,13 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: 2px solid var(--color-gray-85);
|
||||
border-bottom: 1px solid var(--default-border-color);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: var(--text-lg);
|
||||
color: var(--color-gray-85);
|
||||
font-family: 'Georgia', serif;
|
||||
font-family: var(--ui-font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,8 +504,8 @@
|
||||
.modalInput {
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: var(--input-bg-color);
|
||||
color: var(--color-on-surface);
|
||||
font-size: var(--text-sm);
|
||||
@@ -523,7 +513,7 @@
|
||||
|
||||
&:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,27 +526,27 @@
|
||||
|
||||
.modalBtnSecondary {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: 2px;
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--default-border-color);
|
||||
background: transparent;
|
||||
color: var(--color-gray-70);
|
||||
font-size: var(--text-sm);
|
||||
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 {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: 2px;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--default-border-color);
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
font-size: var(--text-sm);
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -9,17 +9,16 @@
|
||||
margin-bottom: var(--space-8);
|
||||
padding: var(--space-5);
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 4px 4px 0 var(--color-gray-85);
|
||||
transform: rotate(0.1deg);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-85);
|
||||
margin-bottom: var(--space-2);
|
||||
font-family: 'Georgia', serif;
|
||||
font-family: var(--ui-font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +44,10 @@
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-radius: 2px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--color-gray-70);
|
||||
background: none;
|
||||
border: 2px solid transparent;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
transition: all var(--duration-fast) var(--ease-out);
|
||||
@@ -57,17 +56,14 @@
|
||||
&:hover {
|
||||
background: var(--color-surface-low);
|
||||
color: var(--color-on-surface);
|
||||
border-color: var(--color-gray-30);
|
||||
transform: rotate(-0.2deg);
|
||||
border-color: var(--default-border-color);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-surface-primary-container);
|
||||
color: var(--color-primary-darkest);
|
||||
font-weight: 600;
|
||||
border-color: var(--color-gray-85);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
transform: rotate(-0.1deg);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +92,8 @@
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@@ -151,26 +147,24 @@
|
||||
|
||||
.themeOption {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: var(--island-bg-color);
|
||||
color: var(--color-gray-70);
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-fast) var(--ease-out);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-30);
|
||||
box-shadow: var(--shadow-island);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
transform: translate(-1px, -1px);
|
||||
box-shadow: 3px 3px 0 var(--color-primary);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-gray-85);
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,16 @@
|
||||
margin-bottom: var(--space-8);
|
||||
padding: var(--space-5);
|
||||
background: var(--island-bg-color);
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 4px 4px 0 var(--color-gray-85);
|
||||
transform: rotate(-0.2deg);
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-85);
|
||||
margin-bottom: var(--space-2);
|
||||
font-family: 'Georgia', serif;
|
||||
font-family: var(--ui-font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,21 +67,20 @@
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
transition: all 0.15s ease;
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
background: var(--island-bg-color);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-surface-low);
|
||||
transform: translateX(2px) rotate(-0.2deg);
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 2px solid var(--color-gray-30);
|
||||
border-bottom: 1px solid var(--default-border-color);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -98,8 +96,8 @@
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
box-shadow: 2px 2px 0 var(--color-gray-85);
|
||||
border: 1px solid var(--default-border-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
|
||||
.memberInfo {
|
||||
@@ -122,13 +120,12 @@
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
background: var(--color-surface-low);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-70);
|
||||
text-transform: capitalize;
|
||||
box-shadow: 1px 1px 0 var(--color-gray-85);
|
||||
}
|
||||
|
||||
.inviteForm {
|
||||
@@ -139,15 +136,15 @@
|
||||
|
||||
.inviteInput {
|
||||
padding: var(--space-3);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
font-size: var(--text-sm);
|
||||
background: var(--input-bg-color);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,12 +184,11 @@
|
||||
|
||||
.roleSelect {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 2px solid var(--color-gray-30);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
font-size: var(--text-sm);
|
||||
background: var(--input-bg-color);
|
||||
cursor: pointer;
|
||||
box-shadow: 1px 1px 0 var(--color-gray-85);
|
||||
}
|
||||
|
||||
.error {
|
||||
|
||||
@@ -78,15 +78,13 @@
|
||||
|
||||
.templateCard {
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--color-gray-85);
|
||||
border-radius: 2px;
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85);
|
||||
transform: rotate(0.1deg);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-island);
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(0) translate(-1px, -1px);
|
||||
box-shadow: 5px 5px 0 var(--color-gray-85);
|
||||
box-shadow: var(--shadow-island-stronger);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,17 +122,17 @@ a {
|
||||
// ============================================
|
||||
|
||||
.excalidraw {
|
||||
--border-radius-md: 2px;
|
||||
--border-radius-md: var(--border-radius-lg);
|
||||
|
||||
.context-menu {
|
||||
border: 2px solid var(--color-gray-85) !important;
|
||||
border-radius: 2px !important;
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85) !important;
|
||||
border: 1px solid var(--default-border-color) !important;
|
||||
border-radius: var(--border-radius-lg) !important;
|
||||
box-shadow: var(--shadow-island) !important;
|
||||
}
|
||||
|
||||
.library-menu-items-container {
|
||||
border: 2px solid var(--color-gray-85) !important;
|
||||
border-radius: 2px !important;
|
||||
box-shadow: 3px 3px 0 var(--color-gray-85) !important;
|
||||
border: 1px solid var(--default-border-color) !important;
|
||||
border-radius: var(--border-radius-lg) !important;
|
||||
box-shadow: var(--shadow-island) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"c31ff144dc4fee3acd0a-bec551c658216ec9862a",
|
||||
"c31ff144dc4fee3acd0a-f87315abf5d197970540",
|
||||
"c31ff144dc4fee3acd0a-fc5e81ebcffdb7687b8e",
|
||||
"c31ff144dc4fee3acd0a-989f5dcca4211fe0b2e4",
|
||||
"c31ff144dc4fee3acd0a-ac5aa3cfe7537125a151",
|
||||
"c31ff144dc4fee3acd0a-7f990aaafdc09c3794e8"
|
||||
]
|
||||
"failedTests": []
|
||||
}
|
||||
@@ -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 | });
|
||||
```
|
||||
-177
@@ -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 |
|
||||
```
|
||||
-155
@@ -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 |
|
||||
```
|
||||
-187
@@ -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 |
|
||||
```
|
||||
Reference in New Issue
Block a user