@sjolystinnovation/app-kit 0.4.0 → 0.6.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.
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { useId, type InputHTMLAttributes, type ReactNode } from "react";
4
+ import { cx } from "./cx";
5
+
6
+ /** The ids and aria attributes a labelled input needs, from the caller's own props. */
7
+ export function useField(props: Pick<InputHTMLAttributes<HTMLInputElement>, "id" | "required" | "aria-required" | "aria-invalid" | "aria-describedby">, error: string | undefined) {
8
+ // A generated id ties the label to its own control, even when two fields on a page share a name.
9
+ const generated = useId();
10
+ const inputId = props.id ?? generated;
11
+ // From useId, which has no spaces. aria-describedby splits its ids on spaces.
12
+ const errorId = `${generated}-error`;
13
+ return {
14
+ inputId,
15
+ errorId,
16
+ inputProps: {
17
+ id: inputId,
18
+ required: props.required,
19
+ "aria-required": props.required ? true : props["aria-required"],
20
+ "aria-invalid": error ? true : props["aria-invalid"],
21
+ "aria-describedby": cx(props["aria-describedby"], error && errorId) || undefined,
22
+ },
23
+ };
24
+ }
25
+
26
+ /** The input's look, with the error border when there is an error. */
27
+ export function inputClass(error: string | undefined, extra?: string): string {
28
+ return cx(
29
+ "w-full rounded-input border px-3 py-input-y text-sm bg-input-bg text-input-text placeholder-input-placeholder focus:outline-hidden focus:ring-(length:--input-focus-ring-width) focus:ring-input-focus-ring",
30
+ extra,
31
+ error ? "border-input-border-error" : "border-input-border focus:border-input-focus-border",
32
+ );
33
+ }
34
+
35
+ export function FieldLabel({ htmlFor, required, children }: { htmlFor: string; required: boolean | undefined; children: ReactNode }) {
36
+ return (
37
+ <label htmlFor={htmlFor} className="block text-field-label font-medium text-field-label-text mb-field-label">
38
+ {children}
39
+ {required && <span className="text-field-required" aria-hidden="true"> *</span>}
40
+ </label>
41
+ );
42
+ }
43
+
44
+ export function FieldError({ id, error }: { id: string; error: string | undefined }) {
45
+ if (!error) return null;
46
+ return (
47
+ <p id={id} className="mt-1 text-xs text-field-error">
48
+ {error}
49
+ </p>
50
+ );
51
+ }
package/src/ui/index.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export { Alert } from "./Alert";
2
2
  export type { AlertProps } from "./Alert";
3
+ export { CredentialsForm } from "./CredentialsForm";
4
+ export type { CredentialsFormProps } from "./CredentialsForm";
5
+ export { SignInRejected, defaultCredentialsFormStrings } from "./credentialsFormStrings";
6
+ export type { CredentialsFormStrings } from "./credentialsFormStrings";
3
7
  export { PasswordInput } from "./PasswordInput";
4
8
  export type { PasswordInputProps } from "./PasswordInput";
9
+ export { TextInput } from "./TextInput";
10
+ export type { TextInputProps } from "./TextInput";
11
+ export type { TextOverrides } from "./withDefaults";
@@ -0,0 +1,14 @@
1
+ /** Text overrides where any key may be missing or undefined. */
2
+ export type TextOverrides<T> = { [K in keyof T]?: T[K] | undefined };
3
+
4
+ /**
5
+ * Fills in defaults for any string that is missing or undefined, so a key missing from an app's
6
+ * dictionary still shows the default text.
7
+ */
8
+ export function withDefaults<T extends object>(defaults: T, overrides: TextOverrides<T> | undefined): T {
9
+ const result = { ...defaults };
10
+ for (const [key, value] of Object.entries(overrides ?? {})) {
11
+ if (value !== undefined) (result as Record<string, unknown>)[key] = value;
12
+ }
13
+ return result;
14
+ }