@plantops/ui 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.
- package/README.md +74 -0
- package/dist/data/data-table.d.ts +41 -0
- package/dist/data/data-table.d.ts.map +1 -0
- package/dist/data/data-table.js +31 -0
- package/dist/data/scope-tree-select.d.ts +51 -0
- package/dist/data/scope-tree-select.d.ts.map +1 -0
- package/dist/data/scope-tree-select.js +59 -0
- package/dist/data/scope-tree.d.ts +127 -0
- package/dist/data/scope-tree.d.ts.map +1 -0
- package/dist/data/scope-tree.js +163 -0
- package/dist/data/status-tag.d.ts +14 -0
- package/dist/data/status-tag.d.ts.map +1 -0
- package/dist/data/status-tag.js +50 -0
- package/dist/feedback/error-copy.d.ts +38 -0
- package/dist/feedback/error-copy.d.ts.map +1 -0
- package/dist/feedback/error-copy.js +97 -0
- package/dist/feedback/page-header.d.ts +17 -0
- package/dist/feedback/page-header.d.ts.map +1 -0
- package/dist/feedback/page-header.js +23 -0
- package/dist/feedback/state-panels.d.ts +50 -0
- package/dist/feedback/state-panels.d.ts.map +1 -0
- package/dist/feedback/state-panels.js +46 -0
- package/dist/forms/auth-layout.d.ts +12 -0
- package/dist/forms/auth-layout.d.ts.map +1 -0
- package/dist/forms/auth-layout.js +28 -0
- package/dist/forms/credentials-form.d.ts +34 -0
- package/dist/forms/credentials-form.d.ts.map +1 -0
- package/dist/forms/credentials-form.js +37 -0
- package/dist/icons/icon-registry.d.ts +25 -0
- package/dist/icons/icon-registry.d.ts.map +1 -0
- package/dist/icons/icon-registry.js +102 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/layout/app-shell.d.ts +23 -0
- package/dist/layout/app-shell.d.ts.map +1 -0
- package/dist/layout/app-shell.js +84 -0
- package/dist/layout/brand.d.ts +10 -0
- package/dist/layout/brand.d.ts.map +1 -0
- package/dist/layout/brand.js +48 -0
- package/dist/layout/nav-menu.d.ts +30 -0
- package/dist/layout/nav-menu.d.ts.map +1 -0
- package/dist/layout/nav-menu.js +75 -0
- package/dist/layout/nav-tree.d.ts +67 -0
- package/dist/layout/nav-tree.d.ts.map +1 -0
- package/dist/layout/nav-tree.js +101 -0
- package/dist/layout/user-menu.d.ts +20 -0
- package/dist/layout/user-menu.d.ts.map +1 -0
- package/dist/layout/user-menu.js +75 -0
- package/dist/theme/color-mode.d.ts +45 -0
- package/dist/theme/color-mode.d.ts.map +1 -0
- package/dist/theme/color-mode.js +81 -0
- package/dist/theme/theme-provider.d.ts +17 -0
- package/dist/theme/theme-provider.d.ts.map +1 -0
- package/dist/theme/theme-provider.js +65 -0
- package/dist/theme/theme.d.ts +23 -0
- package/dist/theme/theme.d.ts.map +1 -0
- package/dist/theme/theme.js +141 -0
- package/dist/theme/tokens.d.ts +130 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/theme/tokens.js +127 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each `IamErrorCode` says to a person (Doc 06 §2 → the screen).
|
|
3
|
+
*
|
|
4
|
+
* The server's `message` is written for an operator reading a log: accurate,
|
|
5
|
+
* terse, and occasionally about a constraint name. This is the other half —
|
|
6
|
+
* what an admin should be told and, more usefully, what they should do next.
|
|
7
|
+
* It lives in `libs/ui` because it is copy, and copy is part of the design
|
|
8
|
+
* language: the gatepass console must not invent a second, differently-worded
|
|
9
|
+
* explanation of a 403.
|
|
10
|
+
*
|
|
11
|
+
* Two rules the table obeys, both from the spec rather than from taste:
|
|
12
|
+
*
|
|
13
|
+
* - **A denial never speculates about what exists.** Doc 06 §2 requires that a
|
|
14
|
+
* 403 not reveal whether the target exists in another tenant, and copy is
|
|
15
|
+
* exactly where that leaks: "this role belongs to another client" would say
|
|
16
|
+
* out loud what the status code was careful not to.
|
|
17
|
+
* - **`ACCOUNT_LOCKED` gets its own words.** Doc 03 §8 makes 423 a distinct
|
|
18
|
+
* state with a distinct remedy — an administrator unlocks it; retrying the
|
|
19
|
+
* password never will — and Doc 09's login screen is required to say so
|
|
20
|
+
* rather than folding it into "sign-in failed".
|
|
21
|
+
*/
|
|
22
|
+
import { IamErrorCode } from '@plantops/contracts';
|
|
23
|
+
/** How prominently a failure should be shown. */
|
|
24
|
+
export type ErrorTone = 'error' | 'warning' | 'info';
|
|
25
|
+
export interface ErrorCopy {
|
|
26
|
+
/** Headline. A statement of what happened, never an apology. */
|
|
27
|
+
title: string;
|
|
28
|
+
/** One or two sentences: what it means, and what to do. */
|
|
29
|
+
description: string;
|
|
30
|
+
tone: ErrorTone;
|
|
31
|
+
/** Whether repeating the same request could plausibly succeed. */
|
|
32
|
+
retryable: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Copy for a request that never reached the API at all. */
|
|
35
|
+
export declare const TRANSPORT_ERROR_COPY: ErrorCopy;
|
|
36
|
+
/** The copy for a code. Unknown codes fall back to the transport wording. */
|
|
37
|
+
export declare function errorCopyFor(code: IamErrorCode | null | undefined): ErrorCopy;
|
|
38
|
+
//# sourceMappingURL=error-copy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-copy.d.ts","sourceRoot":"","sources":["../../src/feedback/error-copy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,iDAAiD;AACjD,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAErD,MAAM,WAAW,SAAS;IACxB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;CACpB;AAuED,4DAA4D;AAC5D,eAAO,MAAM,oBAAoB,EAAE,SAMjC,CAAC;AAEH,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,CAG7E"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each `IamErrorCode` says to a person (Doc 06 §2 → the screen).
|
|
3
|
+
*
|
|
4
|
+
* The server's `message` is written for an operator reading a log: accurate,
|
|
5
|
+
* terse, and occasionally about a constraint name. This is the other half —
|
|
6
|
+
* what an admin should be told and, more usefully, what they should do next.
|
|
7
|
+
* It lives in `libs/ui` because it is copy, and copy is part of the design
|
|
8
|
+
* language: the gatepass console must not invent a second, differently-worded
|
|
9
|
+
* explanation of a 403.
|
|
10
|
+
*
|
|
11
|
+
* Two rules the table obeys, both from the spec rather than from taste:
|
|
12
|
+
*
|
|
13
|
+
* - **A denial never speculates about what exists.** Doc 06 §2 requires that a
|
|
14
|
+
* 403 not reveal whether the target exists in another tenant, and copy is
|
|
15
|
+
* exactly where that leaks: "this role belongs to another client" would say
|
|
16
|
+
* out loud what the status code was careful not to.
|
|
17
|
+
* - **`ACCOUNT_LOCKED` gets its own words.** Doc 03 §8 makes 423 a distinct
|
|
18
|
+
* state with a distinct remedy — an administrator unlocks it; retrying the
|
|
19
|
+
* password never will — and Doc 09's login screen is required to say so
|
|
20
|
+
* rather than folding it into "sign-in failed".
|
|
21
|
+
*/
|
|
22
|
+
import { IamErrorCode } from '@plantops/contracts';
|
|
23
|
+
const COPY = Object.freeze({
|
|
24
|
+
[IamErrorCode.VALIDATION_FAILED]: {
|
|
25
|
+
title: 'Check the highlighted fields',
|
|
26
|
+
description: 'Some values were rejected. Correct them and submit again.',
|
|
27
|
+
tone: 'warning',
|
|
28
|
+
retryable: false,
|
|
29
|
+
},
|
|
30
|
+
[IamErrorCode.AUTH_REQUIRED]: {
|
|
31
|
+
title: 'Your session has ended',
|
|
32
|
+
description: 'Sign in again to continue.',
|
|
33
|
+
tone: 'info',
|
|
34
|
+
retryable: false,
|
|
35
|
+
},
|
|
36
|
+
[IamErrorCode.INVALID_CREDENTIALS]: {
|
|
37
|
+
title: 'Sign-in failed',
|
|
38
|
+
description: 'That email and password combination was not accepted for this client. Check the client, the address and the password, then try again.',
|
|
39
|
+
tone: 'error',
|
|
40
|
+
retryable: true,
|
|
41
|
+
},
|
|
42
|
+
[IamErrorCode.PERMISSION_DENIED]: {
|
|
43
|
+
title: 'You do not have access to this',
|
|
44
|
+
description: 'Your roles do not include the permission this screen needs. An administrator can grant it by binding you to a role that carries it.',
|
|
45
|
+
tone: 'warning',
|
|
46
|
+
retryable: false,
|
|
47
|
+
},
|
|
48
|
+
[IamErrorCode.SCOPE_DENIED]: {
|
|
49
|
+
title: 'Not permitted here',
|
|
50
|
+
description: 'You hold this permission, but not at a place in the organisation that covers what you are trying to reach. Access follows the org tree — ask for a binding higher up, or at the right node.',
|
|
51
|
+
tone: 'warning',
|
|
52
|
+
retryable: false,
|
|
53
|
+
},
|
|
54
|
+
[IamErrorCode.NOT_FOUND]: {
|
|
55
|
+
title: 'Not found',
|
|
56
|
+
description: 'This item no longer exists, or you cannot reach it.',
|
|
57
|
+
tone: 'info',
|
|
58
|
+
retryable: false,
|
|
59
|
+
},
|
|
60
|
+
[IamErrorCode.CONFLICT]: {
|
|
61
|
+
title: 'That conflicts with something already there',
|
|
62
|
+
description: 'A record with these details already exists, or the change would break a rule the server enforces. The details below say which.',
|
|
63
|
+
tone: 'warning',
|
|
64
|
+
retryable: false,
|
|
65
|
+
},
|
|
66
|
+
[IamErrorCode.ACCOUNT_LOCKED]: {
|
|
67
|
+
title: 'This account is locked',
|
|
68
|
+
description: 'Too many failed sign-in attempts, or an administrator locked it. Signing in will keep failing until an administrator unlocks the account — a password reset will not clear the lock.',
|
|
69
|
+
tone: 'error',
|
|
70
|
+
retryable: false,
|
|
71
|
+
},
|
|
72
|
+
[IamErrorCode.RATE_LIMITED]: {
|
|
73
|
+
title: 'Too many requests',
|
|
74
|
+
description: 'You have made too many attempts. Wait a moment and try again.',
|
|
75
|
+
tone: 'warning',
|
|
76
|
+
retryable: true,
|
|
77
|
+
},
|
|
78
|
+
[IamErrorCode.INTERNAL_ERROR]: {
|
|
79
|
+
title: 'Something went wrong on the server',
|
|
80
|
+
description: 'The request did not complete. If it keeps happening, quote the request id below to whoever runs this deployment.',
|
|
81
|
+
tone: 'error',
|
|
82
|
+
retryable: true,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
/** Copy for a request that never reached the API at all. */
|
|
86
|
+
export const TRANSPORT_ERROR_COPY = Object.freeze({
|
|
87
|
+
title: 'Could not reach the server',
|
|
88
|
+
description: 'The request did not get an answer — the network, a proxy, or the service being down. Check your connection and try again.',
|
|
89
|
+
tone: 'error',
|
|
90
|
+
retryable: true,
|
|
91
|
+
});
|
|
92
|
+
/** The copy for a code. Unknown codes fall back to the transport wording. */
|
|
93
|
+
export function errorCopyFor(code) {
|
|
94
|
+
if (code == null)
|
|
95
|
+
return TRANSPORT_ERROR_COPY;
|
|
96
|
+
return COPY[code] ?? TRANSPORT_ERROR_COPY;
|
|
97
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface PageHeaderProps {
|
|
3
|
+
title: React.ReactNode;
|
|
4
|
+
/** One sentence on what the screen is for. Worth writing. */
|
|
5
|
+
description?: React.ReactNode;
|
|
6
|
+
/** Trail above the title. Last entry is the current screen. */
|
|
7
|
+
breadcrumbs?: {
|
|
8
|
+
title: React.ReactNode;
|
|
9
|
+
href?: string;
|
|
10
|
+
}[];
|
|
11
|
+
/** Right-hand actions — normally one primary button and a menu. */
|
|
12
|
+
actions?: React.ReactNode;
|
|
13
|
+
/** Tabs or filters that belong to the header rather than the body. */
|
|
14
|
+
footer?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export declare function PageHeader({ title, description, breadcrumbs, actions, footer, }: PageHeaderProps): React.ReactElement;
|
|
17
|
+
//# sourceMappingURL=page-header.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-header.d.ts","sourceRoot":"","sources":["../../src/feedback/page-header.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,+DAA+D;IAC/D,WAAW,CAAC,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1D,mEAAmE;IACnE,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,WAAW,EACX,OAAO,EACP,MAAM,GACP,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAoCtC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The top of every screen: what this is, and the actions that belong to it.
|
|
5
|
+
*
|
|
6
|
+
* A component rather than a convention because the *placement* of the primary
|
|
7
|
+
* action is what makes a console feel like one product. Doc 09 §4 also puts a
|
|
8
|
+
* requirement here that a hand-rolled header would keep forgetting: actions the
|
|
9
|
+
* subject lacks permission for are hidden, so the action slot has to be the
|
|
10
|
+
* kind of thing a caller can conditionally fill — hence `actions` as a node
|
|
11
|
+
* rather than a button spec.
|
|
12
|
+
*/
|
|
13
|
+
import { Breadcrumb, Space, Typography } from 'antd';
|
|
14
|
+
import { spacing } from '../theme/tokens';
|
|
15
|
+
export function PageHeader({ title, description, breadcrumbs, actions, footer, }) {
|
|
16
|
+
return (_jsxs("header", { style: { marginBlockEnd: spacing.lg }, children: [breadcrumbs !== undefined && breadcrumbs.length > 0 && (_jsx(Breadcrumb, { items: breadcrumbs, style: { marginBlockEnd: spacing.xs, fontSize: 12 } })), _jsxs("div", { style: {
|
|
17
|
+
display: 'flex',
|
|
18
|
+
alignItems: 'flex-start',
|
|
19
|
+
justifyContent: 'space-between',
|
|
20
|
+
gap: spacing.md,
|
|
21
|
+
flexWrap: 'wrap',
|
|
22
|
+
}, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsx(Typography.Title, { level: 3, style: { margin: 0, fontWeight: 600 }, children: title }), description !== undefined && (_jsx(Typography.Paragraph, { type: "secondary", style: { margin: `${spacing.xxs}px 0 0`, maxWidth: 720 }, children: description }))] }), actions !== undefined && _jsx(Space, { wrap: true, children: actions })] }), footer !== undefined && _jsx("div", { style: { marginBlockStart: spacing.md }, children: footer })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type ErrorCopy } from './error-copy';
|
|
3
|
+
export interface ScreenLoadingProps {
|
|
4
|
+
/** `'panel'` for a first paint, `'inline'` for a refresh inside a screen. */
|
|
5
|
+
variant?: 'panel' | 'inline';
|
|
6
|
+
/** Rows of skeleton text, for a variant that stands in for known content. */
|
|
7
|
+
rows?: number;
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function ScreenLoading({ variant, rows, label, }: ScreenLoadingProps): React.ReactElement;
|
|
11
|
+
export interface ScreenEmptyProps {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: React.ReactNode;
|
|
14
|
+
/** The one thing to do about it — "Add the first user". */
|
|
15
|
+
action?: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare function ScreenEmpty({ title, description, action, }: ScreenEmptyProps): React.ReactElement;
|
|
18
|
+
export interface ScreenErrorProps {
|
|
19
|
+
/** From `errorCopyFor(code)` — the words. */
|
|
20
|
+
copy: ErrorCopy;
|
|
21
|
+
/**
|
|
22
|
+
* The server's own message, shown under the copy when it adds something.
|
|
23
|
+
*
|
|
24
|
+
* Kept because a `CONFLICT` whose message names the duplicate field is far
|
|
25
|
+
* more useful than the generic sentence, and suppressed when it is only a
|
|
26
|
+
* restatement.
|
|
27
|
+
*/
|
|
28
|
+
detail?: string | null;
|
|
29
|
+
/** Correlates with the server's logs and audit trail (Doc 06 §2). */
|
|
30
|
+
requestId?: string | null;
|
|
31
|
+
/** Field-level complaints from a `VALIDATION_FAILED`. */
|
|
32
|
+
details?: readonly {
|
|
33
|
+
field: string;
|
|
34
|
+
message: string;
|
|
35
|
+
}[];
|
|
36
|
+
/** Shown only when the failure is plausibly transient. */
|
|
37
|
+
onRetry?: () => void;
|
|
38
|
+
/** An escape route — "Back to the dashboard". */
|
|
39
|
+
action?: React.ReactNode;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A failed screen, explained.
|
|
43
|
+
*
|
|
44
|
+
* `requestId` is rendered in a monospace, selectable line rather than hidden in
|
|
45
|
+
* a console: it is the only handle an operator has for finding this exact
|
|
46
|
+
* failure in the server's logs and audit trail, and the person who can read it
|
|
47
|
+
* out is the one looking at the screen.
|
|
48
|
+
*/
|
|
49
|
+
export declare function ScreenError({ copy, detail, requestId, details, onRetry, action, }: ScreenErrorProps): React.ReactElement;
|
|
50
|
+
//# sourceMappingURL=state-panels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-panels.d.ts","sourceRoot":"","sources":["../../src/feedback/state-panels.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAE,KAAK,SAAS,EAAkB,MAAM,cAAc,CAAC;AAE9D,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC7B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAiB,EACjB,IAAQ,EACR,KAAK,GACN,EAAE,kBAAkB,GAAG,KAAK,CAAC,YAAY,CAgBzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,2DAA2D;IAC3D,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAED,wBAAgB,WAAW,CAAC,EAC1B,KAA0B,EAC1B,WAAW,EACX,MAAM,GACP,EAAE,gBAAgB,GAAG,KAAK,CAAC,YAAY,CASvC;AAQD,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,IAAI,EAAE,SAAS,CAAC;IAChB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,yDAAyD;IACzD,OAAO,CAAC,EAAE,SAAS;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxD,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,iDAAiD;IACjD,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,MAAM,EACN,SAAS,EACT,OAAO,EACP,OAAO,EACP,MAAM,GACP,EAAE,gBAAgB,GAAG,KAAK,CAAC,YAAY,CAkDvC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The four things a screen shows when it is not showing data: loading, empty,
|
|
5
|
+
* denied, broken.
|
|
6
|
+
*
|
|
7
|
+
* Written once because they are the states screens get wrong. Left to each
|
|
8
|
+
* page, "denied" becomes a blank area, "empty" becomes a spinner that never
|
|
9
|
+
* stops, and the difference between "you have no users yet" and "you may not
|
|
10
|
+
* see users" — which are opposite messages with opposite next actions —
|
|
11
|
+
* disappears.
|
|
12
|
+
*
|
|
13
|
+
* {@link ScreenError} is the one that matters most. Doc 09 §4 requires the
|
|
14
|
+
* console to render a 403 cleanly on a deep link into a screen the menu hid,
|
|
15
|
+
* because client-side hiding is UX and the server is the enforcement. That is
|
|
16
|
+
* this component with the copy from `error-copy.ts`.
|
|
17
|
+
*/
|
|
18
|
+
import { ExclamationCircleOutlined, InboxOutlined, LockOutlined, ReloadOutlined, StopOutlined, } from '@ant-design/icons';
|
|
19
|
+
import { Button, Result, Skeleton, Space, Spin, Typography } from 'antd';
|
|
20
|
+
import { spacing } from '../theme/tokens';
|
|
21
|
+
export function ScreenLoading({ variant = 'panel', rows = 4, label, }) {
|
|
22
|
+
if (variant === 'inline') {
|
|
23
|
+
return (_jsxs(Space, { size: "small", style: { padding: spacing.md }, children: [_jsx(Spin, { size: "small" }), label !== undefined && (_jsx(Typography.Text, { type: "secondary", children: label }))] }));
|
|
24
|
+
}
|
|
25
|
+
return (_jsx("div", { style: { padding: spacing.lg }, "aria-busy": "true", "aria-live": "polite", children: _jsx(Skeleton, { active: true, paragraph: { rows }, title: true }) }));
|
|
26
|
+
}
|
|
27
|
+
export function ScreenEmpty({ title = 'Nothing here yet', description, action, }) {
|
|
28
|
+
return (_jsx(Result, { icon: _jsx(InboxOutlined, { style: { color: 'var(--ant-color-text-quaternary)' } }), title: title, subTitle: description, extra: action }));
|
|
29
|
+
}
|
|
30
|
+
const TONE_ICON = {
|
|
31
|
+
error: _jsx(ExclamationCircleOutlined, { style: { color: 'var(--ant-color-error)' } }),
|
|
32
|
+
warning: _jsx(StopOutlined, { style: { color: 'var(--ant-color-warning)' } }),
|
|
33
|
+
info: _jsx(LockOutlined, { style: { color: 'var(--ant-color-info)' } }),
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A failed screen, explained.
|
|
37
|
+
*
|
|
38
|
+
* `requestId` is rendered in a monospace, selectable line rather than hidden in
|
|
39
|
+
* a console: it is the only handle an operator has for finding this exact
|
|
40
|
+
* failure in the server's logs and audit trail, and the person who can read it
|
|
41
|
+
* out is the one looking at the screen.
|
|
42
|
+
*/
|
|
43
|
+
export function ScreenError({ copy, detail, requestId, details, onRetry, action, }) {
|
|
44
|
+
const showDetail = detail != null && detail.trim() !== '' && detail.trim() !== copy.description;
|
|
45
|
+
return (_jsx(Result, { icon: TONE_ICON[copy.tone], title: copy.title, subTitle: _jsxs(Space, { direction: "vertical", size: spacing.xs, style: { maxWidth: 620 }, children: [_jsx(Typography.Text, { type: "secondary", children: copy.description }), showDetail && (_jsx(Typography.Text, { type: "secondary", italic: true, children: detail })), details !== undefined && details.length > 0 && (_jsx("ul", { style: { margin: 0, paddingInlineStart: spacing.lg, textAlign: 'start' }, children: details.map((item) => (_jsx("li", { children: _jsxs(Typography.Text, { type: "secondary", children: [_jsx("strong", { children: item.field }), " \u2014 ", item.message] }) }, `${item.field}:${item.message}`))) })), requestId != null && requestId !== '' && (_jsxs(Typography.Text, { type: "secondary", copyable: { text: requestId }, style: { fontFamily: 'var(--ant-font-family-code)', fontSize: 12 }, children: ["Request ", requestId] }))] }), extra: _jsxs(Space, { children: [action, copy.retryable && onRetry !== undefined && (_jsx(Button, { icon: _jsx(ReloadOutlined, {}), onClick: onRetry, children: "Try again" }))] }) }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface AuthLayoutProps {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
/** The console's name, beside the mark — "IAM", "Gatepass". */
|
|
5
|
+
product?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
subtitle?: React.ReactNode;
|
|
8
|
+
/** Small print under the card — an environment badge, a support address. */
|
|
9
|
+
footer?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare function AuthLayout({ children, product, title, subtitle, footer, }: AuthLayoutProps): React.ReactElement;
|
|
12
|
+
//# sourceMappingURL=auth-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-layout.d.ts","sourceRoot":"","sources":["../../src/forms/auth-layout.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAED,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,OAAO,EACP,KAAiB,EACjB,QAAQ,EACR,MAAM,GACP,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAyCtC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The page a signed-out person sees: a centred card on a branded field.
|
|
5
|
+
*
|
|
6
|
+
* Shared with the consoles that come later so that sign-in, password reset and
|
|
7
|
+
* "your session ended" all look like the same door into the same product. The
|
|
8
|
+
* card is the only thing on the page — an unauthenticated visitor has exactly
|
|
9
|
+
* one thing to do, and every additional element is a way to not do it.
|
|
10
|
+
*/
|
|
11
|
+
import { Card, Typography } from 'antd';
|
|
12
|
+
import { Brand } from '../layout/brand';
|
|
13
|
+
import { navSurface, palette, spacing } from '../theme/tokens';
|
|
14
|
+
export function AuthLayout({ children, product, title = 'Sign in', subtitle, footer, }) {
|
|
15
|
+
return (_jsxs("main", { style: {
|
|
16
|
+
minHeight: '100vh',
|
|
17
|
+
display: 'flex',
|
|
18
|
+
flexDirection: 'column',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'center',
|
|
21
|
+
gap: spacing.lg,
|
|
22
|
+
padding: spacing.lg,
|
|
23
|
+
// A single wash rather than an image: it renders identically on every
|
|
24
|
+
// deployment, costs no request, and cannot be the asset someone forgot
|
|
25
|
+
// to copy into the container.
|
|
26
|
+
background: `radial-gradient(1200px 600px at 50% -10%, ${palette.primary}22, transparent 60%), ${navSurface.background}`,
|
|
27
|
+
}, children: [_jsx(Brand, { product: product }), _jsxs(Card, { style: { width: '100%', maxWidth: 420 }, styles: { body: { padding: spacing.xl } }, children: [_jsx(Typography.Title, { level: 4, style: { marginBlockStart: 0 }, children: title }), subtitle !== undefined && (_jsx(Typography.Paragraph, { type: "secondary", style: { marginBlockEnd: spacing.lg }, children: subtitle })), children] }), footer !== undefined && (_jsx(Typography.Text, { style: { color: navSurface.text, fontSize: 12 }, children: footer }))] }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { ErrorCopy } from '../feedback/error-copy';
|
|
3
|
+
export interface CredentialsFormValues {
|
|
4
|
+
client_slug: string;
|
|
5
|
+
email: string;
|
|
6
|
+
password: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CredentialsFormProps {
|
|
9
|
+
onSubmit: (values: CredentialsFormValues) => void | Promise<void>;
|
|
10
|
+
submitting?: boolean;
|
|
11
|
+
/** Shown above the fields. `null` clears it. */
|
|
12
|
+
error?: ErrorCopy | null;
|
|
13
|
+
/** The server's own sentence, under the copy, when it adds something. */
|
|
14
|
+
errorDetail?: string | null;
|
|
15
|
+
/** Pre-fills the tenant — from the last successful sign-in, or a subdomain. */
|
|
16
|
+
defaultClientSlug?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Hides the tenant field, for a deployment that serves one organisation
|
|
19
|
+
* (Doc 11 §6.5).
|
|
20
|
+
*
|
|
21
|
+
* The value is still submitted — as a hidden field carrying
|
|
22
|
+
* `defaultClientSlug` — because the tenant half of the credential has not gone
|
|
23
|
+
* anywhere, it simply is not the user's to choose. Hiding it is where the
|
|
24
|
+
* change ends: the API refuses a login that names a different organisation
|
|
25
|
+
* whether or not the form ever showed a box, which is what makes this a UX
|
|
26
|
+
* decision rather than a control.
|
|
27
|
+
*/
|
|
28
|
+
hideClientField?: boolean;
|
|
29
|
+
/** e.g. a "Forgot password?" link. Rendered under the submit button. */
|
|
30
|
+
footer?: React.ReactNode;
|
|
31
|
+
submitLabel?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function CredentialsForm({ onSubmit, submitting, error, errorDetail, defaultClientSlug, hideClientField, footer, submitLabel, }: CredentialsFormProps): React.ReactElement;
|
|
34
|
+
//# sourceMappingURL=credentials-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials-form.d.ts","sourceRoot":"","sources":["../../src/forms/credentials-form.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gDAAgD;IAChD,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,UAAkB,EAClB,KAAK,EACL,WAAW,EACX,iBAAsB,EACtB,eAAuB,EACvB,MAAM,EACN,WAAuB,GACxB,EAAE,oBAAoB,GAAG,KAAK,CAAC,YAAY,CAiG3C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The sign-in form: client, email, password (Doc 09 §1, Doc 06 §3).
|
|
5
|
+
*
|
|
6
|
+
* Presentational on purpose — it takes values in and hands values out, and
|
|
7
|
+
* knows nothing about `IamClient`, tokens or routing. That is what makes it
|
|
8
|
+
* reusable by the gatepass and visitor consoles, which sign in against the same
|
|
9
|
+
* IAM with the same three fields but land somewhere else afterwards.
|
|
10
|
+
*
|
|
11
|
+
* ## Why the client field comes first
|
|
12
|
+
*
|
|
13
|
+
* `POST /auth/login` takes `client_slug` because the same email address can
|
|
14
|
+
* exist in several tenants (Doc 06 §8), so the tenant is not a detail — it is
|
|
15
|
+
* the first thing that has to be right, and a person who types their address
|
|
16
|
+
* before realising they are signing in to the wrong plant has to start over.
|
|
17
|
+
* It is remembered between visits for the same reason: almost nobody signs in
|
|
18
|
+
* to two tenants, and the one who does is the one who will notice the field.
|
|
19
|
+
*
|
|
20
|
+
* ## Errors
|
|
21
|
+
*
|
|
22
|
+
* The form renders whatever copy it is given, and the caller decides which. The
|
|
23
|
+
* distinction that must survive is 401 vs 423: Doc 03 §8 makes a locked account
|
|
24
|
+
* a different state with a different remedy, and telling someone their password
|
|
25
|
+
* was wrong when the account is locked sends them to reset a password that will
|
|
26
|
+
* not help.
|
|
27
|
+
*/
|
|
28
|
+
import { LockOutlined, MailOutlined, ShopOutlined } from '@ant-design/icons';
|
|
29
|
+
import { Alert, Button, Form, Input, Typography } from 'antd';
|
|
30
|
+
import { spacing } from '../theme/tokens';
|
|
31
|
+
export function CredentialsForm({ onSubmit, submitting = false, error, errorDetail, defaultClientSlug = '', hideClientField = false, footer, submitLabel = 'Sign in', }) {
|
|
32
|
+
const [form] = Form.useForm();
|
|
33
|
+
return (_jsxs(Form, { form: form, layout: "vertical", requiredMark: false, initialValues: { client_slug: defaultClientSlug, email: '', password: '' }, onFinish: onSubmit, disabled: submitting, size: "large", children: [error != null && (_jsx(Form.Item, { children: _jsx(Alert, { type: error.tone === 'info' ? 'info' : error.tone, showIcon: true, message: error.title, description: _jsxs(_Fragment, { children: [_jsx("div", { children: error.description }), errorDetail != null && errorDetail !== '' && (_jsx(Typography.Text, { type: "secondary", italic: true, style: { fontSize: 12 }, children: errorDetail }))] }) }) })), hideClientField ? (_jsx(Form.Item, { name: "client_slug", hidden: true, children: _jsx(Input, { type: "hidden" }) })) : (_jsx(Form.Item, { name: "client_slug", label: "Client", rules: [{ required: true, message: 'Which client are you signing in to?' }], extra: "The short name of your organisation in PlantOps.", children: _jsx(Input, { prefix: _jsx(ShopOutlined, {}), placeholder: "acme-industries", autoComplete: "organization", autoCapitalize: "none", spellCheck: false }) })), _jsx(Form.Item, { name: "email", label: "Email", rules: [
|
|
34
|
+
{ required: true, message: 'Enter your email address.' },
|
|
35
|
+
{ type: 'email', message: 'That does not look like an email address.' },
|
|
36
|
+
], children: _jsx(Input, { prefix: _jsx(MailOutlined, {}), placeholder: "you@company.com", autoComplete: "username", inputMode: "email", spellCheck: false }) }), _jsx(Form.Item, { name: "password", label: "Password", rules: [{ required: true, message: 'Enter your password.' }], children: _jsx(Input.Password, { prefix: _jsx(LockOutlined, {}), placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: "current-password" }) }), _jsx(Form.Item, { style: { marginBlockEnd: footer === undefined ? 0 : spacing.sm }, children: _jsx(Button, { type: "primary", htmlType: "submit", block: true, loading: submitting, children: submitLabel }) }), footer] }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
type IconComponent = React.ComponentType<{
|
|
3
|
+
className?: string;
|
|
4
|
+
}>;
|
|
5
|
+
/** True when the key has a mapping — for a catalog editor's preview. */
|
|
6
|
+
export declare function isKnownIconKey(key: string): boolean;
|
|
7
|
+
/** Every key this frontend understands, sorted — for an icon picker. */
|
|
8
|
+
export declare function knownIconKeys(): string[];
|
|
9
|
+
/** The component for a key, or the fallback. Prefer {@link NavIcon} in JSX. */
|
|
10
|
+
export declare function iconForKey(key: string | null | undefined): IconComponent;
|
|
11
|
+
export interface NavIconProps {
|
|
12
|
+
/** The `icon` string from a `NavNodeDTO`. `null`/absent is expected. */
|
|
13
|
+
iconKey?: string | null;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Renders a registry icon key.
|
|
18
|
+
*
|
|
19
|
+
* Always renders something. A menu whose rows are sometimes 14px shorter
|
|
20
|
+
* because one node's icon key was misspelt is worse than one that shows a
|
|
21
|
+
* neutral glyph.
|
|
22
|
+
*/
|
|
23
|
+
export declare function NavIcon({ iconKey, className }: NavIconProps): React.ReactElement;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=icon-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icon-registry.d.ts","sourceRoot":"","sources":["../../src/icons/icon-registry.tsx"],"names":[],"mappings":"AAiEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,KAAK,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAwDjE,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wEAAwE;AACxE,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAExC;AAED,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,aAAa,CAGxE;AAED,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,YAAY,GAAG,KAAK,CAAC,YAAY,CAGhF"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `icon` keys from the registry → the icon set this design language uses
|
|
5
|
+
* (Doc 05 §7).
|
|
6
|
+
*
|
|
7
|
+
* Doc 05 is explicit that `nav_node.icon` is "a string key the frontend maps to
|
|
8
|
+
* its icon set", and Doc 02 is explicit that the catalog is data written by
|
|
9
|
+
* platform admins at runtime. Both together mean this map is the *only* place
|
|
10
|
+
* the two vocabularies meet, and that it will always be incomplete: an admin
|
|
11
|
+
* can type `"forklift"` into a manifest tonight and no amount of care here
|
|
12
|
+
* anticipates it. So an unknown key is a normal, non-fatal outcome that falls
|
|
13
|
+
* back to a neutral glyph — never a crash, and never a blank space that makes
|
|
14
|
+
* the menu row jump out of alignment with its neighbours.
|
|
15
|
+
*
|
|
16
|
+
* The keys are deliberately generic nouns (`users`, `key`, `sitemap`) rather
|
|
17
|
+
* than antd component names. A manifest that said `TeamOutlined` would pin the
|
|
18
|
+
* *database* to this icon library, and swapping icon sets would then mean a
|
|
19
|
+
* migration rather than an edit to this file.
|
|
20
|
+
*
|
|
21
|
+
* Adding a key: add the row, and mention it in the application's manifest
|
|
22
|
+
* documentation. Gatepass and visitor management will add their own domain
|
|
23
|
+
* nouns here (`gate`, `pass`, `vehicle`, `visitor`); the block below is
|
|
24
|
+
* pre-seeded with them so those manifests can be written before their consoles
|
|
25
|
+
* exist.
|
|
26
|
+
*/
|
|
27
|
+
import { ApartmentOutlined, AppstoreOutlined, AuditOutlined, BankOutlined, BarChartOutlined, BellOutlined, CarOutlined, ClusterOutlined, ContainerOutlined, DashboardOutlined, DatabaseOutlined, FileTextOutlined, FolderOutlined, GlobalOutlined, GoldOutlined, HomeOutlined, IdcardOutlined, InboxOutlined, KeyOutlined, LinkOutlined, LockOutlined, PartitionOutlined, ProfileOutlined, SafetyCertificateOutlined, SafetyOutlined, ScheduleOutlined, SearchOutlined, SettingOutlined, ShopOutlined, SolutionOutlined, TagOutlined, TeamOutlined, ThunderboltOutlined, ToolOutlined, UserOutlined, WarningOutlined, } from '@ant-design/icons';
|
|
28
|
+
/**
|
|
29
|
+
* The registry. Keys are the strings that appear in `nav_node.icon`.
|
|
30
|
+
*
|
|
31
|
+
* The first block is what `deploy/manifests/iam.manifest.json` uses today; the rest is the
|
|
32
|
+
* shared vocabulary later applications draw on.
|
|
33
|
+
*/
|
|
34
|
+
const ICONS = Object.freeze({
|
|
35
|
+
// ── Used by the IAM's own manifest ───────────────────────────────────────
|
|
36
|
+
building: BankOutlined,
|
|
37
|
+
grid: AppstoreOutlined,
|
|
38
|
+
briefcase: ShopOutlined,
|
|
39
|
+
key: KeyOutlined,
|
|
40
|
+
scroll: AuditOutlined,
|
|
41
|
+
shield: SafetyOutlined,
|
|
42
|
+
sitemap: ApartmentOutlined,
|
|
43
|
+
badge: IdcardOutlined,
|
|
44
|
+
users: TeamOutlined,
|
|
45
|
+
link: LinkOutlined,
|
|
46
|
+
// ── General vocabulary ───────────────────────────────────────────────────
|
|
47
|
+
home: HomeOutlined,
|
|
48
|
+
dashboard: DashboardOutlined,
|
|
49
|
+
user: UserOutlined,
|
|
50
|
+
settings: SettingOutlined,
|
|
51
|
+
search: SearchOutlined,
|
|
52
|
+
bell: BellOutlined,
|
|
53
|
+
file: FileTextOutlined,
|
|
54
|
+
folder: FolderOutlined,
|
|
55
|
+
tag: TagOutlined,
|
|
56
|
+
chart: BarChartOutlined,
|
|
57
|
+
database: DatabaseOutlined,
|
|
58
|
+
globe: GlobalOutlined,
|
|
59
|
+
lock: LockOutlined,
|
|
60
|
+
warning: WarningOutlined,
|
|
61
|
+
tool: ToolOutlined,
|
|
62
|
+
bolt: ThunderboltOutlined,
|
|
63
|
+
cluster: ClusterOutlined,
|
|
64
|
+
partition: PartitionOutlined,
|
|
65
|
+
certificate: SafetyCertificateOutlined,
|
|
66
|
+
profile: ProfileOutlined,
|
|
67
|
+
// ── Reserved for the operational modules (Doc 00 §9) ─────────────────────
|
|
68
|
+
gate: GoldOutlined,
|
|
69
|
+
pass: SolutionOutlined,
|
|
70
|
+
vehicle: CarOutlined,
|
|
71
|
+
visitor: IdcardOutlined,
|
|
72
|
+
schedule: ScheduleOutlined,
|
|
73
|
+
inbox: InboxOutlined,
|
|
74
|
+
container: ContainerOutlined,
|
|
75
|
+
});
|
|
76
|
+
/** The glyph an unrecognised key renders as. */
|
|
77
|
+
const FALLBACK_ICON = AppstoreOutlined;
|
|
78
|
+
/** True when the key has a mapping — for a catalog editor's preview. */
|
|
79
|
+
export function isKnownIconKey(key) {
|
|
80
|
+
return key in ICONS;
|
|
81
|
+
}
|
|
82
|
+
/** Every key this frontend understands, sorted — for an icon picker. */
|
|
83
|
+
export function knownIconKeys() {
|
|
84
|
+
return Object.keys(ICONS).sort();
|
|
85
|
+
}
|
|
86
|
+
/** The component for a key, or the fallback. Prefer {@link NavIcon} in JSX. */
|
|
87
|
+
export function iconForKey(key) {
|
|
88
|
+
if (key === null || key === undefined)
|
|
89
|
+
return FALLBACK_ICON;
|
|
90
|
+
return ICONS[key] ?? FALLBACK_ICON;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Renders a registry icon key.
|
|
94
|
+
*
|
|
95
|
+
* Always renders something. A menu whose rows are sometimes 14px shorter
|
|
96
|
+
* because one node's icon key was misspelt is worse than one that shows a
|
|
97
|
+
* neutral glyph.
|
|
98
|
+
*/
|
|
99
|
+
export function NavIcon({ iconKey, className }) {
|
|
100
|
+
const Icon = iconForKey(iconKey);
|
|
101
|
+
return _jsx(Icon, { className: className });
|
|
102
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@plantops/ui` — the shared React presentation layer for every PlantOps
|
|
3
|
+
* console (Doc 08 §2).
|
|
4
|
+
*
|
|
5
|
+
* Ant Design 6 supplies the components; this library supplies the *product*:
|
|
6
|
+
* the design tokens, the theme built from them, the shell every console renders
|
|
7
|
+
* inside, the icon-key registry Doc 05 §7 requires, and the handful of patterns
|
|
8
|
+
* that must not be reinvented per screen — page headers, list tables bound to
|
|
9
|
+
* the pagination envelope, status tags, and the four not-showing-data states.
|
|
10
|
+
*
|
|
11
|
+
* ## The rule that keeps it reusable
|
|
12
|
+
*
|
|
13
|
+
* Nothing here calls the IAM. The library depends on `@plantops/contracts` for
|
|
14
|
+
* types and on nothing else in the workspace — enforced by the `scope:ui`
|
|
15
|
+
* boundary in the root ESLint config — so a component takes data and callbacks
|
|
16
|
+
* and returns markup. The stateful half (a client, tokens, grants, navigation
|
|
17
|
+
* fetching) lives in `@plantops/web-kit`, which depends on this.
|
|
18
|
+
*
|
|
19
|
+
* That split is what makes the gatepass and visitor consoles cheap: they mount
|
|
20
|
+
* the same `<PlantOpsThemeProvider>` and `<AppShell>`, render the same
|
|
21
|
+
* `<NavMenu>` from their own `/iam/navigation` response, and inherit the
|
|
22
|
+
* product's appearance without inheriting the IAM's screens.
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <PlantOpsThemeProvider>
|
|
26
|
+
* <AppShell brand={<Brand product="Gatepass" />} nav={<NavMenu … />}>
|
|
27
|
+
* <PageHeader title="Passes" />
|
|
28
|
+
* <DataTable result={page} columns={columns} rowKey={(r) => r.id} />
|
|
29
|
+
* </AppShell>
|
|
30
|
+
* </PlantOpsThemeProvider>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export * from './data/data-table';
|
|
34
|
+
export * from './data/scope-tree';
|
|
35
|
+
export * from './data/scope-tree-select';
|
|
36
|
+
export * from './data/status-tag';
|
|
37
|
+
export * from './feedback/error-copy';
|
|
38
|
+
export * from './feedback/page-header';
|
|
39
|
+
export * from './feedback/state-panels';
|
|
40
|
+
export * from './forms/auth-layout';
|
|
41
|
+
export * from './forms/credentials-form';
|
|
42
|
+
export * from './icons/icon-registry';
|
|
43
|
+
export * from './layout/app-shell';
|
|
44
|
+
export * from './layout/brand';
|
|
45
|
+
export * from './layout/nav-menu';
|
|
46
|
+
export * from './layout/nav-tree';
|
|
47
|
+
export * from './layout/user-menu';
|
|
48
|
+
export * from './theme/color-mode';
|
|
49
|
+
export * from './theme/theme';
|
|
50
|
+
export * from './theme/theme-provider';
|
|
51
|
+
export * from './theme/tokens';
|
|
52
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,gBAAgB,CAAC"}
|