@sole-ui/react 0.0.0-stage → 0.2.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.
- package/README.md +98 -2
- package/dist/components/shell/app-frame.d.ts +14 -0
- package/dist/components/shell/app-frame.js +8 -0
- package/dist/components/shell/command-palette.d.ts +32 -0
- package/dist/components/shell/command-palette.js +30 -0
- package/dist/components/shell/index.d.ts +4 -0
- package/dist/components/shell/index.js +4 -0
- package/dist/components/shell/page.d.ts +25 -0
- package/dist/components/shell/page.js +22 -0
- package/dist/components/shell/sidebar.d.ts +81 -0
- package/dist/components/shell/sidebar.js +113 -0
- package/dist/components/sole/avatar.d.ts +19 -0
- package/dist/components/sole/avatar.js +37 -0
- package/dist/components/sole/banner.d.ts +24 -0
- package/dist/components/sole/banner.js +27 -0
- package/dist/components/sole/column-header.d.ts +15 -0
- package/dist/components/sole/column-header.js +15 -0
- package/dist/components/sole/copy-field.d.ts +10 -0
- package/dist/components/sole/copy-field.js +32 -0
- package/dist/components/sole/count-badge.d.ts +8 -0
- package/dist/components/sole/count-badge.js +8 -0
- package/dist/components/sole/field.d.ts +24 -0
- package/dist/components/sole/field.js +27 -0
- package/dist/components/sole/filters.d.ts +33 -0
- package/dist/components/sole/filters.js +48 -0
- package/dist/components/sole/index.d.ts +17 -0
- package/dist/components/sole/index.js +17 -0
- package/dist/components/sole/kbd.d.ts +7 -0
- package/dist/components/sole/kbd.js +14 -0
- package/dist/components/sole/label-tag.d.ts +18 -0
- package/dist/components/sole/label-tag.js +17 -0
- package/dist/components/sole/layout.d.ts +54 -0
- package/dist/components/sole/layout.js +48 -0
- package/dist/components/sole/logo.d.ts +13 -0
- package/dist/components/sole/logo.js +17 -0
- package/dist/components/sole/nav.d.ts +22 -0
- package/dist/components/sole/nav.js +32 -0
- package/dist/components/sole/person-chip.d.ts +8 -0
- package/dist/components/sole/person-chip.js +19 -0
- package/dist/components/sole/person-header.d.ts +21 -0
- package/dist/components/sole/person-header.js +22 -0
- package/dist/components/sole/pill.d.ts +9 -0
- package/dist/components/sole/pill.js +14 -0
- package/dist/components/sole/states.d.ts +48 -0
- package/dist/components/sole/states.js +53 -0
- package/dist/components/sole/table.d.ts +23 -0
- package/dist/components/sole/table.js +32 -0
- package/dist/components/ui/alert-dialog.d.ts +27 -0
- package/dist/components/ui/alert-dialog.js +33 -0
- package/dist/components/ui/badge.d.ts +11 -0
- package/dist/components/ui/badge.js +30 -0
- package/dist/components/ui/button.d.ts +13 -0
- package/dist/components/ui/button.js +49 -0
- package/dist/components/ui/calendar.d.ts +9 -0
- package/dist/components/ui/calendar.js +79 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/checkbox.js +11 -0
- package/dist/components/ui/command.d.ts +90 -0
- package/dist/components/ui/command.js +36 -0
- package/dist/components/ui/dialog.d.ts +18 -0
- package/dist/components/ui/dialog.js +33 -0
- package/dist/components/ui/dropdown-menu.d.ts +25 -0
- package/dist/components/ui/dropdown-menu.js +42 -0
- package/dist/components/ui/index.d.ts +23 -0
- package/dist/components/ui/index.js +23 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/input.js +12 -0
- package/dist/components/ui/label.d.ts +4 -0
- package/dist/components/ui/label.js +10 -0
- package/dist/components/ui/popover.d.ts +10 -0
- package/dist/components/ui/popover.js +22 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/scroll-area.js +14 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +30 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/separator.js +10 -0
- package/dist/components/ui/sheet.d.ts +14 -0
- package/dist/components/ui/sheet.js +35 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.js +10 -0
- package/dist/components/ui/sonner.d.ts +5 -0
- package/dist/components/ui/sonner.js +63 -0
- package/dist/components/ui/switch.d.ts +6 -0
- package/dist/components/ui/switch.js +10 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/tabs.js +24 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/textarea.js +9 -0
- package/dist/components/ui/toggle-group.d.ts +14 -0
- package/dist/components/ui/toggle-group.js +24 -0
- package/dist/components/ui/toggle.d.ts +12 -0
- package/dist/components/ui/toggle.js +28 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/components/ui/tooltip.js +18 -0
- package/dist/hq/activity-line.d.ts +21 -0
- package/dist/hq/activity-line.js +26 -0
- package/dist/hq/board-card.d.ts +12 -0
- package/dist/hq/board-card.js +21 -0
- package/dist/hq/board-chip.d.ts +12 -0
- package/dist/hq/board-chip.js +20 -0
- package/dist/hq/checklist-item.d.ts +14 -0
- package/dist/hq/checklist-item.js +13 -0
- package/dist/hq/comment.d.ts +26 -0
- package/dist/hq/comment.js +15 -0
- package/dist/hq/due-chip.d.ts +11 -0
- package/dist/hq/due-chip.js +25 -0
- package/dist/hq/group-header.d.ts +27 -0
- package/dist/hq/group-header.js +22 -0
- package/dist/hq/inbox-row.d.ts +22 -0
- package/dist/hq/inbox-row.js +13 -0
- package/dist/hq/index.d.ts +21 -0
- package/dist/hq/index.js +21 -0
- package/dist/hq/link-row.d.ts +13 -0
- package/dist/hq/link-row.js +13 -0
- package/dist/hq/mention-chip.d.ts +23 -0
- package/dist/hq/mention-chip.js +28 -0
- package/dist/hq/page-row.d.ts +26 -0
- package/dist/hq/page-row.js +30 -0
- package/dist/hq/priority-icon.d.ts +9 -0
- package/dist/hq/priority-icon.js +23 -0
- package/dist/hq/property-type-icon.d.ts +7 -0
- package/dist/hq/property-type-icon.js +22 -0
- package/dist/hq/recent-item.d.ts +11 -0
- package/dist/hq/recent-item.js +12 -0
- package/dist/hq/review-badge.d.ts +37 -0
- package/dist/hq/review-badge.js +77 -0
- package/dist/hq/state-icon.d.ts +12 -0
- package/dist/hq/state-icon.js +41 -0
- package/dist/hq/state-pill.d.ts +9 -0
- package/dist/hq/state-pill.js +18 -0
- package/dist/hq/sticky.d.ts +13 -0
- package/dist/hq/sticky.js +22 -0
- package/dist/hq/template-card.d.ts +10 -0
- package/dist/hq/template-card.js +9 -0
- package/dist/hq/types.d.ts +49 -0
- package/dist/hq/types.js +7 -0
- package/dist/hq/work-item-row.d.ts +16 -0
- package/dist/hq/work-item-row.js +23 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/lib/cn.d.ts +2 -0
- package/dist/lib/cn.js +15 -0
- package/dist/lib/colour.d.ts +8 -0
- package/dist/lib/colour.js +28 -0
- package/dist/lib/dates.d.ts +62 -0
- package/dist/lib/dates.js +182 -0
- package/dist/lib/hue.d.ts +3 -0
- package/dist/lib/hue.js +1 -0
- package/dist/lib/person.d.ts +16 -0
- package/dist/lib/person.js +17 -0
- package/dist/lib/platform.d.ts +4 -0
- package/dist/lib/platform.js +12 -0
- package/dist/lib/sole-provider.d.ts +35 -0
- package/dist/lib/sole-provider.js +52 -0
- package/dist/styles.css +64 -0
- package/dist/tokens.css +231 -0
- package/dist/tokens.generated.d.ts +219 -0
- package/dist/tokens.generated.js +679 -0
- package/dist/tokens.json +677 -0
- package/package.json +82 -4
|
@@ -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
|
+
}
|
package/dist/lib/hue.js
ADDED
|
@@ -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,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
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* The Sole look for Tailwind CSS v4. In your stylesheet:
|
|
2
|
+
|
|
3
|
+
@import "tailwindcss";
|
|
4
|
+
@import "@sole-ui/react/styles.css";
|
|
5
|
+
|
|
6
|
+
The tokens, the Tailwind mapping and the text-micro utility live in tokens.css, which
|
|
7
|
+
is generated from packages/tokens. This file adds the font, the base layer and the
|
|
8
|
+
reduced-motion rule, as Sole HQ has them. */
|
|
9
|
+
|
|
10
|
+
@import "tw-animate-css";
|
|
11
|
+
@import "@fontsource-variable/inter";
|
|
12
|
+
@import "./tokens.css";
|
|
13
|
+
|
|
14
|
+
/* Light only (D31, owner 29 Sep): no dark theme and no theme switch. Nothing ever sets
|
|
15
|
+
a .dark class, so this keeps any `dark:` utility a shadcn component brings with it
|
|
16
|
+
from switching on under the system's prefers-color-scheme. The dark values wait in
|
|
17
|
+
hq/design/README.md for later. */
|
|
18
|
+
@custom-variant dark (&:is(.dark *));
|
|
19
|
+
|
|
20
|
+
/* ------------------------------------------------------------------ base */
|
|
21
|
+
|
|
22
|
+
@layer base {
|
|
23
|
+
* {
|
|
24
|
+
@apply border-border outline-ring/50;
|
|
25
|
+
}
|
|
26
|
+
html {
|
|
27
|
+
-webkit-text-size-adjust: 100%;
|
|
28
|
+
text-size-adjust: 100%;
|
|
29
|
+
}
|
|
30
|
+
body {
|
|
31
|
+
@apply bg-background text-foreground font-sans text-body antialiased;
|
|
32
|
+
font-feature-settings: "cv11", "ss01";
|
|
33
|
+
}
|
|
34
|
+
/* Keys, dates, times and counts line up (brand-and-ux 1.4). */
|
|
35
|
+
.tnum,
|
|
36
|
+
time,
|
|
37
|
+
kbd {
|
|
38
|
+
font-variant-numeric: tabular-nums;
|
|
39
|
+
}
|
|
40
|
+
:focus-visible {
|
|
41
|
+
outline: none;
|
|
42
|
+
}
|
|
43
|
+
button:not(:disabled),
|
|
44
|
+
[role="button"]:not(:disabled) {
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Motion (5.6): colour changes 120ms, panels 200ms on the rise curve, no bounce, and
|
|
50
|
+
none of it for anyone who asked the system for less motion. */
|
|
51
|
+
@media (prefers-reduced-motion: reduce) {
|
|
52
|
+
*,
|
|
53
|
+
*::before,
|
|
54
|
+
*::after {
|
|
55
|
+
animation-duration: 0.01ms !important;
|
|
56
|
+
animation-iteration-count: 1 !important;
|
|
57
|
+
transition-duration: 0.01ms !important;
|
|
58
|
+
scroll-behavior: auto !important;
|
|
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 "./";
|