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,132 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ import { segmentShares } from "./chartMath";
6
+ import { ChartTable, ChartTip, defaultFormat, type ChartTone } from "./chartParts";
7
+
8
+ export type BarDatum = {
9
+ label: string;
10
+ value: number;
11
+ /** One series takes one tone; set this only to call out a single bar. */
12
+ tone?: ChartTone;
13
+ };
14
+
15
+ const DEFAULT_HEIGHT = 200;
16
+
17
+ /**
18
+ * Magnitude by category. `columns` grow up from one baseline with the value on each cap;
19
+ * `bars` run left to right with the name before and the value after, for long labels and
20
+ * ranked lists. Each mark reads out its value and share on hover and focus.
21
+ */
22
+ export function BarChart({
23
+ data,
24
+ label,
25
+ orientation = "columns",
26
+ tone = "blue",
27
+ height = DEFAULT_HEIGHT,
28
+ formatValue = defaultFormat,
29
+ hasValueLabels = true,
30
+ }: {
31
+ data: BarDatum[];
32
+ /** Accessible name for the chart. */
33
+ label: string;
34
+ orientation?: "columns" | "bars";
35
+ tone?: ChartTone;
36
+ /** Plot height for columns, in px. Bars size to their rows. */
37
+ height?: number;
38
+ formatValue?: (value: number) => string;
39
+ hasValueLabels?: boolean;
40
+ }) {
41
+ const [active, setActive] = useState<number | null>(null);
42
+ const max = Math.max(0, ...data.map((datum) => datum.value));
43
+ const shares = segmentShares(data.map((datum) => datum.value));
44
+ const size = (value: number) => (max > 0 ? `${(Math.max(0, value) / max) * 100}%` : "0%");
45
+
46
+ const handlers = (index: number) => ({
47
+ tabIndex: 0,
48
+ onPointerEnter: () => setActive(index),
49
+ onPointerLeave: () => setActive(null),
50
+ onFocus: () => setActive(index),
51
+ onBlur: () => setActive(null),
52
+ "aria-label": `${data[index].label}: ${formatValue(data[index].value)}`,
53
+ });
54
+
55
+ const tip = (index: number) => (
56
+ <ChartTip
57
+ left="50%"
58
+ top={0}
59
+ rows={[
60
+ {
61
+ label: `${data[index].label} · ${Math.round(shares[index])}%`,
62
+ value: formatValue(data[index].value),
63
+ },
64
+ ]}
65
+ />
66
+ );
67
+
68
+ return (
69
+ <div className="os-chart">
70
+ {orientation === "columns" ? (
71
+ <div className="os-bars-columns" style={{ height }} role="list" aria-label={label}>
72
+ {data.map((datum, index) => (
73
+ <div
74
+ key={datum.label}
75
+ className="os-bars-column"
76
+ role="listitem"
77
+ data-active={active === index || undefined}
78
+ {...handlers(index)}
79
+ >
80
+ <div className="os-bars-plot">
81
+ {hasValueLabels ? (
82
+ <span className="os-bars-value">{formatValue(datum.value)}</span>
83
+ ) : null}
84
+ <span
85
+ className="os-bars-mark"
86
+ data-tone={datum.tone ?? tone}
87
+ style={{ height: size(datum.value) }}
88
+ />
89
+ {active === index ? tip(index) : null}
90
+ </div>
91
+ <span className="os-bars-label">{datum.label}</span>
92
+ </div>
93
+ ))}
94
+ </div>
95
+ ) : (
96
+ <div className="os-bars-rows" role="list" aria-label={label}>
97
+ {data.map((datum, index) => (
98
+ <div
99
+ key={datum.label}
100
+ className="os-bars-row"
101
+ role="listitem"
102
+ data-active={active === index || undefined}
103
+ {...handlers(index)}
104
+ >
105
+ <span className="os-bars-label">{datum.label}</span>
106
+ <span className="os-bars-track">
107
+ <span
108
+ className="os-bars-mark"
109
+ data-tone={datum.tone ?? tone}
110
+ style={{ width: size(datum.value) }}
111
+ />
112
+ {active === index ? tip(index) : null}
113
+ </span>
114
+ {hasValueLabels ? (
115
+ <span className="os-bars-value">{formatValue(datum.value)}</span>
116
+ ) : null}
117
+ </div>
118
+ ))}
119
+ </div>
120
+ )}
121
+ <ChartTable
122
+ caption={label}
123
+ headers={["", "Value", "Share"]}
124
+ rows={data.map((datum, index) => [
125
+ datum.label,
126
+ formatValue(datum.value),
127
+ `${Math.round(shares[index])}%`,
128
+ ])}
129
+ />
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,284 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+
5
+ import { Button, IconButton, SegmentedControl, SegmentedControlItem } from "./Action";
6
+ import { CalendarAgenda } from "./CalendarAgenda";
7
+ import { CalendarMonth } from "./CalendarMonth";
8
+ import { CalendarSchedule } from "./CalendarSchedule";
9
+ import {
10
+ VIEW_LABELS,
11
+ type CalendarEvent,
12
+ type CalendarView,
13
+ type DateRange,
14
+ } from "./calendarTypes";
15
+ import {
16
+ DAYS_PER_WEEK,
17
+ DEFAULT_LOCALE,
18
+ addDays,
19
+ addMonths,
20
+ monthNames,
21
+ startOfDay,
22
+ weekOf,
23
+ type WeekStart,
24
+ } from "./date";
25
+ import { Glyph, icons } from "./Glyph";
26
+ import { useControllable, useNow } from "./hooks";
27
+ import { Icon } from "./Primitives";
28
+
29
+ import type { ControlSize } from "./size";
30
+ import type { HourCycle } from "./time";
31
+
32
+ export type { CalendarEvent, CalendarView, CalendarTone, DateRange } from "./calendarTypes";
33
+
34
+ const NOW_TICK_MS = 60_000;
35
+ const DEFAULT_HOURS: [number, number] = [8, 19];
36
+
37
+ export interface CalendarProps {
38
+ value?: Date | null;
39
+ onChange?: (date: Date) => void;
40
+ /** `range` picks a start, then an end, with a live preview between them. */
41
+ selection?: "single" | "range";
42
+ range?: DateRange | null;
43
+ onRangeChange?: (range: DateRange) => void;
44
+ view?: CalendarView;
45
+ defaultView?: CalendarView;
46
+ onViewChange?: (view: CalendarView) => void;
47
+ /** Adds a view switcher when more than one is listed. */
48
+ views?: CalendarView[];
49
+ size?: ControlSize;
50
+ events?: CalendarEvent[];
51
+ /** Month cells show events as dots under the date or as titled chips. */
52
+ eventDisplay?: "dots" | "chips";
53
+ weekStartsOn?: WeekStart;
54
+ min?: Date;
55
+ max?: Date;
56
+ isDateDisabled?: (date: Date) => boolean;
57
+ /** First and last hour on the week and day grids. */
58
+ hours?: [number, number];
59
+ hourCycle?: HourCycle;
60
+ showWeekNumbers?: boolean;
61
+ /** Content under the grid — presets, a summary, actions. */
62
+ footer?: ReactNode;
63
+ }
64
+
65
+ function MonthYearPicker({ cursor, onPick }: { cursor: Date; onPick: (date: Date) => void }) {
66
+ const [year, setYear] = useState(cursor.getFullYear());
67
+ return (
68
+ <div className="os-cal-jump">
69
+ <div className="os-cal-jump-year">
70
+ <IconButton
71
+ label="Previous year"
72
+ variant="ghost"
73
+ size="sm"
74
+ icon={<Icon icon={icons.chevronLeft} />}
75
+ onClick={() => setYear(year - 1)}
76
+ />
77
+ <span className="os-cal-jump-label">{year}</span>
78
+ <IconButton
79
+ label="Next year"
80
+ variant="ghost"
81
+ size="sm"
82
+ icon={<Icon icon={icons.chevronRight} />}
83
+ onClick={() => setYear(year + 1)}
84
+ />
85
+ </div>
86
+ <div className="os-cal-jump-months">
87
+ {monthNames("short").map((name, month) => (
88
+ <button
89
+ key={name}
90
+ type="button"
91
+ className="os-cal-jump-month"
92
+ aria-pressed={year === cursor.getFullYear() && month === cursor.getMonth()}
93
+ onClick={() => onPick(new Date(year, month, Math.min(cursor.getDate(), 28)))}
94
+ >
95
+ {name}
96
+ </button>
97
+ ))}
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ /** Month, week, day, or agenda — pick a date, a range, or just read the schedule. */
104
+ export function Calendar({
105
+ value,
106
+ onChange,
107
+ selection = "single",
108
+ range,
109
+ onRangeChange,
110
+ view: viewProp,
111
+ defaultView = "month",
112
+ onViewChange,
113
+ views,
114
+ size = "md",
115
+ events = [],
116
+ eventDisplay = "dots",
117
+ weekStartsOn = 0,
118
+ min,
119
+ max,
120
+ isDateDisabled,
121
+ hours = DEFAULT_HOURS,
122
+ hourCycle = "12h",
123
+ showWeekNumbers,
124
+ footer,
125
+ }: CalendarProps) {
126
+ const [view, setView] = useControllable(viewProp, defaultView, onViewChange);
127
+ const [cursor, setCursor] = useState(() => startOfDay(value ?? range?.start ?? new Date()));
128
+ const [jumping, setJumping] = useState(false);
129
+ const now = useNow(NOW_TICK_MS);
130
+ const today = now ? startOfDay(now) : null;
131
+
132
+ const days = view === "day" ? [cursor] : weekOf(cursor, weekStartsOn);
133
+ const schedule = view === "week" || view === "day";
134
+
135
+ function isDisabled(date: Date) {
136
+ return Boolean(
137
+ (min && date < startOfDay(min)) || (max && date > startOfDay(max)) || isDateDisabled?.(date),
138
+ );
139
+ }
140
+
141
+ function pick(date: Date) {
142
+ setCursor(date);
143
+ if (selection === "range") {
144
+ if (!range || range.end) onRangeChange?.({ start: date, end: null });
145
+ else
146
+ onRangeChange?.(
147
+ date < range.start
148
+ ? { start: date, end: range.start }
149
+ : { start: range.start, end: date },
150
+ );
151
+ } else {
152
+ onChange?.(date);
153
+ }
154
+ }
155
+
156
+ function shift(direction: 1 | -1) {
157
+ if (view === "week") setCursor(addDays(cursor, direction * DAYS_PER_WEEK));
158
+ else if (view === "day") setCursor(addDays(cursor, direction));
159
+ else setCursor(addMonths(cursor, direction));
160
+ }
161
+
162
+ const unit = view === "week" ? "week" : view === "day" ? "day" : "month";
163
+ const title =
164
+ view === "week"
165
+ ? `${days[0].toLocaleDateString(DEFAULT_LOCALE, { month: "short", day: "numeric" })} – ${days[
166
+ days.length - 1
167
+ ].toLocaleDateString(DEFAULT_LOCALE, {
168
+ month: days[0].getMonth() === days[days.length - 1].getMonth() ? undefined : "short",
169
+ day: "numeric",
170
+ })}`
171
+ : view === "day"
172
+ ? cursor.toLocaleDateString(DEFAULT_LOCALE, {
173
+ weekday: "long",
174
+ month: "long",
175
+ day: "numeric",
176
+ })
177
+ : cursor.toLocaleDateString(DEFAULT_LOCALE, { month: "long" });
178
+
179
+ return (
180
+ <div
181
+ className={[
182
+ "os-cal",
183
+ `os-cal-${size}`,
184
+ schedule && "os-cal-wide",
185
+ view === "agenda" && "os-cal-agenda-view",
186
+ eventDisplay === "chips" && "os-cal-wide",
187
+ ]
188
+ .filter(Boolean)
189
+ .join(" ")}
190
+ >
191
+ <div className="os-cal-head">
192
+ <button
193
+ type="button"
194
+ className="os-cal-title"
195
+ aria-expanded={jumping}
196
+ onClick={() => setJumping(!jumping)}
197
+ >
198
+ <span>{title}</span>
199
+ {view !== "day" && <span className="os-cal-year">{cursor.getFullYear()}</span>}
200
+ <Glyph name="chevronDown" className="os-cal-title-chevron" />
201
+ </button>
202
+ <div className="os-cal-actions">
203
+ {views && views.length > 1 && (
204
+ <SegmentedControl
205
+ label="Calendar view"
206
+ size="sm"
207
+ value={view}
208
+ onChange={(next) => {
209
+ setJumping(false);
210
+ setView(next as CalendarView);
211
+ }}
212
+ >
213
+ {views.map((v) => (
214
+ <SegmentedControlItem key={v} value={v} label={VIEW_LABELS[v]} />
215
+ ))}
216
+ </SegmentedControl>
217
+ )}
218
+ {size !== "sm" && today && (
219
+ <Button label="Today" variant="secondary" size="sm" onClick={() => setCursor(today)} />
220
+ )}
221
+ <IconButton
222
+ label={`Previous ${unit}`}
223
+ variant="ghost"
224
+ size="sm"
225
+ icon={<Icon icon={icons.chevronLeft} />}
226
+ onClick={() => shift(-1)}
227
+ />
228
+ <IconButton
229
+ label={`Next ${unit}`}
230
+ variant="ghost"
231
+ size="sm"
232
+ icon={<Icon icon={icons.chevronRight} />}
233
+ onClick={() => shift(1)}
234
+ />
235
+ </div>
236
+ </div>
237
+
238
+ {jumping ? (
239
+ <MonthYearPicker
240
+ cursor={cursor}
241
+ onPick={(date) => {
242
+ setCursor(date);
243
+ setJumping(false);
244
+ }}
245
+ />
246
+ ) : view === "month" ? (
247
+ <CalendarMonth
248
+ cursor={cursor}
249
+ onCursorChange={setCursor}
250
+ value={value}
251
+ range={selection === "range" ? range : null}
252
+ onPick={pick}
253
+ events={events}
254
+ eventDisplay={eventDisplay}
255
+ weekStartsOn={weekStartsOn}
256
+ isDisabled={isDisabled}
257
+ today={today}
258
+ showWeekNumbers={showWeekNumbers}
259
+ />
260
+ ) : schedule ? (
261
+ <CalendarSchedule
262
+ days={days}
263
+ events={events}
264
+ value={value}
265
+ onPick={pick}
266
+ today={today}
267
+ now={now}
268
+ hours={hours}
269
+ hourCycle={hourCycle}
270
+ />
271
+ ) : (
272
+ <CalendarAgenda
273
+ cursor={cursor}
274
+ events={events}
275
+ value={value}
276
+ onPick={pick}
277
+ today={today}
278
+ hourCycle={hourCycle}
279
+ />
280
+ )}
281
+ {footer && <div className="os-cal-footer">{footer}</div>}
282
+ </div>
283
+ );
284
+ }
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { DEFAULT_LOCALE, sameDay, sameMonth, startOfDay } from "./date";
4
+ import { displayTime, minutesOf, type HourCycle } from "./time";
5
+
6
+ import type { CalendarEvent } from "./calendarTypes";
7
+
8
+ export interface CalendarAgendaProps {
9
+ cursor: Date;
10
+ events: CalendarEvent[];
11
+ value?: Date | null;
12
+ onPick: (date: Date) => void;
13
+ today: Date | null;
14
+ hourCycle: HourCycle;
15
+ }
16
+
17
+ /** The month's events as a reading list, grouped by day. */
18
+ export function CalendarAgenda({
19
+ cursor,
20
+ events,
21
+ value,
22
+ onPick,
23
+ today,
24
+ hourCycle,
25
+ }: CalendarAgendaProps) {
26
+ const inMonth = events
27
+ .filter((event) => sameMonth(event.date, cursor))
28
+ .sort(
29
+ (a, b) =>
30
+ a.date.getTime() - b.date.getTime() ||
31
+ minutesOf(a.start ?? "00:00") - minutesOf(b.start ?? "00:00"),
32
+ );
33
+ const days = inMonth.reduce<Date[]>((out, event) => {
34
+ if (!out.some((day) => sameDay(day, event.date))) out.push(startOfDay(event.date));
35
+ return out;
36
+ }, []);
37
+
38
+ if (days.length === 0)
39
+ return <p className="os-cal-agenda-empty">Nothing scheduled this month.</p>;
40
+
41
+ return (
42
+ <div className="os-cal-agenda">
43
+ {days.map((day) => (
44
+ <section key={day.toISOString()} className="os-cal-agenda-day">
45
+ <button
46
+ type="button"
47
+ className={[
48
+ "os-cal-agenda-date",
49
+ sameDay(day, today) && "os-cal-agenda-today",
50
+ sameDay(day, value) && "os-cal-agenda-selected",
51
+ ]
52
+ .filter(Boolean)
53
+ .join(" ")}
54
+ onClick={() => onPick(day)}
55
+ >
56
+ <span className="os-cal-agenda-num">{day.getDate()}</span>
57
+ <span className="os-cal-agenda-dow">
58
+ {day.toLocaleDateString(DEFAULT_LOCALE, { weekday: "short" })}
59
+ </span>
60
+ </button>
61
+ <ul className="os-cal-agenda-list">
62
+ {inMonth
63
+ .filter((event) => sameDay(event.date, day))
64
+ .map((event) => (
65
+ <li
66
+ key={event.id}
67
+ className={`os-cal-agenda-item os-cal-tone-${event.tone ?? "accent"}`}
68
+ >
69
+ <span className="os-cal-agenda-bar" aria-hidden />
70
+ <span className="os-cal-agenda-time">
71
+ {event.start ? displayTime(event.start, hourCycle) : "All day"}
72
+ {event.end && (
73
+ <span className="os-cal-agenda-end">{displayTime(event.end, hourCycle)}</span>
74
+ )}
75
+ </span>
76
+ <span className="os-cal-agenda-copy">
77
+ <span className="os-cal-agenda-title">{event.title}</span>
78
+ {event.location && <span className="os-cal-agenda-loc">{event.location}</span>}
79
+ </span>
80
+ </li>
81
+ ))}
82
+ </ul>
83
+ </section>
84
+ ))}
85
+ </div>
86
+ );
87
+ }