@sincpro/web-ui 0.1.0 → 1.0.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 +40 -13
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/activity/ActivityBadge.d.ts +16 -0
- package/dist/components/activity/ActivityBadge.js +29 -0
- package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
- package/dist/components/{chatter → activity}/Messages.js +9 -6
- package/dist/components/activity/index.d.ts +4 -0
- package/dist/components/activity/index.js +5 -0
- package/dist/components/assist/AssistMark.d.ts +3 -3
- package/dist/components/assist/AssistMark.js +1 -1
- package/dist/components/assist/assist.d.ts +1 -1
- package/dist/components/assist/assist.js +1 -1
- package/dist/components/calendar/RecordCalendar.d.ts +48 -0
- package/dist/components/calendar/RecordCalendar.js +101 -0
- package/dist/components/calendar/index.d.ts +1 -0
- package/dist/components/calendar/index.js +1 -0
- package/dist/components/form/Sheet.d.ts +4 -0
- package/dist/components/form/Sheet.js +16 -5
- package/dist/components/gantt/RecordGantt.d.ts +44 -0
- package/dist/components/gantt/RecordGantt.js +110 -0
- package/dist/components/gantt/index.d.ts +1 -0
- package/dist/components/gantt/index.js +1 -0
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.js +4 -1
- package/dist/components/lines/LineItems.d.ts +45 -5
- package/dist/components/lines/LineItems.js +106 -18
- package/dist/components/listing/BoardColumns.d.ts +39 -0
- package/dist/components/listing/BoardColumns.js +138 -0
- package/dist/components/listing/Listing.js +1 -1
- package/dist/components/listing/ListingBar.d.ts +46 -2
- package/dist/components/listing/ListingBar.js +47 -5
- package/dist/components/listing/PivotExplorer.d.ts +34 -0
- package/dist/components/listing/PivotExplorer.js +86 -0
- package/dist/components/listing/RecordCard.d.ts +6 -8
- package/dist/components/listing/RecordCard.js +13 -7
- package/dist/components/listing/Row.d.ts +25 -2
- package/dist/components/listing/Row.js +30 -13
- package/dist/components/listing/index.d.ts +2 -0
- package/dist/components/listing/index.js +2 -0
- package/dist/components/listing/set.d.ts +1 -1
- package/dist/components/navigation/index.d.ts +0 -2
- package/dist/components/navigation/index.js +2 -3
- package/dist/components/record/RecordAudit.d.ts +17 -0
- package/dist/components/record/RecordAudit.js +27 -0
- package/dist/components/record/RecordHeader.d.ts +27 -17
- package/dist/components/record/RecordHeader.js +64 -20
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/RecordRibbon.d.ts +13 -0
- package/dist/components/record/RecordRibbon.js +22 -0
- package/dist/components/record/ThumbBar.js +1 -1
- package/dist/components/record/index.d.ts +2 -0
- package/dist/components/record/index.js +2 -0
- package/dist/components/search/CriteriaChips.d.ts +6 -1
- package/dist/components/search/CriteriaChips.js +71 -20
- package/dist/components/search/SearchBar.d.ts +4 -1
- package/dist/components/search/SearchBar.js +15 -7
- package/dist/components/timeline/RecordTimeline.d.ts +60 -0
- package/dist/components/timeline/RecordTimeline.js +214 -0
- package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
- package/dist/components/timeline/TimelineToolbar.js +31 -0
- package/dist/components/timeline/index.d.ts +3 -0
- package/dist/components/timeline/index.js +3 -0
- package/dist/components/timeline/time.d.ts +43 -0
- package/dist/components/timeline/time.js +135 -0
- package/dist/foundations/copy.d.ts +194 -18
- package/dist/foundations/copy.es.js +142 -15
- package/dist/foundations/copy.js +133 -11
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/match.d.ts +23 -0
- package/dist/foundations/match.js +63 -0
- package/dist/foundations/slots.d.ts +1 -1
- package/dist/foundations/slots.js +8 -3
- package/dist/foundations/types.d.ts +7 -0
- package/dist/foundations/types.js +0 -36
- package/dist/primitives/block/index.d.ts +1 -1
- package/dist/primitives/block/index.js +1 -1
- package/dist/primitives/cell/CellStack.d.ts +7 -1
- package/dist/primitives/cell/CellStack.js +5 -2
- package/dist/primitives/choice/ComboBox.d.ts +3 -0
- package/dist/primitives/choice/ComboBox.js +10 -6
- package/dist/primitives/choice/KanbanState.d.ts +14 -0
- package/dist/primitives/choice/KanbanState.js +37 -0
- package/dist/primitives/choice/Priority.d.ts +20 -0
- package/dist/primitives/choice/Priority.js +41 -0
- package/dist/primitives/choice/SearchField.d.ts +10 -1
- package/dist/primitives/choice/SearchField.js +21 -2
- package/dist/primitives/choice/Slider.d.ts +6 -1
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/choice/index.d.ts +2 -0
- package/dist/primitives/choice/index.js +2 -0
- package/dist/primitives/date/Calendar.js +30 -6
- package/dist/primitives/date/DatePicker.d.ts +11 -3
- package/dist/primitives/date/DatePicker.js +12 -8
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +1 -0
- package/dist/primitives/date/index.js +4 -2
- package/dist/primitives/dialog/Dialog.d.ts +9 -3
- package/dist/primitives/dialog/Dialog.js +41 -5
- package/dist/primitives/field/DropZone.js +1 -1
- package/dist/primitives/field/Field.d.ts +14 -2
- package/dist/primitives/field/Field.js +25 -14
- package/dist/primitives/field/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +2 -0
- package/dist/primitives/field/index.js +4 -0
- package/dist/primitives/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -0
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/menu/Menu.d.ts +28 -0
- package/dist/primitives/menu/Menu.js +47 -17
- package/dist/primitives/notice/Callout.js +1 -1
- package/dist/primitives/notice/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -0
- package/dist/primitives/notice/index.d.ts +2 -1
- package/dist/primitives/notice/index.js +2 -1
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +69 -0
- package/dist/primitives/overlay/index.d.ts +1 -0
- package/dist/primitives/overlay/index.js +3 -1
- package/dist/primitives/picker/MatchedText.d.ts +10 -0
- package/dist/primitives/picker/MatchedText.js +12 -0
- package/dist/primitives/picker/Picker.d.ts +67 -4
- package/dist/primitives/picker/Picker.js +142 -23
- package/dist/primitives/table/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -0
- package/dist/primitives/table/DataTable.d.ts +55 -1
- package/dist/primitives/table/DataTable.js +126 -40
- package/dist/primitives/table/EditableTable.d.ts +102 -2
- package/dist/primitives/table/EditableTable.js +287 -32
- package/dist/primitives/table/ReportTable.js +2 -1
- package/dist/primitives/table/RowActions.js +7 -2
- package/dist/primitives/table/SheetTable.d.ts +4 -1
- package/dist/primitives/table/SheetTable.js +38 -5
- package/dist/primitives/table/Table.d.ts +5 -0
- package/dist/primitives/table/Table.js +17 -8
- package/dist/primitives/table/index.d.ts +2 -2
- package/dist/primitives/table/index.js +1 -1
- package/dist/primitives/tabs/Tabs.d.ts +2 -2
- package/dist/primitives/tabs/Tabs.js +1 -1
- package/dist/primitives/tag/Tags.d.ts +2 -0
- package/dist/primitives/tag/Tags.js +2 -1
- package/dist/primitives/tag/TagsField.d.ts +13 -0
- package/dist/primitives/tag/TagsField.js +39 -10
- package/dist/primitives/tag/choices.d.ts +2 -0
- package/dist/primitives/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Avatar.d.ts +16 -0
- package/dist/primitives/value/Avatar.js +91 -0
- package/dist/primitives/value/Copyable.d.ts +11 -0
- package/dist/primitives/value/Copyable.js +27 -6
- package/dist/primitives/value/Fecha.js +10 -1
- package/dist/primitives/value/Person.d.ts +6 -51
- package/dist/primitives/value/Person.js +9 -148
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +22 -5
- package/dist/primitives/value/Relation.js +42 -14
- package/dist/primitives/value/Totals.d.ts +102 -7
- package/dist/primitives/value/Totals.js +52 -2
- package/dist/primitives/value/Value.d.ts +0 -37
- package/dist/primitives/value/Value.js +0 -39
- package/dist/primitives/value/contact.d.ts +30 -0
- package/dist/primitives/value/contact.js +51 -0
- package/dist/primitives/value/index.d.ts +5 -4
- package/dist/primitives/value/index.js +5 -4
- package/dist/tokens.css +179 -7
- package/dist/views/list/List.d.ts +32 -3
- package/dist/views/list/List.js +51 -7
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +70 -13
- package/dist/views/list/columns.js +8 -1
- package/dist/views/list/derive.d.ts +12 -1
- package/dist/views/list/derive.js +29 -3
- package/dist/views/list/presentations.d.ts +5 -2
- package/dist/views/list/presentations.js +35 -3
- package/dist/views/list/types.d.ts +57 -1
- package/dist/views/list/useNarrowList.d.ts +10 -0
- package/dist/views/list/useNarrowList.js +20 -6
- package/dist/views/record/Record.d.ts +41 -8
- package/dist/views/record/Record.js +113 -28
- package/dist/views/record/index.d.ts +1 -1
- package/package.json +15 -4
- package/dist/components/chatter/Chatter.d.ts +0 -161
- package/dist/components/chatter/Chatter.js +0 -232
- package/dist/components/chatter/index.d.ts +0 -3
- package/dist/components/chatter/index.js +0 -4
- package/dist/components/navigation/CommandPalette.d.ts +0 -40
- package/dist/components/navigation/CommandPalette.js +0 -114
- package/dist/components/navigation/commands.d.ts +0 -30
- package/dist/components/navigation/commands.js +0 -30
- /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
- /package/dist/components/{chatter → activity}/Timeline.js +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type PriorityProps = {
|
|
2
|
+
/** 0 is «sin prioridad». */
|
|
3
|
+
value: number;
|
|
4
|
+
/** How many stars. Odoo's three, by default. */
|
|
5
|
+
max?: number;
|
|
6
|
+
/** Without it the stars only show. Pressing the star already set clears it, the way Odoo
|
|
7
|
+
* does — the only way back to «none» that needs no extra control. */
|
|
8
|
+
onChange?: (next: number) => void;
|
|
9
|
+
/** The group's name, when it is not «Prioridad». */
|
|
10
|
+
label?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Priority as stars: how urgent a lead, a task or a ticket is, read in a list at a glance.
|
|
14
|
+
*
|
|
15
|
+
* Context: a select «baja / media / alta» is read as a word in a column of words; stars are
|
|
16
|
+
* counted without reading. They are a radio group: arrows move, and it is named — «Prioridad:
|
|
17
|
+
* 2 de 3» — so it is not colour alone. In the `warn` tone, not the gold: the gold is the
|
|
18
|
+
* screen's primary action.
|
|
19
|
+
*/
|
|
20
|
+
export declare function Priority({ value, max, onChange, label }: PriorityProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { Star } from "lucide-react";
|
|
5
|
+
import { useRef } from "react";
|
|
6
|
+
/**
|
|
7
|
+
* Priority as stars: how urgent a lead, a task or a ticket is, read in a list at a glance.
|
|
8
|
+
*
|
|
9
|
+
* Context: a select «baja / media / alta» is read as a word in a column of words; stars are
|
|
10
|
+
* counted without reading. They are a radio group: arrows move, and it is named — «Prioridad:
|
|
11
|
+
* 2 de 3» — so it is not colour alone. In the `warn` tone, not the gold: the gold is the
|
|
12
|
+
* screen's primary action.
|
|
13
|
+
*/
|
|
14
|
+
export function Priority({ value, max = 3, onChange, label }) {
|
|
15
|
+
const copy = useUiCopy();
|
|
16
|
+
const name = label ?? copy.priority.label;
|
|
17
|
+
const stars = Array.from({ length: max }, (_, at) => at + 1);
|
|
18
|
+
const buttons = useRef([]);
|
|
19
|
+
const said = value === 0 ? copy.priority.none : fillCopy(copy.priority.level, { value, max });
|
|
20
|
+
const icon = (n) => (_jsx(Star, { "aria-hidden": true, className: `size-(--size-icon-md) ${n <= value ? "fill-warn text-warn" : "text-line-strong"}`, strokeWidth: 1.75 }));
|
|
21
|
+
if (onChange === undefined) {
|
|
22
|
+
return (_jsx("span", { role: "img", "aria-label": `${name}: ${said}`, className: "inline-flex items-center gap-0.5", children: stars.map((n) => (_jsx("span", { children: icon(n) }, n))) }));
|
|
23
|
+
}
|
|
24
|
+
const step = (event, n) => {
|
|
25
|
+
const next = event.key === "ArrowRight" || event.key === "ArrowUp"
|
|
26
|
+
? Math.min(max, n + 1)
|
|
27
|
+
: event.key === "ArrowLeft" || event.key === "ArrowDown"
|
|
28
|
+
? Math.max(1, n - 1)
|
|
29
|
+
: null;
|
|
30
|
+
if (next === null)
|
|
31
|
+
return;
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
onChange(next);
|
|
34
|
+
buttons.current[next - 1]?.focus();
|
|
35
|
+
};
|
|
36
|
+
return (_jsx("span", { role: "radiogroup", "aria-label": name, className: "inline-flex items-center gap-0.5", children: stars.map((n) => (_jsx("button", { ref: (node) => {
|
|
37
|
+
buttons.current[n - 1] = node;
|
|
38
|
+
}, type: "button", role: "radio", "aria-checked": value === n, "aria-label": fillCopy(copy.priority.level, { value: n, max }),
|
|
39
|
+
/* One stop in the tab order — the set star, or the first — and the arrows inside. */
|
|
40
|
+
tabIndex: n === Math.max(value, 1) ? 0 : -1, onClick: () => onChange(n === value ? 0 : n), onKeyDown: (event) => step(event, n), className: "relative z-10 rounded-control p-0.5 transition-colors duration-(--duration-instant) hover:bg-ink/6 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: icon(n) }, n))) }));
|
|
41
|
+
}
|
|
@@ -7,6 +7,9 @@ export type SearchFieldProps = {
|
|
|
7
7
|
disabled?: boolean;
|
|
8
8
|
id?: string;
|
|
9
9
|
describedBy?: string;
|
|
10
|
+
/** The name of the box when it is NOT inside a `Field` — the search of a list bar, of a
|
|
11
|
+
* panel. Inside a field, the field's label names it. Without either, see below. */
|
|
12
|
+
"aria-label"?: string;
|
|
10
13
|
};
|
|
11
14
|
/**
|
|
12
15
|
* A plain search box: a text, a clear button, Enter to submit. To filter the rows of a panel,
|
|
@@ -20,5 +23,11 @@ export type SearchFieldProps = {
|
|
|
20
23
|
* It does not follow the form's read mode: what is typed here is never data of the record, so
|
|
21
24
|
* there is nothing to draw as text. The browser's own clear cross is hidden; ours is the one
|
|
22
25
|
* the keyboard and the screen reader know.
|
|
26
|
+
*
|
|
27
|
+
* It is never nameless. Most search boxes live OUTSIDE a form — the bar of a list, a panel —
|
|
28
|
+
* where there is no `Field` to label them, and a nameless one is read as «edit text» (React
|
|
29
|
+
* Aria warns about it in the console). The name is, in this order: the enclosing `Field`'s
|
|
30
|
+
* label, an explicit `aria-label`, the placeholder (what the eye reads is what the ear
|
|
31
|
+
* should hear), and finally the copy table's «Search».
|
|
23
32
|
*/
|
|
24
|
-
export declare function SearchField({ value, onChange, placeholder, onSubmit, disabled, id: ownId, describedBy: ownDescribedBy, }: SearchFieldProps): import("react").JSX.Element;
|
|
33
|
+
export declare function SearchField({ value, onChange, placeholder, onSubmit, disabled, id: ownId, describedBy: ownDescribedBy, "aria-label": ariaLabel, }: SearchFieldProps): import("react").JSX.Element;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
5
|
import { FieldButton, FieldGroup } from "../field/aria.js";
|
|
5
6
|
import { useFieldIds } from "../field/context.js";
|
|
6
7
|
import { Search, X } from "lucide-react";
|
|
7
8
|
import { Input, SearchField as AriaSearchField } from "react-aria-components";
|
|
9
|
+
/** The trailing «…» of a placeholder is a visual cue that typing is expected, not part of
|
|
10
|
+
* the name: «Buscar una cuenta…» is announced as «Buscar una cuenta». */
|
|
11
|
+
function nameFromPlaceholder(placeholder) {
|
|
12
|
+
const name = placeholder?.replace(/[\s.…]+$/u, "").trim();
|
|
13
|
+
return name === undefined || name === "" ? undefined : name;
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* A plain search box: a text, a clear button, Enter to submit. To filter the rows of a panel,
|
|
10
17
|
* the options of a long settings page, the lines of a document.
|
|
@@ -17,11 +24,23 @@ import { Input, SearchField as AriaSearchField } from "react-aria-components";
|
|
|
17
24
|
* It does not follow the form's read mode: what is typed here is never data of the record, so
|
|
18
25
|
* there is nothing to draw as text. The browser's own clear cross is hidden; ours is the one
|
|
19
26
|
* the keyboard and the screen reader know.
|
|
27
|
+
*
|
|
28
|
+
* It is never nameless. Most search boxes live OUTSIDE a form — the bar of a list, a panel —
|
|
29
|
+
* where there is no `Field` to label them, and a nameless one is read as «edit text» (React
|
|
30
|
+
* Aria warns about it in the console). The name is, in this order: the enclosing `Field`'s
|
|
31
|
+
* label, an explicit `aria-label`, the placeholder (what the eye reads is what the ear
|
|
32
|
+
* should hear), and finally the copy table's «Search».
|
|
20
33
|
*/
|
|
21
|
-
export function SearchField({ value, onChange, placeholder, onSubmit, disabled = false, id: ownId, describedBy: ownDescribedBy, }) {
|
|
34
|
+
export function SearchField({ value, onChange, placeholder, onSubmit, disabled = false, id: ownId, describedBy: ownDescribedBy, "aria-label": ariaLabel, }) {
|
|
35
|
+
const copy = useUiCopy();
|
|
22
36
|
const { id, labelledBy, describedBy } = useFieldIds({
|
|
23
37
|
id: ownId,
|
|
24
38
|
describedBy: ownDescribedBy,
|
|
25
39
|
});
|
|
26
|
-
|
|
40
|
+
/* A `Field` names it through its label; anything else falls back so it always has one. */
|
|
41
|
+
const name = ariaLabel ??
|
|
42
|
+
(labelledBy === undefined
|
|
43
|
+
? (nameFromPlaceholder(placeholder) ?? copy.choice.search)
|
|
44
|
+
: undefined);
|
|
45
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaSearchField, { id: id, "aria-labelledby": labelledBy, "aria-label": name, "aria-describedby": describedBy, value: value, onChange: (next) => onChange?.(next), onSubmit: onSubmit, isDisabled: disabled, className: "group w-full min-w-0", children: _jsxs(FieldGroup, { children: [_jsx(Search, { "aria-hidden": true, className: "ml-2.5 size-(--size-icon-md) shrink-0 text-muted forced-colors:text-[ButtonText]", strokeWidth: 2 }), _jsx(Input, { placeholder: placeholder, className: "h-full min-w-0 flex-1 bg-transparent px-2 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed [&::-webkit-search-cancel-button]:hidden" }), _jsx(FieldButton, { className: "mr-1 group-data-empty:invisible", children: _jsx(X, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }) }) }));
|
|
27
46
|
}
|
|
@@ -12,5 +12,10 @@ export type SliderProps = {
|
|
|
12
12
|
describedBy?: string;
|
|
13
13
|
/** Forces the mode. By default it inherits the form's: in read mode it draws the figure. */
|
|
14
14
|
mode?: FormMode;
|
|
15
|
+
/** The name when it is NOT inside a `Field` — a toolbar's zoom. Inside a field, the
|
|
16
|
+
* field's label names it. */
|
|
17
|
+
"aria-label"?: string;
|
|
18
|
+
/** The figure beside the track. Off for a control whose number means nothing — a zoom. */
|
|
19
|
+
output?: boolean;
|
|
15
20
|
};
|
|
16
|
-
export declare function Slider({ value, onChange, min, max, step, format, disabled, id: ownId, describedBy: ownDescribedBy, mode, }: SliderProps): import("react").JSX.Element;
|
|
21
|
+
export declare function Slider({ value, onChange, min, max, step, format, disabled, id: ownId, describedBy: ownDescribedBy, mode, "aria-label": ariaLabel, output, }: SliderProps): import("react").JSX.Element;
|
|
@@ -33,7 +33,7 @@ const thumb = tv({
|
|
|
33
33
|
},
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
|
-
export function Slider({ value, onChange, min, max, step = 1, format, disabled = false, id: ownId, describedBy: ownDescribedBy, mode, }) {
|
|
36
|
+
export function Slider({ value, onChange, min, max, step = 1, format, disabled = false, id: ownId, describedBy: ownDescribedBy, mode, "aria-label": ariaLabel, output = true, }) {
|
|
37
37
|
const copy = useUiCopy();
|
|
38
38
|
const { id, labelledBy, describedBy } = useFieldIds({
|
|
39
39
|
id: ownId,
|
|
@@ -42,5 +42,5 @@ export function Slider({ value, onChange, min, max, step = 1, format, disabled =
|
|
|
42
42
|
if (useFormMode(mode) === "read") {
|
|
43
43
|
return _jsx(ReadValue, { value: new Intl.NumberFormat(copy.locale, format).format(value) });
|
|
44
44
|
}
|
|
45
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaSlider, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: value, onChange: (next) => onChange?.(next), minValue: min, maxValue: max, step: step, formatOptions: format, isDisabled: disabled, className: "flex h-(--size-control-md) w-full min-w-0 items-center gap-3 disabled:opacity-45", children: [_jsxs(SliderTrack, { className: "relative flex h-(--size-control-md) min-w-0 flex-1 cursor-pointer items-center disabled:cursor-not-allowed", children: [_jsx("span", { className: "relative h-1 w-full rounded-pill bg-line-strong forced-colors:bg-[ButtonBorder]", children: _jsx(SliderFill, { className: "rounded-pill bg-primary disabled:bg-muted forced-colors:bg-[Highlight]" }) }), _jsx(SliderThumb, { "aria-labelledby": labelledBy, "aria-describedby": describedBy, className: thumb })] }), _jsx(SliderOutput, { className: "w-14 shrink-0 text-right text-meta text-ink tabular-nums" })] }) }));
|
|
45
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaSlider, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: value, onChange: (next) => onChange?.(next), minValue: min, maxValue: max, step: step, formatOptions: format, isDisabled: disabled, className: "flex h-(--size-control-md) w-full min-w-0 items-center gap-3 disabled:opacity-45", children: [_jsxs(SliderTrack, { className: "relative flex h-(--size-control-md) min-w-0 flex-1 cursor-pointer items-center disabled:cursor-not-allowed", children: [_jsx("span", { className: "relative h-1 w-full rounded-pill bg-line-strong forced-colors:bg-[ButtonBorder]", children: _jsx(SliderFill, { className: "rounded-pill bg-primary disabled:bg-muted forced-colors:bg-[Highlight]" }) }), _jsx(SliderThumb, { "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, className: thumb })] }), output ? (_jsx(SliderOutput, { className: "w-14 shrink-0 text-right text-meta text-ink tabular-nums" })) : null] }) }));
|
|
46
46
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { ComboBox, type ComboBoxProps } from "./ComboBox.js";
|
|
2
|
+
export { KanbanState, type KanbanStateValue, } from "./KanbanState.js";
|
|
2
3
|
export { type ChoiceOption, type SegmentOption, } from "./options.js";
|
|
4
|
+
export { Priority, type PriorityProps } from "./Priority.js";
|
|
3
5
|
export { RadioGroup, type RadioGroupProps, } from "./RadioGroup.js";
|
|
4
6
|
export { SearchField, type SearchFieldProps, } from "./SearchField.js";
|
|
5
7
|
export { SegmentedControl, type SegmentedControlProps, } from "./SegmentedControl.js";
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
(RadioGroup, SegmentedControl), prender o apagar (Switch), un valor en un rango (Slider)
|
|
3
3
|
y buscar un texto suelto (SearchField). El valor es siempre un dato plano. */
|
|
4
4
|
export { ComboBox } from "./ComboBox.js";
|
|
5
|
+
export { KanbanState, } from "./KanbanState.js";
|
|
5
6
|
export {} from "./options.js";
|
|
7
|
+
export { Priority } from "./Priority.js";
|
|
6
8
|
export { RadioGroup, } from "./RadioGroup.js";
|
|
7
9
|
export { SearchField, } from "./SearchField.js";
|
|
8
10
|
export { SegmentedControl, } from "./SegmentedControl.js";
|
|
@@ -3,8 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { parseDate } from "@internationalized/date";
|
|
4
4
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
5
5
|
import { focusRing, tv } from "../../foundations/variants.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { listOptionStyles } from "../field/aria.js";
|
|
7
|
+
import { Popover } from "../overlay/Popover.js";
|
|
8
|
+
import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
|
|
9
|
+
import { Button as AriaButton, Calendar as AriaCalendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarHeading, CalendarMonthPicker, CalendarYearPicker, ListBox, ListBoxItem, RangeCalendar as AriaRangeCalendar, Select as AriaSelect, SelectValue, useLocale, } from "react-aria-components";
|
|
8
10
|
/**
|
|
9
11
|
* The month grid — on its own, or inside the popover of a date picker.
|
|
10
12
|
*
|
|
@@ -12,15 +14,37 @@ import { Button as AriaButton, Calendar as AriaCalendar, CalendarCell, CalendarG
|
|
|
12
14
|
* implements the grid pattern (arrows move a day, PageUp/PageDown a month, Home/End the
|
|
13
15
|
* week). The selected day is the system's `primary`, not the gold: the gold is for the ONE
|
|
14
16
|
* action of a screen, and a calendar is not an action.
|
|
17
|
+
*
|
|
18
|
+
* The month and the year of the heading are two small selects: a birth date in 1987 or a
|
|
19
|
+
* contract signed in 2019 is two picks away, not eighty presses of «previous month». They
|
|
20
|
+
* are ALWAYS there, not behind a prop, because they cost nothing to the look — the words of
|
|
21
|
+
* the heading become the buttons, same weight, same color, the arrows where they were — and
|
|
22
|
+
* a prop is the one every app forgets on the one form (the birth date) that needed it.
|
|
15
23
|
*/
|
|
16
24
|
const navButton = tv({
|
|
17
25
|
extend: focusRing,
|
|
18
26
|
base: "inline-flex size-(--size-control-sm) items-center justify-center rounded-control text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink pressed:bg-ink/10 disabled:text-line-strong",
|
|
19
27
|
});
|
|
20
|
-
|
|
28
|
+
const jumpTrigger = tv({
|
|
29
|
+
extend: focusRing,
|
|
30
|
+
base: "inline-flex h-(--size-control-sm) items-center gap-1 rounded-control px-1.5 text-body font-semibold text-ink transition-colors duration-(--duration-instant) hover:bg-ink/6 pressed:bg-ink/10",
|
|
31
|
+
});
|
|
32
|
+
/* A word of the heading that opens a list. `first-letter` needs a block box: Spanish month
|
|
33
|
+
names are lower case, and a heading that starts «octubre» reads like a typo. */
|
|
34
|
+
function Jump({ "aria-label": label, value, onChange, items }) {
|
|
35
|
+
return (_jsxs(AriaSelect, { "aria-label": label, value: value, onChange: onChange, children: [_jsxs(AriaButton, { className: jumpTrigger, children: [_jsx(SelectValue, { className: "inline-block first-letter:uppercase", children: ({ selectedText }) => selectedText }), _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-sm) text-muted", strokeWidth: 2.5 })] }), _jsx(Popover, { placement: "bottom", className: "min-w-(--trigger-width)", children: _jsx(ListBox, { items: items, className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.id, textValue: item.formatted, className: listOptionStyles, children: _jsx("span", { className: "block tabular-nums first-letter:uppercase", children: item.formatted }) })) }) })] }));
|
|
36
|
+
}
|
|
37
|
+
/* A century around the shown year: wide enough for a birth date or an old contract in one
|
|
38
|
+
pick, and `min`/`max` clip it (a birth date with `max` = today lists back from today).
|
|
39
|
+
The list opens scrolled to the shown year and typing «1987» jumps to it. */
|
|
40
|
+
const YEARS_LISTED = 100;
|
|
41
|
+
/** `jump` turns the heading into the month and year selects. Off only for two months side
|
|
42
|
+
* by side: their heading names a span («octubre – noviembre de 2026») that one month
|
|
43
|
+
* select cannot stand for. */
|
|
44
|
+
function Header({ jump }) {
|
|
21
45
|
const { direction } = useLocale();
|
|
22
46
|
const [Prev, Next] = direction === "rtl" ? [ChevronRight, ChevronLeft] : [ChevronLeft, ChevronRight];
|
|
23
|
-
return (_jsxs("header", { className: "mb-2 flex items-center gap-1", children: [_jsx(AriaButton, { slot: "previous", className: navButton, children: _jsx(Prev, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) }), _jsx(CalendarHeading, { className: "flex-1 text-center text-body font-semibold text-ink first-letter:uppercase" }), _jsx(AriaButton, { slot: "next", className: navButton, children: _jsx(Next, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }));
|
|
47
|
+
return (_jsxs("header", { className: "mb-2 flex items-center gap-1", children: [_jsx(AriaButton, { slot: "previous", className: navButton, children: _jsx(Prev, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) }), jump ? (_jsxs("div", { className: "flex flex-1 items-center justify-center", children: [_jsx(CalendarMonthPicker, { format: "long", children: (aria) => _jsx(Jump, { ...aria }) }), _jsx(CalendarYearPicker, { visibleYears: YEARS_LISTED, children: (aria) => _jsx(Jump, { ...aria }) })] })) : (_jsx(CalendarHeading, { className: "flex-1 text-center text-body font-semibold text-ink first-letter:uppercase" })), _jsx(AriaButton, { slot: "next", className: navButton, children: _jsx(Next, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }));
|
|
24
48
|
}
|
|
25
49
|
function WeekDays() {
|
|
26
50
|
return (_jsx(CalendarGridHeader, { children: (day) => (_jsx(CalendarHeaderCell, { className: "pb-1 text-micro font-medium text-muted", children: day })) }));
|
|
@@ -70,7 +94,7 @@ export function Calendar({ value, onChange, min, max, isUnavailable, ...rest })
|
|
|
70
94
|
? undefined
|
|
71
95
|
: (date) => isUnavailable(date.toString()),
|
|
72
96
|
});
|
|
73
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaCalendar, { ...rest, ...props, className: "w-fit", children: [_jsx(Header, {}), _jsxs(CalendarGrid, { className: "border-separate border-spacing-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => _jsx(CalendarCell, { date: date, className: dayCell }) })] })] }) }));
|
|
97
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaCalendar, { ...rest, ...props, className: "w-fit", children: [_jsx(Header, { jump: true }), _jsxs(CalendarGrid, { className: "border-separate border-spacing-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => _jsx(CalendarCell, { date: date, className: dayCell }) })] })] }) }));
|
|
74
98
|
}
|
|
75
99
|
const rangeDay = tv({
|
|
76
100
|
extend: focusRing,
|
|
@@ -98,7 +122,7 @@ export function RangeCalendar({ months = 1, value, onChange, min, max, ...rest }
|
|
|
98
122
|
minValue: iso(min) ?? undefined,
|
|
99
123
|
maxValue: iso(max) ?? undefined,
|
|
100
124
|
});
|
|
101
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaRangeCalendar, { ...rest, ...props, visibleDuration: { months }, className: "w-fit", children: [_jsx(Header, {}), _jsx("div", { className: "flex gap-4", children: Array.from({ length: months }, (_, i) => (_jsxs(CalendarGrid, { offset: { months: i }, className: "border-separate border-spacing-y-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => (_jsx(CalendarCell, { date: date, className: "group size-(--size-control-md) cursor-default text-meta tabular-nums outside-month:invisible selected:bg-primary/10 selection-start:rounded-s-control selection-end:rounded-e-control", children: ({ formattedDate, isSelected, isSelectionStart, isSelectionEnd, isFocusVisible, isDisabled, }) => (_jsx("span", { className: rangeDay({
|
|
125
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaRangeCalendar, { ...rest, ...props, visibleDuration: { months }, className: "w-fit", children: [_jsx(Header, { jump: months === 1 }), _jsx("div", { className: "flex gap-4", children: Array.from({ length: months }, (_, i) => (_jsxs(CalendarGrid, { offset: { months: i }, className: "border-separate border-spacing-y-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => (_jsx(CalendarCell, { date: date, className: "group size-(--size-control-md) cursor-default text-meta tabular-nums outside-month:invisible selected:bg-primary/10 selection-start:rounded-s-control selection-end:rounded-e-control", children: ({ formattedDate, isSelected, isSelectionStart, isSelectionEnd, isFocusVisible, isDisabled, }) => (_jsx("span", { className: rangeDay({
|
|
102
126
|
position: isSelected && (isSelectionStart || isSelectionEnd)
|
|
103
127
|
? "cap"
|
|
104
128
|
: isSelected
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type FormMode } from "../field/mode.js";
|
|
2
|
+
/** The typed segments inside the field box — shared with `TimeField` and `DateTimePicker`
|
|
3
|
+
* (internal: not in the family barrel). */
|
|
4
|
+
export declare function Segments({ slot }: {
|
|
5
|
+
slot?: "start" | "end";
|
|
6
|
+
}): import("react").JSX.Element;
|
|
2
7
|
type Shared = {
|
|
3
8
|
id?: string;
|
|
4
9
|
describedBy?: string;
|
|
@@ -7,17 +12,20 @@ type Shared = {
|
|
|
7
12
|
/** The earliest and latest day that can be chosen, as ISO dates. */
|
|
8
13
|
min?: string;
|
|
9
14
|
max?: string;
|
|
15
|
+
/** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
|
|
16
|
+
* Inside a field, the field's label names it. */
|
|
17
|
+
"aria-label"?: string;
|
|
10
18
|
/** Forces the mode. By default it inherits the form's: in read mode it draws the value. */
|
|
11
19
|
mode?: FormMode;
|
|
12
20
|
};
|
|
13
21
|
/** A date typed by segments, without a calendar: for a form where dates are known by heart
|
|
14
22
|
* (a birth date, a contract date). */
|
|
15
|
-
export declare function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
|
|
23
|
+
export declare function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, "aria-label": ariaLabel, }: Shared & {
|
|
16
24
|
value: string | null;
|
|
17
25
|
onChange?: (next: string | null) => void;
|
|
18
26
|
}): import("react").JSX.Element;
|
|
19
27
|
/** A date typed by segments, or picked on a calendar from the button. */
|
|
20
|
-
export declare function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
|
|
28
|
+
export declare function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, "aria-label": ariaLabel, }: Shared & {
|
|
21
29
|
value: string | null;
|
|
22
30
|
onChange?: (next: string | null) => void;
|
|
23
31
|
}): import("react").JSX.Element;
|
|
@@ -27,7 +35,7 @@ export type DateRangeValue = {
|
|
|
27
35
|
};
|
|
28
36
|
/** A span of days — a period of a report, the validity of a contract — typed or picked on
|
|
29
37
|
* two months at once. */
|
|
30
|
-
export declare function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
|
|
38
|
+
export declare function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, "aria-label": ariaLabel, }: Shared & {
|
|
31
39
|
value: DateRangeValue | null;
|
|
32
40
|
onChange?: (next: DateRangeValue | null) => void;
|
|
33
41
|
}): import("react").JSX.Element;
|
|
@@ -35,7 +35,9 @@ const segment = tv({
|
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
/** The typed segments inside the field box — shared with `TimeField` and `DateTimePicker`
|
|
39
|
+
* (internal: not in the family barrel). */
|
|
40
|
+
export function Segments({ slot }) {
|
|
39
41
|
return (_jsx(DateInput, { slot: slot, className: "flex min-w-0 items-center px-2.5 whitespace-nowrap", children: (part) => _jsx(DateSegment, { segment: part, className: segment }) }));
|
|
40
42
|
}
|
|
41
43
|
function parse(iso) {
|
|
@@ -50,7 +52,9 @@ function parse(iso) {
|
|
|
50
52
|
}
|
|
51
53
|
function useReadDate() {
|
|
52
54
|
const copy = useUiCopy();
|
|
53
|
-
|
|
55
|
+
/* `toDate("UTC")` is midnight in UTC, so the formatter has to read it in UTC too: in the
|
|
56
|
+
zone of the machine, anywhere west of Greenwich, the 30th was drawn as the 29th. */
|
|
57
|
+
const format = new DateFormatter(copy.locale, { dateStyle: "medium", timeZone: "UTC" });
|
|
54
58
|
return (iso) => {
|
|
55
59
|
const date = parse(iso);
|
|
56
60
|
return date === null ? "" : format.format(date.toDate("UTC"));
|
|
@@ -58,7 +62,7 @@ function useReadDate() {
|
|
|
58
62
|
}
|
|
59
63
|
/** A date typed by segments, without a calendar: for a form where dates are known by heart
|
|
60
64
|
* (a birth date, a contract date). */
|
|
61
|
-
export function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
|
|
65
|
+
export function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, "aria-label": ariaLabel, }) {
|
|
62
66
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
63
67
|
id: ownId,
|
|
64
68
|
describedBy: ownDescribedBy,
|
|
@@ -67,10 +71,10 @@ export function DateField({ value, onChange, id: ownId, describedBy: ownDescribe
|
|
|
67
71
|
const read = useReadDate();
|
|
68
72
|
if (useFormMode(mode) === "read")
|
|
69
73
|
return _jsx(ReadValue, { value: read(value) });
|
|
70
|
-
return (_jsx(AriaLocale, { children: _jsx(AriaDateField, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
|
|
74
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaDateField, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
|
|
71
75
|
}
|
|
72
76
|
/** A date typed by segments, or picked on a calendar from the button. */
|
|
73
|
-
export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
|
|
77
|
+
export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, "aria-label": ariaLabel, }) {
|
|
74
78
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
75
79
|
id: ownId,
|
|
76
80
|
describedBy: ownDescribedBy,
|
|
@@ -79,11 +83,11 @@ export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescrib
|
|
|
79
83
|
const read = useReadDate();
|
|
80
84
|
if (useFormMode(mode) === "read")
|
|
81
85
|
return _jsx(ReadValue, { value: read(value) });
|
|
82
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
|
|
86
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
|
|
83
87
|
}
|
|
84
88
|
/** A span of days — a period of a report, the validity of a contract — typed or picked on
|
|
85
89
|
* two months at once. */
|
|
86
|
-
export function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
|
|
90
|
+
export function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, "aria-label": ariaLabel, }) {
|
|
87
91
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
88
92
|
id: ownId,
|
|
89
93
|
describedBy: ownDescribedBy,
|
|
@@ -95,5 +99,5 @@ export function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDe
|
|
|
95
99
|
}
|
|
96
100
|
const start = parse(value?.start);
|
|
97
101
|
const end = parse(value?.end);
|
|
98
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaDateRangePicker, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: start === null || end === null ? null : { start, end }, onChange: (next) => onChange?.(next === null ? null : { start: next.start.toString(), end: next.end.toString() }), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, { slot: "start" }), _jsx("span", { "aria-hidden": true, className: "text-muted", children: "\u2013" }), _jsx(Segments, { slot: "end" }), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(RangeCalendar, { months: 2 }) }) })] }) }));
|
|
102
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaDateRangePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: start === null || end === null ? null : { start, end }, onChange: (next) => onChange?.(next === null ? null : { start: next.start.toString(), end: next.end.toString() }), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, { slot: "start" }), _jsx("span", { "aria-hidden": true, className: "text-muted", children: "\u2013" }), _jsx(Segments, { slot: "end" }), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(RangeCalendar, { months: 2 }) }) })] }) }));
|
|
99
103
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type FormMode } from "../field/mode.js";
|
|
2
|
+
type HourCycle = 12 | 24;
|
|
3
|
+
type Shared = {
|
|
4
|
+
id?: string;
|
|
5
|
+
describedBy?: string;
|
|
6
|
+
invalid?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
/** 12h or 24h. By default, the one of the copy's locale. Only the display: the value is
|
|
9
|
+
* always 24h. */
|
|
10
|
+
hourCycle?: HourCycle;
|
|
11
|
+
/** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
|
|
12
|
+
* Inside a field, the field's label names it. */
|
|
13
|
+
"aria-label"?: string;
|
|
14
|
+
/** Forces the mode. By default it inherits the form's: in read mode it draws the value. */
|
|
15
|
+
mode?: FormMode;
|
|
16
|
+
};
|
|
17
|
+
/** A time of day typed by segments — the hour of an appointment, the opening of a shift.
|
|
18
|
+
* The value is `"HH:MM"` in 24h, or `null`. */
|
|
19
|
+
export declare function TimeField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, hourCycle, mode, "aria-label": ariaLabel, }: Shared & {
|
|
20
|
+
value: string | null;
|
|
21
|
+
onChange?: (next: string | null) => void;
|
|
22
|
+
/** The earliest and latest time that can be typed, as `"HH:MM"`. */
|
|
23
|
+
min?: string;
|
|
24
|
+
max?: string;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
26
|
+
/** A day and its time — a delivery slot, an appointment: the day typed or picked on the
|
|
27
|
+
* calendar, the time typed after it. The value is a local `"YYYY-MM-DDTHH:MM"`, or `null`. */
|
|
28
|
+
export declare function DateTimePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, hourCycle, mode, "aria-label": ariaLabel, }: Shared & {
|
|
29
|
+
value: string | null;
|
|
30
|
+
onChange?: (next: string | null) => void;
|
|
31
|
+
/** The earliest and latest moment, as `"YYYY-MM-DDTHH:MM"` or a bare day `"YYYY-MM-DD"`. */
|
|
32
|
+
min?: string;
|
|
33
|
+
max?: string;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { DateFormatter, parseDateTime, parseTime, toCalendarDate, } from "@internationalized/date";
|
|
4
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
5
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
6
|
+
import { Calendar } from "./Calendar.js";
|
|
7
|
+
import { Segments } from "./DatePicker.js";
|
|
8
|
+
import { FieldButton, FieldGroup } from "../field/aria.js";
|
|
9
|
+
import { useFieldIds } from "../field/context.js";
|
|
10
|
+
import { ReadValue } from "../field/Field.js";
|
|
11
|
+
import { useFormMode } from "../field/mode.js";
|
|
12
|
+
import { Popover } from "../overlay/Popover.js";
|
|
13
|
+
import { CalendarDays } from "lucide-react";
|
|
14
|
+
import { DatePicker as AriaDatePicker, Dialog, TimeField as AriaTimeField, } from "react-aria-components";
|
|
15
|
+
/**
|
|
16
|
+
* A time of day, and a day with its time — an appointment, a delivery slot — as an ERP
|
|
17
|
+
* stores them: `"09:05"` and `"2026-10-30T09:05"`.
|
|
18
|
+
*
|
|
19
|
+
* Context: the same contract as `DatePicker` — a string in, a string out, converted here
|
|
20
|
+
* once. The time is 24h and without seconds because that is what a backend column of a
|
|
21
|
+
* time keeps; the time with a date is LOCAL and without a zone, because «the delivery is
|
|
22
|
+
* on the 30th at nine» is a wall-clock time where the warehouse is, not an instant: putting
|
|
23
|
+
* a zone on it is the backend's decision, not the field's.
|
|
24
|
+
*
|
|
25
|
+
* What the person SEES follows the locale of the copy: the hour cycle (12h with «p. m.» or
|
|
26
|
+
* 24h) is the one `Intl` gives that locale through `AriaLocale`. CLDR writes `es-BO` in 12h;
|
|
27
|
+
* a company that works in 24h passes `hourCycle={24}` — the value stays `"15:05"` either way.
|
|
28
|
+
*/
|
|
29
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
30
|
+
const hhmm = (time) => `${pad(time.hour)}:${pad(time.minute)}`;
|
|
31
|
+
/* `"09:05"` or `"09:05:00"` — a column of type time often comes with its seconds. */
|
|
32
|
+
function parseClock(value) {
|
|
33
|
+
if (value === null || value === undefined || value === "")
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return parseTime(value);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/* `"2026-10-30T09:05"`; a bare `"2026-10-30"` is that day at midnight, so a `min` can be a day. */
|
|
43
|
+
function parseStamp(value) {
|
|
44
|
+
if (value === null || value === undefined || value === "")
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
return parseDateTime(value);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/* React Aria's `toString()` adds the seconds; the contract is to the minute. */
|
|
54
|
+
function stampOf(value) {
|
|
55
|
+
const time = "hour" in value ? hhmm(value) : "00:00";
|
|
56
|
+
return `${toCalendarDate(value).toString()}T${time}`;
|
|
57
|
+
}
|
|
58
|
+
/* Read mode draws what the segments would: the locale's format, its hour cycle unless the
|
|
59
|
+
control forces one. Everything is formatted in UTC from UTC parts, so the zone of the
|
|
60
|
+
machine never moves the clock. */
|
|
61
|
+
function useReadClock(options, hourCycle) {
|
|
62
|
+
const copy = useUiCopy();
|
|
63
|
+
return new DateFormatter(copy.locale, {
|
|
64
|
+
...options,
|
|
65
|
+
timeZone: "UTC",
|
|
66
|
+
...(hourCycle === undefined ? {} : { hourCycle: hourCycle === 24 ? "h23" : "h12" }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** A time of day typed by segments — the hour of an appointment, the opening of a shift.
|
|
70
|
+
* The value is `"HH:MM"` in 24h, or `null`. */
|
|
71
|
+
export function TimeField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, hourCycle, mode, "aria-label": ariaLabel, }) {
|
|
72
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
73
|
+
id: ownId,
|
|
74
|
+
describedBy: ownDescribedBy,
|
|
75
|
+
invalid: ownInvalid,
|
|
76
|
+
});
|
|
77
|
+
const format = useReadClock({ timeStyle: "short" }, hourCycle);
|
|
78
|
+
if (useFormMode(mode) === "read") {
|
|
79
|
+
const time = parseClock(value);
|
|
80
|
+
return (_jsx(ReadValue, { value: time === null
|
|
81
|
+
? ""
|
|
82
|
+
: format.format(new Date(Date.UTC(1970, 0, 1, time.hour, time.minute))) }));
|
|
83
|
+
}
|
|
84
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaTimeField, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parseClock(value), onChange: (next) => onChange?.(next === null ? null : hhmm(next)), minValue: parseClock(min) ?? undefined, maxValue: parseClock(max) ?? undefined, hourCycle: hourCycle, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
|
|
85
|
+
}
|
|
86
|
+
/** A day and its time — a delivery slot, an appointment: the day typed or picked on the
|
|
87
|
+
* calendar, the time typed after it. The value is a local `"YYYY-MM-DDTHH:MM"`, or `null`. */
|
|
88
|
+
export function DateTimePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, hourCycle, mode, "aria-label": ariaLabel, }) {
|
|
89
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
90
|
+
id: ownId,
|
|
91
|
+
describedBy: ownDescribedBy,
|
|
92
|
+
invalid: ownInvalid,
|
|
93
|
+
});
|
|
94
|
+
const format = useReadClock({ dateStyle: "medium", timeStyle: "short" }, hourCycle);
|
|
95
|
+
if (useFormMode(mode) === "read") {
|
|
96
|
+
const stamp = parseStamp(value);
|
|
97
|
+
return _jsx(ReadValue, { value: stamp === null ? "" : format.format(stamp.toDate("UTC")) });
|
|
98
|
+
}
|
|
99
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parseStamp(value), onChange: (next) => onChange?.(next === null ? null : stampOf(next)), minValue: parseStamp(min) ?? undefined, maxValue: parseStamp(max) ?? undefined, granularity: "minute", hourCycle: hourCycle, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
|
|
100
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
/* LAS FECHAS — escritas por segmentos o elegidas en un calendario, con el orden
|
|
2
|
-
la copia. El valor es un ISO (`"2026-10-30"`
|
|
1
|
+
/* LAS FECHAS Y LAS HORAS — escritas por segmentos o elegidas en un calendario, con el orden
|
|
2
|
+
del idioma de la copia. El valor es un ISO (`"2026-10-30"`, `"09:05"`,
|
|
3
|
+
`"2026-10-30T09:05"`), lo que manda cualquier backend. */
|
|
3
4
|
export { Calendar, RangeCalendar, } from "./Calendar.js";
|
|
4
5
|
export { DateField, DatePicker, DateRangePicker, } from "./DatePicker.js";
|
|
6
|
+
export { DateTimePicker, TimeField } from "./DateTime.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
1
|
+
import { type PointerEvent, type ReactNode } from "react";
|
|
2
2
|
/** Where this dialog came from. It is drawn in the header, small. */
|
|
3
3
|
export interface DialogContext {
|
|
4
4
|
/** The category, in small caps: «factura», «cliente». It answers *what kind of
|
|
@@ -52,13 +52,19 @@ export interface DialogProps {
|
|
|
52
52
|
* It is the tooltip and the accessible name, and it is not drawn. */
|
|
53
53
|
expandLabel?: string;
|
|
54
54
|
footer?: ReactNode;
|
|
55
|
+
/** The window can be dragged by its header, to see what is behind it — the form a «search
|
|
56
|
+
* more» was opened from. Off on a phone, where the window is a bottom sheet. */
|
|
57
|
+
movable?: boolean;
|
|
55
58
|
children: ReactNode;
|
|
56
59
|
}
|
|
57
60
|
/** The inside of the window: React Aria's `Dialog` with the header, body and footer.
|
|
58
61
|
*
|
|
59
62
|
* Internal — exported only so a test can read its markup, which the modal (a portal)
|
|
60
63
|
* does not produce on the server. */
|
|
61
|
-
export declare function DialogSurface({ title, subtitle, context, onClose, onExpand, expandLabel, footer, children, }: Omit<DialogProps, "open" | "width"
|
|
64
|
+
export declare function DialogSurface({ title, subtitle, context, onClose, onExpand, expandLabel, footer, children, onDragStart, }: Omit<DialogProps, "open" | "width" | "movable"> & {
|
|
65
|
+
/** Set by a movable `Dialog`: the header is the handle. */
|
|
66
|
+
onDragStart?: (event: PointerEvent<HTMLElement>) => void;
|
|
67
|
+
}): import("react").JSX.Element;
|
|
62
68
|
/**
|
|
63
69
|
* What has to be answered before moving on: a short form that blocks the page and keeps
|
|
64
70
|
* saying what it is about.
|
|
@@ -68,7 +74,7 @@ export declare function DialogSurface({ title, subtitle, context, onClose, onExp
|
|
|
68
74
|
* stays mounted for the exit transition after `open` turns `false`, so the window fades
|
|
69
75
|
* out instead of vanishing.
|
|
70
76
|
*/
|
|
71
|
-
export declare function Dialog({ open, width, onClose, ...surface }: DialogProps): import("react").JSX.Element;
|
|
77
|
+
export declare function Dialog({ open, width, onClose, movable, ...surface }: DialogProps): import("react").JSX.Element;
|
|
72
78
|
export interface WizardStep {
|
|
73
79
|
id: string;
|
|
74
80
|
label: string;
|