mlola-ui 1.0.7 → 1.0.8
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/package.json +1 -1
- package/registry/agents.md +4 -0
- package/registry/assets.json +627 -3
- package/registry/contract.json +29 -2
- package/registry/examples.json +268 -8
- package/registry/index.json +288 -47
- package/registry/source/packages/components/_internal/clipboard.ts +31 -0
- package/registry/source/packages/components/activity-heatmap/activity-heatmap.tsx +2 -2
- package/registry/source/packages/components/button/button.tsx +2 -1
- package/registry/source/packages/components/copy-button/copy-button.tsx +59 -0
- package/registry/source/packages/components/password-input/password-input.tsx +118 -0
- package/registry/source/packages/components/spinner/spinner.tsx +36 -0
- package/registry/source/packages/components/toast/toast.tsx +2 -1
- package/registry/source-manifest.json +7 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { IconCheck, IconCopy } from "@mlola-ui/icons";
|
|
5
|
+
import { Button, type ButtonProps } from "../button/button";
|
|
6
|
+
import { useCopy } from "../_internal/clipboard";
|
|
7
|
+
import { cx } from "../_internal/react";
|
|
8
|
+
|
|
9
|
+
export interface CopyButtonProps extends Omit<ButtonProps, "value" | "onClick" | "loading"> {
|
|
10
|
+
/** The text to copy, or a function that returns it at the moment of copying. */
|
|
11
|
+
value: string | (() => string);
|
|
12
|
+
/** The action, spoken on an icon-only button and shown as its tooltip: "Copy link". */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Announced, and shown on a text button, once the copy succeeds. */
|
|
15
|
+
copiedLabel?: string;
|
|
16
|
+
/** Announced when the browser refuses (no permission, an insecure page). */
|
|
17
|
+
errorLabel?: string;
|
|
18
|
+
/** After each attempt: true when the text reached the clipboard. */
|
|
19
|
+
onCopied?: (copied: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Copies a value and confirms it: the icon turns to a check for a moment and
|
|
24
|
+
* a screen reader hears "Copied". Without children it is an icon button;
|
|
25
|
+
* with children it keeps its text and shows the confirmation in place.
|
|
26
|
+
*/
|
|
27
|
+
const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
|
|
28
|
+
({ value, label = "Copy", copiedLabel = "Copied", errorLabel = "Could not copy; select the text and copy it by hand", onCopied, variant = "secondary", size, className, children, ...props }, ref) => {
|
|
29
|
+
const { state, copy } = useCopy();
|
|
30
|
+
const iconOnly = children === undefined || children === null;
|
|
31
|
+
const icon = state === "success" ? <IconCheck aria-hidden="true" size="1em" animate /> : <IconCopy aria-hidden="true" size="1em" />;
|
|
32
|
+
return (
|
|
33
|
+
<Button
|
|
34
|
+
ref={ref}
|
|
35
|
+
variant={variant}
|
|
36
|
+
size={size ?? (iconOnly ? "icon" : "md")}
|
|
37
|
+
aria-label={iconOnly ? label : undefined}
|
|
38
|
+
title={iconOnly ? label : undefined}
|
|
39
|
+
className={cx("ml-copy-button", className)}
|
|
40
|
+
data-status={state === "idle" ? undefined : state}
|
|
41
|
+
onClick={async () => {
|
|
42
|
+
const copied = await copy(typeof value === "function" ? value() : value);
|
|
43
|
+
onCopied?.(copied);
|
|
44
|
+
}}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{icon}
|
|
48
|
+
{iconOnly ? null : state === "success" ? copiedLabel : children}
|
|
49
|
+
{/* The outcome is spoken where a changing label would not be. */}
|
|
50
|
+
<span className="ml-visually-hidden" role="status">
|
|
51
|
+
{state === "success" ? copiedLabel : state === "error" ? errorLabel : ""}
|
|
52
|
+
</span>
|
|
53
|
+
</Button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
CopyButton.displayName = "CopyButton";
|
|
58
|
+
|
|
59
|
+
export { CopyButton };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { passwordStrength } from "@mlola-ui/behavior/logic";
|
|
5
|
+
import { IconEye, IconEyeOff } from "@mlola-ui/icons";
|
|
6
|
+
import { Input, type InputProps } from "../input/input";
|
|
7
|
+
|
|
8
|
+
export type PasswordStrength = ReturnType<typeof passwordStrength>;
|
|
9
|
+
|
|
10
|
+
export interface PasswordInputProps extends Omit<InputProps, "type" | "trailing"> {
|
|
11
|
+
/**
|
|
12
|
+
* Show how strong a new password is while it is typed. For a sign-in
|
|
13
|
+
* field, leave it off: the person is recalling a password, not choosing one.
|
|
14
|
+
*/
|
|
15
|
+
strength?: boolean;
|
|
16
|
+
/** The length below which a password is weak. */
|
|
17
|
+
minLength?: number;
|
|
18
|
+
/** The length at which a password is strong. */
|
|
19
|
+
strongLength?: number;
|
|
20
|
+
/** What the meter says for each level: guidance first, then a verdict with the next step. */
|
|
21
|
+
strengthHints?: Partial<Record<PasswordStrength, React.ReactNode>>;
|
|
22
|
+
/** The toggle's accessible name. Its pressed state says whether the password shows. */
|
|
23
|
+
toggleLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const HINTS = (min: number): Record<PasswordStrength, React.ReactNode> => ({
|
|
27
|
+
empty: `Use ${min} or more characters.`,
|
|
28
|
+
weak: (
|
|
29
|
+
<>
|
|
30
|
+
<strong>Too short.</strong> Use {min} or more characters.
|
|
31
|
+
</>
|
|
32
|
+
),
|
|
33
|
+
medium: (
|
|
34
|
+
<>
|
|
35
|
+
<strong>Good.</strong> Longer, or mixed symbols, is stronger.
|
|
36
|
+
</>
|
|
37
|
+
),
|
|
38
|
+
strong: <strong>Strong password.</strong>,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A password field with a show and hide toggle, and, for a new password, a
|
|
43
|
+
* meter that says how strong it is and what would make it stronger. The
|
|
44
|
+
* strength is the same shared decision the framework-free runtime uses.
|
|
45
|
+
*/
|
|
46
|
+
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
47
|
+
(
|
|
48
|
+
{
|
|
49
|
+
strength: showStrength = false,
|
|
50
|
+
minLength = 8,
|
|
51
|
+
strongLength = 12,
|
|
52
|
+
strengthHints,
|
|
53
|
+
toggleLabel = "Show password",
|
|
54
|
+
value,
|
|
55
|
+
defaultValue,
|
|
56
|
+
onChange,
|
|
57
|
+
autoComplete,
|
|
58
|
+
"aria-describedby": describedBy,
|
|
59
|
+
...props
|
|
60
|
+
},
|
|
61
|
+
ref,
|
|
62
|
+
) => {
|
|
63
|
+
const [visible, setVisible] = React.useState(false);
|
|
64
|
+
const [typed, setTyped] = React.useState(String(defaultValue ?? ""));
|
|
65
|
+
const current = value === undefined ? typed : String(value);
|
|
66
|
+
const level = passwordStrength(current, { min: minLength, strong: strongLength });
|
|
67
|
+
const meterId = React.useId();
|
|
68
|
+
const hints = { ...HINTS(minLength), ...strengthHints };
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="ml-password-input">
|
|
72
|
+
<Input
|
|
73
|
+
ref={ref}
|
|
74
|
+
{...props}
|
|
75
|
+
type={visible ? "text" : "password"}
|
|
76
|
+
value={value}
|
|
77
|
+
defaultValue={defaultValue}
|
|
78
|
+
autoComplete={autoComplete ?? (showStrength ? "new-password" : "current-password")}
|
|
79
|
+
// A password is not a word: no autocorrect, no capital letter at the start.
|
|
80
|
+
autoCapitalize="none"
|
|
81
|
+
autoCorrect="off"
|
|
82
|
+
spellCheck={false}
|
|
83
|
+
aria-describedby={[describedBy, showStrength ? meterId : null].filter(Boolean).join(" ") || undefined}
|
|
84
|
+
onChange={(event) => {
|
|
85
|
+
if (value === undefined) setTyped(event.target.value);
|
|
86
|
+
onChange?.(event);
|
|
87
|
+
}}
|
|
88
|
+
trailing={
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
className="ml-password-input-toggle"
|
|
92
|
+
aria-label={toggleLabel}
|
|
93
|
+
aria-pressed={visible}
|
|
94
|
+
data-hit="expand"
|
|
95
|
+
disabled={props.disabled}
|
|
96
|
+
onClick={() => setVisible((shown) => !shown)}
|
|
97
|
+
>
|
|
98
|
+
{visible ? <IconEyeOff aria-hidden="true" size="1em" /> : <IconEye aria-hidden="true" size="1em" />}
|
|
99
|
+
</button>
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
{showStrength ? (
|
|
103
|
+
<div id={meterId} className="ml-password-input-strength" data-strength={level} aria-live="polite">
|
|
104
|
+
<span className="ml-password-input-meter" aria-hidden="true">
|
|
105
|
+
<span />
|
|
106
|
+
<span />
|
|
107
|
+
<span />
|
|
108
|
+
</span>
|
|
109
|
+
<p>{hints[level]}</p>
|
|
110
|
+
</div>
|
|
111
|
+
) : null}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
PasswordInput.displayName = "PasswordInput";
|
|
117
|
+
|
|
118
|
+
export { PasswordInput };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cx } from "../_internal/react";
|
|
5
|
+
|
|
6
|
+
export type SpinnerSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
export interface SpinnerProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
|
|
9
|
+
size?: SpinnerSize;
|
|
10
|
+
/**
|
|
11
|
+
* What is loading, spoken to screen readers ("Loading results"). Leave it
|
|
12
|
+
* out when visible text beside the spinner already says so, as in a busy
|
|
13
|
+
* button; the spinner is then decorative.
|
|
14
|
+
*/
|
|
15
|
+
label?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Work in progress whose end is unknown. It takes the color of the text
|
|
20
|
+
* around it, so it reads on any surface and inside any control.
|
|
21
|
+
*/
|
|
22
|
+
const Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(({ size = "md", label, className, ...props }, ref) => (
|
|
23
|
+
<span
|
|
24
|
+
ref={ref}
|
|
25
|
+
role={label ? "status" : undefined}
|
|
26
|
+
aria-hidden={label ? undefined : true}
|
|
27
|
+
data-size={size}
|
|
28
|
+
className={cx("ml-spinner", className)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{label ? <span className="ml-visually-hidden">{label}</span> : null}
|
|
32
|
+
</span>
|
|
33
|
+
));
|
|
34
|
+
Spinner.displayName = "Spinner";
|
|
35
|
+
|
|
36
|
+
export { Spinner };
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { IconCircleCheck, IconCircleX, IconInfo, IconTriangleAlert, IconX } from "@mlola-ui/icons";
|
|
5
5
|
import { cx } from "../_internal/react";
|
|
6
|
+
import { Spinner } from "../spinner/spinner";
|
|
6
7
|
|
|
7
8
|
/** The meaning of the message, from the theme's color roles. */
|
|
8
9
|
export type ToastTone = "neutral" | "info" | "success" | "warning" | "danger";
|
|
@@ -193,7 +194,7 @@ function ToastCard({ item, paused }: { item: ToastItem; paused: boolean }) {
|
|
|
193
194
|
}, [paused, item.closing, item.duration, item.id, item.revision]);
|
|
194
195
|
|
|
195
196
|
const urgent = item.tone === "danger" || item.tone === "warning";
|
|
196
|
-
const icon = item.loading ? <
|
|
197
|
+
const icon = item.loading ? <Spinner /> : item.icon === undefined ? TONE_ICONS[item.tone] : item.icon;
|
|
197
198
|
|
|
198
199
|
return (
|
|
199
200
|
<div className="ml-toast-slot" data-state={item.closing ? "closed" : "open"}>
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packages/components/_internal/anchor.ts": "sha256-OPm/4Jf4UUo8IG+/4TAKpGDHtYpvxvGwyq3/RSZQIGc=",
|
|
3
|
+
"packages/components/_internal/clipboard.ts": "sha256-0y5p88CTXWuOrUYlC5J4XP1VCrOwkx9jYWVHmD2LouQ=",
|
|
3
4
|
"packages/components/_internal/dialog.ts": "sha256-796y3oNOVWarLvICTdi+d0/2axqSrzLVJiyBeh9X7bU=",
|
|
4
5
|
"packages/components/_internal/floating.ts": "sha256-+WxgC4CtAlSr2qA1EkoDTgto535XLIPsvtOfdOK6CHk=",
|
|
5
6
|
"packages/components/_internal/match.ts": "sha256-nKBpKWeow5ubAHUhOW8yOjlrKs0lqYdjbaYYfE8VoII=",
|
|
6
7
|
"packages/components/_internal/react.ts": "sha256-SH63/haiB4Q93uFjMIMe5xZbaxENu3VI+GcidV8HELM=",
|
|
7
8
|
"packages/components/_internal/scroll.ts": "sha256-/YxhPPJa6Iax4q5eEs8dmpQp634TbvB+421MphEyscQ=",
|
|
8
9
|
"packages/components/accordion/accordion.tsx": "sha256-0ALaLJd1fvj8Vbx86HLBKm3qUItrtwbrGJTykVfNSMg=",
|
|
9
|
-
"packages/components/activity-heatmap/activity-heatmap.tsx": "sha256-
|
|
10
|
+
"packages/components/activity-heatmap/activity-heatmap.tsx": "sha256-IAcU8FLAJbgKAS+2X+mt6PMqJBzxr4QVdwsVAl67BH4=",
|
|
10
11
|
"packages/components/activity-heatmap/calendar.ts": "sha256-NB+AjXY5Xc+KzR8MMTRBgZxdzOeoHhkW0KQOW7WJPuQ=",
|
|
11
12
|
"packages/components/alert/alert.tsx": "sha256-j680akXvCIh4MTVnUo79acvVrv51XyQ3UBJ5FBAy8Dw=",
|
|
12
13
|
"packages/components/app-shell/app-shell.tsx": "sha256-XAq82XOTH3ZR+hxTkYFu8P7Ih2aI6ERy/TpXc2VMUwM=",
|
|
13
14
|
"packages/components/avatar/avatar.tsx": "sha256-EjqC8ytKqRtsTVdK6vncaDfhO/i7ICe+jwaYCg5bjIw=",
|
|
14
15
|
"packages/components/badge/badge.tsx": "sha256-flEH8S2jZ/64n3Y/ROhC9+9rvF7wXaN0xLcMMwqNOVQ=",
|
|
15
16
|
"packages/components/breadcrumb/breadcrumb.tsx": "sha256-Mt9mvmwwDgdp5/+mj5byIrh8+hicsp7R6UcvUlK0yog=",
|
|
16
|
-
"packages/components/button/button.tsx": "sha256-
|
|
17
|
+
"packages/components/button/button.tsx": "sha256-sXVANbGeFSYPHpiWuPvHHqMoPjG0zl0HSsKEdTgl5ME=",
|
|
17
18
|
"packages/components/calendar/calendar.tsx": "sha256-rWiS4YAueZfVoo6j5Fi3uN7A+yApe5U6HMm1woTVjys=",
|
|
18
19
|
"packages/components/calendar/dates.ts": "sha256-mygYkoC70av2ZQ0vkxf885trwW97Wd10I4dptYQe+4A=",
|
|
19
20
|
"packages/components/card/card.tsx": "sha256-aMKbv5DWYYWq7GZAFR9O1fuYE0H61WCjkvEbRMOd894=",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"packages/components/combobox/combobox.tsx": "sha256-CvuYc/hJoS7yw6QM9vv1SIkwflx0Kwd95UjchtC+AOs=",
|
|
25
26
|
"packages/components/command-menu/command-menu.tsx": "sha256-mF1z92yrPY609pjx5Dt1l5EhGHl9EWV8F8Sq0hyJRw4=",
|
|
26
27
|
"packages/components/context-menu/context-menu.tsx": "sha256-YgYsIzvLGFcJG1XX5zFJwrid5Z1Kn0lyWVzmLKmbMVU=",
|
|
28
|
+
"packages/components/copy-button/copy-button.tsx": "sha256-6fvfifoiDOtW+WxKWon4SGEn3THtERuCX2hqGSZDtVM=",
|
|
27
29
|
"packages/components/date-picker/date-picker.tsx": "sha256-rsmDweD3RVfKTXR2i+/pnTb/MsrDAZXmHAYBvcUGxig=",
|
|
28
30
|
"packages/components/dropdown-menu/dropdown-menu.tsx": "sha256-xgybCu9HBxIieafu5CjAtY4aCOQoaAj/VUCH6D6vLP8=",
|
|
29
31
|
"packages/components/dropzone/dropzone.tsx": "sha256-tHCCpEWE6839UeLOszTo+R2vAL4Sp9UxuRdLkFLfnq4=",
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"packages/components/number-input/number.ts": "sha256-IrHCQrEhI5ein9ifMFuSViSoxQUBoT3tNmmCu/paGGM=",
|
|
37
39
|
"packages/components/otp-input/otp-input.tsx": "sha256-QURn8g5HqCQXBTdeENPbVxoPlqs3ZtoAPLRlFjQDxmI=",
|
|
38
40
|
"packages/components/pagination/pagination.tsx": "sha256-FBwk8tq4JcsK2D0dlWEYiDSRx3owgBmGzEIlVRaNBoE=",
|
|
41
|
+
"packages/components/password-input/password-input.tsx": "sha256-oY//vTILhUicJyfAWZrSUTl8WWsf4P2S1Bh0x4OxwLs=",
|
|
39
42
|
"packages/components/popover/popover.tsx": "sha256-/kTiAuFklU4IuP97XaiCUGdEpKtNyMPLPS359mfzPHw=",
|
|
40
43
|
"packages/components/priority-icon/labels.ts": "sha256-zhmJIb7/gS16wU+xCJ7hkReBi9q5RI3SMwdjkJvvvKU=",
|
|
41
44
|
"packages/components/priority-icon/priority-icon.tsx": "sha256-3D6hFRpht3YuWouLhakE2DFdldavmJx0B/povEDwrtQ=",
|
|
@@ -48,6 +51,7 @@
|
|
|
48
51
|
"packages/components/sheet/sheet.tsx": "sha256-EIn3OeRDmXGHbYf/uihRh8aw3LmzarIFiyC9DUsL1jg=",
|
|
49
52
|
"packages/components/skeleton/skeleton.tsx": "sha256-2Fk72NbLr3+CrXH7jGbHNsc3t+vskMqSyySe/UwbGCs=",
|
|
50
53
|
"packages/components/slider/slider.tsx": "sha256-l+VrBqPYHIIcemiVixxwDxNDB0HEMmYRZekx/H5HroQ=",
|
|
54
|
+
"packages/components/spinner/spinner.tsx": "sha256-FZQmcGkQ8N0485n9bYsLTpTnolbM729YKjkQrXriz9A=",
|
|
51
55
|
"packages/components/status-icon/labels.ts": "sha256-EE8q+geQBbJUs7I3RmaHZnPwn7Vjk2GjCfxvI4ruXKM=",
|
|
52
56
|
"packages/components/status-icon/status-icon.tsx": "sha256-TtrTeoePyfH1VhKuWC0v12FuwfgND1CHnH5x8fyf4AM=",
|
|
53
57
|
"packages/components/stepper/stepper.tsx": "sha256-uI/dRmkbijlixHiGUceWcpaDzpEixrzQ1UTmoPbGZAY=",
|
|
@@ -58,7 +62,7 @@
|
|
|
58
62
|
"packages/components/time-picker/time-picker.tsx": "sha256-og8Cbf+e8oFYewfagUgjRzLHSIIlAZh+1OpAn9yNq70=",
|
|
59
63
|
"packages/components/time-picker/times.ts": "sha256-V/4uqFZPn9gVnyx8cB9vwp/RocmTYaPmLxjfy8DPQOI=",
|
|
60
64
|
"packages/components/timeline/timeline.tsx": "sha256-nVg7lBcAUAzqEb0YrHagJXSaHE2aWWE9bUbQMWQY6PU=",
|
|
61
|
-
"packages/components/toast/toast.tsx": "sha256-
|
|
65
|
+
"packages/components/toast/toast.tsx": "sha256-f0DwN0XuV46fSsQf3i9RJcfQbAs2mSz7A8UJoIIICHg=",
|
|
62
66
|
"packages/components/toggle/toggle.tsx": "sha256-8qPHeSliHNdzQS7OLJ47BrvRR75MYyUGJlK4s21XS/U=",
|
|
63
67
|
"packages/components/tooltip/tooltip.tsx": "sha256-NbaOUmlUbBlUIdovchrWwH2QqzuvXB97DjnJnrHf4Gc=",
|
|
64
68
|
"packages/components/tour/tour.tsx": "sha256-mrAE8aVDBVIibGvQah9Gh3mzIiWDFxB6aObzqFo5xXU=",
|