app/pages/templates/conflict-resolution-framework.vue

2981 lines
92 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
<!-- Export Controls -->
<div class="export-controls no-print no-pdf">
<div class="export-content">
<div class="export-section">
<h3 class="export-title">Export Policy Document:</h3>
<div class="export-buttons">
<button
@click="validateForm"
class="export-btn validation"
title="Check form completion">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path d="M9 11l3 3 8-8" />
<path
d="M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9c1.4 0 2.7.3 3.9.8" />
</svg>
CHECK
</button>
<button
@click="exportMarkdown"
class="export-btn primary"
title="Download Policy as Markdown">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14,2 14,8 20,8" />
<line x1="12" y1="18" x2="12" y2="12" />
<path d="m9 15 3-3 3 3" />
</svg>
MARKDOWN
</button>
<button
@click="exportPDF"
class="export-btn"
title="Download Policy as PDF">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14,2 14,8 20,8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10,9 9,9 8,9" />
</svg>
PDF
</button>
<button
@click="handlePrint"
class="export-btn"
title="Print Policy Document">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<polyline points="6,9 6,2 18,2 18,9" />
<path
d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2 2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
<rect x="6" y="14" width="12" height="8" />
</svg>
PRINT
</button>
</div>
</div>
</div>
</div>
<!-- Document Container -->
<div
class="max-w-4xl mx-auto bg-white dark:bg-neutral-950 relative p-8 border-2 border-neutral-900 dark:border-neutral-100">
<div class="template-content">
<!-- Document Header -->
<div class="text-center mb-8">
<h1
class="text-3xl md:text-5xl font-bold uppercase m-0 py-4 border-t-2 border-b-2 border-neutral-900 dark:border-neutral-100 text-neutral-900 dark:text-neutral-100"
:data-org-name="formData.orgName || 'Organization'">
CONFLICT RESOLUTION FRAMEWORK
</h1>
</div>
<!-- Quick Start Section -->
<div
class="no-print no-pdf bg-neutral-50 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-lg p-5 mb-8">
<h3 class="m-0 mb-2 text-[1.2rem] text-neutral-800">
Quick Start - Choose a Template
</h3>
<p class="text-neutral-700">
Select a preset to auto-fill common configurations:
</p>
<div class="flex gap-2 flex-wrap mt-4">
<button
:class="
selectedPreset === 'small-coop'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-blue-600 hover:bg-blue-600 hover:text-white border-blue-600'
"
class="px-4 py-2 border-2 rounded-md text-sm transition-all"
@click="loadPreset('small-coop')">
Small Co-op (2-8 members)
</button>
<button
:class="
selectedPreset === 'community-org'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-blue-600 hover:bg-blue-600 hover:text-white border-blue-600'
"
class="px-4 py-2 border-2 rounded-md text-sm transition-all"
@click="loadPreset('community-org')">
Community Organization
</button>
<button
:class="
selectedPreset === 'nonprofit'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-blue-600 hover:bg-blue-600 hover:text-white border-blue-600'
"
class="px-4 py-2 border-2 rounded-md text-sm transition-all"
@click="loadPreset('nonprofit')">
Nonprofit Organization
</button>
<button
:class="
selectedPreset === 'activist'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-blue-600 hover:bg-blue-600 hover:text-white border-blue-600'
"
class="px-4 py-2 border-2 rounded-md text-sm transition-all"
@click="loadPreset('activist')">
Activist Collective
</button>
<button
:class="
selectedPreset === 'custom'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-blue-600 hover:bg-blue-600 hover:text-white border-blue-600'
"
class="px-4 py-2 border-2 rounded-md text-sm transition-all"
@click="loadPreset('custom')">
Custom Framework
</button>
</div>
</div>
<!-- Section 1: Organization Information -->
<div class="section-card">
<h2 class="section-title">1. Organization Information</h2>
<div class="space-y-6">
<UFormField label="Organization Name" class="form-group-large">
<UInput
v-model="formData.orgName"
placeholder="Enter your organization name"
size="xl"
class="w-full"
:error="validationErrors.orgName"
@input="debouncedAutoSave" />
</UFormField>
<div class="flex flex-row gap-4 space-x-4">
<UFormField label="Organization Type" class="form-group-large">
<USelect
v-model="formData.orgType"
:items="orgTypeOptions"
placeholder="Select organization type..."
size="xl"
class="w-full"
:error="validationErrors.orgType"
@change="autoSave" />
</UFormField>
<UFormField
label="Number of Members/Staff"
class="form-group-large">
<UInput
v-model="formData.memberCount"
type="number"
min="2"
placeholder="e.g., 5"
size="xl"
:error="validationErrors.memberCount"
@change="autoSave" />
</UFormField>
</div>
</div>
</div>
<!-- Section 2: Core Values -->
<div class="section-card">
<div class="flex flex-row justify-between items-center">
<h2 class="section-title">2. Guiding Principles & Values</h2>
<div class="flex flex-row gap-2 items-center no-print no-pdf">
<USwitch
v-model="sectionsEnabled.values"
size="sm"
label="Include this section"
:ui="{
label: 'text-xs text-neutral-700 dark:text-neutral-300',
}" />
</div>
</div>
<div class="space-y-6" v-show="sectionsEnabled.values">
<UFormField
label="Select Core Values (check all that apply)"
class="form-group-large">
<div class="values-grid">
<div
v-for="(value, index) in coreValues"
:key="index"
class="checkbox-item">
<UCheckbox
v-model="value.checked"
:id="`core-value-${index}`"
:label="value.label"
@change="autoSave" />
</div>
</div>
</UFormField>
<UFormField
label="Additional Values or Principles"
class="form-group-large">
<UTextarea
v-model="formData.customValues"
:rows="3"
placeholder="Add any additional values specific to your organization..."
size="xl"
class="w-full"
@input="debouncedAutoSave" />
</UFormField>
</div>
</div>
<!-- Section 3: Conflict Types -->
<div class="section-card">
<h2 class="section-title">3. Types of Conflicts Covered</h2>
<p class="mb-4">
Select which types of conflicts this framework will address:
</p>
<div class="checkbox-group space-y-3">
<div
v-for="(conflict, index) in conflictTypes"
:key="index"
class="checkbox-item">
<UCheckbox
v-model="conflict.checked"
:id="`conflict-type-${index}`"
:label="conflict.label"
@change="autoSave" />
</div>
</div>
<div v-if="validationErrors.conflictTypes" class="validation-error">
{{ validationErrors.conflictTypes }}
</div>
</div>
<!-- Section 4: Resolution Approach -->
<div class="section-card">
<h2 class="section-title">4. Primary Resolution Approach</h2>
<div class="space-y-6">
<UFormField
label="Choose your primary conflict resolution philosophy:">
<URadioGroup
v-model="formData.approach"
:items="approachOptions"
variant="card"
size="lg"
class="mt-2" />
<div v-if="validationErrors.approach" class="validation-error">
{{ validationErrors.approach }}
</div>
</UFormField>
<UFormField class="form-group-large">
<UCheckbox
v-model="formData.anonymousReporting"
id="anonymous-reporting"
label="Allow anonymous reporting"
help="Members can report issues without revealing their identity"
@change="autoSave" />
</UFormField>
</div>
</div>
<!-- Section 5: Roles & Responsibilities -->
<div class="section-card">
<h2 class="section-title">5. Roles & Responsibilities</h2>
<div class="space-y-6">
<UFormField
label="Who can receive initial conflict reports? (check all that apply)"
class="form-group-large">
<div class="checkbox-group space-y-2 mt-4">
<div
v-for="(receiver, index) in reportReceivers"
:key="index"
class="checkbox-item">
<UCheckbox
v-model="receiver.checked"
:id="`report-receiver-${index}`"
:label="receiver.label"
@change="autoSave" />
</div>
</div>
<div
v-if="validationErrors.reportReceivers"
class="validation-error">
{{ validationErrors.reportReceivers }}
</div>
</UFormField>
<UFormField
label="Mediator/Facilitator Structure"
class="form-group-large">
<USelect
v-model="formData.mediatorType"
:items="mediatorTypeOptions"
placeholder="Select mediator structure..."
size="xl"
:error="validationErrors.mediatorType"
@change="autoSave" />
</UFormField>
<UFormField class="form-group-large">
<UCheckbox
v-model="formData.supportPeople"
id="support-people"
label="Allow support people in mediation sessions"
help="Parties can bring a trusted person for emotional support"
@change="autoSave" />
</UFormField>
</div>
</div>
<!-- Section 6: Timeline & Process -->
<div class="section-card">
<h2 class="section-title">6. Timeline & Process Steps</h2>
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UFormField
label="Initial Response Time"
class="form-group-large">
<USelect
v-model="formData.initialResponse"
:items="responseTimeOptions"
placeholder="Select response time..."
size="xl"
class="w-full"
:error="validationErrors.initialResponse"
@change="autoSave" />
</UFormField>
<UFormField
label="Target Resolution Time"
class="form-group-large">
<USelect
v-model="formData.resolutionTarget"
:items="resolutionTimeOptions"
placeholder="Select target time..."
size="xl"
class="w-full"
:error="validationErrors.resolutionTarget"
@change="autoSave" />
</UFormField>
</div>
<UFormField
label="Process Steps (select applicable steps)"
class="form-group-large">
<div class="checkbox-group mt-4 space-y-3">
<div
v-for="(step, index) in processSteps"
:key="index"
class="checkbox-item">
<UCheckbox
v-model="step.checked"
:id="`process-step-${index}`"
:label="`${index + 1}. ${step.label}`"
@change="autoSave" />
</div>
</div>
<div
v-if="validationErrors.processSteps"
class="validation-error">
{{ validationErrors.processSteps }}
</div>
</UFormField>
</div>
</div>
<!-- Section 7: Documentation & Privacy -->
<div class="section-card">
<div
class="section-header flex flex-row justify-between items-center">
<h2 class="section-title">7. Documentation & Privacy</h2>
<div class="flex flex-row gap-2 items-center no-print no-pdf">
<USwitch
v-model="sectionsEnabled.documentation"
size="sm"
label="Include this section"
:ui="{
label: 'text-xs text-neutral-700 dark:text-neutral-300',
}" />
</div>
</div>
<div class="space-y-6" v-show="sectionsEnabled.documentation">
<UFormField label="Documentation Level" class="form-group-large">
<USelect
v-model="formData.docLevel"
:items="docLevelOptions"
placeholder="Select documentation level..."
size="xl"
class="w-full"
@change="autoSave" />
</UFormField>
<UFormField label="Confidentiality Level" class="form-group-large">
<USelect
v-model="formData.confidentiality"
:items="confidentialityOptions"
placeholder="Select confidentiality level..."
size="xl"
class="w-full"
@change="autoSave" />
</UFormField>
<UFormField
label="Record Retention Period"
class="form-group-large">
<USelect
v-model="formData.retention"
:items="retentionOptions"
placeholder="Select retention period..."
size="xl"
class="w-full"
@change="autoSave" />
</UFormField>
</div>
</div>
<!-- Section 8: Consequences & Actions -->
<div class="section-card">
<h2 class="section-title">8. Consequences & Remedial Actions</h2>
<div class="space-y-6">
<UFormField
label="Available Actions (check all that apply)"
class="form-group-large">
<div class="checkbox-group mt-4 space-y-3">
<div
v-for="(action, index) in availableActions"
:key="index"
class="checkbox-item">
<UCheckbox
v-model="action.checked"
:id="`available-action-${index}`"
:label="action.label"
@change="autoSave" />
</div>
</div>
<div
v-if="validationErrors.availableActions"
class="validation-error">
{{ validationErrors.availableActions }}
</div>
</UFormField>
<UFormField
class="form-group-large border-t border-neutral-200 dark:border-neutral-800 pt-4">
<UCheckbox
v-model="formData.appealProcess"
id="appeal-process"
label="Include appeals process"
help="Parties can request review of decisions"
@change="autoSave" />
</UFormField>
</div>
</div>
<!-- Section 9: Special Circumstances -->
<div class="section-card">
<div
class="section-header flex flex-row justify-between items-center">
<h2 class="section-title">9. Special Circumstances</h2>
<div class="flex flex-row gap-2 items-center no-print no-pdf">
<USwitch
v-model="sectionsEnabled.special"
size="sm"
label="Include this section"
:ui="{
label: 'text-xs text-neutral-700 dark:text-neutral-300',
}" />
</div>
</div>
<div class="space-y-6" v-show="sectionsEnabled.special">
<div class="checkbox-group space-y-3">
<div
v-for="(circumstance, index) in specialCircumstances"
:key="index"
class="checkbox-item">
<UCheckbox
v-model="circumstance.checked"
:id="`special-circumstance-${index}`"
:label="circumstance.label"
@change="autoSave" />
</div>
</div>
</div>
</div>
<!-- Section 10: Implementation -->
<div class="section-card">
<h2 class="section-title">10. Implementation Details</h2>
<div class="space-y-6">
<UFormField label="Training Requirements" class="form-group-large">
<UTextarea
v-model="formData.training"
:rows="3"
class="w-full"
placeholder="Describe any training needed for members, facilitators, or committee members..."
size="xl"
@input="debouncedAutoSave" />
</UFormField>
<div class="flex flex-row space-x-4">
<UFormField
label="Policy Review Schedule"
class="form-group-large">
<USelect
v-model="formData.reviewSchedule"
:items="reviewScheduleOptions"
placeholder="Select review schedule..."
size="xl"
class="w-full"
:error="validationErrors.reviewSchedule"
@change="autoSave" />
</UFormField>
<UFormField
label="How can this policy be amended?"
class="form-group-large">
<USelect
v-model="formData.amendments"
:items="amendmentOptions"
placeholder="Select amendment process..."
size="xl"
class="w-full"
:error="validationErrors.amendments"
@change="autoSave" />
</UFormField>
</div>
<div class="p-6 bg-neutral-50 dark:bg-neutral-900 rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UFormField label="Framework Created Date">
<UInput
v-model="formData.createdDate"
type="date"
size="xl"
class="w-full mb-0"
@change="autoSave" />
</UFormField>
<UFormField label="Next Review Date">
<UInput
v-model="formData.reviewDate"
type="date"
size="xl"
class="w-full mb-0"
@change="autoSave" />
</UFormField>
</div>
</div>
</div>
</div>
<!-- Section 11: Reflection Process -->
<div class="section-card">
<div
class="section-header flex flex-row justify-between items-center">
<h2 class="section-title">11. Reflection Process</h2>
<div class="flex flex-row gap-2 items-center no-print no-pdf">
<USwitch
v-model="sectionsEnabled.reflection"
size="sm"
label="Include detailed reflection guidance"
:ui="{
label: 'text-xs text-neutral-700 dark:text-neutral-300',
}" />
</div>
</div>
<div class="space-y-6" v-show="sectionsEnabled.reflection">
<UFormField
label="Reflection Period Timeframe"
class="form-group-large">
<USelect
v-model="formData.reflectionPeriod"
:items="reflectionPeriodOptions"
placeholder="Select reflection timeframe..."
size="xl"
class="w-full md:w-1/2 mt-2"
@change="autoSave" />
</UFormField>
<UFormField
label="Additional Reflection Prompts"
class="form-group-large">
<UTextarea
v-model="formData.customReflectionPrompts"
:rows="4"
placeholder="Add any organization-specific reflection questions or prompts..."
size="xl"
class="w-full mt-2"
@input="debouncedAutoSave" />
</UFormField>
</div>
</div>
<!-- Section 12: Direct Resolution Guidelines -->
<div class="section-card">
<div
class="section-header flex flex-row justify-between items-center">
<h2 class="section-title">12. Direct Resolution Guidelines</h2>
<div class="flex flex-row gap-2 items-center no-print no-pdf">
<USwitch
v-model="sectionsEnabled.directResolution"
size="sm"
label="Include detailed conversation guidance"
:ui="{
label: 'text-xs text-neutral-700 dark:text-neutral-300',
}" />
</div>
</div>
<div class="space-y-6" v-show="sectionsEnabled.directResolution">
<UFormField
label="Communication Channels (select preferred escalation order)"
class="form-group-large">
<div class="checkbox-group space-y-3 mt-4">
<div
v-for="(channel, index) in communicationChannels"
:key="index"
class="checkbox-item">
<UCheckbox
v-model="channel.checked"
:id="`comm-channel-${index}`"
:label="channel.label"
@change="autoSave" />
</div>
</div>
</UFormField>
<UFormField
class="form-group-large border-t border-neutral-200 dark:border-neutral-800 pt-4">
<UCheckbox
v-model="formData.requireDirectAttempt"
id="require-direct-attempt"
label="Require direct resolution attempt before escalation"
help="Parties must try to resolve directly before filing complaints"
@change="autoSave" />
</UFormField>
<UFormField
class="form-group-large border-t border-neutral-200 dark:border-neutral-800 pt-4">
<UCheckbox
v-model="formData.documentDirectResolution"
id="document-direct-resolution"
label="Require written record of direct resolution attempts"
help="Parties should document outcomes of direct conversations"
@change="autoSave" />
</UFormField>
</div>
</div>
<!-- Section 13: Responsible Contact People -->
<div class="section-card">
<h2 class="section-title">
13. Responsible Contact People Structure
</h2>
<div class="space-y-6">
<UFormField
label="Internal Advisor Designation"
class="form-group-large">
<USelect
v-model="formData.internalAdvisorType"
:items="internalAdvisorOptions"
placeholder="Select internal advisor structure..."
class="w-full md:w-1/2"
size="xl"
@change="autoSave" />
</UFormField>
<UFormField
label="Staff Liaison for Conflict Resolution Committee"
class="form-group-large">
<UInput
v-model="formData.staffLiaison"
placeholder="Title/role of designated staff liaison"
size="xl"
class="w-full md:w-1/2"
@input="debouncedAutoSave" />
</UFormField>
<UFormField
label="Board Chair Role in Conflict Resolution"
class="form-group-large">
<USelect
v-model="formData.boardChairRole"
:items="boardChairRoleOptions"
placeholder="Select board chair involvement..."
size="xl"
class="w-full md:w-1/2"
@change="autoSave" />
</UFormField>
</div>
</div>
<!-- Section 14: Formal Complaint Process -->
<div class="section-card">
<h2 class="section-title">14. Formal Complaint Requirements</h2>
<div class="space-y-6">
<UFormField
label="Required Complaint Elements"
class="form-group-large">
<div class="checkbox-group space-y-3 mt-4">
<div
v-for="(element, index) in formalComplaintElements"
:key="index"
class="checkbox-item">
<UCheckbox
v-model="element.checked"
:id="`complaint-element-${index}`"
:label="element.label"
@change="autoSave" />
</div>
</div>
</UFormField>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UFormField
label="Formal Complaint Acknowledgment Time"
class="form-group-large">
<USelect
v-model="formData.formalAcknowledgmentTime"
:items="acknowledgmentTimeOptions"
placeholder="Select acknowledgment timeframe..."
class="w-full"
size="xl"
@change="autoSave" />
</UFormField>
<UFormField
label="Formal Review Completion Time"
class="form-group-large">
<USelect
v-model="formData.formalReviewTime"
:items="reviewTimeOptions"
placeholder="Select review timeframe..."
size="xl"
class="w-full"
@change="autoSave" />
</UFormField>
</div>
<UFormField class="form-group-large">
<UCheckbox
v-model="formData.requireExternalAdvice"
id="require-external-advice"
label="Require external legal advice for complex complaints"
help="Seek external expertise for multi-party or staff/director complaints"
@change="autoSave" />
</UFormField>
</div>
</div>
<!-- Section 15: Settlement & Documentation -->
<div class="section-card">
<h2 class="section-title">15. Settlement & Documentation</h2>
<div class="space-y-6">
<UFormField class="form-group-large">
<UCheckbox
v-model="formData.requireMinutesOfSettlement"
id="minutes-of-settlement"
label="Require 'Minutes of Settlement' for resolved complaints"
help="Agreements must be documented in writing and signed by both parties"
@change="autoSave" />
</UFormField>
<div class="flex flex-row space-x-4">
<UFormField
label="Settlement Confidentiality Level"
class="form-group-large">
<USelect
v-model="formData.settlementConfidentiality"
:items="confidentialityOptions"
placeholder="Select confidentiality level..."
size="xl"
class="w-full"
@change="autoSave" />
</UFormField>
<UFormField
label="Conflict Resolution File Retention"
class="form-group-large">
<USelect
v-model="formData.conflictFileRetention"
:items="retentionOptions"
placeholder="Select retention period..."
size="xl"
class="w-full"
@change="autoSave" />
</UFormField>
</div>
</div>
</div>
<!-- Section 16: External Resources -->
<div class="section-card">
<div
class="section-header flex flex-row justify-between items-center">
<h2 class="section-title">16. External Resources & Redress</h2>
<div class="flex flex-row gap-2 items-center no-print no-pdf">
<USwitch
v-model="sectionsEnabled.externalResources"
size="sm"
label="Include external resource information"
:ui="{
label: 'text-xs text-neutral-700 dark:text-neutral-300',
}" />
</div>
</div>
<div class="space-y-6" v-show="sectionsEnabled.externalResources">
<UFormField class="form-group-large">
<UCheckbox
v-model="formData.includeHumanRights"
id="include-human-rights"
label="Include Human Rights Commission information"
help="Reference external discrimination complaint options"
@change="autoSave" />
</UFormField>
<UFormField
label="Additional External Resources"
class="form-group-large">
<UTextarea
v-model="formData.additionalResources"
:rows="3"
class="w-full"
placeholder="List any other external resources, legal aid contacts, or specialized support services..."
size="xl"
@input="debouncedAutoSave" />
</UFormField>
<UFormField
label="Acknowledgment/Attribution"
class="form-group-large">
<UTextarea
v-model="formData.acknowledgments"
:rows="2"
placeholder="Credit any sources used in developing this policy..."
class="w-full"
size="xl"
@input="debouncedAutoSave" />
</UFormField>
</div>
</div>
<!-- Document Preview Section -->
<div class="no-print" v-if="showPreview">
<div
class="border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-950 p-5 rounded-lg shadow-sm">
<div class="flex flex-row justify-between items-center mb-4">
<h2 class="section-title m-0">📄 Policy Document Preview</h2>
<UButton
size="sm"
variant="ghost"
@click="showPreview = false"
title="Hide preview">
</UButton>
</div>
<div
class="policy-preview prose prose-neutral dark:prose-invert max-w-none px-6 py-5 bg-neutral-50 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-md max-h-[600px] overflow-y-auto text-left"
v-html="markdownToHtml(generatePolicyDocument())"></div>
</div>
</div>
<!-- Export Controls at Bottom -->
<div class="bottom-export-controls no-print">
<div class="export-buttons-bottom">
<UButton
size="lg"
variant="solid"
@click="showPreview = !showPreview"
:title="
showPreview ? 'Hide preview' : 'Show policy document preview'
">
{{ showPreview ? "👁️ Hide Preview" : "👁️ Show Preview" }}
</UButton>
<UButton
size="lg"
color="primary"
variant="solid"
@click="exportMarkdown"
title="Download Policy as Markdown">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14,2 14,8 20,8" />
<line x1="12" y1="18" x2="12" y2="12" />
<path d="m9 15 3-3 3 3" />
</svg>
Download Policy (Markdown)
</UButton>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed, onMounted } from "vue";
definePageMeta({
layout: false,
});
useHead({
title: "Conflict Resolution Framework - Co-op Pay & Value Tool",
meta: [
{
name: "description",
content:
"Create a customized conflict resolution framework for your cooperative or organization with restorative justice principles.",
},
],
});
// Import PDF export composable
const { exportToPDF } = usePdfExportBasic();
const selectedPreset = ref("");
const showPreview = ref(false);
// Options for dropdowns (using simple string arrays like the working membership template)
const orgTypeOptions = [
"Worker Cooperative",
"Consumer Cooperative",
"Nonprofit",
"Collective",
"Community Group",
"Other",
];
const approachOptions = [
{
value: "restorative",
label: "Restorative/Loving Justice",
description:
"Focus on healing, understanding root causes, and repairing relationships",
},
{
value: "mediation",
label: "Mediation-First",
description: "Neutral third-party facilitates dialogue between parties",
},
{
value: "progressive",
label: "Progressive Discipline",
description: "Clear escalation steps with defined consequences",
},
{
value: "hybrid",
label: "Hybrid Approach",
description: "Combines multiple approaches based on conflict type",
},
];
const mediatorTypeOptions = [
"Internal trained mediators",
"External professional mediators",
"Rotating member facilitators",
"Standing committee",
"Decided case-by-case",
];
const responseTimeOptions = [
"Within 24 hours",
"Within 48 hours",
"Within 72 hours",
"Within 1 week",
"Within 2 weeks",
];
const resolutionTimeOptions = [
"1 week",
"2 weeks",
"30 days",
"60 days",
"90 days",
];
const docLevelOptions = [
"Minimal - outcomes only",
"Standard - key points and decisions",
"Comprehensive - detailed records",
];
const confidentialityOptions = [
"Strict - only parties and facilitators",
"Need-to-know basis",
"Transparent to membership",
];
const retentionOptions = ["1 year", "3 years", "5 years", "Permanent"];
const reviewScheduleOptions = [
"Every 6 months",
"Annually",
"Every 2 years",
"As needed",
];
const amendmentOptions = [
"Full consensus required",
"Consent process (no objections)",
"2/3 majority vote",
"Simple majority vote",
];
const reflectionPeriodOptions = [
"Before any escalation",
"24-48 hours before complaint",
"1 week before formal process",
"Optional but encouraged",
"Not required",
];
const internalAdvisorOptions = [
"Single Board-appointed advisor",
"Rotating Board members",
"External neutral advisor",
"Committee-designated advisor",
"Staff member with training",
];
const boardChairRoleOptions = [
"First contact for ED complaints",
"Appeals reviewer",
"Final decision maker",
"Advisory role only",
"Not involved in conflicts",
];
const acknowledgmentTimeOptions = [
"Within 24 hours",
"Within 48 hours",
"Within 1 week",
"Within 2 weeks",
];
const reviewTimeOptions = [
"2 weeks",
"1 month",
"6 weeks",
"2 months",
"3 months",
];
const sectionsEnabled = ref({
values: true,
documentation: true,
special: true,
reflection: true,
directResolution: true,
externalResources: true,
});
const communicationChannels = ref([
{ label: "Asynchronous text (Slack, email)", checked: true },
{ label: "Synchronous text (planned chat session)", checked: true },
{ label: "Audio call or huddle", checked: true },
{ label: "Video conference", checked: true },
{ label: "In-person meeting", checked: false },
]);
const formalComplaintElements = ref([
{ label: "The complainant's name", checked: true },
{ label: "The respondent's name", checked: true },
{
label: "Detailed information about the issue (what, where, when)",
checked: true,
},
{ label: "Details of all prior resolution attempts", checked: true },
{
label: "The specific outcome(s) the complainant is seeking",
checked: true,
},
{ label: "Supporting documentation or evidence", checked: false },
{ label: "Names of potential witnesses", checked: false },
]);
const coreValues = ref([
{ label: "Mutual Care", checked: true },
{ label: "Transparency", checked: true },
{ label: "Accountability", checked: false },
{ label: "Consent-Based", checked: false },
{ label: "Anti-Oppression", checked: false },
{ label: "Restorative Justice", checked: false },
{ label: "Collective Liberation", checked: false },
{ label: "Accessibility", checked: false },
]);
const conflictTypes = ref([
{ label: "Interpersonal disputes between members", checked: true },
{ label: "Code of Conduct violations", checked: true },
{ label: "Harassment or discrimination", checked: false },
{ label: "Work performance issues", checked: false },
{ label: "Conflicts of interest", checked: false },
{ label: "External organization disputes", checked: false },
{ label: "Financial disagreements", checked: false },
]);
const reportReceivers = ref([
{ label: "Designated conflict resolution committee", checked: true },
{ label: "Any board member", checked: false },
{ label: "Executive Director(s)", checked: false },
{ label: "Designated staff liaison", checked: false },
{ label: "Any member", checked: false },
]);
const processSteps = ref([
{ label: "Initial report/complaint received", checked: true },
{ label: "Acknowledgment sent to complainant", checked: true },
{ label: "Initial assessment by designated party", checked: true },
{ label: "Informal resolution attempted", checked: true },
{ label: "Formal investigation if needed", checked: false },
{ label: "Mediation/facilitated dialogue", checked: true },
{ label: "Agreement/resolution documented", checked: true },
{ label: "Follow-up check-in", checked: false },
]);
const availableActions = ref([
{ label: "Verbal warning", checked: true },
{ label: "Written warning", checked: true },
{ label: "Required training/education", checked: true },
{ label: "Temporary suspension", checked: true },
{ label: "Role/responsibility changes", checked: false },
{ label: "Mediated agreement", checked: false },
{ label: "Removal from organization", checked: true },
{ label: "Restorative circle/process", checked: false },
]);
const specialCircumstances = ref([
{
label: "Include immediate removal protocol for safety threats",
checked: true,
},
{
label: "Reference external reporting options (Human Rights Tribunal, etc.)",
checked: true,
},
{ label: "Include anti-retaliation provisions", checked: true },
{ label: "Include trauma-informed approach language", checked: false },
]);
const formData = ref({
orgName: "",
orgType: "",
memberCount: "",
customValues: "",
approach: "restorative",
anonymousReporting: true,
mediatorType: "Internal trained mediators",
supportPeople: true,
initialResponse: "Within 1 week",
resolutionTarget: "30 days",
docLevel: "Standard - key points and decisions",
confidentiality: "Need-to-know basis",
retention: "5 years",
appealProcess: true,
training: "",
reviewSchedule: "Annually",
amendments: "Consent process (no objections)",
createdDate: new Date().toISOString().split("T")[0],
reviewDate: "",
// New fields for enhanced sections
reflectionPeriod: "Before any escalation",
customReflectionPrompts: "",
requireDirectAttempt: true,
documentDirectResolution: true,
internalAdvisorType: "Single Board-appointed advisor",
staffLiaison: "",
boardChairRole: "First contact for ED complaints",
formalAcknowledgmentTime: "Within 1 week",
formalReviewTime: "1 month",
requireExternalAdvice: true,
requireMinutesOfSettlement: true,
settlementConfidentiality: "Need-to-know basis",
conflictFileRetention: "5 years",
includeHumanRights: true,
additionalResources: "",
acknowledgments: "",
});
// Validation logic
const validationErrors = ref({});
const validateForm = () => {
const errors = {};
// Required text fields
if (!formData.value.orgName?.trim()) {
errors.orgName = "Organization name is required";
}
if (!formData.value.orgType?.trim()) {
errors.orgType = "Organization type is required";
}
if (!formData.value.memberCount?.toString().trim()) {
errors.memberCount = "Number of members/staff is required";
}
if (!formData.value.approach?.trim()) {
errors.approach = "Primary resolution approach is required";
}
if (!formData.value.mediatorType?.trim()) {
errors.mediatorType = "Mediator/facilitator structure is required";
}
if (!formData.value.initialResponse?.trim()) {
errors.initialResponse = "Initial response time is required";
}
if (!formData.value.resolutionTarget?.trim()) {
errors.resolutionTarget = "Target resolution time is required";
}
if (!formData.value.reviewSchedule?.trim()) {
errors.reviewSchedule = "Policy review schedule is required";
}
if (!formData.value.amendments?.trim()) {
errors.amendments = "Amendment process is required";
}
// Required checkbox groups (must have at least one checked)
const checkedConflictTypes = conflictTypes.value.filter(
(item) => item.checked
);
if (checkedConflictTypes.length === 0) {
errors.conflictTypes = "Please select at least one type of conflict";
}
// Note: Guiding Principles & Values section is optional - no validation needed
const checkedReportReceivers = reportReceivers.value.filter(
(item) => item.checked
);
if (checkedReportReceivers.length === 0) {
errors.reportReceivers = "Please select at least one report receiver";
}
const checkedProcessSteps = processSteps.value.filter((item) => item.checked);
if (checkedProcessSteps.length === 0) {
errors.processSteps = "Please select at least one process step";
}
const checkedAvailableActions = availableActions.value.filter(
(item) => item.checked
);
if (checkedAvailableActions.length === 0) {
errors.availableActions = "Please select at least one available action";
}
// Note: Special circumstances section is optional - no validation needed
validationErrors.value = errors;
const isValid = Object.keys(errors).length === 0;
// Provide user feedback
if (isValid) {
alert("✅ Form is complete and ready for export!");
} else {
const errorCount = Object.keys(errors).length;
alert(
`❌ Please complete ${errorCount} required field${
errorCount > 1 ? "s" : ""
} before exporting.`
);
}
return isValid;
};
// Completion percentage computation
const completionPercentage = computed(() => {
const allInputs = [
formData.value.orgName,
formData.value.orgType,
formData.value.memberCount,
formData.value.approach,
formData.value.mediatorType,
formData.value.initialResponse,
formData.value.resolutionTarget,
formData.value.reviewSchedule,
formData.value.amendments,
];
const checkboxInputs = [
...coreValues.value,
...conflictTypes.value,
...reportReceivers.value,
...processSteps.value,
...availableActions.value,
...specialCircumstances.value,
];
const filledInputs = allInputs.filter(
(val) => val && val.toString().trim() !== ""
).length;
const checkedBoxes = checkboxInputs.filter((item) => item.checked).length;
const totalFields = allInputs.length + checkboxInputs.length;
const completedFields = filledInputs + checkedBoxes;
return Math.round((completedFields / totalFields) * 100);
});
// Load saved data
const loadSavedData = () => {
if (process.client) {
const saved = localStorage.getItem("conflict-resolution-framework-data");
if (saved) {
try {
const parsedData = JSON.parse(saved);
// Load form data
if (parsedData.formData) {
formData.value = { ...formData.value, ...parsedData.formData };
}
// Load checkbox arrays
if (parsedData.coreValues) coreValues.value = parsedData.coreValues;
if (parsedData.conflictTypes)
conflictTypes.value = parsedData.conflictTypes;
if (parsedData.reportReceivers)
reportReceivers.value = parsedData.reportReceivers;
if (parsedData.processSteps)
processSteps.value = parsedData.processSteps;
if (parsedData.availableActions)
availableActions.value = parsedData.availableActions;
if (parsedData.specialCircumstances)
specialCircumstances.value = parsedData.specialCircumstances;
if (parsedData.communicationChannels)
communicationChannels.value = parsedData.communicationChannels;
if (parsedData.formalComplaintElements)
formalComplaintElements.value = parsedData.formalComplaintElements;
if (parsedData.sectionsEnabled)
sectionsEnabled.value = parsedData.sectionsEnabled;
} catch (error) {
console.error("Error loading saved data:", error);
}
}
}
};
// Auto-save functionality
const autoSave = () => {
// Clear validation errors when users start correcting fields
clearValidationErrors();
if (process.client) {
const dataToSave = {
formData: formData.value,
coreValues: coreValues.value,
conflictTypes: conflictTypes.value,
reportReceivers: reportReceivers.value,
processSteps: processSteps.value,
availableActions: availableActions.value,
specialCircumstances: specialCircumstances.value,
communicationChannels: communicationChannels.value,
formalComplaintElements: formalComplaintElements.value,
sectionsEnabled: sectionsEnabled.value,
};
localStorage.setItem(
"conflict-resolution-framework-data",
JSON.stringify(dataToSave)
);
}
};
const clearValidationErrors = () => {
validationErrors.value = {};
};
// Simple Markdown to HTML converter for preview
const markdownToHtml = (markdown) => {
return (
markdown
// Headers
.replace(/^### (.*$)/gm, "<h3>$1</h3>")
.replace(/^## (.*$)/gm, "<h2>$1</h2>")
.replace(/^# (.*$)/gm, "<h1>$1</h1>")
// Bold and italic
.replace(/\*\*\*(.*?)\*\*\*/g, "<strong><em>$1</em></strong>")
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.*?)\*/g, "<em>$1</em>")
// Lists
.replace(/^- (.*$)/gm, "<li>$1</li>")
.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>")
.replace(/<\/li>\s*<ul>/g, "</li>")
.replace(/<\/ul>\s*<li>/g, "<li>")
// Tables (basic support)
.replace(/^\|(.+)\|$/gm, (match, content) => {
const cells = content.split("|").map((cell) => cell.trim());
if (cells.every((cell) => cell.match(/^-+$/))) {
return ""; // Skip separator rows
}
const cellTags = cells
.map((cell) =>
cell.startsWith("**") && cell.endsWith("**")
? `<th>${cell.slice(2, -2)}</th>`
: `<td>${cell}</td>`
)
.join("");
return `<tr>${cellTags}</tr>`;
})
.replace(/(<tr>.*<\/tr>)/s, "<table>$1</table>")
.replace(/<\/tr>\s*<table>/g, "</tr>")
.replace(/<\/table>\s*<tr>/g, "<tr>")
// Line breaks
.replace(/\n\n/g, "</p><p>")
.replace(/^(.+)$/gm, "<p>$1</p>")
// Horizontal rules
.replace(/^---$/gm, "<hr>")
// Clean up extra paragraphs around headers and lists
.replace(/<p><h([1-6])>/g, "<h$1>")
.replace(/<\/h([1-6])><\/p>/g, "</h$1>")
.replace(/<p><ul>/g, "<ul>")
.replace(/<\/ul><\/p>/g, "</ul>")
.replace(/<p><table>/g, "<table>")
.replace(/<\/table><\/p>/g, "</table>")
.replace(/<p><hr><\/p>/g, "<hr>")
.replace(/<p><\/p>/g, "")
);
};
// Debounced auto-save
const debouncedAutoSave = debounce(autoSave, 300);
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Watch for changes and auto-save
watch(
[
formData,
coreValues,
conflictTypes,
reportReceivers,
processSteps,
availableActions,
specialCircumstances,
communicationChannels,
formalComplaintElements,
sectionsEnabled,
],
autoSave,
{ deep: true }
);
// Toggle section visibility
const toggleSection = (sectionName) => {
sectionsEnabled.value[sectionName] = !sectionsEnabled.value[sectionName];
autoSave();
};
// Preset loading
const loadPreset = (preset) => {
selectedPreset.value = preset;
switch (preset) {
case "small-coop":
formData.value.orgType = "Worker Cooperative";
formData.value.memberCount = "5";
formData.value.approach = "restorative";
formData.value.initialResponse = "Within 48 hours";
formData.value.resolutionTarget = "2 weeks";
formData.value.docLevel = "Standard - key points and decisions";
coreValues.value.forEach((val, index) => {
val.checked = [0, 1, 3, 5].includes(index); // Mutual Care, Transparency, Consent-Based, Restorative Justice
});
break;
case "community-org":
formData.value.orgType = "Nonprofit";
formData.value.memberCount = "15";
formData.value.approach = "mediation";
formData.value.initialResponse = "Within 72 hours";
formData.value.resolutionTarget = "30 days";
formData.value.docLevel = "Comprehensive - detailed records";
break;
case "nonprofit":
formData.value.orgType = "Nonprofit";
formData.value.memberCount = "25";
formData.value.approach = "progressive";
formData.value.initialResponse = "Within 1 week";
formData.value.resolutionTarget = "60 days";
formData.value.docLevel = "Comprehensive - detailed records";
coreValues.value.forEach((val, index) => {
val.checked = [0, 1, 2, 7].includes(index); // Mutual Care, Transparency, Accountability, Accessibility
});
break;
case "activist":
formData.value.orgType = "Collective";
formData.value.approach = "restorative";
coreValues.value.forEach((val, index) => {
val.checked = [4, 5, 6].includes(index); // Anti-Oppression, Restorative Justice, Collective Liberation
});
specialCircumstances.value[3].checked = true; // trauma-informed
break;
}
autoSave();
};
// Export functions
const exportPDF = async () => {
if (process.server || typeof window === "undefined") {
console.warn("PDF export attempted on server side");
return;
}
try {
const orgName = formData.value.orgName || "organization";
const filename = `${orgName.replace(
/[^a-zA-Z0-9]/g,
"_"
)}_conflict_resolution_policy.pdf`;
await exportToPDF(".document-page", filename);
} catch (error) {
console.error("PDF export failed:", error);
alert(
`PDF generation failed: ${
error?.message || "Unknown error"
}\n\nFalling back to print dialog.`
);
window.print();
}
};
const handlePrint = () => {
window.print();
};
// Removed exportText - focusing on Markdown as the primary text export format
const exportMarkdown = () => {
const content = generateMarkdownContent();
const orgName = formData.value.orgName || "organization";
const filename = `${orgName.replace(
/[^a-zA-Z0-9]/g,
"_"
)}_conflict_resolution_policy.md`;
downloadFile(content, filename, "text/markdown");
};
const generatePolicyDocument = () => {
const orgName = formData.value.orgName || "[Organization Name]";
const valuesText = generateValuesSection();
const conflictTable = generateConflictTypesTable();
const proceduresText = generateProceduresSection();
const definitionsText = generateDefinitionsSection();
return `# ${orgName} Conflict Resolution Policy
## Purpose
Disagreements in groups are par for the course. But ignoring conflicts, or managing them poorly, can deeply harm individuals and our whole community.
Addressing conflict head-on is ***a way of caring for each other***.
Our policies and procedures around conduct and conflict are the tools we rely on to prevent harm and intervene early when things go wrong.
This policy aims to offer a straightforward, consistently enforced, and transparent approach to resolving conflicts and disputes. These issues could emerge in relation to ${
orgName.endsWith("s") ? orgName + "'" : orgName + "'s"
} programs, governance, or the actions of its staff${
formData.value.orgType === "Worker Cooperative" ||
formData.value.orgType === "Consumer Cooperative"
? ", members,"
: ""
} or directors.
## Who does this policy apply to?
Staff${
formData.value.orgType === "Worker Cooperative" ||
formData.value.orgType === "Consumer Cooperative"
? ", members,"
: ""
} and community members must comply with the ${orgName} Conflict Resolution Policy and related by-laws and policies as a condition of ${
formData.value.orgType === "Worker Cooperative"
? "membership, employment,"
: "employment,"
} or participation. Failure to cooperate may result in termination${
formData.value.orgType === "Worker Cooperative" ||
formData.value.orgType === "Consumer Cooperative"
? " or removal from membership"
: ""
}.
## What policy should be used?
${conflictTable}
${sectionsEnabled.value.values ? valuesText : ""}
${definitionsText}
## Responsibility for implementation
This policy acknowledges and respects the governance structure of ${orgName}, which states that the ${getMediatorStructureText()} ${
getMediatorStructureText().includes("mediators") ? "are" : "is"
} responsible for conflict resolution operations and activities, while the ${
formData.value.orgType === "Worker Cooperative"
? "Board of Directors"
: "governing body"
} is responsible for matters related to policy, decisions, activities, and governance.
${proceduresText}
---
*This policy was created on ${
formData.value.createdDate || "[Date]"
} and is scheduled for review ${
formData.value.reviewSchedule?.toLowerCase() || "as needed"
}.*
*Next review date: ${formData.value.reviewDate || "[To be scheduled]"}*`;
};
const getApproachDescription = (approach) => {
const approaches = {
restorative: "restorative/loving justice",
mediation: "mediation-first",
progressive: "progressive discipline",
hybrid: "hybrid",
};
return approaches[approach] || approach;
};
const generateValuesSection = () => {
const selectedValues = coreValues.value.filter((v) => v.checked);
const customValues = formData.value.customValues?.trim();
if (selectedValues.length === 0 && !customValues) {
return "";
}
let valuesText = "## Guiding principles\n\n";
if (selectedValues.length > 0) {
valuesText += `Our core values of ${selectedValues
.map((v) => v.label)
.join(", ")} apply in every interaction.\n\n`;
}
if (customValues) {
valuesText += `\n${customValues}\n\n`;
}
valuesText += `- Our skills and resources will be developed and used to resolve conflicts in a way that is ${getApproachDescription(
formData.value.approach
)}based whenever possible.\n`;
valuesText +=
"- All parties to a complaint will *actively participate* and strive to achieve a *collaborative* outcome at the earliest possible stage of the process.\n";
valuesText += `- Information about a complaint will only be given to parties directly involved, ${getMediatorStructureText()}, and others on a need-to-know basis as determined by ${
formData.value.mediatorType === "Standing committee"
? "the committee"
: "the mediator"
}.\n`;
valuesText +=
"- The parties will be provided with clear and understandable reasons for complaint decisions. All parties will be provided with updates during the review process.\n";
valuesText += `- Complaints will be dealt with promptly and resolved as quickly as possible (target: ${
formData.value.resolutionTarget || "[time not specified]"
}).\n`;
valuesText +=
"- Review of complaints will be fair, impartial, and respectful, allowing all parties to have their perspectives heard.\n";
valuesText +=
"- The review of complaints will be thorough and as detailed as possible based on the information provided by the parties.\n";
valuesText +=
"- The process will be accessible and clearly communicated to members.\n\n";
const orgPossessive =
formData.value.orgName && formData.value.orgName.endsWith("s")
? formData.value.orgName + "'"
: formData.value.orgName + "'s";
if (formData.value.anonymousReporting) {
valuesText += `This process is ${orgPossessive} responsibility. Members have the right to request a mediator if required, and anonymous reporting is supported.\n\n`;
} else {
valuesText += `This process is ${orgPossessive} responsibility. Members have the right to request a mediator if required.\n\n`;
}
return valuesText;
};
const generateConflictTypesTable = () => {
const selectedConflicts = conflictTypes.value.filter((v) => v.checked);
let table =
"| **Who Can File** | **Type of Complaint** | **Policy Reference** | **Additional Notes** |\n";
table += "| --- | --- | --- | --- |\n";
const whoCanFile = getWhoCanFile();
selectedConflicts.forEach((conflict) => {
const policyRef = getConflictPolicyReference(conflict.label);
const notes = getConflictNotes(conflict.label);
table += `| ${whoCanFile} | ${conflict.label} | ${policyRef} | ${notes} |\n`;
});
return table;
};
const getWhoCanFile = () => {
const orgType = formData.value.orgType;
if (orgType === "Worker Cooperative" || orgType === "Consumer Cooperative") {
return "Directors, staff, members";
}
return "Directors, staff, community members";
};
const getConflictPolicyReference = (conflictType) => {
const references = {
"Interpersonal disputes between members": "This policy",
"Code of Conduct violations": "[[Code of Conduct]]",
"Harassment or discrimination": "[[Code of Conduct]]",
"Work performance issues": "[[HR Policy]]",
"Conflicts of interest": "[[Conflict of Interest Policy]]",
"External organization disputes": "Per agreement dispute clause",
"Financial disagreements": "This policy",
};
return references[conflictType] || "This policy";
};
const getConflictNotes = (conflictType) => {
const notes = {
"Harassment or discrimination":
"May also pursue through Human Rights Tribunal",
"Work performance issues": "Directly through HR procedures",
"Conflicts of interest":
"Addressed alongside the organization's specific policy",
"External organization disputes":
"Resolved per agreement's dispute resolution clause",
};
return notes[conflictType] || "-";
};
const getMediatorStructureText = () => {
const mediatorType = formData.value.mediatorType;
const typeMap = {
"Internal trained mediators": "trained internal mediators",
"External professional mediators": "external mediators",
"Rotating member facilitators": "rotating member facilitators",
"Standing committee": "Conflict Resolution Committee",
"Decided case-by-case": "designated mediators",
};
return typeMap[mediatorType] || mediatorType;
};
const generateDefinitionsSection = () => {
let definitions = "## Definitions\n\n";
definitions +=
"- **Conflict** and **dispute** are ongoing experiences of tension and misunderstandings, often leading to interpersonal discord. These terms are used interchangeably.\n";
if (formData.value.mediatorType === "Standing committee") {
definitions += `- The **Conflict Resolution Committee** is a ${
formData.value.orgType === "Worker Cooperative"
? "standing committee of the Board"
: "designated committee"
} to which unresolved formal complaints are sent for review and recommendations.\n`;
}
definitions +=
"- A **complainant** is the individual lodging a complaint against another related individual, policy, or practice.\n";
if (formData.value.mediatorType !== "External professional mediators") {
definitions += `- The **Internal Advisor** is the ${
formData.value.orgType === "Worker Cooperative"
? "Board-appointed"
: "organization-appointed"
} mediator who facilitates the conflict resolution process. They work to achieve a satisfactory solution by acting as an intermediary and convening authority.\n`;
}
definitions +=
"- A **respondent** is an individual against whom a complaint has been made, and/or someone responsible for the policy or activity complained about.\n";
const selectedReceivers = reportReceivers.value.filter((v) => v.checked);
if (selectedReceivers.length > 0) {
definitions += `- **Responsible contact people (RCP)** are those who are accountable for assisting in conflict resolution and addressing formal complaints. In our organization, this includes: ${selectedReceivers
.map((r) => r.label.toLowerCase())
.join(
", "
)}. They do not act as advocates for any party in the conflict.\n`;
}
if (formData.value.supportPeople) {
definitions +=
"- **Support people** are individuals not connected to the conflicts or disputes being addressed, which either the complainant or respondent may choose to have in attendance at mediation meetings.\n";
}
return definitions + "\n";
};
const generateProceduresSection = () => {
const selectedActions = availableActions.value.filter((v) => v.checked);
let procedures = "## Procedures\n\n";
procedures +=
"We do our best to resolve conflicts at the lowest possible escalation step (direct resolution), but agree to escalate conflicts (to assisted resolution) if they are not resolved.\n\n";
// Reflection Section
if (sectionsEnabled.value.reflection) {
procedures += generateReflectionSection();
}
// Direct Resolution Section
procedures += generateDirectResolutionSection();
// Assisted Resolution Section
procedures += generateAssistedResolutionSection();
// Formal Complaints Section
procedures += generateFormalComplaintsSection();
if (selectedActions.length > 0) {
procedures += "### Available Actions and Consequences\n\n";
procedures +=
"Depending on the nature and severity of the conflict, the following actions may be taken:\n\n";
selectedActions.forEach((action) => {
procedures += `- ${action.label}\n`;
});
procedures += "\n";
}
if (formData.value.appealProcess) {
procedures += "### Appeals Process\n\n";
procedures +=
"Parties may request a review of decisions made through this conflict resolution process. Appeals must be submitted in writing within 30 days of the decision.\n\n";
}
// Settlement Documentation
if (formData.value.requireMinutesOfSettlement) {
procedures += generateSettlementSection();
}
const selectedCircumstances = specialCircumstances.value.filter(
(v) => v.checked
);
if (sectionsEnabled.value.special && selectedCircumstances.length > 0) {
procedures += "### Special Circumstances\n\n";
selectedCircumstances.forEach((circumstance) => {
procedures += `- ${circumstance.label}\n`;
});
procedures += "\n";
}
// External Resources
if (sectionsEnabled.value.externalResources) {
procedures += generateExternalResourcesSection();
}
return procedures;
};
const generateReflectionSection = () => {
let section = "### Reflection\n\n";
section += `Before escalating a conflict (timeframe: ${
formData.value.reflectionPeriod?.toLowerCase() || "as needed"
}), we encourage the following reflection process:\n\n`;
section +=
"1. Set aside some time to think through what happened. What was the other person's behaviour? How did it affect you? *Distinguish other people's **actions** from your **feelings** about them.*\n";
section +=
"2. Consider what uncertainties or misunderstandings may have occurred.\n";
section +=
"3. Distinguish disagreement from personal hostility. Disagreement and dissent are part of healthy discussion. Hostility is not.\n";
section +=
"4. Use your personal support system (friends, family, therapist, etc.) to work through and clarify your perspective.\n";
section +=
"5. Ask yourself what part you played, how you could have behaved differently, and what your needs are.\n\n";
if (formData.value.customReflectionPrompts?.trim()) {
section += "#### Additional Reflection Questions\n\n";
section += formData.value.customReflectionPrompts + "\n\n";
}
return section;
};
const generateDirectResolutionSection = () => {
let section = "## Direct Resolution\n\n";
section +=
"A *direct resolution* process occurs when individuals communicate their concerns and work together to resolve disputes without filing an informal or formal complaint.\n\n";
if (formData.value.requireDirectAttempt) {
section +=
"**Note: Direct resolution attempts are required before escalation in most cases.**\n\n";
}
section += "### Have a Conversation\n\n";
section +=
"When there is a disagreement, the involved people should first **communicate with each other** about their concerns.\n\n";
section +=
"1. Choose a time and place to meet that is private and agreeable to both.\n";
section += "2. Allow a reasonable amount of time.\n";
section +=
"3. The point of the meeting is not to determine who is right or wrong, but rather to reach **a mutual understanding**. Achieving this requires patience and a willingness to listen to the other person's perspective without immediately dismissing it as incorrect.\n";
section +=
'4. Express your thoughts and feelings directly and without belittling or dismissing the other person\'s perspective. Use "I" statements and active listening techniques.\n';
section +=
"5. Communicate your **wants** and **needs** and make **offers** and **requests**.\n";
section +=
"6. During the conversation, try your best to learn how to avoid miscommunication and misunderstandings in the future.\n";
if (formData.value.documentDirectResolution) {
section +=
"7. Keep a written record of the resolution of this conversation, agreed to by both parties.\n\n";
} else {
section += "\n";
}
// Escalating Bandwidth
const selectedChannels = communicationChannels.value.filter((c) => c.checked);
if (sectionsEnabled.value.directResolution && selectedChannels.length > 0) {
section += "#### Escalating Bandwidth\n\n";
section +=
"Whenever a misunderstanding or conflict arises, **escalate the bandwidth of the channel**:\n\n";
selectedChannels.forEach((channel, index) => {
section += `${index + 1}. ${channel.label}\n`;
});
section +=
"\nMove from text to voice, voice to video, and ultimately to in-person when possible.\n\n";
}
return section;
};
const generateAssistedResolutionSection = () => {
let section = "## Assisted Resolution\n\n";
section += "### Informal Complaints\n\n";
section +=
"If talking things out doesn't work, you can ask a responsible contact person for help in writing. Mention that you're making an *informal complaint* and seeking assistance.\n\n";
section += generateResponsibleContactTable();
const responseTime =
formData.value.initialResponse?.toLowerCase() || "a reasonable timeframe";
section += `When someone makes an informal complaint, the responsible contact person (RCP) will ask if they have tried to resolve the issue themselves and will talk to the Internal Advisor if necessary. Within ${
responseTime.startsWith("within")
? responseTime.substring(6).trim()
: responseTime
} of the complaint being made, the person in charge will work to settle the matter through the informal complaint resolution process.\n\n`;
section += "#### Procedure\n\n";
section +=
"The RCP will speak with each person separately to hear their perspectives and to review the direct resolution process.\n\n";
section +=
"The parties will be invited to attend informal meetings, negotiations, facilitated discussions, or mediation. These invitations will be extended by either the RCP, the Conflict Resolution Committee, or the Internal Advisor.\n\n";
section += `Depending on their training and neutrality, either the RCP or the ${
formData.value.internalAdvisorType?.toLowerCase() || "Internal Advisor"
} will act as a facilitator or mediator in the selected process.\n\n`;
section +=
"If the chosen process results in an acceptable informal outcome for both parties, the matter will be considered resolved.\n\n";
return section;
};
const generateFormalComplaintsSection = () => {
let section = "### Formal Complaints\n\n";
section += `If informal resolution efforts do not result in an acceptable outcome within ${
formData.value.formalReviewTime?.toLowerCase() || "a reasonable timeframe"
} or to the satisfaction of the complainant, they may file a *formal complaint* in writing.\n\n`;
const selectedElements = formalComplaintElements.value.filter(
(e) => e.checked
);
if (selectedElements.length > 0) {
section += "The written complaint must include:\n\n";
selectedElements.forEach((element, index) => {
section += `${index + 1}. ${element.label}\n`;
});
section += "\n";
}
section += "It should be submitted to the appropriate RCP.\n\n";
if (formData.value.requireExternalAdvice) {
section +=
"*Seeking external advice and expertise to aid this process is recommended for complex complaints.*\n\n";
}
section += generateResponsibleContactTable("formal");
const ackTime =
formData.value.formalAcknowledgmentTime?.toLowerCase() || "one week";
section += `The RCP will acknowledge receipt of the complaint within ${
ackTime.startsWith("within") ? ackTime.substring(6).trim() : ackTime
} and forward it to the Internal Advisor (if not self). The Internal Advisor will then proceed to:\n\n`;
section +=
"1. Review the complaint to ensure all information is included and that enough information is present to assess the situation and respond.\n";
section +=
"2. Assess and make note of organizational by-laws, policies and codes that might have been violated.\n";
section +=
"3. If needed, seek advice from external sources of expertise.\n\n";
if (formData.value.requireExternalAdvice) {
section +=
"> **Tip:** If the complaint involves multiple complainants or respondents who are staff and/or Directors, seek external legal advice.\n\n";
}
return section;
};
const generateResponsibleContactTable = (type = "informal") => {
const orgType = formData.value.orgType;
const isCooperative =
orgType === "Worker Cooperative" || orgType === "Consumer Cooperative";
let table = `#### Responsible Contact People (${type} complaints)\n\n`;
table +=
"| Complainant | First Contact | If First Contact Is the Respondent | Additional Steps |\n";
table += "| --- | --- | --- | --- |\n";
// Staff row
const staffLiaison =
formData.value.staffLiaison ||
"staff liaison on the Conflict Resolution Committee";
table += `| **Staff** | Immediate supervisor | ${
isCooperative ? "One or both Co-EDs" : "Executive Director"
} | If supervisor is ED and the Respondent, contact ${staffLiaison} |\n`;
// ED/Leadership row
if (isCooperative) {
table += `| **Executive Director (ED)** | ${
formData.value.boardChairRole?.includes("Chair")
? "Chair of the Board of Directors"
: "Board Chair"
} | ${staffLiaison} | If Respondents involve multiple directors, contact the Internal Advisor |\n`;
} else {
table += `| **Executive Director** | ${
formData.value.boardChairRole?.includes("Chair")
? "Chair of the Board"
: "Board Chair"
} | ${staffLiaison} | If involving multiple directors, contact the Internal Advisor |\n`;
}
// Director row
table += `| **Director** | ${
isCooperative ? "One or both Co-EDs" : "Executive Director"
} | Internal Advisor | - |\n`;
// Member/Public row
const memberType = isCooperative
? "Member of the organization"
: "Community member";
table += `| **${memberType}, or member of the public** | Designated staff | ${
isCooperative ? "ED" : "Executive Director"
} | If the Respondent is the ED, then contact the Internal Advisor |\n\n`;
return table;
};
const generateSettlementSection = () => {
let section = "#### Reaching an Agreement\n\n";
section +=
'Any resolution of a complaint that is agreed upon through direct negotiation must be documented in writing and signed by both the complainant and respondent. These "Minutes of Settlement" will be kept confidential and only shared with the Internal Advisor, Staff, Board, legal counsel, or other parties who need to know to fulfill their organizational duties.\n\n';
section += "Considerations when agreeing should include:\n\n";
section +=
"1. Is the agreement within the scope of the parties' decision-making powers in relation to their organizational role?\n";
section += "2. Is the agreement realistic and durable?\n";
section += "3. Does the agreement in any way compromise the organization?\n";
section +=
"4. Are there elements of the agreement that impact the organization's operations, policies, reputation, external relationships or public perceptions?\n";
section += `5. Does it align with our values and commitment to ${getApproachDescription(
formData.value.approach
)}?\n\n`;
section += `**Confidentiality Level**: ${
formData.value.settlementConfidentiality || "Need-to-know basis"
}\n`;
section += `**File Retention**: ${
formData.value.conflictFileRetention || "5 years"
}\n\n`;
return section;
};
const generateExternalResourcesSection = () => {
if (!sectionsEnabled.value.externalResources) return "";
let section = "## Other Redress\n\n";
if (formData.value.includeHumanRights) {
section +=
"An individual who is not satisfied with the outcome of a **harassment** complaint process may file a discrimination complaint with the [Canadian Human Rights Commission](https://www.chrc-ccdp.gc.ca/eng).\n\n";
}
if (formData.value.additionalResources?.trim()) {
section += "### Additional Resources\n\n";
section += formData.value.additionalResources + "\n\n";
}
if (formData.value.acknowledgments?.trim()) {
section += "## Acknowledgments\n\n";
section += formData.value.acknowledgments + "\n\n";
} else {
section += "## Acknowledgments\n\n";
section +=
"This work is inspired in part by the work of the Media Arts Network of Ontario led by Sheila Wilmot, January 2016.\n\n";
}
return section;
};
// Removed generateTextContent - using generatePolicyDocument directly
const generateMarkdownContent = () => {
return generatePolicyDocument();
};
const downloadFile = (content, filename, mimeType) => {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Initialize
onMounted(() => {
loadSavedData();
});
</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");
@reference "tailwindcss";
/* Template wrapper and document styling */
/* rely on Tailwind bg utilities applied on wrapper */
.template-wrapper {
min-height: 100vh;
padding: 2rem;
font-family: "Ubuntu", monospace;
position: relative;
}
.template-wrapper::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: radial-gradient(
circle at 25% 25%,
black 1px,
transparent 1px
),
radial-gradient(circle at 75% 75%, black 1px, transparent 1px);
background-size: 8px 8px, 8px 8px;
background-position: 0 0, 4px 4px;
opacity: 0.1;
pointer-events: none;
z-index: -1;
}
.document-page::before {
content: "";
position: absolute;
top: 4px;
left: 4px;
right: -4px;
bottom: -4px;
background: black;
background-image: radial-gradient(white 1px, transparent 1px);
background-size: 2px 2px;
z-index: -1;
}
.document-page::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid black;
z-index: 1;
pointer-events: none;
}
/* Progress bar */
.progress-bar {
width: 100%;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
margin-bottom: 30px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #66bb6a);
border-radius: 3px;
transition: width 0.3s ease;
}
/* Document header */
.document-header {
text-align: center;
margin-bottom: 2rem;
}
.document-title {
font-size: 2.25rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 2px;
margin: 0 0 0.5rem;
padding: 1rem 0;
border-top: 2px solid #111827;
border-bottom: 2px solid #111827;
}
.subtitle {
color: #666;
font-size: 1.1rem;
margin: 0;
}
/* Quick start section */
.quick-start {
background: #f0f7ff;
border: 1px solid #d0e3ff;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.quick-start h3 {
margin: 0 0 10px 0;
font-size: 1.2rem;
color: #333;
}
.preset-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 15px;
}
.preset-btn {
padding: 8px 16px;
border: 2px solid #0066cc;
background: white;
color: #0066cc;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
.preset-btn:hover {
background: #0066cc;
color: white;
}
.preset-btn.selected {
background: #0066cc;
color: white;
}
/* Section styling */
.section-card {
@apply border border-neutral-200 dark:border-neutral-800 rounded-lg p-5 mb-8;
}
/* Styling for sections when toggled off */
.section-card:has(.space-y-6[style*="display: none"]) {
opacity: 0.7;
}
.section-card:has(.space-y-6[style*="display: none"]) .section-title {
color: #666;
}
.section-header {
/* presentational container only */
}
.section-title {
font-size: 1.75rem;
font-weight: 800;
color: inherit;
margin: 0 0 1rem 0;
}
/* Toggle styling */
.toggle-section {
display: flex;
align-items: center;
gap: 8px;
}
.toggle {
position: relative;
width: 48px;
height: 24px;
background: #ccc;
border-radius: 24px;
cursor: pointer;
transition: background 0.3s;
}
.toggle.active {
background: #4caf50;
}
.toggle-slider {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform 0.3s;
}
.toggle.active .toggle-slider {
transform: translateX(24px);
}
/* Form styling */
.form-group-large {
margin-bottom: 1.5rem;
}
/* Ensure form field containers are also full-width */
.form-group-large {
width: 100%;
}
.form-group-large > * {
width: 100%;
}
/* Ensure consistent alignment for all form fields */
.form-group-large :deep(textarea),
.form-group-large :deep(input),
.form-group-large :deep(select),
.form-group-large :deep(.ui-select),
.form-group-large :deep(.ui-input),
.form-group-large :deep(.ui-textarea) {
margin-left: 0 !important;
padding-left: 0.75rem !important;
}
/* Additional targeting for UInput, USelect, UTextarea components */
.form-group-large :deep(.u-input),
.form-group-large :deep(.u-select),
.form-group-large :deep(.u-textarea),
.form-group-large :deep([data-headlessui-state]),
.form-group-large :deep(.relative) {
margin-left: 0 !important;
}
.form-group-large :deep(.u-input input),
.form-group-large :deep(.u-select select),
.form-group-large :deep(.u-textarea textarea) {
margin-left: 0 !important;
padding-left: 0.75rem !important;
}
.help-text {
/* moved to inline classes */
}
.checkbox-group {
@apply flex flex-col space-y-3;
}
.checkbox-item {
@apply flex;
}
/* Approach radio styling removed - using pure Tailwind classes */
.values-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
margin-top: 10px;
}
.inline-field {
display: inline-block;
margin: 0 0.25rem;
min-width: 120px;
border: none;
background: #f9fafb;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.content-paragraph {
margin-bottom: 0.75rem;
line-height: 1.6;
text-align: left;
}
/* Validation error styling */
.validation-error {
color: #ef4444;
font-size: 0.875rem;
font-weight: 500;
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
/* Preview styling */
.preview-controls {
text-align: center;
margin: 2rem 0;
}
.preview-btn {
background: #6366f1;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.preview-btn:hover {
background: #5856eb;
}
/* Bottom export controls */
.export-buttons-bottom {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: center;
}
.export-btn.large {
padding: 1rem 2rem;
font-size: 1.1rem;
font-weight: 600;
}
.export-btn.large svg {
width: 20px;
height: 20px;
}
.close-preview-btn {
background: #ef4444;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
transition: background 0.2s;
}
.close-preview-btn:hover {
background: #dc2626;
}
.policy-preview {
line-height: 1.6;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji" !important;
font-size: 0.95rem;
}
.policy-preview h1 {
font-size: 2rem;
font-weight: bold;
color: #111827;
margin-bottom: 1.5rem;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 0.5rem;
}
.policy-preview h2 {
font-size: 1.5rem;
font-weight: 600;
color: #374151;
margin-top: 2rem;
margin-bottom: 1rem;
}
.policy-preview h3 {
font-size: 1.25rem;
font-weight: 600;
color: #4b5563;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.policy-preview p {
margin-bottom: 1.25rem;
color: #374151;
}
.policy-preview p:last-child {
margin-bottom: 0;
}
.policy-preview :is(p, span, div, li, ol, ul, table, th, td, blockquote) {
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji" !important;
}
.policy-preview a {
color: #2563eb;
text-decoration: underline;
}
html.dark .policy-preview a {
color: #93c5fd;
}
.policy-preview blockquote {
border-left: 4px solid #e5e7eb;
margin: 1rem 0;
padding: 0.5rem 1rem;
color: #4b5563;
background: #f9fafb;
}
html.dark .policy-preview blockquote {
border-left-color: #374151;
background: #0f1115;
color: #9ca3af;
}
.policy-preview code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace !important;
background: #f3f4f6;
padding: 0.1rem 0.35rem;
}
html.dark .policy-preview code {
background: #111827;
}
.policy-preview ul {
margin: 1rem 0;
padding-left: 1.5rem;
}
.policy-preview li {
margin-bottom: 0.5rem;
color: #374151;
}
.policy-preview table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
background: white;
}
.policy-preview th,
.policy-preview td {
border: 1px solid #d1d5db;
padding: 0.75rem;
text-align: left;
}
.policy-preview th {
background: #f3f4f6;
font-weight: 600;
color: #374151;
}
.policy-preview hr {
border: none;
border-top: 1px solid #e5e7eb;
margin: 2rem 0;
}
.policy-preview strong {
font-weight: 600;
color: #111827;
}
.policy-preview em {
font-style: italic;
}
/* Export controls */
.export-controls {
max-width: 8.5in;
margin: 0 auto 1.5rem;
background: white;
border: 1px solid black;
padding: 1rem;
position: relative;
}
.export-controls::before {
content: "";
position: absolute;
top: 2px;
left: 2px;
right: -2px;
bottom: -2px;
background: black;
background-image: radial-gradient(white 1px, transparent 1px);
background-size: 2px 2px;
z-index: -1;
}
.export-content {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
align-items: center;
justify-content: space-between;
}
.export-section {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
flex: 1;
}
.export-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
.export-title {
font-weight: 600;
color: #374151;
margin: 0 1rem 0 0;
font-size: 0.875rem;
white-space: nowrap;
}
.export-btn {
background: #f9fafb;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 0.5rem 1rem;
font-size: 0.875rem;
color: #374151;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.export-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.export-btn.primary {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
.export-btn.primary:hover {
background: #2563eb;
border-color: #2563eb;
}
.export-btn.validation {
background: #10b981;
border-color: #10b981;
color: white;
}
.export-btn.validation:hover {
background: #059669;
border-color: #059669;
}
/* Hide elements from print */
.no-print,
.no-pdf {
display: block;
}
/* Print styles */
@media print {
.no-print,
.no-pdf {
display: none !important;
}
.template-wrapper {
background: white !important;
padding: 0 !important;
}
.document-page {
max-width: none !important;
box-shadow: none !important;
border-radius: 0 !important;
padding: 0 !important;
}
.document-title {
font-size: 18pt;
background: none;
padding: 0.5rem 0;
border-width: 1px;
}
.section-title {
font-size: 14pt;
page-break-after: avoid;
}
.section-card {
break-inside: avoid;
margin-bottom: 1rem;
padding: 0.5rem;
border: 1px solid #ccc;
}
.checkbox-item {
font-size: 10pt;
margin: 2pt 0;
}
.inline-field {
background: none !important;
border: none !important;
border-bottom: 1pt solid #000 !important;
padding: 2pt !important;
}
}
/* Bitmap aesthetic overrides - remove all rounded corners */
* {
border-radius: 0 !important;
font-family: "Ubuntu", monospace !important;
}
/* Form fields with bitmap styling */
input,
textarea,
select {
border: 1px solid black !important;
background: white !important;
color: black !important;
font-family: "Ubuntu Mono", monospace !important;
}
input:focus,
textarea:focus,
select:focus {
outline: 2px solid black !important;
outline-offset: -2px !important;
background: white !important;
}
/* Dark mode form fields */
html.dark input,
html.dark textarea,
html.dark select {
border: 1px solid white !important;
background: #0a0a0a !important;
color: white !important;
}
html.dark input:focus,
html.dark textarea:focus,
html.dark select:focus {
outline: 2px solid white !important;
background: #0a0a0a !important;
}
/* Buttons with bitmap styling */
button:not(.export-btn) {
background: white !important;
border: 1px solid black !important;
color: black !important;
font-family: "Ubuntu Mono", monospace !important;
text-transform: uppercase !important;
font-weight: bold !important;
letter-spacing: 0.5px !important;
}
button:not(.export-btn):hover {
background: black !important;
color: white !important;
transform: translateY(-1px) translateX(-1px) !important;
}
/* Dark mode buttons */
html.dark button:not(.export-btn) {
background: #0a0a0a !important;
border: 1px solid white !important;
color: white !important;
}
html.dark button:not(.export-btn):hover {
background: white !important;
color: black !important;
}
/* Export buttons specifically */
.export-btn {
background: white !important;
border: 1px solid black !important;
color: black !important;
font-family: "Ubuntu Mono", monospace !important;
text-transform: uppercase !important;
font-weight: bold !important;
letter-spacing: 0.5px !important;
}
.export-btn:hover {
background: black !important;
color: white !important;
transform: translateY(-1px) translateX(-1px) !important;
}
.export-btn.primary {
background: black !important;
color: white !important;
}
.export-btn.primary:hover {
background: black !important;
transform: translateY(-1px) translateX(-1px) !important;
}
/* Remove any card styling roundness */
.section-card,
.form-group-large {
border-radius: 0 !important;
}
/* Document titles */
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Ubuntu", monospace !important;
color: inherit !important;
}
/* All text */
p,
span,
div {
color: inherit !important;
font-family: "Ubuntu", monospace !important;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.template-wrapper {
padding: 1rem;
}
.preset-buttons {
flex-direction: column;
}
.values-grid {
grid-template-columns: 1fr;
}
.export-content {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
}
</style>