refactor: enhance ProjectBudgetEstimate component layout, improve budget estimation calculations, and update CSS for better visual consistency and dark mode support
This commit is contained in:
parent
f073f91569
commit
b6e8d3b7ec
6 changed files with 502 additions and 358 deletions
|
|
@ -24,12 +24,13 @@
|
|||
<!-- Purpose Section -->
|
||||
<div class="section-card">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-neutral-800 mb-4">
|
||||
<h2
|
||||
class="text-2xl font-bold text-neutral-800 dark:text-white font-display mb-4">
|
||||
Charter Purpose
|
||||
</h2>
|
||||
<p class="text-neutral-600 mb-4">
|
||||
<p class="text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
Describe what this charter will guide and why it matters to
|
||||
your group.
|
||||
you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -44,10 +45,11 @@
|
|||
<!-- Unified Principles & Importance Section -->
|
||||
<div class="section-card">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-neutral-800 mb-4">
|
||||
<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 mb-6">
|
||||
<p class="text-neutral-600 dark:text-neutral-400 mb-6">
|
||||
Select principles and set their importance. Zero means
|
||||
excluded, 5 means critical.
|
||||
</p>
|
||||
|
|
@ -67,7 +69,7 @@
|
|||
:class="[
|
||||
'relative transition-all',
|
||||
principleWeights[principle.id] > 0
|
||||
? 'item-selected border-2 border-black dark:border-white bg-white dark:bg-neutral-950'
|
||||
? '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">
|
||||
|
|
@ -90,7 +92,7 @@
|
|||
? 'text-neutral-700'
|
||||
: 'text-neutral-600'
|
||||
"
|
||||
class="text-sm">
|
||||
class="text-sm dark:text-neutral-200">
|
||||
{{ principle.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -139,7 +141,7 @@
|
|||
<!-- 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 dark:border-neutral-800">
|
||||
<label
|
||||
:class="[
|
||||
'flex items-center gap-3 cursor-pointer item-label-bg px-2 py-1',
|
||||
|
|
@ -152,7 +154,7 @@
|
|||
:checked="nonNegotiables.includes(principle.id)"
|
||||
@change="toggleNonNegotiable(principle.id)"
|
||||
class="w-4 h-4" />
|
||||
<span class="text-sm font-medium text-red-600">
|
||||
<span class="text-sm font-medium text-white">
|
||||
Make this non-negotiable
|
||||
</span>
|
||||
</label>
|
||||
|
|
@ -163,7 +165,7 @@
|
|||
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-500 mb-1">
|
||||
class="text-xs font-bold uppercase text-neutral-300 mb-1">
|
||||
Evaluation Criteria:
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
|
|
@ -180,15 +182,17 @@
|
|||
<div class="section-card">
|
||||
<div>
|
||||
<h2
|
||||
class="text-2xl font-bold text-neutral-800 mb-2"
|
||||
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 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">Authentication</legend>
|
||||
<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"
|
||||
|
|
@ -209,7 +213,7 @@
|
|||
: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'
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black dark:!text-black'
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]">
|
||||
{{ option.label }}
|
||||
|
|
@ -218,8 +222,10 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">Hosting Model</legend>
|
||||
<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"
|
||||
|
|
@ -240,7 +246,7 @@
|
|||
: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-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
? '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 }}
|
||||
|
|
@ -249,11 +255,12 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">
|
||||
<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 mb-4">
|
||||
<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">
|
||||
|
|
@ -273,7 +280,7 @@
|
|||
: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'
|
||||
? '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 }}
|
||||
|
|
@ -282,7 +289,7 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
|
||||
<legend class="font-semibold text-lg">
|
||||
Support Expectations
|
||||
</legend>
|
||||
|
|
@ -306,7 +313,7 @@
|
|||
: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-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
? '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 }}
|
||||
|
|
@ -315,8 +322,8 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">
|
||||
<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
|
||||
|
|
@ -339,7 +346,7 @@
|
|||
: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-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
? '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 }}
|
||||
|
|
@ -406,10 +413,8 @@
|
|||
<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, so
|
||||
that we can choose tools that don't contradict our values.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -672,7 +677,7 @@ definePageMeta({
|
|||
|
||||
// State
|
||||
const charterPurpose = ref(
|
||||
"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, so that we can choose tools that don't contradict our values."
|
||||
);
|
||||
const principleWeights = ref({});
|
||||
const nonNegotiables = ref([]);
|
||||
|
|
@ -690,7 +695,7 @@ const constraints = ref({
|
|||
const principles = [
|
||||
{
|
||||
id: "privacy",
|
||||
name: "Privacy & Data Control",
|
||||
name: "Privacy and data control",
|
||||
description: "Data minimization, encryption, sovereignty, and user consent",
|
||||
rubricDescription:
|
||||
"Data collection practices, encryption standards, jurisdiction control",
|
||||
|
|
@ -698,14 +703,14 @@ const principles = [
|
|||
},
|
||||
{
|
||||
id: "accessibility",
|
||||
name: "Universal Access",
|
||||
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",
|
||||
name: "Data freedom",
|
||||
description: "Easy export, no vendor lock-in, migration-friendly",
|
||||
rubricDescription:
|
||||
"Export capabilities, proprietary formats, switching costs",
|
||||
|
|
@ -713,7 +718,7 @@ const principles = [
|
|||
},
|
||||
{
|
||||
id: "opensource",
|
||||
name: "Open Source & Community",
|
||||
name: "Open source and community",
|
||||
description:
|
||||
"FOSS preference, transparent development, community governance",
|
||||
rubricDescription: "License type, community involvement, code transparency",
|
||||
|
|
@ -721,7 +726,7 @@ const principles = [
|
|||
},
|
||||
{
|
||||
id: "sustainability",
|
||||
name: "Sustainable Operations",
|
||||
name: "Sustainable operations",
|
||||
description: "Predictable costs, green hosting, efficient resource use",
|
||||
rubricDescription:
|
||||
"Total cost of ownership, carbon footprint, resource efficiency",
|
||||
|
|
@ -729,14 +734,14 @@ const principles = [
|
|||
},
|
||||
{
|
||||
id: "localization",
|
||||
name: "Local Support",
|
||||
name: "Local support",
|
||||
description: "Multi-language, timezone aware, cultural sensitivity",
|
||||
rubricDescription: "Language options, cultural awareness, regional support",
|
||||
defaultWeight: 2,
|
||||
},
|
||||
{
|
||||
id: "usability",
|
||||
name: "User Experience",
|
||||
name: "User experience",
|
||||
description:
|
||||
"Intuitive interface, minimal learning curve, daily efficiency",
|
||||
rubricDescription:
|
||||
|
|
@ -746,30 +751,30 @@ const principles = [
|
|||
];
|
||||
|
||||
const authOptions = [
|
||||
{ value: "required", label: "SSO Required" },
|
||||
{ value: "preferred", label: "SSO Preferred" },
|
||||
{ value: "optional", label: "SSO Optional" },
|
||||
{ value: "required", label: "SSO required" },
|
||||
{ value: "preferred", label: "SSO preferred" },
|
||||
{ value: "optional", label: "SSO optional" },
|
||||
];
|
||||
|
||||
const hostingOptions = [
|
||||
{ value: "self", label: "Self-Hosted Only" },
|
||||
{ value: "self", label: "Self-hosted only" },
|
||||
{ value: "either", label: "Either" },
|
||||
{ value: "managed", label: "Managed Only" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ value: "immediate", label: "This month" },
|
||||
{ value: "quarter", label: "This quarter" },
|
||||
{ value: "year", label: "This year" },
|
||||
{ value: "exploring", label: "Just exploring" },
|
||||
];
|
||||
|
||||
// Computed
|
||||
|
|
@ -853,7 +858,7 @@ const toggleIntegration = (integration) => {
|
|||
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 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, 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue