@sincpro/web-framework 0.1.0 → 0.2.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 +261 -40
- package/dist/core/cache.js +2 -2
- package/dist/core/copy.d.ts +24 -0
- package/dist/core/copy.js +30 -0
- package/dist/core/edit.d.ts +13 -0
- package/dist/core/edit.js +30 -0
- package/dist/core/entity.d.ts +34 -0
- package/dist/core/entity.js +25 -0
- package/dist/core/form.d.ts +21 -0
- package/dist/core/form.js +30 -0
- package/dist/core/index.d.ts +10 -3
- package/dist/core/index.js +9 -2
- package/dist/core/list.d.ts +64 -0
- package/dist/core/list.js +22 -0
- package/dist/core/lookup.d.ts +5 -2
- package/dist/core/lookup.js +6 -2
- package/dist/core/measure.d.ts +16 -0
- package/dist/core/measure.js +38 -0
- package/dist/core/memory.js +18 -4
- package/dist/core/relation.d.ts +28 -0
- package/dist/core/relation.js +30 -0
- package/dist/core/rest.js +13 -3
- package/dist/core/retry.d.ts +24 -0
- package/dist/core/retry.js +25 -0
- package/dist/core/summary.d.ts +50 -0
- package/dist/core/summary.js +54 -0
- package/dist/core/trail.d.ts +44 -0
- package/dist/core/trail.js +70 -0
- package/dist/core/transport.d.ts +13 -1
- package/dist/react/context.d.ts +12 -1
- package/dist/react/context.js +10 -2
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.js +3 -1
- package/dist/react/useCommand.d.ts +3 -1
- package/dist/react/useCommand.js +12 -4
- package/dist/react/useGroups.d.ts +40 -0
- package/dist/react/useGroups.js +69 -0
- package/dist/react/useListing.d.ts +21 -3
- package/dist/react/useListing.js +38 -3
- package/dist/react/useLookup.d.ts +30 -5
- package/dist/react/useLookup.js +56 -3
- package/dist/react/useRecord.d.ts +17 -2
- package/dist/react/useRecord.js +105 -9
- package/dist/react/useSummary.d.ts +15 -0
- package/dist/react/useSummary.js +22 -0
- package/dist/web/index.d.ts +9 -3
- package/dist/web/index.js +9 -3
- package/dist/web/list/EntityList.d.ts +27 -4
- package/dist/web/list/EntityList.js +162 -15
- package/dist/web/list/EntitySearchBar.d.ts +16 -8
- package/dist/web/list/EntitySearchBar.js +34 -4
- package/dist/web/list/groups.d.ts +15 -0
- package/dist/web/list/groups.js +73 -0
- package/dist/web/list/rowCommands.d.ts +41 -0
- package/dist/web/list/rowCommands.js +56 -0
- package/dist/web/record/EntityPicker.d.ts +42 -13
- package/dist/web/record/EntityPicker.js +53 -19
- package/dist/web/record/EntityTags.d.ts +38 -0
- package/dist/web/record/EntityTags.js +41 -0
- package/dist/web/record/RelatedList.d.ts +2 -1
- package/dist/web/record/RelatedList.js +12 -2
- package/dist/web/record/edit.d.ts +12 -5
- package/dist/web/record/edit.js +67 -8
- package/dist/web/record/opening.d.ts +26 -0
- package/dist/web/record/opening.js +10 -0
- package/dist/web/record/summary.d.ts +22 -0
- package/dist/web/record/summary.js +55 -0
- package/dist/web/workspace/SincproApp.d.ts +4 -1
- package/dist/web/workspace/SincproApp.js +2 -2
- package/dist/web/workspace/composer.d.ts +26 -4
- package/dist/web/workspace/composer.js +59 -2
- package/dist/web/workspace/open.d.ts +61 -0
- package/dist/web/workspace/open.js +62 -0
- package/dist/web/workspace/record.d.ts +30 -0
- package/dist/web/workspace/record.js +80 -0
- package/dist/web/workspace/skin.d.ts +0 -9
- package/dist/web/workspace/skin.js +25 -13
- package/dist/web/workspace/workspace.d.ts +31 -3
- package/dist/web/workspace/workspace.js +57 -4
- package/package.json +5 -5
|
@@ -11,21 +11,33 @@ import { AppWindow, ArrowLeft, Ellipsis, Eye, Maximize2, Minimize2, Minus, Panel
|
|
|
11
11
|
* `Peek` drawer, its `Dialog` for «discard what you typed?», its `Missing` where a view could not
|
|
12
12
|
* be drawn. `<SincproApp>` uses it by default; any piece can still be replaced on top of it.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* A lucide icon at the composer's size. The composer draws an icon as `<Icon />`, sized by the
|
|
16
|
+
* icon itself (its own glyphs are 16 px), and lucide's default is 24 px: unsized, every glyph
|
|
17
|
+
* overflowed its 22 px control — a record link's peek/side/window grew larger than the name.
|
|
18
|
+
*/
|
|
19
|
+
function sized(Icon) {
|
|
20
|
+
function Sized({ className, strokeWidth = 2 }) {
|
|
21
|
+
return _jsx(Icon, { size: 16, strokeWidth: strokeWidth, className: className, "aria-hidden": true });
|
|
22
|
+
}
|
|
23
|
+
Sized.displayName = Icon.displayName ?? "Icon";
|
|
24
|
+
return Sized;
|
|
25
|
+
}
|
|
14
26
|
export const webUiSkin = {
|
|
15
27
|
icons: {
|
|
16
|
-
more: Ellipsis,
|
|
17
|
-
expand: SquareArrowOutUpRight,
|
|
18
|
-
minimize: Minus,
|
|
19
|
-
maximize: Maximize2,
|
|
20
|
-
restore: Minimize2,
|
|
21
|
-
close: X,
|
|
22
|
-
left: PanelLeft,
|
|
23
|
-
right: PanelRight,
|
|
24
|
-
bottom: PanelBottom,
|
|
25
|
-
window: AppWindow,
|
|
26
|
-
back: ArrowLeft,
|
|
27
|
-
search: Search,
|
|
28
|
-
peek: Eye,
|
|
28
|
+
more: sized(Ellipsis),
|
|
29
|
+
expand: sized(SquareArrowOutUpRight),
|
|
30
|
+
minimize: sized(Minus),
|
|
31
|
+
maximize: sized(Maximize2),
|
|
32
|
+
restore: sized(Minimize2),
|
|
33
|
+
close: sized(X),
|
|
34
|
+
left: sized(PanelLeft),
|
|
35
|
+
right: sized(PanelRight),
|
|
36
|
+
bottom: sized(PanelBottom),
|
|
37
|
+
window: sized(AppWindow),
|
|
38
|
+
back: sized(ArrowLeft),
|
|
39
|
+
search: sized(Search),
|
|
40
|
+
peek: sized(Eye),
|
|
29
41
|
},
|
|
30
42
|
Peek: ({ title, onClose, actions, children }) => (_jsx(Peek, { open: true, title: title, onClose: onClose, actions: actions, children: children })),
|
|
31
43
|
Confirm: ({ open, title, detail, keep, discard, onAnswer }) => (_jsx(Dialog, { open: open, title: title, onClose: () => onAnswer(false), footer: _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { size: "sm", role: "secondary", onClick: () => onAnswer(true), children: discard }), _jsx(Button, { size: "sm", role: "primary", onClick: () => onAnswer(false), children: keep })] }), children: _jsx("p", { className: "text-ink", children: detail }) })),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
1
|
+
import { type AnyRecord } from "@sincpro/criteria";
|
|
2
|
+
import { type WorkspaceApi } from "@sincpro/web-composer";
|
|
3
|
+
import type { Entity } from "../../core/entity.js";
|
|
3
4
|
import type { RecordReading } from "../../react/useRecord.js";
|
|
4
5
|
/**
|
|
5
6
|
* A record told to the workspace it is open in: its real name and whether it holds unsaved work.
|
|
@@ -10,7 +11,34 @@ import type { RecordReading } from "../../react/useRecord.js";
|
|
|
10
11
|
* away what was typed. The record already knows both (its `display`, its `edit.dirty`), so the
|
|
11
12
|
* screen only has to connect them — one line.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
+
* A NEW record (opened with `entity.newRef()`) reads «Facturas · nuevo» until it exists; the
|
|
15
|
+
* moment its create answers, the surface is REPLACED by the record it became (`api.replace`):
|
|
16
|
+
* no crumb leads back to an empty form, and the window, peek or page it was in stays.
|
|
17
|
+
*
|
|
18
|
+
* const invoice = useRecord(invoices, resource.id ?? "");
|
|
14
19
|
* useRecordInWorkspace(api, invoice);
|
|
15
20
|
*/
|
|
16
21
|
export declare function useRecordInWorkspace<T extends AnyRecord>(api: WorkspaceApi, record: RecordReading<T>): void;
|
|
22
|
+
/**
|
|
23
|
+
* «Browse everything» of an `EntityPicker` as the composer's chooser: the entity's own list
|
|
24
|
+
* opens in a window and the row clicked there answers the field.
|
|
25
|
+
*
|
|
26
|
+
* <EntityPicker entity={customers} onChange={set} onBrowseAll={chooserIn(api, customers, set)} />
|
|
27
|
+
*/
|
|
28
|
+
export declare function chooserIn<T extends AnyRecord>(api: WorkspaceApi, entity: Entity<T>, onChange: (id: string) => void, options?: Parameters<WorkspaceApi["pick"]>[2]): (query?: unknown) => void;
|
|
29
|
+
/**
|
|
30
|
+
* The record of an entity the person is on right now — what a detail panel beside a list
|
|
31
|
+
* follows, without knowing the list.
|
|
32
|
+
*
|
|
33
|
+
* Context: a list publishes what is ticked (`api.select`) and the workspace knows what is
|
|
34
|
+
* focused; both are references (`domain:kind/id`). A panel that shows «the invoice I am on»
|
|
35
|
+
* only needs the last one of ITS entity: the last ticked, else the focused record. It redraws
|
|
36
|
+
* when either changes (composer's `useWorkspaceContext`), and is `undefined` when the person is
|
|
37
|
+
* on none — the panel says so.
|
|
38
|
+
*
|
|
39
|
+
* function Detail({ api }: ViewProps) {
|
|
40
|
+
* const id = useFollowed(api, invoices);
|
|
41
|
+
* const invoice = useRecord(invoices, id ?? ""); // or «tick an invoice»
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
export declare function useFollowed<T extends AnyRecord>(api: WorkspaceApi, entity: Entity<T>): string | undefined;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { nameOf } from "@sincpro/criteria";
|
|
3
|
+
import { useWorkspaceContext, } from "@sincpro/web-composer";
|
|
4
|
+
import { frameworkCopy } from "../../core/copy.js";
|
|
2
5
|
import { textOf } from "../../core/present.js";
|
|
3
6
|
import { useEffect } from "react";
|
|
4
7
|
/**
|
|
@@ -10,15 +13,28 @@ import { useEffect } from "react";
|
|
|
10
13
|
* away what was typed. The record already knows both (its `display`, its `edit.dirty`), so the
|
|
11
14
|
* screen only has to connect them — one line.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
16
|
+
* A NEW record (opened with `entity.newRef()`) reads «Facturas · nuevo» until it exists; the
|
|
17
|
+
* moment its create answers, the surface is REPLACED by the record it became (`api.replace`):
|
|
18
|
+
* no crumb leads back to an empty form, and the window, peek or page it was in stays.
|
|
19
|
+
*
|
|
20
|
+
* const invoice = useRecord(invoices, resource.id ?? "");
|
|
14
21
|
* useRecordInWorkspace(api, invoice);
|
|
15
22
|
*/
|
|
16
23
|
export function useRecordInWorkspace(api, record) {
|
|
17
24
|
const display = record.meta?.display || record.meta?.identity || "";
|
|
18
|
-
const title = record.
|
|
19
|
-
?
|
|
20
|
-
:
|
|
25
|
+
const title = record.isNew
|
|
26
|
+
? newTitle(record.meta, record.locale)
|
|
27
|
+
: record.status === "ready" && display !== ""
|
|
28
|
+
? textOf(record.meta, display, record.record[display], record.locale)
|
|
29
|
+
: null;
|
|
21
30
|
const dirty = record.edit.dirty;
|
|
31
|
+
const opened = api.self.id ?? "";
|
|
32
|
+
const { entity, id } = record;
|
|
33
|
+
// Created here: show what it became, in place of the form.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (opened === "" && id !== "")
|
|
36
|
+
api.replace(entity.ref(id));
|
|
37
|
+
}, [api, opened, entity, id]);
|
|
22
38
|
useEffect(() => {
|
|
23
39
|
api.setTitle(title);
|
|
24
40
|
}, [api, title]);
|
|
@@ -27,3 +43,40 @@ export function useRecordInWorkspace(api, record) {
|
|
|
27
43
|
return () => api.setDirty(false);
|
|
28
44
|
}, [api, dirty]);
|
|
29
45
|
}
|
|
46
|
+
function newTitle(meta, locale) {
|
|
47
|
+
return frameworkCopy().record.creating(nameOf(meta, locale) || "");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* «Browse everything» of an `EntityPicker` as the composer's chooser: the entity's own list
|
|
51
|
+
* opens in a window and the row clicked there answers the field.
|
|
52
|
+
*
|
|
53
|
+
* <EntityPicker entity={customers} onChange={set} onBrowseAll={chooserIn(api, customers, set)} />
|
|
54
|
+
*/
|
|
55
|
+
export function chooserIn(api, entity, onChange, options) {
|
|
56
|
+
// A button's `onClick` hands it the event: only a text is what was typed.
|
|
57
|
+
return (query) => api.pick(entity.chooserRef(typeof query === "string" ? query : undefined), (ref) => ref.id === undefined || onChange(ref.id), options);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The record of an entity the person is on right now — what a detail panel beside a list
|
|
61
|
+
* follows, without knowing the list.
|
|
62
|
+
*
|
|
63
|
+
* Context: a list publishes what is ticked (`api.select`) and the workspace knows what is
|
|
64
|
+
* focused; both are references (`domain:kind/id`). A panel that shows «the invoice I am on»
|
|
65
|
+
* only needs the last one of ITS entity: the last ticked, else the focused record. It redraws
|
|
66
|
+
* when either changes (composer's `useWorkspaceContext`), and is `undefined` when the person is
|
|
67
|
+
* on none — the panel says so.
|
|
68
|
+
*
|
|
69
|
+
* function Detail({ api }: ViewProps) {
|
|
70
|
+
* const id = useFollowed(api, invoices);
|
|
71
|
+
* const invoice = useRecord(invoices, id ?? ""); // or «tick an invoice»
|
|
72
|
+
* }
|
|
73
|
+
*/
|
|
74
|
+
export function useFollowed(api, entity) {
|
|
75
|
+
const { focused, selection } = useWorkspaceContext(api);
|
|
76
|
+
const mine = (ref) => ref != null &&
|
|
77
|
+
ref.domain === entity.domain &&
|
|
78
|
+
ref.kind === entity.kind &&
|
|
79
|
+
ref.id !== undefined;
|
|
80
|
+
const ticked = [...selection].reverse().find(mine);
|
|
81
|
+
return ticked?.id ?? (mine(focused) ? focused.id : undefined);
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sincpro/web-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The web client of sincpro_framework: entities, lists and records bound to the metadata the server publishes, drawn with @sincpro/web-ui and arranged with @sincpro/web-composer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"@sincpro/criteria": "^0.3.0",
|
|
27
27
|
"@sincpro/criteria-react": "^0.3.0",
|
|
28
|
-
"@sincpro/web-composer": "^1.
|
|
29
|
-
"@sincpro/web-ui": "^
|
|
28
|
+
"@sincpro/web-composer": "^1.4.0",
|
|
29
|
+
"@sincpro/web-ui": "^1.1.0",
|
|
30
30
|
"react": ">=19",
|
|
31
31
|
"react-dom": ">=19"
|
|
32
32
|
},
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"@eslint/js": "^10.0.1",
|
|
35
35
|
"@sincpro/criteria": "^0.3.0",
|
|
36
36
|
"@sincpro/criteria-react": "^0.3.0",
|
|
37
|
-
"@sincpro/web-composer": "^1.
|
|
38
|
-
"@sincpro/web-ui": "^
|
|
37
|
+
"@sincpro/web-composer": "^1.4.0",
|
|
38
|
+
"@sincpro/web-ui": "^1.1.0",
|
|
39
39
|
"@storybook/addon-a11y": "10.6.5",
|
|
40
40
|
"@storybook/addon-docs": "10.6.5",
|
|
41
41
|
"@storybook/addon-mcp": "10.6.5",
|