Merge branch 'feature/skill-use-button-os'

This commit is contained in:
“BeeRad”
2026-03-15 16:24:03 +11:00
3 changed files with 227 additions and 79 deletions
+30 -79
View File
@@ -1,11 +1,11 @@
"use client"; "use client";
import { useState, useEffect } from 'react'; import { useEffect, useRef, useState } from 'react';
import { ArrowLeft, Trash2 } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import PaneHeader from './PaneHeader'; import PaneHeader from './PaneHeader';
import type { BasePaneProps } from './types'; import type { BasePaneProps } from './types';
import SkillCard from '@/components/skills/SkillCard';
import SkillMarkdown from '@/components/skills/SkillMarkdown';
interface SkillMeta { interface SkillMeta {
name: string; name: string;
@@ -27,12 +27,13 @@ export default function SkillsPane({
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null); const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [deleting, setDeleting] = useState<string | null>(null); const [deleting, setDeleting] = useState<string | null>(null);
const detailScrollRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
fetchSkills(); void fetchSkills();
const handleSkillUpdated = () => { const handleSkillUpdated = () => {
fetchSkills(); void fetchSkills();
}; };
window.addEventListener('skills:updated', handleSkillUpdated); window.addEventListener('skills:updated', handleSkillUpdated);
window.addEventListener('guides:updated', handleSkillUpdated); window.addEventListener('guides:updated', handleSkillUpdated);
@@ -43,6 +44,12 @@ export default function SkillsPane({
}; };
}, []); }, []);
useEffect(() => {
if (selectedSkill && detailScrollRef.current) {
detailScrollRef.current.scrollTo({ top: 0, behavior: 'auto' });
}
}, [selectedSkill?.name]);
const fetchSkills = async () => { const fetchSkills = async () => {
try { try {
const res = await fetch('/api/skills'); const res = await fetch('/api/skills');
@@ -69,7 +76,7 @@ export default function SkillsPane({
} }
}; };
const handleDeleteSkill = async (name: string, e: React.MouseEvent) => { const handleDeleteSkill = async (name: string, e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
if (!confirm(`Delete skill "${name}"?`)) return; if (!confirm(`Delete skill "${name}"?`)) return;
@@ -78,7 +85,7 @@ export default function SkillsPane({
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' }); const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
fetchSkills(); await fetchSkills();
if (selectedSkill?.name === name) { if (selectedSkill?.name === name) {
setSelectedSkill(null); setSelectedSkill(null);
} }
@@ -104,6 +111,7 @@ export default function SkillsPane({
{selectedSkill ? ( {selectedSkill ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button <button
type="button"
onClick={() => setSelectedSkill(null)} onClick={() => setSelectedSkill(null)}
style={{ style={{
background: 'none', background: 'none',
@@ -115,12 +123,6 @@ export default function SkillsPane({
padding: '4px', padding: '4px',
borderRadius: '4px', borderRadius: '4px',
}} }}
onMouseEnter={e => {
e.currentTarget.style.color = '#ccc';
}}
onMouseLeave={e => {
e.currentTarget.style.color = '#888';
}}
> >
<ArrowLeft size={16} /> <ArrowLeft size={16} />
</button> </button>
@@ -131,12 +133,17 @@ export default function SkillsPane({
)} )}
</PaneHeader> </PaneHeader>
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px' }}> <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px' }} ref={detailScrollRef}>
{loading ? ( {loading ? (
<div style={{ color: '#555', fontSize: '13px', textAlign: 'center', paddingTop: '24px' }}>Loading...</div> <div style={{ color: '#555', fontSize: '13px', textAlign: 'center', paddingTop: '24px' }}>Loading...</div>
) : selectedSkill ? ( ) : selectedSkill ? (
<div className="skill-content" style={{ color: '#ccc', fontSize: '13px', lineHeight: '1.6' }}> <div>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedSkill.content}</ReactMarkdown> <div style={{ marginBottom: '12px' }}>
<div style={{ color: '#eee', fontSize: '16px', fontWeight: 600 }}>{selectedSkill.name}</div>
<div style={{ color: '#888', fontSize: '13px', lineHeight: 1.4, marginTop: '6px' }}>{selectedSkill.description}</div>
</div>
<div style={{ borderTop: '1px solid #222', margin: '12px 0' }} />
<SkillMarkdown content={selectedSkill.content} />
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
@@ -146,69 +153,13 @@ export default function SkillsPane({
</div> </div>
) : ( ) : (
skills.map((skill) => ( skills.map((skill) => (
<button <SkillCard
key={skill.name} key={skill.name}
onClick={() => handleSelectSkill(skill.name)} skill={skill}
style={{ onSelect={handleSelectSkill}
display: 'flex', onDelete={handleDeleteSkill}
alignItems: 'center', deleting={deleting}
gap: '8px', />
padding: '12px',
background: '#161616',
border: '1px solid #222',
borderRadius: '8px',
cursor: 'pointer',
textAlign: 'left',
transition: 'all 0.15s ease',
}}
onMouseEnter={e => {
e.currentTarget.style.background = '#1a1a1a';
e.currentTarget.style.borderColor = '#333';
}}
onMouseLeave={e => {
e.currentTarget.style.background = '#161616';
e.currentTarget.style.borderColor = '#222';
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<span style={{ color: '#ddd', fontSize: '13px', fontWeight: 500 }}>{skill.name}</span>
<span
style={{
color: '#777',
fontSize: '12px',
lineHeight: '1.4',
display: 'block',
marginTop: '2px',
}}
>
{skill.description}
</span>
</div>
<button
onClick={e => handleDeleteSkill(skill.name, e)}
disabled={deleting === skill.name}
style={{
background: 'none',
border: 'none',
color: '#555',
cursor: 'pointer',
padding: '4px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
flexShrink: 0,
opacity: deleting === skill.name ? 0.3 : 1,
}}
onMouseEnter={e => {
e.currentTarget.style.color = '#ef4444';
}}
onMouseLeave={e => {
e.currentTarget.style.color = '#555';
}}
>
<Trash2 size={14} />
</button>
</button>
)) ))
)} )}
</div> </div>
+100
View File
@@ -0,0 +1,100 @@
"use client";
import { Trash2 } from 'lucide-react';
interface SkillMeta {
name: string;
description: string;
immutable: boolean;
}
interface SkillCardProps {
skill: SkillMeta;
onSelect: (name: string) => void;
onDelete?: (name: string, e: React.MouseEvent<HTMLButtonElement>) => void;
deleting?: string | null;
}
export default function SkillCard({
skill,
onSelect,
onDelete,
deleting = null,
}: SkillCardProps) {
const isDeleting = deleting === skill.name;
return (
<div
onMouseEnter={(e) => {
e.currentTarget.style.background = '#1a1a1a';
e.currentTarget.style.borderColor = '#383838';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#161616';
e.currentTarget.style.borderColor = '#222';
}}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px',
background: '#161616',
border: '1px solid #222',
borderRadius: '8px',
}}
>
<button
type="button"
onClick={() => onSelect(skill.name)}
style={{
flex: 1,
minWidth: 0,
background: 'transparent',
border: 'none',
cursor: 'pointer',
textAlign: 'left',
padding: 0,
}}
>
<span style={{ color: '#ddd', fontSize: '13px', fontWeight: 500, display: 'block' }}>{skill.name}</span>
<span
style={{
color: '#777',
fontSize: '12px',
lineHeight: '1.4',
display: 'block',
marginTop: '2px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={skill.description}
>
{skill.description}
</span>
</button>
{onDelete && !skill.immutable && (
<button
type="button"
onClick={(e) => onDelete(skill.name, e)}
disabled={isDeleting}
style={{
background: 'transparent',
border: 'none',
color: '#555',
cursor: isDeleting ? 'default' : 'pointer',
padding: '4px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
opacity: isDeleting ? 0.3 : 1,
}}
>
<Trash2 size={14} />
</button>
)}
</div>
);
}
+97
View File
@@ -0,0 +1,97 @@
"use client";
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
export default function SkillMarkdown({ content }: { content: string }) {
return (
<div className="skill-content" style={{ color: '#ccc', fontSize: '13px', lineHeight: '1.6' }}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
h1: ({ children }) => (
<h1 style={{ fontSize: '18px', fontWeight: 600, color: '#eee', margin: '0 0 16px 0' }}>{children}</h1>
),
h2: ({ children }) => (
<h2 style={{ fontSize: '15px', fontWeight: 600, color: '#ddd', margin: '20px 0 8px 0' }}>{children}</h2>
),
h3: ({ children }) => (
<h3 style={{ fontSize: '14px', fontWeight: 600, color: '#ccc', margin: '16px 0 6px 0' }}>{children}</h3>
),
p: ({ children }) => (
<p style={{ margin: '0 0 12px 0' }}>{children}</p>
),
ul: ({ children }) => (
<ul style={{ margin: '0 0 12px 0', paddingLeft: '20px' }}>{children}</ul>
),
ol: ({ children }) => (
<ol style={{ margin: '0 0 12px 0', paddingLeft: '20px' }}>{children}</ol>
),
li: ({ children }) => (
<li style={{ margin: '0 0 4px 0' }}>{children}</li>
),
code: ({ className, children, ...props }) => {
const isInline = !className;
if (isInline) {
return (
<code
style={{
background: '#1a1a1a',
padding: '2px 6px',
borderRadius: '4px',
fontSize: '12px',
color: '#22c55e',
}}
{...props}
>
{children}
</code>
);
}
return (
<code
style={{
display: 'block',
background: '#0d0d0d',
padding: '12px',
borderRadius: '6px',
fontSize: '12px',
overflowX: 'auto',
margin: '0 0 12px 0',
color: '#aaa',
whiteSpace: 'pre-wrap',
}}
{...props}
>
{children}
</code>
);
},
pre: ({ children }) => (
<pre style={{ margin: '0 0 12px 0' }}>{children}</pre>
),
strong: ({ children }) => (
<strong style={{ color: '#eee', fontWeight: 600 }}>{children}</strong>
),
hr: () => (
<hr style={{ border: 'none', borderTop: '1px solid #2a2a2a', margin: '16px 0' }} />
),
blockquote: ({ children }) => (
<blockquote
style={{
borderLeft: '3px solid #333',
paddingLeft: '12px',
margin: '0 0 12px 0',
color: '#999',
}}
>
{children}
</blockquote>
),
}}
>
{content}
</ReactMarkdown>
</div>
);
}