@design-edito/tools 0.5.1 → 0.5.3

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 (156) hide show
  1. package/agnostic/arrays/index.d.ts +1 -1
  2. package/agnostic/arrays/index.js +1 -1
  3. package/agnostic/colors/index.d.ts +1 -1
  4. package/agnostic/colors/index.js +1 -1
  5. package/agnostic/css/index.d.ts +1 -1
  6. package/agnostic/css/index.js +1 -1
  7. package/agnostic/html/deep-select/index.js +5 -3
  8. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
  9. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
  10. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +1 -1
  11. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +1 -1
  12. package/agnostic/html/index.d.ts +3 -3
  13. package/agnostic/html/index.js +3 -3
  14. package/agnostic/index.d.ts +3 -3
  15. package/agnostic/index.js +3 -3
  16. package/agnostic/misc/index.d.ts +1 -1
  17. package/agnostic/misc/index.js +1 -1
  18. package/agnostic/misc/logs/index.d.ts +1 -1
  19. package/agnostic/misc/logs/index.js +1 -1
  20. package/agnostic/numbers/index.d.ts +3 -3
  21. package/agnostic/numbers/index.js +3 -3
  22. package/agnostic/objects/index.d.ts +3 -3
  23. package/agnostic/objects/index.js +3 -3
  24. package/agnostic/random/index.d.ts +1 -1
  25. package/agnostic/random/index.js +1 -1
  26. package/agnostic/strings/index.d.ts +3 -3
  27. package/agnostic/strings/index.js +3 -3
  28. package/agnostic/time/dates/format-date/index.d.ts +4 -2
  29. package/agnostic/time/dates/format-date/index.js +43 -94
  30. package/agnostic/time/dates/format-date/index.test.js +154 -0
  31. package/agnostic/time/dates/format-date/parts.d.ts +28 -0
  32. package/agnostic/time/dates/format-date/parts.js +136 -0
  33. package/agnostic/time/dates/format-date/types.d.ts +84 -0
  34. package/agnostic/time/dates/format-date/types.js +1 -0
  35. package/agnostic/time/duration/format-duration/index.d.ts +42 -0
  36. package/agnostic/time/duration/format-duration/index.js +94 -0
  37. package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
  38. package/agnostic/time/duration/format-duration/index.test.js +148 -0
  39. package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
  40. package/agnostic/time/duration/format-duration/parts.js +107 -0
  41. package/agnostic/time/duration/format-duration/types.d.ts +93 -0
  42. package/agnostic/time/duration/format-duration/types.js +1 -0
  43. package/agnostic/time/duration/index.d.ts +6 -1
  44. package/agnostic/time/duration/index.js +2 -2
  45. package/agnostic/time/duration/index.test.d.ts +1 -0
  46. package/agnostic/time/duration/index.test.js +168 -0
  47. package/components/BeforeAfter/index.controlled.d.ts +25 -25
  48. package/components/BeforeAfter/index.controlled.js +47 -53
  49. package/components/BeforeAfter/index.d.ts +20 -23
  50. package/components/BeforeAfter/index.js +48 -40
  51. package/components/Button/index.js +1 -1
  52. package/components/Clippable/index.d.ts +17 -26
  53. package/components/Clippable/index.js +21 -23
  54. package/components/Disclaimer/index.d.ts +29 -22
  55. package/components/Disclaimer/index.js +29 -24
  56. package/components/Drawer/index.d.ts +45 -25
  57. package/components/Drawer/index.js +61 -52
  58. package/components/EventListener/index.d.ts +4 -5
  59. package/components/Gallery/index.d.ts +14 -16
  60. package/components/Gallery/index.js +25 -30
  61. package/components/Iframe/index.js +2 -2
  62. package/components/Image/index.d.ts +5 -12
  63. package/components/Image/index.js +11 -32
  64. package/components/Input/index.d.ts +23 -19
  65. package/components/Input/index.js +30 -20
  66. package/components/JsonEditor/index.d.ts +177 -9
  67. package/components/JsonEditor/index.js +133 -75
  68. package/components/JsonEditor/types.d.ts +11 -0
  69. package/components/JsonEditor/types.js +1 -0
  70. package/components/JsonEditor/utils.d.ts +8 -0
  71. package/components/JsonEditor/utils.js +23 -0
  72. package/components/ListLoader/index.controlled.d.ts +78 -0
  73. package/components/ListLoader/index.controlled.js +99 -0
  74. package/components/ListLoader/index.d.ts +69 -0
  75. package/components/ListLoader/index.js +146 -0
  76. package/components/ListLoader/utils.d.ts +8 -0
  77. package/components/ListLoader/utils.js +10 -0
  78. package/components/Overlayer/index.js +4 -4
  79. package/components/Paginator/index.d.ts +7 -12
  80. package/components/Paginator/index.js +7 -11
  81. package/components/ResizeObserver/index.d.ts +14 -3
  82. package/components/ResizeObserver/index.js +49 -24
  83. package/components/ResizeObserver/styles.module.css +0 -0
  84. package/components/Scrllgngn/index.d.ts +17 -13
  85. package/components/Scrllgngn/index.js +25 -84
  86. package/components/Scrllgngn/utils.d.ts +52 -0
  87. package/components/Scrllgngn/utils.js +102 -0
  88. package/components/ScrollListener/index.d.ts +43 -28
  89. package/components/ScrollListener/index.js +59 -79
  90. package/components/ScrollListener/utils.d.ts +37 -16
  91. package/components/ScrollListener/utils.js +141 -28
  92. package/components/Select/index.d.ts +24 -20
  93. package/components/Select/index.js +30 -20
  94. package/components/Sequencer/index.controlled.d.ts +16 -26
  95. package/components/Sequencer/index.controlled.js +15 -12
  96. package/components/Sequencer/index.d.ts +25 -30
  97. package/components/Sequencer/index.js +20 -34
  98. package/components/Subtitles/index.d.ts +29 -5
  99. package/components/Subtitles/index.js +90 -12
  100. package/components/Textarea/index.d.ts +26 -20
  101. package/components/Textarea/index.js +40 -21
  102. package/components/Theatre/index.d.ts +35 -39
  103. package/components/Theatre/index.js +46 -58
  104. package/components/UIModule/index.d.ts +25 -29
  105. package/components/UIModule/index.js +81 -110
  106. package/components/Video/index.controlled.d.ts +51 -51
  107. package/components/Video/index.controlled.js +75 -72
  108. package/components/Video/index.d.ts +12 -14
  109. package/components/Video/index.js +41 -86
  110. package/components/Video/utils.d.ts +35 -3
  111. package/components/Video/utils.js +37 -14
  112. package/components/index.d.ts +5 -4
  113. package/components/index.js +5 -4
  114. package/components/public-classnames.d.ts +1 -0
  115. package/components/public-classnames.js +1 -0
  116. package/components/utils/index.d.ts +22 -0
  117. package/components/utils/index.js +31 -0
  118. package/node/@google-cloud/storage/directory/index.d.ts +2 -2
  119. package/node/@google-cloud/storage/directory/index.js +2 -2
  120. package/node/@google-cloud/storage/file/index.d.ts +2 -2
  121. package/node/@google-cloud/storage/file/index.js +2 -2
  122. package/node/@google-cloud/storage/index.d.ts +1 -1
  123. package/node/@google-cloud/storage/index.js +1 -1
  124. package/node/cloud-storage/operations/index.d.ts +2 -2
  125. package/node/cloud-storage/operations/index.js +2 -2
  126. package/node/ftps/directory/index.d.ts +1 -1
  127. package/node/ftps/directory/index.js +1 -1
  128. package/node/ftps/file/index.d.ts +2 -2
  129. package/node/ftps/file/index.js +2 -2
  130. package/node/images/index.d.ts +1 -1
  131. package/node/images/index.js +1 -1
  132. package/node/images/transform/operations/index.d.ts +3 -3
  133. package/node/images/transform/operations/index.js +3 -3
  134. package/node/index.d.ts +2 -2
  135. package/node/index.js +2 -2
  136. package/node/process/index.d.ts +1 -1
  137. package/node/process/index.js +1 -1
  138. package/node/sftp/directory/index.d.ts +1 -1
  139. package/node/sftp/directory/index.js +1 -1
  140. package/node/sftp/file/index.d.ts +1 -1
  141. package/node/sftp/file/index.js +1 -1
  142. package/package.json +27 -5
  143. package/TODO.md +0 -269
  144. package/components/BeforeAfter/utils.d.ts +0 -4
  145. package/components/BeforeAfter/utils.js +0 -9
  146. package/components/Input/index.controlled.d.ts +0 -42
  147. package/components/Input/index.controlled.js +0 -54
  148. package/components/Select/index.controlled.d.ts +0 -43
  149. package/components/Select/index.controlled.js +0 -54
  150. package/components/Subtitles/types.d.ts +0 -24
  151. package/components/Subtitles/utils.d.ts +0 -32
  152. package/components/Subtitles/utils.js +0 -107
  153. package/components/Textarea/index.controlled.d.ts +0 -50
  154. package/components/Textarea/index.controlled.js +0 -75
  155. /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
  156. /package/components/{ResizeObserver/style.module.css → ListLoader/styles.module.css} +0 -0
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatDate } from './index.js';
3
+ import { getDateParts } from './parts.js';
4
+ // Thursday, January 1st 2026, 15:05:09.042 — read in the runtime's own timezone,
5
+ // which is also the one the fixture is built in, so the tests hold anywhere.
6
+ const thursdayAfternoon = new Date(2026, 0, 1, 15, 5, 9, 42);
7
+ describe('formatDate', () => {
8
+ describe('template', () => {
9
+ it('fills a template with the parts of the date', () => {
10
+ expect(formatDate(thursdayAfternoon, '{{YYYY}}-{{MM}}-{{DD}} {{hh}}:{{mm}} {{A}}'))
11
+ .toBe('2026-01-01 03:05 PM');
12
+ });
13
+ it('keeps whatever sits outside the placeholders', () => {
14
+ expect(formatDate(thursdayAfternoon, 'le {{D}} {{MMMM}} {{YYYY}}', 'fr'))
15
+ .toBe('le 1 janvier 2026');
16
+ });
17
+ it('does not substitute a bare letter outside a placeholder', () => {
18
+ expect(formatDate(thursdayAfternoon, 'D M Y {{D}}')).toBe('D M Y 1');
19
+ });
20
+ it('substitutes every occurrence of a repeated token', () => {
21
+ expect(formatDate(thursdayAfternoon, '{{D}}/{{D}}')).toBe('1/1');
22
+ });
23
+ it('leaves an unknown token untouched, braces included', () => {
24
+ expect(formatDate(thursdayAfternoon, '{{ZZ}} {{YYYY}}')).toBe('{{ZZ}} 2026');
25
+ });
26
+ });
27
+ describe('padding', () => {
28
+ it('pads the doubled tokens to two digits', () => {
29
+ expect(formatDate(thursdayAfternoon, '{{DD}} {{MM}} {{HH}} {{hh}} {{mm}} {{ss}}'))
30
+ .toBe('01 01 15 03 05 09');
31
+ });
32
+ it('leaves the bare twins unpadded', () => {
33
+ expect(formatDate(thursdayAfternoon, '{{D}} {{M}} {{H}} {{h}} {{m}} {{s}}'))
34
+ .toBe('1 1 15 3 5 9');
35
+ });
36
+ it('pads milliseconds to three digits', () => {
37
+ expect(formatDate(thursdayAfternoon, '{{ms}}')).toBe('042');
38
+ });
39
+ it('truncates YY to the last two digits of the year', () => {
40
+ expect(formatDate(thursdayAfternoon, '{{YY}}')).toBe('26');
41
+ });
42
+ });
43
+ describe('12-hour clock', () => {
44
+ it('writes midnight as 12 AM', () => {
45
+ expect(formatDate(new Date(2026, 0, 1, 0, 30), '{{h}}:{{mm}} {{A}}')).toBe('12:30 AM');
46
+ });
47
+ it('writes noon as 12 PM', () => {
48
+ expect(formatDate(new Date(2026, 0, 1, 12, 30), '{{h}}:{{mm}} {{A}}')).toBe('12:30 PM');
49
+ });
50
+ it('starts the afternoon back at 1', () => {
51
+ expect(formatDate(new Date(2026, 0, 1, 13), '{{h}} {{a}}')).toBe('1 pm');
52
+ });
53
+ });
54
+ describe('locales', () => {
55
+ it('names the weekday and the month in English by default', () => {
56
+ expect(formatDate(thursdayAfternoon, '{{dd}} {{d}} {{MMMM}} {{MMM}}'))
57
+ .toBe('Thursday Thu January Jan');
58
+ });
59
+ it('names them in the requested locale', () => {
60
+ expect(formatDate(thursdayAfternoon, '{{dd}} {{MMMM}}', 'fr')).toBe('jeudi janvier');
61
+ });
62
+ });
63
+ describe('ordinal suffix', () => {
64
+ it('follows the English rule', () => {
65
+ const suffix = (day) => formatDate(new Date(2026, 0, day), '{{D}}{{th}}');
66
+ expect(suffix(1)).toBe('1st');
67
+ expect(suffix(2)).toBe('2nd');
68
+ expect(suffix(3)).toBe('3rd');
69
+ expect(suffix(4)).toBe('4th');
70
+ expect(suffix(21)).toBe('21st');
71
+ expect(suffix(22)).toBe('22nd');
72
+ expect(suffix(23)).toBe('23rd');
73
+ });
74
+ it('keeps the teens on th, where the last digit would say otherwise', () => {
75
+ const suffix = (day) => formatDate(new Date(2026, 0, day), '{{D}}{{th}}');
76
+ expect(suffix(11)).toBe('11th');
77
+ expect(suffix(12)).toBe('12th');
78
+ expect(suffix(13)).toBe('13th');
79
+ });
80
+ it('marks only the 1st in French', () => {
81
+ expect(formatDate(new Date(2026, 0, 1), '{{D}}{{th}}', 'fr')).toBe('1er');
82
+ expect(formatDate(new Date(2026, 0, 2), '{{D}}{{th}}', 'fr')).toBe('2');
83
+ });
84
+ });
85
+ });
86
+ describe('getDateParts', () => {
87
+ describe('quantities', () => {
88
+ it('hands over plain numbers, unpadded', () => {
89
+ expect(getDateParts(thursdayAfternoon)).toMatchObject({
90
+ D: 1,
91
+ M: 1,
92
+ YYYY: 2026,
93
+ H: 15,
94
+ h: 3,
95
+ m: 5,
96
+ s: 9,
97
+ ms: 42
98
+ });
99
+ });
100
+ it('numbers the months from 1, where Date numbers them from 0', () => {
101
+ expect(getDateParts(new Date(2026, 11, 25)).M).toBe(12);
102
+ });
103
+ it('reaches every part under its readable name too', () => {
104
+ const parts = getDateParts(thursdayAfternoon);
105
+ expect(parts).toMatchObject({
106
+ dayOfMonth: parts.D,
107
+ shortWeekdayName: parts.d,
108
+ fullWeekdayName: parts.dd,
109
+ monthNumber: parts.M,
110
+ shortMonthName: parts.MMM,
111
+ fullMonthName: parts.MMMM,
112
+ year: parts.YYYY,
113
+ hours24: parts.H,
114
+ hours12: parts.h,
115
+ minutes: parts.m,
116
+ seconds: parts.s,
117
+ milliseconds: parts.ms,
118
+ upperCaseMeridiem: parts.A,
119
+ lowerCaseMeridiem: parts.a,
120
+ dayOrdinalSuffix: parts.th
121
+ });
122
+ });
123
+ });
124
+ describe('12-hour clock', () => {
125
+ it('reads both midnight and noon as 12', () => {
126
+ expect(getDateParts(new Date(2026, 0, 1, 0)).h).toBe(12);
127
+ expect(getDateParts(new Date(2026, 0, 1, 12)).h).toBe(12);
128
+ });
129
+ it('splits the meridiem at noon', () => {
130
+ expect(getDateParts(new Date(2026, 0, 1, 11, 59))).toMatchObject({ A: 'AM', a: 'am' });
131
+ expect(getDateParts(new Date(2026, 0, 1, 12, 0))).toMatchObject({ A: 'PM', a: 'pm' });
132
+ });
133
+ });
134
+ describe('locales', () => {
135
+ it('localises the weekday and month names', () => {
136
+ expect(getDateParts(thursdayAfternoon, 'fr')).toMatchObject({
137
+ d: 'jeu.',
138
+ dd: 'jeudi',
139
+ MMM: 'janv.',
140
+ MMMM: 'janvier'
141
+ });
142
+ });
143
+ it('falls back to English on a locale the runtime does not support', () => {
144
+ expect(getDateParts(thursdayAfternoon, 'xx')).toMatchObject({
145
+ dd: 'Thursday',
146
+ MMMM: 'January',
147
+ th: 'st'
148
+ });
149
+ });
150
+ it('leaves the ordinal suffix empty for a locale that has no rule here', () => {
151
+ expect(getDateParts(thursdayAfternoon, 'es').th).toBe('');
152
+ });
153
+ });
154
+ });
@@ -0,0 +1,28 @@
1
+ import type { DateParts } from './types.js';
2
+ /**
3
+ * Breaks a `Date` down into every fragment `formatDate` knows how to render,
4
+ * localised and ready to concatenate.
5
+ *
6
+ * Reach for this over `formatDate` when the fragments go somewhere a template
7
+ * string can't — separate DOM nodes, a `<time>` element's parts, an object
8
+ * handed to a view layer.
9
+ *
10
+ * @param date - The `Date` to break down.
11
+ * @param locale - Optional locale code, falling back to `'en'` when the runtime
12
+ * doesn't support it.
13
+ * @returns The parts, each one reachable under its unit token and under its
14
+ * readable name. Quantities come out as numbers — `formatDate` is what pads
15
+ * them.
16
+ * @see {@link DateParts}
17
+ *
18
+ * @remarks
19
+ * Every fragment is read off the `Date` in the runtime's local timezone, since a
20
+ * `Date` carries no timezone of its own.
21
+ *
22
+ * @example
23
+ * const parts = getDateParts(new Date(2026, 0, 1, 15, 5), 'fr')
24
+ * parts.MMMM // => 'janvier'
25
+ * parts.fullMonthName // => 'janvier', the same value
26
+ * parts.D // => 1, the number — '01' is formatDate's business
27
+ */
28
+ export declare function getDateParts(date: Date, locale?: string): DateParts;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The localised weekday names, indexed the way `Date.prototype.getDay` numbers
3
+ * them. January 1st 2023 was a Sunday, so the week is walked from there.
4
+ */
5
+ function weekdayNames(locale, weekday) {
6
+ const formatter = new Intl.DateTimeFormat(locale, { weekday });
7
+ return [...Array(7)].map((_, i) => formatter.format(new Date(2023, 0, i + 1)));
8
+ }
9
+ /**
10
+ * The localised month names, indexed the way `Date.prototype.getMonth` numbers
11
+ * them — January at 0.
12
+ */
13
+ function monthNames(locale, month) {
14
+ const formatter = new Intl.DateTimeFormat(locale, { month });
15
+ return [...Array(12)].map((_, i) => formatter.format(new Date(2023, i, 1)));
16
+ }
17
+ /** The ordinal suffix a locale appends to a day number, empty when it has none. */
18
+ function dayOrdinalSuffix(day, locale) {
19
+ if (locale.startsWith('fr'))
20
+ return day === 1 ? 'er' : '';
21
+ if (!locale.startsWith('en'))
22
+ return '';
23
+ const mod10 = day % 10;
24
+ const mod100 = day % 100;
25
+ if (mod10 === 1 && mod100 !== 11)
26
+ return 'st';
27
+ if (mod10 === 2 && mod100 !== 12)
28
+ return 'nd';
29
+ if (mod10 === 3 && mod100 !== 13)
30
+ return 'rd';
31
+ return 'th';
32
+ }
33
+ /**
34
+ * Breaks a `Date` down into every fragment `formatDate` knows how to render,
35
+ * localised and ready to concatenate.
36
+ *
37
+ * Reach for this over `formatDate` when the fragments go somewhere a template
38
+ * string can't — separate DOM nodes, a `<time>` element's parts, an object
39
+ * handed to a view layer.
40
+ *
41
+ * @param date - The `Date` to break down.
42
+ * @param locale - Optional locale code, falling back to `'en'` when the runtime
43
+ * doesn't support it.
44
+ * @returns The parts, each one reachable under its unit token and under its
45
+ * readable name. Quantities come out as numbers — `formatDate` is what pads
46
+ * them.
47
+ * @see {@link DateParts}
48
+ *
49
+ * @remarks
50
+ * Every fragment is read off the `Date` in the runtime's local timezone, since a
51
+ * `Date` carries no timezone of its own.
52
+ *
53
+ * @example
54
+ * const parts = getDateParts(new Date(2026, 0, 1, 15, 5), 'fr')
55
+ * parts.MMMM // => 'janvier'
56
+ * parts.fullMonthName // => 'janvier', the same value
57
+ * parts.D // => 1, the number — '01' is formatDate's business
58
+ */
59
+ export function getDateParts(date, locale = 'en') {
60
+ const safeLocale = Intl.DateTimeFormat.supportedLocalesOf(locale).length === 0
61
+ ? 'en'
62
+ : locale;
63
+ const day = date.getDate();
64
+ const dayOfWeek = date.getDay();
65
+ const month = date.getMonth();
66
+ const hours = date.getHours();
67
+ const isPM = hours >= 12;
68
+ const tokens = {
69
+ 'D': day,
70
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
71
+ 'd': weekdayNames(safeLocale, 'short')[dayOfWeek],
72
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
73
+ 'dd': weekdayNames(safeLocale, 'long')[dayOfWeek],
74
+ 'M': month + 1,
75
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
76
+ 'MMM': monthNames(safeLocale, 'short')[month],
77
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
78
+ 'MMMM': monthNames(safeLocale, 'long')[month],
79
+ 'YYYY': date.getFullYear(),
80
+ 'H': hours,
81
+ 'h': hours % 12 === 0 ? 12 : hours % 12,
82
+ 'm': date.getMinutes(),
83
+ 's': date.getSeconds(),
84
+ 'ms': date.getMilliseconds(),
85
+ 'A': isPM ? 'PM' : 'AM',
86
+ 'a': isPM ? 'pm' : 'am',
87
+ 'th': dayOrdinalSuffix(day, safeLocale)
88
+ };
89
+ return {
90
+ ...tokens,
91
+ dayOfMonth: tokens.D,
92
+ shortWeekdayName: tokens.d,
93
+ fullWeekdayName: tokens.dd,
94
+ monthNumber: tokens.M,
95
+ shortMonthName: tokens.MMM,
96
+ fullMonthName: tokens.MMMM,
97
+ year: tokens.YYYY,
98
+ hours24: tokens.H,
99
+ hours12: tokens.h,
100
+ minutes: tokens.m,
101
+ seconds: tokens.s,
102
+ milliseconds: tokens.ms,
103
+ upperCaseMeridiem: tokens.A,
104
+ lowerCaseMeridiem: tokens.a,
105
+ dayOrdinalSuffix: tokens.th
106
+ };
107
+ }
108
+ // [WIP] timezone tokens were removed since JavaScript's Date object doesn't hold
109
+ // timezone data — those templates would only reflect the current client's
110
+ // timezone, no matter what the input Date obj was
111
+ // 'ZZ': () => {
112
+ // const timezoneOffset = date.getTimezoneOffset()
113
+ // const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60)
114
+ // const offsetMinutes = Math.abs(timezoneOffset) % 60
115
+ // const sign = timezoneOffset > 0 ? '-' : '+'
116
+ // return `${sign}${String(offsetHours).padStart(2, '0')}:${String(offsetMinutes).padStart(2, '0')}`
117
+ // },
118
+ // 'Z': () => {
119
+ // const timezoneOffset = date.getTimezoneOffset()
120
+ // if (timezoneOffset === 0) return 'Z'
121
+ // const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60)
122
+ // const offsetMinutes = Math.abs(timezoneOffset) % 60
123
+ // const sign = timezoneOffset > 0 ? '-' : '+'
124
+ // return `${sign}${String(offsetHours).padStart(2, '0')}:${String(offsetMinutes).padStart(2, '0')}`
125
+ // },
126
+ // 'z': () => {
127
+ // return Intl.DateTimeFormat(locale, { timeZoneName: 'short' })
128
+ // .formatToParts(date)
129
+ // .find(part => part.type === 'timeZoneName')?.value || ''
130
+ // },
131
+ // 'zz': () => {
132
+ // return Intl.DateTimeFormat(locale, { timeZoneName: 'long' })
133
+ // .formatToParts(date)
134
+ // .find(part => part.type === 'timeZoneName')?.value || ''
135
+ // },
136
+ // 'tz': () => Intl.DateTimeFormat().resolvedOptions().timeZone || ''
@@ -0,0 +1,84 @@
1
+ /** Every token `formatDate` substitutes in a template. */
2
+ export type DateToken = 'D' | 'DD' | 'd' | 'dd' | 'M' | 'MM' | 'MMM' | 'MMMM' | 'YY' | 'YYYY' | 'H' | 'HH' | 'h' | 'hh' | 'm' | 'mm' | 's' | 'ss' | 'ms' | 'A' | 'a' | 'th';
3
+ /**
4
+ * A date's parts, keyed by unit token.
5
+ *
6
+ * Quantities are **numbers** — a day of month is `1`, not `'01'`. Padding and
7
+ * truncation are rendering concerns and belong to `formatDate`, which is why the
8
+ * `DD`, `MM`, `YY`, `HH`, `hh`, `mm` and `ss` tokens have no entry here: they
9
+ * render the same quantity as their unpadded twin. The parts that are localised
10
+ * text rather than a quantity stay strings.
11
+ */
12
+ export type DateTokenParts = {
13
+ /** Day of month, 1–31. */
14
+ D: number;
15
+ /** Short weekday name, localised. */
16
+ d: string;
17
+ /** Full weekday name, localised. */
18
+ dd: string;
19
+ /** Month number, 1–12 — January is `1`, not `0`. */
20
+ M: number;
21
+ /** Short month name, localised. */
22
+ MMM: string;
23
+ /** Full month name, localised. */
24
+ MMMM: string;
25
+ /** Full year. */
26
+ YYYY: number;
27
+ /** Hours on a 24-hour clock, 0–23. */
28
+ H: number;
29
+ /** Hours on a 12-hour clock, 1–12. Midnight and noon are both `12`. */
30
+ h: number;
31
+ /** Minutes, 0–59. */
32
+ m: number;
33
+ /** Seconds, 0–59. */
34
+ s: number;
35
+ /** Milliseconds, 0–999. */
36
+ ms: number;
37
+ /** Upper-case meridiem, `'AM'` or `'PM'`. */
38
+ A: string;
39
+ /** Lower-case meridiem, `'am'` or `'pm'`. */
40
+ a: string;
41
+ /**
42
+ * Day ordinal suffix — `'st'`, `'nd'`, `'rd'` or `'th'` in English, `'er'` for
43
+ * the French 1st. Empty for every other locale, and for French days 2 onwards.
44
+ */
45
+ th: string;
46
+ };
47
+ /**
48
+ * The same parts as {@link DateTokenParts}, under names that read on their own —
49
+ * one per token, same value.
50
+ */
51
+ export type NamedDateParts = {
52
+ /** {@link DateTokenParts.D} */
53
+ dayOfMonth: number;
54
+ /** {@link DateTokenParts.d} */
55
+ shortWeekdayName: string;
56
+ /** {@link DateTokenParts.dd} */
57
+ fullWeekdayName: string;
58
+ /** {@link DateTokenParts.M} */
59
+ monthNumber: number;
60
+ /** {@link DateTokenParts.MMM} */
61
+ shortMonthName: string;
62
+ /** {@link DateTokenParts.MMMM} */
63
+ fullMonthName: string;
64
+ /** {@link DateTokenParts.YYYY} */
65
+ year: number;
66
+ /** {@link DateTokenParts.H} */
67
+ hours24: number;
68
+ /** {@link DateTokenParts.h} */
69
+ hours12: number;
70
+ /** {@link DateTokenParts.m} */
71
+ minutes: number;
72
+ /** {@link DateTokenParts.s} */
73
+ seconds: number;
74
+ /** {@link DateTokenParts.ms} */
75
+ milliseconds: number;
76
+ /** {@link DateTokenParts.A} */
77
+ upperCaseMeridiem: string;
78
+ /** {@link DateTokenParts.a} */
79
+ lowerCaseMeridiem: string;
80
+ /** {@link DateTokenParts.th} */
81
+ dayOrdinalSuffix: string;
82
+ };
83
+ /** Every part of a date, reachable both by unit token and by name. */
84
+ export type DateParts = DateTokenParts & NamedDateParts;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import type { Duration } from '../index.js';
2
+ import type { FormatDurationOptions } from './types.js';
3
+ /**
4
+ * Formats a duration into a string according to a custom template.
5
+ *
6
+ * The breakdown follows the template: the units it mentions are the units the
7
+ * duration is split across, each taking its whole part and handing the rest
8
+ * down. `'{{h}}:{{mm}}:{{ss}}'` therefore reads a duration as hours, minutes and
9
+ * seconds, while `'{{s}}s'` puts the whole duration on seconds.
10
+ *
11
+ * Supported tokens (placeholders must be wrapped in `{{...}}`). The bare form is
12
+ * the plain number, the doubled form the same number padded to two digits:
13
+ *
14
+ * - `Y` / `YY` : Years
15
+ * - `M` / `MM` : Months
16
+ * - `w` / `ww` : Weeks
17
+ * - `d` / `dd` : Days
18
+ * - `h` / `hh` : Hours
19
+ * - `m` / `mm` : Minutes
20
+ * - `s` / `ss` : Seconds
21
+ * - `ms` : Milliseconds, padded to three digits
22
+ *
23
+ * @param duration - The duration to format, as a {@link Duration} or a number of
24
+ * milliseconds.
25
+ * @param format - The template string containing tokens.
26
+ * @param options - How to handle the remainder and the month/year approximation.
27
+ * @returns Formatted duration string. An unknown token is left untouched, braces
28
+ * included.
29
+ * @see {@link getDurationParts} to reach the same parts as plain values.
30
+ *
31
+ * @remarks
32
+ * A negative duration yields negative parts, so every token renders a signed
33
+ * number — `'{{m}}:{{ss}}'` gives `-1:-30`, not `-1:30`. This is deliberate: the
34
+ * output stays faithful to the parts, and keeping a duration positive is the
35
+ * caller's business. Format the magnitude and write the sign yourself when a
36
+ * single leading minus is what you need.
37
+ *
38
+ * @example
39
+ * formatDuration(seconds(3725), '{{h}}:{{mm}}:{{ss}}')
40
+ * // => "1:02:05"
41
+ */
42
+ export declare function formatDuration(duration: Duration | number, format: string, options?: FormatDurationOptions): string;
@@ -0,0 +1,94 @@
1
+ import { getDurationParts } from './parts.js';
2
+ // Alternation is ordered, and the first branch that fits wins — so the longest
3
+ // tokens come first. `m` ahead of `ms` or `mm` would swallow them.
4
+ const tokenRegexp = /\{\{(YY|MM|ww|dd|hh|mm|ss|ms|Y|M|w|d|h|m|s)\}\}/gv;
5
+ /** Which tokens read which unit, longest unit first. */
6
+ const unitTokens = [
7
+ ['Y', ['Y', 'YY']],
8
+ ['M', ['M', 'MM']],
9
+ ['w', ['w', 'ww']],
10
+ ['d', ['d', 'dd']],
11
+ ['h', ['h', 'hh']],
12
+ ['m', ['m', 'mm']],
13
+ ['s', ['s', 'ss']],
14
+ ['ms', ['ms']]
15
+ ];
16
+ function pad(value, length) {
17
+ return `${value}`.padStart(length, '0');
18
+ }
19
+ /**
20
+ * How each token renders the parts it reads. This is where padding lives —
21
+ * {@link getDurationParts} hands over plain quantities, and a doubled token is
22
+ * the same quantity written on two digits.
23
+ */
24
+ const renderers = {
25
+ 'Y': parts => `${parts.Y}`,
26
+ 'YY': parts => pad(parts.Y, 2),
27
+ 'M': parts => `${parts.M}`,
28
+ 'MM': parts => pad(parts.M, 2),
29
+ 'w': parts => `${parts.w}`,
30
+ 'ww': parts => pad(parts.w, 2),
31
+ 'd': parts => `${parts.d}`,
32
+ 'dd': parts => pad(parts.d, 2),
33
+ 'h': parts => `${parts.h}`,
34
+ 'hh': parts => pad(parts.h, 2),
35
+ 'm': parts => `${parts.m}`,
36
+ 'mm': parts => pad(parts.m, 2),
37
+ 's': parts => `${parts.s}`,
38
+ 'ss': parts => pad(parts.s, 2),
39
+ 'ms': parts => pad(parts.ms, 3)
40
+ };
41
+ /** The units a template asks for, read off the tokens it actually uses. */
42
+ function unitsInFormat(format) {
43
+ return unitTokens
44
+ .filter(([, tokens]) => tokens.some(token => format.includes(`{{${token}}}`)))
45
+ .map(([unit]) => unit);
46
+ }
47
+ /**
48
+ * Formats a duration into a string according to a custom template.
49
+ *
50
+ * The breakdown follows the template: the units it mentions are the units the
51
+ * duration is split across, each taking its whole part and handing the rest
52
+ * down. `'{{h}}:{{mm}}:{{ss}}'` therefore reads a duration as hours, minutes and
53
+ * seconds, while `'{{s}}s'` puts the whole duration on seconds.
54
+ *
55
+ * Supported tokens (placeholders must be wrapped in `{{...}}`). The bare form is
56
+ * the plain number, the doubled form the same number padded to two digits:
57
+ *
58
+ * - `Y` / `YY` : Years
59
+ * - `M` / `MM` : Months
60
+ * - `w` / `ww` : Weeks
61
+ * - `d` / `dd` : Days
62
+ * - `h` / `hh` : Hours
63
+ * - `m` / `mm` : Minutes
64
+ * - `s` / `ss` : Seconds
65
+ * - `ms` : Milliseconds, padded to three digits
66
+ *
67
+ * @param duration - The duration to format, as a {@link Duration} or a number of
68
+ * milliseconds.
69
+ * @param format - The template string containing tokens.
70
+ * @param options - How to handle the remainder and the month/year approximation.
71
+ * @returns Formatted duration string. An unknown token is left untouched, braces
72
+ * included.
73
+ * @see {@link getDurationParts} to reach the same parts as plain values.
74
+ *
75
+ * @remarks
76
+ * A negative duration yields negative parts, so every token renders a signed
77
+ * number — `'{{m}}:{{ss}}'` gives `-1:-30`, not `-1:30`. This is deliberate: the
78
+ * output stays faithful to the parts, and keeping a duration positive is the
79
+ * caller's business. Format the magnitude and write the sign yourself when a
80
+ * single leading minus is what you need.
81
+ *
82
+ * @example
83
+ * formatDuration(seconds(3725), '{{h}}:{{mm}}:{{ss}}')
84
+ * // => "1:02:05"
85
+ */
86
+ export function formatDuration(duration, format, options = {}) {
87
+ const { floorSmallestUnit = true, useApproximateMonthAndYear = true } = options;
88
+ const parts = getDurationParts(duration, {
89
+ units: unitsInFormat(format),
90
+ floorSmallestUnit,
91
+ useApproximateMonthAndYear
92
+ });
93
+ return format.replace(tokenRegexp, (_match, token) => renderers[token](parts));
94
+ }