@anchrd/intel-ui 0.3.0 → 0.5.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 (52) hide show
  1. package/components.json +7 -1
  2. package/package.json +3 -1
  3. package/src/app/action-slot/action-slot.tsx +23 -0
  4. package/src/app/app-sidebar/app-sidebar.tsx +71 -0
  5. package/src/app/app-tree/app-tree.tsx +798 -0
  6. package/src/app/app.tsx +99 -54
  7. package/src/app/header-search/header-search.tsx +291 -0
  8. package/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
  9. package/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
  10. package/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
  11. package/src/app/tree-move/tree-move.tsx +197 -0
  12. package/src/app/user-footer/user-footer.tsx +103 -0
  13. package/src/app/view-toggle/view-toggle.tsx +77 -0
  14. package/src/blocknote-view/blocknote-view.tsx +19 -2
  15. package/src/branding/favicon.default.svg +2 -2
  16. package/src/branding/favicon.svg +2 -2
  17. package/src/components/ui/breadcrumb.tsx +102 -0
  18. package/src/components/ui/button.tsx +64 -0
  19. package/src/components/ui/collapsible.tsx +20 -0
  20. package/src/components/ui/command.tsx +160 -0
  21. package/src/components/ui/dialog.tsx +143 -0
  22. package/src/components/ui/dropdown-menu.tsx +162 -0
  23. package/src/components/ui/input.tsx +21 -0
  24. package/src/components/ui/separator.tsx +26 -0
  25. package/src/components/ui/sheet.tsx +136 -0
  26. package/src/components/ui/sidebar.tsx +693 -0
  27. package/src/components/ui/skeleton.tsx +13 -0
  28. package/src/components/ui/tooltip.tsx +51 -0
  29. package/src/data/intel-data-provider/intel-data-provider.ts +170 -85
  30. package/src/data/intel-data-provider/intel-data-provider.types.ts +64 -20
  31. package/src/document-link/document-link.tsx +132 -0
  32. package/src/flow-runs/flow-runs.tsx +225 -0
  33. package/src/flows/flows.tsx +684 -368
  34. package/src/flows/node-icon/node-icon.ts +28 -0
  35. package/src/flows/node-palette/node-palette.tsx +174 -0
  36. package/src/flows/node-palette/node-palette.types.ts +15 -0
  37. package/src/graph-pane/graph-pane.tsx +44 -0
  38. package/src/hooks/use-mobile.ts +19 -0
  39. package/src/i18n/en.json +188 -52
  40. package/src/knowledge/knowledge.tsx +133 -709
  41. package/src/knowledge-editor/knowledge-editor.tsx +169 -21
  42. package/src/knowledge-graph/knowledge-graph.ts +26 -24
  43. package/src/knowledge-graph/knowledge-graph.tsx +33 -24
  44. package/src/knowledge-table/knowledge-table.tsx +129 -0
  45. package/src/main.tsx +2 -2
  46. package/src/resource-menu/resource-menu.tsx +580 -0
  47. package/src/router/router.tsx +4 -0
  48. package/src/router/selection-search.ts +43 -0
  49. package/src/save-button/save-button.tsx +103 -0
  50. package/src/styles.css +91 -51
  51. package/src/theme/theme.ts +24 -0
  52. package/src/tools/tools.tsx +175 -158
@@ -0,0 +1,197 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { ChevronLeft, Folder } from "lucide-react";
3
+ import { useState } from "react";
4
+ import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
5
+ import { Modal } from "@/modal/modal.tsx";
6
+ import { useIntelRouterContext } from "@/router/router-context.ts";
7
+
8
+ // Where a row would land. `null` is the root of the shared tree; the title is carried along because
9
+ // the dialog names the destination and the row that supplied it is not always still on screen.
10
+ export type MoveDestination = { id: string | null; title: string };
11
+
12
+ export type MoveVerdict = "ok" | "self" | "descendant" | "same-place";
13
+
14
+ // The parent a row is filed under. Knowledge and Flows keep their own record (ADR-0004), so the
15
+ // answer is read from whichever one this row is, never from a merged shape.
16
+ export function parentOf(entry: TreeEntry): string | null {
17
+ return entry.type === "knowledge" ? entry.node.parentId : entry.flow.parentId;
18
+ }
19
+
20
+ /**
21
+ * ⚠️ The trap this ticket is built around: a node must not travel into its own descendants. The
22
+ * service refuses it (`move_cycle`, `knowledge.ts:421`), but a refusal that only arrives once the
23
+ * mouse button is already up is the one answer worse than no dragging at all — the drop has to be
24
+ * offered or withheld while the pointer is still moving.
25
+ *
26
+ * It costs no request. The tree already carries the ancestor path of every rendered row for its own
27
+ * cycle guard (`app-tree.tsx`), and "is this target inside the row I am dragging" is exactly the
28
+ * question that path answers: the dragged id appears above the target, or it does not.
29
+ */
30
+ export function moveVerdict(input: {
31
+ draggedId: string;
32
+ draggedParentId: string | null;
33
+ targetId: string | null;
34
+ targetAncestors: ReadonlySet<string>;
35
+ }): MoveVerdict {
36
+ if (input.targetId === input.draggedId) return "self";
37
+ if (input.targetId !== null && input.targetAncestors.has(input.draggedId)) return "descendant";
38
+ if (input.targetId === input.draggedParentId) return "same-place";
39
+ return "ok";
40
+ }
41
+
42
+ // ⚠️ Four refusals, four sentences. The service tells them apart by `code`
43
+ // (`knowledge.ts:331/335/338/361`, `flows.ts:205–217/752`), and so does this: one message for all
44
+ // of them would leave the reader guessing which of "you may not write there", "that is not a
45
+ // folder", "that would be a loop" and "somebody else was faster" they have just hit.
46
+ export function moveErrorKey(error: unknown): string {
47
+ const code =
48
+ typeof error === "object" && error !== null && "code" in error
49
+ ? String((error as { code: unknown }).code)
50
+ : null;
51
+ switch (code) {
52
+ case "knowledge_forbidden":
53
+ case "flow_edit_forbidden":
54
+ return "tree.move.failed.forbidden";
55
+ case "parent_not_folder":
56
+ return "tree.move.failed.notFolder";
57
+ case "move_cycle":
58
+ return "tree.move.failed.cycle";
59
+ case "update_conflict":
60
+ case "flow_update_conflict":
61
+ return "tree.move.failed.conflict";
62
+ case "flow_parent_not_found":
63
+ return "tree.move.failed.gone";
64
+ default:
65
+ return "tree.move.failed.other";
66
+ }
67
+ }
68
+
69
+ /**
70
+ * The move, named before it happens.
71
+ *
72
+ * ⚠️ Since #16 a share hangs on the folder and reaches everything beneath it (`db-grants.ts`,
73
+ * `nodeVerbQuery`), so moving a document is a change of who may read it — without the word "share"
74
+ * appearing anywhere. That is why every move is confirmed rather than performed on drop.
75
+ *
76
+ * What this deliberately does not do is name the people. `listKnowledgeGrants` answers with the
77
+ * grants sitting on that one folder, needs `share` on it (`knowledge.ts:459`), and says nothing
78
+ * about the grants above it that reach down. A list built from that would be short of exactly the
79
+ * entries that matter and would read as complete — "nobody is on this folder" while an ancestor
80
+ * opens it to the whole organization. Naming the mechanism truthfully beats enumerating it wrongly;
81
+ * an effective-audience answer is the server's to give, and no endpoint offers one today.
82
+ */
83
+ export function MoveDialog({
84
+ entry,
85
+ initial,
86
+ close,
87
+ submit,
88
+ }: {
89
+ entry: TreeEntry;
90
+ initial: MoveDestination | null;
91
+ close(): void;
92
+ submit(destination: MoveDestination): void;
93
+ }) {
94
+ const { data, i18n } = useIntelRouterContext();
95
+ const [destination, setDestination] = useState<MoveDestination | null>(initial);
96
+ // The browsed path, root first. It is also the descendant guard for the keyboard route: the row
97
+ // being moved is never offered, so its subtree can never be entered in the first place.
98
+ const [path, setPath] = useState<MoveDestination[]>([]);
99
+ const here: MoveDestination = path.at(-1) ?? { id: null, title: i18n.t("tree.move.root") };
100
+ const level = useQuery({
101
+ queryKey: ["tree", here.id],
102
+ queryFn: async () => await data.listTreeChildren(here.id),
103
+ enabled: destination === null,
104
+ });
105
+ const folders = (level.data ?? []).filter(
106
+ (child) => child.kind === "folder" && child.id !== entry.id,
107
+ );
108
+ const verdict = moveVerdict({
109
+ draggedId: entry.id,
110
+ draggedParentId: parentOf(entry),
111
+ targetId: here.id,
112
+ targetAncestors: new Set(path.map((step) => step.id).filter((id) => id !== null)),
113
+ });
114
+
115
+ return (
116
+ <Modal title={i18n.t("tree.move.title", { title: entry.title })} close={close}>
117
+ {destination === null ? (
118
+ <div className="space-y-3">
119
+ <p className="text-sm text-muted-foreground">{i18n.t("tree.move.pick")}</p>
120
+ <div className="flex items-center gap-2">
121
+ <button
122
+ type="button"
123
+ disabled={path.length === 0}
124
+ onClick={() => setPath((current) => current.slice(0, -1))}
125
+ aria-label={i18n.t("tree.move.up")}
126
+ className="rounded-md border p-1 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
127
+ >
128
+ <ChevronLeft aria-hidden="true" className="size-4" />
129
+ </button>
130
+ <p className="min-w-0 flex-1 truncate text-sm font-medium">{here.title}</p>
131
+ </div>
132
+ <ul className="max-h-56 space-y-1 overflow-y-auto">
133
+ {level.isPending ? (
134
+ <li className="px-2 py-1.5 text-sm text-muted-foreground">
135
+ {i18n.t("common.loading")}
136
+ </li>
137
+ ) : folders.length === 0 ? (
138
+ <li className="px-2 py-1.5 text-sm text-muted-foreground">
139
+ {i18n.t("tree.move.noFolders")}
140
+ </li>
141
+ ) : (
142
+ folders.map((folder) => (
143
+ <li key={folder.id}>
144
+ <button
145
+ type="button"
146
+ onClick={() => setPath((current) => [...current, folder])}
147
+ aria-label={i18n.t("tree.move.open", { title: folder.title })}
148
+ className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
149
+ >
150
+ <Folder aria-hidden="true" className="size-4 shrink-0" />
151
+ <span className="truncate">{folder.title}</span>
152
+ </button>
153
+ </li>
154
+ ))
155
+ )}
156
+ </ul>
157
+ <button
158
+ type="button"
159
+ disabled={verdict !== "ok"}
160
+ onClick={() => setDestination(here)}
161
+ className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
162
+ >
163
+ {i18n.t("tree.move.here", { title: here.title })}
164
+ </button>
165
+ {verdict === "same-place" ? (
166
+ <p className="text-sm text-muted-foreground">{i18n.t("tree.move.alreadyThere")}</p>
167
+ ) : null}
168
+ </div>
169
+ ) : (
170
+ <div className="space-y-4">
171
+ <p className="text-sm">{i18n.t("tree.move.destination", { title: destination.title })}</p>
172
+ {/* Not a footnote: this is the only place the change of audience is stated, and it is
173
+ stated before the move, not in the audit log afterwards. */}
174
+ <p role="note" className="rounded-md border border-destructive/50 p-3 text-sm">
175
+ {i18n.t("tree.move.sharingWarning", { title: destination.title })}
176
+ </p>
177
+ <div className="flex gap-2">
178
+ <button
179
+ type="button"
180
+ onClick={() => setDestination(null)}
181
+ className="flex-1 rounded-md border px-4 py-2 text-sm font-medium outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
182
+ >
183
+ {i18n.t("tree.move.elsewhere")}
184
+ </button>
185
+ <button
186
+ type="button"
187
+ onClick={() => submit(destination)}
188
+ className="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring"
189
+ >
190
+ {i18n.t("tree.move.confirm")}
191
+ </button>
192
+ </div>
193
+ </div>
194
+ )}
195
+ </Modal>
196
+ );
197
+ }
@@ -0,0 +1,103 @@
1
+ import { useMutation, useQuery } from "@tanstack/react-query";
2
+ import { useNavigate, useRouterState } from "@tanstack/react-router";
3
+ import { ChevronsUpDown, LogOut, User, Wrench } from "lucide-react";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
10
+ } from "@/components/ui/dropdown-menu";
11
+ import { SidebarMenu, SidebarMenuItem } from "@/components/ui/sidebar";
12
+ import { loginPath } from "@/data/intel-data-provider/intel-data-provider.ts";
13
+ import { useIntelRouterContext } from "@/router/router-context.ts";
14
+
15
+ /**
16
+ * Everything that belongs to the person rather than to the content, behind the person (#24).
17
+ *
18
+ * The three rows this used to be — the tool catalogue, the name, the sign-out — spent three lines of
19
+ * sidebar on things nobody opens twice a day. The name and the address stay on screen, because they
20
+ * are the answer to "who am I signed in as", which is a reading and not an action; the two actions
21
+ * move behind them.
22
+ *
23
+ * The catalogue itself is a live portal query with the signed-in user's own token (ADR-0003), so
24
+ * what it shows depends on who is asking.
25
+ */
26
+ export function UserFooter() {
27
+ const { data, i18n } = useIntelRouterContext();
28
+ const navigate = useNavigate();
29
+ const pathname = useRouterState({ select: (state) => state.location.pathname });
30
+ const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
31
+ const logout = useMutation({
32
+ mutationFn: () => data.logout(),
33
+ onSuccess: () => window.location.assign(loginPath()),
34
+ });
35
+
36
+ const user = session.data ?? null;
37
+ const primary = user
38
+ ? (user.name ?? user.email)
39
+ : session.isPending
40
+ ? i18n.t("common.loading")
41
+ : i18n.t("shell.userUnavailable");
42
+ const secondary = user?.name ? user.email : null;
43
+
44
+ return (
45
+ <SidebarMenu>
46
+ <SidebarMenuItem>
47
+ <DropdownMenu>
48
+ <DropdownMenuTrigger
49
+ aria-label={i18n.t("shell.userMenu", { name: primary })}
50
+ data-slot="signed-in-user"
51
+ data-active={pathname === "/tools"}
52
+ className="flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring data-[active=true]:bg-sidebar-accent data-[state=open]:bg-sidebar-accent"
53
+ >
54
+ <span className="flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-accent-foreground">
55
+ <User aria-hidden="true" className="size-4" />
56
+ </span>
57
+ {/* Read out in both states; sighted users see it in neither. It is what tells a screen
58
+ reader that the name below is a name and not a heading. */}
59
+ <span className="sr-only">{i18n.t("shell.signedInUser")}</span>
60
+ <span className="grid min-w-0 flex-1 leading-tight">
61
+ <span className="truncate font-medium">{primary}</span>
62
+ {secondary ? (
63
+ <span className="truncate text-xs text-muted-foreground">{secondary}</span>
64
+ ) : null}
65
+ </span>
66
+ <ChevronsUpDown aria-hidden="true" className="ml-auto size-4 shrink-0" />
67
+ </DropdownMenuTrigger>
68
+ {/* Upwards and as wide as its trigger: it opens from the bottom edge of the screen, and
69
+ a menu that has to be hunted for beside its button is a menu that was not found. */}
70
+ <DropdownMenuContent
71
+ side="top"
72
+ align="start"
73
+ className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
74
+ >
75
+ <DropdownMenuItem onSelect={() => void navigate({ to: "/tools" })}>
76
+ <Wrench aria-hidden="true" />
77
+ {i18n.t("nav.tools")}
78
+ </DropdownMenuItem>
79
+ <DropdownMenuSeparator />
80
+ <DropdownMenuItem
81
+ disabled={logout.isPending}
82
+ onSelect={() => logout.mutate()}
83
+ className="text-destructive focus:text-destructive"
84
+ >
85
+ <LogOut aria-hidden="true" />
86
+ {i18n.t("auth.signOut")}
87
+ </DropdownMenuItem>
88
+ </DropdownMenuContent>
89
+ </DropdownMenu>
90
+ </SidebarMenuItem>
91
+ {/* ⚠️ Outside the menu on purpose. Choosing sign-out closes the popup, so a refusal rendered
92
+ inside it would be gone in the same frame it was written — the one message that must
93
+ survive is the one saying you are still signed in. */}
94
+ {logout.isError ? (
95
+ <SidebarMenuItem>
96
+ <p role="alert" className="px-2 py-1 text-sm text-destructive">
97
+ {i18n.t("auth.signOutFailed")}
98
+ </p>
99
+ </SidebarMenuItem>
100
+ ) : null}
101
+ </SidebarMenu>
102
+ );
103
+ }
@@ -0,0 +1,77 @@
1
+ import { useNavigate, useRouterState } from "@tanstack/react-router";
2
+ import { History, Network, PencilRuler } from "lucide-react";
3
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
4
+ import { useIntelRouterContext } from "@/router/router-context.ts";
5
+ import { type IntelView, viewFrom } from "@/router/selection-search.ts";
6
+
7
+ const icons = { editor: PencilRuler, graph: Network, runs: History } as const;
8
+ const labels = {
9
+ editor: "view.showEditor",
10
+ graph: "view.showGraph",
11
+ runs: "view.showRuns",
12
+ } as const;
13
+
14
+ /**
15
+ * The switch between the ways the level the sidebar has selected can be shown: editor ↔ graph
16
+ * everywhere (#19), and editor ↔ graph ↔ runs on a flow (#35).
17
+ *
18
+ * **Head, not row — the decision and its reason.** The switch changes how the *current view* is
19
+ * shown, and the current view is what the header already names in its breadcrumb; a state of the
20
+ * view belongs beside its name. In the tree row it would have to live inside the row's overflow
21
+ * menu (#24), and switching back would mean opening that menu again — a toggle that costs two
22
+ * clicks in one direction and two in the other is no longer a toggle. The objection the ticket
23
+ * raises against the header is answered by not rendering the switch where there is no graph: a
24
+ * document, an attachment and the tools screen never mount it, so it cannot stand there with
25
+ * nothing to point at.
26
+ *
27
+ * **Icon, no text.** The visible label is gone and nothing else: the accessible name is on the
28
+ * button and repeated as a tooltip, so the header stops growing with every new view while a screen
29
+ * reader still hears the whole sentence.
30
+ *
31
+ * ⚠️ It offers the views you are *not* in, one button each, rather than cycling through them. With
32
+ * two views that is the single toggle #19 decided on, unchanged. With three, a cycle would put the
33
+ * runs two clicks away from the graph and make the button's meaning depend on where you already
34
+ * are — every destination stays one click, and every button keeps one name.
35
+ */
36
+ export function ViewToggle({ views = ["editor", "graph"] }: { views?: readonly IntelView[] }) {
37
+ const { i18n } = useIntelRouterContext();
38
+ const navigate = useNavigate();
39
+ const state = useRouterState({
40
+ select: (current) => ({
41
+ pathname: current.location.pathname,
42
+ search: current.location.search as Record<string, unknown>,
43
+ view: viewFrom(current.location.search),
44
+ }),
45
+ });
46
+ return (
47
+ // Its own provider: the shell supplies one, but these buttons are portalled into the header
48
+ // from a screen and have to keep working wherever they are mounted.
49
+ <TooltipProvider delayDuration={300}>
50
+ {views
51
+ .filter((view) => view !== state.view)
52
+ .map((view) => {
53
+ const label = i18n.t(labels[view]);
54
+ const Icon = icons[view];
55
+ return (
56
+ <Tooltip key={view}>
57
+ <TooltipTrigger
58
+ type="button"
59
+ aria-label={label}
60
+ onClick={() => {
61
+ const { view: _view, ...rest } = state.search;
62
+ void navigate({
63
+ to: state.pathname,
64
+ search: view === "editor" ? rest : { ...rest, view },
65
+ });
66
+ }}
67
+ className="inline-flex size-8 items-center justify-center rounded-md border bg-background outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
68
+ >
69
+ <Icon aria-hidden="true" className="size-4" />
70
+ </TooltipTrigger>
71
+ <TooltipContent>{label}</TooltipContent>
72
+ </Tooltip>
73
+ );
74
+ })}
75
+ </TooltipProvider>
76
+ );
77
+ }
@@ -1,13 +1,30 @@
1
+ import { type DefaultReactSuggestionItem, getDefaultReactSlashMenuItems } from "@blocknote/react";
1
2
  import { BlockNoteView as LibraryBlockNoteView } from "@blocknote/shadcn";
2
- import type { ComponentType } from "react";
3
+ import type { ComponentType, ReactNode } from "react";
3
4
 
4
5
  // BlockNote 0.52's generic view declaration conflicts with exactOptionalPropertyTypes even though
5
6
  // useCreateBlockNote returns its matching default editor. Keep that upstream type seam in one place.
6
7
  const CompatibleBlockNoteView = LibraryBlockNoteView as unknown as ComponentType<{
7
8
  editor: unknown;
8
9
  onChange(): void;
10
+ children?: ReactNode;
9
11
  }>;
10
12
 
11
- export function BlockNoteView(props: { editor: unknown; onChange(): void }) {
13
+ // `children` is how BlockNote mounts its own controllers — the slash menu among them — inside the
14
+ // editor's context. It travels through this seam rather than around it, so there is still one place
15
+ // that knows about the library's types.
16
+ export function BlockNoteView(props: { editor: unknown; onChange(): void; children?: ReactNode }) {
12
17
  return <CompatibleBlockNoteView {...props} />;
13
18
  }
19
+
20
+ // The same seam once more: the parameter type of BlockNote's own slash-menu helper does not accept
21
+ // an editor built from a schema of ours, because `heading`'s optional `isToggleable` prop fails the
22
+ // library's own `PropSchema` constraint under `exactOptionalPropertyTypes`. The editor is exactly
23
+ // what the helper wants at run time — it is the declaration that cannot say so.
24
+ const compatibleDefaultSlashMenuItems = getDefaultReactSlashMenuItems as unknown as (
25
+ editor: unknown,
26
+ ) => DefaultReactSuggestionItem[];
27
+
28
+ export function defaultSlashMenuItems(editor: unknown): DefaultReactSuggestionItem[] {
29
+ return compatibleDefaultSlashMenuItems(editor);
30
+ }
@@ -1,5 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
2
  <title>Intel</title>
3
- <rect width="32" height="32" rx="8" fill="oklch(0.45 0.17 265)"/>
4
- <path d="M10 10h12v12H10zM7 14h3m12 0h3M14 7v3m4-3v3M14 22v3m4-3v3" fill="none" stroke="oklch(0.98 0.01 265)" stroke-width="2" stroke-linecap="round"/>
3
+ <rect width="32" height="32" rx="8" fill="oklch(0.205 0 0)"/>
4
+ <path d="M10 10h12v12H10zM7 14h3m12 0h3M14 7v3m4-3v3M14 22v3m4-3v3" fill="none" stroke="oklch(0.985 0 0)" stroke-width="2" stroke-linecap="round"/>
5
5
  </svg>
@@ -1,5 +1,5 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
2
  <title>Intel</title>
3
- <rect width="32" height="32" rx="8" fill="oklch(0.45 0.17 265)"/>
4
- <path d="M10 10h12v12H10zM7 14h3m12 0h3M14 7v3m4-3v3M14 22v3m4-3v3" fill="none" stroke="oklch(0.98 0.01 265)" stroke-width="2" stroke-linecap="round"/>
3
+ <rect width="32" height="32" rx="8" fill="oklch(0.205 0 0)"/>
4
+ <path d="M10 10h12v12H10zM7 14h3m12 0h3M14 7v3m4-3v3M14 22v3m4-3v3" fill="none" stroke="oklch(0.985 0 0)" stroke-width="2" stroke-linecap="round"/>
5
5
  </svg>
@@ -0,0 +1,102 @@
1
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
2
+ import { Slot } from "radix-ui";
3
+ import type * as React from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
9
+ }
10
+
11
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12
+ return (
13
+ <ol
14
+ data-slot="breadcrumb-list"
15
+ className={cn(
16
+ "flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25
+ return (
26
+ <li
27
+ data-slot="breadcrumb-item"
28
+ className={cn("inline-flex items-center gap-1.5", className)}
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function BreadcrumbLink({
35
+ asChild,
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<"a"> & {
39
+ asChild?: boolean;
40
+ }) {
41
+ const Comp = asChild ? Slot.Root : "a";
42
+
43
+ return (
44
+ <Comp
45
+ data-slot="breadcrumb-link"
46
+ className={cn("transition-colors hover:text-foreground", className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
52
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53
+ return (
54
+ <span
55
+ data-slot="breadcrumb-page"
56
+ role="link"
57
+ aria-disabled="true"
58
+ aria-current="page"
59
+ className={cn("font-normal text-foreground", className)}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
66
+ return (
67
+ <li
68
+ data-slot="breadcrumb-separator"
69
+ role="presentation"
70
+ aria-hidden="true"
71
+ className={cn("[&>svg]:size-3.5", className)}
72
+ {...props}
73
+ >
74
+ {children ?? <ChevronRight />}
75
+ </li>
76
+ );
77
+ }
78
+
79
+ function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) {
80
+ return (
81
+ <span
82
+ data-slot="breadcrumb-ellipsis"
83
+ role="presentation"
84
+ aria-hidden="true"
85
+ className={cn("flex size-9 items-center justify-center", className)}
86
+ {...props}
87
+ >
88
+ <MoreHorizontal className="size-4" />
89
+ <span className="sr-only">More</span>
90
+ </span>
91
+ );
92
+ }
93
+
94
+ export {
95
+ Breadcrumb,
96
+ BreadcrumbEllipsis,
97
+ BreadcrumbItem,
98
+ BreadcrumbLink,
99
+ BreadcrumbList,
100
+ BreadcrumbPage,
101
+ BreadcrumbSeparator,
102
+ };
@@ -0,0 +1,64 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import { Slot } from "radix-ui";
3
+ import type * as React from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ // The registry hard-codes the destructive label colour. `--destructive-foreground` exists
14
+ // and a customer theme has to be able to move it, so the token replaces the fixed colour.
15
+ destructive:
16
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
17
+ outline:
18
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
19
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
20
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
26
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
30
+ "icon-sm": "size-8",
31
+ "icon-lg": "size-10",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: "default",
36
+ size: "default",
37
+ },
38
+ },
39
+ );
40
+
41
+ function Button({
42
+ className,
43
+ variant = "default",
44
+ size = "default",
45
+ asChild = false,
46
+ ...props
47
+ }: React.ComponentProps<"button"> &
48
+ VariantProps<typeof buttonVariants> & {
49
+ asChild?: boolean;
50
+ }) {
51
+ const Comp = asChild ? Slot.Root : "button";
52
+
53
+ return (
54
+ <Comp
55
+ data-slot="button"
56
+ data-variant={variant}
57
+ data-size={size}
58
+ className={cn(buttonVariants({ variant, size, className }))}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ export { Button, buttonVariants };
@@ -0,0 +1,20 @@
1
+ import { Collapsible as CollapsiblePrimitive } from "radix-ui";
2
+ import type * as React from "react";
3
+
4
+ function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
5
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
6
+ }
7
+
8
+ function CollapsibleTrigger({
9
+ ...props
10
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
11
+ return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />;
12
+ }
13
+
14
+ function CollapsibleContent({
15
+ ...props
16
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
17
+ return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />;
18
+ }
19
+
20
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger };