@poodle64/ui 2026.8.15 → 2026.8.17
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 +300 -11
- package/dist/components/ui/form/form-button.svelte +7 -0
- package/dist/components/ui/form/form-button.svelte.d.ts +4 -0
- package/dist/components/ui/form/form-description.svelte +18 -0
- package/dist/components/ui/form/form-description.svelte.d.ts +4 -0
- package/dist/components/ui/form/form-element-field.svelte +25 -0
- package/dist/components/ui/form/form-element-field.svelte.d.ts +28 -0
- package/dist/components/ui/form/form-field-errors.svelte +31 -0
- package/dist/components/ui/form/form-field-errors.svelte.d.ts +8 -0
- package/dist/components/ui/form/form-field.svelte +25 -0
- package/dist/components/ui/form/form-field.svelte.d.ts +28 -0
- package/dist/components/ui/form/form-fieldset.svelte +16 -0
- package/dist/components/ui/form/form-fieldset.svelte.d.ts +27 -0
- package/dist/components/ui/form/form-label.svelte +25 -0
- package/dist/components/ui/form/form-label.svelte.d.ts +4 -0
- package/dist/components/ui/form/form-legend.svelte +17 -0
- package/dist/components/ui/form/form-legend.svelte.d.ts +4 -0
- package/dist/components/ui/form/index.d.ts +11 -0
- package/dist/components/ui/form/index.js +13 -0
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/page-header/page-header.svelte +70 -33
- package/dist/components/ui/page-header/page-header.svelte.d.ts +4 -0
- package/dist/format.d.ts +222 -0
- package/dist/format.js +422 -0
- package/dist/styles.css +179 -5
- package/package.json +16 -2
- package/registry/component-map.json +74 -4
- package/registry/component-map.md +18 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as FormPrimitive from 'formsnap';
|
|
3
|
+
import { Label } from '../label/index.js';
|
|
4
|
+
import { cn } from '../../../utils.js';
|
|
5
|
+
import type { WithoutChild } from '../../../utils.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
ref = $bindable(null),
|
|
9
|
+
children,
|
|
10
|
+
class: className,
|
|
11
|
+
...restProps
|
|
12
|
+
}: WithoutChild<FormPrimitive.LabelProps> = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<FormPrimitive.Label {...restProps} bind:ref>
|
|
16
|
+
{#snippet child({ props })}
|
|
17
|
+
<Label
|
|
18
|
+
{...props}
|
|
19
|
+
data-slot="form-label"
|
|
20
|
+
class={cn('data-[fs-error]:text-destructive', className)}
|
|
21
|
+
>
|
|
22
|
+
{@render children?.()}
|
|
23
|
+
</Label>
|
|
24
|
+
{/snippet}
|
|
25
|
+
</FormPrimitive.Label>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as FormPrimitive from 'formsnap';
|
|
3
|
+
import { cn } from '../../../utils.js';
|
|
4
|
+
import type { WithoutChild } from '../../../utils.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
ref = $bindable(null),
|
|
8
|
+
class: className,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithoutChild<FormPrimitive.LegendProps> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<FormPrimitive.Legend
|
|
14
|
+
bind:ref
|
|
15
|
+
class={cn('data-[fs-error]:text-destructive text-sm leading-none font-medium', className)}
|
|
16
|
+
{...restProps}
|
|
17
|
+
/>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as FormPrimitive from 'formsnap';
|
|
2
|
+
import Description from './form-description.svelte';
|
|
3
|
+
import Label from './form-label.svelte';
|
|
4
|
+
import FieldErrors from './form-field-errors.svelte';
|
|
5
|
+
import Field from './form-field.svelte';
|
|
6
|
+
import Fieldset from './form-fieldset.svelte';
|
|
7
|
+
import Legend from './form-legend.svelte';
|
|
8
|
+
import ElementField from './form-element-field.svelte';
|
|
9
|
+
import Button from './form-button.svelte';
|
|
10
|
+
declare const Control: import("svelte").Component<FormPrimitive.ControlProps, {}, "">;
|
|
11
|
+
export { Field, Control, Label, Button, FieldErrors, Description, Fieldset, Legend, ElementField, Field as FormField, Control as FormControl, Description as FormDescription, Label as FormLabel, FieldErrors as FormFieldErrors, Fieldset as FormFieldset, Legend as FormLegend, ElementField as FormElementField, Button as FormButton };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as FormPrimitive from 'formsnap';
|
|
2
|
+
import Description from './form-description.svelte';
|
|
3
|
+
import Label from './form-label.svelte';
|
|
4
|
+
import FieldErrors from './form-field-errors.svelte';
|
|
5
|
+
import Field from './form-field.svelte';
|
|
6
|
+
import Fieldset from './form-fieldset.svelte';
|
|
7
|
+
import Legend from './form-legend.svelte';
|
|
8
|
+
import ElementField from './form-element-field.svelte';
|
|
9
|
+
import Button from './form-button.svelte';
|
|
10
|
+
const Control = FormPrimitive.Control;
|
|
11
|
+
export { Field, Control, Label, Button, FieldErrors, Description, Fieldset, Legend, ElementField,
|
|
12
|
+
//
|
|
13
|
+
Field as FormField, Control as FormControl, Description as FormDescription, Label as FormLabel, FieldErrors as FormFieldErrors, Fieldset as FormFieldset, Legend as FormLegend, ElementField as FormElementField, Button as FormButton };
|
|
@@ -2,7 +2,7 @@ declare const InputGroupInput: import("svelte").Component<(Omit<import("svelte/e
|
|
|
2
2
|
type: "file";
|
|
3
3
|
files?: FileList;
|
|
4
4
|
} | {
|
|
5
|
-
type?: "number" | "hidden" | "color" | "submit" | "reset" | "button" | "search" | "checkbox" | "radio" | (string & {}) | "text" | "tel" | "url" | "email" | "date" | "time" | "image" | "datetime-local" | "
|
|
5
|
+
type?: "number" | "month" | "hidden" | "color" | "submit" | "reset" | "button" | "search" | "checkbox" | "radio" | (string & {}) | "text" | "tel" | "url" | "email" | "date" | "time" | "image" | "datetime-local" | "password" | "range" | "week";
|
|
6
6
|
files?: undefined;
|
|
7
7
|
})) & {
|
|
8
8
|
ref?: HTMLElement | null | undefined;
|
|
@@ -27,13 +27,22 @@
|
|
|
27
27
|
* of routed links, and a package with no SvelteKit runtime of its own cannot
|
|
28
28
|
* own those (the same reasoning that makes AppShell take `currentPath` as a
|
|
29
29
|
* prop rather than importing `$app/state`).
|
|
30
|
+
*
|
|
31
|
+
* `icon` and `meta` are snippets for the same reason and earn their place the
|
|
32
|
+
* same way `breadcrumbs` did: three apps had hand-rolled the meta row and two
|
|
33
|
+
* the icon square, one of them across 38 of its 49 page headers in a full
|
|
34
|
+
* local reimplementation of this component. The app supplies the GLYPH and
|
|
35
|
+
* the ITEMS; the package owns the treatment — the tinted square, its size,
|
|
36
|
+
* and the row's spacing — because the treatment is the half that drifts.
|
|
30
37
|
*/
|
|
31
38
|
let {
|
|
32
39
|
eyebrow,
|
|
33
40
|
breadcrumbs,
|
|
41
|
+
icon,
|
|
34
42
|
title,
|
|
35
43
|
subtitle,
|
|
36
44
|
info,
|
|
45
|
+
meta,
|
|
37
46
|
actions,
|
|
38
47
|
ref = $bindable(null),
|
|
39
48
|
class: className,
|
|
@@ -43,10 +52,14 @@
|
|
|
43
52
|
eyebrow?: string;
|
|
44
53
|
/** The trail above the title — the app's own routed links. */
|
|
45
54
|
breadcrumbs?: Snippet;
|
|
55
|
+
/** The glyph alone. The tinted square around it is this component's. */
|
|
56
|
+
icon?: Snippet;
|
|
46
57
|
/** Omit for a header that is a breadcrumb bar with actions. */
|
|
47
58
|
title?: string;
|
|
48
59
|
subtitle?: string;
|
|
49
60
|
info?: string;
|
|
61
|
+
/** Facts about the page, under the title: badges, dates, a status. */
|
|
62
|
+
meta?: Snippet;
|
|
50
63
|
actions?: Snippet;
|
|
51
64
|
} = $props();
|
|
52
65
|
</script>
|
|
@@ -56,43 +69,67 @@
|
|
|
56
69
|
class={cn('mb-6 flex flex-wrap items-start justify-between gap-4', className)}
|
|
57
70
|
{...restProps}
|
|
58
71
|
>
|
|
59
|
-
<div class="min-w-0">
|
|
60
|
-
{#if
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
<div class="flex min-w-0 items-start gap-3">
|
|
73
|
+
{#if icon}
|
|
74
|
+
<!-- aria-hidden: the glyph restates the title beside it, so a reader
|
|
75
|
+
already has the fact. `shrink-0` so it never collapses when the
|
|
76
|
+
title is long, and the square is the package's rather than the
|
|
77
|
+
app's precisely so two apps cannot pick two sizes for it. -->
|
|
78
|
+
<span
|
|
79
|
+
class="bg-primary/10 text-primary mt-0.5 inline-flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg]:size-5"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
>
|
|
82
|
+
{@render icon()}
|
|
83
|
+
</span>
|
|
69
84
|
{/if}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
{
|
|
74
|
-
</
|
|
75
|
-
|
|
85
|
+
<div class="min-w-0">
|
|
86
|
+
{#if breadcrumbs}
|
|
87
|
+
<div class="text-muted-foreground mb-1.5 flex min-w-0 items-center text-sm">
|
|
88
|
+
{@render breadcrumbs()}
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
91
|
+
{#if eyebrow}
|
|
92
|
+
<div class="text-primary text-2xs tracking-eyebrow mb-1 font-medium uppercase">
|
|
93
|
+
{eyebrow}
|
|
94
|
+
</div>
|
|
95
|
+
{/if}
|
|
96
|
+
{#if title}
|
|
97
|
+
<div class="flex items-center gap-2">
|
|
98
|
+
<h1 class="font-display text-display leading-tight font-semibold tracking-[-0.02em]">
|
|
99
|
+
{title}
|
|
100
|
+
</h1>
|
|
101
|
+
{#if info}
|
|
102
|
+
<InfoTip
|
|
103
|
+
text={info}
|
|
104
|
+
class="text-muted-foreground/50 hover:text-muted-foreground mt-0.5 size-4 transition-colors"
|
|
105
|
+
/>
|
|
106
|
+
{/if}
|
|
107
|
+
</div>
|
|
108
|
+
{:else if info}
|
|
109
|
+
<!-- Title-less, but the page still has something to explain. The tip
|
|
110
|
+
keeps a row of its own rather than being dropped along with the
|
|
111
|
+
heading it usually sits beside. -->
|
|
112
|
+
<div class="flex items-center gap-2">
|
|
76
113
|
<InfoTip
|
|
77
114
|
text={info}
|
|
78
|
-
class="text-muted-foreground/50 hover:text-muted-foreground
|
|
115
|
+
class="text-muted-foreground/50 hover:text-muted-foreground size-4 transition-colors"
|
|
79
116
|
/>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
</div>
|
|
118
|
+
{/if}
|
|
119
|
+
{#if subtitle}
|
|
120
|
+
<p class="text-muted-foreground mt-1.5 line-clamp-1 max-w-4xl text-sm">{subtitle}</p>
|
|
121
|
+
{/if}
|
|
122
|
+
{#if meta}
|
|
123
|
+
<!-- `min-w-0` for the reason the actions row carries it: this is a flex
|
|
124
|
+
item and would otherwise floor at its own min-content width, which
|
|
125
|
+
wrapping cannot get below. -->
|
|
126
|
+
<div
|
|
127
|
+
class="text-muted-foreground mt-2 flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1.5 text-xs"
|
|
128
|
+
>
|
|
129
|
+
{@render meta()}
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
</div>
|
|
96
133
|
</div>
|
|
97
134
|
{#if actions}
|
|
98
135
|
<!--
|
|
@@ -6,10 +6,14 @@ type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
|
6
6
|
eyebrow?: string;
|
|
7
7
|
/** The trail above the title — the app's own routed links. */
|
|
8
8
|
breadcrumbs?: Snippet;
|
|
9
|
+
/** The glyph alone. The tinted square around it is this component's. */
|
|
10
|
+
icon?: Snippet;
|
|
9
11
|
/** Omit for a header that is a breadcrumb bar with actions. */
|
|
10
12
|
title?: string;
|
|
11
13
|
subtitle?: string;
|
|
12
14
|
info?: string;
|
|
15
|
+
/** Facts about the page, under the title: badges, dates, a status. */
|
|
16
|
+
meta?: Snippet;
|
|
13
17
|
actions?: Snippet;
|
|
14
18
|
};
|
|
15
19
|
declare const PageHeader: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The household's Australian value formatters.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* Two apps had hand-rolled the same job — `godswood/frontend/src/lib/utils/
|
|
7
|
+
* formatters.ts` and `pebblestone/frontend/src/lib/utils/format.ts` — and had
|
|
8
|
+
* already drifted on every decision that matters: whether a percentage arrives
|
|
9
|
+
* as `4.5` or `0.045`, whether money arrives as dollars or as integer cents,
|
|
10
|
+
* whether a date renders `19 Dec 2024` or `19/12/2024`, and whether a missing
|
|
11
|
+
* value reads `N/A` or `-`. All four are user-visible, and the fleet is
|
|
12
|
+
* entirely Australian, so the disagreement bought nothing.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IS IN AND WHAT IS NOT
|
|
15
|
+
* --------------------------
|
|
16
|
+
* In: every formatter for a value class BOTH apps format — money, dates and
|
|
17
|
+
* times, percentages, plain numbers — including the variants only one app has
|
|
18
|
+
* today, because a value class the package owns it owns completely. Splitting
|
|
19
|
+
* money across two homes is how the drift started.
|
|
20
|
+
*
|
|
21
|
+
* Out: formatters for a value class only ONE app has, which are a domain
|
|
22
|
+
* vocabulary rather than a shared value class — loan repayment frequencies and
|
|
23
|
+
* AI model IDs (godswood), file sizes (godswood), pager arithmetic and the
|
|
24
|
+
* per-line GST recompute for bill approvals (pebblestone, and bound to Xero tax
|
|
25
|
+
* codes besides). Relative time ("2 hours ago") is out too: it lives on
|
|
26
|
+
* `date-fns` in the one app that has it, and a display formatter is not worth
|
|
27
|
+
* making that a dependency of every consumer of this package.
|
|
28
|
+
*
|
|
29
|
+
* THE CONVENTIONS THESE ENCODE
|
|
30
|
+
* ----------------------------
|
|
31
|
+
* - `en-AU`, AUD, and `Australia/Brisbane` wherever a timezone is implied. The
|
|
32
|
+
* zone is pinned rather than taken from the browser: the household's books
|
|
33
|
+
* are kept in AEST, so a laptop in another zone should not renumber them.
|
|
34
|
+
* - Dates render `DD Mon YYYY` or, on request, `DD/MM/YYYY` — both sanctioned
|
|
35
|
+
* Australian forms.
|
|
36
|
+
* - A negative amount carries its sign OUTSIDE the symbol: `-$1,234.56`, never
|
|
37
|
+
* `$-1,234.56`.
|
|
38
|
+
* - A missing value renders `N/A`, and every formatter takes a `fallback` to
|
|
39
|
+
* say otherwise. `-` is deliberately not the default: beside a money column
|
|
40
|
+
* it reads as a minus sign, which is the one wrong meaning available.
|
|
41
|
+
*
|
|
42
|
+
* No dependencies, no DOM: this module is `Intl` and arithmetic.
|
|
43
|
+
*/
|
|
44
|
+
/** The one locale every household app formats in. */
|
|
45
|
+
export declare const AU_LOCALE = "en-AU";
|
|
46
|
+
/** The one timezone the household's records are kept in. */
|
|
47
|
+
export declare const AU_TIME_ZONE = "Australia/Brisbane";
|
|
48
|
+
export interface CurrencyOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Fraction digits. Defaults to 2: dropping cents is a loss of fidelity the
|
|
51
|
+
* caller should have to ask for, not the default that quietly rounds
|
|
52
|
+
* $1,234.56 up to $1,235. Pass `0` for a dashboard figure.
|
|
53
|
+
*/
|
|
54
|
+
decimals?: number;
|
|
55
|
+
/** Rendered when the value is null, undefined, empty or not a number. */
|
|
56
|
+
fallback?: string;
|
|
57
|
+
/**
|
|
58
|
+
* ISO 4217 code. Defaults to AUD, which renders as a bare `$`; a non-AUD
|
|
59
|
+
* currency renders disambiguated (`USD 1,234.56`) rather than as another
|
|
60
|
+
* `$`, because an app that holds a foreign account is exactly the app that
|
|
61
|
+
* must not confuse the two.
|
|
62
|
+
*/
|
|
63
|
+
currency?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Format a DOLLAR value as currency.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* formatCurrency(1234.56) // '$1,234.56'
|
|
70
|
+
* formatCurrency('1234.56', { decimals: 0 }) // '$1,235'
|
|
71
|
+
* formatCurrency(-500, { decimals: 0 }) // '-$500'
|
|
72
|
+
* formatCurrency(null) // 'N/A'
|
|
73
|
+
*/
|
|
74
|
+
export declare function formatCurrency(value: string | number | null | undefined, options?: CurrencyOptions): string;
|
|
75
|
+
/**
|
|
76
|
+
* Format an INTEGER-CENTS value as currency.
|
|
77
|
+
*
|
|
78
|
+
* Money held as integer cents is the correct storage for money, and the app
|
|
79
|
+
* that does it should not have to divide by 100 at every call site and hope
|
|
80
|
+
* the float lands.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* formatCurrencyFromCents(123456) // '$1,234.56'
|
|
84
|
+
* formatCurrencyFromCents(-123456, { decimals: 0 }) // '-$1,235'
|
|
85
|
+
* formatCurrencyFromCents(null) // 'N/A'
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatCurrencyFromCents(cents: number | null | undefined, options?: CurrencyOptions): string;
|
|
88
|
+
/** Convert a dollar value to whole cents. */
|
|
89
|
+
export declare function dollarsToCents(dollars: number): number;
|
|
90
|
+
/**
|
|
91
|
+
* Abbreviate a money value for a chart axis or a dense tile.
|
|
92
|
+
*
|
|
93
|
+
* A y-axis tick has room for four or five characters, not for `$1,109,057.64`.
|
|
94
|
+
* Full precision belongs in the tooltip and the table; the axis exists to tell
|
|
95
|
+
* the reader which order of magnitude they are looking at.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* compactCurrency(1_109_057.64) // '$1.1m'
|
|
99
|
+
* compactCurrency(-1_500_000) // '-$1.5m'
|
|
100
|
+
* compactCurrency(12_400) // '$12k'
|
|
101
|
+
*/
|
|
102
|
+
export declare function compactCurrency(value: number | null | undefined, options?: {
|
|
103
|
+
currency?: string;
|
|
104
|
+
fallback?: string;
|
|
105
|
+
}): string;
|
|
106
|
+
/**
|
|
107
|
+
* Format a money figure that arrives as a STRING, without ever parsing it to a
|
|
108
|
+
* JS number.
|
|
109
|
+
*
|
|
110
|
+
* A securities tax P&L reaches a tax return, so a figure must render with the
|
|
111
|
+
* exact digits the API sent — `parseFloat` rounds at 2^53 and quietly changes
|
|
112
|
+
* cents on a nine-figure total. This groups the integer part with thousands
|
|
113
|
+
* separators and preserves the fractional part verbatim, so the displayed
|
|
114
|
+
* string is faithful to the source decimal whatever its magnitude.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* formatCurrencyString('1234.56') // '$1,234.56'
|
|
118
|
+
* formatCurrencyString('-1234567.89') // '-$1,234,567.89'
|
|
119
|
+
* formatCurrencyString('0') // '$0'
|
|
120
|
+
* formatCurrencyString(null) // 'N/A'
|
|
121
|
+
*/
|
|
122
|
+
export declare function formatCurrencyString(value: string | null | undefined, options?: {
|
|
123
|
+
fallback?: string;
|
|
124
|
+
}): string;
|
|
125
|
+
/**
|
|
126
|
+
* Report the sign of a money value without parsing a string to a number — the
|
|
127
|
+
* float-free companion to `formatCurrencyString`, for choosing a tone class.
|
|
128
|
+
*/
|
|
129
|
+
export declare function isNegativeMoney(value: string | number | null | undefined): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Format a number with thousands separators.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* formatNumber(1234567) // '1,234,567'
|
|
135
|
+
* formatNumber(1234.567, { decimals: 2 }) // '1,234.57'
|
|
136
|
+
* formatNumber(null) // 'N/A'
|
|
137
|
+
*/
|
|
138
|
+
export declare function formatNumber(value: number | string | null | undefined, options?: {
|
|
139
|
+
decimals?: number;
|
|
140
|
+
fallback?: string;
|
|
141
|
+
}): string;
|
|
142
|
+
/**
|
|
143
|
+
* Format a value that is ALREADY in percentage points: `4.5` renders `4.5%`.
|
|
144
|
+
*
|
|
145
|
+
* Its counterpart for a 0–1 ratio is `formatRatioAsPercentage`. The two are
|
|
146
|
+
* named apart on purpose — the apps disagreed about which one `formatPercent`
|
|
147
|
+
* meant, and picking either spelling for both would make a 100x error a
|
|
148
|
+
* one-character mistake.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* formatPercentage(4.5) // '4.5%'
|
|
152
|
+
* formatPercentage('4.5', { decimals: 2 }) // '4.50%'
|
|
153
|
+
* formatPercentage(null) // 'N/A'
|
|
154
|
+
*/
|
|
155
|
+
export declare function formatPercentage(value: number | string | null | undefined, options?: {
|
|
156
|
+
decimals?: number;
|
|
157
|
+
fallback?: string;
|
|
158
|
+
}): string;
|
|
159
|
+
/**
|
|
160
|
+
* Format a 0–1 RATIO as a percentage: `0.045` renders `4.5%`.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* formatRatioAsPercentage(0.045) // '4.5%'
|
|
164
|
+
* formatRatioAsPercentage(1) // '100.0%'
|
|
165
|
+
* formatRatioAsPercentage(null) // 'N/A'
|
|
166
|
+
*/
|
|
167
|
+
export declare function formatRatioAsPercentage(value: number | string | null | undefined, options?: {
|
|
168
|
+
decimals?: number;
|
|
169
|
+
fallback?: string;
|
|
170
|
+
}): string;
|
|
171
|
+
/** `19 Dec 2024`, `19 December 2024`, or `19/12/2024`. */
|
|
172
|
+
export type DateFormat = 'short' | 'long' | 'numeric';
|
|
173
|
+
export interface DateOptions {
|
|
174
|
+
format?: DateFormat;
|
|
175
|
+
/** Rendered when the value is null, undefined or empty. */
|
|
176
|
+
fallback?: string;
|
|
177
|
+
/** IANA zone a timestamp is read in. Defaults to `Australia/Brisbane`. */
|
|
178
|
+
timeZone?: string;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Parse a timestamp the API returned.
|
|
182
|
+
*
|
|
183
|
+
* A backend that stores naive UTC (`datetime.now(UTC).replace(tzinfo=None)`)
|
|
184
|
+
* and serialises it with no offset hands JavaScript a string it reads as LOCAL
|
|
185
|
+
* time: in Brisbane that lands every stored moment ten hours early, so anything
|
|
186
|
+
* after 2pm shows the wrong DAY, not merely the wrong hour. An offset-less
|
|
187
|
+
* value carrying a time is therefore read as UTC; one that carries an offset is
|
|
188
|
+
* left alone, and a date-only value is left alone too (appending a `Z` to it
|
|
189
|
+
* produces an Invalid Date).
|
|
190
|
+
*/
|
|
191
|
+
export declare function parseApiDate(iso: string): Date;
|
|
192
|
+
/**
|
|
193
|
+
* Format a date for an Australian reader.
|
|
194
|
+
*
|
|
195
|
+
* A date-only value (`YYYY-MM-DD`) renders the day it NAMES, with no timezone
|
|
196
|
+
* conversion — a date is not an instant, and converting one is how a booking
|
|
197
|
+
* dated the 1st shows as the 31st. A value carrying a time is an instant, and
|
|
198
|
+
* is read in `timeZone` (`Australia/Brisbane` by default, not the browser's
|
|
199
|
+
* zone, so the same record reads the same on a laptop in another country).
|
|
200
|
+
*
|
|
201
|
+
* A string that will not parse is returned unchanged rather than hidden behind
|
|
202
|
+
* the fallback: an unexpected date format is worth seeing.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* formatDate('2024-12-19') // '19 Dec 2024'
|
|
206
|
+
* formatDate('2024-12-19', { format: 'long' }) // '19 December 2024'
|
|
207
|
+
* formatDate('2024-12-19', { format: 'numeric' }) // '19/12/2024'
|
|
208
|
+
* formatDate(null) // 'N/A'
|
|
209
|
+
*/
|
|
210
|
+
export declare function formatDate(value: string | Date | null | undefined, options?: DateOptions): string;
|
|
211
|
+
/**
|
|
212
|
+
* Format a timestamp as a date and a 24-hour time: `19 Dec 2024, 14:05`.
|
|
213
|
+
*
|
|
214
|
+
* 24 hours, not `2:05 pm`: it is unambiguous and it aligns in a column, which
|
|
215
|
+
* is where a timestamp almost always sits.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* formatDateTime('2024-12-19T04:05:00') // '19 Dec 2024, 14:05'
|
|
219
|
+
* formatDateTime('2024-12-19T04:05:00Z', { format: 'numeric' }) // '19/12/2024, 14:05'
|
|
220
|
+
* formatDateTime(null) // 'N/A'
|
|
221
|
+
*/
|
|
222
|
+
export declare function formatDateTime(value: string | Date | null | undefined, options?: DateOptions): string;
|