@forwardreach/saas-ui 0.5.0 → 0.8.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/CHANGELOG.md +90 -0
- package/dist/components/account-menu.d.ts +42 -0
- package/dist/components/account-menu.js +41 -0
- package/dist/components/button.d.ts +1 -1
- package/dist/components/checkbox.d.ts +51 -0
- package/dist/components/checkbox.js +61 -0
- package/dist/components/choice-card.d.ts +33 -0
- package/dist/components/choice-card.js +40 -0
- package/dist/components/collapsible.d.ts +37 -0
- package/dist/components/collapsible.js +60 -0
- package/dist/components/combobox.d.ts +37 -0
- package/dist/components/combobox.js +103 -0
- package/dist/components/field-group.d.ts +22 -0
- package/dist/components/field-group.js +22 -0
- package/dist/components/form-field.d.ts +48 -0
- package/dist/components/form-field.js +51 -0
- package/dist/components/index.d.ts +15 -0
- package/dist/components/index.js +15 -0
- package/dist/components/input.js +2 -1
- package/dist/components/rail-toggle.d.ts +19 -0
- package/dist/components/rail-toggle.js +17 -0
- package/dist/components/role-menu.d.ts +95 -0
- package/dist/components/role-menu.js +52 -0
- package/dist/components/select.d.ts +9 -0
- package/dist/components/select.js +12 -0
- package/dist/components/settings-layout.d.ts +10 -0
- package/dist/components/settings-layout.js +10 -0
- package/dist/components/settings-nav.d.ts +46 -0
- package/dist/components/settings-nav.js +21 -0
- package/dist/components/switch.d.ts +20 -0
- package/dist/components/switch.js +35 -0
- package/dist/components/textarea.js +2 -1
- package/dist/components/time-zone-field.d.ts +30 -0
- package/dist/components/time-zone-field.js +72 -0
- package/dist/components/workspace-switcher.d.ts +33 -0
- package/dist/components/workspace-switcher.js +34 -0
- package/dist/styles/index.css +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/time-zones.d.ts +26 -0
- package/dist/utils/time-zones.js +79 -0
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TimeZoneOption } from "../utils/time-zones.js";
|
|
2
|
+
export type TimeZoneMode = "auto" | "fixed";
|
|
3
|
+
export interface TimeZoneFieldProps {
|
|
4
|
+
/** Initial mode. Defaults to "auto". */
|
|
5
|
+
defaultMode?: TimeZoneMode;
|
|
6
|
+
/** Initial fixed zone. Defaults to the detected browser zone. */
|
|
7
|
+
defaultTimeZone?: string;
|
|
8
|
+
/** Notified whenever mode or zone changes. */
|
|
9
|
+
onChange?: (state: {
|
|
10
|
+
mode: TimeZoneMode;
|
|
11
|
+
timeZone: string;
|
|
12
|
+
}) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Zone options to render; defaults to `timeZoneOptions()` computed
|
|
15
|
+
* client-side. Provide to SSR the list or trim it.
|
|
16
|
+
*/
|
|
17
|
+
options?: TimeZoneOption[];
|
|
18
|
+
/** Accessible label for the mode choice. Defaults to "Time zone mode". */
|
|
19
|
+
"aria-label"?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Composite time-zone preference control: an auto/fixed mode choice rendered
|
|
24
|
+
* as `ChoiceCard`s (auto shows the detected browser zone inline) plus a
|
|
25
|
+
* filterable zone `Combobox` shown only in fixed mode. Search matches zone
|
|
26
|
+
* names with spaces or underscores and offset text ("GMT+09"). Submits
|
|
27
|
+
* through hidden inputs — `timeZoneMode`, `timeZone`, `browserTimeZone` —
|
|
28
|
+
* for server-action form handling.
|
|
29
|
+
*/
|
|
30
|
+
export declare function TimeZoneField({ defaultMode, defaultTimeZone, onChange, options, className, "aria-label": ariaLabel, }: TimeZoneFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
import { detectBrowserTimeZone, formatTimeZoneOffset, timeZoneOptions, } from "../utils/time-zones.js";
|
|
6
|
+
import { ChoiceCard, ChoiceCardGroup } from "./choice-card.js";
|
|
7
|
+
import { Combobox } from "./combobox.js";
|
|
8
|
+
function matchesZoneQuery(option, query) {
|
|
9
|
+
const normalized = query.trim().toLowerCase();
|
|
10
|
+
if (!normalized)
|
|
11
|
+
return true;
|
|
12
|
+
const underscored = normalized.replaceAll(" ", "_");
|
|
13
|
+
const haystacks = [
|
|
14
|
+
option.value.toLowerCase(),
|
|
15
|
+
option.label.toLowerCase(),
|
|
16
|
+
typeof option.trailing === "string" ? option.trailing.toLowerCase() : "",
|
|
17
|
+
];
|
|
18
|
+
return haystacks.some((haystack) => haystack.includes(normalized) || haystack.includes(underscored));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Composite time-zone preference control: an auto/fixed mode choice rendered
|
|
22
|
+
* as `ChoiceCard`s (auto shows the detected browser zone inline) plus a
|
|
23
|
+
* filterable zone `Combobox` shown only in fixed mode. Search matches zone
|
|
24
|
+
* names with spaces or underscores and offset text ("GMT+09"). Submits
|
|
25
|
+
* through hidden inputs — `timeZoneMode`, `timeZone`, `browserTimeZone` —
|
|
26
|
+
* for server-action form handling.
|
|
27
|
+
*/
|
|
28
|
+
export function TimeZoneField({ defaultMode = "auto", defaultTimeZone, onChange, options, className, "aria-label": ariaLabel = "Time zone mode", }) {
|
|
29
|
+
const [mode, setMode] = React.useState(defaultMode);
|
|
30
|
+
const [browserTimeZone, setBrowserTimeZone] = React.useState("UTC");
|
|
31
|
+
const [fixedTimeZone, setFixedTimeZone] = React.useState(defaultTimeZone ?? "");
|
|
32
|
+
// Detection runs in an effect so server and first client render agree.
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
const detected = detectBrowserTimeZone();
|
|
35
|
+
setBrowserTimeZone(detected);
|
|
36
|
+
setFixedTimeZone((current) => current || detected);
|
|
37
|
+
}, []);
|
|
38
|
+
// Building the full IANA list constructs one `Intl.DateTimeFormat` per zone
|
|
39
|
+
// (~400 of them, tens of ms). The combobox only renders in fixed mode, so
|
|
40
|
+
// defer the build until fixed is first selected, then cache it for the
|
|
41
|
+
// component's lifetime. Consumer-provided `options` skip the build entirely.
|
|
42
|
+
const builtOptionsRef = React.useRef(null);
|
|
43
|
+
const comboboxOptions = React.useMemo(() => {
|
|
44
|
+
if (options) {
|
|
45
|
+
return options.map((zone) => ({
|
|
46
|
+
value: zone.value,
|
|
47
|
+
label: zone.label,
|
|
48
|
+
trailing: zone.offset,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
if (mode !== "fixed")
|
|
52
|
+
return [];
|
|
53
|
+
if (!builtOptionsRef.current) {
|
|
54
|
+
builtOptionsRef.current = timeZoneOptions().map((zone) => ({
|
|
55
|
+
value: zone.value,
|
|
56
|
+
label: zone.label,
|
|
57
|
+
trailing: zone.offset,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
return builtOptionsRef.current;
|
|
61
|
+
}, [options, mode]);
|
|
62
|
+
function update(next) {
|
|
63
|
+
const nextMode = next.mode ?? mode;
|
|
64
|
+
const nextZone = next.timeZone ?? fixedTimeZone;
|
|
65
|
+
if (next.mode !== undefined)
|
|
66
|
+
setMode(next.mode);
|
|
67
|
+
if (next.timeZone !== undefined)
|
|
68
|
+
setFixedTimeZone(next.timeZone);
|
|
69
|
+
onChange?.({ mode: nextMode, timeZone: nextZone });
|
|
70
|
+
}
|
|
71
|
+
return (_jsxs("div", { className: cn("space-y-3", className), children: [_jsx("input", { name: "timeZoneMode", type: "hidden", value: mode }), _jsx("input", { name: "timeZone", type: "hidden", value: fixedTimeZone }), _jsx("input", { name: "browserTimeZone", type: "hidden", value: browserTimeZone }), _jsxs(ChoiceCardGroup, { "aria-label": ariaLabel, onValueChange: (value) => update({ mode: value }), value: mode, children: [_jsx(ChoiceCard, { description: _jsxs(_Fragment, { children: ["Use the browser time zone", _jsxs("span", { className: "mt-0.5 block font-mono text-[color:var(--ssui-text-muted)]", children: [browserTimeZone, " (", formatTimeZoneOffset(browserTimeZone), ")"] })] }), title: "Auto", value: "auto" }), _jsx(ChoiceCard, { description: "Always use one time zone", title: "Fixed", value: "fixed" })] }), mode === "fixed" ? (_jsx(Combobox, { "aria-label": "Fixed time zone", emptyMessage: "No matching time zones", filter: matchesZoneQuery, onValueChange: (value) => update({ timeZone: value }), options: comboboxOptions, placeholder: "America/New_York", value: fixedTimeZone })) : null] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface WorkspaceSwitcherWorkspace {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface WorkspaceSwitcherProps {
|
|
7
|
+
workspaces: ReadonlyArray<WorkspaceSwitcherWorkspace>;
|
|
8
|
+
activeWorkspaceId: string;
|
|
9
|
+
/** Called with the selected workspace id; never fired for the active row. */
|
|
10
|
+
onSelect: (id: string) => void | Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Brand badge rendered at the left of the trigger (e.g. a product tile).
|
|
13
|
+
* Defaults to a primary-colored tile with the active workspace's initials.
|
|
14
|
+
*/
|
|
15
|
+
badge?: React.ReactNode;
|
|
16
|
+
/** Icon-only rail state: badge-only trigger with a name tooltip, menu opens right. */
|
|
17
|
+
collapsed?: boolean;
|
|
18
|
+
/** Renders a separated "New workspace" item when provided. */
|
|
19
|
+
onCreateWorkspace?: () => void;
|
|
20
|
+
/** Defaults to "New workspace". */
|
|
21
|
+
createWorkspaceLabel?: string;
|
|
22
|
+
/** Open the menu by default (uncontrolled). Mainly useful in tests. */
|
|
23
|
+
defaultOpen?: boolean;
|
|
24
|
+
/** Extra classes for the trigger. */
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Top-left workspace context menu for the app shell: a trigger showing the
|
|
29
|
+
* brand badge and active workspace name, opening a list of the user's
|
|
30
|
+
* workspaces with the active one checked. Identity/session actions
|
|
31
|
+
* deliberately do not belong here; that's the bottom-left `AccountMenu`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function WorkspaceSwitcher({ workspaces, activeWorkspaceId, onSelect, badge, collapsed, onCreateWorkspace, createWorkspaceLabel, defaultOpen, className, }: WorkspaceSwitcherProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
4
|
+
import { Check, ChevronDown, Plus } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { cn } from "../utils/cn.js";
|
|
7
|
+
import { getInitials } from "../utils/initials.js";
|
|
8
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
9
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip.js";
|
|
10
|
+
/**
|
|
11
|
+
* Top-left workspace context menu for the app shell: a trigger showing the
|
|
12
|
+
* brand badge and active workspace name, opening a list of the user's
|
|
13
|
+
* workspaces with the active one checked. Identity/session actions
|
|
14
|
+
* deliberately do not belong here; that's the bottom-left `AccountMenu`.
|
|
15
|
+
*/
|
|
16
|
+
export function WorkspaceSwitcher({ workspaces, activeWorkspaceId, onSelect, badge, collapsed = false, onCreateWorkspace, createWorkspaceLabel = "New workspace", defaultOpen, className, }) {
|
|
17
|
+
const active = workspaces.find((workspace) => workspace.id === activeWorkspaceId);
|
|
18
|
+
const activeName = active?.name ?? "";
|
|
19
|
+
// Mirror open state for the chevron cue: the trigger's data-state is
|
|
20
|
+
// unreliable here because TooltipTrigger's own data-state wins the
|
|
21
|
+
// asChild prop merge over DropdownMenuTrigger's.
|
|
22
|
+
const [open, setOpen] = React.useState(defaultOpen ?? false);
|
|
23
|
+
return (_jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, children: [_jsx(TooltipProvider, { delayDuration: 300, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": collapsed ? activeName : `Switch workspace (current: ${activeName})`, className: cn("flex h-10 w-full items-center gap-2 rounded-[var(--ssui-radius)] px-1.5 text-left outline-none transition-colors hover:bg-[color:var(--ssui-overlay-hover)] focus-visible:ring-2 focus-visible:ring-[color:var(--ssui-focus-ring)]",
|
|
24
|
+
// Collapsed = badge-only: the label/chevron are omitted from the
|
|
25
|
+
// layout entirely (not just faded) so the trigger truly fits an
|
|
26
|
+
// icon rail instead of relying on horizontal clipping.
|
|
27
|
+
collapsed && "justify-center px-0", className), children: [_jsx("span", { className: "flex shrink-0 items-center justify-center", children: badge ?? (_jsx("span", { className: "flex size-7 items-center justify-center rounded-[var(--ssui-radius-sm)] bg-[color:var(--ssui-primary)] text-xs font-semibold text-[color:var(--ssui-primary-foreground)]", children: getInitials(activeName) })) }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1 truncate text-[13px] font-medium leading-tight text-[color:var(--ssui-text)]", children: activeName }), _jsx(ChevronDown, { className: cn("size-4 shrink-0 text-[color:var(--ssui-text-subtle)] transition-transform duration-150", open && "rotate-180") })] }))] }) }) }), collapsed ? _jsx(TooltipContent, { side: "right", children: activeName }) : null] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "w-60 min-w-[var(--radix-dropdown-menu-trigger-width)]", side: collapsed ? "right" : "bottom", sideOffset: 10, children: [_jsx(DropdownMenuPrimitive.RadioGroup, { value: activeWorkspaceId, onValueChange: (id) => {
|
|
28
|
+
if (id !== activeWorkspaceId)
|
|
29
|
+
void onSelect(id);
|
|
30
|
+
}, children: workspaces.map((workspace) => {
|
|
31
|
+
const isActive = workspace.id === activeWorkspaceId;
|
|
32
|
+
return (_jsxs(DropdownMenuPrimitive.RadioItem, { value: workspace.id, className: cn("relative flex cursor-default select-none items-center gap-2 rounded-[var(--ssui-radius-sm)] px-2 py-1.5 text-sm text-[color:var(--ssui-text)] outline-none transition-colors focus:bg-[color:var(--ssui-overlay-hover)] data-[highlighted]:bg-[color:var(--ssui-overlay-hover)]", isActive && "font-medium"), children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: workspace.name }), isActive ? (_jsx(Check, { className: "size-4 shrink-0 text-[color:var(--ssui-primary)]" })) : null] }, workspace.id));
|
|
33
|
+
}) }), onCreateWorkspace ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => onCreateWorkspace(), children: [_jsx(Plus, { className: "size-4 text-[color:var(--ssui-text-muted)]" }), createWorkspaceLabel] })] })) : null] })] }));
|
|
34
|
+
}
|
package/dist/styles/index.css
CHANGED
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
--ssui-destructive-hover: #b91c1c;
|
|
21
21
|
--ssui-destructive-foreground: #ffffff;
|
|
22
22
|
--ssui-focus-ring: #2563eb;
|
|
23
|
+
--ssui-switch-track: var(--ssui-border);
|
|
24
|
+
--ssui-switch-track-checked: var(--ssui-primary);
|
|
25
|
+
--ssui-switch-thumb: #ffffff;
|
|
26
|
+
/* Thumb color while checked. Defaults to the unchecked thumb; override when the
|
|
27
|
+
checked track (--ssui-switch-track-checked) is too light for a white thumb. */
|
|
28
|
+
--ssui-switch-thumb-checked: #ffffff;
|
|
23
29
|
--ssui-radius-sm: 0.25rem;
|
|
24
30
|
--ssui-radius: 0.375rem;
|
|
25
31
|
--ssui-radius-lg: 0.5rem;
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface TimeZoneOption {
|
|
2
|
+
/** IANA zone id, e.g. "America/New_York". */
|
|
3
|
+
value: string;
|
|
4
|
+
/** Display label, e.g. "America/New_York". */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Current offset label, e.g. "GMT-04:00". */
|
|
7
|
+
offset: string;
|
|
8
|
+
/** Offset in minutes, for sorting. */
|
|
9
|
+
offsetMinutes: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Zones surfaced ahead of the full IANA list — the major business hubs both
|
|
13
|
+
* consuming products' audiences are most likely to pick.
|
|
14
|
+
*/
|
|
15
|
+
export declare const COMMON_TIME_ZONES: string[];
|
|
16
|
+
/** Detect the browser's IANA time zone, falling back to UTC. */
|
|
17
|
+
export declare function detectBrowserTimeZone(): string;
|
|
18
|
+
/** Format a zone's current UTC offset as "GMT±HH:MM". Unknown zones map to GMT+00:00. */
|
|
19
|
+
export declare function formatTimeZoneOffset(timeZone: string, date?: Date): string;
|
|
20
|
+
/**
|
|
21
|
+
* Build the ordered, offset-annotated zone option list `TimeZoneField`
|
|
22
|
+
* renders: common zones first, then every zone the runtime supports, each
|
|
23
|
+
* sorted by current offset then name. Offsets are computed for `date`
|
|
24
|
+
* (defaults to now), so DST is reflected.
|
|
25
|
+
*/
|
|
26
|
+
export declare function timeZoneOptions(date?: Date): TimeZoneOption[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zones surfaced ahead of the full IANA list — the major business hubs both
|
|
3
|
+
* consuming products' audiences are most likely to pick.
|
|
4
|
+
*/
|
|
5
|
+
export const COMMON_TIME_ZONES = [
|
|
6
|
+
"UTC",
|
|
7
|
+
"America/New_York",
|
|
8
|
+
"America/Chicago",
|
|
9
|
+
"America/Denver",
|
|
10
|
+
"America/Los_Angeles",
|
|
11
|
+
"America/Toronto",
|
|
12
|
+
"America/Vancouver",
|
|
13
|
+
"Europe/London",
|
|
14
|
+
"Europe/Paris",
|
|
15
|
+
"Europe/Berlin",
|
|
16
|
+
"Asia/Kolkata",
|
|
17
|
+
"Asia/Singapore",
|
|
18
|
+
"Asia/Tokyo",
|
|
19
|
+
"Australia/Sydney",
|
|
20
|
+
];
|
|
21
|
+
/** Detect the browser's IANA time zone, falling back to UTC. */
|
|
22
|
+
export function detectBrowserTimeZone() {
|
|
23
|
+
try {
|
|
24
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return "UTC";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Format a zone's current UTC offset as "GMT±HH:MM". Unknown zones map to GMT+00:00. */
|
|
31
|
+
export function formatTimeZoneOffset(timeZone, date = new Date()) {
|
|
32
|
+
try {
|
|
33
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
34
|
+
hour: "2-digit",
|
|
35
|
+
minute: "2-digit",
|
|
36
|
+
timeZone,
|
|
37
|
+
timeZoneName: "longOffset",
|
|
38
|
+
}).formatToParts(date);
|
|
39
|
+
const offset = parts.find((part) => part.type === "timeZoneName")?.value;
|
|
40
|
+
return offset && offset !== "GMT" ? offset : "GMT+00:00";
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "GMT+00:00";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function offsetMinutesFromLabel(offset) {
|
|
47
|
+
const match = offset.match(/^GMT([+-])(\d{2}):(\d{2})$/);
|
|
48
|
+
if (!match)
|
|
49
|
+
return 0;
|
|
50
|
+
const minutes = Number(match[2]) * 60 + Number(match[3]);
|
|
51
|
+
return match[1] === "-" ? -minutes : minutes;
|
|
52
|
+
}
|
|
53
|
+
function toOption(value, date) {
|
|
54
|
+
const offset = formatTimeZoneOffset(value, date);
|
|
55
|
+
return {
|
|
56
|
+
value,
|
|
57
|
+
label: value,
|
|
58
|
+
offset,
|
|
59
|
+
offsetMinutes: offsetMinutesFromLabel(offset),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the ordered, offset-annotated zone option list `TimeZoneField`
|
|
64
|
+
* renders: common zones first, then every zone the runtime supports, each
|
|
65
|
+
* sorted by current offset then name. Offsets are computed for `date`
|
|
66
|
+
* (defaults to now), so DST is reflected.
|
|
67
|
+
*/
|
|
68
|
+
export function timeZoneOptions(date = new Date()) {
|
|
69
|
+
const intl = Intl;
|
|
70
|
+
const allZones = intl.supportedValuesOf?.("timeZone") ?? [];
|
|
71
|
+
const common = new Set(COMMON_TIME_ZONES);
|
|
72
|
+
const sortByOffsetThenName = (a, b) => a.offsetMinutes - b.offsetMinutes || a.value.localeCompare(b.value);
|
|
73
|
+
const commonOptions = COMMON_TIME_ZONES.map((zone) => toOption(zone, date)).sort(sortByOffsetThenName);
|
|
74
|
+
const rest = allZones
|
|
75
|
+
.filter((zone) => !common.has(zone))
|
|
76
|
+
.map((zone) => toOption(zone, date))
|
|
77
|
+
.sort(sortByOffsetThenName);
|
|
78
|
+
return [...commonOptions, ...rest];
|
|
79
|
+
}
|
package/package.json
CHANGED