refactor: replace Wizard with CoopBuilder in navigation, enhance budget store structure, and streamline template components for improved user experience
This commit is contained in:
parent
eede87a273
commit
f67b138d95
33 changed files with 4970 additions and 2451 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue