@lerianstudio/sindarian-ui 1.2.0-beta.10 → 1.2.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ui/badge/index.d.ts.map +1 -1
- package/dist/components/ui/badge/index.js +2 -2
- package/dist/components/ui/button/button.test.d.ts +2 -0
- package/dist/components/ui/button/button.test.d.ts.map +1 -0
- package/dist/components/ui/button/button.test.js +65 -0
- package/dist/components/ui/button/index.d.ts.map +1 -1
- package/dist/components/ui/button/index.js +1 -1
- package/dist/components/ui/copy-field/copy-field.test.d.ts +2 -0
- package/dist/components/ui/copy-field/copy-field.test.d.ts.map +1 -0
- package/dist/components/ui/copy-field/copy-field.test.js +268 -0
- package/dist/components/ui/copy-field/index.d.ts +56 -0
- package/dist/components/ui/copy-field/index.d.ts.map +1 -0
- package/dist/components/ui/copy-field/index.js +150 -0
- package/dist/components/ui/icon-button/styles.css +24 -24
- package/dist/components/ui/input/index.d.ts.map +1 -1
- package/dist/components/ui/input/index.js +4 -3
- package/dist/components/ui/input/input.test.d.ts +2 -0
- package/dist/components/ui/input/input.test.d.ts.map +1 -0
- package/dist/components/ui/input/input.test.js +52 -0
- package/dist/components/ui/qr-code/index.d.ts +26 -0
- package/dist/components/ui/qr-code/index.d.ts.map +1 -0
- package/dist/components/ui/qr-code/index.js +27 -0
- package/dist/components/ui/qr-code/qr-code.test.d.ts +2 -0
- package/dist/components/ui/qr-code/qr-code.test.d.ts.map +1 -0
- package/dist/components/ui/qr-code/qr-code.test.js +60 -0
- package/dist/components/ui/sidebar/sidebar-item-icon-button.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-item-icon-button.d.ts.map +1 -1
- package/dist/components/ui/sidebar/sidebar-item-icon-button.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/badge/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAKjE,QAAA,MAAM,aAAa;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/badge/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAKjE,QAAA,MAAM,aAAa;;8EA+BlB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,GACnD,YAAY,CAAC,OAAO,aAAa,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAE5D,iBAAS,KAAK,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,EAAE,UAAU,qBAUnE;AAED,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAA"}
|
|
@@ -10,8 +10,8 @@ const badgeVariants = (0, class_variance_authority_1.cva)('inline-flex items-cen
|
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
12
12
|
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
|
13
|
-
active: 'bg-system-success-surface text-system-success-h1a border-
|
|
14
|
-
inactive: 'bg-muted text-foreground border-
|
|
13
|
+
active: 'bg-system-success-surface text-system-success-h1a border-system-success-border py-[2px] px-3',
|
|
14
|
+
inactive: 'bg-muted text-foreground border-muted-foreground/40 py-[2px] px-3',
|
|
15
15
|
secondary: 'border-transparent bg-muted-foreground text-white dark:text-black',
|
|
16
16
|
destructive: 'border-transparent bg-red-500 text-primary-foreground hover:bg-red-500/80',
|
|
17
17
|
error: 'border-system-error-border bg-system-error-surface text-system-error-text px-[10px] py-1',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button.test.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/button/button.test.tsx"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAA"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
require("@testing-library/jest-dom");
|
|
5
|
+
const react_1 = require("@testing-library/react");
|
|
6
|
+
const _1 = require(".");
|
|
7
|
+
/**
|
|
8
|
+
* Regression coverage for `asChild`.
|
|
9
|
+
*
|
|
10
|
+
* Button always renders three children (the two icon slots evaluate to `false`
|
|
11
|
+
* when no `icon` is given). Without a `Slottable` marking the real child, Radix
|
|
12
|
+
* Slot >=1.3.0 cannot tell which of the three to merge onto and throws
|
|
13
|
+
* "Slot failed to slot onto its children" — which broke `<Button asChild>` for
|
|
14
|
+
* every consumer, icon or not.
|
|
15
|
+
*/
|
|
16
|
+
describe('Button asChild', () => {
|
|
17
|
+
it('renders the child element instead of a button, with no icon', () => {
|
|
18
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Button, { asChild: true, variant: "link", children: (0, jsx_runtime_1.jsx)("a", { href: "/somewhere", children: "Go somewhere" }) }));
|
|
19
|
+
const link = react_1.screen.getByRole('link', { name: 'Go somewhere' });
|
|
20
|
+
expect(link).toBeInTheDocument();
|
|
21
|
+
expect(link).toHaveAttribute('href', '/somewhere');
|
|
22
|
+
// asChild must NOT leave a wrapping <button> around the anchor.
|
|
23
|
+
expect(react_1.screen.queryByRole('button')).not.toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
it('merges the button classes onto the child element', () => {
|
|
26
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Button, { asChild: true, variant: "link", size: "small", className: "custom-class", children: (0, jsx_runtime_1.jsx)("a", { href: "/x", children: "Styled" }) }));
|
|
27
|
+
const link = react_1.screen.getByRole('link', { name: 'Styled' });
|
|
28
|
+
expect(link).toHaveClass('custom-class');
|
|
29
|
+
expect(link).toHaveAttribute('data-slot', 'button');
|
|
30
|
+
});
|
|
31
|
+
it('keeps a start icon alongside the slotted child', () => {
|
|
32
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Button, { asChild: true, icon: (0, jsx_runtime_1.jsx)("span", { "data-testid": "icon", children: "*" }), children: (0, jsx_runtime_1.jsx)("a", { href: "/x", children: "With icon" }) }));
|
|
33
|
+
const link = react_1.screen.getByRole('link', { name: /With icon/ });
|
|
34
|
+
expect(link).toBeInTheDocument();
|
|
35
|
+
// The icon renders inside the slotted element, not as a sibling of it.
|
|
36
|
+
expect(link).toContainElement(react_1.screen.getByTestId('icon'));
|
|
37
|
+
});
|
|
38
|
+
it('keeps an end icon alongside the slotted child', () => {
|
|
39
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Button, { asChild: true, icon: (0, jsx_runtime_1.jsx)("span", { "data-testid": "icon", children: "*" }), iconPlacement: "end", children: (0, jsx_runtime_1.jsx)("a", { href: "/x", children: "Trailing" }) }));
|
|
40
|
+
expect(react_1.screen.getByRole('link', { name: /Trailing/ })).toContainElement(react_1.screen.getByTestId('icon'));
|
|
41
|
+
});
|
|
42
|
+
it('accepts a child whose own content is multiple nodes', () => {
|
|
43
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Button, { asChild: true, variant: "link", children: (0, jsx_runtime_1.jsxs)("a", { href: "/x", children: [(0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: "\u21A9" }), "Back to list"] }) }));
|
|
44
|
+
expect(react_1.screen.getByRole('link', { name: 'Back to list' })).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('Button without asChild', () => {
|
|
48
|
+
it('still renders a native button (the Slottable path must be transparent)', () => {
|
|
49
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Button, { variant: "primary", children: "Click me" }));
|
|
50
|
+
const button = react_1.screen.getByRole('button', { name: 'Click me' });
|
|
51
|
+
expect(button.tagName).toBe('BUTTON');
|
|
52
|
+
expect(button).toHaveAttribute('data-slot', 'button');
|
|
53
|
+
});
|
|
54
|
+
it('still renders its icon', () => {
|
|
55
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Button, { icon: (0, jsx_runtime_1.jsx)("span", { "data-testid": "icon", children: "*" }), children: "Has an icon" }));
|
|
56
|
+
expect(react_1.screen.getByTestId('icon')).toBeInTheDocument();
|
|
57
|
+
expect(react_1.screen.getByRole('button', { name: /Has an icon/ })).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
it('suppresses onClick when readOnly', () => {
|
|
60
|
+
const onClick = jest.fn();
|
|
61
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Button, { readOnly: true, onClick: onClick, children: "Read only" }));
|
|
62
|
+
react_1.screen.getByRole('button', { name: 'Read only' }).click();
|
|
63
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/button/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;;8EA0BlB,CAAA;AAEF,QAAA,MAAM,YAAY;;;8EAgBhB,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GACtD,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,GAAG;IACF,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,aAAa,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,CAAA;IAC3C,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAEH,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAe,EACf,IAAI,EACJ,aAAuB,EACvB,SAAiB,EACjB,QAAgB,EAChB,OAAO,EACP,GAAG,KAAK,EACT,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/button/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;;8EA0BlB,CAAA;AAEF,QAAA,MAAM,YAAY;;;8EAgBhB,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GACtD,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,GAAG;IACF,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,aAAa,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,CAAA;IAC3C,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAEH,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAe,EACf,IAAI,EACJ,aAAuB,EACvB,SAAiB,EACjB,QAAgB,EAChB,OAAO,EACP,GAAG,KAAK,EACT,EAAE,WAAW,qBAqCb;AAED,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -60,5 +60,5 @@ function Button({ className, variant, size, asChild = false, icon, iconPlacement
|
|
|
60
60
|
}
|
|
61
61
|
onClick?.(e);
|
|
62
62
|
};
|
|
63
|
-
return ((0, jsx_runtime_1.jsxs)(Comp, { className: (0, utils_1.cn)(buttonVariants({ variant, size, fullWidth }), className), "data-read-only": readOnly, "data-slot": "button", ...props, onClick: handleClick, children: [icon && iconPlacement === 'start' && ((0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)(iconVariants({ position: iconPlacement, size })), children: icon })), props.children, icon && iconPlacement !== 'start' && ((0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)(iconVariants({ position: iconPlacement, size })), children: icon }))] }));
|
|
63
|
+
return ((0, jsx_runtime_1.jsxs)(Comp, { className: (0, utils_1.cn)(buttonVariants({ variant, size, fullWidth }), className), "data-read-only": readOnly, "data-slot": "button", ...props, onClick: handleClick, children: [icon && iconPlacement === 'start' && ((0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)(iconVariants({ position: iconPlacement, size })), children: icon })), (0, jsx_runtime_1.jsx)(react_slot_1.Slottable, { children: props.children }), icon && iconPlacement !== 'start' && ((0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)(iconVariants({ position: iconPlacement, size })), children: icon }))] }));
|
|
64
64
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copy-field.test.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/copy-field/copy-field.test.tsx"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAA"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
require("@testing-library/jest-dom");
|
|
5
|
+
const react_1 = require("@testing-library/react");
|
|
6
|
+
const _1 = require(".");
|
|
7
|
+
const mockToast = jest.fn();
|
|
8
|
+
jest.mock('@/hooks/use-toast', () => ({
|
|
9
|
+
useToast: () => ({ toast: mockToast })
|
|
10
|
+
}));
|
|
11
|
+
const FALLBACK_COPY_LABEL = 'Copy not available — text selected, press Ctrl/Cmd+C';
|
|
12
|
+
const writeText = jest.fn().mockResolvedValue(undefined);
|
|
13
|
+
const readText = jest.fn().mockResolvedValue('');
|
|
14
|
+
const setClipboard = (value) => {
|
|
15
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
16
|
+
value,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockToast.mockClear();
|
|
23
|
+
writeText.mockClear();
|
|
24
|
+
writeText.mockResolvedValue(undefined);
|
|
25
|
+
readText.mockClear();
|
|
26
|
+
readText.mockResolvedValue('');
|
|
27
|
+
setClipboard({ writeText, readText });
|
|
28
|
+
});
|
|
29
|
+
describe('CopyField', () => {
|
|
30
|
+
it('renders the value in a read-only input', () => {
|
|
31
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret" }));
|
|
32
|
+
const input = react_1.screen.getByLabelText('Secret');
|
|
33
|
+
expect(input).toHaveValue('SECRET123');
|
|
34
|
+
expect(input).toHaveAttribute('readonly');
|
|
35
|
+
});
|
|
36
|
+
it('associates the visible label with the input via htmlFor/id', () => {
|
|
37
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "abc", label: "API Key" }));
|
|
38
|
+
expect(react_1.screen.getByLabelText('API Key')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
it('copies the value to the clipboard when the copy button is clicked', async () => {
|
|
41
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret" }));
|
|
42
|
+
await (0, react_1.act)(async () => {
|
|
43
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /copy secret/i }));
|
|
44
|
+
});
|
|
45
|
+
expect(writeText).toHaveBeenCalledWith('SECRET123');
|
|
46
|
+
});
|
|
47
|
+
it('fires a success toast with the provided onCopyLabel after copying', async () => {
|
|
48
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret", onCopyLabel: "Secret copied!" }));
|
|
49
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /copy secret/i }));
|
|
50
|
+
await (0, react_1.waitFor)(() => expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ variant: 'success', title: 'Secret copied!' })));
|
|
51
|
+
});
|
|
52
|
+
it('uses the default copy label when onCopyLabel is not provided', async () => {
|
|
53
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "abc" }));
|
|
54
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /copy/i }));
|
|
55
|
+
await (0, react_1.waitFor)(() => expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ title: 'Copied to clipboard!' })));
|
|
56
|
+
});
|
|
57
|
+
it('gives the copy button an accessible name even without a label', () => {
|
|
58
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "abc" }));
|
|
59
|
+
expect(react_1.screen.getByRole('button', { name: /copy/i })).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
it('overrides the input aria-label via valueLabel when no visible label', () => {
|
|
62
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "abc", valueLabel: "Valor para copiar" }));
|
|
63
|
+
expect(react_1.screen.getByLabelText('Valor para copiar')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
describe('masked', () => {
|
|
66
|
+
it('obscures the displayed value while keeping the real value copyable', async () => {
|
|
67
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "TOTPSECRET", label: "Secret", masked: true }));
|
|
68
|
+
const input = react_1.screen.getByLabelText('Secret');
|
|
69
|
+
expect(input).toHaveAttribute('type', 'password');
|
|
70
|
+
expect(input).toHaveValue('TOTPSECRET');
|
|
71
|
+
await (0, react_1.act)(async () => {
|
|
72
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /copy secret/i }));
|
|
73
|
+
});
|
|
74
|
+
expect(writeText).toHaveBeenCalledWith('TOTPSECRET');
|
|
75
|
+
});
|
|
76
|
+
it('reveals and then hides the value with the reveal toggle (aria-pressed)', () => {
|
|
77
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "s3cr3t", label: "Secret", masked: true }));
|
|
78
|
+
const input = react_1.screen.getByLabelText('Secret');
|
|
79
|
+
expect(input).toHaveAttribute('type', 'password');
|
|
80
|
+
const toggle = react_1.screen.getByRole('button', { name: /show value/i });
|
|
81
|
+
expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
|
82
|
+
react_1.fireEvent.click(toggle);
|
|
83
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
84
|
+
const hideToggle = react_1.screen.getByRole('button', { name: /hide value/i });
|
|
85
|
+
expect(hideToggle).toHaveAttribute('aria-pressed', 'true');
|
|
86
|
+
react_1.fireEvent.click(hideToggle);
|
|
87
|
+
expect(input).toHaveAttribute('type', 'password');
|
|
88
|
+
expect(react_1.screen.getByRole('button', { name: /show value/i })).toHaveAttribute('aria-pressed', 'false');
|
|
89
|
+
});
|
|
90
|
+
it('supports localized reveal/hide labels', () => {
|
|
91
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "s3cr3t", label: "Secret", masked: true, revealLabel: "Mostrar valor", hideLabel: "Ocultar valor" }));
|
|
92
|
+
const toggle = react_1.screen.getByRole('button', { name: 'Mostrar valor' });
|
|
93
|
+
react_1.fireEvent.click(toggle);
|
|
94
|
+
expect(react_1.screen.getByRole('button', { name: 'Ocultar valor' })).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
it('does not render a reveal toggle when not masked', () => {
|
|
97
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "abc", label: "Token" }));
|
|
98
|
+
expect(react_1.screen.queryByRole('button', { name: /show value/i })).not.toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('clipboard unavailable fallback', () => {
|
|
102
|
+
it('selects the input text and toasts the fallback label when clipboard API is absent', async () => {
|
|
103
|
+
setClipboard(undefined);
|
|
104
|
+
const selectSpy = jest.spyOn(HTMLInputElement.prototype, 'select');
|
|
105
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "abc", label: "Token" }));
|
|
106
|
+
expect(() => react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /copy token/i }))).not.toThrow();
|
|
107
|
+
expect(selectSpy).toHaveBeenCalled();
|
|
108
|
+
await (0, react_1.waitFor)(() => expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ title: FALLBACK_COPY_LABEL })));
|
|
109
|
+
selectSpy.mockRestore();
|
|
110
|
+
});
|
|
111
|
+
it('fires the fallback toast and NOT the success toast when writeText rejects', async () => {
|
|
112
|
+
writeText.mockRejectedValueOnce(new Error('permission denied'));
|
|
113
|
+
const selectSpy = jest.spyOn(HTMLInputElement.prototype, 'select');
|
|
114
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "abc", label: "Token" }));
|
|
115
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /copy token/i }));
|
|
116
|
+
await (0, react_1.waitFor)(() => expect(selectSpy).toHaveBeenCalled());
|
|
117
|
+
await (0, react_1.waitFor)(() => expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ title: FALLBACK_COPY_LABEL })));
|
|
118
|
+
expect(mockToast).not.toHaveBeenCalledWith(expect.objectContaining({ variant: 'success' }));
|
|
119
|
+
selectSpy.mockRestore();
|
|
120
|
+
});
|
|
121
|
+
it('makes a masked field textual (selectable) before the fallback selection when clipboard is absent', async () => {
|
|
122
|
+
setClipboard(undefined);
|
|
123
|
+
const selectSpy = jest.spyOn(HTMLInputElement.prototype, 'select');
|
|
124
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "TOTPSECRET", label: "Secret", masked: true }));
|
|
125
|
+
const input = react_1.screen.getByLabelText('Secret');
|
|
126
|
+
expect(input).toHaveAttribute('type', 'password');
|
|
127
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /copy secret/i }));
|
|
128
|
+
// The field must be text (selectable) at/after the fallback, and it must
|
|
129
|
+
// still hold the real secret so Ctrl/Cmd+C copies the true value.
|
|
130
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
131
|
+
expect(input).toHaveValue('TOTPSECRET');
|
|
132
|
+
expect(selectSpy).toHaveBeenCalled();
|
|
133
|
+
selectSpy.mockRestore();
|
|
134
|
+
});
|
|
135
|
+
it('makes a masked field textual before the fallback selection when writeText rejects', async () => {
|
|
136
|
+
writeText.mockRejectedValueOnce(new Error('permission denied'));
|
|
137
|
+
const selectSpy = jest.spyOn(HTMLInputElement.prototype, 'select');
|
|
138
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "TOTPSECRET", label: "Secret", masked: true }));
|
|
139
|
+
const input = react_1.screen.getByLabelText('Secret');
|
|
140
|
+
expect(input).toHaveAttribute('type', 'password');
|
|
141
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /copy secret/i }));
|
|
142
|
+
await (0, react_1.waitFor)(() => expect(selectSpy).toHaveBeenCalled());
|
|
143
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
144
|
+
expect(input).toHaveValue('TOTPSECRET');
|
|
145
|
+
selectSpy.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('clearClipboardAfter', () => {
|
|
149
|
+
const CLEAR_AFTER = 30_000;
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
jest.useFakeTimers();
|
|
152
|
+
});
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
jest.runOnlyPendingTimers();
|
|
155
|
+
jest.useRealTimers();
|
|
156
|
+
});
|
|
157
|
+
/** Runs the pending clear timer and drains its await chain. */
|
|
158
|
+
const runClearTimer = async () => {
|
|
159
|
+
await (0, react_1.act)(async () => {
|
|
160
|
+
jest.advanceTimersByTime(CLEAR_AFTER);
|
|
161
|
+
});
|
|
162
|
+
// readText -> comparison -> writeText: two extra microtask turns.
|
|
163
|
+
await (0, react_1.act)(async () => {
|
|
164
|
+
await Promise.resolve();
|
|
165
|
+
await Promise.resolve();
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const copy = async (name) => {
|
|
169
|
+
await (0, react_1.act)(async () => {
|
|
170
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name }));
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
it('wipes the clipboard after the delay when it still holds the copied value', async () => {
|
|
174
|
+
readText.mockResolvedValue('SECRET123');
|
|
175
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret", clearClipboardAfter: CLEAR_AFTER }));
|
|
176
|
+
await copy(/copy secret/i);
|
|
177
|
+
expect(writeText).toHaveBeenCalledWith('SECRET123');
|
|
178
|
+
// Nothing cleared before the delay elapses.
|
|
179
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
180
|
+
await runClearTimer();
|
|
181
|
+
expect(readText).toHaveBeenCalled();
|
|
182
|
+
expect(writeText).toHaveBeenLastCalledWith('');
|
|
183
|
+
});
|
|
184
|
+
it('leaves the clipboard untouched when it now holds an unrelated value', async () => {
|
|
185
|
+
readText.mockResolvedValue('something the user copied afterwards');
|
|
186
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret", clearClipboardAfter: CLEAR_AFTER }));
|
|
187
|
+
await copy(/copy secret/i);
|
|
188
|
+
await runClearTimer();
|
|
189
|
+
expect(readText).toHaveBeenCalled();
|
|
190
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
191
|
+
expect(writeText).toHaveBeenLastCalledWith('SECRET123');
|
|
192
|
+
});
|
|
193
|
+
it('clears against the value copied at click time, not a later prop value', async () => {
|
|
194
|
+
readText.mockResolvedValue('FIRST');
|
|
195
|
+
const { rerender } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "FIRST", label: "Secret", clearClipboardAfter: CLEAR_AFTER }));
|
|
196
|
+
await copy(/copy secret/i);
|
|
197
|
+
rerender((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECOND", label: "Secret", clearClipboardAfter: CLEAR_AFTER }));
|
|
198
|
+
await runClearTimer();
|
|
199
|
+
expect(writeText).toHaveBeenLastCalledWith('');
|
|
200
|
+
});
|
|
201
|
+
it('does not clear when the clipboard cannot be read back', async () => {
|
|
202
|
+
setClipboard({ writeText });
|
|
203
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret", clearClipboardAfter: CLEAR_AFTER }));
|
|
204
|
+
await copy(/copy secret/i);
|
|
205
|
+
await runClearTimer();
|
|
206
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
207
|
+
expect(writeText).toHaveBeenLastCalledWith('SECRET123');
|
|
208
|
+
});
|
|
209
|
+
it('does not clear when readText rejects', async () => {
|
|
210
|
+
readText.mockRejectedValue(new Error('permission denied'));
|
|
211
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret", clearClipboardAfter: CLEAR_AFTER }));
|
|
212
|
+
await copy(/copy secret/i);
|
|
213
|
+
await runClearTimer();
|
|
214
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
215
|
+
});
|
|
216
|
+
it('does not schedule a clear when the copy itself failed', async () => {
|
|
217
|
+
writeText.mockRejectedValue(new Error('permission denied'));
|
|
218
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret", clearClipboardAfter: CLEAR_AFTER }));
|
|
219
|
+
await copy(/copy secret/i);
|
|
220
|
+
await runClearTimer();
|
|
221
|
+
expect(readText).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
it('never clears when the prop is omitted or non-positive', async () => {
|
|
224
|
+
const { unmount } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret" }));
|
|
225
|
+
await copy(/copy secret/i);
|
|
226
|
+
await runClearTimer();
|
|
227
|
+
expect(readText).not.toHaveBeenCalled();
|
|
228
|
+
unmount();
|
|
229
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret", clearClipboardAfter: 0 }));
|
|
230
|
+
await copy(/copy secret/i);
|
|
231
|
+
await runClearTimer();
|
|
232
|
+
expect(readText).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
it('still clears after the field unmounts before the delay elapses', async () => {
|
|
235
|
+
readText.mockResolvedValue('SECRET123');
|
|
236
|
+
const { unmount } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "SECRET123", label: "Secret", clearClipboardAfter: CLEAR_AFTER }));
|
|
237
|
+
await copy(/copy secret/i);
|
|
238
|
+
unmount();
|
|
239
|
+
await runClearTimer();
|
|
240
|
+
expect(writeText).toHaveBeenLastCalledWith('');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('copied-indicator timer', () => {
|
|
244
|
+
beforeEach(() => {
|
|
245
|
+
jest.useFakeTimers();
|
|
246
|
+
});
|
|
247
|
+
afterEach(() => {
|
|
248
|
+
jest.runOnlyPendingTimers();
|
|
249
|
+
jest.useRealTimers();
|
|
250
|
+
});
|
|
251
|
+
it('does not throw on rapid double-copy or unmount before the timeout', async () => {
|
|
252
|
+
const { unmount } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.CopyField, { value: "abc", label: "Token" }));
|
|
253
|
+
const button = react_1.screen.getByRole('button', { name: /copy token/i });
|
|
254
|
+
await (0, react_1.act)(async () => {
|
|
255
|
+
react_1.fireEvent.click(button);
|
|
256
|
+
});
|
|
257
|
+
await (0, react_1.act)(async () => {
|
|
258
|
+
react_1.fireEvent.click(button);
|
|
259
|
+
});
|
|
260
|
+
// Unmount before the copied-icon timeout fires; the cleanup effect must
|
|
261
|
+
// clear the pending timer so no setState-after-unmount occurs.
|
|
262
|
+
expect(() => {
|
|
263
|
+
unmount();
|
|
264
|
+
jest.advanceTimersByTime(2000);
|
|
265
|
+
}).not.toThrow();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type CopyFieldProps = {
|
|
3
|
+
/**
|
|
4
|
+
* The string shown and copied verbatim (e.g. a TOTP manual-entry secret or a
|
|
5
|
+
* single recovery code). Copy always uses this raw value, even while masked.
|
|
6
|
+
*/
|
|
7
|
+
value: string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional visible label rendered above the field and associated with the
|
|
10
|
+
* input via `htmlFor`/`id`.
|
|
11
|
+
*/
|
|
12
|
+
label?: string;
|
|
13
|
+
/**
|
|
14
|
+
* When true the displayed value is obscured (native password dots) while the
|
|
15
|
+
* underlying `value` stays copyable. A reveal toggle is offered.
|
|
16
|
+
* @defaultValue false
|
|
17
|
+
*/
|
|
18
|
+
masked?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* i18n message used as the success toast text. Falls back to a sensible
|
|
21
|
+
* default when omitted.
|
|
22
|
+
*/
|
|
23
|
+
onCopyLabel?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Accessible name for the reveal toggle when the value is hidden. Override
|
|
26
|
+
* for localization. Defaults to English.
|
|
27
|
+
* @defaultValue 'Show value'
|
|
28
|
+
*/
|
|
29
|
+
revealLabel?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Accessible name for the reveal toggle when the value is shown. Override
|
|
32
|
+
* for localization. Defaults to English.
|
|
33
|
+
* @defaultValue 'Hide value'
|
|
34
|
+
*/
|
|
35
|
+
hideLabel?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Accessible name for the input when no visible `label` is provided. Override
|
|
38
|
+
* for localization. Defaults to English.
|
|
39
|
+
* @defaultValue 'Value to copy'
|
|
40
|
+
*/
|
|
41
|
+
valueLabel?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Milliseconds after a *successful* copy before the clipboard is wiped, for
|
|
44
|
+
* sensitive values that shouldn't linger there (TOTP secrets, recovery
|
|
45
|
+
* codes). Omit — or pass a non-positive number — to never clear.
|
|
46
|
+
*
|
|
47
|
+
* The clear is best-effort and never destructive: before wiping, the
|
|
48
|
+
* clipboard is read back and left untouched unless it still holds exactly
|
|
49
|
+
* the value this field wrote, so a copy the user made in the meantime
|
|
50
|
+
* survives. Browsers without `clipboard.readText` (Firefox, Safari) or that
|
|
51
|
+
* deny the read can't be verified, so nothing is cleared there.
|
|
52
|
+
*/
|
|
53
|
+
clearClipboardAfter?: number;
|
|
54
|
+
};
|
|
55
|
+
export declare function CopyField({ value, label, masked, onCopyLabel, revealLabel, hideLabel, valueLabel, clearClipboardAfter }: CopyFieldProps): React.JSX.Element;
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/copy-field/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAmCD,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,KAAK,EACL,MAAc,EACd,WAAW,EACX,WAA0B,EAC1B,SAAwB,EACxB,UAA4B,EAC5B,mBAAmB,EACpB,EAAE,cAAc,qBAuJhB"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.CopyField = CopyField;
|
|
38
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
39
|
+
const React = __importStar(require("react"));
|
|
40
|
+
const lucide_react_1 = require("lucide-react");
|
|
41
|
+
const icon_button_1 = require("../../../components/ui/icon-button");
|
|
42
|
+
const use_toast_1 = require("../../../hooks/use-toast");
|
|
43
|
+
const DEFAULT_COPY_LABEL = 'Copied to clipboard!';
|
|
44
|
+
const FALLBACK_COPY_LABEL = 'Copy not available — text selected, press Ctrl/Cmd+C';
|
|
45
|
+
const COPIED_ICON_TIMEOUT = 1500;
|
|
46
|
+
const isClipboardAvailable = () => typeof navigator !== 'undefined' &&
|
|
47
|
+
typeof navigator.clipboard?.writeText === 'function';
|
|
48
|
+
const isClipboardReadable = () => typeof navigator !== 'undefined' &&
|
|
49
|
+
typeof navigator.clipboard?.readText === 'function';
|
|
50
|
+
/**
|
|
51
|
+
* Wipes the clipboard, but only if it still holds `copiedValue` — the user may
|
|
52
|
+
* have copied something else in the meantime, and clobbering that would be
|
|
53
|
+
* worse than leaving the secret behind. Unverifiable (no `readText`, permission
|
|
54
|
+
* denied, unfocused document) means "leave it alone".
|
|
55
|
+
*/
|
|
56
|
+
const clearClipboardIfUnchanged = async (copiedValue) => {
|
|
57
|
+
if (!isClipboardReadable()) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
if ((await navigator.clipboard.readText()) !== copiedValue) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await navigator.clipboard.writeText('');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Read or write refused; the clipboard stays as-is.
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function CopyField({ value, label, masked = false, onCopyLabel, revealLabel = 'Show value', hideLabel = 'Hide value', valueLabel = 'Value to copy', clearClipboardAfter }) {
|
|
71
|
+
const { toast } = (0, use_toast_1.useToast)();
|
|
72
|
+
const generatedId = React.useId();
|
|
73
|
+
const inputId = `${generatedId}-copy-field`;
|
|
74
|
+
const inputRef = React.useRef(null);
|
|
75
|
+
const copiedTimerRef = React.useRef(null);
|
|
76
|
+
const clearTimerRef = React.useRef(null);
|
|
77
|
+
const [revealed, setRevealed] = React.useState(false);
|
|
78
|
+
const [copied, setCopied] = React.useState(false);
|
|
79
|
+
const showAsText = !masked || revealed;
|
|
80
|
+
const copyAccessibleName = label ? `Copy ${label}` : 'Copy value';
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
return () => {
|
|
83
|
+
if (copiedTimerRef.current) {
|
|
84
|
+
clearTimeout(copiedTimerRef.current);
|
|
85
|
+
}
|
|
86
|
+
// The clipboard-clear timer is deliberately *not* cancelled here: it
|
|
87
|
+
// touches no state (so it can't warn about an unmounted update) and the
|
|
88
|
+
// secret must still leave the clipboard when the dialog holding this
|
|
89
|
+
// field closes before the timeout elapses.
|
|
90
|
+
};
|
|
91
|
+
}, []);
|
|
92
|
+
const selectFieldText = () => {
|
|
93
|
+
const input = inputRef.current;
|
|
94
|
+
if (!input) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Reveal so the user can copy the selection manually. Flip the DOM `type`
|
|
98
|
+
// imperatively *before* select(): `setRevealed` is batched, so a plain
|
|
99
|
+
// state update would leave the input as type="password" at select() time,
|
|
100
|
+
// and React's subsequent type -> "text" re-render clears the selection —
|
|
101
|
+
// the fallback would then select nothing. We still push the state update so
|
|
102
|
+
// the re-render stays consistent with the DOM we just mutated.
|
|
103
|
+
if (masked) {
|
|
104
|
+
input.type = 'text';
|
|
105
|
+
setRevealed(true);
|
|
106
|
+
}
|
|
107
|
+
input.focus();
|
|
108
|
+
input.select();
|
|
109
|
+
};
|
|
110
|
+
const handleFallback = () => {
|
|
111
|
+
selectFieldText();
|
|
112
|
+
toast({ title: FALLBACK_COPY_LABEL });
|
|
113
|
+
};
|
|
114
|
+
const handleCopy = async () => {
|
|
115
|
+
if (!isClipboardAvailable()) {
|
|
116
|
+
handleFallback();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await navigator.clipboard.writeText(value);
|
|
121
|
+
toast({ variant: 'success', title: onCopyLabel ?? DEFAULT_COPY_LABEL });
|
|
122
|
+
setCopied(true);
|
|
123
|
+
if (copiedTimerRef.current) {
|
|
124
|
+
clearTimeout(copiedTimerRef.current);
|
|
125
|
+
}
|
|
126
|
+
copiedTimerRef.current = setTimeout(() => setCopied(false), COPIED_ICON_TIMEOUT);
|
|
127
|
+
// Only a confirmed write earns a clear — the fallback path never wrote to
|
|
128
|
+
// the clipboard, so it has nothing of ours to wipe. `value` is captured
|
|
129
|
+
// per copy so a later prop change can't make us clear the wrong string.
|
|
130
|
+
if (clearTimerRef.current) {
|
|
131
|
+
clearTimeout(clearTimerRef.current);
|
|
132
|
+
}
|
|
133
|
+
if (clearClipboardAfter !== undefined && clearClipboardAfter > 0) {
|
|
134
|
+
const copiedValue = value;
|
|
135
|
+
clearTimerRef.current = setTimeout(() => {
|
|
136
|
+
void clearClipboardIfUnchanged(copiedValue);
|
|
137
|
+
}, clearClipboardAfter);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
handleFallback();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
return ((0, jsx_runtime_1.jsxs)("div", { "data-slot": "copy-field", className: "flex w-full flex-col gap-2", children: [label ? ((0, jsx_runtime_1.jsx)("label", { htmlFor: inputId, className: "text-muted-foreground text-sm font-semibold", children: label })) : null, (0, jsx_runtime_1.jsxs)("div", { "data-slot": "input-wrapper", className: "border-input-border focus-within:border-ring flex h-10 w-full cursor-text items-center gap-1 rounded-md border pr-1 pl-4 transition-[color,box-shadow]", children: [(0, jsx_runtime_1.jsx)("input", { ref: inputRef, id: inputId, "data-slot": "input", "data-testid": "copy-field-input", type: showAsText ? 'text' : 'password', value: value, readOnly: true, "aria-label": label ? undefined : valueLabel,
|
|
145
|
+
// `min-w-0` lets this flex item shrink below its content width so a
|
|
146
|
+
// long value (e.g. a UUID recovery code) scrolls inside the field
|
|
147
|
+
// instead of overflowing its container. No `font-mono`: the value
|
|
148
|
+
// inherits the app sans font (Inter) so it matches every other field.
|
|
149
|
+
className: "text-input-foreground h-full min-w-0 flex-1 cursor-text border-none bg-transparent text-sm outline-none select-text focus:ring-0 focus:ring-offset-0" }), masked ? ((0, jsx_runtime_1.jsx)(icon_button_1.IconButton, { type: "button", variant: "outline", size: "small", rounded: true, "aria-pressed": revealed, "aria-label": revealed ? hideLabel : revealLabel, onClick: () => setRevealed((prev) => !prev), children: revealed ? (0, jsx_runtime_1.jsx)(lucide_react_1.EyeOff, {}) : (0, jsx_runtime_1.jsx)(lucide_react_1.Eye, {}) })) : null, (0, jsx_runtime_1.jsx)(icon_button_1.IconButton, { type: "button", variant: "outline", size: "small", rounded: true, "aria-label": copyAccessibleName, onClick: handleCopy, children: copied ? (0, jsx_runtime_1.jsx)(lucide_react_1.Check, {}) : (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, {}) })] })] }));
|
|
150
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
@theme inline {
|
|
2
|
-
|
|
2
|
+
--spacing-icon-button-p: calc(var(--spacing) * 2);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
@layer components {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
.icon-button-base {
|
|
7
|
+
@apply p-icon-button-p size-10 rounded-md [&>*]:size-6 [&>*]:shrink-0;
|
|
8
|
+
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
.icon-button-read-only {
|
|
11
|
+
@apply data-[read-only=true]:border-button-border data-[read-only=true]:border;
|
|
12
|
+
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
.icon-button-disabled {
|
|
15
|
+
@apply disabled:border-button-border disabled:border;
|
|
16
|
+
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
.icon-button-small {
|
|
19
|
+
@apply size-8 [&>*]:size-4;
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
.icon-button-rounded {
|
|
23
|
+
@apply rounded-full;
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
26
|
+
/* Overwrite for outline style */
|
|
27
|
+
.button-outline.icon-button-disabled {
|
|
28
|
+
@apply disabled:border-transparent;
|
|
29
|
+
}
|
|
30
|
+
.button-outline.icon-button-read-only {
|
|
31
|
+
@apply data-[read-only=true]:border-transparent;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/input/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/input/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAO,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D,QAAA,MAAM,aAAa;;;8EAejB,CAAA;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,IAAI,EAAE,MAAM,IAAI,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,IAAI,CAC3B,YAAY,CAAC,OAAO,aAAa,CAAC,EAClC,gBAAgB,GAAG,cAAc,CAClC,GACC,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG;IAC9B,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;CAC1B,CAAA;AAEH,wBAAgB,KAAK,CAAC,EACpB,SAAS,EACT,cAAc,EACd,YAAY,EACZ,OAAO,EACP,MAAM,EACN,OAAO,EACP,GAAG,EACH,GAAG,KAAK,EACT,EAAE,UAAU,qBAyEZ;AAED,cAAc,aAAa,CAAA"}
|
|
@@ -40,7 +40,6 @@ exports.Input = Input;
|
|
|
40
40
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
41
41
|
const React = __importStar(require("react"));
|
|
42
42
|
const utils_1 = require("../../../lib/utils");
|
|
43
|
-
const form_1 = require("../../../components/ui/form");
|
|
44
43
|
const class_variance_authority_1 = require("class-variance-authority");
|
|
45
44
|
const inputVariants = (0, class_variance_authority_1.cva)('input-base input-disabled input-read-only', {
|
|
46
45
|
variants: {
|
|
@@ -59,7 +58,9 @@ const inputVariants = (0, class_variance_authority_1.cva)('input-base input-disa
|
|
|
59
58
|
}
|
|
60
59
|
});
|
|
61
60
|
function Input({ className, startAdornment, endAdornment, onFocus, onBlur, onClick, ref, ...props }) {
|
|
62
|
-
|
|
61
|
+
// Fallback id so a standalone Input is still labelable. Inside a form,
|
|
62
|
+
// FormControl injects its own id through `props` below and overrides this.
|
|
63
|
+
const generatedId = React.useId();
|
|
63
64
|
const [focus, setFocus] = React.useState(false);
|
|
64
65
|
const inputRef = React.useRef(null);
|
|
65
66
|
React.useImperativeHandle(ref, () => ({
|
|
@@ -90,7 +91,7 @@ function Input({ className, startAdornment, endAdornment, onFocus, onBlur, onCli
|
|
|
90
91
|
setFocus(false);
|
|
91
92
|
onBlur?.(e);
|
|
92
93
|
};
|
|
93
|
-
return ((0, jsx_runtime_1.jsxs)("div", { "data-slot": "input-wrapper", className: (0, utils_1.cn)('input-wrapper input-wrapper-focus'), "data-focus": focus, onClick: handleWrapperClick, children: [startAdornment, (0, jsx_runtime_1.jsx)("input", { ref: inputRef, id:
|
|
94
|
+
return ((0, jsx_runtime_1.jsxs)("div", { "data-slot": "input-wrapper", className: (0, utils_1.cn)('input-wrapper input-wrapper-focus'), "data-focus": focus, onClick: handleWrapperClick, children: [startAdornment, (0, jsx_runtime_1.jsx)("input", { ref: inputRef, id: generatedId, "data-slot": "input", className: (0, utils_1.cn)(inputVariants({
|
|
94
95
|
startAdornment: !!startAdornment,
|
|
95
96
|
endAdornment: !!endAdornment
|
|
96
97
|
}), className), onFocus: handleFocus, onBlur: handleBlur, ...props }), endAdornment] }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.test.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/input/input.test.tsx"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
require("@testing-library/jest-dom");
|
|
5
|
+
const react_1 = require("@testing-library/react");
|
|
6
|
+
const react_hook_form_1 = require("react-hook-form");
|
|
7
|
+
const _1 = require(".");
|
|
8
|
+
const form_1 = require("../form");
|
|
9
|
+
/**
|
|
10
|
+
* Regression coverage for standalone use.
|
|
11
|
+
*
|
|
12
|
+
* `Input` is a primitive with two callers: `InputField`, which wraps it in
|
|
13
|
+
* `FormField`/`FormItem`, and plain standalone use such as a search box. It
|
|
14
|
+
* used to call `useFormField`, which destructures `useFormContext()` — null
|
|
15
|
+
* with no provider above — so every standalone `<Input />` threw "Cannot
|
|
16
|
+
* destructure property 'getFieldState' of useFormContext(...) as it is null".
|
|
17
|
+
*
|
|
18
|
+
* It now reads no form context at all: a local `useId` covers the standalone
|
|
19
|
+
* case, and inside a form `FormControl`'s Slot injects the real id as a prop.
|
|
20
|
+
*/
|
|
21
|
+
describe('Input outside a form', () => {
|
|
22
|
+
it('renders with no react-hook-form provider', () => {
|
|
23
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Input, { placeholder: "Search permissions by name..." }));
|
|
24
|
+
expect(react_1.screen.getByPlaceholderText('Search permissions by name...')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
it('still gets an id so a label can be associated with it', () => {
|
|
27
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Input, { "aria-label": "Search" }));
|
|
28
|
+
expect(react_1.screen.getByRole('textbox', { name: 'Search' })).toHaveAttribute('id');
|
|
29
|
+
});
|
|
30
|
+
it('keeps value/onChange behaviour intact', () => {
|
|
31
|
+
const onChange = jest.fn();
|
|
32
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.Input, { value: "abc", onChange: onChange, "aria-label": "Search" }));
|
|
33
|
+
const input = react_1.screen.getByRole('textbox', { name: 'Search' });
|
|
34
|
+
expect(input).toHaveValue('abc');
|
|
35
|
+
react_1.fireEvent.change(input, { target: { value: 'abcd' } });
|
|
36
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
37
|
+
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ target: input }));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
function FormHarness() {
|
|
41
|
+
const form = (0, react_hook_form_1.useForm)({ defaultValues: { email: 'ada@example.com' } });
|
|
42
|
+
return ((0, jsx_runtime_1.jsx)(form_1.Form, { ...form, children: (0, jsx_runtime_1.jsx)(form_1.FormField, { control: form.control, name: "email", render: ({ field }) => ((0, jsx_runtime_1.jsxs)(form_1.FormItem, { children: [(0, jsx_runtime_1.jsx)(form_1.FormLabel, { children: "E-mail" }), (0, jsx_runtime_1.jsx)(form_1.FormControl, { children: (0, jsx_runtime_1.jsx)(_1.Input, { ...field }) })] })) }) }));
|
|
43
|
+
}
|
|
44
|
+
describe('Input inside a form', () => {
|
|
45
|
+
it('is still wired to its FormItem id, so the label points at it', () => {
|
|
46
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(FormHarness, {}));
|
|
47
|
+
const input = react_1.screen.getByLabelText('E-mail');
|
|
48
|
+
expect(input).toHaveValue('ada@example.com');
|
|
49
|
+
// The id must come from FormItem's context, not the standalone fallback.
|
|
50
|
+
expect(input.getAttribute('id')).toMatch(/-form-item$/);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type QRCodeProps = {
|
|
2
|
+
/**
|
|
3
|
+
* The `otpauth://totp/...` URI to encode. An empty or whitespace-only value
|
|
4
|
+
* renders nothing (never a broken QR).
|
|
5
|
+
*/
|
|
6
|
+
value: string;
|
|
7
|
+
/**
|
|
8
|
+
* Rendered square edge in px. Drives both width and height of the SVG.
|
|
9
|
+
* @defaultValue 160
|
|
10
|
+
*/
|
|
11
|
+
size?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Optional image URL rendered in the center of the code (e.g. a brand logo).
|
|
14
|
+
* When set, the error-correction level is raised to `H` so the code stays
|
|
15
|
+
* scannable despite the occluded modules, and the modules behind the image
|
|
16
|
+
* are excavated to the background color.
|
|
17
|
+
*/
|
|
18
|
+
logoSrc?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Accessible name for the QR image. Falls back to a generic label when
|
|
21
|
+
* omitted. Consumers should pass an i18n string.
|
|
22
|
+
*/
|
|
23
|
+
'aria-label'?: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function QRCode({ value, size, logoSrc, 'aria-label': ariaLabel }: QRCodeProps): import("react").JSX.Element | null;
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/qr-code/index.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GAAG;IACxB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAUD,wBAAgB,MAAM,CAAC,EACrB,KAAK,EACL,IAAmB,EACnB,OAAO,EACP,YAAY,EAAE,SAA8B,EAC7C,EAAE,WAAW,sCA2Bb"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.QRCode = QRCode;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const qrcode_react_1 = require("qrcode.react");
|
|
7
|
+
const DEFAULT_SIZE = 160;
|
|
8
|
+
const DEFAULT_ARIA_LABEL = 'QR code';
|
|
9
|
+
/**
|
|
10
|
+
* Center image edge as a fraction of the code size. Kept small so error
|
|
11
|
+
* correction can recover the excavated modules.
|
|
12
|
+
*/
|
|
13
|
+
const LOGO_SIZE_RATIO = 0.22;
|
|
14
|
+
function QRCode({ value, size = DEFAULT_SIZE, logoSrc, 'aria-label': ariaLabel = DEFAULT_ARIA_LABEL }) {
|
|
15
|
+
if (!value || value.trim() === '') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const logoSize = Math.round(size * LOGO_SIZE_RATIO);
|
|
19
|
+
return ((0, jsx_runtime_1.jsx)(qrcode_react_1.QRCodeSVG, { "data-slot": "qr-code", value: value, size: size, role: "img", "aria-label": ariaLabel, level: logoSrc ? 'H' : 'L', imageSettings: logoSrc
|
|
20
|
+
? {
|
|
21
|
+
src: logoSrc,
|
|
22
|
+
height: logoSize,
|
|
23
|
+
width: logoSize,
|
|
24
|
+
excavate: true
|
|
25
|
+
}
|
|
26
|
+
: undefined }));
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr-code.test.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/qr-code/qr-code.test.tsx"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
require("@testing-library/jest-dom");
|
|
5
|
+
const react_1 = require("@testing-library/react");
|
|
6
|
+
const _1 = require(".");
|
|
7
|
+
const SAMPLE = 'otpauth://totp/Lerian:user@acme?secret=JBSWY3DPEHPK3PXP&issuer=Lerian';
|
|
8
|
+
describe('QRCode', () => {
|
|
9
|
+
it('renders an svg when value is present', () => {
|
|
10
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: SAMPLE }));
|
|
11
|
+
const svg = container.querySelector('svg');
|
|
12
|
+
expect(svg).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
it('exposes the QR as an image to assistive tech', () => {
|
|
15
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: SAMPLE }));
|
|
16
|
+
expect(react_1.screen.getByRole('img')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
it('renders nothing when value is an empty string', () => {
|
|
19
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: "" }));
|
|
20
|
+
expect(container).toBeEmptyDOMElement();
|
|
21
|
+
expect(react_1.screen.queryByRole('img')).not.toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
it('renders nothing when value is whitespace only', () => {
|
|
24
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: " " }));
|
|
25
|
+
expect(container).toBeEmptyDOMElement();
|
|
26
|
+
expect(react_1.screen.queryByRole('img')).not.toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
it('applies the provided aria-label', () => {
|
|
29
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: SAMPLE, "aria-label": "Scan this QR code" }));
|
|
30
|
+
expect(react_1.screen.getByRole('img', { name: 'Scan this QR code' })).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
it('falls back to a generic accessible name when aria-label is omitted', () => {
|
|
33
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: SAMPLE }));
|
|
34
|
+
const svg = react_1.screen.getByRole('img');
|
|
35
|
+
expect(svg).toHaveAccessibleName();
|
|
36
|
+
expect(svg.getAttribute('aria-label')).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
it('applies the provided size to width and height', () => {
|
|
39
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: SAMPLE, size: 200 }));
|
|
40
|
+
const svg = container.querySelector('svg');
|
|
41
|
+
expect(svg).toHaveAttribute('width', '200');
|
|
42
|
+
expect(svg).toHaveAttribute('height', '200');
|
|
43
|
+
});
|
|
44
|
+
it('defaults width and height to 160 when size is omitted', () => {
|
|
45
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: SAMPLE }));
|
|
46
|
+
const svg = container.querySelector('svg');
|
|
47
|
+
expect(svg).toHaveAttribute('width', '160');
|
|
48
|
+
expect(svg).toHaveAttribute('height', '160');
|
|
49
|
+
});
|
|
50
|
+
it('embeds the center logo when logoSrc is provided', () => {
|
|
51
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: SAMPLE, logoSrc: "/logo.svg" }));
|
|
52
|
+
const image = container.querySelector('image');
|
|
53
|
+
expect(image).toBeInTheDocument();
|
|
54
|
+
expect(image).toHaveAttribute('href', '/logo.svg');
|
|
55
|
+
});
|
|
56
|
+
it('renders no embedded image when logoSrc is omitted', () => {
|
|
57
|
+
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(_1.QRCode, { value: SAMPLE }));
|
|
58
|
+
expect(container.querySelector('image')).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -7,6 +7,6 @@ type SidebarItemIconButtonProps = React.ComponentProps<typeof Link> & {
|
|
|
7
7
|
inactive?: boolean;
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
};
|
|
10
|
-
export declare const SidebarItemIconButton: ({ title, icon, href, active, inactive, disabled, ...props }: SidebarItemIconButtonProps) => React.JSX.Element;
|
|
10
|
+
export declare const SidebarItemIconButton: ({ className, title, icon, href, active, inactive, disabled, ...props }: SidebarItemIconButtonProps) => React.JSX.Element;
|
|
11
11
|
export {};
|
|
12
12
|
//# sourceMappingURL=sidebar-item-icon-button.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar-item-icon-button.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/sidebar/sidebar-item-icon-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,IAAI,MAAM,WAAW,CAAA;AAW5B,KAAK,0BAA0B,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,IAAI,CAAC,GAAG;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,KAAK,CAAC,SAAS,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,qBAAqB,GAAI,
|
|
1
|
+
{"version":3,"file":"sidebar-item-icon-button.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/sidebar/sidebar-item-icon-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,IAAI,MAAM,WAAW,CAAA;AAW5B,KAAK,0BAA0B,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,IAAI,CAAC,GAAG;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,KAAK,CAAC,SAAS,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,qBAAqB,GAAI,wEASnC,0BAA0B,sBAqC5B,CAAA"}
|
|
@@ -11,10 +11,10 @@ const button_1 = require("../../ui/button");
|
|
|
11
11
|
const tooltip_1 = require("../../ui/tooltip");
|
|
12
12
|
const utils_1 = require("../../../lib/utils");
|
|
13
13
|
const icon_button_1 = require("../icon-button");
|
|
14
|
-
const SidebarItemIconButton = ({ title, icon, href, active, inactive, disabled, ...props }) => {
|
|
14
|
+
const SidebarItemIconButton = ({ className, title, icon, href, active, inactive, disabled, ...props }) => {
|
|
15
15
|
const sharedClassName = (0, utils_1.cn)((0, button_1.buttonVariants)({
|
|
16
16
|
variant: active ? 'tertiary' : 'outline'
|
|
17
|
-
}), (0, icon_button_1.iconButtonVariants)(), inactive && 'hover:border-transparent', disabled && 'cursor-default opacity-30');
|
|
17
|
+
}), (0, icon_button_1.iconButtonVariants)(), inactive && 'hover:border-transparent', disabled && 'cursor-default opacity-30', className);
|
|
18
18
|
return ((0, jsx_runtime_1.jsx)(tooltip_1.TooltipProvider, { children: (0, jsx_runtime_1.jsxs)(tooltip_1.Tooltip, { delayDuration: 0, children: [(0, jsx_runtime_1.jsx)(tooltip_1.TooltipTrigger, { asChild: true, children: disabled || inactive ? ((0, jsx_runtime_1.jsx)("div", { "data-slot": "sidebar-item-icon-button", className: sharedClassName, children: icon })) : ((0, jsx_runtime_1.jsx)(link_1.default, { "data-slot": "sidebar-item-icon-button", href: href, className: sharedClassName, ...props, children: icon })) }), (0, jsx_runtime_1.jsx)(tooltip_1.TooltipContent, { side: "right", children: title })] }) }));
|
|
19
19
|
};
|
|
20
20
|
exports.SidebarItemIconButton = SidebarItemIconButton;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from './components/ui/card';
|
|
|
11
11
|
export * from './components/ui/checkbox';
|
|
12
12
|
export * from './components/ui/collapsible';
|
|
13
13
|
export * from './components/ui/command';
|
|
14
|
+
export * from './components/ui/copy-field';
|
|
14
15
|
export * from './components/ui/dialog';
|
|
15
16
|
export * from './components/ui/dropdown-menu';
|
|
16
17
|
export * from './components/ui/input';
|
|
@@ -22,6 +23,7 @@ export * from './components/ui/paper';
|
|
|
22
23
|
export * from './components/ui/paper-collapsible';
|
|
23
24
|
export * from './components/ui/popover';
|
|
24
25
|
export * from './components/ui/progress';
|
|
26
|
+
export * from './components/ui/qr-code';
|
|
25
27
|
export * from './components/ui/select';
|
|
26
28
|
export * from './components/ui/separator';
|
|
27
29
|
export * from './components/ui/sheet';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAGA,cAAc,uBAAuB,CAAA;AACrC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,wBAAwB,CAAA;AACtC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,0BAA0B,CAAA;AACxC,cAAc,sBAAsB,CAAA;AACpC,cAAc,0BAA0B,CAAA;AACxC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,uBAAuB,CAAA;AACrC,cAAc,2BAA2B,CAAA;AACzC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,uBAAuB,CAAA;AACrC,cAAc,mCAAmC,CAAA;AACjD,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,wBAAwB,CAAA;AACtC,cAAc,2BAA2B,CAAA;AACzC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA;AACpC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,mBAAmB,CAAA;AACjC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AAGpC,OAAO,EAAE,UAAU,IAAI,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC7E,cAAc,8CAA8C,CAAA;AAC5D,cAAc,mBAAmB,CAAA;AACjC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAGA,cAAc,uBAAuB,CAAA;AACrC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,wBAAwB,CAAA;AACtC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,0BAA0B,CAAA;AACxC,cAAc,sBAAsB,CAAA;AACpC,cAAc,0BAA0B,CAAA;AACxC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,wBAAwB,CAAA;AACtC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,uBAAuB,CAAA;AACrC,cAAc,2BAA2B,CAAA;AACzC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,uBAAuB,CAAA;AACrC,cAAc,mCAAmC,CAAA;AACjD,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,2BAA2B,CAAA;AACzC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA;AACpC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,mBAAmB,CAAA;AACjC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AAGpC,OAAO,EAAE,UAAU,IAAI,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC7E,cAAc,8CAA8C,CAAA;AAC5D,cAAc,mBAAmB,CAAA;AACjC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,oBAAoB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ __exportStar(require("./components/ui/card"), exports);
|
|
|
30
30
|
__exportStar(require("./components/ui/checkbox"), exports);
|
|
31
31
|
__exportStar(require("./components/ui/collapsible"), exports);
|
|
32
32
|
__exportStar(require("./components/ui/command"), exports);
|
|
33
|
+
__exportStar(require("./components/ui/copy-field"), exports);
|
|
33
34
|
__exportStar(require("./components/ui/dialog"), exports);
|
|
34
35
|
__exportStar(require("./components/ui/dropdown-menu"), exports);
|
|
35
36
|
__exportStar(require("./components/ui/input"), exports);
|
|
@@ -41,6 +42,7 @@ __exportStar(require("./components/ui/paper"), exports);
|
|
|
41
42
|
__exportStar(require("./components/ui/paper-collapsible"), exports);
|
|
42
43
|
__exportStar(require("./components/ui/popover"), exports);
|
|
43
44
|
__exportStar(require("./components/ui/progress"), exports);
|
|
45
|
+
__exportStar(require("./components/ui/qr-code"), exports);
|
|
44
46
|
__exportStar(require("./components/ui/select"), exports);
|
|
45
47
|
__exportStar(require("./components/ui/separator"), exports);
|
|
46
48
|
__exportStar(require("./components/ui/sheet"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lerianstudio/sindarian-ui",
|
|
3
|
-
"version": "1.2.0-beta.
|
|
3
|
+
"version": "1.2.0-beta.12",
|
|
4
4
|
"description": "Sindarian UI - A UI library for Midaz Console",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": {
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"dayjs": "^1.11.21",
|
|
54
54
|
"input-otp": "^1.4.2",
|
|
55
55
|
"postcss": "^8.5.16",
|
|
56
|
+
"qrcode.react": "^4.2.0",
|
|
56
57
|
"sonner": "^2.0.7",
|
|
57
58
|
"tailwind-merge": "^3.6.0",
|
|
58
59
|
"tailwindcss": "^4.3.2",
|