diff --git a/assets/css/main.css b/assets/css/main.css
index 08a5c19..0575114 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -3,10 +3,12 @@
@import "tailwindcss";
@import "@nuxt/ui";
+/* Ubuntu font import */
+@import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&family=Ubuntu+Mono:wght@400;700&display=swap");
+
[data-theme="dark"] {
html { @apply bg-white text-neutral-900; }
html.dark { @apply bg-neutral-950 text-neutral-100; }
-
}
/* Disable all animations, transitions, and smooth scrolling app-wide */
@@ -22,12 +24,347 @@ body {
transition: none !important;
}
-.document-page {
- @apply max-w-4xl mx-auto relative p-8 border-1 border-neutral-900 dark:border-neutral-100;
+/* =========================
+ TEMPLATE DOCUMENT LAYOUT
+ ========================= */
+
+/* Template wrapper and document styling */
+.template-wrapper {
+ @apply min-h-screen relative;
+
}
-/* Bitmap aesthetic overrides - remove all rounded corners */
+.document-page {
+ @apply max-w-full mx-auto relative p-8 border-1 border-neutral-900 dark:border-neutral-100;
+
+}
+
+
+
+/* =========================
+ SECTION STYLING
+ ========================= */
+
+.section-card {
+ @apply border border-neutral-200 dark:border-neutral-800 rounded-lg p-5 mb-8;
+}
+
+.section-card::before {
+ content: "";
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ right: -4px;
+ bottom: -4px;
+ background: black;
+ background-image: radial-gradient(white 1px, transparent 1px);
+ background-size: 2px 2px;
+ z-index: -1;
+}
+
+.section-title {
+ font-size: 1.75rem;
+ font-weight: 800;
+ color: inherit;
+ margin: 0 0 1rem 0;
+}
+
+.subsection-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #374151;
+ margin: 0 0 0.75rem 0;
+ text-decoration: none;
+ border-bottom: 1px solid #e5e7eb;
+ padding-bottom: 0.25rem;
+}
+
+/* =========================
+ FORM STYLING
+ ========================= */
+
+.form-group-large {
+ margin-bottom: 1.5rem;
+ width: 100%;
+}
+
+.form-group-large > * {
+ width: 100%;
+}
+
+/* Ensure consistent alignment for all form fields */
+.form-group-large :deep(textarea),
+.form-group-large :deep(input),
+.form-group-large :deep(select),
+.form-group-large :deep(.ui-select),
+.form-group-large :deep(.ui-input),
+.form-group-large :deep(.ui-textarea) {
+ margin-left: 0 !important;
+ padding-left: 0.75rem !important;
+}
+
+/* Additional targeting for UInput, USelect, UTextarea components */
+.form-group-large :deep(.u-input),
+.form-group-large :deep(.u-select),
+.form-group-large :deep(.u-textarea),
+.form-group-large :deep([data-headlessui-state]),
+.form-group-large :deep(.relative) {
+ margin-left: 0 !important;
+}
+
+.form-group-large :deep(.u-input input),
+.form-group-large :deep(.u-select select),
+.form-group-large :deep(.u-textarea textarea) {
+ margin-left: 0 !important;
+ padding-left: 0.75rem !important;
+}
+
+.form-group-large .large-field {
+ @apply block w-full mt-2 text-lg rounded-md border-none transition-colors duration-150 ease-in-out;
+}
+
+.form-group-large .large-field:focus {
+ background: #f3f4f6;
+ box-shadow: none;
+ outline: 2px solid #3b82f6;
+ outline-offset: -2px;
+}
+
+.inline-field {
+ display: inline-block;
+ margin: 0 0.25rem;
+ min-width: 120px;
+ border: none;
+ background: #f9fafb;
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+}
+
+.inline-field:focus {
+ background: #f3f4f6;
+ outline: 1px solid #3b82f6;
+ outline-offset: -1px;
+}
+
+.number-field {
+ min-width: 80px !important;
+ text-align: center;
+}
+
+.wide-field {
+ min-width: 250px !important;
+}
+
+.form-group-block .block-field {
+ display: block;
+ width: 100%;
+ margin-top: 0.25rem;
+ border: none;
+ background: #f9fafb;
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+}
+
+.form-group-block .block-field:focus {
+ background: #f3f4f6;
+ outline: 1px solid #3b82f6;
+ outline-offset: -1px;
+}
+
+/* =========================
+ CONTENT STYLING
+ ========================= */
+
+.content-paragraph {
+ margin-bottom: 0.75rem;
+ line-height: 1.6;
+ text-align: left;
+}
+
+.content-list {
+ margin: 0.5rem 0;
+ padding-left: 1.5rem;
+}
+
+.content-list li {
+ margin-bottom: 0.5rem;
+ line-height: 1.5;
+ display: list-item;
+ list-style-position: outside;
+}
+
+.content-list.numbered {
+ list-style-type: decimal;
+ counter-reset: list-counter;
+}
+
+.content-list.numbered li {
+ list-style-type: decimal;
+ display: list-item;
+}
+
+.content-list:not(.numbered) {
+ list-style-type: disc;
+}
+
+.content-list:not(.numbered) li {
+ list-style-type: disc;
+ display: list-item;
+}
+
+/* Ensure bullets are visible even with flex items */
+.content-list li.flex {
+ display: list-item;
+}
+
+.content-list li .flex {
+ display: flex;
+ width: 100%;
+}
+
+/* Fix flex list items to show bullets */
+.content-list li[class*="flex"] {
+ display: list-item !important;
+ list-style-position: outside !important;
+}
+
+/* Ensure numbered lists show numbers even with flex */
+.content-list.numbered li[class*="flex"] {
+ list-style-type: decimal !important;
+}
+
+/* Ensure bullet lists show bullets even with flex */
+.content-list:not(.numbered) li[class*="flex"] {
+ list-style-type: disc !important;
+}
+
+/* =========================
+ CHECKBOX AND VALUES GRID
+ ========================= */
+
+.checkbox-item {
+ @apply flex;
+}
+
+.checkbox-group {
+ @apply flex flex-col space-y-3;
+}
+
+.values-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 0.75rem;
+ margin-top: 10px;
+}
+
+/* =========================
+ DITHERED SHADOW EFFECTS
+ ========================= */
+
+.dither-shadow {
+ background: black;
+ background-image: radial-gradient(white 1px, transparent 1px);
+ background-size: 2px 2px;
+}
+
+html.dark .dither-shadow {
+ background: white;
+ background-image: radial-gradient(black 1px, transparent 1px);
+}
+
+/* =========================
+ BUTTON STYLING
+ ========================= */
+
+.export-btn {
+ @apply bg-white border border-black text-black font-mono uppercase font-normal tracking-wide;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ text-decoration: none;
+ min-width: fit-content;
+}
+
+.export-btn:hover {
+ @apply bg-black text-white -translate-x-px -translate-y-px;
+}
+
+.export-btn.primary {
+ @apply bg-black text-white;
+}
+
+.export-btn.primary:hover {
+ @apply bg-black -translate-x-px -translate-y-px;
+}
+
+.export-btn svg {
+ flex-shrink: 0;
+ width: 16px;
+ height: 16px;
+}
+
+.export-btn.large svg {
+ width: 20px;
+ height: 20px;
+}
+
+/* Dark mode export buttons */
+html.dark .export-btn {
+ @apply bg-neutral-950 border-white text-white;
+}
+
+html.dark .export-btn:hover {
+ @apply bg-white text-black;
+}
+
+html.dark .export-btn.primary {
+ @apply bg-white text-black;
+}
+
+html.dark .export-btn.primary:hover {
+ @apply bg-white text-black;
+}
+
+/* General buttons with bitmap styling */
+button:not(.export-btn) {
+ background: white !important;
+ border: 1px solid black !important;
+ color: black !important;
+ font-family: "Ubuntu Mono", monospace !important;
+ text-transform: uppercase !important;
+ font-weight: bold !important;
+ letter-spacing: 0.5px !important;
+ cursor: pointer !important;
+}
+
+button:not(.export-btn):hover {
+ background: black !important;
+ color: white !important;
+ transform: translateY(-1px) translateX(-1px) !important;
+}
+
+/* Dark mode buttons */
+html.dark button:not(.export-btn) {
+ background: #0a0a0a !important;
+ border: 1px solid white !important;
+ color: white !important;
+}
+
+html.dark button:not(.export-btn):hover {
+ background: white !important;
+ color: black !important;
+}
+
+/* =========================
+ BITMAP AESTHETIC OVERRIDES
+ ========================= */
+
+/* Remove all rounded corners */
* {
border-radius: 0 !important;
font-family: "Ubuntu", monospace !important;
@@ -37,6 +374,188 @@ body {
input,
textarea,
select {
+ border: 1px solid black !important;
+ background: white !important;
+ color: black !important;
font-family: "Ubuntu Mono", monospace !important;
}
+input:focus,
+textarea:focus,
+select:focus {
+ outline: 2px solid black !important;
+ outline-offset: -2px !important;
+ background: white !important;
+}
+
+/* Dark mode form fields */
+html.dark input,
+html.dark textarea,
+html.dark select {
+ border: 1px solid white !important;
+ background: #0a0a0a !important;
+ color: white !important;
+}
+
+html.dark input:focus,
+html.dark textarea:focus,
+html.dark select:focus {
+ outline: 2px solid white !important;
+ background: #0a0a0a !important;
+}
+
+/* Checkbox and radio button styling */
+input[type="checkbox"],
+input[type="radio"] {
+ border: 2px solid black !important;
+ background: white !important;
+}
+
+input[type="checkbox"]:checked,
+input[type="radio"]:checked {
+ background: black !important;
+ color: white !important;
+}
+
+html.dark input[type="checkbox"],
+html.dark input[type="radio"] {
+ border: 2px solid white !important;
+ background: #0a0a0a !important;
+}
+
+html.dark input[type="checkbox"]:checked,
+html.dark input[type="radio"]:checked {
+ background: white !important;
+ color: black !important;
+}
+
+/* Document titles */
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Ubuntu", monospace !important;
+ color: inherit !important;
+}
+
+/* All text */
+p,
+span,
+div {
+ color: inherit !important;
+ font-family: "Ubuntu", monospace !important;
+}
+
+/* =========================
+ HIDE ELEMENTS FROM PRINT
+ ========================= */
+
+.no-print,
+.no-pdf {
+ display: block;
+}
+
+/* =========================
+ PRINT STYLES
+ ========================= */
+
+@media print {
+ .no-print,
+ .no-pdf {
+ display: none !important;
+ }
+
+ .template-wrapper {
+ background: white !important;
+ padding: 0 !important;
+ min-height: auto !important;
+ }
+
+ .document-page {
+ max-width: none !important;
+ width: 100% !important;
+ margin: 0 !important;
+ box-shadow: none !important;
+ border-radius: 0 !important;
+ padding: 0 !important;
+ }
+
+ .document-page::before {
+ content: "";
+ display: block;
+ height: 0.5in;
+ }
+
+ .section-title {
+ font-size: 14pt;
+ page-break-after: avoid;
+ }
+
+ .section-card {
+ break-inside: avoid;
+ margin-bottom: 1rem;
+ padding: 0.5rem;
+ border: 1px solid #ccc;
+ box-shadow: none;
+ }
+
+ .checkbox-item {
+ font-size: 10pt;
+ margin: 2pt 0;
+ }
+
+ .inline-field,
+ .large-field,
+ .block-field {
+ background: none !important;
+ border: none !important;
+ border-bottom: 1pt solid #000 !important;
+ padding: 2pt !important;
+ }
+
+ .signature-space {
+ border: 1px solid #000;
+ background: none;
+ min-height: 3rem;
+ padding: 1rem;
+ }
+}
+
+/* =========================
+ MOBILE RESPONSIVENESS
+ ========================= */
+
+@media (max-width: 768px) {
+ .template-wrapper {
+ padding: 1rem;
+ }
+
+ .values-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .export-content {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 1rem;
+ }
+
+ /* Make principle cards full width on mobile */
+ .principle-grid {
+ grid-template-columns: 1fr;
+ }
+
+ /* Stack constraint buttons vertically on mobile */
+ .constraint-buttons {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .constraint-buttons button {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
diff --git a/components/ExportOptions.vue b/components/ExportOptions.vue
index c2e83b4..b4860e9 100644
--- a/components/ExportOptions.vue
+++ b/components/ExportOptions.vue
@@ -4,32 +4,31 @@
Export Options:
-
+ ref="copyButton"
+ >
Copy as Text
-
+
-
-
+ ref="downloadButton"
+ >
Download Markdown
-
+
-
@@ -45,9 +44,9 @@ interface Props {
}
const props = withDefaults(defineProps(), {
- filename: 'export',
- title: 'Export Data',
- containerClass: 'centered'
+ filename: "export",
+ title: "Export Data",
+ containerClass: "centered",
});
const isProcessing = ref(false);
@@ -58,53 +57,56 @@ const copyButton = ref();
const downloadButton = ref();
// Success feedback animation
-const showSuccessFeedback = (buttonRef: Ref, successRef: Ref) => {
+const showSuccessFeedback = (
+ buttonRef: Ref,
+ successRef: Ref
+) => {
if (!buttonRef.value) return;
-
+
successRef.value = true;
-
+
// Add checkmark overlay animation
const button = buttonRef.value;
- button.classList.add('success-state');
-
+ button.classList.add("success-state");
+
setTimeout(() => {
successRef.value = false;
- button.classList.remove('success-state');
+ button.classList.remove("success-state");
}, 2000);
};
const copyToClipboard = async () => {
if (isProcessing.value) return;
-
+
isProcessing.value = true;
-
+
try {
const textContent = extractTextContent();
-
+
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(textContent);
} else {
// Fallback for browsers without clipboard API
- const textarea = document.createElement('textarea');
+ const textarea = document.createElement("textarea");
textarea.value = textContent;
- textarea.style.position = 'fixed';
- textarea.style.opacity = '0';
+ textarea.style.position = "fixed";
+ textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
-
- const successful = document.execCommand('copy');
+
+ const successful = document.execCommand("copy");
document.body.removeChild(textarea);
-
+
if (!successful) {
- throw new Error('execCommand copy failed');
+ throw new Error("execCommand copy failed");
}
}
-
+
showSuccessFeedback(copyButton, showCopySuccess);
} catch (error) {
- console.error('Copy failed:', error);
- alert('Copy failed. Please try again or use the download option.');
+ console.error("Copy failed:", error);
+ alert("Copy failed. Please try again or use the download option.");
} finally {
isProcessing.value = false;
}
@@ -112,71 +114,74 @@ const copyToClipboard = async () => {
const downloadAsMarkdown = () => {
if (isProcessing.value) return;
-
+
isProcessing.value = true;
-
+
try {
const content = convertToMarkdown();
- downloadFile(content, `${props.filename}.md`, 'text/markdown');
+ downloadFile(content, `${props.filename}.md`, "text/markdown");
showSuccessFeedback(downloadButton, showDownloadSuccess);
} catch (error) {
- console.error('Markdown download failed:', error);
- alert('Download failed. Please try again.');
+ console.error("Markdown download failed:", error);
+ alert("Download failed. Please try again.");
} finally {
isProcessing.value = false;
}
};
-
const extractTextContent = (): string => {
- const today = new Date().toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
+ const today = new Date().toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
});
- let content = `${props.title.toUpperCase()}\n${'='.repeat(props.title.length)}\n\nExported ${today}\n\n`;
-
+ let content = `${props.title.toUpperCase()}\n${"=".repeat(
+ props.title.length
+ )}\n\nExported ${today}\n\n`;
+
// Convert data to readable text format
if (props.exportData) {
content += formatDataAsText(props.exportData);
}
-
+
return content;
};
const convertToMarkdown = (): string => {
- const today = new Date().toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
+ const today = new Date().toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
});
let content = `# ${props.title}\n\n*Exported ${today}*\n\n`;
-
+
// Convert data to markdown format
if (props.exportData) {
content += formatDataAsMarkdown(props.exportData);
}
-
+
return content;
};
const formatDataAsText = (data: any): string => {
// Special handling for different template types
- if (data.section === 'tech-charter') {
+ if (data.section === "tech-charter") {
return formatTechCharterAsText(data);
- } else if (data.section === 'membership-agreement') {
+ } else if (data.section === "membership-agreement") {
return formatMembershipAgreementAsText(data);
- } else if (data.section === 'conflict-resolution-framework') {
+ } else if (data.section === "conflict-resolution-framework") {
return formatConflictResolutionAsText(data);
- } else if (data.section === 'decision-framework') {
+ } else if (data.section === "decision-framework") {
return formatDecisionFrameworkAsText(data);
}
-
+
if (Array.isArray(data)) {
- return data.map((item, index) => `${index + 1}. ${formatObjectAsText(item)}`).join('\n');
- } else if (typeof data === 'object' && data !== null) {
+ return data
+ .map((item, index) => `${index + 1}. ${formatObjectAsText(item)}`)
+ .join("\n");
+ } else if (typeof data === "object" && data !== null) {
return formatObjectAsText(data);
}
return String(data);
@@ -184,19 +189,21 @@ const formatDataAsText = (data: any): string => {
const formatDataAsMarkdown = (data: any): string => {
// Special handling for different template types
- if (data.section === 'tech-charter') {
+ if (data.section === "tech-charter") {
return formatTechCharterAsMarkdown(data);
- } else if (data.section === 'membership-agreement') {
+ } else if (data.section === "membership-agreement") {
return formatMembershipAgreementAsMarkdown(data);
- } else if (data.section === 'conflict-resolution-framework') {
+ } else if (data.section === "conflict-resolution-framework") {
return formatConflictResolutionAsMarkdown(data);
- } else if (data.section === 'decision-framework') {
+ } else if (data.section === "decision-framework") {
return formatDecisionFrameworkAsMarkdown(data);
}
-
+
if (Array.isArray(data)) {
- return data.map((item, index) => `${index + 1}. ${formatObjectAsMarkdown(item)}`).join('\n\n');
- } else if (typeof data === 'object' && data !== null) {
+ return data
+ .map((item, index) => `${index + 1}. ${formatObjectAsMarkdown(item)}`)
+ .join("\n\n");
+ } else if (typeof data === "object" && data !== null) {
return formatObjectAsMarkdown(data);
}
return String(data);
@@ -208,7 +215,7 @@ const formatTechCharterAsText = (data: any): string => {
const selectedPrinciples = Object.keys(data.principleWeights).filter(
(p) => data.principleWeights[p] > 0
);
-
+
if (selectedPrinciples.filter((p) => !data.nonNegotiables.includes(p)).length > 0) {
content += `CORE PRINCIPLES\n---------------\n\n`;
selectedPrinciples
@@ -245,7 +252,9 @@ const formatTechCharterAsText = (data: any): string => {
if (data.sortedWeights.length > 0) {
content += `EVALUATION RUBRIC\n-----------------\n\nScore each vendor option using these weighted criteria (0-5 scale):\n\n`;
data.sortedWeights.forEach((principle: any) => {
- content += `${principle.name} (Weight: ${data.principleWeights[principle.id]})\n${principle.rubricDescription}\n\n`;
+ content += `${principle.name} (Weight: ${data.principleWeights[principle.id]})\n${
+ principle.rubricDescription
+ }\n\n`;
});
}
@@ -258,7 +267,7 @@ const formatTechCharterAsMarkdown = (data: any): string => {
const selectedPrinciples = Object.keys(data.principleWeights).filter(
(p) => data.principleWeights[p] > 0
);
-
+
if (selectedPrinciples.filter((p) => !data.nonNegotiables.includes(p)).length > 0) {
content += `## Core Principles\n\n`;
selectedPrinciples
@@ -297,7 +306,9 @@ const formatTechCharterAsMarkdown = (data: any): string => {
content += `| Criterion | Description | Weight |\n`;
content += `|-----------|-------------|--------|\n`;
data.sortedWeights.forEach((principle: any) => {
- content += `| ${principle.name} | ${principle.rubricDescription} | ${data.principleWeights[principle.id]} |\n`;
+ content += `| ${principle.name} | ${principle.rubricDescription} | ${
+ data.principleWeights[principle.id]
+ } |\n`;
});
}
@@ -305,75 +316,79 @@ const formatTechCharterAsMarkdown = (data: any): string => {
};
const formatObjectAsText = (obj: any): string => {
- if (!obj || typeof obj !== 'object') return String(obj);
-
+ if (!obj || typeof obj !== "object") return String(obj);
+
return Object.entries(obj)
- .filter(([key, value]) => value !== null && value !== undefined && key !== 'id')
+ .filter(([key, value]) => value !== null && value !== undefined && key !== "id")
.map(([key, value]) => {
- const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
- if (typeof value === 'object') {
+ const formattedKey = key
+ .replace(/([A-Z])/g, " $1")
+ .replace(/^./, (str) => str.toUpperCase());
+ if (typeof value === "object") {
return `${formattedKey}: ${JSON.stringify(value)}`;
}
return `${formattedKey}: ${value}`;
})
- .join('\n');
+ .join("\n");
};
const formatObjectAsMarkdown = (obj: any): string => {
- if (!obj || typeof obj !== 'object') return String(obj);
-
+ if (!obj || typeof obj !== "object") return String(obj);
+
return Object.entries(obj)
- .filter(([key, value]) => value !== null && value !== undefined && key !== 'id')
+ .filter(([key, value]) => value !== null && value !== undefined && key !== "id")
.map(([key, value]) => {
- const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
- if (typeof value === 'object') {
+ const formattedKey = key
+ .replace(/([A-Z])/g, " $1")
+ .replace(/^./, (str) => str.toUpperCase());
+ if (typeof value === "object") {
return `**${formattedKey}**: \`${JSON.stringify(value)}\``;
}
return `**${formattedKey}**: ${value}`;
})
- .join(' \n');
+ .join(" \n");
};
// Membership Agreement formatting
const formatMembershipAgreementAsText = (data: any): string => {
let content = `MEMBERSHIP AGREEMENT\n====================\n\n`;
content += `Cooperative Name: ${data.cooperativeName}\n`;
- content += `Member Name: ${data.formData.memberName || '[Member Name]'}\n`;
- content += `Effective Date: ${data.formData.effectiveDate || '[Date]'}\n\n`;
-
+ content += `Member Name: ${data.formData.memberName || "[Member Name]"}\n`;
+ content += `Effective Date: ${data.formData.effectiveDate || "[Date]"}\n\n`;
+
if (data.formData.purpose) {
content += `PURPOSE\n-------\n${data.formData.purpose}\n\n`;
}
-
+
if (data.formData.membershipRequirements) {
content += `MEMBERSHIP REQUIREMENTS\n----------------------\n${data.formData.membershipRequirements}\n\n`;
}
-
+
if (data.formData.rightsAndResponsibilities) {
content += `RIGHTS AND RESPONSIBILITIES\n--------------------------\n${data.formData.rightsAndResponsibilities}\n\n`;
}
-
+
return content;
};
const formatMembershipAgreementAsMarkdown = (data: any): string => {
let content = `## Membership Agreement\n\n`;
content += `**Cooperative Name:** ${data.cooperativeName} \n`;
- content += `**Member Name:** ${data.formData.memberName || '[Member Name]'} \n`;
- content += `**Effective Date:** ${data.formData.effectiveDate || '[Date]'} \n\n`;
-
+ content += `**Member Name:** ${data.formData.memberName || "[Member Name]"} \n`;
+ content += `**Effective Date:** ${data.formData.effectiveDate || "[Date]"} \n\n`;
+
if (data.formData.purpose) {
content += `### Purpose\n\n${data.formData.purpose}\n\n`;
}
-
+
if (data.formData.membershipRequirements) {
content += `### Membership Requirements\n\n${data.formData.membershipRequirements}\n\n`;
}
-
+
if (data.formData.rightsAndResponsibilities) {
content += `### Rights and Responsibilities\n\n${data.formData.rightsAndResponsibilities}\n\n`;
}
-
+
return content;
};
@@ -383,33 +398,35 @@ const formatConflictResolutionAsText = (data: any): string => {
content += `Organization: ${data.orgName}\n`;
content += `Organization Type: ${data.orgType}\n`;
content += `Member Count: ${data.memberCount}\n\n`;
-
+
if (data.coreValues?.length > 0) {
content += `CORE VALUES\n-----------\n`;
data.coreValues.forEach((value: string, index: number) => {
content += `${index + 1}. ${value}\n`;
});
- content += '\n';
+ content += "\n";
}
-
+
if (data.principles?.length > 0) {
content += `PRINCIPLES\n----------\n`;
data.principles.forEach((principle: string, index: number) => {
content += `${index + 1}. ${principle}\n`;
});
- content += '\n';
+ content += "\n";
}
-
+
if (data.policies) {
content += `POLICIES\n--------\n`;
Object.entries(data.policies).forEach(([key, value]) => {
if (value) {
- const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
+ const formattedKey = key
+ .replace(/([A-Z])/g, " $1")
+ .replace(/^./, (str) => str.toUpperCase());
content += `${formattedKey}: ${value}\n`;
}
});
}
-
+
return content;
};
@@ -418,40 +435,42 @@ const formatConflictResolutionAsMarkdown = (data: any): string => {
content += `**Organization:** ${data.orgName} \n`;
content += `**Organization Type:** ${data.orgType} \n`;
content += `**Member Count:** ${data.memberCount} \n\n`;
-
+
if (data.coreValues?.length > 0) {
content += `### Core Values\n\n`;
data.coreValues.forEach((value: string, index: number) => {
content += `${index + 1}. ${value}\n`;
});
- content += '\n';
+ content += "\n";
}
-
+
if (data.principles?.length > 0) {
content += `### Principles\n\n`;
data.principles.forEach((principle: string, index: number) => {
content += `${index + 1}. ${principle}\n`;
});
- content += '\n';
+ content += "\n";
}
-
+
if (data.policies) {
content += `### Policies\n\n`;
Object.entries(data.policies).forEach(([key, value]) => {
if (value) {
- const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
+ const formattedKey = key
+ .replace(/([A-Z])/g, " $1")
+ .replace(/^./, (str) => str.toUpperCase());
content += `**${formattedKey}:** ${value} \n`;
}
});
}
-
+
return content;
};
// Decision Framework formatting
const formatDecisionFrameworkAsText = (data: any): string => {
let content = `DECISION FRAMEWORK RESULTS\n=========================\n\n`;
-
+
if (data.surveyResponses) {
content += `SURVEY RESPONSES\n----------------\n`;
content += `Urgency: ${data.surveyResponses.urgency}\n`;
@@ -462,37 +481,37 @@ const formatDecisionFrameworkAsText = (data: any): string => {
content += `Investment: ${data.surveyResponses.investment}\n`;
content += `Team Size: ${data.surveyResponses.teamSize}\n\n`;
}
-
+
if (data.recommendedFramework) {
const framework = data.recommendedFramework;
content += `RECOMMENDED FRAMEWORK\n--------------------\n`;
content += `Method: ${framework.method}\n`;
content += `Tagline: ${framework.tagline}\n\n`;
content += `Reasoning: ${framework.reasoning}\n\n`;
-
+
if (framework.steps) {
content += `IMPLEMENTATION STEPS\n-------------------\n`;
framework.steps.forEach((step: string, index: number) => {
content += `${index + 1}. ${step}\n`;
});
- content += '\n';
+ content += "\n";
}
-
+
if (framework.tips) {
content += `PRO TIPS\n--------\n`;
framework.tips.forEach((tip: string, index: number) => {
content += `${index + 1}. ${tip}\n`;
});
- content += '\n';
+ content += "\n";
}
}
-
+
return content;
};
const formatDecisionFrameworkAsMarkdown = (data: any): string => {
let content = `## Decision Framework Results\n\n`;
-
+
if (data.surveyResponses) {
content += `### Survey Responses\n\n`;
content += `**Urgency:** ${data.surveyResponses.urgency} \n`;
@@ -503,38 +522,38 @@ const formatDecisionFrameworkAsMarkdown = (data: any): string => {
content += `**Investment:** ${data.surveyResponses.investment} \n`;
content += `**Team Size:** ${data.surveyResponses.teamSize} \n\n`;
}
-
+
if (data.recommendedFramework) {
const framework = data.recommendedFramework;
content += `### Recommended Framework\n\n`;
content += `**Method:** ${framework.method} \n`;
content += `**Tagline:** ${framework.tagline} \n\n`;
content += `**Reasoning:** ${framework.reasoning}\n\n`;
-
+
if (framework.steps) {
content += `#### Implementation Steps\n\n`;
framework.steps.forEach((step: string, index: number) => {
content += `${index + 1}. ${step}\n`;
});
- content += '\n';
+ content += "\n";
}
-
+
if (framework.tips) {
content += `#### Pro Tips\n\n`;
framework.tips.forEach((tip: string, index: number) => {
content += `${index + 1}. ${tip}\n`;
});
- content += '\n';
+ content += "\n";
}
}
-
+
return content;
};
const downloadFile = (content: string, filename: string, type: string) => {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
+ const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
@@ -545,31 +564,7 @@ const downloadFile = (content: string, filename: string, type: string) => {
\ No newline at end of file
+
diff --git a/pages/templates/conflict-resolution-framework.vue b/pages/templates/conflict-resolution-framework.vue
index 00bc782..450dc98 100644
--- a/pages/templates/conflict-resolution-framework.vue
+++ b/pages/templates/conflict-resolution-framework.vue
@@ -4,13 +4,12 @@
-
-
-
+
+
@@ -1392,57 +1389,9 @@ const exportData = computed(() => ({
diff --git a/pages/templates/decision-framework.vue b/pages/templates/decision-framework.vue
index 5a0a9a3..ac16d47 100644
--- a/pages/templates/decision-framework.vue
+++ b/pages/templates/decision-framework.vue
@@ -4,34 +4,39 @@
-
-
-
+
+ style="font-family: 'Ubuntu', monospace"
+ >
+ class="bg-white dark:bg-neutral-950 border-2 border-neutral-900 dark:border-neutral-100 decision-framework-container"
+ >
+ class="relative bg-black dark:bg-white text-white dark:text-black px-4 py-4 border-2 border-neutral-100 dark:border-neutral-900"
+ >
+ style="font-family: 'Ubuntu', monospace"
+ >
Decision Framework Helper
@@ -41,24 +46,22 @@
- Step {{ currentStep }} of {{ totalSteps }}
- {{ Math.round((currentStep / totalSteps) * 100) }}%
+ class="w-full bg-white dark:bg-black h-2 border-2 border-neutral-100 dark:border-neutral-900"
+ >
+ }"
+ >
@@ -72,14 +75,15 @@
+ style="font-family: 'Ubuntu', monospace"
+ >
How urgent is this decision?
+ class="bg-white dark:bg-neutral-950 p-8 border-2 border-neutral-900 dark:border-neutral-100 relative"
+ >
-
+
@@ -101,10 +105,12 @@
min="1"
max="5"
step="1"
- class="w-full h-2 bg-white dark:bg-black appearance-none cursor-pointer slider" />
+ class="w-full h-2 bg-white dark:bg-black appearance-none cursor-pointer slider"
+ />
+ style="font-family: 'Ubuntu Mono', monospace"
+ >
1
2
3
@@ -120,7 +126,8 @@
+ style="font-family: 'Ubuntu', monospace"
+ >
Can we change our minds later?
@@ -133,7 +140,8 @@
? 'border-violet-700 bg-violet-700 text-white'
: 'border-neutral-200 hover:border-violet-700 hover:bg-violet-50',
]"
- @click="selectOption('reversible', option.value)">
+ @click="selectOption('reversible', option.value)"
+ >
{{ option.title }}
{{ option.description }}
@@ -157,7 +165,8 @@
? 'border-violet-700 bg-violet-700 text-white'
: 'border-neutral-200 hover:border-violet-700 hover:bg-violet-50',
]"
- @click="selectOption('expertise', option.value)">
+ @click="selectOption('expertise', option.value)"
+ >
{{ option.title }}
{{ option.description }}
@@ -181,7 +190,8 @@
? 'border-violet-700 bg-violet-700 text-white'
: 'border-neutral-200 hover:border-violet-700 hover:bg-violet-50',
]"
- @click="selectOption('impact', option.value)">
+ @click="selectOption('impact', option.value)"
+ >
{{ option.title }}
{{ option.description }}
@@ -205,7 +215,8 @@
? 'border-violet-700 bg-violet-700 text-white'
: 'border-neutral-200 hover:border-violet-700 hover:bg-violet-50',
]"
- @click="selectOption('options', option.value)">
+ @click="selectOption('options', option.value)"
+ >
{{ option.title }}
{{ option.description }}
@@ -229,7 +240,8 @@
? 'border-violet-700 bg-violet-700 text-white'
: 'border-neutral-200 hover:border-violet-700 hover:bg-violet-50',
]"
- @click="selectOption('investment', option.value)">
+ @click="selectOption('investment', option.value)"
+ >
{{ option.title }}
{{ option.description }}
@@ -253,7 +265,8 @@
? 'bg-violet-700 text-white border-violet-700'
: 'bg-white text-neutral-700 border-neutral-200 hover:border-violet-700 hover:bg-violet-50',
]"
- @click="selectOption('teamSize', size)">
+ @click="selectOption('teamSize', size)"
+ >
{{ size }}
@@ -261,11 +274,13 @@
+ class="flex justify-between items-center mt-12 pt-8 border-t-2 border-neutral-200"
+ >
+ class="px-6 py-3 text-violet-700 border-2 border-violet-700 rounded-md hover:bg-violet-50 transition-all duration-200"
+ >
← Previous
@@ -273,13 +288,15 @@
+ class="px-6 py-3 bg-violet-700 text-white rounded-md hover:bg-violet-800 transition-all duration-200"
+ >
Next →
+ class="px-6 py-3 bg-violet-700 text-white rounded-md hover:bg-violet-800 transition-all duration-200"
+ >
Get Recommendation
@@ -289,7 +306,8 @@
+ class="border-t-2 border-neutral-200 pt-12"
+ >
@@ -317,10 +335,9 @@
- →
+ class="flex items-start"
+ >
+ →
{{ step }}
@@ -334,10 +351,9 @@
- →
+ class="flex items-start"
+ >
+ →
{{ tip }}
@@ -351,7 +367,8 @@
variant="soft"
:title="'Watch out for:'"
:description="result.warning"
- class="mb-6" />
+ class="mb-6"
+ />
+ class="mb-6"
+ />
@@ -369,7 +387,8 @@
+ class="bg-white"
+ >
{{ alt.method }}:
{{ alt.when }}
@@ -380,10 +399,7 @@
Try Another Decision
-
+
Print Recommendation
@@ -395,17 +411,16 @@
-
-
+ title="Decision Framework Helper"
+ />
\ No newline at end of file
+
diff --git a/pages/templates/tech-charter.vue b/pages/templates/tech-charter.vue
index 3101e0d..e9ba808 100644
--- a/pages/templates/tech-charter.vue
+++ b/pages/templates/tech-charter.vue
@@ -4,23 +4,23 @@
-
-
-
+
+ class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100"
+ >
+ class="text-3xl md:text-5xl font-bold uppercase text-neutral-900 dark:text-white m-0 py-4 border-t-2 border-b-2 border-neutral-900 dark:border-neutral-100"
+ >
Tech Charter
@@ -30,12 +30,9 @@
-
- Charter Purpose
-
+
Charter Purpose
- Describe what this charter will guide and why it matters to
- your group.
+ Describe what this charter will guide and why it matters to your group.
@@ -43,7 +40,8 @@
+ rows="4"
+ />
@@ -54,20 +52,18 @@
Define Your Principles & Importance
- Select principles and set their importance. Zero means
- excluded, 5 means critical.
+ Select principles and set their importance. Zero means excluded, 5 means
+ critical.
-
+
+ class="absolute top-2 left-2 w-full h-full dither-shadow"
+ >
+ ]"
+ >
@@ -83,10 +80,9 @@
+ principleWeights[principle.id] > 0 ? 'selected' : '',
+ ]"
+ >
{{ principle.name }}
@@ -96,7 +92,8 @@
? 'text-neutral-700'
: 'text-neutral-600'
"
- class="text-sm">
+ class="text-sm"
+ >
{{ principle.description }}
@@ -105,7 +102,8 @@
+ class="text-xs font-bold text-neutral-500 uppercase tracking-wider"
+ >
Importance
@@ -121,7 +119,8 @@
? 'bg-black text-white border-black dark:bg-white dark:text-black dark:border-white'
: 'bg-white border-neutral-300 hover:border-neutral-500 dark:bg-neutral-950',
]"
- :title="`Set importance to ${level}`">
+ :title="`Set importance to ${level}`"
+ >
{{ level }}
@@ -132,11 +131,7 @@
{{ principleWeights[principle.id] || 0 }}
- {{
- getWeightLabel(
- principleWeights[principle.id] || 0
- )
- }}
+ {{ getWeightLabel(principleWeights[principle.id] || 0) }}
@@ -145,19 +140,20 @@
+ class="mt-4 pt-4 border-t border-neutral-200"
+ >
+ nonNegotiables.includes(principle.id) ? 'selected' : '',
+ ]"
+ >
+ class="w-4 h-4"
+ />
Make this non-negotiable
@@ -167,9 +163,9 @@
-
+ class="mt-4 p-3 principle-label-bg selected border border-neutral-200"
+ >
+
Evaluation Criteria:
@@ -187,7 +183,8 @@
+ id="constraints-heading"
+ >
Technical Constraints
@@ -198,15 +195,18 @@
@@ -229,15 +230,18 @@
@@ -256,32 +261,29 @@
-
- Required Integrations
-
-
- Select all that apply
-
+ Required Integrations
+ Select all that apply
@@ -289,21 +291,22 @@
-
- Support Expectations
-
+ Support Expectations
@@ -322,21 +326,22 @@
-
- Migration Timeline
-
+ Migration Timeline
@@ -361,7 +367,8 @@
+ title="Clear all form data and start over"
+ >
Reset Form
@@ -374,19 +381,17 @@
v-if="charterGenerated"
class="relative animate-fadeIn"
role="main"
- aria-label="Generated Technology Charter">
+ aria-label="Generated Technology Charter"
+ >
-
+
-
-
+ class="relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white p-8"
+ >
+
+
Technology Charter
@@ -402,7 +407,8 @@
+ class="text-sm text-neutral-600 hover:text-neutral-800 underline focus:outline-none focus:ring-2 focus:ring-neutral-500 rounded"
+ >
Back to form
@@ -412,10 +418,10 @@
Purpose
- This charter guides our cooperative's technology decisions
- based on our shared values and operational needs. It ensures
- we choose tools that support our mission while respecting our
- principles of autonomy, sustainability, and mutual aid.
+ This charter guides our cooperative's technology decisions based on our
+ shared values and operational needs. It ensures we choose tools that
+ support our mission while respecting our principles of autonomy,
+ sustainability, and mutual aid.
@@ -423,25 +429,21 @@
class="mb-8"
v-if="
Object.keys(principleWeights).filter(
- (p) =>
- principleWeights[p] > 0 && !nonNegotiables.includes(p)
+ (p) => principleWeights[p] > 0 && !nonNegotiables.includes(p)
).length > 0
- ">
-
- Core Principles
-
+ "
+ >
+
Core Principles
+ class="flex items-start"
+ >
→
- {{
- principles.find((p) => p.id === principleId)?.name
- }}
+ {{ principles.find((p) => p.id === principleId)?.name }}
@@ -451,18 +453,16 @@
Non-Negotiable Requirements
- Any vendor failing these requirements is automatically
- disqualified.
+ Any vendor failing these requirements is automatically disqualified.
+ class="flex items-start text-red-600 font-semibold"
+ >
→
- {{
- principles.find((p) => p.id === principleId)?.name
- }}
+ {{ principles.find((p) => p.id === principleId)?.name }}
@@ -477,8 +477,7 @@
Authentication:
{{
- authOptions.find((o) => o.value === constraints.sso)
- ?.label
+ authOptions.find((o) => o.value === constraints.sso)?.label
}}
@@ -487,15 +486,11 @@
Hosting:
{{
- hostingOptions.find(
- (o) => o.value === constraints.hosting
- )?.label
+ hostingOptions.find((o) => o.value === constraints.hosting)?.label
}}
-
+
→
Required Integrations:
@@ -507,9 +502,7 @@
Support Level:
{{
- supportOptions.find(
- (o) => o.value === constraints.support
- )?.label
+ supportOptions.find((o) => o.value === constraints.support)?.label
}}
@@ -518,9 +511,8 @@
Migration Timeline:
{{
- timelineOptions.find(
- (o) => o.value === constraints.timeline
- )?.label
+ timelineOptions.find((o) => o.value === constraints.timeline)
+ ?.label
}}
@@ -528,27 +520,27 @@
-
- Evaluation Rubric
-
+ Evaluation Rubric
- Score each vendor option using these weighted criteria (0-5
- scale):
+ Score each vendor option using these weighted criteria (0-5 scale):
+ class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-left"
+ >
Criterion
+ class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-left"
+ >
Description
+ class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center"
+ >
Weight
@@ -557,17 +549,21 @@
+ class="hover:bg-neutral-50"
+ >
+ class="border-2 border-neutral-900 dark:border-neutral-100 p-3 font-semibold"
+ >
{{ weight.name }}
+ class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-sm text-neutral-600"
+ >
{{ weight.rubricDescription }}
+ class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center font-bold text-neutral-600"
+ >
{{ principleWeights[weight.id] }}
@@ -584,8 +580,8 @@
→
Any vendor failing a non-negotiable requirement is
- automatically eliminated Any vendor failing a non-negotiable requirement is automatically
+ eliminated
@@ -598,8 +594,8 @@
→
When scores are within 10%, choose based on alignment
- with cooperative values When scores are within 10%, choose based on alignment with
+ cooperative values
@@ -642,16 +638,11 @@
→
- Document any exceptions with clear justification
+ Document any exceptions with clear justification
→
- Share learnings with other cooperatives in our
- network
+ Share learnings with other cooperatives in our network
@@ -662,13 +653,11 @@
-
-
-
+
@@ -716,15 +705,13 @@ const principles = [
id: "portability",
name: "Data Freedom",
description: "Easy export, no vendor lock-in, migration-friendly",
- rubricDescription:
- "Export capabilities, proprietary formats, switching costs",
+ rubricDescription: "Export capabilities, proprietary formats, switching costs",
defaultWeight: 4,
},
{
id: "opensource",
name: "Open Source & Community",
- description:
- "FOSS preference, transparent development, community governance",
+ description: "FOSS preference, transparent development, community governance",
rubricDescription: "License type, community involvement, code transparency",
defaultWeight: 3,
},
@@ -732,8 +719,7 @@ const principles = [
id: "sustainability",
name: "Sustainable Operations",
description: "Predictable costs, green hosting, efficient resource use",
- rubricDescription:
- "Total cost of ownership, carbon footprint, resource efficiency",
+ rubricDescription: "Total cost of ownership, carbon footprint, resource efficiency",
defaultWeight: 3,
},
{
@@ -746,10 +732,8 @@ const principles = [
{
id: "usability",
name: "User Experience",
- description:
- "Intuitive interface, minimal learning curve, daily efficiency",
- rubricDescription:
- "Onboarding time, user satisfaction, workflow integration",
+ description: "Intuitive interface, minimal learning curve, daily efficiency",
+ rubricDescription: "Onboarding time, user satisfaction, workflow integration",
defaultWeight: 3,
},
];
@@ -785,9 +769,7 @@ const timelineOptions = [
const sortedWeights = computed(() => {
return principles
.filter((p) => principleWeights.value[p.id] > 0)
- .sort(
- (a, b) => principleWeights.value[b.id] - principleWeights.value[a.id]
- );
+ .sort((a, b) => principleWeights.value[b.id] - principleWeights.value[a.id]);
});
const canGenerateCharter = computed(() => {
@@ -805,15 +787,15 @@ const exportData = computed(() => ({
principleWeights: principleWeights.value,
nonNegotiables: nonNegotiables.value,
constraints: constraints.value,
- principles: principles.filter(p => principleWeights.value[p.id] > 0),
+ principles: principles.filter((p) => principleWeights.value[p.id] > 0),
sortedWeights: sortedWeights.value,
summary: {
selectedPrincipleCount: selectedPrincipleCount.value,
nonNegotiableCount: nonNegotiables.value.length,
- canGenerateCharter: canGenerateCharter.value
+ canGenerateCharter: canGenerateCharter.value,
},
exportedAt: new Date().toISOString(),
- section: "tech-charter"
+ section: "tech-charter",
}));
// Methods
@@ -880,12 +862,9 @@ const resetForm = () => {
};
const scrollToTop = () => {
- document
- .querySelector(".template-wrapper")
- .scrollIntoView({ behavior: "smooth" });
+ document.querySelector(".template-wrapper").scrollIntoView({ behavior: "smooth" });
};
-
// Load saved data
const loadSavedData = () => {
const saved = localStorage.getItem("tech-charter-data");
@@ -916,7 +895,6 @@ const autoSave = () => {
localStorage.setItem("tech-charter-data", JSON.stringify(data));
};
-
// Load data on mount
onMounted(() => {
// Initialize all principle weights to 0
@@ -927,28 +905,16 @@ onMounted(() => {
});
// Auto-save when data changes
-watch(
- [charterPurpose, principleWeights, nonNegotiables, constraints],
- autoSave,
- {
- deep: true,
- }
-);
+watch([charterPurpose, principleWeights, nonNegotiables, constraints], autoSave, {
+ deep: true,
+});