@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.
Files changed (80) hide show
  1. package/README.md +261 -40
  2. package/dist/core/cache.js +2 -2
  3. package/dist/core/copy.d.ts +24 -0
  4. package/dist/core/copy.js +30 -0
  5. package/dist/core/edit.d.ts +13 -0
  6. package/dist/core/edit.js +30 -0
  7. package/dist/core/entity.d.ts +34 -0
  8. package/dist/core/entity.js +25 -0
  9. package/dist/core/form.d.ts +21 -0
  10. package/dist/core/form.js +30 -0
  11. package/dist/core/index.d.ts +10 -3
  12. package/dist/core/index.js +9 -2
  13. package/dist/core/list.d.ts +64 -0
  14. package/dist/core/list.js +22 -0
  15. package/dist/core/lookup.d.ts +5 -2
  16. package/dist/core/lookup.js +6 -2
  17. package/dist/core/measure.d.ts +16 -0
  18. package/dist/core/measure.js +38 -0
  19. package/dist/core/memory.js +18 -4
  20. package/dist/core/relation.d.ts +28 -0
  21. package/dist/core/relation.js +30 -0
  22. package/dist/core/rest.js +13 -3
  23. package/dist/core/retry.d.ts +24 -0
  24. package/dist/core/retry.js +25 -0
  25. package/dist/core/summary.d.ts +50 -0
  26. package/dist/core/summary.js +54 -0
  27. package/dist/core/trail.d.ts +44 -0
  28. package/dist/core/trail.js +70 -0
  29. package/dist/core/transport.d.ts +13 -1
  30. package/dist/react/context.d.ts +12 -1
  31. package/dist/react/context.js +10 -2
  32. package/dist/react/index.d.ts +3 -1
  33. package/dist/react/index.js +3 -1
  34. package/dist/react/useCommand.d.ts +3 -1
  35. package/dist/react/useCommand.js +12 -4
  36. package/dist/react/useGroups.d.ts +40 -0
  37. package/dist/react/useGroups.js +69 -0
  38. package/dist/react/useListing.d.ts +21 -3
  39. package/dist/react/useListing.js +38 -3
  40. package/dist/react/useLookup.d.ts +30 -5
  41. package/dist/react/useLookup.js +56 -3
  42. package/dist/react/useRecord.d.ts +17 -2
  43. package/dist/react/useRecord.js +105 -9
  44. package/dist/react/useSummary.d.ts +15 -0
  45. package/dist/react/useSummary.js +22 -0
  46. package/dist/web/index.d.ts +9 -3
  47. package/dist/web/index.js +9 -3
  48. package/dist/web/list/EntityList.d.ts +27 -4
  49. package/dist/web/list/EntityList.js +162 -15
  50. package/dist/web/list/EntitySearchBar.d.ts +16 -8
  51. package/dist/web/list/EntitySearchBar.js +34 -4
  52. package/dist/web/list/groups.d.ts +15 -0
  53. package/dist/web/list/groups.js +73 -0
  54. package/dist/web/list/rowCommands.d.ts +41 -0
  55. package/dist/web/list/rowCommands.js +56 -0
  56. package/dist/web/record/EntityPicker.d.ts +42 -13
  57. package/dist/web/record/EntityPicker.js +53 -19
  58. package/dist/web/record/EntityTags.d.ts +38 -0
  59. package/dist/web/record/EntityTags.js +41 -0
  60. package/dist/web/record/RelatedList.d.ts +2 -1
  61. package/dist/web/record/RelatedList.js +12 -2
  62. package/dist/web/record/edit.d.ts +12 -5
  63. package/dist/web/record/edit.js +67 -8
  64. package/dist/web/record/opening.d.ts +26 -0
  65. package/dist/web/record/opening.js +10 -0
  66. package/dist/web/record/summary.d.ts +22 -0
  67. package/dist/web/record/summary.js +55 -0
  68. package/dist/web/workspace/SincproApp.d.ts +4 -1
  69. package/dist/web/workspace/SincproApp.js +2 -2
  70. package/dist/web/workspace/composer.d.ts +26 -4
  71. package/dist/web/workspace/composer.js +59 -2
  72. package/dist/web/workspace/open.d.ts +61 -0
  73. package/dist/web/workspace/open.js +62 -0
  74. package/dist/web/workspace/record.d.ts +30 -0
  75. package/dist/web/workspace/record.js +80 -0
  76. package/dist/web/workspace/skin.d.ts +0 -9
  77. package/dist/web/workspace/skin.js +25 -13
  78. package/dist/web/workspace/workspace.d.ts +31 -3
  79. package/dist/web/workspace/workspace.js +57 -4
  80. 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 { AnyRecord } from "@sincpro/criteria";
2
- import type { WorkspaceApi } from "@sincpro/web-composer";
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
- * const invoice = useRecord(invoices, resource.id);
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
- * const invoice = useRecord(invoices, resource.id);
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.status === "ready" && display !== ""
19
- ? textOf(record.meta, display, record.record[display], record.locale)
20
- : null;
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.1.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.0.0",
29
- "@sincpro/web-ui": "^0.2.0",
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.0.0",
38
- "@sincpro/web-ui": "^0.2.0",
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",