@hashsome/ui 0.8.0 → 0.9.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/dist/.tsbuildinfo +1 -1
- package/dist/entities/attention-dot.d.ts +4 -0
- package/dist/entities/attention-dot.js +12 -0
- package/dist/entities/media-browser.js +12 -1
- package/dist/entities/nav-dock.js +2 -1
- package/dist/entities/nav-rail.d.ts +2 -0
- package/dist/entities/nav-rail.js +4 -2
- package/dist/entities/top-bar.d.ts +1 -1
- package/dist/entities/top-bar.js +24 -11
- package/dist/gallery/gallery.js +27 -1
- package/dist/gallery/props-data.js +106 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/layout/check-mark.d.ts +5 -0
- package/dist/layout/check-mark.js +9 -0
- package/dist/layout/confirm-dialog.d.ts +28 -0
- package/dist/layout/confirm-dialog.js +62 -0
- package/dist/layout/debug-menu.js +10 -5
- package/dist/layout/spinner.d.ts +4 -1
- package/dist/layout/spinner.js +3 -8
- package/dist/layout/task-tile.d.ts +28 -0
- package/dist/layout/task-tile.js +78 -0
- package/dist/layout/use-timed.d.ts +7 -0
- package/dist/layout/use-timed.js +22 -0
- package/dist/theme/grid.d.ts +3 -0
- package/dist/theme/grid.js +3 -0
- package/dist/theme/index.js +3 -0
- package/dist/theme/tokens.d.ts +4 -0
- package/package.json +2 -2
package/dist/layout/spinner.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PaletteKey } from 'e-prim';
|
|
1
2
|
/** A turning ring: something is in progress and will not say how long it takes. Kept turning when the
|
|
2
3
|
* app is set to reduced motion (`data-keep-motion`): a spinner that stopped would look stuck, and one
|
|
3
4
|
* small turning icon costs next to nothing. */
|
|
@@ -8,6 +9,8 @@ export declare function Spinner({ size, label }: {
|
|
|
8
9
|
/** A ring that turns around the edge of a round button (give the button `position: relative`): the
|
|
9
10
|
* button is waiting for an answer and will not take another press. Kept turning under reduced motion
|
|
10
11
|
* for the same reason `Spinner` is. */
|
|
11
|
-
export declare function SpinnerRing({ label }: {
|
|
12
|
+
export declare function SpinnerRing({ label, color, }: {
|
|
12
13
|
label?: string;
|
|
14
|
+
/** A palette color: the theme's accent by default. Pass `accentText` for a button that is itself accent-colored (or filled with a color of the app's own). */
|
|
15
|
+
color?: PaletteKey;
|
|
13
16
|
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
package/dist/layout/spinner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
2
2
|
/** @jsxImportSource @emotion/react */
|
|
3
3
|
import { keyframes } from '@emotion/react';
|
|
4
|
+
import { Box } from 'e-prim';
|
|
4
5
|
import { Icon } from '../icon.js';
|
|
5
6
|
const turn = keyframes({ to: { transform: 'rotate(360deg)' } });
|
|
6
7
|
/** A turning ring: something is in progress and will not say how long it takes. Kept turning when the
|
|
@@ -17,12 +18,6 @@ export function Spinner({ size = 16, label = 'Loading' }) {
|
|
|
17
18
|
/** A ring that turns around the edge of a round button (give the button `position: relative`): the
|
|
18
19
|
* button is waiting for an answer and will not take another press. Kept turning under reduced motion
|
|
19
20
|
* for the same reason `Spinner` is. */
|
|
20
|
-
export function SpinnerRing({ label = 'Loading' }) {
|
|
21
|
-
return (_jsx("span",
|
|
22
|
-
position: 'absolute',
|
|
23
|
-
inset: 0,
|
|
24
|
-
pointerEvents: 'none',
|
|
25
|
-
color: palette.accent,
|
|
26
|
-
animation: `${turn} 0.9s linear infinite`,
|
|
27
|
-
}), children: _jsx("svg", { viewBox: "0 0 100 100", width: "100%", height: "100%", "aria-hidden": "true", children: _jsx("circle", { cx: "50", cy: "50", r: "47", fill: "none", stroke: "currentColor", strokeWidth: "5", strokeLinecap: "round", strokeDasharray: "75 221" }) }) }));
|
|
21
|
+
export function SpinnerRing({ label = 'Loading', color = 'accent', }) {
|
|
22
|
+
return (_jsx(Box, { as: "span", role: "status", "aria-label": label, "data-keep-motion": true, position: "absolute", color: color, css: { inset: 0, pointerEvents: 'none', animation: `${turn} 0.9s linear infinite` }, children: _jsx("svg", { viewBox: "0 0 100 100", width: "100%", height: "100%", "aria-hidden": "true", children: _jsx("circle", { cx: "50", cy: "50", r: "47", fill: "none", stroke: "currentColor", strokeWidth: "5", strokeLinecap: "round", strokeDasharray: "75 221" }) }) }));
|
|
28
23
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IconName } from '../icon-data.ts';
|
|
2
|
+
import type { EntityStatus } from '../status.ts';
|
|
3
|
+
/** Where a recurring task stands: not due for a while, due before long, or past its date. */
|
|
4
|
+
export type TaskState = 'ok' | 'dueSoon' | 'overdue';
|
|
5
|
+
export interface TaskTileProps {
|
|
6
|
+
/** What is to be done, e.g. `Septic additive`. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Icon id, e.g. `lu:wrench`. */
|
|
9
|
+
icon?: IconName;
|
|
10
|
+
/** Where the task stands. Whoever has the task decides it: how soon is "soon" is different for a weekly task and a yearly one. */
|
|
11
|
+
state: TaskState;
|
|
12
|
+
/** When it is next due. Shown as "Due in 2 days" or "Overdue by 3 days". */
|
|
13
|
+
dueAt?: Date | undefined;
|
|
14
|
+
/** When it was last done, shown in the question that marks it done again. */
|
|
15
|
+
lastDoneAt?: Date | undefined;
|
|
16
|
+
/** Status of the thing behind it, as for `Tile`: anything but `ready` shows why instead and disables the tile. */
|
|
17
|
+
status?: EntityStatus;
|
|
18
|
+
/** The person confirmed it is done, on this day (today unless they chose another, in case it was forgotten at the time). Rejecting shows the error in the dialog and leaves it open. */
|
|
19
|
+
onComplete: (doneOn: Date) => void | Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** The line under a task's name. */
|
|
22
|
+
export declare function taskLine(state: TaskState, dueAt: Date | undefined, now: Date): string;
|
|
23
|
+
/**
|
|
24
|
+
* A recurring task as a tile: its name, where it stands (up to date, due soon, overdue) and when it is due.
|
|
25
|
+
* Tapping it asks whether it is done, with the day it was done on (today, or an earlier one if it was
|
|
26
|
+
* forgotten), and only a confirmation calls `onComplete`, so a stray tap does nothing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function TaskTile({ label, icon, state, dueAt, lastDoneAt, status, onComplete, }: TaskTileProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import { Flex, Typography } from 'e-prim';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { ConfirmDialog } from './confirm-dialog.js';
|
|
6
|
+
import { IconButton, Tile } from './tile.js';
|
|
7
|
+
const DAY_MS = 86_400_000;
|
|
8
|
+
const startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
9
|
+
/** How many calendar days from `now` to `then`: negative when `then` has passed. */
|
|
10
|
+
const daysTo = (then, now) => Math.round((startOfDay(then).getTime() - startOfDay(now).getTime()) / DAY_MS);
|
|
11
|
+
const plural = (n) => `${n} day${n === 1 ? '' : 's'}`;
|
|
12
|
+
const short = (date) => date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
|
|
13
|
+
/** The line under a task's name. */
|
|
14
|
+
export function taskLine(state, dueAt, now) {
|
|
15
|
+
if (!dueAt) {
|
|
16
|
+
return state === 'overdue' ? 'Overdue' : state === 'dueSoon' ? 'Due soon' : 'Up to date';
|
|
17
|
+
}
|
|
18
|
+
const days = daysTo(dueAt, now);
|
|
19
|
+
if (state === 'ok') {
|
|
20
|
+
return `Next ${short(dueAt)}`;
|
|
21
|
+
}
|
|
22
|
+
if (days === 0) {
|
|
23
|
+
return 'Due today';
|
|
24
|
+
}
|
|
25
|
+
if (days > 0) {
|
|
26
|
+
return days === 1 ? 'Due tomorrow' : `Due in ${plural(days)}`;
|
|
27
|
+
}
|
|
28
|
+
return `Overdue by ${plural(-days)}`;
|
|
29
|
+
}
|
|
30
|
+
/** The palette color of each state's line. */
|
|
31
|
+
const TONE = { ok: 'textMuted', dueSoon: 'warm', overdue: 'danger' };
|
|
32
|
+
/**
|
|
33
|
+
* A recurring task as a tile: its name, where it stands (up to date, due soon, overdue) and when it is due.
|
|
34
|
+
* Tapping it asks whether it is done, with the day it was done on (today, or an earlier one if it was
|
|
35
|
+
* forgotten), and only a confirmation calls `onComplete`, so a stray tap does nothing.
|
|
36
|
+
*/
|
|
37
|
+
export function TaskTile({ label, icon, state, dueAt, lastDoneAt, status = 'ready', onComplete, }) {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
// How many days before today it was done: 0 is today.
|
|
40
|
+
const [daysAgo, setDaysAgo] = useState(0);
|
|
41
|
+
const [busy, setBusy] = useState(false);
|
|
42
|
+
const [error, setError] = useState();
|
|
43
|
+
// The words under the name follow the clock: "Due tomorrow" becomes "Due today" at midnight.
|
|
44
|
+
const [now, setNow] = useState(() => new Date());
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const id = setInterval(() => setNow(new Date()), 60_000);
|
|
47
|
+
return () => clearInterval(id);
|
|
48
|
+
}, []);
|
|
49
|
+
const ask = () => {
|
|
50
|
+
setDaysAgo(0);
|
|
51
|
+
setError(undefined);
|
|
52
|
+
setOpen(true);
|
|
53
|
+
};
|
|
54
|
+
const confirm = async () => {
|
|
55
|
+
setBusy(true);
|
|
56
|
+
try {
|
|
57
|
+
await onComplete(daysAgo === 0 ? new Date() : startOfDay(new Date(now.getTime() - daysAgo * DAY_MS)));
|
|
58
|
+
setOpen(false);
|
|
59
|
+
}
|
|
60
|
+
catch (failure) {
|
|
61
|
+
setError(failure instanceof Error ? failure.message : String(failure));
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
setBusy(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tile, { label: label, ...(icon ? { icon } : {}), status: status, onPress: ask, secondary: _jsx(Typography, { as: "span", variant: "secondary", css: ({ colorByKey }) => ({ color: colorByKey(TONE[state]) }), children: taskLine(state, dueAt, now) }) }), _jsx(ConfirmDialog, { open: open, title: `Mark “${label}” as done?`, description: lastDoneAt ? `Last done ${short(lastDoneAt)}.` : undefined, confirmLabel: "Mark as done", busy: busy, onConfirm: () => void confirm(), onCancel: () => setOpen(false), children: _jsxs(Flex, { direction: "column", gap: 2, children: [_jsx(DayStepper, { label: "Done on", daysAgo: daysAgo, now: now, onChange: setDaysAgo }), error ? (_jsx(Typography, { as: "span", variant: "secondary", color: "danger", role: "alert", children: error })) : null] }) })] }));
|
|
68
|
+
}
|
|
69
|
+
/** A day chosen by stepping back from today one at a time, which is what is wanted for a task that was done a day or two ago and is quicker, and far smaller, than a calendar. It cannot go past today. */
|
|
70
|
+
function DayStepper({ label, daysAgo, now, onChange, }) {
|
|
71
|
+
const day = new Date(now.getTime() - daysAgo * DAY_MS);
|
|
72
|
+
const words = daysAgo === 0
|
|
73
|
+
? 'Today'
|
|
74
|
+
: daysAgo === 1
|
|
75
|
+
? 'Yesterday'
|
|
76
|
+
: day.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' });
|
|
77
|
+
return (_jsxs(Flex, { align: "center", justify: "space-between", gap: 3, role: "group", "aria-label": label, children: [_jsx(Typography, { as: "span", variant: "secondary", color: "textMuted", children: label }), _jsxs(Flex, { align: "center", gap: 2, children: [_jsx(IconButton, { icon: "lu:chevron-left", label: "Previous day", glyph: 16, onClick: () => onChange(daysAgo + 1) }), _jsx(Typography, { as: "span", variant: "bodyStrong", css: { minWidth: 96, textAlign: 'center' }, children: words }), _jsx(IconButton, { icon: "lu:chevron-right", label: "Next day", glyph: 16, disabled: daysAgo === 0, onClick: () => onChange(daysAgo - 1) })] })] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Whether something that plays for a fixed time is playing. `start` begins it, and it is over `ms`
|
|
2
|
+
* later whatever else happens; starting it while it plays does nothing, so a press cannot be repeated until
|
|
3
|
+
* the animation it set off is over. */
|
|
4
|
+
export declare function useTimed(ms: number): {
|
|
5
|
+
active: boolean;
|
|
6
|
+
start: () => boolean;
|
|
7
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
/** Whether something that plays for a fixed time is playing. `start` begins it, and it is over `ms`
|
|
3
|
+
* later whatever else happens; starting it while it plays does nothing, so a press cannot be repeated until
|
|
4
|
+
* the animation it set off is over. */
|
|
5
|
+
export function useTimed(ms) {
|
|
6
|
+
const [active, setActive] = useState(false);
|
|
7
|
+
const timer = useRef(undefined);
|
|
8
|
+
// Cleared with the component: a timer left running would set state on what is gone.
|
|
9
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
10
|
+
const start = () => {
|
|
11
|
+
if (timer.current !== undefined) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
setActive(true);
|
|
15
|
+
timer.current = setTimeout(() => {
|
|
16
|
+
timer.current = undefined;
|
|
17
|
+
setActive(false);
|
|
18
|
+
}, ms);
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
return { active, start };
|
|
22
|
+
}
|
package/dist/theme/grid.d.ts
CHANGED
|
@@ -28,3 +28,6 @@ export declare function centeringOffsets(available: number, module: number): {
|
|
|
28
28
|
top: number;
|
|
29
29
|
bottom: number;
|
|
30
30
|
};
|
|
31
|
+
/** The width of the page navigation rail (`NavRail`), whose icons are centered in it. Things that sit at the
|
|
32
|
+
* left edge of the screen over a dashboard (the debug button) are centered on the same line. */
|
|
33
|
+
export declare const NAV_RAIL = 76;
|
package/dist/theme/grid.js
CHANGED
|
@@ -36,3 +36,6 @@ export function centeringOffsets(available, module) {
|
|
|
36
36
|
const top = Math.floor(leftover / 2);
|
|
37
37
|
return { top, bottom: leftover - top };
|
|
38
38
|
}
|
|
39
|
+
/** The width of the page navigation rail (`NavRail`), whose icons are centered in it. Things that sit at the
|
|
40
|
+
* left edge of the screen over a dashboard (the debug button) are centered on the same line. */
|
|
41
|
+
export const NAV_RAIL = 76;
|
package/dist/theme/index.js
CHANGED
|
@@ -35,6 +35,7 @@ const shared = {
|
|
|
35
35
|
scrim: 20,
|
|
36
36
|
drawer: 21,
|
|
37
37
|
popover: 30,
|
|
38
|
+
modal: 40,
|
|
38
39
|
},
|
|
39
40
|
shadow: {
|
|
40
41
|
drawer: '0 20px 48px rgba(0, 0, 0, 0.5)',
|
|
@@ -81,6 +82,7 @@ export const darkTheme = {
|
|
|
81
82
|
onAccent: '#1B1B1F',
|
|
82
83
|
warm: '#E3B341',
|
|
83
84
|
danger: '#F2554A',
|
|
85
|
+
success: '#34D399',
|
|
84
86
|
line: '#726C6A',
|
|
85
87
|
rail: '#141216',
|
|
86
88
|
border: 'rgba(255, 255, 255, 0.09)',
|
|
@@ -100,6 +102,7 @@ export const lightTheme = {
|
|
|
100
102
|
onAccent: '#1B1B1F',
|
|
101
103
|
warm: '#E3B341',
|
|
102
104
|
danger: '#F2554A',
|
|
105
|
+
success: '#34D399',
|
|
103
106
|
line: '#4A4540',
|
|
104
107
|
rail: '#EAE4DA',
|
|
105
108
|
border: 'rgba(0, 0, 0, 0.08)',
|
package/dist/theme/tokens.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ declare module 'e-prim' {
|
|
|
20
20
|
onAccent: string;
|
|
21
21
|
warm: string;
|
|
22
22
|
danger: string;
|
|
23
|
+
/** Something fine or connected (a status dot, a healthy link). */
|
|
24
|
+
success: string;
|
|
23
25
|
line: string;
|
|
24
26
|
/** The left nav rail / bottom nav dock's background — a third near-black, distinct from
|
|
25
27
|
* `bg`/`surface`. */
|
|
@@ -54,6 +56,8 @@ declare module 'e-prim' {
|
|
|
54
56
|
dropdown: number;
|
|
55
57
|
/** A floating panel anchored to a control (the date picker's calendar), above what it sits in. */
|
|
56
58
|
popover: number;
|
|
59
|
+
/** A dialog (`ConfirmDialog`) and the scrim behind it, over everything else on the page. */
|
|
60
|
+
modal: number;
|
|
57
61
|
}
|
|
58
62
|
interface TTypography {
|
|
59
63
|
/** A tile's own name (`Tile`'s label). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hashsome/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Design system components and hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@emotion/react": "11.14.0",
|
|
19
|
-
"@hashsome/core": "0.
|
|
19
|
+
"@hashsome/core": "0.9.0",
|
|
20
20
|
"e-prim": "2.0.1",
|
|
21
21
|
"motion": "13.4.5",
|
|
22
22
|
"react": "19.3.0",
|