@danxbot/ui 3.8.0 → 3.9.1

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/dist/pure.js ADDED
@@ -0,0 +1,17 @@
1
+ function u(r) {
2
+ if (!Number.isFinite(r) || r < 0) return "";
3
+ if (r < 1024) return `${r} B`;
4
+ const t = ["KB", "MB", "GB", "TB"];
5
+ let i = r / 1024, e = 0;
6
+ for (; i >= 1024 && e < t.length - 1; )
7
+ i /= 1024, e += 1;
8
+ return `${i < 10 ? i.toFixed(1) : Math.round(i)} ${t[e]}`;
9
+ }
10
+ function n(r) {
11
+ return Number.isFinite(r) ? r < 1e3 ? String(Math.round(r)) : r < 1e6 ? `${Number((r / 1e3).toFixed(1))}k` : `${Number((r / 1e6).toFixed(1))}M` : "—";
12
+ }
13
+ export {
14
+ u as formatBytes,
15
+ n as formatTokens
16
+ };
17
+ //# sourceMappingURL=pure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pure.js","sources":["../src/lib/format.ts"],"sourcesContent":["/* ----------------------------------------------------------------------------\n NUMBERS, TURNED INTO THE STRINGS PEOPLE READ.\n\n These lived inside the components that needed them first — `formatBytes` in\n `chat/Attachment.tsx`, `formatTokens` in `chat/ContextMeter.tsx` — which was\n fine right up until somebody outside a component wanted one. The package\n exports a single entry, so importing `formatBytes` into an application's own\n formatting layer dragged React and every component in this library into a\n layer that is deliberately framework-free.\n\n They are here because they are DECISIONS, not utilities: how much precision\n a reader can act on, and where a rounded figure stops telling the truth. Two\n places rendering the same quantity to different precision is the defect this\n file prevents — see each function for the call it makes.\n\n NOTHING IN THIS FILE MAY IMPORT REACT, directly or transitively. It is the\n whole content of the `@danxbot/ui/pure` entry; `src/pure.ts` states the\n contract and `pure-entry.test.ts` enforces it against both the source graph\n and the built artifact.\n -------------------------------------------------------------------------- */\n\n/** Binary units, because these are file sizes and every OS reports them this way. */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return \"\";\n if (bytes < 1024) return `${bytes} B`;\n const units = [\"KB\", \"MB\", \"GB\", \"TB\"];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n /* One decimal below 10 and none above: \"1.4 MB\" is useful precision,\n \"847.3 KB\" is noise on a number nobody acts on. */\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;\n}\n\n/**\n * 241_600 → \"241.6k\".\n *\n * One decimal all the way to 1M, and exact below 1k. Unlike a file size — where\n * a tenth of a kilobyte is noise on a number nobody acts on — the tenth here is\n * the digit that moves while you work: at 1M capacity, a whole percent is\n * 10_000 tokens, so rounding to \"242k\" holds a figure still for several\n * exchanges and makes a live meter look frozen.\n */\nexport function formatTokens(tokens: number): string {\n if (!Number.isFinite(tokens)) return \"—\";\n if (tokens < 1_000) return String(Math.round(tokens));\n if (tokens < 1_000_000) return `${Number((tokens / 1_000).toFixed(1))}k`;\n return `${Number((tokens / 1_000_000).toFixed(1))}M`;\n}\n"],"names":["formatBytes","bytes","units","value","unit","formatTokens","tokens"],"mappings":"AAsBO,SAASA,EAAYC,GAAuB;AACjD,MAAI,CAAC,OAAO,SAASA,CAAK,KAAKA,IAAQ,EAAG,QAAO;AACjD,MAAIA,IAAQ,KAAM,QAAO,GAAGA,CAAK;AACjC,QAAMC,IAAQ,CAAC,MAAM,MAAM,MAAM,IAAI;AACrC,MAAIC,IAAQF,IAAQ,MAChBG,IAAO;AACX,SAAOD,KAAS,QAAQC,IAAOF,EAAM,SAAS;AAC5C,IAAAC,KAAS,MACTC,KAAQ;AAIV,SAAO,GAAGD,IAAQ,KAAKA,EAAM,QAAQ,CAAC,IAAI,KAAK,MAAMA,CAAK,CAAC,IAAID,EAAME,CAAI,CAAC;AAC5E;AAWO,SAASC,EAAaC,GAAwB;AACnD,SAAK,OAAO,SAASA,CAAM,IACvBA,IAAS,MAAc,OAAO,KAAK,MAAMA,CAAM,CAAC,IAChDA,IAAS,MAAkB,GAAG,QAAQA,IAAS,KAAO,QAAQ,CAAC,CAAC,CAAC,MAC9D,GAAG,QAAQA,IAAS,KAAW,QAAQ,CAAC,CAAC,CAAC,MAHZ;AAIvC;"}
@@ -58,8 +58,36 @@ declare const badge: import("tailwind-variants").TVReturnType<{
58
58
  false: "";
59
59
  };
60
60
  }, undefined>>;
61
+ /**
62
+ * What a badge needs in order to be removable, in one object.
63
+ *
64
+ * ONE PROP RATHER THAN TWO, and the label being a required FIELD is the point:
65
+ * a dismiss control with no accessible name is not expressible. An icon carries
66
+ * no text, and a row of filter chips is exactly where "Remove" spoken five
67
+ * times distinguishes nothing — the same argument that makes `IconButton.label`
68
+ * required. `SettingRow`'s `readOnly: { reason }` uses the same shape for the
69
+ * same reason: an explanation that is part of the type cannot be forgotten.
70
+ *
71
+ * Name what it removes ("Remove the infra filter"), not the picture ("Close").
72
+ */
73
+ export interface BadgeDismiss {
74
+ label: string;
75
+ onDismiss: () => void;
76
+ }
61
77
  export interface BadgeProps extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badge> {
62
78
  icon?: ReactNode;
79
+ /**
80
+ * Makes the badge a chip you can take off — a trailing × after the label.
81
+ *
82
+ * Absent means a plain label, which is what every existing badge is: an ×
83
+ * nobody asked for is a destructive control appearing inside a caption.
84
+ *
85
+ * NOT FOR A BADGE INSIDE A BUTTON. `StatusSelect` renders its Badge as the
86
+ * body of a trigger, and a `<button>` inside a `<button>` is invalid and
87
+ * unreachable by keyboard. A dismissible badge belongs on a surface, not in
88
+ * a control.
89
+ */
90
+ dismiss?: BadgeDismiss;
63
91
  /**
64
92
  * Ping the dot outward — for genuinely LIVE states only (running, connected,
65
93
  * streaming). A pulse on a static label is the badge equivalent of a blinking
@@ -69,5 +97,5 @@ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement>, VariantProp
69
97
  pulse?: boolean;
70
98
  ref?: Ref<HTMLSpanElement>;
71
99
  }
72
- export declare function Badge({ className, tone, size, dot, icon, pulse, children, ref, ...props }: BadgeProps): import("react").JSX.Element;
100
+ export declare function Badge({ className, tone, size, dot, icon, pulse, dismiss, children, ref, ...props }: BadgeProps): import("react").JSX.Element;
73
101
  export { badge as badgeVariants };
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import { type BadgeProps } from "./Badge";
3
3
  export interface StatusOption<Value extends string = string> {
4
4
  value: Value;
@@ -9,6 +9,29 @@ export interface StatusOption<Value extends string = string> {
9
9
  dot?: boolean;
10
10
  /** Reserved for a genuinely live state — see `Badge`'s own `pulse` doc. */
11
11
  pulse?: boolean;
12
+ /**
13
+ * Why this status cannot be chosen HERE. Absent means it can.
14
+ *
15
+ * The option is still listed, still carries its own tone and label, and is
16
+ * still what the trigger shows when the record is sitting in it — it simply
17
+ * never accepts. That is the difference between a status the consumer does
18
+ * not assign and a status the consumer does not have, and only one of them
19
+ * belongs out of the list.
20
+ *
21
+ * Written for the person who would have picked it — "the pipeline enters
22
+ * this stage on its own", not "not allowed". It is rendered under the badge
23
+ * rather than only dimming the row, and attached to the row as its
24
+ * description, so the refusal is never mute. Same call `IconButton`'s
25
+ * `disabledReason` makes, and it is `aria-disabled` there for the same
26
+ * reason: a natively disabled control cannot be reached to be asked why.
27
+ *
28
+ * AN EMPTY STRING THROWS, matching `Step.blocked` rather than
29
+ * `KanbanLane.refusal`. There the reason is supporting detail on a lane that
30
+ * is already visibly refusing; here the sentence IS the mechanism, and an
31
+ * empty one produces exactly the dead row with nothing beside it that this
32
+ * prop exists to prevent.
33
+ */
34
+ refusal?: string;
12
35
  }
13
36
  export interface StatusSelectProps<Value extends string = string> {
14
37
  value: Value;
@@ -18,6 +41,14 @@ export interface StatusSelectProps<Value extends string = string> {
18
41
  /** Accessible name for the trigger — there is no visible label otherwise. */
19
42
  "aria-label"?: string;
20
43
  disabled?: boolean;
44
+ /**
45
+ * What the trigger says when `value` matches no option — a state that is
46
+ * always a mistake somewhere, so this is the wording of the report rather
47
+ * than a label anyone should be seeing.
48
+ *
49
+ * Defaults to "Unknown". It is NOT the raw value: see the component body.
50
+ */
51
+ placeholder?: ReactNode;
21
52
  className?: string;
22
53
  }
23
- export declare function StatusSelect<Value extends string = string>({ value, options, onValueChange, size, "aria-label": ariaLabel, disabled, className, }: StatusSelectProps<Value>): import("react").JSX.Element;
54
+ export declare function StatusSelect<Value extends string = string>({ value, options, onValueChange, size, "aria-label": ariaLabel, disabled, placeholder, className, }: StatusSelectProps<Value>): import("react").JSX.Element;
@@ -40,13 +40,46 @@ export interface DataTableProps<TData> {
40
40
  */
41
41
  pageSize?: number;
42
42
  emptyState?: ReactNode;
43
+ /**
44
+ * An extra class for the rows a consumer needs to mark — "this figure could
45
+ * not be read from the source document", "this record is superseded". Return
46
+ * `undefined` for the rows that are unremarkable.
47
+ *
48
+ * WHY A HOOK AND NOT A `tone` UNION. The reasons a row deserves a tint are
49
+ * the consumer's domain and there is no useful shortlist of them; a fixed set
50
+ * would be wrong for the second consumer and every one after. This is the
51
+ * same call `getRowId` makes about identity.
52
+ *
53
+ * THE CLASS IS APPLIED LAST, so it beats the density, zebra and selection
54
+ * styling the table picked — `cn` is tailwind-merge, and the last conflicting
55
+ * utility wins. That is deliberate: a tint that loses to the zebra fill is a
56
+ * tint nobody sees. It also means a background handed over here REPLACES the
57
+ * row's hover fill, so pass the hover with it (`bg-danger-subtle
58
+ * hover:bg-danger-subtle-hover`) if the row should still respond.
59
+ *
60
+ * THAT EXAMPLE USED TO NAME `warning`, WHICH HAS NO HOVER STEP. `-subtle`
61
+ * exists for every semantic family; `-subtle-hover` exists for `accent` and
62
+ * `danger` only. A consumer following the old wording got a hover class that
63
+ * compiled to nothing — no rule, no fill, no error anywhere, which is the
64
+ * same silent-nothing failure the theme bridge in `postbuild-css.mjs` exists
65
+ * to prevent one level up. Pick a family that has the step, or supply the
66
+ * hover colour yourself. `token-parity.test.ts` now sweeps this docblock.
67
+ *
68
+ * `index` is the row's position in `data`, not on the current page — the same
69
+ * number `getRowId` receives, so the two never disagree about which row is
70
+ * being talked about.
71
+ *
72
+ * NOT CALLED FOR SKELETON ROWS. A skeleton stands in for a record nobody has
73
+ * yet, so there is nothing to classify and nothing to hand over.
74
+ */
75
+ getRowClassName?: (row: TData, index: number) => string | undefined;
43
76
  onRowClick?: (row: TData) => void;
44
77
  /** Rendered above the table; receives the table instance for counts/filters. */
45
78
  toolbar?: (table: TanstackTable<TData>) => ReactNode;
46
79
  className?: string;
47
80
  getRowId?: (row: TData, index: number) => string;
48
81
  }
49
- export declare function DataTable<TData>({ data, columns, density, selectable, maxHeight, zebra, loading, refreshing, loadingRows, pageSize, emptyState, onRowClick, toolbar, className, getRowId, }: DataTableProps<TData>): import("react").JSX.Element;
82
+ export declare function DataTable<TData>({ data, columns, density, selectable, maxHeight, zebra, loading, refreshing, loadingRows, pageSize, emptyState, getRowClassName, onRowClick, toolbar, className, getRowId, }: DataTableProps<TData>): import("react").JSX.Element;
50
83
  export declare function TableEmptyState({ icon, title, description, action, }: {
51
84
  icon?: ReactNode;
52
85
  title?: string;
@@ -3,6 +3,8 @@ import { Toggle as BaseToggle } from "@base-ui/react/toggle";
3
3
  import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
4
4
  import { Toolbar as BaseToolbar } from "@base-ui/react/toolbar";
5
5
  import { type VariantProps } from "tailwind-variants";
6
+ import { type ButtonProps } from "./Button";
7
+ import { type InputProps } from "./Input";
6
8
  declare const toggle: import("tailwind-variants").TVReturnType<{
7
9
  size: {
8
10
  sm: "h-control-sm px-2.5 text-xs";
@@ -62,4 +64,100 @@ export declare function Toolbar({ className, children, ref, ...props }: ToolbarP
62
64
  export declare function ToolbarSeparator({ className }: {
63
65
  className?: string;
64
66
  }): import("react").JSX.Element;
67
+ type ToolbarButtonBase = Omit<BaseToolbar.Button.Props, "className" | "nativeButton" | "render"> & {
68
+ className?: string;
69
+ ref?: Ref<HTMLButtonElement>;
70
+ };
71
+ /**
72
+ * TWO FORMS, AND THE TYPE REFUSES TO MIX THEM.
73
+ *
74
+ * Either this renders its own `<button>` and wears `buttonVariants` — in which
75
+ * case `variant` / `size` / `iconOnly` choose which row of that table — or it
76
+ * renders somebody else's control through `render`, in which case that control
77
+ * already carries a variant table and this one must not add a second. The two
78
+ * are mutually exclusive rather than merely discouraged, because a `render`
79
+ * plus a `size` is not a preference somebody expressed: it is two size classes
80
+ * in one attribute with no rule for which wins. See `ToolbarButton`'s own doc
81
+ * for the measurement.
82
+ */
83
+ export type ToolbarButtonProps = (ToolbarButtonBase & Pick<ButtonProps, "variant" | "size" | "iconOnly"> & {
84
+ /** The styled form owns the element, so there is nothing to render into. */
85
+ render?: undefined;
86
+ }) | (ToolbarButtonBase & {
87
+ render: NonNullable<BaseToolbar.Button.Props["render"]>;
88
+ variant?: never;
89
+ size?: never;
90
+ iconOnly?: never;
91
+ });
92
+ /**
93
+ * A button that is part of the toolbar's rotation.
94
+ *
95
+ * It wears `buttonVariants` — the same recipe `Button` compiles — rather than a
96
+ * second class list, so the two can never drift apart. It is not `Button`
97
+ * itself because the registration has to own the element: Base UI needs to put
98
+ * its own `tabIndex` and key handling on the rendered node, and the
99
+ * `focusableWhenDisabled` behaviour below depends on NOT setting the native
100
+ * `disabled` attribute, which `Button` sets unconditionally.
101
+ *
102
+ * `ghost` at `sm` by default — the right resting state in a row of controls,
103
+ * the same default `IconButton` picks. A row of solid buttons reads as a row of
104
+ * primary actions, which no toolbar is.
105
+ *
106
+ * ANYTHING ALREADY BUILT ON `Button` GOES IN THROUGH `render`, which is what
107
+ * keeps this from becoming a second button component:
108
+ * `<ToolbarButton render={<IconButton icon={…} label="Bold" />} />` registers
109
+ * exactly the same and keeps the tooltip and the `disabledReason`.
110
+ *
111
+ * THAT PATH APPLIES NO VARIANT TABLE AT ALL, and the type enforces it —
112
+ * `variant`, `size` and `iconOnly` are not accepted beside `render`. Base UI's
113
+ * render slot CONCATENATES `className`; it does not run `cn`. So a recipe
114
+ * applied here landed in the same attribute as the rendered component's own,
115
+ * and which one won was decided by Tailwind's emission order rather than by
116
+ * either component. It measured fine while the two happened to agree — a
117
+ * default `render={<IconButton/>}` came out 36×36, identical to a standalone
118
+ * one, while carrying both `aspect-square` and `px-3.5` — and broke visibly the
119
+ * moment they disagreed: `<ToolbarButton size="lg" render={<IconButton
120
+ * size="sm" …/>} />` carried `h-control-lg` AND `h-control-sm` and rendered
121
+ * 48 × 36 with 24px of padding, a squashed oval neither component asked for.
122
+ *
123
+ * Unrepresentable beats documented, and it is free to do NOW: nothing has
124
+ * adopted the pair yet, so narrowing the type costs nothing that removing the
125
+ * props after adoption would have cost.
126
+ */
127
+ export declare function ToolbarButton({ variant, size, iconOnly, className, children, ref, ...props }: ToolbarButtonProps): import("react").JSX.Element;
128
+ export interface ToolbarInputProps extends Omit<InputProps, "ref">, Pick<BaseToolbar.Input.Props, "focusableWhenDisabled"> {
129
+ ref?: Ref<HTMLInputElement>;
130
+ }
131
+ /**
132
+ * A text field that is part of the toolbar's rotation — a filter box beside the
133
+ * buttons that filter with it.
134
+ *
135
+ * Renders this library's own `Input` through Base UI's `render` slot, so the
136
+ * adornments, sizes and focus ring are the ones every other field in the
137
+ * application already has. The composite's props land on the `<input>` itself
138
+ * rather than on `Input`'s adornment wrapper, which is what keeps DOM order —
139
+ * and therefore the arrow-key order — matching what the reader sees.
140
+ *
141
+ * Arrow keys still move OUT of it, which is Base UI's deliberate call and not
142
+ * an oversight: a toolbar field is a filter box, not a document, and a caret
143
+ * that traps the only navigation key in the pattern is worse than one that
144
+ * moves on.
145
+ */
146
+ export declare function ToolbarInput({ focusableWhenDisabled, ref, ...props }: ToolbarInputProps): import("react").JSX.Element;
147
+ export interface ToolbarGroupProps extends Omit<BaseToolbar.Group.Props, "className"> {
148
+ className?: string;
149
+ ref?: Ref<HTMLDivElement>;
150
+ }
151
+ /**
152
+ * A labelled cluster of toolbar controls — "Alignment", "Text style".
153
+ *
154
+ * IT IS NOT ITSELF A STOP. The group renders `role="group"` and its CHILDREN
155
+ * register; a group that registered would insert a phantom position between the
156
+ * controls it contains. Give it an `aria-label`: a group announced as an
157
+ * unnamed group is noise in the rotation rather than context for it.
158
+ *
159
+ * Distinct from `ToggleGroup`, which is a set of related toggles with a shared
160
+ * VALUE. This one shares only a name.
161
+ */
162
+ export declare function ToolbarGroup({ className, children, ref, ...props }: ToolbarGroupProps): import("react").JSX.Element;
65
163
  export {};
@@ -1,13 +1,39 @@
1
1
  import { type ReactNode, type Ref } from "react";
2
2
  import type { ChatFile } from "./types";
3
- /** Binary units, because these are file sizes and every OS reports them this way. */
4
- export declare function formatBytes(bytes: number): string;
3
+ import type { IconName } from "../Icon";
5
4
  export type AttachmentLayout = "tile" | "row";
6
5
  export interface AttachmentProps {
7
6
  file: ChatFile;
8
7
  layout?: AttachmentLayout;
9
8
  /** Overrides the size line — "Sheet 2 of 4", a page count, anything truer. */
10
9
  meta?: ReactNode;
10
+ /**
11
+ * The consumer's own control, rendered first in the action cluster — "Extract
12
+ * this", "Attach to the demand", "Cancel the upload".
13
+ *
14
+ * A SLOT RATHER THAN ANOTHER BOOLEAN, because the library has no opinion
15
+ * about what the verb is and a fixed list would be wrong for the second
16
+ * consumer and every one after. Remove, preview and download stay separate
17
+ * props: they are the three actions an attachment has REGARDLESS of the
18
+ * application, and each carries its own rule about when it may appear.
19
+ *
20
+ * NO RULE IS IMPOSED ON THIS ONE. Download hides itself while a file is still
21
+ * uploading, because there is nothing complete to fetch; the consumer's action
22
+ * may well BE "cancel this upload", so it is rendered whatever the file's
23
+ * state, and the consumer decides whether their verb makes sense yet.
24
+ *
25
+ * It goes inside the cluster, above the full-card preview trigger — rendered
26
+ * anywhere else it would sit UNDER that trigger and never receive a click.
27
+ * Keep it to a control or two: this is a corner of a tile, not a menu bar.
28
+ *
29
+ * FILL IT WITH `AttachmentAction`, which is exported for exactly this. The
30
+ * built-in download and remove are that component, so anything else lands in
31
+ * the cluster at a different size and, on a tile, without the hover fade the
32
+ * two beside it have. Any node is accepted — the slot has no opinion — but
33
+ * "matches the built-ins" is a real requirement and there is no way to hit it
34
+ * by hand. Pass `tinted` when `layout` is `"tile"`.
35
+ */
36
+ action?: ReactNode;
11
37
  onRemove?: (file: ChatFile) => void;
12
38
  onPreview?: (file: ChatFile) => void;
13
39
  onDownload?: (file: ChatFile) => void;
@@ -17,7 +43,46 @@ export interface AttachmentProps {
17
43
  className?: string;
18
44
  ref?: Ref<HTMLDivElement>;
19
45
  }
20
- export declare function Attachment({ file, layout, meta, onRemove, onPreview, onDownload, selected, disabled, className, ref, }: AttachmentProps): import("react").JSX.Element;
46
+ export declare function Attachment({ file, layout, meta, action, onRemove, onPreview, onDownload, selected, disabled, className, ref, }: AttachmentProps): import("react").JSX.Element;
47
+ export interface AttachmentActionProps {
48
+ icon: IconName;
49
+ /** The accessible name AND the tooltip. Include the file: "Extract report.pdf". */
50
+ label: string;
51
+ onClick: () => void;
52
+ disabled?: boolean;
53
+ /**
54
+ * On a tile the buttons sit over imagery, so they need their own ground —
55
+ * and they FADE IN on hover rather than sitting permanently over the picture.
56
+ *
57
+ * Pass `layout === "tile"`. Getting it wrong is not a style preference: a
58
+ * tinted control on a row is dark-on-dark, and an untinted one on a tile is
59
+ * a grey glyph over an arbitrary photograph.
60
+ */
61
+ tinted?: boolean;
62
+ }
63
+ /**
64
+ * One control in an attachment's action cluster.
65
+ *
66
+ * THE CONTROL `Attachment.action` IS MEANT TO BE FILLED WITH, and until now it
67
+ * was module-private — so the slot existed with nothing in the library that
68
+ * fits it. A consumer's own `IconButton` is a different size and a different
69
+ * resting state, and on a tile it does not fade in on `group-hover/attachment`
70
+ * with the built-in download and remove beside it, so the one control the
71
+ * consumer added was the one that looked wrong.
72
+ *
73
+ * It is deliberately NOT `IconButton`: `size-6` rather than a control height,
74
+ * no ripple, no `disabledReason`, and a tinted variant that only means anything
75
+ * inside `.group/attachment`. That is a corner-of-a-tile control, not a general
76
+ * one, and generalising it would give the cluster two different buttons again.
77
+ *
78
+ * <Attachment
79
+ * file={file}
80
+ * layout="tile"
81
+ * action={<AttachmentAction icon="sparkles" label={`Extract ${file.name}`}
82
+ * onClick={() => extract(file)} tinted />}
83
+ * />
84
+ */
85
+ export declare function AttachmentAction({ icon, label, onClick, disabled, tinted, }: AttachmentActionProps): import("react").JSX.Element;
21
86
  export interface AttachmentPreviewProps {
22
87
  file: ChatFile | null;
23
88
  onOpenChange: (open: boolean) => void;
@@ -38,6 +103,15 @@ export declare function AttachmentPreview({ file, onOpenChange, onDownload, rend
38
103
  export interface AttachmentListProps {
39
104
  files: ChatFile[];
40
105
  layout?: AttachmentLayout;
106
+ /**
107
+ * `Attachment.action`, per file — a FUNCTION rather than a node, because the
108
+ * control almost always has to know which file it is on.
109
+ *
110
+ * Without it the slot would exist only on a lone `Attachment`, and a consumer
111
+ * wanting one on a list would have to stop using the list — which is the
112
+ * rebuild this slot was added to prevent, one level up.
113
+ */
114
+ action?: (file: ChatFile) => ReactNode;
41
115
  onRemove?: (file: ChatFile) => void;
42
116
  onPreview?: (file: ChatFile) => void;
43
117
  onDownload?: (file: ChatFile) => void;
@@ -46,4 +120,4 @@ export interface AttachmentListProps {
46
120
  disabled?: boolean;
47
121
  className?: string;
48
122
  }
49
- export declare function AttachmentList({ files, layout, onRemove, onPreview, onDownload, max, disabled, className, }: AttachmentListProps): import("react").JSX.Element | null;
123
+ export declare function AttachmentList({ files, layout, action, onRemove, onPreview, onDownload, max, disabled, className, }: AttachmentListProps): import("react").JSX.Element | null;
@@ -6,16 +6,6 @@ export interface ContextSegment {
6
6
  /** Any CSS colour. Defaults walk a fixed palette in declaration order. */
7
7
  color?: string;
8
8
  }
9
- /**
10
- * 241_600 → "241.6k".
11
- *
12
- * One decimal all the way to 1M, and exact below 1k. Unlike a file size — where
13
- * a tenth of a kilobyte is noise on a number nobody acts on — the tenth here is
14
- * the digit that moves while you work: at 1M capacity, a whole percent is
15
- * 10_000 tokens, so rounding to "242k" holds a figure still for several
16
- * exchanges and makes a live meter look frozen.
17
- */
18
- export declare function formatTokens(tokens: number): string;
19
9
  export interface UsageLimit {
20
10
  id: string;
21
11
  label: string;
@@ -14,7 +14,7 @@ export { registerIcon, registerIcons, getIcon, hasIcon, iconNames, type IconNode
14
14
  export { coreIcons, type CoreIconName } from "./icons/data";
15
15
  export { Button, buttonVariants, type ButtonProps } from "./components/Button";
16
16
  export { IconButton, type IconButtonProps, type IconButtonVariant, type IconButtonTone, } from "./components/IconButton";
17
- export { Badge, badgeVariants, type BadgeProps } from "./components/Badge";
17
+ export { Badge, badgeVariants, type BadgeProps, type BadgeDismiss, } from "./components/Badge";
18
18
  export { Callout, calloutVariants, type CalloutProps } from "./components/Callout";
19
19
  export { Spinner, type SpinnerProps } from "./components/Spinner";
20
20
  export { Skeleton, SkeletonText, LoadingOverlay, type SkeletonProps, type SkeletonTextProps, type LoadingOverlayProps, } from "./components/Loading";
@@ -48,7 +48,7 @@ export { Accordion, AccordionItem, type AccordionProps, type AccordionItemProps,
48
48
  export { Breadcrumb, Pagination, type BreadcrumbProps, type PaginationProps, type Crumb, } from "./components/Navigation";
49
49
  export { Stepper, stepStatus, stepperProgress, advanceBlockReason, assertSteps, type StepperProps, type Step, type StepStatus, type StepperProgress, } from "./components/stepper";
50
50
  export { StatusChecklist, tallyChecklist, type StatusChecklistProps, type ChecklistItem, type ChecklistState, type ChecklistTally, } from "./components/checklist";
51
- export { Toggle, ToggleGroup, Toolbar, ToolbarSeparator, type ToggleProps, type ToggleGroupProps, type ToolbarProps, } from "./components/Toggle";
51
+ export { Toggle, ToggleGroup, Toolbar, ToolbarButton, ToolbarGroup, ToolbarInput, ToolbarSeparator, type ToggleProps, type ToggleGroupProps, type ToolbarProps, type ToolbarButtonProps, type ToolbarGroupProps, type ToolbarInputProps, } from "./components/Toggle";
52
52
  export { Separator, ScrollArea, Collapsible, EmptyState, Timeline, type SeparatorProps, type ScrollAreaProps, type CollapsibleProps, type EmptyStateProps, type TimelineProps, type TimelineItem, } from "./components/Surface";
53
53
  export { PanelGroup, Panel, PanelResizer, type PanelGroupProps, type PanelProps, type PanelResizerProps, type PanelDirection, type PanelNarrowMode, } from "./components/Panel";
54
54
  export { Avatar, AvatarGroup, type AvatarProps, type AvatarGroupProps, type AvatarStatus, } from "./components/Avatar";
@@ -85,11 +85,12 @@ export { Message, MessagePartView, ToolCall, ChatMarker, type MessageProps, type
85
85
  export { MessageScroller, type MessageScrollerProps, } from "./components/chat/MessageScroller";
86
86
  export { Composer, type ComposerProps } from "./components/chat/Composer";
87
87
  export { Markdown, CodeBlock, type MarkdownProps, type CodeBlockProps } from "./components/chat/Markdown";
88
- export { Attachment, AttachmentList, AttachmentPreview, formatBytes, type AttachmentProps, type AttachmentListProps, type AttachmentPreviewProps, type AttachmentLayout, } from "./components/chat/Attachment";
88
+ export { Attachment, AttachmentAction, AttachmentList, AttachmentPreview, type AttachmentProps, type AttachmentActionProps, type AttachmentListProps, type AttachmentPreviewProps, type AttachmentLayout, } from "./components/chat/Attachment";
89
89
  export { ModelSelector, type ModelSelectorProps, type ChatModel, } from "./components/chat/ModelSelector";
90
90
  export { EffortControl, DEFAULT_EFFORT_LEVELS, type EffortControlProps, type EffortLevel, } from "./components/chat/EffortControl";
91
- export { ContextMeter, ChatStatus, formatTokens, type ContextMeterProps, type ContextSegment, type UsageLimit, type ChatStatusProps, } from "./components/chat/ContextMeter";
91
+ export { ContextMeter, ChatStatus, type ContextMeterProps, type ContextSegment, type UsageLimit, type ChatStatusProps, } from "./components/chat/ContextMeter";
92
92
  export { cn } from "./lib/cn";
93
+ export { formatBytes, formatTokens } from "./lib/format";
93
94
  export { useFillHeight } from "./lib/use-fill-height";
94
95
  export { useTheme, useUiScale, useContrast, useMotionPreference, type Theme, type UiScale, type ContrastPreference, type MotionPreference, } from "./lib/preferences";
95
96
  export { usePersistedState, readStored, writeStored, clearStored, storageKey, type PersistedStateOptions, } from "./lib/persist";
@@ -0,0 +1,12 @@
1
+ /** Binary units, because these are file sizes and every OS reports them this way. */
2
+ export declare function formatBytes(bytes: number): string;
3
+ /**
4
+ * 241_600 → "241.6k".
5
+ *
6
+ * One decimal all the way to 1M, and exact below 1k. Unlike a file size — where
7
+ * a tenth of a kilobyte is noise on a number nobody acts on — the tenth here is
8
+ * the digit that moves while you work: at 1M capacity, a whole percent is
9
+ * 10_000 tokens, so rounding to "242k" holds a figure still for several
10
+ * exchanges and makes a live meter look frozen.
11
+ */
12
+ export declare function formatTokens(tokens: number): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `@danxbot/ui/pure` — the half of this package that is not a UI.
3
+ *
4
+ * WHY A SECOND ENTRY EXISTS. The package exported exactly one, `.`, which is
5
+ * the whole design system. So an application's own formatting layer — a plain
6
+ * module that turns records into strings, imported by workers, tests, server
7
+ * code and components alike — could not reach `formatBytes` without importing
8
+ * React and every component in this library into a layer that is deliberately
9
+ * framework-free. The consumer's answer was to copy the function, which is how
10
+ * two places start rendering the same quantity to different precision.
11
+ *
12
+ * THE CONTRACT, AND IT IS THE ONLY REASON THIS ENTRY IS WORTH HAVING: nothing
13
+ * reachable from here imports React, `react-dom`, or the JSX runtime. An entry
14
+ * that quietly pulls the React graph back in is not a smaller import, it is the
15
+ * same import with a second name — and it would fail silently, because a
16
+ * consumer's bundle does not announce what got dragged into it.
17
+ *
18
+ * So the claim is checked against the BUILT ARTIFACT rather than reasoned about
19
+ * from the source: `pure-entry.test.ts` reads `dist/pure.js`, extracts every
20
+ * import specifier it actually emitted, and asserts none of them is React —
21
+ * plus a size ceiling, so React cannot have been inlined instead of imported.
22
+ * A source-graph sweep in the same file catches it one step earlier, before a
23
+ * build has run.
24
+ *
25
+ * WHAT BELONGS HERE. Decisions about how a value READS, with no dependencies:
26
+ * precision, units, the point at which a rounded figure stops telling the
27
+ * truth. Deliberately NOT the calendar and time layers — `formatDateRange`,
28
+ * `hoursInDay` and the rest are equally pure and would drag the whole `Intl`
29
+ * time layer behind them, which is a bigger entry answering a question nobody
30
+ * has asked yet. That is a separate decision, to be made when a consumer needs
31
+ * it and not before.
32
+ *
33
+ * Everything exported here is ALSO exported from the main entry, so nothing
34
+ * about this is a migration and no existing import has to change.
35
+ */
36
+ export { formatBytes, formatTokens } from "./lib/format";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danxbot/ui",
3
- "version": "3.8.0",
3
+ "version": "3.9.1",
4
4
  "type": "module",
5
5
  "description": "Danxbot — a domain-agnostic React design system with motion as a first-class primitive.",
6
6
  "license": "MIT",
@@ -35,6 +35,10 @@
35
35
  "types": "./dist/types/index.d.ts",
36
36
  "import": "./dist/index.js"
37
37
  },
38
+ "./pure": {
39
+ "types": "./dist/types/pure.d.ts",
40
+ "import": "./dist/pure.js"
41
+ },
38
42
  "./styles.css": "./dist/styles.css",
39
43
  "./theme.css": "./dist/theme.css",
40
44
  "./fonts.css": "./dist/fonts.css",
@@ -56,13 +60,13 @@
56
60
  "audit:cards": "node scripts/audit-card-theme.mjs",
57
61
  "audit:hover": "node scripts/audit-hover.mjs",
58
62
  "test": "vitest run",
59
- "build:lib": "vite build --config vite.lib.config.ts && node scripts/postbuild-css.mjs && npm run build:types",
63
+ "build:lib": "vite build --config vite.lib.config.ts && vite build --config vite.pure.config.ts && node scripts/postbuild-css.mjs && npm run build:types",
60
64
  "fonts": "node scripts/fetch-fonts.mjs",
61
65
  "icons": "node scripts/build-icons.mjs",
62
66
  "build:types": "tsc -p tsconfig.build.json",
63
67
  "build:demo": "vite build",
64
68
  "build": "npm run build:lib && npm run build:demo",
65
- "verify": "npm run typecheck && npm run test && npm run build && npm run typecheck:previews && npm run audit:previews && npm run audit:visual-stale",
69
+ "verify": "npm run typecheck && npm run build && npm run test && npm run typecheck:previews && npm run audit:previews && npm run audit:visual-stale",
66
70
  "audit:visual-stale": "node scripts/audit-visual-stale.mjs",
67
71
  "preview": "vite preview",
68
72
  "preview:built": "vite preview --outDir demo-dist --port 5801 --strictPort",