@forthtilliath/forth-ui 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +16 -12
  2. package/dist/components/column-menu/column-menu.d.ts +39 -0
  3. package/dist/components/column-menu/column-menu.d.ts.map +1 -0
  4. package/dist/components/column-menu/column-menu.js +74 -0
  5. package/dist/components/column-menu/index.d.ts +6 -0
  6. package/dist/components/column-menu/index.d.ts.map +1 -0
  7. package/dist/components/column-menu/index.js +3 -0
  8. package/dist/components/column-menu/sortable-column-row.d.ts +13 -0
  9. package/dist/components/column-menu/sortable-column-row.d.ts.map +1 -0
  10. package/dist/components/column-menu/sortable-column-row.js +22 -0
  11. package/dist/components/column-menu/types.d.ts +21 -0
  12. package/dist/components/column-menu/types.d.ts.map +1 -0
  13. package/dist/components/column-menu/types.js +7 -0
  14. package/dist/components/column-menu/use-dnd-sensors.d.ts +6 -0
  15. package/dist/components/column-menu/use-dnd-sensors.d.ts.map +1 -0
  16. package/dist/components/column-menu/use-dnd-sensors.js +14 -0
  17. package/dist/components/command-palette/command-palette.js +1 -1
  18. package/dist/components/gradient-frame/gradient-frame.d.ts +27 -0
  19. package/dist/components/gradient-frame/gradient-frame.d.ts.map +1 -0
  20. package/dist/components/gradient-frame/gradient-frame.js +16 -0
  21. package/dist/components/gradient-frame/index.d.ts +3 -0
  22. package/dist/components/gradient-frame/index.d.ts.map +1 -0
  23. package/dist/components/gradient-frame/index.js +1 -0
  24. package/dist/components/grid/variants.d.ts +1 -1
  25. package/dist/components/icon-bubble/icon-bubble.d.ts +21 -0
  26. package/dist/components/icon-bubble/icon-bubble.d.ts.map +1 -0
  27. package/dist/components/icon-bubble/icon-bubble.js +16 -0
  28. package/dist/components/icon-bubble/index.d.ts +4 -0
  29. package/dist/components/icon-bubble/index.d.ts.map +1 -0
  30. package/dist/components/icon-bubble/index.js +1 -0
  31. package/dist/components/icon-bubble/variants.d.ts +12 -0
  32. package/dist/components/icon-bubble/variants.d.ts.map +1 -0
  33. package/dist/components/icon-bubble/variants.js +19 -0
  34. package/dist/components/image-input/image-input.js +1 -1
  35. package/dist/components/relative-time/relative-time.js +1 -1
  36. package/dist/components/toggle-switch/index.d.ts +4 -0
  37. package/dist/components/toggle-switch/index.d.ts.map +1 -0
  38. package/dist/components/toggle-switch/index.js +1 -0
  39. package/dist/components/toggle-switch/toggle-switch.d.ts +37 -0
  40. package/dist/components/toggle-switch/toggle-switch.d.ts.map +1 -0
  41. package/dist/components/toggle-switch/toggle-switch.js +25 -0
  42. package/dist/components/toggle-switch/variants.d.ts +9 -0
  43. package/dist/components/toggle-switch/variants.d.ts.map +1 -0
  44. package/dist/components/toggle-switch/variants.js +30 -0
  45. package/dist/globals.css +290 -18
  46. package/package.json +24 -6
package/README.md CHANGED
@@ -8,7 +8,7 @@ built from `Popover` + `Command`, a `PasswordInput` with a strength meter, a
8
8
  `Dropdrawer` that's a `DropdownMenu` on desktop and a `Drawer` on mobile —
9
9
  so you don't have to wire the same recipe from scratch in every project.
10
10
 
11
- 58 components across 8 categories, styled with Tailwind CSS and
11
+ 62 components across 8 categories, styled with Tailwind CSS and
12
12
  `class-variance-authority`, fully typed, and browsable in Storybook
13
13
  (`pnpm run dev:sb` from the repo root).
14
14
 
@@ -186,6 +186,7 @@ Grouped the same way as the Storybook sidebar (`forth-ui/<Category>/...`).
186
186
  | `Rating` | `components/rating` | A star rating control with keyboard navigation and hover preview. |
187
187
  | `SubmitButton` | `components/submit-button` | A submit `Button` that shows its own loading state via `useFormStatus`. |
188
188
  | `TagsInput` | `components/tags-input` | A flexible input for adding/removing multiple tags. |
189
+ | `ToggleSwitch` | `components/toggle-switch` | An on/off switch with an optional visible label and description. |
189
190
 
190
191
  ### Feedback
191
192
 
@@ -211,17 +212,20 @@ Grouped the same way as the Storybook sidebar (`forth-ui/<Category>/...`).
211
212
 
212
213
  ### Data Display
213
214
 
214
- | Component | Import | Description |
215
- | ----------------------- | ----------------------------- | ------------------------------------------------------------------------ |
216
- | `Avatar`, `AvatarGroup` | `components/avatar` | An image with a fallback, status/badge indicators, and stacked grouping. |
217
- | `DescriptionList` | `components/description-list` | A description list, with terms and descriptions. |
218
- | `FileList` | `components/file-list` | Displays a list of files with name, size, and upload progress. |
219
- | `Kbd` | `components/kbd` | Displays which key or combination of keys performs a given action. |
220
- | `QrCode` | `components/qr-code` | Generates a QR code from a string, rendered as inline SVG. |
221
- | `RelativeTime` | `components/relative-time` | Displays a live-updating time, defaulting to the local timezone. |
222
- | `Sortable` | `components/sortable` | A drag-to-reorder list built on native HTML5 drag events. |
223
- | `Timeline` | `components/timeline` | A vertical sequence of dated events, connected by a line. |
224
- | `Tree` | `components/tree` | A hierarchical, expandable/collapsible list of nodes. |
215
+ | Component | Import | Description |
216
+ | ------------------------------ | ----------------------------- | ------------------------------------------------------------------------ |
217
+ | `Avatar`, `AvatarGroup` | `components/avatar` | An image with a fallback, status/badge indicators, and stacked grouping. |
218
+ | `ColumnMenu` (`useDndSensors`) | `components/column-menu` | A dropdown to show/hide and reorder (drag and drop) a table's columns. |
219
+ | `DescriptionList` | `components/description-list` | A description list, with terms and descriptions. |
220
+ | `FileList` | `components/file-list` | Displays a list of files with name, size, and upload progress. |
221
+ | `GradientFrame` | `components/gradient-frame` | A visual laid on an offset, gradient-filled frame. |
222
+ | `IconBubble` | `components/icon-bubble` | An icon inside a round, tinted bubble. |
223
+ | `Kbd` | `components/kbd` | Displays which key or combination of keys performs a given action. |
224
+ | `QrCode` | `components/qr-code` | Generates a QR code from a string, rendered as inline SVG. |
225
+ | `RelativeTime` | `components/relative-time` | Displays a live-updating time, defaulting to the local timezone. |
226
+ | `Sortable` | `components/sortable` | A drag-to-reorder list built on native HTML5 drag events. |
227
+ | `Timeline` | `components/timeline` | A vertical sequence of dated events, connected by a line. |
228
+ | `Tree` | `components/tree` | A hierarchical, expandable/collapsible list of nodes. |
225
229
 
226
230
  ### Code & Content
227
231
 
@@ -0,0 +1,39 @@
1
+ import type { ColumnMenuColumn, ColumnMenuLabels } from "./types";
2
+ export interface ColumnMenuProps<C extends ColumnMenuColumn> {
3
+ columns: C[];
4
+ /** Restored by the reset button. */
5
+ defaultColumns: C[];
6
+ /** Receives an updater, so a `useState` setter can be passed directly. */
7
+ onColumnsChange: (update: (prev: C[]) => C[]) => void;
8
+ /** Overrides some or all of the displayed texts. */
9
+ labels?: Partial<ColumnMenuLabels>;
10
+ /** Side of the trigger the dropdown is aligned to (default: `"end"`). */
11
+ align?: "start" | "end";
12
+ /**
13
+ * Custom classes for each part of the component.
14
+ *
15
+ * - `root` wraps the trigger and the dropdown.
16
+ * - `trigger` is the toggle button.
17
+ * - `badge` is the "visible / total" counter inside the trigger.
18
+ * - `content` is the dropdown panel.
19
+ * - `header` is the title + reset bar.
20
+ * - `hint` is the usage hint under the header.
21
+ * - `list` wraps the rows.
22
+ * - `row` is applied to every column row.
23
+ */
24
+ className?: Partial<Record<"root" | "trigger" | "badge" | "content" | "header" | "hint" | "list" | "row", string>>;
25
+ }
26
+ /**
27
+ * A dropdown to show/hide and reorder (drag and drop, touch and keyboard
28
+ * included) the columns of a table, with a "visible / total" counter and a
29
+ * reset button. Closes on outside click and `Escape`.
30
+ *
31
+ * Pair it with `createColumnStorage` from `@forthtilliath/ts-kit` to persist
32
+ * the user's layout.
33
+ *
34
+ * @version 0.1.0
35
+ * @author Forth
36
+ * @copyright 2026 Forth
37
+ */
38
+ export declare function ColumnMenu<C extends ColumnMenuColumn>({ columns, defaultColumns, onColumnsChange, labels, align, className, }: ColumnMenuProps<C>): import("react").JSX.Element;
39
+ //# sourceMappingURL=column-menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"column-menu.d.ts","sourceRoot":"","sources":["../../../src/components/column-menu/column-menu.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAIlE,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,gBAAgB;IACzD,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,oCAAoC;IACpC,cAAc,EAAE,CAAC,EAAE,CAAC;IACpB,0EAA0E;IAC1E,eAAe,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,KAAK,IAAI,CAAC;IACtD,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,yEAAyE;IACzE,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACxB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,OAAO,CACjB,MAAM,CACF,MAAM,GACN,SAAS,GACT,OAAO,GACP,SAAS,GACT,QAAQ,GACR,MAAM,GACN,MAAM,GACN,KAAK,EACP,MAAM,CACP,CACF,CAAC;CACH;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,gBAAgB,EAAE,EACrD,OAAO,EACP,cAAc,EACd,eAAe,EACf,MAAM,EACN,KAAa,EACb,SAAS,GACV,EAAE,eAAe,CAAC,CAAC,CAAC,+BAyJpB"}
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { closestCenter, DndContext } from "@dnd-kit/core";
5
+ import { arrayMove, SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable";
6
+ import { ChevronDown, RotateCcw } from "lucide-react";
7
+ import { cn } from "@forthtilliath/shadcn-ui/lib/utils";
8
+ import { SortableColumnRow } from "./sortable-column-row";
9
+ import { DEFAULT_COLUMN_MENU_LABELS } from "./types";
10
+ import { useDndSensors } from "./use-dnd-sensors";
11
+ /**
12
+ * A dropdown to show/hide and reorder (drag and drop, touch and keyboard
13
+ * included) the columns of a table, with a "visible / total" counter and a
14
+ * reset button. Closes on outside click and `Escape`.
15
+ *
16
+ * Pair it with `createColumnStorage` from `@forthtilliath/ts-kit` to persist
17
+ * the user's layout.
18
+ *
19
+ * @version 0.1.0
20
+ * @author Forth
21
+ * @copyright 2026 Forth
22
+ */
23
+ export function ColumnMenu({ columns, defaultColumns, onColumnsChange, labels, align = "end", className, }) {
24
+ const text = { ...DEFAULT_COLUMN_MENU_LABELS, ...labels };
25
+ // Stable id: avoids a hydration mismatch on dnd-kit's aria-describedby
26
+ const dndId = useId();
27
+ const contentId = useId();
28
+ const rootRef = useRef(null);
29
+ const [open, setOpen] = useState(false);
30
+ const sensors = useDndSensors();
31
+ const visibleCount = columns.filter((c) => c.visible).length;
32
+ const allVisible = visibleCount === columns.length;
33
+ useEffect(() => {
34
+ if (!open)
35
+ return;
36
+ function handlePointerDown(event) {
37
+ if (!rootRef.current?.contains(event.target))
38
+ setOpen(false);
39
+ }
40
+ function handleKeyDown(event) {
41
+ if (event.key === "Escape")
42
+ setOpen(false);
43
+ }
44
+ document.addEventListener("pointerdown", handlePointerDown);
45
+ document.addEventListener("keydown", handleKeyDown);
46
+ return () => {
47
+ document.removeEventListener("pointerdown", handlePointerDown);
48
+ document.removeEventListener("keydown", handleKeyDown);
49
+ };
50
+ }, [open]);
51
+ function handleDragEnd({ active, over }) {
52
+ if (!over || active.id === over.id)
53
+ return;
54
+ onColumnsChange((prev) => {
55
+ const oldIndex = prev.findIndex((c) => c.key === active.id);
56
+ const newIndex = prev.findIndex((c) => c.key === over.id);
57
+ return arrayMove(prev, oldIndex, newIndex);
58
+ });
59
+ }
60
+ function toggleColumn(key) {
61
+ onColumnsChange((prev) => prev.map((c) => (c.key === key ? { ...c, visible: !c.visible } : c)));
62
+ }
63
+ function reset() {
64
+ onColumnsChange(() => defaultColumns);
65
+ setOpen(false);
66
+ }
67
+ return (_jsxs("div", { ref: rootRef, "data-slot": "column-menu", className: cn("relative", className?.root), children: [_jsxs("button", { type: "button", "aria-expanded": open, "aria-controls": contentId, onClick: () => {
68
+ setOpen((v) => !v);
69
+ }, className: cn("flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-sm transition-all", open
70
+ ? "border-primary bg-primary/10 text-primary"
71
+ : "border-border text-foreground/60 hover:border-primary hover:text-foreground", className?.trigger), children: [_jsx("span", { className: "hidden sm:inline", children: text.trigger }), _jsxs("span", { className: cn("rounded-full px-1.5 py-0.5 text-xs font-medium", allVisible
72
+ ? "bg-foreground/10 text-foreground/40"
73
+ : "bg-primary text-primary-foreground", className?.badge), children: [visibleCount, "/", columns.length] }), _jsx(ChevronDown, { className: cn("size-3.5 opacity-60 transition-transform", open && "rotate-180"), "aria-hidden": "true" })] }), open && (_jsxs("div", { id: contentId, "data-slot": "column-menu-content", className: cn("bg-background border-border absolute top-full z-20 mt-2 w-64 overflow-hidden rounded-xl border shadow-xl", align === "end" ? "right-0" : "left-0", className?.content), children: [_jsxs("div", { className: cn("border-border bg-muted flex items-center justify-between border-b px-4 py-3", className?.header), children: [_jsx("p", { className: "text-foreground text-xs font-medium", children: text.title }), _jsxs("button", { type: "button", onClick: reset, className: "text-foreground/50 hover:text-primary flex items-center gap-1 text-xs transition-colors", children: [_jsx(RotateCcw, { className: "size-3", "aria-hidden": "true" }), text.reset] })] }), _jsx("p", { className: cn("border-border bg-muted/50 text-foreground/50 border-b px-4 py-2 text-xs", className?.hint), children: text.hint }), _jsx("div", { className: cn("p-2", className?.list), children: _jsx(DndContext, { id: dndId, sensors: sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: columns.map((c) => c.key), strategy: verticalListSortingStrategy, children: columns.map((column, index) => (_jsx(SortableColumnRow, { column: column, index: index, total: columns.length, dragHandleLabel: text.dragHandle(column.label), onToggle: toggleColumn, className: className?.row }, column.key))) }) }) })] }))] }));
74
+ }
@@ -0,0 +1,6 @@
1
+ export type { ColumnMenuProps } from "./column-menu";
2
+ export { ColumnMenu } from "./column-menu";
3
+ export type { ColumnMenuColumn, ColumnMenuLabels } from "./types";
4
+ export { DEFAULT_COLUMN_MENU_LABELS } from "./types";
5
+ export { useDndSensors } from "./use-dnd-sensors";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/column-menu/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { ColumnMenu } from "./column-menu";
2
+ export { DEFAULT_COLUMN_MENU_LABELS } from "./types";
3
+ export { useDndSensors } from "./use-dnd-sensors";
@@ -0,0 +1,13 @@
1
+ import type { ColumnMenuColumn } from "./types";
2
+ export interface SortableColumnRowProps<K extends string> {
3
+ column: ColumnMenuColumn<K>;
4
+ index: number;
5
+ total: number;
6
+ dragHandleLabel: string;
7
+ onToggle: (key: K) => void;
8
+ /** Carries `data-dragging` while the row is being dragged. */
9
+ className?: string;
10
+ }
11
+ /** One row of `ColumnMenu`: drag handle, visibility switch, position. */
12
+ export declare function SortableColumnRow<K extends string>({ column, index, total, dragHandleLabel, onToggle, className, }: SortableColumnRowProps<K>): import("react").JSX.Element;
13
+ //# sourceMappingURL=sortable-column-row.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sortable-column-row.d.ts","sourceRoot":"","sources":["../../../src/components/column-menu/sortable-column-row.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,MAAM;IACtD,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC;IAC3B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAAE,EAClD,MAAM,EACN,KAAK,EACL,KAAK,EACL,eAAe,EACf,QAAQ,EACR,SAAS,GACV,EAAE,sBAAsB,CAAC,CAAC,CAAC,+BAuD3B"}
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useSortable } from "@dnd-kit/sortable";
4
+ import { CSS } from "@dnd-kit/utilities";
5
+ import { GripVertical } from "lucide-react";
6
+ import { cn } from "@forthtilliath/shadcn-ui/lib/utils";
7
+ import { ToggleSwitch } from "../toggle-switch";
8
+ /** One row of `ColumnMenu`: drag handle, visibility switch, position. */
9
+ export function SortableColumnRow({ column, index, total, dragHandleLabel, onToggle, className, }) {
10
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: column.key });
11
+ return (_jsxs("div", { ref: setNodeRef, "data-slot": "column-menu-row", "data-dragging": isDragging || undefined, style: {
12
+ transform: CSS.Transform.toString(transform),
13
+ transition,
14
+ opacity: isDragging ? 0.5 : 1,
15
+ zIndex: isDragging ? 10 : undefined,
16
+ }, className: cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-colors", isDragging ? "bg-primary/5 border-primary/20 border" : "hover:bg-muted", className), children: [_jsx("button", { type: "button", ...attributes, ...listeners, "aria-label": dragHandleLabel, title: dragHandleLabel, className: "text-foreground/30 hover:text-foreground/60 shrink-0 cursor-grab touch-none transition-colors active:cursor-grabbing", children: _jsx(GripVertical, { className: "size-4", "aria-hidden": "true" }) }), _jsx(ToggleSwitch, { size: "sm", checked: column.visible, onCheckedChange: () => {
17
+ onToggle(column.key);
18
+ }, label: column.label, className: {
19
+ root: "flex-1 gap-3",
20
+ label: "text-foreground/40 group-data-[state=checked]/toggle-switch:text-foreground flex-1 text-left transition-colors",
21
+ } }), _jsxs("span", { className: "text-foreground/30 shrink-0 font-mono text-xs", children: [index + 1, "/", total] })] }));
22
+ }
@@ -0,0 +1,21 @@
1
+ /** A table column the user can show/hide and reorder. */
2
+ export interface ColumnMenuColumn<K extends string = string> {
3
+ key: K;
4
+ label: string;
5
+ visible: boolean;
6
+ }
7
+ /** Texts displayed by `ColumnMenu` (English by default). */
8
+ export interface ColumnMenuLabels {
9
+ /** Trigger button text (hidden on small screens). */
10
+ trigger: string;
11
+ /** Menu title. */
12
+ title: string;
13
+ /** Reset button text. */
14
+ reset: string;
15
+ /** Usage hint under the title. */
16
+ hint: string;
17
+ /** Tooltip / accessible name of a row's drag handle. */
18
+ dragHandle: (label: string) => string;
19
+ }
20
+ export declare const DEFAULT_COLUMN_MENU_LABELS: ColumnMenuLabels;
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/column-menu/types.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IACzD,GAAG,EAAE,CAAC,CAAC;IACP,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,eAAO,MAAM,0BAA0B,EAAE,gBAMxC,CAAC"}
@@ -0,0 +1,7 @@
1
+ export const DEFAULT_COLUMN_MENU_LABELS = {
2
+ trigger: "Columns",
3
+ title: "Visible columns",
4
+ reset: "Reset",
5
+ hint: "Drag to reorder · toggle to show/hide",
6
+ dragHandle: (label) => `Move column ${label}`,
7
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Mouse (5px threshold, so clicks still work), touch (250ms long press, so
3
+ * the list still scrolls) and keyboard sensors for a sortable list.
4
+ */
5
+ export declare function useDndSensors(): import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
6
+ //# sourceMappingURL=use-dnd-sensors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-dnd-sensors.d.ts","sourceRoot":"","sources":["../../../src/components/column-menu/use-dnd-sensors.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,wBAAgB,aAAa,sFAU5B"}
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, } from "@dnd-kit/core";
3
+ import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
4
+ /**
5
+ * Mouse (5px threshold, so clicks still work), touch (250ms long press, so
6
+ * the list still scrolls) and keyboard sensors for a sortable list.
7
+ */
8
+ export function useDndSensors() {
9
+ return useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 5 } }), useSensor(TouchSensor, {
10
+ activationConstraint: { delay: 250, tolerance: 5 },
11
+ }), useSensor(KeyboardSensor, {
12
+ coordinateGetter: sortableKeyboardCoordinates,
13
+ }));
14
+ }
@@ -35,7 +35,7 @@ export function CommandPalette({ groups, open, onOpenChange, placeholder = "Type
35
35
  return () => {
36
36
  document.removeEventListener("keydown", onKeyDown);
37
37
  };
38
- // eslint-disable-next-line @eslint-react/exhaustive-deps, react-hooks/exhaustive-deps -- `setOpen` is a plain function redefined every render; including it would re-register this listener every render instead of only when `isOpen`/`shortcutKey` actually change, which is what its own re-closure over `isOpen` depends on
38
+ // eslint-disable-next-line @eslint-react/exhaustive-deps -- `setOpen` is a plain function redefined every render; including it would re-register this listener every render instead of only when `isOpen`/`shortcutKey` actually change, which is what its own re-closure over `isOpen` depends on
39
39
  }, [isOpen, shortcutKey]);
40
40
  return (_jsxs(CommandDialog, { open: isOpen, onOpenChange: setOpen, title: "Command Palette", description: placeholder, children: [_jsx(CommandInput, { placeholder: placeholder }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: emptyMessage }), groups.map((group, index) => (
41
41
  // eslint-disable-next-line @eslint-react/no-array-index-key -- `heading` is optional and not guaranteed unique
@@ -0,0 +1,27 @@
1
+ import type React from "react";
2
+ export interface GradientFrameProps {
3
+ /** Side the gradient frame sticks out on (default: `"right"`). */
4
+ side?: "left" | "right";
5
+ children: React.ReactNode;
6
+ /**
7
+ * Custom classes for each part of the component.
8
+ *
9
+ * - `root` wraps the frame and the content.
10
+ * - `frame` is the offset gradient panel — set its colors here with
11
+ * gradient stops (`from-* via-* to-*`), the direction (`bg-linear-135`)
12
+ * is already set.
13
+ * - `content` is the rounded, clipped container around `children`.
14
+ */
15
+ className?: Partial<Record<"root" | "frame" | "content", string>>;
16
+ }
17
+ /**
18
+ * A visual (image, video, card…) laid on a rounded frame offset diagonally
19
+ * behind it, filled with a gradient — a decorative "photo on a colored card"
20
+ * effect. The offset grows from `md` screens on.
21
+ *
22
+ * @version 0.1.0
23
+ * @author Forth
24
+ * @copyright 2026 Forth
25
+ */
26
+ export declare function GradientFrame({ side, children, className, }: GradientFrameProps): React.JSX.Element;
27
+ //# sourceMappingURL=gradient-frame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gradient-frame.d.ts","sourceRoot":"","sources":["../../../src/components/gradient-frame/gradient-frame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,WAAW,kBAAkB;IACjC,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,OAAO,GAAG,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;CACnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAc,EACd,QAAQ,EACR,SAAS,GACV,EAAE,kBAAkB,qBAuBpB"}
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@forthtilliath/shadcn-ui/lib/utils";
3
+ /**
4
+ * A visual (image, video, card…) laid on a rounded frame offset diagonally
5
+ * behind it, filled with a gradient — a decorative "photo on a colored card"
6
+ * effect. The offset grows from `md` screens on.
7
+ *
8
+ * @version 0.1.0
9
+ * @author Forth
10
+ * @copyright 2026 Forth
11
+ */
12
+ export function GradientFrame({ side = "right", children, className, }) {
13
+ return (_jsxs("div", { "data-slot": "gradient-frame", className: cn("relative", className?.root), children: [_jsx("div", { "aria-hidden": "true", className: cn("from-primary to-primary/40 absolute inset-0 rounded-3xl bg-linear-135", side === "right"
14
+ ? "translate-x-3 translate-y-3 md:translate-x-5 md:translate-y-5"
15
+ : "-translate-x-3 translate-y-3 md:-translate-x-5 md:translate-y-5", className?.frame) }), _jsx("div", { className: cn("relative overflow-hidden rounded-3xl shadow-xl", className?.content), children: children })] }));
16
+ }
@@ -0,0 +1,3 @@
1
+ export type { GradientFrameProps } from "./gradient-frame";
2
+ export { GradientFrame } from "./gradient-frame";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/gradient-frame/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1 @@
1
+ export { GradientFrame } from "./gradient-frame";
@@ -1,6 +1,6 @@
1
1
  import type { VariantProps } from "class-variance-authority";
2
2
  export declare const gridVariants: (props?: ({
3
- cols?: 1 | 2 | 3 | 7 | 12 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | null | undefined;
3
+ cols?: 1 | 2 | 3 | 7 | 10 | 5 | 12 | 4 | 6 | 8 | 9 | 11 | null | undefined;
4
4
  spacing?: "sm" | "lg" | "none" | "xs" | "md" | "xl" | null | undefined;
5
5
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
6
  export type GridVariants = VariantProps<typeof gridVariants>;
@@ -0,0 +1,21 @@
1
+ import type React from "react";
2
+ import type { IconBubbleVariants } from "./variants";
3
+ export type IconBubbleProps = Omit<React.ComponentProps<"span">, "children"> & IconBubbleVariants & {
4
+ /** Icon component, e.g. a `lucide-react` icon. Decorative (`aria-hidden`). */
5
+ icon: React.ComponentType<{
6
+ size?: number | string;
7
+ "aria-hidden"?: boolean | "true" | "false";
8
+ }>;
9
+ };
10
+ /**
11
+ * An icon inside a round, tinted bubble — e.g. to lead an information line
12
+ * (date, place…). Tinted with `primary` by default; override both the
13
+ * background and the icon color through `className`
14
+ * (`bg-sky-500/15 text-sky-600`).
15
+ *
16
+ * @version 0.1.0
17
+ * @author Forth
18
+ * @copyright 2026 Forth
19
+ */
20
+ export declare function IconBubble({ icon: Icon, size, className, ...props }: IconBubbleProps): React.JSX.Element;
21
+ //# sourceMappingURL=icon-bubble.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon-bubble.d.ts","sourceRoot":"","sources":["../../../src/components/icon-bubble/icon-bubble.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGrD,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,GAC1E,kBAAkB,GAAG;IACnB,8EAA8E;IAC9E,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC;QACxB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;KAC5C,CAAC,CAAC;CACJ,CAAC;AAEJ;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EAAE,IAAI,EACV,IAAI,EACJ,SAAS,EACT,GAAG,KAAK,EACT,EAAE,eAAe,qBAajB"}
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "@forthtilliath/shadcn-ui/lib/utils";
3
+ import { ICON_BUBBLE_ICON_SIZES, iconBubbleVariants } from "./variants";
4
+ /**
5
+ * An icon inside a round, tinted bubble — e.g. to lead an information line
6
+ * (date, place…). Tinted with `primary` by default; override both the
7
+ * background and the icon color through `className`
8
+ * (`bg-sky-500/15 text-sky-600`).
9
+ *
10
+ * @version 0.1.0
11
+ * @author Forth
12
+ * @copyright 2026 Forth
13
+ */
14
+ export function IconBubble({ icon: Icon, size, className, ...props }) {
15
+ return (_jsx("span", { "data-slot": "icon-bubble", className: cn(iconBubbleVariants({ size }), className), ...props, children: _jsx(Icon, { size: ICON_BUBBLE_ICON_SIZES[size ?? "default"], "aria-hidden": "true" }) }));
16
+ }
@@ -0,0 +1,4 @@
1
+ export type { IconBubbleProps } from "./icon-bubble";
2
+ export { IconBubble } from "./icon-bubble";
3
+ export type { IconBubbleVariants } from "./variants";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/icon-bubble/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ export { IconBubble } from "./icon-bubble";
@@ -0,0 +1,12 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ export declare const iconBubbleVariants: (props?: ({
3
+ size?: "default" | "sm" | "lg" | null | undefined;
4
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
5
+ /** Icon size (px) matching each bubble size. */
6
+ export declare const ICON_BUBBLE_ICON_SIZES: {
7
+ readonly sm: 14;
8
+ readonly default: 17;
9
+ readonly lg: 20;
10
+ };
11
+ export type IconBubbleVariants = VariantProps<typeof iconBubbleVariants>;
12
+ //# sourceMappingURL=variants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.d.ts","sourceRoot":"","sources":["../../../src/components/icon-bubble/variants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,eAAO,MAAM,kBAAkB;;8EAc9B,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,sBAAsB;;;;CAIzB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { cva } from "class-variance-authority";
2
+ export const iconBubbleVariants = cva("bg-primary/15 text-primary grid shrink-0 place-items-center rounded-full", {
3
+ variants: {
4
+ size: {
5
+ sm: "size-7",
6
+ default: "size-9",
7
+ lg: "size-11",
8
+ },
9
+ },
10
+ defaultVariants: {
11
+ size: "default",
12
+ },
13
+ });
14
+ /** Icon size (px) matching each bubble size. */
15
+ export const ICON_BUBBLE_ICON_SIZES = {
16
+ sm: 14,
17
+ default: 17,
18
+ lg: 20,
19
+ };
@@ -18,7 +18,7 @@ export function ImageInput({ value, onFileChange, disabled = false, className, a
18
18
  const inputRef = React.useRef(null);
19
19
  React.useEffect(() => {
20
20
  if (file === null) {
21
- // eslint-disable-next-line @eslint-react/set-state-in-effect, react-hooks/set-state-in-effect -- syncs the object URL lifecycle (create/revoke) with the selected File, an external browser resource that can't be derived during render
21
+ // eslint-disable-next-line @eslint-react/set-state-in-effect -- syncs the object URL lifecycle (create/revoke) with the selected File, an external browser resource that can't be derived during render
22
22
  setObjectUrl(null);
23
23
  return;
24
24
  }
@@ -39,7 +39,7 @@ export function RelativeTime({ date, timeZone, label, formatOptions, className,
39
39
  // ones — there's no way to read "now" during render without
40
40
  // reintroducing the hydration mismatch this state is starting
41
41
  // `undefined` to avoid.
42
- // eslint-disable-next-line @eslint-react/set-state-in-effect, react-hooks/set-state-in-effect
42
+ // eslint-disable-next-line @eslint-react/set-state-in-effect
43
43
  setLiveNow(new Date());
44
44
  const interval = setInterval(() => {
45
45
  setLiveNow(new Date());
@@ -0,0 +1,4 @@
1
+ export type { ToggleSwitchProps } from "./toggle-switch";
2
+ export { ToggleSwitch } from "./toggle-switch";
3
+ export type { ToggleSwitchSizeVariants } from "./variants";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/toggle-switch/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,YAAY,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ export { ToggleSwitch } from "./toggle-switch";
@@ -0,0 +1,37 @@
1
+ import type React from "react";
2
+ import type { ToggleSwitchSizeVariants } from "./variants";
3
+ export type ToggleSwitchProps = Omit<React.ComponentProps<"button">, "children" | "className" | "onChange" | "onClick" | "role" | "type" | "value"> & ToggleSwitchSizeVariants & {
4
+ checked: boolean;
5
+ onCheckedChange: (checked: boolean) => void;
6
+ /** Visible text, also used as the accessible name. */
7
+ label?: React.ReactNode;
8
+ /** Secondary text under the label. */
9
+ description?: React.ReactNode;
10
+ /** Side of the text relative to the track (default: `"end"`). */
11
+ labelPosition?: "start" | "end";
12
+ /**
13
+ * Custom classes for each part of the component.
14
+ *
15
+ * - `root` is the `<button role="switch">` element — a `group/toggle-switch`
16
+ * carrying `data-state="checked" | "unchecked"`, so any part can be
17
+ * styled per state (`group-data-[state=checked]/toggle-switch:...`).
18
+ * - `track` / `thumb` are the switch itself.
19
+ * - `text` wraps the label and description (only when a description is set).
20
+ * - `label` / `description` are the text elements.
21
+ */
22
+ className?: Partial<Record<"root" | "track" | "thumb" | "text" | "label" | "description", string>>;
23
+ };
24
+ /**
25
+ * An on/off switch (`role="switch"`) with an optional visible label and
26
+ * description — clicking the text toggles it too. Visually distinct from an
27
+ * action button.
28
+ *
29
+ * Unlike shadcn's `Switch`, it has no Radix dependency and bundles the label,
30
+ * so a whole settings row is a single, fully clickable element.
31
+ *
32
+ * @version 0.1.0
33
+ * @author Forth
34
+ * @copyright 2026 Forth
35
+ */
36
+ export declare function ToggleSwitch({ checked, onCheckedChange, label, description, labelPosition, size, className, ...props }: ToggleSwitchProps): React.JSX.Element;
37
+ //# sourceMappingURL=toggle-switch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle-switch.d.ts","sourceRoot":"","sources":["../../../src/components/toggle-switch/toggle-switch.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAM3D,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAClC,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,EAC9B,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAC9E,GACC,wBAAwB,GAAG;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,sDAAsD;IACtD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,sCAAsC;IACtC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,iEAAiE;IACjE,aAAa,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAChC;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,OAAO,CACjB,MAAM,CACJ,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,aAAa,EAC7D,MAAM,CACP,CACF,CAAC;CACH,CAAC;AAEJ;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,eAAe,EACf,KAAK,EACL,WAAW,EACX,aAAqB,EACrB,IAAI,EACJ,SAAS,EACT,GAAG,KAAK,EACT,EAAE,iBAAiB,qBAmDnB"}
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "@forthtilliath/shadcn-ui/lib/utils";
4
+ import { toggleSwitchThumbVariants, toggleSwitchTrackVariants, } from "./variants";
5
+ /**
6
+ * An on/off switch (`role="switch"`) with an optional visible label and
7
+ * description — clicking the text toggles it too. Visually distinct from an
8
+ * action button.
9
+ *
10
+ * Unlike shadcn's `Switch`, it has no Radix dependency and bundles the label,
11
+ * so a whole settings row is a single, fully clickable element.
12
+ *
13
+ * @version 0.1.0
14
+ * @author Forth
15
+ * @copyright 2026 Forth
16
+ */
17
+ export function ToggleSwitch({ checked, onCheckedChange, label, description, labelPosition = "end", size, className, ...props }) {
18
+ const labelNode = label === undefined ? null : (_jsx("span", { className: cn("text-foreground text-sm", className?.label), children: label }));
19
+ // Without a description, the label is a direct flex child so that hiding it
20
+ // (e.g. `hidden sm:inline`) also removes the gap.
21
+ const text = description === undefined ? (labelNode) : (_jsxs("span", { className: cn("flex flex-col text-left", className?.text), children: [labelNode, _jsx("span", { className: cn("text-foreground/60 text-xs", className?.description), children: description })] }));
22
+ return (_jsxs("button", { type: "button", role: "switch", "aria-checked": checked, "data-slot": "toggle-switch", "data-state": checked ? "checked" : "unchecked", onClick: () => {
23
+ onCheckedChange(!checked);
24
+ }, className: cn("group/toggle-switch inline-flex cursor-pointer items-center gap-2 rounded-md focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", className?.root), ...props, children: [labelPosition === "start" && text, _jsx("span", { "data-slot": "toggle-switch-track", className: cn(toggleSwitchTrackVariants({ size }), className?.track), children: _jsx("span", { className: cn(toggleSwitchThumbVariants({ size }), className?.thumb) }) }), labelPosition === "end" && text] }));
25
+ }
@@ -0,0 +1,9 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ export declare const toggleSwitchTrackVariants: (props?: ({
3
+ size?: "default" | "sm" | "lg" | null | undefined;
4
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
5
+ export declare const toggleSwitchThumbVariants: (props?: ({
6
+ size?: "default" | "sm" | "lg" | null | undefined;
7
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
+ export type ToggleSwitchSizeVariants = VariantProps<typeof toggleSwitchTrackVariants>;
9
+ //# sourceMappingURL=variants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.d.ts","sourceRoot":"","sources":["../../../src/components/toggle-switch/variants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,eAAO,MAAM,yBAAyB;;8EAmBrC,CAAC;AAEF,eAAO,MAAM,yBAAyB;;8EAerC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,YAAY,CACjD,OAAO,yBAAyB,CACjC,CAAC"}