PageSourceSearch

https://pendragonx.com/assets/CaptureHub-F3qt1CD-.js

js pendragonx.com collected 2026-10-02 01:42:05 UTC 6,479 bytes, 164 lines download raw bytes

1const e=`import { lazy, Suspense, useState, useMemo } from "react";
2import { useSearchParams } from "react-router-dom";
3import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
4import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
5import { PenLine, Layers, StickyNote, HelpCircle } from "lucide-react";
6import { useZettelCards } from "@/hooks/useZettelCards";
7import type { ZettelCard as ZettelCardType } from "@/types/zettel";
8
9const ScratchPad = lazy(() => import("@/components/ScratchPad").then(m => ({ default: m.ScratchPad })));
10const CardsNotesWorkspace = lazy(() => import("@/components/workspaces/CardsNotesWorkspace").then(m => ({ default: m.CardsNotesWorkspace })));
11const StickyNotesSimple = lazy(() => import("@/components/StickyNotesSimple").then(m => ({ default: m.StickyNotesSimple })));
12
13type HubTab = "scratchpad" | "workspace" | "stickynotes";
14
15const TAXONOMY: Array<{
16  id: HubTab;
17  label: string;
18  icon: React.ComponentType<{ className?: string }>;
19  tagline: string;
20  when: string;
21  limit: string;
22  accent: string;
23}> = [
24  {
25    id: "scratchpad",
26    label: "Scratchpad",
27    icon: PenLine,
28    tagline: "Napkin scribbles",
29    when: "Quick ideas, web snippets, brain dumps you don't want to lose.",
30    limit: "≤ 500 characters",
31    accent: "text-amber-600",
32  },
33  {
34    id: "workspace",
35    label: "Cards & Notes",
36    icon: Layers,
37    tagline: "Atomic ideas → long-form",
38    when: "Unified split-pane workspace. Atomic cards (≤1,500 chars) live alongside long-form notes. Type [[ to link, drop cards into notes to cite, and promote cards to notes when they outgrow the card limit.",
39    limit: "Cards ≤ 1,500 · Notes unlimited",
40    accent: "text-sky-600",
41  },
42  {
43    id: "stickynotes",
44    label: "Sticky Notes",
45    icon: StickyNote,
46    tagline: "Reminders & tasks",
47    when: "Quick reminders, follow-ups, and short to-dos.",
48    limit: "Short",
49    accent: "text-rose-600",
50  },
51];
52
53export function CaptureHub() {
54  const [params, setParams] = useSearchParams();
55  const raw = params.get("hub");
56  const initial: HubTab = raw === "cards" || raw === "notes" || raw === "workspace"
57    ? "workspace"
58    : raw === "stickynotes" ? "stickynotes" : "scratchpad";
59  const [tab, setTab] = useState<HubTab>(initial);
60  const { createCard } = useZettelCards();
61
62  const handleChange = (val: string) => {
63    setTab(val as HubTab);
64    const next = new URLSearchParams(params);
65    next.set("hub", val);
66    setParams(next, { replace: true });
67  };
68
69  const active = useMemo(() => TAXONOMY.find(t => t.id === tab)!, [tab]);
70
71  const handleCreateCard = (card: Omit<ZettelCardType, "id" | "created" | "modified">) => {
72    createCard(card);
73  };
74
75  return (
76    <div className="px-2 sm:px-4 py-2 space-y-2">
77      <div className="flex items-center gap-1.5">
78        <div
79          role="tablist"
80          aria-label="Capture type"
81          className="flex flex-1 items-center gap-1 rounded-full border border-border/60 bg-card/60 p-1 overflow-x-auto scrollbar-hide"
82        >
83          {TAXONOMY.map((t) => {
84            const Icon = t.icon;
85            const isActive = t.id === tab;
86            return (
87              <button
88                key={t.id}
89                role="tab"
90                aria-selected={isActive}
91                onClick={() => handleChange(t.id)}
92                title={\`\${t.label} · \${t.tagline} · \${t.limit}\`}
93                className={\`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium whitespace-nowrap transition-all \${
94                  isActive
95                    ? "bg-primary text-primary-foreground shadow-sm"
96                    : "text-muted-foreground hover:text-foreground hover:bg-accent/60"
97                }\`}
98              >
99                <Icon className={\`h-3.5 w-3.5 \${isActive ? "" : t.accent}\`} />
100                {t.label}
101              </button>
102            );
103          })}
104        </div>
105
106        <Popover>
107          <PopoverTrigger asChild>
108            <button
109              aria-label="How capture types work"
110              className="shrink-0 rounded-full border border-border/60 bg-card/60 p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors"
111            >
112              <HelpCircle className="h-3.5 w-3.5" />
113            </button>
114          </PopoverTrigger>
115          <PopoverContent align="end" className="w-80 p-3 space-y-2.5">
116            <div className="text-xs">
117              <p className="font-semibold text-foreground mb-1">One place to capture everything.</p>
118              <p className="text-muted-foreground leading-relaxed">
119                Pick the shape that fits. Cards & Notes share one split-pane workspace — promote a card to a note the moment it outgrows 1,500 characters.
120              </p>
121            </div>
122            <div className="border-t border-border/60 pt-2 space-y-2">
123              {TAXONOMY.map((t) => {
124                const Icon = t.icon;
125                return (
126                  <div key={t.id} className="flex items-start gap-2">
127                    <Icon className={\`h-3.5 w-3.5 mt-0.5 shrink-0 \${t.accent}\`} />
128                    <div className="text-[11px] leading-snug">
129                      <span className="font-semibold text-foreground">{t.label}</span>
130                      <span className="text-muted-foreground/80"> · {t.limit}</span>
131                      <p className="text-muted-foreground mt-0.5">{t.when}</p>
132                    </div>
133                  </div>
134                );
135              })}
136            </div>
137          </PopoverContent>
138        </Popover>
139      </div>
140
141      <Tabs value={tab} onValueChange={handleChange} className="w-full">
142        <TabsList className="sr-only">
143          {TAXONOMY.map((t) => (
144            <TabsTrigger key={t.id} value={t.id}>{t.label}</TabsTrigger>
145          ))}
146        </TabsList>
147
148        <Suspense fallback={<div className="text-xs text-muted-foreground p-4">Loading…</div>}>
149          <TabsContent value="scratchpad" className="mt-0">
150            <ScratchPad onCreateCard={handleCreateCard} />
151          </TabsContent>
152          <TabsContent value="workspace" className="mt-0">
153            <CardsNotesWorkspace />
154          </TabsContent>
155          <TabsContent value="stickynotes" className="mt-0">
156            <StickyNotesSimple />
157          </TabsContent>
158        </Suspense>
159      </Tabs>
160    </div>
161  );
162}
163
164`;export{e as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.