@tea-ui/core 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 +21 -0
- package/README.md +33 -0
- package/dist/card.d.ts +60 -0
- package/dist/card.js +116 -0
- package/dist/card.js.map +1 -0
- package/dist/feedback/alert.d.ts +57 -0
- package/dist/feedback/alert.js +104 -0
- package/dist/feedback/alert.js.map +1 -0
- package/dist/feedback/badge.d.ts +34 -0
- package/dist/feedback/badge.js +62 -0
- package/dist/feedback/badge.js.map +1 -0
- package/dist/feedback/index.d.ts +13 -0
- package/dist/feedback/index.js +62 -0
- package/dist/feedback/index.js.map +1 -0
- package/dist/feedback/progress.d.ts +25 -0
- package/dist/feedback/progress.js +49 -0
- package/dist/feedback/progress.js.map +1 -0
- package/dist/feedback/skeleton.d.ts +38 -0
- package/dist/feedback/skeleton.js +49 -0
- package/dist/feedback/skeleton.js.map +1 -0
- package/dist/feedback/spinner.d.ts +31 -0
- package/dist/feedback/spinner.js +55 -0
- package/dist/feedback/spinner.js.map +1 -0
- package/dist/feedback/status.d.ts +63 -0
- package/dist/feedback/status.js +156 -0
- package/dist/feedback/status.js.map +1 -0
- package/dist/feedback/toast.d.ts +72 -0
- package/dist/feedback/toast.js +211 -0
- package/dist/feedback/toast.js.map +1 -0
- package/dist/format/index.d.ts +63 -0
- package/dist/format/index.js +84 -0
- package/dist/format/index.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs/button-group.d.ts +70 -0
- package/dist/inputs/button-group.js +59 -0
- package/dist/inputs/button-group.js.map +1 -0
- package/dist/inputs/button.d.ts +52 -0
- package/dist/inputs/button.js +84 -0
- package/dist/inputs/button.js.map +1 -0
- package/dist/inputs/checkbox.d.ts +48 -0
- package/dist/inputs/checkbox.js +74 -0
- package/dist/inputs/checkbox.js.map +1 -0
- package/dist/inputs/combobox.d.ts +77 -0
- package/dist/inputs/combobox.js +273 -0
- package/dist/inputs/combobox.js.map +1 -0
- package/dist/inputs/field.d.ts +220 -0
- package/dist/inputs/field.js +183 -0
- package/dist/inputs/field.js.map +1 -0
- package/dist/inputs/icon-button.d.ts +44 -0
- package/dist/inputs/icon-button.js +30 -0
- package/dist/inputs/icon-button.js.map +1 -0
- package/dist/inputs/index.d.ts +22 -0
- package/dist/inputs/index.js +92 -0
- package/dist/inputs/index.js.map +1 -0
- package/dist/inputs/input-group.d.ts +77 -0
- package/dist/inputs/input-group.js +76 -0
- package/dist/inputs/input-group.js.map +1 -0
- package/dist/inputs/input.d.ts +58 -0
- package/dist/inputs/input.js +68 -0
- package/dist/inputs/input.js.map +1 -0
- package/dist/inputs/label.d.ts +47 -0
- package/dist/inputs/label.js +29 -0
- package/dist/inputs/label.js.map +1 -0
- package/dist/inputs/number-input.d.ts +62 -0
- package/dist/inputs/number-input.js +86 -0
- package/dist/inputs/number-input.js.map +1 -0
- package/dist/inputs/password-input.d.ts +66 -0
- package/dist/inputs/password-input.js +73 -0
- package/dist/inputs/password-input.js.map +1 -0
- package/dist/inputs/radio.d.ts +66 -0
- package/dist/inputs/radio.js +96 -0
- package/dist/inputs/radio.js.map +1 -0
- package/dist/inputs/search-input.d.ts +66 -0
- package/dist/inputs/search-input.js +76 -0
- package/dist/inputs/search-input.js.map +1 -0
- package/dist/inputs/select.d.ts +65 -0
- package/dist/inputs/select.js +139 -0
- package/dist/inputs/select.js.map +1 -0
- package/dist/inputs/slider.d.ts +67 -0
- package/dist/inputs/slider.js +107 -0
- package/dist/inputs/slider.js.map +1 -0
- package/dist/inputs/switch.d.ts +42 -0
- package/dist/inputs/switch.js +69 -0
- package/dist/inputs/switch.js.map +1 -0
- package/dist/inputs/textarea.d.ts +51 -0
- package/dist/inputs/textarea.js +82 -0
- package/dist/inputs/textarea.js.map +1 -0
- package/dist/inputs/toggle.d.ts +57 -0
- package/dist/inputs/toggle.js +91 -0
- package/dist/inputs/toggle.js.map +1 -0
- package/dist/internal/index.d.ts +5 -0
- package/dist/internal/index.js +19 -0
- package/dist/internal/index.js.map +1 -0
- package/dist/internal/refs.d.ts +31 -0
- package/dist/internal/refs.js +34 -0
- package/dist/internal/refs.js.map +1 -0
- package/dist/internal/slot.d.ts +76 -0
- package/dist/internal/slot.js +40 -0
- package/dist/internal/slot.js.map +1 -0
- package/dist/internal/use-controllable-state.d.ts +25 -0
- package/dist/internal/use-controllable-state.js +38 -0
- package/dist/internal/use-controllable-state.js.map +1 -0
- package/dist/layout/index.d.ts +131 -0
- package/dist/layout/index.js +212 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/navigation/index.d.ts +114 -0
- package/dist/navigation/index.js +523 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/overlays/alert-dialog.d.ts +75 -0
- package/dist/overlays/alert-dialog.js +163 -0
- package/dist/overlays/alert-dialog.js.map +1 -0
- package/dist/overlays/dialog.d.ts +44 -0
- package/dist/overlays/dialog.js +121 -0
- package/dist/overlays/dialog.js.map +1 -0
- package/dist/overlays/drawer.d.ts +49 -0
- package/dist/overlays/drawer.js +125 -0
- package/dist/overlays/drawer.js.map +1 -0
- package/dist/overlays/dropdown-menu.d.ts +53 -0
- package/dist/overlays/dropdown-menu.js +192 -0
- package/dist/overlays/dropdown-menu.js.map +1 -0
- package/dist/overlays/index.d.ts +10 -0
- package/dist/overlays/index.js +169 -0
- package/dist/overlays/index.js.map +1 -0
- package/dist/overlays/popover.d.ts +58 -0
- package/dist/overlays/popover.js +105 -0
- package/dist/overlays/popover.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/typography/index.d.ts +79 -0
- package/dist/typography/index.js +280 -0
- package/dist/typography/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { VariantProps } from '@tea-ui/utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TEA UI — Input.
|
|
7
|
+
*
|
|
8
|
+
* A single-line text field, and the reference for every other TEA UI control:
|
|
9
|
+
* it wires itself to a `Field` through {@link useFieldControlProps} rather than
|
|
10
|
+
* accepting `id` and `aria-describedby` by hand, and it derives its height from
|
|
11
|
+
* `--tea-control-h` so one `data-density` attribute retunes every input in a
|
|
12
|
+
* container.
|
|
13
|
+
*
|
|
14
|
+
* Three decisions that are not obvious:
|
|
15
|
+
*
|
|
16
|
+
* 1. **The font goes to 16px on a coarse pointer.** A field rendered below
|
|
17
|
+
* 16px makes iOS Safari zoom the page on focus, and it does not zoom back
|
|
18
|
+
* out when the user tabs away — the page is left magnified. The source
|
|
19
|
+
* project patched this with `md:text-sm`, which is a viewport-width guess:
|
|
20
|
+
* it leaves a phone in landscape at 14px and zooms anyway. `pointer-coarse:`
|
|
21
|
+
* asks the only question that matters, which is whether the input is being
|
|
22
|
+
* operated by a finger. Tailwind emits the media query after the bare size
|
|
23
|
+
* utilities, so it wins without an `!`-prefix.
|
|
24
|
+
* 2. **`invalid` is an appearance and a state.** It sets `aria-invalid` *and*
|
|
25
|
+
* moves the border and ring to the critical tone. A red border with no
|
|
26
|
+
* `aria-invalid` is decoration, and a screen reader user is not told.
|
|
27
|
+
* 3. **The native `size` attribute is gone.** `size` is a styling axis in TEA
|
|
28
|
+
* UI, and a character count on a text input is a suggestion nobody follows.
|
|
29
|
+
* Use `width` in `className` for a size hint.
|
|
30
|
+
*/
|
|
31
|
+
declare const inputVariants: (props?: ({
|
|
32
|
+
size?: "sm" | "md" | "lg" | null | undefined;
|
|
33
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
34
|
+
interface InputProps extends Omit<React.ComponentProps<"input">, "size" | "color">, VariantProps<typeof inputVariants> {
|
|
35
|
+
/**
|
|
36
|
+
* The value is not acceptable. Sets `aria-invalid` and moves the border and
|
|
37
|
+
* ring to the critical tone.
|
|
38
|
+
*
|
|
39
|
+
* Inside a `Field` the field's `invalid` state wins, because a control that
|
|
40
|
+
* disagrees with the field that describes it is the bug the Field system
|
|
41
|
+
* exists to prevent. Set it here only for a standalone input.
|
|
42
|
+
*/
|
|
43
|
+
invalid?: boolean | undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <Field required invalid={!!error}>
|
|
49
|
+
* <FieldLabel>E-Mail</FieldLabel>
|
|
50
|
+
* <Input type="email" autoComplete="email" />
|
|
51
|
+
* <FieldDescription>Wir senden keine Bestätigung.</FieldDescription>
|
|
52
|
+
* <FieldError>{error}</FieldError>
|
|
53
|
+
* </Field>
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
declare const Input: React.ForwardRefExoticComponent<Omit<InputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
57
|
+
|
|
58
|
+
export { Input, type InputProps, inputVariants };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cva, cn } from "@tea-ui/utils";
|
|
4
|
+
import { dataSlot, stateAttributes } from "../internal/index.js";
|
|
5
|
+
import { useFieldControlProps } from "./field.js";
|
|
6
|
+
import { useInputGroupContext } from "./input-group.js";
|
|
7
|
+
const inputVariants = cva(
|
|
8
|
+
[
|
|
9
|
+
"w-full min-w-0 rounded-none border border-line bg-surface text-fg",
|
|
10
|
+
"px-[length:var(--tea-control-px)]",
|
|
11
|
+
"transition-[background-color,border-color,color,box-shadow] duration-[120ms] ease-standard",
|
|
12
|
+
"outline-none",
|
|
13
|
+
"placeholder:text-fg-subtle",
|
|
14
|
+
// Below 16px, iOS Safari zooms the page on focus and never zooms back out.
|
|
15
|
+
"pointer-coarse:text-body",
|
|
16
|
+
// `focus-visible:` and never `focus:` — a ring on every mouse press is
|
|
17
|
+
// noise, and it is the reason the audit's tables looked like this.
|
|
18
|
+
"focus-visible:border-ring-strong focus-visible:ring-2 focus-visible:ring-ring",
|
|
19
|
+
"disabled:cursor-not-allowed disabled:border-line disabled:bg-surface-2 disabled:text-fg-subtle",
|
|
20
|
+
"read-only:cursor-default read-only:bg-surface-2 read-only:text-fg-muted",
|
|
21
|
+
"aria-[invalid=true]:border-critical aria-[invalid=true]:ring-critical",
|
|
22
|
+
"file:me-3 file:rounded-none file:border-0 file:bg-transparent file:font-medium file:text-ui"
|
|
23
|
+
],
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
size: {
|
|
27
|
+
sm: "control-h text-micro",
|
|
28
|
+
md: "control-h text-ui",
|
|
29
|
+
lg: "h-[calc(var(--tea-control-h)+0.25rem)] text-ui"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
size: "md"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
const Input = React.forwardRef(function Input2({ className, size, invalid, disabled, readOnly, required, type = "text", ...props }, ref) {
|
|
38
|
+
const field = useFieldControlProps();
|
|
39
|
+
const group = useInputGroupContext();
|
|
40
|
+
const isInvalid = field["aria-invalid"] ?? invalid ?? false;
|
|
41
|
+
const isDisabled = field.disabled ?? disabled ?? false;
|
|
42
|
+
const isReadOnly = field.readOnly ?? readOnly ?? false;
|
|
43
|
+
const isRequired = field["aria-required"] ?? required ?? false;
|
|
44
|
+
return /* @__PURE__ */ jsx(
|
|
45
|
+
"input",
|
|
46
|
+
{
|
|
47
|
+
ref,
|
|
48
|
+
type,
|
|
49
|
+
id: field.id,
|
|
50
|
+
"aria-describedby": field["aria-describedby"],
|
|
51
|
+
"aria-invalid": isInvalid || void 0,
|
|
52
|
+
"aria-required": isRequired || void 0,
|
|
53
|
+
disabled: isDisabled || void 0,
|
|
54
|
+
readOnly: isReadOnly || void 0,
|
|
55
|
+
required: isRequired || void 0,
|
|
56
|
+
"data-tea-touch": true,
|
|
57
|
+
...stateAttributes({ disabled: isDisabled, invalid: isInvalid, readOnly: isReadOnly }),
|
|
58
|
+
...dataSlot("input"),
|
|
59
|
+
className: cn(inputVariants({ size }), group?.controlClassName, className),
|
|
60
|
+
...props
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
export {
|
|
65
|
+
Input,
|
|
66
|
+
inputVariants
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/inputs/input.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cva, cn, type VariantProps } from \"@tea-ui/utils\";\n\nimport { dataSlot, stateAttributes } from \"../internal\";\nimport { useFieldControlProps } from \"./field\";\nimport { useInputGroupContext } from \"./input-group\";\n\n/**\n * TEA UI — Input.\n *\n * A single-line text field, and the reference for every other TEA UI control:\n * it wires itself to a `Field` through {@link useFieldControlProps} rather than\n * accepting `id` and `aria-describedby` by hand, and it derives its height from\n * `--tea-control-h` so one `data-density` attribute retunes every input in a\n * container.\n *\n * Three decisions that are not obvious:\n *\n * 1. **The font goes to 16px on a coarse pointer.** A field rendered below\n * 16px makes iOS Safari zoom the page on focus, and it does not zoom back\n * out when the user tabs away — the page is left magnified. The source\n * project patched this with `md:text-sm`, which is a viewport-width guess:\n * it leaves a phone in landscape at 14px and zooms anyway. `pointer-coarse:`\n * asks the only question that matters, which is whether the input is being\n * operated by a finger. Tailwind emits the media query after the bare size\n * utilities, so it wins without an `!`-prefix.\n * 2. **`invalid` is an appearance and a state.** It sets `aria-invalid` *and*\n * moves the border and ring to the critical tone. A red border with no\n * `aria-invalid` is decoration, and a screen reader user is not told.\n * 3. **The native `size` attribute is gone.** `size` is a styling axis in TEA\n * UI, and a character count on a text input is a suggestion nobody follows.\n * Use `width` in `className` for a size hint.\n */\nexport const inputVariants = cva(\n [\n \"w-full min-w-0 rounded-none border border-line bg-surface text-fg\",\n \"px-[length:var(--tea-control-px)]\",\n \"transition-[background-color,border-color,color,box-shadow] duration-[120ms] ease-standard\",\n \"outline-none\",\n \"placeholder:text-fg-subtle\",\n // Below 16px, iOS Safari zooms the page on focus and never zooms back out.\n \"pointer-coarse:text-body\",\n // `focus-visible:` and never `focus:` — a ring on every mouse press is\n // noise, and it is the reason the audit's tables looked like this.\n \"focus-visible:border-ring-strong focus-visible:ring-2 focus-visible:ring-ring\",\n \"disabled:cursor-not-allowed disabled:border-line disabled:bg-surface-2 disabled:text-fg-subtle\",\n \"read-only:cursor-default read-only:bg-surface-2 read-only:text-fg-muted\",\n \"aria-[invalid=true]:border-critical aria-[invalid=true]:ring-critical\",\n \"file:me-3 file:rounded-none file:border-0 file:bg-transparent file:font-medium file:text-ui\",\n ],\n {\n variants: {\n size: {\n sm: \"control-h text-micro\",\n md: \"control-h text-ui\",\n lg: \"h-[calc(var(--tea-control-h)+0.25rem)] text-ui\",\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n },\n);\n\nexport interface InputProps\n extends Omit<React.ComponentProps<\"input\">, \"size\" | \"color\">,\n VariantProps<typeof inputVariants> {\n /**\n * The value is not acceptable. Sets `aria-invalid` and moves the border and\n * ring to the critical tone.\n *\n * Inside a `Field` the field's `invalid` state wins, because a control that\n * disagrees with the field that describes it is the bug the Field system\n * exists to prevent. Set it here only for a standalone input.\n */\n invalid?: boolean | undefined;\n}\n\n/**\n * @example\n * ```tsx\n * <Field required invalid={!!error}>\n * <FieldLabel>E-Mail</FieldLabel>\n * <Input type=\"email\" autoComplete=\"email\" />\n * <FieldDescription>Wir senden keine Bestätigung.</FieldDescription>\n * <FieldError>{error}</FieldError>\n * </Field>\n * ```\n */\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input(\n { className, size, invalid, disabled, readOnly, required, type = \"text\", ...props },\n ref,\n) {\n const field = useFieldControlProps();\n const group = useInputGroupContext();\n\n const isInvalid = field[\"aria-invalid\"] ?? invalid ?? false;\n const isDisabled = field.disabled ?? disabled ?? false;\n const isReadOnly = field.readOnly ?? readOnly ?? false;\n const isRequired = field[\"aria-required\"] ?? required ?? false;\n\n return (\n <input\n ref={ref}\n type={type}\n id={field.id}\n aria-describedby={field[\"aria-describedby\"]}\n aria-invalid={isInvalid || undefined}\n aria-required={isRequired || undefined}\n disabled={isDisabled || undefined}\n readOnly={isReadOnly || undefined}\n required={isRequired || undefined}\n data-tea-touch\n {...stateAttributes({ disabled: isDisabled, invalid: isInvalid, readOnly: isReadOnly })}\n {...dataSlot(\"input\")}\n className={cn(inputVariants({ size }), group?.controlClassName, className)}\n {...props}\n />\n );\n});\n"],"mappings":"AAsGI;AAtGH,YAAY,WAAW;AACxB,SAAS,KAAK,UAA6B;AAE3C,SAAS,UAAU,uBAAuB;AAC1C,SAAS,4BAA4B;AACrC,SAAS,4BAA4B;AA4B9B,MAAM,gBAAgB;AAAA,EAC3B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AA2BO,MAAM,QAAQ,MAAM,WAAyC,SAASA,OAC3E,EAAE,WAAW,MAAM,SAAS,UAAU,UAAU,UAAU,OAAO,QAAQ,GAAG,MAAM,GAClF,KACA;AACA,QAAM,QAAQ,qBAAqB;AACnC,QAAM,QAAQ,qBAAqB;AAEnC,QAAM,YAAY,MAAM,cAAc,KAAK,WAAW;AACtD,QAAM,aAAa,MAAM,YAAY,YAAY;AACjD,QAAM,aAAa,MAAM,YAAY,YAAY;AACjD,QAAM,aAAa,MAAM,eAAe,KAAK,YAAY;AAEzD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,IAAI,MAAM;AAAA,MACV,oBAAkB,MAAM,kBAAkB;AAAA,MAC1C,gBAAc,aAAa;AAAA,MAC3B,iBAAe,cAAc;AAAA,MAC7B,UAAU,cAAc;AAAA,MACxB,UAAU,cAAc;AAAA,MACxB,UAAU,cAAc;AAAA,MACxB,kBAAc;AAAA,MACb,GAAG,gBAAgB,EAAE,UAAU,YAAY,SAAS,WAAW,UAAU,WAAW,CAAC;AAAA,MACrF,GAAG,SAAS,OAAO;AAAA,MACpB,WAAW,GAAG,cAAc,EAAE,KAAK,CAAC,GAAG,OAAO,kBAAkB,SAAS;AAAA,MACxE,GAAG;AAAA;AAAA,EACN;AAEJ,CAAC;","names":["Input"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Label as Label$1 } from 'radix-ui';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TEA UI — Label.
|
|
6
|
+
*
|
|
7
|
+
* A label for a control, for the cases where `FieldLabel` is not the right
|
|
8
|
+
* thing: a filter chip row, a settings list, a dialog's field grid that already
|
|
9
|
+
* has its own layout.
|
|
10
|
+
*
|
|
11
|
+
* It is a real `<label>` with a real `htmlFor`, which is the whole point. The
|
|
12
|
+
* audit found that most of the problems it was supposed to solve came from
|
|
13
|
+
* treating a label as a styling element that happens to sit near a control.
|
|
14
|
+
* `htmlFor` is not decoration: it is what gives the control its accessible
|
|
15
|
+
* name, and without it a text field is an unnamed box to a screen reader and
|
|
16
|
+
* an unspeakable one to a voice-control user.
|
|
17
|
+
*
|
|
18
|
+
* `Label.Root` from `radix-ui` rather than a bare `<label>`, because it also
|
|
19
|
+
* makes clicking the label activate the control when the control is a Radix
|
|
20
|
+
* primitive rendering a `<button>`.
|
|
21
|
+
*/
|
|
22
|
+
interface LabelProps extends React.ComponentProps<typeof Label$1.Root> {
|
|
23
|
+
/**
|
|
24
|
+
* Mark the label's control as required. Renders a visible asterisk plus a
|
|
25
|
+
* visually hidden "Pflichtfeld", because an asterisk is a shape a screen
|
|
26
|
+
* reader cannot report.
|
|
27
|
+
*
|
|
28
|
+
* The control still needs `aria-required` — inside a `Field` that comes from
|
|
29
|
+
* the field, and `FieldLabel` reads it from the same place.
|
|
30
|
+
*/
|
|
31
|
+
required?: boolean | undefined;
|
|
32
|
+
/** The control this label names is disabled. Styles the label to match. */
|
|
33
|
+
disabled?: boolean | undefined;
|
|
34
|
+
className?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <Label htmlFor="display-name" required>
|
|
40
|
+
* Anzeigename
|
|
41
|
+
* </Label>
|
|
42
|
+
* <Input id="display-name" aria-required />
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare const Label: React.ForwardRefExoticComponent<Omit<LabelProps, "ref"> & React.RefAttributes<HTMLLabelElement>>;
|
|
46
|
+
|
|
47
|
+
export { Label, type LabelProps };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Label as LabelPrimitive } from "radix-ui";
|
|
4
|
+
import { cn } from "@tea-ui/utils";
|
|
5
|
+
import { COPY } from "@tea-ui/ux-standards";
|
|
6
|
+
import { dataSlot, stateAttributes } from "../internal/index.js";
|
|
7
|
+
const Label = React.forwardRef(function Label2({ children, className, required = false, disabled, ...props }, ref) {
|
|
8
|
+
return /* @__PURE__ */ jsxs(
|
|
9
|
+
LabelPrimitive.Root,
|
|
10
|
+
{
|
|
11
|
+
ref,
|
|
12
|
+
className: cn("inline-flex items-center gap-1 text-ui font-medium text-fg", className),
|
|
13
|
+
...stateAttributes({ disabled }),
|
|
14
|
+
...dataSlot("label"),
|
|
15
|
+
...props,
|
|
16
|
+
children: [
|
|
17
|
+
children,
|
|
18
|
+
required ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
19
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-critical", children: "*" }),
|
|
20
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: ` ${COPY.states.required}` })
|
|
21
|
+
] }) : null
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
export {
|
|
27
|
+
Label
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=label.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/inputs/label.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { Label as LabelPrimitive } from \"radix-ui\";\nimport { cn } from \"@tea-ui/utils\";\nimport { COPY } from \"@tea-ui/ux-standards\";\n\nimport { dataSlot, stateAttributes } from \"../internal\";\n\n/**\n * TEA UI — Label.\n *\n * A label for a control, for the cases where `FieldLabel` is not the right\n * thing: a filter chip row, a settings list, a dialog's field grid that already\n * has its own layout.\n *\n * It is a real `<label>` with a real `htmlFor`, which is the whole point. The\n * audit found that most of the problems it was supposed to solve came from\n * treating a label as a styling element that happens to sit near a control.\n * `htmlFor` is not decoration: it is what gives the control its accessible\n * name, and without it a text field is an unnamed box to a screen reader and\n * an unspeakable one to a voice-control user.\n *\n * `Label.Root` from `radix-ui` rather than a bare `<label>`, because it also\n * makes clicking the label activate the control when the control is a Radix\n * primitive rendering a `<button>`.\n */\n\nexport interface LabelProps extends React.ComponentProps<typeof LabelPrimitive.Root> {\n /**\n * Mark the label's control as required. Renders a visible asterisk plus a\n * visually hidden \"Pflichtfeld\", because an asterisk is a shape a screen\n * reader cannot report.\n *\n * The control still needs `aria-required` — inside a `Field` that comes from\n * the field, and `FieldLabel` reads it from the same place.\n */\n required?: boolean | undefined;\n /** The control this label names is disabled. Styles the label to match. */\n disabled?: boolean | undefined;\n className?: string | undefined;\n}\n\n/**\n * @example\n * ```tsx\n * <Label htmlFor=\"display-name\" required>\n * Anzeigename\n * </Label>\n * <Input id=\"display-name\" aria-required />\n * ```\n */\nexport const Label = React.forwardRef<HTMLLabelElement, LabelProps>(function Label(\n { children, className, required = false, disabled, ...props },\n ref,\n) {\n return (\n <LabelPrimitive.Root\n ref={ref}\n className={cn(\"inline-flex items-center gap-1 text-ui font-medium text-fg\", className)}\n {...stateAttributes({ disabled })}\n {...dataSlot(\"label\")}\n {...props}\n >\n {children}\n {required ? (\n <>\n <span aria-hidden=\"true\" className=\"text-critical\">\n *\n </span>\n <span className=\"sr-only\">{` ${COPY.states.required}`}</span>\n </>\n ) : null}\n </LabelPrimitive.Root>\n );\n});\n"],"mappings":"AAgEQ,mBACE,KADF;AAhEP,YAAY,WAAW;AACxB,SAAS,SAAS,sBAAsB;AACxC,SAAS,UAAU;AACnB,SAAS,YAAY;AAErB,SAAS,UAAU,uBAAuB;AA6CnC,MAAM,QAAQ,MAAM,WAAyC,SAASA,OAC3E,EAAE,UAAU,WAAW,WAAW,OAAO,UAAU,GAAG,MAAM,GAC5D,KACA;AACA,SACE;AAAA,IAAC,eAAe;AAAA,IAAf;AAAA,MACC;AAAA,MACA,WAAW,GAAG,8DAA8D,SAAS;AAAA,MACpF,GAAG,gBAAgB,EAAE,SAAS,CAAC;AAAA,MAC/B,GAAG,SAAS,OAAO;AAAA,MACnB,GAAG;AAAA,MAEH;AAAA;AAAA,QACA,WACC,iCACE;AAAA,8BAAC,UAAK,eAAY,QAAO,WAAU,iBAAgB,eAEnD;AAAA,UACA,oBAAC,UAAK,WAAU,WAAW,cAAI,KAAK,OAAO,QAAQ,IAAG;AAAA,WACxD,IACE;AAAA;AAAA;AAAA,EACN;AAEJ,CAAC;","names":["Label"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { InputProps } from './input.js';
|
|
3
|
+
import 'class-variance-authority/types';
|
|
4
|
+
import '@tea-ui/utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* TEA UI — NumberInput.
|
|
8
|
+
*
|
|
9
|
+
* A numeric field that clamps **on blur**, not on every keystroke.
|
|
10
|
+
*
|
|
11
|
+
* This is the whole design. The obvious implementation — clamp in `onChange` —
|
|
12
|
+
* is unusable: the moment a user selects `150` in order to type `90`, the clamp
|
|
13
|
+
* fires on the first keystroke, rewrites the box to `100`, and puts the cursor
|
|
14
|
+
* somewhere the user did not choose. A user cannot type a value *below* the
|
|
15
|
+
* current one. The same implementation makes a field that starts empty
|
|
16
|
+
* permanently unwritable, because the intermediate `-` or `,` is not a number.
|
|
17
|
+
*
|
|
18
|
+
* So the field holds whatever the user typed, and on blur the value is parsed,
|
|
19
|
+
* clamped into `[min, max]` and reported once. The user can type anything; the
|
|
20
|
+
* model can only receive a number inside the declared range. And no value is
|
|
21
|
+
* ever reported twice for the same number.
|
|
22
|
+
*
|
|
23
|
+
* `step` is passed to the DOM but deliberately **not** enforced on blur. A field
|
|
24
|
+
* that silently rewrites `0.333` to `0.33` is lying about what was typed, and no
|
|
25
|
+
* native numeric control snaps on blur either. The stepper and the arrow keys
|
|
26
|
+
* honour `step`; free typing does not.
|
|
27
|
+
*/
|
|
28
|
+
interface NumberInputProps extends Omit<InputProps, "value" | "defaultValue" | "onChange" | "type" | "inputMode" | "size"> {
|
|
29
|
+
/** Controlled numeric value. */
|
|
30
|
+
value?: number | undefined;
|
|
31
|
+
/** Uncontrolled initial numeric value. */
|
|
32
|
+
defaultValue?: number | undefined;
|
|
33
|
+
/** Called with the parsed number on change, and with the clamped value on blur. */
|
|
34
|
+
onValueChange?: ((value: number) => void) | undefined;
|
|
35
|
+
/** Lower bound. Enforced on blur. */
|
|
36
|
+
min?: number | undefined;
|
|
37
|
+
/** Upper bound. Enforced on blur. */
|
|
38
|
+
max?: number | undefined;
|
|
39
|
+
/** Increment for the native stepper and the arrow keys. Not enforced on blur. */
|
|
40
|
+
step?: number | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* `decimal` by default rather than `numeric`, because `numeric` asks a phone
|
|
43
|
+
* for a keypad with no comma key — and a German user then cannot type `0,5`
|
|
44
|
+
* at all, on a form that is otherwise entirely in German.
|
|
45
|
+
*/
|
|
46
|
+
inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
|
|
47
|
+
/** The control height. */
|
|
48
|
+
size?: InputProps["size"];
|
|
49
|
+
className?: string | undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <Field>
|
|
55
|
+
* <FieldLabel>Anzahl</FieldLabel>
|
|
56
|
+
* <NumberInput min={1} max={64} defaultValue={4} />
|
|
57
|
+
* </Field>
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
declare const NumberInput: React.ForwardRefExoticComponent<Omit<NumberInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
61
|
+
|
|
62
|
+
export { NumberInput, type NumberInputProps };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@tea-ui/utils";
|
|
4
|
+
import { Input } from "./input.js";
|
|
5
|
+
const NumberInput = React.forwardRef(
|
|
6
|
+
function NumberInput2({
|
|
7
|
+
value,
|
|
8
|
+
defaultValue,
|
|
9
|
+
onValueChange,
|
|
10
|
+
min,
|
|
11
|
+
max,
|
|
12
|
+
step,
|
|
13
|
+
inputMode = "decimal",
|
|
14
|
+
size,
|
|
15
|
+
className,
|
|
16
|
+
onBlur,
|
|
17
|
+
...props
|
|
18
|
+
}, ref) {
|
|
19
|
+
const isControlled = value !== void 0;
|
|
20
|
+
const [text, setText] = React.useState(() => stringify(value ?? defaultValue));
|
|
21
|
+
const lastEmitted = React.useRef(void 0);
|
|
22
|
+
const lastExternal = React.useRef(value);
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
if (isControlled && !Object.is(value, lastExternal.current)) {
|
|
25
|
+
lastExternal.current = value;
|
|
26
|
+
setText(stringify(value));
|
|
27
|
+
}
|
|
28
|
+
}, [isControlled, value]);
|
|
29
|
+
const emit = React.useCallback(
|
|
30
|
+
(next) => {
|
|
31
|
+
if (Object.is(lastEmitted.current, next)) return;
|
|
32
|
+
lastEmitted.current = next;
|
|
33
|
+
onValueChange?.(next);
|
|
34
|
+
},
|
|
35
|
+
[onValueChange]
|
|
36
|
+
);
|
|
37
|
+
const parse = (raw) => {
|
|
38
|
+
const parsed = Number.parseFloat(raw.trim().replace(",", "."));
|
|
39
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
40
|
+
};
|
|
41
|
+
return /* @__PURE__ */ jsx(
|
|
42
|
+
Input,
|
|
43
|
+
{
|
|
44
|
+
ref,
|
|
45
|
+
type: "number",
|
|
46
|
+
size,
|
|
47
|
+
inputMode,
|
|
48
|
+
min,
|
|
49
|
+
max,
|
|
50
|
+
step,
|
|
51
|
+
className: cn(className),
|
|
52
|
+
value: text,
|
|
53
|
+
onChange: (event) => {
|
|
54
|
+
setText(event.target.value);
|
|
55
|
+
const parsed = parse(event.target.value);
|
|
56
|
+
if (parsed !== null) emit(clamp(parsed, min, max));
|
|
57
|
+
},
|
|
58
|
+
onBlur: (event) => {
|
|
59
|
+
const parsed = parse(text);
|
|
60
|
+
if (parsed === null) {
|
|
61
|
+
if (text.trim() === "") setText(stringify(value ?? defaultValue));
|
|
62
|
+
} else {
|
|
63
|
+
const next = clamp(parsed, min, max);
|
|
64
|
+
setText(stringify(next));
|
|
65
|
+
emit(next);
|
|
66
|
+
}
|
|
67
|
+
onBlur?.(event);
|
|
68
|
+
},
|
|
69
|
+
...props
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
function stringify(value) {
|
|
75
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "";
|
|
76
|
+
}
|
|
77
|
+
function clamp(value, min, max) {
|
|
78
|
+
let next = value;
|
|
79
|
+
if (typeof min === "number" && next < min) next = min;
|
|
80
|
+
if (typeof max === "number" && next > max) next = max;
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
NumberInput
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=number-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/inputs/number-input.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@tea-ui/utils\";\n\nimport { Input, type InputProps } from \"./input\";\n\n/**\n * TEA UI — NumberInput.\n *\n * A numeric field that clamps **on blur**, not on every keystroke.\n *\n * This is the whole design. The obvious implementation — clamp in `onChange` —\n * is unusable: the moment a user selects `150` in order to type `90`, the clamp\n * fires on the first keystroke, rewrites the box to `100`, and puts the cursor\n * somewhere the user did not choose. A user cannot type a value *below* the\n * current one. The same implementation makes a field that starts empty\n * permanently unwritable, because the intermediate `-` or `,` is not a number.\n *\n * So the field holds whatever the user typed, and on blur the value is parsed,\n * clamped into `[min, max]` and reported once. The user can type anything; the\n * model can only receive a number inside the declared range. And no value is\n * ever reported twice for the same number.\n *\n * `step` is passed to the DOM but deliberately **not** enforced on blur. A field\n * that silently rewrites `0.333` to `0.33` is lying about what was typed, and no\n * native numeric control snaps on blur either. The stepper and the arrow keys\n * honour `step`; free typing does not.\n */\nexport interface NumberInputProps\n extends Omit<InputProps, \"value\" | \"defaultValue\" | \"onChange\" | \"type\" | \"inputMode\" | \"size\"> {\n /** Controlled numeric value. */\n value?: number | undefined;\n /** Uncontrolled initial numeric value. */\n defaultValue?: number | undefined;\n /** Called with the parsed number on change, and with the clamped value on blur. */\n onValueChange?: ((value: number) => void) | undefined;\n /** Lower bound. Enforced on blur. */\n min?: number | undefined;\n /** Upper bound. Enforced on blur. */\n max?: number | undefined;\n /** Increment for the native stepper and the arrow keys. Not enforced on blur. */\n step?: number | undefined;\n /**\n * `decimal` by default rather than `numeric`, because `numeric` asks a phone\n * for a keypad with no comma key — and a German user then cannot type `0,5`\n * at all, on a form that is otherwise entirely in German.\n */\n inputMode?: React.HTMLAttributes<HTMLInputElement>[\"inputMode\"];\n /** The control height. */\n size?: InputProps[\"size\"];\n className?: string | undefined;\n}\n\n/**\n * @example\n * ```tsx\n * <Field>\n * <FieldLabel>Anzahl</FieldLabel>\n * <NumberInput min={1} max={64} defaultValue={4} />\n * </Field>\n * ```\n */\nexport const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n function NumberInput(\n {\n value,\n defaultValue,\n onValueChange,\n min,\n max,\n step,\n inputMode = \"decimal\",\n size,\n className,\n onBlur,\n ...props\n },\n ref,\n ) {\n const isControlled = value !== undefined;\n // The text in the box belongs to the user and is allowed to be temporarily\n // unparseable. That is the entire reason this component exists.\n const [text, setText] = React.useState(() => stringify(value ?? defaultValue));\n const lastEmitted = React.useRef<number | undefined>(undefined);\n\n // A controlled value that changes from outside must reach the box — but the\n // box is only overwritten when the *outside* value actually changed, never\n // because a render happened.\n const lastExternal = React.useRef(value);\n React.useEffect(() => {\n if (isControlled && !Object.is(value, lastExternal.current)) {\n lastExternal.current = value;\n setText(stringify(value));\n }\n }, [isControlled, value]);\n\n // The contract is explicit: never emit a change for a value that did not\n // change. Without this, a blur re-reports the number that the last\n // keystroke already reported, and a consumer writing that straight into\n // state renders a second time for nothing.\n const emit = React.useCallback(\n (next: number) => {\n if (Object.is(lastEmitted.current, next)) return;\n lastEmitted.current = next;\n onValueChange?.(next);\n },\n [onValueChange],\n );\n\n const parse = (raw: string): number | null => {\n const parsed = Number.parseFloat(raw.trim().replace(\",\", \".\"));\n return Number.isFinite(parsed) ? parsed : null;\n };\n\n return (\n <Input\n ref={ref}\n type=\"number\"\n size={size}\n inputMode={inputMode}\n min={min}\n max={max}\n step={step}\n className={cn(className)}\n value={text}\n onChange={(event) => {\n setText(event.target.value);\n const parsed = parse(event.target.value);\n if (parsed !== null) emit(clamp(parsed, min, max));\n }}\n onBlur={(event) => {\n const parsed = parse(text);\n if (parsed === null) {\n // An empty or half-typed box reverts to the last known good value\n // instead of reporting `NaN` into the model.\n if (text.trim() === \"\") setText(stringify(value ?? defaultValue));\n } else {\n const next = clamp(parsed, min, max);\n setText(stringify(next));\n emit(next);\n }\n onBlur?.(event);\n }}\n {...props}\n />\n );\n },\n);\n\nfunction stringify(value: number | undefined): string {\n return typeof value === \"number\" && Number.isFinite(value) ? String(value) : \"\";\n}\n\nfunction clamp(value: number, min: number | undefined, max: number | undefined): number {\n let next = value;\n if (typeof min === \"number\" && next < min) next = min;\n if (typeof max === \"number\" && next > max) next = max;\n return next;\n}\n"],"mappings":"AAkHM;AAlHN,YAAY,WAAW;AACvB,SAAS,UAAU;AAEnB,SAAS,aAA8B;AA0DhC,MAAM,cAAc,MAAM;AAAA,EAC/B,SAASA,aACP;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,GACA,KACA;AACA,UAAM,eAAe,UAAU;AAG/B,UAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,MAAM,UAAU,SAAS,YAAY,CAAC;AAC7E,UAAM,cAAc,MAAM,OAA2B,MAAS;AAK9D,UAAM,eAAe,MAAM,OAAO,KAAK;AACvC,UAAM,UAAU,MAAM;AACpB,UAAI,gBAAgB,CAAC,OAAO,GAAG,OAAO,aAAa,OAAO,GAAG;AAC3D,qBAAa,UAAU;AACvB,gBAAQ,UAAU,KAAK,CAAC;AAAA,MAC1B;AAAA,IACF,GAAG,CAAC,cAAc,KAAK,CAAC;AAMxB,UAAM,OAAO,MAAM;AAAA,MACjB,CAAC,SAAiB;AAChB,YAAI,OAAO,GAAG,YAAY,SAAS,IAAI,EAAG;AAC1C,oBAAY,UAAU;AACtB,wBAAgB,IAAI;AAAA,MACtB;AAAA,MACA,CAAC,aAAa;AAAA,IAChB;AAEA,UAAM,QAAQ,CAAC,QAA+B;AAC5C,YAAM,SAAS,OAAO,WAAW,IAAI,KAAK,EAAE,QAAQ,KAAK,GAAG,CAAC;AAC7D,aAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAAA,IAC5C;AAEA,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,GAAG,SAAS;AAAA,QACvB,OAAO;AAAA,QACP,UAAU,CAAC,UAAU;AACnB,kBAAQ,MAAM,OAAO,KAAK;AAC1B,gBAAM,SAAS,MAAM,MAAM,OAAO,KAAK;AACvC,cAAI,WAAW,KAAM,MAAK,MAAM,QAAQ,KAAK,GAAG,CAAC;AAAA,QACnD;AAAA,QACA,QAAQ,CAAC,UAAU;AACjB,gBAAM,SAAS,MAAM,IAAI;AACzB,cAAI,WAAW,MAAM;AAGnB,gBAAI,KAAK,KAAK,MAAM,GAAI,SAAQ,UAAU,SAAS,YAAY,CAAC;AAAA,UAClE,OAAO;AACL,kBAAM,OAAO,MAAM,QAAQ,KAAK,GAAG;AACnC,oBAAQ,UAAU,IAAI,CAAC;AACvB,iBAAK,IAAI;AAAA,UACX;AACA,mBAAS,KAAK;AAAA,QAChB;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA,EAEJ;AACF;AAEA,SAAS,UAAU,OAAmC;AACpD,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI;AAC/E;AAEA,SAAS,MAAM,OAAe,KAAyB,KAAiC;AACtF,MAAI,OAAO;AACX,MAAI,OAAO,QAAQ,YAAY,OAAO,IAAK,QAAO;AAClD,MAAI,OAAO,QAAQ,YAAY,OAAO,IAAK,QAAO;AAClD,SAAO;AACT;","names":["NumberInput"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { InputProps } from './input.js';
|
|
3
|
+
import 'class-variance-authority/types';
|
|
4
|
+
import '@tea-ui/utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* TEA UI — PasswordInput.
|
|
8
|
+
*
|
|
9
|
+
* A password field with a show/hide toggle. Three decisions:
|
|
10
|
+
*
|
|
11
|
+
* - **`aria-pressed`, not just an icon swap.** A toggle that only changes which
|
|
12
|
+
* glyph is on screen tells a screen reader user nothing about the state. The
|
|
13
|
+
* button reports whether the password is *currently* visible, and its name
|
|
14
|
+
* says what pressing it will do.
|
|
15
|
+
* - **The name says the effect, not the state.** "Passwort anzeigen" while
|
|
16
|
+
* hidden, "Passwort verbergen" while shown. A button labelled "Anzeigen"
|
|
17
|
+
* that sometimes hides is a button nobody trusts.
|
|
18
|
+
* - **Paste is never blocked.** A password manager fills this field by
|
|
19
|
+
* pasting. A `onPaste` handler that refuses a paste — a pattern the source
|
|
20
|
+
* project shipped — breaks every password manager on the planet, and is a
|
|
21
|
+
* security control that does not measure security: a password in the
|
|
22
|
+
* clipboard is a password the user chose to copy.
|
|
23
|
+
*/
|
|
24
|
+
interface PasswordInputProps extends Omit<InputProps, "type" | "value" | "defaultValue" | "onChange" | "size"> {
|
|
25
|
+
/** Controlled value. */
|
|
26
|
+
value?: string | undefined;
|
|
27
|
+
/** Uncontrolled initial value. */
|
|
28
|
+
defaultValue?: string | undefined;
|
|
29
|
+
/** Called on every keystroke, in both controlled and uncontrolled mode. */
|
|
30
|
+
onValueChange?: ((value: string) => void) | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Start with the password visible. Almost always `false`; it exists for the
|
|
33
|
+
* one-time-secret case, and for tests.
|
|
34
|
+
*/
|
|
35
|
+
visible?: boolean | undefined;
|
|
36
|
+
/** Uncontrolled initial visibility. */
|
|
37
|
+
defaultVisible?: boolean | undefined;
|
|
38
|
+
/** Called whenever the visibility flips. */
|
|
39
|
+
onVisibleChange?: ((visible: boolean) => void) | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Defaults to `current-password`, the only value a browser password manager
|
|
42
|
+
* will offer to fill. Override with `new-password` on a registration form —
|
|
43
|
+
* leaving it as `current-password` there is why managers offer the *old*
|
|
44
|
+
* password for a *new* one.
|
|
45
|
+
*/
|
|
46
|
+
autoComplete?: string | undefined;
|
|
47
|
+
/** Name for the toggle while the password is hidden. */
|
|
48
|
+
showLabel?: string | undefined;
|
|
49
|
+
/** Name for the toggle while the password is visible. */
|
|
50
|
+
hideLabel?: string | undefined;
|
|
51
|
+
/** The control height. */
|
|
52
|
+
size?: InputProps["size"];
|
|
53
|
+
className?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <Field>
|
|
59
|
+
* <FieldLabel>Passwort</FieldLabel>
|
|
60
|
+
* <PasswordInput autoComplete="new-password" />
|
|
61
|
+
* </Field>
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
declare const PasswordInput: React.ForwardRefExoticComponent<Omit<PasswordInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
65
|
+
|
|
66
|
+
export { PasswordInput, type PasswordInputProps };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Eye, EyeOff } from "@tea-ui/icons";
|
|
4
|
+
import { cn } from "@tea-ui/utils";
|
|
5
|
+
import { dataSlot, useControllableState, withPrivateRef } from "../internal/index.js";
|
|
6
|
+
import { IconButton } from "./icon-button.js";
|
|
7
|
+
import { Input } from "./input.js";
|
|
8
|
+
import { InputGroup, InputGroupEnd } from "./input-group.js";
|
|
9
|
+
const DEFAULT_SHOW_LABEL = "Passwort anzeigen";
|
|
10
|
+
const DEFAULT_HIDE_LABEL = "Passwort verbergen";
|
|
11
|
+
const PasswordInput = React.forwardRef(
|
|
12
|
+
function PasswordInput2({
|
|
13
|
+
value,
|
|
14
|
+
defaultValue = "",
|
|
15
|
+
onValueChange,
|
|
16
|
+
visible,
|
|
17
|
+
defaultVisible = false,
|
|
18
|
+
onVisibleChange,
|
|
19
|
+
autoComplete = "current-password",
|
|
20
|
+
showLabel = DEFAULT_SHOW_LABEL,
|
|
21
|
+
hideLabel = DEFAULT_HIDE_LABEL,
|
|
22
|
+
size,
|
|
23
|
+
className,
|
|
24
|
+
disabled,
|
|
25
|
+
readOnly,
|
|
26
|
+
...props
|
|
27
|
+
}, ref) {
|
|
28
|
+
const [password, setPassword] = useControllableState({
|
|
29
|
+
value,
|
|
30
|
+
defaultValue,
|
|
31
|
+
onChange: onValueChange,
|
|
32
|
+
name: "PasswordInput"
|
|
33
|
+
});
|
|
34
|
+
const [isVisible, setIsVisible] = useControllableState({
|
|
35
|
+
value: visible,
|
|
36
|
+
defaultValue: defaultVisible,
|
|
37
|
+
onChange: onVisibleChange,
|
|
38
|
+
name: "PasswordInput.visible"
|
|
39
|
+
});
|
|
40
|
+
const inputRef = React.useRef(null);
|
|
41
|
+
return /* @__PURE__ */ jsxs(InputGroup, { className: cn(className), ...dataSlot("password-input"), children: [
|
|
42
|
+
/* @__PURE__ */ jsx(
|
|
43
|
+
Input,
|
|
44
|
+
{
|
|
45
|
+
ref: withPrivateRef(ref, void 0, inputRef),
|
|
46
|
+
type: isVisible ? "text" : "password",
|
|
47
|
+
size,
|
|
48
|
+
autoComplete,
|
|
49
|
+
value: password,
|
|
50
|
+
onChange: (event) => setPassword(event.target.value),
|
|
51
|
+
disabled,
|
|
52
|
+
readOnly,
|
|
53
|
+
...props
|
|
54
|
+
}
|
|
55
|
+
),
|
|
56
|
+
/* @__PURE__ */ jsx(InputGroupEnd, { children: /* @__PURE__ */ jsx(
|
|
57
|
+
IconButton,
|
|
58
|
+
{
|
|
59
|
+
label: isVisible ? hideLabel : showLabel,
|
|
60
|
+
"aria-pressed": isVisible,
|
|
61
|
+
size: "sm",
|
|
62
|
+
disabled,
|
|
63
|
+
onClick: () => setIsVisible(!isVisible),
|
|
64
|
+
children: isVisible ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
|
|
65
|
+
}
|
|
66
|
+
) })
|
|
67
|
+
] });
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
export {
|
|
71
|
+
PasswordInput
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=password-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/inputs/password-input.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { Eye, EyeOff } from \"@tea-ui/icons\";\nimport { cn } from \"@tea-ui/utils\";\n\nimport { dataSlot, useControllableState, withPrivateRef } from \"../internal\";\nimport { IconButton } from \"./icon-button\";\nimport { Input, type InputProps } from \"./input\";\nimport { InputGroup, InputGroupEnd } from \"./input-group\";\n\n/**\n * TEA UI — PasswordInput.\n *\n * A password field with a show/hide toggle. Three decisions:\n *\n * - **`aria-pressed`, not just an icon swap.** A toggle that only changes which\n * glyph is on screen tells a screen reader user nothing about the state. The\n * button reports whether the password is *currently* visible, and its name\n * says what pressing it will do.\n * - **The name says the effect, not the state.** \"Passwort anzeigen\" while\n * hidden, \"Passwort verbergen\" while shown. A button labelled \"Anzeigen\"\n * that sometimes hides is a button nobody trusts.\n * - **Paste is never blocked.** A password manager fills this field by\n * pasting. A `onPaste` handler that refuses a paste — a pattern the source\n * project shipped — breaks every password manager on the planet, and is a\n * security control that does not measure security: a password in the\n * clipboard is a password the user chose to copy.\n */\nexport interface PasswordInputProps\n extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\" | \"onChange\" | \"size\"> {\n /** Controlled value. */\n value?: string | undefined;\n /** Uncontrolled initial value. */\n defaultValue?: string | undefined;\n /** Called on every keystroke, in both controlled and uncontrolled mode. */\n onValueChange?: ((value: string) => void) | undefined;\n /**\n * Start with the password visible. Almost always `false`; it exists for the\n * one-time-secret case, and for tests.\n */\n visible?: boolean | undefined;\n /** Uncontrolled initial visibility. */\n defaultVisible?: boolean | undefined;\n /** Called whenever the visibility flips. */\n onVisibleChange?: ((visible: boolean) => void) | undefined;\n /**\n * Defaults to `current-password`, the only value a browser password manager\n * will offer to fill. Override with `new-password` on a registration form —\n * leaving it as `current-password` there is why managers offer the *old*\n * password for a *new* one.\n */\n autoComplete?: string | undefined;\n /** Name for the toggle while the password is hidden. */\n showLabel?: string | undefined;\n /** Name for the toggle while the password is visible. */\n hideLabel?: string | undefined;\n /** The control height. */\n size?: InputProps[\"size\"];\n className?: string | undefined;\n}\n\n/** No German string for these exists in `COPY`, so they are typed here. */\nconst DEFAULT_SHOW_LABEL = \"Passwort anzeigen\";\nconst DEFAULT_HIDE_LABEL = \"Passwort verbergen\";\n\n/**\n * @example\n * ```tsx\n * <Field>\n * <FieldLabel>Passwort</FieldLabel>\n * <PasswordInput autoComplete=\"new-password\" />\n * </Field>\n * ```\n */\nexport const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(\n function PasswordInput(\n {\n value,\n defaultValue = \"\",\n onValueChange,\n visible,\n defaultVisible = false,\n onVisibleChange,\n autoComplete = \"current-password\",\n showLabel = DEFAULT_SHOW_LABEL,\n hideLabel = DEFAULT_HIDE_LABEL,\n size,\n className,\n disabled,\n readOnly,\n ...props\n },\n ref,\n ) {\n const [password, setPassword] = useControllableState({\n value,\n defaultValue,\n onChange: onValueChange,\n name: \"PasswordInput\",\n });\n const [isVisible, setIsVisible] = useControllableState({\n value: visible,\n defaultValue: defaultVisible,\n onChange: onVisibleChange,\n name: \"PasswordInput.visible\",\n });\n\n const inputRef = React.useRef<HTMLInputElement | null>(null);\n\n return (\n <InputGroup className={cn(className)} {...dataSlot(\"password-input\")}>\n <Input\n ref={withPrivateRef(ref, undefined, inputRef)}\n type={isVisible ? \"text\" : \"password\"}\n size={size}\n autoComplete={autoComplete}\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n disabled={disabled}\n readOnly={readOnly}\n {...props}\n />\n <InputGroupEnd>\n <IconButton\n label={isVisible ? hideLabel : showLabel}\n aria-pressed={isVisible}\n size=\"sm\"\n disabled={disabled}\n onClick={() => setIsVisible(!isVisible)}\n >\n {isVisible ? <EyeOff /> : <Eye />}\n </IconButton>\n </InputGroupEnd>\n </InputGroup>\n );\n },\n);\n"],"mappings":"AA6GM,SACE,KADF;AA7GL,YAAY,WAAW;AACxB,SAAS,KAAK,cAAc;AAC5B,SAAS,UAAU;AAEnB,SAAS,UAAU,sBAAsB,sBAAsB;AAC/D,SAAS,kBAAkB;AAC3B,SAAS,aAA8B;AACvC,SAAS,YAAY,qBAAqB;AAsD1C,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAWpB,MAAM,gBAAgB,MAAM;AAAA,EACjC,SAASA,eACP;AAAA,IACE;AAAA,IACA,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,IACjB;AAAA,IACA,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,GACA,KACA;AACA,UAAM,CAAC,UAAU,WAAW,IAAI,qBAAqB;AAAA,MACnD;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,MAAM;AAAA,IACR,CAAC;AACD,UAAM,CAAC,WAAW,YAAY,IAAI,qBAAqB;AAAA,MACrD,OAAO;AAAA,MACP,cAAc;AAAA,MACd,UAAU;AAAA,MACV,MAAM;AAAA,IACR,CAAC;AAED,UAAM,WAAW,MAAM,OAAgC,IAAI;AAE3D,WACE,qBAAC,cAAW,WAAW,GAAG,SAAS,GAAI,GAAG,SAAS,gBAAgB,GACjE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK,eAAe,KAAK,QAAW,QAAQ;AAAA,UAC5C,MAAM,YAAY,SAAS;AAAA,UAC3B;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP,UAAU,CAAC,UAAU,YAAY,MAAM,OAAO,KAAK;AAAA,UACnD;AAAA,UACA;AAAA,UACC,GAAG;AAAA;AAAA,MACN;AAAA,MACA,oBAAC,iBACC;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,YAAY,YAAY;AAAA,UAC/B,gBAAc;AAAA,UACd,MAAK;AAAA,UACL;AAAA,UACA,SAAS,MAAM,aAAa,CAAC,SAAS;AAAA,UAErC,sBAAY,oBAAC,UAAO,IAAK,oBAAC,OAAI;AAAA;AAAA,MACjC,GACF;AAAA,OACF;AAAA,EAEJ;AACF;","names":["PasswordInput"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { RadioGroup as RadioGroup$1 } from 'radix-ui';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TEA UI — RadioGroup and Radio.
|
|
6
|
+
*
|
|
7
|
+
* One choice out of a small set. The pair is a family for a reason: the
|
|
8
|
+
* grouping *is* the semantics. `role="radiogroup"` is what tells a screen reader
|
|
9
|
+
* that the arrow keys move between the options, and that leaving the group
|
|
10
|
+
* leaves the choice. A column of loose radios has neither.
|
|
11
|
+
*
|
|
12
|
+
* Two details that are easy to lose:
|
|
13
|
+
*
|
|
14
|
+
* - **Arrow-key navigation is not optional and is not ours.** Radix implements
|
|
15
|
+
* it, and it is the only correct implementation: arrows move *and select*,
|
|
16
|
+
* Tab enters and leaves the group as a single stop, and the roving tabindex
|
|
17
|
+
* means the whole group is one tab stop rather than four. A hand-rolled
|
|
18
|
+
* version gets one of those three wrong every time.
|
|
19
|
+
* - **A radio with no group is a radio group of one.** The `Radio` reads the
|
|
20
|
+
* group context and wires itself, so it cannot end up outside one.
|
|
21
|
+
*/
|
|
22
|
+
interface RadioGroupProps extends Omit<React.ComponentProps<typeof RadioGroup$1.Root>, "value" | "defaultValue" | "onValueChange" | "orientation" | "dir"> {
|
|
23
|
+
/** Controlled value — the `value` of the selected `Radio`. */
|
|
24
|
+
value?: string | undefined;
|
|
25
|
+
/** Uncontrolled initial value. */
|
|
26
|
+
defaultValue?: string | undefined;
|
|
27
|
+
/** Called when the selection changes, in both controlled and uncontrolled mode. */
|
|
28
|
+
onValueChange?: ((value: string) => void) | undefined;
|
|
29
|
+
/** Layout direction. Decides the arrow keys and the row/column flow. */
|
|
30
|
+
orientation?: "horizontal" | "vertical";
|
|
31
|
+
/**
|
|
32
|
+
* Name for the group. Give it one whenever the group is not already named by
|
|
33
|
+
* a `FieldGroup` legend: a `role="radiogroup"` with no name is an entry in the
|
|
34
|
+
* structure list that says nothing. A `Field` + `FieldLabel` pair is *not* a
|
|
35
|
+
* substitute here — a label cannot be associated with a `role="radiogroup"`
|
|
36
|
+
* container, only with each option.
|
|
37
|
+
*/
|
|
38
|
+
label?: string | undefined;
|
|
39
|
+
className?: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <FieldGroup legend="Zeitraum">
|
|
45
|
+
* <RadioGroup defaultValue="week">
|
|
46
|
+
* <Radio value="day">Tag</Radio>
|
|
47
|
+
* <Radio value="week">Woche</Radio>
|
|
48
|
+
* </RadioGroup>
|
|
49
|
+
* </FieldGroup>
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare const RadioGroup: React.ForwardRefExoticComponent<Omit<RadioGroupProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
53
|
+
interface RadioProps extends Omit<React.ComponentProps<typeof RadioGroup$1.Item>, "children"> {
|
|
54
|
+
/** Text beside the control. Makes the option self-naming. */
|
|
55
|
+
label?: React.ReactNode | undefined;
|
|
56
|
+
children?: React.ReactNode;
|
|
57
|
+
className?: string | undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One option. Takes `label` (or children) and wires itself to the surrounding
|
|
61
|
+
* `RadioGroup` through Radix's own context, so `name` and selection cannot
|
|
62
|
+
* disagree.
|
|
63
|
+
*/
|
|
64
|
+
declare const Radio: React.ForwardRefExoticComponent<Omit<RadioProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
65
|
+
|
|
66
|
+
export { Radio, RadioGroup, type RadioGroupProps, type RadioProps };
|