@tensor_1/react 2.30.0 → 2.31.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/README.md CHANGED
@@ -377,3 +377,83 @@ markers). It pairs with `SchedulingCalendar`: same entry states, the same explic
377
377
  - **Keyboard:** one tab stop. ←/→ go to the previous or next bar in a lane, ↑/↓ to the nearest bar in the next lane, Home/End to the lane's ends, and Enter selects.
378
378
  - **Windows and markers:** `overlays` (`freeze`, `maintenance`, `blackout`; all lanes, or one with `laneId`), `markers` (a point in time) and `now` are drawn behind the bars and listed for screen readers.
379
379
  - Pure helpers are exported for tests and server code: `timelineTicks`, `placeSpan`, `layoutLane`, `shiftSpan`, `snapToStep`, `pixelsToMs`, `defaultTimelineStep`.
380
+
381
+ ### FilterEditor (typed column filters)
382
+
383
+ `FilterEditor` is the content of one column-filter popover with a typed value (HAR-1365; TENSOR's
384
+ `table-filter-editor.tsx`, MOTUS's "+ filter"). `summarizeFilter` turns the applied value into the chip text.
385
+ `FilterPopover` stays for the single-string case.
386
+
387
+ ```tsx
388
+ const field: FilterField = { id: 'state', label: t('state'), kind: 'enum', options: states };
389
+ <Popover anchor={chipRef} popover="manual">
390
+ <FilterEditor field={field} value={draft} onValueChange={setDraft} onApply={() => apply(field.id, draft)} onClear={() => clear(field.id)} />
391
+ </Popover>
392
+ <Chip onClick={edit} onRemove={remove}>{summarizeFilter(field, applied, { formatDate: formatDisplayDate })}</Chip>
393
+ ```
394
+
395
+ | `kind` | Value | Editor |
396
+ |---|---|---|
397
+ | `enum` | `{ values: string[] }` | checkboxes; a diacritic-insensitive search above more than `searchThreshold` (8) options; Select all / Clear selection |
398
+ | `text` | `{ operator, text }` | condition (`contains`, `not-contains`, `is`, `is-not`, `starts-with`) + text; Enter applies |
399
+ | `number` | `{ operator, value, to? }` | condition (`eq`…`gte`, `between`) + value(s), optional `unit` |
400
+ | `date-range` | `{ from?, to? }` | two date fields that bound each other |
401
+ | `boolean` | `{ value?: boolean }` | Any / Yes / No (`trueLabel`, `falseLabel`) |
402
+ | `reference` | `{ values: { value, label }[] }` | `onSearch(query)` → records, debounced (`searchDelay`), with loading, empty and error (retry) states; chosen records as removable chips |
403
+
404
+ `isFilterEmpty`, `emptyFilterValue`, the operator lists and all words (`labels`) are exported, and the value is plain
405
+ data for your own query string. Stale search results are ignored.
406
+ ### EntityPicker
407
+
408
+ A form field that holds one record found by an async search (HAR-1366; TENSOR's `entity-picker.tsx` and its
409
+ person, incident and CI pickers; MOTUS's author filter). Built on `SearchSuggest`.
410
+
411
+ ```tsx
412
+ <Field label={t('assignee')} htmlFor="assignee" hint={t('assigneeHint')}>
413
+ <EntityPicker id="assignee" name="assignee_id" label={t('assignee')} value={assignee} onValueChange={setAssignee}
414
+ onSearch={(q) => api.people.search(q)} labels={{ none: t('choosePerson') }} />
415
+ </Field>
416
+ ```
417
+
418
+ - **Chosen:** the record shows in the field (title plus its last breadcrumb, or `renderValue`), with a × named "Clear {title}". Choosing the field again opens a fresh search; Escape with an empty query returns to the record.
419
+ - **Searching:** debounced (`delay`, 250 ms) from `minQueryLength` (0) characters, with loading, empty and error states. **Retry is a list row**, so arrow keys reach it. Stale results are ignored. Focus returns to the field after a choice.
420
+ - **Forms:** `name` posts the chosen id in a hidden input; `id` and the `Field` hint and error wire to the active control. `SearchSuggest` gains `inputId` and `inputDescribedBy` for the same purpose.
421
+
422
+ ### Lightbox
423
+
424
+ A full-screen media viewer (HAR-1367; TENSOR's capture viewer, MOTUS's programme gallery and photo zoom).
425
+
426
+ ```tsx
427
+ <Lightbox items={photos.map((p) => ({ src: p.url, alt: p.alt, caption: p.caption, width: p.w, height: p.h }))}
428
+ index={index} onIndexChange={setIndex} open={open} onClose={() => setOpen(false)} />
429
+ ```
430
+
431
+ - **Moving:** previous/next buttons, ←/→, Home/End and a swipe on touch (`loop` wraps). An announced "{index} of {total}" counter and the caption sit under the media.
432
+ - **Size:** "Show actual size" switches between fitted and natural size; the stage scrolls when zoomed.
433
+ - **Video:** `kind: 'video'` renders a native player, which keeps its own arrow keys.
434
+ - **Dialog:** native `<dialog>`. Focus starts on Close, Escape and a backdrop click close it, and focus returns to the opener. Every word is in `labels`.
435
+ - **Image size:** pass `width` and `height` so an SVG or a slow image keeps its box.
436
+
437
+ ### FileUpload and Attachment
438
+
439
+ `FileUpload` is a drop zone, file picker and upload list (HAR-984; TENSOR's attachment panel, MOTUS's photo intake). `Attachment` is a row for a file that is already stored (HAR-985).
440
+
441
+ ```tsx
442
+ <FileUpload items={uploads} accept=".pdf,image/*" maxSize={20_000_000} maxFiles={10}
443
+ hint="PDF or photos, up to 20 MB"
444
+ onFilesAdded={(accepted) => accepted.forEach(startUpload)}
445
+ onRetry={retryUpload} onRemove={removeUpload} />
446
+
447
+ <AttachmentList label={`Attachments (${files.length})`}>
448
+ {files.map((f) => (
449
+ <Attachment key={f.id} name={f.name} size={f.size} type={f.type} href={f.url} download
450
+ meta={`${f.addedBy} · ${f.addedAt}`} onRemove={() => confirmRemove(f)} />
451
+ ))}
452
+ </AttachmentList>
453
+ ```
454
+
455
+ - **The product uploads.** `onFilesAdded(accepted, rejected)` gets the files that passed `accept`, `maxSize` and `maxFiles` (counting `items`). Report progress, success and failure back through `items` (`status`, `progress`, `error`). Rejected files are listed in an alert, one sentence each.
456
+ - **Keyboard:** "Choose files" is a real button, described by the `hint`. Dropping is an extra, not the only way in. `capture` opens the camera on phones.
457
+ - **List:** each file shows its size and status. Uploads show a progress bar (indeterminate without `progress`), and failures show the reason and a Retry button. `previewUrl` shows a thumbnail, and `onAltChange` asks for alt text on images.
458
+ - **Attachment:** the name is the link (`href`, `download`, `renderLink`). It has a quieter `meta` line, a `state` slot (e.g. a "Scanning" `StatusPill`), `actions`, `onRemove`, and `status` `loading` | `error` | `forbidden` (no link, and the reason shown). It's server-renderable.
459
+ - Pure helpers: `formatFileSize`, `fileMatchesAccept`, `partitionFiles`, `fileKind`.
@@ -0,0 +1,55 @@
1
+ import * as react from 'react';
2
+ import { LiHTMLAttributes, ReactNode, HTMLAttributes } from 'react';
3
+ import { UixRenderLink } from '../link.js';
4
+
5
+ interface AttachmentLabels {
6
+ /** Remove button. `{name}`. */
7
+ remove: string;
8
+ /** Download link name when `download` is set. `{name}`. */
9
+ download: string;
10
+ loading: string;
11
+ forbidden: string;
12
+ }
13
+ declare const DEFAULT_ATTACHMENT_LABELS: AttachmentLabels;
14
+ interface AttachmentProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'children'> {
15
+ /** File name as the user should read it (decrypted, if the product stores it encrypted). */
16
+ name: string;
17
+ /** Bytes. */
18
+ size?: number;
19
+ /** MIME type, for the file glyph. */
20
+ type?: string;
21
+ /** Opens or downloads the file. The name becomes the link. */
22
+ href?: string;
23
+ /** Download instead of opening; a string sets the saved file name. */
24
+ download?: boolean | string;
25
+ renderLink?: UixRenderLink;
26
+ /** A quieter line: who added it and when. */
27
+ meta?: ReactNode;
28
+ /** A state slot after the name, e.g. `<StatusPill tone="warning">Scanning</StatusPill>`. */
29
+ state?: ReactNode;
30
+ /** Replaces the file glyph (e.g. an image thumbnail). */
31
+ thumbnail?: ReactNode;
32
+ /** Adds a remove button; confirm destructive removals in the handler (`ConfirmDialog`/`Popconfirm`). */
33
+ onRemove?: () => void;
34
+ /** `loading` (no link yet), `error` (shows `error`), `forbidden` (no link, a reason). */
35
+ status?: 'ready' | 'loading' | 'error' | 'forbidden';
36
+ error?: ReactNode;
37
+ /** Extra actions at the end of the row. */
38
+ actions?: ReactNode;
39
+ locale?: string;
40
+ labels?: Partial<AttachmentLabels>;
41
+ }
42
+ /**
43
+ * One file row (HAR-985; TENSOR C15/B12 `attachments-panel-view.tsx`, MEDx S19/S52): glyph or
44
+ * thumbnail, the name as a link, size and meta, a state slot, and remove. Server-renderable.
45
+ */
46
+ declare function Attachment({ name, size, type, href, download, renderLink, meta, state, thumbnail, onRemove, status, error, actions, locale, labels: labelOverrides, className, ...props }: AttachmentProps): react.JSX.Element;
47
+ interface AttachmentListProps extends HTMLAttributes<HTMLUListElement> {
48
+ /** Names the list, e.g. "Attachments (3)". */
49
+ label?: string;
50
+ children?: ReactNode;
51
+ }
52
+ /** A list of `Attachment` rows. */
53
+ declare function AttachmentList({ label, className, children, ...props }: AttachmentListProps): react.JSX.Element;
54
+
55
+ export { Attachment, type AttachmentLabels, AttachmentList, type AttachmentListProps, type AttachmentProps, DEFAULT_ATTACHMENT_LABELS };
@@ -0,0 +1,69 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../cx.js";
3
+ import { fillLabel } from "../fill-label.js";
4
+ import { formatFileSize } from "../file-model.js";
5
+ import { renderUixLink } from "../link.js";
6
+ import { Button } from "./Button.js";
7
+ import { FileIcon } from "./FileIcon.js";
8
+ const DEFAULT_ATTACHMENT_LABELS = {
9
+ remove: "Remove {name}",
10
+ download: "Download {name}",
11
+ loading: "Loading\u2026",
12
+ forbidden: "You do not have access to this file."
13
+ };
14
+ function Attachment({
15
+ name,
16
+ size,
17
+ type,
18
+ href,
19
+ download,
20
+ renderLink,
21
+ meta,
22
+ state,
23
+ thumbnail,
24
+ onRemove,
25
+ status = "ready",
26
+ error,
27
+ actions,
28
+ locale,
29
+ labels: labelOverrides,
30
+ className,
31
+ ...props
32
+ }) {
33
+ const labels = { ...DEFAULT_ATTACHMENT_LABELS, ...labelOverrides };
34
+ const linked = href != null && status !== "loading" && status !== "forbidden";
35
+ const nameNode = linked ? renderUixLink(renderLink, {
36
+ href,
37
+ className: "uix-attachment__name",
38
+ children: name,
39
+ ...download ? { download: typeof download === "string" ? download : "", "aria-label": fillLabel(labels.download, { name }) } : {}
40
+ }) : /* @__PURE__ */ jsx("span", { className: "uix-attachment__name", children: name });
41
+ return /* @__PURE__ */ jsxs("li", { className: cx("uix-attachment", className), "data-status": status === "ready" ? void 0 : status, "aria-busy": status === "loading" || void 0, ...props, children: [
42
+ /* @__PURE__ */ jsx("span", { className: "uix-attachment__icon", "aria-hidden": "true", children: thumbnail ?? /* @__PURE__ */ jsx(FileIcon, { name, type }) }),
43
+ /* @__PURE__ */ jsxs("span", { className: "uix-attachment__body", children: [
44
+ /* @__PURE__ */ jsxs("span", { className: "uix-attachment__line", children: [
45
+ nameNode,
46
+ state != null && /* @__PURE__ */ jsx("span", { className: "uix-attachment__state", children: state })
47
+ ] }),
48
+ /* @__PURE__ */ jsxs("span", { className: "uix-attachment__meta", children: [
49
+ size !== void 0 && /* @__PURE__ */ jsx("span", { className: "uix-attachment__size", children: formatFileSize(size, locale) }),
50
+ meta != null && /* @__PURE__ */ jsx("span", { children: meta }),
51
+ status === "loading" && /* @__PURE__ */ jsx("span", { children: labels.loading }),
52
+ status === "forbidden" && /* @__PURE__ */ jsx("span", { children: labels.forbidden })
53
+ ] }),
54
+ status === "error" && error != null && /* @__PURE__ */ jsx("span", { className: "uix-attachment__error", role: "alert", children: error })
55
+ ] }),
56
+ (actions != null || onRemove) && /* @__PURE__ */ jsxs("span", { className: "uix-attachment__actions", children: [
57
+ actions,
58
+ onRemove && /* @__PURE__ */ jsx(Button, { type: "button", size: "xs", variant: "ghost", icon: true, "aria-label": fillLabel(labels.remove, { name }), onClick: onRemove, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14" }) }) })
59
+ ] })
60
+ ] });
61
+ }
62
+ function AttachmentList({ label, className, children, ...props }) {
63
+ return /* @__PURE__ */ jsx("ul", { className: cx("uix-attachments", "uix-attachments--list", className), "aria-label": label, ...props, children });
64
+ }
65
+ export {
66
+ Attachment,
67
+ AttachmentList,
68
+ DEFAULT_ATTACHMENT_LABELS
69
+ };
@@ -0,0 +1,56 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { SearchSuggestOption } from './SearchSuggest.js';
4
+
5
+ interface EntityPickerLabels {
6
+ /** Accessible name of the search field while picking. */
7
+ search: string;
8
+ loading: string;
9
+ empty: string;
10
+ error: string;
11
+ retry: string;
12
+ /** Clears the chosen record. `{title}`. */
13
+ clear: string;
14
+ /** Names the chosen-record button that reopens the search. `{label}`, `{title}`. */
15
+ change: string;
16
+ /** Shown in the field when nothing is chosen. */
17
+ none: string;
18
+ /** Polite count after a search. `{count}`. */
19
+ results: string;
20
+ }
21
+ declare const DEFAULT_ENTITY_PICKER_LABELS: EntityPickerLabels;
22
+ interface EntityPickerProps {
23
+ /** The chosen record, or null. */
24
+ value: SearchSuggestOption | null;
25
+ onValueChange: (next: SearchSuggestOption | null) => void;
26
+ /** Finds records for the query; a rejection shows the error state with a Retry row. */
27
+ onSearch: (query: string) => Promise<readonly SearchSuggestOption[]>;
28
+ /** The field's name (the visible label usually comes from a surrounding `Field`). */
29
+ label: string;
30
+ /** Id of the control, for `Field htmlFor`/`label for`. */
31
+ id?: string;
32
+ /** Hidden form value: the chosen record's id. */
33
+ name?: string;
34
+ placeholder?: string;
35
+ /** Search only from this many characters. Default 0 (an empty query lists suggestions). */
36
+ minQueryLength?: number;
37
+ /** Debounce before searching, in ms. Default 250. */
38
+ delay?: number;
39
+ disabled?: boolean;
40
+ invalid?: boolean;
41
+ /** Ids of hint/error text (from `Field`). */
42
+ 'aria-describedby'?: string;
43
+ /** How the chosen record shows in the field. Default: title, then the last breadcrumb. */
44
+ renderValue?: (option: SearchSuggestOption) => ReactNode;
45
+ labels?: Partial<EntityPickerLabels>;
46
+ className?: string;
47
+ }
48
+ /**
49
+ * A form field that holds one record found by an async search (HAR-1366; TENSOR B28/C5
50
+ * `entity-picker.tsx`, MOTUS B-P10): the chosen record shows as the field's value with a
51
+ * clear ×; choosing again or typing opens a `SearchSuggest` list with loading, empty and error
52
+ * states, and Retry is a list row, so the keyboard reaches it. Stale results are ignored.
53
+ */
54
+ declare function EntityPicker({ value, onValueChange, onSearch, label, id, name, placeholder, minQueryLength, delay, disabled, invalid, 'aria-describedby': describedBy, renderValue, labels: labelOverrides, className, }: EntityPickerProps): react.JSX.Element;
55
+
56
+ export { DEFAULT_ENTITY_PICKER_LABELS, EntityPicker, type EntityPickerLabels, type EntityPickerProps };
@@ -0,0 +1,168 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { fillLabel } from "../fill-label.js";
6
+ import { SearchSuggest } from "./SearchSuggest.js";
7
+ const DEFAULT_ENTITY_PICKER_LABELS = {
8
+ search: "Search",
9
+ loading: "Searching\u2026",
10
+ empty: "No matches.",
11
+ error: "Search failed.",
12
+ retry: "Try again",
13
+ clear: "Clear {title}",
14
+ change: "{label}: {title}. Change",
15
+ none: "Choose\u2026",
16
+ results: "{count} results"
17
+ };
18
+ function EntityPicker({
19
+ value,
20
+ onValueChange,
21
+ onSearch,
22
+ label,
23
+ id,
24
+ name,
25
+ placeholder,
26
+ minQueryLength = 0,
27
+ delay = 250,
28
+ disabled,
29
+ invalid,
30
+ "aria-describedby": describedBy,
31
+ renderValue,
32
+ labels: labelOverrides,
33
+ className
34
+ }) {
35
+ const labels = { ...DEFAULT_ENTITY_PICKER_LABELS, ...labelOverrides };
36
+ const [editing, setEditing] = useState(value == null);
37
+ const [query, setQuery] = useState("");
38
+ const [results, setResults] = useState([]);
39
+ const [status, setStatus] = useState("idle");
40
+ const [attempt, setAttempt] = useState(0);
41
+ const latest = useRef(0);
42
+ const inputRef = useRef(null);
43
+ const valueButtonRef = useRef(null);
44
+ const returnFocus = useRef(false);
45
+ useEffect(() => {
46
+ if (value) setEditing(false);
47
+ else setEditing(true);
48
+ }, [value?.id]);
49
+ useEffect(() => {
50
+ if (!editing || disabled) return;
51
+ const q = query.trim();
52
+ if (q.length < minQueryLength) {
53
+ setStatus("idle");
54
+ setResults([]);
55
+ return;
56
+ }
57
+ const run = ++latest.current;
58
+ const timer = window.setTimeout(() => {
59
+ setStatus("loading");
60
+ onSearch(q).then(
61
+ (found) => {
62
+ if (run === latest.current) {
63
+ setResults(found);
64
+ setStatus("done");
65
+ }
66
+ },
67
+ () => {
68
+ if (run === latest.current) {
69
+ setResults([]);
70
+ setStatus("error");
71
+ }
72
+ }
73
+ );
74
+ }, delay);
75
+ return () => window.clearTimeout(timer);
76
+ }, [editing, query, attempt, minQueryLength, delay, disabled]);
77
+ useEffect(() => {
78
+ if (!returnFocus.current) return;
79
+ returnFocus.current = false;
80
+ (editing ? inputRef.current : valueButtonRef.current)?.focus();
81
+ }, [editing]);
82
+ const startEditing = () => {
83
+ setQuery("");
84
+ returnFocus.current = true;
85
+ setEditing(true);
86
+ };
87
+ const choose = (option) => {
88
+ onValueChange(option);
89
+ returnFocus.current = true;
90
+ setEditing(false);
91
+ };
92
+ const clear = () => {
93
+ onValueChange(null);
94
+ returnFocus.current = true;
95
+ setEditing(true);
96
+ };
97
+ const hidden = name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: value?.id ?? "" }) : null;
98
+ if (!editing && value) {
99
+ const shown = renderValue?.(value) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
100
+ /* @__PURE__ */ jsx("span", { className: "uix-entity-picker__title", children: value.title }),
101
+ value.meta?.length ? /* @__PURE__ */ jsx("span", { className: "uix-entity-picker__meta", children: value.meta[value.meta.length - 1] }) : null
102
+ ] });
103
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-entity-picker", className), "data-invalid": invalid || void 0, children: [
104
+ /* @__PURE__ */ jsxs(
105
+ "button",
106
+ {
107
+ ref: valueButtonRef,
108
+ id,
109
+ type: "button",
110
+ className: "uix-input uix-entity-picker__value",
111
+ "aria-label": fillLabel(labels.change, { label, title: value.title }),
112
+ "aria-describedby": describedBy,
113
+ "aria-invalid": invalid || void 0,
114
+ disabled,
115
+ onClick: startEditing,
116
+ children: [
117
+ value.icon != null && /* @__PURE__ */ jsx("span", { className: "uix-entity-picker__icon", "aria-hidden": "true", children: value.icon }),
118
+ /* @__PURE__ */ jsx("span", { className: "uix-entity-picker__text", children: shown })
119
+ ]
120
+ }
121
+ ),
122
+ !disabled && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-entity-picker__clear", "aria-label": fillLabel(labels.clear, { title: value.title }), onClick: clear, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) }) }),
123
+ hidden
124
+ ] });
125
+ }
126
+ return /* @__PURE__ */ jsxs(
127
+ "div",
128
+ {
129
+ className: cx("uix-entity-picker", "uix-entity-picker--editing", className),
130
+ "data-invalid": invalid || void 0,
131
+ onKeyDown: (event) => {
132
+ if (event.key === "Escape" && value && !query) {
133
+ event.preventDefault();
134
+ returnFocus.current = true;
135
+ setEditing(false);
136
+ }
137
+ },
138
+ children: [
139
+ /* @__PURE__ */ jsx(
140
+ SearchSuggest,
141
+ {
142
+ inputId: id,
143
+ inputDescribedBy: describedBy,
144
+ inputRef,
145
+ value: query,
146
+ onValueChange: setQuery,
147
+ options: status === "done" ? results : [],
148
+ onSelect: (_, option) => choose(option),
149
+ label: `${label}. ${labels.search}`,
150
+ placeholder: placeholder ?? labels.none,
151
+ loading: status === "loading",
152
+ loadingLabel: labels.loading,
153
+ empty: labels.empty,
154
+ error: status === "error" ? labels.error : void 0,
155
+ footer: status === "error" ? { label: labels.retry, onSelect: () => setAttempt((n) => n + 1) } : void 0,
156
+ status: status === "done" ? fillLabel(labels.results, { count: results.length }) : void 0,
157
+ strategy: "fixed"
158
+ }
159
+ ),
160
+ hidden
161
+ ]
162
+ }
163
+ );
164
+ }
165
+ export {
166
+ DEFAULT_ENTITY_PICKER_LABELS,
167
+ EntityPicker
168
+ };
@@ -0,0 +1,9 @@
1
+ import * as react from 'react';
2
+
3
+ /** A decorative glyph for a file's kind (Lucide-style strokes). Internal to FileUpload and Attachment. */
4
+ declare function FileIcon({ name, type }: {
5
+ name: string;
6
+ type?: string;
7
+ }): react.JSX.Element;
8
+
9
+ export { FileIcon };
@@ -0,0 +1,17 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { fileKind } from "../file-model.js";
3
+ const PATHS = {
4
+ image: "M4 4h16v16H4zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM20 15l-5-5L4 20",
5
+ video: "M4 6h12v12H4zM16 10l4-2v8l-4-2",
6
+ pdf: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5M8 13h8M8 17h5",
7
+ sheet: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5M8 12h8M8 16h8M12 12v8",
8
+ archive: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5M10 6h1M10 9h1M10 12h1M10 15h1v3h-1z",
9
+ text: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5M8 13h8M8 17h8",
10
+ other: "M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8zM14 3v5h5"
11
+ };
12
+ function FileIcon({ name, type }) {
13
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: PATHS[fileKind({ name, type })] }) });
14
+ }
15
+ export {
16
+ FileIcon
17
+ };
@@ -0,0 +1,87 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { FileRejection } from '../file-model.js';
4
+
5
+ type FileUploadStatus = 'queued' | 'uploading' | 'done' | 'error';
6
+ interface FileUploadItem {
7
+ id: string;
8
+ name: string;
9
+ /** Bytes. */
10
+ size: number;
11
+ type?: string;
12
+ status: FileUploadStatus;
13
+ /** 0–100 while uploading; without it the bar is indeterminate. */
14
+ progress?: number;
15
+ /** Why it failed (shown under the name with a Retry button). */
16
+ error?: ReactNode;
17
+ /** An object URL or remote URL for an image thumbnail. */
18
+ previewUrl?: string;
19
+ /** Alternative text, when the product asks for it (`onAltChange`). */
20
+ alt?: string;
21
+ }
22
+ interface FileUploadLabels {
23
+ /** Main line of the drop zone. */
24
+ title: string;
25
+ choose: string;
26
+ /** Shown while a file is dragged over. */
27
+ drop: string;
28
+ list: string;
29
+ remove: string;
30
+ /** Accessible name of Retry. `{name}`. */
31
+ retry: string;
32
+ /** Visible text of Retry. */
33
+ retryShort: string;
34
+ queued: string;
35
+ /** `{progress}`. */
36
+ uploading: string;
37
+ done: string;
38
+ failed: string;
39
+ /** `{name}` — set `alt` for images. */
40
+ alt: string;
41
+ /** `{count}` files added. */
42
+ added: string;
43
+ /** `{name}`, `{accept}`. */
44
+ wrongType: string;
45
+ /** `{name}`, `{max}`. */
46
+ tooLarge: string;
47
+ /** `{name}`, `{max}`. */
48
+ tooMany: string;
49
+ }
50
+ declare const DEFAULT_FILE_UPLOAD_LABELS: FileUploadLabels;
51
+ interface FileUploadProps {
52
+ /** The files so far, with their upload state (controlled). */
53
+ items: readonly FileUploadItem[];
54
+ /**
55
+ * Called with the files that passed `accept`, `maxSize` and `maxFiles`, and the ones that did
56
+ * not. Add accepted files to `items` and upload them; the component never uploads.
57
+ */
58
+ onFilesAdded: (accepted: File[], rejected: FileRejection[]) => void;
59
+ onRemove?: (id: string) => void;
60
+ onRetry?: (id: string) => void;
61
+ /** Ask for alt text on image items (MOTUS photo intake). */
62
+ onAltChange?: (id: string, alt: string) => void;
63
+ /** Accepted types, as for `<input accept>`. */
64
+ accept?: string;
65
+ /** Largest file in bytes. */
66
+ maxSize?: number;
67
+ /** Most files in `items`. */
68
+ maxFiles?: number;
69
+ /** Default true. */
70
+ multiple?: boolean;
71
+ /** Open the camera on phones (`user` or `environment`). */
72
+ capture?: 'user' | 'environment';
73
+ disabled?: boolean;
74
+ /** Second line of the drop zone, e.g. "PDF or images, up to 20 MB". */
75
+ hint?: ReactNode;
76
+ locale?: string;
77
+ labels?: Partial<FileUploadLabels>;
78
+ className?: string;
79
+ }
80
+ /**
81
+ * Drop zone, file picker and upload list (HAR-984; TENSOR C15, MOTUS C-7, MEDx S18/S19). The
82
+ * product owns the upload: it receives validated `File`s and reports progress, success and
83
+ * failure through `items`. Keyboard users get a real button; drop is an extra, not the only way.
84
+ */
85
+ declare function FileUpload({ items, onFilesAdded, onRemove, onRetry, onAltChange, accept, maxSize, maxFiles, multiple, capture, disabled, hint, locale, labels: labelOverrides, className, }: FileUploadProps): react.JSX.Element;
86
+
87
+ export { DEFAULT_FILE_UPLOAD_LABELS, FileUpload, type FileUploadItem, type FileUploadLabels, type FileUploadProps, type FileUploadStatus };