kennel-plugins 0.1.2 → 0.1.4

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.
@@ -3752,7 +3752,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
3752
3752
  const _normalized = cached(() => normalizeDef(def));
3753
3753
  const generateFastpass = (shape) => {
3754
3754
  const doc = new Doc(["shape", "payload", "ctx"]);
3755
- const normalized2 = _normalized.value;
3755
+ const normalized = _normalized.value;
3756
3756
  const parseStr = (key) => {
3757
3757
  const k = esc(key);
3758
3758
  return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
@@ -3760,11 +3760,11 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
3760
3760
  doc.write(`const input = payload.value;`);
3761
3761
  const ids = /* @__PURE__ */ Object.create(null);
3762
3762
  let counter = 0;
3763
- for (const key of normalized2.keys) {
3763
+ for (const key of normalized.keys) {
3764
3764
  ids[key] = `key_${counter++}`;
3765
3765
  }
3766
3766
  doc.write(`const newResult = {};`);
3767
- for (const key of normalized2.keys) {
3767
+ for (const key of normalized.keys) {
3768
3768
  const id = ids[key];
3769
3769
  const k = esc(key);
3770
3770
  const schema = shape[key];
@@ -15140,23 +15140,23 @@ function fromJSONSchema(schema, params) {
15140
15140
  if (typeof schema === "boolean") {
15141
15141
  return schema ? z.any() : z.never();
15142
15142
  }
15143
- let normalized2;
15143
+ let normalized;
15144
15144
  try {
15145
- normalized2 = JSON.parse(JSON.stringify(schema));
15145
+ normalized = JSON.parse(JSON.stringify(schema));
15146
15146
  } catch {
15147
15147
  throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
15148
15148
  }
15149
- const version2 = detectVersion(normalized2, params?.defaultTarget);
15150
- const defs = normalized2.$defs || normalized2.definitions || {};
15149
+ const version2 = detectVersion(normalized, params?.defaultTarget);
15150
+ const defs = normalized.$defs || normalized.definitions || {};
15151
15151
  const ctx = {
15152
15152
  version: version2,
15153
15153
  defs,
15154
15154
  refs: /* @__PURE__ */ new Map(),
15155
15155
  processing: /* @__PURE__ */ new Set(),
15156
- rootSchema: normalized2,
15156
+ rootSchema: normalized,
15157
15157
  registry: params?.registry ?? globalRegistry
15158
15158
  };
15159
- return convertSchema(normalized2, ctx);
15159
+ return convertSchema(normalized, ctx);
15160
15160
  }
15161
15161
 
15162
15162
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/coerce.js
@@ -15270,13 +15270,6 @@ var project = external_exports.object({
15270
15270
  relativePath: external_exports.string(),
15271
15271
  worktree: external_exports.string()
15272
15272
  });
15273
- var skill = external_exports.object({
15274
- description: external_exports.string().optional(),
15275
- name: external_exports.string(),
15276
- openHref: external_exports.string().optional(),
15277
- relativePath: external_exports.string(),
15278
- source: external_exports.enum(["global", "local"])
15279
- });
15280
15273
  var session = external_exports.object({
15281
15274
  client: external_exports.enum(["claude", "codex", "cursor", "opencode", "pi"]),
15282
15275
  href: external_exports.string().optional(),
@@ -15306,6 +15299,7 @@ var workspace = external_exports.object({
15306
15299
  var plugin = external_exports.object({
15307
15300
  count: external_exports.number().optional(),
15308
15301
  error: external_exports.string().optional(),
15302
+ icon: external_exports.object({ paths: external_exports.array(external_exports.string().min(1)).min(1) }).optional(),
15309
15303
  id: external_exports.string(),
15310
15304
  label: external_exports.string()
15311
15305
  });
@@ -15323,7 +15317,6 @@ var folderMetadataSchema = external_exports.object({
15323
15317
  plugins: plugins.optional(),
15324
15318
  projects: items(project),
15325
15319
  sessions: items(session),
15326
- skills: items(skill).optional(),
15327
15320
  workspace: workspace.optional()
15328
15321
  });
15329
15322
  var collection = coreCollectionSchema;
@@ -15347,7 +15340,6 @@ var metadataSchemas = {
15347
15340
  configWarnings: external_exports.array(external_exports.string()).optional(),
15348
15341
  plugins: plugins.optional()
15349
15342
  }),
15350
- skills: external_exports.object({ ...folder, skills: items(skill) }),
15351
15343
  // Legacy selectors currently return the complete metadata payload.
15352
15344
  sessions: folderMetadataSchema,
15353
15345
  calendar: folderMetadataSchema,
@@ -15527,7 +15519,7 @@ function url2(value) {
15527
15519
  }
15528
15520
  function tabSection(tab) {
15529
15521
  if (tab === "files") return "notes";
15530
- if (tab === "projects" || tab === "sessions" || tab === "skills") return tab;
15522
+ if (tab === "projects" || tab === "sessions") return tab;
15531
15523
  return void 0;
15532
15524
  }
15533
15525
  function decodeFolderPathId(id) {
@@ -15579,7 +15571,7 @@ function parseViewerRoute(value) {
15579
15571
  }
15580
15572
  };
15581
15573
  }
15582
- if (section === "notes" || section === "projects" || section === "sessions" || section === "skills") {
15574
+ if (section === "notes" || section === "projects" || section === "sessions") {
15583
15575
  return { ...route, section };
15584
15576
  }
15585
15577
  if (section === "document" && parts[3]) {
@@ -15872,461 +15864,11 @@ function FilePreviewList({ paths }) {
15872
15864
  return /* @__PURE__ */ jsx5("span", { className: "flex min-w-0 flex-wrap gap-2", children: paths.map((path) => /* @__PURE__ */ jsx5(FilePreview, { path }, path)) });
15873
15865
  }
15874
15866
 
15875
- // ../ui/src/components/folder-filter-bar.tsx
15876
- import { createContext, useContext } from "react";
15877
- import { Check as Check2, Folder, FolderSearch } from "lucide-react";
15878
-
15879
- // ../ui/src/components/badge.tsx
15880
- import { cva as cva2 } from "class-variance-authority";
15881
- import { Slot as SlotPrimitive } from "radix-ui";
15882
- import { jsx as jsx6 } from "react/jsx-runtime";
15883
- var badgeVariants = cva2(
15884
- "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
15885
- {
15886
- variants: {
15887
- variant: {
15888
- default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
15889
- secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
15890
- destructive: "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15891
- outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground"
15892
- }
15893
- },
15894
- defaultVariants: {
15895
- variant: "default"
15896
- }
15897
- }
15898
- );
15899
- function Badge({
15900
- className,
15901
- variant,
15902
- asChild = false,
15903
- ...props
15904
- }) {
15905
- const Comp = asChild ? SlotPrimitive.Slot : "span";
15906
- return /* @__PURE__ */ jsx6(
15907
- Comp,
15908
- {
15909
- "data-slot": "badge",
15910
- className: cn(badgeVariants({ variant }), className),
15911
- ...props
15912
- }
15913
- );
15914
- }
15915
-
15916
- // ../ui/src/components/command.tsx
15917
- import { Command as CommandPrimitive } from "cmdk";
15918
- import { SearchIcon } from "lucide-react";
15919
- import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
15920
- function Command({
15921
- className,
15922
- ...props
15923
- }) {
15924
- return /* @__PURE__ */ jsx7(
15925
- CommandPrimitive,
15926
- {
15927
- "data-slot": "command",
15928
- className: cn(
15929
- `bg-popover text-popover-foreground flex h-full w-full flex-col
15930
- overflow-hidden rounded-md`,
15931
- className
15932
- ),
15933
- ...props
15934
- }
15935
- );
15936
- }
15937
- function CommandInput({
15938
- className,
15939
- ...props
15940
- }) {
15941
- return /* @__PURE__ */ jsxs3(
15942
- "div",
15943
- {
15944
- "data-slot": "command-input-wrapper",
15945
- className: "flex h-9 items-center gap-2 border-b px-3",
15946
- children: [
15947
- /* @__PURE__ */ jsx7(SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
15948
- /* @__PURE__ */ jsx7(
15949
- CommandPrimitive.Input,
15950
- {
15951
- "data-slot": "command-input",
15952
- className: cn(
15953
- `placeholder:text-muted-foreground flex h-10 w-full rounded-md
15954
- bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed
15955
- disabled:opacity-50`,
15956
- className
15957
- ),
15958
- ...props
15959
- }
15960
- )
15961
- ]
15962
- }
15963
- );
15964
- }
15965
- function CommandList({
15966
- className,
15967
- ...props
15968
- }) {
15969
- return /* @__PURE__ */ jsx7(
15970
- CommandPrimitive.List,
15971
- {
15972
- "data-slot": "command-list",
15973
- className: cn(
15974
- "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
15975
- className
15976
- ),
15977
- ...props
15978
- }
15979
- );
15980
- }
15981
- function CommandEmpty({
15982
- ...props
15983
- }) {
15984
- return /* @__PURE__ */ jsx7(
15985
- CommandPrimitive.Empty,
15986
- {
15987
- "data-slot": "command-empty",
15988
- className: "py-6 text-center text-sm",
15989
- ...props
15990
- }
15991
- );
15992
- }
15993
- function CommandGroup({
15994
- className,
15995
- ...props
15996
- }) {
15997
- return /* @__PURE__ */ jsx7(
15998
- CommandPrimitive.Group,
15999
- {
16000
- "data-slot": "command-group",
16001
- className: cn(
16002
- `text-foreground [&_[cmdk-group-heading]]:text-muted-foreground
16003
- overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2
16004
- [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs
16005
- [&_[cmdk-group-heading]]:font-medium`,
16006
- className
16007
- ),
16008
- ...props
16009
- }
16010
- );
16011
- }
16012
- function CommandItem({
16013
- className,
16014
- ...props
16015
- }) {
16016
- return /* @__PURE__ */ jsx7(
16017
- CommandPrimitive.Item,
16018
- {
16019
- "data-slot": "command-item",
16020
- className: cn(
16021
- `data-[selected=true]:bg-accent
16022
- data-[selected=true]:text-accent-foreground
16023
- [&_svg:not([class*='text-'])]:text-muted-foreground relative flex
16024
- cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm
16025
- outline-hidden select-none data-[disabled=true]:pointer-events-none
16026
- data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none
16027
- [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
16028
- className
16029
- ),
16030
- ...props
16031
- }
16032
- );
16033
- }
16034
-
16035
- // ../ui/src/components/popover.tsx
16036
- import { Popover as PopoverPrimitive } from "radix-ui";
16037
- import { jsx as jsx8 } from "react/jsx-runtime";
16038
- function Popover({
16039
- ...props
16040
- }) {
16041
- return /* @__PURE__ */ jsx8(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
16042
- }
16043
- function PopoverTrigger({
16044
- ...props
16045
- }) {
16046
- return /* @__PURE__ */ jsx8(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
16047
- }
16048
- function PopoverContent({
16049
- className,
16050
- align = "center",
16051
- sideOffset = 4,
16052
- ...props
16053
- }) {
16054
- return /* @__PURE__ */ jsx8(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx8(
16055
- PopoverPrimitive.Content,
16056
- {
16057
- "data-slot": "popover-content",
16058
- align,
16059
- sideOffset,
16060
- className: cn(
16061
- `bg-popover text-popover-foreground data-[state=open]:animate-in
16062
- data-[state=closed]:animate-out data-[state=closed]:fade-out-0
16063
- data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95
16064
- data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2
16065
- data-[side=left]:slide-in-from-right-2
16066
- data-[side=right]:slide-in-from-left-2
16067
- data-[side=top]:slide-in-from-bottom-2 z-50 w-72
16068
- origin-(--radix-popover-content-transform-origin) rounded-md border
16069
- p-4 shadow-md outline-hidden`,
16070
- className
16071
- ),
16072
- ...props
16073
- }
16074
- ) });
16075
- }
16076
-
16077
- // ../ui/src/components/folder-filter-bar.tsx
16078
- import { Fragment, jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
16079
- var FolderFilterIconContext = createContext(() => /* @__PURE__ */ jsx9(Folder, { "aria-hidden": "true" }));
16080
- function folderIcon(renderIcon, folder2) {
16081
- return /* @__PURE__ */ jsx9(
16082
- "span",
16083
- {
16084
- "aria-hidden": "true",
16085
- className: "inline-flex size-4 shrink-0 items-center justify-center\n [&>img]:size-full [&>img]:rounded-sm [&>img]:object-contain\n [&>svg]:size-full",
16086
- children: renderIcon(folder2)
16087
- }
16088
- );
16089
- }
16090
- function FolderFilterBar({
16091
- allLabel = "All folders",
16092
- ariaLabel = "Filter by folder",
16093
- choices,
16094
- onSelect,
16095
- selected,
16096
- total
16097
- }) {
16098
- const renderIcon = useContext(FolderFilterIconContext);
16099
- const isSelected = (folder2) => selected.includes(folder2);
16100
- const toggle = (folder2) => isSelected(folder2) ? onSelect(selected.filter((entry) => entry !== folder2)) : onSelect([...selected, folder2]);
16101
- return /* @__PURE__ */ jsxs4(Popover, { children: [
16102
- /* @__PURE__ */ jsx9(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs4(
16103
- Button,
16104
- {
16105
- "aria-label": ariaLabel,
16106
- className: "gap-1.5 border-[#dde1dc] bg-white text-[0.82rem]\n font-medium text-[#4a554b] hover:bg-[#f3f5f2]",
16107
- size: "sm",
16108
- type: "button",
16109
- variant: "outline",
16110
- children: [
16111
- /* @__PURE__ */ jsx9(FolderSearch, { "aria-hidden": "true", className: "size-4 text-[#6c7a6d]" }),
16112
- "Folder",
16113
- /* @__PURE__ */ jsx9(
16114
- Badge,
16115
- {
16116
- className: "ml-0.5 bg-[#dce8de] px-1.5 text-[#3f6048]",
16117
- variant: "secondary",
16118
- children: selected.length > 0 ? selected.length : total
16119
- }
16120
- )
16121
- ]
16122
- }
16123
- ) }),
16124
- /* @__PURE__ */ jsx9(
16125
- PopoverContent,
16126
- {
16127
- align: "start",
16128
- className: "w-72 overflow-hidden p-0",
16129
- sideOffset: 6,
16130
- children: /* @__PURE__ */ jsxs4(Command, { children: [
16131
- /* @__PURE__ */ jsx9(CommandInput, { placeholder: "Search folders\u2026" }),
16132
- /* @__PURE__ */ jsxs4(CommandList, { children: [
16133
- /* @__PURE__ */ jsx9(CommandEmpty, { children: "No folders match" }),
16134
- /* @__PURE__ */ jsx9(CommandGroup, { children: /* @__PURE__ */ jsxs4(CommandItem, { onSelect: () => onSelect([]), value: "__all__", children: [
16135
- /* @__PURE__ */ jsx9(
16136
- FolderSearch,
16137
- {
16138
- "aria-hidden": "true",
16139
- className: "size-4 text-[#6c7a6d]"
16140
- }
16141
- ),
16142
- /* @__PURE__ */ jsx9("span", { children: allLabel }),
16143
- /* @__PURE__ */ jsx9(
16144
- "span",
16145
- {
16146
- className: cn(
16147
- "ml-auto text-[0.75rem] font-medium",
16148
- selected.length === 0 ? "text-[#3f6048]" : "text-[#6c7a6d]"
16149
- ),
16150
- children: total
16151
- }
16152
- ),
16153
- selected.length === 0 ? /* @__PURE__ */ jsx9(Check2, { "aria-hidden": "true", className: "size-4 text-[#3f6048]" }) : null
16154
- ] }) }),
16155
- /* @__PURE__ */ jsx9(CommandGroup, { heading: "Folders", children: choices.map((choice) => {
16156
- const active = isSelected(choice.folder);
16157
- return /* @__PURE__ */ jsxs4(
16158
- CommandItem,
16159
- {
16160
- onSelect: () => toggle(choice.folder),
16161
- title: choice.folder,
16162
- value: choice.folder,
16163
- children: [
16164
- folderIcon(renderIcon, choice.folder),
16165
- /* @__PURE__ */ jsx9("span", { className: "truncate", children: choice.relativePath === "" ? "This folder" : choice.label }),
16166
- /* @__PURE__ */ jsx9(
16167
- "span",
16168
- {
16169
- className: cn(
16170
- "ml-auto text-[0.75rem] font-medium",
16171
- active ? "text-[#3f6048]" : "text-[#6c7a6d]"
16172
- ),
16173
- children: choice.count
16174
- }
16175
- ),
16176
- active ? /* @__PURE__ */ jsx9(
16177
- Check2,
16178
- {
16179
- "aria-hidden": "true",
16180
- className: "size-4 shrink-0 text-[#3f6048]"
16181
- }
16182
- ) : null
16183
- ]
16184
- },
16185
- choice.folder
16186
- );
16187
- }) })
16188
- ] })
16189
- ] })
16190
- }
16191
- )
16192
- ] });
16193
- }
16194
-
16195
- // ../ui/src/hooks/use-folder-filter.ts
16196
- import { useCallback as useCallback2, useMemo, useState as useState3 } from "react";
16197
-
16198
- // ../ui/src/lib/folder-filter.ts
16199
- function normalized(value) {
16200
- return value.replaceAll("\\", "/").replace(/\/+$/, "");
16201
- }
16202
- function folderFilterRelativePath(folder2, rootFolder) {
16203
- const root = normalized(rootFolder);
16204
- const candidate = normalized(folder2);
16205
- if (!root || !candidate) return null;
16206
- if (candidate === root) return "";
16207
- if (!candidate.startsWith(`${root}/`)) return null;
16208
- const relative = candidate.slice(root.length + 1);
16209
- if (!relative) return null;
16210
- if (relative.split("/").some((segment) => segment === ".." || segment === "."))
16211
- return null;
16212
- return relative;
16213
- }
16214
- function rootLabel(rootFolder) {
16215
- return normalized(rootFolder).split("/").filter((segment) => Boolean(segment)).at(-1) ?? "This folder";
16216
- }
16217
- function baseName(value) {
16218
- return normalized(value).split("/").filter((segment) => Boolean(segment)).at(-1) ?? value;
16219
- }
16220
- function folderFilterChoices(items2, rootFolder, getFolder, options) {
16221
- const counts = /* @__PURE__ */ new Map();
16222
- for (const item of items2) {
16223
- const folder2 = getFolder(item);
16224
- if (!folder2) continue;
16225
- const contained = folderFilterRelativePath(folder2, rootFolder);
16226
- if (contained === null && !options?.allowOutside) continue;
16227
- counts.set(folder2, (counts.get(folder2) ?? 0) + 1);
16228
- }
16229
- const choices = [];
16230
- for (const [folder2, count] of counts) {
16231
- const contained = folderFilterRelativePath(folder2, rootFolder);
16232
- const relativePath = contained ?? (options?.allowOutside ? baseName(folder2) : "");
16233
- choices.push({
16234
- count,
16235
- folder: folder2,
16236
- label: relativePath || rootLabel(rootFolder),
16237
- relativePath
16238
- });
16239
- }
16240
- return choices.sort((a, b) => {
16241
- if (a.relativePath === "") return -1;
16242
- if (b.relativePath === "") return 1;
16243
- return a.relativePath.localeCompare(b.relativePath);
16244
- });
16245
- }
16246
- function matchTarget(folder2, target, mode) {
16247
- if (!folder2) return false;
16248
- const candidate = normalized(folder2);
16249
- const goal = normalized(target);
16250
- if (mode === "subtree")
16251
- return candidate === goal || candidate.startsWith(`${goal}/`);
16252
- return candidate === goal;
16253
- }
16254
- function folderFilterMatches(folder2, selected, mode = "exact") {
16255
- if (selected === null || selected === "") return true;
16256
- if (Array.isArray(selected)) {
16257
- if (selected.length === 0) return true;
16258
- return selected.some((entry) => folderFilterMatches(folder2, entry, mode));
16259
- }
16260
- return matchTarget(folder2, selected, mode);
16261
- }
16262
- function applyFolderFilter(input) {
16263
- const {
16264
- allowOutside,
16265
- getFolder,
16266
- items: items2,
16267
- mode = "exact",
16268
- rootFolder,
16269
- selected
16270
- } = input;
16271
- const choices = folderFilterChoices(items2, rootFolder, getFolder, {
16272
- allowOutside
16273
- });
16274
- const rawEntries = selected === null ? [] : Array.isArray(selected) ? selected : [selected];
16275
- const effectiveSelected = rawEntries.filter(
16276
- (entry) => items2.some((item) => folderFilterMatches(getFolder(item), entry, mode))
16277
- );
16278
- const visibleItems = effectiveSelected.length === 0 ? [...items2] : items2.filter(
16279
- (item) => folderFilterMatches(getFolder(item), effectiveSelected, mode)
16280
- );
16281
- return { choices, effectiveSelected, total: items2.length, visibleItems };
16282
- }
16283
-
16284
- // ../ui/src/hooks/use-folder-filter.ts
16285
- function useFolderFilter(items2, options) {
16286
- const { allowOutside, getFolder, mode, rootFolder } = options;
16287
- const [selected, setSelectedState] = useState3(() => {
16288
- const initial = options.initialFolder;
16289
- if (initial === null || initial === void 0) return [];
16290
- return Array.isArray(initial) ? initial : [initial];
16291
- });
16292
- const setSelected = useCallback2((folder2) => {
16293
- setSelectedState(
16294
- folder2 === null || folder2 === void 0 ? [] : Array.isArray(folder2) ? folder2 : [folder2]
16295
- );
16296
- }, []);
16297
- const toggle = useCallback2((folder2) => {
16298
- setSelectedState(
16299
- (current) => current.includes(folder2) ? current.filter((entry) => entry !== folder2) : [...current, folder2]
16300
- );
16301
- }, []);
16302
- const reset = useCallback2(() => setSelectedState([]), []);
16303
- const result = useMemo(
16304
- () => applyFolderFilter({
16305
- allowOutside,
16306
- getFolder,
16307
- items: items2,
16308
- mode,
16309
- rootFolder,
16310
- selected
16311
- }),
16312
- [allowOutside, getFolder, items2, mode, rootFolder, selected]
16313
- );
16314
- return {
16315
- ...result,
16316
- selected,
16317
- setSelected,
16318
- toggle,
16319
- reset
16320
- };
16321
- }
16322
-
16323
15867
  export {
16324
15868
  EntityPage,
16325
15869
  EntityStatus,
16326
15870
  EntityError,
16327
15871
  CopyContextButton,
16328
15872
  FilePreview,
16329
- FilePreviewList,
16330
- FolderFilterBar,
16331
- useFolderFilter
15873
+ FilePreviewList
16332
15874
  };
@@ -0,0 +1,12 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /** Host boundary. The host and plugin must resolve this module once. */
5
+ declare function FolderPathProvider({ folder, children, }: {
6
+ folder: string;
7
+ children: ReactNode;
8
+ }): react.FunctionComponentElement<react.ProviderProps<string | undefined>>;
9
+ /** Current viewed folder. This is not a filter selection or a permission check. */
10
+ declare function useFolderPath(): string;
11
+
12
+ export { FolderPathProvider, useFolderPath };
package/dist/hooks.js ADDED
@@ -0,0 +1,22 @@
1
+ import "./chunk-MLKGABMK.js";
2
+
3
+ // src/hooks.ts
4
+ import { createContext, createElement, useContext } from "react";
5
+ var FolderPathContext = createContext(void 0);
6
+ function FolderPathProvider({
7
+ folder,
8
+ children
9
+ }) {
10
+ return createElement(FolderPathContext.Provider, { value: folder }, children);
11
+ }
12
+ function useFolderPath() {
13
+ const folder = useContext(FolderPathContext);
14
+ if (folder === void 0) {
15
+ throw new Error("useFolderPath must be used inside FolderPathProvider");
16
+ }
17
+ return folder;
18
+ }
19
+ export {
20
+ FolderPathProvider,
21
+ useFolderPath
22
+ };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { CopyContextButton, FilePreview, FilePreviewList, FolderFilterBar, FolderFilterChoice, FolderFilterMode, FolderSelection, PluginPage, PluginPageError, PluginPageStatus, useFolderFilter } from './page.js';
1
+ export { CopyContextButton, FilePreview, FilePreviewList, PluginPage, PluginPageError, PluginPageStatus } from './page.js';
2
2
  export { FolderPluginClient, FolderPluginHostActions, FolderPluginViewProps, folderPathId } from './client.js';
3
3
  export { ApiError, ApiErrorCode, ApiRouteHandle, ApiRouteSpec, apiError, jsonError, jsonResponse, parseJsonBody, readBoundedBody, runApiRoute, sameOrigin } from './endpoint.js';
4
4
  export { FolderPluginContext, FolderPluginData, FolderPluginFailure, FolderPluginInitFile, FolderPluginInitResult, FolderPluginServer } from './server.js';
5
- export { FolderPluginDataset, FolderPluginDetection, FolderPluginId, FolderPluginManifest, FolderPluginPayload, FolderPluginPayloadError, isPluginPayloadError } from './types.js';
5
+ export { FolderPluginDataset, FolderPluginDetection, FolderPluginIcon, FolderPluginId, FolderPluginManifest, FolderPluginPayload, FolderPluginPayloadError, isPluginPayloadError } from './types.js';
6
6
  import 'react';
7
7
  import 'class-variance-authority/types';
8
8
  import 'class-variance-authority';
package/dist/index.js CHANGED
@@ -14,10 +14,8 @@ import {
14
14
  EntityPage,
15
15
  EntityStatus,
16
16
  FilePreview,
17
- FilePreviewList,
18
- FolderFilterBar,
19
- useFolderFilter
20
- } from "./chunk-454D7WI6.js";
17
+ FilePreviewList
18
+ } from "./chunk-FLSLPC4P.js";
21
19
  import {
22
20
  folderPathId
23
21
  } from "./chunk-STR4VH4C.js";
@@ -33,7 +31,6 @@ export {
33
31
  CopyContextButton,
34
32
  FilePreview,
35
33
  FilePreviewList,
36
- FolderFilterBar,
37
34
  FolderPluginFailure,
38
35
  EntityPage as PluginPage,
39
36
  EntityError as PluginPageError,
@@ -46,6 +43,5 @@ export {
46
43
  parseJsonBody,
47
44
  readBoundedBody,
48
45
  runApiRoute,
49
- sameOrigin,
50
- useFolderFilter
46
+ sameOrigin
51
47
  };
package/dist/page.d.ts CHANGED
@@ -20,7 +20,7 @@ declare function EntityPage({ header, extra, children, }: {
20
20
  */
21
21
  declare function EntityStatus({ children }: {
22
22
  children: ReactNode;
23
- }): number | bigint | boolean | react.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
23
+ }): number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
24
24
  /**
25
25
  * An error block: plain strings render as a destructive line with an
26
26
  * optional retry button; richer nodes pass through untouched.
@@ -28,10 +28,10 @@ declare function EntityStatus({ children }: {
28
28
  declare function EntityError({ error, onRetry, }: {
29
29
  error: ReactNode;
30
30
  onRetry?: () => void;
31
- }): number | bigint | boolean | react.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
31
+ }): number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
32
32
 
33
33
  declare const buttonVariants: (props?: ({
34
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
34
+ variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
35
35
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
36
36
  } & class_variance_authority_types.ClassProp) | undefined) => string;
37
37
  declare function Button({ className, variant, size, asChild, ...props }: react.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
@@ -80,125 +80,4 @@ declare function FilePreviewList({ paths }: {
80
80
  paths: string[];
81
81
  }): react.JSX.Element;
82
82
 
83
- /**
84
- * Generalized folder filtering for lists of items that carry an owning
85
- * folder path.
86
- *
87
- * Pure string/array logic — no React, no DOM, no `node:` builtins — so the
88
- * exact same helpers power web chips, folder-plugin views, and CLI
89
- * filtering. The model mirrors the mail plugin's draft selector but is
90
- * accessor-driven: you hand over your items, a function that reads each
91
- * item's owning folder, and the viewed root folder; you get back the
92
- * distinct folder choices (root first, then descendants by relative path,
93
- * each with a count) and a predicate that decides which items a selection
94
- * keeps.
95
- *
96
- * CLI example (no React):
97
- *
98
- * ```ts
99
- * import {
100
- * folderFilterChoices,
101
- * folderFilterMatches,
102
- * } from "@raggle-ai/ui/lib/folder-filter"
103
- *
104
- * const choices = folderFilterChoices(items, root, (item) => item.folder)
105
- * const visible = items.filter((item) =>
106
- * folderFilterMatches(item.folder, flags.folder, "subtree"),
107
- * )
108
- * ```
109
- */
110
- /** How a selected folder applies to an item's owning folder. */
111
- type FolderFilterMode = "exact" | "subtree";
112
- /**
113
- * One clickable chip: a distinct owning folder in the item list. `folder`
114
- * is the full owning path and the exact filter key (duplicate leaf names
115
- * stay distinct); `label` is the short display text (basename for the
116
- * root, relative path for descendants).
117
- */
118
- type FolderFilterChoice = {
119
- /** Exact owning folder path — the filter key. */
120
- folder: string;
121
- /** Number of items owned by this folder. */
122
- count: number;
123
- /** Short display label: basename for the root, relative path for descendants. */
124
- label: string;
125
- /** Path relative to the viewed root; "" for the root itself. */
126
- relativePath: string;
127
- };
128
- /**
129
- * A selection for folder filtering: a single owning folder, several
130
- * (multi-select — an item passes when any of them matches), or null/
131
- * empty for "all items". Single-string callers keep working unchanged.
132
- */
133
- type FolderSelection = string | readonly string[] | null;
134
-
135
- /**
136
- * Multi-select folder filter as a single "Folder (count)" button. The
137
- * count badge shows how many folders are selected, or the total when
138
- * nothing is filtered. The popover holds a searchable command list with
139
- * an "All folders" row that clears back to everything; every folder row
140
- * toggles into the selection in real time.
141
- *
142
- * Rows are portal-mounted on open (like this repo's Select), so closed
143
- * dropdown content is not in the SSR/tree — the trigger carries the
144
- * accessible count instead.
145
- */
146
- declare function FolderFilterBar({ allLabel, ariaLabel, choices, onSelect, selected, total, }: {
147
- /** Label of the row that clears the selection. */
148
- allLabel?: string;
149
- /** Accessible name for the filter trigger. */
150
- ariaLabel?: string;
151
- choices: FolderFilterChoice[];
152
- /** All currently selected owning folders; pass [] to show everything. */
153
- onSelect: (folders: readonly string[]) => void;
154
- /** Currently selected owning folders; empty means "all". */
155
- selected: readonly string[];
156
- /** Count shown on the trigger while nothing is selected. */
157
- total: number;
158
- }): react.JSX.Element;
159
-
160
- type UseFolderFilterOptions<T> = {
161
- /** Owning folder of each item; null when the item has none. */
162
- getFolder: (item: T) => string | null;
163
- /** "exact" keeps the selected folder's items only; "subtree" also keeps descendant items. */
164
- mode?: FolderFilterMode;
165
- /** Include ancestor/outside folders as chips (see `folderFilterChoices`). */
166
- allowOutside?: boolean;
167
- /** Folder everything is shown relative to (the viewed root). */
168
- rootFolder: string;
169
- /** Initial selection (owning folder paths); a selection with no items falls back to all. */
170
- initialFolder?: FolderSelection;
171
- };
172
- type UseFolderFilterResult<T> = {
173
- /** Distinct owning-folder choices with counts, root first. */
174
- choices: FolderFilterChoice[];
175
- /**
176
- * Selection entries that still match items. Empty means "all items" —
177
- * either nothing is selected or every selected folder lost its items.
178
- */
179
- effectiveSelected: readonly string[];
180
- /** Raw selection state; entries whose folder has no items left may be stale. */
181
- selected: readonly string[];
182
- /**
183
- * Replace the selection. Pass a single folder, an array of folders (an
184
- * item passes when any of them matches), or null/[] to show everything.
185
- */
186
- setSelected: (folder: FolderSelection) => void;
187
- /** Toggle one owning folder in/out of the selection. */
188
- toggle: (folder: string) => void;
189
- /** Clear the selection back to "all items". */
190
- reset: () => void;
191
- /** Number of items before filtering — shown on the "All" chip. */
192
- total: number;
193
- /** Items after applying the effective selection. */
194
- visibleItems: T[];
195
- };
196
- /**
197
- * Folder-filter state for a plugin view: pass the items and a folder
198
- * accessor, get the choices, the reconciled (multi-)selection, and the
199
- * filtered `visibleItems` in one place. Recomputes only when the items,
200
- * accessor, root, mode, or selection change.
201
- */
202
- declare function useFolderFilter<T>(items: readonly T[], options: UseFolderFilterOptions<T>): UseFolderFilterResult<T>;
203
-
204
- export { CopyContextButton, FilePreview, FilePreviewList, FolderFilterBar, type FolderFilterChoice, type FolderFilterMode, type FolderSelection, EntityPage as PluginPage, EntityError as PluginPageError, EntityStatus as PluginPageStatus, useFolderFilter };
83
+ export { CopyContextButton, FilePreview, FilePreviewList, EntityPage as PluginPage, EntityError as PluginPageError, EntityStatus as PluginPageStatus };
package/dist/page.js CHANGED
@@ -4,10 +4,8 @@ import {
4
4
  EntityPage,
5
5
  EntityStatus,
6
6
  FilePreview,
7
- FilePreviewList,
8
- FolderFilterBar,
9
- useFolderFilter
10
- } from "./chunk-454D7WI6.js";
7
+ FilePreviewList
8
+ } from "./chunk-FLSLPC4P.js";
11
9
  import "./chunk-STR4VH4C.js";
12
10
  import "./chunk-YLSYYX2L.js";
13
11
  import "./chunk-MLKGABMK.js";
@@ -15,9 +13,7 @@ export {
15
13
  CopyContextButton,
16
14
  FilePreview,
17
15
  FilePreviewList,
18
- FolderFilterBar,
19
16
  EntityPage as PluginPage,
20
17
  EntityError as PluginPageError,
21
- EntityStatus as PluginPageStatus,
22
- useFolderFilter
18
+ EntityStatus as PluginPageStatus
23
19
  };
package/dist/types.d.ts CHANGED
@@ -11,9 +11,16 @@ type ContentIssue = {
11
11
  * invalidate content — the file still detects as valid. */
12
12
  severity?: "warning";
13
13
  };
14
+ /** Plugin-owned SVG path data. No React or page imports are needed for tabs. */
15
+ type FolderPluginIcon = {
16
+ /** Paths use a 24 × 24 view box and the navigation text color. */
17
+ paths: string[];
18
+ };
14
19
  type FolderPluginManifest = {
15
20
  /** Stable identifier used in routes, URLs, and registries. Never a URL segment turned into an import path. */
16
21
  id: FolderPluginId;
22
+ /** Optional plugin-owned navigation icon. Omit for a text-only tab. */
23
+ icon?: FolderPluginIcon;
17
24
  /** Navigation label shown in the folder shell. */
18
25
  label: string;
19
26
  };
@@ -78,4 +85,4 @@ type FolderPluginPayload<T> = {
78
85
  /** True when the decoded data is an in-band error payload. */
79
86
  declare function isPluginPayloadError(data: unknown): data is FolderPluginPayloadError;
80
87
 
81
- export { type ContentIssue, type FolderPluginDataset, type FolderPluginDetection, type FolderPluginId, type FolderPluginManifest, type FolderPluginPayload, type FolderPluginPayloadError, isPluginPayloadError };
88
+ export { type ContentIssue, type FolderPluginDataset, type FolderPluginDetection, type FolderPluginIcon, type FolderPluginId, type FolderPluginManifest, type FolderPluginPayload, type FolderPluginPayloadError, isPluginPayloadError };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "kennel-plugins",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "description": "Small shared contract for trusted folder view plugins in the Kennel web viewer.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -32,6 +32,10 @@
32
32
  "types": "./dist/endpoint.d.ts",
33
33
  "import": "./dist/endpoint.js"
34
34
  },
35
+ "./hooks": {
36
+ "types": "./dist/hooks.d.ts",
37
+ "import": "./dist/hooks.js"
38
+ },
35
39
  "./page": {
36
40
  "types": "./dist/page.d.ts",
37
41
  "import": "./dist/page.js"