akanjs 3.0.0-alpha.97 → 3.0.0-alpha.98

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.97",
3
+ "version": "3.0.0-alpha.98",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/store/action.ts CHANGED
@@ -584,6 +584,12 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
584
584
 
585
585
  const current = (this.get() as { [key: string]: any })[names.modelDraft] as DraftState | null;
586
586
  if (current?.key !== draft.key) return;
587
+
588
+ const openedForm = (this.get() as { [key: string]: any })[names.modelForm] as object;
589
+ if (DraftStore.contentHash(record.form) === DraftStore.contentHash(DraftStore.encodeForm(refName, openedForm))) {
590
+ await DraftStore.remove(draft.key);
591
+ return;
592
+ }
587
593
  let form: object;
588
594
  try {
589
595
  form = DraftStore.decodeForm(refName, record.form);
@@ -1222,6 +1228,13 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
1222
1228
  * asking for what comes after them cannot drift from what is displayed. It also leaves `pageOf<Model>` at
1223
1229
  * 1, which is what keeps live placement — refused anywhere but the first page — working past the first
1224
1230
  * "more".
1231
+ *
1232
+ * It reads `<model>ListLoading` and never sets it: an append leaves the rows on screen, so raising it
1233
+ * would put the whole-list spinner over one and would make `applyLive<Model>` drop every live event until
1234
+ * the batch lands. So concurrent calls are NOT rejected — two of them fetch the same offset, since the
1235
+ * list they measure has not grown yet. That is a wasted round trip and never a wrong list: the ticket
1236
+ * makes only the newest response apply, and both asked for the same rows. A caller that minds the wasted
1237
+ * request holds its own in-flight flag, the way `InfiniteScroll` does.
1225
1238
  */
1226
1239
  [namesOfSlice.loadMoreOfModel]: async function (this: SetGet, options?: FetchPolicy) {
1227
1240
  const currentState = this.get() as { [key: string]: any };
@@ -17,6 +17,9 @@ const MAX_DRAFTS_PER_IDENTITY = 30;
17
17
  */
18
18
  const VOLATILE_CLAIMS = ["iat", "exp", "nbf", "jti"] as const;
19
19
 
20
+ /** The fields a form carries from its row rather than from what the user typed. */
21
+ const RECORD_STAMPS = new Set(["createdAt", "updatedAt", "removedAt"]);
22
+
20
23
  export interface DraftRecord {
21
24
  v: number;
22
25
  /** ISO. Drives both the "n minutes ago" label and the TTL/LRU sweep. */
@@ -143,6 +146,17 @@ export class DraftStore {
143
146
  return DraftStore.#hash8(DraftStore.#stableStringify(DraftStore.encodeForm(refName, form)));
144
147
  }
145
148
 
149
+ /**
150
+ * What two encoded forms holding the same values hash to, whatever record stamps they carry.
151
+ *
152
+ * The stamps come from the row rather than from the user, so a save the form made itself moves `updatedAt` and
153
+ * would otherwise make a draft look different from the record that already holds it.
154
+ */
155
+ static contentHash(encoded: Record<string, unknown>): string {
156
+ const content = Object.fromEntries(Object.entries(encoded).filter(([key]) => !RECORD_STAMPS.has(key)));
157
+ return DraftStore.#hash8(DraftStore.#stableStringify(content));
158
+ }
159
+
146
160
  static async read(key: string): Promise<DraftRecord | null> {
147
161
  const storage = DraftStore.#storage();
148
162
  if (!storage) return null;
@@ -36,6 +36,13 @@ export declare class DraftStore {
36
36
  static decodeForm(refName: string, plain: Record<string, unknown>): object;
37
37
  /** What the dirty check compares. Cheap enough to take once per debounce window, not once per keystroke. */
38
38
  static formHash(refName: string, form: object): string;
39
+ /**
40
+ * What two encoded forms holding the same values hash to, whatever record stamps they carry.
41
+ *
42
+ * The stamps come from the row rather than from the user, so a save the form made itself moves `updatedAt` and
43
+ * would otherwise make a draft look different from the record that already holds it.
44
+ */
45
+ static contentHash(encoded: Record<string, unknown>): string;
39
46
  static read(key: string): Promise<DraftRecord | null>;
40
47
  static write(key: string, record: DraftRecord): Promise<void>;
41
48
  static remove(key: string): Promise<void>;
@@ -2,6 +2,11 @@ export interface InfiniteScrollProps {
2
2
  hasMore: boolean;
3
3
  onLoadMore: () => Promise<void>;
4
4
  children: React.ReactNode;
5
+ /**
6
+ * Load earlier rows above the ones in hand, preserving the reading position across the prepend. Assumes
7
+ * normal column flow. It does not scroll anywhere at mount, so a list meant to open at its newest row scrolls
8
+ * itself — and until it does, the sentinel is on screen and loads one window unasked.
9
+ */
5
10
  reverse?: boolean;
6
11
  }
7
12
  export declare const InfiniteScroll: ({ hasMore, onLoadMore, children, reverse }: InfiniteScrollProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,14 +1,24 @@
1
1
  import type { SliceMeta } from "akanjs/fetch";
2
+ export interface DraftBarViewProps {
3
+ className?: string;
4
+ /** The model the recovered form belongs to. */
5
+ refName: string;
6
+ /**
7
+ * `conflict` is a decision the user has to settle — the record moved since the draft was taken, so the form
8
+ * shows the server's value and the bar offers the older one. `applied` is a notice: the draft is what is on
9
+ * screen, and the bar is the way back.
10
+ */
11
+ state: "conflict" | "applied";
12
+ /** When the draft was taken. */
13
+ savedAt: Date;
14
+ /** Puts the offered draft into the form. Passed in the `conflict` state only. */
15
+ onRestore?: () => void;
16
+ /** Drops the draft and keeps the form as it was opened. */
17
+ onDiscard: () => void;
18
+ }
2
19
  interface DraftBarProps {
3
20
  className?: string;
4
21
  slice: SliceMeta;
5
22
  }
6
- /**
7
- * What the user is told about a recovered form.
8
- *
9
- * Two states, and the difference is whether the draft is already in the form. A pending one is a conflict the
10
- * user has to settle — the record moved since the draft was taken, so the form shows the server's value and this
11
- * offers the older one. An applied one is just a notice: the draft is what is on screen, and this is the way back.
12
- */
13
23
  export default function DraftBar({ className, slice }: DraftBarProps): import("react/jsx-runtime").JSX.Element | null;
14
24
  export {};
@@ -1,5 +1,6 @@
1
1
  import type { SliceMeta } from "akanjs/fetch";
2
2
  import type { ReactNode } from "react";
3
+ import type { DraftProp } from "./draftScope.d.ts";
3
4
  interface EditProps {
4
5
  type?: "icon" | "button";
5
6
  className?: string;
@@ -11,6 +12,8 @@ interface EditProps {
11
12
  renderTitle?: ((model: {
12
13
  id: string;
13
14
  }) => string | ReactNode) | string;
15
+ /** Draft recovery for the form this opens. `false` turns it off; a string names the scope explicitly. */
16
+ draft?: DraftProp;
14
17
  }
15
- export default function Edit({ className, wrapperClassName, type, children, slice, modelId, modal, renderTitle, }: EditProps): import("react/jsx-runtime").JSX.Element;
18
+ export default function Edit({ className, wrapperClassName, type, children, slice, modelId, modal, renderTitle, draft, }: EditProps): import("react/jsx-runtime").JSX.Element;
16
19
  export {};
@@ -9,6 +9,8 @@ interface EditModelProps<Full> {
9
9
  slice: SliceMeta;
10
10
  /** Additional classes for the wrapper. */
11
11
  className?: string;
12
+ /** Additional classes for the recovered-form banner this shell draws above the form. */
13
+ draftBarClassName?: string;
12
14
  /** Re-check submit eligibility when form state changes. */
13
15
  checkSubmit?: boolean;
14
16
  /** Client edit promise or partial form seed. */
@@ -54,5 +56,5 @@ interface EditModalProps<Full extends {
54
56
  }
55
57
  export default function EditModal<Full extends {
56
58
  id: string;
57
- }>({ type, slice, id, className, disabled, checkSubmit, modalClassName, edit, modal, renderTitle, children, submitText, submitClassName, submitOption, renderSubmit, loadingWrapper, draft, onSubmit, onCancel, }: EditModalProps<Full>): import("react/jsx-runtime").JSX.Element | undefined;
59
+ }>({ type, slice, id, className, draftBarClassName, disabled, checkSubmit, modalClassName, edit, modal, renderTitle, children, submitText, submitClassName, submitOption, renderSubmit, loadingWrapper, draft, onSubmit, onCancel, }: EditModalProps<Full>): import("react/jsx-runtime").JSX.Element | undefined;
58
60
  export {};
@@ -22,6 +22,7 @@ import type { SkeletonProps } from "../Loading/Skeleton.d.ts";
22
22
  import type { SpinProps } from "../Loading/Spin.d.ts";
23
23
  import type { MenuProps } from "../Menu.d.ts";
24
24
  import type { ModalProps } from "../Modal.d.ts";
25
+ import type { DraftBarViewProps } from "../Model/DraftBar.d.ts";
25
26
  import type { PaginationProps } from "../Pagination.d.ts";
26
27
  import type { PopconfirmProps } from "../Popconfirm.d.ts";
27
28
  import type { ItemProps as RadioItemProps, RadioProps } from "../Radio.d.ts";
@@ -54,6 +55,7 @@ export interface AkanUiOverrides {
54
55
  Menu: ComponentType<MenuProps>;
55
56
  Tooltip: ComponentType<TooltipProps>;
56
57
  Unauthorized: ComponentType<UnauthorizedProps>;
58
+ DraftBar: ComponentType<DraftBarViewProps>;
57
59
  AgentChat: ComponentType<AgentChatProps>;
58
60
  AgentLauncher: ComponentType<AgentLauncherProps>;
59
61
  AgentBubble: ComponentType<AgentBubbleProps>;
@@ -6,19 +6,70 @@ export interface InfiniteScrollProps {
6
6
  hasMore: boolean;
7
7
  onLoadMore: () => Promise<void>;
8
8
  children: React.ReactNode;
9
+ /**
10
+ * Load earlier rows above the ones in hand, preserving the reading position across the prepend. Assumes
11
+ * normal column flow. It does not scroll anywhere at mount, so a list meant to open at its newest row scrolls
12
+ * itself — and until it does, the sentinel is on screen and loads one window unasked.
13
+ */
9
14
  reverse?: boolean;
10
15
  }
11
16
 
17
+ let warnedColumnReverse = false;
18
+
19
+ /**
20
+ * The sentinel is positioned by DOM order alone — first child to load earlier, last child to load more — so a
21
+ * `column-reverse` parent paints it at the opposite end from the rows it controls, and `scrollTop: 0` is then
22
+ * that same end, so it also fires at mount. `flex-col-reverse` is the usual no-JS way to pin a chat to the
23
+ * bottom, so a caller reaching for `reverse` may well already have it; the result reads as a control placed
24
+ * wrongly rather than as an error, which is why it is worth saying out loud once.
25
+ */
26
+ const warnColumnReverse = (sentinel: Element | null) => {
27
+ if (warnedColumnReverse || process.env.AKAN_PUBLIC_ENV !== "local") return;
28
+ const parent = sentinel?.parentElement;
29
+ if (!parent || getComputedStyle(parent).flexDirection !== "column-reverse") return;
30
+ warnedColumnReverse = true;
31
+ console.warn(
32
+ "<InfiniteScroll> sits in a `flex-col-reverse` parent, which paints its load sentinel at the end opposite the rows it loads, and fires it at mount. Drop `flex-col-reverse` and let `reverse` hold the reading position instead.",
33
+ );
34
+ };
35
+
36
+ const scrollableOverflows = new Set(["auto", "scroll", "overlay"]);
37
+
38
+ /**
39
+ * The element that actually scrolls the sentinel — the document only once no ancestor has taken the job.
40
+ *
41
+ * A chat timeline or a log tail scrolls inside its own `overflow-y-auto` box, and there the document does not
42
+ * move at all, so anchoring `document.scrollingElement` restores a position nothing changed. Resolved per load
43
+ * rather than once, because the box that scrolls is a layout outcome and a caller cannot be asked to name it.
44
+ */
45
+ const scrollerOf = (sentinel: Element | null) => {
46
+ if (typeof document === "undefined") return null;
47
+ let el = sentinel?.parentElement ?? null;
48
+ while (el && el !== document.body && el !== document.documentElement) {
49
+
50
+ if (el.scrollHeight > el.clientHeight && scrollableOverflows.has(getComputedStyle(el).overflowY)) return el;
51
+ el = el.parentElement;
52
+ }
53
+ return document.scrollingElement;
54
+ };
55
+
12
56
  export const InfiniteScroll = ({ hasMore, onLoadMore, children, reverse }: InfiniteScrollProps) => {
13
57
  const [isFetching, setIsFetching] = useState(false);
14
58
  const isFetchingRef = useRef(false);
15
59
  const target = useRef<HTMLDivElement>(null);
16
60
 
17
61
  useEffect(() => {
18
- const observer = new IntersectionObserver((entries) => {
19
- const [entry] = entries;
20
- if (entry.isIntersecting) void fetchMoreItems();
21
- });
62
+
63
+ warnColumnReverse(target.current);
64
+ const scroller = scrollerOf(target.current);
65
+ const root = scroller && scroller !== document.scrollingElement ? scroller : null;
66
+ const observer = new IntersectionObserver(
67
+ (entries) => {
68
+ const [entry] = entries;
69
+ if (entry.isIntersecting) void fetchMoreItems();
70
+ },
71
+ { root },
72
+ );
22
73
  if (target.current) observer.observe(target.current);
23
74
  return () => {
24
75
  observer.disconnect();
@@ -28,7 +79,7 @@ export const InfiniteScroll = ({ hasMore, onLoadMore, children, reverse }: Infin
28
79
  const fetchMoreItems = async () => {
29
80
  if (isFetchingRef.current) return;
30
81
 
31
- const scroller = reverse ? document.scrollingElement : null;
82
+ const scroller = reverse ? scrollerOf(target.current) : null;
32
83
  const prevScrollHeight = scroller?.scrollHeight ?? 0;
33
84
  const prevScrollTop = scroller?.scrollTop ?? 0;
34
85
 
@@ -9,21 +9,75 @@ import { AiOutlineDelete, AiOutlineHistory, AiOutlineRollback } from "react-icon
9
9
  import { agentAttrs } from "../agentAttrs";
10
10
  import { Button } from "../Button";
11
11
  import { RecentTime } from "../RecentTime";
12
+ import { createOverridable } from "../UiOverride";
12
13
 
13
- interface DraftBarProps {
14
+ export interface DraftBarViewProps {
14
15
  className?: string;
15
- slice: SliceMeta;
16
+ /** The model the recovered form belongs to. */
17
+ refName: string;
18
+ /**
19
+ * `conflict` is a decision the user has to settle — the record moved since the draft was taken, so the form
20
+ * shows the server's value and the bar offers the older one. `applied` is a notice: the draft is what is on
21
+ * screen, and the bar is the way back.
22
+ */
23
+ state: "conflict" | "applied";
24
+ /** When the draft was taken. */
25
+ savedAt: Date;
26
+ /** Puts the offered draft into the form. Passed in the `conflict` state only. */
27
+ onRestore?: () => void;
28
+ /** Drops the draft and keeps the form as it was opened. */
29
+ onDiscard: () => void;
16
30
  }
17
31
 
32
+ const DefaultDraftBar = ({ className, state, savedAt, onRestore, onDiscard }: DraftBarViewProps) => {
33
+ const { l } = usePage();
34
+ if (state === "conflict")
35
+ return (
36
+ <div
37
+ className={cn(
38
+ "mb-4 flex flex-wrap items-center gap-2 rounded-box border border-warning/40 bg-warning/10 p-3",
39
+ className,
40
+ )}
41
+ >
42
+ <AiOutlineHistory className="text-warning" />
43
+ <span className="flex-1 text-foreground/80 text-sm">
44
+ {l("base.draftConflict")} <RecentTime date={savedAt} />
45
+ </span>
46
+ <Button {...agentAttrs(onRestore)} size="sm" onClick={() => onRestore?.()}>
47
+ <AiOutlineRollback /> {l("base.draftRestore")}
48
+ </Button>
49
+ <Button {...agentAttrs(onDiscard)} size="sm" variant="ghost" onClick={() => onDiscard()}>
50
+ <AiOutlineDelete /> {l("base.draftDiscard")}
51
+ </Button>
52
+ </div>
53
+ );
54
+ return (
55
+ <div className={cn("mb-4 flex flex-wrap items-center gap-2 text-foreground/60 text-xs", className)}>
56
+ <AiOutlineHistory />
57
+ <span className="flex-1">
58
+ {l("base.draftApplied")} <RecentTime date={savedAt} />
59
+ </span>
60
+ <Button {...agentAttrs(onDiscard)} size="xs" variant="ghost" onClick={() => onDiscard()}>
61
+ {l("base.draftStartOver")}
62
+ </Button>
63
+ </div>
64
+ );
65
+ };
66
+
18
67
  /**
19
- * What the user is told about a recovered form.
68
+ * The banner itself, route-overridable through `page/**\/_overrides.tsx` (slot `DraftBar`).
20
69
  *
21
- * Two states, and the difference is whether the draft is already in the form. A pending one is a conflict the
22
- * user has to settle — the record moved since the draft was taken, so the form shows the server's value and this
23
- * offers the older one. An applied one is just a notice: the draft is what is on screen, and this is the way back.
70
+ * The shell below keeps the draft state and publishes the two agent tools, so a replacement re-skins the notice
71
+ * without reaching into the store under string keys or re-declaring what an agent may pull.
24
72
  */
73
+ const DraftBarView = createOverridable("DraftBar", DefaultDraftBar);
74
+
75
+ interface DraftBarProps {
76
+ className?: string;
77
+ slice: SliceMeta;
78
+ }
79
+
25
80
  export default function DraftBar({ className, slice }: DraftBarProps) {
26
- const { l } = usePage();
27
81
  const { refName } = slice;
28
82
  const [modelName, ModelName] = useMemo(() => [lowerlize(refName), capitalize(refName)], []);
29
83
  const names = useMemo(
@@ -50,35 +104,24 @@ export default function DraftBar({ className, slice }: DraftBarProps) {
50
104
 
51
105
  if (draft?.pending)
52
106
  return (
53
- <div
54
- className={cn(
55
- "mb-4 flex flex-wrap items-center gap-2 rounded-box border border-warning/40 bg-warning/10 p-3",
56
- className,
57
- )}
58
- >
59
- <AiOutlineHistory className="text-warning" />
60
- <span className="flex-1 text-foreground/80 text-sm">
61
- {l("base.draftConflict")} <RecentTime date={draft.pending.savedAt} />
62
- </span>
63
- <Button {...agentAttrs(restoreDraft)} size="sm" onClick={() => restoreDraft()}>
64
- <AiOutlineRollback /> {l("base.draftRestore")}
65
- </Button>
66
- <Button {...agentAttrs(discardDraft)} size="sm" variant="ghost" onClick={() => discardDraft()}>
67
- <AiOutlineDelete /> {l("base.draftDiscard")}
68
- </Button>
69
- </div>
107
+ <DraftBarView
108
+ className={className}
109
+ refName={refName}
110
+ state="conflict"
111
+ savedAt={draft.pending.savedAt}
112
+ onRestore={restoreDraft}
113
+ onDiscard={discardDraft}
114
+ />
70
115
  );
71
116
  if (draft?.appliedAt)
72
117
  return (
73
- <div className={cn("mb-4 flex flex-wrap items-center gap-2 text-foreground/60 text-xs", className)}>
74
- <AiOutlineHistory />
75
- <span className="flex-1">
76
- {l("base.draftApplied")} <RecentTime date={draft.appliedAt} />
77
- </span>
78
- <Button {...agentAttrs(discardDraft)} size="xs" variant="ghost" onClick={() => discardDraft()}>
79
- {l("base.draftStartOver")}
80
- </Button>
81
- </div>
118
+ <DraftBarView
119
+ className={className}
120
+ refName={refName}
121
+ state="applied"
122
+ savedAt={draft.appliedAt}
123
+ onDiscard={discardDraft}
124
+ />
82
125
  );
83
126
  return null;
84
127
  }
package/ui/Model/Edit.tsx CHANGED
@@ -3,6 +3,7 @@ import type { SliceMeta } from "akanjs/fetch";
3
3
  import type { ReactNode } from "react";
4
4
  import { AiOutlineEdit } from "react-icons/ai";
5
5
 
6
+ import type { DraftProp } from "./draftScope";
6
7
  import EditModal from "./EditModal";
7
8
  import EditWrapper from "./EditWrapper";
8
9
 
@@ -15,6 +16,8 @@ interface EditProps {
15
16
  modelId: string;
16
17
  modal?: string | null;
17
18
  renderTitle?: ((model: { id: string }) => string | ReactNode) | string;
19
+ /** Draft recovery for the form this opens. `false` turns it off; a string names the scope explicitly. */
20
+ draft?: DraftProp;
18
21
  }
19
22
 
20
23
  export default function Edit({
@@ -26,6 +29,7 @@ export default function Edit({
26
29
  modelId,
27
30
  modal,
28
31
  renderTitle,
32
+ draft,
29
33
  }: EditProps) {
30
34
  const { l } = usePage();
31
35
  return (
@@ -35,10 +39,11 @@ export default function Edit({
35
39
  slice={slice}
36
40
  modelId={modelId}
37
41
  modal={modal}
42
+ draft={draft}
38
43
  >
39
44
  <AiOutlineEdit /> {type === "button" ? l("base.edit") : null}
40
45
  </EditWrapper>
41
- <EditModal renderTitle={renderTitle} slice={slice} id={modelId}>
46
+ <EditModal renderTitle={renderTitle} slice={slice} id={modelId} draft={draft}>
42
47
  {children}
43
48
  </EditModal>
44
49
  </div>
@@ -27,6 +27,8 @@ interface EditModelProps<Full> {
27
27
  slice: SliceMeta;
28
28
  /** Additional classes for the wrapper. */
29
29
  className?: string;
30
+ /** Additional classes for the recovered-form banner this shell draws above the form. */
31
+ draftBarClassName?: string;
30
32
  /** Re-check submit eligibility when form state changes. */
31
33
  checkSubmit?: boolean;
32
34
  /** Client edit promise or partial form seed. */
@@ -52,6 +54,7 @@ const EditModel = <Full,>({
52
54
  type = "modal",
53
55
  slice,
54
56
  className,
57
+ draftBarClassName,
55
58
  checkSubmit = true,
56
59
  edit,
57
60
  modal,
@@ -107,7 +110,7 @@ const EditModel = <Full,>({
107
110
 
108
111
  return (
109
112
  <LoadingWrapper className={cn("w-full", className)}>
110
- <DraftBar slice={slice} />
113
+ <DraftBar className={draftBarClassName} slice={slice} />
111
114
  {children}
112
115
  </LoadingWrapper>
113
116
  );
@@ -142,6 +145,7 @@ export default function EditModal<Full extends { id: string }>({
142
145
  slice,
143
146
  id,
144
147
  className,
148
+ draftBarClassName,
145
149
  disabled,
146
150
  checkSubmit = true,
147
151
  modalClassName,
@@ -368,6 +372,7 @@ export default function EditModal<Full extends { id: string }>({
368
372
  type={type}
369
373
  slice={slice}
370
374
  className={className}
375
+ draftBarClassName={draftBarClassName}
371
376
  checkSubmit={checkSubmit}
372
377
  edit={edit}
373
378
  modal={modal}
@@ -385,6 +390,7 @@ export default function EditModal<Full extends { id: string }>({
385
390
  type={type}
386
391
  slice={slice}
387
392
  className={className}
393
+ draftBarClassName={draftBarClassName}
388
394
  checkSubmit={checkSubmit}
389
395
  edit={edit}
390
396
  modal={modal}
@@ -24,6 +24,7 @@ import type { SkeletonProps } from "../Loading/Skeleton";
24
24
  import type { SpinProps } from "../Loading/Spin";
25
25
  import type { MenuProps } from "../Menu";
26
26
  import type { ModalProps } from "../Modal";
27
+ import type { DraftBarViewProps } from "../Model/DraftBar";
27
28
  import type { PaginationProps } from "../Pagination";
28
29
  import type { PopconfirmProps } from "../Popconfirm";
29
30
  import type { ItemProps as RadioItemProps, RadioProps } from "../Radio";
@@ -58,6 +59,8 @@ export interface AkanUiOverrides {
58
59
  Menu: ComponentType<MenuProps>;
59
60
  Tooltip: ComponentType<TooltipProps>;
60
61
  Unauthorized: ComponentType<UnauthorizedProps>;
62
+
63
+ DraftBar: ComponentType<DraftBarViewProps>;
61
64
  AgentChat: ComponentType<AgentChatProps>;
62
65
 
63
66
  AgentLauncher: ComponentType<AgentLauncherProps>;