refactor: replace Wizard with CoopBuilder in navigation, enhance budget store structure, and streamline template components for improved user experience

This commit is contained in:
Jennie Robinson Faber 2025-08-17 17:25:04 +01:00
parent eede87a273
commit f67b138d95
33 changed files with 4970 additions and 2451 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -222,29 +222,7 @@ useHead({
</script>
<style scoped>
/* 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");
/* Removed full-screen dither pattern to avoid gray haze in dark mode */
/* Exact shadow style from value-flow inspiration */
.dither-shadow {
background: black;
background-image: radial-gradient(white 1px, transparent 1px);
background-size: 2px 2px;
}
@media (prefers-color-scheme: dark) {
.dither-shadow {
background: white;
background-image: radial-gradient(black 1px, transparent 1px);
}
}
:global(.dark) .dither-shadow {
background: white;
background-image: radial-gradient(black 1px, transparent 1px);
}
/* Template index specific styles - no longer duplicated in main.css */
.dither-shadow-disabled {
background: black;
@ -265,74 +243,6 @@ useHead({
background-image: radial-gradient(black 1px, transparent 1px);
}
/* Rely on Tailwind bg utilities on container */
.template-card {
@apply relative;
font-family: "Ubuntu", monospace;
}
.help-section {
@apply relative;
}
.coming-soon {
opacity: 0.7;
}
.dither-tag {
position: relative;
background: white;
}
.dither-tag::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: repeating-linear-gradient(
45deg,
transparent 0px,
transparent 1px,
black 1px,
black 2px
);
opacity: 0.1;
pointer-events: none;
}
/* Button styling - pure bitmap, no colors */
.bitmap-button {
font-family: "Ubuntu Mono", monospace !important;
text-transform: uppercase;
font-weight: bold;
letter-spacing: 0.5px;
position: relative;
}
.bitmap-button:hover {
transform: translateY(-1px) translateX(-1px);
transition: transform 0.1s ease;
}
.bitmap-button:hover::after {
content: "";
position: absolute;
top: 1px;
left: 1px;
right: -1px;
bottom: -1px;
border: 1px solid black;
background: white;
z-index: -1;
}
.disabled-button {
opacity: 0.6;
cursor: not-allowed;
}
/* Remove any inherited rounded corners */
.template-card > *,
.help-section > *,

View file

@ -1,18 +1,13 @@
<template>
<div>
<!-- Wizard Subnav -->
<WizardSubnav />
<!-- Export Options - Top -->
<ExportOptions
:export-data="exportData"
filename="membership-agreement"
title="Membership Agreement"
/>
title="Membership Agreement" />
<div
class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100"
>
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">
@ -20,8 +15,9 @@
<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"
:data-coop-name="formData.cooperativeName || 'Worker Cooperative'"
>
:data-coop-name="
formData.cooperativeName || 'Worker Cooperative'
">
MEMBERSHIP AGREEMENT
</h1>
</div>
@ -29,8 +25,7 @@
<!-- Section 1: Who We Are -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
1. Who We Are
</h2>
@ -42,8 +37,7 @@
size="xl"
class="w-full"
@input="debouncedAutoSave"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
<UFormField label="Date Established" class="form-group-large">
@ -52,8 +46,7 @@
type="date"
size="xl"
class="large-field"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
<UFormField label="Our Purpose" class="form-group-large">
@ -64,8 +57,7 @@
size="xl"
class="large-field"
@input="debouncedAutoSave"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
<UFormField label="Our Core Values" class="form-group-large">
@ -76,8 +68,7 @@
size="xl"
class="large-field"
@input="debouncedAutoSave"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
<div class="form-group-large">
@ -91,8 +82,7 @@
size="sm"
color="primary"
variant="outline"
icon="i-heroicons-plus"
>
icon="i-heroicons-plus">
Add Member
</UButton>
</div>
@ -101,10 +91,10 @@
<div
v-for="(member, index) in formData.members"
:key="index"
class="border border-neutral-600 rounded-lg p-4"
>
class="border border-neutral-600 rounded-lg p-4">
<div class="flex items-start justify-between mb-3">
<h4 class="font-medium text-neutral-900 dark:text-neutral-100">
<h4
class="font-medium text-neutral-900 dark:text-neutral-100">
Member {{ index + 1 }}
</h4>
<UButton
@ -113,8 +103,7 @@
size="sm"
color="red"
variant="ghost"
icon="i-heroicons-trash"
>
icon="i-heroicons-trash">
</UButton>
</div>
@ -126,8 +115,7 @@
size="xl"
class="large-field"
@input="debouncedAutoSave"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
<UFormField label="Email" class="form-group-large">
@ -138,8 +126,7 @@
size="xl"
class="large-field"
@input="debouncedAutoSave"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
<UFormField label="Join Date" class="form-group-large">
@ -148,22 +135,19 @@
type="date"
size="xl"
class="large-field"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
<UFormField
label="Current Role (Optional)"
class="form-group-large"
>
class="form-group-large">
<UInput
v-model="member.role"
placeholder="e.g., Coordinator, Developer, etc."
size="xl"
class="large-field"
@input="debouncedAutoSave"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
</div>
</div>
@ -175,16 +159,14 @@
<!-- Section 2: Membership -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
2. Membership
</h2>
<div class="space-y-4">
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Who Can Be a Member
</h3>
<p class="content-paragraph mb-3 leading-relaxed text-left">
@ -193,8 +175,8 @@
<ul class="content-list my-2 pl-6 list-disc">
<li>Shares our values and purpose</li>
<li>
Contributes labour to the cooperative (by doing actual work, not just
investing money)
Contributes labour to the cooperative (by doing actual work,
not just investing money)
</li>
<li>Commits to collective decision-making</li>
<li>Participates in governance responsibilities</li>
@ -203,14 +185,14 @@
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Becoming a Member
</h3>
<p class="content-paragraph">
New members join through a consent process, which means existing members
must agree that adding this person won't harm the cooperative.
New members join through a consent process, which means
existing members must agree that adding this person won't harm
the cooperative.
</p>
<ol class="content-list numbered my-2 pl-6 list-decimal">
@ -221,8 +203,7 @@
type="number"
placeholder="3"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
months working together
</li>
<li>Values alignment conversation</li>
@ -233,8 +214,7 @@
type="number"
placeholder="1000"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
(can be paid over time or waived based on need)
</li>
</ol>
@ -242,20 +222,19 @@
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Leaving the Cooperative
</h3>
<p class="content-paragraph flex items-baseline gap-2 flex-wrap">
<p
class="content-paragraph flex items-baseline gap-2 flex-wrap">
Members can leave anytime with
<UInput
v-model="formData.noticeDays"
type="number"
placeholder="30"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
days notice. The cooperative will:
</p>
@ -267,8 +246,7 @@
type="number"
placeholder="30"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
days
</li>
<li class="flex items-baseline gap-2 flex-wrap">
@ -278,11 +256,12 @@
type="number"
placeholder="90"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
days
</li>
<li>Maintain respectful ongoing relationships when possible</li>
<li>
Maintain respectful ongoing relationships when possible
</li>
</ul>
</div>
</div>
@ -291,80 +270,71 @@
<!-- Section 3: How We Make Decisions -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
3. How We Make Decisions
</h2>
<div class="space-y-4">
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Consent-Based Decisions
</h3>
<p class="content-paragraph mb-3 leading-relaxed text-left">
We use consent, not consensus. This means we move forward when no one
has a principled objection that would harm the cooperative. An objection
must explain how the proposal would contradict our values or threaten
our sustainability.
We use consent, not consensus. This means we move forward when
no one has a principled objection that would harm the
cooperative. An objection must explain how the proposal would
contradict our values or threaten our sustainability.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Day-to-Day Decisions
</h3>
<p
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
>
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
Decisions under $<UInput
v-model="formData.dayToDayLimit"
type="number"
placeholder="100"
class="inline-field number-field"
@change="autoSave"
/>
can be made by any member. Just tell others what you did at the next
meeting.
@change="autoSave" />
can be made by any member. Just tell others what you did at
the next meeting.
</p>
</div>
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Regular Decisions
</h3>
<p
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
>
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
Decisions between $<UInput
v-model="formData.regularDecisionMin"
type="number"
placeholder="100"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
and $<UInput
v-model="formData.regularDecisionMax"
type="number"
placeholder="1000"
class="inline-field number-field"
@change="autoSave"
/>
need consent from members present at a meeting (minimum 2 members).
@change="autoSave" />
need consent from members present at a meeting (minimum 2
members).
</p>
</div>
</div>
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Major Decisions
</h3>
<p class="content-paragraph mb-3 leading-relaxed text-left">
@ -379,8 +349,7 @@
type="number"
placeholder="5000"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
</li>
<li>Fundamental changes to our purpose or structure</li>
<li>Dissolution of the cooperative</li>
@ -389,8 +358,7 @@
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Meeting Structure
</h3>
<ul class="content-list my-2 pl-6 list-disc">
@ -400,8 +368,7 @@
v-model="formData.meetingFrequency"
placeholder="weekly"
class="inline-field"
@change="autoSave"
/>
@change="autoSave" />
</li>
<li class="flex items-baseline gap-2 flex-wrap">
Emergency meetings need
@ -410,12 +377,13 @@
type="number"
placeholder="24"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
hours notice
</li>
<li>We rotate who facilitates meetings</li>
<li>Decisions and reasoning get documented in shared notes</li>
<li>
Decisions and reasoning get documented in shared notes
</li>
</ul>
</div>
</div>
@ -424,28 +392,25 @@
<!-- Section 4: Money and Labour -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
4. Money and Labour
</h2>
<div class="space-y-4">
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Equal Ownership
</h3>
<p class="content-paragraph mb-3 leading-relaxed text-left">
Each member owns an equal share of the cooperative, regardless of hours
worked or tenure.
Each member owns an equal share of the cooperative, regardless
of hours worked or tenure.
</p>
</div>
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Paying Ourselves
</h3>
<ul class="content-list my-2 pl-6 list-disc">
@ -455,8 +420,7 @@
type="number"
placeholder="25"
class="inline-field number-field"
@change="autoSave"
/>/hour for all members
@change="autoSave" />/hour for all members
</li>
<li class="flex items-baseline gap-2 flex-wrap">
Or: Equal monthly draw of $<UInput
@ -464,8 +428,7 @@
type="number"
placeholder="2000"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
per member
</li>
<li class="flex items-baseline gap-2 flex-wrap">
@ -475,8 +438,7 @@
:items="dayOptions"
placeholder="15"
class="inline-field"
@change="autoSave"
/>
@change="autoSave" />
of each month
</li>
<li class="flex items-baseline gap-2 flex-wrap">
@ -485,8 +447,7 @@
v-model="formData.surplusFrequency"
placeholder="quarter"
class="inline-field"
@change="autoSave"
/>
@change="autoSave" />
</li>
</ul>
</div>
@ -494,8 +455,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Work Expectations
</h3>
<ul class="content-list my-2 pl-6 list-disc">
@ -506,29 +466,31 @@
type="number"
placeholder="40"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
(flexible based on capacity)
</li>
<li>We explicitly reject crunch culture</li>
<li>Members communicate their capacity openly</li>
<li>
We adjust workload collectively when someone needs reduced hours
We adjust workload collectively when someone needs reduced
hours
</li>
</ul>
</div>
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Financial Transparency
</h3>
<ul class="content-list my-2 pl-6 list-disc">
<li>All members can access all financial records anytime</li>
<li>
All members can access all financial records anytime
</li>
<li>Monthly financial check-ins at meetings</li>
<li>
Quarterly reviews of our runway (how many months we can operate)
Quarterly reviews of our runway (how many months we can
operate)
</li>
</ul>
</div>
@ -539,34 +501,31 @@
<!-- Section 5: Roles and Responsibilities -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
5. Roles and Responsibilities
</h2>
<div class="space-y-4">
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Rotating Roles
</h3>
<p
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
>
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
We rotate operational roles every
<UInput
v-model="formData.roleRotationMonths"
type="number"
placeholder="6"
class="inline-field number-field"
@change="autoSave"
/>
@change="autoSave" />
months. Current roles include:
</p>
<ul class="content-list">
<li>
Financial coordinator (handles bookkeeping, not financial decisions)
Financial coordinator (handles bookkeeping, not financial
decisions)
</li>
<li>Meeting facilitator</li>
<li>External communications</li>
@ -576,8 +535,7 @@
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Shared Responsibilities
</h3>
<p class="content-paragraph mb-3 leading-relaxed text-left">
@ -595,16 +553,14 @@
<!-- Section 6: Conflict and Care -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
6. Conflict and Care
</h2>
<div class="space-y-4">
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
When Conflict Happens
</h3>
<ol class="content-list numbered my-2 pl-6 list-decimal">
@ -617,15 +573,16 @@
<div>
<h3
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
>
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
Care Commitments
</h3>
<ul class="content-list my-2 pl-6 list-disc">
<li>We check in about capacity and wellbeing regularly</li>
<li>We honour diverse access needs</li>
<li>We maintain flexibility for life circumstances</li>
<li>We contribute to mutual aid when members face hardship</li>
<li>
We contribute to mutual aid when members face hardship
</li>
</ul>
</div>
</div>
@ -634,31 +591,27 @@
<!-- Section 7: Changing This Agreement -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
7. Changing This Agreement
</h2>
<p
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
>
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
This is a living document. We review it every
<UInput
v-model="formData.reviewFrequency"
placeholder="year"
class="inline-field"
@change="autoSave"
/>
and update it through our consent process. Small clarifications can happen
anytime; structural changes need full member consent.
@change="autoSave" />
and update it through our consent process. Small clarifications
can happen anytime; structural changes need full member consent.
</p>
</div>
<!-- Section 8: If We Need to Close -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
8. If We Need to Close
</h2>
@ -675,8 +628,7 @@
<UInput
v-model="formData.assetDonationTarget"
placeholder="Enter organization name"
class="inline-field wide-field"
/>
class="inline-field wide-field" />
</li>
</ol>
</div>
@ -685,8 +637,7 @@
<!-- Section 9: Legal Bits -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
9. Legal Bits
</h2>
@ -697,8 +648,7 @@
v-model="formData.legalStructure"
size="xl"
class="w-full"
placeholder="Cooperative corporation, LLC, partnership, etc."
/>
placeholder="Cooperative corporation, LLC, partnership, etc." />
</UFormField>
<UFormField label="Registered in" class="form-group-inline">
@ -707,8 +657,7 @@
placeholder="State/Province"
size="xl"
class="inline-field w-full"
@change="autoSave"
/>
@change="autoSave" />
</UFormField>
<div class="fiscal-year-group">
@ -720,25 +669,23 @@
placeholder="Month"
size="xl"
class="w-60"
@change="autoSave"
/>
@change="autoSave" />
<USelect
v-model="formData.fiscalYearEndDay"
:items="dayOptions"
placeholder="Day"
size="xl"
class="w-40"
@change="autoSave"
/>
@change="autoSave" />
</div>
</UFormField>
</div>
</div>
<p class="content-paragraph mb-3 leading-relaxed text-left">
This agreement works alongside but doesn't replace our legal incorporation
documents. Where they conflict, we follow the law but work to align our
legal structure with our values.
This agreement works alongside but doesn't replace our legal
incorporation documents. Where they conflict, we follow the law
but work to align our legal structure with our values.
</p>
</div>
</div>
@ -746,46 +693,39 @@
<!-- Section 10: Agreement Review -->
<div class="section-card">
<h2
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
>
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
10. Agreement Review
</h2>
<div class="space-y-4">
<p class="content-paragraph mb-3 leading-relaxed text-left">
By using this agreement, we commit to these principles and to showing up
for each other.
By using this agreement, we commit to these principles and to
showing up for each other.
</p>
<div
class="bg-neutral-50 dark:bg-neutral-900 p-4 rounded-md border-l-4 border-emerald-300"
>
class="bg-neutral-50 dark:bg-neutral-900 p-4 rounded-md border-l-4 border-emerald-300">
<p
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
>
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
This agreement was last updated on
<UInput
v-model="formData.lastUpdated"
type="date"
class="inline-field"
@change="autoSave"
/>. We commit to reviewing it on
@change="autoSave" />. We commit to reviewing it on
<UInput
v-model="formData.nextReview"
type="date"
class="inline-field"
@change="autoSave"
/>
@change="autoSave" />
or sooner if circumstances require.
</p>
</div>
<div
class="signature-space mt-8 p-8 border border-dashed border-neutral-300 rounded-md bg-neutral-50 dark:bg-neutral-950"
>
class="signature-space mt-8 p-8 border border-dashed border-neutral-300 rounded-md bg-neutral-50 dark:bg-neutral-950">
<p
class="content-paragraph mb-3 leading-relaxed text-center text-neutral-600 italic"
>
class="content-paragraph mb-3 leading-relaxed text-center text-neutral-600 italic">
[Space for member signatures when printed]
</p>
</div>
@ -799,8 +739,7 @@
<ExportOptions
:export-data="exportData"
filename="membership-agreement"
title="Membership Agreement"
/>
title="Membership Agreement" />
</div>
</template>
@ -898,7 +837,10 @@ onMounted(() => {
// Auto-save individual field changes immediately
const autoSave = () => {
localStorage.setItem("membership-agreement-data", JSON.stringify(formData.value));
localStorage.setItem(
"membership-agreement-data",
JSON.stringify(formData.value)
);
console.log("Manual auto-save triggered:", formData.value);
};
@ -950,8 +892,12 @@ const handlePrint = () => {
`;
// Add signature lines for each member
const membersWithNames = formData.value.members?.filter((m) => m.name) || [];
const numSignatures = Math.max(2, Math.min(8, membersWithNames.length || 4));
const membersWithNames =
formData.value.members?.filter((m) => m.name) || [];
const numSignatures = Math.max(
2,
Math.min(8, membersWithNames.length || 4)
);
for (let i = 0; i < numSignatures; i++) {
const memberName = membersWithNames[i]?.name || "";
@ -981,7 +927,8 @@ const handlePrint = () => {
value = formData.value.cooperativeName;
else if (input.closest('[label="Date Established"]'))
value = formData.value.dateEstablished;
else if (input.closest('[label="Our Purpose"]')) value = formData.value.purpose;
else if (input.closest('[label="Our Purpose"]'))
value = formData.value.purpose;
else if (input.closest('[label="Our Core Values"]'))
value = formData.value.coreValues;
else if (input.closest('[label="Legal Structure"]'))
@ -998,13 +945,16 @@ const handlePrint = () => {
// Handle member fields
else if (input.closest(".border-neutral-200")) {
const memberCard = input.closest(".border-neutral-200");
const memberIndex = Array.from(memberCard.parentNode.children).indexOf(memberCard);
const memberIndex = Array.from(memberCard.parentNode.children).indexOf(
memberCard
);
const member = formData.value.members?.[memberIndex];
if (member) {
if (input.closest('[label="Full Name"]')) value = member.name;
else if (input.closest('[label="Email"]')) value = member.email;
else if (input.closest('[label="Join Date"]')) value = member.joinDate;
else if (input.closest('[label="Current Role (Optional)"]')) value = member.role;
else if (input.closest('[label="Current Role (Optional)"]'))
value = member.role;
}
}
// Fallback to input.value
@ -1143,11 +1093,13 @@ const exportData = computed(() => ({
.template-content.font-ubuntu,
.template-content.font-ubuntu * {
font-family: "Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
font-family: "Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif !important;
}
.template-content.font-inter,
.template-content.font-inter * {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif !important;
}
</style>

View file

@ -1,26 +1,20 @@
<template>
<div>
<!-- Wizard Subnav -->
<WizardSubnav />
<!-- Export Options - Top -->
<ExportOptions
:export-data="exportData"
filename="tech-charter"
title="Technology Charter"
/>
title="Technology Charter" />
<div
class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100"
>
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"
>
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>
@ -30,9 +24,12 @@
<!-- Purpose Section -->
<div class="section-card">
<div>
<h2 class="text-2xl font-bold text-neutral-800 mb-4">Charter Purpose</h2>
<h2 class="text-2xl font-bold text-neutral-800 mb-4">
Charter Purpose
</h2>
<p class="text-neutral-600 mb-4">
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.
</p>
</div>
@ -40,8 +37,7 @@
<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"
/>
rows="4" />
</div>
</div>
@ -52,37 +48,39 @@
Define Your Principles & Importance
</h2>
<p class="text-neutral-600 mb-6">
Select principles and set their importance. Zero means excluded, 5 means
critical.
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">
<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>
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<div
:class="[
'relative transition-all',
principleWeights[principle.id] > 0
? 'principle-selected border-2 border-black dark:border-white bg-white dark:bg-neutral-950'
? 'item-selected border-2 border-black dark:border-white 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="[
'principle-text-bg mb-3',
principleWeights[principle.id] > 0 ? 'selected' : '',
]"
>
'item-text-bg mb-3',
principleWeights[principle.id] > 0
? 'selected'
: '',
]">
<h3 class="font-bold text-lg mb-2">
{{ principle.name }}
</h3>
@ -92,8 +90,7 @@
? 'text-neutral-700'
: 'text-neutral-600'
"
class="text-sm"
>
class="text-sm">
{{ principle.description }}
</p>
</div>
@ -102,8 +99,7 @@
<!-- Importance selector -->
<div class="flex flex-col items-center gap-2">
<label
class="text-xs font-bold text-neutral-500 uppercase tracking-wider"
>
class="text-xs font-bold text-neutral-500 uppercase tracking-wider">
Importance
</label>
@ -119,8 +115,7 @@
? '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 }}
</button>
</div>
@ -131,7 +126,11 @@
{{ principleWeights[principle.id] || 0 }}
</div>
<div class="text-xs text-neutral-500">
{{ getWeightLabel(principleWeights[principle.id] || 0) }}
{{
getWeightLabel(
principleWeights[principle.id] || 0
)
}}
</div>
</div>
</div>
@ -140,20 +139,19 @@
<!-- 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"
>
class="mt-4 pt-4 border-t border-neutral-200">
<label
:class="[
'flex items-center gap-3 cursor-pointer principle-label-bg px-2 py-1',
nonNegotiables.includes(principle.id) ? 'selected' : '',
]"
>
'flex items-center gap-3 cursor-pointer item-label-bg px-2 py-1',
nonNegotiables.includes(principle.id)
? 'selected'
: '',
]">
<input
type="checkbox"
:checked="nonNegotiables.includes(principle.id)"
@change="toggleNonNegotiable(principle.id)"
class="w-4 h-4"
/>
class="w-4 h-4" />
<span class="text-sm font-medium text-red-600">
Make this non-negotiable
</span>
@ -163,9 +161,9 @@
<!-- Show rubric description when selected -->
<div
v-if="principleWeights[principle.id] > 0"
class="mt-4 p-3 principle-label-bg selected border border-neutral-200"
>
<div class="text-xs font-bold uppercase text-neutral-500 mb-1">
class="mt-4 p-3 item-label-bg selected border border-neutral-200">
<div
class="text-xs font-bold uppercase text-neutral-500 mb-1">
Evaluation Criteria:
</div>
<div class="text-sm">
@ -183,8 +181,7 @@
<div>
<h2
class="text-2xl font-bold text-neutral-800 mb-2"
id="constraints-heading"
>
id="constraints-heading">
Technical Constraints
</h2>
</div>
@ -195,18 +192,15 @@
<div
class="flex flex-wrap gap-3 constraint-buttons"
role="radiogroup"
aria-labelledby="auth-heading"
>
aria-labelledby="auth-heading">
<div
v-for="option in authOptions"
:key="option.value"
class="relative"
>
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>
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"
@ -217,8 +211,7 @@
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>
@ -230,18 +223,15 @@
<div
class="flex flex-wrap gap-3 constraint-buttons"
role="radiogroup"
aria-labelledby="hosting-heading"
>
aria-labelledby="hosting-heading">
<div
v-for="option in hostingOptions"
:key="option.value"
class="relative"
>
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>
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"
@ -252,8 +242,7 @@
constraints.hosting === 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>
@ -261,29 +250,32 @@
</fieldset>
<fieldset class="bg-neutral-50 p-6 rounded-lg">
<legend class="font-semibold text-lg">Required Integrations</legend>
<p class="text-sm text-neutral-600 mb-4">Select all that apply</p>
<legend class="font-semibold text-lg">
Required Integrations
</legend>
<p class="text-sm text-neutral-600 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"
>
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>
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<button
@click="toggleIntegration(integration)"
:aria-pressed="constraints.integrations.includes(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-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',
]"
>
]">
{{ integration }}
</button>
</div>
@ -291,22 +283,21 @@
</fieldset>
<fieldset class="bg-neutral-50 p-6 rounded-lg">
<legend class="font-semibold text-lg">Support Expectations</legend>
<legend class="font-semibold text-lg">
Support Expectations
</legend>
<div
class="flex flex-wrap gap-3 constraint-buttons"
role="radiogroup"
aria-labelledby="support-heading"
>
aria-labelledby="support-heading">
<div
v-for="option in supportOptions"
:key="option.value"
class="relative"
>
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>
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"
@ -317,8 +308,7 @@
constraints.support === 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>
@ -326,22 +316,21 @@
</fieldset>
<fieldset class="bg-neutral-50 p-6 rounded-lg">
<legend class="font-semibold text-lg">Migration Timeline</legend>
<legend class="font-semibold text-lg">
Migration Timeline
</legend>
<div
class="flex flex-wrap gap-3 constraint-buttons"
role="radiogroup"
aria-labelledby="timeline-heading"
>
aria-labelledby="timeline-heading">
<div
v-for="option in timelineOptions"
:key="option.value"
class="relative"
>
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>
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"
@ -352,8 +341,7 @@
constraints.timeline === 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>
@ -367,8 +355,7 @@
<button
@click="resetForm"
class="export-btn"
title="Clear all form data and start over"
>
title="Clear all form data and start over">
<UIcon name="i-heroicons-arrow-path" />
Reset Form
</button>
@ -381,17 +368,19 @@
v-if="charterGenerated"
class="relative animate-fadeIn"
role="main"
aria-label="Generated Technology Charter"
>
aria-label="Generated Technology Charter">
<!-- Dithered shadow -->
<div class="absolute top-4 left-4 right-0 bottom-0 dither-shadow"></div>
<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">
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">
@ -407,8 +396,7 @@
<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"
>
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>
@ -418,10 +406,10 @@
<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 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.
</p>
</section>
@ -429,21 +417,25 @@
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
"
>
<h3 class="text-xl font-bold text-neutral-800 mb-3">Core Principles</h3>
">
<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)
(p) =>
principleWeights[p] > 0 && !nonNegotiables.includes(p)
)"
:key="principleId"
class="flex items-start"
>
class="flex items-start">
<span class="text-neutral-600 mr-2"></span>
<span>{{ principles.find((p) => p.id === principleId)?.name }}</span>
<span>{{
principles.find((p) => p.id === principleId)?.name
}}</span>
</li>
</ul>
</section>
@ -453,16 +445,18 @@
Non-Negotiable Requirements
</h3>
<p class="text-red-600 font-semibold mb-3">
Any vendor failing these requirements is automatically disqualified.
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"
>
class="flex items-start text-red-600 font-semibold">
<span class="mr-2"></span>
<span>{{ principles.find((p) => p.id === principleId)?.name }}</span>
<span>{{
principles.find((p) => p.id === principleId)?.name
}}</span>
</li>
</ul>
</section>
@ -477,7 +471,8 @@
<span
>Authentication:
{{
authOptions.find((o) => o.value === constraints.sso)?.label
authOptions.find((o) => o.value === constraints.sso)
?.label
}}</span
>
</li>
@ -486,11 +481,15 @@
<span
>Hosting:
{{
hostingOptions.find((o) => o.value === constraints.hosting)?.label
hostingOptions.find(
(o) => o.value === constraints.hosting
)?.label
}}</span
>
</li>
<li v-if="constraints.integrations.length > 0" class="flex items-start">
<li
v-if="constraints.integrations.length > 0"
class="flex items-start">
<span class="text-purple-600 mr-2"></span>
<span
>Required Integrations:
@ -502,7 +501,9 @@
<span
>Support Level:
{{
supportOptions.find((o) => o.value === constraints.support)?.label
supportOptions.find(
(o) => o.value === constraints.support
)?.label
}}</span
>
</li>
@ -511,8 +512,9 @@
<span
>Migration Timeline:
{{
timelineOptions.find((o) => o.value === constraints.timeline)
?.label
timelineOptions.find(
(o) => o.value === constraints.timeline
)?.label
}}</span
>
</li>
@ -520,27 +522,27 @@
</section>
<section class="mb-8">
<h3 class="text-xl font-bold text-neutral-800 mb-3">Evaluation Rubric</h3>
<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):
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"
>
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"
>
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"
>
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center">
Weight
</th>
</tr>
@ -549,21 +551,17 @@
<tr
v-for="weight in sortedWeights"
:key="weight.id"
class="hover:bg-neutral-50"
>
class="hover:bg-neutral-50">
<td
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 font-semibold"
>
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"
>
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"
>
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center font-bold text-neutral-600">
{{ principleWeights[weight.id] }}
</td>
</tr>
@ -580,8 +578,8 @@
<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
>Any vendor failing a non-negotiable requirement is
automatically eliminated</span
>
</li>
<li class="flex items-start">
@ -594,8 +592,8 @@
<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
>When scores are within 10%, choose based on alignment
with cooperative values</span
>
</li>
<li class="flex items-start">
@ -638,11 +636,16 @@
</li>
<li class="flex items-start">
<span class="text-neutral-600 mr-2"></span>
<span>Document any exceptions with clear justification</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>
<span
>Share learnings with other cooperatives in our
network</span
>
</li>
</ul>
</section>
@ -656,8 +659,7 @@
<ExportOptions
:export-data="exportData"
filename="tech-charter"
title="Technology Charter"
/>
title="Technology Charter" />
</div>
</template>
@ -705,13 +707,15 @@ 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,
},
@ -719,7 +723,8 @@ 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,
},
{
@ -732,8 +737,10 @@ 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,
},
];
@ -769,7 +776,9 @@ 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(() => {
@ -862,7 +871,9 @@ const resetForm = () => {
};
const scrollToTop = () => {
document.querySelector(".template-wrapper").scrollIntoView({ behavior: "smooth" });
document
.querySelector(".template-wrapper")
.scrollIntoView({ behavior: "smooth" });
};
// Load saved data
@ -905,9 +916,13 @@ onMounted(() => {
});
// Auto-save when data changes
watch([charterPurpose, principleWeights, nonNegotiables, constraints], autoSave, {
deep: true,
});
watch(
[charterPurpose, principleWeights, nonNegotiables, constraints],
autoSave,
{
deep: true,
}
);
</script>
<style scoped>
@ -919,115 +934,6 @@ watch([charterPurpose, principleWeights, nonNegotiables, constraints], autoSave,
@apply mb-8 relative;
}
/* Principle card selected styling - using dithered shadow and background */
.principle-selected {
position: relative;
background: white;
}
.principle-selected::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: repeating-linear-gradient(
45deg,
transparent 0px,
transparent 1px,
black 1px,
black 2px
);
opacity: 0.1;
pointer-events: none;
z-index: 0;
}
.principle-selected > * {
position: relative;
z-index: 1;
}
/* Dark mode */
html.dark .principle-selected {
background: #0a0a0a;
}
html.dark .principle-selected::after {
background-image: repeating-linear-gradient(
45deg,
transparent 0px,
transparent 1px,
white 1px,
white 2px
);
}
/* Text background for better readability */
.principle-label-bg {
background: rgba(255, 255, 255, 0.85);
border-radius: 4px;
}
.principle-label-bg.selected {
background: rgba(255, 255, 255, 0.95);
}
/* Dark mode text backgrounds */
html.dark .principle-text-bg {
background: rgba(10, 10, 10, 0.9);
}
html.dark .principle-text-bg.selected {
background: rgba(10, 10, 10, 0.95);
}
html.dark .principle-label-bg {
background: rgba(10, 10, 10, 0.85);
}
html.dark .principle-label-bg.selected {
background: rgba(10, 10, 10, 0.95);
}
/* Constraint button selected styling - black background */
button.constraint-selected {
background: black !important;
color: white !important;
}
button.constraint-selected:hover {
background: black !important;
color: white !important;
}
/* Dark mode */
html.dark button.constraint-selected {
background: white !important;
color: black !important;
}
html.dark button.constraint-selected:hover {
background: white !important;
color: black !important;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.5s ease-out;
}
.content-title {
font-size: 2.5rem;
font-weight: 700;