@sole-ui/react 0.1.0 → 0.2.1

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 (159) hide show
  1. package/README.md +34 -3
  2. package/dist/components/shell/app-frame.d.ts +14 -0
  3. package/dist/components/shell/app-frame.js +8 -0
  4. package/dist/components/shell/command-palette.d.ts +32 -0
  5. package/dist/components/shell/command-palette.js +30 -0
  6. package/dist/components/shell/index.d.ts +4 -0
  7. package/dist/components/shell/index.js +4 -0
  8. package/dist/components/shell/page.d.ts +25 -0
  9. package/dist/components/shell/page.js +22 -0
  10. package/dist/components/shell/sidebar.d.ts +81 -0
  11. package/dist/components/shell/sidebar.js +113 -0
  12. package/dist/components/sole/avatar.d.ts +19 -0
  13. package/dist/components/sole/avatar.js +37 -0
  14. package/dist/components/sole/banner.d.ts +24 -0
  15. package/dist/components/sole/banner.js +27 -0
  16. package/dist/components/sole/column-header.d.ts +15 -0
  17. package/dist/components/sole/column-header.js +15 -0
  18. package/dist/components/sole/copy-field.d.ts +10 -0
  19. package/dist/components/sole/copy-field.js +32 -0
  20. package/dist/components/sole/count-badge.d.ts +8 -0
  21. package/dist/components/sole/count-badge.js +8 -0
  22. package/dist/components/sole/field.d.ts +24 -0
  23. package/dist/components/sole/field.js +27 -0
  24. package/dist/components/sole/filters.d.ts +33 -0
  25. package/dist/components/sole/filters.js +48 -0
  26. package/dist/components/sole/index.d.ts +17 -0
  27. package/dist/components/sole/index.js +17 -0
  28. package/dist/components/sole/kbd.d.ts +7 -0
  29. package/dist/components/sole/kbd.js +14 -0
  30. package/dist/components/sole/label-tag.d.ts +18 -0
  31. package/dist/components/sole/label-tag.js +17 -0
  32. package/dist/components/sole/layout.d.ts +54 -0
  33. package/dist/components/sole/layout.js +48 -0
  34. package/dist/components/sole/logo.d.ts +13 -0
  35. package/dist/components/sole/logo.js +17 -0
  36. package/dist/components/sole/nav.d.ts +22 -0
  37. package/dist/components/sole/nav.js +32 -0
  38. package/dist/components/sole/person-chip.d.ts +8 -0
  39. package/dist/components/sole/person-chip.js +19 -0
  40. package/dist/components/sole/person-header.d.ts +21 -0
  41. package/dist/components/sole/person-header.js +22 -0
  42. package/dist/components/sole/pill.d.ts +9 -0
  43. package/dist/components/sole/pill.js +14 -0
  44. package/dist/components/sole/states.d.ts +48 -0
  45. package/dist/components/sole/states.js +53 -0
  46. package/dist/components/sole/table.d.ts +23 -0
  47. package/dist/components/sole/table.js +32 -0
  48. package/dist/components/ui/alert-dialog.d.ts +27 -0
  49. package/dist/components/ui/alert-dialog.js +33 -0
  50. package/dist/components/ui/badge.d.ts +11 -0
  51. package/dist/components/ui/badge.js +30 -0
  52. package/dist/components/ui/button.d.ts +13 -0
  53. package/dist/components/ui/button.js +49 -0
  54. package/dist/components/ui/calendar.d.ts +9 -0
  55. package/dist/components/ui/calendar.js +79 -0
  56. package/dist/components/ui/checkbox.d.ts +4 -0
  57. package/dist/components/ui/checkbox.js +11 -0
  58. package/dist/components/ui/command.d.ts +90 -0
  59. package/dist/components/ui/command.js +36 -0
  60. package/dist/components/ui/dialog.d.ts +18 -0
  61. package/dist/components/ui/dialog.js +33 -0
  62. package/dist/components/ui/dropdown-menu.d.ts +25 -0
  63. package/dist/components/ui/dropdown-menu.js +42 -0
  64. package/dist/components/ui/index.d.ts +23 -0
  65. package/dist/components/ui/index.js +23 -0
  66. package/dist/components/ui/input.d.ts +3 -0
  67. package/dist/components/ui/input.js +12 -0
  68. package/dist/components/ui/label.d.ts +4 -0
  69. package/dist/components/ui/label.js +10 -0
  70. package/dist/components/ui/popover.d.ts +10 -0
  71. package/dist/components/ui/popover.js +22 -0
  72. package/dist/components/ui/scroll-area.d.ts +5 -0
  73. package/dist/components/ui/scroll-area.js +14 -0
  74. package/dist/components/ui/select.d.ts +15 -0
  75. package/dist/components/ui/select.js +30 -0
  76. package/dist/components/ui/separator.d.ts +4 -0
  77. package/dist/components/ui/separator.js +10 -0
  78. package/dist/components/ui/sheet.d.ts +14 -0
  79. package/dist/components/ui/sheet.js +35 -0
  80. package/dist/components/ui/skeleton.d.ts +3 -0
  81. package/dist/components/ui/skeleton.js +10 -0
  82. package/dist/components/ui/sonner.d.ts +5 -0
  83. package/dist/components/ui/sonner.js +63 -0
  84. package/dist/components/ui/switch.d.ts +6 -0
  85. package/dist/components/ui/switch.js +10 -0
  86. package/dist/components/ui/tabs.d.ts +7 -0
  87. package/dist/components/ui/tabs.js +24 -0
  88. package/dist/components/ui/textarea.d.ts +3 -0
  89. package/dist/components/ui/textarea.js +9 -0
  90. package/dist/components/ui/toggle-group.d.ts +14 -0
  91. package/dist/components/ui/toggle-group.js +24 -0
  92. package/dist/components/ui/toggle.d.ts +12 -0
  93. package/dist/components/ui/toggle.js +28 -0
  94. package/dist/components/ui/tooltip.d.ts +7 -0
  95. package/dist/components/ui/tooltip.js +18 -0
  96. package/dist/hq/activity-line.d.ts +21 -0
  97. package/dist/hq/activity-line.js +26 -0
  98. package/dist/hq/board-card.d.ts +12 -0
  99. package/dist/hq/board-card.js +21 -0
  100. package/dist/hq/board-chip.d.ts +12 -0
  101. package/dist/hq/board-chip.js +20 -0
  102. package/dist/hq/checklist-item.d.ts +14 -0
  103. package/dist/hq/checklist-item.js +13 -0
  104. package/dist/hq/comment.d.ts +26 -0
  105. package/dist/hq/comment.js +15 -0
  106. package/dist/hq/due-chip.d.ts +11 -0
  107. package/dist/hq/due-chip.js +25 -0
  108. package/dist/hq/group-header.d.ts +27 -0
  109. package/dist/hq/group-header.js +22 -0
  110. package/dist/hq/inbox-row.d.ts +22 -0
  111. package/dist/hq/inbox-row.js +13 -0
  112. package/dist/hq/index.d.ts +21 -0
  113. package/dist/hq/index.js +21 -0
  114. package/dist/hq/link-row.d.ts +13 -0
  115. package/dist/hq/link-row.js +13 -0
  116. package/dist/hq/mention-chip.d.ts +23 -0
  117. package/dist/hq/mention-chip.js +28 -0
  118. package/dist/hq/page-row.d.ts +26 -0
  119. package/dist/hq/page-row.js +30 -0
  120. package/dist/hq/priority-icon.d.ts +9 -0
  121. package/dist/hq/priority-icon.js +23 -0
  122. package/dist/hq/property-type-icon.d.ts +7 -0
  123. package/dist/hq/property-type-icon.js +22 -0
  124. package/dist/hq/recent-item.d.ts +11 -0
  125. package/dist/hq/recent-item.js +12 -0
  126. package/dist/hq/review-badge.d.ts +37 -0
  127. package/dist/hq/review-badge.js +77 -0
  128. package/dist/hq/state-icon.d.ts +12 -0
  129. package/dist/hq/state-icon.js +41 -0
  130. package/dist/hq/state-pill.d.ts +9 -0
  131. package/dist/hq/state-pill.js +18 -0
  132. package/dist/hq/sticky.d.ts +13 -0
  133. package/dist/hq/sticky.js +22 -0
  134. package/dist/hq/template-card.d.ts +10 -0
  135. package/dist/hq/template-card.js +9 -0
  136. package/dist/hq/types.d.ts +49 -0
  137. package/dist/hq/types.js +7 -0
  138. package/dist/hq/work-item-row.d.ts +16 -0
  139. package/dist/hq/work-item-row.js +23 -0
  140. package/dist/index.d.ts +13 -223
  141. package/dist/index.js +12 -678
  142. package/dist/lib/cn.d.ts +2 -0
  143. package/dist/lib/cn.js +15 -0
  144. package/dist/lib/colour.d.ts +8 -0
  145. package/dist/lib/colour.js +28 -0
  146. package/dist/lib/dates.d.ts +62 -0
  147. package/dist/lib/dates.js +182 -0
  148. package/dist/lib/hue.d.ts +3 -0
  149. package/dist/lib/hue.js +1 -0
  150. package/dist/lib/person.d.ts +16 -0
  151. package/dist/lib/person.js +17 -0
  152. package/dist/lib/platform.d.ts +4 -0
  153. package/dist/lib/platform.js +12 -0
  154. package/dist/lib/sole-provider.d.ts +35 -0
  155. package/dist/lib/sole-provider.js +52 -0
  156. package/dist/styles.css +5 -0
  157. package/dist/tokens.generated.d.ts +219 -0
  158. package/dist/tokens.generated.js +679 -0
  159. package/package.json +34 -3
@@ -0,0 +1,8 @@
1
+ /** FNV-1a, 32 bit. Stable across runtimes, used to pick colours from ids. */
2
+ export declare function fnv1a(value: string): number;
3
+ /** Dialer's 8 avatar colours (brand-and-ux 5.12). Hashed on the person id, so a rename keeps the colour. */
4
+ export declare const AVATAR_COLOURS: readonly ['#0485F7', '#0EA5A4', '#D97706', '#DB2777', '#2563EB', '#7C3AED', '#059669', '#EA580C'];
5
+ export declare function avatarColour(id: string): string;
6
+ /** Board colours: 8 muted two-stop gradients (brand-and-ux 5.13), for the header strip and chip dot. */
7
+ export declare const BOARD_GRADIENTS: readonly [readonly ['#3B5B8C', '#263B5C'], readonly ['#2F6E68', '#1E4844'], readonly ['#6B4C7A', '#463151'], readonly ['#8A5A2B', '#5A3A1C'], readonly ['#4E6B3A', '#334527'], readonly ['#4A4E8C', '#31345C'], readonly ['#8A4257', '#5A2B39'], readonly ['#46607A', '#2E3F50']];
8
+ export declare function boardGradient(id: string): readonly [string, string];
@@ -0,0 +1,28 @@
1
+ /** FNV-1a, 32 bit. Stable across runtimes, used to pick colours from ids. */
2
+ export function fnv1a(value) {
3
+ let h = 0x811c9dc5;
4
+ for (let i = 0; i < value.length; i++) {
5
+ h ^= value.charCodeAt(i);
6
+ h = Math.imul(h, 0x01000193);
7
+ }
8
+ return h >>> 0;
9
+ }
10
+ /** Dialer's 8 avatar colours (brand-and-ux 5.12). Hashed on the person id, so a rename keeps the colour. */
11
+ export const AVATAR_COLOURS = ['#0485F7', '#0EA5A4', '#D97706', '#DB2777', '#2563EB', '#7C3AED', '#059669', '#EA580C'];
12
+ export function avatarColour(id) {
13
+ return AVATAR_COLOURS[fnv1a(id) % AVATAR_COLOURS.length];
14
+ }
15
+ /** Board colours: 8 muted two-stop gradients (brand-and-ux 5.13), for the header strip and chip dot. */
16
+ export const BOARD_GRADIENTS = [
17
+ ['#3B5B8C', '#263B5C'],
18
+ ['#2F6E68', '#1E4844'],
19
+ ['#6B4C7A', '#463151'],
20
+ ['#8A5A2B', '#5A3A1C'],
21
+ ['#4E6B3A', '#334527'],
22
+ ['#4A4E8C', '#31345C'],
23
+ ['#8A4257', '#5A2B39'],
24
+ ['#46607A', '#2E3F50'],
25
+ ];
26
+ export function boardGradient(id) {
27
+ return BOARD_GRADIENTS[fnv1a(id) % BOARD_GRADIENTS.length];
28
+ }
@@ -0,0 +1,62 @@
1
+ /** A calendar date as `YYYY-MM-DD`, with no time zone. */
2
+ export type DateOnly = string;
3
+ /** An instant as an ISO 8601 string. */
4
+ export type Timestamp = string;
5
+ export declare const SYDNEY = "Australia/Sydney";
6
+ export declare function isDateOnly(value: unknown): value is DateOnly;
7
+ /** The calendar date at instant `at` in `tz`. */
8
+ export declare function dateIn(at: Date | Timestamp, tz?: string): DateOnly;
9
+ /** Today on the work clock (Sydney unless told otherwise). */
10
+ export declare function todayDate(now: Date, tz?: string): DateOnly;
11
+ export declare function addDays(date: DateOnly, days: number): DateOnly;
12
+ /** Calendar months, clamped: 31 Aug plus 6 months is 28 Feb (29 in a leap year). */
13
+ export declare function addMonths(date: DateOnly, months: number): DateOnly;
14
+ /** Whole days from `from` to `to`; negative when `to` is earlier. */
15
+ export declare function daysBetween(from: DateOnly, to: DateOnly): number;
16
+ /** 0 for Monday to 6 for Sunday. */
17
+ export declare function weekdayIndex(date: DateOnly): number;
18
+ /** The work week containing `date`: Monday to Sunday. */
19
+ export declare function weekOf(date: DateOnly): {
20
+ start: DateOnly;
21
+ end: DateOnly;
22
+ };
23
+ export type DueTone = 'overdue' | 'today' | 'soon' | 'plain' | 'done';
24
+ export interface DueChipModel {
25
+ tone: DueTone;
26
+ /** What the chip says: "9 Sep", "Today", "Tomorrow", "Fri", "9 Oct 2027". */
27
+ label: string;
28
+ /** For screen readers, e.g. "Overdue, due 9 Sep". */
29
+ ariaLabel: string;
30
+ /** The full date, for the tooltip. */
31
+ title: string;
32
+ }
33
+ /**
34
+ * The due-date chip (brand-and-ux 5.10). `done` is true for completed and cancelled
35
+ * items: their date is a plain fact and is never red.
36
+ */
37
+ export declare function dueChip(due: DateOnly, today: DateOnly, done?: boolean): DueChipModel;
38
+ /** "9 Sep" in the current year, "9 Sep 2027" otherwise. Pass `today` to decide the year. */
39
+ export declare function formatDate(date: DateOnly, today?: DateOnly, opts?: {
40
+ year?: boolean;
41
+ }): string;
42
+ /** "9 September 2026", or "Wednesday, 9 September 2026" with `weekday`. `year: false` drops the year. */
43
+ export declare function formatLongDate(date: DateOnly, opts?: {
44
+ weekday?: boolean;
45
+ year?: boolean;
46
+ }): string;
47
+ /** "3:00 PM", "9:30 AM": no leading zero, always minutes, upper-case AM and PM. */
48
+ export declare function formatTime(at: Date | Timestamp, tz?: string): string;
49
+ /** A timestamp as a date: "9 Sep", or "9 Sep 2025" in another year. */
50
+ export declare function formatTimestampDate(at: Date | Timestamp, now: Date, tz?: string): string;
51
+ /**
52
+ * Relative time for the Inbox and comments only (brand-and-ux 5.19): "Just now",
53
+ * "5 min ago", "2 h ago" today; "Yesterday"; the weekday within the last week;
54
+ * then the date. Always pair it with the absolute time in a tooltip.
55
+ */
56
+ export declare function formatRelative(at: Date | Timestamp, now: Date, tz?: string): string;
57
+ /** "Today 3:12 PM", "Yesterday 3:12 PM", "Fri 3:12 PM" within a week, else "9 Sep, 3:12 PM". */
58
+ export declare function formatDayTime(at: Date | Timestamp, now: Date, tz?: string): string;
59
+ /** The full stamp for tooltips: "Tuesday, 29 September 2026, 3:12 PM". */
60
+ export declare function formatFullTimestamp(at: Date | Timestamp, tz?: string): string;
61
+ /** Home's greeting: before 12 is morning, before 6 PM is afternoon. */
62
+ export declare function greeting(now: Date, tz?: string): 'Good morning' | 'Good afternoon' | 'Good evening';
@@ -0,0 +1,182 @@
1
+ import { TZDate } from '@date-fns/tz';
2
+ // Every date rule in one place (brand-and-ux 5.10 and 5.19, behaviour-core 3.3, D15).
3
+ //
4
+ // Two clocks, on purpose:
5
+ // - The WORK clock is Australia/Sydney, always. "Today", "overdue" and "this week"
6
+ // must mean the same thing for every colleague, whatever their own timezone is.
7
+ // - DISPLAY of timestamps (a comment's time) uses the viewer's profile timezone,
8
+ // which defaults to Sydney too.
9
+ //
10
+ // Names of months and weekdays are written out here rather than taken from Intl:
11
+ // current ICU prints "Sept" for en-AU, and the house format is "9 Sep".
12
+ // Nothing reads the clock: every function that needs the moment takes `now` or `today`.
13
+ // A screen gets them from SoleProvider (useNow, useToday).
14
+ export const SYDNEY = 'Australia/Sydney';
15
+ const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
16
+ const MONTHS_LONG = [
17
+ 'January', 'February', 'March', 'April', 'May', 'June',
18
+ 'July', 'August', 'September', 'October', 'November', 'December',
19
+ ];
20
+ /** Index 0 is Monday, the first day of the work week. */
21
+ const WEEKDAYS_SHORT = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
22
+ const WEEKDAYS_LONG = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
23
+ const pad = (n) => String(n).padStart(2, '0');
24
+ const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
25
+ export function isDateOnly(value) {
26
+ if (typeof value !== 'string')
27
+ return false;
28
+ const m = DATE_ONLY.exec(value);
29
+ if (!m)
30
+ return false;
31
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
32
+ const t = new Date(Date.UTC(y, mo - 1, d));
33
+ return t.getUTCFullYear() === y && t.getUTCMonth() === mo - 1 && t.getUTCDate() === d;
34
+ }
35
+ function ymd(date) {
36
+ const m = DATE_ONLY.exec(date);
37
+ if (!m)
38
+ throw new Error(`Not a calendar date: ${date}`);
39
+ return { y: Number(m[1]), m: Number(m[2]), d: Number(m[3]) };
40
+ }
41
+ function fromUtc(t) {
42
+ return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
43
+ }
44
+ function toUtc(date) {
45
+ const { y, m, d } = ymd(date);
46
+ return new Date(Date.UTC(y, m - 1, d));
47
+ }
48
+ /** The calendar date at instant `at` in `tz`. */
49
+ export function dateIn(at, tz = SYDNEY) {
50
+ const t = typeof at === 'string' ? new Date(at) : at;
51
+ const z = new TZDate(t.getTime(), tz);
52
+ return `${z.getFullYear()}-${pad(z.getMonth() + 1)}-${pad(z.getDate())}`;
53
+ }
54
+ /** Today on the work clock (Sydney unless told otherwise). */
55
+ export function todayDate(now, tz = SYDNEY) {
56
+ return dateIn(now, tz);
57
+ }
58
+ export function addDays(date, days) {
59
+ const t = toUtc(date);
60
+ t.setUTCDate(t.getUTCDate() + days);
61
+ return fromUtc(t);
62
+ }
63
+ /** Calendar months, clamped: 31 Aug plus 6 months is 28 Feb (29 in a leap year). */
64
+ export function addMonths(date, months) {
65
+ const { y, m, d } = ymd(date);
66
+ const index = y * 12 + (m - 1) + months;
67
+ const ny = Math.floor(index / 12);
68
+ const nm = index - ny * 12; // 0 to 11
69
+ const last = new Date(Date.UTC(ny, nm + 1, 0)).getUTCDate();
70
+ return fromUtc(new Date(Date.UTC(ny, nm, Math.min(d, last))));
71
+ }
72
+ /** Whole days from `from` to `to`; negative when `to` is earlier. */
73
+ export function daysBetween(from, to) {
74
+ return Math.round((toUtc(to).getTime() - toUtc(from).getTime()) / 86_400_000);
75
+ }
76
+ /** 0 for Monday to 6 for Sunday. */
77
+ export function weekdayIndex(date) {
78
+ return (toUtc(date).getUTCDay() + 6) % 7;
79
+ }
80
+ /** The work week containing `date`: Monday to Sunday. */
81
+ export function weekOf(date) {
82
+ const start = addDays(date, -weekdayIndex(date));
83
+ return { start, end: addDays(start, 6) };
84
+ }
85
+ /**
86
+ * The due-date chip (brand-and-ux 5.10). `done` is true for completed and cancelled
87
+ * items: their date is a plain fact and is never red.
88
+ */
89
+ export function dueChip(due, today, done = false) {
90
+ const title = formatLongDate(due, { weekday: true });
91
+ const short = formatDate(due, today);
92
+ if (done)
93
+ return { tone: 'done', label: short, ariaLabel: `Due ${short}`, title };
94
+ if (due < today)
95
+ return { tone: 'overdue', label: short, ariaLabel: `Overdue, due ${short}`, title };
96
+ if (due === today)
97
+ return { tone: 'today', label: 'Today', ariaLabel: 'Due today', title };
98
+ if (due === addDays(today, 1))
99
+ return { tone: 'soon', label: 'Tomorrow', ariaLabel: 'Due tomorrow', title };
100
+ if (due <= weekOf(today).end) {
101
+ const day = WEEKDAYS_SHORT[weekdayIndex(due)];
102
+ return { tone: 'soon', label: day, ariaLabel: `Due ${WEEKDAYS_LONG[weekdayIndex(due)]}`, title };
103
+ }
104
+ return { tone: 'plain', label: short, ariaLabel: `Due ${short}`, title };
105
+ }
106
+ // ---------------------------------------------------------------- formatting
107
+ /** "9 Sep" in the current year, "9 Sep 2027" otherwise. Pass `today` to decide the year. */
108
+ export function formatDate(date, today, opts = {}) {
109
+ const { y, m, d } = ymd(date);
110
+ const withYear = opts.year ?? (today ? ymd(today).y !== y : true);
111
+ return withYear ? `${d} ${MONTHS_SHORT[m - 1]} ${y}` : `${d} ${MONTHS_SHORT[m - 1]}`;
112
+ }
113
+ /** "9 September 2026", or "Wednesday, 9 September 2026" with `weekday`. `year: false` drops the year. */
114
+ export function formatLongDate(date, opts = {}) {
115
+ const { y, m, d } = ymd(date);
116
+ const core = opts.year === false ? `${d} ${MONTHS_LONG[m - 1]}` : `${d} ${MONTHS_LONG[m - 1]} ${y}`;
117
+ return opts.weekday ? `${WEEKDAYS_LONG[weekdayIndex(date)]}, ${core}` : core;
118
+ }
119
+ /** "3:00 PM", "9:30 AM": no leading zero, always minutes, upper-case AM and PM. */
120
+ export function formatTime(at, tz = SYDNEY) {
121
+ const t = typeof at === 'string' ? new Date(at) : at;
122
+ const z = new TZDate(t.getTime(), tz);
123
+ const h = z.getHours();
124
+ return `${h % 12 || 12}:${pad(z.getMinutes())} ${h < 12 ? 'AM' : 'PM'}`;
125
+ }
126
+ /** A timestamp as a date: "9 Sep", or "9 Sep 2025" in another year. */
127
+ export function formatTimestampDate(at, now, tz = SYDNEY) {
128
+ return formatDate(dateIn(at, tz), dateIn(now, tz));
129
+ }
130
+ /**
131
+ * Relative time for the Inbox and comments only (brand-and-ux 5.19): "Just now",
132
+ * "5 min ago", "2 h ago" today; "Yesterday"; the weekday within the last week;
133
+ * then the date. Always pair it with the absolute time in a tooltip.
134
+ */
135
+ export function formatRelative(at, now, tz = SYDNEY) {
136
+ const t = typeof at === 'string' ? new Date(at) : at;
137
+ const seconds = Math.floor((now.getTime() - t.getTime()) / 1000);
138
+ const day = dateIn(t, tz);
139
+ const current = dateIn(now, tz);
140
+ const ago = daysBetween(day, current);
141
+ if (seconds < 0 || ago < 0)
142
+ return formatDate(day, current);
143
+ if (ago === 0) {
144
+ if (seconds < 60)
145
+ return 'Just now';
146
+ if (seconds < 3600)
147
+ return `${Math.floor(seconds / 60)} min ago`;
148
+ return `${Math.floor(seconds / 3600)} h ago`;
149
+ }
150
+ if (ago === 1)
151
+ return 'Yesterday';
152
+ if (ago < 7)
153
+ return WEEKDAYS_LONG[weekdayIndex(day)];
154
+ return formatDate(day, current);
155
+ }
156
+ /** "Today 3:12 PM", "Yesterday 3:12 PM", "Fri 3:12 PM" within a week, else "9 Sep, 3:12 PM". */
157
+ export function formatDayTime(at, now, tz = SYDNEY) {
158
+ const day = dateIn(at, tz);
159
+ const current = dateIn(now, tz);
160
+ const ago = daysBetween(day, current);
161
+ const time = formatTime(at, tz);
162
+ if (ago === 0)
163
+ return `Today ${time}`;
164
+ if (ago === 1)
165
+ return `Yesterday ${time}`;
166
+ if (ago > 1 && ago < 7)
167
+ return `${WEEKDAYS_SHORT[weekdayIndex(day)]} ${time}`;
168
+ return `${formatDate(day, current)}, ${time}`;
169
+ }
170
+ /** The full stamp for tooltips: "Tuesday, 29 September 2026, 3:12 PM". */
171
+ export function formatFullTimestamp(at, tz = SYDNEY) {
172
+ return `${formatLongDate(dateIn(at, tz), { weekday: true })}, ${formatTime(at, tz)}`;
173
+ }
174
+ /** Home's greeting: before 12 is morning, before 6 PM is afternoon. */
175
+ export function greeting(now, tz = SYDNEY) {
176
+ const h = new TZDate(now.getTime(), tz).getHours();
177
+ if (h < 12)
178
+ return 'Good morning';
179
+ if (h < 18)
180
+ return 'Good afternoon';
181
+ return 'Good evening';
182
+ }
@@ -0,0 +1,3 @@
1
+ /** The twelve label hues. Each one is a `.hue-<name>` class in tokens.css. */
2
+ export type Hue = 'slate' | 'indigo' | 'violet' | 'blue' | 'cyan' | 'teal' | 'green' | 'lime' | 'amber' | 'orange' | 'rose' | 'pink';
3
+ export declare const HUES: readonly Hue[];
@@ -0,0 +1 @@
1
+ export const HUES = ['slate', 'indigo', 'violet', 'blue', 'cyan', 'teal', 'green', 'lime', 'amber', 'orange', 'rose', 'pink'];
@@ -0,0 +1,16 @@
1
+ /** The one neutral shape for a person. HQ, the CRM and the Dialer each map their own rows to it. */
2
+ export interface SolePerson {
3
+ id: string;
4
+ name: string;
5
+ avatarUrl?: string | null;
6
+ isBot?: boolean;
7
+ }
8
+ /** What a part shows for someone who has left (brand-and-ux 5.12). */
9
+ export declare const FORMER_MEMBER = "Former member";
10
+ /**
11
+ * First letter of the first two words: "Ava Thompson" is AT. A name that is still an
12
+ * email local part is one word, so "ava.t" is A.
13
+ */
14
+ export declare function initials(name: string): string;
15
+ /** The name a part shows: the person's name, or "Former member" when there is no person. */
16
+ export declare function personName(person: SolePerson | null | undefined): string;
@@ -0,0 +1,17 @@
1
+ /** What a part shows for someone who has left (brand-and-ux 5.12). */
2
+ export const FORMER_MEMBER = 'Former member';
3
+ /**
4
+ * First letter of the first two words: "Ava Thompson" is AT. A name that is still an
5
+ * email local part is one word, so "ava.t" is A.
6
+ */
7
+ export function initials(name) {
8
+ const words = name.trim().split(/\s+/).filter(Boolean);
9
+ const letters = words.slice(0, 2).map((w) => [...w][0] ?? '');
10
+ return letters.join('').toUpperCase() || '?';
11
+ }
12
+ /** The name a part shows: the person's name, or "Former member" when there is no person. */
13
+ export function personName(person) {
14
+ if (!person)
15
+ return FORMER_MEMBER;
16
+ return person.name.trim() || FORMER_MEMBER;
17
+ }
@@ -0,0 +1,4 @@
1
+ /** True on macOS and iOS, where the command key is Cmd rather than Ctrl. */
2
+ export declare function isApple(): boolean;
3
+ /** "Cmd" on Apple machines, "Ctrl" elsewhere. */
4
+ export declare function modKeyName(): 'Cmd' | 'Ctrl';
@@ -0,0 +1,12 @@
1
+ /** True on macOS and iOS, where the command key is Cmd rather than Ctrl. */
2
+ export function isApple() {
3
+ if (typeof navigator === 'undefined')
4
+ return false;
5
+ const data = navigator.userAgentData;
6
+ const platform = data?.platform || navigator.platform || navigator.userAgent;
7
+ return /mac|iphone|ipad|ipod/i.test(platform);
8
+ }
9
+ /** "Cmd" on Apple machines, "Ctrl" elsewhere. */
10
+ export function modKeyName() {
11
+ return isApple() ? 'Cmd' : 'Ctrl';
12
+ }
@@ -0,0 +1,35 @@
1
+ import type { AnchorHTMLAttributes, ComponentType, ReactNode, RefAttributes } from 'react';
2
+ import type { DateOnly } from './dates.ts';
3
+ import type { SolePerson } from './person.ts';
4
+ export interface SoleLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
5
+ href: string;
6
+ }
7
+ /** A router's link, adapted to take `href`: react-router's `Link`, Next's `Link`, or a plain anchor. */
8
+ export type SoleLinkComponent = ComponentType<SoleLinkProps & RefAttributes<HTMLAnchorElement>>;
9
+ export interface SoleProviderProps {
10
+ children: ReactNode;
11
+ /** The link every part uses for an in-app address. Defaults to a plain `<a>`. */
12
+ linkComponent?: SoleLinkComponent;
13
+ /** The work clock's time zone. Defaults to Australia/Sydney. */
14
+ timeZone?: string;
15
+ /** The app's clock. Defaults to the real one; a demo or a test pins it. */
16
+ now?: () => Date;
17
+ /** Where a person's name links to, or null for no link. Defaults to no link. */
18
+ personHref?: (person: SolePerson) => string | null;
19
+ }
20
+ interface SoleContextValue {
21
+ linkComponent: SoleLinkComponent;
22
+ timeZone: string;
23
+ now: () => Date;
24
+ personHref: (person: SolePerson) => string | null;
25
+ }
26
+ export declare function SoleProvider({ children, linkComponent, timeZone, now, personHref }: SoleProviderProps): import("react").JSX.Element;
27
+ /** The provider's four values, or the defaults when there is no provider. */
28
+ export declare function useSole(): SoleContextValue;
29
+ /** A link to an address inside the app. An absolute `http` address is always a plain anchor. */
30
+ export declare const SoleLink: import("react").ForwardRefExoticComponent<SoleLinkProps & RefAttributes<HTMLAnchorElement>>;
31
+ /** The current time, refreshed every `intervalMs` (a minute by default). */
32
+ export declare function useNow(intervalMs?: number): Date;
33
+ /** Today on the work clock, rolling over at midnight. */
34
+ export declare function useToday(): DateOnly;
35
+ export {};
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, forwardRef, useContext, useEffect, useMemo, useState } from 'react';
4
+ import { Tooltip as TooltipPrimitive } from 'radix-ui';
5
+ import { SYDNEY, todayDate } from './dates.js';
6
+ const PlainLink = forwardRef(function PlainLink(props, ref) {
7
+ return _jsx("a", { ref: ref, ...props });
8
+ });
9
+ const defaults = {
10
+ linkComponent: PlainLink,
11
+ timeZone: SYDNEY,
12
+ now: () => new Date(),
13
+ personHref: () => null,
14
+ };
15
+ const SoleContext = createContext(defaults);
16
+ export function SoleProvider({ children, linkComponent, timeZone, now, personHref }) {
17
+ const value = useMemo(() => ({
18
+ linkComponent: linkComponent ?? defaults.linkComponent,
19
+ timeZone: timeZone ?? defaults.timeZone,
20
+ now: now ?? defaults.now,
21
+ personHref: personHref ?? defaults.personHref,
22
+ }), [linkComponent, timeZone, now, personHref]);
23
+ // Radix tooltips need a provider above them. It lives here so a consumer wraps the app once.
24
+ return (_jsx(SoleContext.Provider, { value: value, children: _jsx(TooltipPrimitive.Provider, { delayDuration: 400, children: children }) }));
25
+ }
26
+ /** The provider's four values, or the defaults when there is no provider. */
27
+ export function useSole() {
28
+ return useContext(SoleContext);
29
+ }
30
+ /** A link to an address inside the app. An absolute `http` address is always a plain anchor. */
31
+ export const SoleLink = forwardRef(function SoleLink(props, ref) {
32
+ const { linkComponent: Link } = useSole();
33
+ if (/^https?:/i.test(props.href))
34
+ return _jsx(PlainLink, { ref: ref, ...props });
35
+ return _jsx(Link, { ref: ref, ...props });
36
+ });
37
+ /** The current time, refreshed every `intervalMs` (a minute by default). */
38
+ export function useNow(intervalMs = 60_000) {
39
+ const { now } = useSole();
40
+ const [value, setValue] = useState(() => now());
41
+ useEffect(() => {
42
+ setValue(now());
43
+ const timer = setInterval(() => setValue(now()), intervalMs);
44
+ return () => clearInterval(timer);
45
+ }, [now, intervalMs]);
46
+ return value;
47
+ }
48
+ /** Today on the work clock, rolling over at midnight. */
49
+ export function useToday() {
50
+ const { timeZone } = useSole();
51
+ return todayDate(useNow(), timeZone);
52
+ }
package/dist/styles.css CHANGED
@@ -7,6 +7,7 @@
7
7
  is generated from packages/tokens. This file adds the font, the base layer and the
8
8
  reduced-motion rule, as Sole HQ has them. */
9
9
 
10
+ @import "tw-animate-css";
10
11
  @import "@fontsource-variable/inter";
11
12
  @import "./tokens.css";
12
13
 
@@ -57,3 +58,7 @@
57
58
  scroll-behavior: auto !important;
58
59
  }
59
60
  }
61
+
62
+ /* The consumer's Tailwind scans the package's own files for the classes the parts use, so
63
+ a fresh app needs only the two imports. In dist this resolves to dist/. */
64
+ @source "./";
@@ -0,0 +1,219 @@
1
+ export type TokenType = 'color' | 'dimension' | 'fontFamily' | 'shadow' | 'cubicBezier' | 'number';
2
+ /** One design token in the DTCG shape. */
3
+ export interface Token {
4
+ readonly $value: string;
5
+ readonly $type: TokenType;
6
+ readonly $description?: string;
7
+ }
8
+ /** Sole's design tokens, the same tree as tokens.json. */
9
+ export declare const tokens: {
10
+ readonly color: {
11
+ readonly background: Token;
12
+ readonly foreground: Token;
13
+ readonly card: Token;
14
+ readonly "card-foreground": Token;
15
+ readonly popover: Token;
16
+ readonly "popover-foreground": Token;
17
+ readonly primary: Token;
18
+ readonly "primary-foreground": Token;
19
+ readonly "primary-hover": Token;
20
+ readonly brand: Token;
21
+ readonly link: Token;
22
+ readonly secondary: Token;
23
+ readonly "secondary-foreground": Token;
24
+ readonly muted: Token;
25
+ readonly "muted-foreground": Token;
26
+ readonly "subtle-foreground": Token;
27
+ readonly faint: Token;
28
+ readonly accent: Token;
29
+ readonly "accent-foreground": Token;
30
+ readonly "row-hover": Token;
31
+ readonly border: Token;
32
+ readonly hairline: Token;
33
+ readonly input: Token;
34
+ readonly field: Token;
35
+ readonly ring: Token;
36
+ readonly destructive: Token;
37
+ readonly "destructive-foreground": Token;
38
+ readonly "danger-text": Token;
39
+ readonly "danger-soft": Token;
40
+ readonly "danger-border": Token;
41
+ readonly warning: Token;
42
+ readonly "warning-text": Token;
43
+ readonly "warning-soft": Token;
44
+ readonly "warning-border": Token;
45
+ readonly success: Token;
46
+ readonly "success-text": Token;
47
+ readonly "success-soft": Token;
48
+ readonly "success-border": Token;
49
+ readonly "neutral-soft": Token;
50
+ readonly "neutral-text": Token;
51
+ readonly skeleton: Token;
52
+ readonly sidebar: Token;
53
+ readonly "sidebar-foreground": Token;
54
+ readonly "sidebar-accent": Token;
55
+ readonly "sidebar-accent-foreground": Token;
56
+ readonly "sidebar-hover": Token;
57
+ readonly "sidebar-border": Token;
58
+ readonly "sidebar-ring": Token;
59
+ readonly tooltip: Token;
60
+ readonly "tooltip-foreground": Token;
61
+ readonly overdue: Token;
62
+ readonly "overdue-foreground": Token;
63
+ readonly mark: Token;
64
+ readonly "state-backlog": Token;
65
+ readonly "state-unstarted": Token;
66
+ readonly "state-started": Token;
67
+ readonly "state-completed": Token;
68
+ readonly "state-cancelled": Token;
69
+ readonly "sticky-gray": Token;
70
+ readonly "sticky-peach": Token;
71
+ readonly "sticky-pink": Token;
72
+ readonly "sticky-orange": Token;
73
+ readonly "sticky-green": Token;
74
+ readonly "sticky-light-blue": Token;
75
+ readonly "sticky-dark-blue": Token;
76
+ readonly "sticky-purple": Token;
77
+ readonly "sticky-foreground": Token;
78
+ };
79
+ readonly hue: {
80
+ readonly slate: {
81
+ readonly bg: Token;
82
+ readonly border: Token;
83
+ readonly fg: Token;
84
+ readonly dot: Token;
85
+ };
86
+ readonly indigo: {
87
+ readonly bg: Token;
88
+ readonly border: Token;
89
+ readonly fg: Token;
90
+ readonly dot: Token;
91
+ };
92
+ readonly violet: {
93
+ readonly bg: Token;
94
+ readonly border: Token;
95
+ readonly fg: Token;
96
+ readonly dot: Token;
97
+ };
98
+ readonly blue: {
99
+ readonly bg: Token;
100
+ readonly border: Token;
101
+ readonly fg: Token;
102
+ readonly dot: Token;
103
+ };
104
+ readonly cyan: {
105
+ readonly bg: Token;
106
+ readonly border: Token;
107
+ readonly fg: Token;
108
+ readonly dot: Token;
109
+ };
110
+ readonly teal: {
111
+ readonly bg: Token;
112
+ readonly border: Token;
113
+ readonly fg: Token;
114
+ readonly dot: Token;
115
+ };
116
+ readonly green: {
117
+ readonly bg: Token;
118
+ readonly border: Token;
119
+ readonly fg: Token;
120
+ readonly dot: Token;
121
+ };
122
+ readonly lime: {
123
+ readonly bg: Token;
124
+ readonly border: Token;
125
+ readonly fg: Token;
126
+ readonly dot: Token;
127
+ };
128
+ readonly amber: {
129
+ readonly bg: Token;
130
+ readonly border: Token;
131
+ readonly fg: Token;
132
+ readonly dot: Token;
133
+ };
134
+ readonly orange: {
135
+ readonly bg: Token;
136
+ readonly border: Token;
137
+ readonly fg: Token;
138
+ readonly dot: Token;
139
+ };
140
+ readonly rose: {
141
+ readonly bg: Token;
142
+ readonly border: Token;
143
+ readonly fg: Token;
144
+ readonly dot: Token;
145
+ };
146
+ readonly pink: {
147
+ readonly bg: Token;
148
+ readonly border: Token;
149
+ readonly fg: Token;
150
+ readonly dot: Token;
151
+ };
152
+ };
153
+ readonly radius: {
154
+ readonly base: Token;
155
+ };
156
+ readonly shadow: {
157
+ readonly "elevation-sm": Token;
158
+ readonly "elevation-lg": Token;
159
+ readonly "focus-ring": Token;
160
+ };
161
+ readonly ease: {
162
+ readonly rise: Token;
163
+ };
164
+ readonly font: {
165
+ readonly sans: Token;
166
+ readonly mono: Token;
167
+ };
168
+ readonly text: {
169
+ readonly label: {
170
+ readonly size: Token;
171
+ readonly "line-height": Token;
172
+ readonly weight: Token;
173
+ };
174
+ readonly body: {
175
+ readonly size: Token;
176
+ readonly "line-height": Token;
177
+ };
178
+ readonly strong: {
179
+ readonly size: Token;
180
+ readonly "line-height": Token;
181
+ readonly weight: Token;
182
+ readonly "letter-spacing": Token;
183
+ };
184
+ readonly read: {
185
+ readonly size: Token;
186
+ readonly "line-height": Token;
187
+ };
188
+ readonly title: {
189
+ readonly size: Token;
190
+ readonly "line-height": Token;
191
+ readonly weight: Token;
192
+ readonly "letter-spacing": Token;
193
+ };
194
+ readonly page: {
195
+ readonly size: Token;
196
+ readonly "line-height": Token;
197
+ readonly weight: Token;
198
+ readonly "letter-spacing": Token;
199
+ };
200
+ readonly doc: {
201
+ readonly size: Token;
202
+ readonly "line-height": Token;
203
+ readonly weight: Token;
204
+ readonly "letter-spacing": Token;
205
+ };
206
+ readonly metric: {
207
+ readonly size: Token;
208
+ readonly "line-height": Token;
209
+ readonly weight: Token;
210
+ readonly "letter-spacing": Token;
211
+ };
212
+ readonly micro: {
213
+ readonly size: Token;
214
+ readonly "line-height": Token;
215
+ readonly weight: Token;
216
+ readonly "letter-spacing": Token;
217
+ };
218
+ };
219
+ };