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 */
|
/* 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 {
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">×</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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 | });
|
|
||||||
```
|
|
||||||
-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