@mistriwork/ui 0.0.0 → 1.0.0-rc.9

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -1
  3. package/dist/Avatar.d.ts +11 -0
  4. package/dist/Avatar.js +28 -0
  5. package/dist/Button.d.ts +36 -0
  6. package/dist/Button.js +22 -0
  7. package/dist/Carousel.d.ts +52 -0
  8. package/dist/Carousel.js +110 -0
  9. package/dist/Choices.d.ts +77 -0
  10. package/dist/Choices.js +88 -0
  11. package/dist/DateField.d.ts +66 -0
  12. package/dist/DateField.js +255 -0
  13. package/dist/Drawer.d.ts +33 -0
  14. package/dist/Drawer.js +12 -0
  15. package/dist/DropMenu.d.ts +59 -0
  16. package/dist/DropMenu.js +52 -0
  17. package/dist/Dropdown.d.ts +73 -0
  18. package/dist/Dropdown.js +85 -0
  19. package/dist/Field.d.ts +53 -0
  20. package/dist/Field.js +58 -0
  21. package/dist/FormParts.d.ts +68 -0
  22. package/dist/FormParts.js +53 -0
  23. package/dist/HeroBlocks.d.ts +37 -0
  24. package/dist/HeroBlocks.js +26 -0
  25. package/dist/Icon.d.ts +29 -0
  26. package/dist/Icon.js +78 -0
  27. package/dist/Modal.d.ts +48 -0
  28. package/dist/Modal.js +57 -0
  29. package/dist/Pagination.d.ts +33 -0
  30. package/dist/Pagination.js +25 -0
  31. package/dist/Popover.d.ts +21 -0
  32. package/dist/Popover.js +20 -0
  33. package/dist/Resizable.d.ts +48 -0
  34. package/dist/Resizable.js +122 -0
  35. package/dist/ScrollArea.d.ts +29 -0
  36. package/dist/ScrollArea.js +62 -0
  37. package/dist/Select.d.ts +26 -0
  38. package/dist/Select.js +20 -0
  39. package/dist/SensitiveField.d.ts +26 -0
  40. package/dist/SensitiveField.js +27 -0
  41. package/dist/Slider.d.ts +17 -0
  42. package/dist/Slider.js +10 -0
  43. package/dist/Switch.d.ts +28 -0
  44. package/dist/Switch.js +24 -0
  45. package/dist/Tabs.d.ts +25 -0
  46. package/dist/Tabs.js +5 -0
  47. package/dist/TextField.d.ts +44 -0
  48. package/dist/TextField.js +19 -0
  49. package/dist/Tooltip.d.ts +29 -0
  50. package/dist/Tooltip.js +39 -0
  51. package/dist/Typeahead.d.ts +63 -0
  52. package/dist/Typeahead.js +90 -0
  53. package/dist/fieldFrame.d.ts +37 -0
  54. package/dist/fieldFrame.js +16 -0
  55. package/dist/forms.css +192 -0
  56. package/dist/hero.css +23 -0
  57. package/dist/index.d.ts +38 -0
  58. package/dist/index.js +40 -0
  59. package/dist/internal/ComponentStyleScope.d.ts +1 -0
  60. package/dist/internal/ComponentStyleScope.js +4 -0
  61. package/dist/internal/anchor.d.ts +19 -0
  62. package/dist/internal/anchor.js +52 -0
  63. package/dist/internal/datePickerModel.d.ts +53 -0
  64. package/dist/internal/datePickerModel.js +175 -0
  65. package/dist/internal/popups.d.ts +13 -0
  66. package/dist/internal/popups.js +16 -0
  67. package/dist/internal/ruler.d.ts +37 -0
  68. package/dist/internal/ruler.js +55 -0
  69. package/dist/masks.d.ts +32 -0
  70. package/dist/masks.js +62 -0
  71. package/dist/preferences.css +26 -0
  72. package/dist/primitives.css +188 -0
  73. package/dist/standalone.css +413 -0
  74. package/dist/style.css +19 -0
  75. package/dist/tokens.css +233 -0
  76. package/mark.svg +1 -0
  77. package/package.json +85 -3
@@ -0,0 +1,53 @@
1
+ /**
2
+ * A calendar's arithmetic on ISO dates (`YYYY-MM-DD`) and wall-clock date-times: no value is ever an instant, so no
3
+ * timezone moves a day. The errors it throws are a caller's mistake (a date it should have checked first), said to a
4
+ * developer, so they are plain English rather than a catalog's words.
5
+ */
6
+ export declare const FIRST_DATE = "0001-01-01";
7
+ export declare const LAST_DATE = "9999-12-31";
8
+ interface DateParts {
9
+ year: number;
10
+ month: number;
11
+ day: number;
12
+ }
13
+ interface DateBounds {
14
+ min: string;
15
+ max: string;
16
+ }
17
+ interface CalendarDay {
18
+ date: string | null;
19
+ inMonth: boolean;
20
+ disabled: boolean;
21
+ }
22
+ export declare function dateFormat(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
23
+ /** Reuse command validation, narrowing its year-zero support to the form domain. */
24
+ export declare function isPickerDate(value: unknown): value is string;
25
+ export declare function dateParts(value: string): DateParts;
26
+ export declare function daysInMonth(year: number, month: number): number;
27
+ export declare function addDateDays(value: string, step: number): string;
28
+ export declare function addDateMonths(value: string, step: number): string;
29
+ /** Empty ends mean unbounded. Refusing a malformed bound prevents accidental widening. */
30
+ export declare function dateBounds(min?: string, max?: string): DateBounds;
31
+ export declare function dateWithinBounds(value: string, bounds: DateBounds): boolean;
32
+ export declare function clampDate(value: string, bounds: DateBounds): string;
33
+ /** Six stable weeks; out-of-domain cells are blanks rather than repeated edge days. */
34
+ export declare function monthGrid(value: string, firstDay?: number, bounds?: DateBounds): CalendarDay[];
35
+ export declare function monthLabel(value: string, locale?: string): string;
36
+ export declare function weekdayLabels(locale?: string, firstDay?: number): Array<{
37
+ short: string;
38
+ long: string;
39
+ }>;
40
+ export declare function isPickerTime(value: unknown): value is string;
41
+ /** A date and a wall-clock time, split at the T a datetime-local control writes or the space a person types. */
42
+ export declare function splitDateTime(value: string): {
43
+ date: string;
44
+ time: string;
45
+ } | null;
46
+ export declare function isPickerDateTime(value: unknown): value is string;
47
+ /** The date a calendar picked, keeping the time already typed; a value with none starts at midnight. Said as the box says it, `YYYY-MM-DD HH:MM`. */
48
+ export declare function withDate(value: string, date: string): string;
49
+ /** Inclusive bounds on a wall time; an end that is not a complete date and time bounds nothing. */
50
+ export declare function dateTimeWithinBounds(value: string, min?: string, max?: string): boolean;
51
+ /** Only 'today' depends on a timezone; values and calendar movement never do. */
52
+ export declare function todayInZone(now: Date, timeZone?: string): string;
53
+ export {};
@@ -0,0 +1,175 @@
1
+ /**
2
+ * A calendar's arithmetic on ISO dates (`YYYY-MM-DD`) and wall-clock date-times: no value is ever an instant, so no
3
+ * timezone moves a day. The errors it throws are a caller's mistake (a date it should have checked first), said to a
4
+ * developer, so they are plain English rather than a catalog's words.
5
+ */
6
+ /** A real calendar date written as ISO: the shape, and a day the month has. */
7
+ function isIsoDate(value) {
8
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
9
+ return false;
10
+ const date = new Date(`${value}T00:00:00Z`);
11
+ return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
12
+ }
13
+ export const FIRST_DATE = '0001-01-01';
14
+ export const LAST_DATE = '9999-12-31';
15
+ /* One formatter per locale and options, made once: a date field drew its
16
+ weekday names, its month and "today" on every render, and each
17
+ Intl.DateTimeFormat costs about a millisecond to build on a phone — a
18
+ filter sheet with two date ranges spent most of its opening building them
19
+ (test/built-client-vitals.mjs). An option the platform refuses still
20
+ throws, every time, since only a built formatter is kept. */
21
+ const formatters = new Map();
22
+ export function dateFormat(locale, options) {
23
+ const key = JSON.stringify([locale ?? null, options]);
24
+ let format = formatters.get(key);
25
+ if (!format) {
26
+ format = new Intl.DateTimeFormat(locale, options);
27
+ formatters.set(key, format);
28
+ }
29
+ return format;
30
+ }
31
+ /** Reuse command validation, narrowing its year-zero support to the form domain. */
32
+ export function isPickerDate(value) {
33
+ return isIsoDate(value) && value >= FIRST_DATE && value <= LAST_DATE;
34
+ }
35
+ export function dateParts(value) {
36
+ if (!isPickerDate(value))
37
+ throw new RangeError('Expected an existing ISO date from 0001-01-01 through 9999-12-31.');
38
+ return { year: Number(value.slice(0, 4)), month: Number(value.slice(5, 7)), day: Number(value.slice(8, 10)) };
39
+ }
40
+ /** UTC is only a ruler for calendar arithmetic, never a conversion of user input.
41
+ setUTCFullYear avoids the Date constructor's special treatment of years 0–99. */
42
+ function calendarDate({ year, month, day }) {
43
+ const date = new Date(0);
44
+ date.setUTCFullYear(year, month - 1, day);
45
+ return date;
46
+ }
47
+ function iso(date) {
48
+ return `${String(date.getUTCFullYear()).padStart(4, '0')}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;
49
+ }
50
+ function integer(value) {
51
+ if (!Number.isSafeInteger(value))
52
+ throw new RangeError('Calendar steps must be safe integers.');
53
+ }
54
+ export function daysInMonth(year, month) {
55
+ integer(year);
56
+ integer(month);
57
+ if (year < 1 || year > 9999 || month < 1 || month > 12)
58
+ throw new RangeError('Invalid calendar month.');
59
+ return [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
60
+ }
61
+ export function addDateDays(value, step) {
62
+ const date = calendarDate(dateParts(value));
63
+ integer(step);
64
+ const min = calendarDate(dateParts(FIRST_DATE)).getTime();
65
+ const max = calendarDate(dateParts(LAST_DATE)).getTime();
66
+ return iso(new Date(Math.min(max, Math.max(min, date.getTime() + step * 86_400_000))));
67
+ }
68
+ export function addDateMonths(value, step) {
69
+ const current = dateParts(value);
70
+ integer(step);
71
+ const index = Math.min(9999 * 12 - 1, Math.max(0, (current.year - 1) * 12 + current.month - 1 + step));
72
+ const year = Math.floor(index / 12) + 1, month = (index % 12) + 1;
73
+ return iso(calendarDate({ year, month, day: Math.min(current.day, daysInMonth(year, month)) }));
74
+ }
75
+ /** Empty ends mean unbounded. Refusing a malformed bound prevents accidental widening. */
76
+ export function dateBounds(min, max) {
77
+ // Command declarations historically accept year zero; as with native date
78
+ // controls, that lower bound must not disable all supported modern dates.
79
+ const lower = min && isIsoDate(min) && min < FIRST_DATE ? FIRST_DATE : min || FIRST_DATE;
80
+ const upper = max || LAST_DATE;
81
+ dateParts(lower);
82
+ dateParts(upper);
83
+ if (lower > upper)
84
+ throw new RangeError('Minimum date must not exceed maximum date.');
85
+ return { min: lower, max: upper };
86
+ }
87
+ export function dateWithinBounds(value, bounds) {
88
+ return isPickerDate(value) && value >= bounds.min && value <= bounds.max;
89
+ }
90
+ export function clampDate(value, bounds) {
91
+ dateParts(value);
92
+ return value < bounds.min ? bounds.min : value > bounds.max ? bounds.max : value;
93
+ }
94
+ /** Six stable weeks; out-of-domain cells are blanks rather than repeated edge days. */
95
+ export function monthGrid(value, firstDay = 1, bounds = dateBounds()) {
96
+ integer(firstDay);
97
+ if (firstDay < 0 || firstDay > 6)
98
+ throw new RangeError('First weekday must be between 0 (Sunday) and 6.');
99
+ const parts = dateParts(value), start = calendarDate({ ...parts, day: 1 });
100
+ const offset = (start.getUTCDay() - firstDay + 7) % 7;
101
+ return Array.from({ length: 42 }, (_, index) => {
102
+ const day = new Date(start.getTime() + (index - offset) * 86_400_000);
103
+ if (day.getUTCFullYear() < 1 || day.getUTCFullYear() > 9999)
104
+ return { date: null, inMonth: false, disabled: true };
105
+ const date = iso(day);
106
+ return { date, inMonth: day.getUTCMonth() + 1 === parts.month, disabled: !dateWithinBounds(date, bounds) };
107
+ });
108
+ }
109
+ export function monthLabel(value, locale) {
110
+ return dateFormat(locale, { calendar: 'gregory', timeZone: 'UTC', month: 'long', year: 'numeric' }).format(calendarDate(dateParts(value)));
111
+ }
112
+ export function weekdayLabels(locale, firstDay = 1) {
113
+ integer(firstDay);
114
+ if (firstDay < 0 || firstDay > 6)
115
+ throw new RangeError('First weekday must be between 0 (Sunday) and 6.');
116
+ const short = dateFormat(locale, { calendar: 'gregory', timeZone: 'UTC', weekday: 'short' });
117
+ const long = dateFormat(locale, { calendar: 'gregory', timeZone: 'UTC', weekday: 'long' });
118
+ return Array.from({ length: 7 }, (_, index) => {
119
+ const day = calendarDate({ year: 2023, month: 1, day: 1 + ((firstDay + index) % 7) });
120
+ return { short: short.format(day), long: long.format(day) };
121
+ });
122
+ }
123
+ /* A wall-clock time as a datetime-local control writes one: HH:MM, then
124
+ optional seconds and milliseconds. No zone, ever: a value with an offset or
125
+ a Z is an instant, and an instant is not a picker's to edit as a date. */
126
+ const WALL_TIME = /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d{1,3})?)?$/;
127
+ export function isPickerTime(value) {
128
+ return typeof value === 'string' && WALL_TIME.test(value);
129
+ }
130
+ /** A date and a wall-clock time, split at the T a datetime-local control writes or the space a person types. */
131
+ export function splitDateTime(value) {
132
+ const at = value.search(/[T ]/);
133
+ return at === 10 ? { date: value.slice(0, 10), time: value.slice(11) } : null;
134
+ }
135
+ export function isPickerDateTime(value) {
136
+ const parts = typeof value === 'string' ? splitDateTime(value) : null;
137
+ return Boolean(parts && isPickerDate(parts.date) && isPickerTime(parts.time));
138
+ }
139
+ /** The date a calendar picked, keeping the time already typed; a value with none starts at midnight. Said as the box says it, `YYYY-MM-DD HH:MM`. */
140
+ export function withDate(value, date) {
141
+ const time = splitDateTime(value)?.time;
142
+ return `${date} ${time && isPickerTime(time) ? time : '00:00'}`;
143
+ }
144
+ /** Seconds and milliseconds filled in, so two wall times compare as text. Null for anything that is not one. */
145
+ function comparable(value) {
146
+ if (!isPickerDateTime(value))
147
+ return null;
148
+ const { date, time } = splitDateTime(value);
149
+ const [hm, rest = '00'] = [time.slice(0, 5), time.slice(6)];
150
+ const [seconds = '00', fraction = ''] = rest.split('.');
151
+ return `${date}T${hm}:${seconds.padEnd(2, '0')}.${fraction.padEnd(3, '0')}`;
152
+ }
153
+ /** Inclusive bounds on a wall time; an end that is not a complete date and time bounds nothing. */
154
+ export function dateTimeWithinBounds(value, min, max) {
155
+ const at = comparable(value);
156
+ if (at === null)
157
+ return false;
158
+ const lower = min ? comparable(min) : null, upper = max ? comparable(max) : null;
159
+ return (lower === null || at >= lower) && (upper === null || at <= upper);
160
+ }
161
+ /** Only 'today' depends on a timezone; values and calendar movement never do. */
162
+ export function todayInZone(now, timeZone) {
163
+ const parts = dateFormat('en-US', {
164
+ calendar: 'gregory',
165
+ numberingSystem: 'latn',
166
+ timeZone,
167
+ year: 'numeric',
168
+ month: '2-digit',
169
+ day: '2-digit',
170
+ }).formatToParts(now);
171
+ const get = (type) => parts.find((part) => part.type === type).value;
172
+ const result = `${get('year').padStart(4, '0')}-${get('month')}-${get('day')}`;
173
+ dateParts(result);
174
+ return result;
175
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Where the primitives' popups — a dialog, a menu, a listbox — are drawn.
3
+ *
4
+ * Undefined lets Base UI choose, which is the body, or the enclosing portal
5
+ * for a popup inside a popup, which an explicit body would not keep. That is
6
+ * right for a page of its own; a shell drawn inside another page (an embed)
7
+ * names its own container with `drawPopupsIn`
8
+ * (packages/mistri/src/embed/seams/shellHost.ts).
9
+ */
10
+ /** The container handed to Base UI's portals. */
11
+ export declare const popupContainer: () => HTMLElement | undefined;
12
+ /** Draw every popup in this container from now on. */
13
+ export declare function drawPopupsIn(where: () => HTMLElement | undefined): void;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Where the primitives' popups — a dialog, a menu, a listbox — are drawn.
3
+ *
4
+ * Undefined lets Base UI choose, which is the body, or the enclosing portal
5
+ * for a popup inside a popup, which an explicit body would not keep. That is
6
+ * right for a page of its own; a shell drawn inside another page (an embed)
7
+ * names its own container with `drawPopupsIn`
8
+ * (packages/mistri/src/embed/seams/shellHost.ts).
9
+ */
10
+ let container = () => undefined;
11
+ /** The container handed to Base UI's portals. */
12
+ export const popupContainer = () => container();
13
+ /** Draw every popup in this container from now on. */
14
+ export function drawPopupsIn(where) {
15
+ container = where;
16
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The zoom-aware ruler (docs/design.md, Density): every box a popup or an
3
+ * overlay is placed by is measured with it.
4
+ *
5
+ * The density preference puts a CSS `zoom` on the root, for anything that
6
+ * positions a fixed overlay. getBoundingClientRect answers in real screen
7
+ * pixels, but a `top` set on a fixed element inside the zoomed root is
8
+ * scaled on its way back — measured with one ruler, placed with the
9
+ * other, a peek card landed a room away from its chip. Divide every
10
+ * measured coordinate (and window.inner*) by this before assigning it;
11
+ * offsetWidth/offsetHeight are already layout pixels and stay as read.
12
+ *
13
+ * The root is the page's own element unless a shell says otherwise: an
14
+ * embedded shell wears its zoom on its own `.tw-root` and names it with
15
+ * `measureFrom` (packages/mistri/src/embed/seams/shellHost.ts). The kit
16
+ * reads these through core.js, under the names it always had.
17
+ */
18
+ /** The element whose zoom the ruler divides by, for a shell that draws inside a page that is not its own. */
19
+ export declare function measureFrom(element: () => Element): void;
20
+ /** The zoom on the root. */
21
+ export declare function viewportScale(): number;
22
+ /** Called by the density preference after it moves the root zoom. */
23
+ export declare function refreshViewportScale(): void;
24
+ /** An element's box in layout pixels — getBoundingClientRect, un-zoomed. */
25
+ export declare function viewportBox(node: Element): {
26
+ top: number;
27
+ bottom: number;
28
+ left: number;
29
+ right: number;
30
+ width: number;
31
+ height: number;
32
+ };
33
+ /** The viewport in the same layout pixels the boxes above use. */
34
+ export declare function viewportSize(): {
35
+ width: number;
36
+ height: number;
37
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The zoom-aware ruler (docs/design.md, Density): every box a popup or an
3
+ * overlay is placed by is measured with it.
4
+ *
5
+ * The density preference puts a CSS `zoom` on the root, for anything that
6
+ * positions a fixed overlay. getBoundingClientRect answers in real screen
7
+ * pixels, but a `top` set on a fixed element inside the zoomed root is
8
+ * scaled on its way back — measured with one ruler, placed with the
9
+ * other, a peek card landed a room away from its chip. Divide every
10
+ * measured coordinate (and window.inner*) by this before assigning it;
11
+ * offsetWidth/offsetHeight are already layout pixels and stay as read.
12
+ *
13
+ * The root is the page's own element unless a shell says otherwise: an
14
+ * embedded shell wears its zoom on its own `.tw-root` and names it with
15
+ * `measureFrom` (packages/mistri/src/embed/seams/shellHost.ts). The kit
16
+ * reads these through core.js, under the names it always had.
17
+ */
18
+ let root = () => document.documentElement;
19
+ let zoomScale = null;
20
+ /** The element whose zoom the ruler divides by, for a shell that draws inside a page that is not its own. */
21
+ export function measureFrom(element) {
22
+ root = element;
23
+ zoomScale = null;
24
+ }
25
+ /** The zoom on the root. */
26
+ export function viewportScale() {
27
+ /* Cached: the zoom is written in exactly one place (the density
28
+ preference), but this is read per keystroke by the completion's caret
29
+ math and per repaint by the pinned-column offsets — a fresh
30
+ getComputedStyle on each read bought nothing. */
31
+ zoomScale ??= Number(getComputedStyle(root()).zoom) || 1;
32
+ return zoomScale;
33
+ }
34
+ /** Called by the density preference after it moves the root zoom. */
35
+ export function refreshViewportScale() {
36
+ zoomScale = null;
37
+ }
38
+ /** An element's box in layout pixels — getBoundingClientRect, un-zoomed. */
39
+ export function viewportBox(node) {
40
+ const z = viewportScale();
41
+ const r = node.getBoundingClientRect();
42
+ return {
43
+ top: r.top / z,
44
+ bottom: r.bottom / z,
45
+ left: r.left / z,
46
+ right: r.right / z,
47
+ width: r.width / z,
48
+ height: r.height / z,
49
+ };
50
+ }
51
+ /** The viewport in the same layout pixels the boxes above use. */
52
+ export function viewportSize() {
53
+ const z = viewportScale();
54
+ return { width: window.innerWidth / z, height: window.innerHeight / z };
55
+ }
@@ -0,0 +1,32 @@
1
+ export interface Mask {
2
+ format: (raw: string) => string;
3
+ parse: (typed: string) => string;
4
+ /** How many raw characters make a whole value, when there is one (a phone is 10 digits). */
5
+ length?: number;
6
+ placeholder?: string;
7
+ inputMode?: 'text' | 'numeric' | 'tel' | 'email';
8
+ }
9
+ export declare const digits: (s: string) => string;
10
+ /** A whole number with thousands separators (the locale is the caller's: en-US here, as the presets are). */
11
+ export declare const grouped: (n: number) => string;
12
+ export declare const masks: {
13
+ phone: Mask;
14
+ date: Mask;
15
+ month: Mask;
16
+ ssn: Mask;
17
+ zip: Mask;
18
+ digits: {
19
+ format: (raw: string) => string;
20
+ parse: (typed: string) => string;
21
+ placeholder?: string;
22
+ inputMode?: 'text' | 'numeric' | 'tel' | 'email';
23
+ length: undefined;
24
+ };
25
+ /** Whole money: `$2,500` for `symbol: '$'`, `25,000` for ''. */
26
+ money: (symbol?: string, max?: number) => Mask;
27
+ };
28
+ export type MaskName = 'phone' | 'date' | 'month' | 'ssn' | 'zip' | 'digits' | 'money' | 'grouped';
29
+ /** A mask by its name, for a spec that is data. */
30
+ export declare function maskNamed(name: MaskName | Mask): Mask;
31
+ /** A typed MM/DD/YYYY as YYYY-MM-DD, or '' when it is not a real day. */
32
+ export declare function isoDay(typed: string): string;
package/dist/masks.js ADDED
@@ -0,0 +1,62 @@
1
+ /* Input masks as a pair of functions. A field keeps the raw value (digits, usually); `format` draws it in the box and
2
+ `parse` reads what was typed back to raw, so a mask never has to know where the cursor is. A page may pass its own.
3
+ Preview: @mistriwork/ui is Preview at 1.0. */
4
+ export const digits = (s) => s.replace(/\D/g, '');
5
+ /** A whole number with thousands separators (the locale is the caller's: en-US here, as the presets are). */
6
+ export const grouped = (n) => n.toLocaleString('en-US');
7
+ /** Digits cut into groups: sizes [3,3,4] with '-' gives 678-678-6781. A partial value ends where its digits do. */
8
+ function grouping(sizes, sep, placeholder, inputMode = 'numeric') {
9
+ const max = sizes.reduce((a, b) => a + b, 0);
10
+ return {
11
+ parse: (typed) => digits(typed).slice(0, max),
12
+ format: (raw) => {
13
+ const parts = [];
14
+ let at = 0;
15
+ for (const size of sizes) {
16
+ if (at >= raw.length)
17
+ break;
18
+ parts.push(raw.slice(at, at + size));
19
+ at += size;
20
+ }
21
+ return parts.join(sep);
22
+ },
23
+ length: max,
24
+ placeholder,
25
+ inputMode,
26
+ };
27
+ }
28
+ export const masks = {
29
+ phone: grouping([3, 3, 4], '-', undefined, 'tel'),
30
+ date: grouping([2, 2, 4], '/', 'MM/DD/YYYY'),
31
+ month: grouping([2, 4], '/', 'MM/YYYY'),
32
+ ssn: grouping([3, 2, 4], '-'),
33
+ zip: grouping([5], '-'),
34
+ digits: { ...grouping([30], ''), length: undefined },
35
+ /** Whole money: `$2,500` for `symbol: '$'`, `25,000` for ''. */
36
+ money: (symbol = '$', max = 7) => ({
37
+ parse: (typed) => digits(typed).slice(0, max),
38
+ format: (raw) => (raw === '' ? '' : `${symbol}${grouped(Number(raw))}`),
39
+ inputMode: 'numeric',
40
+ }),
41
+ };
42
+ /** A mask by its name, for a spec that is data. */
43
+ export function maskNamed(name) {
44
+ if (typeof name !== 'string')
45
+ return name;
46
+ if (name === 'money')
47
+ return masks.money('$');
48
+ if (name === 'grouped')
49
+ return masks.money('');
50
+ return masks[name];
51
+ }
52
+ /** A typed MM/DD/YYYY as YYYY-MM-DD, or '' when it is not a real day. */
53
+ export function isoDay(typed) {
54
+ const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(typed);
55
+ if (!m)
56
+ return '';
57
+ const [, mm, dd, yyyy] = m;
58
+ const d = new Date(Date.UTC(Number(yyyy), Number(mm) - 1, Number(dd)));
59
+ if (d.getUTCFullYear() !== Number(yyyy) || d.getUTCMonth() !== Number(mm) - 1 || d.getUTCDate() !== Number(dd))
60
+ return '';
61
+ return `${yyyy}-${mm}-${dd}`;
62
+ }
@@ -0,0 +1,26 @@
1
+ /* @mistriwork/ui's answers to the reader's own settings: more contrast, and less transparency. Apart from forms.css, whose
2
+ sheet holds that nothing in it is !important: this one is, on purpose. A worn theme writes its colours on the root as
3
+ inline custom properties, which nothing but !important beats, and the reader's setting must beat the theme's. Loaded
4
+ beside the form controls (formControls.css) because the first stylesheet has no bytes to spare for it; a page that
5
+ draws no control keeps its themed ink and scrims. Moving it to tokens.css costs about 0.5 KB raw there. */
6
+ @media (prefers-contrast: more) {
7
+ :root {
8
+ /* Quiet ink and the hairlines toward the ink, the control's edge and the focus ring in it. */
9
+ --ink-muted: color-mix(in srgb, var(--ink) 85%, var(--surface)) !important;
10
+ --ink-subtle: var(--ink-muted) !important;
11
+ --line: color-mix(in srgb, var(--ink) 45%, var(--surface)) !important;
12
+ --line-strong: var(--ink-muted) !important;
13
+ --line-control: var(--ink) !important;
14
+ --field-edge: var(--ink) !important;
15
+ --focus: var(--ink) !important;
16
+ }
17
+ }
18
+ @media (prefers-reduced-transparency: reduce) {
19
+ /* The scrims behind a dialog and a drawer solid (the page is hidden, not dimmed), and the photo viewer's ground. */
20
+ :root {
21
+ --scrim: color-mix(in srgb, black 80%, var(--ground)) !important;
22
+ --scrim-light: color-mix(in srgb, black 45%, var(--ground)) !important;
23
+ --lightbox-ground: rgb(10 12 16) !important;
24
+ }
25
+ .modal-backdrop { background: var(--scrim-light); }
26
+ }