@burdenoff/microfe-timecampus 2026.827.1 → 2026.828.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"TimelinePage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"names":[],"mappings":"AAiQA,wBAAgB,YAAY,gCAmP3B"}
1
+ {"version":3,"file":"TimelinePage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"names":[],"mappings":"AAiQA,wBAAgB,YAAY,gCAiP3B"}
@@ -292,13 +292,13 @@ function H() {
292
292
  }),
293
293
  /* @__PURE__ */ S("div", {
294
294
  className: "flex flex-wrap gap-4",
295
- children: F.filter((e) => Z.has(e)).map((e) => {
295
+ children: F.map((e) => {
296
296
  let t = N[e], n = Z.get(e);
297
297
  return /* @__PURE__ */ S(R, {
298
298
  category: e,
299
299
  label: t.label,
300
300
  dotClassName: t.dot,
301
- totalSeconds: n.totalSeconds
301
+ totalSeconds: n?.totalSeconds ?? 0
302
302
  }, e);
303
303
  })
304
304
  })
@@ -1 +1 @@
1
- {"version":3,"file":"TimelinePage.js","names":[],"sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n DndContext,\n DragOverlay,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n type DragEndEvent,\n type DragStartEvent,\n} from \"@dnd-kit/core\";\nimport {\n Activity,\n Brain,\n Calendar,\n Coffee,\n FolderOpen,\n Github,\n GripVertical,\n MessageSquare,\n Users,\n Zap,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { formatDuration, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeCampusTimelineQuery,\n useListProjectsQuery,\n useRecategorizeTimeCampusCaptureEventMutation,\n type TimelineSegmentFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\ntype Category = \"MEETING\" | \"DEEP_WORK\" | \"COMMUNICATION\" | \"BREAK\" | \"OTHER\";\n\nconst CATEGORY_META: Record<\n Category,\n { label: string; icon: typeof Brain; dot: string; chip: string }\n> = {\n DEEP_WORK: {\n label: \"Deep work\",\n icon: Brain,\n dot: \"bg-status-success-bg\",\n chip: \"bg-status-success-bg text-status-success-text\",\n },\n MEETING: {\n label: \"Meeting\",\n icon: Users,\n dot: \"bg-status-info-bg\",\n chip: \"bg-status-info-bg text-status-info-text\",\n },\n COMMUNICATION: {\n label: \"Communication\",\n icon: MessageSquare,\n dot: \"bg-status-warning-bg\",\n chip: \"bg-status-warning-bg text-status-warning-text\",\n },\n BREAK: {\n label: \"Break\",\n icon: Coffee,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n OTHER: {\n label: \"Other\",\n icon: Activity,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n};\n\nconst SOURCE_ICON: Record<string, typeof Brain> = {\n TIME_ENTRY: Zap,\n MEETING: Users,\n POMODORO: Brain,\n EMAIL: MessageSquare,\n CAPTURE: Github,\n};\n\nconst CATEGORY_ORDER: Category[] = [\n \"DEEP_WORK\",\n \"MEETING\",\n \"COMMUNICATION\",\n \"BREAK\",\n \"OTHER\",\n];\n\n/**\n * How a segment came by its category. The service writes these lowercase\n * (`timeline.service.ts` → 'auto' | 'user' | 'channel'), so normalise before\n * matching rather than trusting the string's casing.\n */\nconst CATEGORY_SOURCE_LABEL: Record<string, string> = {\n auto: \"Auto-categorized\",\n user: \"You set this\",\n channel: \"From channel rules\",\n};\n\nfunction categorySourceLabel(source: string | null | undefined): string | null {\n if (!source) return null;\n return CATEGORY_SOURCE_LABEL[source.toLowerCase()] ?? source;\n}\n\n/** Droppable target: a category chip in the \"Where the time went\" summary. */\nfunction DroppableCategoryChip({\n category,\n label,\n dotClassName,\n totalSeconds,\n}: {\n category: Category;\n label: string;\n dotClassName: string;\n totalSeconds: number;\n}) {\n const { isOver, setNodeRef } = useDroppable({ id: `category:${category}` });\n return (\n <div\n ref={setNodeRef}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${\n isOver\n ? \"border-action-primary-bg bg-action-primary-bg/10\"\n : \"border-border-seam bg-bg-surface\"\n }`}\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${dotClassName}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">{label}</span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(totalSeconds)}\n </span>\n {isOver && (\n <span className=\"text-xs font-medium text-primary\">Drop here</span>\n )}\n </div>\n );\n}\n\n/** Draggable row for a single timeline segment, with the existing dropdown kept as an accessible fallback. */\nfunction DraggableSegmentRow({\n segment,\n category,\n projectName,\n recategorizing,\n onRecategorize,\n}: {\n segment: TimelineSegmentFieldsFragment;\n category: Category;\n /** Resolved name for `segment.projectId`; undefined when it carries none. */\n projectName: string | undefined;\n recategorizing: boolean;\n onRecategorize: (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => void;\n}) {\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n const provenance = categorySourceLabel(segment.categorySource);\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: segment.id,\n data: { segment },\n disabled: !segment.isRecategorizable,\n });\n\n return (\n <li\n ref={setNodeRef}\n className={`flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${\n isDragging ? \"opacity-40\" : \"\"\n }`}\n >\n <div className=\"flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary\">\n <span>{formatTime(segment.startTime)}</span>\n <span className=\"text-text-tertiary\">\n {formatDuration(segment.durationSeconds)}\n </span>\n </div>\n {segment.isRecategorizable && (\n <button\n type=\"button\"\n aria-label={`Drag to recategorize ${segment.title || meta.label}`}\n className=\"mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing\"\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"h-4 w-4\" />\n </button>\n )}\n <div\n className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${meta.chip}`}\n >\n <SourceIcon className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {segment.title || meta.label}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${meta.chip}`}\n >\n {meta.label}\n </span>\n {projectName !== undefined && (\n <span className=\"inline-flex max-w-[12rem] items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <FolderOpen className=\"h-3 w-3 shrink-0\" />\n <span className=\"truncate\">{projectName}</span>\n </span>\n )}\n {segment.app && (\n <span className=\"text-xs text-text-secondary\">{segment.app}</span>\n )}\n {provenance && (\n <span className=\"text-xs text-text-tertiary\">{provenance}</span>\n )}\n </div>\n </div>\n {segment.isRecategorizable && (\n <select\n aria-label=\"Recategorize activity\"\n disabled={recategorizing}\n value={category}\n onChange={(e) => onRecategorize(segment, e.target.value as Category)}\n className=\"shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n {CATEGORY_ORDER.map((c) => (\n <option key={c} value={c}>\n {CATEGORY_META[c].label}\n </option>\n ))}\n </select>\n )}\n </li>\n );\n}\n\nfunction toLocalDateInput(date: Date): string {\n const offset = date.getTimezoneOffset();\n const local = new Date(date.getTime() - offset * 60 * 1000);\n return local.toISOString().slice(0, 10);\n}\n\nfunction dayBounds(dateStr: string): { from: string; to: string } {\n const start = new Date(`${dateStr}T00:00:00`);\n const end = new Date(`${dateStr}T23:59:59.999`);\n return { from: start.toISOString(), to: end.toISOString() };\n}\n\nexport function TimelinePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [dateStr, setDateStr] = useState(() => toLocalDateInput(new Date()));\n const [draggingSegment, setDraggingSegment] =\n useState<TimelineSegmentFieldsFragment | null>(null);\n\n const { from, to } = useMemo(() => dayBounds(dateStr), [dateStr]);\n\n const { data, loading, error, refetch } = useGetTimeCampusTimelineQuery({\n variables: { from, to },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n // Segments carry a `projectId` only; the workspace's projects supply the\n // name to show next to them. A segment whose id isn't in this list simply\n // keeps no project chip — it is never labelled from a guess.\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 200 } },\n skip: !workspaceId,\n });\n\n const [recategorize, { loading: recategorizing }] =\n useRecategorizeTimeCampusCaptureEventMutation();\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n );\n\n const timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const projectNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const project of projectsData?.projects?.items ?? []) {\n map.set(project.id, project.name);\n }\n return map;\n }, [projectsData?.projects?.items]);\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<\n string,\n { totalSeconds: number; segmentCount: number }\n >();\n for (const t of summary?.byCategory ?? []) {\n map.set(t.category, {\n totalSeconds: t.totalSeconds,\n segmentCount: t.segmentCount,\n });\n }\n return map;\n }, [summary]);\n\n const handleRecategorize = async (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => {\n if (!segment.isRecategorizable || segment.category === category) return;\n await recategorize({\n variables: { id: segment.sourceId, category },\n });\n await refetch();\n };\n\n const handleDragStart = (event: DragStartEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(segment ?? null);\n };\n\n const handleDragEnd = (event: DragEndEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(null);\n const overId = event.over?.id;\n if (!segment || typeof overId !== \"string\") return;\n const category = overId.replace(/^category:/, \"\") as Category;\n if (!CATEGORY_ORDER.includes(category)) return;\n void handleRecategorize(segment, category);\n };\n\n const focusPct =\n summary && summary.focusRatio != null\n ? Math.round(summary.focusRatio * 100)\n : 0;\n\n // The real span of the day, straight off the summary — not inferred from the\n // segment list, which may be paginated or filtered differently.\n const daySpan =\n summary?.firstActivityAt && summary?.lastActivityAt\n ? `${formatTime(summary.firstActivityAt)} – ${formatTime(summary.lastActivityAt)}`\n : null;\n\n return (\n <PageLayout\n title=\"Timeline\"\n description=\"Your day, assembled automatically\"\n purpose=\"See where your time actually went — meetings, deep work, communication and breaks stitched into one honest, chronological view from everything TimeCampus captures, so you can spot the patterns worth changing.\"\n actions={\n <div className=\"flex flex-wrap items-center gap-3\">\n {daySpan && (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-1 text-xs font-medium tabular-nums text-text-secondary\">\n <Activity className=\"h-3.5 w-3.5\" />\n {daySpan}\n </span>\n )}\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Calendar className=\"h-4 w-4\" />\n <input\n type=\"date\"\n value={dateStr}\n max={toLocalDateInput(new Date())}\n onChange={(e) => setDateStr(e.target.value)}\n className=\"rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n />\n </label>\n </div>\n }\n >\n {loading && !timeline ? (\n <div className=\"space-y-6\">\n <LoadingSkeleton variant=\"card\" count={4} />\n <LoadingSkeleton variant=\"list\" count={6} />\n </div>\n ) : error ? (\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load your timeline. Please try again later.\n </div>\n ) : (\n <DndContext\n sensors={sensors}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n >\n <div className=\"space-y-6\">\n {/* Summary */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Tracked time\"\n value={formatDuration(summary?.totalTrackedSeconds ?? 0)}\n icon={<Activity className=\"h-5 w-5\" />}\n description=\"Total activity across the day\"\n />\n <StatCard\n title=\"Focus ratio\"\n value={`${focusPct}%`}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Share of time in deep work\"\n />\n <StatCard\n title=\"Context switches\"\n value={summary?.contextSwitches ?? 0}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Category changes through the day\"\n />\n <StatCard\n title=\"Longest focus block\"\n value={formatDuration(summary?.longestFocusBlockSeconds ?? 0)}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Your deepest uninterrupted stretch\"\n />\n </div>\n\n {/* Category breakdown — also the drop targets for drag-to-recategorize */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\n <p className=\"mb-4 text-xs text-text-secondary\">\n Drag a segment from the timeline below onto a category to\n recategorize it — or use its dropdown.\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {CATEGORY_ORDER.filter((c) => totalsByCategory.has(c)).map(\n (category) => {\n const meta = CATEGORY_META[category];\n const total = totalsByCategory.get(category)!;\n return (\n <DroppableCategoryChip\n key={category}\n category={category}\n label={meta.label}\n dotClassName={meta.dot}\n totalSeconds={total.totalSeconds}\n />\n );\n },\n )}\n </div>\n </GlassCard>\n )}\n\n {/* Chronological timeline */}\n {segments.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No activity captured for this day\"\n description=\"As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order.\"\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <ol className=\"divide-y divide-border-seam\">\n {segments.map((segment: TimelineSegmentFieldsFragment) => (\n <DraggableSegmentRow\n key={segment.id}\n segment={segment}\n category={(segment.category as Category) ?? \"OTHER\"}\n projectName={\n segment.projectId\n ? projectNameById.get(segment.projectId)\n : undefined\n }\n recategorizing={recategorizing}\n onRecategorize={handleRecategorize}\n />\n ))}\n </ol>\n </GlassCard>\n )}\n </div>\n\n <DragOverlay>\n {draggingSegment && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop\">\n <GripVertical className=\"h-4 w-4 text-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {draggingSegment.title ||\n CATEGORY_META[\n (draggingSegment.category as Category) ?? \"OTHER\"\n ].label}\n </span>\n </div>\n )}\n </DragOverlay>\n </DndContext>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimelinePage.js","names":[],"sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n DndContext,\n DragOverlay,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n type DragEndEvent,\n type DragStartEvent,\n} from \"@dnd-kit/core\";\nimport {\n Activity,\n Brain,\n Calendar,\n Coffee,\n FolderOpen,\n Github,\n GripVertical,\n MessageSquare,\n Users,\n Zap,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { formatDuration, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeCampusTimelineQuery,\n useListProjectsQuery,\n useRecategorizeTimeCampusCaptureEventMutation,\n type TimelineSegmentFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\ntype Category = \"MEETING\" | \"DEEP_WORK\" | \"COMMUNICATION\" | \"BREAK\" | \"OTHER\";\n\nconst CATEGORY_META: Record<\n Category,\n { label: string; icon: typeof Brain; dot: string; chip: string }\n> = {\n DEEP_WORK: {\n label: \"Deep work\",\n icon: Brain,\n dot: \"bg-status-success-bg\",\n chip: \"bg-status-success-bg text-status-success-text\",\n },\n MEETING: {\n label: \"Meeting\",\n icon: Users,\n dot: \"bg-status-info-bg\",\n chip: \"bg-status-info-bg text-status-info-text\",\n },\n COMMUNICATION: {\n label: \"Communication\",\n icon: MessageSquare,\n dot: \"bg-status-warning-bg\",\n chip: \"bg-status-warning-bg text-status-warning-text\",\n },\n BREAK: {\n label: \"Break\",\n icon: Coffee,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n OTHER: {\n label: \"Other\",\n icon: Activity,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n};\n\nconst SOURCE_ICON: Record<string, typeof Brain> = {\n TIME_ENTRY: Zap,\n MEETING: Users,\n POMODORO: Brain,\n EMAIL: MessageSquare,\n CAPTURE: Github,\n};\n\nconst CATEGORY_ORDER: Category[] = [\n \"DEEP_WORK\",\n \"MEETING\",\n \"COMMUNICATION\",\n \"BREAK\",\n \"OTHER\",\n];\n\n/**\n * How a segment came by its category. The service writes these lowercase\n * (`timeline.service.ts` → 'auto' | 'user' | 'channel'), so normalise before\n * matching rather than trusting the string's casing.\n */\nconst CATEGORY_SOURCE_LABEL: Record<string, string> = {\n auto: \"Auto-categorized\",\n user: \"You set this\",\n channel: \"From channel rules\",\n};\n\nfunction categorySourceLabel(source: string | null | undefined): string | null {\n if (!source) return null;\n return CATEGORY_SOURCE_LABEL[source.toLowerCase()] ?? source;\n}\n\n/** Droppable target: a category chip in the \"Where the time went\" summary. */\nfunction DroppableCategoryChip({\n category,\n label,\n dotClassName,\n totalSeconds,\n}: {\n category: Category;\n label: string;\n dotClassName: string;\n totalSeconds: number;\n}) {\n const { isOver, setNodeRef } = useDroppable({ id: `category:${category}` });\n return (\n <div\n ref={setNodeRef}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${\n isOver\n ? \"border-action-primary-bg bg-action-primary-bg/10\"\n : \"border-border-seam bg-bg-surface\"\n }`}\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${dotClassName}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">{label}</span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(totalSeconds)}\n </span>\n {isOver && (\n <span className=\"text-xs font-medium text-primary\">Drop here</span>\n )}\n </div>\n );\n}\n\n/** Draggable row for a single timeline segment, with the existing dropdown kept as an accessible fallback. */\nfunction DraggableSegmentRow({\n segment,\n category,\n projectName,\n recategorizing,\n onRecategorize,\n}: {\n segment: TimelineSegmentFieldsFragment;\n category: Category;\n /** Resolved name for `segment.projectId`; undefined when it carries none. */\n projectName: string | undefined;\n recategorizing: boolean;\n onRecategorize: (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => void;\n}) {\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n const provenance = categorySourceLabel(segment.categorySource);\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: segment.id,\n data: { segment },\n disabled: !segment.isRecategorizable,\n });\n\n return (\n <li\n ref={setNodeRef}\n className={`flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${\n isDragging ? \"opacity-40\" : \"\"\n }`}\n >\n <div className=\"flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary\">\n <span>{formatTime(segment.startTime)}</span>\n <span className=\"text-text-tertiary\">\n {formatDuration(segment.durationSeconds)}\n </span>\n </div>\n {segment.isRecategorizable && (\n <button\n type=\"button\"\n aria-label={`Drag to recategorize ${segment.title || meta.label}`}\n className=\"mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing\"\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"h-4 w-4\" />\n </button>\n )}\n <div\n className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${meta.chip}`}\n >\n <SourceIcon className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {segment.title || meta.label}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${meta.chip}`}\n >\n {meta.label}\n </span>\n {projectName !== undefined && (\n <span className=\"inline-flex max-w-[12rem] items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <FolderOpen className=\"h-3 w-3 shrink-0\" />\n <span className=\"truncate\">{projectName}</span>\n </span>\n )}\n {segment.app && (\n <span className=\"text-xs text-text-secondary\">{segment.app}</span>\n )}\n {provenance && (\n <span className=\"text-xs text-text-tertiary\">{provenance}</span>\n )}\n </div>\n </div>\n {segment.isRecategorizable && (\n <select\n aria-label=\"Recategorize activity\"\n disabled={recategorizing}\n value={category}\n onChange={(e) => onRecategorize(segment, e.target.value as Category)}\n className=\"shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n {CATEGORY_ORDER.map((c) => (\n <option key={c} value={c}>\n {CATEGORY_META[c].label}\n </option>\n ))}\n </select>\n )}\n </li>\n );\n}\n\nfunction toLocalDateInput(date: Date): string {\n const offset = date.getTimezoneOffset();\n const local = new Date(date.getTime() - offset * 60 * 1000);\n return local.toISOString().slice(0, 10);\n}\n\nfunction dayBounds(dateStr: string): { from: string; to: string } {\n const start = new Date(`${dateStr}T00:00:00`);\n const end = new Date(`${dateStr}T23:59:59.999`);\n return { from: start.toISOString(), to: end.toISOString() };\n}\n\nexport function TimelinePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [dateStr, setDateStr] = useState(() => toLocalDateInput(new Date()));\n const [draggingSegment, setDraggingSegment] =\n useState<TimelineSegmentFieldsFragment | null>(null);\n\n const { from, to } = useMemo(() => dayBounds(dateStr), [dateStr]);\n\n const { data, loading, error, refetch } = useGetTimeCampusTimelineQuery({\n variables: { from, to },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n // Segments carry a `projectId` only; the workspace's projects supply the\n // name to show next to them. A segment whose id isn't in this list simply\n // keeps no project chip — it is never labelled from a guess.\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 200 } },\n skip: !workspaceId,\n });\n\n const [recategorize, { loading: recategorizing }] =\n useRecategorizeTimeCampusCaptureEventMutation();\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n );\n\n const timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const projectNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const project of projectsData?.projects?.items ?? []) {\n map.set(project.id, project.name);\n }\n return map;\n }, [projectsData?.projects?.items]);\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<\n string,\n { totalSeconds: number; segmentCount: number }\n >();\n for (const t of summary?.byCategory ?? []) {\n map.set(t.category, {\n totalSeconds: t.totalSeconds,\n segmentCount: t.segmentCount,\n });\n }\n return map;\n }, [summary]);\n\n const handleRecategorize = async (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => {\n if (!segment.isRecategorizable || segment.category === category) return;\n await recategorize({\n variables: { id: segment.sourceId, category },\n });\n await refetch();\n };\n\n const handleDragStart = (event: DragStartEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(segment ?? null);\n };\n\n const handleDragEnd = (event: DragEndEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(null);\n const overId = event.over?.id;\n if (!segment || typeof overId !== \"string\") return;\n const category = overId.replace(/^category:/, \"\") as Category;\n if (!CATEGORY_ORDER.includes(category)) return;\n void handleRecategorize(segment, category);\n };\n\n const focusPct =\n summary && summary.focusRatio != null\n ? Math.round(summary.focusRatio * 100)\n : 0;\n\n // The real span of the day, straight off the summary — not inferred from the\n // segment list, which may be paginated or filtered differently.\n const daySpan =\n summary?.firstActivityAt && summary?.lastActivityAt\n ? `${formatTime(summary.firstActivityAt)} – ${formatTime(summary.lastActivityAt)}`\n : null;\n\n return (\n <PageLayout\n title=\"Timeline\"\n description=\"Your day, assembled automatically\"\n purpose=\"See where your time actually went — meetings, deep work, communication and breaks stitched into one honest, chronological view from everything TimeCampus captures, so you can spot the patterns worth changing.\"\n actions={\n <div className=\"flex flex-wrap items-center gap-3\">\n {daySpan && (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-1 text-xs font-medium tabular-nums text-text-secondary\">\n <Activity className=\"h-3.5 w-3.5\" />\n {daySpan}\n </span>\n )}\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Calendar className=\"h-4 w-4\" />\n <input\n type=\"date\"\n value={dateStr}\n max={toLocalDateInput(new Date())}\n onChange={(e) => setDateStr(e.target.value)}\n className=\"rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n />\n </label>\n </div>\n }\n >\n {loading && !timeline ? (\n <div className=\"space-y-6\">\n <LoadingSkeleton variant=\"card\" count={4} />\n <LoadingSkeleton variant=\"list\" count={6} />\n </div>\n ) : error ? (\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load your timeline. Please try again later.\n </div>\n ) : (\n <DndContext\n sensors={sensors}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n >\n <div className=\"space-y-6\">\n {/* Summary */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Tracked time\"\n value={formatDuration(summary?.totalTrackedSeconds ?? 0)}\n icon={<Activity className=\"h-5 w-5\" />}\n description=\"Total activity across the day\"\n />\n <StatCard\n title=\"Focus ratio\"\n value={`${focusPct}%`}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Share of time in deep work\"\n />\n <StatCard\n title=\"Context switches\"\n value={summary?.contextSwitches ?? 0}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Category changes through the day\"\n />\n <StatCard\n title=\"Longest focus block\"\n value={formatDuration(summary?.longestFocusBlockSeconds ?? 0)}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Your deepest uninterrupted stretch\"\n />\n </div>\n\n {/* Category breakdown — also the drop targets for drag-to-recategorize */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\n <p className=\"mb-4 text-xs text-text-secondary\">\n Drag a segment from the timeline below onto a category to\n recategorize it — or use its dropdown.\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {CATEGORY_ORDER.map((category) => {\n const meta = CATEGORY_META[category];\n const total = totalsByCategory.get(category);\n return (\n <DroppableCategoryChip\n key={category}\n category={category}\n label={meta.label}\n dotClassName={meta.dot}\n totalSeconds={total?.totalSeconds ?? 0}\n />\n );\n })}\n </div>\n </GlassCard>\n )}\n\n {/* Chronological timeline */}\n {segments.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No activity captured for this day\"\n description=\"As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order.\"\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <ol className=\"divide-y divide-border-seam\">\n {segments.map((segment: TimelineSegmentFieldsFragment) => (\n <DraggableSegmentRow\n key={segment.id}\n segment={segment}\n category={(segment.category as Category) ?? \"OTHER\"}\n projectName={\n segment.projectId\n ? projectNameById.get(segment.projectId)\n : undefined\n }\n recategorizing={recategorizing}\n onRecategorize={handleRecategorize}\n />\n ))}\n </ol>\n </GlassCard>\n )}\n </div>\n\n <DragOverlay>\n {draggingSegment && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop\">\n <GripVertical className=\"h-4 w-4 text-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {draggingSegment.title ||\n CATEGORY_META[\n (draggingSegment.category as Category) ?? \"OTHER\"\n ].label}\n </span>\n </div>\n )}\n </DragOverlay>\n </DndContext>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-timecampus",
3
- "version": "2026.827.1",
3
+ "version": "2026.828.1",
4
4
  "description": "Burdenoff TimeCampus Microfrontend - AI-powered time management and productivity platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -125,7 +125,7 @@
125
125
  "@hono/node-server": ">=1.19.15",
126
126
  "ip-address": ">=10.3.1",
127
127
  "dompurify": ">=3.4.13",
128
- "react-router": ">=7.18.2 <9.0.0",
128
+ "react-router": ">=8.3.0 <9.0.0",
129
129
  "uuid": ">=11.1.1",
130
130
  "xlsx": ">=0.19.3",
131
131
  "vitest": ">=3.2.6 <4.0.0",