@orvain/utils 0.0.0-stage → 0.1.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 (51) hide show
  1. package/LICENSE +64 -0
  2. package/README.md +48 -2
  3. package/dist/a11y.d.cts +102 -0
  4. package/dist/a11y.d.ts +103 -0
  5. package/dist/breakpoints.d.cts +36 -0
  6. package/dist/breakpoints.d.ts +37 -0
  7. package/dist/calendar.d.cts +127 -0
  8. package/dist/calendar.d.ts +128 -0
  9. package/dist/chart/color.d.cts +72 -0
  10. package/dist/chart/color.d.ts +73 -0
  11. package/dist/chart/index.d.cts +2 -0
  12. package/dist/chart/index.d.ts +3 -0
  13. package/dist/chart/math.d.cts +66 -0
  14. package/dist/chart/math.d.ts +67 -0
  15. package/dist/chart-items/index.d.cts +64 -0
  16. package/dist/chart-items/index.d.ts +65 -0
  17. package/dist/codeTokenizer.d.cts +29 -0
  18. package/dist/codeTokenizer.d.ts +30 -0
  19. package/dist/cssModule.d.cts +12 -0
  20. package/dist/cssModule.d.ts +13 -0
  21. package/dist/dismiss.d.cts +40 -0
  22. package/dist/dismiss.d.ts +41 -0
  23. package/dist/floating.d.cts +137 -0
  24. package/dist/floating.d.ts +138 -0
  25. package/dist/formField.d.cts +58 -0
  26. package/dist/formField.d.ts +59 -0
  27. package/dist/helpers.d.cts +17 -0
  28. package/dist/helpers.d.ts +18 -0
  29. package/dist/index.cjs +11 -0
  30. package/dist/index.d.cts +17 -0
  31. package/dist/index.d.ts +18 -0
  32. package/dist/index.js +2819 -0
  33. package/dist/kanbanDnd.d.cts +71 -0
  34. package/dist/kanbanDnd.d.ts +72 -0
  35. package/dist/listNavigation.d.cts +36 -0
  36. package/dist/listNavigation.d.ts +37 -0
  37. package/dist/richText/blockquote.d.cts +11 -0
  38. package/dist/richText/blockquote.d.ts +12 -0
  39. package/dist/richText/editor.d.cts +117 -0
  40. package/dist/richText/editor.d.ts +118 -0
  41. package/dist/richText/index.d.cts +3 -0
  42. package/dist/richText/index.d.ts +4 -0
  43. package/dist/richText/model.d.cts +161 -0
  44. package/dist/richText/model.d.ts +162 -0
  45. package/dist/slider.d.cts +18 -0
  46. package/dist/slider.d.ts +19 -0
  47. package/dist/tree.d.cts +78 -0
  48. package/dist/tree.d.ts +79 -0
  49. package/dist/types.d.cts +22 -0
  50. package/dist/types.d.ts +23 -0
  51. package/package.json +56 -3
package/LICENSE ADDED
@@ -0,0 +1,64 @@
1
+ Orvain UI Free Components Licence
2
+ Version 1.0
3
+
4
+ Copyright (c) 2026 Oussema Ajmi, trading as Orvain UI ("Orvain UI", "we").
5
+
6
+ This licence covers @orvain/react, @orvain/vue, @orvain/tokens, @orvain/utils,
7
+ @orvain/icons and @orvain/assets: their source code, compiled files, design
8
+ tokens, icons and documentation (the "Software").
9
+
10
+ 1. What you may do
11
+ You may install and use the Software free of charge, in personal and
12
+ commercial projects, including client work and products you sell. You may
13
+ modify it for your own projects. The products you build with it are yours.
14
+
15
+ 2. What you may not do
16
+ - Sell, rent, sublicense or give away the Software, or any part of it, on its
17
+ own, whether modified or not.
18
+ - Publish the Software, or a modified copy, as your own UI kit, component
19
+ library, template or package.
20
+ - Use it in a product whose main purpose is to let other people build designs
21
+ or interfaces with it.
22
+ - Remove or change the copyright notices in it.
23
+
24
+ 3. Paid products
25
+ The Software is the free part of Orvain UI. The Orvain Library (more
26
+ components and blocks, the MCP server and the CLI) and the Orvain Figma
27
+ library are separate products under the Orvain UI Licence at
28
+ https://orvain.dev/license. This licence gives you no rights to them.
29
+
30
+ 4. Third-party material
31
+ The Software includes material that belongs to others and has its own terms:
32
+ - Country flags are derived from circle-flags (MIT). See NOTICE.
33
+ - Brand names and logos in @orvain/assets belong to their owners. They are
34
+ included for illustration. You receive no rights to them.
35
+ - The components are designed for the Satoshi typeface. It is not included in
36
+ the Software. It is licensed by its foundry (Fontshare), not by Orvain UI,
37
+ and your use of it is governed by the foundry's licence.
38
+
39
+ 5. Ownership
40
+ Orvain UI keeps ownership of the Software and the Orvain UI name and brand.
41
+ This licence gives you the right to use the Software. It does not transfer
42
+ ownership.
43
+
44
+ 6. No warranty
45
+ The Software is provided "as is", without warranty of any kind, express or
46
+ implied, including fitness for a particular purpose and non-infringement.
47
+
48
+ 7. Limit of liability
49
+ To the extent the law allows, Orvain UI is not liable for any claim, damages
50
+ or other liability arising from the Software or its use, including indirect
51
+ or consequential loss. Nothing in this licence limits liability that the law
52
+ does not allow to be limited.
53
+
54
+ 8. Ending
55
+ This licence ends automatically if you break it. You must then stop using the
56
+ Software and delete your copies. Products you have already delivered may stay
57
+ in use.
58
+
59
+ 9. Governing law
60
+ Tunisian law applies. Mandatory consumer protections in your own country still
61
+ apply.
62
+
63
+ 10. Contact
64
+ Questions about permitted use: hello@contactoussema.uk
package/README.md CHANGED
@@ -1,3 +1,49 @@
1
- # Temporary Holding Version
1
+ # @orvain/utils
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Small shared helpers and types used across the Orvain Design System packages.
4
+
5
+ > Commercial software. See [LICENSE](./LICENSE) — use is governed by your
6
+ > agreement with Orvain Labs.
7
+
8
+ This is an internal support package, published because `@orvain/react` and
9
+ `@orvain/vue` depend on it. It is not a general-purpose utility library and its
10
+ surface may change between minors. Prefer a dedicated library for anything you
11
+ need in application code.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @orvain/utils
17
+ ```
18
+
19
+ ## Exports
20
+
21
+ ```ts
22
+ import { clamp, cx, omit, typedEntries, generateId, announce, trapFocus } from '@orvain/utils';
23
+ ```
24
+
25
+ | Export | Signature | Notes |
26
+ |---|---|---|
27
+ | `clamp` | `(value, min, max) => number` | inclusive |
28
+ | `cx` | `(...classes) => string` | joins truthy class strings |
29
+ | `omit` | `(obj, keys) => Omit<T, K>` | for prop forwarding |
30
+ | `typedEntries` | `(obj) => [keyof T, T[keyof T]][]` | `Object.entries` with inferred keys |
31
+ | `generateId` | `(prefix?) => string` | module-scoped counter — see *Caveats* |
32
+ | `getFocusableElements` | `(container) => HTMLElement[]` | |
33
+ | `trapFocus` | `(container) => cleanup` | cycles Tab within `container` |
34
+ | `announce` | `(message, politeness?) => void` | writes to a shared `aria-live` region |
35
+ | `Size`, `Variant`, `Intent`, `ColorScheme` | type aliases | shared prop vocabularies |
36
+
37
+ ## Caveats
38
+
39
+ - **`generateId` is not SSR-safe.** It increments a module-level counter, so
40
+ server and client can disagree and hydration will mismatch. Use React's
41
+ `useId` or Vue's `useId` instead; `generateId` is for non-component code.
42
+ - **`trapFocus` does not restore focus** when it tears down, and does not move
43
+ focus into the container on setup. You need to handle both yourself.
44
+ - **`announce` and `trapFocus` touch `document`** and must only run in the
45
+ browser.
46
+
47
+ ## Related packages
48
+
49
+ - [`@orvain/react`](../react), [`@orvain/vue`](../vue) — the component libraries
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Generates a stable, unique ID for associating form labels with inputs.
3
+ * In React 18+ prefer useId(); use this in Vue or outside React.
4
+ */
5
+ export declare function generateId(prefix?: string): string;
6
+ export declare function getFocusableElements(container: HTMLElement): HTMLElement[];
7
+ /** Where a dialog puts focus when it opens: an element, or a CSS selector resolved inside the dialog. */
8
+ export type DialogInitialFocus = HTMLElement | string | null | undefined;
9
+ /**
10
+ * Moves focus into a dialog that has just opened, per the APG dialog pattern:
11
+ *
12
+ * 1. `initialFocus`, when given;
13
+ * 2. an element marked `data-autofocus` (an alert dialog's least destructive
14
+ * action, e.g. ConfirmModal's Cancel);
15
+ * 3. the first form field;
16
+ * 4. the first other control, skipping the close (×) button, which is the
17
+ * one action a user rarely wants first;
18
+ * 5. the dialog itself (made programmatically focusable), so a screen reader
19
+ * starts at its name.
20
+ *
21
+ * Returns the element that received focus.
22
+ */
23
+ export declare function focusDialogInitial(dialog: HTMLElement, initialFocus?: DialogInitialFocus): HTMLElement;
24
+ /**
25
+ * Traps keyboard focus within `container`.
26
+ * Returns a cleanup function to remove the listener.
27
+ */
28
+ export declare function trapFocus(container: HTMLElement): () => void;
29
+ /** Announce a message to screen readers via an aria-live region. */
30
+ export declare function announce(message: string, politeness?: 'polite' | 'assertive'): void;
31
+ /** Selector for the enabled items of one `role="menu"` list. */
32
+ export declare const MENU_ITEM_SELECTOR = "[role=\"menuitem\"]:not([aria-disabled=\"true\"]):not(:disabled), [role=\"menuitemcheckbox\"]:not([aria-disabled=\"true\"]):not(:disabled), [role=\"menuitemradio\"]:not([aria-disabled=\"true\"]):not(:disabled)";
33
+ /** The items that belong to `menu` itself (items of nested menus are skipped). */
34
+ export declare function getMenuItems(menu: HTMLElement): HTMLElement[];
35
+ /** Roving tabindex: `target` (or the current stop, or the first item) gets 0, the rest −1. */
36
+ export declare function setMenuRovingItem(menu: HTMLElement, target?: HTMLElement | null): HTMLElement | undefined;
37
+ /** Move focus to an item of `menu` (index wraps; `'first'` / `'last'` accepted). */
38
+ export declare function focusMenuItem(menu: HTMLElement, index: number | 'first' | 'last'): HTMLElement | undefined;
39
+ /**
40
+ * Handles the APG menu keys inside one `role="menu"`: ArrowDown / ArrowUp
41
+ * (wrapping), Home / End, and single-character typeahead on the item text
42
+ * (or `aria-label`). Returns true when the key was consumed. Submenu keys
43
+ * (ArrowRight / ArrowLeft / Enter / Escape) are left to the caller.
44
+ */
45
+ export declare function handleMenuKeydown(event: KeyboardEvent, menu: HTMLElement): boolean;
46
+ export interface SubmenuHoverIntentOptions {
47
+ /** Rect of the open submenu panel, or null when none is open. */
48
+ getSubmenuRect: () => {
49
+ left: number;
50
+ right: number;
51
+ top: number;
52
+ bottom: number;
53
+ } | null;
54
+ /** Called with the row key to activate (null = a row without a submenu). */
55
+ onActivate: (key: string | null) => void;
56
+ /** Delay before the hovered row takes over (ms). */
57
+ delay?: number;
58
+ /** Longer delay while the pointer is heading for the open submenu (ms). */
59
+ aimDelay?: number;
60
+ }
61
+ export interface SubmenuHoverIntent {
62
+ /** Pointer moved over the menu (records the path). */
63
+ move: (event: {
64
+ clientX: number;
65
+ clientY: number;
66
+ }) => void;
67
+ /** Pointer entered a row (`key` = row with a submenu, null = plain row). */
68
+ enter: (key: string | null) => void;
69
+ /** Pointer reached the submenu: cancel any pending switch. */
70
+ enterSubmenu: () => void;
71
+ /** Cancel pending timers (unmount). */
72
+ cancel: () => void;
73
+ }
74
+ /**
75
+ * Hover-to-open for side submenus. A row takes over after `delay`; while the
76
+ * pointer moves inside the triangle between where it was and the near edge of
77
+ * the open submenu (it is "aiming" at it), the switch waits for `aimDelay` and
78
+ * re-checks, so crossing other rows on the way does not close the submenu.
79
+ * Framework-agnostic: React and Vue feed it pointer events.
80
+ */
81
+ export declare function createSubmenuHoverIntent({ getSubmenuRect, onActivate, delay, aimDelay, }: SubmenuHoverIntentOptions): SubmenuHoverIntent;
82
+ /**
83
+ * Sideways-scrolling strip with a hidden scrollbar: a vertical mouse wheel
84
+ * scrolls it horizontally, and `data-scroll-end` is set while it cannot
85
+ * scroll further right (so a right-edge fade can drop away). Returns cleanup.
86
+ */
87
+ export declare function bindHorizontalStrip(strip: HTMLElement): () => void;
88
+ /**
89
+ * Marks which ends of a sideways-scrolling element still hide content:
90
+ * `data-more-start` / `data-more-end` are set while it can scroll further
91
+ * that way (direction-aware, so they follow `dir="rtl"`), and
92
+ * `--scrollbar-size` holds the height of its scrollbar (0px for overlay
93
+ * scrollbars), so an edge fade can leave the scrollbar alone. Returns cleanup.
94
+ */
95
+ export declare function bindScrollEdges(el: HTMLElement): () => void;
96
+ /**
97
+ * Arrow-key movement between the columns of a panel of links (e.g. a mega
98
+ * menu). `columns` are the column containers in reading order; Up / Down move
99
+ * within the focused column, Left / Right jump to the neighbouring column's
100
+ * link nearest in height. Tab is untouched. Returns true when it moved focus.
101
+ */
102
+ export declare function handleColumnArrowKeys(event: KeyboardEvent, columns: HTMLElement[]): boolean;
package/dist/a11y.d.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Generates a stable, unique ID for associating form labels with inputs.
3
+ * In React 18+ prefer useId(); use this in Vue or outside React.
4
+ */
5
+ export declare function generateId(prefix?: string): string;
6
+ export declare function getFocusableElements(container: HTMLElement): HTMLElement[];
7
+ /** Where a dialog puts focus when it opens: an element, or a CSS selector resolved inside the dialog. */
8
+ export type DialogInitialFocus = HTMLElement | string | null | undefined;
9
+ /**
10
+ * Moves focus into a dialog that has just opened, per the APG dialog pattern:
11
+ *
12
+ * 1. `initialFocus`, when given;
13
+ * 2. an element marked `data-autofocus` (an alert dialog's least destructive
14
+ * action, e.g. ConfirmModal's Cancel);
15
+ * 3. the first form field;
16
+ * 4. the first other control, skipping the close (×) button, which is the
17
+ * one action a user rarely wants first;
18
+ * 5. the dialog itself (made programmatically focusable), so a screen reader
19
+ * starts at its name.
20
+ *
21
+ * Returns the element that received focus.
22
+ */
23
+ export declare function focusDialogInitial(dialog: HTMLElement, initialFocus?: DialogInitialFocus): HTMLElement;
24
+ /**
25
+ * Traps keyboard focus within `container`.
26
+ * Returns a cleanup function to remove the listener.
27
+ */
28
+ export declare function trapFocus(container: HTMLElement): () => void;
29
+ /** Announce a message to screen readers via an aria-live region. */
30
+ export declare function announce(message: string, politeness?: 'polite' | 'assertive'): void;
31
+ /** Selector for the enabled items of one `role="menu"` list. */
32
+ export declare const MENU_ITEM_SELECTOR = "[role=\"menuitem\"]:not([aria-disabled=\"true\"]):not(:disabled), [role=\"menuitemcheckbox\"]:not([aria-disabled=\"true\"]):not(:disabled), [role=\"menuitemradio\"]:not([aria-disabled=\"true\"]):not(:disabled)";
33
+ /** The items that belong to `menu` itself (items of nested menus are skipped). */
34
+ export declare function getMenuItems(menu: HTMLElement): HTMLElement[];
35
+ /** Roving tabindex: `target` (or the current stop, or the first item) gets 0, the rest −1. */
36
+ export declare function setMenuRovingItem(menu: HTMLElement, target?: HTMLElement | null): HTMLElement | undefined;
37
+ /** Move focus to an item of `menu` (index wraps; `'first'` / `'last'` accepted). */
38
+ export declare function focusMenuItem(menu: HTMLElement, index: number | 'first' | 'last'): HTMLElement | undefined;
39
+ /**
40
+ * Handles the APG menu keys inside one `role="menu"`: ArrowDown / ArrowUp
41
+ * (wrapping), Home / End, and single-character typeahead on the item text
42
+ * (or `aria-label`). Returns true when the key was consumed. Submenu keys
43
+ * (ArrowRight / ArrowLeft / Enter / Escape) are left to the caller.
44
+ */
45
+ export declare function handleMenuKeydown(event: KeyboardEvent, menu: HTMLElement): boolean;
46
+ export interface SubmenuHoverIntentOptions {
47
+ /** Rect of the open submenu panel, or null when none is open. */
48
+ getSubmenuRect: () => {
49
+ left: number;
50
+ right: number;
51
+ top: number;
52
+ bottom: number;
53
+ } | null;
54
+ /** Called with the row key to activate (null = a row without a submenu). */
55
+ onActivate: (key: string | null) => void;
56
+ /** Delay before the hovered row takes over (ms). */
57
+ delay?: number;
58
+ /** Longer delay while the pointer is heading for the open submenu (ms). */
59
+ aimDelay?: number;
60
+ }
61
+ export interface SubmenuHoverIntent {
62
+ /** Pointer moved over the menu (records the path). */
63
+ move: (event: {
64
+ clientX: number;
65
+ clientY: number;
66
+ }) => void;
67
+ /** Pointer entered a row (`key` = row with a submenu, null = plain row). */
68
+ enter: (key: string | null) => void;
69
+ /** Pointer reached the submenu: cancel any pending switch. */
70
+ enterSubmenu: () => void;
71
+ /** Cancel pending timers (unmount). */
72
+ cancel: () => void;
73
+ }
74
+ /**
75
+ * Hover-to-open for side submenus. A row takes over after `delay`; while the
76
+ * pointer moves inside the triangle between where it was and the near edge of
77
+ * the open submenu (it is "aiming" at it), the switch waits for `aimDelay` and
78
+ * re-checks, so crossing other rows on the way does not close the submenu.
79
+ * Framework-agnostic: React and Vue feed it pointer events.
80
+ */
81
+ export declare function createSubmenuHoverIntent({ getSubmenuRect, onActivate, delay, aimDelay, }: SubmenuHoverIntentOptions): SubmenuHoverIntent;
82
+ /**
83
+ * Sideways-scrolling strip with a hidden scrollbar: a vertical mouse wheel
84
+ * scrolls it horizontally, and `data-scroll-end` is set while it cannot
85
+ * scroll further right (so a right-edge fade can drop away). Returns cleanup.
86
+ */
87
+ export declare function bindHorizontalStrip(strip: HTMLElement): () => void;
88
+ /**
89
+ * Marks which ends of a sideways-scrolling element still hide content:
90
+ * `data-more-start` / `data-more-end` are set while it can scroll further
91
+ * that way (direction-aware, so they follow `dir="rtl"`), and
92
+ * `--scrollbar-size` holds the height of its scrollbar (0px for overlay
93
+ * scrollbars), so an edge fade can leave the scrollbar alone. Returns cleanup.
94
+ */
95
+ export declare function bindScrollEdges(el: HTMLElement): () => void;
96
+ /**
97
+ * Arrow-key movement between the columns of a panel of links (e.g. a mega
98
+ * menu). `columns` are the column containers in reading order; Up / Down move
99
+ * within the focused column, Left / Right jump to the neighbouring column's
100
+ * link nearest in height. Tab is untouched. Returns true when it moved focus.
101
+ */
102
+ export declare function handleColumnArrowKeys(event: KeyboardEvent, columns: HTMLElement[]): boolean;
103
+ //# sourceMappingURL=a11y.d.ts.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Figma "Responsive Tokens" breakpoints for JS (matchMedia, resize logic).
3
+ * Same values as `breakpoint.*` in `@orvain/tokens` (src/breakpoints.json):
4
+ * mobile 320–767, tablet 768–1023, desktop ≥ 1024. CSS Modules use literal
5
+ * media queries with a token comment instead (see docs/responsive.md).
6
+ */
7
+ export declare const BREAKPOINTS: {
8
+ readonly mobile: {
9
+ readonly min: 320;
10
+ readonly max: 767;
11
+ };
12
+ readonly tablet: {
13
+ readonly min: 768;
14
+ readonly max: 1023;
15
+ };
16
+ readonly desktop: {
17
+ readonly min: 1024;
18
+ readonly max: 3200;
19
+ };
20
+ };
21
+ export type Breakpoint = keyof typeof BREAKPOINTS;
22
+ /** Media query strings matching the CSS convention (mobile-first). */
23
+ export declare const MEDIA: {
24
+ /** below tablet: 0–767 */
25
+ readonly mobile: "(max-width: 767px)";
26
+ /** tablet only: 768–1023 */
27
+ readonly tablet: "(min-width: 768px) and (max-width: 1023px)";
28
+ /** tablet and up: ≥ 768 */
29
+ readonly tabletUp: "(min-width: 768px)";
30
+ /** below desktop: 0–1023 */
31
+ readonly belowDesktop: "(max-width: 1023px)";
32
+ /** desktop: ≥ 1024 */
33
+ readonly desktop: "(min-width: 1024px)";
34
+ };
35
+ /** Which Figma breakpoint a viewport width falls in. */
36
+ export declare function breakpointFor(width: number): Breakpoint;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Figma "Responsive Tokens" breakpoints for JS (matchMedia, resize logic).
3
+ * Same values as `breakpoint.*` in `@orvain/tokens` (src/breakpoints.json):
4
+ * mobile 320–767, tablet 768–1023, desktop ≥ 1024. CSS Modules use literal
5
+ * media queries with a token comment instead (see docs/responsive.md).
6
+ */
7
+ export declare const BREAKPOINTS: {
8
+ readonly mobile: {
9
+ readonly min: 320;
10
+ readonly max: 767;
11
+ };
12
+ readonly tablet: {
13
+ readonly min: 768;
14
+ readonly max: 1023;
15
+ };
16
+ readonly desktop: {
17
+ readonly min: 1024;
18
+ readonly max: 3200;
19
+ };
20
+ };
21
+ export type Breakpoint = keyof typeof BREAKPOINTS;
22
+ /** Media query strings matching the CSS convention (mobile-first). */
23
+ export declare const MEDIA: {
24
+ /** below tablet: 0–767 */
25
+ readonly mobile: "(max-width: 767px)";
26
+ /** tablet only: 768–1023 */
27
+ readonly tablet: "(min-width: 768px) and (max-width: 1023px)";
28
+ /** tablet and up: ≥ 768 */
29
+ readonly tabletUp: "(min-width: 768px)";
30
+ /** below desktop: 0–1023 */
31
+ readonly belowDesktop: "(max-width: 1023px)";
32
+ /** desktop: ≥ 1024 */
33
+ readonly desktop: "(min-width: 1024px)";
34
+ };
35
+ /** Which Figma breakpoint a viewport width falls in. */
36
+ export declare function breakpointFor(width: number): Breakpoint;
37
+ //# sourceMappingURL=breakpoints.d.ts.map
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Calendar (scheduler) helpers, shared by the React and Vue `Calendar`.
3
+ *
4
+ * Calendar, DatePicker and RangePicker share this day/month arithmetic
5
+ * (local time, weeks starting on `weekStartsOn`), so they agree on which
6
+ * days a month grid shows.
7
+ */
8
+ export type CalendarView = 'day' | 'week' | 'month';
9
+ export type CalendarWeekStart = 0 | 1 | 2 | 3 | 4 | 5 | 6;
10
+ /** Figma "Application" axis of the Calendar Items sets. */
11
+ export type CalendarApplication = 'neutral' | 'brand' | 'purple' | 'success' | 'warning' | 'error';
12
+ /** One scheduled event. `start`/`end` are local `Date`s; `end` is exclusive. */
13
+ export interface CalendarEventData {
14
+ id: string;
15
+ title: string;
16
+ /** Shorter title for narrow chips (e.g. "Standup" for "Engineering standup"). Month chips always use it. */
17
+ shortTitle?: string;
18
+ start: Date;
19
+ end: Date;
20
+ /** Colour family (Figma "Application"). Default `brand`. */
21
+ application?: CalendarApplication;
22
+ /** Second line (Figma "Sub Title"). */
23
+ description?: string;
24
+ /** Short trailing label (Figma "Caption", e.g. "Cap."). */
25
+ caption?: string;
26
+ /** Location tag + map (Calendar Items v1 Type=v4). */
27
+ location?: string;
28
+ /** Image behind the location tag (Calendar Items v1 Type=v4). */
29
+ imageSrc?: string;
30
+ /** Glyph of the event's icon container: an `@orvain/icons` name or icon
31
+ * definition (`IconSource`; typed loosely here because utils does not depend
32
+ * on the icon package). Omitted, the chip keeps Figma's default `car-01`. */
33
+ icon?: string | object;
34
+ disabled?: boolean;
35
+ }
36
+ /** A timed event placed on the day/week grid. Units: hours and day columns. */
37
+ export interface CalendarPlacedEvent {
38
+ event: CalendarEventData;
39
+ /** First day column (0-based) the event covers in the visible range. */
40
+ dayIndex: number;
41
+ /** Number of day columns it spans (≥ 1). */
42
+ daySpan: number;
43
+ /** Start, in hours from the grid's `startHour` (clamped to the grid). */
44
+ top: number;
45
+ /** Length in hours (clamped to the grid). */
46
+ height: number;
47
+ /** Lane among events that overlap it (0-based) and total lanes in its cluster. */
48
+ lane: number;
49
+ lanes: number;
50
+ }
51
+ export declare function startOfDay(date: Date): Date;
52
+ export declare function addDays(date: Date, delta: number): Date;
53
+ export declare function startOfMonth(date: Date): Date;
54
+ /** First day of the month `delta` months away (month navigation). */
55
+ export declare function shiftMonth(date: Date, delta: number): Date;
56
+ /** Number of days in `date`'s month. */
57
+ export declare function daysInMonth(date: Date): number;
58
+ /** Same day-of-month `delta` months away, clamped to that month's last day (Jan 31 + 1 → Feb 28/29). */
59
+ export declare function addMonthsClamped(date: Date, delta: number): Date;
60
+ export declare function isSameDay(a: Date, b: Date): boolean;
61
+ export declare function isSameMonth(a: Date, b: Date): boolean;
62
+ export declare function startOfWeek(date: Date, weekStartsOn?: CalendarWeekStart): Date;
63
+ /** Local midnight of `date` in epoch ms: a numeric day key that also orders days. */
64
+ export declare function dayStamp(date: Date): number;
65
+ /** `a` is an earlier calendar day than `b` (times of day ignored). */
66
+ export declare function isBeforeDay(a: Date, b: Date): boolean;
67
+ /** `date` falls before `min` or after `max` (whole days; either bound optional). */
68
+ export declare function isDayOutOfRange(date: Date, min?: Date | null, max?: Date | null): boolean;
69
+ /** `date` moved inside `[min, max]` (whole days; a clamped result is that bound's midnight). */
70
+ export declare function clampDay(date: Date, min?: Date | null, max?: Date | null): Date;
71
+ /** Stable `YYYY-M-D` key for a local day. */
72
+ export declare function dayKey(date: Date): string;
73
+ /** Days shown by a view: 1 (day), 7 (week) or the full weeks of the month grid. */
74
+ export declare function visibleDays(date: Date, view: CalendarView, weekStartsOn?: CalendarWeekStart): Date[];
75
+ /**
76
+ * Month grid as weeks of 7 days, leading/trailing days from the adjacent
77
+ * months included. `fixedWeeks` always returns 6 weeks, so a picker keeps
78
+ * its height from month to month.
79
+ */
80
+ export declare function buildMonthGrid(month: Date, weekStartsOn?: CalendarWeekStart, fixedWeeks?: boolean): Date[][];
81
+ /** One day of a month grid. */
82
+ export interface MonthGridCell {
83
+ date: Date;
84
+ /** Day of the month (1–31). */
85
+ day: number;
86
+ /** In the grid's own month (not a leading or trailing day). */
87
+ inMonth: boolean;
88
+ /** A day of the following month (after the last day). */
89
+ trailing: boolean;
90
+ }
91
+ /** `buildMonthGrid` with each day's place in the month, for pickers that style leading and trailing days. */
92
+ export declare function monthGridCells(month: Date, weekStartsOn?: CalendarWeekStart, fixedWeeks?: boolean): MonthGridCell[][];
93
+ /** Moves `date` one period of `view` forward (`delta` = 1) or back (-1). */
94
+ export declare function shiftCalendarDate(date: Date, view: CalendarView, delta: number): Date;
95
+ /** Weekday labels starting on `weekStartsOn` (e.g. "Sun", "Mon"…). */
96
+ export declare function weekdayLabels(weekStartsOn?: CalendarWeekStart, locale?: string, weekday?: 'short' | 'long'): string[];
97
+ /** "February 2026": month and year, first letter capitalised (fr-FR writes "février 2026"). */
98
+ export declare function formatMonthLabel(date: Date, locale?: string): string;
99
+ /** "Aug 03, 2025": a range endpoint in pickers and their fields. */
100
+ export declare function formatShortDate(date: Date, locale?: string): string;
101
+ /** "Aug 03, 2025 – Aug 09, 2025", one endpoint alone, or `empty` when neither is set. */
102
+ export declare function formatDateRange(start: Date | null | undefined, end: Date | null | undefined, locale?: string, empty?: string): string;
103
+ /** Nav bar title per view: "April 26, 2026" · "Apr 20 - 26, 2026" · "April 2026". */
104
+ export declare function formatCalendarTitle(date: Date, view: CalendarView, locale?: string, weekStartsOn?: CalendarWeekStart): string;
105
+ /** Hour-gutter label: "7am", "12pm" (Figma Time Item text). */
106
+ export declare function formatHourLabel(hour: number, locale?: string): string;
107
+ /** Clock label: "11:32 AM". */
108
+ export declare function formatTimeLabel(date: Date, locale?: string): string;
109
+ /** "9:00 AM – 10:30 AM" for an event (same-day), with dates when it spans days. */
110
+ export declare function formatEventTimeRange(event: Pick<CalendarEventData, 'start' | 'end'>, locale?: string): string;
111
+ /** Events that touch the local day `day` (end exclusive), in start order. */
112
+ export declare function eventsOnDay(events: readonly CalendarEventData[], day: Date): CalendarEventData[];
113
+ /**
114
+ * Places timed events on a day/week grid of `days`, from `startHour` to
115
+ * `endHour`. An event that crosses midnight inside the range spans several
116
+ * day columns (one chip, as the Figma weekly view draws it) and keeps its
117
+ * start time as its row. Overlapping events (same columns and times) share
118
+ * the width in lanes.
119
+ */
120
+ export declare function layoutTimedEvents(events: readonly CalendarEventData[], days: readonly Date[], startHour?: number, endHour?: number, minHours?: number): CalendarPlacedEvent[];
121
+ /**
122
+ * Day view rows: events whose times overlap the first event of a row share that row
123
+ * (side by side, anchored at the row's start); the rest start new rows. Input in any order.
124
+ */
125
+ export declare function groupEventRows(placed: readonly CalendarPlacedEvent[]): CalendarPlacedEvent[][];
126
+ /** True when `now` falls inside the period `view` shows around `date` (day, week or month). */
127
+ export declare function isCurrentPeriod(date: Date, now: Date, view: CalendarView, weekStartsOn?: CalendarWeekStart): boolean;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Calendar (scheduler) helpers, shared by the React and Vue `Calendar`.
3
+ *
4
+ * Calendar, DatePicker and RangePicker share this day/month arithmetic
5
+ * (local time, weeks starting on `weekStartsOn`), so they agree on which
6
+ * days a month grid shows.
7
+ */
8
+ export type CalendarView = 'day' | 'week' | 'month';
9
+ export type CalendarWeekStart = 0 | 1 | 2 | 3 | 4 | 5 | 6;
10
+ /** Figma "Application" axis of the Calendar Items sets. */
11
+ export type CalendarApplication = 'neutral' | 'brand' | 'purple' | 'success' | 'warning' | 'error';
12
+ /** One scheduled event. `start`/`end` are local `Date`s; `end` is exclusive. */
13
+ export interface CalendarEventData {
14
+ id: string;
15
+ title: string;
16
+ /** Shorter title for narrow chips (e.g. "Standup" for "Engineering standup"). Month chips always use it. */
17
+ shortTitle?: string;
18
+ start: Date;
19
+ end: Date;
20
+ /** Colour family (Figma "Application"). Default `brand`. */
21
+ application?: CalendarApplication;
22
+ /** Second line (Figma "Sub Title"). */
23
+ description?: string;
24
+ /** Short trailing label (Figma "Caption", e.g. "Cap."). */
25
+ caption?: string;
26
+ /** Location tag + map (Calendar Items v1 Type=v4). */
27
+ location?: string;
28
+ /** Image behind the location tag (Calendar Items v1 Type=v4). */
29
+ imageSrc?: string;
30
+ /** Glyph of the event's icon container: an `@orvain/icons` name or icon
31
+ * definition (`IconSource`; typed loosely here because utils does not depend
32
+ * on the icon package). Omitted, the chip keeps Figma's default `car-01`. */
33
+ icon?: string | object;
34
+ disabled?: boolean;
35
+ }
36
+ /** A timed event placed on the day/week grid. Units: hours and day columns. */
37
+ export interface CalendarPlacedEvent {
38
+ event: CalendarEventData;
39
+ /** First day column (0-based) the event covers in the visible range. */
40
+ dayIndex: number;
41
+ /** Number of day columns it spans (≥ 1). */
42
+ daySpan: number;
43
+ /** Start, in hours from the grid's `startHour` (clamped to the grid). */
44
+ top: number;
45
+ /** Length in hours (clamped to the grid). */
46
+ height: number;
47
+ /** Lane among events that overlap it (0-based) and total lanes in its cluster. */
48
+ lane: number;
49
+ lanes: number;
50
+ }
51
+ export declare function startOfDay(date: Date): Date;
52
+ export declare function addDays(date: Date, delta: number): Date;
53
+ export declare function startOfMonth(date: Date): Date;
54
+ /** First day of the month `delta` months away (month navigation). */
55
+ export declare function shiftMonth(date: Date, delta: number): Date;
56
+ /** Number of days in `date`'s month. */
57
+ export declare function daysInMonth(date: Date): number;
58
+ /** Same day-of-month `delta` months away, clamped to that month's last day (Jan 31 + 1 → Feb 28/29). */
59
+ export declare function addMonthsClamped(date: Date, delta: number): Date;
60
+ export declare function isSameDay(a: Date, b: Date): boolean;
61
+ export declare function isSameMonth(a: Date, b: Date): boolean;
62
+ export declare function startOfWeek(date: Date, weekStartsOn?: CalendarWeekStart): Date;
63
+ /** Local midnight of `date` in epoch ms: a numeric day key that also orders days. */
64
+ export declare function dayStamp(date: Date): number;
65
+ /** `a` is an earlier calendar day than `b` (times of day ignored). */
66
+ export declare function isBeforeDay(a: Date, b: Date): boolean;
67
+ /** `date` falls before `min` or after `max` (whole days; either bound optional). */
68
+ export declare function isDayOutOfRange(date: Date, min?: Date | null, max?: Date | null): boolean;
69
+ /** `date` moved inside `[min, max]` (whole days; a clamped result is that bound's midnight). */
70
+ export declare function clampDay(date: Date, min?: Date | null, max?: Date | null): Date;
71
+ /** Stable `YYYY-M-D` key for a local day. */
72
+ export declare function dayKey(date: Date): string;
73
+ /** Days shown by a view: 1 (day), 7 (week) or the full weeks of the month grid. */
74
+ export declare function visibleDays(date: Date, view: CalendarView, weekStartsOn?: CalendarWeekStart): Date[];
75
+ /**
76
+ * Month grid as weeks of 7 days, leading/trailing days from the adjacent
77
+ * months included. `fixedWeeks` always returns 6 weeks, so a picker keeps
78
+ * its height from month to month.
79
+ */
80
+ export declare function buildMonthGrid(month: Date, weekStartsOn?: CalendarWeekStart, fixedWeeks?: boolean): Date[][];
81
+ /** One day of a month grid. */
82
+ export interface MonthGridCell {
83
+ date: Date;
84
+ /** Day of the month (1–31). */
85
+ day: number;
86
+ /** In the grid's own month (not a leading or trailing day). */
87
+ inMonth: boolean;
88
+ /** A day of the following month (after the last day). */
89
+ trailing: boolean;
90
+ }
91
+ /** `buildMonthGrid` with each day's place in the month, for pickers that style leading and trailing days. */
92
+ export declare function monthGridCells(month: Date, weekStartsOn?: CalendarWeekStart, fixedWeeks?: boolean): MonthGridCell[][];
93
+ /** Moves `date` one period of `view` forward (`delta` = 1) or back (-1). */
94
+ export declare function shiftCalendarDate(date: Date, view: CalendarView, delta: number): Date;
95
+ /** Weekday labels starting on `weekStartsOn` (e.g. "Sun", "Mon"…). */
96
+ export declare function weekdayLabels(weekStartsOn?: CalendarWeekStart, locale?: string, weekday?: 'short' | 'long'): string[];
97
+ /** "February 2026": month and year, first letter capitalised (fr-FR writes "février 2026"). */
98
+ export declare function formatMonthLabel(date: Date, locale?: string): string;
99
+ /** "Aug 03, 2025": a range endpoint in pickers and their fields. */
100
+ export declare function formatShortDate(date: Date, locale?: string): string;
101
+ /** "Aug 03, 2025 – Aug 09, 2025", one endpoint alone, or `empty` when neither is set. */
102
+ export declare function formatDateRange(start: Date | null | undefined, end: Date | null | undefined, locale?: string, empty?: string): string;
103
+ /** Nav bar title per view: "April 26, 2026" · "Apr 20 - 26, 2026" · "April 2026". */
104
+ export declare function formatCalendarTitle(date: Date, view: CalendarView, locale?: string, weekStartsOn?: CalendarWeekStart): string;
105
+ /** Hour-gutter label: "7am", "12pm" (Figma Time Item text). */
106
+ export declare function formatHourLabel(hour: number, locale?: string): string;
107
+ /** Clock label: "11:32 AM". */
108
+ export declare function formatTimeLabel(date: Date, locale?: string): string;
109
+ /** "9:00 AM – 10:30 AM" for an event (same-day), with dates when it spans days. */
110
+ export declare function formatEventTimeRange(event: Pick<CalendarEventData, 'start' | 'end'>, locale?: string): string;
111
+ /** Events that touch the local day `day` (end exclusive), in start order. */
112
+ export declare function eventsOnDay(events: readonly CalendarEventData[], day: Date): CalendarEventData[];
113
+ /**
114
+ * Places timed events on a day/week grid of `days`, from `startHour` to
115
+ * `endHour`. An event that crosses midnight inside the range spans several
116
+ * day columns (one chip, as the Figma weekly view draws it) and keeps its
117
+ * start time as its row. Overlapping events (same columns and times) share
118
+ * the width in lanes.
119
+ */
120
+ export declare function layoutTimedEvents(events: readonly CalendarEventData[], days: readonly Date[], startHour?: number, endHour?: number, minHours?: number): CalendarPlacedEvent[];
121
+ /**
122
+ * Day view rows: events whose times overlap the first event of a row share that row
123
+ * (side by side, anchored at the row's start); the rest start new rows. Input in any order.
124
+ */
125
+ export declare function groupEventRows(placed: readonly CalendarPlacedEvent[]): CalendarPlacedEvent[][];
126
+ /** True when `now` falls inside the period `view` shows around `date` (day, week or month). */
127
+ export declare function isCurrentPeriod(date: Date, now: Date, view: CalendarView, weekStartsOn?: CalendarWeekStart): boolean;
128
+ //# sourceMappingURL=calendar.d.ts.map