@spark-web/field 0.0.0-snapshot-release-20260409001813

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/CLAUDE.md ADDED
@@ -0,0 +1,80 @@
1
+ # @spark-web/field — AI Context
2
+
3
+ ## What this is
4
+
5
+ The form field wrapper. `Field` provides the label, optional description,
6
+ optional validation message, and the accessibility wiring (aria-describedby,
7
+ aria-invalid, id) that connects a label to its input. Every `TextInput`,
8
+ `Select`, and other form input **must** be wrapped in a `Field`.
9
+
10
+ ## What this is NOT
11
+
12
+ - Not the input itself — `Field` only provides the label and context; the input
13
+ (`TextInput`, `Select`, etc.) is the `children`
14
+ - Not a layout component — use `Stack` or `Columns` to arrange multiple `Field`
15
+ components
16
+
17
+ ## Props interface
18
+
19
+ | Prop | Type | Default | Notes |
20
+ | ----------------- | ------------------------------------------ | ----------- | --------------------------------------------------------------------------------------- |
21
+ | `label` | `string` | required | Always required — used for accessibility even if hidden |
22
+ | `labelVisibility` | `'visible' \| 'hidden' \| 'reserve-space'` | `'visible'` | `'hidden'` hides visually but keeps accessible; `'reserve-space'` reserves layout space |
23
+ | `description` | `string \| ReactNode` | — | Hint text rendered below the label |
24
+ | `message` | `string` | — | Validation message; tone controls its appearance |
25
+ | `tone` | `'neutral' \| 'critical' \| 'positive'` | `'neutral'` | `'critical'` marks the input invalid |
26
+ | `secondaryLabel` | `string` | — | Supplementary label text (e.g. "Optional") |
27
+ | `disabled` | `boolean` | `false` | Disables the child input via context |
28
+ | `readOnly` | `boolean` | `false` | Makes the child input read-only via context |
29
+ | `adornment` | `ReactElement` | — | Utility element placed inline with the label (e.g. tooltip) |
30
+ | `data` | `DataAttributeMap` | — | Test/analytics attributes |
31
+
32
+ ## Common patterns
33
+
34
+ ### Standard labeled input
35
+
36
+ ```tsx
37
+ <Field label="Email address">
38
+ <TextInput type="email" placeholder="you@example.com" />
39
+ </Field>
40
+ ```
41
+
42
+ ### Hidden label (filter context)
43
+
44
+ Use `labelVisibility="hidden"` when the placeholder or surrounding context makes
45
+ the label redundant visually, but the label is still required for a11y:
46
+
47
+ ```tsx
48
+ <Field label="Search users" labelVisibility="hidden">
49
+ <TextInput placeholder="Search users">
50
+ <InputAdornment placement="start">
51
+ <SearchIcon size="xsmall" tone="placeholder" />
52
+ </InputAdornment>
53
+ </TextInput>
54
+ </Field>
55
+ ```
56
+
57
+ ### Validation state
58
+
59
+ ```tsx
60
+ <Field label="Password" tone="critical" message="Password is required">
61
+ <TextInput type="password" />
62
+ </Field>
63
+ ```
64
+
65
+ ### Optional field
66
+
67
+ ```tsx
68
+ <Field label="Phone number" secondaryLabel="Optional">
69
+ <TextInput type="tel" />
70
+ </Field>
71
+ ```
72
+
73
+ ## Do NOTs
74
+
75
+ - NEVER use `TextInput` or `Select` without wrapping them in `Field`
76
+ - NEVER omit `label` — it is always required for accessibility, even with
77
+ `labelVisibility="hidden"`
78
+ - NEVER pass `id` to the input directly — `Field` manages the id via context
79
+ - NEVER use `tone="critical"` without also providing `message` — the tone alone
80
+ does not communicate the error to the user
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ ---
2
+ title: Field
3
+ isExperimentalPackage: false
4
+ ---
5
+
6
+ Using context, the field component connects the label, description, and message
7
+ to the input element.
8
+
9
+ ```jsx live
10
+ <Field label="Label">
11
+ <TextInput />
12
+ </Field>
13
+ ```
14
+
15
+ ## Example
16
+
17
+ ### Label
18
+
19
+ Each field must be accompanied by a label. Effective form labeling helps users
20
+ understand what information to enter into an input.
21
+
22
+ Using placeholder text in lieu of a label is sometimes employed as a
23
+ space-saving method. However, this is not recommended because it hides context
24
+ and presents accessibility issues.
25
+
26
+ ```jsx live
27
+ <Field label="Name">
28
+ <TextInput />
29
+ </Field>
30
+ ```
31
+
32
+ #### Label visibility
33
+
34
+ The label must always be provided for assistive technology, but you may hide it
35
+ from sighted users when the intent can be inferred from context.
36
+
37
+ ```jsx live
38
+ <Stack gap="xlarge">
39
+ <Field label="Name" labelVisibility="hidden">
40
+ <TextInput placeholder="hidden" />
41
+ </Field>
42
+ <Columns gap="small">
43
+ <Field label="Name">
44
+ <TextInput placeholder="visible" />
45
+ </Field>
46
+ <Field label="Name" labelVisibility="reserve-space">
47
+ <TextInput placeholder="reserve-space" />
48
+ </Field>
49
+ </Columns>
50
+ </Stack>
51
+ ```
52
+
53
+ #### Secondary label
54
+
55
+ Provide additional context, typically used to indicate that the field is
56
+ optional.
57
+
58
+ ```jsx live
59
+ <Field label="Name" secondaryLabel="(Optional)">
60
+ <TextInput />
61
+ </Field>
62
+ ```
63
+
64
+ ### Adornment
65
+
66
+ Optionally provide a utility or contextual hint, related to the field.
67
+
68
+ ```jsx live
69
+ <Field
70
+ label="Username"
71
+ adornment={
72
+ <Text>
73
+ <TextLink href="#">Forgot username?</TextLink>
74
+ </Text>
75
+ }
76
+ >
77
+ <TextInput />
78
+ </Field>
79
+ ```
80
+
81
+ ### Description
82
+
83
+ Provides pertinent information that assists the user in completing a field.
84
+ Description text is always visible and appears underneath the label. Use
85
+ sentence-style capitalisation, and in most cases, write the text as full
86
+ sentences with punctuation.
87
+
88
+ ```jsx live
89
+ <Field
90
+ label="Email"
91
+ description="We take your privacy seriously. We will never give your email to a third party."
92
+ >
93
+ <TextInput type="email" />
94
+ </Field>
95
+ ```
96
+
97
+ ### Message and tone
98
+
99
+ The “message” is used to communicate the status of a field, such as an error
100
+ message. This will be announced on focus and can be combined with a “tone” to
101
+ illustrate intent.
102
+
103
+ ```jsx live
104
+ <Stack gap="xlarge">
105
+ <Field label="Label" tone="critical" message="Critical message">
106
+ <TextInput />
107
+ </Field>
108
+ <Field label="Label" tone="positive" message="Positive message">
109
+ <TextInput />
110
+ </Field>
111
+ <Field label="Label" tone="neutral" message="Neutral message">
112
+ <TextInput />
113
+ </Field>
114
+ </Stack>
115
+ ```
116
+
117
+ ### Disabled
118
+
119
+ Mark the field as disabled by passing true to the disabled prop.
120
+
121
+ ```jsx live
122
+ <Field label="Label" secondaryLabel="Secondary label" disabled>
123
+ <TextInput value="Text in disabled field" />
124
+ </Field>
125
+ ```
126
+
127
+ ## Props
128
+
129
+ <PropsTable displayName="Field" />
130
+
131
+ [data-attribute-map]:
132
+ https://github.com/brighte-labs/spark-web/blob/e7f6f4285b4cfd876312cc89fbdd094039aa239a/packages/utils/src/internal/buildDataAttributes.ts#L1
@@ -0,0 +1,15 @@
1
+ export type FieldState = {
2
+ disabled: boolean;
3
+ invalid: boolean;
4
+ readOnly?: boolean;
5
+ };
6
+ export type InputPropsDerivedFromField = {
7
+ 'aria-describedby'?: string;
8
+ 'aria-invalid': true | undefined;
9
+ id: string;
10
+ };
11
+ export type FieldContextType = [FieldState, InputPropsDerivedFromField];
12
+ export declare const FieldContext: import("react").Context<FieldContextType | null>;
13
+ export declare const FieldContextProvider: import("react").Provider<FieldContextType | null>;
14
+ export declare const FIELD_CONTEXT_ERROR_MESSAGE = "Input components must be inside a `Field`.";
15
+ export declare function useFieldContext(): FieldContextType;
@@ -0,0 +1,53 @@
1
+ import type { DataAttributeMap } from '@spark-web/utils/internal';
2
+ import type { ReactElement, ReactNode } from 'react';
3
+ export type Tone = keyof typeof messageToneMap;
4
+ export type FieldProps = {
5
+ /** Sets a unique identifier for the component. */
6
+ id?: string;
7
+ /** Sets data attributes on the component. */
8
+ data?: DataAttributeMap;
9
+ /** Optionally provide a utility or contextual hint, related to the field. */
10
+ adornment?: ReactElement;
11
+ /** Input component */
12
+ children: ReactNode;
13
+ /**
14
+ * Indicates that the field is perceivable but disabled, so it is not editable
15
+ * or otherwise operable.
16
+ */
17
+ disabled?: boolean;
18
+ /** Provide additional information that will aid user input. */
19
+ description?: string | ReactNode;
20
+ /** Concisely label the field. */
21
+ label: string;
22
+ /**
23
+ * The label must always be provided for assistive technology, but you may
24
+ * hide it from sighted users when the intent can be inferred from context.
25
+ */
26
+ labelVisibility?: 'hidden' | 'reserve-space' | 'visible';
27
+ /** Provide a message, informing the user about changes in state. */
28
+ message?: string;
29
+ /** Additional context, typically used to indicate that the field is optional. */
30
+ secondaryLabel?: string;
31
+ /** Provide a tone to influence elements of the field, and its input. */
32
+ tone?: Tone;
33
+ /** Sets input to readonly, which is focusable. */
34
+ readOnly?: boolean;
35
+ };
36
+ /**
37
+ * Using a [context](https://reactjs.org/docs/context.html), the field
38
+ * component connects the label, description, and message to the input element.
39
+ */
40
+ export declare const Field: import("react").ForwardRefExoticComponent<FieldProps & import("react").RefAttributes<HTMLDivElement>>;
41
+ export declare function useFieldIds(id?: string): {
42
+ descriptionId: string;
43
+ inputId: string;
44
+ messageId: string;
45
+ };
46
+ declare const messageToneMap: {
47
+ readonly critical: "critical";
48
+ readonly neutral: "muted";
49
+ readonly positive: "positive";
50
+ };
51
+ type FieldMessageProps = Required<Pick<FieldProps, 'message' | 'id' | 'tone'>>;
52
+ export declare const FieldMessage: ({ message, id, tone }: FieldMessageProps) => import("@emotion/react/jsx-runtime").JSX.Element;
53
+ export {};
@@ -0,0 +1,4 @@
1
+ export { FieldContextProvider, useFieldContext } from "./context.js";
2
+ export { Field, FieldMessage, useFieldIds } from "./field.js";
3
+ export type { FieldContextType, FieldState, InputPropsDerivedFromField, } from "./context.js";
4
+ export type { FieldProps, Tone } from "./field.js";
@@ -0,0 +1,2 @@
1
+ export * from "./declarations/src/index.js";
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3Bhcmstd2ViLWZpZWxkLmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi9kZWNsYXJhdGlvbnMvc3JjL2luZGV4LmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEifQ==
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var react = require('react');
6
+ var react$1 = require('@emotion/react');
7
+ var a11y = require('@spark-web/a11y');
8
+ var box = require('@spark-web/box');
9
+ var icon = require('@spark-web/icon');
10
+ var stack = require('@spark-web/stack');
11
+ var text = require('@spark-web/text');
12
+ var theme = require('@spark-web/theme');
13
+ var jsxRuntime = require('@emotion/react/jsx-runtime');
14
+
15
+ var FieldContext = /*#__PURE__*/react.createContext(null);
16
+ var FieldContextProvider = FieldContext.Provider;
17
+ var FIELD_CONTEXT_ERROR_MESSAGE = 'Input components must be inside a `Field`.';
18
+ function useFieldContext() {
19
+ var ctx = react.useContext(FieldContext);
20
+ if (!ctx) {
21
+ throw new Error(FIELD_CONTEXT_ERROR_MESSAGE);
22
+ }
23
+ return ctx;
24
+ }
25
+
26
+ /**
27
+ * Using a [context](https://reactjs.org/docs/context.html), the field
28
+ * component connects the label, description, and message to the input element.
29
+ */
30
+ var Field = /*#__PURE__*/react.forwardRef(function (_ref, forwardedRef) {
31
+ var children = _ref.children,
32
+ idProp = _ref.id,
33
+ data = _ref.data,
34
+ description = _ref.description,
35
+ _ref$disabled = _ref.disabled,
36
+ disabled = _ref$disabled === void 0 ? false : _ref$disabled,
37
+ label = _ref.label,
38
+ adornment = _ref.adornment,
39
+ _ref$labelVisibility = _ref.labelVisibility,
40
+ labelVisibility = _ref$labelVisibility === void 0 ? 'visible' : _ref$labelVisibility,
41
+ message = _ref.message,
42
+ secondaryLabel = _ref.secondaryLabel,
43
+ _ref$tone = _ref.tone,
44
+ tone = _ref$tone === void 0 ? 'neutral' : _ref$tone,
45
+ _ref$readOnly = _ref.readOnly,
46
+ readOnly = _ref$readOnly === void 0 ? false : _ref$readOnly;
47
+ var _useFieldIds = useFieldIds(idProp),
48
+ descriptionId = _useFieldIds.descriptionId,
49
+ inputId = _useFieldIds.inputId,
50
+ messageId = _useFieldIds.messageId;
51
+
52
+ // field context
53
+ var invalid = Boolean(message && tone === 'critical');
54
+ var fieldContext = react.useMemo(function () {
55
+ return [{
56
+ disabled: disabled,
57
+ invalid: invalid,
58
+ readOnly: readOnly
59
+ }, {
60
+ 'aria-describedby': a11y.mergeIds(message && messageId, description ? descriptionId : undefined),
61
+ 'aria-invalid': invalid || undefined,
62
+ id: inputId
63
+ }];
64
+ }, [description, descriptionId, disabled, inputId, invalid, message, messageId, readOnly]);
65
+
66
+ // label prep
67
+ var hiddenLabel = jsxRuntime.jsxs(a11y.VisuallyHidden, {
68
+ as: "label",
69
+ htmlFor: inputId,
70
+ children: [label, " ", secondaryLabel]
71
+ });
72
+ var labelElement = {
73
+ hidden: hiddenLabel,
74
+ visible: jsxRuntime.jsx(box.Box, {
75
+ as: "label",
76
+ htmlFor: inputId,
77
+ children: jsxRuntime.jsxs(text.Text, {
78
+ tone: disabled || readOnly ? 'field' : 'neutral',
79
+ weight: "semibold",
80
+ children: [label, ' ', secondaryLabel && jsxRuntime.jsx(text.Text, {
81
+ inline: true,
82
+ tone: disabled || readOnly ? 'field' : 'muted',
83
+ weight: "regular",
84
+ children: secondaryLabel
85
+ })]
86
+ })
87
+ }),
88
+ 'reserve-space': jsxRuntime.jsxs(react.Fragment, {
89
+ children: [hiddenLabel, jsxRuntime.jsx(text.Text, {
90
+ "aria-hidden": true,
91
+ children: "\xA0"
92
+ })]
93
+ })
94
+ };
95
+ var LabelWrapper = labelVisibility === 'hidden' ? react.Fragment : FieldLabelWrapper;
96
+ return jsxRuntime.jsx(FieldContextProvider, {
97
+ value: fieldContext,
98
+ children: jsxRuntime.jsxs(stack.Stack, {
99
+ ref: forwardedRef,
100
+ data: data,
101
+ gap: "medium",
102
+ children: [jsxRuntime.jsxs(LabelWrapper, {
103
+ children: [labelElement[labelVisibility], adornment]
104
+ }), description && (typeof description === 'string' ? jsxRuntime.jsx(text.Text, {
105
+ tone: "muted",
106
+ size: "small",
107
+ id: descriptionId,
108
+ children: description
109
+ }) : jsxRuntime.jsx(box.Box, {
110
+ as: "label",
111
+ htmlFor: descriptionId,
112
+ children: description
113
+ })), children, message && jsxRuntime.jsx(FieldMessage, {
114
+ tone: tone,
115
+ id: messageId,
116
+ message: message
117
+ })]
118
+ })
119
+ });
120
+ });
121
+ Field.displayName = 'Field';
122
+
123
+ // Utils
124
+ // ------------------------------
125
+
126
+ function useFieldIds(id) {
127
+ var inputId = a11y.useId(id);
128
+ var descriptionId = a11y.composeId(inputId, 'description');
129
+ var messageId = a11y.composeId(inputId, 'message');
130
+ return {
131
+ descriptionId: descriptionId,
132
+ inputId: inputId,
133
+ messageId: messageId
134
+ };
135
+ }
136
+
137
+ // Styled components
138
+ // ------------------------------
139
+ function FieldLabelWrapper(_ref2) {
140
+ var children = _ref2.children;
141
+ return jsxRuntime.jsx(box.Box, {
142
+ display: "flex",
143
+ alignItems: "center",
144
+ justifyContent: "spaceBetween",
145
+ gap: "large",
146
+ children: children
147
+ });
148
+ }
149
+ var messageToneMap = {
150
+ critical: 'critical',
151
+ neutral: 'muted',
152
+ positive: 'positive'
153
+ };
154
+
155
+ // NOTE: use icons in addition to color for folks with visions issues
156
+ var messageIconMap = {
157
+ critical: icon.ExclamationCircleIcon,
158
+ neutral: null,
159
+ positive: icon.CheckCircleIcon
160
+ };
161
+ var FieldMessage = function FieldMessage(_ref3) {
162
+ var message = _ref3.message,
163
+ id = _ref3.id,
164
+ tone = _ref3.tone;
165
+ var textTone = messageToneMap[tone];
166
+ var Icon = messageIconMap[tone];
167
+ return jsxRuntime.jsxs(box.Box, {
168
+ display: "flex",
169
+ gap: "xsmall",
170
+ children: [Icon ? jsxRuntime.jsx(IndicatorContainer, {
171
+ children: jsxRuntime.jsx(Icon, {
172
+ size: "xxsmall",
173
+ tone: tone
174
+ })
175
+ }) : null, jsxRuntime.jsx(text.Text, {
176
+ tone: textTone,
177
+ size: "small",
178
+ id: id,
179
+ children: message
180
+ })]
181
+ });
182
+ };
183
+ function IndicatorContainer(_ref4) {
184
+ var children = _ref4.children;
185
+ var theme$1 = theme.useTheme();
186
+ var _theme$typography$tex = theme$1.typography.text.small,
187
+ mobile = _theme$typography$tex.mobile,
188
+ tablet = _theme$typography$tex.tablet;
189
+ var responsiveStyles = theme$1.utils.responsiveStyles({
190
+ mobile: {
191
+ height: mobile.capHeight
192
+ },
193
+ tablet: {
194
+ height: tablet.capHeight
195
+ }
196
+ });
197
+ return jsxRuntime.jsx(box.Box, {
198
+ display: "flex",
199
+ alignItems: "center",
200
+ "aria-hidden": true,
201
+ cursor: "default",
202
+ flexShrink: 0,
203
+ css: react$1.css(responsiveStyles),
204
+ children: children
205
+ });
206
+ }
207
+
208
+ exports.Field = Field;
209
+ exports.FieldContextProvider = FieldContextProvider;
210
+ exports.FieldMessage = FieldMessage;
211
+ exports.useFieldContext = useFieldContext;
212
+ exports.useFieldIds = useFieldIds;
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ if (process.env.NODE_ENV === "production") {
4
+ module.exports = require("./spark-web-field.cjs.prod.js");
5
+ } else {
6
+ module.exports = require("./spark-web-field.cjs.dev.js");
7
+ }