@astryxdesign/core 0.7.1-canary.ff40b78 → 0.7.1
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 +116 -0
- package/dist/Button/Button.d.ts +1 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +5 -2
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +191 -32
- package/dist/Chat/ChatComposerInput.d.ts +9 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +5 -0
- package/dist/Chat/ChatMessage.d.ts +11 -0
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessage.js +18 -9
- package/dist/Chat/useTriggerMenu.d.ts +9 -2
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +222 -50
- package/dist/CodeBlock/CodeBlock.d.ts +10 -1
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +158 -38
- package/dist/CodeBlock/tokenizer.d.ts +39 -0
- package/dist/CodeBlock/tokenizer.d.ts.map +1 -1
- package/dist/CodeBlock/tokenizer.js +108 -0
- package/dist/CommandPalette/CommandPalette.d.ts +9 -5
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +38 -20
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts +4 -3
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.js +4 -3
- package/dist/CommandPalette/CommandPaletteInput.d.ts +5 -4
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +35 -18
- package/dist/CommandPalette/CommandPaletteItem.d.ts +11 -6
- package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteItem.js +8 -9
- package/dist/CommandPalette/CommandPaletteList.d.ts +4 -2
- package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteList.js +10 -5
- package/dist/ComplexSelector/ComplexSelector.d.ts +8 -5
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +12 -3
- package/dist/ContextMenu/ContextMenu.d.ts +3 -3
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +62 -48
- package/dist/DateInput/DateInput.js +1 -1
- package/dist/DateInput/MonthScroller.d.ts.map +1 -1
- package/dist/DateInput/MonthScroller.js +12 -4
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +12 -2
- package/dist/Dialog/Dialog.d.ts +1 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +16 -5
- package/dist/Dialog/DialogHeader.d.ts +1 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +7 -7
- package/dist/Divider/Divider.d.ts +1 -0
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Divider/Divider.js +5 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +161 -65
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +7 -5
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +12 -5
- package/dist/Field/InputClearButton.d.ts +8 -4
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +23 -7
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +1 -0
- package/dist/Grid/Grid.d.ts +13 -4
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +7 -1
- package/dist/HoverCard/useHoverCard.d.ts +1 -1
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +67 -49
- package/dist/InputGroup/InputGroup.d.ts +2 -4
- package/dist/InputGroup/InputGroup.d.ts.map +1 -1
- package/dist/InputGroup/InputGroup.js +2 -4
- package/dist/InputGroup/groupStyles.d.ts +10 -2
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +19 -2
- package/dist/Layer/useLayerDismissal.d.ts +9 -9
- package/dist/Layer/useLayerDismissal.js +9 -9
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +25 -4
- package/dist/MobileNav/MobileNav.d.ts +7 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +45 -2
- package/dist/MultiSelector/MultiSelector.d.ts +7 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +24 -3
- package/dist/NumberInput/NumberInput.js +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +6 -0
- package/dist/Resizable/ResizeHandle.d.ts +1 -1
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +83 -15
- package/dist/Resizable/useResizable.d.ts +23 -0
- package/dist/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +78 -0
- package/dist/Selector/Selector.d.ts +6 -3
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +28 -6
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +10 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +18 -7
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +16 -4
- package/dist/Toolbar/Toolbar.d.ts +2 -1
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +3 -1
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +6 -3
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +7 -1
- package/dist/astryx.css +22 -0
- package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
- package/dist/hooks/useScrollOverflow.js +55 -12
- package/dist/hooks/useTreeFocus.d.ts +21 -0
- package/dist/hooks/useTreeFocus.d.ts.map +1 -1
- package/dist/hooks/useTreeFocus.js +40 -2
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +3 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
- package/dist/reset.a11y.chromium.spec.js +6 -1
- package/dist/reset.until-found.a11y.browser.spec.d.ts +2 -0
- package/dist/reset.until-found.a11y.browser.spec.d.ts.map +1 -0
- package/dist/reset.until-found.a11y.browser.spec.js +71 -0
- package/dist/theme/syntax/SyntaxTheme.d.ts.map +1 -1
- package/dist/theme/syntax/SyntaxTheme.js +43 -2
- package/dist/theme/syntax/defineSyntaxTheme.d.ts +18 -5
- package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
- package/dist/theme/syntax/defineSyntaxTheme.js +38 -9
- package/dist/theme/syntax/index.d.ts +1 -1
- package/dist/theme/syntax/index.d.ts.map +1 -1
- package/dist/theme/syntax/tokens.d.ts +12 -2
- package/dist/theme/syntax/tokens.d.ts.map +1 -1
- package/dist/theme/syntax/tokens.js +25 -4
- package/dist/utils/plainDate.d.ts +12 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +63 -10
- package/locales/en.json +13 -1
- package/locales/pseudo.json +9 -0
- package/package.json +4 -4
- package/src/AlertDialog/AlertDialog.doc.mjs +6 -0
- package/src/AlertDialog/AlertDialog.spec.md +205 -0
- package/src/AlertDialog/AlertDialog.test.tsx +3 -14
- package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +115 -0
- package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +52 -0
- package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +68 -0
- package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +144 -0
- package/src/Button/Button.test.tsx +37 -0
- package/src/Button/Button.tsx +5 -1
- package/src/Calendar/Calendar.doc.mjs +234 -60
- package/src/Calendar/Calendar.spec.md +209 -0
- package/src/Calendar/Calendar.test.tsx +559 -20
- package/src/Calendar/Calendar.tsx +258 -41
- package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +126 -0
- package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +12 -0
- package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +57 -0
- package/src/Calendar/__tests__/Calendar.a11y.states.ts +139 -0
- package/src/Calendar/__tests__/Calendar.a11y.test.tsx +137 -0
- package/src/Carousel/Carousel.test.tsx +54 -71
- package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +192 -0
- package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +27 -0
- package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +39 -0
- package/src/Carousel/__tests__/Carousel.a11y.states.ts +139 -0
- package/src/Carousel/__tests__/Carousel.a11y.test.tsx +127 -0
- package/src/Chat/ChatComposerInput.doc.mjs +1 -1
- package/src/Chat/ChatComposerInput.spec.md +94 -55
- package/src/Chat/ChatComposerInput.test.tsx +202 -1
- package/src/Chat/ChatComposerInput.tsx +13 -0
- package/src/Chat/ChatMessage.doc.mjs +31 -16
- package/src/Chat/ChatMessage.spec.md +265 -0
- package/src/Chat/ChatMessage.test.tsx +246 -0
- package/src/Chat/ChatMessage.tsx +30 -7
- package/src/Chat/ChatSystemMessage.spec.md +36 -37
- package/src/Chat/ChatSystemMessage.test.tsx +4 -4
- package/src/Chat/useTriggerMenu.test.tsx +994 -0
- package/src/Chat/useTriggerMenu.tsx +278 -46
- package/src/CodeBlock/CodeBlock.doc.mjs +7 -0
- package/src/CodeBlock/CodeBlock.spec.md +40 -41
- package/src/CodeBlock/CodeBlock.test.tsx +89 -9
- package/src/CodeBlock/CodeBlock.tsx +205 -25
- package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
- package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +93 -0
- package/src/CodeBlock/tokenizer.test.ts +112 -0
- package/src/CodeBlock/tokenizer.ts +106 -0
- package/src/Collapsible/CollapsibleGroup.spec.md +17 -8
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +45 -0
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +139 -0
- package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +135 -0
- package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +128 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +126 -23
- package/src/CommandPalette/CommandPalette.spec.md +100 -49
- package/src/CommandPalette/CommandPalette.test.tsx +241 -2
- package/src/CommandPalette/CommandPalette.tsx +47 -21
- package/src/CommandPalette/CommandPaletteEmpty.tsx +4 -3
- package/src/CommandPalette/CommandPaletteInput.doc.mjs +61 -4
- package/src/CommandPalette/CommandPaletteInput.spec.md +215 -0
- package/src/CommandPalette/CommandPaletteInput.test.tsx +375 -51
- package/src/CommandPalette/CommandPaletteInput.tsx +74 -21
- package/src/CommandPalette/CommandPaletteItem.doc.mjs +73 -22
- package/src/CommandPalette/CommandPaletteItem.spec.md +223 -0
- package/src/CommandPalette/CommandPaletteItem.test.tsx +200 -5
- package/src/CommandPalette/CommandPaletteItem.tsx +16 -11
- package/src/CommandPalette/CommandPaletteList.doc.mjs +3 -3
- package/src/CommandPalette/CommandPaletteList.spec.md +41 -36
- package/src/CommandPalette/CommandPaletteList.test.tsx +80 -0
- package/src/CommandPalette/CommandPaletteList.tsx +10 -5
- package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +599 -0
- package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -8
- package/src/ComplexSelector/ComplexSelector.spec.md +14 -9
- package/src/ComplexSelector/ComplexSelector.test.tsx +64 -16
- package/src/ComplexSelector/ComplexSelector.tsx +37 -8
- package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
- package/src/ContextMenu/ContextMenu.spec.md +51 -53
- package/src/ContextMenu/ContextMenu.test.tsx +132 -1
- package/src/ContextMenu/ContextMenu.tsx +78 -48
- package/src/DateInput/DateInput.doc.mjs +52 -0
- package/src/DateInput/DateInput.spec.md +27 -15
- package/src/DateInput/DateInput.test.tsx +38 -0
- package/src/DateInput/DateInput.tsx +1 -1
- package/src/DateInput/DateInputTouch.test.tsx +22 -0
- package/src/DateInput/MonthScroller.tsx +12 -4
- package/src/DateRangeInput/DateRangeInput.test.tsx +49 -0
- package/src/DateRangeInput/DateRangeInput.tsx +20 -2
- package/src/Dialog/Dialog.test.tsx +27 -1
- package/src/Dialog/Dialog.tsx +16 -5
- package/src/Dialog/DialogHeader.test.tsx +27 -2
- package/src/Dialog/DialogHeader.tsx +7 -8
- package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
- package/src/Divider/Divider.doc.mjs +5 -3
- package/src/Divider/Divider.spec.md +140 -54
- package/src/Divider/Divider.test.tsx +15 -0
- package/src/Divider/Divider.tsx +6 -4
- package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenu.spec.md +121 -45
- package/src/DropdownMenu/DropdownMenu.test.tsx +288 -23
- package/src/DropdownMenu/DropdownMenu.tsx +212 -81
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +38 -2
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +32 -18
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +146 -1
- package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +136 -0
- package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +12 -0
- package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +67 -0
- package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +141 -0
- package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +138 -0
- package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +40 -5
- package/src/Field/InputClearButton.test.tsx +29 -4
- package/src/Field/InputClearButton.tsx +26 -8
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
- package/src/FormLayout/FormLayout.tsx +1 -0
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +1 -1
- package/src/Grid/Grid.doc.mjs +14 -11
- package/src/Grid/Grid.spec.md +50 -32
- package/src/Grid/Grid.tsx +13 -4
- package/src/HoverCard/HoverCard.test.tsx +46 -1
- package/src/HoverCard/useHoverCard.tsx +68 -48
- package/src/InputGroup/InputGroup.doc.mjs +11 -1
- package/src/InputGroup/InputGroup.tsx +2 -4
- package/src/InputGroup/groupStyles.ts +40 -2
- package/src/Layer/useLayerDismissal.ts +9 -9
- package/src/Link/__tests__/Link.a11y.chromium.spec.ts +142 -0
- package/src/Link/__tests__/Link.a11y.renders.tsx +97 -0
- package/src/Link/__tests__/Link.a11y.states.ts +104 -0
- package/src/Link/__tests__/Link.a11y.test.tsx +127 -0
- package/src/Markdown/Markdown.doc.mjs +1 -1
- package/src/Markdown/Markdown.fr23.perf.test.ts +161 -40
- package/src/Markdown/Markdown.spec.md +45 -23
- package/src/Markdown/Markdown.test.tsx +38 -0
- package/src/Markdown/Markdown.tsx +24 -2
- package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
- package/src/MobileNav/MobileNav.tsx +50 -2
- package/src/MobileNav/MobileNavFocus.test.tsx +553 -0
- package/src/MoreMenu/MoreMenu.spec.md +18 -7
- package/src/MoreMenu/MoreMenu.test.tsx +38 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +8 -8
- package/src/MultiSelector/MultiSelector.spec.md +1 -1
- package/src/MultiSelector/MultiSelector.test.tsx +188 -21
- package/src/MultiSelector/MultiSelector.tsx +50 -11
- package/src/NumberInput/NumberInput.test.tsx +24 -0
- package/src/NumberInput/NumberInput.tsx +1 -1
- package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +78 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -0
- package/src/Resizable/Resizable.spec.md +41 -9
- package/src/Resizable/ResizeHandle.test.tsx +565 -2
- package/src/Resizable/ResizeHandle.tsx +126 -21
- package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +119 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +39 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +80 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +197 -0
- package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +123 -0
- package/src/Resizable/useResizable.ts +95 -0
- package/src/Selector/Selector.doc.mjs +7 -6
- package/src/Selector/Selector.spec.md +37 -24
- package/src/Selector/Selector.test.tsx +176 -10
- package/src/Selector/Selector.tsx +59 -14
- package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +27 -0
- package/src/Selector/__tests__/Combobox.a11y.states.ts +145 -0
- package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +108 -0
- package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +126 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/SideNav/SideNav.tsx +16 -2
- package/src/Slider/Slider.spec.md +43 -17
- package/src/Slider/Slider.test.tsx +134 -0
- package/src/Slider/Slider.tsx +29 -6
- package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +112 -0
- package/src/Slider/__tests__/Slider.a11y.known-failures.ts +12 -0
- package/src/Slider/__tests__/Slider.a11y.renders.tsx +42 -0
- package/src/Slider/__tests__/Slider.a11y.states.ts +106 -0
- package/src/Slider/__tests__/Slider.a11y.test.tsx +105 -0
- package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
- package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
- package/src/Tokenizer/Tokenizer.test.tsx +87 -0
- package/src/Tokenizer/Tokenizer.tsx +31 -3
- package/src/Toolbar/Toolbar.test.tsx +131 -4
- package/src/Toolbar/Toolbar.tsx +3 -1
- package/src/TreeList/TreeList.spec.md +33 -8
- package/src/TreeList/TreeList.test.tsx +72 -0
- package/src/TreeList/TreeList.tsx +10 -6
- package/src/TreeList/TreeListItem.tsx +5 -0
- package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +125 -0
- package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +44 -0
- package/src/TreeList/__tests__/TreeList.a11y.states.ts +164 -0
- package/src/TreeList/__tests__/TreeList.a11y.test.tsx +138 -0
- package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +82 -0
- package/src/hooks/useScrollOverflow.test.tsx +222 -0
- package/src/hooks/useScrollOverflow.ts +55 -13
- package/src/hooks/useTreeFocus.test.tsx +37 -0
- package/src/hooks/useTreeFocus.ts +55 -2
- package/src/i18n/generated-locales/en.generated.ts +3 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +3 -0
- package/src/reset.a11y.chromium.spec.ts +9 -1
- package/src/reset.css +8 -3
- package/src/reset.until-found.a11y.browser.spec.ts +75 -0
- package/src/reset.until-found.test.ts +32 -0
- package/src/stylexValidation.source-build.test.mjs +42 -0
- package/src/theme/syntax/SyntaxTheme.tsx +46 -5
- package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +91 -0
- package/src/theme/syntax/defineSyntaxTheme.ts +75 -15
- package/src/theme/syntax/index.ts +5 -1
- package/src/theme/syntax/tokens.ts +32 -2
- package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +40 -0
- package/src/utils/plainDate.test.ts +147 -0
- package/src/utils/plainDate.ts +75 -10
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it, vi} from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
defineSyntaxTheme,
|
|
6
|
+
REQUIRED_SYNTAX_KEYS,
|
|
7
|
+
syntaxThemeStyle,
|
|
8
|
+
syntaxThemeToCSS,
|
|
9
|
+
} from './defineSyntaxTheme';
|
|
10
|
+
import {syntaxTokenDefaults} from './tokens';
|
|
11
|
+
import {dracula} from './presets';
|
|
12
|
+
|
|
13
|
+
const base = Object.fromEntries(
|
|
14
|
+
REQUIRED_SYNTAX_KEYS.map(key => [key, '#123456']),
|
|
15
|
+
) as Parameters<typeof defineSyntaxTheme>[0]['tokens'];
|
|
16
|
+
|
|
17
|
+
describe('diff row slots in a syntax theme', () => {
|
|
18
|
+
it('are optional: a theme that sets none keeps the semantic defaults', () => {
|
|
19
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
20
|
+
const theme = defineSyntaxTheme({name: 't', tokens: base});
|
|
21
|
+
expect(warn).not.toHaveBeenCalled();
|
|
22
|
+
expect(theme.tokens.added).toBe(
|
|
23
|
+
syntaxTokenDefaults['--color-syntax-added'],
|
|
24
|
+
);
|
|
25
|
+
expect(theme.tokens.removed).toBe(
|
|
26
|
+
syntaxTokenDefaults['--color-syntax-removed'],
|
|
27
|
+
);
|
|
28
|
+
// A defined theme that sets no header paint gets none: the header row
|
|
29
|
+
// sits on the theme's own background with punctuation text.
|
|
30
|
+
expect(theme.tokens['diff-header']).toBe('transparent');
|
|
31
|
+
// A theme that sets no marker colour keeps its own line-number colour.
|
|
32
|
+
expect(theme.tokens['added-marker']).toBe(
|
|
33
|
+
'var(--color-syntax-punctuation)',
|
|
34
|
+
);
|
|
35
|
+
expect(theme.tokens['removed-marker']).toBe(
|
|
36
|
+
'var(--color-syntax-punctuation)',
|
|
37
|
+
);
|
|
38
|
+
warn.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('may be overridden to harmonize with the theme', () => {
|
|
42
|
+
const theme = defineSyntaxTheme({
|
|
43
|
+
name: 't',
|
|
44
|
+
tokens: {...base, added: ['#e6ffed', '#1b3a24'], removed: '#ffeef0'},
|
|
45
|
+
});
|
|
46
|
+
expect(theme.tokens.added).toBe('light-dark(#e6ffed, #1b3a24)');
|
|
47
|
+
expect(theme.tokens.removed).toBe('#ffeef0');
|
|
48
|
+
expect(theme.tokens['diff-header']).toBe('transparent');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('skips unset diff slots in the CSS and style output of a hand-built definition', () => {
|
|
52
|
+
const tokens = {...dracula.tokens};
|
|
53
|
+
delete (tokens as Partial<typeof tokens>).added;
|
|
54
|
+
delete (tokens as Partial<typeof tokens>)['added-marker'];
|
|
55
|
+
const theme = {...dracula, tokens};
|
|
56
|
+
expect(syntaxThemeToCSS(theme)).not.toContain('undefined');
|
|
57
|
+
expect(syntaxThemeToCSS(theme)).not.toContain('--color-syntax-added:');
|
|
58
|
+
expect(syntaxThemeStyle(theme)).not.toHaveProperty('--color-syntax-added');
|
|
59
|
+
expect(syntaxThemeStyle(theme)).toHaveProperty('--color-syntax-removed');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('leaves the shipped presets valid without them: row paints default, markers follow punctuation', () => {
|
|
63
|
+
expect(dracula.tokens.added).toBe(
|
|
64
|
+
syntaxTokenDefaults['--color-syntax-added'],
|
|
65
|
+
);
|
|
66
|
+
expect(dracula.tokens['added-marker']).toBe(
|
|
67
|
+
'var(--color-syntax-punctuation)',
|
|
68
|
+
);
|
|
69
|
+
expect(dracula.tokens['removed-marker']).toBe(
|
|
70
|
+
'var(--color-syntax-punctuation)',
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('defaults derive from the semantic tokens, never a literal colour', () => {
|
|
75
|
+
expect(syntaxTokenDefaults['--color-syntax-added']).toBe(
|
|
76
|
+
'color-mix(in srgb, var(--color-success-muted) 25%, transparent)',
|
|
77
|
+
);
|
|
78
|
+
expect(syntaxTokenDefaults['--color-syntax-removed']).toBe(
|
|
79
|
+
'color-mix(in srgb, var(--color-error-muted) 25%, transparent)',
|
|
80
|
+
);
|
|
81
|
+
expect(syntaxTokenDefaults['--color-syntax-diff-header']).toBe(
|
|
82
|
+
'var(--color-background-muted)',
|
|
83
|
+
);
|
|
84
|
+
expect(syntaxTokenDefaults['--color-syntax-added-marker']).toBe(
|
|
85
|
+
'var(--color-text-green)',
|
|
86
|
+
);
|
|
87
|
+
expect(syntaxTokenDefaults['--color-syntax-removed-marker']).toBe(
|
|
88
|
+
'var(--color-text-red)',
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* @see https://github.com/facebook/astryx/issues/1148
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
syntaxTokenDefaults,
|
|
16
|
+
DIFF_SYNTAX_TOKEN_NAMES,
|
|
17
|
+
type SyntaxTokenName,
|
|
18
|
+
} from './tokens';
|
|
15
19
|
import {devWarn} from '../../utils/devWarning';
|
|
16
20
|
|
|
17
21
|
// =============================================================================
|
|
@@ -33,7 +37,12 @@ export type SyntaxThemeTokenKey =
|
|
|
33
37
|
| 'attribute'
|
|
34
38
|
| 'property'
|
|
35
39
|
| 'punctuation'
|
|
36
|
-
| 'background'
|
|
40
|
+
| 'background'
|
|
41
|
+
| DiffSyntaxThemeTokenKey;
|
|
42
|
+
|
|
43
|
+
/** The diff row slots a syntax theme MAY set; unset ones keep their defaults. */
|
|
44
|
+
export type DiffSyntaxThemeTokenKey =
|
|
45
|
+
'added' | 'removed' | 'diff-header' | 'added-marker' | 'removed-marker';
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
48
|
* Token value — either a single string or a [light, dark] tuple.
|
|
@@ -41,14 +50,28 @@ export type SyntaxThemeTokenKey =
|
|
|
41
50
|
*/
|
|
42
51
|
export type SyntaxTokenValue = string | [light: string, dark: string];
|
|
43
52
|
|
|
44
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Token map for defineSyntaxTheme input — values can be strings or tuples.
|
|
55
|
+
* The diff row slots are optional: a theme that sets none keeps the defaults
|
|
56
|
+
* (semantic success/error muted tints and the muted surface).
|
|
57
|
+
*/
|
|
45
58
|
export type SyntaxThemeTokenInput = Record<
|
|
46
|
-
SyntaxThemeTokenKey,
|
|
59
|
+
Exclude<SyntaxThemeTokenKey, DiffSyntaxThemeTokenKey>,
|
|
47
60
|
SyntaxTokenValue
|
|
48
|
-
|
|
61
|
+
> &
|
|
62
|
+
Partial<Record<DiffSyntaxThemeTokenKey, SyntaxTokenValue>>;
|
|
49
63
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Resolved token map — all values are CSS strings (tuples resolved to
|
|
66
|
+
* light-dark()). The diff row slots are optional here as in the input, so a
|
|
67
|
+
* map written before they existed still type-checks; an unset slot renders
|
|
68
|
+
* its default.
|
|
69
|
+
*/
|
|
70
|
+
export type SyntaxThemeTokenMap = Record<
|
|
71
|
+
Exclude<SyntaxThemeTokenKey, DiffSyntaxThemeTokenKey>,
|
|
72
|
+
string
|
|
73
|
+
> &
|
|
74
|
+
Partial<Record<DiffSyntaxThemeTokenKey, string>>;
|
|
52
75
|
|
|
53
76
|
/** Input to defineSyntaxTheme. */
|
|
54
77
|
export interface SyntaxThemeInput {
|
|
@@ -81,6 +104,22 @@ export const ALL_SYNTAX_KEYS: SyntaxThemeTokenKey[] = Object.keys(
|
|
|
81
104
|
syntaxTokenDefaults,
|
|
82
105
|
).map(k => k.replace(CSS_PREFIX, '') as SyntaxThemeTokenKey);
|
|
83
106
|
|
|
107
|
+
const DIFF_KEYS = new Set<SyntaxThemeTokenKey>(
|
|
108
|
+
DIFF_SYNTAX_TOKEN_NAMES.map(
|
|
109
|
+
name => name.replace(CSS_PREFIX, '') as SyntaxThemeTokenKey,
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/** The marker slots: unset in a theme, they follow that theme's punctuation. */
|
|
114
|
+
const MARKER_KEYS = new Set<SyntaxThemeTokenKey>([
|
|
115
|
+
'added-marker',
|
|
116
|
+
'removed-marker',
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/** The keys every syntax theme must set: everything but the diff rows. */
|
|
120
|
+
export const REQUIRED_SYNTAX_KEYS: SyntaxThemeTokenKey[] =
|
|
121
|
+
ALL_SYNTAX_KEYS.filter(key => !DIFF_KEYS.has(key));
|
|
122
|
+
|
|
84
123
|
// =============================================================================
|
|
85
124
|
// Helpers
|
|
86
125
|
// =============================================================================
|
|
@@ -138,19 +177,37 @@ export function resolveSyntaxTokenForMode(
|
|
|
138
177
|
export function defineSyntaxTheme(
|
|
139
178
|
input: SyntaxThemeInput,
|
|
140
179
|
): SyntaxThemeDefinition {
|
|
141
|
-
const missing =
|
|
180
|
+
const missing = REQUIRED_SYNTAX_KEYS.filter(key => !(key in input.tokens));
|
|
142
181
|
if (missing.length > 0) {
|
|
143
182
|
devWarn(
|
|
144
183
|
'defineSyntaxTheme',
|
|
145
184
|
`"${input.name}": missing tokens: ${missing.join(', ')}. ` +
|
|
146
|
-
`All
|
|
185
|
+
`All ${REQUIRED_SYNTAX_KEYS.length} syntax colour tokens are required; ` +
|
|
186
|
+
`the diff row slots (added, removed, diff-header, added-marker, removed-marker) are optional.`,
|
|
147
187
|
);
|
|
148
188
|
}
|
|
149
189
|
|
|
150
|
-
// Resolve tuples to light-dark() CSS strings
|
|
190
|
+
// Resolve tuples to light-dark() CSS strings. An unset diff row paint keeps
|
|
191
|
+
// its default so every existing theme renders a readable diff unchanged; an
|
|
192
|
+
// unset marker slot takes this theme's own line-number colour (its
|
|
193
|
+
// punctuation token) rather than the page theme's green and red, and an
|
|
194
|
+
// unset header paint is none at all (the header row sits on this theme's
|
|
195
|
+
// own background with punctuation text), so number, glyph, and header
|
|
196
|
+
// text stay legible on this theme's background in every page mode — the
|
|
197
|
+
// glyph's shape carries the meaning where the colour does not.
|
|
151
198
|
const resolved: Partial<SyntaxThemeTokenMap> = {};
|
|
152
199
|
for (const key of ALL_SYNTAX_KEYS) {
|
|
153
|
-
|
|
200
|
+
const value = (
|
|
201
|
+
input.tokens as Partial<Record<SyntaxThemeTokenKey, SyntaxTokenValue>>
|
|
202
|
+
)[key];
|
|
203
|
+
resolved[key] =
|
|
204
|
+
value !== undefined
|
|
205
|
+
? resolveTokenValue(value)
|
|
206
|
+
: MARKER_KEYS.has(key)
|
|
207
|
+
? 'var(--color-syntax-punctuation)'
|
|
208
|
+
: key === 'diff-header'
|
|
209
|
+
? 'transparent'
|
|
210
|
+
: syntaxTokenDefaults[toCSSProperty(key)];
|
|
154
211
|
}
|
|
155
212
|
|
|
156
213
|
return {
|
|
@@ -170,14 +227,17 @@ export function syntaxThemeStyle(
|
|
|
170
227
|
): Record<string, string> {
|
|
171
228
|
const vars: Record<string, string> = {};
|
|
172
229
|
for (const key of ALL_SYNTAX_KEYS) {
|
|
173
|
-
|
|
230
|
+
// An unset diff slot is left to its default rather than written empty.
|
|
231
|
+
const value = theme.tokens[key];
|
|
232
|
+
if (value !== undefined) vars[toCSSProperty(key)] = value;
|
|
174
233
|
}
|
|
175
234
|
return vars;
|
|
176
235
|
}
|
|
177
236
|
|
|
178
237
|
/** Convert a syntax theme to CSS declarations (no selector wrapper). */
|
|
179
238
|
export function syntaxThemeToCSS(theme: SyntaxThemeDefinition): string {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
239
|
+
// An unset diff slot is left to its default rather than written `undefined`.
|
|
240
|
+
return ALL_SYNTAX_KEYS.filter(key => theme.tokens[key] !== undefined)
|
|
241
|
+
.map(key => toCSSProperty(key) + ': ' + theme.tokens[key] + ';')
|
|
242
|
+
.join('\n ');
|
|
183
243
|
}
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
export {syntaxTokenDefaults} from './tokens';
|
|
15
15
|
export type {SyntaxTokenName} from './tokens';
|
|
16
16
|
|
|
17
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
defineSyntaxTheme,
|
|
19
|
+
syntaxThemeStyle,
|
|
20
|
+
syntaxThemeToCSS,
|
|
21
|
+
} from './defineSyntaxTheme';
|
|
18
22
|
export type {
|
|
19
23
|
SyntaxThemeDefinition,
|
|
20
24
|
SyntaxThemeInput,
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
* colors automatically adapt to any theme's color system. Themes can also
|
|
14
14
|
* set an explicit syntax theme via defineTheme({ syntax: dracula }).
|
|
15
15
|
*
|
|
16
|
-
* 14
|
|
17
|
-
*
|
|
16
|
+
* 14 token colours and the code background, validated against 11 community
|
|
17
|
+
* code themes — all of them map cleanly to those slots — plus five diff row
|
|
18
|
+
* slots (added, removed, header paints; added and removed markers) that default
|
|
19
|
+
* to semantic tokens, so a syntax theme that does not set them keeps a
|
|
20
|
+
* readable diff.
|
|
18
21
|
*
|
|
19
22
|
* @see https://github.com/facebook/astryx/issues/1148
|
|
20
23
|
*/
|
|
@@ -51,6 +54,33 @@ export const syntaxTokenDefaults = {
|
|
|
51
54
|
'--color-syntax-punctuation': 'var(--color-text-secondary)',
|
|
52
55
|
// background -> muted surface
|
|
53
56
|
'--color-syntax-background': 'var(--color-background-muted)',
|
|
57
|
+
// Diff rows (CodeBlock isDiff): whole-row tints behind unchanged syntax
|
|
58
|
+
// colours. Added/removed derive from the semantic success/error muted
|
|
59
|
+
// tokens at 25% strength: at full strength the secondary-grey syntax
|
|
60
|
+
// tokens (operator, punctuation, comment) fall under 4.5:1 on the tints
|
|
61
|
+
// in light; at 25% every default token clears it on both schemes with
|
|
62
|
+
// margin while the tint still reads against the plain row. Header rows
|
|
63
|
+
// (@@, and ---/+++ before the first hunk) sit on the muted surface.
|
|
64
|
+
'--color-syntax-added':
|
|
65
|
+
'color-mix(in srgb, var(--color-success-muted) 25%, transparent)',
|
|
66
|
+
'--color-syntax-removed':
|
|
67
|
+
'color-mix(in srgb, var(--color-error-muted) 25%, transparent)',
|
|
68
|
+
'--color-syntax-diff-header': 'var(--color-background-muted)',
|
|
69
|
+
// The margin of a changed row — its line number and +/- glyph — in the
|
|
70
|
+
// default theme's tinted-text tokens, made for tinted surfaces; a defined
|
|
71
|
+
// syntax theme that leaves them unset follows its own punctuation colour
|
|
72
|
+
// (see defineSyntaxTheme). Code text keeps the syntax theme's colours.
|
|
73
|
+
'--color-syntax-added-marker': 'var(--color-text-green)',
|
|
74
|
+
'--color-syntax-removed-marker': 'var(--color-text-red)',
|
|
54
75
|
} as const;
|
|
55
76
|
|
|
56
77
|
export type SyntaxTokenName = keyof typeof syntaxTokenDefaults;
|
|
78
|
+
|
|
79
|
+
/** The five diff row slots: optional in a syntax theme, defaulted when unset. */
|
|
80
|
+
export const DIFF_SYNTAX_TOKEN_NAMES = [
|
|
81
|
+
'--color-syntax-added',
|
|
82
|
+
'--color-syntax-removed',
|
|
83
|
+
'--color-syntax-diff-header',
|
|
84
|
+
'--color-syntax-added-marker',
|
|
85
|
+
'--color-syntax-removed-marker',
|
|
86
|
+
] as const satisfies ReadonlyArray<SyntaxTokenName>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file useSyntaxTheme.diff.test.tsx
|
|
5
|
+
* @input A SyntaxTheme provider with a preset that sets no diff slots
|
|
6
|
+
* @output The hook hands a canvas consumer paintable colours for every slot
|
|
7
|
+
* @position Diff slot resolution for the imperative (canvas) path
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {render} from '@testing-library/react';
|
|
11
|
+
import {describe, expect, it} from 'vitest';
|
|
12
|
+
import {SyntaxTheme, useSyntaxTheme} from './SyntaxTheme';
|
|
13
|
+
import {dracula} from './presets';
|
|
14
|
+
import {parseColor} from '../../utils/color';
|
|
15
|
+
|
|
16
|
+
function Probe({onTokens}: {onTokens: (t: Record<string, string>) => void}) {
|
|
17
|
+
const syntax = useSyntaxTheme();
|
|
18
|
+
if (syntax) onTokens(syntax.tokens);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('useSyntaxTheme diff slots', () => {
|
|
23
|
+
it("resolves unset marker slots to the theme's own punctuation colour", () => {
|
|
24
|
+
let tokens: Record<string, string> = {};
|
|
25
|
+
render(
|
|
26
|
+
<SyntaxTheme theme={dracula}>
|
|
27
|
+
<Probe onTokens={t => (tokens = t)} />
|
|
28
|
+
</SyntaxTheme>,
|
|
29
|
+
);
|
|
30
|
+
expect(tokens.punctuation).toMatch(/^#|^rgb/);
|
|
31
|
+
expect(tokens['added-marker']).toBe(tokens.punctuation);
|
|
32
|
+
expect(tokens['removed-marker']).toBe(tokens.punctuation);
|
|
33
|
+
// The row paints derive from the page theme's resolved muted tokens at
|
|
34
|
+
// the default strength, as rgba() a canvas can paint.
|
|
35
|
+
expect(tokens.added).toMatch(/^rgba\(\d+, \d+, \d+, 0\.\d+\)$/);
|
|
36
|
+
expect(tokens.removed).toMatch(/^rgba\(\d+, \d+, \d+, 0\.\d+\)$/);
|
|
37
|
+
expect(parseColor(tokens.added)?.a).toBeCloseTo(0.2 * 0.25, 3);
|
|
38
|
+
expect(tokens['diff-header']).toBe('transparent');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
formatSharedDate,
|
|
31
31
|
DATE_FORMAT_WITH_WEEKDAY,
|
|
32
32
|
DATE_FORMAT_MONTH_YEAR,
|
|
33
|
+
plainDateWeekFieldName,
|
|
33
34
|
} from './plainDate';
|
|
34
35
|
import type {ISODateString} from './dateTypes';
|
|
35
36
|
|
|
@@ -149,6 +150,14 @@ describe('plainDateToDate / plainDateFromDate', () => {
|
|
|
149
150
|
expect(d.getDate()).toBe(15);
|
|
150
151
|
expect(plainDateFromDate(d)).toEqual(pd);
|
|
151
152
|
});
|
|
153
|
+
|
|
154
|
+
it('keeps years 1-99 instead of mapping them to 1900-1999', () => {
|
|
155
|
+
const pd: PlainDate = {year: 1, month: 1, day: 15};
|
|
156
|
+
const d = plainDateToDate(pd);
|
|
157
|
+
expect(d.getFullYear()).toBe(1);
|
|
158
|
+
expect(d.getDay()).toBe(1); // 0001-01-15 is a Monday
|
|
159
|
+
expect(plainDateFromDate(d)).toEqual(pd);
|
|
160
|
+
});
|
|
152
161
|
});
|
|
153
162
|
|
|
154
163
|
describe('plainDateToday', () => {
|
|
@@ -361,6 +370,15 @@ describe('plainDateDiffDays', () => {
|
|
|
361
370
|
),
|
|
362
371
|
).toBe(2);
|
|
363
372
|
});
|
|
373
|
+
|
|
374
|
+
it('counts across the year 99 to 100 boundary', () => {
|
|
375
|
+
expect(
|
|
376
|
+
plainDateDiffDays(
|
|
377
|
+
{year: 99, month: 12, day: 31},
|
|
378
|
+
{year: 100, month: 1, day: 1},
|
|
379
|
+
),
|
|
380
|
+
).toBe(1);
|
|
381
|
+
});
|
|
364
382
|
});
|
|
365
383
|
|
|
366
384
|
describe('plainDateToInstant / plainDateFromInstant', () => {
|
|
@@ -393,6 +411,119 @@ describe('plainDateToInstant / plainDateFromInstant', () => {
|
|
|
393
411
|
),
|
|
394
412
|
).toBe(Date.UTC(2026, 4, 13, 16, 30));
|
|
395
413
|
});
|
|
414
|
+
|
|
415
|
+
it.each([
|
|
416
|
+
// Both spring forward from a negative offset at local midnight.
|
|
417
|
+
[
|
|
418
|
+
'America/Santiago',
|
|
419
|
+
{year: 2026, month: 9, day: 6},
|
|
420
|
+
Date.UTC(2026, 8, 6, 4),
|
|
421
|
+
],
|
|
422
|
+
['America/Havana', {year: 2026, month: 3, day: 8}, Date.UTC(2026, 2, 8, 5)],
|
|
423
|
+
// A positive offset already resolved forward; it must not move.
|
|
424
|
+
['Asia/Beirut', {year: 2026, month: 3, day: 29}, Date.UTC(2026, 2, 28, 22)],
|
|
425
|
+
] as const)(
|
|
426
|
+
'starts a day whose midnight daylight saving skips at its first instant (%s)',
|
|
427
|
+
(timezoneID, date, firstInstant) => {
|
|
428
|
+
const instant = plainDateToInstant(date, timezoneID);
|
|
429
|
+
expect(instant).toBe(firstInstant);
|
|
430
|
+
expect(plainDateFromInstant(instant, timezoneID)).toEqual(date);
|
|
431
|
+
expect(plainDateFromInstant(instant - 1, timezoneID)).not.toEqual(date);
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
it('resolves any wall time inside a daylight-saving gap forward', () => {
|
|
436
|
+
// 2:30 AM does not happen on March 8, 2026 in Los Angeles: 3:30 AM PDT.
|
|
437
|
+
expect(
|
|
438
|
+
plainDateToInstant(
|
|
439
|
+
{year: 2026, month: 3, day: 8},
|
|
440
|
+
'America/Los_Angeles',
|
|
441
|
+
2,
|
|
442
|
+
30,
|
|
443
|
+
),
|
|
444
|
+
).toBe(Date.UTC(2026, 2, 8, 10, 30));
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('keeps the earlier instant of a wall time that happens twice', () => {
|
|
448
|
+
// Havana falls back at 1:00 AM on November 1, 2026: midnight once, then
|
|
449
|
+
// 12:00–1:00 AM twice; the first 12:30 AM is still daylight time.
|
|
450
|
+
expect(
|
|
451
|
+
plainDateToInstant(
|
|
452
|
+
{year: 2026, month: 11, day: 1},
|
|
453
|
+
'America/Havana',
|
|
454
|
+
0,
|
|
455
|
+
30,
|
|
456
|
+
),
|
|
457
|
+
).toBe(Date.UTC(2026, 10, 1, 4, 30));
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it.each([
|
|
461
|
+
// Berlin falls back at 3:00 AM CEST on October 25, 2026: 2:00–3:00 AM
|
|
462
|
+
// happens twice, first at +2, then at +1.
|
|
463
|
+
[
|
|
464
|
+
'Europe/Berlin',
|
|
465
|
+
{year: 2026, month: 10, day: 25},
|
|
466
|
+
2,
|
|
467
|
+
30,
|
|
468
|
+
Date.UTC(2026, 9, 25, 0, 30),
|
|
469
|
+
],
|
|
470
|
+
// Sydney falls back at 3:00 AM AEDT on April 5, 2026: first at +11.
|
|
471
|
+
[
|
|
472
|
+
'Australia/Sydney',
|
|
473
|
+
{year: 2026, month: 4, day: 5},
|
|
474
|
+
2,
|
|
475
|
+
30,
|
|
476
|
+
Date.UTC(2026, 3, 4, 15, 30),
|
|
477
|
+
],
|
|
478
|
+
] as const)(
|
|
479
|
+
'keeps the earlier instant of a repeated wall time in a positive-offset zone (%s)',
|
|
480
|
+
(timezoneID, date, hour, minute, earlier) => {
|
|
481
|
+
expect(plainDateToInstant(date, timezoneID, hour, minute)).toBe(earlier);
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
it('starts a date the zone skips at the first instant after the gap', () => {
|
|
486
|
+
// Samoa moved across the date line after December 29, 2011: December 30
|
|
487
|
+
// never happened in Pacific/Apia, so it resolves to 00:00 on December 31
|
|
488
|
+
// (+14), the next date.
|
|
489
|
+
const instant = plainDateToInstant(
|
|
490
|
+
{year: 2011, month: 12, day: 30},
|
|
491
|
+
'Pacific/Apia',
|
|
492
|
+
);
|
|
493
|
+
expect(instant).toBe(Date.UTC(2011, 11, 30, 10));
|
|
494
|
+
expect(plainDateFromInstant(instant, 'Pacific/Apia')).toEqual({
|
|
495
|
+
year: 2011,
|
|
496
|
+
month: 12,
|
|
497
|
+
day: 31,
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('moves a wall time in a 30-minute gap forward by 30 minutes', () => {
|
|
502
|
+
// Lord Howe Island jumps from 2:00 to 2:30 AM on October 4, 2026, from
|
|
503
|
+
// +10:30 to +11: 2:15 AM does not happen and reads as 2:45 AM.
|
|
504
|
+
expect(
|
|
505
|
+
plainDateToInstant(
|
|
506
|
+
{year: 2026, month: 10, day: 4},
|
|
507
|
+
'Australia/Lord_Howe',
|
|
508
|
+
2,
|
|
509
|
+
15,
|
|
510
|
+
),
|
|
511
|
+
).toBe(Date.UTC(2026, 9, 3, 15, 45));
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('keeps years 1-99 instead of mapping them to 1900-1999', () => {
|
|
515
|
+
const date: PlainDate = {year: 1, month: 1, day: 15};
|
|
516
|
+
// 0001-01-01T00:00:00Z is -62135596800000
|
|
517
|
+
expect(plainDateToInstant(date, 'UTC', 9, 30)).toBe(
|
|
518
|
+
-62135596800000 + (14 * 24 + 9) * 3600000 + 30 * 60000,
|
|
519
|
+
);
|
|
520
|
+
expect(
|
|
521
|
+
plainDateFromInstant(
|
|
522
|
+
plainDateToInstant(date, 'America/Los_Angeles'),
|
|
523
|
+
'America/Los_Angeles',
|
|
524
|
+
),
|
|
525
|
+
).toEqual(date);
|
|
526
|
+
});
|
|
396
527
|
});
|
|
397
528
|
|
|
398
529
|
describe('plainDateIsBefore / plainDateIsAfter', () => {
|
|
@@ -579,6 +710,11 @@ describe('plainDateGetWeekNumber', () => {
|
|
|
579
710
|
it('returns week 1 for Jan 4 (always in ISO week 1)', () => {
|
|
580
711
|
expect(plainDateGetWeekNumber({year: 2026, month: 1, day: 4})).toBe(1);
|
|
581
712
|
});
|
|
713
|
+
|
|
714
|
+
it('keeps years 1-99 instead of mapping them to 1900-1999', () => {
|
|
715
|
+
// 0004-01-01 is a Thursday (ISO week 1); 1904-01-01 was a Friday.
|
|
716
|
+
expect(plainDateGetWeekNumber({year: 4, month: 1, day: 1})).toBe(1);
|
|
717
|
+
});
|
|
582
718
|
});
|
|
583
719
|
|
|
584
720
|
describe('plainDateFormat', () => {
|
|
@@ -663,3 +799,14 @@ describe('formatSharedDate', () => {
|
|
|
663
799
|
);
|
|
664
800
|
});
|
|
665
801
|
});
|
|
802
|
+
|
|
803
|
+
describe('plainDateWeekFieldName', () => {
|
|
804
|
+
it("returns the locale's CLDR name for the week field", () => {
|
|
805
|
+
expect(plainDateWeekFieldName('en-US')).toBe('week');
|
|
806
|
+
expect(plainDateWeekFieldName('de-DE')).toBe('Woche');
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('never returns an empty name', () => {
|
|
810
|
+
expect(plainDateWeekFieldName('zxx').length).toBeGreaterThan(0);
|
|
811
|
+
});
|
|
812
|
+
});
|
package/src/utils/plainDate.ts
CHANGED
|
@@ -45,7 +45,11 @@ export function plainDateToISO(pd: PlainDate): ISODateString {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function plainDateToDate(pd: PlainDate): Date {
|
|
48
|
-
|
|
48
|
+
// Unlike the Date constructor, setFullYear keeps years 0-99 as given
|
|
49
|
+
// instead of reading them as 1900-1999.
|
|
50
|
+
const d = new Date(0, 0, 1);
|
|
51
|
+
d.setFullYear(pd.year, pd.month - 1, pd.day);
|
|
52
|
+
return d;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export function plainDateFromDate(d: Date): PlainDate {
|
|
@@ -89,6 +93,24 @@ export function plainDateAddDays(pd: PlainDate, n: number): PlainDate {
|
|
|
89
93
|
return plainDateFromDate(d);
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
/**
|
|
97
|
+
* `Date.UTC` with years 0-99 kept as given; `Date.UTC` itself reads them as
|
|
98
|
+
* 1900-1999.
|
|
99
|
+
*/
|
|
100
|
+
function utcTime(
|
|
101
|
+
year: number,
|
|
102
|
+
monthIndex: number,
|
|
103
|
+
day: number,
|
|
104
|
+
hour = 0,
|
|
105
|
+
minute = 0,
|
|
106
|
+
second = 0,
|
|
107
|
+
): number {
|
|
108
|
+
const d = new Date(0);
|
|
109
|
+
d.setUTCFullYear(year, monthIndex, day);
|
|
110
|
+
d.setUTCHours(hour, minute, second);
|
|
111
|
+
return d.getTime();
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
/**
|
|
93
115
|
* Whole calendar days from `a` to `b`, ignoring time of day. Positive when
|
|
94
116
|
* `b` is after `a`, negative when before. Uses UTC midnight so DST shifts
|
|
@@ -96,8 +118,8 @@ export function plainDateAddDays(pd: PlainDate, n: number): PlainDate {
|
|
|
96
118
|
*/
|
|
97
119
|
export function plainDateDiffDays(a: PlainDate, b: PlainDate): number {
|
|
98
120
|
const msPerDay = 24 * 60 * 60 * 1000;
|
|
99
|
-
const aUTC =
|
|
100
|
-
const bUTC =
|
|
121
|
+
const aUTC = utcTime(a.year, a.month - 1, a.day);
|
|
122
|
+
const bUTC = utcTime(b.year, b.month - 1, b.day);
|
|
101
123
|
return Math.round((bUTC - aUTC) / msPerDay);
|
|
102
124
|
}
|
|
103
125
|
|
|
@@ -152,7 +174,7 @@ export function getTimeZoneParts(
|
|
|
152
174
|
function getTimezoneOffsetMS(timezoneID: string, instant: number): number {
|
|
153
175
|
const parts = getTimeZoneParts(instant, timezoneID);
|
|
154
176
|
return (
|
|
155
|
-
|
|
177
|
+
utcTime(
|
|
156
178
|
parts.year,
|
|
157
179
|
parts.month - 1,
|
|
158
180
|
parts.day,
|
|
@@ -163,17 +185,35 @@ function getTimezoneOffsetMS(timezoneID: string, instant: number): number {
|
|
|
163
185
|
);
|
|
164
186
|
}
|
|
165
187
|
|
|
188
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
189
|
+
|
|
166
190
|
export function plainDateToInstant(
|
|
167
191
|
date: PlainDate,
|
|
168
192
|
timezoneID: string,
|
|
169
193
|
hour = 0,
|
|
170
194
|
minute = 0,
|
|
171
195
|
): number {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
// The wall time read as if it were UTC; subtracting an offset gives a
|
|
197
|
+
// candidate instant.
|
|
198
|
+
const wallTime = utcTime(date.year, date.month - 1, date.day, hour, minute);
|
|
199
|
+
// The offsets in effect a day either side cover any transition that can make
|
|
200
|
+
// this wall time skipped or repeated.
|
|
201
|
+
const offsetBefore = getTimezoneOffsetMS(timezoneID, wallTime - DAY_MS);
|
|
202
|
+
const offsetAfter = getTimezoneOffsetMS(timezoneID, wallTime + DAY_MS);
|
|
203
|
+
// AST-067 (Temporal's "compatible" disambiguation): a wall time that happens
|
|
204
|
+
// twice resolves to its earlier occurrence, and one that never happens is
|
|
205
|
+
// read in the offset before the transition, which moves it forward by the
|
|
206
|
+
// length of the gap.
|
|
207
|
+
const candidates = [offsetBefore, offsetAfter]
|
|
208
|
+
.map(offset => wallTime - offset)
|
|
209
|
+
.filter(
|
|
210
|
+
(instant, index) =>
|
|
211
|
+
getTimezoneOffsetMS(timezoneID, instant) ===
|
|
212
|
+
(index === 0 ? offsetBefore : offsetAfter),
|
|
213
|
+
);
|
|
214
|
+
return candidates.length > 0
|
|
215
|
+
? Math.min(...candidates)
|
|
216
|
+
: wallTime - offsetBefore;
|
|
177
217
|
}
|
|
178
218
|
|
|
179
219
|
export function plainDateFromInstant(
|
|
@@ -245,7 +285,7 @@ export function plainDateGetWeekNumber(pd: PlainDate): number {
|
|
|
245
285
|
const d = plainDateToDate(pd);
|
|
246
286
|
const dayNum = d.getDay() || 7;
|
|
247
287
|
d.setDate(d.getDate() + 4 - dayNum);
|
|
248
|
-
const yearStart =
|
|
288
|
+
const yearStart = plainDateToDate({year: d.getFullYear(), month: 1, day: 1});
|
|
249
289
|
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
250
290
|
}
|
|
251
291
|
|
|
@@ -330,6 +370,31 @@ export function plainDateFormat(
|
|
|
330
370
|
}).format(plainDateToDate(pd));
|
|
331
371
|
}
|
|
332
372
|
|
|
373
|
+
/**
|
|
374
|
+
* The locale's name for the "week of year" date field ("week", "Woche", "週"),
|
|
375
|
+
* from CLDR through `Intl.DisplayNames`. Calendar names its week-number
|
|
376
|
+
* column header with it, so no catalog string is needed for a word the
|
|
377
|
+
* platform already carries in every locale.
|
|
378
|
+
*
|
|
379
|
+
* `Intl.DisplayNames` with `type: 'dateTimeField'` is inside the Baseline − 2
|
|
380
|
+
* support tier (Chrome 106, Safari 15.4, Firefox 110). Should a host lack it
|
|
381
|
+
* or return nothing, the field's English CLDR name stands in rather than an
|
|
382
|
+
* empty header.
|
|
383
|
+
*/
|
|
384
|
+
export function plainDateWeekFieldName(locale: Locale = 'en'): string {
|
|
385
|
+
try {
|
|
386
|
+
const name = new Intl.DisplayNames(locale, {type: 'dateTimeField'}).of(
|
|
387
|
+
'weekOfYear',
|
|
388
|
+
);
|
|
389
|
+
if (name != null && name !== '') {
|
|
390
|
+
return name;
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
// Fall through to the English CLDR value.
|
|
394
|
+
}
|
|
395
|
+
return 'week';
|
|
396
|
+
}
|
|
397
|
+
|
|
333
398
|
// =============================================================================
|
|
334
399
|
// Shared date-only format vocabulary
|
|
335
400
|
// =============================================================================
|