@alpaca-editor/core 1.0.4104 → 1.0.4105

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 (51) hide show
  1. package/dist/editor/MainLayout.js +1 -1
  2. package/dist/editor/MainLayout.js.map +1 -1
  3. package/dist/editor/Terminal.js +2 -2
  4. package/dist/editor/Terminal.js.map +1 -1
  5. package/dist/editor/ai/AgentTerminal.js +5 -3
  6. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  7. package/dist/editor/ai/Agents.js +9 -1
  8. package/dist/editor/ai/Agents.js.map +1 -1
  9. package/dist/editor/ai/AiResponseMessage.js +4 -2
  10. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  11. package/dist/editor/client/EditorShell.js +2 -0
  12. package/dist/editor/client/EditorShell.js.map +1 -1
  13. package/dist/editor/control-center/IndexOverview.js +3 -2
  14. package/dist/editor/control-center/IndexOverview.js.map +1 -1
  15. package/dist/editor/control-center/WebSocketMessages.js +3 -3
  16. package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
  17. package/dist/editor/media-selector/MediaFolderBrowser.js +2 -2
  18. package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
  19. package/dist/editor/menubar/ActiveUsers.js +3 -2
  20. package/dist/editor/menubar/ActiveUsers.js.map +1 -1
  21. package/dist/editor/reviews/Comments.js +96 -35
  22. package/dist/editor/reviews/Comments.js.map +1 -1
  23. package/dist/editor/sidebar/Completions.js +2 -1
  24. package/dist/editor/sidebar/Completions.js.map +1 -1
  25. package/dist/editor/ui/Icons.d.ts +2 -1
  26. package/dist/editor/ui/Icons.js +2 -2
  27. package/dist/editor/ui/Icons.js.map +1 -1
  28. package/dist/editor/ui/SimpleTable.js +1 -1
  29. package/dist/editor/ui/SimpleTable.js.map +1 -1
  30. package/dist/editor/utils.d.ts +12 -1
  31. package/dist/editor/utils.js +60 -12
  32. package/dist/editor/utils.js.map +1 -1
  33. package/dist/revision.d.ts +2 -2
  34. package/dist/revision.js +2 -2
  35. package/package.json +1 -1
  36. package/src/editor/MainLayout.tsx +1 -1
  37. package/src/editor/Terminal.tsx +2 -2
  38. package/src/editor/ai/AgentTerminal.tsx +8 -20
  39. package/src/editor/ai/Agents.tsx +14 -2
  40. package/src/editor/ai/AiResponseMessage.tsx +7 -5
  41. package/src/editor/client/EditorShell.tsx +2 -0
  42. package/src/editor/control-center/IndexOverview.tsx +3 -2
  43. package/src/editor/control-center/WebSocketMessages.tsx +3 -5
  44. package/src/editor/media-selector/MediaFolderBrowser.tsx +2 -2
  45. package/src/editor/menubar/ActiveUsers.tsx +4 -3
  46. package/src/editor/reviews/Comments.tsx +123 -38
  47. package/src/editor/sidebar/Completions.tsx +2 -1
  48. package/src/editor/ui/Icons.tsx +3 -0
  49. package/src/editor/ui/SimpleTable.tsx +2 -2
  50. package/src/editor/utils.ts +73 -15
  51. package/src/revision.ts +2 -2
@@ -1,2 +1,2 @@
1
- export declare const version = "1.0.4104";
2
- export declare const buildDate = "2025-09-23 14:05:05";
1
+ export declare const version = "1.0.4105";
2
+ export declare const buildDate = "2025-09-23 15:04:32";
package/dist/revision.js CHANGED
@@ -1,3 +1,3 @@
1
- export const version = "1.0.4104";
2
- export const buildDate = "2025-09-23 14:05:05";
1
+ export const version = "1.0.4105";
2
+ export const buildDate = "2025-09-23 15:04:32";
3
3
  //# sourceMappingURL=revision.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpaca-editor/core",
3
- "version": "1.0.4104",
3
+ "version": "1.0.4105",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -84,7 +84,7 @@ export default function MainLayout(props: MainLayoutProps) {
84
84
  }
85
85
 
86
86
  return (
87
- <div className={classNames("flex font-light select-none", className)}>
87
+ <div className={classNames("flex font-normal select-none", className)}>
88
88
  <div className="flex flex-1">
89
89
  {!props.view.hideViewSelector && <LeftToolbar />}
90
90
  <div className="flex flex-1 flex-col">
@@ -175,10 +175,10 @@ export const Terminal = forwardRef<
175
175
  }, [messages, response]);
176
176
 
177
177
  const responseBoxClasses =
178
- "text-xs self-stretch text-gray-700 p-2 rounded border border-theme-secondary bg-theme-secondary-light min-h-8 flex flex-col justify-end";
178
+ "text-xs self-stretch text-dark p-2 rounded border border-theme-secondary bg-theme-secondary-light min-h-8 flex flex-col justify-end";
179
179
  const getClasses = (m: Message) => {
180
180
  if (m.type === "response") return responseBoxClasses;
181
- return "text-xs text-gray-700 bg-indigo-100 border border-indigo-300 p-2 rounded prompt";
181
+ return "text-xs text-dark bg-indigo-100 border border-indigo-300 p-2 rounded prompt";
182
182
  };
183
183
 
184
184
  // Expose the submit function via the ref.
@@ -5,22 +5,7 @@ import React, {
5
5
  useCallback,
6
6
  useLayoutEffect,
7
7
  } from "react";
8
- import {
9
- Send,
10
- Bot,
11
- AlertCircle,
12
- Loader2,
13
- User,
14
- X,
15
- FileText,
16
- Puzzle,
17
- Type,
18
- Plus,
19
- MessageSquare,
20
- Wand2,
21
- Square,
22
- ChevronDown,
23
- } from "lucide-react";
8
+ import { Send, AlertCircle, Loader2, User, Wand2, Square } from "lucide-react";
24
9
  import { DancingDots } from "./DancingDots";
25
10
  import {
26
11
  AgentChatMessage,
@@ -52,6 +37,8 @@ import {
52
37
  PopoverContent,
53
38
  PopoverTrigger,
54
39
  } from "../../components/ui/popover";
40
+ import { SecretAgentIcon } from "../ui/Icons";
41
+ import { formatTime, formatDateTime } from "../utils";
55
42
 
56
43
  // Simple user message component
57
44
  const UserMessage = ({ message }: { message: AgentChatMessage }) => {
@@ -62,10 +49,10 @@ const UserMessage = ({ message }: { message: AgentChatMessage }) => {
62
49
  </div>
63
50
  <div className="min-w-0 flex-1 select-text">
64
51
  <div className="mb-1 flex items-center gap-2">
65
- <span className="text-sm font-medium text-gray-900">You</span>
52
+ <span className="text-xs font-medium text-gray-900">You</span>
66
53
  {message.createdDate && (
67
54
  <span className="text-xs text-gray-400">
68
- {new Date(message.createdDate).toLocaleTimeString()}
55
+ {formatTime(new Date(message.createdDate))}
69
56
  </span>
70
57
  )}
71
58
  </div>
@@ -2233,9 +2220,10 @@ export function AgentTerminal({
2233
2220
  {messages.length === 0 && !error && (
2234
2221
  <div className="flex h-full items-center justify-center p-8">
2235
2222
  <div className="text-center">
2236
- <Bot
2237
- className="mx-auto mb-4 h-12 w-12 text-gray-400"
2223
+ <SecretAgentIcon
2224
+ size={48}
2238
2225
  strokeWidth={1}
2226
+ className="mx-auto mb-4 text-gray-400"
2239
2227
  />
2240
2228
  <h3 className="mb-2 text-lg font-medium text-gray-900">
2241
2229
  Start a conversation
@@ -502,7 +502,6 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
502
502
  return (
503
503
  <div className="flex h-full items-center justify-center">
504
504
  <div className="w-72">
505
-
506
505
  {loadingProfiles ? (
507
506
  <div className="text-center text-xs text-gray-500">
508
507
  Loading profiles...
@@ -670,7 +669,20 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
670
669
  {agent.name}
671
670
  </div>
672
671
  <div className="text-xs text-gray-400">
673
- {new Date(agent.updatedDate).toLocaleString()}
672
+ {(() => {
673
+ try {
674
+ const {
675
+ formatDateTime,
676
+ } = require("../utils");
677
+ return formatDateTime(
678
+ new Date(agent.updatedDate),
679
+ );
680
+ } catch {
681
+ return new Date(
682
+ agent.updatedDate,
683
+ ).toLocaleString();
684
+ }
685
+ })()}
674
686
  </div>
675
687
  </div>
676
688
  <SimpleIconButton
@@ -1,13 +1,15 @@
1
- import React, { useState, useEffect, useMemo } from "react";
1
+ import { useState, useEffect, useMemo } from "react";
2
+ import { formatTime } from "../utils";
2
3
 
3
4
  import { useEditContext } from "../client/editContext";
4
5
  import { EditOperation } from "../../types";
5
6
  import { Message, ToolCall } from "./types";
6
7
  import { ToolCallDisplay } from "./ToolCallDisplay";
7
8
 
8
- import { X, Bot, Loader2 } from "lucide-react";
9
+ import { X } from "lucide-react";
9
10
  import { Button } from "../../components/ui/button";
10
11
  import { Checkbox } from "../../components/ui/checkbox";
12
+ import { SecretAgentIcon } from "../ui/Icons";
11
13
 
12
14
  type QuickAction = {
13
15
  id?: string;
@@ -464,15 +466,15 @@ export function AiResponseMessage({
464
466
  return (
465
467
  <div className="flex gap-3 p-4" data-testid="agent-message">
466
468
  <div className="flex-shrink-0">
467
- <Bot className="h-6 w-6 text-green-600" strokeWidth={1} />
469
+ <SecretAgentIcon size={24} strokeWidth={1} className="text-gray-2" />
468
470
  </div>
469
471
  <div className="min-w-0 flex-1 select-text">
470
472
  <div className="mb-1 flex items-center gap-2">
471
- <span className="text-sm font-medium text-gray-900">Agent</span>
473
+ <span className="text-dark text-xs font-medium">Agent</span>
472
474
 
473
475
  {lastMessage && lastMessage.createdDate && (
474
476
  <span className="text-xs text-gray-400">
475
- {new Date(lastMessage.createdDate).toLocaleTimeString()}
477
+ {formatTime(new Date(lastMessage.createdDate))}
476
478
  </span>
477
479
  )}
478
480
  </div>
@@ -2157,6 +2157,7 @@ export function EditorShell({
2157
2157
  revision,
2158
2158
  comments,
2159
2159
  setComments,
2160
+ availableCommentTags,
2160
2161
  selectedComment,
2161
2162
  setSelectedComment,
2162
2163
  loadComments,
@@ -2172,6 +2173,7 @@ export function EditorShell({
2172
2173
  setShowSuggestedEdits,
2173
2174
  showSuggestedEditsDiff,
2174
2175
  setShowSuggestedEditsDiff,
2176
+ showComments,
2175
2177
  showResolvedComments,
2176
2178
  setShowResolvedComments,
2177
2179
  showComponentNavigator,
@@ -9,6 +9,7 @@ import {
9
9
  } from "lucide-react";
10
10
 
11
11
  import { classNames } from "primereact/utils";
12
+ import { formatDateOnly, formatDateTime } from "../utils";
12
13
  import {
13
14
  IndexStatus,
14
15
  StagingStatus,
@@ -527,7 +528,7 @@ export function IndexOverview() {
527
528
  </div>
528
529
  <div className="text-xs text-gray-500">
529
530
  {batch.created
530
- ? new Date(batch.created).toLocaleDateString()
531
+ ? formatDateOnly(new Date(batch.created))
531
532
  : "Unknown"}
532
533
  </div>
533
534
  </div>
@@ -584,7 +585,7 @@ export function IndexOverview() {
584
585
  )}
585
586
  </div>
586
587
  <div className="ml-2 shrink-0 text-xs text-gray-500">
587
- {new Date(it.created).toLocaleString()}
588
+ {formatDateTime(new Date(it.created))}
588
589
  </div>
589
590
  </div>
590
591
  ))}
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
+ import { formatTime } from "../utils";
2
3
  import { useEditContext } from "../client/editContext";
3
4
 
4
5
  export function WebSocketMessages() {
@@ -34,11 +35,8 @@ export function WebSocketMessages() {
34
35
 
35
36
  const formatTimestamp = (isoString: string) => {
36
37
  const date = new Date(isoString);
37
- return (
38
- date.toLocaleTimeString() +
39
- "." +
40
- date.getMilliseconds().toString().padStart(3, "0")
41
- );
38
+ const base = formatTime(date);
39
+ return base + "." + date.getMilliseconds().toString().padStart(3, "0");
42
40
  };
43
41
 
44
42
  const getMessageTypeColor = (type: string) => {
@@ -195,7 +195,7 @@ export function MediaFolderBrowser({
195
195
  />
196
196
  </div>
197
197
  <div className="min-w-0 flex-1">
198
- <div className="truncate text-sm font-medium text-gray-900">
198
+ <div className="text-dark truncate text-xs font-medium">
199
199
  {t.name}
200
200
  </div>
201
201
  <div className="mt-1 text-xs text-gray-500">Media File</div>
@@ -305,7 +305,7 @@ export function MediaFolderBrowser({
305
305
  {/* Selected Item Info */}
306
306
  {selectedImage && (
307
307
  <div className="border-t bg-gray-50 p-3">
308
- <div className="truncate text-sm font-medium text-gray-900">
308
+ <div className="text-dark truncate text-xs font-medium">
309
309
  Selected: {selectedImage.name}
310
310
  </div>
311
311
  <div className="mt-1 text-xs text-gray-500">
@@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
9
9
  import { getItemVisitors, ItemVisitor } from "../services/editService";
10
10
  import { VerticalDotsIcon } from "../ui/Icons";
11
11
  import { AboutDialog } from "../client/AboutDialog";
12
+ import { formatDateOnly } from "../utils";
12
13
 
13
14
  type UserListItem = {
14
15
  type: "active" | "visitor";
@@ -258,7 +259,7 @@ export function ActiveUsers() {
258
259
  />
259
260
  </div>
260
261
  <div className="min-w-0 flex-1">
261
- <div className="text-sm font-medium text-gray-900">About</div>
262
+ <div className="text-dark text-xs font-medium">About</div>
262
263
  </div>
263
264
  </div>
264
265
  <div
@@ -272,7 +273,7 @@ export function ActiveUsers() {
272
273
  />
273
274
  </div>
274
275
  <div className="min-w-0 flex-1">
275
- <div className="text-sm font-medium text-gray-900">Log Out</div>
276
+ <div className="text-dark text-xs font-medium">Log Out</div>
276
277
  </div>
277
278
  </div>
278
279
  </div>
@@ -296,5 +297,5 @@ function formatTimeAgo(date: Date): string {
296
297
  if (diffInSeconds < 604800)
297
298
  return `${Math.floor(diffInSeconds / 86400)}d ago`;
298
299
 
299
- return date.toLocaleDateString();
300
+ return formatDateOnly(date);
300
301
  }
@@ -22,6 +22,8 @@ import {
22
22
  Tags,
23
23
  Lightbulb,
24
24
  } from "lucide-react";
25
+ import { getChildren } from "../services/contentService";
26
+ import { getComments as getCommentsApi } from "../services/reviewsService";
25
27
 
26
28
  // Define a union type for feedback items:
27
29
  export type FeedbackItem = CommentType | SuggestedEdit;
@@ -56,52 +58,135 @@ export function Comments() {
56
58
  const availableTags = editContext?.availableCommentTags || [];
57
59
 
58
60
  useEffect(() => {
59
- // Retrieve your list of comments (and ensure there's an array of suggested edits,
60
- // for instance from your editContext)
61
- const comments: CommentType[] = editContext?.comments || [];
62
- const suggestedEdits: SuggestedEdit[] = editContext?.suggestedEdits || [];
61
+ let cancelled = false;
63
62
 
64
- // Filter out applied suggestions if hideAppliedSuggestions is true
65
- const filteredSuggestedEdits = hideAppliedSuggestions
66
- ? suggestedEdits.filter((edit) => edit.status !== "applied")
67
- : suggestedEdits;
63
+ const load = async () => {
64
+ // Base lists from context
65
+ const mainComments: CommentType[] = editContext?.comments || [];
66
+ const suggestedEdits: SuggestedEdit[] = editContext?.suggestedEdits || [];
68
67
 
69
- // Start with combined list
70
- const combined: FeedbackItem[] = [...comments, ...filteredSuggestedEdits];
68
+ // Filter out applied suggestions if hideAppliedSuggestions is true
69
+ const filteredSuggestedEdits = hideAppliedSuggestions
70
+ ? suggestedEdits.filter((edit) => edit.status !== "applied")
71
+ : suggestedEdits;
71
72
 
72
- // Apply scope filter (item-only vs item + children)
73
- const baseItemId =
74
- (editContext?.selection && editContext.selection[0]) ||
75
- editContext?.currentItemDescriptor?.id ||
76
- undefined;
73
+ // Start with main page comments and suggestions
74
+ let allComments: CommentType[] = [...mainComments];
77
75
 
78
- const scoped =
79
- scope === "itemOnly" && baseItemId
80
- ? combined.filter((item: any) => item.itemId === baseItemId)
76
+ // If including children, fetch comments for descendant pages and merge
77
+ const basePageId = editContext?.currentItemDescriptor?.id;
78
+ const baseLanguage = editContext?.currentItemDescriptor?.language;
79
+ const baseVersion = editContext?.currentItemDescriptor?.version;
80
+
81
+ if (scope === "all" && basePageId && baseLanguage && baseVersion) {
82
+ try {
83
+ const sessionId = editContext?.sessionId || "";
84
+
85
+ // BFS to collect all descendant nodes
86
+ const queue: string[] = [basePageId];
87
+ const visited = new Set<string>();
88
+ const descendantPages: {
89
+ id: string;
90
+ language: string;
91
+ version: number;
92
+ hasChildren: boolean;
93
+ }[] = [];
94
+
95
+ while (queue.length > 0) {
96
+ const parentId = queue.shift()!;
97
+ if (visited.has(parentId)) continue;
98
+ visited.add(parentId);
99
+
100
+ const children = await getChildren(
101
+ parentId,
102
+ sessionId,
103
+ [],
104
+ false,
105
+ baseLanguage,
106
+ undefined,
107
+ );
108
+
109
+ for (const child of children) {
110
+ // Enqueue for deeper traversal when it has children
111
+ if (child.hasChildren) queue.push(child.id);
112
+ // Only pages (hasLayout) contribute comments
113
+ if (child.hasLayout) {
114
+ descendantPages.push({
115
+ id: child.id,
116
+ language: child.language,
117
+ version: child.version,
118
+ hasChildren: child.hasChildren,
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ // Fetch comments for all descendant pages in parallel
125
+ const results = await Promise.all(
126
+ descendantPages.map((p) =>
127
+ getCommentsApi(p.id, p.language, p.version),
128
+ ),
129
+ );
130
+
131
+ const childComments = results
132
+ .map((r) => (r.data || []) as CommentType[])
133
+ .flat();
134
+
135
+ // Merge unique by id
136
+ const existingIds = new Set(allComments.map((c) => c.id));
137
+ for (const c of childComments) {
138
+ if (!existingIds.has(c.id)) {
139
+ allComments.push(c);
140
+ existingIds.add(c.id);
141
+ }
142
+ }
143
+ } catch {
144
+ // Ignore child-loading errors to avoid blocking main list
145
+ }
146
+ }
147
+
148
+ // Combine with suggestions
149
+ let combined: FeedbackItem[] = [
150
+ ...allComments,
151
+ ...filteredSuggestedEdits,
152
+ ];
153
+
154
+ // Apply scope filter for item-only: keep only items whose mainItemId is the current page
155
+ if (scope === "itemOnly" && basePageId) {
156
+ combined = combined.filter(
157
+ (item: any) => item.mainItemId === basePageId,
158
+ );
159
+ }
160
+
161
+ // Apply tag filter to comments only (suggested edits do not have tags)
162
+ const tagFiltered = selectedTagsFilter.length
163
+ ? combined.filter((item: any) => {
164
+ // Keep suggested edits
165
+ if ("oldValue" in item && "newValue" in item) return true;
166
+ const tags = (item.tags || "")
167
+ .split(",")
168
+ .map((t: string) => t.trim())
169
+ .filter(Boolean);
170
+ // OR logic: include if any selected tag matches
171
+ return selectedTagsFilter.some((t) => tags.includes(t));
172
+ })
81
173
  : combined;
82
174
 
83
- // Apply tag filter to comments only (suggested edits do not have tags)
84
- const tagFiltered = selectedTagsFilter.length
85
- ? scoped.filter((item: any) => {
86
- // Keep suggested edits
87
- if ("oldValue" in item && "newValue" in item) return true;
88
- const tags = (item.tags || "")
89
- .split(",")
90
- .map((t: string) => t.trim())
91
- .filter(Boolean);
92
- // OR logic: include if any selected tag matches
93
- return selectedTagsFilter.some((t) => tags.includes(t));
94
- })
95
- : scoped;
175
+ // Sort by creation date
176
+ tagFiltered.sort(
177
+ (a, b) =>
178
+ new Date(b.created || "").getTime() -
179
+ new Date(a.created || "").getTime(),
180
+ );
181
+
182
+ if (!cancelled) setFeedbackItems(tagFiltered);
183
+ };
96
184
 
97
- // Sort by creation date. Adjust the comparison as needed if the date properties differ.
98
- tagFiltered.sort(
99
- (a, b) =>
100
- new Date(b.created || "").getTime() -
101
- new Date(a.created || "").getTime(),
102
- );
185
+ load();
103
186
 
104
- setFeedbackItems(tagFiltered);
187
+ return () => {
188
+ cancelled = true;
189
+ };
105
190
  }, [editContext, hideAppliedSuggestions, scope, selectedTagsFilter]);
106
191
 
107
192
  useEffect(() => {
@@ -4,6 +4,7 @@ import { Switch } from "../../components/ui/switch";
4
4
  import { Button } from "../../components/ui/button";
5
5
  import { Badge } from "../../components/ui/badge";
6
6
  import { Loader2, RefreshCw, FileText } from "lucide-react";
7
+ import { formatTime } from "../utils";
7
8
 
8
9
  import {
9
10
  generatePageContext,
@@ -120,7 +121,7 @@ export function Completions() {
120
121
  {pageContext.abstract}
121
122
  </p>
122
123
  <div className="text-xs text-gray-400">
123
- Generated: {pageContext.lastGenerated.toLocaleTimeString()}
124
+ {`Generated: ${formatTime(pageContext.lastGenerated)}`}
124
125
  </div>
125
126
  </div>
126
127
  ) : (
@@ -891,10 +891,12 @@ export function SecretAgentIcon({
891
891
  title,
892
892
  strokeWidth = 1,
893
893
  size = 20,
894
+ className,
894
895
  }: {
895
896
  title?: string;
896
897
  strokeWidth?: number;
897
898
  size?: number;
899
+ className?: string;
898
900
  }) {
899
901
  return (
900
902
  <svg
@@ -909,6 +911,7 @@ export function SecretAgentIcon({
909
911
  strokeLinejoin="round"
910
912
  role="img"
911
913
  aria-label={title}
914
+ className={className}
912
915
  >
913
916
  <path d="M3 10h18" />
914
917
  <path d="M7 10l2-5h6l2 5" />
@@ -23,10 +23,10 @@ export function SimpleTable<T>({
23
23
  }) {
24
24
  return (
25
25
  <table className="text-surface min-w-full table-auto text-left text-xs font-light">
26
- <thead className="border-b border-neutral-200 font-medium">
26
+ <thead className="border-b border-neutral-200">
27
27
  <tr>
28
28
  {columns.map((col, index) => (
29
- <th key={index} className="px-1.5 py-1.5">
29
+ <th key={index} className="px-1.5 py-1.5 font-medium">
30
30
  {col.header}
31
31
  </th>
32
32
  ))}
@@ -504,23 +504,81 @@ export function normalizeGuid(id: string) {
504
504
  return "{" + id + "}";
505
505
  }
506
506
 
507
- const dateOptions: Intl.DateTimeFormatOptions = {
508
- year: "numeric",
509
- month: "numeric",
510
- day: "numeric",
511
- hour: "numeric",
512
- minute: "numeric",
513
- second: "numeric",
514
- hour12: false,
515
- };
507
+ function getLocale() {
508
+ try {
509
+ if (typeof navigator !== "undefined" && navigator.language)
510
+ return navigator.language;
511
+ } catch {}
512
+ return "en";
513
+ }
514
+
515
+ function getUserTimeZone() {
516
+ try {
517
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
518
+ return tz || "UTC";
519
+ } catch {
520
+ return "UTC";
521
+ }
522
+ }
523
+
524
+ export function formatDate(
525
+ date: Date,
526
+ options?: Intl.DateTimeFormatOptions & { timeZone?: string },
527
+ ) {
528
+ const locale = getLocale();
529
+ const timeZone = options?.timeZone || getUserTimeZone();
530
+ const fmt = new Intl.DateTimeFormat(locale, {
531
+ year: "numeric",
532
+ month: "numeric",
533
+ day: "numeric",
534
+ hour: "numeric",
535
+ minute: "numeric",
536
+ second: "numeric",
537
+ hour12: false,
538
+ timeZone,
539
+ ...options,
540
+ });
541
+ return fmt.format(date);
542
+ }
516
543
 
517
- const dateFormat = Intl.DateTimeFormat(
518
- typeof navigator !== "undefined" ? navigator.language : "en",
519
- dateOptions,
520
- );
544
+ export function formatDateOnly(
545
+ date: Date,
546
+ options?: Intl.DateTimeFormatOptions & { timeZone?: string },
547
+ ) {
548
+ const locale = getLocale();
549
+ const timeZone = options?.timeZone || getUserTimeZone();
550
+ const fmt = new Intl.DateTimeFormat(locale, {
551
+ year: "numeric",
552
+ month: "numeric",
553
+ day: "numeric",
554
+ timeZone,
555
+ ...options,
556
+ });
557
+ return fmt.format(date);
558
+ }
521
559
 
522
- export function formatDate(date: Date) {
523
- return dateFormat.format(date);
560
+ export function formatTime(
561
+ date: Date,
562
+ options?: Intl.DateTimeFormatOptions & { timeZone?: string },
563
+ ) {
564
+ const locale = getLocale();
565
+ const timeZone = options?.timeZone || getUserTimeZone();
566
+ const fmt = new Intl.DateTimeFormat(locale, {
567
+ hour: "numeric",
568
+ minute: "numeric",
569
+ second: "numeric",
570
+ hour12: false,
571
+ timeZone,
572
+ ...options,
573
+ });
574
+ return fmt.format(date);
575
+ }
576
+
577
+ export function formatDateTime(
578
+ date: Date,
579
+ options?: Intl.DateTimeFormatOptions & { timeZone?: string },
580
+ ) {
581
+ return formatDate(date, options);
524
582
  }
525
583
 
526
584
  export function findClosestFieldElement(node: Node | null): HTMLElement | null {
package/src/revision.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const version = "1.0.4104";
2
- export const buildDate = "2025-09-23 14:05:05";
1
+ export const version = "1.0.4105";
2
+ export const buildDate = "2025-09-23 15:04:32";