@danxbot/ui 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,38 @@ 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-warning-subtle
58
+ * hover:bg-warning-subtle-hover`) if the row should still respond.
59
+ *
60
+ * `index` is the row's position in `data`, not on the current page — the same
61
+ * number `getRowId` receives, so the two never disagree about which row is
62
+ * being talked about.
63
+ *
64
+ * NOT CALLED FOR SKELETON ROWS. A skeleton stands in for a record nobody has
65
+ * yet, so there is nothing to classify and nothing to hand over.
66
+ */
67
+ getRowClassName?: (row: TData, index: number) => string | undefined;
43
68
  onRowClick?: (row: TData) => void;
44
69
  /** Rendered above the table; receives the table instance for counts/filters. */
45
70
  toolbar?: (table: TanstackTable<TData>) => ReactNode;
46
71
  className?: string;
47
72
  getRowId?: (row: TData, index: number) => string;
48
73
  }
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;
74
+ 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
75
  export declare function TableEmptyState({ icon, title, description, action, }: {
51
76
  icon?: ReactNode;
52
77
  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,64 @@ 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
+ export interface ToolbarButtonProps extends Omit<BaseToolbar.Button.Props, "className" | "nativeButton">, Pick<ButtonProps, "variant" | "size" | "iconOnly"> {
68
+ className?: string;
69
+ ref?: Ref<HTMLButtonElement>;
70
+ }
71
+ /**
72
+ * A button that is part of the toolbar's rotation.
73
+ *
74
+ * It wears `buttonVariants` — the same recipe `Button` compiles — rather than a
75
+ * second class list, so the two can never drift apart. It is not `Button`
76
+ * itself because the registration has to own the element: Base UI needs to put
77
+ * its own `tabIndex` and key handling on the rendered node, and the
78
+ * `focusableWhenDisabled` behaviour below depends on NOT setting the native
79
+ * `disabled` attribute, which `Button` sets unconditionally.
80
+ *
81
+ * `ghost` at `sm` by default — the right resting state in a row of controls,
82
+ * the same default `IconButton` picks. A row of solid buttons reads as a row of
83
+ * primary actions, which no toolbar is.
84
+ *
85
+ * ANYTHING ALREADY BUILT ON `Button` GOES IN THROUGH `render`, which is what
86
+ * keeps this from becoming a second button component:
87
+ * `<ToolbarButton render={<IconButton icon={…} label="Bold" />} />` registers
88
+ * exactly the same, keeps the tooltip and the `disabledReason`, and never
89
+ * duplicates a variant table.
90
+ */
91
+ export declare function ToolbarButton({ variant, size, iconOnly, className, children, ref, ...props }: ToolbarButtonProps): import("react").JSX.Element;
92
+ export interface ToolbarInputProps extends Omit<InputProps, "ref">, Pick<BaseToolbar.Input.Props, "focusableWhenDisabled"> {
93
+ ref?: Ref<HTMLInputElement>;
94
+ }
95
+ /**
96
+ * A text field that is part of the toolbar's rotation — a filter box beside the
97
+ * buttons that filter with it.
98
+ *
99
+ * Renders this library's own `Input` through Base UI's `render` slot, so the
100
+ * adornments, sizes and focus ring are the ones every other field in the
101
+ * application already has. The composite's props land on the `<input>` itself
102
+ * rather than on `Input`'s adornment wrapper, which is what keeps DOM order —
103
+ * and therefore the arrow-key order — matching what the reader sees.
104
+ *
105
+ * Arrow keys still move OUT of it, which is Base UI's deliberate call and not
106
+ * an oversight: a toolbar field is a filter box, not a document, and a caret
107
+ * that traps the only navigation key in the pattern is worse than one that
108
+ * moves on.
109
+ */
110
+ export declare function ToolbarInput({ focusableWhenDisabled, ref, ...props }: ToolbarInputProps): import("react").JSX.Element;
111
+ export interface ToolbarGroupProps extends Omit<BaseToolbar.Group.Props, "className"> {
112
+ className?: string;
113
+ ref?: Ref<HTMLDivElement>;
114
+ }
115
+ /**
116
+ * A labelled cluster of toolbar controls — "Alignment", "Text style".
117
+ *
118
+ * IT IS NOT ITSELF A STOP. The group renders `role="group"` and its CHILDREN
119
+ * register; a group that registered would insert a phantom position between the
120
+ * controls it contains. Give it an `aria-label`: a group announced as an
121
+ * unnamed group is noise in the rotation rather than context for it.
122
+ *
123
+ * Distinct from `ToggleGroup`, which is a set of related toggles with a shared
124
+ * VALUE. This one shares only a name.
125
+ */
126
+ export declare function ToolbarGroup({ className, children, ref, ...props }: ToolbarGroupProps): import("react").JSX.Element;
65
127
  export {};
@@ -1,13 +1,31 @@
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;
5
3
  export type AttachmentLayout = "tile" | "row";
6
4
  export interface AttachmentProps {
7
5
  file: ChatFile;
8
6
  layout?: AttachmentLayout;
9
7
  /** Overrides the size line — "Sheet 2 of 4", a page count, anything truer. */
10
8
  meta?: ReactNode;
9
+ /**
10
+ * The consumer's own control, rendered first in the action cluster — "Extract
11
+ * this", "Attach to the demand", "Cancel the upload".
12
+ *
13
+ * A SLOT RATHER THAN ANOTHER BOOLEAN, because the library has no opinion
14
+ * about what the verb is and a fixed list would be wrong for the second
15
+ * consumer and every one after. Remove, preview and download stay separate
16
+ * props: they are the three actions an attachment has REGARDLESS of the
17
+ * application, and each carries its own rule about when it may appear.
18
+ *
19
+ * NO RULE IS IMPOSED ON THIS ONE. Download hides itself while a file is still
20
+ * uploading, because there is nothing complete to fetch; the consumer's action
21
+ * may well BE "cancel this upload", so it is rendered whatever the file's
22
+ * state, and the consumer decides whether their verb makes sense yet.
23
+ *
24
+ * It goes inside the cluster, above the full-card preview trigger — rendered
25
+ * anywhere else it would sit UNDER that trigger and never receive a click.
26
+ * Keep it to a control or two: this is a corner of a tile, not a menu bar.
27
+ */
28
+ action?: ReactNode;
11
29
  onRemove?: (file: ChatFile) => void;
12
30
  onPreview?: (file: ChatFile) => void;
13
31
  onDownload?: (file: ChatFile) => void;
@@ -17,7 +35,7 @@ export interface AttachmentProps {
17
35
  className?: string;
18
36
  ref?: Ref<HTMLDivElement>;
19
37
  }
20
- export declare function Attachment({ file, layout, meta, onRemove, onPreview, onDownload, selected, disabled, className, ref, }: AttachmentProps): import("react").JSX.Element;
38
+ export declare function Attachment({ file, layout, meta, action, onRemove, onPreview, onDownload, selected, disabled, className, ref, }: AttachmentProps): import("react").JSX.Element;
21
39
  export interface AttachmentPreviewProps {
22
40
  file: ChatFile | null;
23
41
  onOpenChange: (open: boolean) => void;
@@ -38,6 +56,15 @@ export declare function AttachmentPreview({ file, onOpenChange, onDownload, rend
38
56
  export interface AttachmentListProps {
39
57
  files: ChatFile[];
40
58
  layout?: AttachmentLayout;
59
+ /**
60
+ * `Attachment.action`, per file — a FUNCTION rather than a node, because the
61
+ * control almost always has to know which file it is on.
62
+ *
63
+ * Without it the slot would exist only on a lone `Attachment`, and a consumer
64
+ * wanting one on a list would have to stop using the list — which is the
65
+ * rebuild this slot was added to prevent, one level up.
66
+ */
67
+ action?: (file: ChatFile) => ReactNode;
41
68
  onRemove?: (file: ChatFile) => void;
42
69
  onPreview?: (file: ChatFile) => void;
43
70
  onDownload?: (file: ChatFile) => void;
@@ -46,4 +73,4 @@ export interface AttachmentListProps {
46
73
  disabled?: boolean;
47
74
  className?: string;
48
75
  }
49
- export declare function AttachmentList({ files, layout, onRemove, onPreview, onDownload, max, disabled, className, }: AttachmentListProps): import("react").JSX.Element | null;
76
+ 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, AttachmentList, AttachmentPreview, type AttachmentProps, 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.0",
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,7 +60,7 @@
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",