sid-ui 0.0.0-stage → 0.1.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 (116) 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
@@ -0,0 +1,223 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState, type KeyboardEvent } from "react";
4
+
5
+ import {
6
+ DAYS_PER_WEEK,
7
+ DEFAULT_LOCALE,
8
+ MS_PER_DAY,
9
+ addDays,
10
+ addMonths,
11
+ isBetween,
12
+ monthGrid,
13
+ sameDay,
14
+ sameMonth,
15
+ startOfWeek,
16
+ weekdayNames,
17
+ type WeekStart,
18
+ } from "./date";
19
+
20
+ import type { CalendarEvent, DateRange } from "./calendarTypes";
21
+
22
+ const MAX_CHIPS = 2;
23
+ const MAX_DOTS = 3;
24
+
25
+ export interface CalendarMonthProps {
26
+ cursor: Date;
27
+ onCursorChange: (date: Date) => void;
28
+ value?: Date | null;
29
+ range?: DateRange | null;
30
+ onPick: (date: Date) => void;
31
+ events: CalendarEvent[];
32
+ eventDisplay: "dots" | "chips";
33
+ weekStartsOn: WeekStart;
34
+ isDisabled: (date: Date) => boolean;
35
+ today: Date | null;
36
+ showWeekNumbers?: boolean;
37
+ }
38
+
39
+ function isoWeek(date: Date) {
40
+ const target = new Date(
41
+ date.getFullYear(),
42
+ date.getMonth(),
43
+ date.getDate() + 4 - (date.getDay() || DAYS_PER_WEEK),
44
+ );
45
+ const yearStart = new Date(target.getFullYear(), 0, 1);
46
+ return Math.ceil(((target.getTime() - yearStart.getTime()) / MS_PER_DAY + 1) / DAYS_PER_WEEK);
47
+ }
48
+
49
+ /** Six fixed weeks. Arrow keys walk days, PageUp/PageDown walk months. */
50
+ export function CalendarMonth({
51
+ cursor,
52
+ onCursorChange,
53
+ value,
54
+ range,
55
+ onPick,
56
+ events,
57
+ eventDisplay,
58
+ weekStartsOn,
59
+ isDisabled,
60
+ today,
61
+ showWeekNumbers,
62
+ }: CalendarMonthProps) {
63
+ const cells = monthGrid(cursor, weekStartsOn);
64
+ const [hover, setHover] = useState<Date | null>(null);
65
+ const gridRef = useRef<HTMLDivElement>(null);
66
+ const wantsFocus = useRef(false);
67
+ const rangeEnd = range?.end ?? (range && hover ? hover : null);
68
+
69
+ useEffect(() => {
70
+ if (!wantsFocus.current) return;
71
+ wantsFocus.current = false;
72
+ gridRef.current?.querySelector<HTMLElement>("[tabindex='0']")?.focus();
73
+ }, [cursor]);
74
+
75
+ function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
76
+ const moves: Record<string, () => Date> = {
77
+ ArrowLeft: () => addDays(cursor, -1),
78
+ ArrowRight: () => addDays(cursor, 1),
79
+ ArrowUp: () => addDays(cursor, -DAYS_PER_WEEK),
80
+ ArrowDown: () => addDays(cursor, DAYS_PER_WEEK),
81
+ PageUp: () => addMonths(cursor, event.shiftKey ? -12 : -1),
82
+ PageDown: () => addMonths(cursor, event.shiftKey ? 12 : 1),
83
+ Home: () => startOfWeek(cursor, weekStartsOn),
84
+ End: () => addDays(startOfWeek(cursor, weekStartsOn), DAYS_PER_WEEK - 1),
85
+ };
86
+ const move = moves[event.key];
87
+ if (move) {
88
+ event.preventDefault();
89
+ wantsFocus.current = true;
90
+ onCursorChange(move());
91
+ } else if ((event.key === "Enter" || event.key === " ") && !isDisabled(cursor)) {
92
+ event.preventDefault();
93
+ onPick(cursor);
94
+ }
95
+ }
96
+
97
+ return (
98
+ <div
99
+ ref={gridRef}
100
+ className={[
101
+ "os-cal-month",
102
+ eventDisplay === "chips" && "os-cal-month-chips",
103
+ showWeekNumbers && "os-cal-month-weeks",
104
+ ]
105
+ .filter(Boolean)
106
+ .join(" ")}
107
+ role="grid"
108
+ aria-label={cursor.toLocaleDateString(DEFAULT_LOCALE, { month: "long", year: "numeric" })}
109
+ onKeyDown={onKeyDown}
110
+ onPointerLeave={() => setHover(null)}
111
+ >
112
+ <div className="os-cal-row os-cal-dows" role="row">
113
+ {showWeekNumbers && (
114
+ <span className="os-cal-weekno" role="columnheader" aria-label="Week" />
115
+ )}
116
+ {weekdayNames(weekStartsOn, "short").map((name) => (
117
+ <span key={name} className="os-cal-dow" role="columnheader" aria-label={name}>
118
+ {eventDisplay === "chips" ? name : name.slice(0, 2)}
119
+ </span>
120
+ ))}
121
+ </div>
122
+ {Array.from({ length: cells.length / DAYS_PER_WEEK }, (_, week) => (
123
+ <div key={week} className="os-cal-row" role="row">
124
+ {showWeekNumbers && (
125
+ <span className="os-cal-weekno" role="rowheader">
126
+ {isoWeek(cells[week * DAYS_PER_WEEK])}
127
+ </span>
128
+ )}
129
+ {cells.slice(week * DAYS_PER_WEEK, week * DAYS_PER_WEEK + DAYS_PER_WEEK).map((day) => {
130
+ const dayEvents = events.filter((event) => sameDay(event.date, day));
131
+ const outside = !sameMonth(day, cursor);
132
+ const disabled = isDisabled(day);
133
+ const isStart = sameDay(range?.start, day);
134
+ const isEnd = sameDay(rangeEnd, day);
135
+ const inRange = Boolean(
136
+ range?.start && rangeEnd && isBetween(day, range.start, rangeEnd),
137
+ );
138
+ const selected = sameDay(value, day) || isStart || isEnd;
139
+ const reversed = Boolean(range?.start && rangeEnd && rangeEnd < range.start);
140
+ return (
141
+ <div
142
+ key={day.toISOString()}
143
+ role="gridcell"
144
+ aria-selected={selected}
145
+ className={[
146
+ "os-cal-cell",
147
+ inRange && "os-cal-in-range",
148
+ range?.start &&
149
+ rangeEnd &&
150
+ !sameDay(range.start, rangeEnd) &&
151
+ (reversed ? isEnd : isStart) &&
152
+ "os-cal-range-start",
153
+ range?.start &&
154
+ rangeEnd &&
155
+ !sameDay(range.start, rangeEnd) &&
156
+ (reversed ? isStart : isEnd) &&
157
+ "os-cal-range-end",
158
+ ]
159
+ .filter(Boolean)
160
+ .join(" ")}
161
+ >
162
+ <button
163
+ type="button"
164
+ tabIndex={sameDay(day, cursor) ? 0 : -1}
165
+ disabled={disabled}
166
+ aria-label={day.toLocaleDateString(DEFAULT_LOCALE, {
167
+ weekday: "long",
168
+ month: "long",
169
+ day: "numeric",
170
+ year: "numeric",
171
+ })}
172
+ aria-current={sameDay(day, today) ? "date" : undefined}
173
+ className={[
174
+ "os-cal-day",
175
+ outside && "os-cal-day-outside",
176
+ selected && "os-cal-day-selected",
177
+ sameDay(day, today) && "os-cal-day-today",
178
+ ]
179
+ .filter(Boolean)
180
+ .join(" ")}
181
+ onClick={() => {
182
+ onCursorChange(day);
183
+ onPick(day);
184
+ }}
185
+ onPointerEnter={() => range?.start && !range.end && setHover(day)}
186
+ >
187
+ <span className="os-cal-num">{day.getDate()}</span>
188
+ {eventDisplay === "dots" && dayEvents.length > 0 && (
189
+ <span className="os-cal-dots" aria-hidden>
190
+ {dayEvents.slice(0, MAX_DOTS).map((event) => (
191
+ <span
192
+ key={event.id}
193
+ className={`os-cal-dot os-cal-tone-${event.tone ?? "accent"}`}
194
+ />
195
+ ))}
196
+ </span>
197
+ )}
198
+ </button>
199
+ {eventDisplay === "chips" && dayEvents.length > 0 && (
200
+ <div className="os-cal-chips">
201
+ {dayEvents.slice(0, MAX_CHIPS).map((event) => (
202
+ <span
203
+ key={event.id}
204
+ className={`os-cal-chip os-cal-tone-${event.tone ?? "accent"}`}
205
+ title={event.title}
206
+ >
207
+ {event.start && <span className="os-cal-chip-time">{event.start}</span>}
208
+ {event.title}
209
+ </span>
210
+ ))}
211
+ {dayEvents.length > MAX_CHIPS && (
212
+ <span className="os-cal-more">+{dayEvents.length - MAX_CHIPS} more</span>
213
+ )}
214
+ </div>
215
+ )}
216
+ </div>
217
+ );
218
+ })}
219
+ </div>
220
+ ))}
221
+ </div>
222
+ );
223
+ }
@@ -0,0 +1,210 @@
1
+ "use client";
2
+
3
+ import { DEFAULT_LOCALE, sameDay } from "./date";
4
+ import { MINUTES_PER_HOUR, displayTime, minutesOf, range, type HourCycle } from "./time";
5
+
6
+ import type { CalendarEvent } from "./calendarTypes";
7
+ import type { CSSProperties } from "react";
8
+
9
+ /** Shortest block drawn, so a 10-minute event still has room for its title. */
10
+ const MIN_EVENT_MINUTES = 30;
11
+ /** Below this, title and time share one line. */
12
+ const SHORT_EVENT_MINUTES = 50;
13
+
14
+ export interface CalendarScheduleProps {
15
+ days: Date[];
16
+ events: CalendarEvent[];
17
+ value?: Date | null;
18
+ onPick: (date: Date) => void;
19
+ today: Date | null;
20
+ now: Date | null;
21
+ /** First and last hour shown, 0–24. */
22
+ hours: [number, number];
23
+ hourCycle: HourCycle;
24
+ }
25
+
26
+ interface Placed {
27
+ event: CalendarEvent;
28
+ start: number;
29
+ end: number;
30
+ column: number;
31
+ columns: number;
32
+ }
33
+
34
+ /** Side-by-side columns for events that overlap, like any calendar app. */
35
+ function layout(events: CalendarEvent[]): Placed[] {
36
+ const timed = events
37
+ .filter((event) => event.start)
38
+ .map((event) => {
39
+ const start = minutesOf(event.start!);
40
+ const end = Math.max(minutesOf(event.end ?? event.start!), start + MIN_EVENT_MINUTES);
41
+ return { event, start, end, column: 0, columns: 1 };
42
+ })
43
+ .sort((a, b) => a.start - b.start || b.end - a.end);
44
+
45
+ const placed: Placed[] = [];
46
+ let cluster: Placed[] = [];
47
+ let clusterEnd = -1;
48
+ const flush = () => {
49
+ const width = Math.max(1, ...cluster.map((p) => p.column + 1));
50
+ cluster.forEach((p) => (p.columns = width));
51
+ cluster = [];
52
+ };
53
+ for (const item of timed) {
54
+ if (item.start >= clusterEnd) flush();
55
+ const used = new Set(cluster.filter((p) => p.end > item.start).map((p) => p.column));
56
+ let column = 0;
57
+ while (used.has(column)) column++;
58
+ item.column = column;
59
+ cluster.push(item);
60
+ placed.push(item);
61
+ clusterEnd = Math.max(clusterEnd, item.end);
62
+ }
63
+ flush();
64
+ return placed;
65
+ }
66
+
67
+ function hourLabel(hour: number, hourCycle: HourCycle) {
68
+ return displayTime(`${String(hour).padStart(2, "0")}:00`, hourCycle).replace(":00", "");
69
+ }
70
+
71
+ /** A time grid for one day or a week, with overlap columns and a now line. */
72
+ export function CalendarSchedule({
73
+ days,
74
+ events,
75
+ value,
76
+ onPick,
77
+ today,
78
+ now,
79
+ hours,
80
+ hourCycle,
81
+ }: CalendarScheduleProps) {
82
+ const [first, last] = hours;
83
+ const span = last - first;
84
+ const nowMinutes = now ? now.getHours() * MINUTES_PER_HOUR + now.getMinutes() : null;
85
+ const allDay = events.filter(
86
+ (event) => !event.start && days.some((day) => sameDay(day, event.date)),
87
+ );
88
+ const style = { "--os-cal-days": days.length, "--os-cal-span": span } as CSSProperties;
89
+
90
+ return (
91
+ <div
92
+ className={days.length === 1 ? "os-cal-schedule os-cal-schedule-day" : "os-cal-schedule"}
93
+ style={style}
94
+ >
95
+ <div className="os-cal-sched-head">
96
+ <span className="os-cal-gutter" />
97
+ {days.map((day) => (
98
+ <button
99
+ key={day.toISOString()}
100
+ type="button"
101
+ className={[
102
+ "os-cal-sched-day",
103
+ sameDay(day, value) && "os-cal-sched-day-selected",
104
+ sameDay(day, today) && "os-cal-sched-day-today",
105
+ ]
106
+ .filter(Boolean)
107
+ .join(" ")}
108
+ aria-pressed={sameDay(day, value)}
109
+ onClick={() => onPick(day)}
110
+ >
111
+ <span className="os-cal-sched-dow">
112
+ {day.toLocaleDateString(DEFAULT_LOCALE, { weekday: "short" })}
113
+ </span>
114
+ <span className="os-cal-sched-num">{day.getDate()}</span>
115
+ </button>
116
+ ))}
117
+ </div>
118
+
119
+ {allDay.length > 0 && (
120
+ <div className="os-cal-allday">
121
+ <span className="os-cal-gutter os-cal-allday-label">All day</span>
122
+ {days.map((day) => (
123
+ <div key={day.toISOString()} className="os-cal-allday-cell">
124
+ {allDay
125
+ .filter((event) => sameDay(event.date, day))
126
+ .map((event) => (
127
+ <span
128
+ key={event.id}
129
+ className={`os-cal-chip os-cal-tone-${event.tone ?? "accent"}`}
130
+ >
131
+ {event.title}
132
+ </span>
133
+ ))}
134
+ </div>
135
+ ))}
136
+ </div>
137
+ )}
138
+
139
+ <div className="os-cal-sched-body">
140
+ <div className="os-cal-gutter os-cal-hours" aria-hidden>
141
+ {range(first, last - 1).map((hour) => (
142
+ <span key={hour} className="os-cal-hour">
143
+ {hour === first ? "" : hourLabel(hour, hourCycle)}
144
+ </span>
145
+ ))}
146
+ </div>
147
+ {days.map((day) => {
148
+ const placed = layout(events.filter((event) => sameDay(event.date, day)));
149
+ const showNow =
150
+ sameDay(day, today) &&
151
+ nowMinutes != null &&
152
+ nowMinutes >= first * MINUTES_PER_HOUR &&
153
+ nowMinutes <= last * MINUTES_PER_HOUR;
154
+ return (
155
+ <div
156
+ key={day.toISOString()}
157
+ className={sameDay(day, today) ? "os-cal-lane os-cal-lane-today" : "os-cal-lane"}
158
+ >
159
+ {placed.map(({ event, start, end, column, columns }) => {
160
+ const top = (start - first * MINUTES_PER_HOUR) / MINUTES_PER_HOUR;
161
+ const height = (end - start) / MINUTES_PER_HOUR;
162
+ if (top + height <= 0 || top >= span) return null;
163
+ return (
164
+ <div
165
+ key={event.id}
166
+ className={[
167
+ "os-cal-event",
168
+ `os-cal-tone-${event.tone ?? "accent"}`,
169
+ end - start < SHORT_EVENT_MINUTES && "os-cal-event-short",
170
+ ]
171
+ .filter(Boolean)
172
+ .join(" ")}
173
+ style={
174
+ {
175
+ "--os-cal-top": Math.max(top, 0),
176
+ "--os-cal-height": Math.min(height, span - Math.max(top, 0)),
177
+ "--os-cal-col": column,
178
+ "--os-cal-cols": columns,
179
+ } as CSSProperties
180
+ }
181
+ >
182
+ <span className="os-cal-event-title">{event.title}</span>
183
+ <span className="os-cal-event-time">
184
+ {displayTime(event.start!, hourCycle)}
185
+ {event.end ? ` – ${displayTime(event.end, hourCycle)}` : ""}
186
+ </span>
187
+ {event.location && (
188
+ <span className="os-cal-event-time os-cal-event-loc">{event.location}</span>
189
+ )}
190
+ </div>
191
+ );
192
+ })}
193
+ {showNow && (
194
+ <span
195
+ className="os-cal-now"
196
+ style={
197
+ {
198
+ "--os-cal-top": (nowMinutes - first * MINUTES_PER_HOUR) / MINUTES_PER_HOUR,
199
+ } as CSSProperties
200
+ }
201
+ aria-label="Now"
202
+ />
203
+ )}
204
+ </div>
205
+ );
206
+ })}
207
+ </div>
208
+ </div>
209
+ );
210
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { ClickableCard } from "./Container";
4
+ import { Card, Heading, Stack, Text, type CardVariant } from "./Primitives";
5
+
6
+ import type { ReactNode } from "react";
7
+
8
+ /** Card width in px, or any CSS width. */
9
+ type Width = number | string;
10
+
11
+ export interface JobCardProps {
12
+ title: string;
13
+ meta?: string;
14
+ /** Status, badges, or anything that describes the room at a glance. */
15
+ children?: ReactNode;
16
+ footer?: ReactNode;
17
+ /** Makes the whole card a link. */
18
+ href?: string;
19
+ /** Makes the whole card a button. */
20
+ onClick?: () => void;
21
+ /** Tints the card to mark the chosen room or bid. */
22
+ selected?: boolean;
23
+ /** Lifts the card — only for a card that overlaps content beneath it. */
24
+ raised?: boolean;
25
+ width?: Width;
26
+ }
27
+
28
+ /** The Joined room card: title, meta line, optional body and footer, on an Astryx Card. */
29
+ export function JobCard({
30
+ title,
31
+ meta,
32
+ children,
33
+ footer,
34
+ href,
35
+ onClick,
36
+ selected,
37
+ raised,
38
+ width,
39
+ }: JobCardProps) {
40
+ const variant: CardVariant = selected ? "blue" : "default";
41
+ const elevation = raised ? "low" : undefined;
42
+ const body = (
43
+ <Stack gap={2}>
44
+ <Stack gap={0.5}>
45
+ <Heading level={4}>{title}</Heading>
46
+ {meta && (
47
+ <Text type="supporting" color="secondary">
48
+ {meta}
49
+ </Text>
50
+ )}
51
+ </Stack>
52
+ {children}
53
+ {footer && (
54
+ <Text type="supporting" color="secondary">
55
+ {footer}
56
+ </Text>
57
+ )}
58
+ </Stack>
59
+ );
60
+
61
+ if (href || onClick) {
62
+ return (
63
+ <ClickableCard
64
+ label={title}
65
+ href={href}
66
+ onClick={onClick}
67
+ variant={variant}
68
+ elevation={elevation}
69
+ width={width}
70
+ >
71
+ {body}
72
+ </ClickableCard>
73
+ );
74
+ }
75
+ return (
76
+ <Card variant={variant} elevation={elevation} width={width}>
77
+ {body}
78
+ </Card>
79
+ );
80
+ }
@@ -0,0 +1,64 @@
1
+ import { segmentShares } from "./chartMath";
2
+
3
+ import type { ChartTone } from "./chartParts";
4
+
5
+ /**
6
+ * A proportion bar that sits inside a KpiWidget or a card.
7
+ * Colours come only from the theme's data tokens (see styles/components/charts.css).
8
+ */
9
+
10
+ export type SegmentTone = ChartTone;
11
+
12
+ export type Segment = {
13
+ label: string;
14
+ value: number;
15
+ tone?: SegmentTone;
16
+ /** How the value reads in the legend and to screen readers, e.g. "$12.50". Defaults to the number. */
17
+ display?: string;
18
+ };
19
+
20
+ /** One bar split by share, with an optional legend. Empty data renders an empty track. */
21
+ export function SegmentBar({
22
+ segments,
23
+ hasLegend = true,
24
+ unit,
25
+ }: {
26
+ segments: Segment[];
27
+ hasLegend?: boolean;
28
+ /** Read after each value in the accessible summary, e.g. "jobs". */
29
+ unit?: string;
30
+ }) {
31
+ const shares = segmentShares(segments.map((segment) => segment.value));
32
+ const summary = segments
33
+ .map(
34
+ (segment) => `${segment.label} ${segment.display ?? segment.value}${unit ? ` ${unit}` : ""}`,
35
+ )
36
+ .join(", ");
37
+ return (
38
+ <div className="os-segments">
39
+ <div className="os-segment-track" role="img" aria-label={summary}>
40
+ {segments.map((segment, index) =>
41
+ shares[index] ? (
42
+ <span
43
+ key={segment.label}
44
+ className="os-segment"
45
+ data-tone={segment.tone ?? "blue"}
46
+ style={{ width: `${shares[index]}%` }}
47
+ />
48
+ ) : null,
49
+ )}
50
+ </div>
51
+ {hasLegend ? (
52
+ <ul className="os-segment-legend">
53
+ {segments.map((segment) => (
54
+ <li key={segment.label}>
55
+ <span className="os-segment-key" data-tone={segment.tone ?? "blue"} />
56
+ <span>{segment.label}</span>
57
+ <strong>{segment.display ?? segment.value}</strong>
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ ) : null}
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Chat is original Astryx — Joined only themes it. Compose ChatLayout,
3
+ * ChatMessageList, ChatMessage, and ChatComposer for any thread.
4
+ */
5
+ export {
6
+ ChatLayout,
7
+ ChatLayoutScrollButton,
8
+ ChatMessageList,
9
+ ChatMessage,
10
+ ChatMessageBubble,
11
+ ChatMessageMetadata,
12
+ ChatSystemMessage,
13
+ ChatComposer,
14
+ ChatComposerInput,
15
+ ChatComposerDrawer,
16
+ ChatSendButton,
17
+ ChatToolCalls,
18
+ useChatStreamScroll,
19
+ useChatNewMessages,
20
+ } from "@astryxdesign/core/Chat";
21
+ export type {
22
+ ChatLayoutProps,
23
+ ChatMessageListProps,
24
+ ChatMessageProps,
25
+ ChatMessageBubbleProps,
26
+ ChatMessageMetadataProps,
27
+ ChatMessageStatus,
28
+ ChatMessageSender,
29
+ ChatSystemMessageProps,
30
+ ChatComposerProps,
31
+ ChatComposerStatus,
32
+ ChatToolCallsProps,
33
+ ChatToolCallItem,
34
+ ChatToolCallStatus,
35
+ } from "@astryxdesign/core/Chat";
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+
5
+ import { Tokenizer, Typeahead, type SearchableItem } from "./DataInput";
6
+ import {
7
+ PLACES_DEBOUNCE_MS,
8
+ PLACES_MIN_QUERY,
9
+ PlacesCredit,
10
+ placeItem,
11
+ placeSearch,
12
+ } from "./places-search";
13
+ import { Stack } from "./Primitives";
14
+
15
+ import type { Address } from "./places";
16
+
17
+ type Common = {
18
+ label: string;
19
+ isLabelHidden?: boolean;
20
+ isDisabled?: boolean;
21
+ placeholder?: string;
22
+ };
23
+
24
+ type SingleProps = Common & {
25
+ multiple?: false;
26
+ value: string;
27
+ onChange: (city: string) => void;
28
+ };
29
+
30
+ type MultipleProps = Common & {
31
+ multiple: true;
32
+ value: string[];
33
+ onChange: (cities: string[]) => void;
34
+ };
35
+
36
+ /** Search cities with Geoapify as the person types, or pick several. */
37
+ export function CitySelector(props: SingleProps | MultipleProps) {
38
+ const source = useMemo(() => placeSearch("city"), []);
39
+ const placeholder = props.placeholder ?? (props.multiple ? "Add a city" : "Search cities");
40
+
41
+ if (props.multiple) {
42
+ const selected = props.value
43
+ .map((label) => placeItem(label))
44
+ .filter((city): city is SearchableItem<Address> => city != null);
45
+ return (
46
+ <Stack gap={1}>
47
+ <Tokenizer
48
+ label={props.label}
49
+ isLabelHidden={props.isLabelHidden}
50
+ isDisabled={props.isDisabled}
51
+ searchSource={source}
52
+ value={selected}
53
+ onChange={(items) => props.onChange(items.map((item) => item.label))}
54
+ placeholder={placeholder}
55
+ minQueryLength={PLACES_MIN_QUERY}
56
+ debounceMs={PLACES_DEBOUNCE_MS}
57
+ emptySearchResultsText="No cities match."
58
+ />
59
+ <PlacesCredit />
60
+ </Stack>
61
+ );
62
+ }
63
+
64
+ return (
65
+ <Stack gap={1}>
66
+ <Typeahead
67
+ label={props.label}
68
+ isLabelHidden={props.isLabelHidden}
69
+ isDisabled={props.isDisabled}
70
+ searchSource={source}
71
+ value={placeItem(props.value)}
72
+ onChange={(city) => props.onChange(city?.label ?? "")}
73
+ placeholder={placeholder}
74
+ minQueryLength={PLACES_MIN_QUERY}
75
+ debounceMs={PLACES_DEBOUNCE_MS}
76
+ emptySearchResultsText="No cities match."
77
+ />
78
+ <PlacesCredit />
79
+ </Stack>
80
+ );
81
+ }