@hearthlog/ui 0.0.0-stage → 0.1.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.
- package/LICENSE +235 -0
- package/package.json +37 -3
- package/src/assets/fonts/fira-code-latin-ext-ui.woff2 +0 -0
- package/src/assets/fonts/fira-code-latin-ui.woff2 +0 -0
- package/src/assets/fonts/inter-latin-ext-ui.woff2 +0 -0
- package/src/assets/fonts/inter-latin-ui.woff2 +0 -0
- package/src/components/ConfirmDialog.svelte +104 -0
- package/src/components/Drawer.svelte +135 -0
- package/src/components/EmptyState.svelte +33 -0
- package/src/components/JsonViewer.svelte +113 -0
- package/src/components/Modal.svelte +76 -0
- package/src/components/NotificationCenter.svelte +106 -0
- package/src/components/PageHeader.svelte +26 -0
- package/src/components/commands/CommandConfirmDialog.svelte +32 -0
- package/src/components/commands/CommandDrawer.svelte +31 -0
- package/src/components/commands/CommandFieldset.svelte +18 -0
- package/src/components/commands/CommandModal.svelte +25 -0
- package/src/components/commands/CommandOnly.svelte +26 -0
- package/src/components/data/DataPlaceholder.svelte +8 -0
- package/src/components/data/DataRegion.svelte +8 -0
- package/src/components/data/DataValue.svelte +11 -0
- package/src/components/data-table/DataTable.svelte +139 -0
- package/src/components/data-table/DataTableHeader.svelte +78 -0
- package/src/components/data-table/DataTablePagination.svelte +91 -0
- package/src/components/data-table/table.ts +30 -0
- package/src/components/data-table/types.ts +63 -0
- package/src/components/endless/DayPager.svelte +355 -0
- package/src/components/endless/EndlessList.svelte +498 -0
- package/src/components/endless/bucket-store.svelte.ts +81 -0
- package/src/components/json-viewer.ts +49 -0
- package/src/components/ui/Amount.svelte +17 -0
- package/src/components/ui/Badge.svelte +40 -0
- package/src/components/ui/Button.svelte +53 -0
- package/src/components/ui/Checkbox.svelte +29 -0
- package/src/components/ui/ChoiceChips.svelte +53 -0
- package/src/components/ui/CollectionToolbar.svelte +29 -0
- package/src/components/ui/DatePicker.svelte +375 -0
- package/src/components/ui/DateTimePicker.svelte +16 -0
- package/src/components/ui/Field.svelte +52 -0
- package/src/components/ui/FoldLine.svelte +38 -0
- package/src/components/ui/Input.svelte +21 -0
- package/src/components/ui/Label.svelte +18 -0
- package/src/components/ui/Metric.svelte +34 -0
- package/src/components/ui/MonthNavigator.svelte +194 -0
- package/src/components/ui/Panel.svelte +37 -0
- package/src/components/ui/RouteTabs.svelte +57 -0
- package/src/components/ui/Select.svelte +93 -0
- package/src/components/ui/Switch.svelte +41 -0
- package/src/components/ui/Textarea.svelte +19 -0
- package/src/components/ui/Tooltip.svelte +53 -0
- package/src/components/ui/tab-indicator.ts +84 -0
- package/src/state/commands.ts +22 -0
- package/src/state/notifications.svelte.ts +49 -0
- package/src/state/page-context.ts +9 -0
- package/src/state/readiness.ts +20 -0
- package/src/state/sheet-stack.svelte.ts +49 -0
- package/src/styles/base.css +243 -0
- package/src/styles/index.css +13 -0
- package/src/styles/recipes.css +174 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/endless-axis.ts +153 -0
- package/src/utils/number-format.ts +82 -0
- package/README.md +0 -3
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys and indexes for endless lists. An axis lists keys that sort in order
|
|
3
|
+
* (`yyyy-MM` months, for example) and maps each one to its place in the list,
|
|
4
|
+
* so a virtualized list can span a century without building it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The keys an endless list holds, in order. */
|
|
8
|
+
export interface EndlessAxis {
|
|
9
|
+
/** How many keys the list holds. */
|
|
10
|
+
readonly count: number;
|
|
11
|
+
/** The key at an index. */
|
|
12
|
+
keyAt(index: number): string;
|
|
13
|
+
/** The index of a key, clamped into the list. */
|
|
14
|
+
indexOf(key: string): number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Keys a whole number of steps from `base`, from step `first` to step `last`.
|
|
19
|
+
* `shift` moves a key by steps and `steps` counts them from the base.
|
|
20
|
+
*/
|
|
21
|
+
export function stepAxis(
|
|
22
|
+
base: string,
|
|
23
|
+
first: number,
|
|
24
|
+
last: number,
|
|
25
|
+
shift: (key: string, steps: number) => string,
|
|
26
|
+
steps: (key: string, base: string) => number,
|
|
27
|
+
): EndlessAxis {
|
|
28
|
+
const count = last - first + 1;
|
|
29
|
+
return {
|
|
30
|
+
count,
|
|
31
|
+
keyAt: (index) => shift(base, index + first),
|
|
32
|
+
indexOf: (key) => Math.max(0, Math.min(count - 1, steps(key, base) - first)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** How a list axis orders its keys. */
|
|
37
|
+
export interface ListAxisOptions {
|
|
38
|
+
/** Keys read newest first, so the top of the list is the latest key. */
|
|
39
|
+
descending?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Orders keys when plain string order is not list order, as with the
|
|
42
|
+
* Gallery's months that split into parts. Takes precedence over
|
|
43
|
+
* `descending`.
|
|
44
|
+
*/
|
|
45
|
+
compare?: (left: string, right: string) => number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compareStrings(left: string, right: string): number {
|
|
49
|
+
if (left === right) return 0;
|
|
50
|
+
return left < right ? -1 : 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Keys read from data (months that hold photos, for example), in list order.
|
|
55
|
+
* A key it does not list finds the listed key that would follow it, or the
|
|
56
|
+
* last one.
|
|
57
|
+
*/
|
|
58
|
+
export function listAxis(keys: readonly string[], { descending = false, compare }: ListAxisOptions = {}): EndlessAxis {
|
|
59
|
+
const order = compare ?? (descending ? (left: string, right: string) => compareStrings(right, left) : compareStrings);
|
|
60
|
+
return {
|
|
61
|
+
count: keys.length,
|
|
62
|
+
keyAt: (index) => keys[index],
|
|
63
|
+
indexOf: (key) => {
|
|
64
|
+
let low = 0;
|
|
65
|
+
let high = keys.length;
|
|
66
|
+
while (low < high) {
|
|
67
|
+
const middle = (low + high) >> 1;
|
|
68
|
+
if (order(keys[middle], key) < 0) low = middle + 1;
|
|
69
|
+
else high = middle;
|
|
70
|
+
}
|
|
71
|
+
return Math.max(0, Math.min(keys.length - 1, low));
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The nearest key an axis lists. */
|
|
77
|
+
export function nearestKey(axis: EndlessAxis, key: string): string {
|
|
78
|
+
return axis.keyAt(axis.indexOf(key));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------- months
|
|
82
|
+
|
|
83
|
+
export function monthKeyOf(day: string): string {
|
|
84
|
+
return day.slice(0, 7);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function shiftMonthKey(key: string, offset: number): string {
|
|
88
|
+
const total = Number(key.slice(0, 4)) * 12 + Number(key.slice(5, 7)) - 1 + offset;
|
|
89
|
+
return `${String(Math.floor(total / 12)).padStart(4, '0')}-${String((total % 12 + 12) % 12 + 1).padStart(2, '0')}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Whole months from `base` to `key`. */
|
|
93
|
+
export function monthOffset(key: string, base: string): number {
|
|
94
|
+
return (Number(key.slice(0, 4)) - Number(base.slice(0, 4))) * 12 + Number(key.slice(5, 7)) - Number(base.slice(5, 7));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Months around `base`, from `first` to `last`. An open end (null) runs
|
|
99
|
+
* `span` months from the base, and no end runs further than that.
|
|
100
|
+
*/
|
|
101
|
+
export function monthAxis(base: string, range: { first: string | null; last: string | null; span: number }): EndlessAxis {
|
|
102
|
+
const first = range.first ? Math.max(-range.span, monthOffset(range.first, base)) : -range.span;
|
|
103
|
+
const last = range.last ? Math.min(range.span, monthOffset(range.last, base)) : range.span;
|
|
104
|
+
return stepAxis(base, first, last, shiftMonthKey, monthOffset);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------- days and weeks
|
|
108
|
+
|
|
109
|
+
const DAY_MS = 86_400_000;
|
|
110
|
+
|
|
111
|
+
/** Days since 1970-01-01 for a `yyyy-MM-dd` key, free of time zones. */
|
|
112
|
+
function dayNumber(day: string): number {
|
|
113
|
+
return Date.UTC(Number(day.slice(0, 4)), Number(day.slice(5, 7)) - 1, Number(day.slice(8, 10))) / DAY_MS;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function dayKey(number: number): string {
|
|
117
|
+
return new Date(number * DAY_MS).toISOString().slice(0, 10);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function shiftDay(day: string, offset: number): string {
|
|
121
|
+
return dayKey(dayNumber(day) + offset);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Whole days from `base` to `day`. */
|
|
125
|
+
export function dayOffset(day: string, base: string): number {
|
|
126
|
+
return dayNumber(day) - dayNumber(base);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Days keyed `yyyy-MM-dd`, from `first` days before `base` to `last` days after it. */
|
|
130
|
+
export function dayAxis(base: string, first: number, last: number): EndlessAxis {
|
|
131
|
+
return stepAxis(base, -first, last, shiftDay, dayOffset);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The Monday starting the week that holds `day`. */
|
|
135
|
+
export function weekStartOf(day: string): string {
|
|
136
|
+
const number = dayNumber(day);
|
|
137
|
+
// 1970-01-01 was a Thursday, three days after a Monday.
|
|
138
|
+
return dayKey(number - ((number + 3) % 7 + 7) % 7);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function shiftWeek(key: string, offset: number): string {
|
|
142
|
+
return shiftDay(key, offset * 7);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Whole weeks from the week starting `base` to the week holding `day`. */
|
|
146
|
+
export function weekOffset(day: string, base: string): number {
|
|
147
|
+
return Math.round((dayNumber(weekStartOf(day)) - dayNumber(base)) / 7);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Weeks keyed by their Monday, `span` weeks either side of the week starting `base`. */
|
|
151
|
+
export function weekAxis(base: string, span: number): EndlessAxis {
|
|
152
|
+
return stepAxis(base, -span, span, shiftWeek, weekOffset);
|
|
153
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared number and money formatting.
|
|
3
|
+
*
|
|
4
|
+
* One Czech locale for every figure in the app, a true minus sign (U+2212),
|
|
5
|
+
* and decimals only when an amount has a fractional part, so summaries read
|
|
6
|
+
* `42 180 Kč` while `1 234,50 Kč` keeps its haléře.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const MINUS = '−';
|
|
10
|
+
|
|
11
|
+
const cache = new Map<string, Intl.NumberFormat>();
|
|
12
|
+
|
|
13
|
+
function formatter(options: Intl.NumberFormatOptions): Intl.NumberFormat {
|
|
14
|
+
const key = JSON.stringify(options);
|
|
15
|
+
let cached = cache.get(key);
|
|
16
|
+
if (!cached) {
|
|
17
|
+
cached = new Intl.NumberFormat('cs-CZ', options);
|
|
18
|
+
cache.set(key, cached);
|
|
19
|
+
}
|
|
20
|
+
return cached;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fractionDigits(value: number, decimals: 'auto' | 'always' | 'never'): number {
|
|
24
|
+
if (decimals === 'always') return 2;
|
|
25
|
+
if (decimals === 'never') return 0;
|
|
26
|
+
return Number.isInteger(Math.round(value * 100) / 100) ? 0 : 2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function withSign(text: string, value: number, signed: boolean): string {
|
|
30
|
+
const unsigned = text.replace(/^[-−]/, '');
|
|
31
|
+
if (value < 0 && unsigned !== text) return `${MINUS}${unsigned}`;
|
|
32
|
+
if (signed && value > 0) return `+${unsigned}`;
|
|
33
|
+
return unsigned;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AmountOptions {
|
|
37
|
+
/** `auto` shows two decimals only for fractional amounts. */
|
|
38
|
+
decimals?: 'auto' | 'always' | 'never';
|
|
39
|
+
/** Prefix positive values with `+`. */
|
|
40
|
+
signed?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `42 180`, `−480`, `+1 234,50` — an amount without its currency unit. */
|
|
44
|
+
export function formatAmount(value: number, { decimals = 'auto', signed = false }: AmountOptions = {}): string {
|
|
45
|
+
const digits = fractionDigits(value, decimals);
|
|
46
|
+
const text = formatter({ minimumFractionDigits: digits, maximumFractionDigits: digits }).format(value);
|
|
47
|
+
return withSign(text, value, signed);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `42 180 Kč`, `−480 Kč` — an amount with its currency. */
|
|
51
|
+
export function formatMoney(value: number, options: AmountOptions & { currency?: string } = {}): string {
|
|
52
|
+
const { currency = 'CZK', decimals = 'auto', signed = false } = options;
|
|
53
|
+
const digits = fractionDigits(value, decimals);
|
|
54
|
+
const text = formatter({ style: 'currency', currency, minimumFractionDigits: digits, maximumFractionDigits: digits }).format(value);
|
|
55
|
+
return withSign(text, value, signed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `6 420`, `79,3` — plain counts and measurements in the app locale. */
|
|
59
|
+
export function formatNumber(value: number, maximumFractionDigits = 0): string {
|
|
60
|
+
return withSign(formatter({ maximumFractionDigits }).format(value), value, false);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `80,0`, `−1,4`, `+0,3` — always `digits` decimals, so a column of weights lines up. */
|
|
64
|
+
export function formatFixed(value: number, digits = 1, { signed = false }: { signed?: boolean } = {}): string {
|
|
65
|
+
const text = formatter({ minimumFractionDigits: digits, maximumFractionDigits: digits }).format(value);
|
|
66
|
+
// A value that rounds to zero carries no sign.
|
|
67
|
+
if (/^[-−]?0(?:[.,]0+)?$/.test(text)) return text.replace(/^[-−]/, '');
|
|
68
|
+
return withSign(text, value, signed);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Human file size for stored originals; em dash without a size. */
|
|
72
|
+
export function formatFileSize(bytes: number): string {
|
|
73
|
+
if (!bytes) return '—';
|
|
74
|
+
const units = ['B', 'KiB', 'MiB'];
|
|
75
|
+
let value = bytes;
|
|
76
|
+
let unit = 0;
|
|
77
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
78
|
+
value /= 1024;
|
|
79
|
+
unit += 1;
|
|
80
|
+
}
|
|
81
|
+
return `${unit === 0 || value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
82
|
+
}
|
package/README.md
DELETED