@agent-native/core 0.98.8 → 0.98.9

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 (35) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/mcp/build-server.ts +99 -26
  5. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +31 -2
  6. package/corpus/templates/calendar/AGENTS.md +11 -0
  7. package/corpus/templates/calendar/README.md +2 -1
  8. package/corpus/templates/calendar/actions/create-event.ts +7 -6
  9. package/corpus/templates/calendar/actions/event-action-helpers.ts +49 -0
  10. package/corpus/templates/calendar/actions/update-event.ts +225 -14
  11. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +185 -70
  12. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +26 -4
  13. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +112 -69
  14. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +621 -524
  15. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +275 -164
  16. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +222 -0
  17. package/corpus/templates/calendar/app/hooks/use-events.ts +151 -79
  18. package/corpus/templates/calendar/app/i18n/zh-TW.ts +6 -0
  19. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  20. package/corpus/templates/calendar/app/lib/all-day-layout.ts +126 -0
  21. package/corpus/templates/calendar/app/lib/event-form-utils.ts +18 -1
  22. package/corpus/templates/calendar/app/lib/event-mutation-inputs.ts +1 -1
  23. package/corpus/templates/calendar/app/lib/working-location.ts +163 -0
  24. package/corpus/templates/calendar/app/pages/CalendarView.tsx +21 -2
  25. package/corpus/templates/calendar/changelog/2026-07-07-working-locations-from-google-calendar-now-appear-as-native-.md +6 -0
  26. package/corpus/templates/calendar/server/lib/calendar-availability.ts +2 -0
  27. package/corpus/templates/calendar/server/lib/google-api.ts +6 -1
  28. package/corpus/templates/calendar/server/lib/google-calendar.ts +49 -19
  29. package/corpus/templates/calendar/shared/api.ts +2 -0
  30. package/dist/mcp/build-server.d.ts.map +1 -1
  31. package/dist/mcp/build-server.js +82 -28
  32. package/dist/mcp/build-server.js.map +1 -1
  33. package/dist/observability/routes.d.ts +5 -5
  34. package/dist/secrets/routes.d.ts +9 -9
  35. package/package.json +1 -1
@@ -1,5 +1,11 @@
1
+ import { useT } from "@agent-native/core/client";
1
2
  import type { CalendarEvent } from "@shared/api";
2
- import { IconAlertTriangleFilled } from "@tabler/icons-react";
3
+ import {
4
+ IconAlertTriangleFilled,
5
+ IconBuilding,
6
+ IconHome,
7
+ IconMapPin,
8
+ } from "@tabler/icons-react";
3
9
  import {
4
10
  startOfWeek,
5
11
  endOfWeek,
@@ -31,6 +37,11 @@ import {
31
37
  useViewPreferences,
32
38
  type ViewPreferences,
33
39
  } from "@/hooks/use-view-preferences";
40
+ import {
41
+ groupAdjacentAllDayPlacements,
42
+ layoutAllDayEvents,
43
+ partitionAllDayEvents,
44
+ } from "@/lib/all-day-layout";
34
45
  import { getEventDisplayColor, allOtherDeclined } from "@/lib/event-colors";
35
46
  import {
36
47
  shouldSuppressAfterPopoverClose,
@@ -38,6 +49,11 @@ import {
38
49
  } from "@/lib/popover-click-guard";
39
50
  import { EventStatusIcon } from "@/lib/rsvp-status";
40
51
  import { cn } from "@/lib/utils";
52
+ import {
53
+ createWorkingLocationDisplayLabels,
54
+ getWorkingLocationChipLabel,
55
+ getWorkingLocationTitle,
56
+ } from "@/lib/working-location";
41
57
 
42
58
  import { EventDetailPopover } from "./EventDetailPopover";
43
59
  import { shouldRenderWeekDragSegment } from "./week-drag-segment";
@@ -209,31 +225,6 @@ function computeLayout(
209
225
  return result;
210
226
  }
211
227
 
212
- /** Determine which day columns an all-day event spans within a given week */
213
- function getAllDaySpan(
214
- event: CalendarEvent,
215
- days: Date[],
216
- ): { startCol: number; endCol: number } | null {
217
- const evStart = parseISO(event.start);
218
- const evEnd = event.end ? parseISO(event.end) : addDays(evStart, 1);
219
-
220
- let startCol = -1;
221
- let endCol = -1;
222
-
223
- for (let i = 0; i < days.length; i++) {
224
- const dayStart = startOfDay(days[i]);
225
- const dayEnd = addDays(dayStart, 1);
226
- // Event overlaps this day if it starts before day ends and ends after day starts
227
- if (evStart < dayEnd && evEnd > dayStart) {
228
- if (startCol === -1) startCol = i;
229
- endCol = i;
230
- }
231
- }
232
-
233
- if (startCol === -1) return null;
234
- return { startCol, endCol };
235
- }
236
-
237
228
  function getSegmentStyle(event: CalendarEvent, day: Date) {
238
229
  const evStart = parseISO(event.start);
239
230
  const evEnd = parseISO(event.end);
@@ -330,6 +321,7 @@ const WeekEventCard = memo(function WeekEventCard({
330
321
  onDraftCreate,
331
322
  onDraftDiscard,
332
323
  }: WeekEventCardProps) {
324
+ const t = useT();
333
325
  const li = layout.get(event.id) ?? {
334
326
  left: 0,
335
327
  width: 0,
@@ -616,6 +608,11 @@ export const WeekView = memo(function WeekView({
616
608
  onDraftDiscard,
617
609
  isLoading = false,
618
610
  }: WeekViewProps) {
611
+ const t = useT();
612
+ const workingLocationLabels = useMemo(
613
+ () => createWorkingLocationDisplayLabels(t),
614
+ [t],
615
+ );
619
616
  const { setFocusedEvent } = useCalendarSetters();
620
617
  const isMobile = useIsMobile();
621
618
  const GUTTER_WIDTH = isMobile ? MOBILE_GUTTER_WIDTH : DESKTOP_GUTTER_WIDTH;
@@ -680,18 +677,33 @@ export const WeekView = memo(function WeekView({
680
677
 
681
678
  const timedEvents = useMemo(() => events.filter((e) => !e.allDay), [events]);
682
679
 
683
- // Pre-compute all-day event spans
684
- const allDaySpans = useMemo(() => {
685
- const spans: { event: CalendarEvent; startCol: number; endCol: number }[] =
686
- [];
687
- for (const ev of allDayEvents) {
688
- const span = getAllDaySpan(ev, days);
689
- if (span) {
690
- spans.push({ event: ev, ...span });
691
- }
692
- }
693
- return spans;
694
- }, [allDayEvents, days]);
680
+ const { workingLocations, regularEvents } = useMemo(
681
+ () => partitionAllDayEvents(allDayEvents),
682
+ [allDayEvents],
683
+ );
684
+ const workingLocationLayout = useMemo(
685
+ () => layoutAllDayEvents(workingLocations, days),
686
+ [days, workingLocations],
687
+ );
688
+ const workingLocationGroups = useMemo(
689
+ () =>
690
+ groupAdjacentAllDayPlacements(
691
+ workingLocationLayout.placements,
692
+ ({ event }) =>
693
+ [
694
+ event.accountEmail,
695
+ event.overlayEmail,
696
+ event.ownerColor,
697
+ getEventDisplayColor(event, prefs),
698
+ getWorkingLocationChipLabel(event, workingLocationLabels),
699
+ JSON.stringify(event.workingLocationProperties ?? {}),
700
+ ].join(":"),
701
+ ),
702
+ [prefs, workingLocationLabels, workingLocationLayout.placements],
703
+ );
704
+ const regularAllDayLayout = useMemo(() => {
705
+ return layoutAllDayEvents(regularEvents, days);
706
+ }, [days, regularEvents]);
695
707
 
696
708
  // Pre-compute timed events per day with layout — include events spanning into this day
697
709
  const dayData = useMemo(() => {
@@ -714,68 +726,23 @@ export const WeekView = memo(function WeekView({
714
726
  const showNowIndicator =
715
727
  nowMinutes >= 0 && nowMinutes <= (END_HOUR - START_HOUR) * 60;
716
728
 
717
- const hasAnyAllDay = allDaySpans.length > 0;
718
-
719
- // Compute the number of "rows" needed for all-day events (to handle stacking)
720
- const allDayRows = useMemo(() => {
721
- if (allDaySpans.length === 0) return 0;
722
- // Simple row-packing algorithm
723
- const rows: { startCol: number; endCol: number }[][] = [];
724
- for (const span of allDaySpans) {
725
- let placed = false;
726
- for (const row of rows) {
727
- // i18n-ignore scanner false positive for layout property access
728
- const hasConflict = row.some(
729
- (existing) =>
730
- /* i18n-ignore scanner false positive */ span.startCol <=
731
- existing.endCol && span.endCol >= existing.startCol,
732
- );
733
- if (!hasConflict) {
734
- row.push(span);
735
- placed = true;
736
- break;
737
- }
738
- }
739
- if (!placed) {
740
- rows.push([span]);
741
- }
742
- }
743
- return rows.length;
744
- }, [allDaySpans]);
745
-
746
- // Assign row index to each all-day span
747
- const allDayRowAssignments = useMemo(() => {
748
- const assignments = new Map<string, number>();
749
- if (allDaySpans.length === 0) return assignments;
750
- const rows: { startCol: number; endCol: number; id: string }[][] = [];
751
- for (const span of allDaySpans) {
752
- let placed = false;
753
- for (let r = 0; r < rows.length; r++) {
754
- // i18n-ignore scanner false positive for layout property access
755
- const hasConflict = rows[r].some(
756
- (existing) =>
757
- /* i18n-ignore scanner false positive */ span.startCol <=
758
- existing.endCol && span.endCol >= existing.startCol,
759
- );
760
- if (!hasConflict) {
761
- rows[r].push({ ...span, id: span.event.id });
762
- assignments.set(span.event.id, r);
763
- placed = true;
764
- break;
765
- }
766
- }
767
- if (!placed) {
768
- rows.push([{ ...span, id: span.event.id }]);
769
- assignments.set(span.event.id, rows.length - 1);
770
- }
771
- }
772
- return assignments;
773
- }, [allDaySpans]);
774
-
729
+ const hasWorkingLocations = workingLocationLayout.rowCount > 0;
730
+ const hasRegularAllDayEvents = regularAllDayLayout.rowCount > 0;
731
+ const hasAnyAllDay = hasWorkingLocations || hasRegularAllDayEvents;
732
+ const workingLocationRowHeight = 16;
775
733
  const allDayRowHeight = 20;
776
- const allDaySectionHeight = hasAnyAllDay
777
- ? allDayRows * allDayRowHeight + 6
734
+ const workingLocationLaneHeight = hasWorkingLocations
735
+ ? workingLocationLayout.rowCount * workingLocationRowHeight + 2
736
+ : 0;
737
+ const laneSeparatorHeight =
738
+ hasWorkingLocations && hasRegularAllDayEvents ? 1 : 0;
739
+ const regularAllDayLaneOffset =
740
+ workingLocationLaneHeight + laneSeparatorHeight;
741
+ const regularAllDayLaneHeight = hasRegularAllDayEvents
742
+ ? regularAllDayLayout.rowCount * allDayRowHeight + 6
778
743
  : 0;
744
+ const allDaySectionHeight =
745
+ workingLocationLaneHeight + laneSeparatorHeight + regularAllDayLaneHeight;
779
746
  const allDayHeaderSpacerWidth = Math.max(
780
747
  0,
781
748
  timeGridScrollbarWidth - allDayScrollbarWidth,
@@ -1010,10 +977,17 @@ export const WeekView = memo(function WeekView({
1010
977
  >
1011
978
  {/* Gutter label */}
1012
979
  <div
1013
- className="flex shrink-0 items-start justify-end border-r border-border pr-2 pt-1"
980
+ className="relative shrink-0 border-r border-border"
1014
981
  style={{ width: `${GUTTER_WIDTH}px` }}
1015
982
  >
1016
- <span className="text-[10px] text-muted-foreground">all day</span>
983
+ {hasRegularAllDayEvents && (
984
+ <span
985
+ className="absolute right-2 text-[10px] text-muted-foreground"
986
+ style={{ top: `${regularAllDayLaneOffset + 4}px` }}
987
+ >
988
+ {t("eventForm.allDay")}
989
+ </span>
990
+ )}
1017
991
  </div>
1018
992
 
1019
993
  {/* All-day columns container (relative, for absolute-positioned spans) */}
@@ -1029,70 +1003,207 @@ export const WeekView = memo(function WeekView({
1029
1003
  />
1030
1004
  ))}
1031
1005
 
1032
- {/* Spanning all-day event bars */}
1033
- {allDaySpans.map(({ event, startCol, endCol }) => {
1034
- const color = getEventDisplayColor(event, prefs);
1035
- const rowIdx = allDayRowAssignments.get(event.id) ?? 0;
1036
- const colCount = days.length;
1037
- const leftPct = (startCol / colCount) * 100;
1038
- const widthPct = ((endCol - startCol + 1) / colCount) * 100;
1039
-
1040
- return (
1041
- <EventDetailPopover
1042
- key={event.id}
1043
- event={event}
1044
- onDelete={onDeleteEvent}
1045
- isDraft={draftEventIds.includes(event.id)}
1046
- defaultOpen={quickEditEventId === event.id}
1047
- onTitleSave={onQuickEditSave}
1048
- onDismissNew={onQuickEditCancel}
1049
- onDraftUpdate={onDraftUpdate}
1050
- onDraftCreate={onDraftCreate}
1051
- onDraftDiscard={onDraftDiscard}
1052
- >
1053
- <button
1054
- className={cn(
1055
- "absolute flex items-center gap-1 truncate rounded px-1.5 text-left text-[11px] font-medium text-foreground transition-opacity hover:opacity-80",
1056
- event.ownerColor && "pr-3.5",
1057
- )}
1058
- aria-label={
1059
- event.ownerName || event.overlayEmail
1060
- ? `${event.title}, ${
1061
- event.ownerName || event.overlayEmail
1062
- }'s calendar`
1063
- : event.title
1064
- }
1065
- style={{
1066
- top: `${rowIdx * allDayRowHeight + 4}px`,
1067
- left: `${leftPct}%`,
1068
- width: `calc(${widthPct}% - 4px)`,
1069
- height: `${allDayRowHeight - 4}px`,
1070
- backgroundColor: color
1071
- ? `${color}30`
1072
- : "hsl(var(--primary) / 0.15)",
1073
- borderLeft: `3px solid ${color ?? "hsl(var(--primary))"}`,
1074
- marginLeft: "2px",
1075
- }}
1076
- >
1077
- {allOtherDeclined(event) && (
1078
- <IconAlertTriangleFilled
1079
- size={10}
1080
- className="shrink-0 text-current opacity-70"
1081
- />
1082
- )}
1083
- <EventStatusIcon event={event} className="shrink-0" />
1084
- <span className="truncate">{event.title}</span>
1085
- {event.ownerColor && (
1086
- <span
1087
- aria-hidden="true"
1088
- className="absolute right-1 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
1089
- style={{ backgroundColor: event.ownerColor }}
1090
- />
1091
- )}
1092
- </button>
1093
- </EventDetailPopover>
1094
- );
1095
- })}
1006
+ {laneSeparatorHeight > 0 && (
1007
+ <div
1008
+ aria-hidden="true"
1009
+ className="absolute inset-x-0 border-t border-border/60"
1010
+ style={{ top: `${workingLocationLaneHeight}px` }}
1011
+ />
1012
+ )}
1013
+
1014
+ <div data-working-location-lane className="contents">
1015
+ {workingLocationGroups.map((group) => {
1016
+ const firstPlacement = group[0];
1017
+ const lastPlacement = group[group.length - 1];
1018
+ const groupKey = group.map(({ event }) => event.id).join(":");
1019
+ const colCount = days.length;
1020
+ const groupLeftPct =
1021
+ (firstPlacement.startCol / colCount) * 100;
1022
+ const groupWidthPct =
1023
+ ((lastPlacement.endCol - firstPlacement.startCol + 1) /
1024
+ colCount) *
1025
+ 100;
1026
+ const groupColor = getEventDisplayColor(
1027
+ firstPlacement.event,
1028
+ prefs,
1029
+ );
1030
+
1031
+ return (
1032
+ <div key={groupKey} className="contents">
1033
+ <div
1034
+ aria-hidden="true"
1035
+ className="pointer-events-none absolute rounded-full opacity-35"
1036
+ style={{
1037
+ top: `${firstPlacement.row * workingLocationRowHeight + 7}px`,
1038
+ left: `calc(${groupLeftPct}% + 4px)`,
1039
+ width: `calc(${groupWidthPct}% - 8px)`,
1040
+ height: "3px",
1041
+ backgroundColor: groupColor,
1042
+ }}
1043
+ />
1044
+ {group.map(({ event, startCol, endCol, row }, index) => {
1045
+ const colCount = days.length;
1046
+ const leftPct = (startCol / colCount) * 100;
1047
+ const widthPct =
1048
+ ((endCol - startCol + 1) / colCount) * 100;
1049
+ const title = getWorkingLocationChipLabel(
1050
+ event,
1051
+ workingLocationLabels,
1052
+ );
1053
+ const ariaTitle = getWorkingLocationTitle(
1054
+ event,
1055
+ workingLocationLabels,
1056
+ );
1057
+ const WorkingLocationIcon =
1058
+ event.workingLocationProperties?.type === "homeOffice"
1059
+ ? IconHome
1060
+ : event.workingLocationProperties?.type ===
1061
+ "officeLocation"
1062
+ ? IconBuilding
1063
+ : IconMapPin;
1064
+
1065
+ return (
1066
+ <EventDetailPopover
1067
+ key={`${event.overlayEmail ?? event.accountEmail ?? "primary"}:${event.id}`}
1068
+ event={event}
1069
+ onDelete={onDeleteEvent}
1070
+ isDraft={draftEventIds.includes(event.id)}
1071
+ defaultOpen={quickEditEventId === event.id}
1072
+ onTitleSave={onQuickEditSave}
1073
+ onDismissNew={onQuickEditCancel}
1074
+ onDraftUpdate={onDraftUpdate}
1075
+ onDraftCreate={onDraftCreate}
1076
+ onDraftDiscard={onDraftDiscard}
1077
+ >
1078
+ <button
1079
+ className={cn(
1080
+ "group/working-location-day absolute z-10 flex items-center px-1 text-left outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1",
1081
+ )}
1082
+ aria-label={
1083
+ event.ownerName || event.overlayEmail
1084
+ ? `${ariaTitle}, ${
1085
+ event.ownerName || event.overlayEmail
1086
+ }'s calendar`
1087
+ : ariaTitle
1088
+ }
1089
+ style={{
1090
+ top: `${row * workingLocationRowHeight + 1}px`,
1091
+ left: `${leftPct}%`,
1092
+ width: `${widthPct}%`,
1093
+ height: `${workingLocationRowHeight - 2}px`,
1094
+ }}
1095
+ >
1096
+ <span
1097
+ aria-hidden="true"
1098
+ className="pointer-events-none absolute inset-x-0.5 inset-y-0 rounded-sm opacity-0 transition-opacity group-hover/working-location-day:opacity-100"
1099
+ style={{
1100
+ backgroundColor: `color-mix(in srgb, ${groupColor} 14%, transparent)`,
1101
+ boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${groupColor} 22%, transparent)`,
1102
+ }}
1103
+ />
1104
+ {index === 0 && (
1105
+ <span
1106
+ className="relative inline-flex h-3.5 max-w-full items-center gap-0.5 rounded-sm px-1 text-[10px] font-medium leading-none text-foreground"
1107
+ style={{
1108
+ backgroundColor: `color-mix(in srgb, ${groupColor} 18%, hsl(var(--background)))`,
1109
+ boxShadow: `0 0 0 1px color-mix(in srgb, ${groupColor} 28%, transparent)`,
1110
+ }}
1111
+ >
1112
+ <WorkingLocationIcon
1113
+ aria-hidden="true"
1114
+ className="size-2.5 shrink-0"
1115
+ style={{ color: groupColor }}
1116
+ />
1117
+ <span className="truncate">{title}</span>
1118
+ </span>
1119
+ )}
1120
+ </button>
1121
+ </EventDetailPopover>
1122
+ );
1123
+ })}
1124
+ </div>
1125
+ );
1126
+ })}
1127
+ </div>
1128
+
1129
+ <div data-all-day-event-lane className="contents">
1130
+ {regularAllDayLayout.placements.map(
1131
+ ({ event, startCol, endCol, row }) => {
1132
+ const color = getEventDisplayColor(event, prefs);
1133
+ const colCount = days.length;
1134
+ const leftPct = (startCol / colCount) * 100;
1135
+ const widthPct = ((endCol - startCol + 1) / colCount) * 100;
1136
+ const title = getWorkingLocationChipLabel(
1137
+ event,
1138
+ workingLocationLabels,
1139
+ );
1140
+ const ariaTitle = getWorkingLocationTitle(
1141
+ event,
1142
+ workingLocationLabels,
1143
+ );
1144
+
1145
+ return (
1146
+ <EventDetailPopover
1147
+ key={`${event.overlayEmail ?? event.accountEmail ?? "primary"}:${event.id}`}
1148
+ event={event}
1149
+ onDelete={onDeleteEvent}
1150
+ isDraft={draftEventIds.includes(event.id)}
1151
+ defaultOpen={quickEditEventId === event.id}
1152
+ onTitleSave={onQuickEditSave}
1153
+ onDismissNew={onQuickEditCancel}
1154
+ onDraftUpdate={onDraftUpdate}
1155
+ onDraftCreate={onDraftCreate}
1156
+ onDraftDiscard={onDraftDiscard}
1157
+ >
1158
+ <button
1159
+ className={cn(
1160
+ "absolute flex items-center gap-1 truncate rounded px-1.5 text-left text-[11px] font-medium text-foreground transition-opacity hover:opacity-80",
1161
+ event.ownerColor && "pr-3.5",
1162
+ )}
1163
+ aria-label={
1164
+ event.ownerName || event.overlayEmail
1165
+ ? `${ariaTitle}, ${
1166
+ event.ownerName || event.overlayEmail
1167
+ }'s calendar`
1168
+ : ariaTitle
1169
+ }
1170
+ style={{
1171
+ top: `${
1172
+ regularAllDayLaneOffset +
1173
+ row * allDayRowHeight +
1174
+ 4
1175
+ }px`,
1176
+ left: `${leftPct}%`,
1177
+ width: `calc(${widthPct}% - 4px)`,
1178
+ height: `${allDayRowHeight - 4}px`,
1179
+ backgroundColor: color
1180
+ ? `${color}30`
1181
+ : "hsl(var(--primary) / 0.15)",
1182
+ borderLeft: `3px solid ${color ?? "hsl(var(--primary))"}`,
1183
+ marginLeft: "2px",
1184
+ }}
1185
+ >
1186
+ {allOtherDeclined(event) && (
1187
+ <IconAlertTriangleFilled
1188
+ size={10}
1189
+ className="shrink-0 text-current opacity-70"
1190
+ />
1191
+ )}
1192
+ <EventStatusIcon event={event} className="shrink-0" />
1193
+ <span className="truncate">{title}</span>
1194
+ {event.ownerColor && (
1195
+ <span
1196
+ aria-hidden="true"
1197
+ className="absolute right-1 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
1198
+ style={{ backgroundColor: event.ownerColor }}
1199
+ />
1200
+ )}
1201
+ </button>
1202
+ </EventDetailPopover>
1203
+ );
1204
+ },
1205
+ )}
1206
+ </div>
1096
1207
  </div>
1097
1208
  {allDayHeaderSpacerWidth > 0 && (
1098
1209
  <div