@reliability-design/web 1.4.2 → 1.4.4
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/CHANGELOG.md +27 -0
- package/dist/components/rel-autocomplete/README.md +4 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +80 -3
- package/dist/components/rel-button/rel-button.ts +2 -2
- package/dist/components/rel-checkbox/README.md +69 -22
- package/dist/components/rel-checkbox/rel-checkbox.ts +116 -9
- package/dist/components/rel-pagination/README.md +14 -0
- package/dist/components/rel-pagination/rel-pagination.ts +30 -14
- package/dist/components/rel-radio/README.md +16 -1
- package/dist/components/rel-radio/rel-radio.ts +52 -1
- package/dist/components/rel-range-picker/rel-range-picker.ts +117 -2
- package/dist/components/rel-select/README.md +7 -0
- package/dist/components/rel-select/rel-select.ts +1237 -1119
- package/dist/components/rel-textfield/README.md +16 -0
- package/dist/components/rel-textfield/rel-textfield.ts +104 -6
- package/dist/components/rel-time-picker/README.md +199 -0
- package/dist/components/rel-time-picker/rel-time-picker.ts +994 -0
- package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +134 -0
- package/dist/components/rel-upload/rel-upload-actions.ts +163 -0
- package/dist/components/rel-upload/rel-upload.ts +943 -0
- package/dist/index.d.mts +453 -39
- package/dist/index.d.ts +453 -39
- package/dist/index.js +2659 -339
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2663 -346
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# rel-upload — implementation vs. Figma, and what to update in the design
|
|
2
|
+
|
|
3
|
+
**For:** design team · **From:** engineering · **Date:** 2026-08-09
|
|
4
|
+
**Figma reference:** Reliability DS V1.0 → "↳ File Upload" page (node `8160:917`)
|
|
5
|
+
— `Upload Dropzone` (`8160:2615`), `Upload File Row` (`8160:2566`), `Upload Modal` (`8160:2622`)
|
|
6
|
+
**Shipped:** `<rel-upload>` + `<rel-upload-actions>` in `packages/web/src/components/rel-upload/`
|
|
7
|
+
|
|
8
|
+
This replaces the previous version of this doc, which was written against the
|
|
9
|
+
outdated sandbox design. Per our agreement, engineering adapted the design where
|
|
10
|
+
it had gaps or conflicted with how the design system already works — everything
|
|
11
|
+
that differs is listed here so the Figma file can be brought in line.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What matches the Figma exactly
|
|
16
|
+
|
|
17
|
+
- **Dropzone**: 16px radius, dashed border, 32/24 padding, circular icon badge,
|
|
18
|
+
"Drag and drop your files here" (16/24 medium) over the format/size hint
|
|
19
|
+
(12/20), pill "Browse files" button.
|
|
20
|
+
- **File rows**: 8px radius, 12/16 padding, document icon → name (14/22) + meta
|
|
21
|
+
(12/20) → trailing status icon + remove. All four states (Ready, Uploading,
|
|
22
|
+
Success, Error) including the tinted error row.
|
|
23
|
+
- **Progress bar**: 4px, fully rounded, green fill, with the
|
|
24
|
+
`45% - 2.4 MB of 5.1 MB` meta line.
|
|
25
|
+
- **Modal**: composed from the existing `<rel-modal>` — title, supporting text,
|
|
26
|
+
close ×, scrolling body, pinned footer. All six variants are reproduced.
|
|
27
|
+
- **Copy**: every string in the design ships verbatim, including
|
|
28
|
+
`Selected files (3)`, `Uploading 3 files`, `3 files - 2 failed`,
|
|
29
|
+
`Upload 3 files`, `Retry upload`, and both alert messages.
|
|
30
|
+
|
|
31
|
+
The hint row is driven by real props: formats come from `accept`, the size limit
|
|
32
|
+
from `max-size`. The design's "PDF, PNG or JPG" / "10 MB" are example values.
|
|
33
|
+
|
|
34
|
+
## Colour changes the design should adopt
|
|
35
|
+
|
|
36
|
+
All three are contrast or token-layer corrections. Every hex in the design
|
|
37
|
+
already exists in `vars.css`, but three of them are used at the wrong layer.
|
|
38
|
+
|
|
39
|
+
### 1. Secondary text `#7D7D7D` → `--rel-text-caption` `#525252`
|
|
40
|
+
|
|
41
|
+
`#7D7D7D` (gray-400) scores **4.12:1 on white — below the 4.5:1 gate**, and in
|
|
42
|
+
the token system gray-400 is not a body-text token at all. Used for the dropzone
|
|
43
|
+
hint and the file-row meta line. `--rel-text-caption` (`#525252`) scores
|
|
44
|
+
**7.81:1**.
|
|
45
|
+
|
|
46
|
+
### 2. Success text `#008655` → `--rel-text-success` `#006440`
|
|
47
|
+
|
|
48
|
+
Green-600 passes (4.62:1) but the system's success-text token is green-700
|
|
49
|
+
(**7.25:1**), and the green rule fixes success text/icons at `#006440`. Affects
|
|
50
|
+
the "…- Uploaded" meta line and the success check icon.
|
|
51
|
+
|
|
52
|
+
### 3. Dashed dropzone border `#C2C2C2` → `--rel-border-accent` `#8C8C8C`
|
|
53
|
+
|
|
54
|
+
`#C2C2C2` scores **1.78:1**, under the 3:1 gate for UI boundaries, and gray-200
|
|
55
|
+
has no border token. `--rel-border-accent` (gray-350) scores **3.36:1**.
|
|
56
|
+
|
|
57
|
+
Also note: the error row border uses `--rel-border-danger` (`#CE3131`, 5.11:1)
|
|
58
|
+
rather than the design's `#FF5252` (3.19:1) — red-500 has no border token.
|
|
59
|
+
And the error meta on the tinted row (`#CE3131` on `#FFEBEB`) lands at
|
|
60
|
+
**4.46:1**, marginally under. That pairing is inherited from the existing
|
|
61
|
+
`rel-alert` subtle-error style, so it is a system-wide question rather than
|
|
62
|
+
something this component introduced — flagging it, not changing it here.
|
|
63
|
+
|
|
64
|
+
## Structural changes
|
|
65
|
+
|
|
66
|
+
### 4. The footer is a separate element, not part of the upload body
|
|
67
|
+
|
|
68
|
+
In the Figma the Cancel/Upload row sits in the Modal Sheet, outside the slot
|
|
69
|
+
holding the upload body. We matched that: `<rel-upload>` renders the alert,
|
|
70
|
+
dropzone and file list; `<rel-upload-actions>` renders the buttons and goes in
|
|
71
|
+
`<rel-modal>`'s footer slot. This is what keeps the footer **pinned while the
|
|
72
|
+
file list scrolls** — with five files the body scrolls and the buttons stay put.
|
|
73
|
+
The actions element finds its `<rel-upload>` sibling automatically.
|
|
74
|
+
|
|
75
|
+
### 5. The dropzone is not itself a button
|
|
76
|
+
|
|
77
|
+
The design puts a "Browse files" button inside the dropzone. Making the whole
|
|
78
|
+
dropzone clickable *and* focusable would nest interactive controls inside each
|
|
79
|
+
other, which is an accessibility violation. The dropzone stays a drag-and-drop
|
|
80
|
+
target and a mouse click affordance, but "Browse files" is the only focusable
|
|
81
|
+
control — so keyboard and screen-reader users get exactly one clear action.
|
|
82
|
+
|
|
83
|
+
### 6. Buttons and the alert are the standard DS components
|
|
84
|
+
|
|
85
|
+
Cancel/Upload are `rel-button`, and both alerts are `rel-alert` (warning and
|
|
86
|
+
error, subtle). They inherit the system's colours, states and focus rings rather
|
|
87
|
+
than the frame's local styles. One visible consequence: the design draws the
|
|
88
|
+
buttons as pills, which `rel-button` supports, but the disabled Upload uses the
|
|
89
|
+
system's disabled fill rather than the frame's dimmed grey. If design prefers the
|
|
90
|
+
dimmed look, that is a change to the *button* spec, not to upload.
|
|
91
|
+
|
|
92
|
+
## Behaviour the design doesn't specify
|
|
93
|
+
|
|
94
|
+
### 7. Validation failures render as error rows
|
|
95
|
+
|
|
96
|
+
The Figma Failure state shows rows reading "Upload failed - file exceeds the
|
|
97
|
+
10 MB limit" and "Upload failed - unsupported file type". Both are knowable
|
|
98
|
+
*before* any transfer, so we surface rejected files as error rows immediately on
|
|
99
|
+
drop rather than silently discarding them. The wording is unchanged.
|
|
100
|
+
|
|
101
|
+
### 8. Retry only re-sends what can succeed
|
|
102
|
+
|
|
103
|
+
A file rejected for format or size will fail identically every time, so
|
|
104
|
+
"Retry upload" skips it — and if *every* failure is of that kind the button is
|
|
105
|
+
disabled. Removing the offending row returns the card to the normal Upload state.
|
|
106
|
+
|
|
107
|
+
### 9. "Upload" wins over "Retry" while anything is still pending
|
|
108
|
+
|
|
109
|
+
If some rows failed but others are still ready, the primary action stays
|
|
110
|
+
"Upload N files". Retry only appears once there is nothing left to send. The
|
|
111
|
+
Figma Multi-upload and Failure frames are both consistent with this.
|
|
112
|
+
|
|
113
|
+
## States still missing from the Figma
|
|
114
|
+
|
|
115
|
+
These are implemented, but no frame specs them:
|
|
116
|
+
|
|
117
|
+
| State | What ships today |
|
|
118
|
+
| ----- | ---------------- |
|
|
119
|
+
| **Drag-over** | Dashed border becomes solid brand green, surface takes an 8% brand tint |
|
|
120
|
+
| **Disabled** | Whole card at 60% opacity, nothing interactive |
|
|
121
|
+
| **Dark mode** | Works automatically — every colour is a semantic token |
|
|
122
|
+
|
|
123
|
+
A frame for each (drag-over especially) would make the set complete.
|
|
124
|
+
|
|
125
|
+
## Suggested Figma additions
|
|
126
|
+
|
|
127
|
+
1. Drag-over variant
|
|
128
|
+
2. Disabled variant
|
|
129
|
+
3. Dark reference frame
|
|
130
|
+
4. Apply the three colour corrections above to the shared styles
|
|
131
|
+
|
|
132
|
+
Questions and pushback welcome — none of this is precious, but items 1–3 and 5
|
|
133
|
+
fix measurable contrast or accessibility problems, so if the design goes a
|
|
134
|
+
different way it needs to solve those rather than reverting.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
2
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
import "@reliability-design/icons/rel-icon";
|
|
4
|
+
import "../rel-button/rel-button";
|
|
5
|
+
import type { RelUpload, UploadState } from "./rel-upload";
|
|
6
|
+
|
|
7
|
+
const EMPTY_STATE: UploadState = {
|
|
8
|
+
phase: "empty",
|
|
9
|
+
total: 0,
|
|
10
|
+
failed: 0,
|
|
11
|
+
canUpload: false,
|
|
12
|
+
primaryLabel: "Upload",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `<rel-upload-actions>` — the Cancel / Upload footer for `<rel-upload>`.
|
|
17
|
+
*
|
|
18
|
+
* Lives outside `<rel-upload>` so it can sit in `<rel-modal>`'s footer slot and
|
|
19
|
+
* stay pinned while the upload body scrolls. It mirrors the sibling
|
|
20
|
+
* `<rel-upload>`'s derived state, so the button label follows the design:
|
|
21
|
+
* "Upload" → "Upload 3 files" → spinner → "Retry upload".
|
|
22
|
+
*
|
|
23
|
+
* ```html
|
|
24
|
+
* <rel-modal open side="center" title="Upload documents">
|
|
25
|
+
* <rel-upload multiple .uploader=${fn}></rel-upload>
|
|
26
|
+
* <rel-upload-actions slot="footer"></rel-upload-actions>
|
|
27
|
+
* </rel-modal>
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The target is found automatically among its siblings. Use `for` to point at a
|
|
31
|
+
* specific `<rel-upload>` by id when a container holds more than one.
|
|
32
|
+
*
|
|
33
|
+
* @element rel-upload-actions
|
|
34
|
+
*/
|
|
35
|
+
@customElement("rel-upload-actions")
|
|
36
|
+
export class RelUploadActions extends LitElement {
|
|
37
|
+
/** Id of the `<rel-upload>` to control. Omit to auto-discover a sibling. */
|
|
38
|
+
@property({ type: String }) for = "";
|
|
39
|
+
|
|
40
|
+
/** Cancel button label. */
|
|
41
|
+
@property({ type: String, attribute: "cancel-label" }) cancelLabel = "Cancel";
|
|
42
|
+
|
|
43
|
+
/** Hides Cancel for flows that only ever move forward. */
|
|
44
|
+
@property({ type: Boolean, attribute: "no-cancel" }) noCancel = false;
|
|
45
|
+
|
|
46
|
+
@state() private snapshot: UploadState = EMPTY_STATE;
|
|
47
|
+
|
|
48
|
+
private target: RelUpload | null = null;
|
|
49
|
+
|
|
50
|
+
static styles = css`
|
|
51
|
+
:host {
|
|
52
|
+
display: block;
|
|
53
|
+
font-family: var(--rel-fonts-font-family);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.actions {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: flex-end;
|
|
60
|
+
gap: var(--rel-spacing-xs, 8px);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
rel-button rel-icon {
|
|
64
|
+
margin-inline-end: var(--rel-spacing-xs, 8px);
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
connectedCallback() {
|
|
69
|
+
super.connectedCallback();
|
|
70
|
+
// Siblings may not be upgraded yet when the footer slot connects first.
|
|
71
|
+
queueMicrotask(() => this.resolveTarget());
|
|
72
|
+
this.root.addEventListener("rel-state", this.onState as EventListener);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
disconnectedCallback() {
|
|
76
|
+
super.disconnectedCallback();
|
|
77
|
+
this.root.removeEventListener("rel-state", this.onState as EventListener);
|
|
78
|
+
this.target = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
updated(changed: Map<string, unknown>) {
|
|
82
|
+
if (changed.has("for")) this.resolveTarget();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Listen on our own root rather than `document`: a shared root means no
|
|
87
|
+
* shadow boundary sits between us and the sibling `<rel-upload>`, so
|
|
88
|
+
* `event.target` is never retargeted away from it.
|
|
89
|
+
*/
|
|
90
|
+
private get root(): Document | ShadowRoot {
|
|
91
|
+
const node = this.getRootNode();
|
|
92
|
+
return node instanceof ShadowRoot ? node : this.ownerDocument;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private resolveTarget() {
|
|
96
|
+
const root = this.getRootNode() as ParentNode;
|
|
97
|
+
let found: RelUpload | null = null;
|
|
98
|
+
|
|
99
|
+
if (this.for) {
|
|
100
|
+
found = root.querySelector<RelUpload>(`#${CSS.escape(this.for)}`);
|
|
101
|
+
} else {
|
|
102
|
+
found =
|
|
103
|
+
this.parentElement?.querySelector<RelUpload>("rel-upload") ??
|
|
104
|
+
root.querySelector<RelUpload>("rel-upload");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.target = found;
|
|
108
|
+
if (found) this.snapshot = found.state;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private onState = (e: CustomEvent<UploadState>) => {
|
|
112
|
+
if (!this.target) this.resolveTarget();
|
|
113
|
+
if (e.target !== this.target) return;
|
|
114
|
+
this.snapshot = e.detail;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
private onCancel = () => {
|
|
118
|
+
this.target?.cancel();
|
|
119
|
+
this.dispatchEvent(new CustomEvent("rel-cancel", { bubbles: true, composed: true }));
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
private onPrimary = () => {
|
|
123
|
+
if (!this.target) return;
|
|
124
|
+
if (this.snapshot.phase === "failed") this.target.retry();
|
|
125
|
+
else this.target.upload();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
render() {
|
|
129
|
+
const { phase, canUpload, primaryLabel } = this.snapshot;
|
|
130
|
+
const busy = phase === "uploading" || phase === "delayed";
|
|
131
|
+
|
|
132
|
+
return html`
|
|
133
|
+
<div class="actions" part="base">
|
|
134
|
+
${this.noCancel
|
|
135
|
+
? nothing
|
|
136
|
+
: html`<rel-button part="cancel" variant="secondary" @click=${this.onCancel}>
|
|
137
|
+
${this.cancelLabel}
|
|
138
|
+
</rel-button>`}
|
|
139
|
+
|
|
140
|
+
<rel-button
|
|
141
|
+
part="primary"
|
|
142
|
+
variant="primary"
|
|
143
|
+
?loading=${busy}
|
|
144
|
+
?disabled=${!canUpload}
|
|
145
|
+
aria-label=${busy ? "Uploading" : primaryLabel}
|
|
146
|
+
@click=${this.onPrimary}
|
|
147
|
+
>
|
|
148
|
+
${busy
|
|
149
|
+
? nothing
|
|
150
|
+
: html`${phase === "failed"
|
|
151
|
+
? html`<rel-icon name="refresh" size="sm"></rel-icon>`
|
|
152
|
+
: nothing}${primaryLabel}`}
|
|
153
|
+
</rel-button>
|
|
154
|
+
</div>
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
declare global {
|
|
160
|
+
interface HTMLElementTagNameMap {
|
|
161
|
+
"rel-upload-actions": RelUploadActions;
|
|
162
|
+
}
|
|
163
|
+
}
|