@protobase/ui 0.21.7 → 0.21.8
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/app/comments/comment-body.js +13 -0
- package/dist/app/comments/comment-composer.js +149 -0
- package/dist/app/comments/comments-section.js +78 -0
- package/dist/app/comments/mention-text.js +45 -0
- package/dist/app/comments/use-mentionable.js +18 -0
- package/dist/app/comments/use-post-comment.js +15 -0
- package/dist/app/comments/use-record-comments.js +24 -0
- package/dist/app/current-address.d.ts +4 -3
- package/dist/app/current-address.js +1 -0
- package/dist/app/displays/calendar-controls.js +69 -0
- package/dist/app/displays/calendar-day.js +33 -0
- package/dist/app/displays/calendar-display.js +174 -0
- package/dist/app/displays/calendar-dnd.js +27 -0
- package/dist/app/displays/calendar-event.js +72 -0
- package/dist/app/displays/calendar-range.js +125 -0
- package/dist/app/displays/list-displays.js +7 -1
- package/dist/app/displays/use-calendar-move.js +68 -0
- package/dist/app/displays/use-calendar-rows.js +35 -0
- package/dist/app/embedded-list/embedded-list.js +25 -21
- package/dist/app/embedded-list/embedded-row-targets.js +65 -0
- package/dist/app/list-page.js +16 -5
- package/dist/app/pages/use-run-action.js +8 -6
- package/dist/app/record-page.js +24 -10
- package/dist/app/related-opens.js +22 -0
- package/dist/app/related-section.js +4 -1
- package/dist/app/use-app-address.d.ts +3 -1
- package/dist/app/use-app-address.js +8 -9
- package/dist/auth/user-to-shell.js +3 -1
- package/dist/data/api-provider.d.ts +20 -0
- package/dist/data/query-keys.js +15 -0
- package/dist/data-grid/row-menu.js +8 -1
- package/dist/format/date.js +27 -1
- package/dist/package.js +1 -1
- package/dist/primitives/segmented.js +8 -3
- package/dist/primitives/textarea.d.ts +4 -3
- package/dist/primitives/textarea.js +9 -4
- package/dist/shell/assets/app-C01lazBK.js +69 -0
- package/dist/shell/assets/{export-worker-CVd5saLq.js → export-worker-BnOpS_4Q.js} +1 -1
- package/dist/shell/assets/index-CBGigFxb.css +2 -0
- package/dist/shell/assets/{index-CQc-60lQ.js → index-xN6_oZU2.js} +1 -1
- package/dist/shell/assets/{maplibre-BvtgTvfx.js → maplibre-Ba9RapjY.js} +1 -1
- package/dist/shell/assets/{markdown-content-CJ8g5iKO.js → markdown-content-BUxrbovf.js} +1 -1
- package/dist/shell/assets/{record-map-DSXYVoqB.js → record-map-14zotZ2s.js} +1 -1
- package/dist/shell/assets/{records-map-cU_FAK7w.js → records-map-wi-0pXqF.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-Bc0CoTXD.js → shared-protobase-ui-BVwhKZk9.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-BbVdHzEY.js +0 -69
- package/dist/shell/assets/index-C1fzTv_X.css +0 -2
package/dist/app/record-page.js
CHANGED
|
@@ -49,6 +49,7 @@ import { useLines } from "./use-lines.js";
|
|
|
49
49
|
import { useLinesDraft } from "./use-lines-draft.js";
|
|
50
50
|
import { GallerySection } from "./gallery/gallery-section.js";
|
|
51
51
|
import { HistorySection } from "./history/history-section.js";
|
|
52
|
+
import { CommentsSection } from "./comments/comments-section.js";
|
|
52
53
|
import { useRecordEditor } from "./use-record-editor.js";
|
|
53
54
|
import { useRelatedRecords } from "./use-related-records.js";
|
|
54
55
|
import { RelatedSection } from "./related-section.js";
|
|
@@ -62,21 +63,28 @@ import { enumLabel } from "@protobase/schema";
|
|
|
62
63
|
const hidden = (model, name) => name === model.tenant || model.primaryKey.includes(name);
|
|
63
64
|
/**
|
|
64
65
|
* The view's layout, or one section with the editable fields when the view has none, then History when the resource
|
|
65
|
-
* keeps it. The read view, which is all a user who may only read the resource gets,
|
|
66
|
-
* than what can be changed.
|
|
66
|
+
* keeps it and Comments when it takes them. The read view, which is all a user who may only read the resource gets,
|
|
67
|
+
* shows every field: the record rather than what can be changed.
|
|
67
68
|
*/
|
|
68
69
|
const layoutFor = (model, view, read) => {
|
|
69
70
|
if (view && view.layout.length > 0) return view.layout;
|
|
70
71
|
const shown = Object.values(model.fields).filter((field) => !hidden(model, field.name));
|
|
71
72
|
const editable = shown.filter((field) => !field.readOnly);
|
|
72
|
-
return [
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
kind: "section",
|
|
76
|
+
title: "Details",
|
|
77
|
+
fields: (editable.length > 0 && !read ? editable : shown).map((field) => field.name)
|
|
78
|
+
},
|
|
79
|
+
...model.history ? [{
|
|
80
|
+
kind: "history",
|
|
81
|
+
title: "History"
|
|
82
|
+
}] : [],
|
|
83
|
+
...model.comments ? [{
|
|
84
|
+
kind: "comments",
|
|
85
|
+
title: "Comments"
|
|
86
|
+
}] : []
|
|
87
|
+
];
|
|
80
88
|
};
|
|
81
89
|
const RecordPage = ({ resource, recordKey }) => {
|
|
82
90
|
const { resources } = useAdminMeta();
|
|
@@ -424,6 +432,12 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
424
432
|
recordKey,
|
|
425
433
|
etag: stored.etag
|
|
426
434
|
}, `history:${index}`)];
|
|
435
|
+
if (item.kind === "comments") return [/* @__PURE__ */ jsx(CommentsSection, {
|
|
436
|
+
item,
|
|
437
|
+
resource: model.name,
|
|
438
|
+
recordKey,
|
|
439
|
+
canWrite: auth.kind !== "visitor"
|
|
440
|
+
}, `comments:${index}`)];
|
|
427
441
|
if (item.kind !== "section") return [];
|
|
428
442
|
const visibleFields = item.fields.filter((name) => model.fields[name] && visible(name));
|
|
429
443
|
const facts = shown === "read" && item.presentation === "facts";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/app/related-opens.ts
|
|
2
|
+
const opensTo = (model, name, resources) => {
|
|
3
|
+
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
4
|
+
return field?.type === "relation" && field.relation !== void 0 && resources[field.relation.resource]?.primaryKey.length === 1;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The relation field whose record a related section's row opens: `item.open`, else the one relation in the listed
|
|
8
|
+
* resource's primary key besides `item.field`, when there is exactly one; none when the rows are records in their own right.
|
|
9
|
+
*/
|
|
10
|
+
const relatedOpens = (item, model, resources) => {
|
|
11
|
+
if (item.open !== void 0) return item.open !== item.field && opensTo(model, item.open, resources) ? item.open : void 0;
|
|
12
|
+
const candidates = model.primaryKey.filter((name) => name !== item.field && name !== model.tenant && opensTo(model, name, resources));
|
|
13
|
+
return candidates.length === 1 ? candidates[0] : void 0;
|
|
14
|
+
};
|
|
15
|
+
/** The key of the record a row's relation `name` points at; a list row holds a shown relation as `{ id, name }`. Undefined when empty. */
|
|
16
|
+
const relationTarget = (row, name) => {
|
|
17
|
+
const value = row[name];
|
|
18
|
+
const id = typeof value === "object" && value !== null && "id" in value ? value.id : value;
|
|
19
|
+
return typeof id === "string" || typeof id === "number" ? id : void 0;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { relatedOpens, relationTarget };
|
|
@@ -6,6 +6,7 @@ import { ErrorBanner } from "./error-banner.js";
|
|
|
6
6
|
import { listColumnNames } from "../live/columns-from-model.js";
|
|
7
7
|
import { useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
8
8
|
import { searchAll } from "../live/search-all.js";
|
|
9
|
+
import { relatedOpens } from "./related-opens.js";
|
|
9
10
|
import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-list.js";
|
|
10
11
|
import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
|
|
11
12
|
import { relatedHandOrder } from "./hand-order/hand-order.js";
|
|
@@ -82,6 +83,7 @@ const RelatedSection = ({ item, recordKey, owner }) => {
|
|
|
82
83
|
const permissions = usePermissions(item.resource);
|
|
83
84
|
const [sorting, setSorting] = useState(false);
|
|
84
85
|
if (!model) return null;
|
|
86
|
+
const opens = relatedOpens(item, model, resources);
|
|
85
87
|
const columns = item.columns ?? listColumnNames(model, view).filter((name) => name !== item.field);
|
|
86
88
|
const handOrder = relatedHandOrder(item, model, permissions);
|
|
87
89
|
const displaySwitch = /* @__PURE__ */ jsx(EmbeddedDisplaySwitch, {
|
|
@@ -124,7 +126,8 @@ const RelatedSection = ({ item, recordKey, owner }) => {
|
|
|
124
126
|
columns,
|
|
125
127
|
limit: item.limit ?? 10,
|
|
126
128
|
display,
|
|
127
|
-
empty: item.empty
|
|
129
|
+
empty: item.empty,
|
|
130
|
+
opens
|
|
128
131
|
}, match.filter)
|
|
129
132
|
});
|
|
130
133
|
};
|
|
@@ -3,13 +3,15 @@ import { AnchorHTMLAttributes } from "react";
|
|
|
3
3
|
import { AppAddress } from "@protobase/schema";
|
|
4
4
|
//#region src/app/use-app-address.d.ts
|
|
5
5
|
/**
|
|
6
|
-
* The app's address as logical values, and the ways to change it: `set` changes the list in place, `
|
|
6
|
+
* The app's address as logical values, and the ways to change it: `set` changes the list in place, `listTo` is the
|
|
7
|
+
* list after a change for a link to it, `go` moves to
|
|
7
8
|
* another address, `href` is an address for an `<a href>` and `open` follows such an href. Every component reads and
|
|
8
9
|
* writes the address through this hook, never the query string or a path of its own.
|
|
9
10
|
*/
|
|
10
11
|
export declare const useAppAddress: () => {
|
|
11
12
|
address: CurrentAddress | undefined;
|
|
12
13
|
set: (change: ListChange) => void;
|
|
14
|
+
listTo: (change: ListChange) => AppAddress;
|
|
13
15
|
go: (to: AppAddress, options?: {
|
|
14
16
|
replace?: boolean;
|
|
15
17
|
}) => void;
|
|
@@ -7,7 +7,8 @@ import { jsx } from "react/jsx-runtime";
|
|
|
7
7
|
import { appPath, readAppPath } from "@protobase/schema";
|
|
8
8
|
//#region src/app/use-app-address.tsx
|
|
9
9
|
/**
|
|
10
|
-
* The app's address as logical values, and the ways to change it: `set` changes the list in place, `
|
|
10
|
+
* The app's address as logical values, and the ways to change it: `set` changes the list in place, `listTo` is the
|
|
11
|
+
* list after a change for a link to it, `go` moves to
|
|
11
12
|
* another address, `href` is an address for an `<a href>` and `open` follows such an href. Every component reads and
|
|
12
13
|
* writes the address through this hook, never the query string or a path of its own.
|
|
13
14
|
*/
|
|
@@ -32,16 +33,14 @@ const useAppAddress = () => {
|
|
|
32
33
|
]);
|
|
33
34
|
const address = useMemo(() => currentAddress(read, defaults), [read, defaults]);
|
|
34
35
|
const go = useCallback((to, options) => navigate(appPath(to), options), [navigate]);
|
|
36
|
+
const listTo = useCallback((change) => {
|
|
37
|
+
if (read?.kind !== "list") throw new Error("listTo and set change the list the app shows; use go to move to another address");
|
|
38
|
+
return changedList(read, change, defaults(read.resource));
|
|
39
|
+
}, [read, defaults]);
|
|
35
40
|
return {
|
|
36
41
|
address,
|
|
37
|
-
set: useCallback((change) => {
|
|
38
|
-
|
|
39
|
-
go(changedList(read, change, defaults(read.resource)), { replace: true });
|
|
40
|
-
}, [
|
|
41
|
-
read,
|
|
42
|
-
defaults,
|
|
43
|
-
go
|
|
44
|
-
]),
|
|
42
|
+
set: useCallback((change) => go(listTo(change), { replace: true }), [listTo, go]),
|
|
43
|
+
listTo,
|
|
45
44
|
go,
|
|
46
45
|
href: useCallback((to) => appPath(to, basePath), [basePath]),
|
|
47
46
|
open: useCallback((target) => navigate(target.startsWith(`${basePath}/`) ? target.slice(basePath.length) : target), [basePath, navigate])
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//#region src/auth/user-to-shell.ts
|
|
2
|
+
/** The hue of someone's avatar, the same for the same id. */
|
|
2
3
|
const hueOf = (text) => [...text].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) % 360, 7);
|
|
4
|
+
/** The initials of someone's avatar: the first letters of the first two words of their name. */
|
|
3
5
|
const initialsOf = (name) => name.split(/[\s@.]+/).filter(Boolean).slice(0, 2).map((part) => part[0].toUpperCase()).join("");
|
|
4
6
|
const rolesOf = (user) => (user.role ?? "user").split(",").map((role) => role.trim());
|
|
5
7
|
/** The user holds the `admin` role, among others perhaps. */
|
|
@@ -18,4 +20,4 @@ const userToShell = (user) => {
|
|
|
18
20
|
};
|
|
19
21
|
};
|
|
20
22
|
//#endregion
|
|
21
|
-
export { isAdmin, userToShell };
|
|
23
|
+
export { hueOf, initialsOf, isAdmin, userToShell };
|
|
@@ -15,11 +15,21 @@ export declare const useClient: () => {
|
|
|
15
15
|
etag?: string;
|
|
16
16
|
}) => Promise<void>;
|
|
17
17
|
undelete: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
18
|
+
generate: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], action: string) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
18
19
|
reveal: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], field: string) => Promise<unknown>;
|
|
19
20
|
history: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
|
|
20
21
|
pageSize?: number;
|
|
21
22
|
pageToken?: string;
|
|
22
23
|
}) => Promise<import("@protobase/client").HistoryPage>;
|
|
24
|
+
comments: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
|
|
25
|
+
pageSize?: number;
|
|
26
|
+
pageToken?: string;
|
|
27
|
+
}) => Promise<import("@protobase/client").CommentPage>;
|
|
28
|
+
comment: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { body, mentions }: {
|
|
29
|
+
body: string;
|
|
30
|
+
mentions?: string[];
|
|
31
|
+
}) => Promise<import("@protobase/client").RecordComment>;
|
|
32
|
+
mentionable: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], query: string) => Promise<import("@protobase/client").MentionableUser[]>;
|
|
23
33
|
batchWrite: (ops: import("@protobase/client").BatchOp[]) => Promise<import("@protobase/client").BatchResult>;
|
|
24
34
|
facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
|
|
25
35
|
filter?: import("@protobase/client").FilterInput;
|
|
@@ -63,11 +73,21 @@ export declare const useResourceClient: (resource: string) => {
|
|
|
63
73
|
etag?: string;
|
|
64
74
|
}) => Promise<void>;
|
|
65
75
|
undelete: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[]) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
76
|
+
generate: <R extends import("@protobase/client").ResourceRef>(resource: R, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], action: string) => Promise<import("@protobase/client").Stored<import("@protobase/client").RecordOf<R>>>;
|
|
66
77
|
reveal: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], field: string) => Promise<unknown>;
|
|
67
78
|
history: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
|
|
68
79
|
pageSize?: number;
|
|
69
80
|
pageToken?: string;
|
|
70
81
|
}) => Promise<import("@protobase/client").HistoryPage>;
|
|
82
|
+
comments: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { pageSize, pageToken }?: {
|
|
83
|
+
pageSize?: number;
|
|
84
|
+
pageToken?: string;
|
|
85
|
+
}) => Promise<import("@protobase/client").CommentPage>;
|
|
86
|
+
comment: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], { body, mentions }: {
|
|
87
|
+
body: string;
|
|
88
|
+
mentions?: string[];
|
|
89
|
+
}) => Promise<import("@protobase/client").RecordComment>;
|
|
90
|
+
mentionable: (resource: import("@protobase/client").ResourceRef, key: import("@protobase/schema").KeyValue | readonly import("@protobase/schema").KeyValue[], query: string) => Promise<import("@protobase/client").MentionableUser[]>;
|
|
71
91
|
batchWrite: (ops: import("@protobase/client").BatchOp[]) => Promise<import("@protobase/client").BatchResult>;
|
|
72
92
|
facets: (resource: import("@protobase/client").ResourceRef, field: string, o?: {
|
|
73
93
|
filter?: import("@protobase/client").FilterInput;
|
package/dist/data/query-keys.js
CHANGED
|
@@ -66,6 +66,21 @@ const keys = {
|
|
|
66
66
|
"history",
|
|
67
67
|
pageSize
|
|
68
68
|
],
|
|
69
|
+
/** A record's comments; starts with `record`, so refreshing the record refreshes them too. */
|
|
70
|
+
comments: (resource, key) => [
|
|
71
|
+
"record",
|
|
72
|
+
resource,
|
|
73
|
+
key,
|
|
74
|
+
"comments"
|
|
75
|
+
],
|
|
76
|
+
/** The people a comment on a record may mention whose name contains `query`. */
|
|
77
|
+
mentionable: (resource, key, query) => [
|
|
78
|
+
"record",
|
|
79
|
+
resource,
|
|
80
|
+
key,
|
|
81
|
+
"mentionable",
|
|
82
|
+
query
|
|
83
|
+
],
|
|
69
84
|
facets: (resource, field, filter) => [
|
|
70
85
|
"facets",
|
|
71
86
|
resource,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { RouteLink } from "../primitives/route-link.js";
|
|
2
3
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
4
|
import { MoreHorizontal } from "lucide-react";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -70,7 +71,13 @@ const RowMenu = ({ row, actions }) => {
|
|
|
70
71
|
className: "fixed z-50 min-w-36 rounded-md border bg-background py-1 shadow-pop",
|
|
71
72
|
children: actions.map((action) => /* @__PURE__ */ jsx("li", {
|
|
72
73
|
role: "none",
|
|
73
|
-
children: /* @__PURE__ */ jsx(
|
|
74
|
+
children: action.link ? /* @__PURE__ */ jsx(RouteLink, {
|
|
75
|
+
role: "menuitem",
|
|
76
|
+
to: action.link(row),
|
|
77
|
+
onClick: () => setOpen(false),
|
|
78
|
+
className: cn("block w-full px-3 py-1.5 text-left hover:bg-muted", action.destructive && "text-danger-text"),
|
|
79
|
+
children: action.label
|
|
80
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
74
81
|
type: "button",
|
|
75
82
|
role: "menuitem",
|
|
76
83
|
className: cn("block w-full px-3 py-1.5 text-left hover:bg-muted", action.destructive && "text-danger-text"),
|
package/dist/format/date.js
CHANGED
|
@@ -16,9 +16,35 @@ const time = new Intl.DateTimeFormat("en-GB", {
|
|
|
16
16
|
hour12: false,
|
|
17
17
|
timeZone: "UTC"
|
|
18
18
|
});
|
|
19
|
+
const month = new Intl.DateTimeFormat("en-GB", {
|
|
20
|
+
month: "long",
|
|
21
|
+
year: "numeric",
|
|
22
|
+
timeZone: "UTC"
|
|
23
|
+
});
|
|
24
|
+
const weekday = new Intl.DateTimeFormat("en-GB", {
|
|
25
|
+
weekday: "short",
|
|
26
|
+
timeZone: "UTC"
|
|
27
|
+
});
|
|
28
|
+
const weekdayDate = new Intl.DateTimeFormat("en-GB", {
|
|
29
|
+
weekday: "long",
|
|
30
|
+
day: "numeric",
|
|
31
|
+
month: "short",
|
|
32
|
+
year: "numeric",
|
|
33
|
+
timeZone: "UTC"
|
|
34
|
+
});
|
|
19
35
|
const formatDate = (ms) => day.format(ms);
|
|
20
36
|
const formatDayShort = (ms) => dayShort.format(ms);
|
|
21
37
|
const formatDateTime = (ms) => `${day.format(ms)}, ${time.format(ms)}`;
|
|
38
|
+
/** "October 2026". */
|
|
39
|
+
const formatMonth = (ms) => month.format(ms);
|
|
40
|
+
/** "Mon". */
|
|
41
|
+
const formatWeekday = (ms) => weekday.format(ms);
|
|
42
|
+
/** "Monday, 12 Oct 2026". */
|
|
43
|
+
const formatWeekdayDate = (ms) => weekdayDate.format(ms);
|
|
44
|
+
/** "12–18 Oct 2026", "28 Dec 2026 – 3 Jan 2027": the days from `from` to `to`, both included. */
|
|
45
|
+
const formatDayRange = (from, to) => day.formatRange(from, to);
|
|
46
|
+
/** "09:30". */
|
|
47
|
+
const formatTime = (ms) => time.format(ms);
|
|
22
48
|
const formatAgo = (seconds) => {
|
|
23
49
|
if (seconds < 60) return `${seconds} s ago`;
|
|
24
50
|
if (seconds < 3600) return `${Math.floor(seconds / 60)} min ago`;
|
|
@@ -40,4 +66,4 @@ const formatRelativeTime = (seconds) => {
|
|
|
40
66
|
return relative.format(-Math.floor(seconds / size), unit);
|
|
41
67
|
};
|
|
42
68
|
//#endregion
|
|
43
|
-
export { formatAgo, formatDate, formatDateTime, formatDayShort, formatRelativeTime };
|
|
69
|
+
export { formatAgo, formatDate, formatDateTime, formatDayRange, formatDayShort, formatMonth, formatRelativeTime, formatTime, formatWeekday, formatWeekdayDate };
|
package/dist/package.js
CHANGED
|
@@ -11,10 +11,15 @@ const sizes = {
|
|
|
11
11
|
option: "px-3 text-body"
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
|
+
/** A segmented switch's classes, for a row of links that looks like one: the group, and each option, `active` the chosen. */
|
|
15
|
+
const segmentedClass = {
|
|
16
|
+
group: (size = "sm") => cn("inline-flex", sizes[size].group, "rounded-md bg-muted p-0.5"),
|
|
17
|
+
option: (active, size = "sm") => cn("inline-flex items-center gap-1.5 rounded", sizes[size].option, "font-medium transition-colors", active ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground")
|
|
18
|
+
};
|
|
14
19
|
const Segmented = ({ value, options, onChange, label, size = "sm", iconOnly = false }) => /* @__PURE__ */ jsx("span", {
|
|
15
20
|
role: "group",
|
|
16
21
|
"aria-label": label,
|
|
17
|
-
className:
|
|
22
|
+
className: segmentedClass.group(size),
|
|
18
23
|
children: options.map((option) => /* @__PURE__ */ jsxs("button", {
|
|
19
24
|
type: "button",
|
|
20
25
|
"aria-pressed": option.value === value,
|
|
@@ -23,9 +28,9 @@ const Segmented = ({ value, options, onChange, label, size = "sm", iconOnly = fa
|
|
|
23
28
|
title: option.label
|
|
24
29
|
},
|
|
25
30
|
onClick: () => onChange(option.value),
|
|
26
|
-
className:
|
|
31
|
+
className: segmentedClass.option(option.value === value, size),
|
|
27
32
|
children: [option.icon && /* @__PURE__ */ jsx(option.icon, { className: "size-3.5" }), !iconOnly && option.label]
|
|
28
33
|
}, option.value))
|
|
29
34
|
});
|
|
30
35
|
//#endregion
|
|
31
|
-
export { Segmented };
|
|
36
|
+
export { Segmented, segmentedClass };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { TextareaHTMLAttributes } from "react";
|
|
1
|
+
import { Ref, TextareaHTMLAttributes } from "react";
|
|
2
2
|
//#region src/primitives/textarea.d.ts
|
|
3
3
|
export type TextareaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value'> & {
|
|
4
4
|
value: string;
|
|
5
5
|
invalid?: boolean;
|
|
6
|
+
ref?: Ref<HTMLTextAreaElement>;
|
|
6
7
|
};
|
|
7
|
-
/** A text box of several lines that grows with its text instead of scrolling inside. */
|
|
8
|
-
export declare const Textarea: ({ invalid, className, rows, value, ...rest }: TextareaProps) => import("react").JSX.Element;
|
|
8
|
+
/** A text box of several lines that grows with its text instead of scrolling inside; `ref` reaches the box too. */
|
|
9
|
+
export declare const Textarea: ({ invalid, className, rows, value, ref: given, ...rest }: TextareaProps) => import("react").JSX.Element;
|
|
9
10
|
//#endregion
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { useFieldControl } from "./field-control.js";
|
|
3
|
-
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/primitives/textarea.tsx
|
|
6
6
|
const fit = (box) => {
|
|
7
7
|
box.style.height = "auto";
|
|
8
8
|
box.style.height = `${box.scrollHeight + box.offsetHeight - box.clientHeight}px`;
|
|
9
9
|
};
|
|
10
|
-
/** A text box of several lines that grows with its text instead of scrolling inside. */
|
|
11
|
-
const Textarea = ({ invalid, className, rows = 3, value, ...rest }) => {
|
|
10
|
+
/** A text box of several lines that grows with its text instead of scrolling inside; `ref` reaches the box too. */
|
|
11
|
+
const Textarea = ({ invalid, className, rows = 3, value, ref: given, ...rest }) => {
|
|
12
12
|
const ref = useRef(null);
|
|
13
|
+
const setRef = useCallback((box) => {
|
|
14
|
+
ref.current = box;
|
|
15
|
+
if (typeof given === "function") given(box);
|
|
16
|
+
else if (given) given.current = box;
|
|
17
|
+
}, [given]);
|
|
13
18
|
const control = useFieldControl();
|
|
14
19
|
useLayoutEffect(() => {
|
|
15
20
|
if (ref.current) fit(ref.current);
|
|
@@ -27,7 +32,7 @@ const Textarea = ({ invalid, className, rows = 3, value, ...rest }) => {
|
|
|
27
32
|
return () => observer.disconnect();
|
|
28
33
|
}, []);
|
|
29
34
|
return /* @__PURE__ */ jsx("textarea", {
|
|
30
|
-
ref,
|
|
35
|
+
ref: setRef,
|
|
31
36
|
rows,
|
|
32
37
|
value,
|
|
33
38
|
className: cn("block w-full resize-none overflow-hidden rounded-md border bg-background px-2.5 py-1.5 text-body text-foreground shadow-sm outline-none placeholder:text-faint-foreground focus:border-primary focus:ring-2 focus:ring-primary/20", invalid ? "border-danger" : "border-border-strong", className),
|