fix(web_server): update static file path and remove unused index import
Changed static file path to a relative path (`./web/desktop`) for better portability across environments and removed the unused `index` import from `main.rs` to tidy up module references.
This commit is contained in:
parent
6c2a575887
commit
f20771aa85
33 changed files with 2 additions and 4498 deletions
|
|
@ -46,7 +46,7 @@ use crate::session::{create_session, get_session_history, get_sessions, start_se
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::shared::utils::create_conn;
|
use crate::shared::utils::create_conn;
|
||||||
use crate::shared::utils::create_s3_operator;
|
use crate::shared::utils::create_s3_operator;
|
||||||
use crate::web_server::{bot_index, index};
|
use crate::web_server::{bot_index};
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BootstrapProgress {
|
pub enum BootstrapProgress {
|
||||||
StartingBootstrap,
|
StartingBootstrap,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
|
pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
|
||||||
let static_path = Path::new("/home/rodriguez/src/botserver/web/desktop");
|
let static_path = Path::new("./web/desktop");
|
||||||
|
|
||||||
// Serve all static files from desktop directory
|
// Serve all static files from desktop directory
|
||||||
cfg.service(
|
cfg.service(
|
||||||
|
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
<!-- Riot.js component for the dashboard page (converted from app/dashboard/page.tsx) -->
|
|
||||||
<template>
|
|
||||||
<div class="min-h-screen bg-background text-foreground">
|
|
||||||
<main class="container p-4 space-y-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h1 class="text-2xl font-bold text-foreground">Dashboard</h1>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<calendar-date-range-picker />
|
|
||||||
<button class="px-4 py-2 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity">
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<div class="p-6 bg-card border rounded-lg border-border" each={card in cards}>
|
|
||||||
<h3 class="text-sm font-medium text-muted-foreground">{card.title}</h3>
|
|
||||||
<p class="text-2xl font-bold mt-1 text-card-foreground">{card.value}</p>
|
|
||||||
<p class="text-xs text-muted-foreground mt-1">{card.subtext}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
|
||||||
<div class="p-6 bg-card border rounded-lg border-border">
|
|
||||||
<h3 class="text-lg font-medium mb-4 text-card-foreground">Overview</h3>
|
|
||||||
<overview />
|
|
||||||
</div>
|
|
||||||
<div class="p-6 bg-card border rounded-lg space-y-4 border-border">
|
|
||||||
<h3 class="text-lg font-medium text-card-foreground">Recent Sales</h3>
|
|
||||||
<p class="text-card-foreground">You made 265 sales this month.</p>
|
|
||||||
<recent-sales />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script >
|
|
||||||
import { useState } from 'riot';
|
|
||||||
import './style.css';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// Reactive state
|
|
||||||
dateRange: { startDate: new Date(), endDate: new Date() },
|
|
||||||
salesData: [
|
|
||||||
{ name: "Olivia Martin", email: "olivia.martin@email.com", amount: "+$1,999.00" },
|
|
||||||
{ name: "Jackson Lee", email: "jackson.lee@email.com", amount: "+$39.00" },
|
|
||||||
{ name: "Isabella Nguyen", email: "isabella.nguyen@email.com", amount: "+$299.00" },
|
|
||||||
{ name: "William Kim", email: "will@email.com", amount: "+$99.00" },
|
|
||||||
{ name: "Sofia Davis", email: "sofia.davis@email.com", amount: "+$39.00" },
|
|
||||||
],
|
|
||||||
cards: [
|
|
||||||
{ title: "Total Revenue", value: "$45,231.89", subtext: "+20.1% from last month" },
|
|
||||||
{ title: "Subscriptions", value: "+2350", subtext: "+180.1% from last month" },
|
|
||||||
{ title: "Sales", value: "+12,234", subtext: "+19% from last month" },
|
|
||||||
{ title: "Active Now", value: "+573", subtext: "+201 since last hour" },
|
|
||||||
],
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
formatDate(date) {
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: '2-digit',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
async mounted() {
|
|
||||||
// No additional setup needed
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sub‑components
|
|
||||||
// CalendarDateRangePicker
|
|
||||||
components: {
|
|
||||||
'calendar-date-range-picker': {
|
|
||||||
template: `
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button class="px-3 py-1 border rounded text-foreground border-border hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
||||||
@click={setStart}>
|
|
||||||
Start: {formatDate(parent.dateRange.startDate)}
|
|
||||||
</button>
|
|
||||||
<span class="text-foreground">to</span>
|
|
||||||
<button class="px-3 py-1 border rounded text-foreground border-border hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
||||||
@click={setEnd}>
|
|
||||||
End: {formatDate(parent.dateRange.endDate)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
methods: {
|
|
||||||
setStart() {
|
|
||||||
const input = prompt("Enter start date (YYYY-MM-DD)");
|
|
||||||
if (input) {
|
|
||||||
parent.dateRange.startDate = new Date(input);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setEnd() {
|
|
||||||
const input = prompt("Enter end date (YYYY-MM-DD)");
|
|
||||||
if (input) {
|
|
||||||
parent.dateRange.endDate = new Date(input);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formatDate(date) {
|
|
||||||
return parent.formatDate(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Overview
|
|
||||||
overview: {
|
|
||||||
template: `
|
|
||||||
<div class="p-4 border rounded-lg border-border">
|
|
||||||
<div class="flex justify-between items-end h-40">
|
|
||||||
<div each={h, i in [100,80,60,40,20]}
|
|
||||||
class="w-8 opacity-60"
|
|
||||||
style="height:{h}px;background-color:hsl(var(--chart-{(i%5)+1}))">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
},
|
|
||||||
|
|
||||||
// RecentSales
|
|
||||||
'recent-sales': {
|
|
||||||
template: `
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div each={item, i in parent.salesData}
|
|
||||||
class="flex items-center justify-between p-2 border-b border-border">
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-secondary flex items-center justify-center text-secondary-foreground">
|
|
||||||
{item.name[0]}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="font-medium text-foreground">{item.name}</p>
|
|
||||||
<p class="text-sm text-muted-foreground">{item.email}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="font-medium text-foreground">{item.amount}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,342 +0,0 @@
|
||||||
<!-- Riot.js component for the editor page (converted from app/editor/page.tsx) -->
|
|
||||||
<template>
|
|
||||||
<div class="word-clone">
|
|
||||||
<!-- Quick Access Toolbar -->
|
|
||||||
<div class="quick-access">
|
|
||||||
<button class="quick-access-btn" @click={undo}>
|
|
||||||
<svg><!-- Undo icon (use same SVG as in React) --></svg>
|
|
||||||
</button>
|
|
||||||
<button class="quick-access-btn" @click={redo}>
|
|
||||||
<svg><!-- Redo icon --></svg>
|
|
||||||
</button>
|
|
||||||
<div class="title-controls">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="title-input"
|
|
||||||
placeholder="Document name"
|
|
||||||
value={fileName}
|
|
||||||
@input={e => fileName = e.target.value}
|
|
||||||
/>
|
|
||||||
<button class="quick-access-btn" @click={saveDocument}>
|
|
||||||
<svg><!-- Save icon --></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ribbon -->
|
|
||||||
<div class="ribbon">
|
|
||||||
<div class="ribbon-tabs">
|
|
||||||
<button class="ribbon-tab-button {activeTab === 'home' ? 'active' : ''}" @click={() => activeTab = 'home'}>Home</button>
|
|
||||||
<button class="ribbon-tab-button {activeTab === 'insert' ? 'active' : ''}" @click={() => activeTab = 'insert'}>Insert</button>
|
|
||||||
<button class="ribbon-tab-button {activeTab === 'layout' ? 'active' : ''}" @click={() => activeTab = 'layout'}>Layout</button>
|
|
||||||
<button class="ribbon-tab-button {activeTab === 'view' ? 'active' : ''}" @click={() => activeTab = 'view'}>View</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeTab === 'home' && (
|
|
||||||
<div class="ribbon-content">
|
|
||||||
<ribbon-group title="Clipboard">
|
|
||||||
<ribbon-button icon="copy" label="Copy" size="large" @click={copy} />
|
|
||||||
<ribbon-button icon="paste" label="Paste" size="large" @click={paste} />
|
|
||||||
<ribbon-button icon="cut" label="Cut" size="medium" @click={cut} />
|
|
||||||
</ribbon-group>
|
|
||||||
|
|
||||||
<ribbon-group title="Font">
|
|
||||||
<div style="display:flex;flex-direction:column;gap:4px">
|
|
||||||
<div style="display:flex;gap:4px">
|
|
||||||
<select bind={fontFamily} @change={e => setFontFamily(e.target.value)} class="format-select" style="width:120px">
|
|
||||||
<option value="Calibri">Calibri</option>
|
|
||||||
<option value="Arial">Arial</option>
|
|
||||||
<option value="Times New Roman">Times New Roman</option>
|
|
||||||
<option value="Georgia">Georgia</option>
|
|
||||||
<option value="Verdana">Verdana</option>
|
|
||||||
</select>
|
|
||||||
<select bind={fontSize} @change={e => setFontSize(e.target.value)} class="format-select" style="width:60px">
|
|
||||||
<option value="8">8</option>
|
|
||||||
<option value="9">9</option>
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="11">11</option>
|
|
||||||
<option value="12">12</option>
|
|
||||||
<option value="14">14</option>
|
|
||||||
<option value="16">16</option>
|
|
||||||
<option value="18">18</option>
|
|
||||||
<option value="20">20</option>
|
|
||||||
<option value="24">24</option>
|
|
||||||
<option value="28">28</option>
|
|
||||||
<option value="36">36</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:2px">
|
|
||||||
<ribbon-button icon="bold" label="Bold" @click={toggleBold} active={editor?.isActive('bold')} />
|
|
||||||
<ribbon-button icon="italic" label="Italic" @click={toggleItalic} active={editor?.isActive('italic')} />
|
|
||||||
<ribbon-button icon="underline" label="Underline" @click={toggleUnderline} active={editor?.isActive('underline')} />
|
|
||||||
<div class="color-picker-wrapper">
|
|
||||||
<ribbon-button icon="type" label="Text Color" />
|
|
||||||
<input type="color" bind={textColor} @input={e => setTextColor(e.target.value)} class="color-picker" />
|
|
||||||
<div class="color-indicator" style="background-color:{textColor}"></div>
|
|
||||||
</div>
|
|
||||||
<div class="color-picker-wrapper">
|
|
||||||
<ribbon-button icon="highlighter" label="Highlight" />
|
|
||||||
<input type="color" bind={highlightColor} @input={e => setHighlightColor(e.target.value)} class="color-picker" />
|
|
||||||
<div class="color-indicator" style="background-color:{highlightColor}"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ribbon-group>
|
|
||||||
|
|
||||||
<ribbon-group title="Paragraph">
|
|
||||||
<div style="display:flex;flex-direction:column;gap:4px">
|
|
||||||
<div style="display:flex;gap:2px">
|
|
||||||
<ribbon-button icon="align-left" label="Align Left" @click={alignLeft} active={editor?.isActive({textAlign:'left'})} />
|
|
||||||
<ribbon-button icon="align-center" label="Center" @click={alignCenter} active={editor?.isActive({textAlign:'center'})} />
|
|
||||||
<ribbon-button icon="align-right" label="Align Right" @click={alignRight} active={editor?.isActive({textAlign:'right'})} />
|
|
||||||
<ribbon-button icon="align-justify" label="Justify" @click={alignJustify} active={editor?.isActive({textAlign:'justify'})} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ribbon-group>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'insert' && (
|
|
||||||
<div class="ribbon-content">
|
|
||||||
<ribbon-group title="Illustrations">
|
|
||||||
<ribbon-button icon="image" label="Picture" size="large" @click={addImage} />
|
|
||||||
</ribbon-group>
|
|
||||||
|
|
||||||
<ribbon-group title="Tables">
|
|
||||||
<ribbon-button icon="table" label="Table" size="large" @click={insertTable} />
|
|
||||||
</ribbon-group>
|
|
||||||
|
|
||||||
<ribbon-group title="Links">
|
|
||||||
<ribbon-button icon="link" label="Link" size="large" @click={addLink} active={editor?.isActive('link')} />
|
|
||||||
</ribbon-group>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'view' && (
|
|
||||||
<div class="ribbon-content">
|
|
||||||
<ribbon-group title="Zoom">
|
|
||||||
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
|
|
||||||
<div style="font-size:14px;font-weight:600">{zoom}%</div>
|
|
||||||
<input type="range" min="50" max="200" bind={zoom} @input={e => zoom = parseInt(e.target.value)} class="zoom-slider" />
|
|
||||||
</div>
|
|
||||||
</ribbon-group>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Editor Area -->
|
|
||||||
<div class="editor-container">
|
|
||||||
<div class="editor-main">
|
|
||||||
<div class="pages-container">
|
|
||||||
<div each={pageNum in pages} class="page">
|
|
||||||
<div class="page-number">Page {pageNum}</div>
|
|
||||||
<div class="page-content">
|
|
||||||
{pageNum === 1 && <editor-content bind={editor} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bubble Menu -->
|
|
||||||
{editor && (
|
|
||||||
<bubble-menu bind={editor}>
|
|
||||||
<ribbon-button icon="bold" label="Bold" @click={toggleBold} active={editor.isActive('bold')} />
|
|
||||||
<ribbon-button icon="italic" label="Italic" @click={toggleItalic} active={editor.isActive('italic')} />
|
|
||||||
<ribbon-button icon="underline" label="Underline" @click={toggleUnderline} active={editor.isActive('underline')} />
|
|
||||||
<ribbon-button icon="link" label="Link" @click={addLink} active={editor.isActive('link')} />
|
|
||||||
</bubble-menu>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<!-- Status Bar -->
|
|
||||||
<div class="status-bar">
|
|
||||||
<div>Page {pages.length} of {pages.length} | Words: {editor?.storage?.characterCount?.words() || 0}</div>
|
|
||||||
<div class="zoom-controls">
|
|
||||||
<button @click={() => zoom = Math.max(50, zoom - 10)}>-</button>
|
|
||||||
<span>{zoom}%</span>
|
|
||||||
<button @click={() => zoom = Math.min(200, zoom + 10)}>+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden file input -->
|
|
||||||
<input type="file" bind={fileInputRef} @change={handleImageUpload} accept="image/*" style="display:none" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script >
|
|
||||||
<!-- Removed unused Riot imports -->
|
|
||||||
import { useEditor, EditorContent, BubbleMenu, AnyExtension } from '@tiptap/react';
|
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
|
||||||
import Bold from '@tiptap/extension-bold';
|
|
||||||
import Italic from '@tiptap/extension-italic';
|
|
||||||
import History from '@tiptap/extension-history';
|
|
||||||
import TextStyle from '@tiptap/extension-text-style';
|
|
||||||
import FontFamily from '@tiptap/extension-font-family';
|
|
||||||
import Color from '@tiptap/extension-color';
|
|
||||||
import Highlight from '@tiptap/extension-highlight';
|
|
||||||
import TextAlign from '@tiptap/extension-text-align';
|
|
||||||
import Link from '@tiptap/extension-link';
|
|
||||||
import Image from '@tiptap/extension-image';
|
|
||||||
import Underline from '@tiptap/extension-underline';
|
|
||||||
import Table from '@tiptap/extension-table';
|
|
||||||
import TableCell from '@tiptap/extension-table-cell';
|
|
||||||
import TableHeader from '@tiptap/extension-table-header';
|
|
||||||
import TableRow from '@tiptap/extension-table-row';
|
|
||||||
import './style.css';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// Reactive state
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
fileName: 'Document 1',
|
|
||||||
fontSize: '12',
|
|
||||||
fontFamily: 'Calibri',
|
|
||||||
textColor: '#000000',
|
|
||||||
highlightColor: '#ffff00',
|
|
||||||
activeTab: 'home',
|
|
||||||
zoom: 100,
|
|
||||||
pages: [1],
|
|
||||||
editor: null,
|
|
||||||
fileInputRef: null,
|
|
||||||
content: '',
|
|
||||||
activeTab: 'home'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
async mounted() {
|
|
||||||
this.editor = useEditor({
|
|
||||||
extensions: [
|
|
||||||
StarterKit.configure({ history: false }),
|
|
||||||
Bold,
|
|
||||||
Italic,
|
|
||||||
History,
|
|
||||||
TextStyle,
|
|
||||||
FontFamily,
|
|
||||||
Color,
|
|
||||||
Highlight.configure({ multicolor: true }),
|
|
||||||
TextAlign.configure({ types: ['heading', 'paragraph', 'tableCell'] }),
|
|
||||||
Link.configure({ openOnClick: false }),
|
|
||||||
Image,
|
|
||||||
Underline,
|
|
||||||
Table.configure({
|
|
||||||
resizable: true,
|
|
||||||
HTMLAttributes: { class: 'editor-table' },
|
|
||||||
}),
|
|
||||||
TableRow,
|
|
||||||
TableHeader,
|
|
||||||
TableCell,
|
|
||||||
],
|
|
||||||
content: `
|
|
||||||
<h1 style="text-align: center; font-size: 24px; margin-bottom: 20px;">${this.fileName}</h1>
|
|
||||||
<p><br></p>
|
|
||||||
<p>Start typing your document here...</p>
|
|
||||||
<p><br></p>
|
|
||||||
`,
|
|
||||||
onUpdate: ({ editor }) => {
|
|
||||||
const element = document.querySelector('.ProseMirror');
|
|
||||||
if (element) {
|
|
||||||
const contentHeight = element.scrollHeight;
|
|
||||||
const pageHeight = 1123; // A4 height in pixels at 96 DPI
|
|
||||||
const neededPages = Math.max(1, Math.ceil(contentHeight / pageHeight));
|
|
||||||
if (neededPages !== this.pages.length) {
|
|
||||||
this.pages = Array.from({ length: neededPages }, (_, i) => i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
undo() {
|
|
||||||
this.editor?.chain().focus().undo().run();
|
|
||||||
},
|
|
||||||
redo() {
|
|
||||||
this.editor?.chain().focus().redo().run();
|
|
||||||
},
|
|
||||||
saveDocument() {
|
|
||||||
if (!this.editor) return;
|
|
||||||
const content = this.editor.getHTML();
|
|
||||||
const blob = new Blob([content], { type: 'text/html' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `${this.fileName}.html`;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
},
|
|
||||||
setFontFamily(value) {
|
|
||||||
this.fontFamily = value;
|
|
||||||
this.editor?.chain().focus().setFontFamily(value).run();
|
|
||||||
},
|
|
||||||
setFontSize(value) {
|
|
||||||
this.fontSize = value;
|
|
||||||
// TipTap does not have a built‑in font‑size extension; you may need a custom one.
|
|
||||||
},
|
|
||||||
setTextColor(value) {
|
|
||||||
this.textColor = value;
|
|
||||||
this.editor?.chain().focus().setColor(value).run();
|
|
||||||
},
|
|
||||||
setHighlightColor(value) {
|
|
||||||
this.highlightColor = value;
|
|
||||||
this.editor?.chain().focus().setHighlight({ color: value }).run();
|
|
||||||
},
|
|
||||||
toggleBold() {
|
|
||||||
this.editor?.chain().focus().toggleBold().run();
|
|
||||||
},
|
|
||||||
toggleItalic() {
|
|
||||||
this.editor?.chain().focus().toggleItalic().run();
|
|
||||||
},
|
|
||||||
toggleUnderline() {
|
|
||||||
this.editor?.chain().focus().toggleUnderline().run();
|
|
||||||
},
|
|
||||||
alignLeft() {
|
|
||||||
this.editor?.chain().focus().setTextAlign('left').run();
|
|
||||||
},
|
|
||||||
alignCenter() {
|
|
||||||
this.editor?.chain().focus().setTextAlign('center').run();
|
|
||||||
},
|
|
||||||
alignRight() {
|
|
||||||
this.editor?.chain().focus().setTextAlign('right').run();
|
|
||||||
},
|
|
||||||
alignJustify() {
|
|
||||||
this.editor?.chain().focus().setTextAlign('justify').run();
|
|
||||||
},
|
|
||||||
addImage() {
|
|
||||||
if (this.fileInputRef) this.fileInputRef.click();
|
|
||||||
},
|
|
||||||
handleImageUpload(e) {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file && this.editor) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
const imageUrl = event.target?.result;
|
|
||||||
this.editor.chain().focus().setImage({ src: imageUrl }).run();
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
insertTable() {
|
|
||||||
this.editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
||||||
},
|
|
||||||
addLink() {
|
|
||||||
const previousUrl = this.editor?.getAttributes('link').href;
|
|
||||||
const url = window.prompt('URL', previousUrl);
|
|
||||||
if (url === null) return;
|
|
||||||
if (url === '') {
|
|
||||||
this.editor?.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
|
||||||
},
|
|
||||||
copy() {
|
|
||||||
document.execCommand('copy');
|
|
||||||
},
|
|
||||||
paste() {
|
|
||||||
document.execCommand('paste');
|
|
||||||
},
|
|
||||||
cut() {
|
|
||||||
document.execCommand('cut');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
:root {
|
|
||||||
/* 3DBevel Theme */
|
|
||||||
--background: 0 0% 80%;
|
|
||||||
--foreground: 0 0% 10%;
|
|
||||||
--card: 0 0% 75%;
|
|
||||||
--card-foreground: 0 0% 10%;
|
|
||||||
--popover: 0 0% 80%;
|
|
||||||
--popover-foreground: 0 0% 10%;
|
|
||||||
--primary: 210 80% 40%;
|
|
||||||
--primary-foreground: 0 0% 80%;
|
|
||||||
--secondary: 0 0% 70%;
|
|
||||||
--secondary-foreground: 0 0% 10%;
|
|
||||||
--muted: 0 0% 65%;
|
|
||||||
--muted-foreground: 0 0% 30%;
|
|
||||||
--accent: 30 80% 40%;
|
|
||||||
--accent-foreground: 0 0% 80%;
|
|
||||||
--destructive: 0 85% 60%;
|
|
||||||
--destructive-foreground: 0 0% 98%;
|
|
||||||
--border: 0 0% 70%;
|
|
||||||
--input: 0 0% 70%;
|
|
||||||
--ring: 210 80% 40%;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.word-clone {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: hsl(var(--background));
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Title Bar */
|
|
||||||
.title-bar {
|
|
||||||
background: hsl(var(--primary));
|
|
||||||
color: hsl(var(--primary-foreground));
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 2px solid hsl(var(--border));
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar h1 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-input {
|
|
||||||
background: hsl(var(--input));
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quick Access Toolbar */
|
|
||||||
.quick-access {
|
|
||||||
background: hsl(var(--card));
|
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
|
||||||
padding: 4px 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-access-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-access-btn:hover {
|
|
||||||
background: hsl(var(--muted));
|
|
||||||
border-color: hsl(var(--border));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ribbon */
|
|
||||||
.ribbon {
|
|
||||||
background: hsl(var(--card));
|
|
||||||
border-bottom: 2px solid hsl(var(--border));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tabs {
|
|
||||||
display: flex;
|
|
||||||
background: hsl(var(--muted));
|
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-button {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-button:hover {
|
|
||||||
background: hsl(var(--secondary));
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-tab-button.active {
|
|
||||||
background: hsl(var(--card));
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
border-bottom-color: hsl(var(--primary));
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-content {
|
|
||||||
display: flex;
|
|
||||||
padding: 8px;
|
|
||||||
gap: 2px;
|
|
||||||
min-height: 80px;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-right: 1px solid hsl(var(--border));
|
|
||||||
padding-right: 8px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-group:last-child {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-group-content {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 2px;
|
|
||||||
flex: 1;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-group-title {
|
|
||||||
font-size: 10px;
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 4px;
|
|
||||||
border-top: 1px solid hsl(var(--border));
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
transition: all 0.2s;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button:hover {
|
|
||||||
background: hsl(var(--muted));
|
|
||||||
border-color: hsl(var(--border));
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button.active {
|
|
||||||
background: hsl(var(--primary));
|
|
||||||
color: hsl(var(--primary-foreground));
|
|
||||||
border-color: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button.medium {
|
|
||||||
padding: 6px;
|
|
||||||
min-width: 32px;
|
|
||||||
min-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button.large {
|
|
||||||
padding: 8px;
|
|
||||||
min-width: 48px;
|
|
||||||
min-height: 48px;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbon-button-label {
|
|
||||||
font-size: 10px;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-arrow {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Format Controls */
|
|
||||||
.format-select {
|
|
||||||
background: hsl(var(--input));
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-picker-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-picker {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-indicator {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 16px;
|
|
||||||
height: 3px;
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Editor Area */
|
|
||||||
.editor-container {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
background: hsl(var(--muted));
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-sidebar {
|
|
||||||
width: 200px;
|
|
||||||
background: hsl(var(--card));
|
|
||||||
border-right: 1px solid hsl(var(--border));
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(100vh - 200px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pages-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
/* Example: Use a CSS variable for zoom, set --zoom: 1 for 100% */
|
|
||||||
transform: scale(var(--zoom, 1));
|
|
||||||
transform-origin: top center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
width: 210mm;
|
|
||||||
min-height: 297mm;
|
|
||||||
background: white;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 1px hsl(var(--border)),
|
|
||||||
0 4px 8px rgba(0, 0, 0, 0.1),
|
|
||||||
0 8px 16px rgba(0, 0, 0, 0.05);
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-number {
|
|
||||||
position: absolute;
|
|
||||||
top: -30px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 12px;
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
background: hsl(var(--background));
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-content {
|
|
||||||
padding: 25mm;
|
|
||||||
min-height: 247mm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror {
|
|
||||||
outline: none;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror a {
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table styles */
|
|
||||||
.editor-table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 16px 0;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-table td,
|
|
||||||
.editor-table th {
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
padding: 8px 12px;
|
|
||||||
min-width: 50px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-table th {
|
|
||||||
background: hsl(var(--muted));
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bubble Menu */
|
|
||||||
.bubble-menu {
|
|
||||||
background: hsl(var(--card));
|
|
||||||
border: 1px solid hsl(var(--border));
|
|
||||||
border-radius: var(--radius);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
display: flex;
|
|
||||||
padding: 4px;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-menu .ribbon-button {
|
|
||||||
min-width: 28px;
|
|
||||||
min-height: 28px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status Bar */
|
|
||||||
.status-bar {
|
|
||||||
background: hsl(var(--card));
|
|
||||||
border-top: 1px solid hsl(var(--border));
|
|
||||||
padding: 4px 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 11px;
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoom-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoom-slider {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
|
|
||||||
.title-bar,
|
|
||||||
.quick-access,
|
|
||||||
.ribbon,
|
|
||||||
.editor-sidebar,
|
|
||||||
.status-bar {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-main {
|
|
||||||
padding: 0;
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pages-container {
|
|
||||||
transform: none;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
box-shadow: none;
|
|
||||||
margin: 0;
|
|
||||||
break-after: page;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-number {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<!-- Riot.js component for the news page (converted from app/news/page.tsx) -->
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col h-screen bg-gray-50">
|
|
||||||
<!-- Add news page content here -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script >
|
|
||||||
import './style.css';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// No additional state needed for this simple page
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
<!-- Riot.js component for the paper page (converted from app/paper/page.tsx) -->
|
|
||||||
<template>
|
|
||||||
<div class="min-h-screen bg-background text-foreground">
|
|
||||||
<div>
|
|
||||||
<div class="max-w-4xl mx-auto">
|
|
||||||
<!-- Paper Shadow Effect -->
|
|
||||||
<div class="mx-4 my-8 bg-card rounded-lg shadow-2xl shadow-black/20 border border-border">
|
|
||||||
<editor-content bind={editor} class="min-h-[calc(100vh-12rem)]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Floating Selection Toolbar -->
|
|
||||||
{editor && (
|
|
||||||
<bubble-menu bind={editor}>
|
|
||||||
<div class="flex items-center bg-card border border-border rounded-lg shadow-lg p-1">
|
|
||||||
<!-- Text Formatting -->
|
|
||||||
<button @click={() => editor.chain().focus().toggleBold().run()}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('bold') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Bold">
|
|
||||||
<BoldIcon class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click={() => editor.chain().focus().toggleItalic().run()}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('italic') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Italic">
|
|
||||||
<ItalicIcon class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click={() => editor.chain().focus().toggleUnderline().run()}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('underline') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Underline">
|
|
||||||
<Underline class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="w-px h-6 bg-border mx-1"></div>
|
|
||||||
|
|
||||||
<!-- Text Alignment -->
|
|
||||||
<button @click={() => editor.chain().focus().setTextAlign('left').run()}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive({textAlign:'left'}) ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Align Left">
|
|
||||||
<AlignLeft class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click={() => editor.chain().focus().setTextAlign('center').run()}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive({textAlign:'center'}) ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Align Center">
|
|
||||||
<AlignCenter class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click={() => editor.chain().focus().setTextAlign('right').run()}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive({textAlign:'right'}) ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Align Right">
|
|
||||||
<AlignRight class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="w-px h-6 bg-border mx-1"></div>
|
|
||||||
|
|
||||||
<!-- Highlight -->
|
|
||||||
<button @click={() => editor.chain().focus().toggleHighlight({color:'#ffff00'}).run()}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('highlight') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Highlight">
|
|
||||||
<Highlighter class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Link -->
|
|
||||||
<button @click={addLink}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('link') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Add Link">
|
|
||||||
<Link class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="w-px h-6 bg-border mx-1"></div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<button @click={() => {
|
|
||||||
if (editor.isActive('heading')) {
|
|
||||||
editor.chain().focus().setNode('paragraph').run();
|
|
||||||
} else {
|
|
||||||
editor.chain().focus().setNode('heading', {level:2}).run();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
class="p-2 rounded hover:bg-accent transition-colors {editor.isActive('heading') ? 'bg-primary text-primary-foreground' : 'text-foreground'}"
|
|
||||||
title="Heading">
|
|
||||||
<Type class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</bubble-menu>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer-component shortcuts="{shortcuts}" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script >
|
|
||||||
import { useState, useRef, useEffect } from 'riot';
|
|
||||||
import { useEditor, EditorContent, BubbleMenu, AnyExtension } from '@tiptap/react';
|
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
|
||||||
import Bold from '@tiptap/extension-bold';
|
|
||||||
import Italic from '@tiptap/extension-italic';
|
|
||||||
import TextStyle from '@tiptap/extension-text-style';
|
|
||||||
import Color from '@tiptap/extension-color';
|
|
||||||
import Highlight from '@tiptap/extension-highlight';
|
|
||||||
import TextAlign from '@tiptap/extension-text-align';
|
|
||||||
import Link from '@tiptap/extension-link';
|
|
||||||
import Underline from '@tiptap/extension-underline';
|
|
||||||
import { AlignLeft, AlignCenter, AlignRight, BoldIcon, ItalicIcon, Highlighter, Type, Link as LinkIcon, Underline as UnderlineIcon } from 'lucide-react';
|
|
||||||
import Footer from '../footer';
|
|
||||||
import './style.css';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// Reactive state
|
|
||||||
editor: null,
|
|
||||||
shortcuts: [],
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
async mounted() {
|
|
||||||
this.editor = useEditor({
|
|
||||||
extensions: [
|
|
||||||
StarterKit.configure(),
|
|
||||||
Bold,
|
|
||||||
Italic,
|
|
||||||
TextStyle,
|
|
||||||
Color,
|
|
||||||
Highlight.configure({ multicolor: true }),
|
|
||||||
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
|
||||||
Link,
|
|
||||||
Underline,
|
|
||||||
],
|
|
||||||
content: `<p>Start writing your thoughts here...</p>`,
|
|
||||||
editorProps: {
|
|
||||||
attributes: {
|
|
||||||
class: 'prose prose-invert max-w-none focus:outline-none min-h-[calc(100vh-8rem)] p-8 text-foreground',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize shortcuts (same as original component)
|
|
||||||
this.shortcuts = [
|
|
||||||
[
|
|
||||||
{ key: 'Q', label: 'Resume', action: () => {} },
|
|
||||||
{ key: 'W', label: 'Write', action: () => {} },
|
|
||||||
{ key: 'E', label: 'Expand', action: () => {} },
|
|
||||||
{ key: 'R', label: 'One Word', action: () => {} },
|
|
||||||
{ key: 'T', label: 'As List', action: () => {} },
|
|
||||||
{ key: 'Y', label: 'As Mail', action: () => {} },
|
|
||||||
{ key: 'U', label: 'Copy', action: () => document.execCommand('copy') },
|
|
||||||
{ key: 'I', label: 'Paste', action: () => document.execCommand('paste') },
|
|
||||||
{ key: 'O', label: 'Undo', action: () => this.editor?.chain().focus().undo().run() },
|
|
||||||
{ key: 'P', label: 'Redo', action: () => this.editor?.chain().focus().redo().run() },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ key: 'A', label: 'Select', action: () => {} },
|
|
||||||
{ key: 'S', label: 'Select All', action: () => this.editor?.chain().focus().selectAll().run() },
|
|
||||||
{ key: 'D', label: 'Deselect', action: () => {} },
|
|
||||||
{ key: 'G', label: 'Random', action: () => {} },
|
|
||||||
{ key: 'H', label: 'Idea', action: () => {} },
|
|
||||||
{ key: 'J', label: 'Insert Link', action: this.addLink },
|
|
||||||
{ key: 'K', label: 'Highlight', action: () => this.editor?.chain().focus().toggleHighlight({color:'#ffff00'}).run() },
|
|
||||||
{ key: 'L', label: 'To-Do', action: () => {} },
|
|
||||||
{ key: 'Z', label: 'Zoom In', action: () => {} },
|
|
||||||
{ key: 'X', label: 'Zoom Out', action: () => {} },
|
|
||||||
]
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
addLink() {
|
|
||||||
const previousUrl = this.editor?.getAttributes('link').href;
|
|
||||||
const url = window.prompt('Enter URL:', previousUrl);
|
|
||||||
if (url === null) return;
|
|
||||||
if (url === '') {
|
|
||||||
this.editor?.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
.ProseMirror {
|
|
||||||
outline: none;
|
|
||||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
padding: 3rem;
|
|
||||||
|
|
||||||
min-height: calc(100vh - 12rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 2rem 0 1rem 0;
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 1.5rem 0 0.75rem 0;
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 1.25rem 0 0.5rem 0;
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror p {
|
|
||||||
margin: 0.75rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror a {
|
|
||||||
color: hsl(var(--accent));
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror a:hover {
|
|
||||||
color: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror mark {
|
|
||||||
background-color: #ffff0040;
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror ul, .ProseMirror ol {
|
|
||||||
margin: 1rem 0;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror li {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror blockquote {
|
|
||||||
border-left: 4px solid hsl(var(--primary));
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
font-style: italic;
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror code {
|
|
||||||
background-color: hsl(var(--muted));
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror pre {
|
|
||||||
background-color: hsl(var(--muted));
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror pre code {
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selection highlighting */
|
|
||||||
.ProseMirror ::selection {
|
|
||||||
background-color: hsl(var(--primary) / 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Placeholder styling */
|
|
||||||
.ProseMirror p.is-editor-empty:first-child::before {
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
float: left;
|
|
||||||
color: hsl(var(--muted-foreground));
|
|
||||||
pointer-events: none;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<!-- Basic player page -->
|
|
||||||
<template>
|
|
||||||
<div class="player-container">
|
|
||||||
<h1>Player</h1>
|
|
||||||
<div class="player-content">
|
|
||||||
<p>Player functionality will be implemented here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import './style.css';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentTrack: null,
|
|
||||||
isPlaying: false,
|
|
||||||
volume: 80,
|
|
||||||
currentTime: 0,
|
|
||||||
duration: 0,
|
|
||||||
playlist: [],
|
|
||||||
shuffle: false,
|
|
||||||
repeat: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
play() {
|
|
||||||
this.isPlaying = true;
|
|
||||||
},
|
|
||||||
pause() {
|
|
||||||
this.isPlaying = false;
|
|
||||||
},
|
|
||||||
setVolume(vol) {
|
|
||||||
this.volume = Math.max(0, Math.min(100, vol));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
.slider::-webkit-slider-thumb {
|
|
||||||
appearance: none;
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: hsl(var(--primary));
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid hsl(var(--primary-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider::-moz-range-thumb {
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: hsl(var(--primary));
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid hsl(var(--primary-foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider::-webkit-slider-track {
|
|
||||||
height: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: hsl(var(--muted));
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider::-moz-range-track {
|
|
||||||
height: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: hsl(var(--muted));
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
https://whoapi.com/domain-availability-api-pricing/
|
|
||||||
|
|
||||||
|
|
||||||
- **Ports Used**:
|
|
||||||
Main website: (https://www.pragmatismo.com.br).
|
|
||||||
Webmail (Stalwart): (https://mail.pragmatismo.com.br).
|
|
||||||
Database (PostgreSQL): .
|
|
||||||
SSO (Zitadel): (https://sso.pragmatismo.com.br).
|
|
||||||
Storage (MinIO): (https://drive.pragmatismo.com.br).
|
|
||||||
ALM (Forgejo): (https://alm.pragmatismo.com.br).
|
|
||||||
BotServer : (https://gb.pragmatismo.com.br).
|
|
||||||
Meeting: (https://call.pragmatismo.com.br).
|
|
||||||
IMAP: 993.
|
|
||||||
SMTP: 465.
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<!-- Riot.js component for the account form (converted from app/settings/account/account-form.tsx) -->
|
|
||||||
<template>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<controller name="name">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium mb-1">Name</label>
|
|
||||||
<input class="w-full p-2 border rounded"
|
|
||||||
bind="{name}"
|
|
||||||
placeholder="Your name" />
|
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
|
||||||
This is the name that will be displayed on your profile and in emails.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</controller>
|
|
||||||
|
|
||||||
<controller name="dob">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium mb-1">Date of birth</label>
|
|
||||||
<button type="button"
|
|
||||||
class="w-full p-2 border rounded text-left"
|
|
||||||
@click="{() => showDatePicker = true}">
|
|
||||||
{value.toDateString()}
|
|
||||||
</button>
|
|
||||||
{showDatePicker && (
|
|
||||||
<input type="date"
|
|
||||||
bind="{dob}"
|
|
||||||
@change="{e => { showDatePicker = false; if (e.target.value) { dob = new Date(e.target.value); }}}"
|
|
||||||
class="mt-1 p-1 border rounded" />
|
|
||||||
)}
|
|
||||||
<p class="text-sm text-gray-5
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<!-- Placeholder Riot component for appearance-form -->
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,38 +0,0 @@
|
||||||
<!-- Riot.js component for the settings layout (converted from app/settings/layout.tsx) -->
|
|
||||||
<template>
|
|
||||||
<div class="flex-1 overflow-auto">
|
|
||||||
<div class="p-5">
|
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-2xl font-bold">Settings</h1>
|
|
||||||
<p class="text-sm text-gray-500">
|
|
||||||
Manage your account settings and set e-mail preferences.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="border-t border-gray-200 my-6"></div>
|
|
||||||
<div class="flex flex-col md:flex-row gap-6">
|
|
||||||
<div class="w-full md:w-1/4">
|
|
||||||
<sidebar-nav items="{sidebarNavItems}" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script >
|
|
||||||
import './style.css';
|
|
||||||
import './components/sidebar-nav.html';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// Reactive state
|
|
||||||
sidebarNavItems: [
|
|
||||||
{ title: "Profile", href: "/settings" },
|
|
||||||
{ title: "Account", href: "/settings/account" },
|
|
||||||
{ title: "Appearance", href: "/settings/appearance" },
|
|
||||||
{ title: "Notifications", href: "/settings/notifications" },
|
|
||||||
{ title: "Display", href: "/settings/display" },
|
|
||||||
]
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<!-- Riot.js component for the settings page (converted from app/settings/page.tsx) -->
|
|
||||||
<template>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-medium">Profile</h2>
|
|
||||||
<p class="text-sm text-gray-500"></p>
|
|
||||||
</div>
|
|
||||||
<div class="border-t border-gray-200 my-4"></div>
|
|
||||||
<profile-form></profile-form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script >
|
|
||||||
import './profile-form.html';
|
|
||||||
import './style.css';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// No additional state needed
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
<!-- Riot.js component for the profile form (converted from app/settings/profile-form.tsx) -->
|
|
||||||
<template>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<controller name="username">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium mb-1">Username</label>
|
|
||||||
<input class="w-full p-2 border rounded {errors.username ? 'border-red-500' : 'border-gray-300'}"
|
|
||||||
bind="{username}"
|
|
||||||
placeholder="Enter username" />
|
|
||||||
{errors.username && <p class="text-red-500 text-xs mt-1">{errors.username.message}</p>}
|
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
|
||||||
This is your public display name. It can be your real name or a pseudonym. You can only change this once every 30 days.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</controller>
|
|
||||||
|
|
||||||
<controller name="email">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium mb-1">Email</label>
|
|
||||||
<input type="email"
|
|
||||||
class="w-full p-2 border rounded {errors.email ? 'border-red-500' : 'border-gray-300'}"
|
|
||||||
bind="{email}"
|
|
||||||
placeholder="Enter email" />
|
|
||||||
{errors.email && <p class="text-red-500 text-xs mt-1">{errors.email.message}</p>}
|
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
|
||||||
You can manage verified email addresses in your email settings.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</controller>
|
|
||||||
|
|
||||||
<controller name="bio">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium mb-1">Bio</label>
|
|
||||||
<textarea class="w-full p-2 border rounded {errors.bio ? 'border-red-500' : 'border-gray-300'}"
|
|
||||||
bind="{bio}"
|
|
||||||
rows="4"
|
|
||||||
placeholder="Tell us a little bit about yourself"></textarea>
|
|
||||||
{errors.bio && <p class="text-red-500 text-xs mt-1">{errors.bio.message}</p>}
|
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
|
||||||
You can @mention other users and organizations to link to them.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</controller>
|
|
||||||
|
|
||||||
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
|
||||||
@click="{handleSubmit(onSubmit)}">
|
|
||||||
Update profile
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script >
|
|
||||||
import { useState } from 'riot';
|
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import './style.css';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// Reactive state
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
username: '',
|
|
||||||
email: '',
|
|
||||||
bio: '',
|
|
||||||
errors: {
|
|
||||||
username: null,
|
|
||||||
email: null,
|
|
||||||
bio: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Validation schema
|
|
||||||
schema: z.object({
|
|
||||||
username: z.string().min(2, { message: "Username must be at least 2 characters." }).max(30, { message: "Username must not be longer than 30 characters." }),
|
|
||||||
email: z.string().email(),
|
|
||||||
bio: z.string().min(4).max(160)
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
handleSubmit(callback) {
|
|
||||||
const result = this.schema.safeParse({
|
|
||||||
username: this.username,
|
|
||||||
email: this.email,
|
|
||||||
bio: this.bio
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear previous errors
|
|
||||||
this.errors = {
|
|
||||||
username: null,
|
|
||||||
email: null,
|
|
||||||
bio: null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
callback(result.data);
|
|
||||||
} else {
|
|
||||||
result.error.errors.forEach(err => {
|
|
||||||
this.errors[err.path[0]] = err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onSubmit(data) {
|
|
||||||
console.log(JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/* Settings page styles */
|
|
||||||
.settings-container {
|
|
||||||
padding: 20px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-section {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Prompts come from: https://github.com/0xeb/TheBigPromptLibrary
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +0,0 @@
|
||||||
<template x-data="{ items: [] }">
|
|
||||||
<div class="todo-stats">
|
|
||||||
<span x-text="`${items.filter(i => !i.done).length} items left`"></span>
|
|
||||||
<button @click="$store.todo.clearCompleted()">
|
|
||||||
Clear completed
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.todo-stats {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-stats button {
|
|
||||||
background: none;
|
|
||||||
color: #666;
|
|
||||||
padding: 0;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="formula-bar">
|
|
||||||
<div class="cell-ref" id="cellRef">A1</div>
|
|
||||||
<input type="text" class="formula-input" id="formulaInput" placeholder="Enter value or formula (=SUM, =AVERAGE, etc.)">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<template x-data>
|
|
||||||
<h3 x-text="title + ' App'"></h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
title: 'Tables'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="app-container">
|
|
||||||
<Header />
|
|
||||||
<Toolbar />
|
|
||||||
<FormulaBar />
|
|
||||||
<Spreadsheet />
|
|
||||||
<StatusBar />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Header from './Header.html'
|
|
||||||
import Toolbar from './Toolbar.html'
|
|
||||||
import FormulaBar from './FormulaBar.html'
|
|
||||||
import Spreadsheet from './Spreadsheet.html'
|
|
||||||
import StatusBar from './StatusBar.html'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { Header, Toolbar, FormulaBar, Spreadsheet, StatusBar }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,364 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="spreadsheet-container" id="spreadsheetContainer">
|
|
||||||
<table class="spreadsheet" id="spreadsheet">
|
|
||||||
<thead id="tableHead"></thead>
|
|
||||||
<tbody id="tableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
class TablesApp {
|
|
||||||
constructor() {
|
|
||||||
this.data = this.generateMockData(100, 26); // 100 rows, 26 columns (A-Z)
|
|
||||||
this.selectedCell = null;
|
|
||||||
this.cols = 26;
|
|
||||||
this.rows = 100;
|
|
||||||
this.visibleRows = 30;
|
|
||||||
this.rowOffset = 0;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateMockData(rows, cols) {
|
|
||||||
const data = [];
|
|
||||||
const products = ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Headphones', 'Webcam', 'Desk', 'Chair'];
|
|
||||||
const regions = ['North', 'South', 'East', 'West'];
|
|
||||||
|
|
||||||
for (let i = 0; i < rows; i++) {
|
|
||||||
const row = {};
|
|
||||||
for (let j = 0; j < cols; j++) {
|
|
||||||
const col = this.getColumnName(j);
|
|
||||||
if (i === 0) {
|
|
||||||
// Header row
|
|
||||||
if (j === 0) row[col] = 'Product';
|
|
||||||
else if (j === 1) row[col] = 'Region';
|
|
||||||
else if (j === 2) row[col] = 'Q1';
|
|
||||||
else if (j === 3) row[col] = 'Q2';
|
|
||||||
else if (j === 4) row[col] = 'Q3';
|
|
||||||
else if (j === 5) row[col] = 'Q4';
|
|
||||||
else if (j === 6) row[col] = 'Total';
|
|
||||||
else row[col] = `Col ${col}`;
|
|
||||||
} else {
|
|
||||||
if (j === 0) row[col] = products[i % products.length];
|
|
||||||
else if (j === 1) row[col] = regions[i % regions.length];
|
|
||||||
else if (j >= 2 && j <= 5) row[col] = Math.floor(Math.random() * 10000) + 1000;
|
|
||||||
else if (j === 6) {
|
|
||||||
// Calculate total (this will be formatted as formula)
|
|
||||||
row[col] = `=C${i+1}+D${i+1}+E${i+1}+F${i+1}`;
|
|
||||||
}
|
|
||||||
else row[col] = Math.random() > 0.5 ? Math.floor(Math.random() * 1000) : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.push(row);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumnName(index) {
|
|
||||||
let name = '';
|
|
||||||
while (index >= 0) {
|
|
||||||
name = String.fromCharCode(65 + (index % 26)) + name;
|
|
||||||
index = Math.floor(index / 26) - 1;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.renderTable();
|
|
||||||
this.setupEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTable() {
|
|
||||||
const thead = document.getElementById('tableHead');
|
|
||||||
const tbody = document.getElementById('tableBody');
|
|
||||||
|
|
||||||
// Render header
|
|
||||||
let headerHTML = '<tr><th></th>';
|
|
||||||
for (let i = 0; i < this.cols; i++) {
|
|
||||||
headerHTML += `<th>${this.getColumnName(i)}</th>`;
|
|
||||||
}
|
|
||||||
headerHTML += '</tr>';
|
|
||||||
thead.innerHTML = headerHTML;
|
|
||||||
|
|
||||||
// Render visible rows
|
|
||||||
let bodyHTML = '';
|
|
||||||
const endRow = Math.min(this.rowOffset + this.visibleRows, this.rows);
|
|
||||||
|
|
||||||
for (let i = this.rowOffset; i < endRow; i++) {
|
|
||||||
bodyHTML += `<tr><th>${i + 1}</th>`;
|
|
||||||
for (let j = 0; j < this.cols; j++) {
|
|
||||||
const col = this.getColumnName(j);
|
|
||||||
const value = this.data[i][col] || '';
|
|
||||||
const displayValue = this.calculateCell(value, i, j);
|
|
||||||
bodyHTML += `<td data-row="${i}" data-col="${j}" data-cell="${col}${i+1}">${displayValue}</td>`;
|
|
||||||
}
|
|
||||||
bodyHTML += '</tr>';
|
|
||||||
}
|
|
||||||
tbody.innerHTML = bodyHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateCell(value, row, col) {
|
|
||||||
if (typeof value === 'string' && value.startsWith('=')) {
|
|
||||||
try {
|
|
||||||
const formula = value.substring(1).toUpperCase();
|
|
||||||
|
|
||||||
// Handle SUM formula
|
|
||||||
if (formula.includes('SUM')) {
|
|
||||||
const match = formula.match(/SUM\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
|
||||||
if (match) {
|
|
||||||
const sum = this.calculateRange(match[1], match[2], 'sum');
|
|
||||||
return sum.toFixed(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle AVERAGE formula
|
|
||||||
if (formula.includes('AVERAGE')) {
|
|
||||||
const match = formula.match(/AVERAGE\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
|
||||||
if (match) {
|
|
||||||
const avg = this.calculateRange(match[1], match[2], 'avg');
|
|
||||||
return avg.toFixed(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle simple arithmetic (e.g., =C2+D2+E2+F2)
|
|
||||||
let expression = formula;
|
|
||||||
const cellRefs = expression.match(/[A-Z]+\d+/g);
|
|
||||||
if (cellRefs) {
|
|
||||||
cellRefs.forEach(ref => {
|
|
||||||
const val = this.getCellValue(ref);
|
|
||||||
expression = expression.replace(ref, val);
|
|
||||||
});
|
|
||||||
return eval(expression).toFixed(2);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return '#ERROR';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCellValue(cellRef) {
|
|
||||||
const col = cellRef.match(/[A-Z]+/)[0];
|
|
||||||
const row = parseInt(cellRef.match(/\d+/)[0]) - 1;
|
|
||||||
const value = this.data[row][col];
|
|
||||||
|
|
||||||
if (typeof value === 'string' && value.startsWith('=')) {
|
|
||||||
return this.calculateCell(value, row, this.getColIndex(col));
|
|
||||||
}
|
|
||||||
return parseFloat(value) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getColIndex(colName) {
|
|
||||||
let index = 0;
|
|
||||||
for (let i = 0; i < colName.length; i++) {
|
|
||||||
index = index * 26 + (colName.charCodeAt(i) - 64);
|
|
||||||
}
|
|
||||||
return index - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateRange(start, end, operation) {
|
|
||||||
const startCol = start.match(/[A-Z]+/)[0];
|
|
||||||
const startRow = parseInt(start.match(/\d+/)[0]) - 1;
|
|
||||||
const endCol = end.match(/[A-Z]+/)[0];
|
|
||||||
const endRow = parseInt(end.match(/\d+/)[0]) - 1;
|
|
||||||
|
|
||||||
let values = [];
|
|
||||||
for (let r = startRow; r <= endRow; r++) {
|
|
||||||
for (let c = this.getColIndex(startCol); c <= this.getColIndex(endCol); c++) {
|
|
||||||
const col = this.getColumnName(c);
|
|
||||||
const val = parseFloat(this.data[r][col]) || 0;
|
|
||||||
values.push(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operation === 'sum') {
|
|
||||||
return values.reduce((a, b) => a + b, 0);
|
|
||||||
} else if (operation === 'avg') {
|
|
||||||
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
const container = document.getElementById('spreadsheetContainer');
|
|
||||||
const formulaInput = document.getElementById('formulaInput');
|
|
||||||
|
|
||||||
// Infinite scroll
|
|
||||||
container.addEventListener('scroll', () => {
|
|
||||||
const scrollPercentage = (container.scrollTop + container.clientHeight) / container.scrollHeight;
|
|
||||||
if (scrollPercentage > 0.8 && this.rowOffset + this.visibleRows < this.rows) {
|
|
||||||
this.rowOffset += 10;
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cell selection
|
|
||||||
document.getElementById('tableBody').addEventListener('click', (e) => {
|
|
||||||
if (e.target.tagName === 'TD') {
|
|
||||||
this.selectCell(e.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cell editing
|
|
||||||
document.getElementById('tableBody').addEventListener('dblclick', (e) => {
|
|
||||||
if (e.target.tagName === 'TD') {
|
|
||||||
this.editCell(e.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Formula bar
|
|
||||||
formulaInput.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter' && this.selectedCell) {
|
|
||||||
this.updateCellValue(formulaInput.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectCell(cell) {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
this.selectedCell.classList.remove('selected');
|
|
||||||
}
|
|
||||||
this.selectedCell = cell;
|
|
||||||
cell.classList.add('selected');
|
|
||||||
|
|
||||||
const cellRef = cell.dataset.cell;
|
|
||||||
document.getElementById('cellRef').textContent = cellRef;
|
|
||||||
document.getElementById('selectedCell').textContent = cellRef;
|
|
||||||
|
|
||||||
const row = parseInt(cell.dataset.row);
|
|
||||||
const col = this.getColumnName(parseInt(cell.dataset.col));
|
|
||||||
const value = this.data[row][col] || '';
|
|
||||||
document.getElementById('formulaInput').value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
editCell(cell) {
|
|
||||||
const row = parseInt(cell.dataset.row);
|
|
||||||
const col = this.getColumnName(parseInt(cell.dataset.col));
|
|
||||||
const value = this.data[row][col] || '';
|
|
||||||
|
|
||||||
cell.classList.add('editing');
|
|
||||||
cell.innerHTML = `<input type="text" class="cell-editor" value="${value}" />`;
|
|
||||||
const input = cell.querySelector('.cell-editor');
|
|
||||||
input.focus();
|
|
||||||
input.select();
|
|
||||||
|
|
||||||
input.addEventListener('blur', () => {
|
|
||||||
this.updateCellValue(input.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
input.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
this.updateCellValue(input.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCellValue(value) {
|
|
||||||
if (!this.selectedCell) return;
|
|
||||||
|
|
||||||
const row = parseInt(this.selectedCell.dataset.row);
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
|
|
||||||
this.data[row][col] = value;
|
|
||||||
this.renderTable();
|
|
||||||
|
|
||||||
// Reselect the cell
|
|
||||||
const newCell = document.querySelector(`td[data-row="${row}"][data-col="${this.selectedCell.dataset.col}"]`);
|
|
||||||
if (newCell) this.selectCell(newCell);
|
|
||||||
}
|
|
||||||
|
|
||||||
bold() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
this.selectedCell.style.fontWeight = this.selectedCell.style.fontWeight === 'bold' ? 'normal' : 'bold';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addRow() {
|
|
||||||
const newRow = {};
|
|
||||||
for (let i = 0; i < this.cols; i++) {
|
|
||||||
newRow[this.getColumnName(i)] = '';
|
|
||||||
}
|
|
||||||
this.data.push(newRow);
|
|
||||||
this.rows++;
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
addColumn() {
|
|
||||||
const newCol = this.getColumnName(this.cols);
|
|
||||||
this.data.forEach(row => row[newCol] = '');
|
|
||||||
this.cols++;
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRow() {
|
|
||||||
if (this.selectedCell && this.rows > 1) {
|
|
||||||
const row = parseInt(this.selectedCell.dataset.row);
|
|
||||||
this.data.splice(row, 1);
|
|
||||||
this.rows--;
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteColumn() {
|
|
||||||
if (this.selectedCell && this.cols > 1) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
this.data.forEach(row => delete row[col]);
|
|
||||||
this.cols--;
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
const header = this.data[0];
|
|
||||||
const dataRows = this.data.slice(1);
|
|
||||||
|
|
||||||
dataRows.sort((a, b) => {
|
|
||||||
const aVal = a[col] || '';
|
|
||||||
const bVal = b[col] || '';
|
|
||||||
return aVal.toString().localeCompare(bVal.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
this.data = [header, ...dataRows];
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sum() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
document.getElementById('formulaInput').value = `=SUM(${col}2:${col}${this.rows})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
average() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
document.getElementById('formulaInput').value = `=AVERAGE(${col}2:${col}${this.rows})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCell() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
this.updateCellValue('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exportData() {
|
|
||||||
const csv = this.data.map(row => {
|
|
||||||
return Object.values(row).join(',');
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'tables_export.csv';
|
|
||||||
a.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new TablesApp();
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="status-bar">
|
|
||||||
<div class="stats">
|
|
||||||
<span>Rows: <strong id="rowCount">0</strong></span>
|
|
||||||
<span>Cols: <strong id="colCount">0</strong></span>
|
|
||||||
<span>Selected: <strong id="selectedCell">None</strong></span>
|
|
||||||
</div>
|
|
||||||
<div>Ready | Lotus 1-2-3 Mode Active ✨</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="toolbar">
|
|
||||||
<button onclick="app.bold()">📝 Bold</button>
|
|
||||||
<button onclick="app.addRow()">➕ Add Row</button>
|
|
||||||
<button onclick="app.addColumn()">➕ Add Column</button>
|
|
||||||
<button onclick="app.deleteRow()">➖ Delete Row</button>
|
|
||||||
<button onclick="app.deleteColumn()">➖ Delete Column</button>
|
|
||||||
<button onclick="app.sort()">🔽 Sort A-Z</button>
|
|
||||||
<button onclick="app.sum()">Σ Sum</button>
|
|
||||||
<button onclick="app.average()">📊 Average</button>
|
|
||||||
<button onclick="app.clearCell()">🗑️ Clear</button>
|
|
||||||
<button onclick="app.exportData()">💾 Export</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<div class="app-container" x-data="tablesApp()" x-init="init()">
|
|
||||||
<div class="navbar-container">
|
|
||||||
<div x-html="(await fetch('./components/navbar.html')).text()"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>📊 Tables</h1>
|
|
||||||
<div class="subtitle">Excel Clone - Celebrating Lotus 1-2-3 Legacy 🎉</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="resizable-container">
|
|
||||||
<div class="resizable-panel left" style="width: 30%">
|
|
||||||
<!-- Left panel content -->
|
|
||||||
</div>
|
|
||||||
<div class="resizable-handle"></div>
|
|
||||||
<div class="resizable-panel right" style="width: 70%">
|
|
||||||
<div class="spreadsheet-content">
|
|
||||||
<table>
|
|
||||||
<thead id="tableHead"></thead>
|
|
||||||
<tbody id="tableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formula bar -->
|
|
||||||
<div class="formula-bar">
|
|
||||||
<span id="cellRef">A1</span>
|
|
||||||
<input type="text"
|
|
||||||
id="formulaInput"
|
|
||||||
placeholder="Enter formula..."
|
|
||||||
@keypress.enter="updateCellValue($event.target.value)"
|
|
||||||
x-model="formulaInputValue">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status bar -->
|
|
||||||
<div class="status-bar">
|
|
||||||
<span>Rows: <span id="rowCount" x-text="rows"></span></span>
|
|
||||||
<span>Columns: <span id="colCount" x-text="cols"></span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toolbar -->
|
|
||||||
<div class="toolbar">
|
|
||||||
<button @click="addRow()">Add Row</button>
|
|
||||||
<button @click="addColumn()">Add Column</button>
|
|
||||||
<button @click="deleteRow()">Delete Row</button>
|
|
||||||
<button @click="deleteColumn()">Delete Column</button>
|
|
||||||
<button @click="sort()">Sort</button>
|
|
||||||
<button @click="sum()">Sum</button>
|
|
||||||
<button @click="average()">Average</button>
|
|
||||||
<button @click="exportData()">Export</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
/* Tables Component Styles */
|
|
||||||
.app-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 15px 20px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .subtitle {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spreadsheet-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Spreadsheet specific styles */
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
background-color: #e6f2ff;
|
|
||||||
outline: 2px solid #4d90fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing input {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable-container {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable-panel {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable-handle {
|
|
||||||
width: 10px;
|
|
||||||
background: #f0f0f0;
|
|
||||||
cursor: col-resize;
|
|
||||||
}
|
|
||||||
|
|
@ -1,293 +0,0 @@
|
||||||
function tablesApp() {
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
selectedCell: null,
|
|
||||||
cols: 26,
|
|
||||||
rows: 100,
|
|
||||||
visibleRows: 30,
|
|
||||||
rowOffset: 0,
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.data = this.generateMockData(this.rows, this.cols);
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.updateStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
generateMockData(rows, cols) {
|
|
||||||
const data = [];
|
|
||||||
const products = ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Headphones', 'Webcam', 'Desk', 'Chair'];
|
|
||||||
const regions = ['North', 'South', 'East', 'West'];
|
|
||||||
|
|
||||||
for (let i = 0; i < rows; i++) {
|
|
||||||
const row = {};
|
|
||||||
for (let j = 0; j < cols; j++) {
|
|
||||||
const col = this.getColumnName(j);
|
|
||||||
if (i === 0) {
|
|
||||||
if (j === 0) row[col] = 'Product';
|
|
||||||
else if (j === 1) row[col] = 'Region';
|
|
||||||
else if (j === 2) row[col] = 'Q1';
|
|
||||||
else if (j === 3) row[col] = 'Q2';
|
|
||||||
else if (j === 4) row[col] = 'Q3';
|
|
||||||
else if (j === 5) row[col] = 'Q4';
|
|
||||||
else if (j === 6) row[col] = 'Total';
|
|
||||||
else row[col] = `Col ${col}`;
|
|
||||||
} else {
|
|
||||||
if (j === 0) row[col] = products[i % products.length];
|
|
||||||
else if (j === 1) row[col] = regions[i % regions.length];
|
|
||||||
else if (j >= 2 && j <= 5) row[col] = Math.floor(Math.random() * 10000) + 1000;
|
|
||||||
else if (j === 6) {
|
|
||||||
row[col] = `=C${i+1}+D${i+1}+E${i+1}+F${i+1}`;
|
|
||||||
}
|
|
||||||
else row[col] = Math.random() > 0.5 ? Math.floor(Math.random() * 1000) : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.push(row);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getColumnName(index) {
|
|
||||||
let name = '';
|
|
||||||
while (index >= 0) {
|
|
||||||
name = String.fromCharCode(65 + (index % 26)) + name;
|
|
||||||
index = Math.floor(index / 26) - 1;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
},
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
// Will be replaced with Alpine.js directives
|
|
||||||
},
|
|
||||||
|
|
||||||
selectCell(cell) {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
this.selectedCell.classList.remove('selected');
|
|
||||||
}
|
|
||||||
this.selectedCell = cell;
|
|
||||||
cell.classList.add('selected');
|
|
||||||
|
|
||||||
const cellRef = cell.dataset.cell;
|
|
||||||
document.getElementById('cellRef').textContent = cellRef;
|
|
||||||
document.getElementById('selectedCell').textContent = cellRef;
|
|
||||||
|
|
||||||
const row = parseInt(cell.dataset.row);
|
|
||||||
const col = this.getColumnName(parseInt(cell.dataset.col));
|
|
||||||
const value = this.data[row][col] || '';
|
|
||||||
document.getElementById('formulaInput').value = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateCell(value, row, col) {
|
|
||||||
if (typeof value === 'string' && value.startsWith('=')) {
|
|
||||||
try {
|
|
||||||
const formula = value.substring(1).toUpperCase();
|
|
||||||
|
|
||||||
if (formula.includes('SUM')) {
|
|
||||||
const match = formula.match(/SUM\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
|
||||||
if (match) {
|
|
||||||
const sum = this.calculateRange(match[1], match[2], 'sum');
|
|
||||||
return sum.toFixed(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formula.includes('AVERAGE')) {
|
|
||||||
const match = formula.match(/AVERAGE\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
|
||||||
if (match) {
|
|
||||||
const avg = this.calculateRange(match[1], match[2], 'avg');
|
|
||||||
return avg.toFixed(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expression = formula;
|
|
||||||
const cellRefs = expression.match(/[A-Z]+\d+/g);
|
|
||||||
if (cellRefs) {
|
|
||||||
cellRefs.forEach(ref => {
|
|
||||||
const val = this.getCellValue(ref);
|
|
||||||
expression = expression.replace(ref, val);
|
|
||||||
});
|
|
||||||
return eval(expression).toFixed(2);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return '#ERROR';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
getCellValue(cellRef) {
|
|
||||||
const col = cellRef.match(/[A-Z]+/)[0];
|
|
||||||
const row = parseInt(cellRef.match(/\d+/)[0]) - 1;
|
|
||||||
const value = this.data[row][col];
|
|
||||||
|
|
||||||
if (typeof value === 'string' && value.startsWith('=')) {
|
|
||||||
return this.calculateCell(value, row, this.getColIndex(col));
|
|
||||||
}
|
|
||||||
return parseFloat(value) || 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
getColIndex(colName) {
|
|
||||||
let index = 0;
|
|
||||||
for (let i = 0; i < colName.length; i++) {
|
|
||||||
index = index * 26 + (colName.charCodeAt(i) - 64);
|
|
||||||
}
|
|
||||||
return index - 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateRange(start, end, operation) {
|
|
||||||
const startCol = start.match(/[A-Z]+/)[0];
|
|
||||||
const startRow = parseInt(start.match(/\d+/)[0]) - 1;
|
|
||||||
const endCol = end.match(/[A-Z]+/)[0];
|
|
||||||
const endRow = parseInt(end.match(/\d+/)[0]) - 1;
|
|
||||||
|
|
||||||
let values = [];
|
|
||||||
for (let r = startRow; r <= endRow; r++) {
|
|
||||||
for (let c = this.getColIndex(startCol); c <= this.getColIndex(endCol); c++) {
|
|
||||||
const col = this.getColumnName(c);
|
|
||||||
const val = parseFloat(this.data[r][col]) || 0;
|
|
||||||
values.push(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operation === 'sum') {
|
|
||||||
return values.reduce((a, b) => a + b, 0);
|
|
||||||
} else if (operation === 'avg') {
|
|
||||||
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCellValue(value) {
|
|
||||||
if (!this.selectedCell) return;
|
|
||||||
|
|
||||||
const row = parseInt(this.selectedCell.dataset.row);
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
|
|
||||||
this.data[row][col] = value;
|
|
||||||
this.renderTable();
|
|
||||||
|
|
||||||
const newCell = document.querySelector(`td[data-row="${row}"][data-col="${this.selectedCell.dataset.col}"]`);
|
|
||||||
if (newCell) this.selectCell(newCell);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderTable() {
|
|
||||||
const thead = document.getElementById('tableHead');
|
|
||||||
const tbody = document.getElementById('tableBody');
|
|
||||||
|
|
||||||
let headerHTML = '<tr><th></th>';
|
|
||||||
for (let i = 0; i < this.cols; i++) {
|
|
||||||
headerHTML += `<th>${this.getColumnName(i)}</th>`;
|
|
||||||
}
|
|
||||||
headerHTML += '</tr>';
|
|
||||||
thead.innerHTML = headerHTML;
|
|
||||||
|
|
||||||
let bodyHTML = '';
|
|
||||||
const endRow = Math.min(this.rowOffset + this.visibleRows, this.rows);
|
|
||||||
|
|
||||||
for (let i = this.rowOffset; i < endRow; i++) {
|
|
||||||
bodyHTML += `<tr><th>${i + 1}</th>`;
|
|
||||||
for (let j = 0; j < this.cols; j++) {
|
|
||||||
const col = this.getColumnName(j);
|
|
||||||
const value = this.data[i][col] || '';
|
|
||||||
const displayValue = this.calculateCell(value, i, j);
|
|
||||||
bodyHTML += `<td @click="selectCell($el)"
|
|
||||||
data-row="${i}"
|
|
||||||
data-col="${j}"
|
|
||||||
data-cell="${col}${i+1}"
|
|
||||||
:class="{ 'selected': selectedCell === $el }">
|
|
||||||
${displayValue}
|
|
||||||
</td>`;
|
|
||||||
}
|
|
||||||
bodyHTML += '</tr>';
|
|
||||||
}
|
|
||||||
tbody.innerHTML = bodyHTML;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateStats() {
|
|
||||||
document.getElementById('rowCount').textContent = this.rows;
|
|
||||||
document.getElementById('colCount').textContent = this.cols;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Toolbar actions
|
|
||||||
addRow() {
|
|
||||||
const newRow = {};
|
|
||||||
for (let i = 0; i < this.cols; i++) {
|
|
||||||
newRow[this.getColumnName(i)] = '';
|
|
||||||
}
|
|
||||||
this.data.push(newRow);
|
|
||||||
this.rows++;
|
|
||||||
this.renderTable();
|
|
||||||
this.updateStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
addColumn() {
|
|
||||||
const newCol = this.getColumnName(this.cols);
|
|
||||||
this.data.forEach(row => row[newCol] = '');
|
|
||||||
this.cols++;
|
|
||||||
this.renderTable();
|
|
||||||
this.updateStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteRow() {
|
|
||||||
if (this.selectedCell && this.rows > 1) {
|
|
||||||
const row = parseInt(this.selectedCell.dataset.row);
|
|
||||||
this.data.splice(row, 1);
|
|
||||||
this.rows--;
|
|
||||||
this.renderTable();
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteColumn() {
|
|
||||||
if (this.selectedCell && this.cols > 1) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
this.data.forEach(row => delete row[col]);
|
|
||||||
this.cols--;
|
|
||||||
this.renderTable();
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sort() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
const header = this.data[0];
|
|
||||||
const dataRows = this.data.slice(1);
|
|
||||||
|
|
||||||
dataRows.sort((a, b) => {
|
|
||||||
const aVal = a[col] || '';
|
|
||||||
const bVal = b[col] || '';
|
|
||||||
return aVal.toString().localeCompare(bVal.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
this.data = [header, ...dataRows];
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sum() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
this.formulaInputValue = `=SUM(${col}2:${col}${this.rows})`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
average() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
this.formulaInputValue = `=AVERAGE(${col}2:${col}${this.rows})`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
exportData() {
|
|
||||||
const csv = this.data.map(row => {
|
|
||||||
return Object.values(row).join(',');
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'tables_export.csv';
|
|
||||||
a.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
<!-- Riot component: theme-provider.html (converted from app/theme-provider.tsx) -->
|
|
||||||
<script >
|
|
||||||
// This component replicates the ThemeProvider logic without React.
|
|
||||||
// It manages theme selection and persists the choice in localStorage.
|
|
||||||
export default {
|
|
||||||
// Component state
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
themes: [
|
|
||||||
{ name: '3dbevel', label: '3dbevel', cssFile: '/themes/3dbevel.css' },
|
|
||||||
{ name: 'arcadeflash', label: 'Arcadeflash', cssFile: '/themes/arcadeflash.css' },
|
|
||||||
{ name: 'jazzage', label: 'Jazzage', cssFile: '/themes/jazzage.css' },
|
|
||||||
{ name: 'mellowgold', label: 'Mellowgold', cssFile: '/themes/mellowgold.css' },
|
|
||||||
{ name: 'midcenturymod', label: 'Midcenturymod', cssFile: '/themes/midcenturymod.css' },
|
|
||||||
{ name: 'polaroidmemories', label: 'Polaroidmemories', cssFile: '/themes/polaroidmemories.css' },
|
|
||||||
{ name: 'retrowave', label: 'Retrowave', cssFile: '/themes/retrowave.css' },
|
|
||||||
{ name: 'saturdaycartoons', label: 'Saturdaycartoons', cssFile: '/themes/saturdaycartoons.css' },
|
|
||||||
{ name: 'typewriter', label: 'Typewriter', cssFile: '/themes/typewriter.css' },
|
|
||||||
{ name: 'vapordream', label: 'Vapordream', cssFile: '/themes/vapordream.css' },
|
|
||||||
{ name: 'xeroxui', label: 'Xeroxui', cssFile: '/themes/xeroxui.css' },
|
|
||||||
{ name: 'y2kglow', label: 'Y2kglow', cssFile: '/themes/y2kglow.css' },
|
|
||||||
{ name: 'xtreegold', label: 'XTreeGold', cssFile: '/themes/xtreegold.css' },
|
|
||||||
],
|
|
||||||
currentTheme: null,
|
|
||||||
isLoaded: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Lifecycle: component mounted
|
|
||||||
mounted() {
|
|
||||||
// Load saved theme from localStorage or default to first theme
|
|
||||||
const savedThemeName = localStorage.getItem('theme') || this.themes[0].name;
|
|
||||||
const savedTheme = this.themes.find(t => t.name === savedThemeName) || this.themes[0];
|
|
||||||
this.setTheme(savedTheme.name);
|
|
||||||
this.isLoaded = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
setTheme(themeName) {
|
|
||||||
const theme = this.themes.find(t => t.name === themeName);
|
|
||||||
if (!theme) return;
|
|
||||||
|
|
||||||
// Remove any existing theme link
|
|
||||||
const existingLink = document.getElementById('theme-stylesheet');
|
|
||||||
if (existingLink) {
|
|
||||||
document.head.removeChild(existingLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new link element
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.id = 'theme-stylesheet';
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.href = theme.cssFile;
|
|
||||||
link.onload = () => {
|
|
||||||
this.currentTheme = theme;
|
|
||||||
localStorage.setItem('theme', theme.name);
|
|
||||||
};
|
|
||||||
document.head.appendChild(link);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Expose a simple API for other components
|
|
||||||
getTheme() {
|
|
||||||
return this.currentTheme;
|
|
||||||
},
|
|
||||||
|
|
||||||
getThemes() {
|
|
||||||
return this.themes;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* No visual output needed for the provider */
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- No markup needed; this component is used for its side‑effects -->
|
|
||||||
</write_to_file>
|
|
||||||
<task_progress>
|
|
||||||
- [x] Create comprehensive todo list
|
|
||||||
- [x] Analyze React components to convert
|
|
||||||
- [x] Convert app/client-nav.tsx to Riot component (client-nav.html)
|
|
||||||
- [x] Update imports and usage (removed Nav import and usage from layout)
|
|
||||||
- [x] Remove React dependencies from layout.tsx (file cleared)
|
|
||||||
- [x] Convert remaining .tsx files (starting with theme-provider.tsx)
|
|
||||||
- [ ] Verify application works with Riot
|
|
||||||
- [ ] Clean up leftover React files
|
|
||||||
</task_progress>
|
|
||||||
</write_to_file>
|
|
||||||
|
|
@ -1,451 +0,0 @@
|
||||||
<welcome>
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
// Component state
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
isLoading: false,
|
|
||||||
error: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Configuration (ZITADEL)
|
|
||||||
onBeforeMount() {
|
|
||||||
// this.zitadelConfig = {
|
|
||||||
// authority: 'https://your-zitadel-instance.com',
|
|
||||||
// clientId: 'your-client-id',
|
|
||||||
// redirectUri: typeof window !== 'undefined' ? window.location.origin : '',
|
|
||||||
// scopes: ['openid', 'profile', 'email'],
|
|
||||||
// };
|
|
||||||
},
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
methods: {
|
|
||||||
handleSocialLogin(provider) {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.error = '';
|
|
||||||
try {
|
|
||||||
const authUrl = `${this.zitadelConfig.authority}/oauth/v2/authorize?` +
|
|
||||||
`client_id=${this.zitadelConfig.clientId}&` +
|
|
||||||
`redirect_uri=${encodeURIComponent(this.zitadelConfig.redirectUri)}&` +
|
|
||||||
`response_type=code&` +
|
|
||||||
`scope=${encodeURIComponent(this.zitadelConfig.scopes.join(' '))}&` +
|
|
||||||
`provider=${provider}`;
|
|
||||||
window.location.href = authUrl;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = 'Failed to initiate login';
|
|
||||||
console.error('Login error:', err);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleEmailLogin(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.isLoading = true;
|
|
||||||
this.error = '';
|
|
||||||
try {
|
|
||||||
// Mock implementation – store dummy token
|
|
||||||
localStorage.setItem('authToken', 'dummy-token');
|
|
||||||
// Navigate to dashboard (adjust path as needed)
|
|
||||||
window.location.href = '/dashboard';
|
|
||||||
} catch (err) {
|
|
||||||
this.error = 'Login failed. Please check your credentials.';
|
|
||||||
console.error('Login error:', err);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="auth-screen">
|
|
||||||
<div class="auth-content">
|
|
||||||
<div class="auth-left-panel">
|
|
||||||
<div class="auth-logo">
|
|
||||||
<h1>Welcome to General Bots Online</h1>
|
|
||||||
</div>
|
|
||||||
<div class="auth-quote">
|
|
||||||
<p>"Errar é Humano."</p>
|
|
||||||
<p>General Bots</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form-container">
|
|
||||||
<div class="auth-form-header">
|
|
||||||
<h2>Sign in to your account</h2>
|
|
||||||
<p>Choose your preferred login method</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-error" if={error}>{error}</div>
|
|
||||||
|
|
||||||
<div class="auth-social-buttons">
|
|
||||||
<button class="auth-social-button google" @click={()=> handleSocialLogin('google')}
|
|
||||||
disabled={isLoading}>
|
|
||||||
<svg class="auth-social-icon" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
||||||
fill="#4285F4" />
|
|
||||||
<path
|
|
||||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
||||||
fill="#34A853" />
|
|
||||||
<path
|
|
||||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
||||||
fill="#FBBC05" />
|
|
||||||
<path
|
|
||||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
||||||
fill="#EA4335" />
|
|
||||||
</svg>
|
|
||||||
Continue with Google
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="auth-social-button microsoft" @click={()=> handleSocialLogin('microsoft')}
|
|
||||||
disabled={isLoading}>
|
|
||||||
<svg class="auth-social-icon" viewBox="0 0 23 23">
|
|
||||||
<path d="M0 0h11v11H0zM12 0h11v11H12zM0 12h11v11H0zM12 12h11v11H12z" fill="#F25022" />
|
|
||||||
<path d="M12 0h11v11H12z" fill="#7FBA00" />
|
|
||||||
<path d="M0 12h11v11H0z" fill="#00A4EF" />
|
|
||||||
<path d="M12 12h11v11H12z" fill="#FFB900" />
|
|
||||||
</svg>
|
|
||||||
Continue with Microsoft
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="auth-social-button facebook" @click={()=> handleSocialLogin('facebook')}
|
|
||||||
disabled={isLoading}>
|
|
||||||
<svg class="auth-social-icon" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12c0 4.84 3.44 8.87 8 9.8V15H8v-3h2V9.5C10 7.57 11.57 6 13.5 6H16v3h-2c-.55 0-1 .45-1 1v2h3v3h-3v6.95c5.05-.5 9-4.76 9-9.95z"
|
|
||||||
fill="#1877F2" />
|
|
||||||
</svg>
|
|
||||||
Continue with Facebook
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="auth-social-button pragmatismo" @click={()=> handleSocialLogin('pragmatismo')}
|
|
||||||
disabled={isLoading}>
|
|
||||||
<svg class="auth-social-icon" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"
|
|
||||||
fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
Continue with Pragmatismo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-divider">
|
|
||||||
<span>OR</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="auth-form" @submit={handleEmailLogin}>
|
|
||||||
<div class="auth-form-group">
|
|
||||||
<label for="email">Email</label>
|
|
||||||
<input id="email" type="email" value={email} oninput={e=> this.email = e.target.value}
|
|
||||||
placeholder="your@email.com" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form-group">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input id="password" type="password" value={password} oninput={e=> this.password = e.target.value}
|
|
||||||
placeholder="••••••••" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form-options">
|
|
||||||
<div class="auth-remember-me">
|
|
||||||
<input type="checkbox" id="remember" />
|
|
||||||
<label for="remember">Remember me</label>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="auth-forgot-password">Forgot password?</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="auth-submit-button" disabled={isLoading}>
|
|
||||||
{isLoading ? 'Signing in...' : 'Sign in with Email'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="auth-signup-link">
|
|
||||||
Don't have an account? <a href="#">Sign up</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="auth-terms">
|
|
||||||
By continuing, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.auth-screen {
|
|
||||||
--background: hsl(var(--background));
|
|
||||||
--foreground: hsl(var(--foreground));
|
|
||||||
--card: hsl(var(--card));
|
|
||||||
--card-foreground: hsl(var(--card-foreground));
|
|
||||||
--primary: hsl(var(--primary));
|
|
||||||
--primary-foreground: hsl(var(--primary-foreground));
|
|
||||||
--secondary: hsl(var(--secondary));
|
|
||||||
--secondary-foreground: hsl(var(--secondary-foreground));
|
|
||||||
--muted: hsl(var(--muted));
|
|
||||||
--muted-foreground: hsl(var(--muted-foreground));
|
|
||||||
--accent: hsl(var(--accent));
|
|
||||||
--accent-foreground: hsl(var(--accent-foreground));
|
|
||||||
--destructive: hsl(var(--destructive));
|
|
||||||
--destructive-foreground: hsl(var(--destructive-foreground));
|
|
||||||
--border: hsl(var(--border));
|
|
||||||
--input: hsl(var(--input));
|
|
||||||
--ring: hsl(var(--ring));
|
|
||||||
--radius: var(--radius);
|
|
||||||
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-content {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
background-color: var(--card);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-left-panel {
|
|
||||||
flex: 1;
|
|
||||||
padding: 4rem;
|
|
||||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-logo h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-quote {
|
|
||||||
font-style: italic;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-quote p:last-child {
|
|
||||||
text-align: right;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-container {
|
|
||||||
flex: 1;
|
|
||||||
padding: 4rem;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-header {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-header h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-header p {
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-error {
|
|
||||||
background-color: var(--destructive);
|
|
||||||
color: var(--destructive-foreground);
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-buttons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background-color: var(--secondary);
|
|
||||||
color: var(--secondary-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-button:hover {
|
|
||||||
background-color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-button:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-icon {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-divider {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-divider::before,
|
|
||||||
.auth-divider::after {
|
|
||||||
content: "";
|
|
||||||
flex: 1;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-divider span {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-group {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background-color: var(--input);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--ring);
|
|
||||||
box-shadow: 0 0 0 2px var(--ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-options {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-remember-me {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-remember-me input {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-forgot-password {
|
|
||||||
color: var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-forgot-password:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-submit-button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
font-weight: 500;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-submit-button:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--primary), black 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-submit-button:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-signup-link {
|
|
||||||
text-align: center;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-signup-link a {
|
|
||||||
color: var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-signup-link a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-terms {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-terms a {
|
|
||||||
color: var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-terms a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.auth-content {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-left-panel {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-container {
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-buttons {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
</welcome>
|
|
||||||
Loading…
Add table
Reference in a new issue