@w6w/ui 0.1.1 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +7 -5
  3. package/dist/AppPicker.d.ts +1 -0
  4. package/dist/ConfirmModal-ChoiGy2K.cjs +11 -0
  5. package/dist/ConfirmModal-ChoiGy2K.cjs.map +1 -0
  6. package/dist/{ConfirmModal-CD-P8mhN.js → ConfirmModal-Wx3i8F1n.js} +1588 -1564
  7. package/dist/ConfirmModal-Wx3i8F1n.js.map +1 -0
  8. package/dist/ConnectionHeader.d.ts +19 -0
  9. package/dist/EditConnectionModal.d.ts +21 -0
  10. package/dist/app-pages.d.ts +3 -0
  11. package/dist/components/AuthFieldsForm.d.ts +8 -1
  12. package/dist/components/CallableList.d.ts +57 -0
  13. package/dist/components/ConnectionList.d.ts +44 -0
  14. package/dist/components/ListItem.d.ts +16 -6
  15. package/dist/components/ReliabilityChart.d.ts +10 -0
  16. package/dist/components/ReliabilityList.d.ts +47 -0
  17. package/dist/connection-app-meta.d.ts +32 -0
  18. package/dist/flow.cjs +1 -1
  19. package/dist/flow.mjs +2 -2
  20. package/dist/index.cjs +1 -1
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.mjs +1700 -891
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/provider.d.ts +19 -0
  26. package/dist/reliability-board-layout.d.ts +121 -0
  27. package/dist/types.d.ts +8 -0
  28. package/package.json +2 -2
  29. package/src/styles/_callable-list.scss +60 -0
  30. package/src/styles/_connections.scss +100 -0
  31. package/src/styles/_lists.scss +4 -0
  32. package/src/styles/_reliability.scss +339 -0
  33. package/src/styles.css +592 -0
  34. package/src/styles.scss +3 -0
  35. package/dist/ConfirmModal-8avl9Nv8.cjs +0 -11
  36. package/dist/ConfirmModal-8avl9Nv8.cjs.map +0 -1
  37. package/dist/ConfirmModal-CD-P8mhN.js.map +0 -1
@@ -119,12 +119,21 @@ export interface ListAppsPageOptions {
119
119
  compact?: boolean;
120
120
  /** Abort this request. Local request control only; never a wire field. */
121
121
  signal?: AbortSignal;
122
+ /** Snapshot id from a prior {@link AppsPageResult.snapshot}. Navigation normally uses `cursor` alone. */
123
+ snapshot?: string;
124
+ /** Absolute offset into the snapshot. */
125
+ offset?: number;
122
126
  }
123
127
  /** One page of apps, as returned by {@link W6WApi.listAppsPage}. */
124
128
  export interface AppsPageResult {
125
129
  apps: AppSummary[];
126
130
  /** Present unless this is the last page. */
127
131
  nextCursor?: string;
132
+ snapshot?: string;
133
+ total?: number;
134
+ offset?: number;
135
+ limit?: number;
136
+ expiresAt?: string;
128
137
  }
129
138
  export interface W6WApi {
130
139
  /**
@@ -174,6 +183,16 @@ export interface W6WApi {
174
183
  displayName?: string;
175
184
  profile?: Record<string, unknown>;
176
185
  }): Promise<ConnectionSummary>;
186
+ /**
187
+ * Rename a connection and/or replace some of its credential fields (`PATCH /connections/:id`).
188
+ * Send only the fields being changed: the server merges a partial `credential` over the stored
189
+ * one. Optional: when absent, `EditConnectionModal` shows no Save button (a one-line notice
190
+ * instead) and hosts should not offer an Edit action. It never throws for the absence.
191
+ */
192
+ updateConnection?(id: string, body: {
193
+ displayName?: string;
194
+ credential?: Record<string, unknown>;
195
+ }): Promise<ConnectionSummary>;
177
196
  /**
178
197
  * Start an OAuth 2.0 flow. Server builds the provider's authorize URL and
179
198
  * returns it; the caller opens it in a popup and awaits the server's
@@ -0,0 +1,121 @@
1
+ /**
2
+ * How the reliability board arranges itself — the decisions that are arithmetic
3
+ * rather than markup, kept pure (no React, no DOM) so they test without a DOM
4
+ * and so `@w6w/embed` can import them beside the components.
5
+ *
6
+ * **None of these decide a STATE.** Every state read below arrived already
7
+ * decided by the server; nothing here compares a ratio, applies a threshold to
8
+ * call data, or invents a fifth word. What these decide is where a row is PUT,
9
+ * which is presentation.
10
+ */
11
+ /** The four words the server uses for a health verdict. */
12
+ export type ReliabilityViewState = "ok" | "degraded" | "down" | "unknown";
13
+ /**
14
+ * The row shape the reliability components render — owned by `@w6w/ui` so it
15
+ * imports nothing from the SDK. The SDK's `ReliabilityService` assigns to it
16
+ * structurally; the generic functions below keep the caller's richer row type.
17
+ */
18
+ export interface ReliabilityServiceView {
19
+ appId: string;
20
+ displayName: string;
21
+ /** Inline icon fields — the board never asks a catalog for them. */
22
+ iconSrc?: string;
23
+ iconSrcDark?: string;
24
+ brandColor?: string;
25
+ /** "Did YOUR calls succeed". */
26
+ state: ReliabilityViewState;
27
+ calls: number;
28
+ errors: {
29
+ e4xx: number;
30
+ e429: number;
31
+ eAuth: number;
32
+ e5xx: number;
33
+ };
34
+ p95Ms: number | null;
35
+ ongoingOutageId: string | null;
36
+ /** One entry per calendar day, oldest first, exactly as the server sent it. */
37
+ uptime: readonly {
38
+ day: string;
39
+ state: ReliabilityViewState;
40
+ }[];
41
+ /** "What does the vendor say about itself"; `null` when no public check exists. */
42
+ vendorStatus: {
43
+ state: ReliabilityViewState;
44
+ message: string | null;
45
+ } | null;
46
+ /** The server's tier for the row — read, never re-derived. */
47
+ attention: "now" | "recent" | "none";
48
+ }
49
+ /** One point of the performance scatter — a service with both a call volume and a measured p95. */
50
+ export interface PerformancePoint {
51
+ appId: string;
52
+ displayName: string;
53
+ calls: number;
54
+ p95Ms: number;
55
+ state: ReliabilityViewState;
56
+ /** Total errors across all four kinds — drives the point's size, not its position. */
57
+ errorCount: number;
58
+ }
59
+ /**
60
+ * Every service with at least one timed call, as a (calls, p95) point — what
61
+ * the performance chart plots. Excludes `p95Ms: null`: there is no Y position
62
+ * for a service with nothing timed.
63
+ */
64
+ export declare function performancePoints(services: readonly ReliabilityServiceView[]): PerformancePoint[];
65
+ /**
66
+ * Does this service want the reader's attention? READS the tier the server
67
+ * sent (`attention`), never re-derives it from `state` or `vendorStatus`: the
68
+ * server's `boardRank` is the one implementation, and a second copy here would
69
+ * drift. `now` and `recent` both count; they differ in tense only.
70
+ */
71
+ export declare function needsAttention(service: Pick<ReliabilityServiceView, "attention">): boolean;
72
+ /** The board in its three groups, each keeping the server's own ordering. */
73
+ export interface BoardPartition<T extends ReliabilityServiceView = ReliabilityServiceView> {
74
+ /** Broken right now, on either signal. */
75
+ now: T[];
76
+ /** Clear now, but this window holds a failed stretch or an open advisory. */
77
+ recent: T[];
78
+ /** Everything else, in the server's recency order. */
79
+ clear: T[];
80
+ }
81
+ /**
82
+ * Group the board without reordering within any group. Bucketing is not
83
+ * sorting: each group comes out in the order it arrived. An unrecognised
84
+ * `attention` value falls to `clear` rather than throwing — a client can be
85
+ * older than the server that answered it.
86
+ */
87
+ export declare function partitionBoard<T extends ReliabilityServiceView>(services: readonly T[]): BoardPartition<T>;
88
+ /**
89
+ * The uptime strip's cells for one service — the server's own array, in the
90
+ * order it arrived (oldest first). A passthrough on purpose: no sort, no
91
+ * reverse, no gap filling. The strip has no date axis, so a reversed array is
92
+ * visually plausible and would silently move an outage to the wrong end.
93
+ */
94
+ export declare function uptimeDays(service: Pick<ReliabilityServiceView, "uptime">): {
95
+ day: string;
96
+ state: ReliabilityViewState;
97
+ }[];
98
+ /** Compact mode shows at most this many needs-attention cards, `now` filling first. */
99
+ export declare const ATTENTION_DISPLAY_CAP = 3;
100
+ /** Compact mode shows this many table rows unless the caller passes `limit`. */
101
+ export declare const RECENT_DISPLAY_CAP = 5;
102
+ /** What the board shows and what a display cap hid. */
103
+ export interface BoardDisplay<T extends ReliabilityServiceView = ReliabilityServiceView> {
104
+ /** TRUE total of attention rows (the whole input, not the capped slice). */
105
+ attentionCount: number;
106
+ shownNow: T[];
107
+ shownRecent: T[];
108
+ hiddenAttentionCount: number;
109
+ /** Table rows in the server's order, every attention tier included. */
110
+ shownTable: T[];
111
+ hiddenTableCount: number;
112
+ }
113
+ /**
114
+ * Apply the display caps to the complete board. Compact: ≤ 3 attention cards
115
+ * (`now` first, `recent` tops up) and `limit ?? 5` table rows; otherwise
116
+ * uncapped. Hidden counts are computed from the full input, so they are real.
117
+ */
118
+ export declare function displayBoard<T extends ReliabilityServiceView>(services: readonly T[], opts?: {
119
+ compact?: boolean;
120
+ limit?: number;
121
+ }): BoardDisplay<T>;
package/dist/types.d.ts CHANGED
@@ -328,6 +328,14 @@ export interface ConnectionSummary {
328
328
  profile?: Record<string, unknown>;
329
329
  createdAt?: string;
330
330
  updatedAt?: string;
331
+ /**
332
+ * True for the server's synthetic "no credential needed" rows. They have no stored credential,
333
+ * so the server 404s get/test/delete on their ids; the connection UI offers nothing on them.
334
+ * Detect with this flag only, never an id prefix.
335
+ */
336
+ builtIn?: boolean;
337
+ /** The last test result: `false` = failed, `null`/absent = never tested. */
338
+ lastTestOk?: boolean | null;
331
339
  }
332
340
  /** A saved set of action-test input values, scoped to a connection + action. */
333
341
  export interface SavedTest {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@w6w/ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "React components for w6w — central API management. Build plugins, apps and integrations on them.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://w6w.io",
@@ -98,7 +98,7 @@
98
98
  "build:css": "node scripts/build-css.mjs",
99
99
  "check:css": "node scripts/build-css.mjs --check",
100
100
  "typecheck": "tsc -b --noEmit",
101
- "test": "node --conditions=w6w-source --import ./src/test-jsx-loader.mjs --test src/__tests__/*.test.ts src/components/__tests__/*.test.ts",
101
+ "test": "node --max-old-space-size=2048 --conditions=w6w-source --import ./src/test-jsx-loader.mjs --test --test-concurrency=4 --test-timeout=120000 src/__tests__/*.test.ts src/components/__tests__/*.test.ts",
102
102
  "test:picker-layout": "bash test/picker-layout/run.sh",
103
103
  "test:expr-template": "bash test/expr-template/run.sh",
104
104
  "test:action-test-form": "bash test/action-test-form/run.sh",
@@ -0,0 +1,60 @@
1
+ .w6w-callable-list {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--w6w-sp-4);
5
+ }
6
+ .w6w-callable-list-head {
7
+ display: flex;
8
+ justify-content: space-between;
9
+ align-items: flex-start;
10
+ gap: var(--w6w-sp-4);
11
+ }
12
+ .w6w-callable-list-heading {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: var(--w6w-sp-1);
16
+ min-width: 0;
17
+ }
18
+ .w6w-callable-list-title {
19
+ margin: 0;
20
+ }
21
+ .w6w-callable-list-description {
22
+ margin: 0;
23
+ }
24
+ .w6w-callable-list-toggle {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: var(--w6w-sp-2);
28
+ cursor: pointer;
29
+ }
30
+ .w6w-callable-list-actions {
31
+ display: flex;
32
+ gap: var(--w6w-sp-2);
33
+ flex-shrink: 0;
34
+ white-space: nowrap;
35
+ }
36
+ .w6w-callable-list-body {
37
+ border: 1px solid var(--w6w-border);
38
+ border-radius: 8px;
39
+ padding: var(--w6w-sp-2);
40
+ }
41
+ .w6w-callable-list-rows {
42
+ list-style: none;
43
+ margin: 0;
44
+ padding: 0;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: var(--w6w-sp-1);
48
+ }
49
+ .w6w-callable-list-trail {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ gap: var(--w6w-sp-2);
53
+ }
54
+ .w6w-callable-list-badge {
55
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-2);
56
+ border-radius: 999px;
57
+ border: 1px solid var(--w6w-border);
58
+ color: var(--w6w-muted);
59
+ font-size: var(--w6w-fs-xs);
60
+ }
@@ -0,0 +1,100 @@
1
+ /* Connections (D-12u): ConnectionList, ConnectionHeader and the state tags they share. */
2
+ .w6w-connection-list {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--w6w-sp-3);
6
+ }
7
+ .w6w-connection-list-head {
8
+ display: flex;
9
+ justify-content: space-between;
10
+ align-items: flex-start;
11
+ gap: var(--w6w-sp-4);
12
+ }
13
+ .w6w-connection-list-heading {
14
+ min-width: 0;
15
+ }
16
+ .w6w-connection-list-actions {
17
+ display: flex;
18
+ gap: var(--w6w-sp-2);
19
+ flex-shrink: 0;
20
+ }
21
+ .w6w-connection-rows {
22
+ list-style: none;
23
+ margin: 0;
24
+ padding: 0;
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--w6w-sp-1);
28
+ }
29
+ .w6w-connection-row {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: var(--w6w-sp-2);
33
+ }
34
+ .w6w-connection-row-main {
35
+ flex: 1;
36
+ min-width: 0;
37
+ }
38
+ .w6w-connection-row-actions {
39
+ display: flex;
40
+ gap: var(--w6w-sp-1-5);
41
+ flex-shrink: 0;
42
+ }
43
+ .w6w-connection-name {
44
+ margin-right: var(--w6w-sp-2);
45
+ }
46
+
47
+ /* State tags (connected / needs attention / invalid). */
48
+ .w6w-connection-tag {
49
+ display: inline-block;
50
+ background: var(--w6w-panel-2);
51
+ border: 1px solid var(--w6w-border);
52
+ border-radius: 999px;
53
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-2);
54
+ font-size: var(--w6w-fs-xs);
55
+ margin-right: var(--w6w-sp-1);
56
+ }
57
+ .w6w-connection-tag-warn {
58
+ color: #ffb86b;
59
+ border-color: #ffb86b;
60
+ }
61
+ .w6w-connection-tag-danger {
62
+ color: var(--w6w-danger);
63
+ border-color: var(--w6w-danger);
64
+ }
65
+
66
+ /* ConnectionHeader */
67
+ .w6w-connection-header {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: var(--w6w-sp-3);
71
+ }
72
+ .w6w-connection-header-main {
73
+ flex: 1;
74
+ min-width: 0;
75
+ }
76
+ .w6w-connection-header-title {
77
+ margin: 0;
78
+ }
79
+ .w6w-connection-header-meta {
80
+ margin: 0;
81
+ }
82
+ .w6w-connection-header-actions {
83
+ display: flex;
84
+ gap: var(--w6w-sp-1-5);
85
+ align-items: center;
86
+ }
87
+ /* The id's copy button appears on hover/focus only. */
88
+ .w6w-connection-id-copy.w6w-copyable {
89
+ display: inline-flex;
90
+ width: auto;
91
+ min-width: 0;
92
+ vertical-align: middle;
93
+ }
94
+ .w6w-connection-id-copy .w6w-copyable-btn {
95
+ opacity: 0;
96
+ }
97
+ .w6w-connection-id-copy:hover .w6w-copyable-btn,
98
+ .w6w-connection-id-copy:focus-within .w6w-copyable-btn {
99
+ opacity: 1;
100
+ }
@@ -16,6 +16,10 @@ button.w6w-list-item {
16
16
  font: inherit;
17
17
  font-size: var(--w6w-fs-sm);
18
18
  }
19
+ a.w6w-list-item {
20
+ text-decoration: none;
21
+ color: inherit;
22
+ }
19
23
  .w6w-list-item:hover {
20
24
  background: var(--w6w-panel-2);
21
25
  }
@@ -0,0 +1,339 @@
1
+ /* --- ReliabilityList / ReliabilityChart ---------------------------------- */
2
+ /* The service-reliability board: "needs attention" cards, a calls x p95
3
+ scatter and the services table. State colour throughout comes from the
4
+ shared `--w6w-health-*` tokens via HealthStatusPill and UptimeStrip (and,
5
+ for the scatter, read directly) — never from a colour written here.
6
+ Every navigation site (card, service cell, scatter point) may be an `<a>`,
7
+ a `<button>` or a plain element, so the shared rules reset all three. */
8
+
9
+ .w6w-rel-section {
10
+ margin-bottom: var(--w6w-sp-6);
11
+ }
12
+ .w6w-rel-heading {
13
+ margin: 0 0 var(--w6w-sp-2);
14
+ display: flex;
15
+ align-items: baseline;
16
+ justify-content: space-between;
17
+ gap: var(--w6w-sp-3);
18
+ }
19
+ .w6w-rel-intro {
20
+ margin: 0 0 var(--w6w-sp-2);
21
+ }
22
+ .w6w-rel-card {
23
+ background: var(--w6w-panel);
24
+ border: 1px solid var(--w6w-border);
25
+ border-radius: var(--w6w-radius);
26
+ padding: var(--w6w-sp-4);
27
+ }
28
+ .w6w-rel-card h4 {
29
+ margin: 0 0 var(--w6w-sp-2);
30
+ }
31
+ .w6w-rel-tag {
32
+ display: inline-block;
33
+ background: var(--w6w-panel-2);
34
+ border: 1px solid var(--w6w-border);
35
+ border-radius: 999px;
36
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-2);
37
+ font-size: var(--w6w-fs-xs);
38
+ color: var(--w6w-muted);
39
+ margin-right: var(--w6w-sp-1);
40
+ }
41
+ .w6w-rel-group + .w6w-rel-group {
42
+ margin-top: var(--w6w-sp-5);
43
+ }
44
+ .w6w-rel-group-head {
45
+ margin: 0 0 var(--w6w-sp-2);
46
+ font-size: var(--w6w-fs-sm);
47
+ }
48
+ /* The "N more — view all" line under a capped section. */
49
+ .w6w-rel-more-note {
50
+ margin: var(--w6w-sp-2) 0 0;
51
+ }
52
+
53
+ /* --- "Needs attention" callout — a few dense cards, not table rows -------- */
54
+ .w6w-rel-issues {
55
+ display: grid;
56
+ /* `auto-fit`, not `auto-fill`: with fewer cards than fit the row, `auto-fill`
57
+ keeps empty width-reserving tracks and the cards stop short of the edge. */
58
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
59
+ gap: var(--w6w-sp-2);
60
+ }
61
+ .w6w-rel-issue-card {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: var(--w6w-sp-2);
65
+ padding: var(--w6w-sp-3);
66
+ background: var(--w6w-panel);
67
+ border: 1px solid var(--w6w-border);
68
+ /* Same `--w6w-health-*` tokens as the pills inside, so the stripe cannot
69
+ drift from the pill beside it. `down` on either signal escalates it. */
70
+ border-left: 3px solid var(--w6w-health-degraded);
71
+ border-radius: var(--w6w-radius);
72
+ color: inherit;
73
+ text-decoration: none;
74
+ /* button reset (the card is a <button> when only `onOpen` is given) */
75
+ font: inherit;
76
+ text-align: left;
77
+ width: 100%;
78
+ cursor: pointer;
79
+ }
80
+ div.w6w-rel-issue-card {
81
+ cursor: default;
82
+ }
83
+ .w6w-rel-issue-card:has(.w6w-health-pill-down) {
84
+ border-left-color: var(--w6w-health-down);
85
+ }
86
+ a.w6w-rel-issue-card:hover,
87
+ a.w6w-rel-issue-card:focus-visible,
88
+ button.w6w-rel-issue-card:hover,
89
+ button.w6w-rel-issue-card:focus-visible {
90
+ border-color: var(--w6w-accent);
91
+ }
92
+ .w6w-rel-issue-head {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: var(--w6w-sp-2);
96
+ }
97
+ .w6w-rel-issue-head-text {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: var(--w6w-sp-0-5);
101
+ min-width: 0;
102
+ }
103
+ .w6w-rel-issue-signals {
104
+ display: flex;
105
+ gap: var(--w6w-sp-4);
106
+ }
107
+ .w6w-rel-issue-signal {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: var(--w6w-sp-0-5);
111
+ }
112
+
113
+ /* --- Performance scatter — calls x p95, one point per API ----------------- */
114
+ .w6w-rel-scatter svg {
115
+ display: block;
116
+ width: 100%;
117
+ height: auto;
118
+ }
119
+ /* SVG shapes have no fill unless one is set here; without it the plot
120
+ renders as SVG's default opaque black regardless of theme. */
121
+ .w6w-rel-scatter-plot-bg {
122
+ fill: var(--w6w-panel-2);
123
+ }
124
+ .w6w-rel-scatter-grid {
125
+ stroke: var(--w6w-border);
126
+ stroke-width: 1;
127
+ }
128
+ .w6w-rel-scatter-axis-label {
129
+ fill: var(--w6w-muted);
130
+ font-size: var(--w6w-fs-xs);
131
+ }
132
+ .w6w-rel-scatter-axis-title {
133
+ fill: var(--w6w-muted);
134
+ font-size: var(--w6w-fs-xs);
135
+ }
136
+ /* The point's coloured ring — carries exec status. */
137
+ .w6w-rel-scatter-ring {
138
+ fill: var(--w6w-panel);
139
+ stroke-width: 2;
140
+ }
141
+ .w6w-rel-scatter-ring-ok {
142
+ stroke: var(--w6w-health-ok);
143
+ }
144
+ .w6w-rel-scatter-ring-degraded {
145
+ stroke: var(--w6w-health-degraded);
146
+ }
147
+ .w6w-rel-scatter-ring-down {
148
+ stroke: var(--w6w-health-down);
149
+ }
150
+ .w6w-rel-scatter-ring-unknown {
151
+ stroke: var(--w6w-health-unknown);
152
+ }
153
+ .w6w-rel-scatter-point-link {
154
+ cursor: pointer;
155
+ }
156
+ .w6w-rel-scatter-point-link:hover .w6w-rel-scatter-ring,
157
+ .w6w-rel-scatter-point-link:focus-visible .w6w-rel-scatter-ring {
158
+ stroke-width: 3;
159
+ }
160
+ .w6w-rel-scatter-point {
161
+ stroke: var(--w6w-panel);
162
+ stroke-width: 1.5;
163
+ fill-opacity: 0.85;
164
+ }
165
+ .w6w-rel-scatter-point-ok {
166
+ fill: var(--w6w-health-ok);
167
+ }
168
+ .w6w-rel-scatter-point-degraded {
169
+ fill: var(--w6w-health-degraded);
170
+ }
171
+ .w6w-rel-scatter-point-down {
172
+ fill: var(--w6w-health-down);
173
+ }
174
+ .w6w-rel-scatter-point-unknown {
175
+ fill: var(--w6w-health-unknown);
176
+ }
177
+ .w6w-rel-scatter-note {
178
+ margin: var(--w6w-sp-1) 0 0;
179
+ }
180
+
181
+ /* --- The full roster — one real <table> ------------------------------------ */
182
+ .w6w-rel-table-wrap {
183
+ overflow-x: auto;
184
+ }
185
+ .w6w-rel-table {
186
+ width: 100%;
187
+ border-collapse: collapse;
188
+ }
189
+ .w6w-rel-table th {
190
+ padding: var(--w6w-sp-2) var(--w6w-sp-3);
191
+ text-align: left;
192
+ font-size: var(--w6w-fs-xs);
193
+ font-weight: var(--w6w-fw-semibold);
194
+ color: var(--w6w-muted);
195
+ border-bottom: 1px solid var(--w6w-border);
196
+ white-space: nowrap;
197
+ }
198
+ .w6w-rel-table td {
199
+ padding: var(--w6w-sp-2-5) var(--w6w-sp-3);
200
+ border-bottom: 1px solid var(--w6w-border);
201
+ vertical-align: top;
202
+ }
203
+ /* Inside a compact card there is no rule under the last row. */
204
+ .w6w-rel-card .w6w-rel-table tbody tr:last-child td {
205
+ border-bottom: none;
206
+ }
207
+ .w6w-rel-table tbody tr:hover {
208
+ background: var(--w6w-panel-2);
209
+ }
210
+ /* A row's own stripe — same tokens and escalation as the issue card; both the
211
+ tier and the state that select the token are decided server-side. */
212
+ .w6w-rel-tr.w6w-rel-attention {
213
+ box-shadow: inset 3px 0 0 var(--w6w-health-degraded);
214
+ }
215
+ .w6w-rel-tr.w6w-rel-attention:has(.w6w-health-pill-down) {
216
+ box-shadow: inset 3px 0 0 var(--w6w-health-down);
217
+ }
218
+ .w6w-rel-tr[data-attention="recent"] {
219
+ box-shadow: inset 3px 0 0 var(--w6w-health-unknown);
220
+ }
221
+ .w6w-rel-open {
222
+ display: flex;
223
+ align-items: center;
224
+ gap: var(--w6w-sp-2);
225
+ color: inherit;
226
+ text-decoration: none;
227
+ /* button reset (the cell is a <button> when only `onOpen` is given) */
228
+ font: inherit;
229
+ text-align: left;
230
+ background: none;
231
+ border: 0;
232
+ padding: 0;
233
+ }
234
+ a.w6w-rel-open,
235
+ button.w6w-rel-open {
236
+ cursor: pointer;
237
+ }
238
+ .w6w-rel-td-service-text {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: var(--w6w-sp-0-5);
242
+ min-width: 0;
243
+ }
244
+ .w6w-rel-td-service strong {
245
+ overflow-wrap: anywhere;
246
+ }
247
+ a.w6w-rel-open:hover strong,
248
+ a.w6w-rel-open:focus-visible strong,
249
+ button.w6w-rel-open:hover strong,
250
+ button.w6w-rel-open:focus-visible strong {
251
+ color: var(--w6w-accent);
252
+ }
253
+ .w6w-rel-td-uptime {
254
+ min-width: 220px;
255
+ }
256
+ .w6w-rel-td-uptime .w6w-uptime-strip {
257
+ max-width: 260px;
258
+ }
259
+ /* A single short value ("397 ms" / "—"); no caption, whose varying length
260
+ destabilised this column's `auto` layout. */
261
+ .w6w-rel-td-p95 {
262
+ white-space: nowrap;
263
+ }
264
+ /* Both signals stacked as one line each ("Ops: <pill>" / "API: <pill>") rather
265
+ than two columns, so the table fits a smaller screen without scrolling. */
266
+ .w6w-rel-td-status {
267
+ display: flex;
268
+ flex-direction: column;
269
+ gap: var(--w6w-sp-1);
270
+ min-width: 140px;
271
+ }
272
+ .w6w-rel-status-row {
273
+ display: flex;
274
+ align-items: center;
275
+ flex-wrap: wrap;
276
+ gap: var(--w6w-sp-1-5);
277
+ }
278
+ .w6w-rel-status-label {
279
+ min-width: 28px;
280
+ }
281
+ /* Within a status ROW the signal reads inline, beside its label — the issue
282
+ card's own use of `.w6w-rel-signal` (label above pill) is unaffected. */
283
+ .w6w-rel-status-row .w6w-rel-signal {
284
+ flex-direction: row;
285
+ align-items: center;
286
+ flex-wrap: wrap;
287
+ }
288
+ /* One labelled signal inside a state cell/card — label ABOVE its pill, so two
289
+ labelled pills in a narrow column do not wrap unpredictably. */
290
+ .w6w-rel-signal {
291
+ display: flex;
292
+ flex-direction: column;
293
+ align-items: flex-start;
294
+ gap: var(--w6w-sp-0-5);
295
+ min-width: 0;
296
+ }
297
+ .w6w-rel-vendor-msg {
298
+ color: var(--w6w-muted);
299
+ /* The API's own incident title — clamped, full text in the row's `title`, so
300
+ one verbose status page cannot stretch a board row. */
301
+ display: -webkit-box;
302
+ -webkit-line-clamp: 2;
303
+ -webkit-box-orient: vertical;
304
+ overflow: hidden;
305
+ overflow-wrap: anywhere;
306
+ }
307
+ /* A service the account never called: no strip, just a note. */
308
+ .w6w-rel-idle-note {
309
+ font-size: var(--w6w-fs-xs);
310
+ font-style: italic;
311
+ color: var(--w6w-text);
312
+ }
313
+ .w6w-rel-allclear {
314
+ margin-bottom: var(--w6w-sp-4);
315
+ }
316
+ .w6w-rel-allclear p {
317
+ margin: 0 0 var(--w6w-sp-1-5);
318
+ }
319
+ @media (max-width: 900px) {
320
+ /* The table scrolls inside `.w6w-rel-table-wrap` rather than reflowing —
321
+ 4 columns of pills and a strip cannot usefully stack. */
322
+ .w6w-rel-table {
323
+ min-width: 600px;
324
+ }
325
+ }
326
+
327
+ /* "Show all services →" when only `onShowAll` is given: a <button> that reads as the link. */
328
+ .w6w-rel-show-all {
329
+ font: inherit;
330
+ background: none;
331
+ border: 0;
332
+ padding: 0;
333
+ color: inherit;
334
+ cursor: pointer;
335
+ }
336
+ .w6w-rel-show-all:hover,
337
+ .w6w-rel-show-all:focus-visible {
338
+ color: var(--w6w-accent);
339
+ }