@gntik-ai/templates 0.8.0 → 0.10.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.
Files changed (36) hide show
  1. package/dist/index.d.ts +13 -5
  2. package/dist/index.js +136 -103
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -5
  5. package/src/audit-log/Page.test.tsx +53 -0
  6. package/src/audit-log/Page.tsx +13 -5
  7. package/src/audit-log/template.meta.ts +1 -1
  8. package/src/help-home/Page.tsx +3 -1
  9. package/src/help-home/data.ts +3 -4
  10. package/src/incident-board/Page.tsx +6 -1
  11. package/src/incident-board/data.ts +9 -3
  12. package/src/index.ts +1 -1
  13. package/src/keyboard-shortcuts/Page.test.tsx +39 -1
  14. package/src/keyboard-shortcuts/ShortcutSheet.tsx +4 -1
  15. package/src/keyboard-shortcuts/data.ts +29 -14
  16. package/src/live-event-stream/Page.test.tsx +8 -4
  17. package/src/resource-detail/Page.labels.test.tsx +67 -0
  18. package/src/resource-detail/Page.tsx +18 -7
  19. package/src/resource-detail/examples/Labels.tsx +18 -0
  20. package/src/resource-detail/examples/Spanish.tsx +11 -0
  21. package/src/resource-detail/template.meta.ts +1 -1
  22. package/src/resource-gallery/Page.tsx +3 -1
  23. package/src/resource-gallery/data.ts +3 -4
  24. package/src/resource-index/Page.tsx +3 -1
  25. package/src/resource-index/data.ts +7 -4
  26. package/src/search-results/Page.tsx +5 -5
  27. package/src/settings-profile/fixtures/editable.snap +1 -1
  28. package/src/shared/ConsoleShell.test.tsx +13 -0
  29. package/src/shared/ConsoleShell.tsx +35 -30
  30. package/src/test/match-text.test.ts +51 -0
  31. package/src/usage-analytics/Page.tsx +3 -1
  32. package/src/usage-analytics/data.ts +9 -3
  33. package/src/workflow-builder/Page.test.tsx +103 -2
  34. package/src/workflow-builder/Page.tsx +34 -8
  35. package/src/workflow-builder/examples/SaveGraph.tsx +17 -0
  36. package/src/workflow-builder/template.meta.ts +1 -1
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  'use client';
2
- import { ActivityFeed, AppSidebarFooter, AppSidebarHeader, AppSidebarNav, AppTopbar, AuthHeader, BulkActionBar, ChartCard, CodePanel, CommentThread, ConfirmDestructive, CreateApiKeyDialog, DEPLOYMENT_COLUMNS, DEPLOYMENT_DETAILS, DEPLOYMENT_EVENTS, DEPLOYMENT_ROWS, DangerZone, DataTable, DateRangeFilter, DescriptionListCard, ErrorPanel, EvaluationScorecard, Faq, FeatureGrid, FileUploadPanel, FilterBar, FirstRunEmpty, FlowBuilder, ForgotPasswordForm, FormSection, Hero, InlineCallout, InviteMembersDialog, InvoiceTable, KPI_ITEMS, KpiRow, ListSkeleton, LogViewer, MembersTable, MfaChallenge, ModelKeysList, NoResultsEmpty, NotificationMatrix, PROJECT_ACTIVITY, PageHeader, PageToolbar, PaymentMethodCard, PendingInvitations, PlanCard, PricingTable, ProfileHeader, PromptEditor, QuotaMeters, RESOURCE_CARDS, ResetPasswordForm, ResourceCardGrid, SectionHeader, SessionsDevices, SettingsRow, SignInForm, SignUpForm, SsoButtons, StatusTimeline, StickyActionBar, SystemBanner, TokenCostCard, TraceWaterfall, TrendDelta, ValidationSummary, apiKeyExpiries, apiKeyScopes, appSidebarNavGroups, appSidebarUser, appSidebarUser as appSidebarUser$1, appSidebarWorkspaces, appTopbarNotifications, firstRunSteps, formatSpanDuration, generateDemoSecret, memberRoles, noAccessSample, notificationChannels, notificationDefaults, notificationEvents, sampleDeviceSessions, sampleInvitations, sampleInvoices, sampleMembers, samplePaymentMethod, samplePlan, samplePricingTiers, sampleProfile, sampleQuotas, ssoProviders, ssoProviders as ssoProviders$1 } from "@gntik-ai/blocks";
3
- import { Alert, AuthLayout, Avatar, AvatarGroup, Badge, Blockquote, Breadcrumbs, Button, CanvasLayout, Card, CardBody, CardDescription, CardHeader, CardTitle, Checkbox, CheckboxGroup, ClickableCard, CodeBlock, ContextMenu, ContextMenuContent, ContextMenuTrigger, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DocsLayout, Drawer, DrawerBody, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, EmptyState, Field, FieldDescription, FieldError, FieldLabel, Grid, GridItem, HStack, Heading, IconButton, Input, InspectorLayout, InspectorToggle, JsonViewer, KanbanBoard, Kbd, KbdCombo, Link, ListInput, LiveAnnouncer, Logo, MegaMenu, MenuItem, MenuSeparator, Meter, MobileNav, NavList, OtpInput, OverflowList, Page, PasswordInput, PowerSearch, Progress, Section, Select, SelectContent, SelectItem, SelectTrigger, SelectableCard, SelectableCardGroup, Separator, SettingsLayout, SidebarLayout, SimpleSelect, Skeleton, Spinner, SplitLayout, Stack, StackedLayout, StatusDot, StatusLayout, StatusTag, Switch, Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsList, TabsPanel, TabsTab, Text, Textarea, ThemeSwitcher, Thumbnail, ThumbnailList, Timer, Timestamp, Toggle, ToggleGroup, TokenInput, TooltipProvider, TransferList, TreeList, VisuallyHidden, WizardLayout, cn, emptyPowerSearchQuery, formatPowerSearch, matchPowerSearch, megaMenuNavItems, moveKanbanCard, parsePowerSearch, useAnnounce, useHasLiveAnnouncer, useI18n, useInspectorLayout, useTheme } from "@gntik-ai/ui";
2
+ import { ChartLocaleProvider, Heatmap, MiniBar, RadarChart, chartFmt } from "@gntik-ai/charts";
3
+ import { ActivityFeed, AppSidebarFooter, AppSidebarHeader, AppSidebarNav, AppTopbar, AuthHeader, BulkActionBar, ChartCard, CodePanel, CommentThread, ConfirmDestructive, CreateApiKeyDialog, DEPLOYMENT_COLUMNS, DEPLOYMENT_DETAILS, DEPLOYMENT_EVENTS, DEPLOYMENT_ROWS, DangerZone, DataTable, DateRangeFilter, DescriptionListCard, ErrorPanel, EvaluationScorecard, Faq, FeatureGrid, FileUploadPanel, FilterBar, FirstRunEmpty, FlowBuilder, ForgotPasswordForm, FormSection, Hero, InlineCallout, InviteMembersDialog, InvoiceTable, KPI_ITEMS, KpiRow, ListSkeleton, LogViewer, MembersTable, MfaChallenge, ModelKeysList, NoResultsEmpty, NotificationMatrix, PROJECT_ACTIVITY, PageHeader, PageToolbar, PaymentMethodCard, PendingInvitations, PlanCard, PricingTable, ProfileHeader, PromptEditor, QuotaMeters, RESOURCE_CARDS, ResetPasswordForm, ResourceCardGrid, SectionHeader, SessionsDevices, SettingsRow, SignInForm, SignUpForm, SsoButtons, StatusTimeline, StickyActionBar, SystemBanner, TokenCostCard, TraceWaterfall, TrendDelta, ValidationSummary, apiKeyExpiries, apiKeyScopes, appSidebarNavGroups, appSidebarUser, appSidebarUser as appSidebarUser$1, appSidebarWorkspaces, appTopbarNotifications, builderEdges, builderNodes, cleanFlowGraph, firstRunSteps, formatSpanDuration, generateDemoSecret, memberRoles, noAccessSample, notificationChannels, notificationDefaults, notificationEvents, sampleDeviceSessions, sampleInvitations, sampleInvoices, sampleMembers, samplePaymentMethod, samplePlan, samplePricingTiers, sampleProfile, sampleQuotas, ssoProviders, ssoProviders as ssoProviders$1 } from "@gntik-ai/blocks";
4
+ import { Alert, AuthLayout, Avatar, AvatarGroup, Badge, Blockquote, Breadcrumbs, Button, CanvasLayout, Card, CardBody, CardDescription, CardHeader, CardTitle, Checkbox, CheckboxGroup, ClickableCard, CodeBlock, ContextMenu, ContextMenuContent, ContextMenuTrigger, Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DocsLayout, Drawer, DrawerBody, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, EmptyState, Field, FieldDescription, FieldError, FieldLabel, Grid, GridItem, HStack, Heading, IconButton, Input, InspectorLayout, InspectorToggle, JsonViewer, KanbanBoard, Kbd, KbdCombo, Link, ListInput, LiveAnnouncer, Logo, MegaMenu, MenuItem, MenuSeparator, Meter, MobileNav, NavList, OtpInput, OverflowList, Page, PasswordInput, PowerSearch, Progress, Section, Select, SelectContent, SelectItem, SelectTrigger, SelectableCard, SelectableCardGroup, Separator, SettingsLayout, SidebarLayout, SimpleSelect, Skeleton, Spinner, SplitLayout, Stack, StackedLayout, StatusDot, StatusLayout, StatusTag, Switch, Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsList, TabsPanel, TabsTab, Text, Textarea, ThemeSwitcher, Thumbnail, ThumbnailList, Timer, Timestamp, Toggle, ToggleGroup, TokenInput, TooltipProvider, TransferList, TreeList, VisuallyHidden, WizardLayout, cn, emptyPowerSearchQuery, formatPowerSearch, matchPowerSearch, matchText, megaMenuNavItems, moveKanbanCard, parsePowerSearch, useAnnounce, useHasLiveAnnouncer, useI18n, useInspectorLayout, useOs, useTheme } from "@gntik-ai/ui";
4
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
6
  import { Fragment as Fragment$1, createElement, useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
7
  import { ChatAnnouncer, ChatComposer, ChatLayout, ChatMessage, CitationList, Markdown, SuggestionChips, ToolCallCard } from "@gntik-ai/chat";
7
8
  import { Archive, ArrowLeft, ArrowRight, BarChart3, Bell, BellOff, BellRing, BookOpen, Braces, Building2, CalendarClock, Check, CheckCheck, CircleCheck, Clock, Code2, Columns3, Copy, Cpu, CreditCard, Database, Download, ExternalLink, FileCode, FileDiff, FileJson, FileQuestion, FileSearch, FileText, Folder, FolderKanban, FolderOpen, GitBranch, Globe, GripVertical, Hand, HardDrive, Home, KeyRound, Keyboard, LayoutGrid, LayoutTemplate, LifeBuoy, ListOrdered, ListTodo, Lock, Mail, MailOpen, MapPin, MessageSquare, MessagesSquare, Minus, Palette, Phone, Play, PlayCircle, Plug, Plus, RefreshCw, Rocket, RotateCcw, RotateCw, Rss, Save, Search, SearchX, Send, Server, ServerCrash, Shield, ShieldCheck, Square, SquarePen, Table2, ThumbsDown, ThumbsUp, Trash2, Upload, User, UserPlus, Users, Wrench, X } from "@gntik-ai/icons";
8
- import { Heatmap, MiniBar, RadarChart, chartFmt } from "@gntik-ai/charts";
9
9
  import { DiffEditor } from "@gntik-ai/editor";
10
10
  import { flushSync } from "react-dom";
11
11
  //#region src/shared/ConsoleShell.tsx
@@ -16,38 +16,42 @@ import { flushSync } from "react-dom";
16
16
  * link and user-menu items.
17
17
  */
18
18
  function ConsoleShell({ children, nav, currentHref, breadcrumbs, workspaces, currentWorkspaceId, onWorkspaceChange, onCreateWorkspace, user, usage, helpHref, helpLabel, userMenuItems, onSignOut, topbar, storageKey, sidebarLabel, collapseMode }) {
19
- return /* @__PURE__ */ jsx(SidebarLayout, {
20
- fullScreen: true,
21
- storageKey,
22
- sidebarLabel,
23
- collapseMode,
24
- sidebarHeader: ({ collapsed }) => /* @__PURE__ */ jsx(AppSidebarHeader, {
25
- collapsed,
26
- workspaces,
27
- currentWorkspaceId,
28
- onWorkspaceChange,
29
- onCreateWorkspace
30
- }),
31
- sidebar: ({ collapsed }) => /* @__PURE__ */ jsx(AppSidebarNav, {
32
- groups: nav,
33
- currentHref,
34
- collapsed
35
- }),
36
- sidebarFooter: ({ collapsed }) => /* @__PURE__ */ jsx(AppSidebarFooter, {
37
- collapsed,
38
- user,
39
- usage,
40
- helpHref,
41
- helpLabel,
42
- userMenuItems,
43
- onSignOut
44
- }),
45
- topbar: /* @__PURE__ */ jsx(AppTopbar, {
46
- breadcrumbs,
47
- user,
48
- ...topbar
49
- }),
50
- children
19
+ const { locale } = useI18n();
20
+ return /* @__PURE__ */ jsx(ChartLocaleProvider, {
21
+ locale,
22
+ children: /* @__PURE__ */ jsx(SidebarLayout, {
23
+ fullScreen: true,
24
+ storageKey,
25
+ sidebarLabel,
26
+ collapseMode,
27
+ sidebarHeader: ({ collapsed }) => /* @__PURE__ */ jsx(AppSidebarHeader, {
28
+ collapsed,
29
+ workspaces,
30
+ currentWorkspaceId,
31
+ onWorkspaceChange,
32
+ onCreateWorkspace
33
+ }),
34
+ sidebar: ({ collapsed }) => /* @__PURE__ */ jsx(AppSidebarNav, {
35
+ groups: nav,
36
+ currentHref,
37
+ collapsed
38
+ }),
39
+ sidebarFooter: ({ collapsed }) => /* @__PURE__ */ jsx(AppSidebarFooter, {
40
+ collapsed,
41
+ user,
42
+ usage,
43
+ helpHref,
44
+ helpLabel,
45
+ userMenuItems,
46
+ onSignOut
47
+ }),
48
+ topbar: /* @__PURE__ */ jsx(AppTopbar, {
49
+ breadcrumbs,
50
+ user,
51
+ ...topbar
52
+ }),
53
+ children
54
+ })
51
55
  });
52
56
  }
53
57
  //#endregion
@@ -721,8 +725,8 @@ const DAY$1 = 864e5;
721
725
  const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
722
726
  const fieldValue$1 = (e, field) => field === "actor" ? e.actor.name : field === "resource" ? e.resource : field === "result" ? e.result : "";
723
727
  /** Applies the search text, the filter chips (OR within a field, AND across fields) and the date range. */
724
- function filterAuditEvents(events, query, filters, range) {
725
- const q = query.trim().toLowerCase();
728
+ function filterAuditEvents(events, query, filters, range, locale) {
729
+ const q = query.trim();
726
730
  const fields = [...new Set(filters.map((f) => f.field))];
727
731
  const from = range.start ? startOfDay(range.start).getTime() : -Infinity;
728
732
  const to = range.end ? startOfDay(range.end).getTime() + DAY$1 : Infinity;
@@ -732,13 +736,17 @@ function filterAuditEvents(events, query, filters, range) {
732
736
  e.actor.email,
733
737
  e.action,
734
738
  e.target
735
- ].some((v) => v.toLowerCase().includes(q))) return false;
739
+ ].some((v) => matchText(v, q, { locale }))) return false;
736
740
  if (!fields.every((field) => filters.some((f) => f.field === field && f.value === fieldValue$1(e, field)))) return false;
737
741
  const t = e.at.getTime();
738
742
  return t >= from && t < to;
739
743
  });
740
744
  }
741
- const csvCell = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, "\"\"")}"` : v;
745
+ /** Neutralises leading spreadsheet formula triggers before applying CSV quoting. */
746
+ function escapeCsvCell(value) {
747
+ const safe = /^[=+@\t\r-]/.test(value) ? `'${value}` : value;
748
+ return /[",\n]/.test(safe) ? `"${safe.replace(/"/g, "\"\"")}"` : safe;
749
+ }
742
750
  /** CSV of the events (time, actor, action, resource, target, ip, result). */
743
751
  function auditEventsToCsv(events) {
744
752
  return [[
@@ -759,7 +767,7 @@ function auditEventsToCsv(events) {
759
767
  e.target,
760
768
  e.ip,
761
769
  e.result
762
- ])].map((r) => r.map(csvCell).join(",")).join("\n");
770
+ ])].map((r) => r.map(escapeCsvCell).join(",")).join("\n");
763
771
  }
764
772
  function downloadCsv(events) {
765
773
  if (typeof URL.createObjectURL !== "function") return;
@@ -772,6 +780,7 @@ function downloadCsv(events) {
772
780
  }
773
781
  /** Audit log: ConsoleShell + Page with FilterBar + DateRangeFilter, an events DataTable and a diff drawer. */
774
782
  function AuditLogPage(props) {
783
+ const { locale } = useI18n();
775
784
  const { events = auditEvents, filterFields = auditFilterFields, defaultRange, today, onExport = downloadCsv, breadcrumbs = auditBreadcrumbs, currentHref = "/activity", title = "Audit log", description = "Every change made in this workspace: who did it, when, from where and what changed.", shell } = props;
776
785
  const [query, setQuery] = useState("");
777
786
  const [filters, setFilters] = useState([]);
@@ -784,7 +793,7 @@ function AuditLogPage(props) {
784
793
  };
785
794
  });
786
795
  const [diffEvent, setDiffEvent] = useState(null);
787
- const rows = filterAuditEvents(events, query, filters, range);
796
+ const rows = filterAuditEvents(events, query, filters, range, locale);
788
797
  const columns = [
789
798
  {
790
799
  id: "at",
@@ -973,7 +982,7 @@ const meta$2 = {
973
982
  family: "Work",
974
983
  priority: "P2",
975
984
  status: "beta",
976
- description: "Workspace audit trail: FilterBar (search, actor / resource / result filters) and DateRangeFilter over a DataTable of events with actor, action, target and result; a drawer shows the change diff (CodeBlock); export to CSV.",
985
+ description: "Workspace audit trail: FilterBar (search, actor / resource / result filters) and DateRangeFilter over a DataTable of events with actor, action, target and result; a drawer shows the change diff (CodeBlock); export to CSV. CSV export neutralises leading spreadsheet formula triggers.",
977
986
  layout: "SidebarLayout · Page",
978
987
  blocks: [
979
988
  "app-sidebar",
@@ -1933,15 +1942,24 @@ const flowBuilderBreadcrumbs = [{
1933
1942
  * '@gntik-ai/flow/styles.css' once for the React Flow canvas.
1934
1943
  */
1935
1944
  function FlowBuilderPage(props) {
1936
- const { title = flowBuilderContent.title, status = flowBuilderContent.status, nodes, edges, palette, onRun, consoleEntries, defaultConsoleEntries, onConsoleChange, onConsoleClear, onSave, onPublish, nodeTypes, onConnectEdge, onConnect, isValidConnection, onConnectionRejected, validationIssues, mode, nodeStatus, onNodeSelect, codePanel, breadcrumbs = flowBuilderBreadcrumbs, height = 640, fullScreen = true } = props;
1945
+ const { title = flowBuilderContent.title, status = flowBuilderContent.status, nodes = builderNodes, edges = builderEdges, palette, onRun, consoleEntries, defaultConsoleEntries, onConsoleChange, onConsoleClear, onSave, onPublish, nodeTypes, onConnectEdge, onConnect, isValidConnection, onConnectionRejected, validationIssues, mode, nodeStatus, onNodeSelect, graph, onGraphChange, codePanel, breadcrumbs = flowBuilderBreadcrumbs, height = 640, fullScreen = true } = props;
1937
1946
  const [saving, setSaving] = useState(false);
1938
- const [savedNote, setSavedNote] = useState(flowBuilderContent.lastSaved);
1947
+ const [initialGraph] = useState(() => ({
1948
+ nodes,
1949
+ edges
1950
+ }));
1951
+ const [latestGraph, setLatestGraph] = useState();
1952
+ const [saveState, setSaveState] = useState("initial");
1953
+ const revision = useRef(0);
1954
+ const { t } = useI18n();
1955
+ const savedNote = saveState === "initial" ? flowBuilderContent.lastSaved : t(saveState === "unsaved" ? "actionBar.unsaved" : "actionBar.saved");
1939
1956
  const save = async () => {
1940
1957
  setSaving(true);
1958
+ const savedRevision = revision.current;
1941
1959
  try {
1942
- await onSave?.();
1943
- setSavedNote("All changes saved");
1944
- } finally {
1960
+ await onSave?.(cleanFlowGraph(graph ?? latestGraph ?? initialGraph));
1961
+ if (revision.current === savedRevision) setSaveState("saved");
1962
+ } catch {} finally {
1945
1963
  setSaving(false);
1946
1964
  }
1947
1965
  };
@@ -1989,6 +2007,13 @@ function FlowBuilderPage(props) {
1989
2007
  title,
1990
2008
  nodes,
1991
2009
  edges,
2010
+ graph,
2011
+ onGraphChange: (next, change) => {
2012
+ revision.current += 1;
2013
+ setLatestGraph(next);
2014
+ setSaveState("unsaved");
2015
+ onGraphChange?.(next, change);
2016
+ },
1992
2017
  palette,
1993
2018
  onRun,
1994
2019
  consoleEntries,
@@ -2020,7 +2045,7 @@ const meta$60 = {
2020
2045
  family: "Builders",
2021
2046
  priority: "P3",
2022
2047
  status: "experimental",
2023
- description: "Full-screen workflow editor with optional nodeTypes, onConnectEdge, onConnect, isValidConnection, onConnectionRejected, validationIssues, mode=\"run\", nodeStatus and onNodeSelect passed to FlowBuilder; codePanel passes readOnly, markers, onValidityChange, onMonacoReady and loader to an optional CodePanel (branch, parallel, sequence and worker-backed YAML examples). The app must import '@gntik-ai/flow/styles.css' once for the canvas styles. Optional nodeTypes, onConnectEdge, onConnect, isValidConnection, onConnectionRejected, validationIssues, mode, nodeStatus and onNodeSelect pass to FlowBuilder unchanged (see its branch, parallel and sequence examples). mode=\"run\" hides Save, Publish and the saved notice, and preserves focus and node selection. Optional codePanel renders a companion CodePanel and passes all its extensions: readOnly, markers, onValidityChange, onMonacoReady and loader; run mode forces its readOnly to true. For example, WorkflowBuilderPage mode=\"run\" nodeStatus={{step:\"running\"}} onNodeSelect={openDetails}; codePanel={{files,problems:[],loader:yamlLoader,markers}} uses consumer-owned same-origin YAML workers and inline schemas.",
2048
+ description: "onSave(graph) receives the graph on screen (controlled graph, latest reported edit, or initial starter graph), without selected, dragging or measured node fields. Zero-argument handlers remain valid. Optional graph and onGraphChange pass through to FlowBuilder. Committed edits show actionBar.unsaved, a resolved save shows actionBar.saved, and a rejected save retains unsaved status and releases loading. Edits during a pending save remain unsaved. examples/SaveGraph.tsx shows a controlled graph and onSave={(graph) => localStorage.setItem(\"workflow-builder-example\", JSON.stringify(graph))}; hosts own persistence. Full-screen workflow editor with optional nodeTypes, onConnectEdge, onConnect, isValidConnection, onConnectionRejected, validationIssues, mode=\"run\", nodeStatus and onNodeSelect passed to FlowBuilder; codePanel passes readOnly, markers, onValidityChange, onMonacoReady and loader to an optional CodePanel (branch, parallel, sequence and worker-backed YAML examples). The app must import '@gntik-ai/flow/styles.css' once for the canvas styles. Optional nodeTypes, onConnectEdge, onConnect, isValidConnection, onConnectionRejected, validationIssues, mode, nodeStatus and onNodeSelect pass to FlowBuilder unchanged (see its branch, parallel and sequence examples). mode=\"run\" hides Save, Publish and the saved notice, and preserves focus and node selection. Optional codePanel renders a companion CodePanel and passes all its extensions: readOnly, markers, onValidityChange, onMonacoReady and loader; run mode forces its readOnly to true. For example, WorkflowBuilderPage mode=\"run\" nodeStatus={{step:\"running\"}} onNodeSelect={openDetails}; codePanel={{files,problems:[],loader:yamlLoader,markers}} uses consumer-owned same-origin YAML workers and inline schemas.",
2024
2049
  layout: "CanvasLayout",
2025
2050
  blocks: ["flow-builder", "code-panel"]
2026
2051
  };
@@ -3029,23 +3054,15 @@ const detailDangerActions = [{
3029
3054
  //#endregion
3030
3055
  //#region src/resource-detail/Page.tsx
3031
3056
  const TABS = [
3032
- {
3033
- value: "overview",
3034
- label: "Overview"
3035
- },
3036
- {
3037
- value: "activity",
3038
- label: "Activity"
3039
- },
3040
- {
3041
- value: "settings",
3042
- label: "Settings"
3043
- }
3057
+ "overview",
3058
+ "activity",
3059
+ "settings"
3044
3060
  ];
3045
3061
  /** Resource detail: editable tabs by default, or explicit-only Overview when readOnly is true. */
3046
3062
  function ResourceDetailPage(props) {
3063
+ const { t } = useI18n();
3047
3064
  const readOnly = props.readOnly ?? false;
3048
- const { name = readOnly ? "" : detailResource.name, description = readOnly ? "" : detailResource.description, status = readOnly ? "" : detailResource.status, breadcrumbs = readOnly ? [] : detailBreadcrumbs, meta: metaProp, actions: actionsProp, details: detailsProp, overviewTitle = "Configuration", overviewDescription = readOnly ? "" : "Settings of this resource.", historyTitle = "Status history", events = readOnly ? [] : detailEvents, activity = readOnly ? [] : detailActivity, dangerActions = readOnly ? [] : detailDangerActions, defaultTab = "overview", onTabChange, onEditDetails, onSaveSettings, onDangerAction, shell } = props;
3065
+ const { name = readOnly ? "" : detailResource.name, description = readOnly ? "" : detailResource.description, status = readOnly ? "" : detailResource.status, breadcrumbs = readOnly ? [] : detailBreadcrumbs, meta: metaProp, actions: actionsProp, details: detailsProp, overviewTitle = "Configuration", overviewDescription = readOnly ? "" : "Settings of this resource.", historyTitle = "Status history", events = readOnly ? [] : detailEvents, activity = readOnly ? [] : detailActivity, dangerActions = readOnly ? [] : detailDangerActions, defaultTab = "overview", onTabChange, onEditDetails, onSaveSettings, onDangerAction, shell, labels } = props;
3049
3066
  const meta = metaProp ?? (readOnly ? [] : detailMeta);
3050
3067
  const actions = actionsProp ?? (readOnly ? [] : [{
3051
3068
  label: "View logs",
@@ -3068,7 +3085,10 @@ function ResourceDetailPage(props) {
3068
3085
  description
3069
3086
  });
3070
3087
  const dirty = settings.name !== saved.name || settings.description !== saved.description;
3071
- const tabs = readOnly ? TABS.filter((t) => t.value === "overview") : TABS;
3088
+ const tabs = (readOnly ? TABS.filter((value) => value === "overview") : TABS).map((value) => ({
3089
+ value,
3090
+ label: labels?.[value] ?? t(`resourceDetail.${value}`)
3091
+ }));
3072
3092
  return /* @__PURE__ */ jsx(ConsoleShell, {
3073
3093
  currentHref: "/deployments",
3074
3094
  breadcrumbs,
@@ -3093,7 +3113,7 @@ function ResourceDetailPage(props) {
3093
3113
  },
3094
3114
  children: [
3095
3115
  /* @__PURE__ */ jsx(TabsList, {
3096
- "aria-label": "Resource sections",
3116
+ "aria-label": labels?.sections ?? t("resourceDetail.sections"),
3097
3117
  children: tabs.map((t) => /* @__PURE__ */ jsx(TabsTab, {
3098
3118
  value: t.value,
3099
3119
  children: t.label
@@ -3210,7 +3230,7 @@ const meta$30 = {
3210
3230
  family: "Resources",
3211
3231
  priority: "P1",
3212
3232
  status: "beta",
3213
- description: "Resource detail with editable Overview, Activity and Settings tabs by default; optional readOnly (boolean, default false) uses only explicit header and detail props and one Overview tab, with optional history (readonly StatusEvent[], default undefined): omitted hides the history card, [] shows the named empty state, and events render exactly as supplied without invented actors. Read-only ignores activity, events, dangerActions, defaultTab, onSaveSettings and onDangerAction; explicit actions and onEditDetails are honored. The Resource sections tablist retains Tab, arrow, Home/End and Enter/Space keyboard navigation; history is a named group containing the Status history list or empty-state region. Uses token styling in dark (default), light and high_contrast. Examples: runtime-with-transition maps state and lastTransitionAt to one actor-free event, runtime-without-transition maps null to [], and realtime-configuration and postgres-table-detail omit history. ConsoleShell retains sample frame defaults unless explicit shell props are supplied, as in the examples.",
3233
+ description: "Resource detail with editable Overview, Activity and Settings tabs by default; optional readOnly (boolean, default false) uses only explicit header and detail props and one Overview tab, with optional history (readonly StatusEvent[], default undefined): omitted hides the history card, [] shows the named empty state, and events render exactly as supplied without invented actors. Read-only ignores activity, events, dangerActions, defaultTab, onSaveSettings and onDangerAction; explicit actions and onEditDetails are honored. The Resource sections tablist retains Tab, arrow, Home/End and Enter/Space keyboard navigation; history is a named group containing the Status history list or empty-state region. Uses token styling in dark (default), light and high_contrast. Examples: runtime-with-transition maps state and lastTransitionAt to one actor-free event, runtime-without-transition maps null to [], and realtime-configuration and postgres-table-detail omit history. Optional labels?: Partial<ResourceDetailLabels> overrides overview, activity, settings and sections (all strings). Unset keys use I18nProvider (en/es); no provider keeps English. Only the tabs and tablist are localized by this template; fixtures and other section text are unchanged. readOnly keeps only the localized overview tab. See the Catalog labels live preview. ConsoleShell retains sample frame defaults unless explicit shell props are supplied, as in the examples.",
3214
3234
  layout: "SidebarLayout + Page",
3215
3235
  blocks: [
3216
3236
  "app-sidebar",
@@ -3541,19 +3561,18 @@ const galleryColumns = [
3541
3561
  }
3542
3562
  ];
3543
3563
  /** Case-insensitive search over name and description. */
3544
- function searchResources(items, query) {
3545
- const q = query.trim().toLowerCase();
3546
- if (!q) return [...items];
3547
- return items.filter((i) => i.name.toLowerCase().includes(q) || (i.description ?? "").toLowerCase().includes(q));
3564
+ function searchResources(items, query, locale) {
3565
+ return items.filter((i) => matchText(i.name, query, { locale }) || matchText(i.description ?? "", query, { locale }));
3548
3566
  }
3549
3567
  //#endregion
3550
3568
  //#region src/resource-gallery/Page.tsx
3551
3569
  /** Resource gallery: toolbar with search and a grid/table view toggle over the same resources. */
3552
3570
  function ResourceGalleryPage({ title = "Resources", description = "Services, databases and jobs that make up this project.", breadcrumbs = galleryBreadcrumbs, items: initialItems = galleryItems, defaultView = "grid", onViewChange, createLabel = "New resource", onCreate, onAction, shell }) {
3571
+ const { locale } = useI18n();
3553
3572
  const [items, setItems] = useState(initialItems);
3554
3573
  const [view, setView] = useState(defaultView);
3555
3574
  const [query, setQuery] = useState("");
3556
- const visible = searchResources(items, query);
3575
+ const visible = searchResources(items, query, locale);
3557
3576
  const handleAction = (action, item) => {
3558
3577
  if (onAction) onAction(action, item);
3559
3578
  else if (action === "delete") setItems((prev) => prev.filter((i) => i.id !== item.id));
@@ -3728,16 +3747,17 @@ const text = (v) => typeof v === "string" || typeof v === "number" ? String(v).t
3728
3747
  * A filter field reads the column with the same id (its `accessor`), else the row property of that
3729
3748
  * name, so statuses come from the columns.
3730
3749
  */
3731
- function filterResources(rows, query, filters, columns) {
3732
- const q = query.trim().toLowerCase();
3750
+ function filterResources(rows, query, filters, columns, locale) {
3751
+ const q = query.trim();
3733
3752
  const byField = /* @__PURE__ */ new Map();
3734
3753
  for (const f of filters) byField.set(f.field, [...byField.get(f.field) ?? [], f.value.toLowerCase()]);
3735
3754
  return rows.filter((row) => {
3736
3755
  if (q) {
3737
3756
  const r = row;
3738
- let haystack = [r.name, r.id].map(text).filter((v) => v !== void 0);
3739
- if (haystack.length === 0 && columns) haystack = columns.map((c) => text(c.accessor?.(row))).filter((v) => v !== void 0);
3740
- if (!haystack.some((v) => v.includes(q))) return false;
3757
+ const searchText = (v) => typeof v === "string" || typeof v === "number" ? String(v) : void 0;
3758
+ let haystack = [r.name, r.id].map(searchText).filter((v) => v !== void 0);
3759
+ if (haystack.length === 0 && columns) haystack = columns.map((c) => searchText(c.accessor?.(row))).filter((v) => v !== void 0);
3760
+ if (!haystack.some((v) => matchText(v, q, { locale }))) return false;
3741
3761
  }
3742
3762
  for (const [field, values] of byField) {
3743
3763
  const value = text(fieldValue(row, field, columns));
@@ -3758,11 +3778,12 @@ const defaultArchive = (row) => "status" in row ? {
3758
3778
  } : row;
3759
3779
  /** Resource index: header, filter bar, selectable sortable table with pagination, bulk action bar and a no-results state. */
3760
3780
  function ResourceIndexPage({ title = "Deployments", description = "Every service deployed in this workspace, across regions.", breadcrumbs = resourceBreadcrumbs, rows: initialRows = resourceRows, columns = resourceColumns, filterFields = resourceFilterFields, savedViews = resourceSavedViews, getRowId = defaultRowId, getRowHref, onRowClick, linkColumnId, noun = ["deployment", "deployments"], createLabel = "New deployment", onCreate, onOpen, onBulkAction, archiveRow = defaultArchive, loading = false, shell }) {
3781
+ const { locale } = useI18n();
3761
3782
  const [rows, setRows] = useState(initialRows);
3762
3783
  const [query, setQuery] = useState("");
3763
3784
  const [filters, setFilters] = useState([]);
3764
3785
  const [selected, setSelected] = useState([]);
3765
- const visible = filterResources(rows, query, filters, columns);
3786
+ const visible = filterResources(rows, query, filters, columns, locale);
3766
3787
  const visibleSelected = selected.filter((id) => visible.some((r) => getRowId(r) === id));
3767
3788
  const tableColumns = useMemo(() => linkColumns(columns, linkColumnId ?? columns[0]?.id, getRowHref, onRowClick), [
3768
3789
  columns,
@@ -4048,19 +4069,18 @@ const EMPTY_SELECTION = {
4048
4069
  };
4049
4070
  const facetValue = (hit, facet) => facet === "type" ? hit.type : hit.owner;
4050
4071
  /** Hits whose title or snippet contain the query (case-insensitive). */
4051
- function matchQuery(hits, query) {
4052
- const q = query.trim().toLowerCase();
4053
- return q ? hits.filter((h) => `${h.title} ${h.snippet}`.toLowerCase().includes(q)) : [...hits];
4072
+ function matchQuery(hits, query, locale) {
4073
+ return hits.filter((h) => matchText(`${h.title} ${h.snippet}`, query, { locale }));
4054
4074
  }
4055
4075
  /** Search results: ConsoleShell + Page with Checkbox-group facets and results grouped by type. */
4056
4076
  function SearchResultsPage(props) {
4057
- const { t } = useI18n();
4077
+ const { t, locale } = useI18n();
4058
4078
  const { hits = searchHits, facets = searchFacets, defaultQuery = searchDefaultQuery, onQueryChange, onSelectionChange, breadcrumbs = searchBreadcrumbs, currentHref = "/search", shell } = props;
4059
4079
  const id = useId();
4060
4080
  const resultsRef = useRef(null);
4061
4081
  const [query, setQuery] = useState(defaultQuery);
4062
4082
  const [selection, setSelection] = useState(EMPTY_SELECTION);
4063
- const matched = matchQuery(hits, query);
4083
+ const matched = matchQuery(hits, query, locale);
4064
4084
  const results = matched.filter((h) => Object.keys(selection).every((f) => selection[f].length === 0 || selection[f].includes(facetValue(h, f))));
4065
4085
  const groups = searchTypeOrder.map((type) => ({
4066
4086
  type,
@@ -7158,12 +7178,12 @@ const usageSavedViews = [
7158
7178
  * Search by project name; filters match the row field named by the filter id (same field OR,
7159
7179
  * across fields AND). Filters on fields the row does not have are ignored.
7160
7180
  */
7161
- function filterUsage(rows, query, filters) {
7162
- const q = query.trim().toLowerCase();
7181
+ function filterUsage(rows, query, filters, locale) {
7182
+ const q = query.trim();
7163
7183
  const byField = /* @__PURE__ */ new Map();
7164
7184
  for (const f of filters) byField.set(f.field, [...byField.get(f.field) ?? [], f.value]);
7165
7185
  return rows.filter((row) => {
7166
- if (q && !row.project.toLowerCase().includes(q)) return false;
7186
+ if (q && !matchText(row.project, q, { locale })) return false;
7167
7187
  for (const [field, values] of byField) {
7168
7188
  const value = row[field];
7169
7189
  if (value !== void 0 && value !== null && !values.includes(String(value))) return false;
@@ -7286,6 +7306,7 @@ const TRAFFIC_CHART = {
7286
7306
  };
7287
7307
  /** Usage analytics: date range + filters, two charts and a sortable breakdown table with export. */
7288
7308
  function UsageAnalyticsPage({ title = "Usage analytics", description = "Traffic, errors and spend by project and environment.", breadcrumbs = usageBreadcrumbs, rows = usageRows, columns = usageColumns, filterFields = usageFilterFields, searchPlaceholder = "Search projects…", savedViews = usageSavedViews, primaryChart, trafficByRange: traffic = trafficByRange, trafficRanges: ranges = trafficRanges, secondaryChart, onRangeChange, onExport, shell }) {
7309
+ const { locale } = useI18n();
7289
7310
  const secondary = {
7290
7311
  ...TRAFFIC_CHART,
7291
7312
  ...secondaryChart
@@ -7293,7 +7314,7 @@ function UsageAnalyticsPage({ title = "Usage analytics", description = "Traffic,
7293
7314
  const [range, setRange] = useState(null);
7294
7315
  const [query, setQuery] = useState("");
7295
7316
  const [filters, setFilters] = useState([]);
7296
- const visible = filterUsage(rows, query, filters);
7317
+ const visible = filterUsage(rows, query, filters, locale);
7297
7318
  const clearFilters = () => setFilters([]);
7298
7319
  const exportAction = {
7299
7320
  label: "Export CSV",
@@ -12412,15 +12433,14 @@ const helpArticles = [
12412
12433
  }
12413
12434
  ];
12414
12435
  /** Articles whose title or excerpt contains the query. */
12415
- function searchArticles(articles, query) {
12416
- const q = query.trim().toLowerCase();
12417
- if (!q) return [...articles];
12418
- return articles.filter((a) => a.title.toLowerCase().includes(q) || a.excerpt.toLowerCase().includes(q));
12436
+ function searchArticles(articles, query, locale) {
12437
+ return articles.filter((a) => matchText(a.title, query, { locale }) || matchText(a.excerpt, query, { locale }));
12419
12438
  }
12420
12439
  //#endregion
12421
12440
  //#region src/help-home/Page.tsx
12422
12441
  /** Help center home on DocsLayout: search, category cards and popular articles. */
12423
12442
  function HelpHomePage(props) {
12443
+ const { locale } = useI18n();
12424
12444
  const { brand = /* @__PURE__ */ jsx(Logo, {
12425
12445
  size: 22,
12426
12446
  wordmark: true
@@ -12438,7 +12458,7 @@ function HelpHomePage(props) {
12438
12458
  }) } = props;
12439
12459
  const [query, setQuery] = useState("");
12440
12460
  const ids = useId();
12441
- const results = searchArticles(articles, query);
12461
+ const results = searchArticles(articles, query, locale);
12442
12462
  const searching = query.trim() !== "";
12443
12463
  const categoryTitle = (id) => categories.find((c) => c.id === id)?.title ?? id;
12444
12464
  const submit = (e) => {
@@ -13259,7 +13279,7 @@ const shortcutGroups = [
13259
13279
  {
13260
13280
  id: "palette",
13261
13281
  description: "Open the command palette",
13262
- keys: ["⌘", "K"]
13282
+ keys: ["mod", "K"]
13263
13283
  },
13264
13284
  {
13265
13285
  id: "help",
@@ -13269,13 +13289,13 @@ const shortcutGroups = [
13269
13289
  {
13270
13290
  id: "sidebar",
13271
13291
  description: "Collapse or expand the sidebar",
13272
- keys: ["⌘", "B"]
13292
+ keys: ["mod", "B"]
13273
13293
  },
13274
13294
  {
13275
13295
  id: "theme",
13276
13296
  description: "Switch theme",
13277
13297
  keys: [
13278
- "⌘",
13298
+ "mod",
13279
13299
  "⇧",
13280
13300
  "L"
13281
13301
  ]
@@ -13350,12 +13370,12 @@ const shortcutGroups = [
13350
13370
  {
13351
13371
  id: "save",
13352
13372
  description: "Save changes",
13353
- keys: ["⌘", "S"]
13373
+ keys: ["mod", "S"]
13354
13374
  },
13355
13375
  {
13356
13376
  id: "submit",
13357
13377
  description: "Submit a form or comment",
13358
- keys: ["⌘", "↵"]
13378
+ keys: ["mod", "↵"]
13359
13379
  },
13360
13380
  {
13361
13381
  id: "cancel",
@@ -13365,27 +13385,39 @@ const shortcutGroups = [
13365
13385
  {
13366
13386
  id: "undo",
13367
13387
  description: "Undo",
13368
- keys: ["⌘", "Z"]
13388
+ keys: ["mod", "Z"]
13369
13389
  }
13370
13390
  ]
13371
13391
  }
13372
13392
  ];
13373
- /** Groups with only the shortcuts whose description or keys match the query. */
13374
- function filterShortcuts(groups, query) {
13393
+ /** Groups matching descriptions, titles or keys; mod accepts both Command and Ctrl spellings. */
13394
+ function filterShortcuts(groups, query, locale, os = "undetermined") {
13375
13395
  const q = query.trim().toLowerCase();
13376
13396
  if (!q) return [...groups];
13397
+ const modifier = os === "macos" || os === "ios" ? "⌘" : "Ctrl";
13398
+ const alias = modifier === "⌘" ? "Ctrl" : "⌘";
13399
+ const matchesKeys = (shortcut) => {
13400
+ const keys = [...shortcut.keys, ...shortcut.then ?? []];
13401
+ return [
13402
+ "mod",
13403
+ modifier,
13404
+ alias
13405
+ ].some((mod) => keys.map((key) => key === "mod" ? mod : key).join(" ").toLowerCase() === q);
13406
+ };
13377
13407
  return groups.map((g) => ({
13378
13408
  ...g,
13379
- shortcuts: g.shortcuts.filter((s) => s.description.toLowerCase().includes(q) || g.title.toLowerCase().includes(q) || [...s.keys, ...s.then ?? []].join(" ").toLowerCase() === q)
13409
+ shortcuts: g.shortcuts.filter((s) => matchText(s.description, query, { locale }) || matchText(g.title, query, { locale }) || matchesKeys(s))
13380
13410
  })).filter((g) => g.shortcuts.length > 0);
13381
13411
  }
13382
13412
  //#endregion
13383
13413
  //#region src/keyboard-shortcuts/ShortcutSheet.tsx
13384
13414
  /** Searchable shortcut sheet shared by the page and the dialog. */
13385
13415
  function ShortcutSheet({ groups, headingLevel = 2, columns = 2, autoFocus }) {
13416
+ const { locale } = useI18n();
13417
+ const os = useOs();
13386
13418
  const [query, setQuery] = useState("");
13387
13419
  const countId = useId();
13388
- const visible = filterShortcuts(groups, query);
13420
+ const visible = filterShortcuts(groups, query, locale, os);
13389
13421
  const total = visible.reduce((n, g) => n + g.shortcuts.length, 0);
13390
13422
  return /* @__PURE__ */ jsxs(Stack, {
13391
13423
  gap: 5,
@@ -14508,14 +14540,14 @@ function triageFieldValue(i, field) {
14508
14540
  if (field === "owner") return i.owner;
14509
14541
  }
14510
14542
  /** True when the incident matches the search text and every filtered field (OR within a field). */
14511
- function matchesTriage(i, query, filters) {
14512
- const q = query.trim().toLowerCase();
14543
+ function matchesTriage(i, query, filters, locale) {
14544
+ const q = query.trim();
14513
14545
  if (q && ![
14514
14546
  i.id,
14515
14547
  i.title,
14516
14548
  i.service,
14517
14549
  i.owner
14518
- ].some((v) => v.toLowerCase().includes(q))) return false;
14550
+ ].some((v) => matchText(v, q, { locale }))) return false;
14519
14551
  const byField = /* @__PURE__ */ new Map();
14520
14552
  for (const f of filters) byField.set(f.field, [...byField.get(f.field) ?? [], f.value]);
14521
14553
  return [...byField].every(([field, values]) => values.includes(triageFieldValue(i, field) ?? ""));
@@ -14619,6 +14651,7 @@ function IncidentCard({ incident, now }) {
14619
14651
  });
14620
14652
  }
14621
14653
  function IncidentBoard({ title, description, defaultColumns, filterFields, defaultFilters, now, onMove, defaultOpenId, onDeclare, onExport }) {
14654
+ const { locale } = useI18n();
14622
14655
  const announce = useAnnounce();
14623
14656
  const [columns, setColumns] = useState(defaultColumns);
14624
14657
  const [query, setQuery] = useState("");
@@ -14626,7 +14659,7 @@ function IncidentBoard({ title, description, defaultColumns, filterFields, defau
14626
14659
  const [openId, setOpenId] = useState(defaultOpenId);
14627
14660
  const visible = columns.map((c) => ({
14628
14661
  ...c,
14629
- cards: c.cards.filter((i) => matchesTriage(i, query, filters))
14662
+ cards: c.cards.filter((i) => matchesTriage(i, query, filters, locale))
14630
14663
  }));
14631
14664
  const shown = visible.reduce((n, c) => n + c.cards.length, 0);
14632
14665
  const openColumn = columns.find((c) => c.cards.some((i) => i.id === openId));