@sincpro/web-ui 1.6.0 → 1.7.1
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/dist/components/attachments/Attachments.d.ts +56 -0
- package/dist/components/attachments/Attachments.js +93 -0
- package/dist/components/attachments/files.d.ts +28 -0
- package/dist/components/attachments/files.js +100 -0
- package/dist/components/attachments/index.d.ts +2 -0
- package/dist/components/attachments/index.js +3 -0
- package/dist/components/flow/StepFlow.d.ts +37 -0
- package/dist/components/flow/StepFlow.js +74 -0
- package/dist/components/flow/index.d.ts +1 -0
- package/dist/components/flow/index.js +2 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/lines/LineItems.js +55 -45
- package/dist/components/listing/SortMenu.d.ts +24 -20
- package/dist/components/listing/SortMenu.js +51 -91
- package/dist/components/listing/index.d.ts +2 -1
- package/dist/components/listing/index.js +2 -1
- package/dist/components/listing/modelCells.d.ts +7 -0
- package/dist/components/listing/modelCells.js +56 -0
- package/dist/components/listing/modelColumns.d.ts +9 -0
- package/dist/components/listing/modelColumns.js +59 -14
- package/dist/components/search/index.d.ts +1 -1
- package/dist/components/search/index.js +1 -1
- package/dist/components/search/model.d.ts +1 -0
- package/dist/components/search/model.js +7 -0
- package/dist/components/search/query.js +2 -3
- package/dist/foundations/copy.d.ts +131 -0
- package/dist/foundations/copy.es.js +86 -0
- package/dist/foundations/copy.js +86 -0
- package/dist/foundations/form.d.ts +25 -0
- package/dist/foundations/form.js +27 -0
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/index.js +1 -1
- package/dist/primitives/button/Button.js +3 -1
- package/dist/primitives/choice/ComboBox.js +2 -2
- package/dist/primitives/choice/SearchField.js +2 -5
- package/dist/primitives/choice/SegmentedControl.js +2 -2
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/date/DatePicker.js +4 -4
- package/dist/primitives/date/DateTime.js +3 -3
- package/dist/primitives/field/CardNumberInput.d.ts +41 -0
- package/dist/primitives/field/CardNumberInput.js +117 -0
- package/dist/primitives/field/CodeInput.d.ts +18 -0
- package/dist/primitives/field/CodeInput.js +37 -0
- package/dist/primitives/field/ColorInput.d.ts +21 -0
- package/dist/primitives/field/ColorInput.js +42 -0
- package/dist/primitives/field/ContactInputs.d.ts +18 -0
- package/dist/primitives/field/ContactInputs.js +70 -0
- package/dist/primitives/field/DurationInput.d.ts +20 -0
- package/dist/primitives/field/DurationInput.js +54 -0
- package/dist/primitives/field/Field.d.ts +26 -5
- package/dist/primitives/field/Field.js +19 -6
- package/dist/primitives/field/ImageInput.d.ts +51 -0
- package/dist/primitives/field/ImageInput.js +267 -0
- package/dist/primitives/field/PasswordInput.d.ts +29 -0
- package/dist/primitives/field/PasswordInput.js +44 -0
- package/dist/primitives/field/PatternInput.d.ts +23 -0
- package/dist/primitives/field/PatternInput.js +48 -0
- package/dist/primitives/field/RichTextEditor.d.ts +18 -0
- package/dist/primitives/field/RichTextEditor.js +224 -0
- package/dist/primitives/field/RichTextInput.d.ts +30 -0
- package/dist/primitives/field/RichTextInput.js +38 -0
- package/dist/primitives/field/SignatureInput.d.ts +34 -0
- package/dist/primitives/field/SignatureInput.js +215 -0
- package/dist/primitives/field/Suggest.js +2 -2
- package/dist/primitives/field/card.d.ts +33 -0
- package/dist/primitives/field/card.js +173 -0
- package/dist/primitives/field/clipboard.js +3 -2
- package/dist/primitives/field/contact.d.ts +17 -0
- package/dist/primitives/field/contact.js +65 -0
- package/dist/primitives/field/duration.d.ts +5 -0
- package/dist/primitives/field/duration.js +48 -0
- package/dist/primitives/field/index.d.ts +14 -0
- package/dist/primitives/field/index.js +20 -0
- package/dist/primitives/field/mask.d.ts +33 -0
- package/dist/primitives/field/mask.js +56 -0
- package/dist/primitives/field/media.d.ts +99 -0
- package/dist/primitives/field/media.js +136 -0
- package/dist/primitives/field/pattern.d.ts +10 -0
- package/dist/primitives/field/pattern.js +68 -0
- package/dist/primitives/menu/Menu.d.ts +16 -0
- package/dist/primitives/menu/Menu.js +26 -8
- package/dist/primitives/menu/index.d.ts +1 -1
- package/dist/primitives/menu/index.js +1 -1
- package/dist/primitives/picker/Picker.js +2 -2
- package/dist/primitives/picker/TagsField.js +2 -2
- package/dist/primitives/table/DataTable.d.ts +6 -4
- package/dist/primitives/table/DataTable.js +12 -8
- package/dist/primitives/table/EditableTable.js +102 -99
- package/dist/primitives/table/Table.d.ts +5 -8
- package/dist/primitives/table/Table.js +63 -15
- package/dist/primitives/table/index.d.ts +2 -1
- package/dist/primitives/table/index.js +2 -1
- package/dist/primitives/table/sorting.d.ts +42 -0
- package/dist/primitives/table/sorting.js +110 -0
- package/dist/primitives/value/RichText.d.ts +10 -0
- package/dist/primitives/value/RichText.js +130 -0
- package/dist/primitives/value/Value.d.ts +20 -2
- package/dist/primitives/value/Value.js +26 -7
- package/dist/primitives/value/index.d.ts +3 -1
- package/dist/primitives/value/index.js +4 -1
- package/dist/primitives/value/rounding.d.ts +9 -0
- package/dist/primitives/value/rounding.js +48 -0
- package/dist/tokens.css +103 -0
- package/dist/views/list/List.d.ts +5 -4
- package/dist/views/list/ListView.js +2 -2
- package/dist/views/list/types.d.ts +6 -6
- package/package.json +9 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arithmetic behind the two media controls — the crop of `ImageInput` and the strokes of
|
|
3
|
+
* `SignatureInput` — kept apart from the canvas so it can be checked without a browser.
|
|
4
|
+
*
|
|
5
|
+
* Context: a crop that is off by half a pixel, or a pan that lets the frame slide past the
|
|
6
|
+
* edge of the photo, is invisible in a story and obvious on the avatar a hundred screens
|
|
7
|
+
* later. Here the rules are plain functions over sizes and points; the components only
|
|
8
|
+
* read the pointer and the keys, ask these where things go, and paint.
|
|
9
|
+
*/
|
|
10
|
+
/** Past four times the photo is mostly blur: the limit is about what is worth keeping. */
|
|
11
|
+
export const CROP_MAX_ZOOM = 4;
|
|
12
|
+
export const CROP_CENTERED = { zoom: 1, x: 0.5, y: 0.5 };
|
|
13
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
14
|
+
/** The width of the frame, in image pixels, at zoom 1: the largest box of `aspect` that fits
|
|
15
|
+
* inside the image. «Cover», never «contain» — a crop never adds empty bands. */
|
|
16
|
+
function coverWidth(image, aspect) {
|
|
17
|
+
return Math.min(image.width, image.height * aspect);
|
|
18
|
+
}
|
|
19
|
+
/** The crop made valid: zoom within `[1, CROP_MAX_ZOOM]`, and the centre held far enough
|
|
20
|
+
* from the edges that the frame never shows past the image. */
|
|
21
|
+
export function clampCrop(image, aspect, crop) {
|
|
22
|
+
const zoom = clamp(Number.isFinite(crop.zoom) ? crop.zoom : 1, 1, CROP_MAX_ZOOM);
|
|
23
|
+
const width = coverWidth(image, aspect) / zoom;
|
|
24
|
+
const height = width / aspect;
|
|
25
|
+
const halfX = width / 2 / image.width;
|
|
26
|
+
const halfY = height / 2 / image.height;
|
|
27
|
+
return {
|
|
28
|
+
zoom,
|
|
29
|
+
x: clamp(crop.x, halfX, 1 - halfX),
|
|
30
|
+
y: clamp(crop.y, halfY, 1 - halfY),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The part of the image the frame shows, in image pixels — what `drawImage` reads from. */
|
|
34
|
+
export function cropRegion(image, aspect, crop) {
|
|
35
|
+
const valid = clampCrop(image, aspect, crop);
|
|
36
|
+
const width = coverWidth(image, aspect) / valid.zoom;
|
|
37
|
+
const height = width / aspect;
|
|
38
|
+
return {
|
|
39
|
+
x: valid.x * image.width - width / 2,
|
|
40
|
+
y: valid.y * image.height - height / 2,
|
|
41
|
+
width,
|
|
42
|
+
height,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Moves the IMAGE under the frame by a fraction of the frame — the way a drag does: dragging
|
|
47
|
+
* right by a quarter of the frame slides the photo right by a quarter, so the frame now shows
|
|
48
|
+
* more of its left side. The arrow keys move it the same way, so the hand and the keyboard
|
|
49
|
+
* never disagree about which way is which.
|
|
50
|
+
*/
|
|
51
|
+
export function panCrop(image, aspect, crop, dx, dy) {
|
|
52
|
+
const region = cropRegion(image, aspect, crop);
|
|
53
|
+
return clampCrop(image, aspect, {
|
|
54
|
+
...crop,
|
|
55
|
+
x: crop.x - (dx * region.width) / image.width,
|
|
56
|
+
y: crop.y - (dy * region.height) / image.height,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Zooms by `factor` about the frame's centre (the point the person is looking at). */
|
|
60
|
+
export function zoomCrop(image, aspect, crop, factor) {
|
|
61
|
+
return clampCrop(image, aspect, { ...crop, zoom: crop.zoom * factor });
|
|
62
|
+
}
|
|
63
|
+
/** The size the cropped image is written at: the region's own pixels, never upscaled (a
|
|
64
|
+
* bigger file of the same blur), and at most `maxSide` on its longer side. */
|
|
65
|
+
export function outputSize(region, maxSide) {
|
|
66
|
+
const scale = Math.min(1, maxSide / Math.max(region.width, region.height));
|
|
67
|
+
return {
|
|
68
|
+
width: Math.max(1, Math.round(region.width * scale)),
|
|
69
|
+
height: Math.max(1, Math.round(region.height * scale)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Whether a file is an image the browser can draw. A dropped file is not filtered by
|
|
73
|
+
* `accept`, so the type is checked here — and by its extension when the system did not
|
|
74
|
+
* say its type, which happens for a file dragged from some file managers. */
|
|
75
|
+
export function isImageFile(file) {
|
|
76
|
+
if (file.type !== "")
|
|
77
|
+
return file.type.startsWith("image/");
|
|
78
|
+
return /\.(?:png|jpe?g|gif|webp|avif|bmp|svg)$/i.test(file.name);
|
|
79
|
+
}
|
|
80
|
+
/** The type a crop is written in: JPEG and WebP stay what they were (a photo as PNG is five
|
|
81
|
+
* times heavier for nothing); everything else becomes PNG, which keeps transparency. */
|
|
82
|
+
export function croppedType(type) {
|
|
83
|
+
return type === "image/jpeg" || type === "image/webp" ? type : "image/png";
|
|
84
|
+
}
|
|
85
|
+
/** The file's name with the extension of the type it was written in: «logo.gif» cropped to
|
|
86
|
+
* a PNG is «logo.png», so the server does not trust a name that lies about its content. */
|
|
87
|
+
export function croppedName(name, type) {
|
|
88
|
+
const extension = type === "image/jpeg" ? "jpg" : type === "image/webp" ? "webp" : "png";
|
|
89
|
+
const base = name.replace(/\.[^./]+$/, "");
|
|
90
|
+
return `${base === "" ? "imagen" : base}.${extension}`;
|
|
91
|
+
}
|
|
92
|
+
const midpoint = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
|
|
93
|
+
/**
|
|
94
|
+
* The path of one stroke, smoothed: each sampled point becomes the CONTROL of a quadratic
|
|
95
|
+
* curve that runs between the midpoints around it.
|
|
96
|
+
*
|
|
97
|
+
* Context: joining the pointer's samples with straight lines draws a polygon — at the speed
|
|
98
|
+
* a signature is written the samples are far apart, and every corner shows. Curving through
|
|
99
|
+
* the midpoints passes near every sample without a corner at any of them, and costs nothing:
|
|
100
|
+
* no pressure, no velocity, no library. A single sample is a tap — a dot, the one over an i.
|
|
101
|
+
*/
|
|
102
|
+
export function strokeSteps(points) {
|
|
103
|
+
const first = points[0];
|
|
104
|
+
if (first === undefined)
|
|
105
|
+
return [];
|
|
106
|
+
if (points.length === 1)
|
|
107
|
+
return [{ kind: "dot", at: first }];
|
|
108
|
+
const steps = [{ kind: "move", to: first }];
|
|
109
|
+
for (let i = 1; i < points.length - 1; i += 1) {
|
|
110
|
+
steps.push({
|
|
111
|
+
kind: "quad",
|
|
112
|
+
control: points[i],
|
|
113
|
+
to: midpoint(points[i], points[i + 1]),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
steps.push({ kind: "line", to: points[points.length - 1] });
|
|
117
|
+
return steps;
|
|
118
|
+
}
|
|
119
|
+
/** The font size a typed name is written at so it fits `maxWidth`: `size` when it fits, and
|
|
120
|
+
* smaller in proportion when it does not — a long name is still one line, never cut. */
|
|
121
|
+
export function fitFontSize(widthAtSize, size, maxWidth, min = 12) {
|
|
122
|
+
if (widthAtSize <= maxWidth || widthAtSize <= 0)
|
|
123
|
+
return size;
|
|
124
|
+
return Math.max(min, Math.floor((size * maxWidth) / widthAtSize));
|
|
125
|
+
}
|
|
126
|
+
/** Keeps a pointer's moves on the element that started a drag, even when it leaves it — a
|
|
127
|
+
* stroke that runs off the pad is still one stroke. A pointer the browser no longer tracks
|
|
128
|
+
* (a synthetic one, or one already lifted) cannot be captured, and the drag works without. */
|
|
129
|
+
export function capturePointer(event) {
|
|
130
|
+
try {
|
|
131
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* Not capturable: the moves still arrive while the pointer stays over the element. */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** What of a text fills the pattern's slots, in order — a character a slot does not take is
|
|
2
|
+
* dropped, letters go to capitals. «1020304-5» on «###-#######» is «10203045». */
|
|
3
|
+
export declare function patternValue(text: string, pattern: string): string;
|
|
4
|
+
/** The value drawn on the pattern, up to its last character: a separator is drawn only once
|
|
5
|
+
* something comes after it, so deleting never leaves a dangling «-». */
|
|
6
|
+
export declare function formatPattern(value: string, pattern: string): string;
|
|
7
|
+
/** Every slot is filled. */
|
|
8
|
+
export declare function isPatternComplete(value: string, pattern: string): boolean;
|
|
9
|
+
/** How many characters the pattern takes. */
|
|
10
|
+
export declare function patternLength(pattern: string): number;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* ==================================================================== *
|
|
2
|
+
* A PATTERN — where each character of an identifier goes.
|
|
3
|
+
*
|
|
4
|
+
* # a digit
|
|
5
|
+
* A a letter, written in capitals
|
|
6
|
+
* * a letter or a digit
|
|
7
|
+
*
|
|
8
|
+
* Anything else is a separator drawn between them: «###-#######» for a
|
|
9
|
+
* reference, «AA## #### #### #### #### ####» for an IBAN. Separators are never
|
|
10
|
+
* letters or digits — those always mean a slot.
|
|
11
|
+
*
|
|
12
|
+
* The value is only what fills the slots («BO12345…»); the separators are
|
|
13
|
+
* drawn. Pure: `PatternInput` types with it and a form checks with it.
|
|
14
|
+
* ==================================================================== */
|
|
15
|
+
const SLOT = /[#A*]/;
|
|
16
|
+
function fits(slot, char) {
|
|
17
|
+
if (slot === "#")
|
|
18
|
+
return /\d/.test(char);
|
|
19
|
+
if (slot === "A")
|
|
20
|
+
return /[a-z]/i.test(char);
|
|
21
|
+
return /[a-z\d]/i.test(char);
|
|
22
|
+
}
|
|
23
|
+
/** The slots of a pattern, in order. */
|
|
24
|
+
function slotsOf(pattern) {
|
|
25
|
+
return [...pattern].filter((char) => SLOT.test(char));
|
|
26
|
+
}
|
|
27
|
+
/** What of a text fills the pattern's slots, in order — a character a slot does not take is
|
|
28
|
+
* dropped, letters go to capitals. «1020304-5» on «###-#######» is «10203045». */
|
|
29
|
+
export function patternValue(text, pattern) {
|
|
30
|
+
const slots = slotsOf(pattern);
|
|
31
|
+
let value = "";
|
|
32
|
+
for (const char of text) {
|
|
33
|
+
const slot = slots[value.length];
|
|
34
|
+
if (slot === undefined)
|
|
35
|
+
break;
|
|
36
|
+
if (fits(slot, char))
|
|
37
|
+
value += char.toUpperCase();
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
/** The value drawn on the pattern, up to its last character: a separator is drawn only once
|
|
42
|
+
* something comes after it, so deleting never leaves a dangling «-». */
|
|
43
|
+
export function formatPattern(value, pattern) {
|
|
44
|
+
let out = "";
|
|
45
|
+
let pending = "";
|
|
46
|
+
let at = 0;
|
|
47
|
+
for (const char of pattern) {
|
|
48
|
+
if (at >= value.length)
|
|
49
|
+
break;
|
|
50
|
+
if (SLOT.test(char)) {
|
|
51
|
+
out += pending + (value[at] ?? "");
|
|
52
|
+
pending = "";
|
|
53
|
+
at++;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
pending += char;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** Every slot is filled. */
|
|
62
|
+
export function isPatternComplete(value, pattern) {
|
|
63
|
+
return patternValue(value, pattern).length === slotsOf(pattern).length;
|
|
64
|
+
}
|
|
65
|
+
/** How many characters the pattern takes. */
|
|
66
|
+
export function patternLength(pattern) {
|
|
67
|
+
return slotsOf(pattern).length;
|
|
68
|
+
}
|
|
@@ -65,6 +65,22 @@ declare const WIDTH: {
|
|
|
65
65
|
/** Every option of the tree by id, children included: a submenu is a separate React Aria
|
|
66
66
|
* menu with its own `onAction`, and both have to reach the same handlers. */
|
|
67
67
|
export declare function indexOptions(options: readonly MenuOption[], into?: Map<string, MenuOption>): Map<string, MenuOption>;
|
|
68
|
+
/**
|
|
69
|
+
* The look of a list control's trigger — «Filtros ▾», «Columnas ▾», «Ordenar ▾» — without
|
|
70
|
+
* the menu.
|
|
71
|
+
*
|
|
72
|
+
* Context: a control of the bar that opens a PANEL and not a list of options (the sort
|
|
73
|
+
* criteria) still has to look like its neighbours; copying these classes is how the third
|
|
74
|
+
* trigger ends up one pixel off. Same reason `buttonClasses` exists.
|
|
75
|
+
*/
|
|
76
|
+
export declare function menuTriggerClasses({ variant, surface, active, open, focusVisible, iconOnly, }: {
|
|
77
|
+
variant?: "button" | "text";
|
|
78
|
+
surface?: "canvas" | "chrome";
|
|
79
|
+
active?: boolean;
|
|
80
|
+
open?: boolean;
|
|
81
|
+
focusVisible?: boolean;
|
|
82
|
+
iconOnly?: boolean;
|
|
83
|
+
}): string;
|
|
68
84
|
export declare function Menu({ label, groups, onSelect, surface, align, icon: Trigger,
|
|
69
85
|
/** The menu as a control of a list ("filters ▾") carries the chevron; the
|
|
70
86
|
* actions `⋯` does not, because the icon already says there is more. */
|
|
@@ -26,24 +26,35 @@ export function indexOptions(options, into = new Map()) {
|
|
|
26
26
|
}
|
|
27
27
|
return into;
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The look of a list control's trigger — «Filtros ▾», «Columnas ▾», «Ordenar ▾» — without
|
|
31
|
+
* the menu.
|
|
32
|
+
*
|
|
33
|
+
* Context: a control of the bar that opens a PANEL and not a list of options (the sort
|
|
34
|
+
* criteria) still has to look like its neighbours; copying these classes is how the third
|
|
35
|
+
* trigger ends up one pixel off. Same reason `buttonClasses` exists.
|
|
36
|
+
*/
|
|
37
|
+
export function menuTriggerClasses({ variant = "button", surface = "canvas", active = false, open = false, focusVisible = false, iconOnly = false, }) {
|
|
35
38
|
/* `text` is the menu of a BAR: no frame and no background, just the highlight
|
|
36
39
|
on hover. Five bordered menus on top of an application are five buttons
|
|
37
40
|
competing with the ones in the view, and the bar stops reading as an index.
|
|
38
41
|
`button` is the menu of a CONTROL —"filters ▾", "group ▾"—, and there the
|
|
39
42
|
frame is what says it can be pressed. */
|
|
40
|
-
const
|
|
43
|
+
const tone = variant === "text"
|
|
41
44
|
? surface === "chrome"
|
|
42
45
|
? `border-transparent text-chrome-muted hover:bg-chrome-ink/12 hover:text-chrome-ink ${active || open ? "bg-chrome-ink/12 text-chrome-ink" : ""}`
|
|
43
46
|
: `border-transparent text-muted hover:bg-ink/6 hover:text-ink ${active || open ? "bg-ink/6 text-ink" : ""}`
|
|
44
47
|
: surface === "chrome"
|
|
45
48
|
? `border-chrome-muted/35 bg-chrome-raised text-chrome-ink hover:border-chrome-accent/45 ${active ? "border-chrome-accent/60" : ""}`
|
|
46
49
|
: `border-line-strong bg-surface hover:border-primary/45 hover:bg-primary/5 ${active ? "border-primary/60 text-primary" : "text-ink"}`;
|
|
50
|
+
return `inline-flex h-(--size-control-sm) shrink-0 cursor-default items-center gap-1.5 border text-micro font-medium whitespace-nowrap outline-0 transition-colors duration-(--duration-instant) ${focusVisible ? "outline-2 outline-offset-2 outline-primary" : ""} ${iconOnly ? "w-(--size-control-sm) justify-center rounded-pill" : "rounded-control px-2.5"} ${tone}`;
|
|
51
|
+
}
|
|
52
|
+
export function Menu({ label, groups, onSelect, surface = "canvas", align = "start", icon: Trigger,
|
|
53
|
+
/** The menu as a control of a list ("filters ▾") carries the chevron; the
|
|
54
|
+
* actions `⋯` does not, because the icon already says there is more. */
|
|
55
|
+
chevron = true, active = false, iconOnly = false, variant = "button", searchPlaceholder, emptyText, width = "md", role, size = "md", title, closeOnSelect = true, }) {
|
|
56
|
+
const copy = useUiCopy();
|
|
57
|
+
const { contains } = useFilter({ sensitivity: "base" });
|
|
47
58
|
const byId = indexOptions(groups.flatMap((g) => g.options));
|
|
48
59
|
const menu = (_jsx(MenuList, { label: label, groups: groups, width: width, closeOnSelect: closeOnSelect, onAction: (key) => {
|
|
49
60
|
const option = byId.get(String(key));
|
|
@@ -51,7 +62,14 @@ chevron = true, active = false, iconOnly = false, variant = "button", searchPlac
|
|
|
51
62
|
onSelect?.(String(key));
|
|
52
63
|
}, emptyText: emptyText ?? copy.menu.empty }));
|
|
53
64
|
return (_jsxs(MenuTrigger, { children: [_jsx(AriaButton, { "aria-label": iconOnly ? label : undefined, ...(role === undefined ? {} : { "data-emphasis": role }), className: ({ isPressed, isFocusVisible }) => role === undefined
|
|
54
|
-
?
|
|
65
|
+
? menuTriggerClasses({
|
|
66
|
+
variant,
|
|
67
|
+
surface,
|
|
68
|
+
active,
|
|
69
|
+
open: isPressed,
|
|
70
|
+
focusVisible: isFocusVisible,
|
|
71
|
+
iconOnly,
|
|
72
|
+
})
|
|
55
73
|
: `${buttonClasses(role, size, surface)} gap-1.5`, children: _jsxs("span", { className: "contents", title: title ?? (iconOnly ? label : undefined), children: [Trigger === undefined ? (iconOnly ? (_jsx(ChevronDown, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })) : null) : (_jsx(Trigger, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })), iconOnly ? null : label, chevron && !iconOnly ? (_jsx(ChevronDown, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })) : null] }) }), _jsx(Popover, { placement: align === "end" ? "bottom end" : "bottom start", className: WIDTH[width], children: searchPlaceholder === undefined ? (menu) : (
|
|
56
74
|
/* The search box stays at the top: in a menu of twenty-five entries,
|
|
57
75
|
scrolling loses exactly the control that explains why only three are
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Menu, type MenuGroup, type MenuOption } from "./Menu.js";
|
|
1
|
+
export { Menu, type MenuGroup, type MenuOption, menuTriggerClasses, } from "./Menu.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/* A data-driven dropdown: the actions `⋯` and "+ Create". */
|
|
2
|
-
export { Menu } from "./Menu.js";
|
|
2
|
+
export { Menu, menuTriggerClasses, } from "./Menu.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
4
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
5
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
6
6
|
import { useFormMode } from "../../foundations/form.js";
|
|
7
7
|
import { matchedWord, matches } from "../../foundations/match.js";
|
|
8
8
|
import { cn, tv } from "../../foundations/variants.js";
|
|
@@ -198,7 +198,7 @@ function EditablePicker({ id: ownId, items, value, onChange, onSearch, valueLabe
|
|
|
198
198
|
if (next !== value)
|
|
199
199
|
onChange?.(next);
|
|
200
200
|
};
|
|
201
|
-
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
201
|
+
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
202
202
|
setText(next);
|
|
203
203
|
setQuery(next);
|
|
204
204
|
}, onOpenChange: (isOpen, trigger) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
4
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
5
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
6
6
|
import { useFormMode } from "../../foundations/form.js";
|
|
7
7
|
import { cn } from "../../foundations/variants.js";
|
|
8
8
|
import { fieldGroupStyles, listOptionStyles } from "../field/aria.js";
|
|
@@ -171,7 +171,7 @@ function EditableTagsField({ value, onChange, options, onCreate, onSearch, onBro
|
|
|
171
171
|
? ariaLabel === undefined
|
|
172
172
|
? {}
|
|
173
173
|
: { label: ariaLabel }
|
|
174
|
-
: { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
|
|
174
|
+
: { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
|
|
175
175
|
/* Arriving at the field shows what can still be added, the way a many2one opens its
|
|
176
176
|
list on focus. With every tag chosen and no text there is nothing to show, and the
|
|
177
177
|
list stays closed. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Pile } from "../../foundations/grouping.js";
|
|
2
2
|
import type { Tone } from "../../foundations/types.js";
|
|
3
3
|
import { type RowAction } from "./RowActions.js";
|
|
4
|
-
import {
|
|
4
|
+
import type { SortState } from "./sorting.js";
|
|
5
5
|
import { type ReactNode } from "react";
|
|
6
6
|
/** How a column is aggregated when its rows are pulled together into a group.
|
|
7
7
|
*
|
|
@@ -29,7 +29,7 @@ export interface DataColumn<T> {
|
|
|
29
29
|
field?: string;
|
|
30
30
|
/** The raw value: this is what sorting and grouping run on. */
|
|
31
31
|
value: (row: T) => string | number | boolean | null;
|
|
32
|
-
/** How it is drawn. Without this the raw value
|
|
32
|
+
/** How it is drawn. Without this, `text`; without either, the raw value. */
|
|
33
33
|
cell?: (row: T) => ReactNode;
|
|
34
34
|
/** A column of CODES — a SKU, a hash, a key — set in the monospaced face. Numbers do not
|
|
35
35
|
* need it: they align with tabular figures in the same face as the rest of the row. */
|
|
@@ -153,8 +153,10 @@ export declare function DataTable<T extends object>({ data, columns, getRowId, s
|
|
|
153
153
|
data: readonly T[];
|
|
154
154
|
columns: readonly DataColumn<T>[];
|
|
155
155
|
getRowId: (row: T) => string;
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
/** The order, outermost first: the second criterion decides only between rows the first
|
|
157
|
+
* leaves equal. Empty or absent is «as they came». */
|
|
158
|
+
sort?: readonly SortState[];
|
|
159
|
+
onSortChange?: (next: SortState[]) => void;
|
|
158
160
|
/**
|
|
159
161
|
* Los NIVELES por los que se parte, de afuera hacia adentro. Vacío o ausente
|
|
160
162
|
* es «sin agrupar».
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { FieldBoundary } from "../../foundations/form.js";
|
|
4
5
|
import { pathKey, pileFoot, pileItems, } from "../../foundations/grouping.js";
|
|
5
6
|
import { CellCopy } from "./CellCopy.js";
|
|
6
7
|
import { CellEditor } from "./CellEditor.js";
|
|
@@ -8,7 +9,7 @@ import { EditRow, firstControl } from "./EditableTable.js";
|
|
|
8
9
|
import { PileFootLine } from "./PileFootLine.js";
|
|
9
10
|
import { RowActions } from "./RowActions.js";
|
|
10
11
|
import { useRowWindow } from "./rowWindow.js";
|
|
11
|
-
import { SelectBox, Table, Td, Th, THead, Tr
|
|
12
|
+
import { SelectBox, Table, Td, Th, THead, Tr } from "./Table.js";
|
|
12
13
|
import { aggregationFns, columnGroupingFeature, createColumnHelper, createExpandedRowModel, createGroupedRowModel, createSortedRowModel, rowAggregationFeature, rowExpandingFeature, rowSortingFeature, sortFns, tableFeatures, useTable, } from "@tanstack/react-table";
|
|
13
14
|
import { ChevronRight, Plus } from "lucide-react";
|
|
14
15
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -65,9 +66,14 @@ function raw(copy, v) {
|
|
|
65
66
|
return v ? copy.table.yes : copy.table.no;
|
|
66
67
|
return v;
|
|
67
68
|
}
|
|
68
|
-
/** What a cell draws: its `cell
|
|
69
|
+
/** What a cell draws: its `cell`; without one, how it READS (`text`); only then the raw
|
|
70
|
+
* value — which is the sort key, «posted» and not «publicada». */
|
|
69
71
|
function drawCell(copy, column, row) {
|
|
70
|
-
|
|
72
|
+
if (column.cell !== undefined)
|
|
73
|
+
return column.cell(row);
|
|
74
|
+
if (column.text !== undefined)
|
|
75
|
+
return column.text(row);
|
|
76
|
+
return raw(copy, column.value(row));
|
|
71
77
|
}
|
|
72
78
|
/* The features are declared once. In v9 they are opt-in: asking only for what
|
|
73
79
|
is used is what keeps the bundle down to what is actually needed. */
|
|
@@ -247,9 +253,7 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
|
|
|
247
253
|
columns: defs,
|
|
248
254
|
getRowId: (row) => getRowId(row),
|
|
249
255
|
state: {
|
|
250
|
-
sorting: sort
|
|
251
|
-
? []
|
|
252
|
-
: [{ id: sort.key, desc: sort.direction === "desc" }],
|
|
256
|
+
sorting: (sort ?? []).map((one) => ({ id: one.key, desc: one.direction === "desc" })),
|
|
253
257
|
/* Counted elsewhere, the tree comes made: grouping the page again would undo it. */
|
|
254
258
|
grouping: piles === undefined ? (levels ?? []).map((level) => level.id) : [],
|
|
255
259
|
},
|
|
@@ -421,8 +425,8 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
|
|
|
421
425
|
return (_jsx("td", { className: `px-3 py-1.5 text-meta font-semibold text-ink ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: i === 0 && value === null ? (_jsxs("span", { className: "text-muted", children: [data.length.toLocaleString(copy.locale), " ", data.length === 1 ? copy.table.row : copy.table.rows] })) : value === null ? null : (aggregateLabel(copy, c, value)) }, c.id));
|
|
422
426
|
}), rowActions === undefined ? null : _jsx("td", {})] }) }))] }));
|
|
423
427
|
if (create === undefined)
|
|
424
|
-
return grid;
|
|
425
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col", children: [grid, writing ? null : (_jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { ref: addButton, type: "button", onClick: start, className: "flex w-full items-center gap-1.5 rounded-control px-3 py-1 text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary", children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), create.label ?? copy.table.add] }) }))] }));
|
|
428
|
+
return _jsx(FieldBoundary, { children: grid });
|
|
429
|
+
return (_jsx(FieldBoundary, { children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [grid, writing ? null : (_jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { ref: addButton, type: "button", onClick: start, className: "flex w-full items-center gap-1.5 rounded-control px-3 py-1 text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary", children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), create.label ?? copy.table.add] }) }))] }) }));
|
|
426
430
|
}
|
|
427
431
|
/* A cell of the row being written: the table's cell, washed like an edited row. */
|
|
428
432
|
const DRAFT_CELL = "border-b border-line bg-primary/5 px-3 py-1 align-middle text-meta text-ink first:rounded-l-control last:rounded-r-control";
|