Function bodies 254 total
Delete method · go · L102-L112 (11 LOC)backend/internal/storage/sqlite/webhook_repo.go
func (r *webhookRepo) Delete(ctx context.Context, id string) error {
res, err := r.db.ExecContext(ctx, `DELETE FROM webhooks WHERE id = ?`, id)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return storage.ErrNotFound
}
return nil
}getByID method · go · L114-L118 (5 LOC)backend/internal/storage/sqlite/webhook_repo.go
func (r *webhookRepo) getByID(ctx context.Context, id string) (domain.Webhook, error) {
row := r.db.QueryRowContext(ctx,
`SELECT id, board_id, name, url, events, enabled, created_at FROM webhooks WHERE id = ?`, id)
return scanWebhook(row)
}scanWebhook function · go · L120-L138 (19 LOC)backend/internal/storage/sqlite/webhook_repo.go
func scanWebhook(row interface{ Scan(...any) error }) (domain.Webhook, error) {
var wh domain.Webhook
var evts string
var enabled int
err := row.Scan(&wh.ID, &wh.BoardID, &wh.Name, &wh.URL, &evts, &enabled, &wh.CreatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.Webhook{}, storage.ErrNotFound
}
return domain.Webhook{}, err
}
_ = json.Unmarshal([]byte(evts), &wh.Events)
if wh.Events == nil {
wh.Events = []domain.WebhookEvent{}
}
wh.Enabled = enabled != 0
wh.CreatedAt = wh.CreatedAt.UTC()
return wh, nil
}scanWebhooks function · go · L140-L150 (11 LOC)backend/internal/storage/sqlite/webhook_repo.go
func scanWebhooks(rows *sql.Rows) ([]domain.Webhook, error) {
var out []domain.Webhook
for rows.Next() {
wh, err := scanWebhook(rows)
if err != nil {
return nil, err
}
out = append(out, wh)
}
return out, rows.Err()
}Dist function · go · L12-L14 (3 LOC)backend/web/embed_dev.go
func Dist() (fs.FS, error) {
return nil, errors.New("web.Dist: dev build does not embed frontend (use Vite dev server on :5173)")
}Dist function · go · L14-L16 (3 LOC)backend/web/embed_prod.go
func Dist() (fs.FS, error) {
return fs.Sub(embeddedDist, "dist")
}enableAxe function · typescript · L4-L10 (7 LOC)frontend/src/a11y/axe.ts
export async function enableAxe() {
if (!import.meta.env.DEV) return;
const React = await import('react');
const ReactDOM = await import('react-dom');
const axe = (await import('@axe-core/react')).default;
axe(React, ReactDOM, 1000);
}Same scanner, your repo: https://repobility.com — Repobility
axe function · typescript · L4-L20 (17 LOC)frontend/src/a11y/jest-axe-lite.ts
export async function axe(container: Element): Promise<AxeResults> {
return new Promise<AxeResults>((resolve, reject) => {
axeCore.run(
container,
{
rules: {
// jsdom 에서 색상 대비 검사 신뢰할 수 없음; dev 런타임 axe가 별도 감사.
'color-contrast': { enabled: false },
},
},
(err, results) => {
if (err) reject(err);
else resolve(results);
},
);
});
}request function · typescript · L9-L28 (20 LOC)frontend/src/api/client.ts
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(path, {
method,
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const err: ApiError = { status: res.status };
try {
const parsed = (await res.json()) as { error?: { code?: string; message?: string } };
err.code = parsed.error?.code;
err.message = parsed.error?.message;
} catch {
/* ignore */
}
throw err;
}
if (res.status === 204) return undefined as T;
return (await res.json()) as T;
}useBoards function · typescript · L14-L20 (7 LOC)frontend/src/api/hooks.ts
export function useBoards(options?: Partial<UseQueryOptions<Board[]>>) {
return useQuery<Board[]>({
queryKey: boardsKey,
queryFn: () => api.get<Board[]>('/api/boards'),
...options,
});
}useCreateBoard function · typescript · L22-L31 (10 LOC)frontend/src/api/hooks.ts
export function useCreateBoard() {
const qc = useQueryClient();
return useMutation({
mutationFn: (p: { name: string; viewType?: 'kanban' | 'list' }) =>
api.post<Board>('/api/boards', p),
onSuccess: () => {
qc.invalidateQueries({ queryKey: boardsKey });
},
});
}useDeleteBoard function · typescript · L33-L41 (9 LOC)frontend/src/api/hooks.ts
export function useDeleteBoard() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.del<void>(`/api/boards/${id}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: boardsKey });
},
});
}issuesKey function · typescript · L45-L51 (7 LOC)frontend/src/api/hooks.ts
export function issuesKey(filter?: {
boardId?: string;
status?: IssueStatus;
parentId?: string | null;
}) {
return ['issues', filter ?? {}] as const;
}useIssues function · typescript · L53-L67 (15 LOC)frontend/src/api/hooks.ts
export function useIssues(filter?: {
boardId?: string;
status?: IssueStatus;
parentId?: string | null;
}) {
const qs = new URLSearchParams();
if (filter?.boardId) qs.set('board_id', filter.boardId);
if (filter?.status) qs.set('status', filter.status);
if (filter?.parentId !== undefined)
qs.set('parent_id', filter.parentId === null ? '' : filter.parentId);
return useQuery<Issue[]>({
queryKey: issuesKey(filter),
queryFn: () => api.get<Issue[]>(`/api/issues?${qs.toString()}`),
});
}useIssue function · typescript · L69-L75 (7 LOC)frontend/src/api/hooks.ts
export function useIssue(id: string | undefined) {
return useQuery<{ issue: Issue; children: Issue[] }>({
queryKey: ['issue', id],
queryFn: () => api.get(`/api/issues/${id}`),
enabled: !!id,
});
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
useCreateIssue function · typescript · L77-L87 (11 LOC)frontend/src/api/hooks.ts
export function useCreateIssue() {
const qc = useQueryClient();
return useMutation({
mutationFn: (p: { boardId: string; title: string; body?: string; parentId?: string }) =>
api.post<Issue>('/api/issues', p),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['issues'] });
qc.invalidateQueries({ queryKey: ['calendar'] });
},
});
}useUpdateIssue function · typescript · L89-L107 (19 LOC)frontend/src/api/hooks.ts
export function useUpdateIssue() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({
id,
patch,
}: {
id: string;
patch: Partial<Pick<Issue, 'title' | 'body' | 'instructions' | 'status' | 'criteria'>> & {
parentId?: string | null;
};
}) => api.patch<Issue>(`/api/issues/${id}`, patch),
onSuccess: (_data, vars) => {
qc.invalidateQueries({ queryKey: ['issues'] });
qc.invalidateQueries({ queryKey: ['issue', vars.id] });
qc.invalidateQueries({ queryKey: ['calendar'] });
},
});
}useApproveIssue function · typescript · L109-L119 (11 LOC)frontend/src/api/hooks.ts
export function useApproveIssue() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.post<Issue>(`/api/issues/${id}/approve`),
onSuccess: (_data, id) => {
qc.invalidateQueries({ queryKey: ['issues'] });
qc.invalidateQueries({ queryKey: ['issue', id] });
qc.invalidateQueries({ queryKey: ['calendar'] });
},
});
}useDeleteIssue function · typescript · L121-L130 (10 LOC)frontend/src/api/hooks.ts
export function useDeleteIssue() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.del<void>(`/api/issues/${id}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['issues'] });
qc.invalidateQueries({ queryKey: ['calendar'] });
},
});
}useBoardProperties function · typescript · L134-L140 (7 LOC)frontend/src/api/hooks.ts
export function useBoardProperties(boardId: string | undefined) {
return useQuery<BoardProperty[]>({
queryKey: ['boardProperties', boardId],
queryFn: () => api.get<BoardProperty[]>(`/api/boards/${boardId}/properties`),
enabled: !!boardId,
});
}useCreateBoardProperty function · typescript · L142-L151 (10 LOC)frontend/src/api/hooks.ts
export function useCreateBoardProperty() {
const qc = useQueryClient();
return useMutation({
mutationFn: (p: { boardId: string; name: string; type: PropertyType; options?: string[] }) =>
api.post<BoardProperty>(`/api/boards/${p.boardId}/properties`, p),
onSuccess: (_d, v) => {
qc.invalidateQueries({ queryKey: ['boardProperties', v.boardId] });
},
});
}useDeleteBoardProperty function · typescript · L153-L162 (10 LOC)frontend/src/api/hooks.ts
export function useDeleteBoardProperty() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ boardId, propId }: { boardId: string; propId: string }) =>
api.del<void>(`/api/boards/${boardId}/properties/${propId}`),
onSuccess: (_d, v) => {
qc.invalidateQueries({ queryKey: ['boardProperties', v.boardId] });
},
});
}useUpdateIssueProperties function · typescript · L164-L174 (11 LOC)frontend/src/api/hooks.ts
export function useUpdateIssueProperties() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, properties }: { id: string; properties: Record<string, unknown> }) =>
api.patch<Issue>(`/api/issues/${id}`, { properties }),
onSuccess: (_d, v) => {
qc.invalidateQueries({ queryKey: ['issue', v.id] });
qc.invalidateQueries({ queryKey: ['issues'] });
},
});
}Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
useWebhooks function · typescript · L178-L184 (7 LOC)frontend/src/api/hooks.ts
export function useWebhooks(boardId: string | undefined) {
return useQuery<Webhook[]>({
queryKey: ['webhooks', boardId],
queryFn: () => api.get<Webhook[]>(`/api/boards/${boardId}/webhooks`),
enabled: !!boardId,
});
}useCreateWebhook function · typescript · L186-L195 (10 LOC)frontend/src/api/hooks.ts
export function useCreateWebhook() {
const qc = useQueryClient();
return useMutation({
mutationFn: (p: { boardId: string; name: string; url: string; events: WebhookEvent[] }) =>
api.post<Webhook>(`/api/boards/${p.boardId}/webhooks`, p),
onSuccess: (_d, v) => {
qc.invalidateQueries({ queryKey: ['webhooks', v.boardId] });
},
});
}useDeleteWebhook function · typescript · L197-L206 (10 LOC)frontend/src/api/hooks.ts
export function useDeleteWebhook() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ boardId, whId }: { boardId: string; whId: string }) =>
api.del<void>(`/api/boards/${boardId}/webhooks/${whId}`),
onSuccess: (_d, v) => {
qc.invalidateQueries({ queryKey: ['webhooks', v.boardId] });
},
});
}useMonthStats function · typescript · L210-L215 (6 LOC)frontend/src/api/hooks.ts
export function useMonthStats(year: number, month: number) {
return useQuery<DayCount[]>({
queryKey: ['calendar', 'month', year, month],
queryFn: () => api.get<DayCount[]>(`/api/calendar?year=${year}&month=${month}`),
});
}useDayStats function · typescript · L217-L223 (7 LOC)frontend/src/api/hooks.ts
export function useDayStats(date: string | undefined) {
return useQuery<DayStats>({
queryKey: ['calendar', 'day', date],
queryFn: () => api.get<DayStats>(`/api/calendar/day?date=${date}`),
enabled: !!date,
});
}useAddDependency function · typescript · L227-L236 (10 LOC)frontend/src/api/hooks.ts
export function useAddDependency() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ issueId, blockerId }: { issueId: string; blockerId: string }) =>
api.post<void>(`/api/issues/${issueId}/dependencies`, { blockerId }),
onSuccess: (_data, vars) => {
qc.invalidateQueries({ queryKey: ['issue', vars.issueId] });
},
});
}useRemoveDependency function · typescript · L238-L247 (10 LOC)frontend/src/api/hooks.ts
export function useRemoveDependency() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ issueId, blockerId }: { issueId: string; blockerId: string }) =>
api.del<void>(`/api/issues/${issueId}/dependencies/${blockerId}`),
onSuccess: (_data, vars) => {
qc.invalidateQueries({ queryKey: ['issue', vars.issueId] });
},
});
}useReorderIssues function · typescript · L251-L260 (10 LOC)frontend/src/api/hooks.ts
export function useReorderIssues() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ boardId, issueIds }: { boardId: string; issueIds: string[] }) =>
api.post<void>(`/api/boards/${boardId}/issues/reorder`, { issueIds }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['issues'] });
},
});
}Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
useComments function · typescript · L264-L270 (7 LOC)frontend/src/api/hooks.ts
export function useComments(issueId: string | undefined) {
return useQuery<Comment[]>({
queryKey: ['comments', issueId],
queryFn: () => api.get<Comment[]>(`/api/issues/${issueId}/comments`),
enabled: !!issueId,
});
}useCreateComment function · typescript · L272-L281 (10 LOC)frontend/src/api/hooks.ts
export function useCreateComment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ issueId, body }: { issueId: string; body: string }) =>
api.post<Comment>(`/api/issues/${issueId}/comments`, { body }),
onSuccess: (_data, vars) => {
qc.invalidateQueries({ queryKey: ['comments', vars.issueId] });
},
});
}useDeleteComment function · typescript · L283-L292 (10 LOC)frontend/src/api/hooks.ts
export function useDeleteComment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ commentId, issueId: _issueId }: { issueId: string; commentId: string }) =>
api.del<void>(`/api/comments/${commentId}`),
onSuccess: (_data, vars) => {
qc.invalidateQueries({ queryKey: ['comments', vars.issueId] });
},
});
}App function · typescript · L10-L24 (15 LOC)frontend/src/App.tsx
export default function App() {
return (
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/boards" element={<BoardsListPage />} />
<Route path="/boards/:boardId" element={<BoardPage />} />
<Route path="/issues/:issueId" element={<IssuePage />} />
<Route path="/calendar" element={<CalendarPage />} />
<Route path="/approve" element={<ApprovalPage />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}NotFound function · typescript · L26-L33 (8 LOC)frontend/src/App.tsx
function NotFound() {
return (
<section>
<h1 className="text-3xl font-bold">찾을 수 없습니다</h1>
<p className="mt-2 text-ink-secondary">요청하신 경로가 존재하지 않습니다.</p>
</section>
);
}Breadcrumb function · typescript · L6-L24 (19 LOC)frontend/src/components/Breadcrumb.tsx
export function Breadcrumb({ items }: { items: Crumb[] }) {
const { t } = useTranslation();
return (
<nav aria-label={t('common.location')} className="flex items-center gap-1 text-sm text-ink-muted">
{items.map((item, i) => (
<span key={i} className="flex items-center gap-1">
{i > 0 && <span aria-hidden>/</span>}
{item.to ? (
<Link to={item.to} className="hover:text-ink-primary hover:underline">
{item.label}
</Link>
) : (
<span className="text-ink-secondary">{item.label}</span>
)}
</span>
))}
</nav>
);
}Button function · typescript · L30-L38 (9 LOC)frontend/src/components/Button.tsx
function Button({ variant = 'primary', size = 'md', className = '', ...rest }, ref) {
return (
<button
ref={ref}
{...rest}
className={`${base} ${variants[variant]} ${sizes[size]} ${className}`}
/>
);
},Card function · typescript · L7-L16 (10 LOC)frontend/src/components/Card.tsx
export function Card({ className = '', children, ...rest }: Props) {
return (
<div
{...rest}
className={`rounded-xl border border-edge-base bg-surface-subtle p-4 shadow-sm transition-all duration-200 hover:shadow-md hover:border-brand-500/40 ${className}`}
>
{children}
</div>
);
}Same scanner, your repo: https://repobility.com — Repobility
relativeTime function · typescript · L8-L17 (10 LOC)frontend/src/components/CommentSection.tsx
function relativeTime(dateStr: string): string {
const now = Date.now();
const then = new Date(dateStr).getTime();
const diff = Math.floor((now - then) / 1000);
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
return new Date(dateStr).toLocaleDateString();
}CommentSection function · typescript · L23-L107 (85 LOC)frontend/src/components/CommentSection.tsx
export function CommentSection({ issueId }: Props) {
const { t } = useTranslation();
const { data: comments, isLoading } = useComments(issueId);
const createComment = useCreateComment();
const deleteComment = useDeleteComment();
const [body, setBody] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const trimmed = body.trim();
if (!trimmed) return;
await createComment.mutateAsync({ issueId, body: trimmed });
setBody('');
}
return (
<section aria-label={t('comments.section')} className="flex flex-col gap-4">
<h3 className="flex items-center gap-2 text-sm font-semibold text-ink-secondary">
<Activity size={14} />
{t('comments.title')}
{comments && comments.length > 0 && (
<span className="rounded-full bg-surface-muted px-1.5 py-0.5 text-[10px] tabular-nums text-ink-muted">{comments.length}</span>
)}
</h3>
{isLoading && (
<div className="flex fhandleSubmit function · typescript · L30-L36 (7 LOC)frontend/src/components/CommentSection.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const trimmed = body.trim();
if (!trimmed) return;
await createComment.mutateAsync({ issueId, body: trimmed });
setBody('');
}ConfirmDialog function · typescript · L16-L80 (65 LOC)frontend/src/components/ConfirmDialog.tsx
export function ConfirmDialog({
open,
title,
description,
confirmLabel,
onConfirm,
onCancel,
variant = 'danger',
}: Props) {
const { t } = useTranslation();
const cancelRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
cancelRef.current?.focus();
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onCancel();
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [open, onCancel]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/40" onClick={onCancel} aria-hidden />
<div
role="alertdialog"
aria-modal="true"
aria-labelledby="confirm-title"
aria-describedby="confirm-desc"
className="relative z-10 flex w-full max-w-sm flex-col gap-4 rounded-xl border border-edge-base bg-surface-bContextMenu function · typescript · L17-L65 (49 LOC)frontend/src/components/ContextMenu.tsx
export function ContextMenu({ items, position, onClose }: Props) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!position) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
};
const keyHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('mousedown', handler);
document.addEventListener('keydown', keyHandler);
return () => {
document.removeEventListener('mousedown', handler);
document.removeEventListener('keydown', keyHandler);
};
}, [position, onClose]);
if (!position) return null;
return (
<div
ref={ref}
className="fixed z-50 min-w-[160px] rounded-lg border border-edge-base bg-surface-base py-1 shadow-lg animate-in"
style={{ left: position.x, top: position.y }}
>
{items.map((item, i) => (
item.divider ? (
<hr key={i} className="my-1 borInput function · typescript · L8-L32 (25 LOC)frontend/src/components/Input.tsx
export function Input({ label, error, id, className = '', ...rest }: Props) {
const generatedId = useId();
const inputId = id ?? generatedId;
return (
<div className="flex flex-col gap-1">
{label && (
<label htmlFor={inputId} className="text-sm font-medium text-ink-secondary">
{label}
</label>
)}
<input
id={inputId}
aria-invalid={!!error}
aria-describedby={error ? `${inputId}-err` : undefined}
{...rest}
className={`h-10 rounded-md border border-edge-base bg-surface-base px-3 text-ink-primary transition-colors focus-visible:border-brand-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/40 ${className}`}
/>
{error && (
<span id={`${inputId}-err`} className="text-xs text-red-600">
{error}
</span>
)}
</div>
);
}LanguageSwitcher function · typescript · L6-L50 (45 LOC)frontend/src/components/LanguageSwitcher.tsx
export function LanguageSwitcher() {
const { i18n } = useTranslation();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);
return (
<div className="relative" ref={ref}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-edge-base text-ink-secondary transition-all duration-200 hover:bg-surface-muted hover:text-brand-500 active:scale-95"
aria-label="Language"
>
<Globe size={18} />
</button>
{open && (
<div className="absolute left-0 bottom-full mb-1 min-wLayout function · typescript · L196-L314 (119 LOC)frontend/src/components/Layout.tsx
export default function Layout() {
const { t } = useTranslation();
const navigate = useNavigate();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [collapsed, setCollapsed] = useState(() => localStorage.getItem('sidebar-collapsed') === 'true');
useEffect(() => {
localStorage.setItem('sidebar-collapsed', String(collapsed));
}, [collapsed]);
useEffect(() => {
if (!sidebarOpen) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') setSidebarOpen(false);
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [sidebarOpen]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const tag = (e.target as HTMLElement)?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
if ((e.target as HTMLElement)?.isContentEditable) return;
if (e.key === 'a' && !e.metaKey && !e.ctrlKey) {
e.preventDRepobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
MarkdownEditor function · typescript · L12-L61 (50 LOC)frontend/src/components/MarkdownEditor.tsx
export function MarkdownEditor({ value, onChange, label }: Props) {
const { t } = useTranslation();
const [tab, setTab] = useState<'edit' | 'preview' | 'split'>('split');
const resolvedLabel = label ?? t('issue.body');
return (
<section aria-label={t('issue.body')} className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-ink-secondary">{resolvedLabel}</span>
<div role="tablist" className="flex gap-1 rounded-md bg-surface-muted p-0.5 text-xs">
{(['edit', 'split', 'preview'] as const).map((tab_) => (
<button
key={tab_}
role="tab"
aria-selected={tab === tab_}
onClick={() => setTab(tab_)}
className={`rounded px-2 py-1 transition-colors ${
tab === tab_ ? 'bg-surface-base text-ink-primary shadow-sm' : 'text-ink-secondary hover:text-ink-primary'
}`}
>
PropertyEditor function · typescript · L17-L31 (15 LOC)frontend/src/components/PropertyEditor.tsx
export function PropertyEditor({ properties, values, onChange }: Props) {
const { t } = useTranslation();
if (properties.length === 0) return null;
return (
<section className="flex flex-col gap-3 rounded-xl border border-edge-base bg-surface-subtle p-4">
<h3 className="text-sm font-semibold text-ink-secondary">{t('issue.properties')}</h3>
<div className="flex flex-col gap-2.5">
{properties.map((prop) => (
<PropertyField key={prop.id} prop={prop} value={values[prop.id]} onChange={(v) => onChange(prop.id, v)} />
))}
</div>
</section>
);
}PropertyField function · typescript · L33-L136 (104 LOC)frontend/src/components/PropertyEditor.tsx
function PropertyField({ prop, value, onChange }: { prop: BoardProperty; value: unknown; onChange: (v: unknown) => void }) {
const { t } = useTranslation();
const Icon = typeIcons[prop.type];
const [localText, setLocalText] = useState((value as string) ?? '');
const [localNumber, setLocalNumber] = useState((value as string) ?? '');
useEffect(() => {
setLocalText((value as string) ?? '');
}, [value]);
useEffect(() => {
setLocalNumber((value as string) ?? '');
}, [value]);
useEffect(() => {
const timer = setTimeout(() => {
if (localText !== (value ?? '')) {
onChange(localText || null);
}
}, 300);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localText]);
useEffect(() => {
const timer = setTimeout(() => {
const numVal = localNumber !== '' ? Number(localNumber) : null;
const prevVal = value !== undefined && value !== null ? String(value) : '';
if (local