diff --git a/bun.lock b/bun.lock
index 23a3a85..d3f2578 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,6 +5,7 @@
"": {
"name": "item-extractor-solid",
"dependencies": {
+ "@solidjs/router": "^0.15.4",
"@tailwindcss/typography": "^0.5.19",
"lucide-solid": "^0.577.0",
"solid-element": "^1.9.1",
@@ -171,6 +172,8 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="],
+ "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
+
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
diff --git a/index.html b/index.html
index 382fa22..9dd3c18 100644
--- a/index.html
+++ b/index.html
@@ -9,7 +9,7 @@
-
+
diff --git a/package.json b/package.json
index 9e7b1ea..c818cb4 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@solidjs/router": "^0.15.4",
"@tailwindcss/typography": "^0.5.19",
"lucide-solid": "^0.577.0",
"solid-element": "^1.9.1",
diff --git a/src/AppContainer.tsx b/src/AppContainer.tsx
new file mode 100644
index 0000000..0274fd3
--- /dev/null
+++ b/src/AppContainer.tsx
@@ -0,0 +1,49 @@
+import type { Component } from 'solid-js';
+import { A } from '@solidjs/router';
+
+interface AppContainerProps {
+ children?: any;
+}
+
+const AppContainer: Component = (props) => {
+ return (
+
+ {/* Top Navigation Bar */}
+
+
+ {/* Main Content Area */}
+
+ {props.children}
+
+
+ );
+};
+
+export default AppContainer;
diff --git a/src/ItemExtractor.tsx b/src/ItemExtractor.tsx
index cdc64c7..8162e8a 100644
--- a/src/ItemExtractor.tsx
+++ b/src/ItemExtractor.tsx
@@ -1,6 +1,6 @@
import type { Component } from 'solid-js';
import { createSignal, Show } from 'solid-js';
-import { customElement, noShadowDOM } from 'solid-element';
+
import FileUpload from './components/FileUpload';
import EstimateBuilder from './components/EstimateBuilder';
import IgnoredLinesList from './components/IgnoredLinesList';
@@ -15,7 +15,7 @@ interface ItemExtractorProps {
}
const ItemExtractor: Component = () => {
- noShadowDOM();
+
const [items, setItems] = createSignal([]);
const [ignoredLines, setIgnoredLines] = createSignal([]);
const [showToast] = createSignal(false);
@@ -77,7 +77,4 @@ const ItemExtractor: Component = () => {
);
};
-// Register as Custom Element
-customElement('item-extractor', { onItemsExtracted: undefined, onCopy: undefined }, ItemExtractor);
-
export default ItemExtractor;
diff --git a/src/components/template/TemplateItemList.tsx b/src/components/template/TemplateItemList.tsx
new file mode 100644
index 0000000..f6ed677
--- /dev/null
+++ b/src/components/template/TemplateItemList.tsx
@@ -0,0 +1,49 @@
+import type { Component } from 'solid-js';
+import { For } from 'solid-js';
+import { Plus } from 'lucide-solid';
+import type { TemplateItem } from '../../types';
+import TemplateItemRow from './TemplateItemRow';
+
+interface TemplateItemListProps {
+ items: TemplateItem[];
+ onUpdateItem: (id: string, updates: Partial) => void;
+ onDeleteItem: (id: string) => void;
+ onAddItem: () => void;
+}
+
+const TemplateItemList: Component = (props) => {
+ return (
+
+
+
Template Items
+
+
+
+
+
+ {(item) => (
+
+ )}
+
+
+ {props.items.length === 0 && (
+
+
No items added yet. Click "Add Item" to start building this template.
+
+ )}
+
+
+ );
+};
+
+export default TemplateItemList;
diff --git a/src/components/template/TemplateItemRow.tsx b/src/components/template/TemplateItemRow.tsx
new file mode 100644
index 0000000..9439587
--- /dev/null
+++ b/src/components/template/TemplateItemRow.tsx
@@ -0,0 +1,97 @@
+import type { Component } from 'solid-js';
+import { createSignal, createEffect } from 'solid-js';
+import { Trash2, GripVertical } from 'lucide-solid';
+import type { TemplateItem } from '../../types';
+
+interface TemplateItemRowProps {
+ item: TemplateItem;
+ onUpdate: (id: string, updates: Partial) => void;
+ onDelete: (id: string) => void;
+}
+
+const TemplateItemRow: Component = (props) => {
+ let descriptionRef: HTMLTextAreaElement | undefined;
+ const [isEditing, setIsEditing] = createSignal(false);
+ const [localDescription, setLocalDescription] = createSignal(props.item.description);
+
+ createEffect(() => {
+ if (!isEditing()) {
+ setLocalDescription(props.item.description);
+ }
+ });
+
+ const handleBlur = () => {
+ const currentDesc = localDescription();
+ if (currentDesc !== props.item.description) {
+ props.onUpdate(props.item.id, { description: currentDesc });
+ }
+ setIsEditing(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ props.onUpdate(props.item.id, { quantity: parseFloat(e.currentTarget.value) || 0 })}
+ class="w-full bg-muted/30 border border-border/60 rounded-xl px-2 py-1.5 text-xs text-right focus:bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none font-bold text-foreground/80 transition-all"
+ placeholder="Qty"
+ />
+
+
+
+
+ $
+ props.onUpdate(props.item.id, { unitPrice: parseFloat(e.currentTarget.value) || 0 })}
+ class="w-full bg-muted/30 border border-border/60 rounded-xl pl-5 pr-2 py-1.5 text-xs text-right focus:bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none font-bold text-foreground/80 transition-all"
+ placeholder="Price"
+ />
+
+
+
+
+
+
+
+ );
+};
+
+export default TemplateItemRow;
diff --git a/src/index.tsx b/src/index.tsx
index 2f7f931..4adf2c6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,3 +1,21 @@
-/* @refresh reload */
+import { render } from 'solid-js/web';
+import { Router, Route } from '@solidjs/router';
import './index.css';
-import './ItemExtractor';
+import AppContainer from './AppContainer';
+import ItemExtractor from './ItemExtractor';
+import TemplateCreator from './views/TemplateCreator';
+
+const root = document.getElementById('root');
+
+if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
+ throw new Error(
+ 'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?',
+ );
+}
+
+render(() => (
+
+
+
+
+), root!);
diff --git a/src/types.ts b/src/types.ts
index 4567137..8d64e50 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -38,3 +38,15 @@ export interface EstimateItem {
scopeId?: string;
subScopeId?: string;
}
+export interface TemplateItem {
+ id: string;
+ description: string;
+ quantity: number;
+ unitPrice: number;
+}
+
+export interface Template {
+ id: string;
+ name: string;
+ items: TemplateItem[];
+}
diff --git a/src/views/TemplateCreator.tsx b/src/views/TemplateCreator.tsx
new file mode 100644
index 0000000..7610f57
--- /dev/null
+++ b/src/views/TemplateCreator.tsx
@@ -0,0 +1,88 @@
+import type { Component } from 'solid-js';
+import { createSignal } from 'solid-js';
+import { Save } from 'lucide-solid';
+import type { TemplateItem, Template } from '../types';
+import TemplateItemList from '../components/template/TemplateItemList';
+
+const TemplateCreator: Component = () => {
+ const [templateName, setTemplateName] = createSignal('');
+ const [items, setItems] = createSignal([]);
+
+ const handleAddItem = () => {
+ const newItem: TemplateItem = {
+ id: crypto.randomUUID(),
+ description: '',
+ quantity: 1,
+ unitPrice: 0
+ };
+ setItems([...items(), newItem]);
+ };
+
+ const handleUpdateItem = (id: string, updates: Partial) => {
+ setItems(items().map(item => item.id === id ? { ...item, ...updates } : item));
+ };
+
+ const handleDeleteItem = (id: string) => {
+ setItems(items().filter(item => item.id !== id));
+ };
+
+ const handleSaveTemplate = () => {
+ // Here we would typically save to localStorage or backend
+ // For now, we'll just log it or show a toast
+ const template: Template = {
+ id: crypto.randomUUID(),
+ name: templateName(),
+ items: items()
+ };
+ console.log('Template saved:', template);
+ alert(`Template "${template.name}" saved successfully!`);
+
+ // Reset form
+ setTemplateName('');
+ setItems([]);
+ };
+
+ return (
+
+
+
+
+
Template Creator
+
Create templates for your estimate sub-scopes.
+
+
+
+
+
+
+
+ setTemplateName(e.currentTarget.value)}
+ placeholder="e.g. Master Bathroom Rough-in"
+ class="w-full bg-gray-50/50 border border-gray-200 rounded-xl px-4 py-3 text-gray-800 focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none font-medium transition-all shadow-sm"
+ />
+
+
+
+
+
+ );
+};
+
+export default TemplateCreator;