@open-mercato/core 0.6.8-develop.6977.1.5041da373e → 0.6.8-develop.6982.1.55222946d6
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.
- package/dist/modules/customers/components/calendar/AgendaList.js +4 -6
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js +2 -4
- package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
- package/dist/modules/customers/lib/calendar/format.js +4 -0
- package/dist/modules/customers/lib/calendar/format.js.map +2 -2
- package/dist/modules/dashboards/lib/widgets.js +4 -0
- package/dist/modules/dashboards/lib/widgets.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/components/calendar/AgendaList.tsx +4 -7
- package/src/modules/customers/components/calendar/MonthGrid.tsx +2 -5
- package/src/modules/customers/lib/calendar/format.ts +4 -0
- package/src/modules/dashboards/lib/widgets.ts +13 -0
|
@@ -11,6 +11,7 @@ import { cn } from "@open-mercato/shared/lib/utils";
|
|
|
11
11
|
import { Avatar } from "@open-mercato/ui/primitives/avatar";
|
|
12
12
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
13
13
|
import { eventDisplayTitle, pluralCategory } from "../../lib/calendar/labels.js";
|
|
14
|
+
import { formatTimeLabel, formatTimeRangeLabel } from "../../lib/calendar/format.js";
|
|
14
15
|
const MAX_AVATARS_PER_ROW = 2;
|
|
15
16
|
const CATEGORY_BADGE_CLASS = {
|
|
16
17
|
meeting: "bg-status-info-bg text-status-info-text",
|
|
@@ -35,9 +36,6 @@ function formatUrlHost(location) {
|
|
|
35
36
|
return location;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
function formatTime(locale, date) {
|
|
39
|
-
return date.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit" });
|
|
40
|
-
}
|
|
41
39
|
function groupLabelOf(locale, date) {
|
|
42
40
|
return date.toLocaleDateString(locale, { weekday: "long", month: "short", day: "numeric" });
|
|
43
41
|
}
|
|
@@ -100,9 +98,9 @@ function AgendaRow({
|
|
|
100
98
|
teams: t("customers.calendar.platformFull.teams", "Microsoft Teams")
|
|
101
99
|
};
|
|
102
100
|
const locationLabel = item.platform ? platformLabels[item.platform] : item.locationKind === "url" && item.location ? formatUrlHost(item.location) : item.location;
|
|
103
|
-
const startLabel = item.allDay ? t("customers.calendar.grid.allDay", "All day") :
|
|
104
|
-
const endLabel = item.allDay ? null :
|
|
105
|
-
const ariaTime = item.allDay ? startLabel :
|
|
101
|
+
const startLabel = item.allDay ? t("customers.calendar.grid.allDay", "All day") : formatTimeLabel(locale, item.start);
|
|
102
|
+
const endLabel = item.allDay ? null : formatTimeLabel(locale, item.end);
|
|
103
|
+
const ariaTime = item.allDay ? startLabel : formatTimeRangeLabel(locale, item.start, item.end);
|
|
106
104
|
return /* @__PURE__ */ jsxs(
|
|
107
105
|
Button,
|
|
108
106
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/calendar/AgendaList.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { addDays } from 'date-fns/addDays'\nimport { format } from 'date-fns/format'\nimport { isToday } from 'date-fns/isToday'\nimport { isTomorrow } from 'date-fns/isTomorrow'\nimport { startOfDay } from 'date-fns/startOfDay'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { eventDisplayTitle, pluralCategory } from '../../lib/calendar/labels'\nimport type { AgendaListProps, CalendarCategory, CalendarItem } from './types'\n\nconst MAX_AVATARS_PER_ROW = 2\n\nconst CATEGORY_BADGE_CLASS: Record<CalendarCategory, string> = {\n meeting: 'bg-status-info-bg text-status-info-text',\n event: 'bg-status-warning-bg text-status-warning-text',\n task: 'bg-primary text-primary-foreground',\n other: 'bg-status-neutral-bg text-status-neutral-text',\n}\n\nconst CATEGORY_LABEL: Record<CalendarCategory, { key: string; fallback: string }> = {\n meeting: { key: 'customers.calendar.category.meeting', fallback: 'Meeting' },\n event: { key: 'customers.calendar.category.event', fallback: 'Event' },\n task: { key: 'customers.calendar.category.task', fallback: 'Task' },\n other: { key: 'customers.calendar.category.other', fallback: 'Other' },\n}\n\ntype AgendaDayGroup = { date: Date; items: CalendarItem[] }\n\nfunction dayKeyOf(date: Date): string {\n return format(date, 'yyyy-MM-dd')\n}\n\nfunction formatUrlHost(location: string): string {\n try {\n const url = new URL(location.startsWith('http') ? location : `https://${location}`)\n return url.hostname\n } catch {\n return location\n }\n}\n\nfunction
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { addDays } from 'date-fns/addDays'\nimport { format } from 'date-fns/format'\nimport { isToday } from 'date-fns/isToday'\nimport { isTomorrow } from 'date-fns/isTomorrow'\nimport { startOfDay } from 'date-fns/startOfDay'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { eventDisplayTitle, pluralCategory } from '../../lib/calendar/labels'\nimport { formatTimeLabel, formatTimeRangeLabel } from '../../lib/calendar/format'\nimport type { AgendaListProps, CalendarCategory, CalendarItem } from './types'\n\nconst MAX_AVATARS_PER_ROW = 2\n\nconst CATEGORY_BADGE_CLASS: Record<CalendarCategory, string> = {\n meeting: 'bg-status-info-bg text-status-info-text',\n event: 'bg-status-warning-bg text-status-warning-text',\n task: 'bg-primary text-primary-foreground',\n other: 'bg-status-neutral-bg text-status-neutral-text',\n}\n\nconst CATEGORY_LABEL: Record<CalendarCategory, { key: string; fallback: string }> = {\n meeting: { key: 'customers.calendar.category.meeting', fallback: 'Meeting' },\n event: { key: 'customers.calendar.category.event', fallback: 'Event' },\n task: { key: 'customers.calendar.category.task', fallback: 'Task' },\n other: { key: 'customers.calendar.category.other', fallback: 'Other' },\n}\n\ntype AgendaDayGroup = { date: Date; items: CalendarItem[] }\n\nfunction dayKeyOf(date: Date): string {\n return format(date, 'yyyy-MM-dd')\n}\n\nfunction formatUrlHost(location: string): string {\n try {\n const url = new URL(location.startsWith('http') ? location : `https://${location}`)\n return url.hostname\n } catch {\n return location\n }\n}\n\nfunction groupLabelOf(locale: string, date: Date): string {\n return date.toLocaleDateString(locale, { weekday: 'long', month: 'short', day: 'numeric' })\n}\n\nfunction deriveTypeLabel(interactionType: string): string {\n const normalized = interactionType.replace(/[-_]+/g, ' ').trim()\n if (!normalized) return interactionType\n return normalized.charAt(0).toUpperCase() + normalized.slice(1)\n}\n\nfunction participantLabel(participant: CalendarItem['participants'][number]): string {\n return participant.name ?? participant.email ?? participant.userId\n}\n\nfunction buildDayGroups(anchor: Date, horizonDays: number, items: CalendarItem[]): AgendaDayGroup[] {\n const sorted = [...items].sort((first, second) => first.start.getTime() - second.start.getTime())\n const byDay = new Map<string, CalendarItem[]>()\n for (const item of sorted) {\n const key = dayKeyOf(item.start)\n const bucket = byDay.get(key)\n if (bucket) bucket.push(item)\n else byDay.set(key, [item])\n }\n const firstDay = startOfDay(anchor)\n const groups: AgendaDayGroup[] = []\n for (let offset = 0; offset <= horizonDays; offset += 1) {\n const day = addDays(firstDay, offset)\n const dayItems = byDay.get(dayKeyOf(day))\n if (dayItems && dayItems.length > 0) groups.push({ date: day, items: dayItems })\n }\n return groups\n}\n\nfunction AgendaDayHeader({ date, count }: { date: Date; count: number }) {\n const t = useT()\n const locale = useLocale()\n const todayMarker = isToday(date)\n const tomorrowMarker = !todayMarker && isTomorrow(date)\n const countKey = `customers.calendar.agenda.eventsCount.${pluralCategory(locale, count)}`\n const resolvedCount = t(countKey, { count })\n const countLabel =\n resolvedCount === countKey\n ? t('customers.calendar.agenda.eventsCount.other', '{count} events', { count })\n : resolvedCount\n return (\n <div className=\"flex w-full items-center gap-2 bg-muted/50 px-3 py-2.5 sm:px-5\">\n <span className=\"text-sm font-semibold text-foreground\">{groupLabelOf(locale, date)}</span>\n {todayMarker || tomorrowMarker ? (\n <span className={cn('text-xs font-medium', todayMarker ? 'text-foreground' : 'text-muted-foreground')}>\n {`\u00B7 ${todayMarker ? t('customers.calendar.agenda.today', 'Today') : t('customers.calendar.agenda.tomorrow', 'Tomorrow')}`}\n </span>\n ) : null}\n <span aria-hidden=\"true\" className=\"w-2.5 shrink-0\" />\n <span className=\"text-xs font-medium text-muted-foreground\">{countLabel}</span>\n </div>\n )\n}\n\nfunction AgendaRow({\n item,\n typeLabels,\n onItemClick,\n}: {\n item: CalendarItem\n typeLabels?: Record<string, string>\n onItemClick: (item: CalendarItem) => void\n}) {\n const t = useT()\n const locale = useLocale()\n const canceled = item.status === 'canceled'\n const done = item.status === 'done'\n const title = eventDisplayTitle(item.title, t('customers.calendar.grid.untitled', 'Untitled'))\n const typeLabel = typeLabels?.[item.interactionType] ?? deriveTypeLabel(item.interactionType)\n const platformLabels: Record<string, string> = {\n zoom: t('customers.calendar.platformFull.zoom', 'Zoom'),\n meet: t('customers.calendar.platformFull.meet', 'Google Meet'),\n slack: t('customers.calendar.platformFull.slack', 'Slack'),\n teams: t('customers.calendar.platformFull.teams', 'Microsoft Teams'),\n }\n const locationLabel = item.platform\n ? platformLabels[item.platform]\n : item.locationKind === 'url' && item.location\n ? formatUrlHost(item.location)\n : item.location\n const startLabel = item.allDay ? t('customers.calendar.grid.allDay', 'All day') : formatTimeLabel(locale, item.start)\n const endLabel = item.allDay ? null : formatTimeLabel(locale, item.end)\n const ariaTime = item.allDay ? startLabel : formatTimeRangeLabel(locale, item.start, item.end)\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n aria-label={`${title} \u00B7 ${ariaTime}`}\n onClick={() => onItemClick(item)}\n className={cn(\n 'h-auto w-full justify-start whitespace-normal rounded-none bg-background px-3 py-3 text-left transition-colors hover:bg-muted/30 sm:gap-3.5 sm:px-5',\n canceled && 'opacity-60',\n )}\n >\n <div className=\"flex w-16 shrink-0 flex-col gap-0.5 sm:w-[86px]\">\n <span className={cn('truncate text-sm font-semibold leading-4', done || canceled ? 'text-muted-foreground' : 'text-foreground')}>\n {startLabel}\n </span>\n {endLabel ? <span className=\"truncate text-xs text-muted-foreground\">{endLabel}</span> : null}\n </div>\n <span\n aria-hidden=\"true\"\n className={cn('h-9 w-1 shrink-0 rounded-full', !item.color && 'bg-muted-foreground/40')}\n style={item.color ? { backgroundColor: item.color } : undefined}\n />\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span\n className={cn(\n 'truncate text-sm font-semibold',\n done || canceled ? 'text-muted-foreground' : 'text-foreground',\n canceled && 'line-through',\n )}\n >\n {title}\n </span>\n <span className=\"truncate text-xs text-muted-foreground\">\n {locationLabel ? `${typeLabel} \u00B7 ${locationLabel}` : typeLabel}\n </span>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {item.participants.length > 0 ? (\n <span className=\"hidden items-center gap-0.5 sm:flex\">\n {item.participants.slice(0, MAX_AVATARS_PER_ROW).map((participant) => (\n <Avatar key={participant.userId} size=\"xs\" label={participantLabel(participant)} />\n ))}\n {item.participants.length > MAX_AVATARS_PER_ROW ? (\n <span className=\"ps-0.5 text-xs font-medium text-muted-foreground\">\n +{item.participants.length - MAX_AVATARS_PER_ROW}\n </span>\n ) : null}\n </span>\n ) : null}\n <span\n className={cn(\n 'inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-overline font-medium uppercase tracking-wide',\n CATEGORY_BADGE_CLASS[item.category],\n )}\n >\n {t(CATEGORY_LABEL[item.category].key, CATEGORY_LABEL[item.category].fallback)}\n </span>\n </div>\n </Button>\n )\n}\n\nexport function AgendaList({ anchor, horizonDays, items, typeLabels, onItemClick }: AgendaListProps) {\n const t = useT()\n const groups = React.useMemo(() => buildDayGroups(anchor, horizonDays, items), [anchor, horizonDays, items])\n return (\n <div className=\"flex w-full flex-col divide-y divide-border overflow-hidden border border-border bg-background\">\n {groups.map((group) => (\n <React.Fragment key={dayKeyOf(group.date)}>\n <AgendaDayHeader date={group.date} count={group.items.length} />\n {group.items.map((item) => (\n <AgendaRow key={item.id} item={item} typeLabels={typeLabels} onItemClick={onItemClick} />\n ))}\n </React.Fragment>\n ))}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -10,14 +10,12 @@ import { cn } from "@open-mercato/shared/lib/utils";
|
|
|
10
10
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
11
11
|
import { getVisibleRange } from "../../lib/calendar/range.js";
|
|
12
12
|
import { eventDisplayTitle } from "../../lib/calendar/labels.js";
|
|
13
|
+
import { formatTimeRangeLabel } from "../../lib/calendar/format.js";
|
|
13
14
|
const MAX_PILLS_PER_DAY = 2;
|
|
14
15
|
const SOFT_TINT_ALPHA = "1A";
|
|
15
16
|
function dayKeyOf(date) {
|
|
16
17
|
return format(date, "yyyy-MM-dd");
|
|
17
18
|
}
|
|
18
|
-
function formatTime(locale, date) {
|
|
19
|
-
return date.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit" });
|
|
20
|
-
}
|
|
21
19
|
function fullDateLabel(locale, date) {
|
|
22
20
|
return date.toLocaleDateString(locale, { weekday: "long", year: "numeric", month: "long", day: "numeric" });
|
|
23
21
|
}
|
|
@@ -55,7 +53,7 @@ function MonthPill({ item, onItemClick }) {
|
|
|
55
53
|
const locale = useLocale();
|
|
56
54
|
const canceled = item.status === "canceled";
|
|
57
55
|
const title = eventDisplayTitle(item.title, t("customers.calendar.grid.untitled", "Untitled"));
|
|
58
|
-
const timeLabel = item.allDay ? "" : ` \xB7 ${
|
|
56
|
+
const timeLabel = item.allDay ? "" : ` \xB7 ${formatTimeRangeLabel(locale, item.start, item.end)}`;
|
|
59
57
|
const tintStyle = item.color ? { backgroundColor: `${item.color}${SOFT_TINT_ALPHA}`, color: item.color } : void 0;
|
|
60
58
|
return /* @__PURE__ */ jsxs(
|
|
61
59
|
Button,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/calendar/MonthGrid.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { addDays } from 'date-fns/addDays'\nimport { format } from 'date-fns/format'\nimport { isSameMonth } from 'date-fns/isSameMonth'\nimport { isToday } from 'date-fns/isToday'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { getVisibleRange } from '../../lib/calendar/range'\nimport { eventDisplayTitle } from '../../lib/calendar/labels'\nimport type { CalendarItem, MonthGridProps } from './types'\n\nconst MAX_PILLS_PER_DAY = 2\nconst SOFT_TINT_ALPHA = '1A'\n\nfunction dayKeyOf(date: Date): string {\n return format(date, 'yyyy-MM-dd')\n}\n\nfunction
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { addDays } from 'date-fns/addDays'\nimport { format } from 'date-fns/format'\nimport { isSameMonth } from 'date-fns/isSameMonth'\nimport { isToday } from 'date-fns/isToday'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { getVisibleRange } from '../../lib/calendar/range'\nimport { eventDisplayTitle } from '../../lib/calendar/labels'\nimport { formatTimeRangeLabel } from '../../lib/calendar/format'\nimport type { CalendarItem, MonthGridProps } from './types'\n\nconst MAX_PILLS_PER_DAY = 2\nconst SOFT_TINT_ALPHA = '1A'\n\nfunction dayKeyOf(date: Date): string {\n return format(date, 'yyyy-MM-dd')\n}\n\nfunction fullDateLabel(locale: string, date: Date): string {\n return date.toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })\n}\n\nfunction isWeekend(date: Date): boolean {\n const weekday = date.getDay()\n return weekday === 0 || weekday === 6\n}\n\nfunction buildWeeks(anchor: Date): Date[][] {\n const range = getVisibleRange('month', anchor, 0)\n const weeks: Date[][] = []\n let cursor = range.from\n while (cursor.getTime() <= range.to.getTime()) {\n const week: Date[] = []\n for (let dayIndex = 0; dayIndex < 7; dayIndex += 1) {\n week.push(cursor)\n cursor = addDays(cursor, 1)\n }\n weeks.push(week)\n }\n return weeks\n}\n\nfunction groupItemsByDay(items: CalendarItem[]): Map<string, CalendarItem[]> {\n const sorted = [...items].sort((first, second) => first.start.getTime() - second.start.getTime())\n const byDay = new Map<string, CalendarItem[]>()\n for (const item of sorted) {\n const key = dayKeyOf(item.start)\n const bucket = byDay.get(key)\n if (bucket) bucket.push(item)\n else byDay.set(key, [item])\n }\n return byDay\n}\n\nfunction MonthPill({ item, onItemClick }: { item: CalendarItem; onItemClick: (item: CalendarItem) => void }) {\n const t = useT()\n const locale = useLocale()\n const canceled = item.status === 'canceled'\n const title = eventDisplayTitle(item.title, t('customers.calendar.grid.untitled', 'Untitled'))\n const timeLabel = item.allDay ? '' : ` \u00B7 ${formatTimeRangeLabel(locale, item.start, item.end)}`\n const tintStyle = item.color\n ? { backgroundColor: `${item.color}${SOFT_TINT_ALPHA}`, color: item.color }\n : undefined\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n aria-label={`${title}${timeLabel}`}\n onClick={(event) => {\n event.stopPropagation()\n onItemClick(item)\n }}\n className={cn(\n 'h-4 w-full min-w-0 justify-center gap-0 rounded-full p-0 pr-2',\n !item.color && 'bg-muted text-muted-foreground',\n canceled && 'opacity-60',\n )}\n style={tintStyle}\n >\n <span aria-hidden=\"true\" className=\"flex size-4 shrink-0 items-center justify-center\">\n <span\n className={cn('size-1.5 rounded-full', !item.color && 'bg-muted-foreground')}\n style={item.color ? { backgroundColor: item.color } : undefined}\n />\n </span>\n <span\n className={cn(\n 'min-w-0 truncate text-overline font-medium uppercase tracking-wide',\n canceled && 'line-through',\n )}\n >\n {title}\n </span>\n </Button>\n )\n}\n\nfunction MonthDayCell({\n day,\n anchor,\n items,\n onItemClick,\n onDayOpen,\n}: {\n day: Date\n anchor: Date\n items: CalendarItem[]\n onItemClick: (item: CalendarItem) => void\n onDayOpen: (date: Date) => void\n}) {\n const t = useT()\n const locale = useLocale()\n const inMonth = isSameMonth(day, anchor)\n const today = isToday(day)\n const visibleItems = items.slice(0, MAX_PILLS_PER_DAY)\n const hiddenCount = items.length - visibleItems.length\n const dayLabel = fullDateLabel(locale, day)\n return (\n <div\n className={cn(\n 'relative flex h-full min-w-0 flex-1 flex-col items-start gap-1 overflow-hidden border-r border-border p-2 text-left last:border-r-0',\n isWeekend(day) ? 'bg-muted/40' : 'bg-background',\n )}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n aria-label={dayLabel}\n onClick={() => onDayOpen(day)}\n className=\"absolute inset-0 z-0 h-full w-full rounded-none p-0 hover:bg-muted/50\"\n >\n <span className=\"sr-only\">{dayLabel}</span>\n </Button>\n {today ? (\n <span className=\"pointer-events-none relative z-10 flex items-center justify-center rounded-full bg-primary px-1.5 py-0.5 text-xs font-semibold text-primary-foreground\">\n {day.getDate()}\n </span>\n ) : (\n <span className={cn('pointer-events-none relative z-10 text-xs font-medium', inMonth ? 'text-foreground' : 'text-muted-foreground')}>\n {day.getDate()}\n </span>\n )}\n <span className=\"relative z-20 hidden w-full flex-col gap-1 sm:flex\">\n {visibleItems.map((item) => (\n <MonthPill key={item.id} item={item} onItemClick={onItemClick} />\n ))}\n {hiddenCount > 0 ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto self-start p-0 text-overline font-medium text-muted-foreground hover:bg-transparent hover:underline\"\n onClick={(event) => {\n event.stopPropagation()\n onDayOpen(day)\n }}\n >\n {t('customers.calendar.grid.more', '+{count} more', { count: hiddenCount })}\n </Button>\n ) : null}\n </span>\n {items.length > 0 ? (\n <span aria-hidden=\"true\" className=\"pointer-events-none relative z-10 flex w-full flex-wrap items-center gap-0.5 sm:hidden\">\n {items.slice(0, 4).map((item) => (\n <span\n key={item.id}\n className={cn('size-1.5 rounded-full', !item.color && 'bg-muted-foreground')}\n style={item.color ? { backgroundColor: item.color } : undefined}\n />\n ))}\n {items.length > 4 ? (\n <span className=\"text-overline font-medium leading-none text-muted-foreground\">\n +{items.length - 4}\n </span>\n ) : null}\n </span>\n ) : null}\n </div>\n )\n}\n\nexport function MonthGrid({ anchor, items, onItemClick, onDayOpen }: MonthGridProps) {\n const t = useT()\n const locale = useLocale()\n const weeks = React.useMemo(() => buildWeeks(anchor), [anchor])\n const itemsByDay = React.useMemo(() => groupItemsByDay(items), [items])\n return (\n <div className=\"relative flex h-full w-full flex-col overflow-hidden border border-border bg-background\">\n <div className=\"flex h-9 w-full shrink-0 border-b border-border\">\n {weeks[0]?.map((day) => {\n const label = day.toLocaleDateString(locale, { weekday: 'short' }).toLocaleUpperCase(locale)\n return (\n <div\n key={dayKeyOf(day)}\n className=\"flex min-w-0 flex-1 items-center justify-center border-r border-border last:border-r-0 sm:justify-start sm:pl-2.5\"\n >\n <span className=\"truncate text-overline font-medium uppercase tracking-widest text-muted-foreground\">\n <span className=\"sm:hidden\">{label.charAt(0)}</span>\n <span className=\"hidden sm:inline\">{label}</span>\n </span>\n </div>\n )\n })}\n </div>\n {weeks.map((week) => (\n <div\n key={dayKeyOf(week[0])}\n className=\"flex w-full flex-1 border-b border-border last:border-b-0 min-h-14 sm:min-h-[86px]\"\n >\n {week.map((day) => (\n <MonthDayCell\n key={dayKeyOf(day)}\n day={day}\n anchor={anchor}\n items={itemsByDay.get(dayKeyOf(day)) ?? []}\n onItemClick={onItemClick}\n onDayOpen={onDayOpen}\n />\n ))}\n </div>\n ))}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -15,6 +15,9 @@ function formatDateRangeLabel(locale, from, to) {
|
|
|
15
15
|
return normalizeIntlSpacing(`${formatter.format(from)} \u2013 ${formatter.format(to)}`);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
function formatTimeLabel(locale, date) {
|
|
19
|
+
return normalizeIntlSpacing(new Intl.DateTimeFormat(locale, TIME_OPTIONS).format(date));
|
|
20
|
+
}
|
|
18
21
|
function formatTimeRangeLabel(locale, start, end) {
|
|
19
22
|
const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS);
|
|
20
23
|
try {
|
|
@@ -26,6 +29,7 @@ function formatTimeRangeLabel(locale, start, end) {
|
|
|
26
29
|
export {
|
|
27
30
|
formatDateLabel,
|
|
28
31
|
formatDateRangeLabel,
|
|
32
|
+
formatTimeLabel,
|
|
29
33
|
formatTimeRangeLabel
|
|
30
34
|
};
|
|
31
35
|
//# sourceMappingURL=format.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/lib/calendar/format.ts"],
|
|
4
|
-
"sourcesContent": ["const DATE_OPTIONS: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }\n\nconst TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit' }\n\nconst NON_CANONICAL_INTL_SPACING = /[\\u00a0\\u2007\\u2009\\u202f]/g\n\nfunction normalizeIntlSpacing(value: string): string {\n return value.replace(NON_CANONICAL_INTL_SPACING, ' ')\n}\n\nexport function formatDateLabel(locale: string, date: Date): string {\n return new Intl.DateTimeFormat(locale, DATE_OPTIONS).format(date)\n}\n\nexport function formatDateRangeLabel(locale: string, from: Date, to: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, DATE_OPTIONS)\n try {\n return normalizeIntlSpacing(formatter.formatRange(from, to))\n } catch {\n return normalizeIntlSpacing(`${formatter.format(from)} \u2013 ${formatter.format(to)}`)\n }\n}\n\nexport function formatTimeRangeLabel(locale: string, start: Date, end: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS)\n try {\n return normalizeIntlSpacing(formatter.formatRange(start, end))\n } catch {\n return normalizeIntlSpacing(`${formatter.format(start)} \u2013 ${formatter.format(end)}`)\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,MAAM,eAA2C,EAAE,KAAK,WAAW,OAAO,SAAS,MAAM,UAAU;AAEnG,MAAM,eAA2C,EAAE,MAAM,WAAW,QAAQ,UAAU;AAEtF,MAAM,6BAA6B;AAEnC,SAAS,qBAAqB,OAAuB;AACnD,SAAO,MAAM,QAAQ,4BAA4B,GAAG;AACtD;AAEO,SAAS,gBAAgB,QAAgB,MAAoB;AAClE,SAAO,IAAI,KAAK,eAAe,QAAQ,YAAY,EAAE,OAAO,IAAI;AAClE;AAEO,SAAS,qBAAqB,QAAgB,MAAY,IAAkB;AACjF,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,YAAY;AAC9D,MAAI;AACF,WAAO,qBAAqB,UAAU,YAAY,MAAM,EAAE,CAAC;AAAA,EAC7D,QAAQ;AACN,WAAO,qBAAqB,GAAG,UAAU,OAAO,IAAI,CAAC,WAAM,UAAU,OAAO,EAAE,CAAC,EAAE;AAAA,EACnF;AACF;AAEO,SAAS,qBAAqB,QAAgB,OAAa,KAAmB;AACnF,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,YAAY;AAC9D,MAAI;AACF,WAAO,qBAAqB,UAAU,YAAY,OAAO,GAAG,CAAC;AAAA,EAC/D,QAAQ;AACN,WAAO,qBAAqB,GAAG,UAAU,OAAO,KAAK,CAAC,WAAM,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,EACrF;AACF;",
|
|
4
|
+
"sourcesContent": ["const DATE_OPTIONS: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }\n\nconst TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit' }\n\nconst NON_CANONICAL_INTL_SPACING = /[\\u00a0\\u2007\\u2009\\u202f]/g\n\nfunction normalizeIntlSpacing(value: string): string {\n return value.replace(NON_CANONICAL_INTL_SPACING, ' ')\n}\n\nexport function formatDateLabel(locale: string, date: Date): string {\n return new Intl.DateTimeFormat(locale, DATE_OPTIONS).format(date)\n}\n\nexport function formatDateRangeLabel(locale: string, from: Date, to: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, DATE_OPTIONS)\n try {\n return normalizeIntlSpacing(formatter.formatRange(from, to))\n } catch {\n return normalizeIntlSpacing(`${formatter.format(from)} \u2013 ${formatter.format(to)}`)\n }\n}\n\nexport function formatTimeLabel(locale: string, date: Date): string {\n return normalizeIntlSpacing(new Intl.DateTimeFormat(locale, TIME_OPTIONS).format(date))\n}\n\nexport function formatTimeRangeLabel(locale: string, start: Date, end: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS)\n try {\n return normalizeIntlSpacing(formatter.formatRange(start, end))\n } catch {\n return normalizeIntlSpacing(`${formatter.format(start)} \u2013 ${formatter.format(end)}`)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,MAAM,eAA2C,EAAE,KAAK,WAAW,OAAO,SAAS,MAAM,UAAU;AAEnG,MAAM,eAA2C,EAAE,MAAM,WAAW,QAAQ,UAAU;AAEtF,MAAM,6BAA6B;AAEnC,SAAS,qBAAqB,OAAuB;AACnD,SAAO,MAAM,QAAQ,4BAA4B,GAAG;AACtD;AAEO,SAAS,gBAAgB,QAAgB,MAAoB;AAClE,SAAO,IAAI,KAAK,eAAe,QAAQ,YAAY,EAAE,OAAO,IAAI;AAClE;AAEO,SAAS,qBAAqB,QAAgB,MAAY,IAAkB;AACjF,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,YAAY;AAC9D,MAAI;AACF,WAAO,qBAAqB,UAAU,YAAY,MAAM,EAAE,CAAC;AAAA,EAC7D,QAAQ;AACN,WAAO,qBAAqB,GAAG,UAAU,OAAO,IAAI,CAAC,WAAM,UAAU,OAAO,EAAE,CAAC,EAAE;AAAA,EACnF;AACF;AAEO,SAAS,gBAAgB,QAAgB,MAAoB;AAClE,SAAO,qBAAqB,IAAI,KAAK,eAAe,QAAQ,YAAY,EAAE,OAAO,IAAI,CAAC;AACxF;AAEO,SAAS,qBAAqB,QAAgB,OAAa,KAAmB;AACnF,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,YAAY;AAC9D,MAAI;AACF,WAAO,qBAAqB,UAAU,YAAY,OAAO,GAAG,CAAC;AAAA,EAC/D,QAAQ;AACN,WAAO,qBAAqB,GAAG,UAAU,OAAO,KAAK,CAAC,WAAM,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,EACrF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyDashboardWidgetOverridesToEntries } from "@open-mercato/shared/modules/overrides";
|
|
2
2
|
import { getModules } from "@open-mercato/shared/lib/i18n/server";
|
|
3
|
+
import { onModulesRegistered } from "@open-mercato/shared/lib/modules/registry";
|
|
3
4
|
let widgetEntriesPromise = null;
|
|
4
5
|
function invalidateWidgetCache() {
|
|
5
6
|
widgetEntriesPromise = null;
|
|
@@ -36,6 +37,9 @@ async function loadWidgetEntries() {
|
|
|
36
37
|
return widgetEntriesPromise;
|
|
37
38
|
}
|
|
38
39
|
const widgetCache = /* @__PURE__ */ new Map();
|
|
40
|
+
onModulesRegistered(() => {
|
|
41
|
+
invalidateWidgetCache();
|
|
42
|
+
});
|
|
39
43
|
function ensureValidWidgetModule(mod, key, moduleId) {
|
|
40
44
|
if (!mod || typeof mod !== "object") {
|
|
41
45
|
throw new Error(`Invalid dashboard widget module "${key}" from "${moduleId}" (expected object export)`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dashboards/lib/widgets.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Module, ModuleDashboardWidgetEntry } from '@open-mercato/shared/modules/registry'\nimport type { DashboardWidgetMetadata, DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { applyDashboardWidgetOverridesToEntries } from '@open-mercato/shared/modules/overrides'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\n\ntype LoadedWidgetModule = DashboardWidgetModule<any> & { metadata: DashboardWidgetMetadata }\n\ntype WidgetEntry = ModuleDashboardWidgetEntry & { moduleId: string }\n\nlet widgetEntriesPromise: Promise<WidgetEntry[]> | null = null\n\n/**\n * Invalidate the widget entries and widget module cache.\n * Call this when the generated registry is updated or modules are reloaded.\n */\nexport function invalidateWidgetCache() {\n widgetEntriesPromise = null;\n widgetCache.clear();\n}\n/**\n * An empty resolution means the module registry was not populated yet \u2014 a boot\n * race, not \"this app has no dashboard widgets\". Memoizing it would serve an\n * empty registry for the lifetime of the process, so the entry is dropped and\n * the next call retries. A rejected resolution is dropped for the same reason.\n */\nasync function loadWidgetEntries(): Promise<WidgetEntry[]> {\n if (!widgetEntriesPromise) {\n const pending = Promise.resolve().then(() => {\n const list = getModules() as Module[]\n const entries = list.flatMap((mod) => {\n const moduleEntries = mod.dashboardWidgets ?? []\n return moduleEntries.map((entry) => ({\n ...entry,\n moduleId: mod.id,\n }))\n })\n return applyDashboardWidgetOverridesToEntries(entries) as WidgetEntry[]\n })\n widgetEntriesPromise = pending\n const forgetWhenUnusable = (entries: WidgetEntry[] | null) => {\n if (widgetEntriesPromise === pending && (entries === null || entries.length === 0)) {\n widgetEntriesPromise = null\n }\n }\n try {\n const entries = await pending\n forgetWhenUnusable(entries)\n return entries\n } catch (err) {\n forgetWhenUnusable(null)\n throw err\n }\n }\n return widgetEntriesPromise\n}\n\nconst widgetCache = new Map<string, Promise<LoadedWidgetModule>>()\n\nfunction ensureValidWidgetModule(mod: any, key: string, moduleId: string): LoadedWidgetModule {\n if (!mod || typeof mod !== 'object') {\n throw new Error(`Invalid dashboard widget module \"${key}\" from \"${moduleId}\" (expected object export)`)\n }\n const widget = (mod.default ?? mod) as DashboardWidgetModule<any>\n if (!widget || typeof widget !== 'object') {\n throw new Error(`Invalid dashboard widget export \"${key}\" from \"${moduleId}\" (missing default export)`)\n }\n if (!widget.metadata || typeof widget.metadata !== 'object') {\n throw new Error(`Dashboard widget \"${key}\" from \"${moduleId}\" is missing metadata`)\n }\n const { metadata } = widget\n if (typeof metadata.id !== 'string' || metadata.id.length === 0) {\n throw new Error(`Dashboard widget \"${key}\" from \"${moduleId}\" metadata.id must be a non-empty string`)\n }\n if (typeof metadata.title !== 'string' || metadata.title.length === 0) {\n throw new Error(`Dashboard widget \"${metadata.id}\" from \"${moduleId}\" must have a title`)\n }\n return {\n ...widget,\n metadata,\n }\n}\n\nasync function loadEntry(entry: WidgetEntry): Promise<LoadedWidgetModule> {\n if (!widgetCache.has(entry.key)) {\n const promise = entry.loader()\n .then((mod) => ensureValidWidgetModule(mod, entry.key, entry.moduleId))\n widgetCache.set(entry.key, promise)\n // Same policy as the entries cache above: a rejected resolution is a transient\n // failure (a dynamic import that lost a race), not a permanent fact about the\n // widget. Memoizing it would make loadAllWidgets() reject for the lifetime of the\n // process. The identity check mirrors `widgetEntriesPromise === pending`, so a\n // concurrent invalidateWidgetCache() is never clobbered.\n promise.catch(() => {\n if (widgetCache.get(entry.key) === promise) widgetCache.delete(entry.key)\n })\n }\n return widgetCache.get(entry.key)!\n}\n\nexport async function loadAllWidgets(): Promise<Array<LoadedWidgetModule & { moduleId: string; key: string }>> {\n const widgetEntries = await loadWidgetEntries()\n const loaded = await Promise.all(widgetEntries.map(async (entry) => {\n const widget = await loadEntry(entry)\n return { ...widget, moduleId: entry.moduleId, key: entry.key }\n }))\n const byId = new Map<string, LoadedWidgetModule & { moduleId: string; key: string }>()\n for (const widget of loaded) {\n if (!byId.has(widget.metadata.id)) {\n byId.set(widget.metadata.id, widget)\n }\n }\n return Array.from(byId.values())\n}\n\nexport async function loadWidgetById(widgetId: string): Promise<(LoadedWidgetModule & { moduleId: string; key: string }) | null> {\n const widgetEntries = await loadWidgetEntries()\n for (const entry of widgetEntries) {\n const widget = await loadEntry(entry)\n if (widget.metadata.id === widgetId) {\n return { ...widget, moduleId: entry.moduleId, key: entry.key }\n }\n }\n return null\n}\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,8CAA8C;AACvD,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import type { Module, ModuleDashboardWidgetEntry } from '@open-mercato/shared/modules/registry'\nimport type { DashboardWidgetMetadata, DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { applyDashboardWidgetOverridesToEntries } from '@open-mercato/shared/modules/overrides'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { onModulesRegistered } from '@open-mercato/shared/lib/modules/registry'\n\ntype LoadedWidgetModule = DashboardWidgetModule<any> & { metadata: DashboardWidgetMetadata }\n\ntype WidgetEntry = ModuleDashboardWidgetEntry & { moduleId: string }\n\nlet widgetEntriesPromise: Promise<WidgetEntry[]> | null = null\n\n/**\n * Invalidate the widget entries and widget module cache.\n * Call this when the generated registry is updated or modules are reloaded.\n */\nexport function invalidateWidgetCache() {\n widgetEntriesPromise = null;\n widgetCache.clear();\n}\n\n/**\n * An empty resolution means the module registry was not populated yet \u2014 a boot\n * race, not \"this app has no dashboard widgets\". Memoizing it would serve an\n * empty registry for the lifetime of the process, so the entry is dropped and\n * the next call retries. A rejected resolution is dropped for the same reason.\n */\nasync function loadWidgetEntries(): Promise<WidgetEntry[]> {\n if (!widgetEntriesPromise) {\n const pending = Promise.resolve().then(() => {\n const list = getModules() as Module[]\n const entries = list.flatMap((mod) => {\n const moduleEntries = mod.dashboardWidgets ?? []\n return moduleEntries.map((entry) => ({\n ...entry,\n moduleId: mod.id,\n }))\n })\n return applyDashboardWidgetOverridesToEntries(entries) as WidgetEntry[]\n })\n widgetEntriesPromise = pending\n const forgetWhenUnusable = (entries: WidgetEntry[] | null) => {\n if (widgetEntriesPromise === pending && (entries === null || entries.length === 0)) {\n widgetEntriesPromise = null\n }\n }\n try {\n const entries = await pending\n forgetWhenUnusable(entries)\n return entries\n } catch (err) {\n forgetWhenUnusable(null)\n throw err\n }\n }\n return widgetEntriesPromise\n}\n\nconst widgetCache = new Map<string, Promise<LoadedWidgetModule>>()\n\n// Close the boot race at its source (#5103). Bootstrap can register modules more\n// than once \u2014 an i18n-only registration is later reconciled with the full module\n// list \u2014 and a resolution computed in between describes a registry that no longer\n// exists. The registry notifies only when the registered set actually changed, so\n// a healthy boot costs nothing and no work is added to the request path. Declared\n// after both caches so the subscription can never observe them in their temporal\n// dead zone.\nonModulesRegistered(() => {\n invalidateWidgetCache()\n})\n\nfunction ensureValidWidgetModule(mod: any, key: string, moduleId: string): LoadedWidgetModule {\n if (!mod || typeof mod !== 'object') {\n throw new Error(`Invalid dashboard widget module \"${key}\" from \"${moduleId}\" (expected object export)`)\n }\n const widget = (mod.default ?? mod) as DashboardWidgetModule<any>\n if (!widget || typeof widget !== 'object') {\n throw new Error(`Invalid dashboard widget export \"${key}\" from \"${moduleId}\" (missing default export)`)\n }\n if (!widget.metadata || typeof widget.metadata !== 'object') {\n throw new Error(`Dashboard widget \"${key}\" from \"${moduleId}\" is missing metadata`)\n }\n const { metadata } = widget\n if (typeof metadata.id !== 'string' || metadata.id.length === 0) {\n throw new Error(`Dashboard widget \"${key}\" from \"${moduleId}\" metadata.id must be a non-empty string`)\n }\n if (typeof metadata.title !== 'string' || metadata.title.length === 0) {\n throw new Error(`Dashboard widget \"${metadata.id}\" from \"${moduleId}\" must have a title`)\n }\n return {\n ...widget,\n metadata,\n }\n}\n\nasync function loadEntry(entry: WidgetEntry): Promise<LoadedWidgetModule> {\n if (!widgetCache.has(entry.key)) {\n const promise = entry.loader()\n .then((mod) => ensureValidWidgetModule(mod, entry.key, entry.moduleId))\n widgetCache.set(entry.key, promise)\n // Same policy as the entries cache above: a rejected resolution is a transient\n // failure (a dynamic import that lost a race), not a permanent fact about the\n // widget. Memoizing it would make loadAllWidgets() reject for the lifetime of the\n // process. The identity check mirrors `widgetEntriesPromise === pending`, so a\n // concurrent invalidateWidgetCache() is never clobbered.\n promise.catch(() => {\n if (widgetCache.get(entry.key) === promise) widgetCache.delete(entry.key)\n })\n }\n return widgetCache.get(entry.key)!\n}\n\nexport async function loadAllWidgets(): Promise<Array<LoadedWidgetModule & { moduleId: string; key: string }>> {\n const widgetEntries = await loadWidgetEntries()\n const loaded = await Promise.all(widgetEntries.map(async (entry) => {\n const widget = await loadEntry(entry)\n return { ...widget, moduleId: entry.moduleId, key: entry.key }\n }))\n const byId = new Map<string, LoadedWidgetModule & { moduleId: string; key: string }>()\n for (const widget of loaded) {\n if (!byId.has(widget.metadata.id)) {\n byId.set(widget.metadata.id, widget)\n }\n }\n return Array.from(byId.values())\n}\n\nexport async function loadWidgetById(widgetId: string): Promise<(LoadedWidgetModule & { moduleId: string; key: string }) | null> {\n const widgetEntries = await loadWidgetEntries()\n for (const entry of widgetEntries) {\n const widget = await loadEntry(entry)\n if (widget.metadata.id === widgetId) {\n return { ...widget, moduleId: entry.moduleId, key: entry.key }\n }\n }\n return null\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.6982.1.55222946d6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.6982.1.55222946d6",
|
|
258
|
+
"@open-mercato/shared": "0.6.8-develop.6982.1.55222946d6",
|
|
259
|
+
"@open-mercato/ui": "0.6.8-develop.6982.1.55222946d6",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.6982.1.55222946d6",
|
|
265
|
+
"@open-mercato/shared": "0.6.8-develop.6982.1.55222946d6",
|
|
266
|
+
"@open-mercato/ui": "0.6.8-develop.6982.1.55222946d6",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^7.0.0",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -11,6 +11,7 @@ import { cn } from '@open-mercato/shared/lib/utils'
|
|
|
11
11
|
import { Avatar } from '@open-mercato/ui/primitives/avatar'
|
|
12
12
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
13
13
|
import { eventDisplayTitle, pluralCategory } from '../../lib/calendar/labels'
|
|
14
|
+
import { formatTimeLabel, formatTimeRangeLabel } from '../../lib/calendar/format'
|
|
14
15
|
import type { AgendaListProps, CalendarCategory, CalendarItem } from './types'
|
|
15
16
|
|
|
16
17
|
const MAX_AVATARS_PER_ROW = 2
|
|
@@ -44,10 +45,6 @@ function formatUrlHost(location: string): string {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function formatTime(locale: string, date: Date): string {
|
|
48
|
-
return date.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit' })
|
|
49
|
-
}
|
|
50
|
-
|
|
51
48
|
function groupLabelOf(locale: string, date: Date): string {
|
|
52
49
|
return date.toLocaleDateString(locale, { weekday: 'long', month: 'short', day: 'numeric' })
|
|
53
50
|
}
|
|
@@ -132,9 +129,9 @@ function AgendaRow({
|
|
|
132
129
|
: item.locationKind === 'url' && item.location
|
|
133
130
|
? formatUrlHost(item.location)
|
|
134
131
|
: item.location
|
|
135
|
-
const startLabel = item.allDay ? t('customers.calendar.grid.allDay', 'All day') :
|
|
136
|
-
const endLabel = item.allDay ? null :
|
|
137
|
-
const ariaTime = item.allDay ? startLabel :
|
|
132
|
+
const startLabel = item.allDay ? t('customers.calendar.grid.allDay', 'All day') : formatTimeLabel(locale, item.start)
|
|
133
|
+
const endLabel = item.allDay ? null : formatTimeLabel(locale, item.end)
|
|
134
|
+
const ariaTime = item.allDay ? startLabel : formatTimeRangeLabel(locale, item.start, item.end)
|
|
138
135
|
return (
|
|
139
136
|
<Button
|
|
140
137
|
type="button"
|
|
@@ -10,6 +10,7 @@ import { cn } from '@open-mercato/shared/lib/utils'
|
|
|
10
10
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
11
11
|
import { getVisibleRange } from '../../lib/calendar/range'
|
|
12
12
|
import { eventDisplayTitle } from '../../lib/calendar/labels'
|
|
13
|
+
import { formatTimeRangeLabel } from '../../lib/calendar/format'
|
|
13
14
|
import type { CalendarItem, MonthGridProps } from './types'
|
|
14
15
|
|
|
15
16
|
const MAX_PILLS_PER_DAY = 2
|
|
@@ -19,10 +20,6 @@ function dayKeyOf(date: Date): string {
|
|
|
19
20
|
return format(date, 'yyyy-MM-dd')
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function formatTime(locale: string, date: Date): string {
|
|
23
|
-
return date.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit' })
|
|
24
|
-
}
|
|
25
|
-
|
|
26
23
|
function fullDateLabel(locale: string, date: Date): string {
|
|
27
24
|
return date.toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })
|
|
28
25
|
}
|
|
@@ -64,7 +61,7 @@ function MonthPill({ item, onItemClick }: { item: CalendarItem; onItemClick: (it
|
|
|
64
61
|
const locale = useLocale()
|
|
65
62
|
const canceled = item.status === 'canceled'
|
|
66
63
|
const title = eventDisplayTitle(item.title, t('customers.calendar.grid.untitled', 'Untitled'))
|
|
67
|
-
const timeLabel = item.allDay ? '' : ` · ${
|
|
64
|
+
const timeLabel = item.allDay ? '' : ` · ${formatTimeRangeLabel(locale, item.start, item.end)}`
|
|
68
65
|
const tintStyle = item.color
|
|
69
66
|
? { backgroundColor: `${item.color}${SOFT_TINT_ALPHA}`, color: item.color }
|
|
70
67
|
: undefined
|
|
@@ -21,6 +21,10 @@ export function formatDateRangeLabel(locale: string, from: Date, to: Date): stri
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export function formatTimeLabel(locale: string, date: Date): string {
|
|
25
|
+
return normalizeIntlSpacing(new Intl.DateTimeFormat(locale, TIME_OPTIONS).format(date))
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
export function formatTimeRangeLabel(locale: string, start: Date, end: Date): string {
|
|
25
29
|
const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS)
|
|
26
30
|
try {
|
|
@@ -2,6 +2,7 @@ import type { Module, ModuleDashboardWidgetEntry } from '@open-mercato/shared/mo
|
|
|
2
2
|
import type { DashboardWidgetMetadata, DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
3
3
|
import { applyDashboardWidgetOverridesToEntries } from '@open-mercato/shared/modules/overrides'
|
|
4
4
|
import { getModules } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
+
import { onModulesRegistered } from '@open-mercato/shared/lib/modules/registry'
|
|
5
6
|
|
|
6
7
|
type LoadedWidgetModule = DashboardWidgetModule<any> & { metadata: DashboardWidgetMetadata }
|
|
7
8
|
|
|
@@ -17,6 +18,7 @@ export function invalidateWidgetCache() {
|
|
|
17
18
|
widgetEntriesPromise = null;
|
|
18
19
|
widgetCache.clear();
|
|
19
20
|
}
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* An empty resolution means the module registry was not populated yet — a boot
|
|
22
24
|
* race, not "this app has no dashboard widgets". Memoizing it would serve an
|
|
@@ -56,6 +58,17 @@ async function loadWidgetEntries(): Promise<WidgetEntry[]> {
|
|
|
56
58
|
|
|
57
59
|
const widgetCache = new Map<string, Promise<LoadedWidgetModule>>()
|
|
58
60
|
|
|
61
|
+
// Close the boot race at its source (#5103). Bootstrap can register modules more
|
|
62
|
+
// than once — an i18n-only registration is later reconciled with the full module
|
|
63
|
+
// list — and a resolution computed in between describes a registry that no longer
|
|
64
|
+
// exists. The registry notifies only when the registered set actually changed, so
|
|
65
|
+
// a healthy boot costs nothing and no work is added to the request path. Declared
|
|
66
|
+
// after both caches so the subscription can never observe them in their temporal
|
|
67
|
+
// dead zone.
|
|
68
|
+
onModulesRegistered(() => {
|
|
69
|
+
invalidateWidgetCache()
|
|
70
|
+
})
|
|
71
|
+
|
|
59
72
|
function ensureValidWidgetModule(mod: any, key: string, moduleId: string): LoadedWidgetModule {
|
|
60
73
|
if (!mod || typeof mod !== 'object') {
|
|
61
74
|
throw new Error(`Invalid dashboard widget module "${key}" from "${moduleId}" (expected object export)`)
|