@lyeve-labs/ui-kit 0.11.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AccordionItem.svelte +1 -1
  3. package/dist/components/Autocomplete.svelte +191 -125
  4. package/dist/components/Autocomplete.svelte.d.ts +29 -8
  5. package/dist/components/Button.svelte +26 -4
  6. package/dist/components/Card.svelte +61 -3
  7. package/dist/components/Card.svelte.d.ts +24 -2
  8. package/dist/components/Checkbox.svelte +174 -59
  9. package/dist/components/Checkbox.svelte.d.ts +20 -3
  10. package/dist/components/CheckboxGroup.svelte +162 -0
  11. package/dist/components/CheckboxGroup.svelte.d.ts +51 -0
  12. package/dist/components/Collapsible.svelte +142 -0
  13. package/dist/components/Collapsible.svelte.d.ts +32 -0
  14. package/dist/components/CopyButton.svelte +126 -0
  15. package/dist/components/CopyButton.svelte.d.ts +14 -0
  16. package/dist/components/DatePicker.svelte +48 -6
  17. package/dist/components/DateTimePicker.svelte +337 -0
  18. package/dist/components/DateTimePicker.svelte.d.ts +26 -0
  19. package/dist/components/DescriptionList.svelte +78 -0
  20. package/dist/components/DescriptionList.svelte.d.ts +34 -0
  21. package/dist/components/Drawer.svelte +15 -4
  22. package/dist/components/Field.svelte +104 -0
  23. package/dist/components/Field.svelte.d.ts +46 -0
  24. package/dist/components/FileInput.svelte +5 -2
  25. package/dist/components/FormMessage.svelte +85 -0
  26. package/dist/components/FormMessage.svelte.d.ts +11 -0
  27. package/dist/components/Input.svelte +1 -1
  28. package/dist/components/Label.svelte +7 -1
  29. package/dist/components/Label.svelte.d.ts +6 -0
  30. package/dist/components/Modal.svelte +25 -8
  31. package/dist/components/MultiSelect.svelte +199 -109
  32. package/dist/components/MultiSelect.svelte.d.ts +22 -9
  33. package/dist/components/NumberInput.svelte +8 -4
  34. package/dist/components/PageHeader.svelte +37 -4
  35. package/dist/components/PageHeader.svelte.d.ts +15 -0
  36. package/dist/components/PageShell.svelte +85 -0
  37. package/dist/components/PageShell.svelte.d.ts +38 -0
  38. package/dist/components/Pagination.svelte +58 -17
  39. package/dist/components/Panel.svelte +101 -0
  40. package/dist/components/Panel.svelte.d.ts +39 -0
  41. package/dist/components/PasswordInput.svelte +139 -0
  42. package/dist/components/PasswordInput.svelte.d.ts +29 -0
  43. package/dist/components/Radio.svelte +152 -32
  44. package/dist/components/Radio.svelte.d.ts +16 -1
  45. package/dist/components/RadioGroup.svelte +118 -71
  46. package/dist/components/RadioGroup.svelte.d.ts +39 -9
  47. package/dist/components/SectionHeading.svelte +39 -0
  48. package/dist/components/SectionHeading.svelte.d.ts +21 -0
  49. package/dist/components/SegmentedControl.svelte +194 -0
  50. package/dist/components/SegmentedControl.svelte.d.ts +55 -0
  51. package/dist/components/Select.svelte +471 -46
  52. package/dist/components/Select.svelte.d.ts +95 -6
  53. package/dist/components/SidebarNav.svelte +259 -0
  54. package/dist/components/SidebarNav.svelte.d.ts +17 -0
  55. package/dist/components/Stat.svelte +53 -2
  56. package/dist/components/Stat.svelte.d.ts +31 -0
  57. package/dist/components/Textarea.svelte +1 -1
  58. package/dist/components/TimePicker.svelte +480 -0
  59. package/dist/components/TimePicker.svelte.d.ts +23 -0
  60. package/dist/components/Toaster.svelte +9 -2
  61. package/dist/components/Toggle.svelte +5 -1
  62. package/dist/components/Toggle.svelte.d.ts +2 -0
  63. package/dist/components/Toolbar.svelte +39 -0
  64. package/dist/components/Toolbar.svelte.d.ts +26 -0
  65. package/dist/components/Tooltip.svelte +48 -12
  66. package/dist/components/TreeView.svelte +339 -0
  67. package/dist/components/TreeView.svelte.d.ts +37 -0
  68. package/dist/components/dialog/Dialog.svelte +15 -58
  69. package/dist/components/dialog/dialog-manager.svelte.d.ts +2 -2
  70. package/dist/components/dialog/dialog-manager.svelte.js +21 -21
  71. package/dist/index.d.ts +25 -1
  72. package/dist/index.js +20 -1
  73. package/dist/internal/calendar.d.ts +119 -0
  74. package/dist/internal/calendar.js +225 -0
  75. package/dist/internal/choice.d.ts +136 -0
  76. package/dist/internal/choice.js +179 -0
  77. package/dist/internal/field.d.ts +31 -0
  78. package/dist/internal/field.js +42 -1
  79. package/dist/internal/filter.d.ts +80 -0
  80. package/dist/internal/filter.js +80 -0
  81. package/dist/internal/layout.d.ts +119 -0
  82. package/dist/internal/layout.js +132 -0
  83. package/dist/internal/listbox.svelte.d.ts +77 -0
  84. package/dist/internal/listbox.svelte.js +438 -0
  85. package/dist/internal/nav-expansion.svelte.d.ts +36 -0
  86. package/dist/internal/nav-expansion.svelte.js +144 -0
  87. package/dist/internal/nav-tree.d.ts +68 -0
  88. package/dist/internal/nav-tree.js +102 -0
  89. package/dist/internal/overlay.d.ts +25 -0
  90. package/dist/internal/overlay.js +92 -0
  91. package/dist/internal/panel.d.ts +100 -0
  92. package/dist/internal/panel.js +109 -0
  93. package/dist/internal/rollup.d.ts +52 -0
  94. package/dist/internal/rollup.js +67 -0
  95. package/dist/internal/time.d.ts +103 -0
  96. package/dist/internal/time.js +166 -0
  97. package/dist/internal/tree.d.ts +86 -0
  98. package/dist/internal/tree.js +111 -0
  99. package/dist/styles/theme.css +66 -25
  100. package/package.json +4 -2
  101. package/src/lib/styles/theme.css +66 -25
package/dist/index.d.ts CHANGED
@@ -7,32 +7,55 @@
7
7
  * is an implementation detail.
8
8
  */
9
9
  export { default as Card } from './components/Card.svelte';
10
+ export { default as Panel } from './components/Panel.svelte';
11
+ export { default as PageShell } from './components/PageShell.svelte';
10
12
  export { default as PageHeader } from './components/PageHeader.svelte';
13
+ export { default as SectionHeading } from './components/SectionHeading.svelte';
11
14
  export { default as Divider } from './components/Divider.svelte';
12
15
  export { default as Accordion } from './components/Accordion.svelte';
13
16
  export { default as AccordionItem } from './components/AccordionItem.svelte';
17
+ export { default as Collapsible } from './components/Collapsible.svelte';
14
18
  export { default as Table } from './components/Table.svelte';
19
+ export { default as DescriptionList } from './components/DescriptionList.svelte';
20
+ export { default as Toolbar } from './components/Toolbar.svelte';
21
+ export { default as TreeView } from './components/TreeView.svelte';
22
+ export type { PageWidth } from './internal/layout.js';
23
+ export type { TreeNode, TriState } from './internal/tree.js';
15
24
  export { default as Button } from './components/Button.svelte';
16
25
  export { default as ButtonGroup } from './components/ButtonGroup.svelte';
17
26
  export { default as Input } from './components/Input.svelte';
27
+ export { default as PasswordInput } from './components/PasswordInput.svelte';
18
28
  export { default as Textarea } from './components/Textarea.svelte';
19
29
  export { default as NumberInput } from './components/NumberInput.svelte';
20
30
  export { default as SearchInput } from './components/SearchInput.svelte';
21
31
  export { default as FileInput } from './components/FileInput.svelte';
22
32
  export { default as Label } from './components/Label.svelte';
33
+ export { default as Field } from './components/Field.svelte';
34
+ export { default as FormMessage } from './components/FormMessage.svelte';
35
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
23
36
  export { default as Select } from './components/Select.svelte';
24
37
  export { default as MultiSelect } from './components/MultiSelect.svelte';
25
38
  export { default as Autocomplete } from './components/Autocomplete.svelte';
26
39
  export { default as DatePicker } from './components/DatePicker.svelte';
40
+ export { default as TimePicker } from './components/TimePicker.svelte';
41
+ export { default as DateTimePicker } from './components/DateTimePicker.svelte';
27
42
  export { default as Checkbox } from './components/Checkbox.svelte';
43
+ export { default as CheckboxGroup } from './components/CheckboxGroup.svelte';
28
44
  export { default as Radio } from './components/Radio.svelte';
29
45
  export { default as RadioGroup } from './components/RadioGroup.svelte';
30
46
  export { default as Toggle } from './components/Toggle.svelte';
47
+ export type { ChoiceSize, ChoiceVariant, ChoiceOrientation } from './internal/choice.js';
48
+ export type { ChoiceOption } from './components/CheckboxGroup.svelte';
49
+ export type { SelectOption, SelectChangeEvent } from './components/Select.svelte';
50
+ export type { ListOption } from './components/Autocomplete.svelte';
51
+ export type { FilterFn, FilterInput, FilterContext } from './internal/filter.js';
31
52
  export { default as Breadcrumb } from './components/Breadcrumb.svelte';
32
53
  export { default as Tabs } from './components/Tabs.svelte';
33
54
  export { default as Pagination } from './components/Pagination.svelte';
34
55
  export { default as StepIndicator } from './components/StepIndicator.svelte';
35
56
  export { default as Dropdown } from './components/Dropdown.svelte';
57
+ export { default as SidebarNav } from './components/SidebarNav.svelte';
58
+ export type { NavNode, NavTree } from './internal/nav-tree.js';
36
59
  export { default as Modal } from './components/Modal.svelte';
37
60
  export { default as Drawer } from './components/Drawer.svelte';
38
61
  export { default as Tooltip } from './components/Tooltip.svelte';
@@ -47,6 +70,7 @@ export { default as Skeleton } from './components/Skeleton.svelte';
47
70
  export { default as EmptyState } from './components/EmptyState.svelte';
48
71
  export { default as Stat } from './components/Stat.svelte';
49
72
  export { default as Kbd } from './components/Kbd.svelte';
73
+ export { default as CopyButton } from './components/CopyButton.svelte';
50
74
  export { default as Avatar } from './components/Avatar.svelte';
51
75
  export { default as AvatarGroup } from './components/AvatarGroup.svelte';
52
76
  export { default as ThemeToggle } from './components/ThemeToggle.svelte';
@@ -60,4 +84,4 @@ export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, set
60
84
  export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
61
85
  export { cn, type ClassValue } from './utils/cn.js';
62
86
  export { getTheme, setTheme, toggleTheme, themeBootScript, type Theme } from './utils/theme.js';
63
- export declare const VERSION = "0.11.2";
87
+ export declare const VERSION = "0.13.0";
package/dist/index.js CHANGED
@@ -8,25 +8,42 @@
8
8
  */
9
9
  // ── Layout & structure ─────────────────────────────────────────────────────
10
10
  export { default as Card } from './components/Card.svelte';
11
+ export { default as Panel } from './components/Panel.svelte';
12
+ export { default as PageShell } from './components/PageShell.svelte';
11
13
  export { default as PageHeader } from './components/PageHeader.svelte';
14
+ export { default as SectionHeading } from './components/SectionHeading.svelte';
12
15
  export { default as Divider } from './components/Divider.svelte';
13
16
  export { default as Accordion } from './components/Accordion.svelte';
14
17
  export { default as AccordionItem } from './components/AccordionItem.svelte';
18
+ export { default as Collapsible } from './components/Collapsible.svelte';
15
19
  export { default as Table } from './components/Table.svelte';
20
+ export { default as DescriptionList } from './components/DescriptionList.svelte';
21
+ export { default as Toolbar } from './components/Toolbar.svelte';
22
+ export { default as TreeView } from './components/TreeView.svelte';
16
23
  // ── Forms & inputs ─────────────────────────────────────────────────────────
17
24
  export { default as Button } from './components/Button.svelte';
18
25
  export { default as ButtonGroup } from './components/ButtonGroup.svelte';
19
26
  export { default as Input } from './components/Input.svelte';
27
+ export { default as PasswordInput } from './components/PasswordInput.svelte';
20
28
  export { default as Textarea } from './components/Textarea.svelte';
21
29
  export { default as NumberInput } from './components/NumberInput.svelte';
22
30
  export { default as SearchInput } from './components/SearchInput.svelte';
23
31
  export { default as FileInput } from './components/FileInput.svelte';
24
32
  export { default as Label } from './components/Label.svelte';
33
+ // The field furniture as a component, for a consumer composing a control the
34
+ // kit does not ship. internal/field.ts stays private, and a page that could
35
+ // not reach it hand-copied the class strings instead.
36
+ export { default as Field } from './components/Field.svelte';
37
+ export { default as FormMessage } from './components/FormMessage.svelte';
38
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
25
39
  export { default as Select } from './components/Select.svelte';
26
40
  export { default as MultiSelect } from './components/MultiSelect.svelte';
27
41
  export { default as Autocomplete } from './components/Autocomplete.svelte';
28
42
  export { default as DatePicker } from './components/DatePicker.svelte';
43
+ export { default as TimePicker } from './components/TimePicker.svelte';
44
+ export { default as DateTimePicker } from './components/DateTimePicker.svelte';
29
45
  export { default as Checkbox } from './components/Checkbox.svelte';
46
+ export { default as CheckboxGroup } from './components/CheckboxGroup.svelte';
30
47
  export { default as Radio } from './components/Radio.svelte';
31
48
  export { default as RadioGroup } from './components/RadioGroup.svelte';
32
49
  export { default as Toggle } from './components/Toggle.svelte';
@@ -36,6 +53,7 @@ export { default as Tabs } from './components/Tabs.svelte';
36
53
  export { default as Pagination } from './components/Pagination.svelte';
37
54
  export { default as StepIndicator } from './components/StepIndicator.svelte';
38
55
  export { default as Dropdown } from './components/Dropdown.svelte';
56
+ export { default as SidebarNav } from './components/SidebarNav.svelte';
39
57
  // ── Overlays ───────────────────────────────────────────────────────────────
40
58
  export { default as Modal } from './components/Modal.svelte';
41
59
  export { default as Drawer } from './components/Drawer.svelte';
@@ -52,6 +70,7 @@ export { default as Skeleton } from './components/Skeleton.svelte';
52
70
  export { default as EmptyState } from './components/EmptyState.svelte';
53
71
  export { default as Stat } from './components/Stat.svelte';
54
72
  export { default as Kbd } from './components/Kbd.svelte';
73
+ export { default as CopyButton } from './components/CopyButton.svelte';
55
74
  // ── Media ──────────────────────────────────────────────────────────────────
56
75
  export { default as Avatar } from './components/Avatar.svelte';
57
76
  export { default as AvatarGroup } from './components/AvatarGroup.svelte';
@@ -71,4 +90,4 @@ export { getTheme, setTheme, toggleTheme, themeBootScript } from './utils/theme.
71
90
  // ── Version ────────────────────────────────────────────────────────────────
72
91
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
73
92
  // this line; the build and the test suite fail when the two disagree.
74
- export const VERSION = '0.11.2';
93
+ export const VERSION = '0.13.0';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Month arithmetic for the date controls, done without the Date parser.
3
+ *
4
+ * The rule this module exists to enforce: no string is ever handed to the Date
5
+ * parser. `new Date('2026-01-02')` is UTC midnight by specification and
6
+ * `new Date('2026-01-02T00:00')` is local midnight, so one helper applied to a
7
+ * date field and to a datetime field lands in two different zones. Read the
8
+ * first form back through the local getters west of UTC and it is 1 January.
9
+ * That is the off by one day, and it appears only for the part of the world
10
+ * that is not on UTC, which is why it survives a developer's own testing.
11
+ *
12
+ * DatePicker binds `value` to an ISO calendar date spelled `YYYY-MM-DD`: four
13
+ * digit year, two digit zero padded month, two digit zero padded day, with the
14
+ * empty string standing for no date. Its `min` and `max` props take the same
15
+ * spelling and are compared as plain strings, which works only because that
16
+ * spelling sorts lexicographically. DateRangePicker and DateTimePicker will
17
+ * carry the same value and need the same arithmetic, and each of the three is a
18
+ * place the zone bug enters, so the arithmetic is stated once here.
19
+ *
20
+ * The numeric Date constructor is avoided as well. `new Date(24, 0, 1)` is
21
+ * 1924, because the constructor remaps years 0 through 99 into the twentieth
22
+ * century, and the year in a date field comes from a user. Everything below is
23
+ * integer arithmetic on the proleptic Gregorian calendar. The one Date in the
24
+ * file is the no argument one inside todayLocal, which is the only place a real
25
+ * clock is needed.
26
+ *
27
+ * Not exported from the package entry point - this is an implementation detail.
28
+ */
29
+ /** A calendar date with no zone and no time. m is 1-12, the month a human says, not the 0-11 the Date constructor takes. */
30
+ export interface CalendarDate {
31
+ y: number;
32
+ m: number;
33
+ d: number;
34
+ }
35
+ /**
36
+ * Reads the `YYYY-MM-DD` DatePicker binds, or null when the string is not one.
37
+ *
38
+ * Shape checks and round trips. `2024-13-45` returns null rather than silently
39
+ * becoming the 2025-02-14 those three numbers normalize to in a Date: a picker
40
+ * that accepted it would open on a month the user never typed and report a
41
+ * value they never chose. A day is rejected when its month does not have it, so
42
+ * `2024-02-30` and `2023-02-29` are both null while `2024-02-29` is a date.
43
+ * Undefined and the empty string are null too, because an unset field is the
44
+ * ordinary state of a date input and not an error.
45
+ */
46
+ export declare function parseISODate(s: string | undefined | null): CalendarDate | null;
47
+ /**
48
+ * Writes the `YYYY-MM-DD` DatePicker binds.
49
+ *
50
+ * Every part is zero padded, including the year, because the min and max
51
+ * comparison in the control is a string comparison: an unpadded `2024-1-5`
52
+ * sorts after `2024-12-01` and the bound then rejects dates inside it.
53
+ */
54
+ export declare function toISODate(date: CalendarDate): string;
55
+ /**
56
+ * Today in the viewer's own zone, read from the Date's local getters and never
57
+ * from its ISO string.
58
+ *
59
+ * `new Date().toISOString().slice(0, 10)` is the tempting one line version and
60
+ * it is wrong: it converts to UTC first, so a viewer in Jakarta gets yesterday
61
+ * until 7am and a viewer in New York gets tomorrow from the evening on. The
62
+ * date the calendar marks as today has to be the date on the wall behind the
63
+ * viewer.
64
+ */
65
+ export declare function todayLocal(): CalendarDate;
66
+ /**
67
+ * Days in a month, leap years included.
68
+ *
69
+ * A month outside 1 to 12 has no days and returns 0, so a caller bounding a day
70
+ * against it rejects the month rather than accepting a day in a month that does
71
+ * not exist.
72
+ */
73
+ export declare function daysInMonth(y: number, m: number): number;
74
+ /** Weekday of the first of the month, 0 for Sunday. */
75
+ export declare function firstWeekday(y: number, m: number): number;
76
+ /**
77
+ * Adds months, clamping the day rather than rolling into the next month.
78
+ *
79
+ * 31 January plus one month is 28 or 29 February, never 2 or 3 March. A Date
80
+ * rolls, so paging a picker forward from the 31st and back again lands the user
81
+ * on a different day than the one they were looking at, and a range whose start
82
+ * was the 31st can end up after its own end.
83
+ */
84
+ export declare function addMonths(date: CalendarDate, delta: number): CalendarDate;
85
+ /** Adds days across month and year boundaries, leap days included. */
86
+ export declare function addDays(date: CalendarDate, delta: number): CalendarDate;
87
+ /**
88
+ * Orders two dates as -1, 0 or 1.
89
+ *
90
+ * A total order, so a caller can sort and bound without constructing a Date.
91
+ * Returning the field difference would sort just as well and would report 2 for
92
+ * two years apart, which quietly breaks the caller that tests for 1.
93
+ */
94
+ export declare function compareDates(a: CalendarDate, b: CalendarDate): number;
95
+ /**
96
+ * Inclusive on both ends. Either bound may be null, meaning unbounded.
97
+ *
98
+ * Inclusive because a picker given min and max for a single allowed day has to
99
+ * offer that day. DatePicker compares its bounds as strings today, which agrees
100
+ * with this only while every bound is zero padded.
101
+ */
102
+ export declare function withinRange(date: CalendarDate, min: CalendarDate | null, max: CalendarDate | null): boolean;
103
+ /**
104
+ * The six week grid a month renders, always 42 cells so the calendar does not
105
+ * change height between months. Cells outside the viewed month carry
106
+ * `outside: true`.
107
+ *
108
+ * DatePicker pads its lead with empty spans and stops at the last day, so
109
+ * February in a year where it starts on a Sunday draws four rows and a 31 day
110
+ * month starting on a Saturday draws six. The popover resizes as the user pages
111
+ * through the year and the day under the pointer moves out from under it. Fixed
112
+ * at 42 because six weeks is the most any month can touch.
113
+ *
114
+ * `weekStartsOn` is 0 for Sunday and 1 for Monday.
115
+ */
116
+ export declare function monthGrid(y: number, m: number, weekStartsOn?: 0 | 1): {
117
+ date: CalendarDate;
118
+ outside: boolean;
119
+ }[];
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Month arithmetic for the date controls, done without the Date parser.
3
+ *
4
+ * The rule this module exists to enforce: no string is ever handed to the Date
5
+ * parser. `new Date('2026-01-02')` is UTC midnight by specification and
6
+ * `new Date('2026-01-02T00:00')` is local midnight, so one helper applied to a
7
+ * date field and to a datetime field lands in two different zones. Read the
8
+ * first form back through the local getters west of UTC and it is 1 January.
9
+ * That is the off by one day, and it appears only for the part of the world
10
+ * that is not on UTC, which is why it survives a developer's own testing.
11
+ *
12
+ * DatePicker binds `value` to an ISO calendar date spelled `YYYY-MM-DD`: four
13
+ * digit year, two digit zero padded month, two digit zero padded day, with the
14
+ * empty string standing for no date. Its `min` and `max` props take the same
15
+ * spelling and are compared as plain strings, which works only because that
16
+ * spelling sorts lexicographically. DateRangePicker and DateTimePicker will
17
+ * carry the same value and need the same arithmetic, and each of the three is a
18
+ * place the zone bug enters, so the arithmetic is stated once here.
19
+ *
20
+ * The numeric Date constructor is avoided as well. `new Date(24, 0, 1)` is
21
+ * 1924, because the constructor remaps years 0 through 99 into the twentieth
22
+ * century, and the year in a date field comes from a user. Everything below is
23
+ * integer arithmetic on the proleptic Gregorian calendar. The one Date in the
24
+ * file is the no argument one inside todayLocal, which is the only place a real
25
+ * clock is needed.
26
+ *
27
+ * Not exported from the package entry point - this is an implementation detail.
28
+ */
29
+ /** Month lengths in a common year. February is corrected for leap years in daysInMonth. */
30
+ const MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
31
+ /** The only spelling DatePicker reads or writes. Anything else is not a date. */
32
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
33
+ /**
34
+ * The full Gregorian rule, not the every fourth year shorthand.
35
+ *
36
+ * A century is a common year unless it divides by 400. Dropping that clause
37
+ * makes 2000 a common year and puts 29 February 2000 out of reach of the
38
+ * control; keeping only the century clause makes 1900 a leap year and invents a
39
+ * day nobody lived through.
40
+ */
41
+ function isLeapYear(y) {
42
+ return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
43
+ }
44
+ /** The remainder operator keeps the sign of its left operand, so a negative weekday offset needs the extra turn. */
45
+ function mod7(n) {
46
+ return ((n % 7) + 7) % 7;
47
+ }
48
+ /**
49
+ * Days from 1970-01-01, negative before it.
50
+ *
51
+ * Howard Hinnant's days_from_civil, which is exact for every year the
52
+ * arithmetic can express and never touches a Date. Counting days is what makes
53
+ * addDays a single addition rather than a carry the caller has to write across
54
+ * three fields, and it is what gives firstWeekday an answer for a year under
55
+ * 100, where the Date constructor gives the wrong century.
56
+ */
57
+ function toDayNumber(date) {
58
+ const shiftedYear = date.y - (date.m <= 2 ? 1 : 0);
59
+ const era = Math.floor(shiftedYear / 400);
60
+ const yearOfEra = shiftedYear - era * 400;
61
+ const dayOfYear = Math.floor((153 * (date.m + (date.m > 2 ? -3 : 9)) + 2) / 5) + date.d - 1;
62
+ const dayOfEra = yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear;
63
+ return era * 146097 + dayOfEra - 719468;
64
+ }
65
+ /** The inverse of toDayNumber, so a day count can go back to a date the grid can render. */
66
+ function fromDayNumber(dayNumber) {
67
+ const shifted = dayNumber + 719468;
68
+ const era = Math.floor(shifted / 146097);
69
+ const dayOfEra = shifted - era * 146097;
70
+ const yearOfEra = Math.floor((dayOfEra -
71
+ Math.floor(dayOfEra / 1460) +
72
+ Math.floor(dayOfEra / 36524) -
73
+ Math.floor(dayOfEra / 146096)) /
74
+ 365);
75
+ const dayOfYear = dayOfEra - (365 * yearOfEra + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100));
76
+ const monthIndex = Math.floor((5 * dayOfYear + 2) / 153);
77
+ const d = dayOfYear - Math.floor((153 * monthIndex + 2) / 5) + 1;
78
+ const m = monthIndex + (monthIndex < 10 ? 3 : -9);
79
+ return { y: yearOfEra + era * 400 + (m <= 2 ? 1 : 0), m, d };
80
+ }
81
+ /**
82
+ * Reads the `YYYY-MM-DD` DatePicker binds, or null when the string is not one.
83
+ *
84
+ * Shape checks and round trips. `2024-13-45` returns null rather than silently
85
+ * becoming the 2025-02-14 those three numbers normalize to in a Date: a picker
86
+ * that accepted it would open on a month the user never typed and report a
87
+ * value they never chose. A day is rejected when its month does not have it, so
88
+ * `2024-02-30` and `2023-02-29` are both null while `2024-02-29` is a date.
89
+ * Undefined and the empty string are null too, because an unset field is the
90
+ * ordinary state of a date input and not an error.
91
+ */
92
+ export function parseISODate(s) {
93
+ if (!s)
94
+ return null;
95
+ const parts = ISO_DATE.exec(s);
96
+ if (!parts)
97
+ return null;
98
+ const y = Number(parts[1]);
99
+ const m = Number(parts[2]);
100
+ const d = Number(parts[3]);
101
+ if (m < 1 || m > 12)
102
+ return null;
103
+ if (d < 1 || d > daysInMonth(y, m))
104
+ return null;
105
+ return { y, m, d };
106
+ }
107
+ /** Pads a number to a fixed width so the result sorts as text. */
108
+ function pad(n, width) {
109
+ return String(n).padStart(width, '0');
110
+ }
111
+ /**
112
+ * Writes the `YYYY-MM-DD` DatePicker binds.
113
+ *
114
+ * Every part is zero padded, including the year, because the min and max
115
+ * comparison in the control is a string comparison: an unpadded `2024-1-5`
116
+ * sorts after `2024-12-01` and the bound then rejects dates inside it.
117
+ */
118
+ export function toISODate(date) {
119
+ return `${pad(date.y, 4)}-${pad(date.m, 2)}-${pad(date.d, 2)}`;
120
+ }
121
+ /**
122
+ * Today in the viewer's own zone, read from the Date's local getters and never
123
+ * from its ISO string.
124
+ *
125
+ * `new Date().toISOString().slice(0, 10)` is the tempting one line version and
126
+ * it is wrong: it converts to UTC first, so a viewer in Jakarta gets yesterday
127
+ * until 7am and a viewer in New York gets tomorrow from the evening on. The
128
+ * date the calendar marks as today has to be the date on the wall behind the
129
+ * viewer.
130
+ */
131
+ export function todayLocal() {
132
+ const now = new Date();
133
+ return { y: now.getFullYear(), m: now.getMonth() + 1, d: now.getDate() };
134
+ }
135
+ /**
136
+ * Days in a month, leap years included.
137
+ *
138
+ * A month outside 1 to 12 has no days and returns 0, so a caller bounding a day
139
+ * against it rejects the month rather than accepting a day in a month that does
140
+ * not exist.
141
+ */
142
+ export function daysInMonth(y, m) {
143
+ if (!Number.isInteger(m) || m < 1 || m > 12)
144
+ return 0;
145
+ if (m === 2)
146
+ return isLeapYear(y) ? 29 : 28;
147
+ return MONTH_LENGTHS[m - 1];
148
+ }
149
+ /** Weekday of the first of the month, 0 for Sunday. */
150
+ export function firstWeekday(y, m) {
151
+ // Day number 0 is 1970-01-01, which was a Thursday, so the count starts four
152
+ // weekdays into the week.
153
+ return mod7(toDayNumber({ y, m, d: 1 }) + 4);
154
+ }
155
+ /**
156
+ * Adds months, clamping the day rather than rolling into the next month.
157
+ *
158
+ * 31 January plus one month is 28 or 29 February, never 2 or 3 March. A Date
159
+ * rolls, so paging a picker forward from the 31st and back again lands the user
160
+ * on a different day than the one they were looking at, and a range whose start
161
+ * was the 31st can end up after its own end.
162
+ */
163
+ export function addMonths(date, delta) {
164
+ const totalMonths = date.y * 12 + (date.m - 1) + delta;
165
+ const y = Math.floor(totalMonths / 12);
166
+ const m = totalMonths - y * 12 + 1;
167
+ return { y, m, d: Math.min(date.d, daysInMonth(y, m)) };
168
+ }
169
+ /** Adds days across month and year boundaries, leap days included. */
170
+ export function addDays(date, delta) {
171
+ return fromDayNumber(toDayNumber(date) + delta);
172
+ }
173
+ /**
174
+ * Orders two dates as -1, 0 or 1.
175
+ *
176
+ * A total order, so a caller can sort and bound without constructing a Date.
177
+ * Returning the field difference would sort just as well and would report 2 for
178
+ * two years apart, which quietly breaks the caller that tests for 1.
179
+ */
180
+ export function compareDates(a, b) {
181
+ if (a.y !== b.y)
182
+ return a.y < b.y ? -1 : 1;
183
+ if (a.m !== b.m)
184
+ return a.m < b.m ? -1 : 1;
185
+ if (a.d !== b.d)
186
+ return a.d < b.d ? -1 : 1;
187
+ return 0;
188
+ }
189
+ /**
190
+ * Inclusive on both ends. Either bound may be null, meaning unbounded.
191
+ *
192
+ * Inclusive because a picker given min and max for a single allowed day has to
193
+ * offer that day. DatePicker compares its bounds as strings today, which agrees
194
+ * with this only while every bound is zero padded.
195
+ */
196
+ export function withinRange(date, min, max) {
197
+ if (min !== null && compareDates(date, min) < 0)
198
+ return false;
199
+ if (max !== null && compareDates(date, max) > 0)
200
+ return false;
201
+ return true;
202
+ }
203
+ /**
204
+ * The six week grid a month renders, always 42 cells so the calendar does not
205
+ * change height between months. Cells outside the viewed month carry
206
+ * `outside: true`.
207
+ *
208
+ * DatePicker pads its lead with empty spans and stops at the last day, so
209
+ * February in a year where it starts on a Sunday draws four rows and a 31 day
210
+ * month starting on a Saturday draws six. The popover resizes as the user pages
211
+ * through the year and the day under the pointer moves out from under it. Fixed
212
+ * at 42 because six weeks is the most any month can touch.
213
+ *
214
+ * `weekStartsOn` is 0 for Sunday and 1 for Monday.
215
+ */
216
+ export function monthGrid(y, m, weekStartsOn = 0) {
217
+ const lead = mod7(firstWeekday(y, m) - weekStartsOn);
218
+ const start = toDayNumber({ y, m, d: 1 }) - lead;
219
+ const cells = [];
220
+ for (let i = 0; i < 42; i++) {
221
+ const date = fromDayNumber(start + i);
222
+ cells.push({ date, outside: date.y !== y || date.m !== m });
223
+ }
224
+ return cells;
225
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The single source of truth for how a checkbox or a radio looks and announces
3
+ * itself.
4
+ *
5
+ * Checkbox, Radio and RadioGroup paint one control three ways. The two label
6
+ * wrappers hold the same utilities in two orders, Checkbox marks a required
7
+ * option and Radio marks nothing, RadioGroup rests its box on `border-line`
8
+ * where the other two use `border-line-strong`, and RadioGroup still keeps its
9
+ * focus ring inside the selected ternary. That last one is the defect Checkbox
10
+ * and Radio were already fixed for: selecting an option deleted the only
11
+ * indicator a keyboard user had, and in a list of a hundred options that loses
12
+ * your place entirely. Composing from here means a fourth choice control cannot
13
+ * reintroduce any of it.
14
+ *
15
+ * Not exported from the package entry point - this is an implementation detail.
16
+ */
17
+ /** The scale of the painted box: sm 14px, md 16px, lg 20px. */
18
+ export type ChoiceSize = 'sm' | 'md' | 'lg';
19
+ /** Inline is a box beside a label. Card is a bordered option surface. */
20
+ export type ChoiceVariant = 'inline' | 'card';
21
+ /** How a group lays its options out. */
22
+ export type ChoiceOrientation = 'vertical' | 'horizontal';
23
+ /**
24
+ * The clickable wrapper. Card is a bordered option the whole surface of which
25
+ * is the target.
26
+ *
27
+ * The cursor is a ternary rather than a `cursor-pointer` that a disabled state
28
+ * appends `cursor-not-allowed` to. Both set the same property, so a class list
29
+ * carrying both resolves by stylesheet order and not by the order written here,
30
+ * which is how a disabled option kept offering a pointer.
31
+ */
32
+ export declare function choiceWrap(variant: ChoiceVariant, checked: boolean, disabled: boolean): string;
33
+ /**
34
+ * The transparent input that sits over the painted box and carries every native
35
+ * semantic.
36
+ *
37
+ * `sr-only` leaves the input 1x1 and buried under the box that replaces it, so
38
+ * a click aimed at what the user sees lands on nothing. It covers the box
39
+ * instead, and `peer` is what lets the box below read its focus state.
40
+ */
41
+ export declare const CHOICE_INPUT = "peer absolute inset-0 m-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed";
42
+ /**
43
+ * The painted box or circle. Square for a checkbox, round for a radio, sized by
44
+ * ChoiceSize.
45
+ *
46
+ * The resting border is `border-line-strong`. `border-line` reads 1.25:1
47
+ * against the page, and the border is the only thing identifying an empty
48
+ * checkbox as a control, so at that contrast the control is invisible until it
49
+ * is used. A radio keeps a 2px ring because its selected state is a dot inside
50
+ * the ring rather than a fill, and a 1px ring reads as a smudge beside the dot
51
+ * at 14px.
52
+ *
53
+ * `mixed` paints exactly what `checked` paints. A part-selected parent is on,
54
+ * not a third state with a colour of its own; only the mark it holds differs.
55
+ */
56
+ export declare function choiceBox(kind: 'checkbox' | 'radio', size: ChoiceSize, checked: boolean, mixed: boolean): string;
57
+ /**
58
+ * The focus ring, stated ONCE and outside every checked branch.
59
+ *
60
+ * The ring lived inside the checked ternary in three components, so choosing an
61
+ * option removed the only indicator a keyboard user had. It is a ring and not a
62
+ * border colour because a selected box already carries a brand border, which
63
+ * leaves a border-only focus state with nothing to say.
64
+ */
65
+ export declare const CHOICE_FOCUS = "peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-brand peer-focus-visible:outline-offset-2";
66
+ /**
67
+ * The label stack beside or under the box.
68
+ *
69
+ * Checkbox and Radio both build this, in different word orders, which is how
70
+ * two controls in one form ended up with different gaps between a label and its
71
+ * description.
72
+ */
73
+ export declare const CHOICE_LABEL_STACK = "flex flex-col gap-0.5";
74
+ /**
75
+ * The label text, sized with the control.
76
+ *
77
+ * An option label is not a field label: it names one choice rather than the
78
+ * group, so it stays regular weight and FIELD_LABEL keeps the medium.
79
+ */
80
+ export declare function choiceLabel(size: ChoiceSize): string;
81
+ /**
82
+ * Secondary line under the label.
83
+ *
84
+ * The same class the hint under a text field uses, taken from field.ts rather
85
+ * than respelled, because a checkbox hint and an input hint sitting in one form
86
+ * have no reason to differ and had already drifted apart once.
87
+ */
88
+ export declare const CHOICE_DESCRIPTION = "text-xs text-faint";
89
+ /**
90
+ * The icon slot ahead of the label.
91
+ *
92
+ * Fixed to the box size so the label starts at the same x in every row of a
93
+ * list, and given no colour of its own so it inherits the label and dims with
94
+ * the wrapper when the option is disabled.
95
+ */
96
+ export declare function choiceIcon(size: ChoiceSize): string;
97
+ /**
98
+ * Pixel size for a lucide icon at each ChoiceSize, so a consumer does not
99
+ * guess.
100
+ *
101
+ * A lucide icon takes a number, not a class, so the slot above cannot size it.
102
+ * These are the same 14, 16 and 20 the slot reserves; an icon rendered at any
103
+ * other size overflows the slot or floats inside it.
104
+ */
105
+ export declare const CHOICE_ICON_PX: Record<ChoiceSize, number>;
106
+ /**
107
+ * The fieldset a group renders, so the set is announced as a group rather than
108
+ * as loose controls.
109
+ *
110
+ * `min-w-0` is load bearing: a fieldset defaults to `min-width: min-content`,
111
+ * so one long option label widened the whole group past its column instead of
112
+ * wrapping.
113
+ */
114
+ export declare const CHOICE_GROUP = "flex min-w-0 flex-col gap-2";
115
+ /**
116
+ * The row or column the options sit in.
117
+ *
118
+ * A horizontal group gets a wider inline gap than block gap: options read as
119
+ * separate choices across a row and as one list down a column, and an equal gap
120
+ * in both axes makes a wrapped row look like a grid.
121
+ */
122
+ export declare function choiceGroupList(orientation: ChoiceOrientation): string;
123
+ /**
124
+ * The check and the mixed marks, as SVG path data on the box's own grid.
125
+ *
126
+ * Drawn as paths because the consistency suite rejects a Unicode check outright,
127
+ * and because a font glyph lands at a different optical weight from every other
128
+ * icon in the library. Both sit in a 10 by 8 viewBox centred in the box, at
129
+ * stroke width 1.5 with round caps, so the two marks swap without the box
130
+ * shifting. The mixed bar stops short of the edges so its round caps do not
131
+ * touch the border.
132
+ */
133
+ export declare const CHOICE_MARK: {
134
+ check: string;
135
+ mixed: string;
136
+ };