@plantops/web-kit 0.1.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 (42) hide show
  1. package/README.md +88 -0
  2. package/dist/claims.d.ts +35 -0
  3. package/dist/claims.d.ts.map +1 -0
  4. package/dist/claims.js +76 -0
  5. package/dist/errors.d.ts +46 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +52 -0
  8. package/dist/grants-provider.d.ts +51 -0
  9. package/dist/grants-provider.d.ts.map +1 -0
  10. package/dist/grants-provider.js +46 -0
  11. package/dist/iam-provider.d.ts +95 -0
  12. package/dist/iam-provider.d.ts.map +1 -0
  13. package/dist/iam-provider.js +188 -0
  14. package/dist/index.d.ts +51 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +50 -0
  17. package/dist/plantops-provider.d.ts +48 -0
  18. package/dist/plantops-provider.d.ts.map +1 -0
  19. package/dist/plantops-provider.js +8 -0
  20. package/dist/require-auth.d.ts +45 -0
  21. package/dist/require-auth.d.ts.map +1 -0
  22. package/dist/require-auth.js +58 -0
  23. package/dist/scope-coverage.d.ts +54 -0
  24. package/dist/scope-coverage.d.ts.map +1 -0
  25. package/dist/scope-coverage.js +41 -0
  26. package/dist/token-store.d.ts +92 -0
  27. package/dist/token-store.d.ts.map +1 -0
  28. package/dist/token-store.js +144 -0
  29. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  30. package/dist/use-async.d.ts +43 -0
  31. package/dist/use-async.d.ts.map +1 -0
  32. package/dist/use-async.js +76 -0
  33. package/dist/use-navigation.d.ts +32 -0
  34. package/dist/use-navigation.d.ts.map +1 -0
  35. package/dist/use-navigation.js +24 -0
  36. package/dist/use-notices.d.ts +47 -0
  37. package/dist/use-notices.d.ts.map +1 -0
  38. package/dist/use-notices.js +60 -0
  39. package/dist/use-permission.d.ts +91 -0
  40. package/dist/use-permission.d.ts.map +1 -0
  41. package/dist/use-permission.js +48 -0
  42. package/package.json +49 -0
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+ /**
3
+ * "Fetch this when the screen opens, and tell me how it went."
4
+ *
5
+ * Every screen in every console does the same four things: call one endpoint,
6
+ * show a skeleton while it is in flight, show the failure if it fails, and
7
+ * offer a reload. Written per screen, it is four `useState`s and a race
8
+ * condition — the classic one where a slow first request resolves after a fast
9
+ * second and overwrites it.
10
+ *
11
+ * This is deliberately small. It is not a data-fetching library: no cache, no
12
+ * deduplication, no background revalidation. Those matter when many components
13
+ * request the same thing; an admin console's screens each own their data, and
14
+ * the two things that genuinely are shared — grants and navigation — have their
15
+ * own providers precisely so they are fetched once.
16
+ */
17
+ import * as React from 'react';
18
+ /**
19
+ * Runs `request` when `deps` change, and again on `reload()`.
20
+ *
21
+ * `request` is called with an `AbortSignal`; passing it on is optional but
22
+ * turns an abandoned request into a cancelled one. Results from a superseded
23
+ * call are dropped either way — the generation counter below is what makes the
24
+ * race impossible rather than merely unlikely.
25
+ */
26
+ export function useAsync(request, deps, options = {}) {
27
+ const { enabled = true } = options;
28
+ const [data, setData] = React.useState(undefined);
29
+ const [error, setError] = React.useState(null);
30
+ const [loading, setLoading] = React.useState(enabled);
31
+ const [nonce, setNonce] = React.useState(0);
32
+ // The latest `request` without making it a dependency: a caller writing an
33
+ // inline arrow — which is every caller — would otherwise re-fetch on every
34
+ // render. `deps` is the caller's explicit statement of what the request
35
+ // depends on, and it is the only thing that should trigger one.
36
+ const requestRef = React.useRef(request);
37
+ requestRef.current = request;
38
+ React.useEffect(() => {
39
+ if (!enabled) {
40
+ setLoading(false);
41
+ return;
42
+ }
43
+ const controller = new AbortController();
44
+ let current = true;
45
+ setLoading(true);
46
+ requestRef
47
+ .current(controller.signal)
48
+ .then((value) => {
49
+ if (!current)
50
+ return;
51
+ setData(value);
52
+ setError(null);
53
+ })
54
+ .catch((cause) => {
55
+ if (!current || controller.signal.aborted)
56
+ return;
57
+ setError(cause);
58
+ })
59
+ .finally(() => {
60
+ if (current)
61
+ setLoading(false);
62
+ });
63
+ return () => {
64
+ current = false;
65
+ controller.abort();
66
+ };
67
+ // `deps` is the caller's dependency list by design, and `request` is read
68
+ // through a ref so an inline arrow does not re-fetch on every render. The
69
+ // spread is what the rule cannot verify statically, and is the point.
70
+ // eslint-disable-next-line react-hooks/exhaustive-deps
71
+ }, [enabled, nonce, ...deps]);
72
+ const reload = React.useCallback(() => {
73
+ setNonce((value) => value + 1);
74
+ }, []);
75
+ return { data, error, loading, reload };
76
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The menu, from `GET /iam/navigation` (Doc 05 §4).
3
+ *
4
+ * A hook rather than a provider because a console fetches it once, in the shell
5
+ * layout, and passes the tree down as a prop to whatever renders it — the
6
+ * sidebar, a mobile drawer, a command palette. Wrapping it in a context would
7
+ * add an indirection for a single consumer.
8
+ *
9
+ * It is not cached here, for the reason `libs/iam-client/src/endpoints/
10
+ * navigation.ts` gives: the menu changes when the *catalog* changes as well as
11
+ * when grants change, it is fetched once per shell load rather than once per
12
+ * request, and there is no burst for a cache to absorb — only staleness to
13
+ * introduce. {@link UseNavigationResult.reload} exists for the platform admin
14
+ * who has just edited the catalog and wants to see it (Doc 09 §2.1).
15
+ */
16
+ import type { NavigationResponse, NavNodeDTO } from '@plantops/contracts';
17
+ export interface UseNavigationResult {
18
+ /** The response, once it lands. */
19
+ navigation: NavigationResponse | undefined;
20
+ /** The tree alone — `[]` while loading, and for a subject who may see nothing. */
21
+ tree: NavNodeDTO[];
22
+ loading: boolean;
23
+ error: unknown;
24
+ reload: () => void;
25
+ }
26
+ /**
27
+ * @param applicationId Narrows to one application's menu. Omit for the
28
+ * cross-application shell — one top-level node per enabled application
29
+ * (Doc 05 §4), which is what the admin console renders.
30
+ */
31
+ export declare function useNavigation(applicationId?: string): UseNavigationResult;
32
+ //# sourceMappingURL=use-navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-navigation.d.ts","sourceRoot":"","sources":["../src/use-navigation.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAK1E,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,UAAU,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC3C,kFAAkF;IAClF,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAoBzE"}
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+ import { useAuth, useIam } from './iam-provider';
3
+ import { useAsync } from './use-async';
4
+ /**
5
+ * @param applicationId Narrows to one application's menu. Omit for the
6
+ * cross-application shell — one top-level node per enabled application
7
+ * (Doc 05 §4), which is what the admin console renders.
8
+ */
9
+ export function useNavigation(applicationId) {
10
+ const client = useIam();
11
+ const { status, subject } = useAuth();
12
+ const state = useAsync(() => client.navigation.tree(applicationId === undefined ? {} : { applicationId }),
13
+ // Re-fetched when the subject changes: the menu is a projection of *their*
14
+ // grants, so serving the previous user's menu would be both wrong and
15
+ // alarming.
16
+ [client, subject?.id, subject?.sessionId, applicationId], { enabled: status === 'authenticated' });
17
+ return {
18
+ navigation: state.data,
19
+ tree: state.data?.tree ?? [],
20
+ loading: state.loading,
21
+ error: state.error,
22
+ reload: state.reload,
23
+ };
24
+ }
@@ -0,0 +1,47 @@
1
+ import * as React from 'react';
2
+ import { type DescribedError } from './errors';
3
+ export interface Notices {
4
+ /**
5
+ * Reports a failure and hands back what it was, so a caller that also wants
6
+ * to render it inline does not describe it twice.
7
+ */
8
+ error: (error: unknown, options?: {
9
+ title?: string;
10
+ }) => DescribedError;
11
+ success: (text: string) => void;
12
+ info: (text: string) => void;
13
+ /**
14
+ * The Doc 09 §4 notice: "after a role/binding change, surface that access
15
+ * updates may take a few seconds (cache invalidation, Doc 04 §7)".
16
+ *
17
+ * Every screen that grants, revokes, or changes what a role carries owes the
18
+ * admin this sentence. Without it, the admin checks immediately, sees the old
19
+ * access, and concludes the change did not save — the single most predictable
20
+ * misreading of a correctly-working cache.
21
+ */
22
+ accessChanged: (text?: string) => void;
23
+ /**
24
+ * "Are you sure?", resolving to what they chose.
25
+ *
26
+ * Here for the same reason `error` is: `Modal.confirm(…)` called statically
27
+ * renders outside `ConfigProvider`, so it arrives unthemed and, in dark mode,
28
+ * close to unreadable. `App.useApp()` gives the hooked version, and
29
+ * `PlantOpsThemeProvider` mounts the `App` it needs.
30
+ *
31
+ * A promise rather than an `onOk` callback because the thing a caller does
32
+ * next is almost always `await` — deactivate, then reload — and threading
33
+ * that through a callback turns one linear function into two.
34
+ */
35
+ confirm: (options: ConfirmOptions) => Promise<boolean>;
36
+ }
37
+ export interface ConfirmOptions {
38
+ title: string;
39
+ /** What the action does, and what it does *not* do. Worth writing. */
40
+ content?: React.ReactNode;
41
+ okText?: string;
42
+ cancelText?: string;
43
+ /** Renders the confirm button in the danger tone. */
44
+ danger?: boolean;
45
+ }
46
+ export declare function useNotices(): Notices;
47
+ //# sourceMappingURL=use-notices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-notices.d.ts","sourceRoot":"","sources":["../src/use-notices.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAE9D,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,cAAc,CAAC;IACxE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B;;;;;;;;OAQG;IACH,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;;;;;;;;;;OAWG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,UAAU,IAAI,OAAO,CAkFpC"}
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Feedback: what the console says after something succeeded, failed, or will
5
+ * take a moment to take effect.
6
+ *
7
+ * Goes through antd's `App.useApp()` hooks rather than the static
8
+ * `message.error(…)` functions, because the static ones render outside the
9
+ * React tree and therefore outside `ConfigProvider` — they come out unthemed,
10
+ * and in dark mode nearly unreadable. `PlantOpsThemeProvider` mounts the `App`
11
+ * these hooks need.
12
+ *
13
+ * ## Why an error toast is a *notification* and a success is a *message*
14
+ *
15
+ * A success is an acknowledgement: it needs a second of the user's attention
16
+ * and then should get out of the way. A failure needs to be read, may carry a
17
+ * request id worth copying, and must not vanish while the user is still working
18
+ * out what happened — so it stays until dismissed.
19
+ */
20
+ import { App } from 'antd';
21
+ import * as React from 'react';
22
+ import { describeError } from './errors';
23
+ export function useNotices() {
24
+ const { message, modal, notification } = App.useApp();
25
+ return React.useMemo(() => ({
26
+ error: (error, options) => {
27
+ const described = describeError(error);
28
+ notification.error({
29
+ message: options?.title ?? described.copy.title,
30
+ description: (_jsxs(_Fragment, { children: [_jsx("div", { children: described.copy.description }), described.details.length > 0 && (_jsx("ul", { style: { margin: '8px 0 0', paddingInlineStart: 20 }, children: described.details.map((detail) => (_jsxs("li", { children: [_jsx("strong", { children: detail.field }), " \u2014 ", detail.message] }, `${detail.field}:${detail.message}`))) })), described.requestId !== null && (_jsxs("div", { style: { marginBlockStart: 8, fontSize: 12, opacity: 0.75 }, children: ["Request ", described.requestId] }))] })),
31
+ duration: 0,
32
+ });
33
+ return described;
34
+ },
35
+ success: (text) => {
36
+ void message.success(text);
37
+ },
38
+ info: (text) => {
39
+ void message.info(text);
40
+ },
41
+ accessChanged: (text) => {
42
+ void message.info(text ??
43
+ 'Saved. Access changes can take a few seconds to reach every screen.', 4);
44
+ },
45
+ confirm: (options) => new Promise((resolve) => {
46
+ modal.confirm({
47
+ title: options.title,
48
+ content: options.content,
49
+ okText: options.okText ?? 'Confirm',
50
+ cancelText: options.cancelText ?? 'Cancel',
51
+ okButtonProps: options.danger === true ? { danger: true } : undefined,
52
+ onOk: () => resolve(true),
53
+ // Fires for the cancel button, the close icon and the mask click
54
+ // alike, so every way out of the dialog resolves rather than
55
+ // leaving the caller's `await` pending forever.
56
+ onCancel: () => resolve(false),
57
+ });
58
+ }),
59
+ }), [message, modal, notification]);
60
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Permission-aware controls (Doc 09 §4).
3
+ *
4
+ * "Buttons/actions the subject lacks permission for are hidden/disabled (UX
5
+ * only; server enforces)." Every screen session from Session 28 onward uses
6
+ * this hook, which is why it is here rather than in `admin-web`: gatepass and
7
+ * visitor hide their buttons with the same code.
8
+ *
9
+ * ## Hide or disable?
10
+ *
11
+ * Hide by default. A disabled button is a promise that something exists and
12
+ * could be reached, which invites a support ticket asking how; an absent one
13
+ * says nothing. Disable when the control's absence would make a layout
14
+ * incoherent — a toolbar with one button left, a table column of actions where
15
+ * some rows are actionable and some are not — and give it a `title` saying
16
+ * which permission is missing, because "greyed out and unexplained" is the
17
+ * worst of both.
18
+ *
19
+ * ## What this is not
20
+ *
21
+ * It is not authorisation. `usePermission` reads the resolved grants the server
22
+ * sent this browser; anything the browser can read, the browser can lie about.
23
+ * The server checks again on every call (`PermissionGuard`, Session 23) and a
24
+ * deep link into a hidden screen still 403s (Doc 09 §4). This exists so a user
25
+ * is not shown doors they cannot open — not so the doors are locked.
26
+ *
27
+ * ## While grants are loading
28
+ *
29
+ * `usePermission` answers `false` until they arrive. Showing a control and then
30
+ * removing it is worse than showing it a moment late, and a screen that wants
31
+ * to hold the whole thing back reads `loading` and renders a skeleton.
32
+ */
33
+ import type { PermissionKey, ScopePath } from '@plantops/contracts';
34
+ import * as React from 'react';
35
+ export interface PermissionQuery {
36
+ /**
37
+ * Restrict the question to a place in the org tree — "may I do this *here*".
38
+ *
39
+ * Omit for the broad question ("may I do this anywhere"), which is what a nav
40
+ * item or a list screen asks: Doc 05 §3 is explicit that visibility is
41
+ * permission-based, not scope-based, and that scope filters data *within* a
42
+ * screen.
43
+ */
44
+ scopePath?: ScopePath;
45
+ }
46
+ export interface PermissionApi {
47
+ /** Does the subject hold this permission (optionally, at this node)? */
48
+ can: (permission: PermissionKey, query?: PermissionQuery) => boolean;
49
+ /** Any of them — the OR a nav node's `menu_permission` rows express. */
50
+ canAny: (permissions: readonly PermissionKey[], query?: PermissionQuery) => boolean;
51
+ /** All of them — for a screen that genuinely needs two. */
52
+ canAll: (permissions: readonly PermissionKey[], query?: PermissionQuery) => boolean;
53
+ /**
54
+ * The minimal covering paths for a permission (Doc 04 §4.1).
55
+ *
56
+ * What a screen narrows its query by: "show me the gates I may check in at"
57
+ * rather than "show me every gate and let the server refuse most of them".
58
+ */
59
+ scopesFor: (permission: PermissionKey) => readonly ScopePath[];
60
+ /** True until the first resolve lands; every answer above is `false` meanwhile. */
61
+ loading: boolean;
62
+ }
63
+ /** The whole permission surface, for a screen asking several questions. */
64
+ export declare function usePermissions(): PermissionApi;
65
+ /**
66
+ * One permission, as a boolean — the common case.
67
+ *
68
+ * ```tsx
69
+ * const canCreate = usePermission('iam.client.user.create');
70
+ * {canCreate && <Button type="primary">Add user</Button>}
71
+ * ```
72
+ */
73
+ export declare function usePermission(permission: PermissionKey, query?: PermissionQuery): boolean;
74
+ export interface PermittedProps {
75
+ /** Held (at `scopePath`, if given) to render `children`. */
76
+ permission: PermissionKey | readonly PermissionKey[];
77
+ scopePath?: ScopePath;
78
+ children: React.ReactNode;
79
+ /** Rendered instead when the subject lacks it. Usually nothing. */
80
+ fallback?: React.ReactNode;
81
+ }
82
+ /**
83
+ * Renders `children` only if the subject holds the permission.
84
+ *
85
+ * The JSX form of {@link usePermission}, for a block of markup that would
86
+ * otherwise need a variable and a conditional. An array means "any of these",
87
+ * matching the OR semantics `menu_permission` uses for nav visibility
88
+ * (Doc 05 §3).
89
+ */
90
+ export declare function Permitted({ permission, scopePath, children, fallback, }: PermittedProps): React.ReactNode;
91
+ //# sourceMappingURL=use-permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-permission.d.ts","sourceRoot":"","sources":["../src/use-permission.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,GAAG,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC;IACrE,wEAAwE;IACxE,MAAM,EAAE,CAAC,WAAW,EAAE,SAAS,aAAa,EAAE,EAAE,KAAK,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC;IACpF,2DAA2D;IAC3D,MAAM,EAAE,CAAC,WAAW,EAAE,SAAS,aAAa,EAAE,EAAE,KAAK,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC;IACpF;;;;;OAKG;IACH,SAAS,EAAE,CAAC,UAAU,EAAE,aAAa,KAAK,SAAS,SAAS,EAAE,CAAC;IAC/D,mFAAmF;IACnF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,IAAI,aAAa,CAkB9C;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,aAAa,EACzB,KAAK,CAAC,EAAE,eAAe,GACtB,OAAO,CAGT;AAED,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,UAAU,EAAE,aAAa,GAAG,SAAS,aAAa,EAAE,CAAC;IACrD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EACxB,UAAU,EACV,SAAS,EACT,QAAQ,EACR,QAAe,GAChB,EAAE,cAAc,GAAG,KAAK,CAAC,SAAS,CAQlC"}
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ import * as React from 'react';
3
+ import { useGrants } from './grants-provider';
4
+ import { holdsPermission, holdsPermissionAt, permissionScopes, } from './scope-coverage';
5
+ /** The whole permission surface, for a screen asking several questions. */
6
+ export function usePermissions() {
7
+ const { grants, loading } = useGrants();
8
+ return React.useMemo(() => {
9
+ const can = (permission, query) => query?.scopePath === undefined
10
+ ? holdsPermission(grants, permission)
11
+ : holdsPermissionAt(grants, permission, query.scopePath);
12
+ return {
13
+ can,
14
+ canAny: (permissions, query) => permissions.some((key) => can(key, query)),
15
+ canAll: (permissions, query) => permissions.length > 0 && permissions.every((key) => can(key, query)),
16
+ scopesFor: (permission) => permissionScopes(grants, permission),
17
+ loading,
18
+ };
19
+ }, [grants, loading]);
20
+ }
21
+ /**
22
+ * One permission, as a boolean — the common case.
23
+ *
24
+ * ```tsx
25
+ * const canCreate = usePermission('iam.client.user.create');
26
+ * {canCreate && <Button type="primary">Add user</Button>}
27
+ * ```
28
+ */
29
+ export function usePermission(permission, query) {
30
+ const { can } = usePermissions();
31
+ return can(permission, query);
32
+ }
33
+ /**
34
+ * Renders `children` only if the subject holds the permission.
35
+ *
36
+ * The JSX form of {@link usePermission}, for a block of markup that would
37
+ * otherwise need a variable and a conditional. An array means "any of these",
38
+ * matching the OR semantics `menu_permission` uses for nav visibility
39
+ * (Doc 05 §3).
40
+ */
41
+ export function Permitted({ permission, scopePath, children, fallback = null, }) {
42
+ const { can, canAny } = usePermissions();
43
+ const query = scopePath === undefined ? undefined : { scopePath };
44
+ const allowed = Array.isArray(permission)
45
+ ? canAny(permission, query)
46
+ : can(permission, query);
47
+ return allowed ? children : fallback;
48
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@plantops/web-kit",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ ".": {
11
+ "@plantops/source": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "nx": {
18
+ "tags": [
19
+ "type:lib",
20
+ "scope:web"
21
+ ]
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^19.0.0",
25
+ "react-dom": "^19.0.0",
26
+ "antd": "^6.6.1"
27
+ },
28
+ "dependencies": {
29
+ "@plantops/contracts": "^0.1.0",
30
+ "@plantops/iam-client": "^0.1.0",
31
+ "@plantops/ui": "^0.1.0",
32
+ "tslib": "^2.3.0"
33
+ },
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/dileepraghumajji/plant-ops.git",
38
+ "directory": "libs/web-kit"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "devDependencies": {
47
+ "antd": "^6.6.1"
48
+ }
49
+ }