hazo_admin 0.7.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGE_LOG.md +60 -0
  2. package/README.md +65 -4
  3. package/SETUP_CHECKLIST.md +24 -0
  4. package/dist/api/index.d.ts +6 -1
  5. package/dist/api/index.d.ts.map +1 -1
  6. package/dist/api/index.js +166 -2
  7. package/dist/components/issues_panel/board_columns.d.ts +17 -0
  8. package/dist/components/issues_panel/board_columns.d.ts.map +1 -0
  9. package/dist/components/issues_panel/board_columns.js +37 -0
  10. package/dist/components/issues_panel/card_assignee_control.d.ts +17 -0
  11. package/dist/components/issues_panel/card_assignee_control.d.ts.map +1 -0
  12. package/dist/components/issues_panel/card_assignee_control.js +51 -0
  13. package/dist/components/issues_panel/card_type_control.d.ts +18 -0
  14. package/dist/components/issues_panel/card_type_control.d.ts.map +1 -0
  15. package/dist/components/issues_panel/card_type_control.js +42 -0
  16. package/dist/components/issues_panel/facet_sidebar.d.ts +25 -0
  17. package/dist/components/issues_panel/facet_sidebar.d.ts.map +1 -0
  18. package/dist/components/issues_panel/facet_sidebar.js +72 -0
  19. package/dist/components/issues_panel/facet_topbar.d.ts +20 -0
  20. package/dist/components/issues_panel/facet_topbar.d.ts.map +1 -0
  21. package/dist/components/issues_panel/facet_topbar.js +42 -0
  22. package/dist/components/issues_panel/filter.d.ts +12 -0
  23. package/dist/components/issues_panel/filter.d.ts.map +1 -0
  24. package/dist/components/issues_panel/filter.js +41 -0
  25. package/dist/components/issues_panel/index.d.ts.map +1 -1
  26. package/dist/components/issues_panel/index.js +145 -43
  27. package/dist/components/issues_panel/manage_types_dialog.d.ts +28 -0
  28. package/dist/components/issues_panel/manage_types_dialog.d.ts.map +1 -0
  29. package/dist/components/issues_panel/manage_types_dialog.js +84 -0
  30. package/dist/components/issues_panel/ui_helpers.d.ts +21 -0
  31. package/dist/components/issues_panel/ui_helpers.d.ts.map +1 -0
  32. package/dist/components/issues_panel/ui_helpers.js +136 -0
  33. package/dist/components/issues_panel/use_issue_types.d.ts +30 -0
  34. package/dist/components/issues_panel/use_issue_types.d.ts.map +1 -0
  35. package/dist/components/issues_panel/use_issue_types.js +81 -0
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.ui.d.ts +2 -0
  40. package/dist/index.ui.d.ts.map +1 -1
  41. package/dist/index.ui.js +1 -0
  42. package/dist/issues/archive_handler.d.ts +1 -1
  43. package/dist/issues/archive_handler.js +2 -2
  44. package/dist/issues/index.d.ts +6 -0
  45. package/dist/issues/index.d.ts.map +1 -1
  46. package/dist/issues/index.js +4 -0
  47. package/dist/issues/notify_handler.d.ts +26 -0
  48. package/dist/issues/notify_handler.d.ts.map +1 -0
  49. package/dist/issues/notify_handler.js +97 -0
  50. package/dist/issues/raise.d.ts +44 -0
  51. package/dist/issues/raise.d.ts.map +1 -0
  52. package/dist/issues/raise.js +77 -0
  53. package/dist/issues/recipients.d.ts +20 -0
  54. package/dist/issues/recipients.d.ts.map +1 -0
  55. package/dist/issues/recipients.js +61 -0
  56. package/dist/issues/registry.client.d.ts +1 -0
  57. package/dist/issues/registry.client.d.ts.map +1 -1
  58. package/dist/issues/registry.client.js +4 -1
  59. package/dist/issues/registry.d.ts +3 -3
  60. package/dist/issues/registry.d.ts.map +1 -1
  61. package/dist/issues/store.d.ts +10 -5
  62. package/dist/issues/store.d.ts.map +1 -1
  63. package/dist/issues/store.js +58 -28
  64. package/dist/issues/type_catalog.d.ts +48 -0
  65. package/dist/issues/type_catalog.d.ts.map +1 -0
  66. package/dist/issues/type_catalog.js +185 -0
  67. package/package.json +4 -4
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { Tag } from 'lucide-react';
5
+ import { LIGHT_THEME_VARS } from './ui_helpers.js';
6
+ // Small control appended under an issue card to reclassify its type. Renders
7
+ // hazo_ui's Select set when provided, else a native <select> fallback — never
8
+ // imports hazo_ui directly (probed + passed down by the orchestrator).
9
+ export function CardTypeControl({ issue, basePath, catalog, ui, onChanged }) {
10
+ const [saving, setSaving] = useState(false);
11
+ const options = catalog.map((entry) => ({ value: entry.type_key, label: entry.label }));
12
+ if (!options.some((opt) => opt.value === issue.type)) {
13
+ options.unshift({ value: issue.type, label: issue.type });
14
+ }
15
+ const commitType = async (newType) => {
16
+ if (newType === issue.type)
17
+ return;
18
+ setSaving(true);
19
+ try {
20
+ const res = await fetch(`${basePath}/issues/${issue.id}/type`, {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify({ type: newType }),
24
+ });
25
+ if (res.ok) {
26
+ onChanged?.(issue.id, newType);
27
+ }
28
+ }
29
+ finally {
30
+ setSaving(false);
31
+ }
32
+ };
33
+ const stop = (e) => e.stopPropagation();
34
+ const dotColor = catalog.find((c) => c.type_key === issue.type)?.color ?? null;
35
+ const Dot = (_jsx("span", { className: "h-2 w-2 shrink-0 rounded-full ring-1 ring-inset ring-black/5", style: { backgroundColor: dotColor ?? '#cbd5e1' } }));
36
+ const Label = (_jsxs("span", { className: "flex w-14 shrink-0 items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-slate-400", children: [_jsx(Tag, { className: "h-3 w-3" }), " Type"] }));
37
+ if (ui?.Select && ui?.SelectTrigger && ui?.SelectContent && ui?.SelectItem && ui?.SelectValue) {
38
+ const { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } = ui;
39
+ return (_jsxs("div", { className: "mt-2 flex items-center gap-2", onPointerDown: stop, onMouseDown: stop, onClick: stop, children: [Label, _jsxs(Select, { value: issue.type, onValueChange: commitType, disabled: saving, children: [_jsxs(SelectTrigger, { className: "h-7 flex-1 gap-1.5 rounded-md border-slate-200 px-2 text-xs", children: [Dot, _jsx(SelectValue, {})] }), _jsx(SelectContent, { style: LIGHT_THEME_VARS, className: "border border-slate-200 bg-white text-slate-900 shadow-lg", children: options.map((opt) => (_jsx(SelectItem, { value: opt.value, className: "text-xs", children: opt.label }, opt.value))) })] })] }));
40
+ }
41
+ return (_jsxs("div", { className: "mt-2 flex items-center gap-2", onPointerDown: stop, onMouseDown: stop, onClick: stop, children: [Label, _jsxs("div", { className: "flex flex-1 items-center gap-1.5 rounded-md border border-slate-200 px-2", children: [Dot, _jsx("select", { className: "h-7 w-full bg-transparent text-xs outline-none", value: issue.type, disabled: saving, onChange: (e) => commitType(e.target.value), children: options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) })] })] }));
42
+ }
@@ -0,0 +1,25 @@
1
+ import type { FacetValue } from './filter.js';
2
+ export interface FacetSidebarUi {
3
+ ScrollArea?: any;
4
+ Input?: any;
5
+ Checkbox?: any;
6
+ }
7
+ export interface FacetTypeOption {
8
+ type_key: string;
9
+ label: string;
10
+ }
11
+ export interface FacetSidebarProps {
12
+ value: FacetValue;
13
+ onChange: (v: FacetValue) => void;
14
+ statusOptions: string[];
15
+ typeOptions: FacetTypeOption[];
16
+ severityOptions: string[];
17
+ onManageTypes: () => void;
18
+ ui: FacetSidebarUi | null;
19
+ }
20
+ export declare const AGE_OPTIONS: {
21
+ key: string;
22
+ label: string;
23
+ }[];
24
+ export declare function FacetSidebar({ value, onChange, statusOptions, typeOptions, severityOptions, onManageTypes, ui, }: FacetSidebarProps): import("react").JSX.Element;
25
+ //# sourceMappingURL=facet_sidebar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facet_sidebar.d.ts","sourceRoot":"","sources":["../../../src/components/issues_panel/facet_sidebar.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,EAAE,EAAE,cAAc,GAAG,IAAI,CAAC;CAC3B;AAeD,eAAO,MAAM,WAAW,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAKvD,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACb,EAAE,GACH,EAAE,iBAAiB,+BA2LnB"}
@@ -0,0 +1,72 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Search, X, SlidersHorizontal, Layers, Plus } from 'lucide-react';
4
+ import { cx, severityMeta } from './ui_helpers.js';
5
+ function toggleMember(set, key) {
6
+ return set.includes(key) ? set.filter((k) => k !== key) : [...set, key];
7
+ }
8
+ const STATUS_LABELS = {
9
+ new: 'New',
10
+ wip: 'In progress',
11
+ on_hold: 'On Hold',
12
+ closed: 'Closed',
13
+ archived: 'Archived',
14
+ };
15
+ // Age buckets — single-select. Value is compared against issue.first_seen_at.
16
+ export const AGE_OPTIONS = [
17
+ { key: 'lt1d', label: 'Under 1 day' },
18
+ { key: '1to7d', label: '1–7 days' },
19
+ { key: '7to30d', label: '7–30 days' },
20
+ { key: 'gt30d', label: 'Over 30 days' },
21
+ ];
22
+ export function FacetSidebar({ value, onChange, statusOptions, typeOptions, severityOptions, onManageTypes, ui, }) {
23
+ const ScrollArea = ui?.ScrollArea;
24
+ const activeCount = value.statuses.length +
25
+ value.types.length +
26
+ value.severities.length +
27
+ (value.age ? 1 : 0) +
28
+ (value.search ? 1 : 0);
29
+ const setSearch = (search) => onChange({ ...value, search });
30
+ const toggleStatus = (key) => onChange({ ...value, statuses: toggleMember(value.statuses, key) });
31
+ const toggleType = (key) => onChange({ ...value, types: toggleMember(value.types, key) });
32
+ const toggleSeverity = (key) => onChange({ ...value, severities: toggleMember(value.severities, key) });
33
+ // Age is single-select: clicking the active bucket clears it.
34
+ const toggleAge = (key) => onChange({ ...value, age: value.age === key ? '' : key });
35
+ const clearAll = () => onChange({ search: '', statuses: [], types: [], severities: [], age: '' });
36
+ function Row({ label, checked, onToggle, swatch, keyName, }) {
37
+ return (_jsxs("button", { type: "button", onClick: onToggle, className: cx('group flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors', checked
38
+ ? 'bg-indigo-50 font-medium text-indigo-900'
39
+ : 'text-slate-600 hover:bg-slate-50'), children: [_jsx("span", { className: cx('flex h-4 w-4 shrink-0 items-center justify-center rounded-md border transition-colors', checked
40
+ ? 'border-indigo-600 bg-indigo-600 text-white'
41
+ : 'border-slate-300 group-hover:border-slate-400'), children: checked && (_jsx("svg", { viewBox: "0 0 12 12", className: "h-3 w-3", fill: "none", stroke: "currentColor", strokeWidth: 2, children: _jsx("path", { d: "M2.5 6.5l2.5 2.5 4.5-5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), swatch, _jsx("span", { className: "truncate", children: label })] }, keyName));
42
+ }
43
+ function Section({ title, children }) {
44
+ return (_jsxs("div", { children: [_jsx("div", { className: "mb-1.5 px-2 text-[11px] font-semibold uppercase tracking-wider text-slate-400", children: title }), _jsx("div", { className: "space-y-0.5", children: children })] }));
45
+ }
46
+ const body = (_jsxs("div", { className: "space-y-5 p-3", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" }), _jsx("input", { type: "text", placeholder: "Search issues\u2026", value: value.search, onChange: (e) => setSearch(e.target.value), className: "h-9 w-full rounded-xl border border-slate-200 bg-slate-50 pl-8 pr-8 text-sm text-slate-800 placeholder:text-slate-400 transition-colors focus:border-indigo-400 focus:bg-white focus:outline-none focus:ring-4 focus:ring-indigo-500/10" }), value.search && (_jsx("button", { type: "button", onClick: () => setSearch(''), className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-slate-400 hover:text-slate-700", "aria-label": "Clear search", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }), activeCount > 0 && (_jsxs("button", { type: "button", onClick: clearAll, className: "flex w-full items-center justify-between rounded-xl bg-indigo-600 px-2.5 py-2 text-xs font-semibold text-white shadow-sm shadow-indigo-500/25 transition-colors hover:bg-indigo-700", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }), activeCount, " active ", activeCount === 1 ? 'filter' : 'filters'] }), _jsxs("span", { className: "flex items-center gap-1 opacity-80", children: ["Clear ", _jsx(X, { className: "h-3 w-3" })] })] })), _jsx(Section, { title: "Status", children: statusOptions.map((s) => Row({
47
+ label: STATUS_LABELS[s] ?? s,
48
+ checked: value.statuses.includes(s),
49
+ onToggle: () => toggleStatus(s),
50
+ keyName: `status-${s}`,
51
+ })) }), _jsx(Section, { title: "Type", children: typeOptions.length === 0 ? (_jsx("p", { className: "px-2 text-xs text-slate-400", children: "No types" })) : (typeOptions.map((t) => Row({
52
+ label: t.label,
53
+ checked: value.types.includes(t.type_key),
54
+ onToggle: () => toggleType(t.type_key),
55
+ keyName: `type-${t.type_key}`,
56
+ }))) }), _jsx(Section, { title: "Severity", children: severityOptions.map((s) => {
57
+ const meta = severityMeta(s);
58
+ return Row({
59
+ label: meta.label,
60
+ checked: value.severities.includes(s),
61
+ onToggle: () => toggleSeverity(s),
62
+ swatch: _jsx("span", { className: cx('h-2.5 w-2.5 shrink-0 rounded-full', meta.dot) }),
63
+ keyName: `severity-${s}`,
64
+ });
65
+ }) }), _jsx(Section, { title: "Age", children: AGE_OPTIONS.map((a) => Row({
66
+ label: a.label,
67
+ checked: value.age === a.key,
68
+ onToggle: () => toggleAge(a.key),
69
+ keyName: `age-${a.key}`,
70
+ })) })] }));
71
+ return (_jsxs("aside", { className: "flex h-full w-60 shrink-0 flex-col border-r border-slate-200/80 bg-white", children: [ScrollArea ? (_jsx(ScrollArea, { className: "flex-1", children: body })) : (_jsx("div", { className: "flex-1 overflow-y-auto", children: body })), _jsx("div", { className: "border-t border-slate-200/80 p-3", children: _jsxs("button", { type: "button", onClick: onManageTypes, className: "flex w-full items-center justify-center gap-1.5 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Manage types", _jsx(Plus, { className: "ml-auto h-3.5 w-3.5 text-slate-400" })] }) })] }));
72
+ }
@@ -0,0 +1,20 @@
1
+ import type { FacetValue } from './filter.js';
2
+ export interface FacetTopbarUi {
3
+ Popover?: any;
4
+ PopoverTrigger?: any;
5
+ PopoverContent?: any;
6
+ }
7
+ export interface FacetTypeOption {
8
+ type_key: string;
9
+ label: string;
10
+ }
11
+ export interface FacetTopbarProps {
12
+ value: FacetValue;
13
+ onChange: (v: FacetValue) => void;
14
+ typeOptions: FacetTypeOption[];
15
+ severityOptions: string[];
16
+ onManageTypes: () => void;
17
+ ui: FacetTopbarUi | null;
18
+ }
19
+ export declare function FacetTopbar({ value, onChange, typeOptions, severityOptions, onManageTypes, ui, }: FacetTopbarProps): import("react").JSX.Element;
20
+ //# sourceMappingURL=facet_topbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facet_topbar.d.ts","sourceRoot":"","sources":["../../../src/components/issues_panel/facet_topbar.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,cAAc,CAAC,EAAE,GAAG,CAAC;IACrB,cAAc,CAAC,EAAE,GAAG,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,EAAE,EAAE,aAAa,GAAG,IAAI,CAAC;CAC1B;AA6CD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,WAAW,EACX,eAAe,EACf,aAAa,EACb,EAAE,GACH,EAAE,gBAAgB,+BAgKlB"}
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Search, X, Layers, Plus, ChevronDown, SlidersHorizontal } from 'lucide-react';
4
+ import { cx, severityMeta, LIGHT_THEME_VARS } from './ui_helpers.js';
5
+ import { AGE_OPTIONS } from './facet_sidebar.js';
6
+ function toggleMember(set, key) {
7
+ return set.includes(key) ? set.filter((k) => k !== key) : [...set, key];
8
+ }
9
+ // One checkbox line inside a facet popover.
10
+ function OptionRow({ label, checked, onToggle, swatch, }) {
11
+ return (_jsxs("button", { type: "button", onClick: onToggle, className: cx('group flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors', checked ? 'bg-indigo-50 font-medium text-indigo-900' : 'text-slate-600 hover:bg-slate-50'), children: [_jsx("span", { className: cx('flex h-4 w-4 shrink-0 items-center justify-center rounded-md border transition-colors', checked ? 'border-indigo-600 bg-indigo-600 text-white' : 'border-slate-300 group-hover:border-slate-400'), children: checked && (_jsx("svg", { viewBox: "0 0 12 12", className: "h-3 w-3", fill: "none", stroke: "currentColor", strokeWidth: 2, children: _jsx("path", { d: "M2.5 6.5l2.5 2.5 4.5-5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), swatch, _jsx("span", { className: "truncate", children: label })] }));
12
+ }
13
+ export function FacetTopbar({ value, onChange, typeOptions, severityOptions, onManageTypes, ui, }) {
14
+ const Popover = ui?.Popover;
15
+ const PopoverTrigger = ui?.PopoverTrigger;
16
+ const PopoverContent = ui?.PopoverContent;
17
+ const activeCount = value.statuses.length +
18
+ value.severities.length +
19
+ value.types.length +
20
+ (value.age ? 1 : 0);
21
+ const setSearch = (search) => onChange({ ...value, search });
22
+ const toggleType = (key) => onChange({ ...value, types: toggleMember(value.types, key) });
23
+ const toggleSeverity = (key) => onChange({ ...value, severities: toggleMember(value.severities, key) });
24
+ // Age is single-select: clicking the active bucket clears it.
25
+ const toggleAge = (key) => onChange({ ...value, age: value.age === key ? '' : key });
26
+ const clearAll = () => onChange({ search: '', statuses: [], types: [], severities: [], age: '' });
27
+ // A dropdown "chip" — trigger button + popover panel of checkbox rows. Falls
28
+ // back to a disabled-looking static chip if hazo_ui's Popover isn't present.
29
+ function FacetChip({ title, count, children, }) {
30
+ const active = count > 0;
31
+ const trigger = (_jsxs("button", { type: "button", className: cx('inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border px-2.5 text-xs font-semibold transition-colors', active
32
+ ? 'border-indigo-200 bg-indigo-50 text-indigo-700 hover:bg-indigo-100'
33
+ : 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50'), children: [title, active && (_jsx("span", { className: "flex h-4 min-w-4 items-center justify-center rounded-full bg-indigo-600 px-1 text-[10px] font-bold leading-none text-white", children: count })), _jsx(ChevronDown, { className: "h-3 w-3 text-slate-400" })] }));
34
+ if (!Popover || !PopoverTrigger || !PopoverContent)
35
+ return trigger;
36
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: trigger }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, style: LIGHT_THEME_VARS, className: "w-56 rounded-xl border border-slate-200 bg-white p-1.5 text-slate-900 shadow-lg", children: [_jsx("div", { className: "mb-1 px-2 pt-1 text-[11px] font-semibold uppercase tracking-wider text-slate-400", children: title }), _jsx("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: children })] })] }));
37
+ }
38
+ return (_jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-slate-200/80 bg-slate-50/80 px-5 py-2.5", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" }), _jsx("input", { type: "text", placeholder: "Search issues\u2026", value: value.search, onChange: (e) => setSearch(e.target.value), className: "h-8 w-52 rounded-lg border border-slate-200 bg-white pl-8 pr-8 text-sm text-slate-800 placeholder:text-slate-400 transition-colors focus:border-indigo-400 focus:outline-none focus:ring-4 focus:ring-indigo-500/10" }), value.search && (_jsx("button", { type: "button", onClick: () => setSearch(''), className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-slate-400 hover:text-slate-700", "aria-label": "Clear search", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }), _jsx("span", { className: "mx-0.5 h-5 w-px shrink-0 bg-slate-200" }), _jsx(FacetChip, { title: "Type", count: value.types.length, children: typeOptions.length === 0 ? (_jsx("p", { className: "px-2 py-1.5 text-xs text-slate-400", children: "No types" })) : (typeOptions.map((t) => (_jsx(OptionRow, { label: t.label, checked: value.types.includes(t.type_key), onToggle: () => toggleType(t.type_key) }, `type-${t.type_key}`)))) }), _jsx(FacetChip, { title: "Severity", count: value.severities.length, children: severityOptions.map((s) => {
39
+ const meta = severityMeta(s);
40
+ return (_jsx(OptionRow, { label: meta.label, checked: value.severities.includes(s), onToggle: () => toggleSeverity(s), swatch: _jsx("span", { className: cx('h-2.5 w-2.5 shrink-0 rounded-full', meta.dot) }) }, `severity-${s}`));
41
+ }) }), _jsx(FacetChip, { title: "Age", count: value.age ? 1 : 0, children: AGE_OPTIONS.map((a) => (_jsx(OptionRow, { label: a.label, checked: value.age === a.key, onToggle: () => toggleAge(a.key) }, `age-${a.key}`))) }), activeCount > 0 && (_jsxs("button", { type: "button", onClick: clearAll, className: "inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg bg-indigo-600 px-2.5 text-xs font-semibold text-white shadow-sm shadow-indigo-500/25 transition-colors hover:bg-indigo-700", children: [_jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }), "Clear", _jsxs("span", { className: "opacity-80", children: ["\u00B7 ", activeCount] })] })), _jsxs("button", { type: "button", onClick: onManageTypes, className: "ml-auto inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Manage types", _jsx(Plus, { className: "h-3.5 w-3.5 text-slate-400" })] })] }));
42
+ }
@@ -0,0 +1,12 @@
1
+ import type { IssueCardData } from '../../issues/registry.client.js';
2
+ export interface FacetValue {
3
+ statuses: string[];
4
+ types: string[];
5
+ severities: string[];
6
+ age: string;
7
+ search: string;
8
+ }
9
+ export declare const EMPTY_FACET: FacetValue;
10
+ export declare function ageBucket(firstSeenIso: string | null | undefined, nowMs?: number): string;
11
+ export declare function filterIssues(issues: IssueCardData[], facet: FacetValue): IssueCardData[];
12
+ //# sourceMappingURL=filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../../src/components/issues_panel/filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,WAAW,EAAE,UAA6E,CAAC;AAMxG,wBAAgB,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,GAAE,MAAmB,GAAG,MAAM,CASrG;AAID,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,UAAU,GAAG,aAAa,EAAE,CAcxF"}
@@ -0,0 +1,41 @@
1
+ export const EMPTY_FACET = { statuses: [], types: [], severities: [], age: '', search: '' };
2
+ const DAY_MS = 86400000;
3
+ // Bucket an issue by how long ago it was first seen, relative to `nowMs`.
4
+ // Returns one of the AGE_OPTIONS keys, or '' when the timestamp is unparseable.
5
+ export function ageBucket(firstSeenIso, nowMs = Date.now()) {
6
+ if (!firstSeenIso)
7
+ return '';
8
+ const then = Date.parse(firstSeenIso);
9
+ if (Number.isNaN(then))
10
+ return '';
11
+ const days = (nowMs - then) / DAY_MS;
12
+ if (days < 1)
13
+ return 'lt1d';
14
+ if (days < 7)
15
+ return '1to7d';
16
+ if (days <= 30)
17
+ return '7to30d';
18
+ return 'gt30d';
19
+ }
20
+ // status ∈ set (or set empty) ∧ type ∈ set ∧ severity ∈ set ∧ (age empty OR
21
+ // bucket matches) ∧ (search empty OR case-insensitive substring on title/summary).
22
+ export function filterIssues(issues, facet) {
23
+ const search = facet.search.trim().toLowerCase();
24
+ const nowMs = Date.now();
25
+ return issues.filter((issue) => {
26
+ if (facet.statuses.length > 0 && !facet.statuses.includes(issue.status))
27
+ return false;
28
+ if (facet.types.length > 0 && !facet.types.includes(issue.type))
29
+ return false;
30
+ if (facet.severities.length > 0 && !facet.severities.includes(issue.severity))
31
+ return false;
32
+ if (facet.age && ageBucket(issue.first_seen_at, nowMs) !== facet.age)
33
+ return false;
34
+ if (search.length > 0) {
35
+ const haystack = `${issue.title ?? ''} ${issue.summary ?? ''}`.toLowerCase();
36
+ if (!haystack.includes(search))
37
+ return false;
38
+ }
39
+ return true;
40
+ });
41
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/issues_panel/index.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA8BD,wBAAgB,WAAW,CAAC,EAAE,QAAuB,EAAE,aAAa,EAAE,EAAE,gBAAgB,+BA6MvF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/issues_panel/index.tsx"],"names":[],"mappings":"AAiBA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAsED,wBAAgB,WAAW,CAAC,EAAE,QAAuB,EAAE,aAAa,EAAE,EAAE,gBAAgB,+BA6XvF"}
@@ -1,31 +1,42 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState, useCallback } from 'react';
4
+ import { AlertTriangle, Loader2, Inbox, Archive } from 'lucide-react';
5
+ import { cx, LIGHT_THEME_VARS } from './ui_helpers.js';
4
6
  import { getIssueCardRenderer, DefaultIssueCard } from '../../issues/registry.client.js';
5
- const ACTIVE_COLUMNS = [
6
- { key: 'new', title: 'New' },
7
- { key: 'wip', title: 'WIP' },
8
- { key: 'closed', title: 'Closed' },
9
- ];
10
- const ARCHIVED_COLUMNS = [
11
- { key: 'archived', title: 'Archived' },
12
- ];
13
- function toKanbanItem(issue) {
14
- return { ...issue, columnKey: issue.status };
7
+ import { statusColumns, typeColumns, toKanbanItem } from './board_columns.js';
8
+ import { EMPTY_FACET, filterIssues } from './filter.js';
9
+ import { useIssueTypes } from './use_issue_types.js';
10
+ import { CardTypeControl } from './card_type_control.js';
11
+ import { CardAssigneeControl } from './card_assignee_control.js';
12
+ import { FacetTopbar } from './facet_topbar.js';
13
+ import { ManageTypesDialog } from './manage_types_dialog.js';
14
+ const SEVERITY_OPTIONS = ['low', 'medium', 'high', 'critical'];
15
+ // Compact pill segmented control — a track with a highlighted active segment.
16
+ function Segmented({ value, onChange, options, disabled, }) {
17
+ return (_jsx("div", { className: cx('inline-flex items-center gap-0.5 rounded-full bg-slate-100 p-1', disabled && 'opacity-40'), children: options.map((opt) => {
18
+ const active = value === opt.value;
19
+ return (_jsx("button", { type: "button", disabled: disabled, onClick: () => onChange(opt.value), className: cx('rounded-full px-3.5 py-1 text-xs font-semibold transition-all disabled:cursor-not-allowed', active
20
+ ? 'bg-white text-indigo-700 shadow-sm ring-1 ring-black/5'
21
+ : 'text-slate-500 hover:text-slate-800'), children: opt.label }, opt.value));
22
+ }) }));
15
23
  }
16
24
  export function IssuesPanel({ basePath = '/api/admin', currentUserId }) {
17
- const [Kanban, setKanban] = useState(null);
18
- const [KanbanFilter, setKanbanFilter] = useState(null);
19
- const [applyFilter, setApplyFilter] = useState(null);
25
+ const [ui, setUi] = useState(null);
20
26
  const [notInstalled, setNotInstalled] = useState(false);
21
27
  const [uiLoading, setUiLoading] = useState(true);
22
28
  const [tab, setTab] = useState('active');
29
+ const [boardMode, setBoardMode] = useState('status');
23
30
  const [activeIssues, setActiveIssues] = useState([]);
24
31
  const [archivedIssues, setArchivedIssues] = useState([]);
25
32
  const [dataLoading, setDataLoading] = useState(false);
26
33
  const [dataError, setDataError] = useState(null);
27
- const [filter, setFilter] = useState({ search: '', categories: [], priority: null });
28
- // Probe hazo_ui for Kanban components
34
+ const [facet, setFacet] = useState(EMPTY_FACET);
35
+ const [manageOpen, setManageOpen] = useState(false);
36
+ const [assignees, setAssignees] = useState([]);
37
+ const { types: catalog, createType, updateType, deleteType } = useIssueTypes(basePath);
38
+ // Probe hazo_ui once for every component this panel or its children need.
39
+ // hazo_ui stays a lazy runtime peer — never top-level imported.
29
40
  useEffect(() => {
30
41
  import('hazo_ui').catch(() => null).then((pkg) => {
31
42
  if (!pkg || !pkg.HazoUiKanban) {
@@ -33,9 +44,22 @@ export function IssuesPanel({ basePath = '/api/admin', currentUserId }) {
33
44
  setUiLoading(false);
34
45
  return;
35
46
  }
36
- setKanban(() => pkg.HazoUiKanban);
37
- setKanbanFilter(() => pkg.HazoUiKanbanFilter ?? null);
38
- setApplyFilter(() => pkg.applyKanbanFilter ?? null);
47
+ setUi({
48
+ HazoUiKanban: pkg.HazoUiKanban,
49
+ Select: pkg.Select ?? null,
50
+ SelectTrigger: pkg.SelectTrigger ?? null,
51
+ SelectContent: pkg.SelectContent ?? null,
52
+ SelectItem: pkg.SelectItem ?? null,
53
+ SelectValue: pkg.SelectValue ?? null,
54
+ ScrollArea: pkg.ScrollArea ?? null,
55
+ Input: pkg.Input ?? null,
56
+ Checkbox: pkg.Checkbox ?? null,
57
+ Button: pkg.Button ?? null,
58
+ HazoUiDialog: pkg.HazoUiDialog ?? null,
59
+ Popover: pkg.Popover ?? null,
60
+ PopoverTrigger: pkg.PopoverTrigger ?? null,
61
+ PopoverContent: pkg.PopoverContent ?? null,
62
+ });
39
63
  setUiLoading(false);
40
64
  });
41
65
  }, []);
@@ -43,10 +67,10 @@ export function IssuesPanel({ basePath = '/api/admin', currentUserId }) {
43
67
  useEffect(() => {
44
68
  setDataLoading(true);
45
69
  setDataError(null);
46
- fetch(`${basePath}/issues?status=new,wip,closed`)
70
+ fetch(`${basePath}/issues?status=new,wip,on_hold,closed`)
47
71
  .then((r) => r.json())
48
72
  .then((json) => {
49
- const rows = Array.isArray(json) ? json : (json.data ?? []);
73
+ const rows = Array.isArray(json) ? json : (json.issues ?? json.data ?? []);
50
74
  setActiveIssues(rows);
51
75
  })
52
76
  .catch((err) => setDataError(String(err)))
@@ -63,7 +87,7 @@ export function IssuesPanel({ basePath = '/api/admin', currentUserId }) {
63
87
  fetch(`${basePath}/issues?status=archived`)
64
88
  .then((r) => r.json())
65
89
  .then((json) => {
66
- const rows = Array.isArray(json) ? json : (json.data ?? []);
90
+ const rows = Array.isArray(json) ? json : (json.issues ?? json.data ?? []);
67
91
  setArchivedIssues(rows);
68
92
  })
69
93
  .catch((err) => setDataError(String(err)))
@@ -80,14 +104,86 @@ export function IssuesPanel({ basePath = '/api/admin', currentUserId }) {
80
104
  event.revert();
81
105
  }
82
106
  else {
83
- // Update local state
84
- setActiveIssues((prev) => prev.map((iss) => iss.id === event.itemId ? { ...iss, status: event.toColumn, columnKey: event.toColumn } : iss));
107
+ setActiveIssues((prev) => prev.map((iss) => (iss.id === event.itemId ? { ...iss, status: event.toColumn } : iss)));
85
108
  }
86
109
  }
87
110
  catch {
88
111
  event.revert();
89
112
  }
90
113
  }, [basePath, currentUserId]);
114
+ const handleTypeMove = useCallback(async (event) => {
115
+ const newType = event.toColumn;
116
+ setActiveIssues((prev) => prev.map((iss) => (iss.id === event.itemId ? { ...iss, type: newType } : iss)));
117
+ try {
118
+ const res = await fetch(`${basePath}/issues/${event.itemId}/type`, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify({ type: newType }),
122
+ });
123
+ if (!res.ok) {
124
+ event.revert();
125
+ setActiveIssues((prev) => prev.map((iss) => (iss.id === event.itemId ? { ...iss, type: event.fromColumn } : iss)));
126
+ }
127
+ }
128
+ catch {
129
+ event.revert();
130
+ setActiveIssues((prev) => prev.map((iss) => (iss.id === event.itemId ? { ...iss, type: event.fromColumn } : iss)));
131
+ }
132
+ }, [basePath]);
133
+ const handleCardTypeChanged = useCallback((issueId, newType) => {
134
+ setActiveIssues((prev) => prev.map((iss) => (iss.id === issueId ? { ...iss, type: newType } : iss)));
135
+ }, []);
136
+ // Fetch assignable admins once for the reassign picker.
137
+ useEffect(() => {
138
+ fetch(`${basePath}/issues/assignees`)
139
+ .then((r) => r.json())
140
+ .then((json) => {
141
+ const rows = Array.isArray(json) ? json : (json.assignees ?? []);
142
+ setAssignees(rows);
143
+ })
144
+ .catch(() => setAssignees([]));
145
+ }, [basePath]);
146
+ const handleAssigneeChanged = useCallback((issueId, userId) => {
147
+ setActiveIssues((prev) => prev.map((iss) => (iss.id === issueId ? { ...iss, assigned_to: userId } : iss)));
148
+ }, []);
149
+ // "Assign to me" — server assigns to the authenticated actor and echoes the
150
+ // issue back, so the client learns who "me" is from the response.
151
+ const handleAssignToMe = useCallback(async (issueId) => {
152
+ const res = await fetch(`${basePath}/issues/${issueId}/assign-to-me`, { method: 'POST' });
153
+ if (!res.ok)
154
+ return;
155
+ const json = await res.json().catch(() => null);
156
+ const userId = json?.issue?.assigned_to ?? null;
157
+ setActiveIssues((prev) => prev.map((iss) => (iss.id === issueId ? { ...iss, assigned_to: userId } : iss)));
158
+ }, [basePath]);
159
+ // Archive closed issues older than the configured cutoff, then refresh both
160
+ // lists. Global-admin only server-side; a 403 is surfaced as an error banner.
161
+ const [archiving, setArchiving] = useState(false);
162
+ const handleArchiveSweep = useCallback(async () => {
163
+ setArchiving(true);
164
+ setDataError(null);
165
+ try {
166
+ const res = await fetch(`${basePath}/issues/archive-sweep`, { method: 'POST' });
167
+ if (!res.ok) {
168
+ setDataError(res.status === 403 ? 'Only global admins can archive old issues.' : `Archive failed (${res.status}).`);
169
+ return;
170
+ }
171
+ // Reload active (some moved out) and archived (some moved in).
172
+ const [activeRes, archivedRes] = await Promise.all([
173
+ fetch(`${basePath}/issues?status=new,wip,on_hold,closed`).then((r) => r.json()),
174
+ fetch(`${basePath}/issues?status=archived`).then((r) => r.json()),
175
+ ]);
176
+ const pick = (json) => (Array.isArray(json) ? json : (json.issues ?? json.data ?? []));
177
+ setActiveIssues(pick(activeRes));
178
+ setArchivedIssues(pick(archivedRes));
179
+ }
180
+ catch (err) {
181
+ setDataError(String(err));
182
+ }
183
+ finally {
184
+ setArchiving(false);
185
+ }
186
+ }, [basePath]);
91
187
  const handleAction = useCallback(async (issueId, actionKey, params) => {
92
188
  await fetch(`${basePath}/issues/${issueId}/resolve`, {
93
189
  method: 'POST',
@@ -98,33 +194,39 @@ export function IssuesPanel({ basePath = '/api/admin', currentUserId }) {
98
194
  if (uiLoading) {
99
195
  return _jsx("div", { className: "p-6 text-sm text-gray-400", children: "Loading\u2026" });
100
196
  }
101
- if (notInstalled) {
197
+ if (notInstalled || !ui) {
102
198
  return (_jsxs("div", { className: "p-6 max-w-xl", children: [_jsx("h2", { className: "text-base font-semibold text-gray-800 mb-2", children: "Issues" }), _jsxs("p", { className: "text-sm text-gray-500", children: [_jsx("code", { className: "text-xs bg-gray-100 px-1 rounded", children: "hazo_ui" }), " Kanban components are not available. Ensure ", _jsx("code", { className: "text-xs bg-gray-100 px-1 rounded", children: "hazo_ui" }), " is installed and exports", ' ', _jsx("code", { className: "text-xs bg-gray-100 px-1 rounded", children: "HazoUiKanban" }), "."] })] }));
103
199
  }
104
- const HazoUiKanban = Kanban;
105
- const HazoUiKanbanFilter = KanbanFilter;
200
+ const HazoUiKanban = ui.HazoUiKanban;
106
201
  const currentIssues = tab === 'active' ? activeIssues : archivedIssues;
107
- const kanbanItems = currentIssues.map(toKanbanItem);
108
- // Apply filter for active tab
109
- const filteredItems = (tab === 'active' && applyFilter)
110
- ? applyFilter(kanbanItems, filter)
111
- : kanbanItems;
112
- // Unique types for category filter
113
- const uniqueTypes = Array.from(new Set(activeIssues.map((i) => i.type))).sort();
114
- const columns = tab === 'active' ? ACTIVE_COLUMNS : ARCHIVED_COLUMNS;
202
+ const filteredIssues = filterIssues(currentIssues, facet);
203
+ const effectiveBoardMode = tab === 'archived' ? 'status' : boardMode;
204
+ const kanbanItems = filteredIssues.map((issue) => toKanbanItem(issue, effectiveBoardMode));
205
+ const columns = tab === 'archived' ? statusColumns('archived') : boardMode === 'type' ? typeColumns(catalog, activeIssues) : statusColumns('active');
206
+ const typeOptions = catalog.length > 0
207
+ ? catalog.map((t) => ({ type_key: t.type_key, label: t.label }))
208
+ : Array.from(new Set(activeIssues.map((i) => i.type)))
209
+ .sort()
210
+ .map((t) => ({ type_key: t, label: t }));
115
211
  function renderCard(item) {
116
212
  const Renderer = getIssueCardRenderer(item.type);
117
213
  const onAction = async (actionKey, params) => {
118
214
  await handleAction(item.id, actionKey, params);
119
215
  };
120
- if (Renderer) {
121
- return _jsx(Renderer, { issue: item, basePath: basePath, onAction: onAction });
122
- }
123
- return _jsx(DefaultIssueCard, { issue: item, basePath: basePath, onAction: onAction });
216
+ return (_jsxs("div", { className: "group/card", children: [Renderer ? (_jsx(Renderer, { issue: item, basePath: basePath, onAction: onAction })) : (_jsx(DefaultIssueCard, { issue: item, basePath: basePath, onAction: onAction })), _jsxs("div", { className: "mt-2.5 border-t border-slate-100 pt-2", children: [_jsx(CardTypeControl, { issue: item, basePath: basePath, catalog: catalog, ui: ui, onChanged: handleCardTypeChanged }), _jsx(CardAssigneeControl, { issue: item, basePath: basePath, assignees: assignees, onChanged: handleAssigneeChanged, onAssignToMe: handleAssignToMe })] })] }));
124
217
  }
125
- return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsxs("div", { className: "p-4 border-b flex items-center gap-4", children: [_jsx("h2", { className: "text-base font-semibold text-gray-800", children: "Issues" }), _jsxs("div", { className: "flex gap-2 ml-auto", children: [_jsx("button", { onClick: () => setTab('active'), className: `px-3 py-1 rounded text-sm font-medium transition-colors ${tab === 'active'
126
- ? 'bg-blue-100 text-blue-700'
127
- : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`, children: "Active" }), _jsx("button", { onClick: () => setTab('archived'), className: `px-3 py-1 rounded text-sm font-medium transition-colors ${tab === 'archived'
128
- ? 'bg-blue-100 text-blue-700'
129
- : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`, children: "Archived" })] })] }), tab === 'active' && HazoUiKanbanFilter && (_jsx("div", { className: "px-4 py-2 border-b", children: _jsx(HazoUiKanbanFilter, { search: true, searchPlaceholder: "Search issues\u2026", categories: uniqueTypes, value: filter, onChange: setFilter }) })), dataError && (_jsxs("div", { className: "px-4 py-2 text-sm text-red-600 bg-red-50 border-b", children: ["Failed to load issues: ", dataError] })), dataLoading && (_jsx("div", { className: "px-4 py-2 text-sm text-gray-400", children: "Loading issues\u2026" })), _jsx("div", { className: "flex-1 overflow-auto p-4", children: HazoUiKanban && (_jsx(HazoUiKanban, { columns: columns, items: filteredItems, renderCard: renderCard, onMove: tab === 'active' ? handleMove : undefined, disableEdit: tab === 'archived' })) })] }));
218
+ const onMove = tab === 'active' ? (boardMode === 'type' ? handleTypeMove : handleMove) : undefined;
219
+ const totalShown = filteredIssues.length;
220
+ const totalLoaded = currentIssues.length;
221
+ return (_jsxs("div", { style: LIGHT_THEME_VARS, className: "flex h-full flex-col overflow-hidden rounded-2xl border border-slate-200/80 bg-white text-slate-900 shadow-[0_1px_2px_rgba(15,23,42,0.04),0_12px_32px_-12px_rgba(15,23,42,0.12)]", children: [_jsxs("header", { className: "flex flex-wrap items-center gap-3 border-b border-slate-200/80 bg-white px-5 py-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-sm shadow-indigo-500/30", children: _jsx(AlertTriangle, { className: "h-4 w-4" }) }), _jsxs("div", { children: [_jsx("h2", { className: "text-[15px] font-semibold leading-none tracking-tight text-slate-900", children: "Issues" }), _jsx("p", { className: "mt-1.5 text-xs leading-none text-slate-400", children: totalShown === totalLoaded
222
+ ? `${totalLoaded} ${totalLoaded === 1 ? 'issue' : 'issues'}`
223
+ : `${totalShown} of ${totalLoaded} shown` })] })] }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsxs("button", { type: "button", onClick: handleArchiveSweep, disabled: archiving, title: "Move closed issues older than the archive cutoff to Archived", className: "inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600 transition-colors hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700 disabled:cursor-not-allowed disabled:opacity-50", children: [_jsx(Archive, { className: "h-3.5 w-3.5" }), archiving ? 'Archiving…' : 'Archive old'] }), _jsx(Segmented, { value: tab, onChange: (v) => setTab(v), options: [
224
+ { value: 'active', label: 'Active' },
225
+ { value: 'archived', label: 'Archived' },
226
+ ] }), _jsx(Segmented, { value: effectiveBoardMode, onChange: (v) => setBoardMode(v), disabled: tab === 'archived', options: [
227
+ { value: 'status', label: 'Status' },
228
+ { value: 'type', label: 'Type' },
229
+ ] })] })] }), dataError && (_jsxs("div", { className: "flex items-center gap-2 border-b border-rose-100 bg-rose-50 px-5 py-2 text-sm text-rose-700", children: [_jsx(AlertTriangle, { className: "h-4 w-4 shrink-0" }), "Failed to load issues: ", dataError] })), dataLoading && (_jsxs("div", { className: "flex items-center gap-2 border-b border-slate-100 px-5 py-2 text-sm text-slate-400", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Loading issues\u2026"] })), _jsx(FacetTopbar, { value: facet, onChange: setFacet, typeOptions: typeOptions, severityOptions: SEVERITY_OPTIONS, onManageTypes: () => setManageOpen(true), ui: ui }), _jsx("div", { className: "flex flex-1 overflow-hidden", children: _jsx("div", { className: "flex-1 overflow-auto bg-slate-50/60 p-5 [background-image:radial-gradient(circle_at_1px_1px,rgba(99,102,241,0.06)_1px,transparent_0)] [background-size:22px_22px]", children: !dataLoading && totalShown === 0 ? (_jsxs("div", { className: "flex h-full min-h-[300px] flex-col items-center justify-center text-center", children: [_jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-2xl bg-white text-indigo-400 shadow-sm ring-1 ring-slate-200/70", children: _jsx(Inbox, { className: "h-7 w-7" }) }), _jsx("p", { className: "mt-4 text-sm font-semibold text-slate-700", children: totalLoaded === 0 ? 'No issues yet' : 'No issues match your filters' }), _jsx("p", { className: "mt-1 max-w-xs text-xs text-slate-400", children: totalLoaded === 0
230
+ ? 'Raised issues will appear here as soon as they arrive.'
231
+ : 'Try clearing a filter or search term to see more results.' })] })) : (HazoUiKanban && (_jsx(HazoUiKanban, { columns: columns, items: kanbanItems, renderCard: renderCard, onMove: onMove, disableEdit: tab === 'archived' }))) }) }), _jsx(ManageTypesDialog, { open: manageOpen, onOpenChange: setManageOpen, catalog: catalog, onCreate: createType, onUpdate: updateType, onDelete: deleteType, ui: ui })] }));
130
232
  }
@@ -0,0 +1,28 @@
1
+ import type { CatalogType } from './use_issue_types.js';
2
+ export interface ManageTypesDialogUi {
3
+ HazoUiDialog?: any;
4
+ Input?: any;
5
+ Button?: any;
6
+ }
7
+ export interface ManageTypesDialogProps {
8
+ open: boolean;
9
+ onOpenChange: (o: boolean) => void;
10
+ catalog: CatalogType[];
11
+ onCreate: (input: {
12
+ type_key: string;
13
+ label: string;
14
+ color?: string | null;
15
+ description?: string | null;
16
+ sort_order?: number;
17
+ }) => Promise<void>;
18
+ onUpdate: (type_key: string, patch: {
19
+ label?: string;
20
+ color?: string | null;
21
+ description?: string | null;
22
+ sort_order?: number;
23
+ }) => Promise<void>;
24
+ onDelete: (type_key: string) => Promise<void>;
25
+ ui: ManageTypesDialogUi | null;
26
+ }
27
+ export declare function ManageTypesDialog({ open, onOpenChange, catalog, onCreate, onUpdate, onDelete, ui }: ManageTypesDialogProps): import("react").JSX.Element | null;
28
+ //# sourceMappingURL=manage_types_dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manage_types_dialog.d.ts","sourceRoot":"","sources":["../../../src/components/issues_panel/manage_types_dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGxD,MAAM,WAAW,mBAAmB;IAClC,YAAY,CAAC,EAAE,GAAG,CAAC;IACnB,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,MAAM,CAAC,EAAE,GAAG,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,QAAQ,EAAE,CACR,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,KAC/F,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,EAAE,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAChC;AAoBD,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,sBAAsB,sCAqJ1H"}