@tensor_1/react 2.29.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 +103 -0
- package/dist/components/Attachment.d.ts +55 -0
- package/dist/components/Attachment.js +69 -0
- package/dist/components/EntityPicker.d.ts +56 -0
- package/dist/components/EntityPicker.js +168 -0
- package/dist/components/FileIcon.d.ts +9 -0
- package/dist/components/FileIcon.js +17 -0
- package/dist/components/FileUpload.d.ts +87 -0
- package/dist/components/FileUpload.js +139 -0
- package/dist/components/FilterEditor.d.ts +51 -0
- package/dist/components/FilterEditor.js +175 -0
- package/dist/components/Lightbox.d.ts +45 -0
- package/dist/components/Lightbox.js +102 -0
- package/dist/components/SchedulingTimeline.d.ts +109 -0
- package/dist/components/SchedulingTimeline.js +245 -0
- package/dist/components/SearchSuggest.d.ts +6 -2
- package/dist/components/SearchSuggest.js +4 -0
- package/dist/file-model.d.ts +37 -0
- package/dist/file-model.js +50 -0
- package/dist/filter-model.d.ts +88 -0
- package/dist/filter-model.js +91 -0
- package/dist/icons/Icon.d.ts +1 -1
- package/dist/index.cjs +2532 -1365
- package/dist/index.d.cts +574 -3
- package/dist/index.d.ts +9 -0
- package/dist/index.js +58 -0
- package/dist/labels-context.d.ts +3 -0
- package/dist/scheduling-timeline-model.d.ts +71 -0
- package/dist/scheduling-timeline-model.js +108 -0
- package/package.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { fillLabel } from "../fill-label.js";
|
|
6
|
+
import { useUixLabels } from "../labels-context.js";
|
|
7
|
+
import { defaultTimelineStep, layoutLane, pixelsToMs, placeSpan, shiftSpan, timelineTicks } from "../scheduling-timeline-model.js";
|
|
8
|
+
const DEFAULT_SCHEDULING_TIMELINE_LABELS = {
|
|
9
|
+
region: "Schedule timeline in {timeZone}",
|
|
10
|
+
lanes: "Lanes",
|
|
11
|
+
item: "{title}, {state}, {start} to {end}{conflict}",
|
|
12
|
+
conflict: ", overlaps another entry",
|
|
13
|
+
moveHint: "Shift and an arrow key moves it; Alt, Shift and an arrow key changes when it ends.",
|
|
14
|
+
moved: "Moved {title} to {start} \u2013 {end}",
|
|
15
|
+
resized: "{title} now ends {end}",
|
|
16
|
+
loading: "Loading schedule\u2026",
|
|
17
|
+
retry: "Try again",
|
|
18
|
+
empty: "Nothing is scheduled in this range.",
|
|
19
|
+
windows: "Windows and markers in this range",
|
|
20
|
+
now: "Now",
|
|
21
|
+
states: { scheduled: "Scheduled", conflicted: "Conflicted", "in-progress": "In progress", "blackout-violation": "Blackout violation" },
|
|
22
|
+
overlays: { freeze: "Change freeze", maintenance: "Maintenance window", blackout: "Blackout" }
|
|
23
|
+
};
|
|
24
|
+
const TICK_WIDTH = { hour: "3.5rem", day: "7rem", week: "6rem", month: "8rem" };
|
|
25
|
+
const TICK_FORMAT = {
|
|
26
|
+
hour: { hour: "2-digit", minute: "2-digit" },
|
|
27
|
+
day: { weekday: "short", day: "numeric" },
|
|
28
|
+
week: { day: "numeric", month: "short" },
|
|
29
|
+
month: { month: "short", year: "numeric" }
|
|
30
|
+
};
|
|
31
|
+
function SchedulingTimeline({
|
|
32
|
+
lanes,
|
|
33
|
+
items,
|
|
34
|
+
range,
|
|
35
|
+
scale = "day",
|
|
36
|
+
timeZone,
|
|
37
|
+
locale,
|
|
38
|
+
overlays = [],
|
|
39
|
+
markers = [],
|
|
40
|
+
now,
|
|
41
|
+
weekStartsOn = 1,
|
|
42
|
+
tickWidth,
|
|
43
|
+
step: stepProp,
|
|
44
|
+
formatTick,
|
|
45
|
+
formatInstant: formatInstantProp,
|
|
46
|
+
renderItem,
|
|
47
|
+
onSelectItem,
|
|
48
|
+
onMoveItem,
|
|
49
|
+
loading,
|
|
50
|
+
error,
|
|
51
|
+
onRetry,
|
|
52
|
+
labels: labelOverrides,
|
|
53
|
+
className
|
|
54
|
+
}) {
|
|
55
|
+
const uixLabels = useUixLabels();
|
|
56
|
+
const labels = {
|
|
57
|
+
...DEFAULT_SCHEDULING_TIMELINE_LABELS,
|
|
58
|
+
...uixLabels.schedulingTimeline,
|
|
59
|
+
...labelOverrides,
|
|
60
|
+
states: { ...DEFAULT_SCHEDULING_TIMELINE_LABELS.states, ...uixLabels.schedulingTimeline?.states, ...labelOverrides?.states },
|
|
61
|
+
overlays: { ...DEFAULT_SCHEDULING_TIMELINE_LABELS.overlays, ...uixLabels.schedulingTimeline?.overlays, ...labelOverrides?.overlays }
|
|
62
|
+
};
|
|
63
|
+
const id = useId();
|
|
64
|
+
const step = stepProp ?? defaultTimelineStep(scale);
|
|
65
|
+
const ticks = useMemo(() => timelineTicks(range, scale, timeZone, weekStartsOn), [range.start, range.end, scale, timeZone, weekStartsOn]);
|
|
66
|
+
const laneLayouts = useMemo(
|
|
67
|
+
() => lanes.map((lane) => ({ lane, placed: layoutLane(items.filter((item) => item.laneId === lane.id), range) })),
|
|
68
|
+
[lanes, items, range.start, range.end]
|
|
69
|
+
// eslint-disable-line react-hooks/exhaustive-deps
|
|
70
|
+
);
|
|
71
|
+
const ordered = useMemo(() => laneLayouts.flatMap((l) => l.placed.map((p) => p.item.id)), [laneLayouts]);
|
|
72
|
+
const [activeId, setActiveId] = useState();
|
|
73
|
+
const focusId = activeId && ordered.includes(activeId) ? activeId : ordered[0];
|
|
74
|
+
const [drag, setDrag] = useState(null);
|
|
75
|
+
const [announcement, setAnnouncement] = useState("");
|
|
76
|
+
const rootRef = useRef(null);
|
|
77
|
+
const fmtInstant = (instant) => formatInstantProp?.(instant) ?? new Intl.DateTimeFormat(locale, { timeZone, dateStyle: "medium", timeStyle: "short" }).format(new Date(instant));
|
|
78
|
+
const fmtTick = (at) => formatTick?.(new Date(at), scale) ?? new Intl.DateTimeFormat(locale, { timeZone, ...TICK_FORMAT[scale] }).format(new Date(at));
|
|
79
|
+
const movable = (item) => !!onMoveItem && item.movable !== false;
|
|
80
|
+
const focusItem = (itemId) => {
|
|
81
|
+
if (!itemId) return;
|
|
82
|
+
setActiveId(itemId);
|
|
83
|
+
requestAnimationFrame(() => rootRef.current?.querySelector(`[data-timeline-item="${CSS.escape(itemId)}"]`)?.focus());
|
|
84
|
+
};
|
|
85
|
+
const commit = (item, delta, mode) => {
|
|
86
|
+
if (!onMoveItem || delta === 0) return;
|
|
87
|
+
const next = shiftSpan(item, delta, mode, step);
|
|
88
|
+
onMoveItem(item.id, next);
|
|
89
|
+
setAnnouncement(mode === "move" ? fillLabel(labels.moved, { title: item.title, start: fmtInstant(next.start), end: fmtInstant(next.end) }) : fillLabel(labels.resized, { title: item.title, end: fmtInstant(next.end) }));
|
|
90
|
+
};
|
|
91
|
+
const onItemKeyDown = (event, laneIndex, placed, index) => {
|
|
92
|
+
const item = placed[index].item;
|
|
93
|
+
const horizontal = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
|
|
94
|
+
if (horizontal && event.shiftKey && movable(item)) {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
commit(item, horizontal * step, event.altKey ? "resize" : "move");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (horizontal) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
focusItem(placed[index + horizontal]?.item.id);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (event.key === "Home" || event.key === "End") {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
focusItem((event.key === "Home" ? placed[0] : placed[placed.length - 1])?.item.id);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const vertical = event.key === "ArrowUp" ? -1 : event.key === "ArrowDown" ? 1 : 0;
|
|
110
|
+
if (vertical) {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
const start = Date.parse(item.start);
|
|
113
|
+
for (let l = laneIndex + vertical; l >= 0 && l < laneLayouts.length; l += vertical) {
|
|
114
|
+
const candidates = laneLayouts[l].placed;
|
|
115
|
+
if (candidates.length === 0) continue;
|
|
116
|
+
const nearest = candidates.reduce((best, p) => Math.abs(Date.parse(p.item.start) - start) < Math.abs(Date.parse(best.item.start) - start) ? p : best);
|
|
117
|
+
focusItem(nearest.item.id);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const onPointerDown = (event, item) => {
|
|
123
|
+
if (!movable(item) || event.button !== 0) return;
|
|
124
|
+
const track = event.currentTarget.closest(".uix-scheduling-timeline__track");
|
|
125
|
+
if (!track) return;
|
|
126
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
127
|
+
setDrag({ id: item.id, x: event.clientX, width: track.getBoundingClientRect().width, delta: 0, moved: false });
|
|
128
|
+
};
|
|
129
|
+
const onPointerMove = (event) => {
|
|
130
|
+
if (!drag) return;
|
|
131
|
+
const dx = event.clientX - drag.x;
|
|
132
|
+
const delta = pixelsToMs(dx, drag.width, range, step);
|
|
133
|
+
if (delta !== drag.delta || !drag.moved && Math.abs(dx) > 3) setDrag({ ...drag, delta, moved: drag.moved || Math.abs(dx) > 3 });
|
|
134
|
+
};
|
|
135
|
+
const onPointerUp = (item) => {
|
|
136
|
+
if (!drag || drag.id !== item.id) return;
|
|
137
|
+
if (drag.moved) commit(item, drag.delta, "move");
|
|
138
|
+
setDrag(drag.moved ? { ...drag, delta: 0 } : null);
|
|
139
|
+
if (drag.moved) window.setTimeout(() => setDrag(null), 0);
|
|
140
|
+
};
|
|
141
|
+
const nowPos = now ? placeSpan({ start: now, end: now }, range) : null;
|
|
142
|
+
const overlaysFor = (laneId) => overlays.filter((o) => !o.laneId || o.laneId === laneId);
|
|
143
|
+
const markersFor = (laneId) => markers.filter((m) => !m.laneId || m.laneId === laneId);
|
|
144
|
+
const trackStyle = { minWidth: `calc(${Math.max(ticks.length - 1, 1)} * ${tickWidth ?? TICK_WIDTH[scale]})` };
|
|
145
|
+
const band = (o, showLabel) => {
|
|
146
|
+
const pos = placeSpan(o, range);
|
|
147
|
+
return pos && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__overlay", "data-kind": o.kind, style: { left: `${pos.left}%`, width: `${pos.width}%` }, children: showLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__overlay-label", children: o.label }) }, o.id);
|
|
148
|
+
};
|
|
149
|
+
return /* @__PURE__ */ jsxs("section", { ref: rootRef, className: cx("uix-scheduling-timeline", className), "aria-label": fillLabel(labels.region, { timeZone }), children: [
|
|
150
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__state", role: "status", children: labels.loading }) : error ? /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__state", role: "alert", children: [
|
|
151
|
+
/* @__PURE__ */ jsx("p", { children: error }),
|
|
152
|
+
onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: labels.retry })
|
|
153
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
154
|
+
(overlays.length > 0 || markers.length > 0) && /* @__PURE__ */ jsxs("div", { className: "uix-visually-hidden", children: [
|
|
155
|
+
/* @__PURE__ */ jsx("p", { id: `${id}-windows`, children: labels.windows }),
|
|
156
|
+
/* @__PURE__ */ jsxs("ul", { "aria-labelledby": `${id}-windows`, children: [
|
|
157
|
+
overlays.map((o) => /* @__PURE__ */ jsxs("li", { children: [
|
|
158
|
+
labels.overlays[o.kind],
|
|
159
|
+
": ",
|
|
160
|
+
o.label,
|
|
161
|
+
", ",
|
|
162
|
+
fmtInstant(o.start),
|
|
163
|
+
" \u2013 ",
|
|
164
|
+
fmtInstant(o.end)
|
|
165
|
+
] }, o.id)),
|
|
166
|
+
markers.map((m) => /* @__PURE__ */ jsxs("li", { children: [
|
|
167
|
+
m.label,
|
|
168
|
+
", ",
|
|
169
|
+
fmtInstant(m.at)
|
|
170
|
+
] }, m.id))
|
|
171
|
+
] })
|
|
172
|
+
] }),
|
|
173
|
+
/* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__scroller", children: /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__grid", children: [
|
|
174
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__row uix-scheduling-timeline__row--axis", "aria-hidden": "true", children: [
|
|
175
|
+
/* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__lane-label", children: labels.lanes }),
|
|
176
|
+
/* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__track uix-scheduling-timeline__axis", style: trackStyle, children: ticks.map((tick) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__tick", "data-major": tick.major || void 0, style: { left: `${tick.offset}%` }, children: tick.offset < 100 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__tick-label", children: fmtTick(tick.at) }) }, tick.at)) })
|
|
177
|
+
] }),
|
|
178
|
+
laneLayouts.map(({ lane, placed }, laneIndex) => {
|
|
179
|
+
const rows = placed.reduce((max, p) => Math.max(max, p.row + 1), 1);
|
|
180
|
+
const laneLabelId = `${id}-lane-${laneIndex}`;
|
|
181
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__row", style: { "--uix-timeline-rows": rows }, children: [
|
|
182
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__lane-label", id: laneLabelId, children: [
|
|
183
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__lane-name", children: lane.label }),
|
|
184
|
+
lane.meta != null && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__lane-meta", children: lane.meta })
|
|
185
|
+
] }),
|
|
186
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__track", style: trackStyle, children: [
|
|
187
|
+
ticks.map((tick) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__gridline", "data-major": tick.major || void 0, style: { left: `${tick.offset}%` }, "aria-hidden": "true" }, tick.at)),
|
|
188
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: overlaysFor(lane.id).map((o) => band(o, laneIndex === 0 || !!o.laneId)) }),
|
|
189
|
+
nowPos && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__now", style: { left: `${nowPos.left}%` }, "aria-hidden": "true", children: laneIndex === 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__now-label", children: labels.now }) }),
|
|
190
|
+
markersFor(lane.id).map((m) => {
|
|
191
|
+
const pos = placeSpan({ start: m.at, end: m.at }, range);
|
|
192
|
+
return pos && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__marker", style: { left: `${pos.left}%` }, title: m.label, "aria-hidden": "true" }, m.id);
|
|
193
|
+
}),
|
|
194
|
+
/* @__PURE__ */ jsx("ul", { className: "uix-scheduling-timeline__items", "aria-labelledby": laneLabelId, children: placed.map((p, index) => {
|
|
195
|
+
const item = p.item;
|
|
196
|
+
const state = item.state ?? "scheduled";
|
|
197
|
+
const dragging = drag?.id === item.id && drag.moved;
|
|
198
|
+
const shown = dragging && drag ? placeSpan(shiftSpan(item, drag.delta, "move", step), range) ?? p : p;
|
|
199
|
+
return /* @__PURE__ */ jsx("li", { className: "uix-scheduling-timeline__slot", style: { left: `${shown.left}%`, width: `${shown.width}%`, "--uix-timeline-row": p.row }, children: /* @__PURE__ */ jsxs(
|
|
200
|
+
"button",
|
|
201
|
+
{
|
|
202
|
+
type: "button",
|
|
203
|
+
className: "uix-scheduling-timeline__item",
|
|
204
|
+
"data-timeline-item": item.id,
|
|
205
|
+
title: item.title,
|
|
206
|
+
"data-state": state,
|
|
207
|
+
"data-conflict": p.conflict || void 0,
|
|
208
|
+
"data-clipped-start": p.clippedStart || void 0,
|
|
209
|
+
"data-clipped-end": p.clippedEnd || void 0,
|
|
210
|
+
"data-movable": movable(item) || void 0,
|
|
211
|
+
"data-dragging": dragging || void 0,
|
|
212
|
+
tabIndex: focusId === item.id ? 0 : -1,
|
|
213
|
+
"aria-label": fillLabel(labels.item, { title: item.title, state: labels.states[state], start: fmtInstant(item.start), end: fmtInstant(item.end), conflict: p.conflict ? labels.conflict : "" }),
|
|
214
|
+
"aria-describedby": movable(item) ? `${id}-hint` : void 0,
|
|
215
|
+
onFocus: () => setActiveId(item.id),
|
|
216
|
+
onKeyDown: (event) => onItemKeyDown(event, laneIndex, placed, index),
|
|
217
|
+
onPointerDown: (event) => onPointerDown(event, item),
|
|
218
|
+
onPointerMove,
|
|
219
|
+
onPointerUp: () => onPointerUp(item),
|
|
220
|
+
onPointerCancel: () => setDrag(null),
|
|
221
|
+
onClick: () => {
|
|
222
|
+
if (drag?.moved) return;
|
|
223
|
+
onSelectItem?.(item);
|
|
224
|
+
},
|
|
225
|
+
children: [
|
|
226
|
+
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-title", children: renderItem?.(item) ?? item.title }),
|
|
227
|
+
item.meta && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-meta", children: item.meta })
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
) }, item.id);
|
|
231
|
+
}) })
|
|
232
|
+
] })
|
|
233
|
+
] }, lane.id);
|
|
234
|
+
})
|
|
235
|
+
] }) }),
|
|
236
|
+
ordered.length === 0 && /* @__PURE__ */ jsx("p", { className: "uix-scheduling-timeline__empty", children: labels.empty })
|
|
237
|
+
] }),
|
|
238
|
+
onMoveItem && /* @__PURE__ */ jsx("span", { id: `${id}-hint`, className: "uix-visually-hidden", children: labels.moveHint }),
|
|
239
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: announcement })
|
|
240
|
+
] });
|
|
241
|
+
}
|
|
242
|
+
export {
|
|
243
|
+
DEFAULT_SCHEDULING_TIMELINE_LABELS,
|
|
244
|
+
SchedulingTimeline
|
|
245
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ReactNode, HTMLAttributes, Ref } from 'react';
|
|
3
3
|
|
|
4
4
|
/** One result row. */
|
|
5
5
|
interface SearchSuggestOption {
|
|
@@ -74,6 +74,10 @@ interface SearchSuggestProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onCha
|
|
|
74
74
|
autoFocus?: boolean;
|
|
75
75
|
/** Form field name of the input. */
|
|
76
76
|
name?: string;
|
|
77
|
+
/** Id of the input itself (for a `<label for>` / `Field htmlFor`); `id` stays on the wrapper. */
|
|
78
|
+
inputId?: string;
|
|
79
|
+
/** `aria-describedby` of the input (hint and error text from a `Field`). */
|
|
80
|
+
inputDescribedBy?: string;
|
|
77
81
|
}
|
|
78
82
|
/**
|
|
79
83
|
* A search field with a suggestion list underneath: the "search and jump" pattern of Windows
|
|
@@ -82,6 +86,6 @@ interface SearchSuggestProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onCha
|
|
|
82
86
|
* (`aria-activedescendant`), Enter opens it (the first row when none is active), Escape closes the
|
|
83
87
|
* list and a second Escape clears the text.
|
|
84
88
|
*/
|
|
85
|
-
declare function SearchSuggest({ value, onValueChange, options, onSelect, label, placeholder, highlight, heading, headingAction, footer, loading, loadingLabel, empty, error, shortcutHint, clearLabel: clearLabelProp, status, size, open: openProp, strategy, onOpenChange, inputRef, autoFocus, name, className, onBlur, ...props }: SearchSuggestProps): react.JSX.Element;
|
|
89
|
+
declare function SearchSuggest({ value, onValueChange, options, onSelect, label, placeholder, highlight, heading, headingAction, footer, loading, loadingLabel, empty, error, shortcutHint, clearLabel: clearLabelProp, status, size, open: openProp, strategy, onOpenChange, inputRef, autoFocus, name, inputId, inputDescribedBy, className, onBlur, ...props }: SearchSuggestProps): react.JSX.Element;
|
|
86
90
|
|
|
87
91
|
export { SearchSuggest, type SearchSuggestAction, type SearchSuggestOption, type SearchSuggestProps };
|
|
@@ -43,6 +43,8 @@ function SearchSuggest({
|
|
|
43
43
|
inputRef,
|
|
44
44
|
autoFocus,
|
|
45
45
|
name,
|
|
46
|
+
inputId,
|
|
47
|
+
inputDescribedBy,
|
|
46
48
|
className,
|
|
47
49
|
onBlur,
|
|
48
50
|
...props
|
|
@@ -185,6 +187,8 @@ function SearchSuggest({
|
|
|
185
187
|
"input",
|
|
186
188
|
{
|
|
187
189
|
ref: fieldRef,
|
|
190
|
+
id: inputId,
|
|
191
|
+
"aria-describedby": inputDescribedBy,
|
|
188
192
|
className: "uix-input uix-search-suggest__input",
|
|
189
193
|
type: "text",
|
|
190
194
|
role: "combobox",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** File helpers shared by `FileUpload` and `Attachment` (HAR-984/985). Pure. */
|
|
2
|
+
/** "1.2 MB", "340 KB", "12 B" in the locale's number format (base 1000, like file managers). */
|
|
3
|
+
declare function formatFileSize(bytes: number, locale?: string): string;
|
|
4
|
+
/** Whether a file matches an `accept` list (".pdf,image/*,application/zip"). An empty list accepts all. */
|
|
5
|
+
declare function fileMatchesAccept(file: {
|
|
6
|
+
name: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
}, accept?: string): boolean;
|
|
9
|
+
type FileRejectionReason = 'type' | 'size' | 'count';
|
|
10
|
+
interface FileRejection<F = File> {
|
|
11
|
+
file: F;
|
|
12
|
+
reason: FileRejectionReason;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Splits picked files into accepted and rejected (wrong type, over `maxSize` bytes, or beyond
|
|
16
|
+
* `maxFiles` counting the `existing` ones). Order is kept.
|
|
17
|
+
*/
|
|
18
|
+
declare function partitionFiles<F extends {
|
|
19
|
+
name: string;
|
|
20
|
+
type?: string;
|
|
21
|
+
size: number;
|
|
22
|
+
}>(files: readonly F[], { accept, maxSize, maxFiles, existing }: {
|
|
23
|
+
accept?: string;
|
|
24
|
+
maxSize?: number;
|
|
25
|
+
maxFiles?: number;
|
|
26
|
+
existing?: number;
|
|
27
|
+
}): {
|
|
28
|
+
accepted: F[];
|
|
29
|
+
rejected: FileRejection<F>[];
|
|
30
|
+
};
|
|
31
|
+
/** A coarse kind for a file icon. */
|
|
32
|
+
declare function fileKind(file: {
|
|
33
|
+
name: string;
|
|
34
|
+
type?: string;
|
|
35
|
+
}): 'image' | 'video' | 'pdf' | 'sheet' | 'archive' | 'text' | 'other';
|
|
36
|
+
|
|
37
|
+
export { type FileRejection, type FileRejectionReason, fileKind, fileMatchesAccept, formatFileSize, partitionFiles };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
function formatFileSize(bytes, locale) {
|
|
2
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "";
|
|
3
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
4
|
+
let value = bytes;
|
|
5
|
+
let unit = 0;
|
|
6
|
+
while (value >= 1e3 && unit < units.length - 1) {
|
|
7
|
+
value /= 1e3;
|
|
8
|
+
unit += 1;
|
|
9
|
+
}
|
|
10
|
+
const digits = unit === 0 || value >= 100 ? 0 : 1;
|
|
11
|
+
return `${new Intl.NumberFormat(locale, { maximumFractionDigits: digits, minimumFractionDigits: 0 }).format(value)} ${units[unit]}`;
|
|
12
|
+
}
|
|
13
|
+
function fileMatchesAccept(file, accept) {
|
|
14
|
+
if (!accept || !accept.trim()) return true;
|
|
15
|
+
const name = file.name.toLowerCase();
|
|
16
|
+
const type = (file.type ?? "").toLowerCase();
|
|
17
|
+
return accept.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean).some((token) => {
|
|
18
|
+
if (token.startsWith(".")) return name.endsWith(token);
|
|
19
|
+
if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
|
|
20
|
+
return type === token;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function partitionFiles(files, { accept, maxSize, maxFiles, existing = 0 }) {
|
|
24
|
+
const accepted = [];
|
|
25
|
+
const rejected = [];
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (!fileMatchesAccept(file, accept)) rejected.push({ file, reason: "type" });
|
|
28
|
+
else if (maxSize !== void 0 && file.size > maxSize) rejected.push({ file, reason: "size" });
|
|
29
|
+
else if (maxFiles !== void 0 && existing + accepted.length >= maxFiles) rejected.push({ file, reason: "count" });
|
|
30
|
+
else accepted.push(file);
|
|
31
|
+
}
|
|
32
|
+
return { accepted, rejected };
|
|
33
|
+
}
|
|
34
|
+
function fileKind(file) {
|
|
35
|
+
const type = (file.type ?? "").toLowerCase();
|
|
36
|
+
const ext = file.name.toLowerCase().split(".").pop() ?? "";
|
|
37
|
+
if (type.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "webp", "svg", "heic"].includes(ext)) return "image";
|
|
38
|
+
if (type.startsWith("video/") || ["mp4", "mov", "webm"].includes(ext)) return "video";
|
|
39
|
+
if (type === "application/pdf" || ext === "pdf") return "pdf";
|
|
40
|
+
if (["xls", "xlsx", "csv", "ods"].includes(ext) || type.includes("spreadsheet") || type === "text/csv") return "sheet";
|
|
41
|
+
if (["zip", "7z", "gz", "tar", "rar"].includes(ext) || type.includes("zip")) return "archive";
|
|
42
|
+
if (type.startsWith("text/") || ["txt", "md", "log", "json", "xml", "doc", "docx", "odt", "rtf"].includes(ext)) return "text";
|
|
43
|
+
return "other";
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
fileKind,
|
|
47
|
+
fileMatchesAccept,
|
|
48
|
+
formatFileSize,
|
|
49
|
+
partitionFiles
|
|
50
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed column filters for `FilterEditor` (HAR-1365; TENSOR C6 `table-filter-editor.tsx`,
|
|
3
|
+
* MOTUS C-1 "+ filter"). The value is data the product serializes into its own query; the
|
|
4
|
+
* summary feeds a `Chip`. Pure, so a server can summarize a saved view too.
|
|
5
|
+
*/
|
|
6
|
+
interface FilterChoice {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
type TextFilterOperator = 'contains' | 'not-contains' | 'is' | 'is-not' | 'starts-with';
|
|
11
|
+
type NumberFilterOperator = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'between';
|
|
12
|
+
type FilterValue = {
|
|
13
|
+
kind: 'enum';
|
|
14
|
+
values: string[];
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'text';
|
|
17
|
+
operator: TextFilterOperator;
|
|
18
|
+
text: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'number';
|
|
21
|
+
operator: NumberFilterOperator;
|
|
22
|
+
value?: number;
|
|
23
|
+
to?: number;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'date-range';
|
|
26
|
+
from?: string;
|
|
27
|
+
to?: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'boolean';
|
|
30
|
+
value?: boolean;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'reference';
|
|
33
|
+
values: FilterChoice[];
|
|
34
|
+
};
|
|
35
|
+
type FilterValueKind = FilterValue['kind'];
|
|
36
|
+
interface FilterField {
|
|
37
|
+
id: string;
|
|
38
|
+
/** Column name, also the start of the chip summary. */
|
|
39
|
+
label: string;
|
|
40
|
+
kind: FilterValueKind;
|
|
41
|
+
/** `enum`: the choices. */
|
|
42
|
+
options?: readonly FilterChoice[];
|
|
43
|
+
/** `reference`: finds records to pick; rejected promises show the error state. */
|
|
44
|
+
onSearch?: (query: string) => Promise<readonly FilterChoice[]>;
|
|
45
|
+
/** `text` / `number`: the operators offered, in order. Default: all of the kind. */
|
|
46
|
+
operators?: readonly (TextFilterOperator | NumberFilterOperator)[];
|
|
47
|
+
/** `number`: a unit shown after the value, e.g. "days". */
|
|
48
|
+
unit?: string;
|
|
49
|
+
/** `boolean`: words for true and false, e.g. Escalated / Not escalated. Default from labels. */
|
|
50
|
+
trueLabel?: string;
|
|
51
|
+
falseLabel?: string;
|
|
52
|
+
}
|
|
53
|
+
interface FilterSummaryLabels {
|
|
54
|
+
operators: Record<TextFilterOperator | NumberFilterOperator, string>;
|
|
55
|
+
yes: string;
|
|
56
|
+
no: string;
|
|
57
|
+
/** `{from}`, `{to}`; also used for a number range. */
|
|
58
|
+
range: string;
|
|
59
|
+
/** Only a start date. `{from}`. */
|
|
60
|
+
from: string;
|
|
61
|
+
/** Only an end date. `{to}`. */
|
|
62
|
+
until: string;
|
|
63
|
+
/** More than `maxListed` values: `{first}`, `{count}`. */
|
|
64
|
+
more: string;
|
|
65
|
+
}
|
|
66
|
+
declare const DEFAULT_FILTER_SUMMARY_LABELS: FilterSummaryLabels;
|
|
67
|
+
declare const TEXT_FILTER_OPERATORS: readonly TextFilterOperator[];
|
|
68
|
+
declare const NUMBER_FILTER_OPERATORS: readonly NumberFilterOperator[];
|
|
69
|
+
/** The empty value for a field kind (what Clear resets to). */
|
|
70
|
+
declare function emptyFilterValue(kind: FilterValueKind): FilterValue;
|
|
71
|
+
/** True when the value filters nothing (so the chip goes away). */
|
|
72
|
+
declare function isFilterEmpty(value: FilterValue | undefined): boolean;
|
|
73
|
+
interface SummarizeFilterOptions {
|
|
74
|
+
labels?: Partial<FilterSummaryLabels>;
|
|
75
|
+
/** Formats a `YYYY-MM-DD` date (a product format such as DD.MM.YYYY). Default: as given. */
|
|
76
|
+
formatDate?: (date: string) => string;
|
|
77
|
+
/** Formats a number. Default: `String`. */
|
|
78
|
+
formatNumber?: (value: number) => string;
|
|
79
|
+
/** List this many values before "+N". Default 2. */
|
|
80
|
+
maxListed?: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One line for the filter chip, e.g. "State: Open, Pending", "Priority ≥ 2", "Created:
|
|
84
|
+
* 01.10.2026 – 05.10.2026", "Escalated: Yes". Empty values give an empty string.
|
|
85
|
+
*/
|
|
86
|
+
declare function summarizeFilter(field: FilterField, value: FilterValue | undefined, options?: SummarizeFilterOptions): string;
|
|
87
|
+
|
|
88
|
+
export { DEFAULT_FILTER_SUMMARY_LABELS, type FilterChoice, type FilterField, type FilterSummaryLabels, type FilterValue, type FilterValueKind, NUMBER_FILTER_OPERATORS, type NumberFilterOperator, type SummarizeFilterOptions, TEXT_FILTER_OPERATORS, type TextFilterOperator, emptyFilterValue, isFilterEmpty, summarizeFilter };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const DEFAULT_FILTER_SUMMARY_LABELS = {
|
|
2
|
+
operators: {
|
|
3
|
+
contains: "contains",
|
|
4
|
+
"not-contains": "does not contain",
|
|
5
|
+
is: "is",
|
|
6
|
+
"is-not": "is not",
|
|
7
|
+
"starts-with": "starts with",
|
|
8
|
+
eq: "=",
|
|
9
|
+
neq: "\u2260",
|
|
10
|
+
lt: "<",
|
|
11
|
+
lte: "\u2264",
|
|
12
|
+
gt: ">",
|
|
13
|
+
gte: "\u2265",
|
|
14
|
+
between: "between"
|
|
15
|
+
},
|
|
16
|
+
yes: "Yes",
|
|
17
|
+
no: "No",
|
|
18
|
+
range: "{from} \u2013 {to}",
|
|
19
|
+
from: "from {from}",
|
|
20
|
+
until: "until {to}",
|
|
21
|
+
more: "{first} +{count}"
|
|
22
|
+
};
|
|
23
|
+
const TEXT_FILTER_OPERATORS = ["contains", "not-contains", "is", "is-not", "starts-with"];
|
|
24
|
+
const NUMBER_FILTER_OPERATORS = ["eq", "neq", "lt", "lte", "gt", "gte", "between"];
|
|
25
|
+
function emptyFilterValue(kind) {
|
|
26
|
+
switch (kind) {
|
|
27
|
+
case "enum":
|
|
28
|
+
return { kind, values: [] };
|
|
29
|
+
case "text":
|
|
30
|
+
return { kind, operator: "contains", text: "" };
|
|
31
|
+
case "number":
|
|
32
|
+
return { kind, operator: "eq" };
|
|
33
|
+
case "date-range":
|
|
34
|
+
return { kind };
|
|
35
|
+
case "boolean":
|
|
36
|
+
return { kind };
|
|
37
|
+
case "reference":
|
|
38
|
+
return { kind, values: [] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isFilterEmpty(value) {
|
|
42
|
+
if (!value) return true;
|
|
43
|
+
switch (value.kind) {
|
|
44
|
+
case "enum":
|
|
45
|
+
case "reference":
|
|
46
|
+
return value.values.length === 0;
|
|
47
|
+
case "text":
|
|
48
|
+
return value.text.trim() === "";
|
|
49
|
+
case "number":
|
|
50
|
+
return value.value === void 0 || value.operator === "between" && value.to === void 0;
|
|
51
|
+
case "date-range":
|
|
52
|
+
return !value.from && !value.to;
|
|
53
|
+
case "boolean":
|
|
54
|
+
return value.value === void 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const fill = (template, values) => template.replace(/\{(\w+)\}/g, (match, key) => key in values ? String(values[key]) : match);
|
|
58
|
+
function summarizeFilter(field, value, options = {}) {
|
|
59
|
+
if (!value || isFilterEmpty(value)) return "";
|
|
60
|
+
const labels = { ...DEFAULT_FILTER_SUMMARY_LABELS, ...options.labels, operators: { ...DEFAULT_FILTER_SUMMARY_LABELS.operators, ...options.labels?.operators } };
|
|
61
|
+
const date = options.formatDate ?? ((d) => d);
|
|
62
|
+
const num = (n) => `${options.formatNumber?.(n) ?? String(n)}${field.unit ? ` ${field.unit}` : ""}`;
|
|
63
|
+
const list = (items) => {
|
|
64
|
+
const max = Math.max(1, options.maxListed ?? 2);
|
|
65
|
+
return items.length > max ? fill(labels.more, { first: items.slice(0, max).join(", "), count: items.length - max }) : items.join(", ");
|
|
66
|
+
};
|
|
67
|
+
switch (value.kind) {
|
|
68
|
+
case "enum": {
|
|
69
|
+
const byValue = new Map((field.options ?? []).map((o) => [o.value, o.label]));
|
|
70
|
+
return `${field.label}: ${list(value.values.map((v) => byValue.get(v) ?? v))}`;
|
|
71
|
+
}
|
|
72
|
+
case "reference":
|
|
73
|
+
return `${field.label}: ${list(value.values.map((v) => v.label))}`;
|
|
74
|
+
case "text":
|
|
75
|
+
return `${field.label} ${labels.operators[value.operator]} \u201C${value.text.trim()}\u201D`;
|
|
76
|
+
case "number":
|
|
77
|
+
return value.operator === "between" ? `${field.label}: ${fill(labels.range, { from: num(value.value), to: num(value.to) })}` : `${field.label} ${labels.operators[value.operator]} ${num(value.value)}`;
|
|
78
|
+
case "date-range":
|
|
79
|
+
return `${field.label}: ${value.from && value.to ? fill(labels.range, { from: date(value.from), to: date(value.to) }) : value.from ? fill(labels.from, { from: date(value.from) }) : fill(labels.until, { to: date(value.to) })}`;
|
|
80
|
+
case "boolean":
|
|
81
|
+
return `${field.label}: ${value.value ? field.trueLabel ?? labels.yes : field.falseLabel ?? labels.no}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
DEFAULT_FILTER_SUMMARY_LABELS,
|
|
86
|
+
NUMBER_FILTER_OPERATORS,
|
|
87
|
+
TEXT_FILTER_OPERATORS,
|
|
88
|
+
emptyFilterValue,
|
|
89
|
+
isFilterEmpty,
|
|
90
|
+
summarizeFilter
|
|
91
|
+
};
|
package/dist/icons/Icon.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { IconNode, UixIcon, IconProps } from './types.js';
|
|
|
5
5
|
/** Renders a glyph as an inline SVG; the props contract shared by every icon. */
|
|
6
6
|
declare function renderIcon(node: IconNode, { size, tone, label, strokeWidth, className, style, ...rest }: IconProps, ref?: unknown): react.ReactElement<{
|
|
7
7
|
string?: number | string | undefined | undefined;
|
|
8
|
+
r?: number | string | undefined | undefined;
|
|
8
9
|
type?: string | undefined | undefined;
|
|
9
10
|
id?: string | undefined | undefined;
|
|
10
11
|
name?: string | undefined | undefined;
|
|
@@ -252,7 +253,6 @@ declare function renderIcon(node: IconNode, { size, tone, label, strokeWidth, cl
|
|
|
252
253
|
fill: string;
|
|
253
254
|
clip?: number | string | undefined | undefined;
|
|
254
255
|
stroke: string;
|
|
255
|
-
r?: number | string | undefined | undefined;
|
|
256
256
|
filter?: string | undefined | undefined;
|
|
257
257
|
values?: string | undefined | undefined;
|
|
258
258
|
offset?: number | string | undefined | undefined;
|