@anchrd/intel-ui 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/agent/agent-calendar/agent-calendar.tsx +32 -14
- package/src/agent/agent-chat/agent-chat.tsx +3 -1
- package/src/agent/agent-cron/agent-cron.ts +49 -113
- package/src/agent/agent-definition/agent-definition.ts +18 -9
- package/src/agent/agent-entry-title/agent-entry-title.ts +3 -1
- package/src/agent/agent-log/agent-log.tsx +6 -3
- package/src/agent/agent-profile/agent-profile.tsx +37 -12
- package/src/agent/agent.tsx +17 -9
- package/src/app/app-sidebar/app-sidebar.tsx +2 -2
- package/src/app/app-tree/app-tree.tsx +5 -3
- package/src/app/app.tsx +2 -2
- package/src/app/header-search/header-search.tsx +3 -1
- package/src/app/settings-dialog/settings-dialog.tsx +115 -0
- package/src/app/tree-move/tree-move.tsx +3 -1
- package/src/app/user-footer/user-footer.tsx +25 -2
- package/src/app/view-toggle/view-toggle.tsx +2 -2
- package/src/archive/archive.tsx +3 -1
- package/src/components/ui/popover.tsx +41 -0
- package/src/entry-picker/entry-picker.tsx +3 -1
- package/src/flow-runs/flow-runs.tsx +6 -3
- package/src/flows/flows.tsx +13 -9
- package/src/flows/node-palette/node-palette.tsx +2 -2
- package/src/folder-contents/folder-contents.tsx +3 -1
- package/src/graph-pane/graph-pane.tsx +2 -2
- package/src/i18n/de.json +406 -0
- package/src/i18n/en.json +20 -5
- package/src/i18n/es.json +406 -0
- package/src/i18n/i18n-context.tsx +65 -0
- package/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/src/i18n/i18n.ts +15 -11
- package/src/i18n/i18n.types.ts +43 -0
- package/src/main.tsx +33 -12
- package/src/modal/modal.tsx +2 -2
- package/src/node-table/node-table.tsx +3 -1
- package/src/nodes/nodes.tsx +7 -3
- package/src/resource-menu/resource-menu.tsx +8 -4
- package/src/router/router.types.ts +3 -2
- package/src/save-button/save-button.tsx +3 -3
- package/src/sign-in-refused/sign-in-refused.tsx +3 -2
- package/src/theme/theme-context.tsx +89 -0
- package/src/theme/theme.ts +51 -21
- package/src/timezone/timezone-combobox/timezone-combobox.tsx +149 -0
- package/src/timezone/timezone-context.tsx +45 -0
- package/src/timezone/timezone.ts +159 -0
- package/src/tools/tools.tsx +3 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext, useMemo, useState } from "react";
|
|
2
|
+
import { browserTimezone, readTimezone, rememberTimezone } from "./timezone.ts";
|
|
3
|
+
|
|
4
|
+
// The React seam of the timezone preference, the same shape as i18n-context and theme-context.
|
|
5
|
+
|
|
6
|
+
export interface TimezoneSelection {
|
|
7
|
+
current: string;
|
|
8
|
+
select(zone: string): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const TimezoneContext = createContext<TimezoneSelection | null>(null);
|
|
12
|
+
|
|
13
|
+
interface TimezoneProviderProps {
|
|
14
|
+
store?: Pick<Storage, "getItem" | "setItem">;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TimezoneProvider({ store, children }: TimezoneProviderProps) {
|
|
19
|
+
const [current, setCurrent] = useState(() => readTimezone(store));
|
|
20
|
+
|
|
21
|
+
const selection = useMemo<TimezoneSelection>(
|
|
22
|
+
() => ({
|
|
23
|
+
current,
|
|
24
|
+
select: (zone) => {
|
|
25
|
+
rememberTimezone(zone, store);
|
|
26
|
+
setCurrent(zone);
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
[current, store],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return <TimezoneContext value={selection}>{children}</TimezoneContext>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// `null` without a provider — the settings dialog asks whether there is anything to choose.
|
|
36
|
+
export function useTimezoneSelection(): TimezoneSelection | null {
|
|
37
|
+
return useContext(TimezoneContext);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The zone to read times in. Never null: a component without a provider still has to format a date,
|
|
41
|
+
// and the browser's own zone is the honest answer there.
|
|
42
|
+
export function useTimezone(): string {
|
|
43
|
+
const selection = useContext(TimezoneContext);
|
|
44
|
+
return selection?.current ?? browserTimezone();
|
|
45
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The reader's timezone. Next to `intel.language` and `intel.theme`: the same kind of preference,
|
|
2
|
+
// the same place.
|
|
3
|
+
//
|
|
4
|
+
// ⚠️ This is a preference, NOT the truth about when an agent fires. A schedule carries its own
|
|
5
|
+
// timezone (#228); this value is the suggestion when one is created and the lens the calendar is
|
|
6
|
+
// read through. The difference matters the day somebody travels: "08:00 Europe/Berlin" must stay
|
|
7
|
+
// Berlin, and it would not if the schedule took whatever the reader's browser says today.
|
|
8
|
+
const STORE_KEY = "intel.timezone";
|
|
9
|
+
|
|
10
|
+
// What the browser thinks. Used when nothing was ever chosen — deliberately not "UTC": a default
|
|
11
|
+
// nobody lives in is a default that produces schedules nobody meant.
|
|
12
|
+
export function browserTimezone(): string {
|
|
13
|
+
try {
|
|
14
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
15
|
+
} catch {
|
|
16
|
+
return "UTC";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Every IANA name this runtime knows, for the picker. An engine without `supportedValuesOf` (or one
|
|
21
|
+
// built without full ICU) yields the empty list, and the caller falls back to offering just the
|
|
22
|
+
// browser's own zone — one real choice beats a picker that throws.
|
|
23
|
+
export function availableTimezones(): string[] {
|
|
24
|
+
try {
|
|
25
|
+
return Intl.supportedValuesOf("timeZone");
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Whether this runtime can actually format in that zone. The check is the formatting itself: a name
|
|
32
|
+
// the engine rejects throws a RangeError here rather than in the middle of a calendar.
|
|
33
|
+
export function isKnownTimezone(zone: string): boolean {
|
|
34
|
+
try {
|
|
35
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A stored zone the engine no longer knows is discarded rather than kept: IANA renames zones, and a
|
|
43
|
+
// name that stopped resolving would otherwise throw on every date the calendar draws.
|
|
44
|
+
export function readTimezone(store?: Pick<Storage, "getItem">): string {
|
|
45
|
+
let stored: string | null = null;
|
|
46
|
+
try {
|
|
47
|
+
stored = store?.getItem(STORE_KEY) ?? null;
|
|
48
|
+
} catch {
|
|
49
|
+
return browserTimezone();
|
|
50
|
+
}
|
|
51
|
+
return stored !== null && isKnownTimezone(stored) ? stored : browserTimezone();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function rememberTimezone(zone: string, store?: Pick<Storage, "setItem">): void {
|
|
55
|
+
try {
|
|
56
|
+
store?.setItem(STORE_KEY, zone);
|
|
57
|
+
} catch {
|
|
58
|
+
// A preference that cannot be written is still a preference for this session.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One zone as the picker shows it. Everything here is derived, nothing is stored. */
|
|
63
|
+
export interface TimezoneOption {
|
|
64
|
+
zone: string;
|
|
65
|
+
/** "GMT+02:00", "GMT-04:00", "GMT" — present for every zone, which is why it carries the column. */
|
|
66
|
+
offsetLabel: string;
|
|
67
|
+
/** "MESZ", "JST" … or null where the runtime only repeats the offset. */
|
|
68
|
+
abbreviation: string | null;
|
|
69
|
+
/** Minutes east of UTC at `at`, for sorting. */
|
|
70
|
+
offsetMinutes: number;
|
|
71
|
+
/** What the search matches on, lowercased. */
|
|
72
|
+
keywords: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function part(zone: string, style: "short" | "longOffset", at: Date, locale: string): string {
|
|
76
|
+
try {
|
|
77
|
+
return (
|
|
78
|
+
new Intl.DateTimeFormat(locale, { timeZone: zone, timeZoneName: style })
|
|
79
|
+
.formatToParts(at)
|
|
80
|
+
.find((piece) => piece.type === "timeZoneName")?.value ?? ""
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// "GMT+02:00" → 120, "GMT-04:00" → -240, "GMT" → 0. Read from the same string the label shows, so
|
|
88
|
+
// the sort order and the text can never tell different stories.
|
|
89
|
+
function minutesFrom(offsetLabel: string): number {
|
|
90
|
+
const match = /^GMT([+-])(\d{2}):(\d{2})$/.exec(offsetLabel);
|
|
91
|
+
if (!match) return 0;
|
|
92
|
+
const [, sign, hours, minutes] = match;
|
|
93
|
+
const total = Number(hours) * 60 + Number(minutes);
|
|
94
|
+
return sign === "-" ? -total : total;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The label of one zone, at a given moment.
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ Computed at display time and never stored. A zone's offset AND its abbreviation move with
|
|
101
|
+
* daylight saving — Berlin is MEZ/+01:00 in January and MESZ/+02:00 in July — so a label written
|
|
102
|
+
* into a field would be wrong for half of every year.
|
|
103
|
+
*
|
|
104
|
+
* ⚠️ The abbreviation is dropped where `Intl` only echoes the offset — and WHICH zones have one
|
|
105
|
+
* depends on the READER'S LANGUAGE, not on the zone. CLDR ships short names only where a locale has
|
|
106
|
+
* its own word for that zone: a German reader gets "MEZ/MESZ" for Berlin and a bare "GMT-4" for New
|
|
107
|
+
* York; an English reader gets "EST/EDT" for New York and a bare "GMT+2" for Berlin. That is the
|
|
108
|
+
* useful way round — you get the abbreviation for the zones your language actually names — but it
|
|
109
|
+
* means this function's output is not the same in two languages, and a test has to say which one it
|
|
110
|
+
* is asserting. `Asia/Kolkata` has none in either ("GMT+5:30"), which is the common case worldwide.
|
|
111
|
+
*/
|
|
112
|
+
export function timezoneOption(zone: string, at: Date, locale: string): TimezoneOption {
|
|
113
|
+
const offsetLabel = part(zone, "longOffset", at, locale) || "GMT";
|
|
114
|
+
const short = part(zone, "short", at, locale);
|
|
115
|
+
// Dropped when it repeats the offset, and also when it repeats the zone's own name: "UTC" would
|
|
116
|
+
// otherwise render as "GMT · UTC UTC", which stutters without adding anything.
|
|
117
|
+
const abbreviation =
|
|
118
|
+
short && !short.startsWith("GMT") && short !== offsetLabel && short !== zone ? short : null;
|
|
119
|
+
const city = zone.split("/").pop()?.replace(/_/g, " ") ?? zone;
|
|
120
|
+
return {
|
|
121
|
+
zone,
|
|
122
|
+
offsetLabel,
|
|
123
|
+
abbreviation,
|
|
124
|
+
offsetMinutes: minutesFrom(offsetLabel),
|
|
125
|
+
// The offset appears twice on purpose: as written ("GMT+02:00") and as typed ("+2"), because
|
|
126
|
+
// nobody searches for the leading zero.
|
|
127
|
+
keywords: [zone, city, offsetLabel, abbreviation ?? "", shorthandOffset(offsetLabel)]
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.map((word) => word.toLowerCase()),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// "GMT+02:00" → "+2", "GMT+05:30" → "+5:30", "GMT" → "+0". What a person types.
|
|
134
|
+
function shorthandOffset(offsetLabel: string): string {
|
|
135
|
+
const match = /^GMT([+-])(\d{2}):(\d{2})$/.exec(offsetLabel);
|
|
136
|
+
if (!match) return "+0";
|
|
137
|
+
const [, sign, hours, minutes] = match;
|
|
138
|
+
return `${sign}${Number(hours)}${minutes === "00" ? "" : `:${minutes}`}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Every selectable zone, sorted west to east.
|
|
143
|
+
*
|
|
144
|
+
* ⚠️ `current` is folded in even when `supportedValuesOf` does not list it. That is not a corner
|
|
145
|
+
* case: the list holds only CANONICAL names, and "UTC" is not one of them (it is an alias of
|
|
146
|
+
* Etc/UTC) — so the zone every CI machine and many servers report would otherwise be missing from
|
|
147
|
+
* the picker that is supposed to be showing it.
|
|
148
|
+
*/
|
|
149
|
+
export function timezoneOptions(current: string, at: Date, locale: string): TimezoneOption[] {
|
|
150
|
+
const zones = availableTimezones();
|
|
151
|
+
const all = zones.includes(current) ? zones : [current, ...zones];
|
|
152
|
+
return all
|
|
153
|
+
.map((zone) => timezoneOption(zone, at, locale))
|
|
154
|
+
.sort((left, right) =>
|
|
155
|
+
left.offsetMinutes === right.offsetMinutes
|
|
156
|
+
? left.zone.localeCompare(right.zone)
|
|
157
|
+
: left.offsetMinutes - right.offsetMinutes,
|
|
158
|
+
);
|
|
159
|
+
}
|
package/src/tools/tools.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import * as React from "react";
|
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
8
8
|
import type { I18n } from "@/i18n/i18n.types.ts";
|
|
9
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
9
10
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
10
11
|
import { selectedFrom } from "@/router/selection-search.ts";
|
|
11
12
|
|
|
@@ -80,7 +81,8 @@ function attemptStore(): Storage | null {
|
|
|
80
81
|
// The catalog is one live `tools/list` with the signed-in user's own portal token. Nothing here is
|
|
81
82
|
// stored, mirrored or administered — the screen shows what the portal answers and nothing else.
|
|
82
83
|
export function Tools() {
|
|
83
|
-
const { data
|
|
84
|
+
const { data } = useIntelRouterContext();
|
|
85
|
+
const i18n = useI18n();
|
|
84
86
|
const catalog = useQuery({ queryKey: ["tools"], queryFn: () => data.listTools() });
|
|
85
87
|
// The servers behind that same live list (D30). Its own query, and its own failure: a portal that
|
|
86
88
|
// answers tools but no directory still has a usable screen, it just has no boxes to put them in.
|