@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/dist/forms.js ADDED
@@ -0,0 +1,79 @@
1
+ const VALIDATION_TIMING = {
2
+ onSubmit: "always",
3
+ onBlur: true,
4
+ onChangeAfterTouched: true,
5
+ onChangeFromFirstKeystroke: false
6
+ };
7
+ const FIELD_RULES = {
8
+ text: base("text"),
9
+ email: base("email"),
10
+ password: base("password"),
11
+ search: base("search"),
12
+ number: base("number"),
13
+ date: base("date"),
14
+ time: base("time"),
15
+ url: base("url"),
16
+ tel: base("tel"),
17
+ textarea: base("textarea"),
18
+ select: base("select"),
19
+ checkbox: base("checkbox"),
20
+ radio: base("radio"),
21
+ switch: base("switch"),
22
+ slider: base("slider"),
23
+ file: base("file"),
24
+ otp: base("otp")
25
+ };
26
+ function base(control) {
27
+ return {
28
+ control,
29
+ requiresLabelAssociation: true,
30
+ requiresDescribedBy: true,
31
+ requiresErrorAnnouncement: true,
32
+ requiresAccessibleName: true,
33
+ requiresRequiredState: true
34
+ };
35
+ }
36
+ const AUTOCOMPLETE = {
37
+ name: "name",
38
+ givenName: "given-name",
39
+ familyName: "family-name",
40
+ email: "email",
41
+ username: "username",
42
+ currentPassword: "current-password",
43
+ newPassword: "new-password",
44
+ oneTimeCode: "one-time-code",
45
+ organization: "organization",
46
+ streetAddress: "street-address",
47
+ postalCode: "postal-code",
48
+ addressLevel1: "address-level1",
49
+ addressLevel2: "address-level2",
50
+ country: "country",
51
+ tel: "tel",
52
+ url: "url",
53
+ newPasswordConfirm: "new-password"
54
+ };
55
+ const UNSAVED_CHANGES = {
56
+ /** Wording of the guard. `du` register, per the content standard. */
57
+ title: "Ungespeicherte \xC4nderungen",
58
+ detail: "Deine \xC4nderungen wurden noch nicht gespeichert. Wenn du die Seite verl\xE4sst, gehen sie verloren.",
59
+ confirmLabel: "Verwerfen und verlassen",
60
+ cancelLabel: "Weiter bearbeiten",
61
+ /** Saving before navigating is the better path, so it is offered first. */
62
+ preferSave: true
63
+ };
64
+ const FORM_INTERACTION = {
65
+ blockPaste: false,
66
+ blockAutofill: false,
67
+ /** Multi-step forms must allow going back without losing input. */
68
+ allowStepBack: true,
69
+ /** Validation must be correctable: a field the user cannot fix is a bug. */
70
+ allowFixingErrors: true
71
+ };
72
+ export {
73
+ AUTOCOMPLETE,
74
+ FIELD_RULES,
75
+ FORM_INTERACTION,
76
+ UNSAVED_CHANGES,
77
+ VALIDATION_TIMING
78
+ };
79
+ //# sourceMappingURL=forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/forms.ts"],"sourcesContent":["/**\n * TEA UI — form standards.\n *\n * The audit found the same form field implemented five times in one project and\n * three times in the other, with four of the five using a `<label>` that was\n * never associated with its control — so most inputs in both products had no\n * accessible name at all. These constants are what make a field correct by\n * default rather than by remembering.\n */\n\nexport type FieldControl =\n | \"text\"\n | \"email\"\n | \"password\"\n | \"search\"\n | \"number\"\n | \"date\"\n | \"time\"\n | \"url\"\n | \"tel\"\n | \"textarea\"\n | \"select\"\n | \"checkbox\"\n | \"radio\"\n | \"switch\"\n | \"slider\"\n | \"file\"\n | \"otp\";\n\n/**\n * When validation runs. \"Too eager\" and a form nobody can finish are the same\n * defect, so the default is: validate on blur, re-validate on change *after*\n * the field has been touched, and validate on submit. Never on every keystroke\n * from the first character.\n */\nexport const VALIDATION_TIMING = {\n onSubmit: \"always\",\n onBlur: true,\n onChangeAfterTouched: true,\n onChangeFromFirstKeystroke: false,\n} as const;\n\nexport interface FormFieldRules {\n /** The control type, which decides the association and keyboard contract. */\n readonly control: FieldControl;\n /** `htmlFor` is mandatory. A label without it does not name the control. */\n readonly requiresLabelAssociation: true;\n /** A description must be linked with `aria-describedby`, not merely placed nearby. */\n readonly requiresDescribedBy: true;\n /** An error must be linked with `aria-describedby` AND announced. */\n readonly requiresErrorAnnouncement: true;\n /** Icon-only controls require an accessible name. */\n readonly requiresAccessibleName: true;\n /** Required state must be conveyed programmatically, not only with an asterisk. */\n readonly requiresRequiredState: true;\n}\n\nexport const FIELD_RULES: Readonly<Record<FieldControl, FormFieldRules>> = {\n text: base(\"text\"),\n email: base(\"email\"),\n password: base(\"password\"),\n search: base(\"search\"),\n number: base(\"number\"),\n date: base(\"date\"),\n time: base(\"time\"),\n url: base(\"url\"),\n tel: base(\"tel\"),\n textarea: base(\"textarea\"),\n select: base(\"select\"),\n checkbox: base(\"checkbox\"),\n radio: base(\"radio\"),\n switch: base(\"switch\"),\n slider: base(\"slider\"),\n file: base(\"file\"),\n otp: base(\"otp\"),\n};\n\nfunction base(control: FieldControl): FormFieldRules {\n return {\n control,\n requiresLabelAssociation: true,\n requiresDescribedBy: true,\n requiresErrorAnnouncement: true,\n requiresAccessibleName: true,\n requiresRequiredState: true,\n };\n}\n\n/**\n * `autocomplete` tokens. Auto-fill is an accessibility feature, not a\n * convenience: without it a user relying on a password manager has to type\n * what the browser already knows.\n */\nexport const AUTOCOMPLETE = {\n name: \"name\",\n givenName: \"given-name\",\n familyName: \"family-name\",\n email: \"email\",\n username: \"username\",\n currentPassword: \"current-password\",\n newPassword: \"new-password\",\n oneTimeCode: \"one-time-code\",\n organization: \"organization\",\n streetAddress: \"street-address\",\n postalCode: \"postal-code\",\n addressLevel1: \"address-level1\",\n addressLevel2: \"address-level2\",\n country: \"country\",\n tel: \"tel\",\n url: \"url\",\n newPasswordConfirm: \"new-password\",\n} as const;\n\n/**\n * The unsaved-changes standard. Losing typed input to a navigation is a data\n * loss bug, so the guard is mandatory wherever a form can be abandoned — and\n * it is a TEA UI pattern, not something each product re-implements.\n */\nexport const UNSAVED_CHANGES = {\n /** Wording of the guard. `du` register, per the content standard. */\n title: \"Ungespeicherte Änderungen\",\n detail: \"Deine Änderungen wurden noch nicht gespeichert. Wenn du die Seite verlässt, gehen sie verloren.\",\n confirmLabel: \"Verwerfen und verlassen\",\n cancelLabel: \"Weiter bearbeiten\",\n /** Saving before navigating is the better path, so it is offered first. */\n preferSave: true,\n} as const;\n\n/** Paste is never blocked. Masking an input must not break paste. */\nexport const FORM_INTERACTION = {\n blockPaste: false,\n blockAutofill: false,\n /** Multi-step forms must allow going back without losing input. */\n allowStepBack: true,\n /** Validation must be correctable: a field the user cannot fix is a bug. */\n allowFixingErrors: true,\n} as const;\n"],"mappings":"AAmCO,MAAM,oBAAoB;AAAA,EAC/B,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,sBAAsB;AAAA,EACtB,4BAA4B;AAC9B;AAiBO,MAAM,cAA8D;AAAA,EACzE,MAAM,KAAK,MAAM;AAAA,EACjB,OAAO,KAAK,OAAO;AAAA,EACnB,UAAU,KAAK,UAAU;AAAA,EACzB,QAAQ,KAAK,QAAQ;AAAA,EACrB,QAAQ,KAAK,QAAQ;AAAA,EACrB,MAAM,KAAK,MAAM;AAAA,EACjB,MAAM,KAAK,MAAM;AAAA,EACjB,KAAK,KAAK,KAAK;AAAA,EACf,KAAK,KAAK,KAAK;AAAA,EACf,UAAU,KAAK,UAAU;AAAA,EACzB,QAAQ,KAAK,QAAQ;AAAA,EACrB,UAAU,KAAK,UAAU;AAAA,EACzB,OAAO,KAAK,OAAO;AAAA,EACnB,QAAQ,KAAK,QAAQ;AAAA,EACrB,QAAQ,KAAK,QAAQ;AAAA,EACrB,MAAM,KAAK,MAAM;AAAA,EACjB,KAAK,KAAK,KAAK;AACjB;AAEA,SAAS,KAAK,SAAuC;AACnD,SAAO;AAAA,IACL;AAAA,IACA,0BAA0B;AAAA,IAC1B,qBAAqB;AAAA,IACrB,2BAA2B;AAAA,IAC3B,wBAAwB;AAAA,IACxB,uBAAuB;AAAA,EACzB;AACF;AAOO,MAAM,eAAe;AAAA,EAC1B,MAAM;AAAA,EACN,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,aAAa;AAAA,EACb,cAAc;AAAA,EACd,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,eAAe;AAAA,EACf,SAAS;AAAA,EACT,KAAK;AAAA,EACL,KAAK;AAAA,EACL,oBAAoB;AACtB;AAOO,MAAM,kBAAkB;AAAA;AAAA,EAE7B,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,aAAa;AAAA;AAAA,EAEb,YAAY;AACd;AAGO,MAAM,mBAAmB;AAAA,EAC9B,YAAY;AAAA,EACZ,eAAe;AAAA;AAAA,EAEf,eAAe;AAAA;AAAA,EAEf,mBAAmB;AACrB;","names":[]}
@@ -0,0 +1,9 @@
1
+ export { AnyStatusKey, STATUS, StatusDomain, StatusKey, StatusMeta, statusEntries, statusKeysWithTone, statusMeta } from './status.js';
2
+ export { DEFAULT_DENSITY, DENSITIES, Density, MOTION, TONES, Tone, isDensity } from '@tea-ui/tokens';
3
+ export { FEEDBACK, FEEDBACK_STATES, FeedbackKind, FeedbackMeta, FeedbackState, NON_DESTRUCTIVE_STATES, NonDestructiveState, feedbackAttr, feedbackMeta, isNonDestructive } from './feedback.js';
4
+ export { CONSEQUENCE_LEVELS, ConsequenceLevel, DESTRUCTIVE_POLICY, DESTRUCTIVE_VERBS, DestructivePolicy, Protection, consequenceSentence, destructivePolicy } from './destructive.js';
5
+ export { ERROR_TITLES, ErrorAnatomy, ErrorKind, ErrorRecovery, hasTechnicalDetail, renderErrorMessage, shouldOfferRetry, toErrorAnatomy } from './error-anatomy.js';
6
+ export { AUTOCOMPLETE, FIELD_RULES, FORM_INTERACTION, FieldControl, FormFieldRules, UNSAVED_CHANGES, VALIDATION_TIMING } from './forms.js';
7
+ export { COPY, FORBIDDEN_COPY, REGISTER, Register, fill } from './terminology.js';
8
+ export { IA_LIMITS, InformationArchitecture, NAVIGATION_MECHANISMS, NAVIGATION_RULES, NavigationMechanism, NavigationRule, URL_SYNC } from './navigation.js';
9
+ export { BACKGROUND_OPERATION, DATA_VIZ, DataVizRules, EMPTY_STATE_TITLES, EmptyStateAnatomy, EmptyStateTitle, LOADING_AFFORDANCE, MIN_SKELETON_MS, MOTION_PURPOSE, MotionPurpose, REDUCED_MOTION, TRANSITION_PATTERNS } from './motion.js';
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ import {
2
+ STATUS,
3
+ statusMeta,
4
+ statusEntries,
5
+ statusKeysWithTone
6
+ } from "./status.js";
7
+ import { TONES } from "@tea-ui/tokens";
8
+ import {
9
+ FEEDBACK,
10
+ FEEDBACK_STATES,
11
+ NON_DESTRUCTIVE_STATES,
12
+ feedbackMeta,
13
+ feedbackAttr,
14
+ isNonDestructive
15
+ } from "./feedback.js";
16
+ import {
17
+ DESTRUCTIVE_POLICY,
18
+ DESTRUCTIVE_VERBS,
19
+ CONSEQUENCE_LEVELS,
20
+ destructivePolicy,
21
+ consequenceSentence
22
+ } from "./destructive.js";
23
+ import {
24
+ ERROR_TITLES,
25
+ renderErrorMessage,
26
+ shouldOfferRetry,
27
+ hasTechnicalDetail,
28
+ toErrorAnatomy
29
+ } from "./error-anatomy.js";
30
+ import {
31
+ AUTOCOMPLETE,
32
+ FIELD_RULES,
33
+ FORM_INTERACTION,
34
+ UNSAVED_CHANGES,
35
+ VALIDATION_TIMING
36
+ } from "./forms.js";
37
+ import {
38
+ COPY,
39
+ FORBIDDEN_COPY,
40
+ REGISTER,
41
+ fill
42
+ } from "./terminology.js";
43
+ import {
44
+ IA_LIMITS,
45
+ NAVIGATION_RULES,
46
+ NAVIGATION_MECHANISMS,
47
+ URL_SYNC
48
+ } from "./navigation.js";
49
+ import {
50
+ BACKGROUND_OPERATION,
51
+ DATA_VIZ,
52
+ EMPTY_STATE_TITLES,
53
+ LOADING_AFFORDANCE,
54
+ MIN_SKELETON_MS,
55
+ MOTION,
56
+ MOTION_PURPOSE,
57
+ REDUCED_MOTION,
58
+ TRANSITION_PATTERNS
59
+ } from "./motion.js";
60
+ import {
61
+ DENSITIES,
62
+ DEFAULT_DENSITY,
63
+ isDensity
64
+ } from "@tea-ui/tokens";
65
+ export {
66
+ AUTOCOMPLETE,
67
+ BACKGROUND_OPERATION,
68
+ CONSEQUENCE_LEVELS,
69
+ COPY,
70
+ DATA_VIZ,
71
+ DEFAULT_DENSITY,
72
+ DENSITIES,
73
+ DESTRUCTIVE_POLICY,
74
+ DESTRUCTIVE_VERBS,
75
+ EMPTY_STATE_TITLES,
76
+ ERROR_TITLES,
77
+ FEEDBACK,
78
+ FEEDBACK_STATES,
79
+ FIELD_RULES,
80
+ FORBIDDEN_COPY,
81
+ FORM_INTERACTION,
82
+ IA_LIMITS,
83
+ LOADING_AFFORDANCE,
84
+ MIN_SKELETON_MS,
85
+ MOTION,
86
+ MOTION_PURPOSE,
87
+ NAVIGATION_MECHANISMS,
88
+ NAVIGATION_RULES,
89
+ NON_DESTRUCTIVE_STATES,
90
+ REDUCED_MOTION,
91
+ REGISTER,
92
+ STATUS,
93
+ TONES,
94
+ TRANSITION_PATTERNS,
95
+ UNSAVED_CHANGES,
96
+ URL_SYNC,
97
+ VALIDATION_TIMING,
98
+ consequenceSentence,
99
+ destructivePolicy,
100
+ feedbackAttr,
101
+ feedbackMeta,
102
+ fill,
103
+ hasTechnicalDetail,
104
+ isDensity,
105
+ isNonDestructive,
106
+ renderErrorMessage,
107
+ shouldOfferRetry,
108
+ statusEntries,
109
+ statusKeysWithTone,
110
+ statusMeta,
111
+ toErrorAnatomy
112
+ };
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * TEA UI — UX Standards.\n *\n * Standards are only real when something enforces them. This package is that\n * something: the vocabulary every other TEA UI package imports instead of\n * re-deriving. A component asks the registry for a status label; a dialog asks\n * the destructive policy what protection it needs; an error surface asks the\n * anatomy type for its four answers.\n *\n * If a rule here is inconvenient, the rule is what changes — not the call\n * site. That inversion is the entire point of the layer.\n */\n\nexport {\n STATUS,\n statusMeta,\n statusEntries,\n statusKeysWithTone,\n type StatusMeta,\n type StatusDomain,\n type StatusKey,\n type AnyStatusKey,\n} from \"./status\";\n\n/* The tone vocabulary is defined in the token layer — a theme colours it, the\n * standards layer names it — and re-exported here so a product reasoning about\n * status has one import for the whole vocabulary. */\nexport { TONES, type Tone } from \"@tea-ui/tokens\";\n\nexport {\n FEEDBACK,\n FEEDBACK_STATES,\n NON_DESTRUCTIVE_STATES,\n feedbackMeta,\n feedbackAttr,\n isNonDestructive,\n type FeedbackState,\n type FeedbackKind,\n type FeedbackMeta,\n type NonDestructiveState,\n} from \"./feedback\";\n\nexport {\n DESTRUCTIVE_POLICY,\n DESTRUCTIVE_VERBS,\n CONSEQUENCE_LEVELS,\n destructivePolicy,\n consequenceSentence,\n type ConsequenceLevel,\n type DestructivePolicy,\n type Protection,\n} from \"./destructive\";\n\nexport {\n ERROR_TITLES,\n renderErrorMessage,\n shouldOfferRetry,\n hasTechnicalDetail,\n toErrorAnatomy,\n type ErrorAnatomy,\n type ErrorKind,\n type ErrorRecovery,\n} from \"./error-anatomy\";\n\nexport {\n AUTOCOMPLETE,\n FIELD_RULES,\n FORM_INTERACTION,\n UNSAVED_CHANGES,\n VALIDATION_TIMING,\n type FieldControl,\n type FormFieldRules,\n} from \"./forms\";\n\nexport {\n COPY,\n FORBIDDEN_COPY,\n REGISTER,\n fill,\n type Register,\n} from \"./terminology\";\n\nexport {\n IA_LIMITS,\n NAVIGATION_RULES,\n NAVIGATION_MECHANISMS,\n URL_SYNC,\n type InformationArchitecture,\n type NavigationMechanism,\n type NavigationRule,\n} from \"./navigation\";\n\nexport {\n BACKGROUND_OPERATION,\n DATA_VIZ,\n EMPTY_STATE_TITLES,\n LOADING_AFFORDANCE,\n MIN_SKELETON_MS,\n MOTION,\n MOTION_PURPOSE,\n REDUCED_MOTION,\n TRANSITION_PATTERNS,\n type DataVizRules,\n type EmptyStateAnatomy,\n type EmptyStateTitle,\n type MotionPurpose,\n} from \"./motion\";\n\n/* The density vocabulary lives in the token layer, because it is expressed in\n * CSS custom properties. It is re-exported here so a product that reasons about\n * UX Standards has one import for the whole vocabulary. */\nexport {\n DENSITIES,\n DEFAULT_DENSITY,\n isDensity,\n type Density,\n} from \"@tea-ui/tokens\";\n"],"mappings":"AAaA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAKP,SAAS,aAAwB;AAEjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAKP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;","names":[]}
@@ -0,0 +1,126 @@
1
+ export { MOTION } from '@tea-ui/tokens';
2
+
3
+ /**
4
+ * TEA UI — motion, density, performance UX and data-visualisation standards.
5
+ *
6
+ * These four are grouped because they are the same question asked about time
7
+ * and space: how much of the user's attention does the interface get to spend?
8
+ */
9
+
10
+ /**
11
+ * Motion is only justified when it answers one of these questions. Anything
12
+ * else is decoration, and decoration in a working UI is a cost the user pays
13
+ * every time they open the page.
14
+ */
15
+ declare const MOTION_PURPOSE: readonly ["feedback", "spatial-relationship", "state-change", "loading", "attention"];
16
+ type MotionPurpose = (typeof MOTION_PURPOSE)[number];
17
+ /** The permitted transition patterns, per UX-Standards §23. */
18
+ declare const TRANSITION_PATTERNS: {
19
+ /** Hover, press, colour change. Short. */
20
+ readonly feedback: {
21
+ readonly duration: 120;
22
+ readonly ease: "cubic-bezier(0.2, 0, 0, 1)";
23
+ };
24
+ /** Panel, popover, dropdown appearing in place. */
25
+ readonly entrance: {
26
+ readonly duration: 180;
27
+ readonly ease: "cubic-bezier(0.16, 1, 0.3, 1)";
28
+ };
29
+ /** Dismissal. Faster than entrance, because the user asked for it to go. */
30
+ readonly exit: {
31
+ readonly duration: 120;
32
+ readonly ease: "cubic-bezier(0.4, 0, 1, 1)";
33
+ };
34
+ /** A value changing in place — width, position, colour. */
35
+ readonly stateChange: {
36
+ readonly duration: 180;
37
+ readonly ease: "cubic-bezier(0.2, 0, 0, 1)";
38
+ };
39
+ /** Layout shift after content arrives. Deliberately minimal. */
40
+ readonly reflow: {
41
+ readonly duration: 280;
42
+ readonly ease: "cubic-bezier(0.2, 0, 0, 1)";
43
+ };
44
+ };
45
+ /**
46
+ * Reduced motion is a floor, not an option. The audit found zero
47
+ * `prefers-reduced-motion` handling across two projects: 13 unguarded spinners
48
+ * and every overlay animation unconditional. The CSS floor lives in
49
+ * `tokens/src/index.css`; these are the rules about what remains.
50
+ */
51
+ declare const REDUCED_MOTION: {
52
+ /** Essential, non-motion progress indicators must stay, as designed. */
53
+ readonly preserveEssential: true;
54
+ /** Data attributes: add `data-tea-motion="essential"` to opt out. */
55
+ readonly attribute: "data-tea-motion";
56
+ readonly values: readonly ["essential", "decorative"];
57
+ /** An animation that carries meaning must have a non-motion equivalent. */
58
+ readonly requireNonMotionEquivalent: true;
59
+ };
60
+ /**
61
+ * Which loading affordance to use. The audit's clearest defect was treating
62
+ * these as interchangeable and replacing readable content with a spinner on
63
+ * every poll.
64
+ */
65
+ declare const LOADING_AFFORDANCE: {
66
+ /** Nothing on screen yet, first load. A skeleton matching the real layout. */
67
+ readonly initial: "skeleton";
68
+ /** Content exists and stays. A quiet indicator; never replace the content. */
69
+ readonly refresh: "inline-indicator";
70
+ /** A user-triggered operation. Inline progress on the control that started it. */
71
+ readonly processing: "control-progress";
72
+ /** A long operation the user must wait for. Determinate progress if possible. */
73
+ readonly blocking: "progress";
74
+ /** A background operation the user may leave. Announce on completion. */
75
+ readonly background: "toast-on-complete";
76
+ };
77
+ /** Minimum time a skeleton stays up, so it never flashes. */
78
+ declare const MIN_SKELETON_MS = 250;
79
+ /** Never block the UI for a background operation. */
80
+ declare const BACKGROUND_OPERATION: {
81
+ readonly blockUI: false;
82
+ readonly announceOnComplete: true;
83
+ readonly allowCancel: true;
84
+ };
85
+ /**
86
+ * A chart must be readable with no colour perception at all. That is a hard
87
+ * requirement, not a preference: it is the difference between a dashboard that
88
+ * survives a monochrome printout and one that does not.
89
+ */
90
+ interface DataVizRules {
91
+ /** Every series also carries a distinct mark, dash pattern or label. */
92
+ readonly requireNonColourEncoding: boolean;
93
+ /** Axes and gridlines must meet text contrast, not "looks fine" contrast. */
94
+ readonly minimumAxisContrast: number;
95
+ /** Missing data is drawn as a gap or a marker, never interpolated silently. */
96
+ readonly showMissingData: boolean;
97
+ /** Every chart ships a text alternative or a data table. */
98
+ readonly requireTextAlternative: boolean;
99
+ /** Legends label series; tooltips give exact values. */
100
+ readonly requireLegendForMultipleSeries: boolean;
101
+ }
102
+ declare const DATA_VIZ: DataVizRules;
103
+ /**
104
+ * Every meaningful empty state answers three questions. "Keine Einträge." on
105
+ * its own answers none of them, and the audit found ten of them.
106
+ */
107
+ interface EmptyStateAnatomy {
108
+ /** What is empty, named specifically. "Keine Server", not "Keine Einträge". */
109
+ readonly what: string;
110
+ /** Why it is empty, when the reason is not obvious. */
111
+ readonly why?: string;
112
+ /** The primary next action, when there is one. */
113
+ readonly action?: string;
114
+ /** A secondary escape hatch, e.g. clearing a filter. */
115
+ readonly secondaryAction?: string;
116
+ }
117
+ declare const EMPTY_STATE_TITLES: {
118
+ readonly neverCreated: "Noch nichts vorhanden";
119
+ readonly noResults: "Keine Treffer";
120
+ readonly filtered: "Alles ausgefiltert";
121
+ readonly noAccess: "Kein Zugriff";
122
+ readonly error: "Nicht geladen";
123
+ };
124
+ type EmptyStateTitle = (typeof EMPTY_STATE_TITLES)[keyof typeof EMPTY_STATE_TITLES];
125
+
126
+ export { BACKGROUND_OPERATION, DATA_VIZ, type DataVizRules, EMPTY_STATE_TITLES, type EmptyStateAnatomy, type EmptyStateTitle, LOADING_AFFORDANCE, MIN_SKELETON_MS, MOTION_PURPOSE, type MotionPurpose, REDUCED_MOTION, TRANSITION_PATTERNS };
package/dist/motion.js ADDED
@@ -0,0 +1,67 @@
1
+ import { MOTION } from "@tea-ui/tokens";
2
+ const MOTION_PURPOSE = ["feedback", "spatial-relationship", "state-change", "loading", "attention"];
3
+ const TRANSITION_PATTERNS = {
4
+ /** Hover, press, colour change. Short. */
5
+ feedback: { duration: MOTION.duration.fast, ease: MOTION.ease.standard },
6
+ /** Panel, popover, dropdown appearing in place. */
7
+ entrance: { duration: MOTION.duration.normal, ease: MOTION.ease.entrance },
8
+ /** Dismissal. Faster than entrance, because the user asked for it to go. */
9
+ exit: { duration: MOTION.duration.fast, ease: MOTION.ease.exit },
10
+ /** A value changing in place — width, position, colour. */
11
+ stateChange: { duration: MOTION.duration.normal, ease: MOTION.ease.standard },
12
+ /** Layout shift after content arrives. Deliberately minimal. */
13
+ reflow: { duration: MOTION.duration.slow, ease: MOTION.ease.standard }
14
+ };
15
+ const REDUCED_MOTION = {
16
+ /** Essential, non-motion progress indicators must stay, as designed. */
17
+ preserveEssential: true,
18
+ /** Data attributes: add `data-tea-motion="essential"` to opt out. */
19
+ attribute: "data-tea-motion",
20
+ values: ["essential", "decorative"],
21
+ /** An animation that carries meaning must have a non-motion equivalent. */
22
+ requireNonMotionEquivalent: true
23
+ };
24
+ const LOADING_AFFORDANCE = {
25
+ /** Nothing on screen yet, first load. A skeleton matching the real layout. */
26
+ initial: "skeleton",
27
+ /** Content exists and stays. A quiet indicator; never replace the content. */
28
+ refresh: "inline-indicator",
29
+ /** A user-triggered operation. Inline progress on the control that started it. */
30
+ processing: "control-progress",
31
+ /** A long operation the user must wait for. Determinate progress if possible. */
32
+ blocking: "progress",
33
+ /** A background operation the user may leave. Announce on completion. */
34
+ background: "toast-on-complete"
35
+ };
36
+ const MIN_SKELETON_MS = 250;
37
+ const BACKGROUND_OPERATION = {
38
+ blockUI: false,
39
+ announceOnComplete: true,
40
+ allowCancel: true
41
+ };
42
+ const DATA_VIZ = {
43
+ requireNonColourEncoding: true,
44
+ minimumAxisContrast: 3,
45
+ showMissingData: true,
46
+ requireTextAlternative: true,
47
+ requireLegendForMultipleSeries: true
48
+ };
49
+ const EMPTY_STATE_TITLES = {
50
+ neverCreated: "Noch nichts vorhanden",
51
+ noResults: "Keine Treffer",
52
+ filtered: "Alles ausgefiltert",
53
+ noAccess: "Kein Zugriff",
54
+ error: "Nicht geladen"
55
+ };
56
+ export {
57
+ BACKGROUND_OPERATION,
58
+ DATA_VIZ,
59
+ EMPTY_STATE_TITLES,
60
+ LOADING_AFFORDANCE,
61
+ MIN_SKELETON_MS,
62
+ MOTION,
63
+ MOTION_PURPOSE,
64
+ REDUCED_MOTION,
65
+ TRANSITION_PATTERNS
66
+ };
67
+ //# sourceMappingURL=motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/motion.ts"],"sourcesContent":["/**\n * TEA UI — motion, density, performance UX and data-visualisation standards.\n *\n * These four are grouped because they are the same question asked about time\n * and space: how much of the user's attention does the interface get to spend?\n */\n\nimport { MOTION } from \"@tea-ui/tokens\";\n\nexport { MOTION };\n\n/**\n * Motion is only justified when it answers one of these questions. Anything\n * else is decoration, and decoration in a working UI is a cost the user pays\n * every time they open the page.\n */\nexport const MOTION_PURPOSE = [\"feedback\", \"spatial-relationship\", \"state-change\", \"loading\", \"attention\"] as const;\n\nexport type MotionPurpose = (typeof MOTION_PURPOSE)[number];\n\n/** The permitted transition patterns, per UX-Standards §23. */\nexport const TRANSITION_PATTERNS = {\n /** Hover, press, colour change. Short. */\n feedback: { duration: MOTION.duration.fast, ease: MOTION.ease.standard },\n /** Panel, popover, dropdown appearing in place. */\n entrance: { duration: MOTION.duration.normal, ease: MOTION.ease.entrance },\n /** Dismissal. Faster than entrance, because the user asked for it to go. */\n exit: { duration: MOTION.duration.fast, ease: MOTION.ease.exit },\n /** A value changing in place — width, position, colour. */\n stateChange: { duration: MOTION.duration.normal, ease: MOTION.ease.standard },\n /** Layout shift after content arrives. Deliberately minimal. */\n reflow: { duration: MOTION.duration.slow, ease: MOTION.ease.standard },\n} as const;\n\n/**\n * Reduced motion is a floor, not an option. The audit found zero\n * `prefers-reduced-motion` handling across two projects: 13 unguarded spinners\n * and every overlay animation unconditional. The CSS floor lives in\n * `tokens/src/index.css`; these are the rules about what remains.\n */\nexport const REDUCED_MOTION = {\n /** Essential, non-motion progress indicators must stay, as designed. */\n preserveEssential: true,\n /** Data attributes: add `data-tea-motion=\"essential\"` to opt out. */\n attribute: \"data-tea-motion\",\n values: [\"essential\", \"decorative\"] as const,\n /** An animation that carries meaning must have a non-motion equivalent. */\n requireNonMotionEquivalent: true,\n} as const;\n\n/* -------------------------------------------------------------------------- */\n/* Performance UX */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Which loading affordance to use. The audit's clearest defect was treating\n * these as interchangeable and replacing readable content with a spinner on\n * every poll.\n */\nexport const LOADING_AFFORDANCE = {\n /** Nothing on screen yet, first load. A skeleton matching the real layout. */\n initial: \"skeleton\",\n /** Content exists and stays. A quiet indicator; never replace the content. */\n refresh: \"inline-indicator\",\n /** A user-triggered operation. Inline progress on the control that started it. */\n processing: \"control-progress\",\n /** A long operation the user must wait for. Determinate progress if possible. */\n blocking: \"progress\",\n /** A background operation the user may leave. Announce on completion. */\n background: \"toast-on-complete\",\n} as const;\n\n/** Minimum time a skeleton stays up, so it never flashes. */\nexport const MIN_SKELETON_MS = 250;\n\n/** Never block the UI for a background operation. */\nexport const BACKGROUND_OPERATION = {\n blockUI: false,\n announceOnComplete: true,\n allowCancel: true,\n} as const;\n\n/* -------------------------------------------------------------------------- */\n/* Data visualisation */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A chart must be readable with no colour perception at all. That is a hard\n * requirement, not a preference: it is the difference between a dashboard that\n * survives a monochrome printout and one that does not.\n */\nexport interface DataVizRules {\n /** Every series also carries a distinct mark, dash pattern or label. */\n readonly requireNonColourEncoding: boolean;\n /** Axes and gridlines must meet text contrast, not \"looks fine\" contrast. */\n readonly minimumAxisContrast: number;\n /** Missing data is drawn as a gap or a marker, never interpolated silently. */\n readonly showMissingData: boolean;\n /** Every chart ships a text alternative or a data table. */\n readonly requireTextAlternative: boolean;\n /** Legends label series; tooltips give exact values. */\n readonly requireLegendForMultipleSeries: boolean;\n}\n\nexport const DATA_VIZ: DataVizRules = {\n requireNonColourEncoding: true,\n minimumAxisContrast: 3,\n showMissingData: true,\n requireTextAlternative: true,\n requireLegendForMultipleSeries: true,\n};\n\n/* -------------------------------------------------------------------------- */\n/* Empty state */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Every meaningful empty state answers three questions. \"Keine Einträge.\" on\n * its own answers none of them, and the audit found ten of them.\n */\nexport interface EmptyStateAnatomy {\n /** What is empty, named specifically. \"Keine Server\", not \"Keine Einträge\". */\n readonly what: string;\n /** Why it is empty, when the reason is not obvious. */\n readonly why?: string;\n /** The primary next action, when there is one. */\n readonly action?: string;\n /** A secondary escape hatch, e.g. clearing a filter. */\n readonly secondaryAction?: string;\n}\n\nexport const EMPTY_STATE_TITLES = {\n neverCreated: \"Noch nichts vorhanden\",\n noResults: \"Keine Treffer\",\n filtered: \"Alles ausgefiltert\",\n noAccess: \"Kein Zugriff\",\n error: \"Nicht geladen\",\n} as const;\n\nexport type EmptyStateTitle = (typeof EMPTY_STATE_TITLES)[keyof typeof EMPTY_STATE_TITLES];\n"],"mappings":"AAOA,SAAS,cAAc;AAShB,MAAM,iBAAiB,CAAC,YAAY,wBAAwB,gBAAgB,WAAW,WAAW;AAKlG,MAAM,sBAAsB;AAAA;AAAA,EAEjC,UAAU,EAAE,UAAU,OAAO,SAAS,MAAM,MAAM,OAAO,KAAK,SAAS;AAAA;AAAA,EAEvE,UAAU,EAAE,UAAU,OAAO,SAAS,QAAQ,MAAM,OAAO,KAAK,SAAS;AAAA;AAAA,EAEzE,MAAM,EAAE,UAAU,OAAO,SAAS,MAAM,MAAM,OAAO,KAAK,KAAK;AAAA;AAAA,EAE/D,aAAa,EAAE,UAAU,OAAO,SAAS,QAAQ,MAAM,OAAO,KAAK,SAAS;AAAA;AAAA,EAE5E,QAAQ,EAAE,UAAU,OAAO,SAAS,MAAM,MAAM,OAAO,KAAK,SAAS;AACvE;AAQO,MAAM,iBAAiB;AAAA;AAAA,EAE5B,mBAAmB;AAAA;AAAA,EAEnB,WAAW;AAAA,EACX,QAAQ,CAAC,aAAa,YAAY;AAAA;AAAA,EAElC,4BAA4B;AAC9B;AAWO,MAAM,qBAAqB;AAAA;AAAA,EAEhC,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,YAAY;AAAA;AAAA,EAEZ,UAAU;AAAA;AAAA,EAEV,YAAY;AACd;AAGO,MAAM,kBAAkB;AAGxB,MAAM,uBAAuB;AAAA,EAClC,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,aAAa;AACf;AAwBO,MAAM,WAAyB;AAAA,EACpC,0BAA0B;AAAA,EAC1B,qBAAqB;AAAA,EACrB,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,gCAAgC;AAClC;AAqBO,MAAM,qBAAqB;AAAA,EAChC,cAAc;AAAA,EACd,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AACT;","names":[]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * TEA UI — navigation and information architecture standards.
3
+ *
4
+ * Navigation is the one thing a user has to learn before they can do anything
5
+ * else, so it is the thing that must be identical everywhere. The rule below
6
+ * is the whole standard: *the same information architecture problem gets the
7
+ * same navigation mechanism, in every TEA product.*
8
+ */
9
+ declare const NAVIGATION_MECHANISMS: readonly ["sidebar", "tabs", "breadcrumb", "back", "command-palette", "global-search", "local-search", "contextual-nav", "stepper"];
10
+ type NavigationMechanism = (typeof NAVIGATION_MECHANISMS)[number];
11
+ /**
12
+ * Which mechanism answers which question. This is a decision table, not
13
+ * advice: given the situation, the mechanism is already chosen.
14
+ */
15
+ interface NavigationRule {
16
+ readonly situation: string;
17
+ readonly use: NavigationMechanism;
18
+ readonly avoid: string;
19
+ readonly rationale: string;
20
+ }
21
+ declare const NAVIGATION_RULES: readonly NavigationRule[];
22
+ interface InformationArchitecture {
23
+ /** Maximum top-level entries before the IA needs rethinking. */
24
+ readonly maxTopLevel: number;
25
+ /** Maximum entries in one navigation group before it needs a subgroup. */
26
+ readonly maxGroupSize: number;
27
+ /** Maximum tabs before they need to become a different mechanism. */
28
+ readonly maxTabs: number;
29
+ }
30
+ declare const IA_LIMITS: InformationArchitecture;
31
+ /**
32
+ * URL/UI state synchronisation. State that the URL can express belongs in the
33
+ * URL, so the view is linkable, reloadable and back-navigable.
34
+ */
35
+ declare const URL_SYNC: {
36
+ /** Tabs, filters, pagination, sort order, selection, open panels. */
37
+ readonly inUrl: readonly ["tab", "filter", "page", "sort", "selection", "panel"];
38
+ /** Never in the URL: transient state that would make a shared link confusing. */
39
+ readonly notInUrl: readonly ["hover", "focus", "tooltip", "toast", "skeleton", "popover"];
40
+ };
41
+
42
+ export { IA_LIMITS, type InformationArchitecture, NAVIGATION_MECHANISMS, NAVIGATION_RULES, type NavigationMechanism, type NavigationRule, URL_SYNC };
@@ -0,0 +1,73 @@
1
+ const NAVIGATION_MECHANISMS = [
2
+ "sidebar",
3
+ "tabs",
4
+ "breadcrumb",
5
+ "back",
6
+ "command-palette",
7
+ "global-search",
8
+ "local-search",
9
+ "contextual-nav",
10
+ "stepper"
11
+ ];
12
+ const NAVIGATION_RULES = [
13
+ {
14
+ situation: "A product has 5 or more top-level areas a user moves between constantly",
15
+ use: "sidebar",
16
+ avoid: "a horizontal menu bar",
17
+ rationale: "A horizontal bar cannot hold labels that stay readable at this count. Both source projects converged on a 256px left sidebar; that is the ecosystem default."
18
+ },
19
+ {
20
+ situation: "A page has 2\u20137 peer views of the SAME subject",
21
+ use: "tabs",
22
+ avoid: "a sidebar entry per view",
23
+ rationale: "Tabs keep siblings visible. Promoting peers to sidebar entries multiplies the top level and destroys the grouping the tabs were expressing."
24
+ },
25
+ {
26
+ situation: "A user is 3+ levels deep in a hierarchy",
27
+ use: "breadcrumb",
28
+ avoid: "back-button-only",
29
+ rationale: "A breadcrumb shows where you are; back only shows where you were. Both, never either alone."
30
+ },
31
+ {
32
+ situation: "The product has 15+ actions or destinations, or a power-user workflow",
33
+ use: "command-palette",
34
+ avoid: "a settings page with 200 rows",
35
+ rationale: "A palette is searchable and keyboard-first, which is what a high action count actually requires."
36
+ },
37
+ {
38
+ situation: "The list or table on screen is long enough to need filtering",
39
+ use: "local-search",
40
+ avoid: "a dialog with the same data",
41
+ rationale: "Search belongs next to the thing it filters, so the relationship is obvious."
42
+ },
43
+ {
44
+ situation: "A destructive or hard-to-reverse multi-step task",
45
+ use: "stepper",
46
+ avoid: "a scrollable single page",
47
+ rationale: "A stepper makes progress, remaining work and recoverability all visible at once."
48
+ },
49
+ {
50
+ situation: "Viewport is below 1024px",
51
+ use: "back",
52
+ avoid: "a shrunken sidebar",
53
+ rationale: "Below `lg` a sidebar stops being information and becomes an obstruction; it collapses into a drawer. Both source projects already collapse at exactly this point."
54
+ }
55
+ ];
56
+ const IA_LIMITS = {
57
+ maxTopLevel: 7,
58
+ maxGroupSize: 6,
59
+ maxTabs: 7
60
+ };
61
+ const URL_SYNC = {
62
+ /** Tabs, filters, pagination, sort order, selection, open panels. */
63
+ inUrl: ["tab", "filter", "page", "sort", "selection", "panel"],
64
+ /** Never in the URL: transient state that would make a shared link confusing. */
65
+ notInUrl: ["hover", "focus", "tooltip", "toast", "skeleton", "popover"]
66
+ };
67
+ export {
68
+ IA_LIMITS,
69
+ NAVIGATION_MECHANISMS,
70
+ NAVIGATION_RULES,
71
+ URL_SYNC
72
+ };
73
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/navigation.ts"],"sourcesContent":["/**\n * TEA UI — navigation and information architecture standards.\n *\n * Navigation is the one thing a user has to learn before they can do anything\n * else, so it is the thing that must be identical everywhere. The rule below\n * is the whole standard: *the same information architecture problem gets the\n * same navigation mechanism, in every TEA product.*\n */\n\nexport const NAVIGATION_MECHANISMS = [\n \"sidebar\",\n \"tabs\",\n \"breadcrumb\",\n \"back\",\n \"command-palette\",\n \"global-search\",\n \"local-search\",\n \"contextual-nav\",\n \"stepper\",\n] as const;\n\nexport type NavigationMechanism = (typeof NAVIGATION_MECHANISMS)[number];\n\n/**\n * Which mechanism answers which question. This is a decision table, not\n * advice: given the situation, the mechanism is already chosen.\n */\nexport interface NavigationRule {\n readonly situation: string;\n readonly use: NavigationMechanism;\n readonly avoid: string;\n readonly rationale: string;\n}\n\nexport const NAVIGATION_RULES: readonly NavigationRule[] = [\n {\n situation: \"A product has 5 or more top-level areas a user moves between constantly\",\n use: \"sidebar\",\n avoid: \"a horizontal menu bar\",\n rationale:\n \"A horizontal bar cannot hold labels that stay readable at this count. Both source projects converged on a 256px left sidebar; that is the ecosystem default.\",\n },\n {\n situation: \"A page has 2–7 peer views of the SAME subject\",\n use: \"tabs\",\n avoid: \"a sidebar entry per view\",\n rationale:\n \"Tabs keep siblings visible. Promoting peers to sidebar entries multiplies the top level and destroys the grouping the tabs were expressing.\",\n },\n {\n situation: \"A user is 3+ levels deep in a hierarchy\",\n use: \"breadcrumb\",\n avoid: \"back-button-only\",\n rationale: \"A breadcrumb shows where you are; back only shows where you were. Both, never either alone.\",\n },\n {\n situation: \"The product has 15+ actions or destinations, or a power-user workflow\",\n use: \"command-palette\",\n avoid: \"a settings page with 200 rows\",\n rationale: \"A palette is searchable and keyboard-first, which is what a high action count actually requires.\",\n },\n {\n situation: \"The list or table on screen is long enough to need filtering\",\n use: \"local-search\",\n avoid: \"a dialog with the same data\",\n rationale: \"Search belongs next to the thing it filters, so the relationship is obvious.\",\n },\n {\n situation: \"A destructive or hard-to-reverse multi-step task\",\n use: \"stepper\",\n avoid: \"a scrollable single page\",\n rationale: \"A stepper makes progress, remaining work and recoverability all visible at once.\",\n },\n {\n situation: \"Viewport is below 1024px\",\n use: \"back\",\n avoid: \"a shrunken sidebar\",\n rationale:\n \"Below `lg` a sidebar stops being information and becomes an obstruction; it collapses into a drawer. Both source projects already collapse at exactly this point.\",\n },\n];\n\nexport interface InformationArchitecture {\n /** Maximum top-level entries before the IA needs rethinking. */\n readonly maxTopLevel: number;\n /** Maximum entries in one navigation group before it needs a subgroup. */\n readonly maxGroupSize: number;\n /** Maximum tabs before they need to become a different mechanism. */\n readonly maxTabs: number;\n}\n\nexport const IA_LIMITS: InformationArchitecture = {\n maxTopLevel: 7,\n maxGroupSize: 6,\n maxTabs: 7,\n};\n\n/**\n * URL/UI state synchronisation. State that the URL can express belongs in the\n * URL, so the view is linkable, reloadable and back-navigable.\n */\nexport const URL_SYNC = {\n /** Tabs, filters, pagination, sort order, selection, open panels. */\n inUrl: [\"tab\", \"filter\", \"page\", \"sort\", \"selection\", \"panel\"] as const,\n /** Never in the URL: transient state that would make a shared link confusing. */\n notInUrl: [\"hover\", \"focus\", \"tooltip\", \"toast\", \"skeleton\", \"popover\"] as const,\n} as const;\n"],"mappings":"AASO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAeO,MAAM,mBAA8C;AAAA,EACzD;AAAA,IACE,WAAW;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,IACP,WACE;AAAA,EACJ;AAAA,EACA;AAAA,IACE,WAAW;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,IACP,WACE;AAAA,EACJ;AAAA,EACA;AAAA,IACE,WAAW;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,IACP,WAAW;AAAA,EACb;AAAA,EACA;AAAA,IACE,WAAW;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,IACP,WAAW;AAAA,EACb;AAAA,EACA;AAAA,IACE,WAAW;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,IACP,WAAW;AAAA,EACb;AAAA,EACA;AAAA,IACE,WAAW;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,IACP,WAAW;AAAA,EACb;AAAA,EACA;AAAA,IACE,WAAW;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,IACP,WACE;AAAA,EACJ;AACF;AAWO,MAAM,YAAqC;AAAA,EAChD,aAAa;AAAA,EACb,cAAc;AAAA,EACd,SAAS;AACX;AAMO,MAAM,WAAW;AAAA;AAAA,EAEtB,OAAO,CAAC,OAAO,UAAU,QAAQ,QAAQ,aAAa,OAAO;AAAA;AAAA,EAE7D,UAAU,CAAC,SAAS,SAAS,WAAW,SAAS,YAAY,SAAS;AACxE;","names":[]}
@@ -0,0 +1,50 @@
1
+ import { Tone } from '@tea-ui/tokens';
2
+ export { Tone } from '@tea-ui/tokens';
3
+
4
+ interface StatusMeta {
5
+ /** The word a user reads. Nominal, no trailing punctuation. */
6
+ readonly label: string;
7
+ /** The closed semantic tone. The only way a status reaches a colour. */
8
+ readonly tone: Tone;
9
+ /** One sentence explaining the state, for a tooltip or help text. */
10
+ readonly description: string;
11
+ }
12
+ declare const STATUS: {
13
+ readonly health: Readonly<Record<"offline" | "maintenance" | "online" | "degraded" | "unknown", StatusMeta>>;
14
+ readonly resource: Readonly<Record<"critical" | "normal" | "elevated" | "high", StatusMeta>>;
15
+ readonly backup: Readonly<Record<"succeeded" | "running" | "pending" | "failed" | "expired", StatusMeta>>;
16
+ readonly certificate: Readonly<Record<"unknown" | "expired" | "valid" | "expiring" | "invalid", StatusMeta>>;
17
+ readonly container: Readonly<Record<"error" | "running" | "created" | "paused" | "restarting" | "stopped", StatusMeta>>;
18
+ readonly website: Readonly<Record<"error" | "offline" | "online" | "degraded" | "unknown" | "deploying", StatusMeta>>;
19
+ readonly dependency: Readonly<Record<"warning" | "error" | "ok" | "missing", StatusMeta>>;
20
+ readonly security: Readonly<Record<"critical" | "warning" | "ok", StatusMeta>>;
21
+ readonly crm: Readonly<Record<"unprocessed" | "no_website" | "opportunity" | "contacted" | "conversation" | "offer" | "customer" | "archived", StatusMeta>>;
22
+ readonly project: Readonly<Record<"planning" | "active" | "on_hold" | "review" | "delivered" | "cancelled", StatusMeta>>;
23
+ };
24
+ type StatusDomain = keyof typeof STATUS;
25
+ /** The wire values a given domain may take. */
26
+ type StatusKey<D extends StatusDomain> = keyof (typeof STATUS)[D] & string;
27
+ /** Union of every wire value across every domain, for exhaustive switches. */
28
+ type AnyStatusKey = {
29
+ [D in StatusDomain]: StatusKey<D>;
30
+ }[StatusDomain];
31
+ /**
32
+ * Look up the label, tone and description for a wire value.
33
+ *
34
+ * The key is `domain` + `key` rather than a flat string, because a flat string
35
+ * cannot be checked: `statusMeta("healthy")` would compile even if no domain
36
+ * ever produced "healthy". This signature makes a typo a type error, and
37
+ * adding a wire value without a label is a type error too — which is exactly
38
+ * the failure mode that produced five disagreeing tables.
39
+ */
40
+ declare function statusMeta<D extends StatusDomain>(domain: D, key: StatusKey<D>): StatusMeta;
41
+ /** Every entry of a domain, as `[key, meta]` pairs. */
42
+ declare function statusEntries<D extends StatusDomain>(domain: D): Array<[StatusKey<D>, StatusMeta]>;
43
+ /**
44
+ * The wire values of one domain that a given tone covers, in registry order.
45
+ * Used by filter chips and legends, so a filter row can never invent a
46
+ * category the registry does not know.
47
+ */
48
+ declare function statusKeysWithTone<D extends StatusDomain>(domain: D, tone: Tone): Array<StatusKey<D>>;
49
+
50
+ export { type AnyStatusKey, STATUS, type StatusDomain, type StatusKey, type StatusMeta, statusEntries, statusKeysWithTone, statusMeta };