@gooddata/sdk-ui-kit 11.51.0-alpha.0 → 11.51.0-alpha.2

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 (60) hide show
  1. package/esm/@ui/UiGeneralAccessRadio/UiGeneralAccessRadio.d.ts +3 -2
  2. package/esm/@ui/UiGeneralAccessRadio/UiGeneralAccessRadio.d.ts.map +1 -1
  3. package/esm/@ui/UiGeneralAccessRadio/UiGeneralAccessRadio.js +7 -3
  4. package/esm/@ui/UiGranteeAsyncPicker/UiGranteeAsyncPicker.d.ts +5 -0
  5. package/esm/@ui/UiGranteeAsyncPicker/UiGranteeAsyncPicker.d.ts.map +1 -1
  6. package/esm/@ui/UiGranteeAsyncPicker/UiGranteeAsyncPicker.js +3 -5
  7. package/esm/@ui/UiGranteeRowControls/UiGranteeRowControls.d.ts +10 -16
  8. package/esm/@ui/UiGranteeRowControls/UiGranteeRowControls.d.ts.map +1 -1
  9. package/esm/@ui/UiGranteeRowControls/UiGranteeRowControls.js +19 -32
  10. package/esm/@ui/UiLabelsChecklist/UiLabelsChecklist.d.ts +3 -1
  11. package/esm/@ui/UiLabelsChecklist/UiLabelsChecklist.d.ts.map +1 -1
  12. package/esm/@ui/UiLabelsChecklist/UiLabelsChecklist.js +12 -3
  13. package/esm/@ui/UiObjectShareDialog/UiObjectShareDialogCard.d.ts +9 -3
  14. package/esm/@ui/UiObjectShareDialog/UiObjectShareDialogCard.d.ts.map +1 -1
  15. package/esm/@ui/UiObjectShareDialog/UiObjectShareDialogCard.js +3 -3
  16. package/esm/@ui/UiPermissionMenu/UiPermissionMenu.d.ts +18 -10
  17. package/esm/@ui/UiPermissionMenu/UiPermissionMenu.d.ts.map +1 -1
  18. package/esm/@ui/UiPermissionMenu/UiPermissionMenu.js +45 -13
  19. package/esm/@ui/UiSubmenuHeader/UiSubmenuHeader.d.ts +4 -2
  20. package/esm/@ui/UiSubmenuHeader/UiSubmenuHeader.d.ts.map +1 -1
  21. package/esm/@ui/UiSubmenuHeader/UiSubmenuHeader.js +2 -2
  22. package/esm/@ui/hooks/useCloseOnEscape.d.ts +4 -1
  23. package/esm/@ui/hooks/useCloseOnEscape.d.ts.map +1 -1
  24. package/esm/@ui/hooks/useCloseOnEscape.js +11 -5
  25. package/esm/List/InvertableSelect/InvertableSelectVirtualised.d.ts.map +1 -1
  26. package/esm/List/InvertableSelect/InvertableSelectVirtualised.js +4 -1
  27. package/esm/ParameterControl/StringParameterControlDropdown.js +2 -2
  28. package/esm/Timezone/TimezoneSelect.d.ts +108 -0
  29. package/esm/Timezone/TimezoneSelect.d.ts.map +1 -0
  30. package/esm/Timezone/TimezoneSelect.js +90 -0
  31. package/esm/Timezone/timezones.d.ts +86 -0
  32. package/esm/Timezone/timezones.d.ts.map +1 -0
  33. package/esm/Timezone/timezones.js +253 -0
  34. package/esm/index.d.ts +3 -2
  35. package/esm/index.d.ts.map +1 -1
  36. package/esm/index.js +3 -2
  37. package/esm/locales.d.ts +9 -6
  38. package/esm/locales.d.ts.map +1 -1
  39. package/esm/locales.js +4 -3
  40. package/esm/sdk-ui-kit.d.ts +242 -62
  41. package/package.json +11 -11
  42. package/src/@ui/UiGranteeRowControls/UiGranteeRowControls.scss +0 -8
  43. package/src/@ui/UiLabelsChecklist/UiLabelsChecklist.scss +2 -2
  44. package/src/@ui/UiPermissionMenu/UiPermissionMenu.scss +5 -0
  45. package/src/@ui/index.scss +0 -1
  46. package/src/Timezone/TimezoneSelect.scss +22 -0
  47. package/styles/css/button.css.map +1 -1
  48. package/styles/css/header.css +3 -3
  49. package/styles/css/header.css.map +1 -1
  50. package/styles/css/main.css +192 -187
  51. package/styles/css/main.css.map +1 -1
  52. package/styles/scss/Button/_mixins.scss +5 -157
  53. package/styles/scss/Button/_placeholders.scss +169 -0
  54. package/styles/scss/button.scss +1 -0
  55. package/styles/scss/header.scss +8 -3
  56. package/styles/scss/main.scss +1 -0
  57. package/esm/@ui/UiMoreOptionsMenu/UiMoreOptionsMenu.d.ts +0 -28
  58. package/esm/@ui/UiMoreOptionsMenu/UiMoreOptionsMenu.d.ts.map +0 -1
  59. package/esm/@ui/UiMoreOptionsMenu/UiMoreOptionsMenu.js +0 -85
  60. package/src/@ui/UiMoreOptionsMenu/UiMoreOptionsMenu.scss +0 -10
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Hook to trigger a callback when pressing the Escape key.
3
3
  *
4
+ * @param capture - When true, listens in the capture phase and stops propagation, so a nested
5
+ * overlay can dismiss on Escape without an ancestor (e.g. a Dropdown) also closing.
6
+ *
4
7
  * @internal
5
8
  */
6
- export declare function useCloseOnEscape(isOpen: boolean, onClose: () => void): void;
9
+ export declare function useCloseOnEscape(isOpen: boolean, onClose: () => void, capture?: boolean): void;
7
10
  //# sourceMappingURL=useCloseOnEscape.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCloseOnEscape.d.ts","sourceRoot":"","sources":["../../../src/@ui/hooks/useCloseOnEscape.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAe3E"}
1
+ {"version":3,"file":"useCloseOnEscape.d.ts","sourceRoot":"","sources":["../../../src/@ui/hooks/useCloseOnEscape.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,UAAQ,GAAG,IAAI,CAkB5F"}
@@ -1,21 +1,27 @@
1
- // (C) 2025 GoodData Corporation
1
+ // (C) 2025-2026 GoodData Corporation
2
2
  import { useEffect } from "react";
3
3
  /**
4
4
  * Hook to trigger a callback when pressing the Escape key.
5
5
  *
6
+ * @param capture - When true, listens in the capture phase and stops propagation, so a nested
7
+ * overlay can dismiss on Escape without an ancestor (e.g. a Dropdown) also closing.
8
+ *
6
9
  * @internal
7
10
  */
8
- export function useCloseOnEscape(isOpen, onClose) {
11
+ export function useCloseOnEscape(isOpen, onClose, capture = false) {
9
12
  useEffect(() => {
10
13
  if (!isOpen) {
11
14
  return undefined;
12
15
  }
13
16
  const handleKeyDown = (event) => {
14
17
  if (event.key === "Escape") {
18
+ if (capture) {
19
+ event.stopPropagation();
20
+ }
15
21
  onClose();
16
22
  }
17
23
  };
18
- document.addEventListener("keydown", handleKeyDown);
19
- return () => document.removeEventListener("keydown", handleKeyDown);
20
- }, [isOpen, onClose]);
24
+ document.addEventListener("keydown", handleKeyDown, { capture });
25
+ return () => document.removeEventListener("keydown", handleKeyDown, { capture });
26
+ }, [isOpen, onClose, capture]);
21
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"InvertableSelectVirtualised.d.ts","sourceRoot":"","sources":["../../../src/List/InvertableSelect/InvertableSelectVirtualised.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,SAAS,EAKjB,MAAM,OAAO,CAAC;AAuBf,OAAO,EACH,KAAK,iCAAiC,EACtC,KAAK,mCAAmC,EACxC,KAAK,kCAAkC,EACvC,KAAK,qCAAqC,EAC1C,KAAK,qCAAqC,EAC7C,MAAM,uBAAuB,CAAC;AAe/B;;;;GAIG;AACH,MAAM,WAAW,2CAA2C,CAAC,CAAC;IAC1D;;OAEG;IACH,IAAI,EAAE,CAAC,CAAC;IAER;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,QAAQ,EAAE,MAAM,IAAI,CAAC;IAErB;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB;;OAEG;IACH,YAAY,EAAE,MAAM,IAAI,CAAC;IAEzB;;OAEG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAEjC;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,8CAA8C;IAC3D,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,iCAAiC,CAAC,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAClC,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAEhC,yBAAyB,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;IAEjD,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,CAAC,EAAE,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,2BAA2B,CAAC,EAAE,MAAM,CAAC;IAErC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAEpC,KAAK,CAAC,EAAE,GAAG,CAAC;IAEZ,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAE5B,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEhC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,iCAAiC,KAAK,YAAY,CAAC;IACzE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,mCAAmC,KAAK,YAAY,CAAC;IAC7E,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,qCAAqC,KAAK,YAAY,CAAC;IACjF,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,kCAAkC,KAAK,YAAY,CAAC;IAC3E,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,2CAA2C,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC;IACrF,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,qCAAqC,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC;IACpF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,8CAA8C,KAAK,YAAY,CAAC;CAC3F;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,KAAK,EAAE,iCAAiC,CAAC,CAAC,CAAC,2CAwYzF"}
1
+ {"version":3,"file":"InvertableSelectVirtualised.d.ts","sourceRoot":"","sources":["../../../src/List/InvertableSelect/InvertableSelectVirtualised.tsx"],"names":[],"mappings":"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"}
@@ -133,7 +133,9 @@ export function InvertableSelectVirtualised(props) {
133
133
  item,
134
134
  title: getItemTitle(item),
135
135
  isSelected: getIsItemSelected(item),
136
- focusedAction: item === focusedItem ? focusedAction : undefined,
136
+ // `focusedAction` persists across blur (so Tab-back-in resumes sensibly), so gate
137
+ // it on `isFocusWithin` here to avoid reporting a stale focus to consumers.
138
+ focusedAction: isFocusWithin && item === focusedItem ? focusedAction : undefined,
137
139
  listRef: containerRef,
138
140
  index: items.indexOf(item) + 1,
139
141
  itemsCount: items.length,
@@ -144,6 +146,7 @@ export function InvertableSelectVirtualised(props) {
144
146
  getIsItemSelected,
145
147
  focusedItem,
146
148
  focusedAction,
149
+ isFocusWithin,
147
150
  containerRef,
148
151
  items,
149
152
  selectItems,
@@ -22,10 +22,10 @@ export function StringParameterControlDropdown({ name, value, resetValue, constr
22
22
  return (_jsx(ParameterControlDropdown, { name: name, draft: draft, onDraftChange: setDraft, inputType: "text", inputId: inputId, ariaAttributes: ariaAttributes, errorMessage: error ? (_jsx(FormattedMessage, { ...error, values: { minLength: constraints?.minLength, maxLength: constraints?.maxLength } })) : undefined, onReset: showReset ? () => setDraft(resetValue) : undefined, onApply: () => onApply(draft), onCancel: onCancel }));
23
23
  }
24
24
  function getStringDraftValidationError(draft, constraints) {
25
- if (isValidStringParameterValue(draft, constraints)) {
25
+ const { minLength, maxLength } = constraints ?? {};
26
+ if (isValidStringParameterValue(draft, { minLength, maxLength })) {
26
27
  return undefined;
27
28
  }
28
- const { minLength } = constraints ?? {};
29
29
  return minLength !== undefined && draft.length < minLength
30
30
  ? messages.errorTooShort
31
31
  : messages.errorTooLong;
@@ -0,0 +1,108 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import { type IUiListboxItem } from "../@ui/UiListbox/types.js";
3
+ import { type IDropdownButtonRenderProps } from "../Dropdown/Dropdown.js";
4
+ /**
5
+ * Special item rendered above the divider in {@link TimezoneSelect}, ahead of the curated
6
+ * timezone list. Callers use these for entries like "Default (workspace)" or "From browser (...)".
7
+ *
8
+ * @internal
9
+ */
10
+ export interface ITimezoneSelectSpecialItem {
11
+ /**
12
+ * Value emitted by onChange when this item is selected. Undefined typically represents
13
+ * "no explicit timezone" (e.g. inherit the workspace setting).
14
+ */
15
+ id: string | undefined;
16
+ /**
17
+ * Display label of the item.
18
+ */
19
+ label: string;
20
+ }
21
+ /**
22
+ * Render props passed to a custom trigger renderer of {@link TimezoneSelect}.
23
+ *
24
+ * Extends the {@link IDropdownButtonRenderProps} contract of the underlying dropdown
25
+ * (toggle/open/close callbacks, isOpen, buttonRef and accessibility props) with the
26
+ * current selection so that custom triggers can display it.
27
+ *
28
+ * @internal
29
+ */
30
+ export interface ITimezoneSelectButtonRenderProps extends IDropdownButtonRenderProps {
31
+ /**
32
+ * Human-readable label of the current selection: the matching special item label,
33
+ * the selected timezone as "name (offset)" (e.g. "Prague (GMT+01:00)"),
34
+ * or the placeholder when nothing is selected.
35
+ */
36
+ buttonLabel: string;
37
+ /**
38
+ * Currently selected timezone ID (IANA ID, a special item ID or undefined).
39
+ */
40
+ value: string | undefined;
41
+ /**
42
+ * Whether the select is disabled. Custom triggers should reflect this state.
43
+ */
44
+ isDisabled?: boolean;
45
+ }
46
+ /**
47
+ * All label props are required so that callers pass localized strings — the component
48
+ * has no built-in English defaults to leak into the UI.
49
+ *
50
+ * @internal
51
+ */
52
+ export interface ITimezoneSelectProps {
53
+ /**
54
+ * Currently selected timezone ID (IANA ID, a special item ID or undefined).
55
+ */
56
+ value?: string;
57
+ /**
58
+ * Called when the user selects a timezone.
59
+ */
60
+ onChange: (id: string | undefined) => void;
61
+ /**
62
+ * Items rendered above the divider, ahead of the curated timezone list.
63
+ */
64
+ specialItems?: ITimezoneSelectSpecialItem[];
65
+ /**
66
+ * Label of the dropdown button when nothing is selected and no special item matches.
67
+ */
68
+ placeholder?: string;
69
+ /**
70
+ * Placeholder of the search input.
71
+ */
72
+ searchPlaceholder: string;
73
+ /**
74
+ * Accessibility label of the search input and the listbox.
75
+ */
76
+ ariaLabel: string;
77
+ /**
78
+ * Label shown when the search yields no results.
79
+ */
80
+ noMatchLabel: string;
81
+ isDisabled?: boolean;
82
+ /**
83
+ * Optional content rendered above the search input (e.g. a dialog-like header).
84
+ */
85
+ header?: ReactNode;
86
+ /**
87
+ * Custom renderer of the dropdown trigger. When not provided, a standard dropdown
88
+ * button showing the current selection is rendered.
89
+ *
90
+ * Use this to embed the select in surfaces where the default button does not fit,
91
+ * e.g. as an item of an options menu. The renderer receives the dropdown render props
92
+ * ({@link IDropdownButtonRenderProps}: toggleDropdown, isOpen, buttonRef, accessibility
93
+ * props, ...) together with the current selection ({@link ITimezoneSelectButtonRenderProps}).
94
+ */
95
+ renderButton?: (props: ITimezoneSelectButtonRenderProps) => ReactNode;
96
+ }
97
+ type TimezoneListItemData = string | undefined;
98
+ export declare function buildListboxItems(specialItems: ITimezoneSelectSpecialItem[], searchString: string): IUiListboxItem<TimezoneListItemData>[];
99
+ export declare function getSelectedItemId(value: string | undefined, specialItems: ITimezoneSelectSpecialItem[]): string | undefined;
100
+ /**
101
+ * Searchable dropdown for picking a timezone from the curated list, with optional special
102
+ * items (e.g. "Default (workspace)", "From browser (...)") rendered above a divider.
103
+ *
104
+ * @internal
105
+ */
106
+ export declare function TimezoneSelect({ value, onChange, specialItems, placeholder, searchPlaceholder, ariaLabel, noMatchLabel, isDisabled, header, renderButton }: ITimezoneSelectProps): ReactElement;
107
+ export {};
108
+ //# sourceMappingURL=TimezoneSelect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimezoneSelect.d.ts","sourceRoot":"","sources":["../../src/Timezone/TimezoneSelect.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAqB,MAAM,OAAO,CAAC;AAG7E,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhG,OAAO,EAAY,KAAK,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAYpF;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IACvC;;;OAGG;IACH,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;IAEvB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gCAAiC,SAAQ,0BAA0B;IAChF;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAE1B;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACjC;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IAE3C;;OAEG;IACH,YAAY,CAAC,EAAE,0BAA0B,EAAE,CAAC;IAE5C;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,SAAS,CAAC;CACzE;AAED,KAAK,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAAC;AAM/C,wBAAgB,iBAAiB,CAC7B,YAAY,EAAE,0BAA0B,EAAE,EAC1C,YAAY,EAAE,MAAM,GACrB,cAAc,CAAC,oBAAoB,CAAC,EAAE,CA6BxC;AAGD,wBAAgB,iBAAiB,CAC7B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,YAAY,EAAE,0BAA0B,EAAE,GAC3C,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,YAAiB,EACjB,WAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,YAAY,EACZ,UAAU,EACV,MAAM,EACN,YAAY,EACf,EAAE,oBAAoB,GAAG,YAAY,CA2FrC"}
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useMemo, useState } from "react";
4
+ import { UiButton } from "../@ui/UiButton/UiButton.js";
5
+ import { UiListbox } from "../@ui/UiListbox/UiListbox.js";
6
+ import { Dropdown } from "../Dropdown/Dropdown.js";
7
+ import { Input } from "../Form/Input.js";
8
+ import { NoData } from "../NoData/NoData.js";
9
+ import { getTimezoneDisplayLabel, getTimezones, timezoneMatchesSearch } from "./timezones.js";
10
+ const DROPDOWN_ALIGN_POINTS = [{ align: "bl tl" }, { align: "tl bl" }];
11
+ const DROPDOWN_BODY_WIDTH = 300;
12
+ // ~10 rows of 28px compact list items visible before the list scrolls
13
+ const LIST_MAX_HEIGHT = 280;
14
+ // special items may carry an undefined id, so listbox entries need a synthetic unique id
15
+ const SPECIAL_ITEM_ID_PREFIX = "special-item-";
16
+ // exported for tests
17
+ export function buildListboxItems(specialItems, searchString) {
18
+ const search = searchString.trim().toLowerCase();
19
+ const specialListItems = specialItems
20
+ // keep the pre-filter index — getSelectedItemId derives the selected id from the
21
+ // unfiltered array, so filtered-out items must not shift the ids of the remaining ones
22
+ .map((item, index) => ({ item, index }))
23
+ .filter(({ item }) => !search || item.label.toLowerCase().includes(search))
24
+ .map(({ item, index }) => ({
25
+ type: "interactive",
26
+ id: `${SPECIAL_ITEM_ID_PREFIX}${index}`,
27
+ stringTitle: item.label,
28
+ data: item.id,
29
+ }));
30
+ const timezoneListItems = getTimezones()
31
+ .filter((timezone) => timezoneMatchesSearch(timezone, search))
32
+ .map((timezone) => ({
33
+ type: "interactive",
34
+ id: timezone.id,
35
+ stringTitle: getTimezoneDisplayLabel(timezone.id),
36
+ // the label shows the friendly name; surface the IANA ID on hover
37
+ tooltip: timezone.id,
38
+ data: timezone.id,
39
+ }));
40
+ return specialListItems.length > 0 && timezoneListItems.length > 0
41
+ ? [...specialListItems, { type: "separator" }, ...timezoneListItems]
42
+ : [...specialListItems, ...timezoneListItems];
43
+ }
44
+ // exported for tests
45
+ export function getSelectedItemId(value, specialItems) {
46
+ const specialIndex = specialItems.findIndex((item) => item.id === value);
47
+ if (specialIndex >= 0) {
48
+ return `${SPECIAL_ITEM_ID_PREFIX}${specialIndex}`;
49
+ }
50
+ return value;
51
+ }
52
+ /**
53
+ * Searchable dropdown for picking a timezone from the curated list, with optional special
54
+ * items (e.g. "Default (workspace)", "From browser (...)") rendered above a divider.
55
+ *
56
+ * @internal
57
+ */
58
+ export function TimezoneSelect({ value, onChange, specialItems = [], placeholder = "", searchPlaceholder, ariaLabel, noMatchLabel, isDisabled, header, renderButton, }) {
59
+ const [searchString, setSearchString] = useState("");
60
+ const selectedSpecialItem = specialItems.find((item) => item.id === value);
61
+ const buttonLabel = selectedSpecialItem
62
+ ? selectedSpecialItem.label
63
+ : value === undefined
64
+ ? placeholder
65
+ : getTimezoneDisplayLabel(value);
66
+ const items = useMemo(() => buildListboxItems(specialItems, searchString), [specialItems, searchString]);
67
+ const hasNoMatchingData = items.length === 0;
68
+ return (_jsx(Dropdown, { closeOnParentScroll: true, alignPoints: DROPDOWN_ALIGN_POINTS, className: "gd-timezone-select s-timezone-select", onOpenStateChanged: (isOpen) => {
69
+ if (!isOpen) {
70
+ setSearchString("");
71
+ }
72
+ }, renderButton: (renderProps) => renderButton ? (renderButton({ ...renderProps, buttonLabel, value, isDisabled })) : (_jsx(UiButton, { label: buttonLabel, onClick: renderProps.toggleDropdown, isDisabled: isDisabled, iconAfter: renderProps.isOpen ? "navigateUp" : "navigateDown", size: "small", dataTestId: "s-timezone-select-button", accessibilityConfig: {
73
+ ariaLabel,
74
+ ariaExpanded: renderProps.isOpen,
75
+ } })), renderBody: ({ closeDropdown, ariaAttributes }) => (_jsxs("div", { className: "gd-timezone-select__body s-timezone-select-body", children: [header, _jsx("div", { className: "gd-timezone-select__search s-timezone-select-search", children: _jsx(Input, { value: searchString, onChange: (newValue) => setSearchString(newValue.toString()), placeholder: searchPlaceholder, autofocus: true, clearOnEsc: true, isSearch: true, isSmall: true, onEscKeyPress: (event) => {
76
+ if (searchString) {
77
+ event.stopPropagation();
78
+ }
79
+ else {
80
+ closeDropdown();
81
+ }
82
+ }, accessibilityConfig: {
83
+ ariaLabel,
84
+ } }) }), hasNoMatchingData ? (_jsx(NoData, { hasNoMatchingData: true, notFoundLabel: noMatchLabel, noDataLabel: noMatchLabel })) : (_jsx(UiListbox
85
+ // remount when the filter changes so the focused index cannot point
86
+ // past the end of a shortened result list, which would break keyboard selection
87
+ , { width: DROPDOWN_BODY_WIDTH, maxHeight: LIST_MAX_HEIGHT, items: items, selectedItemId: getSelectedItemId(value, specialItems), onSelect: (item) => {
88
+ onChange(item.data);
89
+ }, onClose: closeDropdown, ariaAttributes: ariaAttributes, isCompact: true, dataTestId: "s-timezone-select-list", itemDataTestId: "s-timezone-select-item" }, searchString))] })), overlayPositionType: "sameAsTarget" }));
90
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Item describing a single timezone from the curated list.
3
+ *
4
+ * @internal
5
+ */
6
+ export interface ITimezoneItem {
7
+ /**
8
+ * IANA timezone ID, e.g. "Europe/Prague".
9
+ */
10
+ id: string;
11
+ /**
12
+ * Human readable name without the offset, e.g. "Prague" or "Pacific Time (US & Canada)".
13
+ */
14
+ name: string;
15
+ /**
16
+ * GMT offset label, e.g. "GMT+01:00".
17
+ */
18
+ offsetLabel: string;
19
+ /**
20
+ * Offset from UTC in minutes as observed in January (northern hemisphere winter).
21
+ */
22
+ januaryOffset: number;
23
+ /**
24
+ * Offset from UTC in minutes as observed in June (northern hemisphere summer).
25
+ */
26
+ juneOffset: number;
27
+ }
28
+ /**
29
+ * Returns the full display label of a timezone, combining the offset and the name,
30
+ * e.g. "GMT+01:00 Prague". Use only where the merged label is actually displayed;
31
+ * consumers that need the name or the offset alone should read the fields directly.
32
+ *
33
+ * @internal
34
+ */
35
+ export declare function getTimezoneTitle(item: Pick<ITimezoneItem, "name" | "offsetLabel">): string;
36
+ /**
37
+ * Returns the curated list of timezones.
38
+ *
39
+ * @internal
40
+ */
41
+ export declare function getTimezones(): ITimezoneItem[];
42
+ /**
43
+ * Returns the timezone from the curated list matching the provided IANA timezone ID,
44
+ * or undefined when the ID is not in the curated list.
45
+ *
46
+ * @internal
47
+ */
48
+ export declare function getTimezoneById(id: string | undefined): ITimezoneItem | undefined;
49
+ /**
50
+ * Returns the curated timezone that matches the offsets of the browser/runtime timezone.
51
+ * Falls back to UTC when no curated timezone matches.
52
+ *
53
+ * @internal
54
+ */
55
+ export declare function getUserTimezone(): ITimezoneItem;
56
+ /**
57
+ * Returns the display label used by the timezone picker: the friendly name with the GMT offset
58
+ * as a suffix in brackets, e.g. "Prague (GMT+01:00)". The name falls back to the raw IANA ID
59
+ * for zones outside the curated list, and the offset suffix is dropped when not known.
60
+ *
61
+ * The friendly name (not the IANA ID) is shown so the picker reads the same as the Home UI
62
+ * timezone settings — the id-to-name mapping is not visible to users anywhere.
63
+ *
64
+ * @internal
65
+ */
66
+ export declare function getTimezoneDisplayLabel(id: string): string;
67
+ /**
68
+ * Tests whether the timezone matches the search string. Matches on the picker display label
69
+ * ("name (offset)"), the friendly title (offset + name) and the IANA ID, with underscores
70
+ * treated as spaces so that e.g. "Los Angeles" finds "America/Los_Angeles".
71
+ *
72
+ * @internal
73
+ */
74
+ export declare function timezoneMatchesSearch(timezone: ITimezoneItem, search: string): boolean;
75
+ /**
76
+ * Returns display labels for the provided IANA timezone ID.
77
+ *
78
+ * @remarks
79
+ * The backend accepts any ICU-known timezone, so the ID may not be present in the curated list.
80
+ * In that case the raw ID is used as the name and the offset label is computed via Intl
81
+ * (empty when the runtime does not know the timezone either).
82
+ *
83
+ * @internal
84
+ */
85
+ export declare function getTimezoneLabels(id: string): Pick<ITimezoneItem, "name" | "offsetLabel">;
86
+ //# sourceMappingURL=timezones.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezones.d.ts","sourceRoot":"","sources":["../../src/Timezone/timezones.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC1B;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;CACtB;AAkJD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,MAAM,CAE1F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,aAAa,EAAE,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,aAAa,CAkB/C;AAED;;;;;;;;;GASG;AAGH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAWtF;AAmBD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,aAAa,CAAC,CAUzF"}
@@ -0,0 +1,253 @@
1
+ // (C) 2026 GoodData Corporation
2
+ const timezonesRaw = [
3
+ ["Pacific/Midway", "International Date Line West", "GMT-11:00", -660, -660],
4
+ ["Pacific/Pago_Pago", "Samoa", "GMT-11:00", -660, -660],
5
+ ["Pacific/Honolulu", "Hawaii", "GMT-10:00", -600, -600],
6
+ ["America/Juneau", "Alaska", "GMT-09:00", -540, -480],
7
+ ["America/Los_Angeles", "Pacific Time (US & Canada)", "GMT-08:00", -480, -420],
8
+ ["America/Tijuana", "Tijuana", "GMT-08:00", -480, -420],
9
+ ["America/Phoenix", "Arizona", "GMT-07:00", -420, -420],
10
+ ["America/Chihuahua", "Chihuahua", "GMT-07:00", -420, -360],
11
+ ["America/Mazatlan", "Mazatlan", "GMT-07:00", -420, -360],
12
+ ["America/Denver", "Mountain Time (US & Canada)", "GMT-07:00", -420, -360],
13
+ ["America/Guatemala", "Central America", "GMT-06:00", -360, -360],
14
+ ["America/Chicago", "Central Time (US & Canada)", "GMT-06:00", -360, -300],
15
+ ["America/Winnipeg", "Central - ON (west); Manitoba", "GMT-06:00", -360, -300],
16
+ ["America/Mexico_City", "Mexico City", "GMT-06:00", -360, -300],
17
+ ["America/Monterrey", "Monterrey", "GMT-06:00", -360, -300],
18
+ ["America/Regina", "Saskatchewan", "GMT-06:00", -360, -360],
19
+ ["America/Bogota", "Bogota", "GMT-05:00", -300, -300],
20
+ ["America/New_York", "Eastern Time (US & Canada)", "GMT-05:00", -300, -240],
21
+ ["America/Indiana/Indianapolis", "Indiana (East)", "GMT-05:00", -300, -240],
22
+ ["America/Lima", "Lima", "GMT-05:00", -300, -300],
23
+ ["America/Caracas", "Caracas", "GMT-04:30", -270, -270],
24
+ ["America/Halifax", "Atlantic Time (Canada)", "GMT-04:00", -240, -180],
25
+ ["America/Guyana", "Georgetown", "GMT-04:00", -240, -240],
26
+ ["America/La_Paz", "La Paz", "GMT-04:00", -240, -240],
27
+ ["America/Santiago", "Santiago", "GMT-04:00", -180, -240],
28
+ ["America/St_Johns", "Newfoundland", "GMT-03:30", -210, -150],
29
+ ["America/Sao_Paulo", "Brasilia", "GMT-03:00", -120, -180],
30
+ ["America/Argentina/Buenos_Aires", "Buenos Aires", "GMT-03:00", -180, -180],
31
+ ["America/Godthab", "Greenland", "GMT-03:00", -180, -120],
32
+ ["Atlantic/South_Georgia", "Mid-Atlantic", "GMT-02:00", -120, -120],
33
+ ["Atlantic/Azores", "Azores", "GMT-01:00", -60, 0],
34
+ ["Atlantic/Cape_Verde", "Cape Verde Is.", "GMT-01:00", -60, -60],
35
+ ["Africa/Casablanca", "Casablanca", "GMT+00:00", 0, 0],
36
+ ["Europe/Dublin", "Dublin", "GMT+00:00", 0, 60],
37
+ ["Europe/Lisbon", "Lisbon", "GMT+00:00", 0, 60],
38
+ ["Europe/London", "London", "GMT+00:00", 0, 60],
39
+ ["Africa/Monrovia", "Monrovia", "GMT+00:00", 0, 0],
40
+ ["Etc/UTC", "UTC", "GMT+00:00", 0, 0],
41
+ ["Europe/Amsterdam", "Amsterdam", "GMT+01:00", 60, 120],
42
+ ["Europe/Belgrade", "Belgrade", "GMT+01:00", 60, 120],
43
+ ["Europe/Berlin", "Berlin", "GMT+01:00", 60, 120],
44
+ ["Europe/Bratislava", "Bratislava", "GMT+01:00", 60, 120],
45
+ ["Europe/Brussels", "Brussels", "GMT+01:00", 60, 120],
46
+ ["Europe/Budapest", "Budapest", "GMT+01:00", 60, 120],
47
+ ["Europe/Copenhagen", "Copenhagen", "GMT+01:00", 60, 120],
48
+ ["Europe/Ljubljana", "Ljubljana", "GMT+01:00", 60, 120],
49
+ ["Europe/Madrid", "Madrid", "GMT+01:00", 60, 120],
50
+ ["Europe/Paris", "Paris", "GMT+01:00", 60, 120],
51
+ ["Europe/Prague", "Prague", "GMT+01:00", 60, 120],
52
+ ["Europe/Rome", "Rome", "GMT+01:00", 60, 120],
53
+ ["Europe/Sarajevo", "Sarajevo", "GMT+01:00", 60, 120],
54
+ ["Europe/Skopje", "Skopje", "GMT+01:00", 60, 120],
55
+ ["Europe/Stockholm", "Stockholm", "GMT+01:00", 60, 120],
56
+ ["Europe/Vienna", "Vienna", "GMT+01:00", 60, 120],
57
+ ["Europe/Warsaw", "Warsaw", "GMT+01:00", 60, 120],
58
+ ["Africa/Algiers", "West Central Africa", "GMT+01:00", 60, 60],
59
+ ["Europe/Zagreb", "Zagreb", "GMT+01:00", 60, 120],
60
+ ["Europe/Athens", "Athens", "GMT+02:00", 120, 180],
61
+ ["Europe/Bucharest", "Bucharest", "GMT+02:00", 120, 180],
62
+ ["Africa/Cairo", "Cairo", "GMT+02:00", 120, 180],
63
+ ["Africa/Harare", "Harare", "GMT+02:00", 120, 120],
64
+ ["Europe/Helsinki", "Helsinki", "GMT+02:00", 120, 180],
65
+ ["Europe/Istanbul", "Istanbul", "GMT+02:00", 120, 180],
66
+ ["Asia/Jerusalem", "Jerusalem", "GMT+02:00", 120, 180],
67
+ ["Europe/Kiev", "Kyiv", "GMT+02:00", 120, 180],
68
+ ["Europe/Minsk", "Minsk", "GMT+02:00", 120, 180],
69
+ ["Africa/Johannesburg", "Pretoria", "GMT+02:00", 120, 120],
70
+ ["Europe/Riga", "Riga", "GMT+02:00", 120, 180],
71
+ ["Europe/Sofia", "Sofia", "GMT+02:00", 120, 180],
72
+ ["Europe/Tallinn", "Tallinn", "GMT+02:00", 120, 180],
73
+ ["Europe/Vilnius", "Vilnius", "GMT+02:00", 120, 180],
74
+ ["Asia/Baghdad", "Baghdad", "GMT+03:00", 180, 180],
75
+ ["Asia/Kuwait", "Kuwait", "GMT+03:00", 180, 180],
76
+ ["Europe/Moscow", "Moscow", "GMT+03:00", 180, 240],
77
+ ["Africa/Nairobi", "Nairobi", "GMT+03:00", 180, 180],
78
+ ["Asia/Riyadh", "Riyadh", "GMT+03:00", 180, 180],
79
+ ["Asia/Tehran", "Tehran", "GMT+03:30", 210, 270],
80
+ ["Asia/Baku", "Baku", "GMT+04:00", 240, 300],
81
+ ["Asia/Muscat", "Muscat", "GMT+04:00", 240, 240],
82
+ ["Asia/Tbilisi", "Tbilisi", "GMT+04:00", 240, 240],
83
+ ["Asia/Yerevan", "Yerevan", "GMT+04:00", 240, 300],
84
+ ["Asia/Kabul", "Kabul", "GMT+04:30", 270, 270],
85
+ ["Asia/Yekaterinburg", "Ekaterinburg", "GMT+05:00", 300, 360],
86
+ ["Asia/Karachi", "Karachi", "GMT+05:00", 300, 300],
87
+ ["Asia/Tashkent", "Tashkent", "GMT+05:00", 300, 300],
88
+ ["Asia/Kolkata", "Kolkata", "GMT+05:30", 330, 330],
89
+ ["Asia/Colombo", "Sri Jayawardenepura", "GMT+05:30", 330, 330],
90
+ ["Asia/Kathmandu", "Kathmandu", "GMT+05:45", 345, 345],
91
+ ["Asia/Almaty", "Almaty", "GMT+06:00", 360, 360],
92
+ ["Asia/Dhaka", "Dhaka", "GMT+06:00", 360, 360],
93
+ ["Asia/Novosibirsk", "Novosibirsk", "GMT+06:00", 360, 420],
94
+ ["Asia/Rangoon", "Rangoon", "GMT+06:30", 390, 390],
95
+ ["Asia/Bangkok", "Bangkok", "GMT+07:00", 420, 420],
96
+ ["Asia/Jakarta", "Jakarta", "GMT+07:00", 420, 420],
97
+ ["Asia/Krasnoyarsk", "Krasnoyarsk", "GMT+07:00", 420, 480],
98
+ ["Asia/Shanghai", "Beijing", "GMT+08:00", 480, 480],
99
+ ["Asia/Chongqing", "Chongqing", "GMT+08:00", 480, 480],
100
+ ["Asia/Hong_Kong", "Hong Kong", "GMT+08:00", 480, 480],
101
+ ["Asia/Irkutsk", "Irkutsk", "GMT+08:00", 480, 540],
102
+ ["Asia/Kuala_Lumpur", "Kuala Lumpur", "GMT+08:00", 480, 480],
103
+ ["Australia/Perth", "Perth", "GMT+08:00", 480, 480],
104
+ ["Asia/Singapore", "Singapore", "GMT+08:00", 480, 480],
105
+ ["Asia/Taipei", "Taipei", "GMT+08:00", 480, 480],
106
+ ["Asia/Ulaanbaatar", "Ulaanbaatar", "GMT+08:00", 480, 480],
107
+ ["Asia/Urumqi", "Urumqi", "GMT+08:00", 480, 480],
108
+ ["Asia/Seoul", "Seoul", "GMT+09:00", 540, 540],
109
+ ["Asia/Tokyo", "Tokyo", "GMT+09:00", 540, 540],
110
+ ["Asia/Yakutsk", "Yakutsk", "GMT+09:00", 540, 600],
111
+ ["Australia/Adelaide", "Adelaide", "GMT+09:30", 630, 570],
112
+ ["Australia/Darwin", "Darwin", "GMT+09:30", 570, 570],
113
+ ["Australia/Brisbane", "Brisbane", "GMT+10:00", 600, 600],
114
+ ["Pacific/Guam", "Guam", "GMT+10:00", 600, 600],
115
+ ["Australia/Hobart", "Hobart", "GMT+10:00", 660, 600],
116
+ ["Australia/Melbourne", "Melbourne", "GMT+10:00", 660, 600],
117
+ ["Pacific/Port_Moresby", "Port Moresby", "GMT+10:00", 600, 600],
118
+ ["Australia/Sydney", "Sydney", "GMT+10:00", 660, 600],
119
+ ["Asia/Vladivostok", "Vladivostok", "GMT+10:00", 600, 660],
120
+ ["Asia/Kamchatka", "Kamchatka", "GMT+11:00", 660, 720],
121
+ ["Asia/Magadan", "Magadan", "GMT+11:00", 660, 720],
122
+ ["Pacific/Noumea", "New Caledonia", "GMT+11:00", 660, 660],
123
+ ["Asia/Sakhalin", "Sakhalin Island", "GMT+11:00", 660, 660],
124
+ ["Pacific/Auckland", "Auckland", "GMT+12:00", 780, 720],
125
+ ["Pacific/Fiji", "Fiji", "GMT+12:00", 780, 720],
126
+ ["Pacific/Majuro", "Marshall Is.", "GMT+12:00", 720, 720],
127
+ ["Pacific/Tongatapu", "Nuku'alofa", "GMT+13:00", 780, 780],
128
+ ];
129
+ const timezones = timezonesRaw.map(([id, name, offsetLabel, januaryOffset, juneOffset]) => ({
130
+ id,
131
+ name,
132
+ offsetLabel,
133
+ januaryOffset,
134
+ juneOffset,
135
+ }));
136
+ const TIMEZONE_DEFAULT = timezones.find((tz) => tz.id === "Etc/UTC");
137
+ /**
138
+ * Returns the full display label of a timezone, combining the offset and the name,
139
+ * e.g. "GMT+01:00 Prague". Use only where the merged label is actually displayed;
140
+ * consumers that need the name or the offset alone should read the fields directly.
141
+ *
142
+ * @internal
143
+ */
144
+ export function getTimezoneTitle(item) {
145
+ return item.offsetLabel ? `${item.offsetLabel} ${item.name}` : item.name;
146
+ }
147
+ /**
148
+ * Returns the curated list of timezones.
149
+ *
150
+ * @internal
151
+ */
152
+ export function getTimezones() {
153
+ return timezones;
154
+ }
155
+ /**
156
+ * Returns the timezone from the curated list matching the provided IANA timezone ID,
157
+ * or undefined when the ID is not in the curated list.
158
+ *
159
+ * @internal
160
+ */
161
+ export function getTimezoneById(id) {
162
+ return timezones.find((tz) => tz.id === id);
163
+ }
164
+ /**
165
+ * Returns the curated timezone that matches the offsets of the browser/runtime timezone.
166
+ * Falls back to UTC when no curated timezone matches.
167
+ *
168
+ * @internal
169
+ */
170
+ export function getUserTimezone() {
171
+ // prefer the exact runtime IANA id so zones sharing offsets (e.g. Berlin vs Amsterdam)
172
+ // resolve to the viewer's actual zone; fall back to offset matching for runtime ids
173
+ // that are not part of the curated list
174
+ const runtimeTimezoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
175
+ const exactMatch = getTimezoneById(runtimeTimezoneId);
176
+ if (exactMatch) {
177
+ return exactMatch;
178
+ }
179
+ // sample the offsets on a fixed date in winter and in summer so that both DST variants are covered
180
+ const januaryOffset = -new Date(2011, 0, 1, 0, 0, 0, 0).getTimezoneOffset();
181
+ const juneOffset = -new Date(2011, 5, 1, 0, 0, 0, 0).getTimezoneOffset();
182
+ return (timezones.find((tz) => tz.januaryOffset === januaryOffset && tz.juneOffset === juneOffset) ??
183
+ TIMEZONE_DEFAULT);
184
+ }
185
+ /**
186
+ * Returns the display label used by the timezone picker: the friendly name with the GMT offset
187
+ * as a suffix in brackets, e.g. "Prague (GMT+01:00)". The name falls back to the raw IANA ID
188
+ * for zones outside the curated list, and the offset suffix is dropped when not known.
189
+ *
190
+ * The friendly name (not the IANA ID) is shown so the picker reads the same as the Home UI
191
+ * timezone settings — the id-to-name mapping is not visible to users anywhere.
192
+ *
193
+ * @internal
194
+ */
195
+ // TODO INE (CQ-2725): finalize the picker label format and the ID-in-tooltip presentation
196
+ // after the UX discussion.
197
+ export function getTimezoneDisplayLabel(id) {
198
+ const { name, offsetLabel } = getTimezoneLabels(id);
199
+ return offsetLabel ? `${name} (${offsetLabel})` : name;
200
+ }
201
+ /**
202
+ * Tests whether the timezone matches the search string. Matches on the picker display label
203
+ * ("name (offset)"), the friendly title (offset + name) and the IANA ID, with underscores
204
+ * treated as spaces so that e.g. "Los Angeles" finds "America/Los_Angeles".
205
+ *
206
+ * @internal
207
+ */
208
+ export function timezoneMatchesSearch(timezone, search) {
209
+ const normalizedSearch = search.trim().toLowerCase();
210
+ if (!normalizedSearch) {
211
+ return true;
212
+ }
213
+ return (getTimezoneTitle(timezone).toLowerCase().includes(normalizedSearch) ||
214
+ getTimezoneDisplayLabel(timezone.id).toLowerCase().includes(normalizedSearch) ||
215
+ timezone.id.toLowerCase().replace(/_/g, " ").includes(normalizedSearch.replace(/_/g, " ")));
216
+ }
217
+ function computeOffsetLabel(id) {
218
+ try {
219
+ const parts = new Intl.DateTimeFormat("en-US", {
220
+ timeZone: id,
221
+ timeZoneName: "longOffset",
222
+ }).formatToParts(new Date());
223
+ const offsetPart = parts.find((part) => part.type === "timeZoneName")?.value;
224
+ if (!offsetPart) {
225
+ return "";
226
+ }
227
+ // plain "GMT" is reported for the zero offset; normalize to the curated label format
228
+ return offsetPart === "GMT" ? "GMT+00:00" : offsetPart;
229
+ }
230
+ catch {
231
+ return "";
232
+ }
233
+ }
234
+ /**
235
+ * Returns display labels for the provided IANA timezone ID.
236
+ *
237
+ * @remarks
238
+ * The backend accepts any ICU-known timezone, so the ID may not be present in the curated list.
239
+ * In that case the raw ID is used as the name and the offset label is computed via Intl
240
+ * (empty when the runtime does not know the timezone either).
241
+ *
242
+ * @internal
243
+ */
244
+ export function getTimezoneLabels(id) {
245
+ const known = getTimezoneById(id);
246
+ if (known) {
247
+ return { name: known.name, offsetLabel: known.offsetLabel };
248
+ }
249
+ return {
250
+ name: id,
251
+ offsetLabel: computeOffsetLabel(id),
252
+ };
253
+ }