sid-ui 0.0.0-stage → 0.1.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.
Files changed (122) hide show
  1. package/README.md +54 -2
  2. package/package.json +51 -4
  3. package/src/brand/BrandFooter.tsx +45 -0
  4. package/src/brand/BrandHeading.tsx +24 -0
  5. package/src/brand/BrandLockup.tsx +47 -0
  6. package/src/brand/JoinedLogo.tsx +67 -0
  7. package/src/brand/JoinedMark.tsx +76 -0
  8. package/src/brand/geometry.ts +26 -0
  9. package/src/brand/index.ts +11 -0
  10. package/src/brand/name.ts +5 -0
  11. package/src/brand/paint.tsx +56 -0
  12. package/src/components/Action.tsx +44 -0
  13. package/src/components/AddressSelector.tsx +99 -0
  14. package/src/components/BarChart.tsx +132 -0
  15. package/src/components/Calendar.tsx +284 -0
  16. package/src/components/CalendarAgenda.tsx +87 -0
  17. package/src/components/CalendarMonth.tsx +223 -0
  18. package/src/components/CalendarSchedule.tsx +210 -0
  19. package/src/components/Card.tsx +80 -0
  20. package/src/components/Charts.tsx +64 -0
  21. package/src/components/Chat.tsx +35 -0
  22. package/src/components/CitySelector.tsx +81 -0
  23. package/src/components/Clock.tsx +196 -0
  24. package/src/components/CodeInput.tsx +183 -0
  25. package/src/components/Container.tsx +20 -0
  26. package/src/components/Content.tsx +50 -0
  27. package/src/components/Data.tsx +43 -0
  28. package/src/components/DataInput.tsx +107 -0
  29. package/src/components/DateField.tsx +162 -0
  30. package/src/components/DonutChart.tsx +123 -0
  31. package/src/components/Drawer.tsx +110 -0
  32. package/src/components/Feedback.tsx +37 -0
  33. package/src/components/FileUploader.tsx +377 -0
  34. package/src/components/FunnelChart.tsx +83 -0
  35. package/src/components/Glyph.tsx +330 -0
  36. package/src/components/GoogleSignIn.tsx +67 -0
  37. package/src/components/GridSystem.tsx +131 -0
  38. package/src/components/HeatmapCalendar.tsx +167 -0
  39. package/src/components/KanbanBoard.tsx +421 -0
  40. package/src/components/Kpi.tsx +61 -0
  41. package/src/components/LayoutPrimitives.tsx +86 -0
  42. package/src/components/Lists.tsx +19 -0
  43. package/src/components/LocationSelector.tsx +49 -0
  44. package/src/components/MessageList.tsx +80 -0
  45. package/src/components/Nav.tsx +69 -0
  46. package/src/components/Navigation.tsx +52 -0
  47. package/src/components/Notification.tsx +115 -0
  48. package/src/components/NotificationTrigger.tsx +248 -0
  49. package/src/components/Overlay.tsx +50 -0
  50. package/src/components/Page.tsx +143 -0
  51. package/src/components/PageTabs.tsx +28 -0
  52. package/src/components/PickerShell.tsx +138 -0
  53. package/src/components/PillNav.tsx +194 -0
  54. package/src/components/Primitives.tsx +30 -0
  55. package/src/components/Rating.tsx +78 -0
  56. package/src/components/Responsive.tsx +128 -0
  57. package/src/components/ResponsiveFrame.tsx +122 -0
  58. package/src/components/Segment.tsx +190 -0
  59. package/src/components/Sparkline.tsx +52 -0
  60. package/src/components/StateSelector.tsx +39 -0
  61. package/src/components/Sticky.tsx +95 -0
  62. package/src/components/Table.tsx +344 -0
  63. package/src/components/ThemeToggle.tsx +48 -0
  64. package/src/components/Tile.tsx +23 -0
  65. package/src/components/TimeColumns.tsx +155 -0
  66. package/src/components/TimeDial.tsx +260 -0
  67. package/src/components/TimeField.tsx +196 -0
  68. package/src/components/TimeInput.tsx +213 -0
  69. package/src/components/TimeSlots.tsx +83 -0
  70. package/src/components/Timeline.tsx +111 -0
  71. package/src/components/TokenDemo.tsx +378 -0
  72. package/src/components/Tree.tsx +283 -0
  73. package/src/components/TrendChart.tsx +211 -0
  74. package/src/components/breakpoints.ts +42 -0
  75. package/src/components/calendarTypes.ts +26 -0
  76. package/src/components/chartMath.ts +125 -0
  77. package/src/components/chartParts.tsx +116 -0
  78. package/src/components/codeInputRules.ts +58 -0
  79. package/src/components/date.ts +87 -0
  80. package/src/components/hooks.ts +53 -0
  81. package/src/components/index.ts +195 -0
  82. package/src/components/kanban.ts +78 -0
  83. package/src/components/motion.ts +78 -0
  84. package/src/components/places-search.tsx +73 -0
  85. package/src/components/places.ts +209 -0
  86. package/src/components/size.ts +3 -0
  87. package/src/components/time.ts +83 -0
  88. package/src/components/useFileDrop.ts +104 -0
  89. package/src/places/geoapify.ts +129 -0
  90. package/src/styles/astryx.css +5 -0
  91. package/src/styles/components/brand.css +98 -0
  92. package/src/styles/components/calendar.css +680 -0
  93. package/src/styles/components/charts.css +659 -0
  94. package/src/styles/components/code-input.css +233 -0
  95. package/src/styles/components/google.css +11 -0
  96. package/src/styles/components/message-list.css +153 -0
  97. package/src/styles/components/notification.css +350 -0
  98. package/src/styles/components/picker.css +114 -0
  99. package/src/styles/components/pill-nav.css +179 -0
  100. package/src/styles/components/responsive.css +419 -0
  101. package/src/styles/components/sticky.css +23 -0
  102. package/src/styles/components/table.css +267 -0
  103. package/src/styles/components/time.css +526 -0
  104. package/src/styles/components/timeline.css +426 -0
  105. package/src/styles/components/tree.css +269 -0
  106. package/src/styles/components.css +423 -0
  107. package/src/styles/joined.css +4 -0
  108. package/src/styles/theme.css +79 -0
  109. package/src/styles/tokens.css +338 -0
  110. package/src/theme/JoinedProvider.tsx +50 -0
  111. package/src/theme/index.ts +3 -0
  112. package/src/theme/joined.d.ts +11 -0
  113. package/src/theme/joined.js +569 -0
  114. package/src/theme/joined.theme.ts +74 -0
  115. package/src/theme/joined.variants.d.ts +16 -0
  116. package/src/theme/theme.css +786 -0
  117. package/styles/astryx.css +1 -0
  118. package/styles/components.css +1 -0
  119. package/styles/joined.css +1 -0
  120. package/styles/theme.css +1 -0
  121. package/styles/tokens.css +1 -0
  122. package/styles.css +1 -0
@@ -0,0 +1,116 @@
1
+ import { VisuallyHidden } from "./Lists";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ /**
6
+ * Shared pieces for every chart: the tone names, the legend, the hover readout, and the
7
+ * screen-reader table that keeps each value reachable without hovering.
8
+ * Tones resolve to the theme's data tokens in styles/components/charts.css.
9
+ */
10
+
11
+ /**
12
+ * Categorical tones, in the fixed order series take them: blue, orange, purple, green.
13
+ * red is kept for loss or rejection; neutral is for "other" and for remainders.
14
+ */
15
+ export type ChartTone = "blue" | "orange" | "purple" | "green" | "red" | "neutral";
16
+
17
+ /** The order a chart assigns tones to series that do not name one. Never cycled. */
18
+ export const CHART_TONES: ChartTone[] = ["blue", "orange", "purple", "green"];
19
+
20
+ export function toneAt(index: number, tone?: ChartTone): ChartTone {
21
+ return tone ?? CHART_TONES[index] ?? "neutral";
22
+ }
23
+
24
+ export type LegendItem = { label: string; tone: ChartTone; value?: string };
25
+
26
+ /** Swatch, name, and an optional value. The swatch mirrors the mark: a dash for lines. */
27
+ export function ChartLegend({
28
+ items,
29
+ mark = "dot",
30
+ }: {
31
+ items: LegendItem[];
32
+ mark?: "dot" | "line";
33
+ }) {
34
+ return (
35
+ <ul className="os-chart-legend">
36
+ {items.map((item) => (
37
+ <li key={item.label}>
38
+ <span className="os-chart-key" data-tone={item.tone} data-mark={mark} aria-hidden />
39
+ <span>{item.label}</span>
40
+ {item.value ? <strong>{item.value}</strong> : null}
41
+ </li>
42
+ ))}
43
+ </ul>
44
+ );
45
+ }
46
+
47
+ export type TipRow = { label: string; value: string; tone?: ChartTone };
48
+
49
+ /** The hover readout. The value leads; the series name follows in secondary ink. */
50
+ export function ChartTip({
51
+ title,
52
+ rows,
53
+ left,
54
+ top,
55
+ align = "center",
56
+ }: {
57
+ title?: string;
58
+ rows: TipRow[];
59
+ left: number | string;
60
+ top: number | string;
61
+ align?: "center" | "start" | "end";
62
+ }) {
63
+ return (
64
+ <div className="os-chart-tip" data-align={align} style={{ left, top }} role="presentation">
65
+ {title ? <span className="os-chart-tip-title">{title}</span> : null}
66
+ {rows.map((row) => (
67
+ <span key={row.label} className="os-chart-tip-row">
68
+ {row.tone ? (
69
+ <span className="os-chart-key" data-tone={row.tone} data-mark="line" />
70
+ ) : null}
71
+ <strong>{row.value}</strong>
72
+ <span>{row.label}</span>
73
+ </span>
74
+ ))}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ /** Every plotted value as a table, for screen readers. */
80
+ export function ChartTable({
81
+ caption,
82
+ headers,
83
+ rows,
84
+ }: {
85
+ caption: string;
86
+ headers: string[];
87
+ rows: ReactNode[][];
88
+ }) {
89
+ return (
90
+ <VisuallyHidden>
91
+ <table>
92
+ <caption>{caption}</caption>
93
+ <thead>
94
+ <tr>
95
+ {headers.map((header) => (
96
+ <th key={header} scope="col">
97
+ {header}
98
+ </th>
99
+ ))}
100
+ </tr>
101
+ </thead>
102
+ <tbody>
103
+ {rows.map((row, index) => (
104
+ <tr key={index}>
105
+ {row.map((cell, cellIndex) => (
106
+ <td key={cellIndex}>{cell}</td>
107
+ ))}
108
+ </tr>
109
+ ))}
110
+ </tbody>
111
+ </table>
112
+ </VisuallyHidden>
113
+ );
114
+ }
115
+
116
+ export const defaultFormat = (value: number) => value.toLocaleString();
@@ -0,0 +1,58 @@
1
+ /** Pure editing rules for CodeInput, kept apart from React so they can be tested. */
2
+
3
+ export type CodeCharset = "numeric" | "alphanumeric";
4
+
5
+ export interface CodeEdit {
6
+ value: string;
7
+ /** The cell that should hold focus after the edit. */
8
+ focusIndex: number;
9
+ }
10
+
11
+ const CHARSET_PATTERN: Record<CodeCharset, RegExp> = {
12
+ numeric: /[^0-9]/g,
13
+ alphanumeric: /[^a-z0-9]/gi,
14
+ };
15
+
16
+ /** Drops characters the code does not allow, so a pasted "123 456" still works. */
17
+ export function sanitizeCode(raw: string, charset: CodeCharset = "numeric") {
18
+ const cleaned = raw.replace(CHARSET_PATTERN[charset], "");
19
+ return charset === "alphanumeric" ? cleaned.toUpperCase() : cleaned;
20
+ }
21
+
22
+ /**
23
+ * Types or pastes `raw` into the cell at `index`. The value stays contiguous, so a cell past the
24
+ * end of the value writes at the end. A full-length paste replaces the whole code.
25
+ */
26
+ export function insertCode(
27
+ value: string,
28
+ index: number,
29
+ raw: string,
30
+ length: number,
31
+ charset: CodeCharset = "numeric",
32
+ ): CodeEdit {
33
+ const chars = sanitizeCode(raw, charset);
34
+ if (!chars) return { value, focusIndex: Math.min(index, length - 1) };
35
+
36
+ const start = chars.length >= length ? 0 : Math.min(index, value.length);
37
+ const next = (value.slice(0, start) + chars + value.slice(start + chars.length)).slice(0, length);
38
+ return { value: next, focusIndex: Math.min(start + chars.length, length - 1) };
39
+ }
40
+
41
+ /** Backspace: clears the focused cell, or, when it is empty, the one before it. */
42
+ export function deleteCode(value: string, index: number): CodeEdit {
43
+ if (index < value.length) {
44
+ return { value: value.slice(0, index) + value.slice(index + 1), focusIndex: index };
45
+ }
46
+ const previous = Math.max(value.length - 1, 0);
47
+ return { value: value.slice(0, previous), focusIndex: previous };
48
+ }
49
+
50
+ /**
51
+ * A cell's `onChange` gives its whole text. When the cell already held a character and the browser
52
+ * kept it, what remains after removing that character is what was typed.
53
+ */
54
+ export function typedCharacters(raw: string, previous: string | undefined) {
55
+ if (!previous || raw.length <= 1) return raw;
56
+ const at = raw.indexOf(previous);
57
+ return at === -1 ? raw : raw.slice(0, at) + raw.slice(at + previous.length);
58
+ }
@@ -0,0 +1,87 @@
1
+ export const DAYS_PER_WEEK = 7;
2
+ /** Month grids always show six weeks so the calendar never changes height. */
3
+ export const GRID_WEEKS = 6;
4
+ export const MONTHS_PER_YEAR = 12;
5
+ export const MS_PER_DAY = 86_400_000;
6
+ /** Fixed so server and client format dates the same way. */
7
+ export const DEFAULT_LOCALE = "en-US";
8
+
9
+ export type WeekStart = 0 | 1;
10
+
11
+ export function startOfDay(date: Date) {
12
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
13
+ }
14
+
15
+ export function addDays(date: Date, days: number) {
16
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
17
+ }
18
+
19
+ /** Adds months and clamps the day, so Jan 31 + 1 month is Feb 28/29. */
20
+ export function addMonths(date: Date, months: number) {
21
+ const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
22
+ const last = daysInMonth(target.getFullYear(), target.getMonth());
23
+ return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), last));
24
+ }
25
+
26
+ export function daysInMonth(year: number, month: number) {
27
+ return new Date(year, month + 1, 0).getDate();
28
+ }
29
+
30
+ export function sameDay(a: Date | null | undefined, b: Date | null | undefined) {
31
+ return Boolean(
32
+ a &&
33
+ b &&
34
+ a.getFullYear() === b.getFullYear() &&
35
+ a.getMonth() === b.getMonth() &&
36
+ a.getDate() === b.getDate(),
37
+ );
38
+ }
39
+
40
+ export function sameMonth(a: Date, b: Date) {
41
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
42
+ }
43
+
44
+ export function startOfWeek(date: Date, weekStartsOn: WeekStart) {
45
+ const shift = (date.getDay() - weekStartsOn + DAYS_PER_WEEK) % DAYS_PER_WEEK;
46
+ return addDays(date, -shift);
47
+ }
48
+
49
+ export function weekOf(date: Date, weekStartsOn: WeekStart) {
50
+ const first = startOfWeek(date, weekStartsOn);
51
+ return Array.from({ length: DAYS_PER_WEEK }, (_, i) => addDays(first, i));
52
+ }
53
+
54
+ export function monthGrid(cursor: Date, weekStartsOn: WeekStart) {
55
+ const first = startOfWeek(new Date(cursor.getFullYear(), cursor.getMonth(), 1), weekStartsOn);
56
+ return Array.from({ length: GRID_WEEKS * DAYS_PER_WEEK }, (_, i) => addDays(first, i));
57
+ }
58
+
59
+ export function isBetween(date: Date, start: Date, end: Date) {
60
+ const t = startOfDay(date).getTime();
61
+ const a = startOfDay(start).getTime();
62
+ const b = startOfDay(end).getTime();
63
+ return t > Math.min(a, b) && t < Math.max(a, b);
64
+ }
65
+
66
+ export function weekdayNames(
67
+ weekStartsOn: WeekStart,
68
+ width: "narrow" | "short",
69
+ locale = DEFAULT_LOCALE,
70
+ ) {
71
+ const sunday = new Date(2023, 0, 1);
72
+ return Array.from({ length: DAYS_PER_WEEK }, (_, i) =>
73
+ addDays(sunday, i + weekStartsOn).toLocaleDateString(locale, { weekday: width }),
74
+ );
75
+ }
76
+
77
+ export function monthNames(width: "short" | "long", locale = DEFAULT_LOCALE) {
78
+ return Array.from({ length: MONTHS_PER_YEAR }, (_, i) =>
79
+ new Date(2023, i, 1).toLocaleDateString(locale, { month: width }),
80
+ );
81
+ }
82
+
83
+ /** Local-date ISO: "2026-09-24". */
84
+ export function toISODate(date: Date) {
85
+ const pad = (n: number) => String(n).padStart(2, "0");
86
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
87
+ }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type RefObject } from "react";
4
+
5
+ /** Calls `onDismiss` on a pointer-down outside `ref` or on Escape while `active`. */
6
+ export function useDismiss(
7
+ ref: RefObject<HTMLElement | null>,
8
+ active: boolean,
9
+ onDismiss: () => void,
10
+ ) {
11
+ useEffect(() => {
12
+ if (!active) return;
13
+ function onPointer(event: PointerEvent) {
14
+ if (ref.current && !ref.current.contains(event.target as Node)) onDismiss();
15
+ }
16
+ function onKey(event: KeyboardEvent) {
17
+ if (event.key === "Escape") onDismiss();
18
+ }
19
+ document.addEventListener("pointerdown", onPointer);
20
+ document.addEventListener("keydown", onKey);
21
+ return () => {
22
+ document.removeEventListener("pointerdown", onPointer);
23
+ document.removeEventListener("keydown", onKey);
24
+ };
25
+ }, [ref, active, onDismiss]);
26
+ }
27
+
28
+ /** Current time, refreshed every `intervalMs`. `null` until mounted so SSR output stays stable. */
29
+ export function useNow(intervalMs: number, enabled = true) {
30
+ const [now, setNow] = useState<Date | null>(null);
31
+ useEffect(() => {
32
+ if (!enabled) return;
33
+ setNow(new Date());
34
+ const id = window.setInterval(() => setNow(new Date()), intervalMs);
35
+ return () => window.clearInterval(id);
36
+ }, [intervalMs, enabled]);
37
+ return now;
38
+ }
39
+
40
+ /** Internal state that defers to `controlled` when the caller passes one. */
41
+ export function useControllable<T>(
42
+ controlled: T | undefined,
43
+ initial: T,
44
+ onChange?: (value: T) => void,
45
+ ) {
46
+ const [inner, setInner] = useState<T>(initial);
47
+ const value = controlled !== undefined ? controlled : inner;
48
+ function set(next: T) {
49
+ if (controlled === undefined) setInner(next);
50
+ onChange?.(next);
51
+ }
52
+ return [value, set] as const;
53
+ }
@@ -0,0 +1,195 @@
1
+ // Original Astryx, Joined-themed: primitives, actions, content, containers,
2
+ // data input, feedback, navigation, overlays, lists, and chat.
3
+ export * from "./Primitives";
4
+ export * from "./Action";
5
+ export * from "./Content";
6
+ export * from "./Container";
7
+ export * from "./DataInput";
8
+ export * from "./Feedback";
9
+ export * from "./Navigation";
10
+ export * from "./Overlay";
11
+ export * from "./Lists";
12
+ export * from "./Chat";
13
+
14
+ export type { ControlSize, FieldSize } from "./size";
15
+
16
+ export { JobCard } from "./Card";
17
+ export type { JobCardProps } from "./Card";
18
+
19
+ // Brand — the Joined wordmark, app icon, and product name.
20
+ export * from "../brand";
21
+
22
+ // Sign in with Google — Google's mark and the button every app's sign-in uses.
23
+ export { GoogleMark, GoogleSignInButton } from "./GoogleSignIn";
24
+ export type { GoogleMarkProps, GoogleSignInButtonProps } from "./GoogleSignIn";
25
+
26
+ export { Nav } from "./Nav";
27
+ export type { NavProps, NavItem } from "./Nav";
28
+
29
+ export { TokenDemo } from "./TokenDemo";
30
+
31
+ export * from "./LayoutPrimitives";
32
+
33
+ // Responsive layer — Joined composites on Astryx breakpoints.
34
+ export { GridSystem, GridColumn, GRID_COLUMNS } from "./GridSystem";
35
+ export type { GridSystemProps, GridColumnProps, GridSpanValue, Responsive } from "./GridSystem";
36
+ export {
37
+ ResponsiveStack,
38
+ ResponsiveContainer,
39
+ Show,
40
+ Hide,
41
+ useElementWidth,
42
+ useContainerBreakpoint,
43
+ useViewportBreakpoint,
44
+ } from "./Responsive";
45
+ export type { ResponsiveStackProps, ResponsiveContainerProps, ShowProps } from "./Responsive";
46
+ export { ResponsiveFrame, FRAME_PRESETS } from "./ResponsiveFrame";
47
+ export type { ResponsiveFrameProps, FramePreset } from "./ResponsiveFrame";
48
+ export { Tile } from "./Tile";
49
+ export type { TileProps } from "./Tile";
50
+ export { Sticky } from "./Sticky";
51
+ export type { StickyProps } from "./Sticky";
52
+ export { TIERS, VIEWPORT_TIERS, CONTAINER_TIERS, tierFor } from "./breakpoints";
53
+ export type { Tier, TierOrBase, ResponsiveTo } from "./breakpoints";
54
+
55
+ export { CodeInput, CODE_INPUT_LENGTH } from "./CodeInput";
56
+ export type { CodeInputProps, CodeInputStatus } from "./CodeInput";
57
+ export type { CodeCharset } from "./codeInputRules";
58
+
59
+ export { Glyph, icons } from "./Glyph";
60
+ export type { GlyphName, GlyphProps } from "./Glyph";
61
+
62
+ export { Timeline } from "./Timeline";
63
+ export type {
64
+ TimelineProps,
65
+ TimelineItem,
66
+ TimelineTone,
67
+ TimelineStatus,
68
+ TimelineVariant,
69
+ } from "./Timeline";
70
+
71
+ export { Tree } from "./Tree";
72
+ export type { TreeProps, TreeNode, TreeVariant, TreeCheckState } from "./Tree";
73
+
74
+ export { Table } from "./Table";
75
+ export type {
76
+ TableProps,
77
+ TableColumn,
78
+ TableSort,
79
+ SortDirection,
80
+ TableVariant,
81
+ TableSelection,
82
+ TableDensity,
83
+ } from "./Table";
84
+
85
+ export { Clock } from "./Clock";
86
+ export type { ClockProps, ClockVariant } from "./Clock";
87
+ export { TimeField } from "./TimeField";
88
+ export type { TimeFieldProps, TimePicker } from "./TimeField";
89
+ export { TimeDial } from "./TimeDial";
90
+ export type { TimeDialProps } from "./TimeDial";
91
+ export { TimeColumns } from "./TimeColumns";
92
+ export type { TimeColumnsProps } from "./TimeColumns";
93
+ export { TimeSlots } from "./TimeSlots";
94
+ export type { TimeSlotsProps } from "./TimeSlots";
95
+ export { displayTime } from "./time";
96
+
97
+ export { Calendar } from "./Calendar";
98
+ export type {
99
+ CalendarProps,
100
+ CalendarEvent,
101
+ CalendarView,
102
+ CalendarTone,
103
+ DateRange,
104
+ } from "./Calendar";
105
+ export { DateField } from "./DateField";
106
+ export type { DateFieldProps } from "./DateField";
107
+
108
+ // Drawer — a side or edge panel on Astryx Dialog.
109
+ export { Drawer } from "./Drawer";
110
+ export type { DrawerProps, DrawerSide, DrawerSize } from "./Drawer";
111
+
112
+ // Kanban / scrum board — drag and drop or keyboard, columns and lanes.
113
+ export { KanbanBoard } from "./KanbanBoard";
114
+ export type { KanbanBoardProps, KanbanItemState } from "./KanbanBoard";
115
+ export { moveKanbanItem, cellItems, slotOf } from "./kanban";
116
+ export type { KanbanColumn, KanbanLane, KanbanItemBase, KanbanSlot, KanbanMove } from "./kanban";
117
+
118
+ // Drag-and-drop uploads, built from Astryx parts.
119
+ export { FileUploader } from "./FileUploader";
120
+ export type { FileUploaderProps, UploadItem, UploadStatus, UploadHandler } from "./FileUploader";
121
+ export { useFileDrop, formatBytes, matchesAccept } from "./useFileDrop";
122
+ export type { UseFileDropOptions, FileDropProps } from "./useFileDrop";
123
+
124
+ export { Rating, RATING_MAX } from "./Rating";
125
+ export type { RatingProps } from "./Rating";
126
+
127
+ export { Notification, NotificationList } from "./Notification";
128
+ export type { NotificationProps, NotificationListProps, NotificationTone } from "./Notification";
129
+ export { NotificationViewport, useNotification } from "./NotificationTrigger";
130
+ export type {
131
+ NotificationPosition,
132
+ ShowNotificationOptions,
133
+ NotificationDismiss,
134
+ } from "./NotificationTrigger";
135
+
136
+ // Joined pickers — typed segments plus a dial, wheels, or slot list.
137
+ export { TimeInput } from "./TimeInput";
138
+ export type { TimeInputProps, TimeInputVariant, HourCycle, MinuteStep } from "./TimeInput";
139
+
140
+ export { CitySelector } from "./CitySelector";
141
+ export { LocationSelector } from "./LocationSelector";
142
+ export { StateSelector } from "./StateSelector";
143
+ export { AddressSelector } from "./AddressSelector";
144
+ export {
145
+ US_STATES,
146
+ US_CITIES,
147
+ LOCATION_SEPARATOR,
148
+ cityLabel,
149
+ joinLocations,
150
+ splitLocations,
151
+ formatAddress,
152
+ parseAddress,
153
+ emptyAddress,
154
+ } from "./places";
155
+ export type { State, City, Address } from "./places";
156
+
157
+ export { TreeList, PageBody, PageHero, Preview, PreviewGrid } from "./Data";
158
+
159
+ // Page composites every app shares: content column, title row, section card, stats.
160
+ export { PageContainer, PageHeader, SectionCard, StatCard, StatGrid, PAGE_WIDTHS } from "./Page";
161
+ export type { PageWidth, Stat } from "./Page";
162
+
163
+ export { ThemeToggle } from "./ThemeToggle";
164
+ export type { ThemeName } from "./ThemeToggle";
165
+
166
+ // Workspace shell and dashboard pieces: pill navigation, KPI widget, segment bar.
167
+ export { PillNav } from "./PillNav";
168
+ export type { PillNavProps, PillNavItem } from "./PillNav";
169
+ export { KpiWidget } from "./Kpi";
170
+ export type { KpiDelta } from "./Kpi";
171
+ export { SegmentBar } from "./Charts";
172
+ export type { Segment, SegmentTone } from "./Charts";
173
+
174
+ // Charts — one look for trends, magnitudes, parts of a whole, funnels, and daily activity.
175
+ // Tones come from the theme's data tokens in a fixed, colour-blind-checked order.
176
+ export { TrendChart } from "./TrendChart";
177
+ export type { TrendSeries } from "./TrendChart";
178
+ export { BarChart } from "./BarChart";
179
+ export type { BarDatum } from "./BarChart";
180
+ export { DonutChart } from "./DonutChart";
181
+ export type { DonutSlice } from "./DonutChart";
182
+ export { Sparkline } from "./Sparkline";
183
+ export { FunnelChart } from "./FunnelChart";
184
+ export type { FunnelStage } from "./FunnelChart";
185
+ export { HeatmapCalendar } from "./HeatmapCalendar";
186
+ export type { HeatmapDay } from "./HeatmapCalendar";
187
+ export { ChartLegend, CHART_TONES } from "./chartParts";
188
+ export type { ChartTone, LegendItem } from "./chartParts";
189
+ export { compactNumber } from "./chartMath";
190
+
191
+ // Mail — one line per message, for inboxes and reply feeds.
192
+ export { MessageList } from "./MessageList";
193
+ export type { MessageListItem } from "./MessageList";
194
+ export { PageTabs } from "./PageTabs";
195
+ export type { PageTab } from "./PageTabs";
@@ -0,0 +1,78 @@
1
+ /** Board data and the pure move logic behind KanbanBoard — usable on a server too. */
2
+
3
+ export interface KanbanColumn {
4
+ id: string;
5
+ title: string;
6
+ /** Work-in-progress limit; the count turns to a warning when exceeded. */
7
+ limit?: number;
8
+ /** Nothing can be dropped here — a done-and-locked or archived column. */
9
+ isLocked?: boolean;
10
+ }
11
+
12
+ export interface KanbanLane {
13
+ id: string;
14
+ title: string;
15
+ subtitle?: string;
16
+ }
17
+
18
+ /** Items only need an id and a column; order within a cell is the array order. */
19
+ export interface KanbanItemBase {
20
+ id: string;
21
+ columnId: string;
22
+ laneId?: string;
23
+ }
24
+
25
+ export interface KanbanSlot {
26
+ columnId: string;
27
+ laneId?: string;
28
+ /** Position among the other items in that cell. */
29
+ index: number;
30
+ }
31
+
32
+ export interface KanbanMove {
33
+ itemId: string;
34
+ from: KanbanSlot;
35
+ to: KanbanSlot;
36
+ }
37
+
38
+ export function inCell<T extends KanbanItemBase>(item: T, columnId: string, laneId?: string) {
39
+ return item.columnId === columnId && (laneId === undefined || item.laneId === laneId);
40
+ }
41
+
42
+ export function cellItems<T extends KanbanItemBase>(items: T[], columnId: string, laneId?: string) {
43
+ return items.filter((item) => inCell(item, columnId, laneId));
44
+ }
45
+
46
+ /** Where an item sits now. */
47
+ export function slotOf<T extends KanbanItemBase>(items: T[], itemId: string): KanbanSlot | null {
48
+ const item = items.find((i) => i.id === itemId);
49
+ if (!item) return null;
50
+ const index = cellItems(items, item.columnId, item.laneId).findIndex((i) => i.id === itemId);
51
+ return { columnId: item.columnId, laneId: item.laneId, index };
52
+ }
53
+
54
+ /**
55
+ * Moves one item to a column (and lane) at an index among that cell's other
56
+ * items. Returns a new array; everything else keeps its order.
57
+ */
58
+ export function moveKanbanItem<T extends KanbanItemBase>(
59
+ items: T[],
60
+ itemId: string,
61
+ to: KanbanSlot,
62
+ ): T[] {
63
+ const moving = items.find((i) => i.id === itemId);
64
+ if (!moving) return items;
65
+ const rest = items.filter((i) => i.id !== itemId);
66
+ const laneId = to.laneId ?? moving.laneId;
67
+ const moved = { ...moving, columnId: to.columnId, laneId } as T;
68
+ const siblings = rest.filter((i) => inCell(i, to.columnId, laneId));
69
+ const index = Math.max(0, Math.min(to.index, siblings.length));
70
+ const anchor = siblings[index];
71
+ if (anchor) {
72
+ const at = rest.indexOf(anchor);
73
+ return [...rest.slice(0, at), moved, ...rest.slice(at)];
74
+ }
75
+ const last = siblings[siblings.length - 1];
76
+ const at = last ? rest.indexOf(last) + 1 : rest.length;
77
+ return [...rest.slice(0, at), moved, ...rest.slice(at)];
78
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Pure motion helpers so JS-driven animation can follow the theme's motion tokens
3
+ * (`--duration-*`, `--ease-*`) instead of hardcoding its own timing.
4
+ */
5
+
6
+ export type Easing = (t: number) => number;
7
+ export type Box = { x: number; width: number };
8
+
9
+ const linear: Easing = (t) => t;
10
+
11
+ /** "300ms" or "0.3s" → milliseconds. Anything unreadable → 0, so motion snaps instead of breaking. */
12
+ export function parseDuration(value: string): number {
13
+ const match = /^\s*(-?[\d.]+)\s*(ms|s)\s*$/.exec(value);
14
+ if (!match) return 0;
15
+ const amount = Number(match[1]);
16
+ if (!Number.isFinite(amount) || amount < 0) return 0;
17
+ return match[2] === "s" ? amount * 1000 : amount;
18
+ }
19
+
20
+ /** "cubic-bezier(a, b, c, d)" → an easing function; keywords and junk fall back to linear. */
21
+ export function parseEasing(value: string): Easing {
22
+ const match = /cubic-bezier\(\s*([^)]+)\)/.exec(value);
23
+ if (!match) return linear;
24
+ const points = match[1].split(",").map((part) => Number(part.trim()));
25
+ if (points.length !== 4 || points.some((n) => !Number.isFinite(n))) return linear;
26
+ const [x1, y1, x2, y2] = points as [number, number, number, number];
27
+ return cubicBezier(x1, y1, x2, y2);
28
+ }
29
+
30
+ const NEWTON_STEPS = 8;
31
+ const BISECTION_STEPS = 24;
32
+ const EPSILON = 1e-6;
33
+
34
+ /** The CSS cubic-bezier timing function: for progress x in [0, 1], the eased y. */
35
+ export function cubicBezier(x1: number, y1: number, x2: number, y2: number): Easing {
36
+ const cx = 3 * x1;
37
+ const bx = 3 * (x2 - x1) - cx;
38
+ const ax = 1 - cx - bx;
39
+ const cy = 3 * y1;
40
+ const by = 3 * (y2 - y1) - cy;
41
+ const ay = 1 - cy - by;
42
+ const sampleX = (t: number) => ((ax * t + bx) * t + cx) * t;
43
+ const sampleY = (t: number) => ((ay * t + by) * t + cy) * t;
44
+ const slopeX = (t: number) => (3 * ax * t + 2 * bx) * t + cx;
45
+
46
+ function solveT(x: number) {
47
+ let t = x;
48
+ for (let i = 0; i < NEWTON_STEPS; i++) {
49
+ const error = sampleX(t) - x;
50
+ if (Math.abs(error) < EPSILON) return t;
51
+ const slope = slopeX(t);
52
+ if (Math.abs(slope) < EPSILON) break;
53
+ t -= error / slope;
54
+ }
55
+ let low = 0;
56
+ let high = 1;
57
+ t = x;
58
+ for (let i = 0; i < BISECTION_STEPS; i++) {
59
+ const value = sampleX(t);
60
+ if (Math.abs(value - x) < EPSILON) return t;
61
+ if (value < x) low = t;
62
+ else high = t;
63
+ t = (low + high) / 2;
64
+ }
65
+ return t;
66
+ }
67
+
68
+ return (x) => {
69
+ if (x <= 0) return 0;
70
+ if (x >= 1) return 1;
71
+ return sampleY(solveT(x));
72
+ };
73
+ }
74
+
75
+ /** A box part-way from `from` to `to`. */
76
+ export function lerpBox(from: Box, to: Box, t: number): Box {
77
+ return { x: from.x + (to.x - from.x) * t, width: from.width + (to.width - from.width) * t };
78
+ }