app/pages/tools/templates/tech-charter.vue

967 lines
37 KiB
Vue

<template>
<div>
<!-- Export Options - Top -->
<ExportOptions
:export-data="exportData"
filename="tech-charter"
title="Technology Charter" />
<div
class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
<!-- Document Container -->
<div class="document-page">
<div class="template-content">
<!-- Document Header -->
<div class="text-center mb-8">
<h1
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
</h1>
</div>
<!-- Content -->
<div class="">
<!-- Purpose Section -->
<div class="section-card">
<div>
<h2
class="text-2xl font-bold text-neutral-800 dark:text-white font-display mb-4">
Charter Purpose
</h2>
<p class="text-neutral-600 dark:text-neutral-400 mb-4">
Describe what this charter will guide and why it matters to
you.
</p>
</div>
<div class="relative">
<textarea
v-model="charterPurpose"
class="w-full min-h-32 p-4 border-2 border-neutral-300 bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100 focus:border-black dark:focus:border-white transition-colors resize-y"
rows="4" />
</div>
</div>
<!-- Unified Principles & Importance Section -->
<div class="section-card">
<div>
<h2
class="text-2xl font-bold text-neutral-800 dark:text-white font-display mb-4">
Define Your Principles & Importance
</h2>
<p class="text-neutral-600 dark:text-neutral-400 mb-6">
Select principles and set their importance. Zero means
excluded, 5 means critical.
</p>
</div>
<div class="grid md:grid-cols-1 gap-4">
<div
v-for="principle in principles"
:key="principle.id"
class="relative">
<!-- Dithered shadow for selected cards -->
<div
v-if="principleWeights[principle.id] > 0"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<div
:class="[
'relative transition-all',
principleWeights[principle.id] > 0
? 'item-selected border-2 border-black dark:border-neutral-400 bg-white dark:bg-neutral-950'
: 'border border-black dark:border-white bg-transparent',
]">
<div class="p-6">
<div class="flex items-start gap-6">
<!-- Principle info -->
<div class="flex-1">
<div
:class="[
'item-text-bg mb-3',
principleWeights[principle.id] > 0
? 'selected'
: '',
]">
<h3 class="font-bold text-lg mb-2">
{{ principle.name }}
</h3>
<p
:class="
principleWeights[principle.id] > 0
? 'text-neutral-700'
: 'text-neutral-600'
"
class="text-sm dark:text-neutral-200">
{{ principle.description }}
</p>
</div>
</div>
<!-- Importance selector -->
<div class="flex flex-col items-center gap-2">
<label
class="text-xs font-bold text-neutral-500 uppercase tracking-wider">
Importance
</label>
<!-- Visual weight indicator -->
<div class="flex gap-1 mb-2">
<button
v-for="level in [0, 1, 2, 3, 4, 5]"
:key="level"
@click="setPrincipleWeight(principle.id, level)"
:class="[
'w-8 h-8 border-2 font-mono text-sm transition-all',
principleWeights[principle.id] >= level
? '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}`">
{{ level }}
</button>
</div>
<!-- Weight value display -->
<div class="text-center">
<div class="text-2xl font-bold">
{{ principleWeights[principle.id] || 0 }}
</div>
<div class="text-xs text-neutral-500">
{{
getWeightLabel(
principleWeights[principle.id] || 0
)
}}
</div>
</div>
</div>
</div>
<!-- Non-negotiable toggle (only shows for weights > 0) -->
<div
v-if="principleWeights[principle.id] > 0"
class="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-800">
<UCheckbox
:model-value="nonNegotiables.includes(principle.id)"
@update:model-value="
(checked) =>
toggleNonNegotiableCheckbox(principle.id, checked)
"
label="Make this non-negotiable"
class="item-label-bg px-2 py-1" />
</div>
<!-- Show rubric description when selected -->
<div
v-if="principleWeights[principle.id] > 0"
class="mt-4 p-3 item-label-bg selected border border-neutral-200">
<div
class="text-xs font-bold uppercase text-neutral-800 dark:text-neutral-300 mb-1">
Evaluation Criteria:
</div>
<div class="text-sm">
{{ principle.rubricDescription }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Constraints Section -->
<div class="section-card">
<div>
<h2
class="text-2xl font-bold text-neutral-800 dark:text-white font-display mb-2"
id="constraints-heading">
Technical Constraints
</h2>
</div>
<div class="space-y-6">
<fieldset class="bg-neutral-50 dark:bg-neutral-800 p-6">
<legend class="font-semibold text-lg dark:text-neutral-200">
Authentication
</legend>
<div
class="flex flex-wrap gap-3 constraint-buttons"
role="radiogroup"
aria-labelledby="auth-heading">
<div
v-for="option in authOptions"
:key="option.value"
class="relative">
<!-- Dithered shadow for selected buttons -->
<div
v-if="constraints.sso === option.value"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<button
@click="constraints.sso = option.value"
:aria-pressed="constraints.sso === option.value"
role="radio"
:aria-checked="constraints.sso === option.value"
:class="[
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
constraints.sso === option.value
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
]">
{{ option.label }}
</button>
</div>
</div>
</fieldset>
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
<legend class="font-semibold text-lg dark:text-neutral-200">
Hosting Model
</legend>
<div
class="flex flex-wrap gap-3 constraint-buttons"
role="radiogroup"
aria-labelledby="hosting-heading">
<div
v-for="option in hostingOptions"
:key="option.value"
class="relative">
<!-- Dithered shadow for selected buttons -->
<div
v-if="constraints.hosting === option.value"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<button
@click="constraints.hosting = option.value"
:aria-pressed="constraints.hosting === option.value"
role="radio"
:aria-checked="constraints.hosting === option.value"
:class="[
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
constraints.hosting === option.value
? 'constraint-selected border-black dark:border-neutral-400 cursor-pointer '
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
]">
{{ option.label }}
</button>
</div>
</div>
</fieldset>
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
<legend class="font-semibold text-lg dark:text-neutral-200">
Required Integrations
</legend>
<p
class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
Select all that apply
</p>
<div class="flex flex-wrap gap-3 constraint-buttons">
<div
v-for="integration in integrationOptions"
:key="integration"
class="relative">
<!-- Dithered shadow for selected buttons -->
<div
v-if="constraints.integrations.includes(integration)"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<button
@click="toggleIntegration(integration)"
:aria-pressed="
constraints.integrations.includes(integration)
"
:class="[
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
constraints.integrations.includes(integration)
? 'constraint-selected border-black dark:border-neutral-400 cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
]">
{{ integration }}
</button>
</div>
</div>
</fieldset>
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
<legend class="font-semibold text-lg">
Support Expectations
</legend>
<div
class="flex flex-wrap gap-3 constraint-buttons"
role="radiogroup"
aria-labelledby="support-heading">
<div
v-for="option in supportOptions"
:key="option.value"
class="relative">
<!-- Dithered shadow for selected buttons -->
<div
v-if="constraints.support === option.value"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<button
@click="constraints.support = option.value"
:aria-pressed="constraints.support === option.value"
role="radio"
:aria-checked="constraints.support === option.value"
:class="[
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
constraints.support === option.value
? 'constraint-selected border-black dark:border-neutral-400 cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
]">
{{ option.label }}
</button>
</div>
</div>
</fieldset>
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
<legend class="font-semibold text-lg dark:text-neutral-200">
Migration Timeline
</legend>
<div
class="flex flex-wrap gap-3 constraint-buttons"
role="radiogroup"
aria-labelledby="timeline-heading">
<div
v-for="option in timelineOptions"
:key="option.value"
class="relative">
<!-- Dithered shadow for selected buttons -->
<div
v-if="constraints.timeline === option.value"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<button
@click="constraints.timeline = option.value"
:aria-pressed="constraints.timeline === option.value"
role="radio"
:aria-checked="constraints.timeline === option.value"
:class="[
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
constraints.timeline === option.value
? 'constraint-selected border-black dark:border-neutral-400 cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
]">
{{ option.label }}
</button>
</div>
</div>
</fieldset>
</div>
</div>
<!-- Reset Form Section -->
<div v-if="canGenerateCharter" class="text-center mt-8">
<button
@click="resetForm"
class="export-btn"
title="Clear all form data and start over">
<UIcon name="i-heroicons-arrow-path" />
Reset Form
</button>
</div>
</div>
</div>
<!-- Generated Charter Output -->
<div
v-if="charterGenerated"
class="relative animate-fadeIn"
role="main"
aria-label="Generated Technology Charter">
<!-- Dithered shadow -->
<div
class="absolute top-4 left-4 right-0 bottom-0 dither-shadow"></div>
<!-- Charter container -->
<div
class="relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white p-8">
<div
class="text-center mb-8 pb-6 border-b-2 border-black dark:border-white">
<h2
class="text-3xl font-bold text-neutral-800"
id="charter-title">
Technology Charter
</h2>
<p class="text-neutral-600 mt-2">
Generated
{{
new Date().toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
}}
</p>
<div class="mt-4">
<button
@click="scrollToTop"
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
</button>
</div>
</div>
<div class="prose max-w-none">
<section class="mb-8">
<h3 class="text-xl font-bold text-neutral-800 mb-3">Purpose</h3>
<p class="text-neutral-700 leading-relaxed">
This charter guides our cooperative's technology decisions, so
that we can choose tools that don't contradict our values.
</p>
</section>
<section
class="mb-8"
v-if="
Object.keys(principleWeights).filter(
(p) =>
principleWeights[p] > 0 && !nonNegotiables.includes(p)
).length > 0
">
<h3 class="text-xl font-bold text-neutral-800 mb-3">
Core Principles
</h3>
<ul class="space-y-2">
<li
v-for="principleId in Object.keys(principleWeights).filter(
(p) =>
principleWeights[p] > 0 && !nonNegotiables.includes(p)
)"
:key="principleId"
class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span>{{
principles.find((p) => p.id === principleId)?.name
}}</span>
</li>
</ul>
</section>
<section class="mb-8" v-if="nonNegotiables.length > 0">
<h3 class="text-xl font-bold text-neutral-800 mb-3">
Non-Negotiable Requirements
</h3>
<p class="text-red-600 font-semibold mb-3">
Any vendor failing these requirements is automatically
disqualified.
</p>
<ul class="space-y-2">
<li
v-for="principleId in nonNegotiables"
:key="principleId"
class="flex items-start text-red-600 font-semibold">
<span class="mr-2">→</span>
<span>{{
principles.find((p) => p.id === principleId)?.name
}}</span>
</li>
</ul>
</section>
<section class="mb-8">
<h3 class="text-xl font-bold text-neutral-800 mb-3">
Technical Constraints
</h3>
<ul class="space-y-2">
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Authentication:
{{
authOptions.find((o) => o.value === constraints.sso)
?.label
}}</span
>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Hosting:
{{
hostingOptions.find(
(o) => o.value === constraints.hosting
)?.label
}}</span
>
</li>
<li
v-if="constraints.integrations.length > 0"
class="flex items-start">
<span class="text-purple-600 mr-2">→</span>
<span
>Required Integrations:
{{ constraints.integrations.join(", ") }}</span
>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Support Level:
{{
supportOptions.find(
(o) => o.value === constraints.support
)?.label
}}</span
>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Migration Timeline:
{{
timelineOptions.find(
(o) => o.value === constraints.timeline
)?.label
}}</span
>
</li>
</ul>
</section>
<section class="mb-8">
<h3 class="text-xl font-bold text-neutral-800 mb-3">
Evaluation Rubric
</h3>
<p class="text-neutral-700 mb-4">
Score each vendor option using these weighted criteria (0-5
scale):
</p>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-neutral-100">
<th
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-left">
Criterion
</th>
<th
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-left">
Description
</th>
<th
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center">
Weight
</th>
</tr>
</thead>
<tbody>
<tr
v-for="weight in sortedWeights"
:key="weight.id"
class="hover:bg-neutral-50">
<td
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 font-semibold">
{{ weight.name }}
</td>
<td
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-sm text-neutral-600">
{{ weight.rubricDescription }}
</td>
<td
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center font-bold text-neutral-600">
{{ principleWeights[weight.id] }}
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="mb-8">
<h3 class="text-xl font-bold text-neutral-800 mb-3">
Decision Heuristics
</h3>
<ul class="space-y-2">
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Any vendor failing a non-negotiable requirement is
automatically eliminated</span
>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Prefer open standards and clear data export over feature
abundance</span
>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>When scores are within 10%, choose based on alignment
with cooperative values</span
>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Document all decisions in the Vendor Decision Log for
transparency</span
>
</li>
</ul>
</section>
<section class="mb-8">
<h3 class="text-xl font-bold text-neutral-800 mb-3">
Procurement Process
</h3>
<ol class="space-y-2 list-decimal list-inside">
<li>Identify need through collective discussion</li>
<li>Research 3-5 potential vendors/solutions</li>
<li>Eliminate any failing non-negotiables</li>
<li>Score remaining options using rubric</li>
<li>Trial top 2 options if possible</li>
<li>Make collective decision with documented rationale</li>
<li>Create migration/exit plan before commitment</li>
</ol>
</section>
<section class="mb-8">
<h3 class="text-xl font-bold text-neutral-800 mb-3">
Review & Accountability
</h3>
<ul class="space-y-2">
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span>Review this charter annually at minimum</span>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span>Audit existing tools against charter quarterly</span>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Document any exceptions with clear justification</span
>
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2">→</span>
<span
>Share learnings with other cooperatives in our
network</span
>
</li>
</ul>
</section>
</div>
</div>
</div>
</div>
</div>
<!-- Export Options - Bottom -->
<ExportOptions
:export-data="exportData"
filename="tech-charter"
title="Technology Charter" />
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from "vue";
definePageMeta({
layout: false,
});
// State
const charterPurpose = ref(
"This charter guides our cooperative's technology decisions, so that we can choose tools that don't contradict our values."
);
const principleWeights = ref({});
const nonNegotiables = ref([]);
const charterGenerated = ref(false);
const constraints = ref({
sso: "optional",
hosting: "either",
integrations: [],
support: "business",
timeline: "quarter",
});
// Data - Unified principles with rubric descriptions
const principles = [
{
id: "privacy",
name: "Privacy and data control",
description: "Data minimization, encryption, sovereignty, and user consent",
rubricDescription:
"Data collection practices, encryption standards, jurisdiction control",
defaultWeight: 4,
},
{
id: "accessibility",
name: "Universal access",
description: "WCAG compliance, screen readers, keyboard navigation",
rubricDescription: "WCAG 2.2 AA, keyboard nav, screen reader support",
defaultWeight: 5,
},
{
id: "portability",
name: "Data freedom",
description: "Easy export, no vendor lock-in, migration-friendly",
rubricDescription:
"Export capabilities, proprietary formats, switching costs",
defaultWeight: 4,
},
{
id: "opensource",
name: "Open source and community",
description:
"FOSS preference, transparent development, community governance",
rubricDescription: "License type, community involvement, code transparency",
defaultWeight: 3,
},
{
id: "sustainability",
name: "Sustainable operations",
description: "Predictable costs, green hosting, efficient resource use",
rubricDescription:
"Total cost of ownership, carbon footprint, resource efficiency",
defaultWeight: 3,
},
{
id: "localization",
name: "Local support",
description: "Multi-language, timezone aware, cultural sensitivity",
rubricDescription: "Language options, cultural awareness, regional support",
defaultWeight: 2,
},
{
id: "usability",
name: "User experience",
description:
"Intuitive interface, minimal learning curve, daily efficiency",
rubricDescription:
"Onboarding time, user satisfaction, workflow integration",
defaultWeight: 3,
},
];
const authOptions = [
{ value: "required", label: "SSO required" },
{ value: "preferred", label: "SSO preferred" },
{ value: "optional", label: "SSO optional" },
];
const hostingOptions = [
{ value: "self", label: "Self-hosted only" },
{ value: "either", label: "Either" },
{ value: "managed", label: "Managed only" },
];
const integrationOptions = ["Slack", "OIDC/OAuth", "Webhooks", "REST API"];
const supportOptions = [
{ value: "community", label: "Community only OK" },
{ value: "business", label: "Business hours" },
{ value: "24-7", label: "24/7 required" },
];
const timelineOptions = [
{ value: "immediate", label: "This month" },
{ value: "quarter", label: "This quarter" },
{ value: "year", label: "This year" },
{ value: "exploring", label: "Just exploring" },
];
// Computed
const sortedWeights = computed(() => {
return principles
.filter((p) => principleWeights.value[p.id] > 0)
.sort(
(a, b) => principleWeights.value[b.id] - principleWeights.value[a.id]
);
});
const canGenerateCharter = computed(() => {
// At least one principle must have weight > 0
return Object.values(principleWeights.value).some((w) => w > 0);
});
const selectedPrincipleCount = computed(() => {
return Object.values(principleWeights.value).filter((w) => w > 0).length;
});
// Export data for the ExportOptions component
const exportData = computed(() => ({
charterPurpose: charterPurpose.value,
principleWeights: principleWeights.value,
nonNegotiables: nonNegotiables.value,
constraints: constraints.value,
principles: principles.filter((p) => principleWeights.value[p.id] > 0),
sortedWeights: sortedWeights.value,
summary: {
selectedPrincipleCount: selectedPrincipleCount.value,
nonNegotiableCount: nonNegotiables.value.length,
canGenerateCharter: canGenerateCharter.value,
},
exportedAt: new Date().toISOString(),
section: "tech-charter",
}));
// Methods
const setPrincipleWeight = (principleId, weight) => {
principleWeights.value[principleId] = weight;
// If setting to 0, remove from non-negotiables
if (weight === 0) {
const idx = nonNegotiables.value.indexOf(principleId);
if (idx !== -1) {
nonNegotiables.value.splice(idx, 1);
}
}
};
const getWeightLabel = (weight) => {
const labels = {
0: "Excluded",
1: "Low",
2: "Medium-Low",
3: "Medium",
4: "High",
5: "Critical",
};
return labels[weight] || "";
};
const toggleNonNegotiable = (principleId) => {
const idx = nonNegotiables.value.indexOf(principleId);
if (idx === -1) {
nonNegotiables.value.push(principleId);
} else {
nonNegotiables.value.splice(idx, 1);
}
};
const toggleIntegration = (integration) => {
const idx = constraints.value.integrations.indexOf(integration);
if (idx === -1) {
constraints.value.integrations.push(integration);
} else {
constraints.value.integrations.splice(idx, 1);
}
};
const toggleIntegrationCheckbox = (integration, checked) => {
if (checked) {
if (!constraints.value.integrations.includes(integration)) {
constraints.value.integrations.push(integration);
}
} else {
const idx = constraints.value.integrations.indexOf(integration);
if (idx !== -1) {
constraints.value.integrations.splice(idx, 1);
}
}
};
const toggleNonNegotiableCheckbox = (principleId, checked) => {
if (checked) {
if (!nonNegotiables.value.includes(principleId)) {
nonNegotiables.value.push(principleId);
}
} else {
const idx = nonNegotiables.value.indexOf(principleId);
if (idx !== -1) {
nonNegotiables.value.splice(idx, 1);
}
}
};
const resetForm = () => {
if (confirm("Are you sure you want to clear all form data and start over?")) {
charterPurpose.value =
"This charter guides our cooperative's technology decisions, so that we can choose tools that don't contradict our values.";
// Reset all principle weights to 0
principles.forEach((p) => {
principleWeights.value[p.id] = 0;
});
nonNegotiables.value = [];
constraints.value = {
sso: "optional",
hosting: "either",
integrations: [],
support: "business",
timeline: "quarter",
};
localStorage.removeItem("tech-charter-data");
}
};
const scrollToTop = () => {
document
.querySelector(".template-wrapper")
.scrollIntoView({ behavior: "smooth" });
};
// Load saved data
const loadSavedData = () => {
const saved = localStorage.getItem("tech-charter-data");
if (saved) {
try {
const parsedData = JSON.parse(saved);
if (parsedData.charterPurpose !== undefined) {
charterPurpose.value = parsedData.charterPurpose;
}
principleWeights.value = parsedData.principleWeights || {};
nonNegotiables.value = parsedData.nonNegotiables || [];
constraints.value = { ...constraints.value, ...parsedData.constraints };
} catch (error) {
console.error("Error loading saved data:", error);
}
}
};
// Auto-save data
const autoSave = () => {
const data = {
charterPurpose: charterPurpose.value,
principleWeights: principleWeights.value,
nonNegotiables: nonNegotiables.value,
constraints: constraints.value,
lastUpdated: new Date().toISOString(),
};
localStorage.setItem("tech-charter-data", JSON.stringify(data));
};
// Load data on mount
onMounted(() => {
// Initialize all principle weights to 0
principles.forEach((p) => {
principleWeights.value[p.id] = 0;
});
loadSavedData();
});
// Auto-save when data changes
watch(
[charterPurpose, principleWeights, nonNegotiables, constraints],
autoSave,
{
deep: true,
}
);
</script>
<style scoped>
@reference "tailwindcss";
/* Template-specific styles not in main.css */
.section-card {
@apply mb-8 relative;
}
.content-title {
font-size: 2.5rem;
font-weight: 700;
color: inherit;
text-align: center;
margin-bottom: 0.5rem;
}
</style>