diff --git a/quote-builder/README.md b/quote-builder/README.md
new file mode 100644
index 0000000..95da83b
--- /dev/null
+++ b/quote-builder/README.md
@@ -0,0 +1,124 @@
+# QuoteBuilder
+
+An interactive pricing calculator with real-time updates, featuring a two-column layout with input controls and a live results panel.
+
+## Getting Started
+
+Install dependencies:
+```bash
+npm install
+```
+
+Share the component to your Webflow workspace:
+```bash
+npx webflow library share
+```
+
+For local development:
+```bash
+npm run dev
+```
+
+## Designer Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| ID | Id | — | HTML ID attribute for targeting in custom code |
+| Layout | Variant | side-by-side | Results panel position relative to inputs (side-by-side, stacked) |
+| Heading | TextNode | Get Your Custom Quote | Main heading for the calculator |
+| Subheading | Text | Customize your package and see pricing in real-time | Descriptive subheading below main heading |
+| Currency Symbol | Text | $ | Currency symbol to display before prices |
+| Input Section Title | Text | Configure Your Package | Title for the input controls section |
+| Results Section Title | Text | Your Quote | Title for the results panel |
+| Item 1 Label | Text | Team Members | Label for first line item |
+| Item 1 Type | Variant | number | Input type for first item (number, dropdown, toggle) |
+| Item 1 Default Value | Number | 5 | Default quantity or value for first item |
+| Item 1 Unit Price | Number | 25 | Price per unit for first item |
+| Item 1 Dropdown Options | Text | Basic\|1\|10 Standard\|2\|25 Premium\|3\|50 | Dropdown options for first item (one per line, format: label\|value\|price) |
+| Item 1 Visible | Visibility | — | Show or hide first line item |
+| Item 2 Label | Text | Storage Space (GB) | Label for second line item |
+| Item 2 Type | Variant | dropdown | Input type for second item (number, dropdown, toggle) |
+| Item 2 Default Value | Number | 2 | Default quantity or value for second item |
+| Item 2 Unit Price | Number | 15 | Price per unit for second item |
+| Item 2 Dropdown Options | Text | 50GB\|1\|15 100GB\|2\|25 500GB\|3\|75 | Dropdown options for second item (one per line, format: label\|value\|price) |
+| Item 2 Visible | Visibility | — | Show or hide second line item |
+| Item 3 Label | Text | Priority Support | Label for third line item |
+| Item 3 Type | Variant | toggle | Input type for third item (number, dropdown, toggle) |
+| Item 3 Default Value | Number | 0 | Default quantity or value for third item (0 or 1 for toggle) |
+| Item 3 Unit Price | Number | 99 | Price per unit for third item |
+| Item 3 Dropdown Options | Text | None\|0\|0 Enabled\|1\|99 | Dropdown options for third item (one per line, format: label\|value\|price) |
+| Item 3 Visible | Visibility | — | Show or hide third line item |
+| Item 4 Label | Text | API Access | Label for fourth line item |
+| Item 4 Type | Variant | toggle | Input type for fourth item (number, dropdown, toggle) |
+| Item 4 Default Value | Number | 0 | Default quantity or value for fourth item (0 or 1 for toggle) |
+| Item 4 Unit Price | Number | 49 | Price per unit for fourth item |
+| Item 4 Dropdown Options | Text | None\|0\|0 Enabled\|1\|49 | Dropdown options for fourth item (one per line, format: label\|value\|price) |
+| Item 4 Visible | Visibility | — | Show or hide fourth line item |
+| Show Subtotals | Boolean | true | Display individual subtotals for each line item in results |
+| Show Unit Prices | Boolean | true | Display unit prices for each line item in results |
+| Total Label | Text | Total Monthly Cost | Label for the total amount |
+| CTA Text | Text | Get Your Quote | Call-to-action button text |
+| CTA Link | Link | — | Call-to-action button link destination |
+| CTA Subtext | Text | No credit card required. Get a detailed quote in minutes. | Helper text below the CTA button |
+| Show CTA Subtext | Boolean | true | Display helper text below CTA button |
+
+## Styling
+
+This component automatically adapts to your Webflow site's design system through site variables and inherited properties.
+
+### Site Variables
+
+To match your site's design system, define these CSS variables in your Webflow project settings. The component will use the fallback values shown below until you configure them.
+
+| Site Variable | What It Controls | Fallback |
+|---------------|------------------|----------|
+| --background-primary | Main background color for cards and panels | #ffffff |
+| --background-secondary | Hover states and subtle backgrounds | #f5f5f5 |
+| --text-primary | Main text color for headings and labels | #1a1a1a |
+| --text-secondary | Muted text for subheadings and helper text | #737373 |
+| --border-color | Borders, dividers, and input outlines | #e5e5e5 |
+| --accent-color | CTA button, toggle active state, focus outlines | #1a1a1a |
+| --accent-text-color | Text color on accent backgrounds | #ffffff |
+| --border-radius | Corner rounding for all elements | 8px |
+
+### Inherited Properties
+
+The component inherits these CSS properties from its parent element:
+- `font-family` — Typography style
+- `color` — Text color
+- `line-height` — Text spacing
+
+## Extending in Code
+
+### Custom Calculation Logic
+
+Add custom pricing rules or discounts by listening to value changes:
+
+```javascript
+const calculator = document.querySelector('[data-component="quote-builder"]');
+calculator.addEventListener('change', (e) => {
+ // Apply volume discount when team members > 10
+ const teamMembers = parseInt(e.target.dataset.item);
+ if (teamMembers > 10) {
+ // Apply 15% discount logic
+ }
+});
+```
+
+### Integration with Forms
+
+Connect the calculator to your form submission:
+
+```javascript
+const ctaButton = document.querySelector('.wf-quotebuilder-cta');
+ctaButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ const total = document.querySelector('.wf-quotebuilder-total-amount').textContent;
+ // Send quote data to your backend or CRM
+ submitQuote({ total, items: getSelectedItems() });
+});
+```
+
+## Dependencies
+
+No external dependencies.
\ No newline at end of file
diff --git a/quote-builder/index.html b/quote-builder/index.html
new file mode 100644
index 0000000..798375e
--- /dev/null
+++ b/quote-builder/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ QuoteBuilder
+
+
+
+
+
+
+
diff --git a/quote-builder/metadata.json b/quote-builder/metadata.json
new file mode 100644
index 0000000..7fadb74
--- /dev/null
+++ b/quote-builder/metadata.json
@@ -0,0 +1,5 @@
+{
+ "name": "Calculator / Quote Builder",
+ "description": "Interactive pricing calculator with number inputs, dropdowns, toggle add-ons, and real-time total calculation.",
+ "category": "Forms & Input"
+}
diff --git a/quote-builder/package.json b/quote-builder/package.json
new file mode 100644
index 0000000..9ed29d6
--- /dev/null
+++ b/quote-builder/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "quote-builder",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^19.1.13",
+ "@types/react-dom": "^19.1.9",
+ "@vitejs/plugin-react": "^5.0.3",
+ "@webflow/data-types": "^1.0.1",
+ "@webflow/react": "^1.0.1",
+ "@webflow/webflow-cli": "^1.8.44",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.7"
+ }
+}
\ No newline at end of file
diff --git a/quote-builder/screenshot-brand.png b/quote-builder/screenshot-brand.png
new file mode 100644
index 0000000..f1d4634
Binary files /dev/null and b/quote-builder/screenshot-brand.png differ
diff --git a/quote-builder/screenshot-dark.png b/quote-builder/screenshot-dark.png
new file mode 100644
index 0000000..f1d4634
Binary files /dev/null and b/quote-builder/screenshot-dark.png differ
diff --git a/quote-builder/screenshot-light.png b/quote-builder/screenshot-light.png
new file mode 100644
index 0000000..f1d4634
Binary files /dev/null and b/quote-builder/screenshot-light.png differ
diff --git a/quote-builder/src/components/QuoteBuilder/QuoteBuilder.css b/quote-builder/src/components/QuoteBuilder/QuoteBuilder.css
new file mode 100644
index 0000000..2d1bdd9
--- /dev/null
+++ b/quote-builder/src/components/QuoteBuilder/QuoteBuilder.css
@@ -0,0 +1,398 @@
+/*
+ * Webflow Site Variables Used:
+ * - --background-primary: Main background color for cards and panels
+ * - --background-secondary: Hover states and subtle backgrounds
+ * - --text-primary: Main text color for headings and labels
+ * - --text-secondary: Muted text for subheadings and helper text
+ * - --border-color: Borders, dividers, and input outlines
+ * - --accent-color: CTA button, toggle active state, focus outlines
+ * - --accent-text-color: Text color on accent backgrounds
+ * - --border-radius: Corner rounding for all elements
+ */
+
+/* Box sizing reset scoped to component */
+.wf-quotebuilder *,
+.wf-quotebuilder *::before,
+.wf-quotebuilder *::after {
+ box-sizing: border-box;
+}
+
+/* Root element - inherit Webflow typography + default padding */
+.wf-quotebuilder {
+ font-family: inherit;
+ color: inherit;
+ line-height: inherit;
+ padding: 24px;
+}
+
+/* Layout variants */
+.wf-quotebuilder-layout-side-by-side .wf-quotebuilder-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 32px;
+}
+
+.wf-quotebuilder-layout-stacked .wf-quotebuilder-container {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+}
+
+/* Header section */
+.wf-quotebuilder-header {
+ margin-bottom: 32px;
+}
+
+.wf-quotebuilder-heading {
+ font-size: 32px;
+ font-weight: 700;
+ color: var(--text-primary, #1a1a1a);
+ margin: 0 0 8px 0;
+ line-height: 1.2;
+}
+
+.wf-quotebuilder-subheading {
+ font-size: 16px;
+ color: var(--text-secondary, #737373);
+ margin: 0;
+ line-height: 1.5;
+}
+
+/* Container */
+.wf-quotebuilder-container {
+ margin-bottom: 32px;
+}
+
+/* Input section */
+.wf-quotebuilder-inputs {
+ background: var(--background-primary, #ffffff);
+ border: 1px solid var(--border-color, #e5e5e5);
+ border-radius: var(--border-radius, 8px);
+ padding: 24px;
+}
+
+.wf-quotebuilder-section-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--text-primary, #1a1a1a);
+ margin: 0 0 20px 0;
+ line-height: 1.3;
+}
+
+.wf-quotebuilder-inputs-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.wf-quotebuilder-input-row {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.wf-quotebuilder-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary, #1a1a1a);
+ line-height: 1.4;
+}
+
+/* Number input */
+.wf-quotebuilder-number-input {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: fit-content;
+}
+
+.wf-quotebuilder-decrement,
+.wf-quotebuilder-increment {
+ width: 36px;
+ height: 36px;
+ border: 1px solid var(--border-color, #e5e5e5);
+ background: var(--background-primary, #ffffff);
+ border-radius: var(--border-radius, 8px);
+ color: var(--text-primary, #1a1a1a);
+ font-size: 18px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s, border-color 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+}
+
+.wf-quotebuilder-decrement:hover,
+.wf-quotebuilder-increment:hover {
+ background: var(--background-secondary, #f5f5f5);
+ border-color: var(--text-secondary, #737373);
+}
+
+.wf-quotebuilder-decrement:focus-visible,
+.wf-quotebuilder-increment:focus-visible {
+ outline: 2px solid var(--accent-color, #1a1a1a);
+ outline-offset: 2px;
+}
+
+.wf-quotebuilder-decrement:active,
+.wf-quotebuilder-increment:active {
+ background: var(--border-color, #e5e5e5);
+}
+
+.wf-quotebuilder-number-field {
+ width: 80px;
+ height: 36px;
+ border: 1px solid var(--border-color, #e5e5e5);
+ background: var(--background-primary, #ffffff);
+ border-radius: var(--border-radius, 8px);
+ color: var(--text-primary, #1a1a1a);
+ font-size: 14px;
+ text-align: center;
+ padding: 0 8px;
+ transition: border-color 0.2s;
+}
+
+.wf-quotebuilder-number-field:hover {
+ border-color: var(--text-secondary, #737373);
+}
+
+.wf-quotebuilder-number-field:focus {
+ outline: none;
+ border-color: var(--accent-color, #1a1a1a);
+}
+
+.wf-quotebuilder-number-field:focus-visible {
+ outline: 2px solid var(--accent-color, #1a1a1a);
+ outline-offset: 2px;
+}
+
+/* Select dropdown */
+.wf-quotebuilder-select {
+ width: 100%;
+ height: 40px;
+ border: 1px solid var(--border-color, #e5e5e5);
+ background: var(--background-primary, #ffffff);
+ border-radius: var(--border-radius, 8px);
+ color: var(--text-primary, #1a1a1a);
+ font-size: 14px;
+ padding: 0 12px;
+ cursor: pointer;
+ transition: border-color 0.2s;
+}
+
+.wf-quotebuilder-select:hover {
+ border-color: var(--text-secondary, #737373);
+}
+
+.wf-quotebuilder-select:focus {
+ outline: none;
+ border-color: var(--accent-color, #1a1a1a);
+}
+
+.wf-quotebuilder-select:focus-visible {
+ outline: 2px solid var(--accent-color, #1a1a1a);
+ outline-offset: 2px;
+}
+
+/* Toggle switch */
+.wf-quotebuilder-toggle {
+ position: relative;
+ display: inline-block;
+ width: 48px;
+ height: 24px;
+ cursor: pointer;
+}
+
+.wf-quotebuilder-toggle-input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ position: absolute;
+}
+
+.wf-quotebuilder-toggle-slider {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: var(--border-color, #e5e5e5);
+ border-radius: 24px;
+ transition: background-color 0.2s;
+}
+
+.wf-quotebuilder-toggle-slider::before {
+ content: "";
+ position: absolute;
+ height: 18px;
+ width: 18px;
+ left: 3px;
+ bottom: 3px;
+ background: var(--background-primary, #ffffff);
+ border-radius: 50%;
+ transition: transform 0.2s;
+}
+
+.wf-quotebuilder-toggle:hover .wf-quotebuilder-toggle-slider {
+ background: var(--text-secondary, #737373);
+}
+
+.wf-quotebuilder-toggle-input:checked + .wf-quotebuilder-toggle-slider {
+ background: var(--accent-color, #1a1a1a);
+}
+
+.wf-quotebuilder-toggle-input:checked + .wf-quotebuilder-toggle-slider::before {
+ transform: translateX(24px);
+}
+
+.wf-quotebuilder-toggle-input:focus-visible + .wf-quotebuilder-toggle-slider {
+ outline: 2px solid var(--accent-color, #1a1a1a);
+ outline-offset: 2px;
+}
+
+.wf-quotebuilder-toggle-input:disabled + .wf-quotebuilder-toggle-slider {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Results section */
+.wf-quotebuilder-results {
+ background: var(--background-primary, #ffffff);
+ border: 1px solid var(--border-color, #e5e5e5);
+ border-radius: var(--border-radius, 8px);
+ padding: 24px;
+}
+
+.wf-quotebuilder-results-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-bottom: 24px;
+ padding-bottom: 24px;
+ border-bottom: 1px solid var(--border-color, #e5e5e5);
+}
+
+.wf-quotebuilder-result-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 16px;
+}
+
+.wf-quotebuilder-result-label {
+ font-size: 14px;
+ color: var(--text-primary, #1a1a1a);
+ line-height: 1.5;
+ flex: 1;
+}
+
+.wf-quotebuilder-result-details {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 4px;
+}
+
+.wf-quotebuilder-result-quantity {
+ font-size: 13px;
+ color: var(--text-secondary, #737373);
+ line-height: 1.4;
+}
+
+.wf-quotebuilder-result-unit-price {
+ font-size: 13px;
+ color: var(--text-secondary, #737373);
+}
+
+.wf-quotebuilder-result-subtotal {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-primary, #1a1a1a);
+ line-height: 1.4;
+}
+
+/* Total section */
+.wf-quotebuilder-total {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px;
+}
+
+.wf-quotebuilder-total-label {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary, #1a1a1a);
+ line-height: 1.3;
+}
+
+.wf-quotebuilder-total-amount {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--accent-color, #1a1a1a);
+ line-height: 1.2;
+}
+
+/* Footer section */
+.wf-quotebuilder-footer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+}
+
+.wf-quotebuilder-cta {
+ display: inline-block;
+ padding: 14px 32px;
+ background: var(--accent-color, #1a1a1a);
+ color: var(--accent-text-color, #ffffff);
+ font-size: 16px;
+ font-weight: 600;
+ text-decoration: none;
+ border-radius: var(--border-radius, 8px);
+ transition: opacity 0.2s, transform 0.2s;
+ cursor: pointer;
+ border: none;
+ line-height: 1.4;
+}
+
+.wf-quotebuilder-cta:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+}
+
+.wf-quotebuilder-cta:focus-visible {
+ outline: 2px solid var(--accent-color, #1a1a1a);
+ outline-offset: 2px;
+}
+
+.wf-quotebuilder-cta:active {
+ transform: translateY(0);
+ opacity: 0.8;
+}
+
+.wf-quotebuilder-cta-subtext {
+ font-size: 13px;
+ color: var(--text-secondary, #737373);
+ text-align: center;
+ margin: 0;
+ line-height: 1.5;
+ max-width: 400px;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .wf-quotebuilder-layout-side-by-side .wf-quotebuilder-container {
+ grid-template-columns: 1fr;
+ }
+
+ .wf-quotebuilder-heading {
+ font-size: 24px;
+ }
+
+ .wf-quotebuilder-total-amount {
+ font-size: 24px;
+ }
+}
\ No newline at end of file
diff --git a/quote-builder/src/components/QuoteBuilder/QuoteBuilder.tsx b/quote-builder/src/components/QuoteBuilder/QuoteBuilder.tsx
new file mode 100644
index 0000000..f139bb5
--- /dev/null
+++ b/quote-builder/src/components/QuoteBuilder/QuoteBuilder.tsx
@@ -0,0 +1,346 @@
+import { useState } from "react";
+
+export interface QuoteBuilderProps {
+ id?: string;
+ layout?: "side-by-side" | "stacked";
+ heading?: React.ReactNode;
+ subheading?: string;
+ currencySymbol?: string;
+ inputSectionTitle?: string;
+ resultsSectionTitle?: string;
+ item1Label?: string;
+ item1Type?: "number" | "dropdown" | "toggle";
+ item1DefaultValue?: number;
+ item1UnitPrice?: number;
+ item1DropdownOptions?: string;
+ item1Visible?: boolean;
+ item2Label?: string;
+ item2Type?: "number" | "dropdown" | "toggle";
+ item2DefaultValue?: number;
+ item2UnitPrice?: number;
+ item2DropdownOptions?: string;
+ item2Visible?: boolean;
+ item3Label?: string;
+ item3Type?: "number" | "dropdown" | "toggle";
+ item3DefaultValue?: number;
+ item3UnitPrice?: number;
+ item3DropdownOptions?: string;
+ item3Visible?: boolean;
+ item4Label?: string;
+ item4Type?: "number" | "dropdown" | "toggle";
+ item4DefaultValue?: number;
+ item4UnitPrice?: number;
+ item4DropdownOptions?: string;
+ item4Visible?: boolean;
+ showSubtotals?: boolean;
+ showUnitPrices?: boolean;
+ totalLabel?: string;
+ ctaText?: string;
+ ctaLink?: string;
+ ctaSubtext?: string;
+ showCtaSubtext?: boolean;
+}
+
+interface DropdownOption {
+ label: string;
+ value: number;
+ price: number;
+}
+
+interface LineItem {
+ label: string;
+ type: "number" | "dropdown" | "toggle";
+ defaultValue: number;
+ unitPrice: number;
+ dropdownOptions: string;
+ visible: boolean;
+}
+
+export default function QuoteBuilder({
+ id,
+ layout = "side-by-side",
+ heading = "Get Your Custom Quote",
+ subheading = "Customize your package and see pricing in real-time",
+ currencySymbol = "$",
+ inputSectionTitle = "Configure Your Package",
+ resultsSectionTitle = "Your Quote",
+ item1Label = "Team Members",
+ item1Type = "number",
+ item1DefaultValue = 5,
+ item1UnitPrice = 25,
+ item1DropdownOptions = "Basic|1|10\nStandard|2|25\nPremium|3|50",
+ item1Visible = true,
+ item2Label = "Storage Space (GB)",
+ item2Type = "dropdown",
+ item2DefaultValue = 2,
+ item2UnitPrice = 15,
+ item2DropdownOptions = "50GB|1|15\n100GB|2|25\n500GB|3|75",
+ item2Visible = true,
+ item3Label = "Priority Support",
+ item3Type = "toggle",
+ item3DefaultValue = 0,
+ item3UnitPrice = 99,
+ item3DropdownOptions = "None|0|0\nEnabled|1|99",
+ item3Visible = true,
+ item4Label = "API Access",
+ item4Type = "toggle",
+ item4DefaultValue = 0,
+ item4UnitPrice = 49,
+ item4DropdownOptions = "None|0|0\nEnabled|1|49",
+ item4Visible = true,
+ showSubtotals = true,
+ showUnitPrices = true,
+ totalLabel = "Total Monthly Cost",
+ ctaText = "Get Your Quote",
+ ctaLink = "#",
+ ctaSubtext = "No credit card required. Get a detailed quote in minutes.",
+ showCtaSubtext = true,
+}: QuoteBuilderProps) {
+ const items: LineItem[] = [
+ {
+ label: item1Label,
+ type: item1Type,
+ defaultValue: item1DefaultValue,
+ unitPrice: item1UnitPrice,
+ dropdownOptions: item1DropdownOptions,
+ visible: item1Visible,
+ },
+ {
+ label: item2Label,
+ type: item2Type,
+ defaultValue: item2DefaultValue,
+ unitPrice: item2UnitPrice,
+ dropdownOptions: item2DropdownOptions,
+ visible: item2Visible,
+ },
+ {
+ label: item3Label,
+ type: item3Type,
+ defaultValue: item3DefaultValue,
+ unitPrice: item3UnitPrice,
+ dropdownOptions: item3DropdownOptions,
+ visible: item3Visible,
+ },
+ {
+ label: item4Label,
+ type: item4Type,
+ defaultValue: item4DefaultValue,
+ unitPrice: item4UnitPrice,
+ dropdownOptions: item4DropdownOptions,
+ visible: item4Visible,
+ },
+ ];
+
+ const [values, setValues] = useState(
+ items.map((item) => item.defaultValue)
+ );
+
+ const parseDropdownOptions = (optionsText: string): DropdownOption[] => {
+ return optionsText
+ .split(/\\n|\n/)
+ .filter((line) => line.trim())
+ .map((line) => {
+ const parts = line.split("|");
+ return {
+ label: parts[0]?.trim() || "",
+ value: parseFloat(parts[1]?.trim() || "0"),
+ price: parseFloat(parts[2]?.trim() || "0"),
+ };
+ });
+ };
+
+ const handleValueChange = (index: number, newValue: number) => {
+ const newValues = [...values];
+ newValues[index] = newValue;
+ setValues(newValues);
+ };
+
+ const calculateSubtotal = (index: number): number => {
+ const item = items[index];
+ const value = values[index];
+
+ if (item.type === "dropdown") {
+ const options = parseDropdownOptions(item.dropdownOptions);
+ const selectedOption = options.find((opt) => opt.value === value);
+ return selectedOption?.price || 0;
+ }
+
+ return value * item.unitPrice;
+ };
+
+ const calculateTotal = (): number => {
+ return items.reduce((total, item, index) => {
+ if (!item.visible) return total;
+ return total + calculateSubtotal(index);
+ }, 0);
+ };
+
+ const formatPrice = (price: number): string => {
+ return `${currencySymbol}${price.toFixed(2)}`;
+ };
+
+ const renderInput = (item: LineItem, index: number) => {
+ const value = values[index];
+
+ if (item.type === "number") {
+ return (
+
+ handleValueChange(index, Math.max(0, value - 1))}
+ aria-label="Decrease"
+ >
+ −
+
+
+ handleValueChange(index, Math.max(0, parseFloat(e.target.value) || 0))
+ }
+ min="0"
+ />
+ handleValueChange(index, value + 1)}
+ aria-label="Increase"
+ >
+ +
+
+
+ );
+ }
+
+ if (item.type === "dropdown") {
+ const options = parseDropdownOptions(item.dropdownOptions);
+ return (
+
+ handleValueChange(index, parseFloat(e.target.value))
+ }
+ >
+ {options.map((option, optIndex) => (
+
+ {option.label}
+
+ ))}
+
+ );
+ }
+
+ if (item.type === "toggle") {
+ return (
+
+ handleValueChange(index, e.target.checked ? 1 : 0)}
+ />
+
+
+ );
+ }
+
+ return null;
+ };
+
+ const visibleItems = items.filter((item) => item.visible);
+ const total = calculateTotal();
+
+ return (
+
+
+
{heading}
+ {subheading && (
+
{subheading}
+ )}
+
+
+
+
+
{inputSectionTitle}
+
+ {items.map(
+ (item, index) =>
+ item.visible && (
+
+
+ {item.label}
+
+ {renderInput(item, index)}
+
+ )
+ )}
+
+
+
+
+
+ {resultsSectionTitle}
+
+
+ {visibleItems.map((item, visibleIndex) => {
+ const actualIndex = items.findIndex((i) => i === item);
+ const value = values[actualIndex];
+ const subtotal = calculateSubtotal(actualIndex);
+
+ if (item.type === "toggle" && value === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {item.label}
+
+
+ {item.type === "number" && (
+
+ {value} ×{" "}
+ {showUnitPrices && (
+
+ {formatPrice(item.unitPrice)}
+
+ )}
+
+ )}
+ {showSubtotals && (
+
+ {formatPrice(subtotal)}
+
+ )}
+
+
+ );
+ })}
+
+
+
+ {totalLabel}
+
+ {formatPrice(total)}
+
+
+
+
+
+
+
+ {ctaText}
+
+ {showCtaSubtext && ctaSubtext && (
+
{ctaSubtext}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/quote-builder/src/components/QuoteBuilder/QuoteBuilder.webflow.tsx b/quote-builder/src/components/QuoteBuilder/QuoteBuilder.webflow.tsx
new file mode 100644
index 0000000..7361838
--- /dev/null
+++ b/quote-builder/src/components/QuoteBuilder/QuoteBuilder.webflow.tsx
@@ -0,0 +1,243 @@
+import QuoteBuilder from "./QuoteBuilder";
+import { props } from "@webflow/data-types";
+import { declareComponent } from "@webflow/react";
+import "./QuoteBuilder.css";
+
+export default declareComponent(QuoteBuilder, {
+ name: "QuoteBuilder",
+ description: "An interactive pricing calculator with a two-column layout that displays input controls on the left and a live-updating results panel on the right (stacking vertically on mobile). The input section contains labeled number fields with increment/decrement buttons, dropdown selects for option choices, and toggle switches for add-ons. The results panel shows a breakdown of line items with labels, quantities, unit prices, and subtotals, plus a prominent total with configurable currency formatting. All calculations update in real-time as users interact with inputs. The component features a clean card-based design with clear visual separation between input sections and the results panel, and includes a call-to-action button at the bottom for quote requests or contact.",
+ group: "Interactive",
+ options: {
+ ssr: false,
+ applyTagSelectors: true
+ },
+ props: {
+ id: props.Id({
+ name: "Element ID",
+ group: "Settings",
+ tooltip: "HTML ID attribute for targeting with CSS or JavaScript"
+ }),
+ layout: props.Variant({
+ name: "Layout",
+ options: ["side-by-side", "stacked"],
+ defaultValue: "side-by-side",
+ group: "Style",
+ tooltip: "Results panel position relative to inputs"
+ }),
+ heading: props.TextNode({
+ name: "Heading",
+ defaultValue: "Get Your Custom Quote",
+ group: "Content",
+ tooltip: "Main heading for the calculator"
+ }),
+ subheading: props.Text({
+ name: "Subheading",
+ defaultValue: "Customize your package and see pricing in real-time",
+ group: "Content",
+ tooltip: "Descriptive subheading below main heading"
+ }),
+ currencySymbol: props.Text({
+ name: "Currency Symbol",
+ defaultValue: "$",
+ group: "Settings",
+ tooltip: "Currency symbol to display before prices"
+ }),
+ inputSectionTitle: props.Text({
+ name: "Input Section Title",
+ defaultValue: "Configure Your Package",
+ group: "Content",
+ tooltip: "Title for the input controls section"
+ }),
+ resultsSectionTitle: props.Text({
+ name: "Results Section Title",
+ defaultValue: "Your Quote",
+ group: "Content",
+ tooltip: "Title for the results panel"
+ }),
+ item1Label: props.Text({
+ name: "Item 1 Label",
+ defaultValue: "Team Members",
+ group: "Item 1",
+ tooltip: "Label for first line item"
+ }),
+ item1Type: props.Variant({
+ name: "Item 1 Type",
+ options: ["number", "dropdown", "toggle"],
+ defaultValue: "number",
+ group: "Item 1",
+ tooltip: "Input type for first item"
+ }),
+ item1DefaultValue: props.Number({
+ name: "Item 1 Default Value",
+ defaultValue: 5,
+ group: "Item 1",
+ tooltip: "Default quantity or value for first item"
+ }),
+ item1UnitPrice: props.Number({
+ name: "Item 1 Unit Price",
+ defaultValue: 25,
+ group: "Item 1",
+ tooltip: "Price per unit for first item"
+ }),
+ item1DropdownOptions: props.Text({
+ name: "Item 1 Dropdown Options",
+ defaultValue: "Basic|1|10\nStandard|2|25\nPremium|3|50",
+ group: "Item 1",
+ tooltip: "Dropdown options for first item (one per line, format: label|value|price)"
+ }),
+ item1Visible: props.Visibility({
+ name: "Item 1 Visible",
+ group: "Item 1",
+ tooltip: "Show or hide first line item"
+ }),
+ item2Label: props.Text({
+ name: "Item 2 Label",
+ defaultValue: "Storage Space (GB)",
+ group: "Item 2",
+ tooltip: "Label for second line item"
+ }),
+ item2Type: props.Variant({
+ name: "Item 2 Type",
+ options: ["number", "dropdown", "toggle"],
+ defaultValue: "dropdown",
+ group: "Item 2",
+ tooltip: "Input type for second item"
+ }),
+ item2DefaultValue: props.Number({
+ name: "Item 2 Default Value",
+ defaultValue: 2,
+ group: "Item 2",
+ tooltip: "Default quantity or value for second item"
+ }),
+ item2UnitPrice: props.Number({
+ name: "Item 2 Unit Price",
+ defaultValue: 15,
+ group: "Item 2",
+ tooltip: "Price per unit for second item"
+ }),
+ item2DropdownOptions: props.Text({
+ name: "Item 2 Dropdown Options",
+ defaultValue: "50GB|1|15\n100GB|2|25\n500GB|3|75",
+ group: "Item 2",
+ tooltip: "Dropdown options for second item (one per line, format: label|value|price)"
+ }),
+ item2Visible: props.Visibility({
+ name: "Item 2 Visible",
+ group: "Item 2",
+ tooltip: "Show or hide second line item"
+ }),
+ item3Label: props.Text({
+ name: "Item 3 Label",
+ defaultValue: "Priority Support",
+ group: "Item 3",
+ tooltip: "Label for third line item"
+ }),
+ item3Type: props.Variant({
+ name: "Item 3 Type",
+ options: ["number", "dropdown", "toggle"],
+ defaultValue: "toggle",
+ group: "Item 3",
+ tooltip: "Input type for third item"
+ }),
+ item3DefaultValue: props.Number({
+ name: "Item 3 Default Value",
+ defaultValue: 0,
+ group: "Item 3",
+ tooltip: "Default quantity or value for third item (0 or 1 for toggle)"
+ }),
+ item3UnitPrice: props.Number({
+ name: "Item 3 Unit Price",
+ defaultValue: 99,
+ group: "Item 3",
+ tooltip: "Price per unit for third item"
+ }),
+ item3DropdownOptions: props.Text({
+ name: "Item 3 Dropdown Options",
+ defaultValue: "None|0|0\nEnabled|1|99",
+ group: "Item 3",
+ tooltip: "Dropdown options for third item (one per line, format: label|value|price)"
+ }),
+ item3Visible: props.Visibility({
+ name: "Item 3 Visible",
+ group: "Item 3",
+ tooltip: "Show or hide third line item"
+ }),
+ item4Label: props.Text({
+ name: "Item 4 Label",
+ defaultValue: "API Access",
+ group: "Item 4",
+ tooltip: "Label for fourth line item"
+ }),
+ item4Type: props.Variant({
+ name: "Item 4 Type",
+ options: ["number", "dropdown", "toggle"],
+ defaultValue: "toggle",
+ group: "Item 4",
+ tooltip: "Input type for fourth item"
+ }),
+ item4DefaultValue: props.Number({
+ name: "Item 4 Default Value",
+ defaultValue: 0,
+ group: "Item 4",
+ tooltip: "Default quantity or value for fourth item (0 or 1 for toggle)"
+ }),
+ item4UnitPrice: props.Number({
+ name: "Item 4 Unit Price",
+ defaultValue: 49,
+ group: "Item 4",
+ tooltip: "Price per unit for fourth item"
+ }),
+ item4DropdownOptions: props.Text({
+ name: "Item 4 Dropdown Options",
+ defaultValue: "None|0|0\nEnabled|1|49",
+ group: "Item 4",
+ tooltip: "Dropdown options for fourth item (one per line, format: label|value|price)"
+ }),
+ item4Visible: props.Visibility({
+ name: "Item 4 Visible",
+ group: "Item 4",
+ tooltip: "Show or hide fourth line item"
+ }),
+ showSubtotals: props.Boolean({
+ name: "Show Subtotals",
+ defaultValue: true,
+ group: "Display",
+ tooltip: "Display individual subtotals for each line item in results"
+ }),
+ showUnitPrices: props.Boolean({
+ name: "Show Unit Prices",
+ defaultValue: true,
+ group: "Display",
+ tooltip: "Display unit prices for each line item in results"
+ }),
+ totalLabel: props.Text({
+ name: "Total Label",
+ defaultValue: "Total Monthly Cost",
+ group: "Content",
+ tooltip: "Label for the total amount"
+ }),
+ ctaText: props.Text({
+ name: "CTA Text",
+ defaultValue: "Get Your Quote",
+ group: "Content",
+ tooltip: "Call-to-action button text"
+ }),
+ ctaLink: props.Link({
+ name: "CTA Link",
+ group: "Content",
+ tooltip: "Call-to-action button link destination"
+ }),
+ ctaSubtext: props.Text({
+ name: "CTA Subtext",
+ defaultValue: "No credit card required. Get a detailed quote in minutes.",
+ group: "Content",
+ tooltip: "Helper text below the CTA button"
+ }),
+ showCtaSubtext: props.Boolean({
+ name: "Show CTA Subtext",
+ defaultValue: true,
+ group: "Display",
+ tooltip: "Display helper text below CTA button"
+ }),
+ },
+});
\ No newline at end of file
diff --git a/quote-builder/src/components/QuoteBuilder/QuoteBuilderSimple.webflow.tsx b/quote-builder/src/components/QuoteBuilder/QuoteBuilderSimple.webflow.tsx
new file mode 100644
index 0000000..538c75d
--- /dev/null
+++ b/quote-builder/src/components/QuoteBuilder/QuoteBuilderSimple.webflow.tsx
@@ -0,0 +1,130 @@
+import QuoteBuilder from "./QuoteBuilder";
+import { props } from "@webflow/data-types";
+import { declareComponent } from "@webflow/react";
+import "./QuoteBuilder.css";
+
+export default declareComponent(QuoteBuilder, {
+ name: "QuoteBuilder (Simple)",
+ description: "An interactive pricing calculator with a two-column layout that displays input controls on the left and a live-updating results panel on the right (stacking vertically on mobile). The input section contains labeled number fields with increment/decrement buttons, dropdown selects for option choices, and toggle switches for add-ons. The results panel shows a breakdown of line items with labels, quantities, unit prices, and subtotals, plus a prominent total with configurable currency formatting. All calculations update in real-time as users interact with inputs. The component features a clean card-based design with clear visual separation between input sections and the results panel, and includes a call-to-action button at the bottom for quote requests or contact.",
+ group: "Interactive",
+ options: {
+ ssr: false,
+ applyTagSelectors: true
+ },
+ props: {
+ id: props.Id({
+ name: "Element ID",
+ group: "Settings",
+ tooltip: "HTML ID attribute for targeting with CSS or JavaScript"
+ }),
+ heading: props.TextNode({
+ name: "Heading",
+ defaultValue: "Get Your Custom Quote",
+ group: "Content",
+ tooltip: "Main heading for the calculator"
+ }),
+ item1Label: props.Text({
+ name: "Item 1 Label",
+ defaultValue: "Team Members",
+ group: "Item 1",
+ tooltip: "Label for first line item"
+ }),
+ item1DefaultValue: props.Number({
+ name: "Item 1 Default Value",
+ defaultValue: 5,
+ group: "Item 1",
+ tooltip: "Default quantity or value for first item"
+ }),
+ item1UnitPrice: props.Number({
+ name: "Item 1 Unit Price",
+ defaultValue: 25,
+ group: "Item 1",
+ tooltip: "Price per unit for first item"
+ }),
+ item1Visible: props.Visibility({
+ name: "Item 1 Visible",
+ group: "Item 1",
+ tooltip: "Show or hide first line item"
+ }),
+ item2Label: props.Text({
+ name: "Item 2 Label",
+ defaultValue: "Storage Space (GB)",
+ group: "Item 2",
+ tooltip: "Label for second line item"
+ }),
+ item2DefaultValue: props.Number({
+ name: "Item 2 Default Value",
+ defaultValue: 2,
+ group: "Item 2",
+ tooltip: "Default quantity or value for second item"
+ }),
+ item2UnitPrice: props.Number({
+ name: "Item 2 Unit Price",
+ defaultValue: 15,
+ group: "Item 2",
+ tooltip: "Price per unit for second item"
+ }),
+ item2Visible: props.Visibility({
+ name: "Item 2 Visible",
+ group: "Item 2",
+ tooltip: "Show or hide second line item"
+ }),
+ item3Label: props.Text({
+ name: "Item 3 Label",
+ defaultValue: "Priority Support",
+ group: "Item 3",
+ tooltip: "Label for third line item"
+ }),
+ item3DefaultValue: props.Number({
+ name: "Item 3 Default Value",
+ defaultValue: 0,
+ group: "Item 3",
+ tooltip: "Default quantity or value for third item (0 or 1 for toggle)"
+ }),
+ item3UnitPrice: props.Number({
+ name: "Item 3 Unit Price",
+ defaultValue: 99,
+ group: "Item 3",
+ tooltip: "Price per unit for third item"
+ }),
+ item3Visible: props.Visibility({
+ name: "Item 3 Visible",
+ group: "Item 3",
+ tooltip: "Show or hide third line item"
+ }),
+ item4Label: props.Text({
+ name: "Item 4 Label",
+ defaultValue: "API Access",
+ group: "Item 4",
+ tooltip: "Label for fourth line item"
+ }),
+ item4DefaultValue: props.Number({
+ name: "Item 4 Default Value",
+ defaultValue: 0,
+ group: "Item 4",
+ tooltip: "Default quantity or value for fourth item (0 or 1 for toggle)"
+ }),
+ item4UnitPrice: props.Number({
+ name: "Item 4 Unit Price",
+ defaultValue: 49,
+ group: "Item 4",
+ tooltip: "Price per unit for fourth item"
+ }),
+ item4Visible: props.Visibility({
+ name: "Item 4 Visible",
+ group: "Item 4",
+ tooltip: "Show or hide fourth line item"
+ }),
+ ctaText: props.Text({
+ name: "CTA Text",
+ defaultValue: "Get Your Quote",
+ group: "Content",
+ tooltip: "Call-to-action button text"
+ }),
+ ctaLink: props.Link({
+ name: "CTA Link",
+ group: "Content",
+ tooltip: "Call-to-action button link destination"
+ }),
+ },
+});
\ No newline at end of file
diff --git a/quote-builder/src/main.tsx b/quote-builder/src/main.tsx
new file mode 100644
index 0000000..a82512f
--- /dev/null
+++ b/quote-builder/src/main.tsx
@@ -0,0 +1,371 @@
+import { StrictMode, useState } from "react"
+import { createRoot } from "react-dom/client"
+import QuoteBuilder from "./components/QuoteBuilder/QuoteBuilder"
+import "./components/QuoteBuilder/QuoteBuilder.css"
+
+type ThemeVars = {
+ '--background-primary': string
+ '--background-secondary': string
+ '--text-primary': string
+ '--text-secondary': string
+ '--border-color': string
+ '--accent-color': string
+ '--accent-text-color': string
+ '--border-radius': string
+}
+
+const themes: Record = {
+ light: {
+ '--background-primary': '#ffffff',
+ '--background-secondary': '#f5f5f5',
+ '--text-primary': '#1a1a1a',
+ '--text-secondary': '#737373',
+ '--border-color': '#e5e5e5',
+ '--accent-color': '#2563eb',
+ '--accent-text-color': '#ffffff',
+ '--border-radius': '8px'
+ },
+ dark: {
+ '--background-primary': '#0a0a0a',
+ '--background-secondary': '#1a1a1a',
+ '--text-primary': '#fafafa',
+ '--text-secondary': '#a3a3a3',
+ '--border-color': '#2a2a2a',
+ '--accent-color': '#3b82f6',
+ '--accent-text-color': '#ffffff',
+ '--border-radius': '8px'
+ },
+ brand: {
+ '--background-primary': '#fef7f0',
+ '--background-secondary': '#fde8d0',
+ '--text-primary': '#1c1917',
+ '--text-secondary': '#78716c',
+ '--border-color': '#e7e5e4',
+ '--accent-color': '#ea580c',
+ '--accent-text-color': '#ffffff',
+ '--border-radius': '12px'
+ }
+}
+
+function App() {
+ const [activeTheme, setActiveTheme] = useState<'light' | 'dark' | 'brand' | 'custom'>('light')
+ const [customVars, setCustomVars] = useState(themes.light)
+
+ const currentVars = activeTheme === 'custom' ? customVars : themes[activeTheme]
+
+ const handleThemeChange = (theme: 'light' | 'dark' | 'brand' | 'custom') => {
+ setActiveTheme(theme)
+ if (theme !== 'custom') {
+ setCustomVars(themes[theme])
+ }
+ }
+
+ const handleCustomVarChange = (key: keyof ThemeVars, value: string) => {
+ setCustomVars(prev => ({ ...prev, [key]: value }))
+ }
+
+ const pageBackground = activeTheme === 'dark' ? '#000000' : activeTheme === 'brand' ? '#fef3e8' : '#fafafa'
+
+ return (
+
+
+
+ QuoteBuilder Preview
+ Local development environment - test different themes and configurations
+
+
+ handleThemeChange('light')}
+ style={{
+ padding: '10px 20px',
+ border: `2px solid ${activeTheme === 'light' ? currentVars['--accent-color'] : currentVars['--border-color']}`,
+ borderRadius: currentVars['--border-radius'],
+ background: activeTheme === 'light' ? currentVars['--accent-color'] : currentVars['--background-secondary'],
+ color: activeTheme === 'light' ? currentVars['--accent-text-color'] : currentVars['--text-primary'],
+ fontSize: '14px',
+ fontWeight: '600',
+ cursor: 'pointer',
+ transition: 'all 0.2s ease'
+ }}
+ >Light
+ handleThemeChange('dark')}
+ style={{
+ padding: '10px 20px',
+ border: `2px solid ${activeTheme === 'dark' ? currentVars['--accent-color'] : currentVars['--border-color']}`,
+ borderRadius: currentVars['--border-radius'],
+ background: activeTheme === 'dark' ? currentVars['--accent-color'] : currentVars['--background-secondary'],
+ color: activeTheme === 'dark' ? currentVars['--accent-text-color'] : currentVars['--text-primary'],
+ fontSize: '14px',
+ fontWeight: '600',
+ cursor: 'pointer',
+ transition: 'all 0.2s ease'
+ }}
+ >Dark
+ handleThemeChange('brand')}
+ style={{
+ padding: '10px 20px',
+ border: `2px solid ${activeTheme === 'brand' ? currentVars['--accent-color'] : currentVars['--border-color']}`,
+ borderRadius: currentVars['--border-radius'],
+ background: activeTheme === 'brand' ? currentVars['--accent-color'] : currentVars['--background-secondary'],
+ color: activeTheme === 'brand' ? currentVars['--accent-text-color'] : currentVars['--text-primary'],
+ fontSize: '14px',
+ fontWeight: '600',
+ cursor: 'pointer',
+ transition: 'all 0.2s ease'
+ }}
+ >Brand
+ handleThemeChange('custom')}
+ style={{
+ padding: '10px 20px',
+ border: `2px solid ${activeTheme === 'custom' ? currentVars['--accent-color'] : currentVars['--border-color']}`,
+ borderRadius: currentVars['--border-radius'],
+ background: activeTheme === 'custom' ? currentVars['--accent-color'] : currentVars['--background-secondary'],
+ color: activeTheme === 'custom' ? currentVars['--accent-text-color'] : currentVars['--text-primary'],
+ fontSize: '14px',
+ fontWeight: '600',
+ cursor: 'pointer',
+ transition: 'all 0.2s ease'
+ }}
+ >Custom
+
+
+ {activeTheme === 'custom' && (
+
+
Custom Theme Editor
+
+ {Object.entries(customVars).map(([key, value]) => (
+
+
{key}
+
+ handleCustomVarChange(key as keyof ThemeVars, e.target.value)}
+ style={{
+ width: '50px',
+ height: '38px',
+ border: `1px solid ${currentVars['--border-color']}`,
+ borderRadius: '6px',
+ cursor: 'pointer'
+ }}
+ />
+ handleCustomVarChange(key as keyof ThemeVars, e.target.value)}
+ style={{
+ flex: 1,
+ padding: '8px 12px',
+ border: `1px solid ${currentVars['--border-color']}`,
+ borderRadius: '6px',
+ background: currentVars['--background-primary'],
+ color: currentVars['--text-primary'],
+ fontSize: '14px',
+ fontFamily: 'monospace'
+ }}
+ />
+
+
+ ))}
+
+
+ )}
+
+
+
+
+ Default Configuration
+
+
+
+
+ Stacked Layout - Simple Pricing
+
+
+
+
+ Minimal Configuration - Two Items Only
+
+
+
+
+
+ )
+}
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+)
\ No newline at end of file
diff --git a/quote-builder/src/vite-env.d.ts b/quote-builder/src/vite-env.d.ts
new file mode 100644
index 0000000..151aa68
--- /dev/null
+++ b/quote-builder/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/quote-builder/tsconfig.app.json b/quote-builder/tsconfig.app.json
new file mode 100644
index 0000000..d775f2a
--- /dev/null
+++ b/quote-builder/tsconfig.app.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsBuildInfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/quote-builder/tsconfig.json b/quote-builder/tsconfig.json
new file mode 100644
index 0000000..65f670c
--- /dev/null
+++ b/quote-builder/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/quote-builder/tsconfig.node.json b/quote-builder/tsconfig.node.json
new file mode 100644
index 0000000..c4a9a48
--- /dev/null
+++ b/quote-builder/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsBuildInfo",
+ "target": "ES2023",
+ "lib": [
+ "ES2023"
+ ],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": [
+ "vite.config.ts"
+ ]
+}
\ No newline at end of file
diff --git a/quote-builder/vite.config.ts b/quote-builder/vite.config.ts
new file mode 100644
index 0000000..c7a4f78
--- /dev/null
+++ b/quote-builder/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+});
\ No newline at end of file
diff --git a/quote-builder/webflow.json b/quote-builder/webflow.json
new file mode 100644
index 0000000..a2b55db
--- /dev/null
+++ b/quote-builder/webflow.json
@@ -0,0 +1,10 @@
+{
+ "library": {
+ "name": "QuoteBuilder",
+ "components": [
+ "./src/**/*.webflow.@(js|jsx|mjs|ts|tsx)"
+ ],
+ "description": "An interactive pricing calculator with a two-column layout that displays input controls on the left and a live-updating results panel on the right (stacking vertically on mobile). The input section contains labeled number fields with increment/decrement buttons, dropdown selects for option choices, and toggle switches for add-ons. The results panel shows a breakdown of line items with labels, quantities, unit prices, and subtotals, plus a prominent total with configurable currency formatting. All calculations update in real-time as users interact with inputs. The component features a clean card-based design with clear visual separation between input sections and the results panel, and includes a call-to-action button at the bottom for quote requests or contact.",
+ "id": "quote-builder"
+ }
+}
\ No newline at end of file