@tea-ui/ux-standards 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 landnevermore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @tea-ui/ux-standards
2
+
3
+ The written rules components are held to, as data rather than prose.
4
+
5
+ Part of [TEA UI](https://landnevermore.github.io/TEAui/) — a design system built once and reused across
6
+ products. The whole system is documented at https://landnevermore.github.io/TEAui/docs/.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @tea-ui/ux-standards
12
+ ```
13
+
14
+ React 18.2 or 19 is expected as a peer dependency.
15
+
16
+ ## Use
17
+
18
+ ```ts
19
+ import { tokens } from "@tea-ui/ux-standards";
20
+ ```
21
+
22
+ ## Licence
23
+
24
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,58 @@
1
+ /**
2
+ * TEA UI — destructive action policy.
3
+ *
4
+ * The rule that matters: choose the *weakest* protection that is sufficient
5
+ * for the consequence. Every unnecessary confirmation dialog is a dialog the
6
+ * user learns to dismiss without reading, and that habit is what makes the one
7
+ * dialog they should have read get dismissed too. So confirmation is spent
8
+ * where it buys something, and undo is used wherever it is available.
9
+ *
10
+ * The audit found the opposite at both source projects: a reversible two-step
11
+ * inline confirmation sitting next to an irreversible `DELETE` with no
12
+ * confirmation at all.
13
+ */
14
+ declare const CONSEQUENCE_LEVELS: readonly ["reversible", "recoverable", "irreversible"];
15
+ type ConsequenceLevel = (typeof CONSEQUENCE_LEVELS)[number];
16
+ /** How a destructive action must be protected. */
17
+ type Protection = "undo" | "confirm" | "confirm-typed" | "confirm-hold";
18
+ interface DestructivePolicy {
19
+ /** What the action destroys. */
20
+ readonly level: ConsequenceLevel;
21
+ /** The protection TEA UI requires. */
22
+ readonly protection: Protection;
23
+ /** Whether the consequence must be spelled out in the dialog body. */
24
+ readonly explainConsequence: boolean;
25
+ /** Whether the primary button may be the destructive one. */
26
+ readonly destructivePrimary: boolean;
27
+ }
28
+ /**
29
+ * The policy table.
30
+ *
31
+ * `reversible` — undo is offered instead of a dialog. Nothing is asked.
32
+ * `recoverable` — a confirm dialog, because the data can be recovered but the
33
+ * user would notice its absence. The primary button is NOT
34
+ * destructive: cancelling must be the easy path.
35
+ * `irreversible` — a confirm dialog that names the consequence, and for
36
+ * anything that cannot be undone at all, requires typing the
37
+ * resource name. Cancelling stays the default focus.
38
+ */
39
+ declare const DESTRUCTIVE_POLICY: Readonly<Record<ConsequenceLevel, DestructivePolicy>>;
40
+ declare function destructivePolicy(level: ConsequenceLevel): DestructivePolicy;
41
+ /** The verb pair an alert dialog uses, per the content standard (§22). */
42
+ declare const DESTRUCTIVE_VERBS: {
43
+ readonly confirm: {
44
+ readonly reversible: "Ausführen";
45
+ readonly recoverable: "Trotzdem fortfahren";
46
+ readonly irreversible: "Endgültig löschen";
47
+ };
48
+ readonly cancel: "Abbrechen";
49
+ };
50
+ /**
51
+ * The consequence sentence a dialog body must contain. Written as a function
52
+ * rather than a template so the noun stays in one place — a dialog that says
53
+ * "wird gelöscht" next to a button that says "Wiederherstellen" is worse than
54
+ * no dialog.
55
+ */
56
+ declare function consequenceSentence(what: string, level: ConsequenceLevel): string;
57
+
58
+ export { CONSEQUENCE_LEVELS, type ConsequenceLevel, DESTRUCTIVE_POLICY, DESTRUCTIVE_VERBS, type DestructivePolicy, type Protection, consequenceSentence, destructivePolicy };
@@ -0,0 +1,50 @@
1
+ const CONSEQUENCE_LEVELS = ["reversible", "recoverable", "irreversible"];
2
+ const DESTRUCTIVE_POLICY = {
3
+ reversible: {
4
+ level: "reversible",
5
+ protection: "undo",
6
+ explainConsequence: false,
7
+ destructivePrimary: false
8
+ },
9
+ recoverable: {
10
+ level: "recoverable",
11
+ protection: "confirm",
12
+ explainConsequence: true,
13
+ destructivePrimary: false
14
+ },
15
+ irreversible: {
16
+ level: "irreversible",
17
+ protection: "confirm-typed",
18
+ explainConsequence: true,
19
+ destructivePrimary: false
20
+ }
21
+ };
22
+ function destructivePolicy(level) {
23
+ return DESTRUCTIVE_POLICY[level];
24
+ }
25
+ const DESTRUCTIVE_VERBS = {
26
+ confirm: {
27
+ reversible: "Ausf\xFChren",
28
+ recoverable: "Trotzdem fortfahren",
29
+ irreversible: "Endg\xFCltig l\xF6schen"
30
+ },
31
+ cancel: "Abbrechen"
32
+ };
33
+ function consequenceSentence(what, level) {
34
+ switch (level) {
35
+ case "reversible":
36
+ return `${what} wird kurz zur\xFCckgenommen werden k\xF6nnen.`;
37
+ case "recoverable":
38
+ return `${what} wird endg\xFCltig entfernt. Eine Wiederherstellung ist nur aus einer Sicherung m\xF6glich.`;
39
+ case "irreversible":
40
+ return `${what} wird endg\xFCltig entfernt. Dieser Schritt kann nicht r\xFCckg\xE4ngig gemacht werden.`;
41
+ }
42
+ }
43
+ export {
44
+ CONSEQUENCE_LEVELS,
45
+ DESTRUCTIVE_POLICY,
46
+ DESTRUCTIVE_VERBS,
47
+ consequenceSentence,
48
+ destructivePolicy
49
+ };
50
+ //# sourceMappingURL=destructive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/destructive.ts"],"sourcesContent":["/**\n * TEA UI — destructive action policy.\n *\n * The rule that matters: choose the *weakest* protection that is sufficient\n * for the consequence. Every unnecessary confirmation dialog is a dialog the\n * user learns to dismiss without reading, and that habit is what makes the one\n * dialog they should have read get dismissed too. So confirmation is spent\n * where it buys something, and undo is used wherever it is available.\n *\n * The audit found the opposite at both source projects: a reversible two-step\n * inline confirmation sitting next to an irreversible `DELETE` with no\n * confirmation at all.\n */\n\nexport const CONSEQUENCE_LEVELS = [\"reversible\", \"recoverable\", \"irreversible\"] as const;\n\nexport type ConsequenceLevel = (typeof CONSEQUENCE_LEVELS)[number];\n\n/** How a destructive action must be protected. */\nexport type Protection = \"undo\" | \"confirm\" | \"confirm-typed\" | \"confirm-hold\";\n\nexport interface DestructivePolicy {\n /** What the action destroys. */\n readonly level: ConsequenceLevel;\n /** The protection TEA UI requires. */\n readonly protection: Protection;\n /** Whether the consequence must be spelled out in the dialog body. */\n readonly explainConsequence: boolean;\n /** Whether the primary button may be the destructive one. */\n readonly destructivePrimary: boolean;\n}\n\n/**\n * The policy table.\n *\n * `reversible` — undo is offered instead of a dialog. Nothing is asked.\n * `recoverable` — a confirm dialog, because the data can be recovered but the\n * user would notice its absence. The primary button is NOT\n * destructive: cancelling must be the easy path.\n * `irreversible` — a confirm dialog that names the consequence, and for\n * anything that cannot be undone at all, requires typing the\n * resource name. Cancelling stays the default focus.\n */\nexport const DESTRUCTIVE_POLICY: Readonly<Record<ConsequenceLevel, DestructivePolicy>> = {\n reversible: {\n level: \"reversible\",\n protection: \"undo\",\n explainConsequence: false,\n destructivePrimary: false,\n },\n recoverable: {\n level: \"recoverable\",\n protection: \"confirm\",\n explainConsequence: true,\n destructivePrimary: false,\n },\n irreversible: {\n level: \"irreversible\",\n protection: \"confirm-typed\",\n explainConsequence: true,\n destructivePrimary: false,\n },\n};\n\nexport function destructivePolicy(level: ConsequenceLevel): DestructivePolicy {\n return DESTRUCTIVE_POLICY[level];\n}\n\n/** The verb pair an alert dialog uses, per the content standard (§22). */\nexport const DESTRUCTIVE_VERBS = {\n confirm: {\n reversible: \"Ausführen\",\n recoverable: \"Trotzdem fortfahren\",\n irreversible: \"Endgültig löschen\",\n },\n cancel: \"Abbrechen\",\n} as const;\n\n/**\n * The consequence sentence a dialog body must contain. Written as a function\n * rather than a template so the noun stays in one place — a dialog that says\n * \"wird gelöscht\" next to a button that says \"Wiederherstellen\" is worse than\n * no dialog.\n */\nexport function consequenceSentence(what: string, level: ConsequenceLevel): string {\n switch (level) {\n case \"reversible\":\n return `${what} wird kurz zurückgenommen werden können.`;\n case \"recoverable\":\n return `${what} wird endgültig entfernt. Eine Wiederherstellung ist nur aus einer Sicherung möglich.`;\n case \"irreversible\":\n return `${what} wird endgültig entfernt. Dieser Schritt kann nicht rückgängig gemacht werden.`;\n }\n}\n"],"mappings":"AAcO,MAAM,qBAAqB,CAAC,cAAc,eAAe,cAAc;AA6BvE,MAAM,qBAA4E;AAAA,EACvF,YAAY;AAAA,IACV,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,EACtB;AAAA,EACA,aAAa;AAAA,IACX,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,EACtB;AAAA,EACA,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,EACtB;AACF;AAEO,SAAS,kBAAkB,OAA4C;AAC5E,SAAO,mBAAmB,KAAK;AACjC;AAGO,MAAM,oBAAoB;AAAA,EAC/B,SAAS;AAAA,IACP,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,cAAc;AAAA,EAChB;AAAA,EACA,QAAQ;AACV;AAQO,SAAS,oBAAoB,MAAc,OAAiC;AACjF,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,aAAO,GAAG,IAAI;AAAA,IAChB,KAAK;AACH,aAAO,GAAG,IAAI;AAAA,IAChB,KAAK;AACH,aAAO,GAAG,IAAI;AAAA,EAClB;AACF;","names":[]}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * TEA UI — error anatomy.
3
+ *
4
+ * An error message is not a string, it is an answer to four questions. A
5
+ * message that answers fewer than all four is not "brief" — it is
6
+ * unactionable, and the user is forced to file a support request to find out
7
+ * what happened.
8
+ *
9
+ * 1. What happened?
10
+ * 2. Why did it happen?
11
+ * 3. What can the user do?
12
+ * 4. Can the system recover on its own?
13
+ *
14
+ * The audit found `"Error 500"`, `"Failed"` and `"IPC-Fehler"` in user-facing
15
+ * positions, the last of which was not merely unhelpful but actively wrong: the
16
+ * transport was HTTP, not IPC.
17
+ *
18
+ * {@link ErrorAnatomy} is the type every error surface in TEA UI accepts, and
19
+ * {@link renderErrorMessage} produces the headline sentence from it. The
20
+ * technical detail is optional and belongs behind a disclosure — it is for the
21
+ * person filing the report, not for the person who hit the problem.
22
+ */
23
+ type ErrorRecovery =
24
+ /** Nothing the user can do; the system will retry on its own. */
25
+ "automatic"
26
+ /** A single obvious action, offered as the primary action. */
27
+ | "action"
28
+ /** Several possible causes, so the message must not guess. */
29
+ | "choice"
30
+ /** Nothing can be done here; the user must leave the screen. */
31
+ | "none";
32
+ interface ErrorAnatomy {
33
+ /** Short, human summary of what failed. Never a status code alone. */
34
+ readonly title: string;
35
+ /** Why it failed, in one sentence. "The connection to the server timed out." */
36
+ readonly detail: string;
37
+ /** What the user can do next. Empty only when `recovery` is `automatic`. */
38
+ readonly action?: string;
39
+ /** Label for `action`, e.g. "Erneut versuchen". */
40
+ readonly actionLabel?: string;
41
+ /** Whether the system can recover without the user. */
42
+ readonly recovery: ErrorRecovery;
43
+ /** Raw detail — stack, request id, upstream body. Disclosure material. */
44
+ readonly technical?: string;
45
+ /** A support or debug reference the user can quote. */
46
+ readonly reference?: string;
47
+ }
48
+ declare const ERROR_TITLES: {
49
+ readonly network: "Verbindung fehlgeschlagen";
50
+ readonly timeout: "Zeitüberschreitung";
51
+ readonly unauthorized: "Nicht angemeldet";
52
+ readonly forbidden: "Keine Berechtigung";
53
+ readonly notFound: "Nicht gefunden";
54
+ readonly validation: "Eingabe prüfen";
55
+ readonly conflict: "Konflikt";
56
+ readonly rateLimit: "Zu viele Anfragen";
57
+ readonly server: "Serverfehler";
58
+ readonly unknown: "Unerwarteter Fehler";
59
+ };
60
+ type ErrorKind = keyof typeof ERROR_TITLES;
61
+ /**
62
+ * The headline a user reads: what happened, plus why, in one sentence.
63
+ * Deliberately never a bare code, and deliberately never "Something went
64
+ * wrong", which states nothing and asks the user to do the diagnosis.
65
+ */
66
+ declare function renderErrorMessage(error: ErrorAnatomy): string;
67
+ /**
68
+ * Whether an error surface should expose a retry affordance. Only true when a
69
+ * retry is a real, likely-to-succeed action — offering it for a validation
70
+ * error teaches people that buttons do nothing.
71
+ */
72
+ declare function shouldOfferRetry(error: ErrorAnatomy): boolean;
73
+ /** Whether technical detail belongs behind a disclosure rather than inline. */
74
+ declare function hasTechnicalDetail(error: ErrorAnatomy): boolean;
75
+ /**
76
+ * Build an {@link ErrorAnatomy} from anything thrown, without inventing a
77
+ * message. Unrecognised values produce a real title and a real next step, and
78
+ * never leak `[object Object]`.
79
+ */
80
+ declare function toErrorAnatomy(cause: unknown, fallback?: Partial<ErrorAnatomy>): ErrorAnatomy;
81
+
82
+ export { ERROR_TITLES, type ErrorAnatomy, type ErrorKind, type ErrorRecovery, hasTechnicalDetail, renderErrorMessage, shouldOfferRetry, toErrorAnatomy };
@@ -0,0 +1,68 @@
1
+ const ERROR_TITLES = {
2
+ network: "Verbindung fehlgeschlagen",
3
+ timeout: "Zeit\xFCberschreitung",
4
+ unauthorized: "Nicht angemeldet",
5
+ forbidden: "Keine Berechtigung",
6
+ notFound: "Nicht gefunden",
7
+ validation: "Eingabe pr\xFCfen",
8
+ conflict: "Konflikt",
9
+ rateLimit: "Zu viele Anfragen",
10
+ server: "Serverfehler",
11
+ unknown: "Unerwarteter Fehler"
12
+ };
13
+ function renderErrorMessage(error) {
14
+ const detail = error.detail.trim();
15
+ if (!detail) return error.title;
16
+ return `${error.title}. ${detail}`;
17
+ }
18
+ function shouldOfferRetry(error) {
19
+ return error.recovery === "action" || error.recovery === "automatic";
20
+ }
21
+ function hasTechnicalDetail(error) {
22
+ return typeof error.technical === "string" && error.technical.trim().length > 0;
23
+ }
24
+ function toErrorAnatomy(cause, fallback = {}) {
25
+ if (isErrorAnatomy(cause)) return cause;
26
+ if (cause instanceof Error) {
27
+ return {
28
+ title: ERROR_TITLES.unknown,
29
+ detail: cause.message || "Der Vorgang konnte nicht abgeschlossen werden.",
30
+ recovery: "action",
31
+ action: "Der Vorgang kann wiederholt werden.",
32
+ actionLabel: "Erneut versuchen",
33
+ // The stack is disclosure material, not a headline. It is attached here so
34
+ // an error surface can offer it without the product having to remember.
35
+ ...isDevelopment() && cause.stack ? { technical: cause.stack } : {},
36
+ ...fallback
37
+ };
38
+ }
39
+ return {
40
+ title: ERROR_TITLES.unknown,
41
+ detail: "Der Vorgang konnte nicht abgeschlossen werden.",
42
+ recovery: "action",
43
+ action: "Der Vorgang kann wiederholt werden.",
44
+ actionLabel: "Erneut versuchen",
45
+ ...fallback
46
+ };
47
+ }
48
+ function isDevelopment() {
49
+ try {
50
+ const meta = import.meta;
51
+ const dev = meta.env?.DEV ?? meta.env?.MODE;
52
+ if (typeof dev === "boolean") return dev;
53
+ if (typeof dev === "string") return dev === "development";
54
+ } catch {
55
+ }
56
+ return false;
57
+ }
58
+ function isErrorAnatomy(value) {
59
+ return typeof value === "object" && value !== null && typeof value.title === "string" && typeof value.detail === "string";
60
+ }
61
+ export {
62
+ ERROR_TITLES,
63
+ hasTechnicalDetail,
64
+ renderErrorMessage,
65
+ shouldOfferRetry,
66
+ toErrorAnatomy
67
+ };
68
+ //# sourceMappingURL=error-anatomy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/error-anatomy.ts"],"sourcesContent":["/**\n * TEA UI — error anatomy.\n *\n * An error message is not a string, it is an answer to four questions. A\n * message that answers fewer than all four is not \"brief\" — it is\n * unactionable, and the user is forced to file a support request to find out\n * what happened.\n *\n * 1. What happened?\n * 2. Why did it happen?\n * 3. What can the user do?\n * 4. Can the system recover on its own?\n *\n * The audit found `\"Error 500\"`, `\"Failed\"` and `\"IPC-Fehler\"` in user-facing\n * positions, the last of which was not merely unhelpful but actively wrong: the\n * transport was HTTP, not IPC.\n *\n * {@link ErrorAnatomy} is the type every error surface in TEA UI accepts, and\n * {@link renderErrorMessage} produces the headline sentence from it. The\n * technical detail is optional and belongs behind a disclosure — it is for the\n * person filing the report, not for the person who hit the problem.\n */\n\nexport type ErrorRecovery =\n /** Nothing the user can do; the system will retry on its own. */\n | \"automatic\"\n /** A single obvious action, offered as the primary action. */\n | \"action\"\n /** Several possible causes, so the message must not guess. */\n | \"choice\"\n /** Nothing can be done here; the user must leave the screen. */\n | \"none\";\n\nexport interface ErrorAnatomy {\n /** Short, human summary of what failed. Never a status code alone. */\n readonly title: string;\n /** Why it failed, in one sentence. \"The connection to the server timed out.\" */\n readonly detail: string;\n /** What the user can do next. Empty only when `recovery` is `automatic`. */\n readonly action?: string;\n /** Label for `action`, e.g. \"Erneut versuchen\". */\n readonly actionLabel?: string;\n /** Whether the system can recover without the user. */\n readonly recovery: ErrorRecovery;\n /** Raw detail — stack, request id, upstream body. Disclosure material. */\n readonly technical?: string;\n /** A support or debug reference the user can quote. */\n readonly reference?: string;\n}\n\nexport const ERROR_TITLES = {\n network: \"Verbindung fehlgeschlagen\",\n timeout: \"Zeitüberschreitung\",\n unauthorized: \"Nicht angemeldet\",\n forbidden: \"Keine Berechtigung\",\n notFound: \"Nicht gefunden\",\n validation: \"Eingabe prüfen\",\n conflict: \"Konflikt\",\n rateLimit: \"Zu viele Anfragen\",\n server: \"Serverfehler\",\n unknown: \"Unerwarteter Fehler\",\n} as const;\n\nexport type ErrorKind = keyof typeof ERROR_TITLES;\n\n/**\n * The headline a user reads: what happened, plus why, in one sentence.\n * Deliberately never a bare code, and deliberately never \"Something went\n * wrong\", which states nothing and asks the user to do the diagnosis.\n */\nexport function renderErrorMessage(error: ErrorAnatomy): string {\n const detail = error.detail.trim();\n if (!detail) return error.title;\n return `${error.title}. ${detail}`;\n}\n\n/**\n * Whether an error surface should expose a retry affordance. Only true when a\n * retry is a real, likely-to-succeed action — offering it for a validation\n * error teaches people that buttons do nothing.\n */\nexport function shouldOfferRetry(error: ErrorAnatomy): boolean {\n return error.recovery === \"action\" || error.recovery === \"automatic\";\n}\n\n/** Whether technical detail belongs behind a disclosure rather than inline. */\nexport function hasTechnicalDetail(error: ErrorAnatomy): boolean {\n return typeof error.technical === \"string\" && error.technical.trim().length > 0;\n}\n\n/**\n * Build an {@link ErrorAnatomy} from anything thrown, without inventing a\n * message. Unrecognised values produce a real title and a real next step, and\n * never leak `[object Object]`.\n */\nexport function toErrorAnatomy(cause: unknown, fallback: Partial<ErrorAnatomy> = {}): ErrorAnatomy {\n if (isErrorAnatomy(cause)) return cause;\n\n if (cause instanceof Error) {\n return {\n title: ERROR_TITLES.unknown,\n detail: cause.message || \"Der Vorgang konnte nicht abgeschlossen werden.\",\n recovery: \"action\",\n action: \"Der Vorgang kann wiederholt werden.\",\n actionLabel: \"Erneut versuchen\",\n // The stack is disclosure material, not a headline. It is attached here so\n // an error surface can offer it without the product having to remember.\n ...(isDevelopment() && cause.stack ? { technical: cause.stack } : {}),\n ...fallback,\n };\n }\n\n return {\n title: ERROR_TITLES.unknown,\n detail: \"Der Vorgang konnte nicht abgeschlossen werden.\",\n recovery: \"action\",\n action: \"Der Vorgang kann wiederholt werden.\",\n actionLabel: \"Erneut versuchen\",\n ...fallback,\n };\n}\n\n/**\n * Environment check that does not assume a bundler.\n *\n * This package is imported by browser bundles, by Node scripts and by tests, so\n * neither `process` nor `import.meta.env` is available everywhere it runs — which\n * is why both are behind a guard and why nothing here declares a Node type\n * dependency. A package that cannot say where it runs cannot be used everywhere.\n */\nfunction isDevelopment(): boolean {\n try {\n const meta = import.meta as ImportMeta & { env?: Record<string, unknown> };\n const dev = meta.env?.DEV ?? meta.env?.MODE;\n if (typeof dev === \"boolean\") return dev;\n if (typeof dev === \"string\") return dev === \"development\";\n } catch {\n /* not a bundler context */\n }\n return false;\n}\n\nfunction isErrorAnatomy(value: unknown): value is ErrorAnatomy {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as ErrorAnatomy).title === \"string\" &&\n typeof (value as ErrorAnatomy).detail === \"string\"\n );\n}\n"],"mappings":"AAkDO,MAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,EACT,SAAS;AAAA,EACT,cAAc;AAAA,EACd,WAAW;AAAA,EACX,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,SAAS;AACX;AASO,SAAS,mBAAmB,OAA6B;AAC9D,QAAM,SAAS,MAAM,OAAO,KAAK;AACjC,MAAI,CAAC,OAAQ,QAAO,MAAM;AAC1B,SAAO,GAAG,MAAM,KAAK,KAAK,MAAM;AAClC;AAOO,SAAS,iBAAiB,OAA8B;AAC7D,SAAO,MAAM,aAAa,YAAY,MAAM,aAAa;AAC3D;AAGO,SAAS,mBAAmB,OAA8B;AAC/D,SAAO,OAAO,MAAM,cAAc,YAAY,MAAM,UAAU,KAAK,EAAE,SAAS;AAChF;AAOO,SAAS,eAAe,OAAgB,WAAkC,CAAC,GAAiB;AACjG,MAAI,eAAe,KAAK,EAAG,QAAO;AAElC,MAAI,iBAAiB,OAAO;AAC1B,WAAO;AAAA,MACL,OAAO,aAAa;AAAA,MACpB,QAAQ,MAAM,WAAW;AAAA,MACzB,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,aAAa;AAAA;AAAA;AAAA,MAGb,GAAI,cAAc,KAAK,MAAM,QAAQ,EAAE,WAAW,MAAM,MAAM,IAAI,CAAC;AAAA,MACnE,GAAG;AAAA,IACL;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO,aAAa;AAAA,IACpB,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,GAAG;AAAA,EACL;AACF;AAUA,SAAS,gBAAyB;AAChC,MAAI;AACF,UAAM,OAAO;AACb,UAAM,MAAM,KAAK,KAAK,OAAO,KAAK,KAAK;AACvC,QAAI,OAAO,QAAQ,UAAW,QAAO;AACrC,QAAI,OAAO,QAAQ,SAAU,QAAO,QAAQ;AAAA,EAC9C,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,SAAS,eAAe,OAAuC;AAC7D,SACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAAuB,UAAU,YACzC,OAAQ,MAAuB,WAAW;AAE9C;","names":[]}
@@ -0,0 +1,65 @@
1
+ import { Tone } from '@tea-ui/tokens';
2
+
3
+ /**
4
+ * TEA UI — the feedback state model.
5
+ *
6
+ * Every asynchronous surface in a TEA product is in exactly one of these
7
+ * states, and the states are named here so that two products handling the same
8
+ * situation behave the same way. The audit found eight independent
9
+ * "Lade …" loaders, ten empty states, nine error banners and four dialects of
10
+ * each, with no shared naming and inconsistent live-region behaviour.
11
+ *
12
+ * The progression the model expects, and the reason for it:
13
+ *
14
+ * action -> immediate acknowledgement -> progress -> result -> next action
15
+ *
16
+ * A user action must never be silent. It must never show a spinner over
17
+ * content the user is already reading, either.
18
+ */
19
+ declare const FEEDBACK_STATES: readonly ["idle", "loading", "refreshing", "processing", "success", "warning", "error", "empty", "disabled", "offline", "unauthorized", "forbidden", "notFound", "maintenance", "stale", "syncing", "retrying"];
20
+ type FeedbackState = (typeof FEEDBACK_STATES)[number];
21
+ type FeedbackKind = "initial" | "background" | "blocking";
22
+ interface FeedbackMeta {
23
+ /**
24
+ * `initial` — nothing to show yet, so a skeleton or a spinner replaces the
25
+ * content. Used once, on first load.
26
+ * `background`— content exists and stays visible; the state is communicated
27
+ * without replacing anything. Used for refresh, sync, save.
28
+ * `blocking` — the user cannot proceed; a spinner or progress is required.
29
+ */
30
+ readonly kind: FeedbackKind;
31
+ /** The tone this state should render in, where a colour applies. */
32
+ readonly tone: Tone;
33
+ /**
34
+ * Whether this state is a problem the user must resolve, or merely
35
+ * information. Drives whether the surface offers a recovery action.
36
+ */
37
+ readonly blocking: boolean;
38
+ /** Whether a live region should announce a change into this state. */
39
+ readonly announce: boolean;
40
+ /** Default German noun phrase for a progress line, e.g. "Wird geladen". */
41
+ readonly label: string;
42
+ }
43
+ declare const FEEDBACK: Readonly<Record<FeedbackState, FeedbackMeta>>;
44
+ /**
45
+ * States that must never replace content that is already on screen.
46
+ *
47
+ * The audit's clearest single defect: both source projects replace a whole
48
+ * region with a centred spinner on every poll, so a five-second auto-refresh
49
+ * flashes the UI away from content the user was reading. `refreshing`,
50
+ * `syncing`, `stale` and `retrying` must be communicated as an overlay or a
51
+ * quiet indicator over preserved content.
52
+ */
53
+ declare const NON_DESTRUCTIVE_STATES: readonly ["refreshing", "syncing", "stale", "retrying"];
54
+ type NonDestructiveState = (typeof NON_DESTRUCTIVE_STATES)[number];
55
+ declare function isNonDestructive(state: FeedbackState): state is NonDestructiveState;
56
+ declare function feedbackMeta(state: FeedbackState): FeedbackMeta;
57
+ /**
58
+ * Build a `data-tea-state` value for a surface, so a state is inspectable from
59
+ * the DOM and assertable in a test rather than only visible.
60
+ */
61
+ declare function feedbackAttr(state: FeedbackState): {
62
+ "data-tea-state": FeedbackState;
63
+ };
64
+
65
+ export { FEEDBACK, FEEDBACK_STATES, type FeedbackKind, type FeedbackMeta, type FeedbackState, NON_DESTRUCTIVE_STATES, type NonDestructiveState, feedbackAttr, feedbackMeta, isNonDestructive };
@@ -0,0 +1,159 @@
1
+ const FEEDBACK_STATES = [
2
+ "idle",
3
+ "loading",
4
+ "refreshing",
5
+ "processing",
6
+ "success",
7
+ "warning",
8
+ "error",
9
+ "empty",
10
+ "disabled",
11
+ "offline",
12
+ "unauthorized",
13
+ "forbidden",
14
+ "notFound",
15
+ "maintenance",
16
+ "stale",
17
+ "syncing",
18
+ "retrying"
19
+ ];
20
+ const FEEDBACK = {
21
+ idle: {
22
+ kind: "background",
23
+ tone: "neutral",
24
+ blocking: false,
25
+ announce: false,
26
+ label: "Bereit"
27
+ },
28
+ loading: {
29
+ kind: "initial",
30
+ tone: "neutral",
31
+ blocking: true,
32
+ announce: true,
33
+ label: "Wird geladen"
34
+ },
35
+ refreshing: {
36
+ kind: "background",
37
+ tone: "neutral",
38
+ blocking: false,
39
+ announce: true,
40
+ label: "Wird aktualisiert"
41
+ },
42
+ processing: {
43
+ kind: "blocking",
44
+ tone: "info",
45
+ blocking: true,
46
+ announce: true,
47
+ label: "Wird verarbeitet"
48
+ },
49
+ success: {
50
+ kind: "background",
51
+ tone: "positive",
52
+ blocking: false,
53
+ announce: true,
54
+ label: "Erfolgreich"
55
+ },
56
+ warning: {
57
+ kind: "background",
58
+ tone: "caution",
59
+ blocking: false,
60
+ announce: true,
61
+ label: "Hinweis"
62
+ },
63
+ error: {
64
+ kind: "background",
65
+ tone: "critical",
66
+ blocking: false,
67
+ announce: true,
68
+ label: "Fehler"
69
+ },
70
+ empty: {
71
+ kind: "initial",
72
+ tone: "neutral",
73
+ blocking: false,
74
+ announce: true,
75
+ label: "Keine Eintr\xE4ge"
76
+ },
77
+ disabled: {
78
+ kind: "background",
79
+ tone: "neutral",
80
+ blocking: false,
81
+ announce: false,
82
+ label: "Nicht verf\xFCgbar"
83
+ },
84
+ offline: {
85
+ kind: "background",
86
+ tone: "caution",
87
+ blocking: false,
88
+ announce: true,
89
+ label: "Offline"
90
+ },
91
+ unauthorized: {
92
+ kind: "background",
93
+ tone: "caution",
94
+ blocking: true,
95
+ announce: true,
96
+ label: "Nicht angemeldet"
97
+ },
98
+ forbidden: {
99
+ kind: "background",
100
+ tone: "caution",
101
+ blocking: true,
102
+ announce: true,
103
+ label: "Keine Berechtigung"
104
+ },
105
+ notFound: {
106
+ kind: "background",
107
+ tone: "neutral",
108
+ blocking: false,
109
+ announce: true,
110
+ label: "Nicht gefunden"
111
+ },
112
+ maintenance: {
113
+ kind: "background",
114
+ tone: "info",
115
+ blocking: true,
116
+ announce: true,
117
+ label: "Wartungsmodus"
118
+ },
119
+ stale: {
120
+ kind: "background",
121
+ tone: "caution",
122
+ blocking: false,
123
+ announce: true,
124
+ label: "Veraltete Daten"
125
+ },
126
+ syncing: {
127
+ kind: "background",
128
+ tone: "info",
129
+ blocking: false,
130
+ announce: true,
131
+ label: "Wird synchronisiert"
132
+ },
133
+ retrying: {
134
+ kind: "background",
135
+ tone: "info",
136
+ blocking: false,
137
+ announce: true,
138
+ label: "Neuer Versuch"
139
+ }
140
+ };
141
+ const NON_DESTRUCTIVE_STATES = ["refreshing", "syncing", "stale", "retrying"];
142
+ function isNonDestructive(state) {
143
+ return NON_DESTRUCTIVE_STATES.includes(state);
144
+ }
145
+ function feedbackMeta(state) {
146
+ return FEEDBACK[state];
147
+ }
148
+ function feedbackAttr(state) {
149
+ return { "data-tea-state": state };
150
+ }
151
+ export {
152
+ FEEDBACK,
153
+ FEEDBACK_STATES,
154
+ NON_DESTRUCTIVE_STATES,
155
+ feedbackAttr,
156
+ feedbackMeta,
157
+ isNonDestructive
158
+ };
159
+ //# sourceMappingURL=feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/feedback.ts"],"sourcesContent":["import type { Tone } from \"@tea-ui/tokens\";\n\n/**\n * TEA UI — the feedback state model.\n *\n * Every asynchronous surface in a TEA product is in exactly one of these\n * states, and the states are named here so that two products handling the same\n * situation behave the same way. The audit found eight independent\n * \"Lade …\" loaders, ten empty states, nine error banners and four dialects of\n * each, with no shared naming and inconsistent live-region behaviour.\n *\n * The progression the model expects, and the reason for it:\n *\n * action -> immediate acknowledgement -> progress -> result -> next action\n *\n * A user action must never be silent. It must never show a spinner over\n * content the user is already reading, either.\n */\n\nexport const FEEDBACK_STATES = [\n \"idle\",\n \"loading\",\n \"refreshing\",\n \"processing\",\n \"success\",\n \"warning\",\n \"error\",\n \"empty\",\n \"disabled\",\n \"offline\",\n \"unauthorized\",\n \"forbidden\",\n \"notFound\",\n \"maintenance\",\n \"stale\",\n \"syncing\",\n \"retrying\",\n] as const;\n\nexport type FeedbackState = (typeof FEEDBACK_STATES)[number];\n\nexport type FeedbackKind = \"initial\" | \"background\" | \"blocking\";\n\nexport interface FeedbackMeta {\n /**\n * `initial` — nothing to show yet, so a skeleton or a spinner replaces the\n * content. Used once, on first load.\n * `background`— content exists and stays visible; the state is communicated\n * without replacing anything. Used for refresh, sync, save.\n * `blocking` — the user cannot proceed; a spinner or progress is required.\n */\n readonly kind: FeedbackKind;\n /** The tone this state should render in, where a colour applies. */\n readonly tone: Tone;\n /**\n * Whether this state is a problem the user must resolve, or merely\n * information. Drives whether the surface offers a recovery action.\n */\n readonly blocking: boolean;\n /** Whether a live region should announce a change into this state. */\n readonly announce: boolean;\n /** Default German noun phrase for a progress line, e.g. \"Wird geladen\". */\n readonly label: string;\n}\n\nexport const FEEDBACK: Readonly<Record<FeedbackState, FeedbackMeta>> = {\n idle: {\n kind: \"background\",\n tone: \"neutral\",\n blocking: false,\n announce: false,\n label: \"Bereit\",\n },\n loading: {\n kind: \"initial\",\n tone: \"neutral\",\n blocking: true,\n announce: true,\n label: \"Wird geladen\",\n },\n refreshing: {\n kind: \"background\",\n tone: \"neutral\",\n blocking: false,\n announce: true,\n label: \"Wird aktualisiert\",\n },\n processing: {\n kind: \"blocking\",\n tone: \"info\",\n blocking: true,\n announce: true,\n label: \"Wird verarbeitet\",\n },\n success: {\n kind: \"background\",\n tone: \"positive\",\n blocking: false,\n announce: true,\n label: \"Erfolgreich\",\n },\n warning: {\n kind: \"background\",\n tone: \"caution\",\n blocking: false,\n announce: true,\n label: \"Hinweis\",\n },\n error: {\n kind: \"background\",\n tone: \"critical\",\n blocking: false,\n announce: true,\n label: \"Fehler\",\n },\n empty: {\n kind: \"initial\",\n tone: \"neutral\",\n blocking: false,\n announce: true,\n label: \"Keine Einträge\",\n },\n disabled: {\n kind: \"background\",\n tone: \"neutral\",\n blocking: false,\n announce: false,\n label: \"Nicht verfügbar\",\n },\n offline: {\n kind: \"background\",\n tone: \"caution\",\n blocking: false,\n announce: true,\n label: \"Offline\",\n },\n unauthorized: {\n kind: \"background\",\n tone: \"caution\",\n blocking: true,\n announce: true,\n label: \"Nicht angemeldet\",\n },\n forbidden: {\n kind: \"background\",\n tone: \"caution\",\n blocking: true,\n announce: true,\n label: \"Keine Berechtigung\",\n },\n notFound: {\n kind: \"background\",\n tone: \"neutral\",\n blocking: false,\n announce: true,\n label: \"Nicht gefunden\",\n },\n maintenance: {\n kind: \"background\",\n tone: \"info\",\n blocking: true,\n announce: true,\n label: \"Wartungsmodus\",\n },\n stale: {\n kind: \"background\",\n tone: \"caution\",\n blocking: false,\n announce: true,\n label: \"Veraltete Daten\",\n },\n syncing: {\n kind: \"background\",\n tone: \"info\",\n blocking: false,\n announce: true,\n label: \"Wird synchronisiert\",\n },\n retrying: {\n kind: \"background\",\n tone: \"info\",\n blocking: false,\n announce: true,\n label: \"Neuer Versuch\",\n },\n};\n\n/**\n * States that must never replace content that is already on screen.\n *\n * The audit's clearest single defect: both source projects replace a whole\n * region with a centred spinner on every poll, so a five-second auto-refresh\n * flashes the UI away from content the user was reading. `refreshing`,\n * `syncing`, `stale` and `retrying` must be communicated as an overlay or a\n * quiet indicator over preserved content.\n */\nexport const NON_DESTRUCTIVE_STATES = [\"refreshing\", \"syncing\", \"stale\", \"retrying\"] as const;\n\nexport type NonDestructiveState = (typeof NON_DESTRUCTIVE_STATES)[number];\n\nexport function isNonDestructive(state: FeedbackState): state is NonDestructiveState {\n return (NON_DESTRUCTIVE_STATES as readonly string[]).includes(state);\n}\n\nexport function feedbackMeta(state: FeedbackState): FeedbackMeta {\n return FEEDBACK[state];\n}\n\n/**\n * Build a `data-tea-state` value for a surface, so a state is inspectable from\n * the DOM and assertable in a test rather than only visible.\n */\nexport function feedbackAttr(state: FeedbackState): { \"data-tea-state\": FeedbackState } {\n return { \"data-tea-state\": state };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * TEA UI — form standards.
3
+ *
4
+ * The audit found the same form field implemented five times in one project and
5
+ * three times in the other, with four of the five using a `<label>` that was
6
+ * never associated with its control — so most inputs in both products had no
7
+ * accessible name at all. These constants are what make a field correct by
8
+ * default rather than by remembering.
9
+ */
10
+ type FieldControl = "text" | "email" | "password" | "search" | "number" | "date" | "time" | "url" | "tel" | "textarea" | "select" | "checkbox" | "radio" | "switch" | "slider" | "file" | "otp";
11
+ /**
12
+ * When validation runs. "Too eager" and a form nobody can finish are the same
13
+ * defect, so the default is: validate on blur, re-validate on change *after*
14
+ * the field has been touched, and validate on submit. Never on every keystroke
15
+ * from the first character.
16
+ */
17
+ declare const VALIDATION_TIMING: {
18
+ readonly onSubmit: "always";
19
+ readonly onBlur: true;
20
+ readonly onChangeAfterTouched: true;
21
+ readonly onChangeFromFirstKeystroke: false;
22
+ };
23
+ interface FormFieldRules {
24
+ /** The control type, which decides the association and keyboard contract. */
25
+ readonly control: FieldControl;
26
+ /** `htmlFor` is mandatory. A label without it does not name the control. */
27
+ readonly requiresLabelAssociation: true;
28
+ /** A description must be linked with `aria-describedby`, not merely placed nearby. */
29
+ readonly requiresDescribedBy: true;
30
+ /** An error must be linked with `aria-describedby` AND announced. */
31
+ readonly requiresErrorAnnouncement: true;
32
+ /** Icon-only controls require an accessible name. */
33
+ readonly requiresAccessibleName: true;
34
+ /** Required state must be conveyed programmatically, not only with an asterisk. */
35
+ readonly requiresRequiredState: true;
36
+ }
37
+ declare const FIELD_RULES: Readonly<Record<FieldControl, FormFieldRules>>;
38
+ /**
39
+ * `autocomplete` tokens. Auto-fill is an accessibility feature, not a
40
+ * convenience: without it a user relying on a password manager has to type
41
+ * what the browser already knows.
42
+ */
43
+ declare const AUTOCOMPLETE: {
44
+ readonly name: "name";
45
+ readonly givenName: "given-name";
46
+ readonly familyName: "family-name";
47
+ readonly email: "email";
48
+ readonly username: "username";
49
+ readonly currentPassword: "current-password";
50
+ readonly newPassword: "new-password";
51
+ readonly oneTimeCode: "one-time-code";
52
+ readonly organization: "organization";
53
+ readonly streetAddress: "street-address";
54
+ readonly postalCode: "postal-code";
55
+ readonly addressLevel1: "address-level1";
56
+ readonly addressLevel2: "address-level2";
57
+ readonly country: "country";
58
+ readonly tel: "tel";
59
+ readonly url: "url";
60
+ readonly newPasswordConfirm: "new-password";
61
+ };
62
+ /**
63
+ * The unsaved-changes standard. Losing typed input to a navigation is a data
64
+ * loss bug, so the guard is mandatory wherever a form can be abandoned — and
65
+ * it is a TEA UI pattern, not something each product re-implements.
66
+ */
67
+ declare const UNSAVED_CHANGES: {
68
+ /** Wording of the guard. `du` register, per the content standard. */
69
+ readonly title: "Ungespeicherte Änderungen";
70
+ readonly detail: "Deine Änderungen wurden noch nicht gespeichert. Wenn du die Seite verlässt, gehen sie verloren.";
71
+ readonly confirmLabel: "Verwerfen und verlassen";
72
+ readonly cancelLabel: "Weiter bearbeiten";
73
+ /** Saving before navigating is the better path, so it is offered first. */
74
+ readonly preferSave: true;
75
+ };
76
+ /** Paste is never blocked. Masking an input must not break paste. */
77
+ declare const FORM_INTERACTION: {
78
+ readonly blockPaste: false;
79
+ readonly blockAutofill: false;
80
+ /** Multi-step forms must allow going back without losing input. */
81
+ readonly allowStepBack: true;
82
+ /** Validation must be correctable: a field the user cannot fix is a bug. */
83
+ readonly allowFixingErrors: true;
84
+ };
85
+
86
+ export { AUTOCOMPLETE, FIELD_RULES, FORM_INTERACTION, type FieldControl, type FormFieldRules, UNSAVED_CHANGES, VALIDATION_TIMING };