Merge branch 'feature/skill-use-button-os'
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowLeft, Trash2 } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import PaneHeader from './PaneHeader';
|
||||
import type { BasePaneProps } from './types';
|
||||
import SkillCard from '@/components/skills/SkillCard';
|
||||
import SkillMarkdown from '@/components/skills/SkillMarkdown';
|
||||
|
||||
interface SkillMeta {
|
||||
name: string;
|
||||
@@ -27,12 +27,13 @@ export default function SkillsPane({
|
||||
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
const detailScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkills();
|
||||
void fetchSkills();
|
||||
|
||||
const handleSkillUpdated = () => {
|
||||
fetchSkills();
|
||||
void fetchSkills();
|
||||
};
|
||||
window.addEventListener('skills: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 () => {
|
||||
try {
|
||||
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();
|
||||
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 data = await res.json();
|
||||
if (data.success) {
|
||||
fetchSkills();
|
||||
await fetchSkills();
|
||||
if (selectedSkill?.name === name) {
|
||||
setSelectedSkill(null);
|
||||
}
|
||||
@@ -104,6 +111,7 @@ export default function SkillsPane({
|
||||
{selectedSkill ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedSkill(null)}
|
||||
style={{
|
||||
background: 'none',
|
||||
@@ -115,12 +123,6 @@ export default function SkillsPane({
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.color = '#ccc';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.color = '#888';
|
||||
}}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
@@ -131,12 +133,17 @@ export default function SkillsPane({
|
||||
)}
|
||||
</PaneHeader>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px' }}>
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px' }} ref={detailScrollRef}>
|
||||
{loading ? (
|
||||
<div style={{ color: '#555', fontSize: '13px', textAlign: 'center', paddingTop: '24px' }}>Loading...</div>
|
||||
) : selectedSkill ? (
|
||||
<div className="skill-content" style={{ color: '#ccc', fontSize: '13px', lineHeight: '1.6' }}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{selectedSkill.content}</ReactMarkdown>
|
||||
<div>
|
||||
<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 style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
@@ -146,69 +153,13 @@ export default function SkillsPane({
|
||||
</div>
|
||||
) : (
|
||||
skills.map((skill) => (
|
||||
<button
|
||||
<SkillCard
|
||||
key={skill.name}
|
||||
onClick={() => handleSelectSkill(skill.name)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
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>
|
||||
skill={skill}
|
||||
onSelect={handleSelectSkill}
|
||||
onDelete={handleDeleteSkill}
|
||||
deleting={deleting}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user