@astryxdesign/core 0.4.4 → 0.4.5-canary.1fdecb1

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 (275) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +11 -2
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +4 -0
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts +5 -2
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +65 -15
  8. package/dist/BottomSheet/index.d.ts +1 -1
  9. package/dist/BottomSheet/index.d.ts.map +1 -1
  10. package/dist/BottomSheet/snapOffsets.d.ts +57 -20
  11. package/dist/BottomSheet/snapOffsets.d.ts.map +1 -1
  12. package/dist/BottomSheet/snapOffsets.js +106 -30
  13. package/dist/BottomSheet/useSheetGestures.d.ts +13 -8
  14. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  15. package/dist/BottomSheet/useSheetGestures.js +160 -64
  16. package/dist/Calendar/Calendar.d.ts +17 -0
  17. package/dist/Calendar/Calendar.d.ts.map +1 -1
  18. package/dist/Calendar/Calendar.js +34 -3
  19. package/dist/Calendar/getStandaloneShortWeekdayNames.d.ts +14 -0
  20. package/dist/Calendar/getStandaloneShortWeekdayNames.d.ts.map +1 -0
  21. package/dist/Calendar/getStandaloneShortWeekdayNames.js +26 -0
  22. package/dist/Calendar/hooks/useCalendarConstraints.d.ts +22 -1
  23. package/dist/Calendar/hooks/useCalendarConstraints.d.ts.map +1 -1
  24. package/dist/Calendar/hooks/useCalendarConstraints.js +26 -4
  25. package/dist/Calendar/hooks/useCalendarDays.d.ts.map +1 -1
  26. package/dist/Calendar/hooks/useCalendarDays.js +14 -10
  27. package/dist/Calendar/standaloneShortWeekdayNames.generated.d.ts +47 -0
  28. package/dist/Calendar/standaloneShortWeekdayNames.generated.d.ts.map +1 -0
  29. package/dist/Calendar/standaloneShortWeekdayNames.generated.js +40 -0
  30. package/dist/Center/Center.d.ts +23 -1
  31. package/dist/Center/Center.d.ts.map +1 -1
  32. package/dist/Center/Center.js +12 -6
  33. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  34. package/dist/CheckboxInput/CheckboxInput.js +9 -0
  35. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  36. package/dist/ComplexSelector/ComplexSelector.js +6 -1
  37. package/dist/DateInput/DateInput.d.ts.map +1 -1
  38. package/dist/DateInput/DateInput.js +6 -1
  39. package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
  40. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  41. package/dist/DateRangeInput/DateRangeInput.js +38 -3
  42. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  43. package/dist/DateTimeInput/DateTimeInput.js +7 -2
  44. package/dist/Dialog/Dialog.d.ts.map +1 -1
  45. package/dist/Dialog/Dialog.js +3 -3
  46. package/dist/Field/FieldLabel.d.ts +2 -1
  47. package/dist/Field/FieldLabel.d.ts.map +1 -1
  48. package/dist/Field/FieldLabel.js +21 -3
  49. package/dist/Field/InputClearButton.d.ts +2 -1
  50. package/dist/Field/InputClearButton.d.ts.map +1 -1
  51. package/dist/Field/InputClearButton.js +6 -1
  52. package/dist/FormLayout/FormLayout.d.ts +24 -2
  53. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  54. package/dist/FormLayout/FormLayout.js +7 -2
  55. package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
  56. package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
  57. package/dist/FormLayout/FormLayoutContext.js +19 -4
  58. package/dist/FormLayout/index.d.ts +1 -1
  59. package/dist/FormLayout/index.d.ts.map +1 -1
  60. package/dist/Item/Item.d.ts +9 -1
  61. package/dist/Item/Item.d.ts.map +1 -1
  62. package/dist/Item/Item.js +45 -7
  63. package/dist/Layer/useLayer.d.ts.map +1 -1
  64. package/dist/Layer/useLayer.js +3 -2
  65. package/dist/Layout/container.stylex.d.ts +4 -2
  66. package/dist/Layout/container.stylex.d.ts.map +1 -1
  67. package/dist/Layout/container.stylex.js +19 -11
  68. package/dist/Layout/index.d.ts +1 -0
  69. package/dist/Layout/index.d.ts.map +1 -1
  70. package/dist/Layout/index.js +4 -0
  71. package/dist/Layout/padding.stylex.d.ts +315 -12
  72. package/dist/Layout/padding.stylex.d.ts.map +1 -1
  73. package/dist/Layout/padding.stylex.js +389 -12
  74. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  75. package/dist/Lightbox/Lightbox.js +2 -1
  76. package/dist/Markdown/Markdown.d.ts +8 -0
  77. package/dist/Markdown/Markdown.d.ts.map +1 -1
  78. package/dist/Markdown/Markdown.js +31 -3
  79. package/dist/Markdown/parser.d.ts +14 -2
  80. package/dist/Markdown/parser.d.ts.map +1 -1
  81. package/dist/Markdown/parser.js +50 -2
  82. package/dist/MobileNav/MobileNav.d.ts +1 -0
  83. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  84. package/dist/MobileNav/MobileNav.js +24 -6
  85. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  86. package/dist/MultiSelector/MultiSelector.js +6 -1
  87. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  88. package/dist/NumberInput/NumberInput.js +36 -3
  89. package/dist/Outline/parseOutlineFromMarkdown.d.ts +2 -0
  90. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  91. package/dist/Outline/parseOutlineFromMarkdown.js +5 -31
  92. package/dist/RadioList/RadioList.d.ts.map +1 -1
  93. package/dist/RadioList/RadioList.js +12 -1
  94. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  95. package/dist/Resizable/ResizeHandle.js +3 -3
  96. package/dist/Section/Section.d.ts +36 -1
  97. package/dist/Section/Section.d.ts.map +1 -1
  98. package/dist/Section/Section.js +12 -2
  99. package/dist/Selector/Selector.d.ts +26 -1
  100. package/dist/Selector/Selector.d.ts.map +1 -1
  101. package/dist/Selector/Selector.js +93 -15
  102. package/dist/Selector/SelectorOption.d.ts +14 -1
  103. package/dist/Selector/SelectorOption.d.ts.map +1 -1
  104. package/dist/Selector/SelectorOption.js +6 -0
  105. package/dist/Selector/SelectorRowLayoutContext.d.ts +8 -0
  106. package/dist/Selector/SelectorRowLayoutContext.d.ts.map +1 -0
  107. package/dist/Selector/SelectorRowLayoutContext.js +27 -0
  108. package/dist/Selector/types.d.ts +1 -0
  109. package/dist/Selector/types.d.ts.map +1 -1
  110. package/dist/Slider/Slider.d.ts.map +1 -1
  111. package/dist/Slider/Slider.js +2 -2
  112. package/dist/Stack/Stack.d.ts +23 -1
  113. package/dist/Stack/Stack.d.ts.map +1 -1
  114. package/dist/Stack/Stack.js +12 -6
  115. package/dist/StatusDot/StatusDot.d.ts +43 -3
  116. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  117. package/dist/StatusDot/StatusDot.js +42 -5
  118. package/dist/Switch/Switch.d.ts.map +1 -1
  119. package/dist/Switch/Switch.js +9 -0
  120. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  121. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  122. package/dist/TextArea/TextArea.d.ts.map +1 -1
  123. package/dist/TextArea/TextArea.js +6 -1
  124. package/dist/TextInput/TextInput.d.ts.map +1 -1
  125. package/dist/TextInput/TextInput.js +6 -1
  126. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  127. package/dist/TimeInput/TimeInput.js +6 -1
  128. package/dist/astryx.css +40 -21
  129. package/dist/hooks/scrollbarGutter.d.ts +28 -0
  130. package/dist/hooks/scrollbarGutter.d.ts.map +1 -0
  131. package/dist/hooks/scrollbarGutter.js +103 -0
  132. package/dist/hooks/useResolvedRequired.d.ts +19 -0
  133. package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
  134. package/dist/hooks/useResolvedRequired.js +40 -0
  135. package/dist/hooks/useScrollLock.d.ts +4 -0
  136. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  137. package/dist/hooks/useScrollLock.js +13 -1
  138. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  139. package/dist/theme/derivedVarRegistry.js +7 -0
  140. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  141. package/dist/theme/generateThemeRules.js +37 -4
  142. package/dist/utils/plainDate.d.ts +6 -0
  143. package/dist/utils/plainDate.d.ts.map +1 -1
  144. package/dist/utils/plainDate.js +12 -0
  145. package/locales/af-ZA.json +958 -0
  146. package/locales/ar-SA.json +998 -0
  147. package/locales/ca-ES.json +974 -0
  148. package/locales/cs-CZ.json +990 -0
  149. package/locales/da-DK.json +966 -0
  150. package/locales/de-DE.json +966 -0
  151. package/locales/el-GR.json +990 -0
  152. package/locales/en.json +8 -0
  153. package/locales/es-ES.json +978 -0
  154. package/locales/fi-FI.json +990 -0
  155. package/locales/fr-FR.json +928 -0
  156. package/locales/he-IL.json +990 -0
  157. package/locales/hu-HU.json +990 -0
  158. package/locales/it-IT.json +978 -0
  159. package/locales/ja-JP.json +998 -0
  160. package/locales/ko-KR.json +990 -0
  161. package/locales/nl-NL.json +954 -0
  162. package/locales/no-NO.json +978 -0
  163. package/locales/pl-PL.json +982 -0
  164. package/locales/pseudo.json +6 -0
  165. package/locales/pt-BR.json +982 -0
  166. package/locales/pt-PT.json +982 -0
  167. package/locales/ro-RO.json +982 -0
  168. package/locales/ru-RU.json +990 -0
  169. package/locales/sr-SP.json +990 -0
  170. package/locales/sv-SE.json +986 -0
  171. package/locales/tr-TR.json +990 -0
  172. package/locales/uk-UA.json +990 -0
  173. package/locales/vi-VN.json +986 -0
  174. package/locales/zh-CN.json +998 -0
  175. package/locales/zh-TW.json +998 -0
  176. package/package.json +3 -3
  177. package/src/BottomSheet/BottomSheet.doc.mjs +36 -3
  178. package/src/BottomSheet/BottomSheet.test.tsx +177 -2
  179. package/src/BottomSheet/BottomSheet.tsx +16 -2
  180. package/src/BottomSheet/BottomSheetPanel.test.tsx +41 -0
  181. package/src/BottomSheet/BottomSheetPanel.tsx +95 -15
  182. package/src/BottomSheet/index.ts +5 -1
  183. package/src/BottomSheet/snapOffsets.test.ts +114 -47
  184. package/src/BottomSheet/snapOffsets.ts +119 -33
  185. package/src/BottomSheet/useSheetGestures.test.ts +113 -15
  186. package/src/BottomSheet/useSheetGestures.ts +196 -100
  187. package/src/Calendar/Calendar.doc.mjs +16 -0
  188. package/src/Calendar/Calendar.test.tsx +137 -0
  189. package/src/Calendar/Calendar.tsx +65 -2
  190. package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +54 -0
  191. package/src/Calendar/getStandaloneShortWeekdayNames.ts +38 -0
  192. package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
  193. package/src/Calendar/hooks/useCalendarDays.ts +13 -10
  194. package/src/Calendar/standaloneShortWeekdayNames.generated.ts +50 -0
  195. package/src/Center/Center.doc.mjs +48 -0
  196. package/src/Center/Center.test.tsx +158 -0
  197. package/src/Center/Center.tsx +50 -9
  198. package/src/CheckboxInput/CheckboxInput.tsx +6 -0
  199. package/src/ComplexSelector/ComplexSelector.tsx +3 -1
  200. package/src/DateInput/DateInput.tsx +3 -1
  201. package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
  202. package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
  203. package/src/DateRangeInput/DateRangeInput.tsx +72 -1
  204. package/src/DateTimeInput/DateTimeInput.tsx +4 -2
  205. package/src/Dialog/Dialog.test.tsx +24 -0
  206. package/src/Dialog/Dialog.tsx +3 -0
  207. package/src/Field/Field.doc.mjs +1 -0
  208. package/src/Field/FieldLabel.tsx +20 -4
  209. package/src/Field/InputClearButton.test.tsx +25 -0
  210. package/src/Field/InputClearButton.tsx +4 -1
  211. package/src/FormLayout/FormLayout.doc.mjs +13 -0
  212. package/src/FormLayout/FormLayout.test.tsx +181 -4
  213. package/src/FormLayout/FormLayout.tsx +33 -2
  214. package/src/FormLayout/FormLayoutContext.ts +22 -7
  215. package/src/FormLayout/index.ts +1 -1
  216. package/src/Item/Item.doc.mjs +1 -0
  217. package/src/Item/Item.test.tsx +50 -0
  218. package/src/Item/Item.tsx +40 -1
  219. package/src/Layer/useLayer.tsx +13 -2
  220. package/src/Layout/Layout.doc.mjs +18 -0
  221. package/src/Layout/container.stylex.ts +11 -3
  222. package/src/Layout/index.ts +4 -0
  223. package/src/Layout/overlayPaddingReset.test.tsx +222 -0
  224. package/src/Layout/padding.stylex.ts +192 -12
  225. package/src/Lightbox/Lightbox.tsx +2 -1
  226. package/src/Markdown/Markdown.doc.mjs +3 -0
  227. package/src/Markdown/Markdown.test.tsx +78 -0
  228. package/src/Markdown/Markdown.tsx +41 -1
  229. package/src/Markdown/parser.ts +60 -2
  230. package/src/MobileNav/MobileNav.tsx +61 -5
  231. package/src/MobileNav/MobileNavEntryAnimation.test.tsx +212 -0
  232. package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +109 -0
  233. package/src/MultiSelector/MultiSelector.tsx +3 -1
  234. package/src/NumberInput/NumberInput.doc.mjs +24 -0
  235. package/src/NumberInput/NumberInput.test.tsx +138 -0
  236. package/src/NumberInput/NumberInput.tsx +43 -5
  237. package/src/Outline/Outline.doc.mjs +2 -0
  238. package/src/Outline/parseOutlineFromMarkdown.ts +10 -40
  239. package/src/RadioList/RadioList.tsx +9 -1
  240. package/src/Resizable/ResizeHandle.test.tsx +22 -0
  241. package/src/Resizable/ResizeHandle.tsx +9 -3
  242. package/src/Section/Section.doc.mjs +55 -0
  243. package/src/Section/Section.test.tsx +201 -4
  244. package/src/Section/Section.tsx +69 -0
  245. package/src/Selector/Selector.doc.mjs +16 -2
  246. package/src/Selector/Selector.test.tsx +524 -0
  247. package/src/Selector/Selector.tsx +161 -17
  248. package/src/Selector/SelectorOption.doc.mjs +7 -0
  249. package/src/Selector/SelectorOption.tsx +21 -0
  250. package/src/Selector/SelectorRowLayoutContext.ts +32 -0
  251. package/src/Selector/types.ts +4 -4
  252. package/src/Slider/Slider.tsx +10 -0
  253. package/src/Stack/Stack.doc.mjs +72 -0
  254. package/src/Stack/Stack.test.tsx +156 -0
  255. package/src/Stack/Stack.tsx +50 -8
  256. package/src/StatusDot/StatusDot.doc.mjs +13 -0
  257. package/src/StatusDot/StatusDot.test.tsx +115 -8
  258. package/src/StatusDot/StatusDot.tsx +74 -9
  259. package/src/Switch/Switch.tsx +6 -0
  260. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
  261. package/src/TextArea/TextArea.tsx +3 -1
  262. package/src/TextInput/TextInput.tsx +3 -1
  263. package/src/TimeInput/TimeInput.tsx +3 -1
  264. package/src/hooks/scrollbarGutter.test.ts +167 -0
  265. package/src/hooks/scrollbarGutter.ts +121 -0
  266. package/src/hooks/useResolvedRequired.ts +42 -0
  267. package/src/hooks/useScrollLock.doc.mjs +4 -4
  268. package/src/hooks/useScrollLock.test.ts +73 -0
  269. package/src/hooks/useScrollLock.ts +14 -0
  270. package/src/theme/derivedVarRegistry.test.ts +14 -3
  271. package/src/theme/derivedVarRegistry.ts +4 -0
  272. package/src/theme/generateThemeRules.test.ts +79 -0
  273. package/src/theme/generateThemeRules.ts +49 -4
  274. package/src/utils/plainDate.test.ts +50 -0
  275. package/src/utils/plainDate.ts +12 -0
@@ -67,6 +67,10 @@ export const derivedVarRegistry: Record<string, DerivedVarEntry[]> = {
67
67
  ],
68
68
  field: [{property: 'borderRadius', vars: ['--_field-radius']}],
69
69
  hovercard: [{property: 'borderRadius', vars: ['--_hovercard-radius']}],
70
+ 'number-input': [
71
+ {property: 'padding', expand: 'container'},
72
+ {property: 'borderRadius', vars: ['--_field-radius']},
73
+ ],
70
74
  popover: [{property: 'borderRadius', vars: ['--_popover-radius']}],
71
75
  'progressbar-mark': [
72
76
  {property: 'width', vars: ['--_progressbar-mark-width'], replaces: true},
@@ -584,3 +584,82 @@ describe('brutalist-style derived expansion', () => {
584
584
  expect(rule).toContain('--_dropdown-menu-padding: 4px');
585
585
  });
586
586
  });
587
+
588
+ describe('physical padding longhands', () => {
589
+ const ruleFor = (
590
+ component: string,
591
+ base: Record<string, string>,
592
+ name = 'test-physical-padding',
593
+ ) =>
594
+ generateThemeRules(
595
+ defineTheme({name, components: {[component]: {base}}}),
596
+ ).find(r => r.includes(`.astryx-${component}`));
597
+
598
+ // `padding-top`/`padding-bottom` ARE the block edges in every horizontal
599
+ // writing mode, so the expansion can normalize them with no direction
600
+ // assumption. Without that, the padding lands raw on the element and the
601
+ // component's internals — the NumberInput stepper column, container bleed —
602
+ // read the default instead of what the theme set.
603
+ it.each([
604
+ ['paddingTop', {paddingTop: '14px'}, 'block-start'],
605
+ ['paddingBottom', {paddingBottom: '14px'}, 'block-end'],
606
+ ])('routes %s through the container expansion', (_label, base, edge) => {
607
+ const rule = ruleFor('card', base);
608
+ expect(rule).toContain(`--astryx-card-padding-${edge}: 14px`);
609
+ expect(rule).not.toMatch(/[{;]\s*padding-(top|bottom):/);
610
+ });
611
+
612
+ it('normalizes both block edges together, asymmetrically', () => {
613
+ const rule = ruleFor('number-input', {
614
+ paddingTop: '14px',
615
+ paddingBottom: '6px',
616
+ });
617
+ expect(rule).toContain('--astryx-number-input-padding-block-start: 14px');
618
+ expect(rule).toContain('--astryx-number-input-padding-block-end: 6px');
619
+ });
620
+
621
+ it.each(['card', 'dialog', 'section', 'number-input'])(
622
+ 'reaches %s, which expands its padding',
623
+ component => {
624
+ expect(ruleFor(component, {paddingTop: '14px'})).toContain(
625
+ `--astryx-${component}-padding-block-start: 14px`,
626
+ );
627
+ },
628
+ );
629
+
630
+ // THE RTL GUARD. `paddingLeft` is inline-start in LTR and inline-end in RTL,
631
+ // and the tokens are consumed by logical properties, so mapping it would
632
+ // silently move the padding to the other edge in RTL. It stays physical —
633
+ // which is what the author wrote — and does not reach the tokens.
634
+ it('leaves the direction-relative inline pair physical', () => {
635
+ const rule = ruleFor('card', {paddingLeft: '20px', paddingRight: '8px'});
636
+ expect(rule).toContain('padding-left: 20px');
637
+ expect(rule).toContain('padding-right: 8px');
638
+ expect(rule).not.toContain('--astryx-card-padding-inline');
639
+ });
640
+
641
+ it('expands the block edges while leaving left physical', () => {
642
+ const rule = ruleFor('card', {paddingTop: '14px', paddingLeft: '20px'});
643
+ expect(rule).toContain('--astryx-card-padding-block-start: 14px');
644
+ expect(rule).toContain('padding-left: 20px');
645
+ });
646
+
647
+ // The shorthand-plus-override case was the worst one: the tokens carried
648
+ // 10px while the element painted 14px on top, so every internal compensated
649
+ // by the wrong amount.
650
+ it('lets a physical longhand override the shorthand per edge', () => {
651
+ const rule = ruleFor('card', {padding: '10px', paddingTop: '14px'});
652
+ expect(rule).toContain('--astryx-card-padding-block-start: 14px');
653
+ expect(rule).toContain('--astryx-card-padding-block-end: 10px');
654
+ expect(rule).toContain('--astryx-card-padding-inline: 10px');
655
+ expect(rule).not.toMatch(/[{;]\s*padding(-top)?:/);
656
+ });
657
+
658
+ // A `vars` entry carries one value for the whole box, so a single physical
659
+ // edge must not feed it — only the container expansion takes these.
660
+ it('does not feed a single edge to a whole-box derived var', () => {
661
+ const rule = ruleFor('dropdown-menu', {paddingTop: '14px'});
662
+ expect(rule).toContain('padding-top: 14px');
663
+ expect(rule).not.toContain('--_dropdown-menu-padding');
664
+ });
665
+ });
@@ -128,6 +128,34 @@ const PADDING_PROPS = new Set([
128
128
  'paddingInlineEnd',
129
129
  ]);
130
130
 
131
+ /**
132
+ * Physical block-axis longhands, and the logical longhand each one *is* in
133
+ * every horizontal writing mode. Normalizing them costs no direction
134
+ * assumption, which is why they can join the container expansion.
135
+ *
136
+ * `paddingLeft`/`paddingRight` are deliberately absent. They are
137
+ * direction-relative — left is inline-start in LTR and inline-end in RTL — and
138
+ * the expansion's tokens are consumed by logical properties, so mapping them
139
+ * would put the padding on the opposite edge in RTL. They keep their physical
140
+ * meaning and land on the element as `padding-left`/`padding-right`; the cost
141
+ * is that a component's internals cannot see them.
142
+ */
143
+ const PHYSICAL_BLOCK_PADDING_PROPS: Record<string, string> = {
144
+ paddingTop: 'paddingBlockStart',
145
+ paddingBottom: 'paddingBlockEnd',
146
+ };
147
+
148
+ /**
149
+ * Every padding spelling the container expansion consumes. Kept separate from
150
+ * PADDING_PROPS, which also routes longhands to `vars`-style derived entries —
151
+ * those carry one value for the whole box, so a single physical edge must not
152
+ * reach them.
153
+ */
154
+ const CONTAINER_PADDING_PROPS = new Set([
155
+ ...PADDING_PROPS,
156
+ ...Object.keys(PHYSICAL_BLOCK_PADDING_PROPS),
157
+ ]);
158
+
131
159
  interface ParsedPadding {
132
160
  blockStart?: string;
133
161
  blockEnd?: string;
@@ -138,12 +166,14 @@ interface ParsedPadding {
138
166
 
139
167
  /**
140
168
  * Parse CSS padding shorthand/longhand into block/inline values.
141
- * Supports 1-3 value shorthands and logical properties.
169
+ * Supports 1-3 value shorthands, logical properties, and the physical block
170
+ * longhands normalized by PHYSICAL_BLOCK_PADDING_PROPS.
142
171
  */
143
172
  function parsePadding(props: [string, string][]): ParsedPadding {
144
173
  const result: ParsedPadding = {};
145
174
 
146
- for (const [prop, value] of props) {
175
+ for (const [rawProp, value] of props) {
176
+ const prop = PHYSICAL_BLOCK_PADDING_PROPS[rawProp] ?? rawProp;
147
177
  switch (prop) {
148
178
  case 'padding': {
149
179
  const parts = value.trim().split(/\s+/);
@@ -377,13 +407,28 @@ function generateComponentRules(
377
407
  }
378
408
  }
379
409
  }
410
+ // A physical block longhand reaches the container expansion only. It
411
+ // names one edge, so it must not feed a `vars` entry above, which
412
+ // carries the padding for the whole box.
413
+ if (
414
+ prop in PHYSICAL_BLOCK_PADDING_PROPS &&
415
+ getDerivedVars(component, 'padding').some(
416
+ e => e.expand === 'container',
417
+ )
418
+ ) {
419
+ containerExpanded = true;
420
+ }
380
421
  }
381
422
 
382
423
  // Container padding expansion: replace padding props with
383
424
  // component-scoped container tokens for layout integration.
384
425
  if (containerExpanded) {
385
- const paddingProps = props.filter(([p]) => PADDING_PROPS.has(p));
386
- const nonPaddingProps = props.filter(([p]) => !PADDING_PROPS.has(p));
426
+ const paddingProps = props.filter(([p]) =>
427
+ CONTAINER_PADDING_PROPS.has(p),
428
+ );
429
+ const nonPaddingProps = props.filter(
430
+ ([p]) => !CONTAINER_PADDING_PROPS.has(p),
431
+ );
387
432
  const parsed = parsePadding(paddingProps);
388
433
  const containerTokens = expandContainerPadding(component, parsed);
389
434
  finalProps = [...nonPaddingProps, ...containerTokens];
@@ -13,6 +13,7 @@ import {
13
13
  plainDateDayOfWeek,
14
14
  plainDateAddMonths,
15
15
  plainDateAddDays,
16
+ plainDateDiffDays,
16
17
  plainDateToInstant,
17
18
  plainDateFromInstant,
18
19
  plainDateIsBefore,
@@ -312,6 +313,55 @@ describe('plainDateAddDays', () => {
312
313
  });
313
314
  });
314
315
 
316
+ describe('plainDateDiffDays', () => {
317
+ it('counts whole days forward', () => {
318
+ expect(
319
+ plainDateDiffDays(
320
+ {year: 2026, month: 1, day: 10},
321
+ {year: 2026, month: 1, day: 17},
322
+ ),
323
+ ).toBe(7);
324
+ });
325
+
326
+ it('is negative when the second date is earlier', () => {
327
+ expect(
328
+ plainDateDiffDays(
329
+ {year: 2026, month: 1, day: 10},
330
+ {year: 2026, month: 1, day: 3},
331
+ ),
332
+ ).toBe(-7);
333
+ });
334
+
335
+ it('is zero for the same day', () => {
336
+ expect(
337
+ plainDateDiffDays(
338
+ {year: 2026, month: 6, day: 15},
339
+ {year: 2026, month: 6, day: 15},
340
+ ),
341
+ ).toBe(0);
342
+ });
343
+
344
+ it('counts across month and year boundaries', () => {
345
+ expect(
346
+ plainDateDiffDays(
347
+ {year: 2025, month: 12, day: 30},
348
+ {year: 2026, month: 1, day: 2},
349
+ ),
350
+ ).toBe(3);
351
+ });
352
+
353
+ it('ignores DST — every calendar day counts once across a spring-forward', () => {
354
+ // US DST 2026 begins Mar 8. The gap Mar 7 → Mar 9 is two calendar days
355
+ // even though one of them is 23 hours long.
356
+ expect(
357
+ plainDateDiffDays(
358
+ {year: 2026, month: 3, day: 7},
359
+ {year: 2026, month: 3, day: 9},
360
+ ),
361
+ ).toBe(2);
362
+ });
363
+ });
364
+
315
365
  describe('plainDateToInstant / plainDateFromInstant', () => {
316
366
  it('converts a PlainDate to midnight in the requested timezone', () => {
317
367
  expect(
@@ -88,6 +88,18 @@ export function plainDateAddDays(pd: PlainDate, n: number): PlainDate {
88
88
  return plainDateFromDate(d);
89
89
  }
90
90
 
91
+ /**
92
+ * Whole calendar days from `a` to `b`, ignoring time of day. Positive when
93
+ * `b` is after `a`, negative when before. Uses UTC midnight so DST shifts
94
+ * never add or drop a day.
95
+ */
96
+ export function plainDateDiffDays(a: PlainDate, b: PlainDate): number {
97
+ const msPerDay = 24 * 60 * 60 * 1000;
98
+ const aUTC = Date.UTC(a.year, a.month - 1, a.day);
99
+ const bUTC = Date.UTC(b.year, b.month - 1, b.day);
100
+ return Math.round((bUTC - aUTC) / msPerDay);
101
+ }
102
+
91
103
  /**
92
104
  * The wall-clock fields an instant reads as in a given zone.
93
105
  *