-
-
-
@@ -418,6 +418,7 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contribution-form');
+ if (!form) console.error('[INIT ERROR] Form not found');
const typeSelect = document.getElementById('tool-type');
const submitBtn = document.getElementById('submit-btn');
const submitText = document.getElementById('submit-text');
@@ -619,10 +620,6 @@ related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]`
typeSelect.addEventListener('change', updateFieldVisibility);
}
- if (refreshPreviewBtn) {
- refreshPreviewBtn.addEventListener('click', updateYAMLPreview);
- }
-
// Update preview on form changes
if (form) {
form.addEventListener('input', debounce(updateYAMLPreview, 500));
@@ -631,8 +628,9 @@ related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]`
// Form submission
if (form) {
- form.addEventListener('submit', async (e) => {
+ form?.addEventListener('submit', async (e) => {
e.preventDefault();
+ console.log('[DEBUG] Submit button clicked');
const errors = validateForm();
if (errors.length > 0) {
diff --git a/src/styles/global.css b/src/styles/global.css
index 226c9d8..32275d6 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -279,13 +279,21 @@ input, select, textarea {
background-color: var(--color-bg);
color: var(--color-text);
font-size: 0.875rem;
- transition: var(--transition-fast);
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
-input:focus, select:focus, textarea:focus {
- outline: none;
+input:focus, textarea:focus, select:focus {
border-color: var(--color-primary);
- box-shadow: 0 0 0 3px rgb(37 99 235 / 10%);
+ box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
+}
+
+/* Form validation states */
+input:invalid:not(:focus), textarea:invalid:not(:focus), select:invalid:not(:focus) {
+ border-color: var(--color-error);
+}
+
+input:valid:not(:focus), textarea:valid:not(:focus), select:valid:not(:focus) {
+ border-color: var(--color-accent);
}
select {
@@ -301,14 +309,67 @@ select {
display: flex;
align-items: center;
gap: 0.5rem;
+ cursor: pointer;
+ transition: var(--transition-fast);
+ user-select: none;
+}
+
+.checkbox-wrapper:hover {
+ background-color: var(--color-bg-secondary);
+ border-radius: 0.25rem;
+}
+
+.checkbox-wrapper input[type="checkbox"] {
+ margin-right: 0.5rem;
+ cursor: pointer;
+}
+
+/* Scrollable checkbox containers */
+.checkbox-container {
+ max-height: 200px;
+ overflow-y: auto;
+ border: 1px solid var(--color-border);
+ border-radius: 0.375rem;
+ padding: 0.75rem;
+ background-color: var(--color-bg);
+}
+
+.checkbox-container::-webkit-scrollbar {
+ width: 8px;
+}
+
+.checkbox-container::-webkit-scrollbar-track {
+ background: var(--color-bg-secondary);
+ border-radius: 4px;
+}
+
+.checkbox-container::-webkit-scrollbar-thumb {
+ background: var(--color-border);
+ border-radius: 4px;
+}
+
+.checkbox-container::-webkit-scrollbar-thumb:hover {
+ background: var(--color-text-secondary);
}
input[type="checkbox"] {
- width: auto;
+ width: 16px;
+ height: 16px;
+ accent-color: var(--color-primary);
margin: 0;
cursor: pointer;
}
+/* Better focus states for accessibility */
+input[type="checkbox"]:focus,
+input[type="text"]:focus,
+input[type="url"]:focus,
+textarea:focus,
+select:focus {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
/* Consolidated Card System */
.card {
background-color: var(--color-bg);
@@ -689,6 +750,7 @@ input[type="checkbox"] {
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 50%);
+ backdrop-filter: blur(2px);
z-index: 999;
}
@@ -847,6 +909,27 @@ input[type="checkbox"] {
transform: translateY(-1px);
}
+/* Loading state improvements */
+.btn.loading {
+ opacity: 0.7;
+ pointer-events: none;
+ position: relative;
+}
+
+.btn.loading::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 16px;
+ height: 16px;
+ margin: -8px 0 0 -8px;
+ border: 2px solid transparent;
+ border-top: 2px solid currentColor;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
/* Collaboration Tools */
.collaboration-tools-compact {
display: flex;
@@ -1440,6 +1523,11 @@ This will literally assault the user's retinas. They'll need sunglasses to look
}
}
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
/* Consolidated Responsive Design */
@media (width <= 1200px) {
.modals-side-by-side #tool-details-primary.active,
@@ -1520,6 +1608,10 @@ This will literally assault the user's retinas. They'll need sunglasses to look
width: 95%;
max-width: none;
}
+
+ .form-grid.two-columns {
+ grid-template-columns: 1fr;
+ }
}
@media (width <= 640px) {
@@ -1572,6 +1664,15 @@ This will literally assault the user's retinas. They'll need sunglasses to look
grid-template-columns: 1fr;
gap: 0.5rem;
}
+ .card {
+ padding: 1rem;
+ }
+ .form-grid {
+ gap: 0.75rem;
+ }
+ .checkbox-container {
+ max-height: 150px;
+ }
}
@media (width <= 480px) {
@@ -1844,4 +1945,98 @@ This will literally assault the user's retinas. They'll need sunglasses to look
.flex-start {
display: flex;
align-items: center;
+}
+
+.field-help {
+ font-size: 0.8125rem;
+ color: var(--color-text-secondary);
+ line-height: 1.4;
+}
+
+/* Improved field error styling */
+.field-error {
+ color: var(--color-error);
+ font-size: 0.8125rem;
+ margin-top: 0.25rem;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.field-error::before {
+ content: "⚠";
+ font-size: 0.75rem;
+}
+
+/* Form section improvements */
+.form-section {
+ border: 1px solid var(--color-border);
+ border-radius: 0.5rem;
+ padding: 1rem;
+ margin-bottom: 1.5rem;
+ background-color: var(--color-bg);
+}
+
+.form-section h3 {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ color: var(--color-primary);
+ font-size: 1.125rem;
+}
+
+/* Success/warning notices in forms */
+.form-notice {
+ padding: 1rem;
+ border-radius: 0.5rem;
+ margin-bottom: 1rem;
+ border-left: 3px solid;
+}
+
+.form-notice.success {
+ background-color: var(--color-oss-bg);
+ border-left-color: var(--color-accent);
+ color: var(--color-text);
+}
+
+.form-notice.warning {
+ background-color: var(--color-hosted-bg);
+ border-left-color: var(--color-warning);
+ color: var(--color-text);
+}
+
+.form-notice.info {
+ background-color: var(--color-bg-secondary);
+ border-left-color: var(--color-primary);
+ color: var(--color-text);
+}
+
+/* Better form grid layout */
+.form-grid {
+ display: grid;
+ gap: 1rem;
+}
+
+.form-grid.two-columns {
+ grid-template-columns: 1fr 1fr;
+}
+
+/* Better spacing for form elements */
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-group:last-child {
+ margin-bottom: 0;
+}
+
+.form-label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+.form-label.required::after {
+ content: " *";
+ color: var(--color-error);
}
\ No newline at end of file
diff --git a/src/utils/gitContributions.ts b/src/utils/gitContributions.ts
index 378bc12..163d592 100644
--- a/src/utils/gitContributions.ts
+++ b/src/utils/gitContributions.ts
@@ -1,4 +1,4 @@
-// src/utils/gitContributions.ts - Enhanced for Phase 3
+// src/utils/gitContributions.ts - Enhanced for Phase 3 with YAML preservation
import { execSync, spawn } from 'child_process';
import { promises as fs } from 'fs';
import { load, dump } from 'js-yaml';
@@ -285,144 +285,291 @@ export class GitContributionManager {
}
}
-
-private generateToolYAML(tool: any): string {
- // Clean up the tool object - remove null/undefined values
- const cleanTool: any = {
- name: tool.name,
- type: tool.type,
- description: tool.description,
- domains: tool.domains || [],
- phases: tool.phases || [],
- skillLevel: tool.skillLevel,
- url: tool.url
- };
-
- // Add optional fields only if they have values
- if (tool.icon) cleanTool.icon = tool.icon;
- if (tool.platforms && tool.platforms.length > 0) cleanTool.platforms = tool.platforms;
- if (tool.license) cleanTool.license = tool.license;
- if (tool.accessType) cleanTool.accessType = tool.accessType;
- if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
- if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
- if (tool.related_concepts && tool.related_concepts.length > 0) cleanTool.related_concepts = tool.related_concepts;
- if (tool.tags && tool.tags.length > 0) cleanTool.tags = tool.tags;
-
- // Generate clean YAML
- return dump(cleanTool, {
- lineWidth: -1,
- noRefs: true,
- quotingType: '"',
- forceQuotes: false,
- indent: 2
- });
-}
-
-async submitContribution(data: ContributionData): Promise
{
- const branchName = `tool-${data.type}-${Date.now()}`;
-
- try {
- await this.createBranch(branchName);
-
- const toolsPath = 'src/data/tools.yaml';
- const originalYaml = await this.readFile(toolsPath);
- const yamlData: any = load(originalYaml);
+ /**
+ * CRITICAL FIX: Preserve YAML formatting while updating tools
+ * This prevents the complete rewrite that destroys multiline descriptions
+ */
+ private async preserveYamlFormat(toolsPath: string, newTool: any, isEdit: boolean): Promise {
+ const originalContent = await this.readFile(toolsPath);
+ const yamlData: any = load(originalContent);
if (!yamlData.tools || !Array.isArray(yamlData.tools)) {
throw new Error('Invalid tools.yaml format');
}
- if (data.type === 'edit') {
- yamlData.tools = yamlData.tools.filter((t: any) => (t.name || '').toLowerCase() !== data.tool.name.toLowerCase());
+ if (isEdit) {
+ // Find and replace existing tool
+ const toolIndex = yamlData.tools.findIndex((t: any) =>
+ (t.name || '').toLowerCase() === newTool.name.toLowerCase()
+ );
+
+ if (toolIndex >= 0) {
+ yamlData.tools[toolIndex] = newTool;
+ } else {
+ throw new Error('Tool to edit not found');
+ }
+ } else {
+ // Add new tool - insert alphabetically
+ const insertIndex = yamlData.tools.findIndex((t: any) =>
+ (t.name || '').toLowerCase() > newTool.name.toLowerCase()
+ );
+
+ if (insertIndex >= 0) {
+ yamlData.tools.splice(insertIndex, 0, newTool);
+ } else {
+ yamlData.tools.push(newTool);
+ }
}
- yamlData.tools.push(data.tool);
-
- const newYaml = dump(yamlData, { lineWidth: -1, noRefs: true, quotingType: '"', forceQuotes: false, indent: 2 });
-
- await this.writeFile(toolsPath, newYaml);
-
- const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`;
-
- await this.commitChanges(commitMessage);
-
- await this.pushBranch(branchName);
-
- const prUrl = await this.createPullRequest(
- branchName,
- `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`,
- `Automated contribution for ${data.tool.name}`
- );
-
- return {
- success: true,
- message: `Tool contribution submitted successfully`,
- prUrl,
- branchName
- };
+ // Split original content into sections to preserve formatting
+ const lines = originalContent.split('\n');
+ const toolsStartIndex = lines.findIndex(line => line.trim() === 'tools:');
- } catch (error) {
- // Cleanup on failure
- try {
- await this.deleteBranch(branchName);
- } catch (cleanupError) {
- console.error('Failed to cleanup branch:', cleanupError);
+ if (toolsStartIndex === -1) {
+ // Fallback to full rewrite if structure is unexpected
+ console.warn('Could not find tools section, falling back to full YAML rewrite');
+ return dump(yamlData, {
+ lineWidth: -1,
+ noRefs: true,
+ quotingType: '"',
+ forceQuotes: false,
+ indent: 2
+ });
}
+
+ // Preserve header (everything before tools:)
+ const header = lines.slice(0, toolsStartIndex + 1).join('\n');
- throw error;
+ // Find footer (domains, phases, etc.)
+ const domainsStartIndex = lines.findIndex(line => line.trim() === 'domains:');
+ const footer = domainsStartIndex >= 0 ? '\n' + lines.slice(domainsStartIndex).join('\n') : '';
+
+ // Generate only the tools section with proper formatting
+ const toolsYaml = yamlData.tools.map((tool: any) => {
+ return this.formatToolYaml(tool);
+ }).join('');
+
+ return header + '\n' + toolsYaml + footer;
}
-}
+ /**
+ * Format a single tool entry preserving multiline descriptions
+ */
+ private formatToolYaml(tool: any): string {
+ let toolEntry = ` - name: "${tool.name}"\n`;
+
+ if (tool.icon) toolEntry += ` icon: "${tool.icon}"\n`;
+ toolEntry += ` type: ${tool.type}\n`;
+
+ // PRESERVE multiline description format for longer descriptions
+ if (tool.description && tool.description.length > 80) {
+ toolEntry += ` description: >-\n`;
+ const words: string[] = tool.description.split(' ');
+ const lines: string[] = [];
+ let currentLine: string = '';
+
+ words.forEach((word: string) => {
+ if ((currentLine + ' ' + word).length > 80) {
+ if (currentLine) lines.push(currentLine);
+ currentLine = word;
+ } else {
+ currentLine = currentLine ? currentLine + ' ' + word : word;
+ }
+ });
+ if (currentLine) lines.push(currentLine);
+
+ lines.forEach((line: string) => {
+ toolEntry += ` ${line}\n`;
+ });
+ } else {
+ toolEntry += ` description: "${tool.description}"\n`;
+ }
+
+ // Add array fields
+ if (tool.domains && tool.domains.length > 0) {
+ toolEntry += ` domains:\n`;
+ tool.domains.forEach((domain: string) => {
+ toolEntry += ` - ${domain}\n`;
+ });
+ }
+
+ if (tool.phases && tool.phases.length > 0) {
+ toolEntry += ` phases:\n`;
+ tool.phases.forEach((phase: string) => {
+ toolEntry += ` - ${phase}\n`;
+ });
+ }
+
+ if (tool.platforms && tool.platforms.length > 0) {
+ toolEntry += ` platforms:\n`;
+ tool.platforms.forEach((platform: string) => {
+ toolEntry += ` - ${platform}\n`;
+ });
+ }
+
+ if (tool.related_concepts && tool.related_concepts.length > 0) {
+ toolEntry += ` related_concepts:\n`;
+ tool.related_concepts.forEach((concept: string) => {
+ toolEntry += ` - ${concept}\n`;
+ });
+ }
+
+ if (tool['domain-agnostic-software'] && tool['domain-agnostic-software'].length > 0) {
+ toolEntry += ` domain-agnostic-software:\n`;
+ tool['domain-agnostic-software'].forEach((item: string) => {
+ toolEntry += ` - ${item}\n`;
+ });
+ }
+
+ // Add scalar fields
+ toolEntry += ` skillLevel: ${tool.skillLevel}\n`;
+ if (tool.accessType) toolEntry += ` accessType: ${tool.accessType}\n`;
+ toolEntry += ` url: ${tool.url}\n`;
+ if (tool.projectUrl) toolEntry += ` projectUrl: ${tool.projectUrl}\n`;
+ if (tool.license) toolEntry += ` license: ${tool.license}\n`;
+ if (tool.knowledgebase) toolEntry += ` knowledgebase: ${tool.knowledgebase}\n`;
+
+ if (tool.tags && tool.tags.length > 0) {
+ toolEntry += ` tags:\n`;
+ tool.tags.forEach((tag: string) => {
+ toolEntry += ` - ${tag}\n`;
+ });
+ }
+
+ return toolEntry;
+ }
+
+ private generateToolYAML(tool: any): string {
+ // Clean up the tool object - remove null/undefined values
+ const cleanTool: any = {
+ name: tool.name,
+ type: tool.type,
+ description: tool.description,
+ domains: tool.domains || [],
+ phases: tool.phases || [],
+ skillLevel: tool.skillLevel,
+ url: tool.url
+ };
+
+ // Add optional fields only if they have values
+ if (tool.icon) cleanTool.icon = tool.icon;
+ if (tool.platforms && tool.platforms.length > 0) cleanTool.platforms = tool.platforms;
+ if (tool.license) cleanTool.license = tool.license;
+ if (tool.accessType) cleanTool.accessType = tool.accessType;
+ if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
+ if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
+ if (tool.related_concepts && tool.related_concepts.length > 0) cleanTool.related_concepts = tool.related_concepts;
+ if (tool.tags && tool.tags.length > 0) cleanTool.tags = tool.tags;
+
+ // Generate clean YAML
+ return dump(cleanTool, {
+ lineWidth: -1,
+ noRefs: true,
+ quotingType: '"',
+ forceQuotes: false,
+ indent: 2
+ });
+ }
+
+ async submitContribution(data: ContributionData): Promise {
+ const branchName = `tool-${data.type}-${Date.now()}`;
+
+ try {
+ await this.createBranch(branchName);
+
+ const toolsPath = 'src/data/tools.yaml';
+
+ // CRITICAL FIX: Use format-preserving method instead of dump()
+ const newYaml = await this.preserveYamlFormat(toolsPath, data.tool, data.type === 'edit');
+
+ await this.writeFile(toolsPath, newYaml);
+
+ const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}
+
+Submitted by: ${data.metadata.submitter}
+${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`;
+
+ await this.commitChanges(commitMessage);
+
+ await this.pushBranch(branchName);
+
+ // Generate tool YAML for PR description
+ const toolYaml = this.generateToolYAML(data.tool);
+
+ const prUrl = await this.createPullRequest(
+ branchName,
+ `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`,
+ this.generateEnhancedPRDescription(data, toolYaml)
+ );
+
+ return {
+ success: true,
+ message: `Tool contribution submitted successfully`,
+ prUrl,
+ branchName
+ };
+
+ } catch (error) {
+ // Cleanup on failure
+ try {
+ await this.deleteBranch(branchName);
+ } catch (cleanupError) {
+ console.error('Failed to cleanup branch:', cleanupError);
+ }
+
+ throw error;
+ }
+ }
private generateEnhancedPRDescription(data: ContributionData, toolYaml: string): string {
return `## Tool ${data.type === 'add' ? 'Addition' : 'Update'}: ${data.tool.name}
- **Type:** ${data.tool.type}
- **Submitted by:** ${data.metadata.submitter}
- **Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'}
+**Type:** ${data.tool.type}
+**Submitted by:** ${data.metadata.submitter}
+**Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'}
- ### Tool Details
- - **Name:** ${data.tool.name}
- - **Description:** ${data.tool.description}
- - **URL:** ${data.tool.url}
- - **Skill Level:** ${data.tool.skillLevel}
- ${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''}
- ${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
- ${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''}
- ${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''}
- - **Domains:** ${data.tool.domains.join(', ')}
- - **Phases:** ${data.tool.phases.join(', ')}
- ${data.tool.tags && data.tool.tags.length > 0 ? `- **Tags:** ${data.tool.tags.join(', ')}` : ''}
- ${data.tool.related_concepts && data.tool.related_concepts.length > 0 ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
+### Tool Details
+- **Name:** ${data.tool.name}
+- **Description:** ${data.tool.description}
+- **URL:** ${data.tool.url}
+- **Skill Level:** ${data.tool.skillLevel}
+${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''}
+${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
+${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''}
+${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''}
+- **Domains:** ${data.tool.domains.join(', ')}
+- **Phases:** ${data.tool.phases.join(', ')}
+${data.tool.tags && data.tool.tags.length > 0 ? `- **Tags:** ${data.tool.tags.join(', ')}` : ''}
+${data.tool.related_concepts && data.tool.related_concepts.length > 0 ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
- ${data.metadata.reason ? `### Reason for Contribution
- ${data.metadata.reason}
+${data.metadata.reason ? `### Reason for Contribution
+${data.metadata.reason}
- ` : ''}### Raw Tool Data (Copy & Paste Ready)
+` : ''}### Raw Tool Data (Copy & Paste Ready)
- \`\`\`yaml
- ${toolYaml}\`\`\`
+\`\`\`yaml
+${toolYaml}\`\`\`
- ### For Maintainers
+### For Maintainers
- **To add this tool to tools.yaml:**
- 1. Copy the YAML data above
- 2. ${data.type === 'add' ? 'Add it to the tools array in the appropriate alphabetical position' : 'Replace the existing tool entry with this updated data'}
- 3. Verify all fields are correct
- 4. Test that the tool displays properly
- 5. Close this PR
+**To add this tool to tools.yaml:**
+1. Copy the YAML data above
+2. ${data.type === 'add' ? 'Add it to the tools array in the appropriate alphabetical position' : 'Replace the existing tool entry with this updated data'}
+3. Verify all fields are correct
+4. Test that the tool displays properly
+5. Close this PR
- ### Review Checklist
- - [ ] Tool information is accurate and complete
- - [ ] Description is clear and informative
- - [ ] Domains and phases are correctly assigned
- - [ ] Tags are relevant and consistent with existing tools
- - [ ] License information is correct (for software)
- - [ ] URLs are valid and accessible
- - [ ] No duplicate tool entries
- - [ ] YAML syntax is valid
+### Review Checklist
+- [ ] Tool information is accurate and complete
+- [ ] Description is clear and informative
+- [ ] Domains and phases are correctly assigned
+- [ ] Tags are relevant and consistent with existing tools
+- [ ] License information is correct (for software)
+- [ ] URLs are valid and accessible
+- [ ] No duplicate tool entries
+- [ ] YAML syntax is valid
- ---
- *This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`;
+---
+*This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`;
}
}
\ No newline at end of file