@joewinke/jatui 0.59.0 → 0.61.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/CLAUDE.md +4 -1
- package/dist/actions/tooltip.d.ts +7 -0
- package/dist/actions/tooltip.js +28 -0
- package/dist/ambience/README.md +9 -2
- package/dist/ambience/engine.d.ts +2 -0
- package/dist/ambience/engine.js +101 -4
- package/dist/ambience/scapes/playlist.d.ts +15 -0
- package/dist/ambience/scapes/playlist.js +81 -4
- package/dist/ambience/shared.d.ts +69 -2
- package/dist/ambience/shared.js +110 -8
- package/dist/ambience/types.d.ts +19 -0
- package/dist/components/AmbienceToggle.svelte +260 -11
- package/dist/components/AmbienceToggle.svelte.d.ts +17 -3
- package/dist/components/stats/StatusBanner.svelte +119 -0
- package/dist/components/stats/StatusBanner.svelte.d.ts +13 -0
- package/dist/components/stats/StatusCard.svelte +176 -0
- package/dist/components/stats/StatusCard.svelte.d.ts +18 -0
- package/dist/components/stats/UptimeBar.svelte +254 -0
- package/dist/components/stats/UptimeBar.svelte.d.ts +20 -0
- package/dist/components/stats/uptime.d.ts +105 -0
- package/dist/components/stats/uptime.js +213 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
StatusBanner: the overall status page headline (jatui-i3vxo). "All systems
|
|
3
|
+
operational" / degraded / outage, token colours, check / warning / x icon.
|
|
4
|
+
Token-only colours: the tint comes from the status tokens, never a hex.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts" module>
|
|
7
|
+
export type BannerStatus = 'operational' | 'degraded' | 'outage';
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { tooltip } from '../../actions/tooltip';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** 'operational' | 'degraded' | 'outage' (see aggregateStatus for a pure helper). */
|
|
15
|
+
status: BannerStatus;
|
|
16
|
+
/** Override the message. */
|
|
17
|
+
message?: string;
|
|
18
|
+
/** Extra detail behind an (i) info button. */
|
|
19
|
+
info?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { status, message, info, class: className = '' }: Props = $props();
|
|
24
|
+
|
|
25
|
+
const text = $derived(
|
|
26
|
+
message ?? (status === 'operational' ? 'All systems operational' : status === 'degraded' ? 'Some systems degraded' : 'Partial outage')
|
|
27
|
+
);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class="jatui-status-banner {className}" role="status" data-status={status}>
|
|
31
|
+
{#if status === 'operational'}
|
|
32
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
33
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
34
|
+
<path d="M5.2 8.2 7.1 10l3.7-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
35
|
+
</svg>
|
|
36
|
+
{:else if status === 'outage'}
|
|
37
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
38
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
39
|
+
<path d="M5.8 5.8l4.4 4.4M10.2 5.8 5.8 10.2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
40
|
+
</svg>
|
|
41
|
+
{:else}
|
|
42
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
43
|
+
<path d="M8 2 14.5 13h-13Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
|
44
|
+
<path d="M8 6.4v3.2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
45
|
+
<circle cx="8" cy="11.3" r="0.9" fill="currentColor" />
|
|
46
|
+
</svg>
|
|
47
|
+
{/if}
|
|
48
|
+
<span class="sb-text">{text}</span>
|
|
49
|
+
{#if info}
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
class="sb-info"
|
|
53
|
+
aria-label="More about this status"
|
|
54
|
+
use:tooltip={{ text: info, placement: 'top', delayMs: 0 }}
|
|
55
|
+
>
|
|
56
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
57
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
58
|
+
<path d="M8 7.2v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
59
|
+
<circle cx="8" cy="4.8" r="0.9" fill="currentColor" />
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
{/if}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.jatui-status-banner {
|
|
67
|
+
--_st: var(--jatui-status-positive, var(--color-success));
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: var(--jatui-space-base, 8px);
|
|
71
|
+
padding: var(--jatui-space-base, 8px) var(--jatui-space-lg, 16px);
|
|
72
|
+
border-radius: var(--jatui-radius-box, 0.5rem);
|
|
73
|
+
background: color-mix(in oklch, var(--_st) 12%, var(--color-base-100));
|
|
74
|
+
border: 1px solid color-mix(in oklch, var(--_st) 30%, transparent);
|
|
75
|
+
color: var(--_st);
|
|
76
|
+
font-size: 0.9375rem;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
}
|
|
79
|
+
.jatui-status-banner[data-status='degraded'] {
|
|
80
|
+
--_st: var(--jatui-status-caution, var(--color-warning));
|
|
81
|
+
}
|
|
82
|
+
.jatui-status-banner[data-status='outage'] {
|
|
83
|
+
--_st: var(--jatui-status-negative, var(--color-error));
|
|
84
|
+
}
|
|
85
|
+
.jatui-status-banner svg {
|
|
86
|
+
width: 16px;
|
|
87
|
+
height: 16px;
|
|
88
|
+
flex: none;
|
|
89
|
+
}
|
|
90
|
+
.sb-text {
|
|
91
|
+
flex: 1;
|
|
92
|
+
min-width: 0;
|
|
93
|
+
}
|
|
94
|
+
.sb-info {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
width: 24px;
|
|
99
|
+
height: 24px;
|
|
100
|
+
padding: 0;
|
|
101
|
+
border: 0;
|
|
102
|
+
background: transparent;
|
|
103
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
104
|
+
color: inherit;
|
|
105
|
+
opacity: 0.75;
|
|
106
|
+
cursor: help;
|
|
107
|
+
}
|
|
108
|
+
.sb-info svg {
|
|
109
|
+
width: 14px;
|
|
110
|
+
height: 14px;
|
|
111
|
+
}
|
|
112
|
+
.sb-info:hover {
|
|
113
|
+
opacity: 1;
|
|
114
|
+
}
|
|
115
|
+
.sb-info:focus-visible {
|
|
116
|
+
outline: 2px solid var(--color-primary);
|
|
117
|
+
outline-offset: 1px;
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type BannerStatus = 'operational' | 'degraded' | 'outage';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** 'operational' | 'degraded' | 'outage' (see aggregateStatus for a pure helper). */
|
|
4
|
+
status: BannerStatus;
|
|
5
|
+
/** Override the message. */
|
|
6
|
+
message?: string;
|
|
7
|
+
/** Extra detail behind an (i) info button. */
|
|
8
|
+
info?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const StatusBanner: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type StatusBanner = ReturnType<typeof StatusBanner>;
|
|
13
|
+
export default StatusBanner;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
StatusCard: one measured component on a status page (jatui-i3vxo). Name +
|
|
3
|
+
info tooltip (what is measured), the status icon at the right, the
|
|
4
|
+
UptimeBar strip, and the status word (Normal / Degraded / Outage / …).
|
|
5
|
+
Data in: `{ date, status, downMinutes?, note? }[]`. Composes with
|
|
6
|
+
StatusBanner on top of a status page grid.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { tooltip } from '../../actions/tooltip';
|
|
10
|
+
import { buildUptime, formatUptimePct, uptimeWord, type UptimeDayInput } from './uptime';
|
|
11
|
+
import UptimeBar from './UptimeBar.svelte';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Name of the measured thing (e.g. "API"). */
|
|
15
|
+
name: string;
|
|
16
|
+
/** What is measured, shown behind an (i) info button. */
|
|
17
|
+
description?: string;
|
|
18
|
+
data: readonly UptimeDayInput[];
|
|
19
|
+
/** Window length in days. Default 90. */
|
|
20
|
+
days?: number;
|
|
21
|
+
end?: string | Date;
|
|
22
|
+
/** Override the status word. */
|
|
23
|
+
statusWord?: string;
|
|
24
|
+
locale?: string;
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
name,
|
|
30
|
+
description,
|
|
31
|
+
data,
|
|
32
|
+
days = 90,
|
|
33
|
+
end,
|
|
34
|
+
statusWord,
|
|
35
|
+
locale,
|
|
36
|
+
class: className = ''
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const layout = $derived(buildUptime({ data, days, end }));
|
|
40
|
+
const word = $derived(statusWord ?? uptimeWord(layout.worst));
|
|
41
|
+
|
|
42
|
+
const totalDown = $derived(layout.bars.reduce((s, b) => s + b.downMinutes, 0));
|
|
43
|
+
// Truncating formatter — never overclaims (see formatUptimePct).
|
|
44
|
+
const uptimeText = $derived(
|
|
45
|
+
layout.uptimePct == null
|
|
46
|
+
? 'No data'
|
|
47
|
+
: `${formatUptimePct(layout.uptimePct, locale, totalDown)} uptime`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/** "operational" | "degraded" | "partial" | "outage" | "no-data" — for the tone classes. */
|
|
51
|
+
const heading = $derived(layout.worst);
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<section class="jatui-status-card st-{heading} {className}" aria-label={name}>
|
|
55
|
+
<header class="sc-head">
|
|
56
|
+
<h3 class="sc-name">
|
|
57
|
+
<span>{name}</span>
|
|
58
|
+
{#if description}
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
class="sc-info"
|
|
62
|
+
aria-label={`About ${name}`}
|
|
63
|
+
use:tooltip={{ text: description, placement: 'top', delayMs: 0 }}
|
|
64
|
+
>
|
|
65
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
66
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
67
|
+
<path d="M8 7.2v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
68
|
+
<circle cx="8" cy="4.8" r="0.9" fill="currentColor" />
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
{/if}
|
|
72
|
+
</h3>
|
|
73
|
+
<span class="sc-status">
|
|
74
|
+
{#if heading === 'operational'}
|
|
75
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
76
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
77
|
+
<path d="M5.2 8.2 7.1 10l3.7-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
78
|
+
</svg>
|
|
79
|
+
{:else if heading === 'outage'}
|
|
80
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
81
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
82
|
+
<path d="M5.8 5.8l4.4 4.4M10.2 5.8 5.8 10.2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
83
|
+
</svg>
|
|
84
|
+
{:else if heading === 'no-data'}
|
|
85
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
86
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
87
|
+
<path d="M5.6 8h4.8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
88
|
+
</svg>
|
|
89
|
+
{:else}
|
|
90
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
91
|
+
<path d="M8 2 14.5 13h-13Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
|
92
|
+
<path d="M8 6.4v3.2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
93
|
+
<circle cx="8" cy="11.3" r="0.9" fill="currentColor" />
|
|
94
|
+
</svg>
|
|
95
|
+
{/if}
|
|
96
|
+
<span class="sc-word">{word}</span>
|
|
97
|
+
</span>
|
|
98
|
+
</header>
|
|
99
|
+
|
|
100
|
+
<UptimeBar {data} {days} {end} {locale} label={name} />
|
|
101
|
+
|
|
102
|
+
<!-- Screen-reader text per bar group: the strip's aria-label already
|
|
103
|
+
carries the summary; this paragraph is the visible fallback for the
|
|
104
|
+
heading structure. -->
|
|
105
|
+
<p class="sr-only">{name}: {word}, {uptimeText}</p>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<style>
|
|
109
|
+
.jatui-status-card {
|
|
110
|
+
--_st: var(--jatui-status-positive, var(--color-success));
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
gap: var(--jatui-space-base, 8px);
|
|
114
|
+
padding: var(--jatui-space-lg, 16px);
|
|
115
|
+
border: 1px solid var(--jatui-separator, color-mix(in oklch, var(--color-base-content) 12%, transparent));
|
|
116
|
+
border-radius: var(--jatui-radius-box, 0.5rem);
|
|
117
|
+
background: var(--color-base-100);
|
|
118
|
+
}
|
|
119
|
+
.st-operational { --_st: var(--jatui-status-positive, var(--color-success)); }
|
|
120
|
+
.st-degraded { --_st: var(--jatui-status-caution, var(--color-warning)); }
|
|
121
|
+
.st-partial { --_st: color-mix(in oklch, var(--color-warning) 55%, var(--color-error)); }
|
|
122
|
+
.st-outage { --_st: var(--jatui-status-negative, var(--color-error)); }
|
|
123
|
+
.st-no-data { --_st: var(--jatui-status-neutral, color-mix(in oklab, var(--color-base-content) 40%, transparent)); }
|
|
124
|
+
|
|
125
|
+
.sc-head {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: space-between;
|
|
129
|
+
gap: var(--jatui-space-base, 8px);
|
|
130
|
+
}
|
|
131
|
+
.sc-name {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: var(--jatui-space-sm, 4px);
|
|
135
|
+
margin: 0;
|
|
136
|
+
font-size: 0.9375rem;
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
color: var(--color-base-content);
|
|
139
|
+
}
|
|
140
|
+
.sc-info {
|
|
141
|
+
display: inline-flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
width: 24px;
|
|
145
|
+
height: 24px;
|
|
146
|
+
padding: 0;
|
|
147
|
+
border: 0;
|
|
148
|
+
background: transparent;
|
|
149
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
150
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
151
|
+
cursor: help;
|
|
152
|
+
}
|
|
153
|
+
.sc-info svg {
|
|
154
|
+
width: 14px;
|
|
155
|
+
height: 14px;
|
|
156
|
+
}
|
|
157
|
+
.sc-info:hover {
|
|
158
|
+
color: var(--color-base-content);
|
|
159
|
+
}
|
|
160
|
+
.sc-info:focus-visible {
|
|
161
|
+
outline: 2px solid var(--color-primary);
|
|
162
|
+
outline-offset: 1px;
|
|
163
|
+
}
|
|
164
|
+
.sc-status {
|
|
165
|
+
display: inline-flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: var(--jatui-space-sm, 4px);
|
|
168
|
+
color: var(--_st);
|
|
169
|
+
font-size: 0.875rem;
|
|
170
|
+
font-weight: 500;
|
|
171
|
+
}
|
|
172
|
+
.sc-status svg {
|
|
173
|
+
width: 16px;
|
|
174
|
+
height: 16px;
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type UptimeDayInput } from './uptime';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Name of the measured thing (e.g. "API"). */
|
|
4
|
+
name: string;
|
|
5
|
+
/** What is measured, shown behind an (i) info button. */
|
|
6
|
+
description?: string;
|
|
7
|
+
data: readonly UptimeDayInput[];
|
|
8
|
+
/** Window length in days. Default 90. */
|
|
9
|
+
days?: number;
|
|
10
|
+
end?: string | Date;
|
|
11
|
+
/** Override the status word. */
|
|
12
|
+
statusWord?: string;
|
|
13
|
+
locale?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
declare const StatusCard: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type StatusCard = ReturnType<typeof StatusCard>;
|
|
18
|
+
export default StatusCard;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
UptimeBar: one component's 90-day status strip (jatui-i3vxo). One thin bar
|
|
3
|
+
per day, coloured by status tone (operational / degraded / partial /
|
|
4
|
+
outage / no-data — no-data is hatched neutral, never coloured as up), a
|
|
5
|
+
hover/touch/focus tooltip with date + minutes down + incident note, and the
|
|
6
|
+
footer "90 days ago ——— 99.97% uptime ——— Today" (truncated to 2 decimals,
|
|
7
|
+
never overclaims — see formatUptimePct).
|
|
8
|
+
|
|
9
|
+
Bars are a role="grid" with ONE tab stop and roving focus (arrows move by
|
|
10
|
+
day, PageUp/Down by 7, Home/End to the window ends). Pure CSS sizing:
|
|
11
|
+
the component is an inline-size container and --up-bar is derived from
|
|
12
|
+
the container width (cqi), clamped 3–14px — on a phone the strip scrolls
|
|
13
|
+
horizontally inside the component, starting at the latest day. All colour
|
|
14
|
+
comes from theme tokens (status tokens + color-mix), no hex.
|
|
15
|
+
-->
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import { tooltip } from '../../actions/tooltip';
|
|
18
|
+
import {
|
|
19
|
+
buildUptime,
|
|
20
|
+
formatUptimePct,
|
|
21
|
+
moveUptimeFocus,
|
|
22
|
+
parseDay,
|
|
23
|
+
uptimeWord,
|
|
24
|
+
type UptimeBar as UptimeBarDay,
|
|
25
|
+
type UptimeDayInput
|
|
26
|
+
} from './uptime';
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** One entry per day: `{ date, status, downMinutes?, note? }`. */
|
|
30
|
+
data: readonly UptimeDayInput[];
|
|
31
|
+
/** Window length in days. Default 90. */
|
|
32
|
+
days?: number;
|
|
33
|
+
/** Inclusive window end. Default: today. */
|
|
34
|
+
end?: string | Date;
|
|
35
|
+
/** Name of the measured thing, for the screen-reader label. */
|
|
36
|
+
label?: string;
|
|
37
|
+
/** Custom tooltip / accessible label for a day. */
|
|
38
|
+
formatLabel?: (bar: UptimeBarDay) => string;
|
|
39
|
+
showUptime?: boolean;
|
|
40
|
+
showRangeLabels?: boolean;
|
|
41
|
+
locale?: string;
|
|
42
|
+
class?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
data,
|
|
47
|
+
days = 90,
|
|
48
|
+
end,
|
|
49
|
+
label = 'Uptime',
|
|
50
|
+
formatLabel,
|
|
51
|
+
showUptime = true,
|
|
52
|
+
showRangeLabels = true,
|
|
53
|
+
locale,
|
|
54
|
+
class: className = ''
|
|
55
|
+
}: Props = $props();
|
|
56
|
+
|
|
57
|
+
const layout = $derived(buildUptime({ data, days, end }));
|
|
58
|
+
const byDate = $derived(new Map(layout.bars.map((b) => [b.date, b])));
|
|
59
|
+
|
|
60
|
+
const nf = $derived(new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }));
|
|
61
|
+
const df = $derived(
|
|
62
|
+
new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const totalDown = $derived(layout.bars.reduce((s, b) => s + b.downMinutes, 0));
|
|
66
|
+
// Truncating formatter — never overclaims (see formatUptimePct).
|
|
67
|
+
const uptimeText = $derived(
|
|
68
|
+
layout.uptimePct == null
|
|
69
|
+
? 'No data'
|
|
70
|
+
: `${formatUptimePct(layout.uptimePct, locale, totalDown)} uptime`
|
|
71
|
+
);
|
|
72
|
+
const gridLabel = $derived(`${label}, last ${layout.days} days: ${uptimeText}`);
|
|
73
|
+
|
|
74
|
+
const labelFor = (bar: UptimeBarDay) => {
|
|
75
|
+
if (formatLabel) return formatLabel(bar);
|
|
76
|
+
const parts = [`${df.format(parseDay(bar.date))}: ${uptimeWord(bar.status)}`];
|
|
77
|
+
if (bar.downMinutes > 0) parts.push(`${nf.format(bar.downMinutes)} min down`);
|
|
78
|
+
if (bar.note) parts.push(bar.note);
|
|
79
|
+
return parts.join(' · ');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Roving focus: the focused day, else the last day in range.
|
|
83
|
+
let focusDate = $state<string | null>(null);
|
|
84
|
+
const tabDate = $derived(focusDate && byDate.has(focusDate) ? focusDate : layout.end);
|
|
85
|
+
|
|
86
|
+
let gridEl: HTMLDivElement | undefined = $state();
|
|
87
|
+
let scrollEl: HTMLDivElement | undefined = $state();
|
|
88
|
+
|
|
89
|
+
function cellEl(date: string): HTMLElement | null {
|
|
90
|
+
return gridEl?.querySelector<HTMLElement>(`[data-date="${date}"]`) ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function onKeydown(e: KeyboardEvent) {
|
|
94
|
+
const t = e.target as HTMLElement;
|
|
95
|
+
const date = t.dataset.date;
|
|
96
|
+
if (!date) return;
|
|
97
|
+
const next = moveUptimeFocus(date, e.key, layout);
|
|
98
|
+
if (!next) return;
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
focusDate = next;
|
|
101
|
+
queueMicrotask(() => {
|
|
102
|
+
const el = cellEl(next);
|
|
103
|
+
el?.focus();
|
|
104
|
+
el?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The latest day is the interesting one: start scrolled to the end.
|
|
109
|
+
$effect(() => {
|
|
110
|
+
void layout.bars;
|
|
111
|
+
if (scrollEl) scrollEl.scrollLeft = scrollEl.scrollWidth;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<div class="jatui-uptime {className}" style:--days={layout.days}>
|
|
117
|
+
<div class="up-scroll" bind:this={scrollEl}>
|
|
118
|
+
<div
|
|
119
|
+
class="up-grid"
|
|
120
|
+
role="grid"
|
|
121
|
+
aria-label={gridLabel}
|
|
122
|
+
tabindex="-1"
|
|
123
|
+
bind:this={gridEl}
|
|
124
|
+
onkeydown={onKeydown}
|
|
125
|
+
>
|
|
126
|
+
<div class="up-row" role="row">
|
|
127
|
+
{#each layout.bars as bar (bar.date)}
|
|
128
|
+
<!-- Keys are handled on the grid (roving focus); click focuses the day. -->
|
|
129
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
130
|
+
<div
|
|
131
|
+
role="gridcell"
|
|
132
|
+
class="up-bar st-{bar.status}"
|
|
133
|
+
data-date={bar.date}
|
|
134
|
+
tabindex={bar.date === tabDate ? 0 : -1}
|
|
135
|
+
aria-label={labelFor(bar)}
|
|
136
|
+
use:tooltip={{ text: labelFor(bar), placement: 'top', delayMs: 0, tapToShow: true }}
|
|
137
|
+
onfocus={() => {
|
|
138
|
+
focusDate = bar.date;
|
|
139
|
+
}}
|
|
140
|
+
onclick={() => {
|
|
141
|
+
focusDate = bar.date;
|
|
142
|
+
}}
|
|
143
|
+
></div>
|
|
144
|
+
{/each}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{#if showUptime || showRangeLabels}
|
|
150
|
+
<div class="up-foot" aria-hidden="true">
|
|
151
|
+
{#if showRangeLabels}<span class="up-range">{layout.days} days ago</span>{/if}
|
|
152
|
+
{#if showUptime && showRangeLabels}<span class="up-rule"></span>{/if}
|
|
153
|
+
{#if showUptime}<strong class="up-uptime">{uptimeText}</strong>{/if}
|
|
154
|
+
{#if showUptime && showRangeLabels}<span class="up-rule"></span>{/if}
|
|
155
|
+
{#if showRangeLabels}<span class="up-range">Today</span>{/if}
|
|
156
|
+
</div>
|
|
157
|
+
{/if}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<style>
|
|
161
|
+
.jatui-uptime {
|
|
162
|
+
--up-bar-min: 3px;
|
|
163
|
+
--up-bar-max: 14px;
|
|
164
|
+
--up-gap: 2px;
|
|
165
|
+
--up-pad: 2px;
|
|
166
|
+
--up-fs: 0.75rem;
|
|
167
|
+
--up-empty: color-mix(in oklch, var(--color-base-content) 8%, var(--color-base-100));
|
|
168
|
+
position: relative;
|
|
169
|
+
display: flex;
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
gap: var(--jatui-space-base, 8px);
|
|
172
|
+
min-width: 0;
|
|
173
|
+
max-width: 100%;
|
|
174
|
+
font-size: var(--up-fs);
|
|
175
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
176
|
+
container-type: inline-size;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Bars size to the width the host gives: (container - focus padding) /
|
|
180
|
+
* days - gap, clamped 3–14px. Below the 3px floor the strip overflows and
|
|
181
|
+
* .up-scroll scrolls horizontally inside the component. */
|
|
182
|
+
.jatui-uptime .up-scroll {
|
|
183
|
+
--up-bar: clamp(
|
|
184
|
+
var(--up-bar-min),
|
|
185
|
+
calc((100cqi - 2 * var(--up-pad)) / var(--days) - var(--up-gap)),
|
|
186
|
+
var(--up-bar-max)
|
|
187
|
+
);
|
|
188
|
+
overflow-x: auto;
|
|
189
|
+
overscroll-behavior-x: contain;
|
|
190
|
+
scrollbar-width: thin;
|
|
191
|
+
padding: var(--up-pad);
|
|
192
|
+
}
|
|
193
|
+
.up-grid {
|
|
194
|
+
display: flex;
|
|
195
|
+
outline: none;
|
|
196
|
+
}
|
|
197
|
+
.up-row {
|
|
198
|
+
display: flex;
|
|
199
|
+
gap: var(--up-gap);
|
|
200
|
+
}
|
|
201
|
+
.up-bar {
|
|
202
|
+
flex: none;
|
|
203
|
+
width: var(--up-bar);
|
|
204
|
+
height: var(--up-h, 0.875rem);
|
|
205
|
+
border-radius: calc(var(--up-bar) * 0.25);
|
|
206
|
+
background: var(--up-empty);
|
|
207
|
+
outline: none;
|
|
208
|
+
cursor: default;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Status tones — token-only. 'partial' is warning mixed toward error
|
|
212
|
+
* (orange), never a hardcoded hex. */
|
|
213
|
+
.st-operational { background: var(--jatui-status-positive, var(--color-success)); }
|
|
214
|
+
.st-degraded { background: var(--jatui-status-caution, var(--color-warning)); }
|
|
215
|
+
.st-partial {
|
|
216
|
+
background: color-mix(in oklch, var(--color-warning) 55%, var(--color-error));
|
|
217
|
+
}
|
|
218
|
+
.st-outage { background: var(--jatui-status-negative, var(--color-error)); }
|
|
219
|
+
/* No-data: hatched neutral — clearly different from any incident colour. */
|
|
220
|
+
.st-no-data {
|
|
221
|
+
background: repeating-linear-gradient(
|
|
222
|
+
135deg,
|
|
223
|
+
var(--up-empty) 0 2px,
|
|
224
|
+
color-mix(in oklch, var(--up-empty) 55%, transparent) 2px 4px
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.up-bar:focus-visible {
|
|
229
|
+
box-shadow:
|
|
230
|
+
0 0 0 1.5px var(--color-base-100),
|
|
231
|
+
0 0 0 3px var(--color-primary);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.up-foot {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: var(--jatui-space-sm, 4px);
|
|
238
|
+
white-space: nowrap;
|
|
239
|
+
}
|
|
240
|
+
.up-range {
|
|
241
|
+
flex: none;
|
|
242
|
+
}
|
|
243
|
+
.up-rule {
|
|
244
|
+
flex: 1 1 0;
|
|
245
|
+
min-width: 0;
|
|
246
|
+
height: 1px;
|
|
247
|
+
background: color-mix(in oklch, var(--color-base-content) 15%, transparent);
|
|
248
|
+
}
|
|
249
|
+
.up-uptime {
|
|
250
|
+
flex: none;
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
color: var(--color-base-content);
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type UptimeBar as UptimeBarDay, type UptimeDayInput } from './uptime';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** One entry per day: `{ date, status, downMinutes?, note? }`. */
|
|
4
|
+
data: readonly UptimeDayInput[];
|
|
5
|
+
/** Window length in days. Default 90. */
|
|
6
|
+
days?: number;
|
|
7
|
+
/** Inclusive window end. Default: today. */
|
|
8
|
+
end?: string | Date;
|
|
9
|
+
/** Name of the measured thing, for the screen-reader label. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Custom tooltip / accessible label for a day. */
|
|
12
|
+
formatLabel?: (bar: UptimeBarDay) => string;
|
|
13
|
+
showUptime?: boolean;
|
|
14
|
+
showRangeLabels?: boolean;
|
|
15
|
+
locale?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const UptimeBar: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type UptimeBar = ReturnType<typeof UptimeBar>;
|
|
20
|
+
export default UptimeBar;
|