@1e99/components 0.1.3 → 0.2.1
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/README.md +27 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.js +110 -18
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ O pacote não cria tokens nem importa CSS global.
|
|
|
33
33
|
```tsx
|
|
34
34
|
import {useState} from "react";
|
|
35
35
|
import {
|
|
36
|
+
AccountControl,
|
|
36
37
|
GoogleSignInButton,
|
|
37
38
|
Navbar,
|
|
38
39
|
Selector,
|
|
@@ -90,6 +91,29 @@ biblioteca de navegação. Os itens podem ser adicionados ou removidos, e
|
|
|
90
91
|
um botão, como `onClick`, `disabled` e `aria-*`. Autenticação, estado de
|
|
91
92
|
carregamento e traduções pertencem ao aplicativo que o utiliza.
|
|
92
93
|
|
|
94
|
+
`AccountControl` apresenta a identidade autenticada e a ação de saída. Passe
|
|
95
|
+
nome/e-mail, um `fallbackIdentity`, textos traduzidos e `onSignOut`; a ação
|
|
96
|
+
concreta de logout, Auth.js, rotas e locale continuam no aplicativo consumidor.
|
|
97
|
+
Como ele compõe um `Selector` interativo, use-o a partir de um componente do
|
|
98
|
+
cliente:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
<AccountControl
|
|
102
|
+
email="ada@example.com"
|
|
103
|
+
fallbackIdentity="Conta"
|
|
104
|
+
menuAriaLabel="Menu da conta de Ada Lovelace"
|
|
105
|
+
menuText="Conta"
|
|
106
|
+
name="Ada Lovelace"
|
|
107
|
+
onSignOut={() => logout()}
|
|
108
|
+
signOutAriaLabel="Sair desta conta"
|
|
109
|
+
signOutText="Sair"
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`getInitials(value)` é um utilitário puro exportado pelo pacote. Ele retorna
|
|
114
|
+
até duas iniciais (primeira e última palavras), normaliza espaços e usa `"1"`
|
|
115
|
+
quando não há identidade disponível.
|
|
116
|
+
|
|
93
117
|
`Selector` é controlado: `value` determina a opção visível e
|
|
94
118
|
`onValueChange` informa uma nova escolha. As opções exigem somente `value` e
|
|
95
119
|
`label`, mas podem ser estendidas com outros campos e apresentadas por
|
|
@@ -104,13 +128,15 @@ pnpm lint:fix
|
|
|
104
128
|
pnpm format
|
|
105
129
|
pnpm storybook
|
|
106
130
|
pnpm test
|
|
131
|
+
pnpm test:utils
|
|
107
132
|
pnpm test:stories
|
|
108
133
|
pnpm build
|
|
109
134
|
pnpm build-storybook
|
|
110
135
|
pnpm pack:check
|
|
111
136
|
```
|
|
112
137
|
|
|
113
|
-
Husky executa `pnpm lint` e `pnpm test` antes de cada commit.
|
|
138
|
+
Husky executa `pnpm lint` e `pnpm test` antes de cada commit. Utilitários puros
|
|
139
|
+
são verificados pelo Jest com `pnpm test:utils`. As stories são
|
|
114
140
|
verificadas em Chromium por `pnpm test:stories`; em uma máquina Linux nova,
|
|
115
141
|
instale as dependências uma vez:
|
|
116
142
|
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ type GoogleSignInButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'ch
|
|
|
6
6
|
};
|
|
7
7
|
declare function GoogleSignInButton({ label, ...buttonProps }: GoogleSignInButtonProps): react.JSX.Element;
|
|
8
8
|
|
|
9
|
+
type AccountControlProps = {
|
|
10
|
+
email?: string | null;
|
|
11
|
+
fallbackIdentity: string;
|
|
12
|
+
menuAriaLabel: string;
|
|
13
|
+
menuText: string;
|
|
14
|
+
name?: string | null;
|
|
15
|
+
onSignOut: () => void;
|
|
16
|
+
signOutAriaLabel: string;
|
|
17
|
+
signOutText: string;
|
|
18
|
+
};
|
|
19
|
+
declare function AccountControl({ email, fallbackIdentity, menuAriaLabel, menuText, name, onSignOut, signOutAriaLabel, signOutText, }: AccountControlProps): react.JSX.Element;
|
|
20
|
+
|
|
21
|
+
declare function getInitials(value: string): string;
|
|
22
|
+
|
|
9
23
|
type SelectorOption = {
|
|
10
24
|
label: ReactNode;
|
|
11
25
|
value: string;
|
|
@@ -67,5 +81,5 @@ type NavbarProps = {
|
|
|
67
81
|
};
|
|
68
82
|
declare function Navbar({ actions, brand, items, layer, linkTreatment, linkWeight, mobileNavigation, navigationLabel, renderLink, width, }: NavbarProps): react.JSX.Element;
|
|
69
83
|
|
|
70
|
-
export { GoogleSignInButton, Navbar, Selector };
|
|
71
|
-
export type { GoogleSignInButtonProps, NavbarItem, NavbarLayer, NavbarLinkRenderer, NavbarLinkTreatment, NavbarLinkWeight, NavbarMobileNavigation, NavbarProps, NavbarWidth, SelectorDensity, SelectorOption, SelectorProps, SelectorRenderOptionContext, SelectorRenderTriggerContext };
|
|
84
|
+
export { AccountControl, GoogleSignInButton, Navbar, Selector, getInitials };
|
|
85
|
+
export type { AccountControlProps, GoogleSignInButtonProps, NavbarItem, NavbarLayer, NavbarLinkRenderer, NavbarLinkTreatment, NavbarLinkWeight, NavbarMobileNavigation, NavbarProps, NavbarWidth, SelectorDensity, SelectorOption, SelectorProps, SelectorRenderOptionContext, SelectorRenderTriggerContext };
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,16 @@ function GoogleSignInButton({ label, ...buttonProps }) {
|
|
|
16
16
|
] });
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// src/components/account-control/account-control.tsx
|
|
20
|
+
import { LuLogOut } from "react-icons/lu";
|
|
21
|
+
|
|
22
|
+
// src/utils/get-initials.ts
|
|
23
|
+
function getInitials(value) {
|
|
24
|
+
const parts = value.trim().split(/\s+/).filter(Boolean);
|
|
25
|
+
const selected = parts.length > 1 ? [parts[0], parts.at(-1)] : parts;
|
|
26
|
+
return selected.map((part) => Array.from(part)[0]).join("").toUpperCase().slice(0, 2) || "1";
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
// src/components/selector/selector.tsx
|
|
20
30
|
import { useEffect, useId, useRef, useState } from "react";
|
|
21
31
|
import { LuChevronDown } from "react-icons/lu";
|
|
@@ -157,10 +167,79 @@ function Selector({
|
|
|
157
167
|
] });
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
// src/components/account-control/account-control.tsx
|
|
171
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
172
|
+
function AccountControl({
|
|
173
|
+
email,
|
|
174
|
+
fallbackIdentity,
|
|
175
|
+
menuAriaLabel,
|
|
176
|
+
menuText,
|
|
177
|
+
name,
|
|
178
|
+
onSignOut,
|
|
179
|
+
signOutAriaLabel,
|
|
180
|
+
signOutText
|
|
181
|
+
}) {
|
|
182
|
+
const normalizedName = name?.trim();
|
|
183
|
+
const identity = normalizedName || email || fallbackIdentity;
|
|
184
|
+
const initials = getInitials(identity);
|
|
185
|
+
const showEmail = Boolean(email && email !== identity);
|
|
186
|
+
const options = [
|
|
187
|
+
{ label: identity, value: "account" },
|
|
188
|
+
{ label: signOutText, value: "sign-out" }
|
|
189
|
+
];
|
|
190
|
+
return /* @__PURE__ */ jsx3(
|
|
191
|
+
Selector,
|
|
192
|
+
{
|
|
193
|
+
label: menuText,
|
|
194
|
+
onValueChange: (value) => {
|
|
195
|
+
if (value === "sign-out") {
|
|
196
|
+
onSignOut();
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
options,
|
|
200
|
+
renderOption: ({ option }) => option.value === "account" ? /* @__PURE__ */ jsxs3("span", { "data-account-identity": true, className: "flex min-w-0 flex-col items-start py-1", children: [
|
|
201
|
+
/* @__PURE__ */ jsx3(
|
|
202
|
+
"span",
|
|
203
|
+
{
|
|
204
|
+
className: "w-full truncate text-ui font-semibold text-content-primary",
|
|
205
|
+
title: identity,
|
|
206
|
+
children: identity
|
|
207
|
+
}
|
|
208
|
+
),
|
|
209
|
+
showEmail ? /* @__PURE__ */ jsx3(
|
|
210
|
+
"span",
|
|
211
|
+
{
|
|
212
|
+
className: "mt-1 w-full truncate text-label text-content-secondary",
|
|
213
|
+
title: email ?? void 0,
|
|
214
|
+
children: email
|
|
215
|
+
}
|
|
216
|
+
) : null
|
|
217
|
+
] }) : /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-3", children: [
|
|
218
|
+
/* @__PURE__ */ jsx3(LuLogOut, { "aria-hidden": "true", className: "size-5 shrink-0" }),
|
|
219
|
+
/* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: option.label }),
|
|
220
|
+
/* @__PURE__ */ jsx3("span", { className: "sr-only", children: signOutAriaLabel })
|
|
221
|
+
] }),
|
|
222
|
+
renderTrigger: () => /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 items-center gap-3", children: [
|
|
223
|
+
/* @__PURE__ */ jsx3(
|
|
224
|
+
"span",
|
|
225
|
+
{
|
|
226
|
+
"aria-hidden": "true",
|
|
227
|
+
className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-brand-yellow font-data text-label font-semibold text-brand-navy-dark",
|
|
228
|
+
children: initials
|
|
229
|
+
}
|
|
230
|
+
),
|
|
231
|
+
/* @__PURE__ */ jsx3("span", { className: "text-label font-semibold md:hidden", children: menuText })
|
|
232
|
+
] }),
|
|
233
|
+
triggerAriaLabel: `${initials} ${menuAriaLabel}`,
|
|
234
|
+
value: "account"
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
160
239
|
// src/components/navbar/navbar.tsx
|
|
161
|
-
import { Fragment } from "react";
|
|
240
|
+
import { Fragment, useId as useId2 } from "react";
|
|
162
241
|
import { LuMenu } from "react-icons/lu";
|
|
163
|
-
import { jsx as
|
|
242
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
164
243
|
var baseLinkClassName = "text-label text-content-secondary transition-colors duration-200 ease-brand hover:text-content-primary focus-visible:rounded-control focus-visible:outline-none focus-visible:shadow-focus";
|
|
165
244
|
var underlinedLinkClassName = "relative after:absolute after:-bottom-1.5 after:left-0 after:h-0.5 after:w-full after:origin-left after:scale-x-0 after:bg-action-primary after:transition-transform after:duration-200 after:ease-brand hover:after:scale-x-100 focus-visible:after:scale-x-100";
|
|
166
245
|
var mobileLinkClassName = "flex min-h-12 items-center rounded-control px-3 text-label font-semibold text-content-secondary transition-colors duration-200 ease-brand hover:text-content-primary focus-visible:outline-none focus-visible:shadow-focus";
|
|
@@ -176,6 +255,9 @@ function Navbar({
|
|
|
176
255
|
renderLink,
|
|
177
256
|
width = "content"
|
|
178
257
|
}) {
|
|
258
|
+
const mobileNavigationReference = useId2();
|
|
259
|
+
const mobileNavigationId = `navbar-mobile-navigation-${mobileNavigationReference}`;
|
|
260
|
+
const mobileNavigationAnchorName = `--navbar-mobile-menu-trigger-${mobileNavigationReference.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
179
261
|
const linkClassName = [
|
|
180
262
|
baseLinkClassName,
|
|
181
263
|
linkWeight === "bold" ? "font-bold" : "font-semibold",
|
|
@@ -183,43 +265,51 @@ function Navbar({
|
|
|
183
265
|
].filter(Boolean).join(" ");
|
|
184
266
|
const containerWidthClassName = width === "wide" ? "max-w-7xl" : "max-w-5xl";
|
|
185
267
|
const layerClassName = layer === "above" ? "z-50" : "z-40";
|
|
186
|
-
const renderLinks = (className) => items.map((item) => /* @__PURE__ */
|
|
268
|
+
const renderLinks = (className) => items.map((item) => /* @__PURE__ */ jsx4(Fragment, { children: renderLink ? renderLink({
|
|
187
269
|
children: item.label,
|
|
188
270
|
className,
|
|
189
271
|
href: item.href
|
|
190
|
-
}) : /* @__PURE__ */
|
|
191
|
-
return /* @__PURE__ */
|
|
272
|
+
}) : /* @__PURE__ */ jsx4("a", { className, href: item.href, children: item.label }) }, item.href));
|
|
273
|
+
return /* @__PURE__ */ jsx4(
|
|
192
274
|
"header",
|
|
193
275
|
{
|
|
194
276
|
className: `sticky top-0 ${layerClassName} border-b border-border-subtle bg-surface-page/90 text-label font-semibold backdrop-blur-lg`,
|
|
195
|
-
children: /* @__PURE__ */
|
|
277
|
+
children: /* @__PURE__ */ jsxs4(
|
|
196
278
|
"div",
|
|
197
279
|
{
|
|
198
280
|
className: `mx-auto flex min-h-20 ${containerWidthClassName} items-center justify-between gap-4 px-shell`,
|
|
199
281
|
children: [
|
|
200
|
-
/* @__PURE__ */
|
|
201
|
-
/* @__PURE__ */
|
|
282
|
+
/* @__PURE__ */ jsx4("div", { className: "shrink-0", children: brand }),
|
|
283
|
+
/* @__PURE__ */ jsx4("nav", { "aria-label": navigationLabel, className: "hidden items-center gap-6 lg:flex", children: renderLinks(linkClassName) }),
|
|
202
284
|
actions,
|
|
203
|
-
mobileNavigation ? /* @__PURE__ */
|
|
204
|
-
/* @__PURE__ */
|
|
205
|
-
"
|
|
285
|
+
mobileNavigation ? /* @__PURE__ */ jsxs4("div", { className: "relative md:hidden", children: [
|
|
286
|
+
/* @__PURE__ */ jsxs4(
|
|
287
|
+
"button",
|
|
206
288
|
{
|
|
289
|
+
"aria-controls": mobileNavigationId,
|
|
207
290
|
"aria-label": mobileNavigation.menuLabel,
|
|
208
|
-
className: "flex size-12 cursor-pointer
|
|
291
|
+
className: "flex size-12 cursor-pointer items-center justify-center rounded-control bg-action-primary text-content-inverse shadow-card transition-[translate,scale,background-color] duration-200 ease-brand active:scale-[0.97] focus-visible:outline-none focus-visible:shadow-focus",
|
|
292
|
+
popoverTarget: mobileNavigationId,
|
|
293
|
+
popoverTargetAction: "toggle",
|
|
294
|
+
style: { anchorName: mobileNavigationAnchorName },
|
|
295
|
+
type: "button",
|
|
209
296
|
children: [
|
|
210
|
-
/* @__PURE__ */
|
|
211
|
-
/* @__PURE__ */
|
|
297
|
+
/* @__PURE__ */ jsx4("span", { className: "sr-only", children: mobileNavigation.menuLabel }),
|
|
298
|
+
/* @__PURE__ */ jsx4(LuMenu, { "aria-hidden": "true", className: "size-6" })
|
|
212
299
|
]
|
|
213
300
|
}
|
|
214
301
|
),
|
|
215
|
-
/* @__PURE__ */
|
|
302
|
+
/* @__PURE__ */ jsxs4(
|
|
216
303
|
"nav",
|
|
217
304
|
{
|
|
218
305
|
"aria-label": mobileNavigation.navigationLabel,
|
|
219
|
-
className: "
|
|
306
|
+
className: "fixed top-[4.5rem] right-[1.25rem] m-0 w-72 rounded-card border border-border-subtle bg-surface-raised p-3 shadow-float [@supports(position-anchor:--navbar-mobile-menu-trigger)]:absolute [@supports(position-anchor:--navbar-mobile-menu-trigger)]:top-[calc(anchor(bottom)+0.5rem)] [@supports(position-anchor:--navbar-mobile-menu-trigger)]:right-auto [@supports(position-anchor:--navbar-mobile-menu-trigger)]:left-[anchor(right)] [@supports(position-anchor:--navbar-mobile-menu-trigger)]:-translate-x-full",
|
|
307
|
+
id: mobileNavigationId,
|
|
308
|
+
popover: "auto",
|
|
309
|
+
style: { positionAnchor: mobileNavigationAnchorName },
|
|
220
310
|
children: [
|
|
221
311
|
renderLinks(mobileLinkClassName),
|
|
222
|
-
mobileNavigation.actions ? /* @__PURE__ */
|
|
312
|
+
mobileNavigation.actions ? /* @__PURE__ */ jsx4("div", { className: "mt-2 space-y-3 border-t border-border-subtle pt-3", children: mobileNavigation.actions }) : null
|
|
223
313
|
]
|
|
224
314
|
}
|
|
225
315
|
)
|
|
@@ -231,8 +321,10 @@ function Navbar({
|
|
|
231
321
|
);
|
|
232
322
|
}
|
|
233
323
|
export {
|
|
324
|
+
AccountControl,
|
|
234
325
|
GoogleSignInButton,
|
|
235
326
|
Navbar,
|
|
236
|
-
Selector
|
|
327
|
+
Selector,
|
|
328
|
+
getInitials
|
|
237
329
|
};
|
|
238
330
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/google-sign-in-button/google-sign-in-button.tsx","../src/components/selector/selector.tsx","../src/components/navbar/navbar.tsx"],"sourcesContent":["import type {ButtonHTMLAttributes, ReactNode} from 'react';\nimport {FcGoogle} from 'react-icons/fc';\n\nexport type GoogleSignInButtonProps = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n 'children' | 'className' | 'type'\n> & {\n label: ReactNode;\n};\n\nconst buttonClassName =\n 'inline-flex min-h-12 w-full cursor-pointer items-center justify-center gap-3 rounded-control border border-border-subtle bg-surface-raised px-control-x py-control-y text-label font-semibold whitespace-nowrap text-content-primary shadow-card transition-[translate,scale,background-color] duration-200 ease-brand hover:-translate-y-0.5 hover:bg-brand-cloud active:scale-[0.97] focus-visible:outline-none focus-visible:shadow-focus disabled:cursor-wait disabled:opacity-70 motion-reduce:transform-none';\n\nexport function GoogleSignInButton(\n {label, ...buttonProps}: GoogleSignInButtonProps,\n) {\n return (\n <button {...buttonProps} className={buttonClassName} type=\"button\">\n <FcGoogle\n aria-hidden=\"true\"\n className=\"size-5 shrink-0\"\n data-testid=\"google-mark\"\n />\n {label}\n </button>\n );\n}\n","import {type ReactNode, useEffect, useId, useRef, useState} from 'react';\nimport {LuChevronDown} from 'react-icons/lu';\n\nexport type SelectorOption = {\n label: ReactNode;\n value: string;\n};\n\nexport type SelectorDensity = 'compact' | 'compact-mobile' | 'standard';\n\nexport type SelectorRenderOptionContext<TOption extends SelectorOption> = {\n isSelected: boolean;\n option: TOption;\n select: () => void;\n};\n\nexport type SelectorRenderTriggerContext<TOption extends SelectorOption> = {\n selectedOption: TOption | undefined;\n value: string;\n};\n\ntype SelectorBaseProps = {\n label: string;\n triggerAriaLabel?: string;\n value: string;\n};\n\nexport type SelectorProps<TOption extends SelectorOption = SelectorOption> =\n SelectorBaseProps & {\n density?: SelectorDensity;\n onValueChange: (value: string) => void;\n options: readonly TOption[];\n renderOption?: (context: SelectorRenderOptionContext<TOption>) => ReactNode;\n renderTrigger?: (\n context: SelectorRenderTriggerContext<TOption>,\n ) => ReactNode;\n whiteSurface?: boolean;\n };\n\nconst standardTriggerClassName =\n 'flex w-full cursor-pointer select-none items-center justify-between rounded-control border border-border-subtle py-control-y text-label font-semibold text-content-primary shadow-card transition-colors duration-200 ease-brand hover:bg-brand-cloud focus-visible:outline-none focus-visible:shadow-focus';\n\nconst optionClassName =\n 'flex min-h-12 w-full select-none items-center rounded-control px-3 text-label font-semibold text-content-secondary transition-colors duration-200 ease-brand hover:bg-brand-cloud hover:text-content-primary focus-visible:outline-none focus-visible:shadow-focus';\n\nfunction getNavigationLayout(density: SelectorDensity) {\n if (density === 'compact') {\n return {trigger: 'h-12 gap-2', wrapper: 'w-auto'};\n }\n\n if (density === 'compact-mobile') {\n return {\n trigger: 'h-12 gap-2 px-3 md:gap-4 md:px-4 md:min-w-32',\n wrapper: 'w-auto',\n };\n }\n\n return {trigger: 'min-h-12 gap-4 px-4 md:min-w-32', wrapper: 'w-full md:w-auto'};\n}\n\nfunction SelectorTrigger<TOption extends SelectorOption>({\n densityClassName,\n isOpen,\n onClick,\n optionListId,\n renderTrigger,\n selectedOption,\n triggerAriaLabel,\n value,\n}: {\n densityClassName: string;\n isOpen: boolean;\n onClick: () => void;\n optionListId: string;\n renderTrigger?: (context: SelectorRenderTriggerContext<TOption>) => ReactNode;\n selectedOption: TOption | undefined;\n triggerAriaLabel?: string;\n value: string;\n}) {\n const triggerContent = renderTrigger?.({selectedOption, value}) ??\n selectedOption?.label ?? value;\n\n return (\n <button\n aria-controls={optionListId}\n aria-expanded={isOpen}\n aria-label={triggerAriaLabel}\n className={`${standardTriggerClassName} ${densityClassName}`}\n onClick={onClick}\n type=\"button\"\n >\n <span>{triggerContent}</span>\n <LuChevronDown\n aria-hidden=\"true\"\n className={`selector-disclosure-arrow size-5 shrink-0 text-content-secondary transition-transform duration-200 ease-brand motion-reduce:transition-none ${isOpen ? 'rotate-180' : ''}`}\n />\n </button>\n );\n}\n\nexport function Selector<TOption extends SelectorOption = SelectorOption>({\n density = 'standard',\n label,\n onValueChange,\n options,\n renderOption,\n renderTrigger,\n triggerAriaLabel,\n value,\n whiteSurface = false,\n}: SelectorProps<TOption>) {\n const [isOpen, setIsOpen] = useState(false);\n const [selectionRequest, setSelectionRequest] =\n useState<{value: string} | null>(null);\n const optionListId = useId();\n const notifiedSelectionReference = useRef<{value: string} | null>(null);\n const onValueChangeReference = useRef(onValueChange);\n const selectorReference = useRef<HTMLDivElement>(null);\n const selectedOption = options.find((option) => option.value === value);\n const layout = getNavigationLayout(density);\n const surfaceClassName = whiteSurface ? 'bg-white' : 'bg-surface-raised';\n\n useEffect(() => {\n if (!isOpen) {\n return;\n }\n\n const closeWhenPointerStartsOutside = (event: PointerEvent) => {\n const selectorElement = selectorReference.current;\n\n if (\n !selectorElement ||\n !(event.target instanceof Node) ||\n selectorElement.contains(event.target)\n ) {\n return;\n }\n\n setIsOpen(false);\n };\n\n document.addEventListener('pointerdown', closeWhenPointerStartsOutside, true);\n\n return () => {\n document.removeEventListener('pointerdown', closeWhenPointerStartsOutside, true);\n };\n }, [isOpen]);\n\n useEffect(() => {\n onValueChangeReference.current = onValueChange;\n }, [onValueChange]);\n\n useEffect(() => {\n if (\n isOpen ||\n selectionRequest === null ||\n selectionRequest === notifiedSelectionReference.current\n ) {\n return;\n }\n\n notifiedSelectionReference.current = selectionRequest;\n onValueChangeReference.current(selectionRequest.value);\n }, [isOpen, selectionRequest]);\n\n const selectOption = (nextValue: string) => {\n setIsOpen(false);\n setSelectionRequest({value: nextValue});\n };\n\n return (\n <div ref={selectorReference} className={`selector-disclosure relative ${layout.wrapper}`}>\n <SelectorTrigger\n densityClassName={layout.trigger}\n isOpen={isOpen}\n onClick={() => setIsOpen((currentOpen) => !currentOpen)}\n optionListId={optionListId}\n renderTrigger={renderTrigger}\n selectedOption={selectedOption}\n triggerAriaLabel={triggerAriaLabel}\n value={value}\n />\n {isOpen ? (\n <ul\n aria-label={label}\n className={`mt-2 w-full overflow-hidden rounded-control border border-border-subtle ${surfaceClassName} p-1 shadow-float md:absolute md:top-full md:right-0 md:z-60 md:min-w-44`}\n id={optionListId}\n >\n {options.map((option) => {\n const isSelected = option.value === value;\n const select = () => selectOption(option.value);\n const optionContent =\n renderOption?.({isSelected, option, select}) ?? option.label;\n\n return (\n <li key={option.value}>\n <button\n aria-pressed={isSelected}\n className={`${optionClassName} text-left ${isSelected ? 'bg-brand-cloud text-content-primary' : ''}`}\n onClick={select}\n type=\"button\"\n >\n {optionContent}\n </button>\n </li>\n );\n })}\n </ul>\n ) : null}\n </div>\n );\n}\n","import {Fragment, type ReactNode} from 'react';\nimport {LuMenu} from 'react-icons/lu';\n\nexport type NavbarItem = {\n href: string;\n label: ReactNode;\n};\n\nexport type NavbarLinkRenderer = (props: {\n children: ReactNode;\n className: string;\n href: string;\n}) => ReactNode;\n\nexport type NavbarWidth = 'content' | 'wide';\nexport type NavbarLinkTreatment = 'plain' | 'underlined';\nexport type NavbarLinkWeight = 'bold' | 'semibold';\nexport type NavbarLayer = 'above' | 'standard';\nexport type NavbarMobileNavigation = {\n actions?: ReactNode;\n menuLabel: string;\n navigationLabel: string;\n};\n\nexport type NavbarProps = {\n actions?: ReactNode;\n brand: ReactNode;\n items: readonly NavbarItem[];\n layer?: NavbarLayer;\n linkTreatment?: NavbarLinkTreatment;\n linkWeight?: NavbarLinkWeight;\n mobileNavigation?: NavbarMobileNavigation;\n navigationLabel: string;\n renderLink?: NavbarLinkRenderer;\n width?: NavbarWidth;\n};\n\nconst baseLinkClassName =\n 'text-label text-content-secondary transition-colors duration-200 ease-brand hover:text-content-primary focus-visible:rounded-control focus-visible:outline-none focus-visible:shadow-focus';\n\nconst underlinedLinkClassName =\n 'relative after:absolute after:-bottom-1.5 after:left-0 after:h-0.5 after:w-full after:origin-left after:scale-x-0 after:bg-action-primary after:transition-transform after:duration-200 after:ease-brand hover:after:scale-x-100 focus-visible:after:scale-x-100';\n\nconst mobileLinkClassName =\n 'flex min-h-12 items-center rounded-control px-3 text-label font-semibold text-content-secondary transition-colors duration-200 ease-brand hover:text-content-primary focus-visible:outline-none focus-visible:shadow-focus';\n\nexport function Navbar({\n actions,\n brand,\n items,\n layer = 'standard',\n linkTreatment = 'plain',\n linkWeight = 'semibold',\n mobileNavigation,\n navigationLabel,\n renderLink,\n width = 'content',\n}: NavbarProps) {\n const linkClassName = [\n baseLinkClassName,\n linkWeight === 'bold' ? 'font-bold' : 'font-semibold',\n linkTreatment === 'underlined' ? underlinedLinkClassName : '',\n ]\n .filter(Boolean)\n .join(' ');\n const containerWidthClassName = width === 'wide' ? 'max-w-7xl' : 'max-w-5xl';\n const layerClassName = layer === 'above' ? 'z-50' : 'z-40';\n const renderLinks = (className: string) =>\n items.map((item) => (\n <Fragment key={item.href}>\n {renderLink ? (\n renderLink({\n children: item.label,\n className,\n href: item.href,\n })\n ) : (\n <a className={className} href={item.href}>\n {item.label}\n </a>\n )}\n </Fragment>\n ));\n\n return (\n <header\n className={`sticky top-0 ${layerClassName} border-b border-border-subtle bg-surface-page/90 text-label font-semibold backdrop-blur-lg`}\n >\n <div\n className={`mx-auto flex min-h-20 ${containerWidthClassName} items-center justify-between gap-4 px-shell`}\n >\n <div className=\"shrink-0\">{brand}</div>\n <nav aria-label={navigationLabel} className=\"hidden items-center gap-6 lg:flex\">\n {renderLinks(linkClassName)}\n </nav>\n {actions}\n {mobileNavigation ? (\n <details className=\"relative md:hidden\">\n <summary\n aria-label={mobileNavigation.menuLabel}\n className=\"flex size-12 cursor-pointer list-none items-center justify-center rounded-control bg-action-primary text-content-inverse shadow-card transition-[translate,scale,background-color] duration-200 ease-brand active:scale-[0.97] focus-visible:outline-none focus-visible:shadow-focus [&::-webkit-details-marker]:hidden\"\n >\n <span className=\"sr-only\">{mobileNavigation.menuLabel}</span>\n <LuMenu aria-hidden=\"true\" className=\"size-6\" />\n </summary>\n <nav\n aria-label={mobileNavigation.navigationLabel}\n className=\"absolute top-14 right-0 w-72 rounded-card border border-border-subtle bg-surface-raised p-3 shadow-float\"\n >\n {renderLinks(mobileLinkClassName)}\n {mobileNavigation.actions ? (\n <div className=\"mt-2 space-y-3 border-t border-border-subtle pt-3\">\n {mobileNavigation.actions}\n </div>\n ) : null}\n </nav>\n </details>\n ) : null}\n </div>\n </header>\n );\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../src/components/google-sign-in-button/google-sign-in-button.tsx","../src/components/account-control/account-control.tsx","../src/utils/get-initials.ts","../src/components/selector/selector.tsx","../src/components/navbar/navbar.tsx"],"sourcesContent":["import type {ButtonHTMLAttributes, ReactNode} from 'react';\nimport {FcGoogle} from 'react-icons/fc';\n\nexport type GoogleSignInButtonProps = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n 'children' | 'className' | 'type'\n> & {\n label: ReactNode;\n};\n\nconst buttonClassName =\n 'inline-flex min-h-12 w-full cursor-pointer items-center justify-center gap-3 rounded-control border border-border-subtle bg-surface-raised px-control-x py-control-y text-label font-semibold whitespace-nowrap text-content-primary shadow-card transition-[translate,scale,background-color] duration-200 ease-brand hover:-translate-y-0.5 hover:bg-brand-cloud active:scale-[0.97] focus-visible:outline-none focus-visible:shadow-focus disabled:cursor-wait disabled:opacity-70 motion-reduce:transform-none';\n\nexport function GoogleSignInButton(\n {label, ...buttonProps}: GoogleSignInButtonProps,\n) {\n return (\n <button {...buttonProps} className={buttonClassName} type=\"button\">\n <FcGoogle\n aria-hidden=\"true\"\n className=\"size-5 shrink-0\"\n data-testid=\"google-mark\"\n />\n {label}\n </button>\n );\n}\n","import {LuLogOut} from 'react-icons/lu';\nimport {getInitials} from '../../utils/get-initials';\nimport {Selector} from '../selector/selector';\n\nexport type AccountControlProps = {\n email?: string | null;\n fallbackIdentity: string;\n menuAriaLabel: string;\n menuText: string;\n name?: string | null;\n onSignOut: () => void;\n signOutAriaLabel: string;\n signOutText: string;\n};\n\nexport function AccountControl({\n email,\n fallbackIdentity,\n menuAriaLabel,\n menuText,\n name,\n onSignOut,\n signOutAriaLabel,\n signOutText,\n}: AccountControlProps) {\n const normalizedName = name?.trim();\n const identity = normalizedName || email || fallbackIdentity;\n const initials = getInitials(identity);\n const showEmail = Boolean(email && email !== identity);\n const options = [\n {label: identity, value: 'account'},\n {label: signOutText, value: 'sign-out'},\n ];\n\n return (\n <Selector\n label={menuText}\n onValueChange={(value) => {\n if (value === 'sign-out') {\n onSignOut();\n }\n }}\n options={options}\n renderOption={({option}) => option.value === 'account' ? (\n <span data-account-identity className=\"flex min-w-0 flex-col items-start py-1\">\n <span\n className=\"w-full truncate text-ui font-semibold text-content-primary\"\n title={identity}\n >\n {identity}\n </span>\n {showEmail ? (\n <span\n className=\"mt-1 w-full truncate text-label text-content-secondary\"\n title={email ?? undefined}\n >\n {email}\n </span>\n ) : null}\n </span>\n ) : (\n <span className=\"flex items-center gap-3\">\n <LuLogOut aria-hidden=\"true\" className=\"size-5 shrink-0\" />\n <span aria-hidden=\"true\">{option.label}</span>\n <span className=\"sr-only\">{signOutAriaLabel}</span>\n </span>\n )}\n renderTrigger={() => (\n <span className=\"flex min-w-0 items-center gap-3\">\n <span\n aria-hidden=\"true\"\n className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-brand-yellow font-data text-label font-semibold text-brand-navy-dark\"\n >\n {initials}\n </span>\n <span className=\"text-label font-semibold md:hidden\">{menuText}</span>\n </span>\n )}\n triggerAriaLabel={`${initials} ${menuAriaLabel}`}\n value=\"account\"\n />\n );\n}\n","export function getInitials(value: string) {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const selected = parts.length > 1 ? [parts[0], parts.at(-1)!] : parts;\n\n return (\n selected\n .map((part) => Array.from(part)[0])\n .join('')\n .toUpperCase()\n .slice(0, 2) || '1'\n );\n}\n","import {type ReactNode, useEffect, useId, useRef, useState} from 'react';\nimport {LuChevronDown} from 'react-icons/lu';\n\nexport type SelectorOption = {\n label: ReactNode;\n value: string;\n};\n\nexport type SelectorDensity = 'compact' | 'compact-mobile' | 'standard';\n\nexport type SelectorRenderOptionContext<TOption extends SelectorOption> = {\n isSelected: boolean;\n option: TOption;\n select: () => void;\n};\n\nexport type SelectorRenderTriggerContext<TOption extends SelectorOption> = {\n selectedOption: TOption | undefined;\n value: string;\n};\n\ntype SelectorBaseProps = {\n label: string;\n triggerAriaLabel?: string;\n value: string;\n};\n\nexport type SelectorProps<TOption extends SelectorOption = SelectorOption> =\n SelectorBaseProps & {\n density?: SelectorDensity;\n onValueChange: (value: string) => void;\n options: readonly TOption[];\n renderOption?: (context: SelectorRenderOptionContext<TOption>) => ReactNode;\n renderTrigger?: (\n context: SelectorRenderTriggerContext<TOption>,\n ) => ReactNode;\n whiteSurface?: boolean;\n };\n\nconst standardTriggerClassName =\n 'flex w-full cursor-pointer select-none items-center justify-between rounded-control border border-border-subtle py-control-y text-label font-semibold text-content-primary shadow-card transition-colors duration-200 ease-brand hover:bg-brand-cloud focus-visible:outline-none focus-visible:shadow-focus';\n\nconst optionClassName =\n 'flex min-h-12 w-full select-none items-center rounded-control px-3 text-label font-semibold text-content-secondary transition-colors duration-200 ease-brand hover:bg-brand-cloud hover:text-content-primary focus-visible:outline-none focus-visible:shadow-focus';\n\nfunction getNavigationLayout(density: SelectorDensity) {\n if (density === 'compact') {\n return {trigger: 'h-12 gap-2', wrapper: 'w-auto'};\n }\n\n if (density === 'compact-mobile') {\n return {\n trigger: 'h-12 gap-2 px-3 md:gap-4 md:px-4 md:min-w-32',\n wrapper: 'w-auto',\n };\n }\n\n return {trigger: 'min-h-12 gap-4 px-4 md:min-w-32', wrapper: 'w-full md:w-auto'};\n}\n\nfunction SelectorTrigger<TOption extends SelectorOption>({\n densityClassName,\n isOpen,\n onClick,\n optionListId,\n renderTrigger,\n selectedOption,\n triggerAriaLabel,\n value,\n}: {\n densityClassName: string;\n isOpen: boolean;\n onClick: () => void;\n optionListId: string;\n renderTrigger?: (context: SelectorRenderTriggerContext<TOption>) => ReactNode;\n selectedOption: TOption | undefined;\n triggerAriaLabel?: string;\n value: string;\n}) {\n const triggerContent = renderTrigger?.({selectedOption, value}) ??\n selectedOption?.label ?? value;\n\n return (\n <button\n aria-controls={optionListId}\n aria-expanded={isOpen}\n aria-label={triggerAriaLabel}\n className={`${standardTriggerClassName} ${densityClassName}`}\n onClick={onClick}\n type=\"button\"\n >\n <span>{triggerContent}</span>\n <LuChevronDown\n aria-hidden=\"true\"\n className={`selector-disclosure-arrow size-5 shrink-0 text-content-secondary transition-transform duration-200 ease-brand motion-reduce:transition-none ${isOpen ? 'rotate-180' : ''}`}\n />\n </button>\n );\n}\n\nexport function Selector<TOption extends SelectorOption = SelectorOption>({\n density = 'standard',\n label,\n onValueChange,\n options,\n renderOption,\n renderTrigger,\n triggerAriaLabel,\n value,\n whiteSurface = false,\n}: SelectorProps<TOption>) {\n const [isOpen, setIsOpen] = useState(false);\n const [selectionRequest, setSelectionRequest] =\n useState<{value: string} | null>(null);\n const optionListId = useId();\n const notifiedSelectionReference = useRef<{value: string} | null>(null);\n const onValueChangeReference = useRef(onValueChange);\n const selectorReference = useRef<HTMLDivElement>(null);\n const selectedOption = options.find((option) => option.value === value);\n const layout = getNavigationLayout(density);\n const surfaceClassName = whiteSurface ? 'bg-white' : 'bg-surface-raised';\n\n useEffect(() => {\n if (!isOpen) {\n return;\n }\n\n const closeWhenPointerStartsOutside = (event: PointerEvent) => {\n const selectorElement = selectorReference.current;\n\n if (\n !selectorElement ||\n !(event.target instanceof Node) ||\n selectorElement.contains(event.target)\n ) {\n return;\n }\n\n setIsOpen(false);\n };\n\n document.addEventListener('pointerdown', closeWhenPointerStartsOutside, true);\n\n return () => {\n document.removeEventListener('pointerdown', closeWhenPointerStartsOutside, true);\n };\n }, [isOpen]);\n\n useEffect(() => {\n onValueChangeReference.current = onValueChange;\n }, [onValueChange]);\n\n useEffect(() => {\n if (\n isOpen ||\n selectionRequest === null ||\n selectionRequest === notifiedSelectionReference.current\n ) {\n return;\n }\n\n notifiedSelectionReference.current = selectionRequest;\n onValueChangeReference.current(selectionRequest.value);\n }, [isOpen, selectionRequest]);\n\n const selectOption = (nextValue: string) => {\n setIsOpen(false);\n setSelectionRequest({value: nextValue});\n };\n\n return (\n <div ref={selectorReference} className={`selector-disclosure relative ${layout.wrapper}`}>\n <SelectorTrigger\n densityClassName={layout.trigger}\n isOpen={isOpen}\n onClick={() => setIsOpen((currentOpen) => !currentOpen)}\n optionListId={optionListId}\n renderTrigger={renderTrigger}\n selectedOption={selectedOption}\n triggerAriaLabel={triggerAriaLabel}\n value={value}\n />\n {isOpen ? (\n <ul\n aria-label={label}\n className={`mt-2 w-full overflow-hidden rounded-control border border-border-subtle ${surfaceClassName} p-1 shadow-float md:absolute md:top-full md:right-0 md:z-60 md:min-w-44`}\n id={optionListId}\n >\n {options.map((option) => {\n const isSelected = option.value === value;\n const select = () => selectOption(option.value);\n const optionContent =\n renderOption?.({isSelected, option, select}) ?? option.label;\n\n return (\n <li key={option.value}>\n <button\n aria-pressed={isSelected}\n className={`${optionClassName} text-left ${isSelected ? 'bg-brand-cloud text-content-primary' : ''}`}\n onClick={select}\n type=\"button\"\n >\n {optionContent}\n </button>\n </li>\n );\n })}\n </ul>\n ) : null}\n </div>\n );\n}\n","import {Fragment, type ReactNode, useId} from 'react';\nimport {LuMenu} from 'react-icons/lu';\n\nexport type NavbarItem = {\n href: string;\n label: ReactNode;\n};\n\nexport type NavbarLinkRenderer = (props: {\n children: ReactNode;\n className: string;\n href: string;\n}) => ReactNode;\n\nexport type NavbarWidth = 'content' | 'wide';\nexport type NavbarLinkTreatment = 'plain' | 'underlined';\nexport type NavbarLinkWeight = 'bold' | 'semibold';\nexport type NavbarLayer = 'above' | 'standard';\nexport type NavbarMobileNavigation = {\n actions?: ReactNode;\n menuLabel: string;\n navigationLabel: string;\n};\n\nexport type NavbarProps = {\n actions?: ReactNode;\n brand: ReactNode;\n items: readonly NavbarItem[];\n layer?: NavbarLayer;\n linkTreatment?: NavbarLinkTreatment;\n linkWeight?: NavbarLinkWeight;\n mobileNavigation?: NavbarMobileNavigation;\n navigationLabel: string;\n renderLink?: NavbarLinkRenderer;\n width?: NavbarWidth;\n};\n\nconst baseLinkClassName =\n 'text-label text-content-secondary transition-colors duration-200 ease-brand hover:text-content-primary focus-visible:rounded-control focus-visible:outline-none focus-visible:shadow-focus';\n\nconst underlinedLinkClassName =\n 'relative after:absolute after:-bottom-1.5 after:left-0 after:h-0.5 after:w-full after:origin-left after:scale-x-0 after:bg-action-primary after:transition-transform after:duration-200 after:ease-brand hover:after:scale-x-100 focus-visible:after:scale-x-100';\n\nconst mobileLinkClassName =\n 'flex min-h-12 items-center rounded-control px-3 text-label font-semibold text-content-secondary transition-colors duration-200 ease-brand hover:text-content-primary focus-visible:outline-none focus-visible:shadow-focus';\n\nexport function Navbar({\n actions,\n brand,\n items,\n layer = 'standard',\n linkTreatment = 'plain',\n linkWeight = 'semibold',\n mobileNavigation,\n navigationLabel,\n renderLink,\n width = 'content',\n}: NavbarProps) {\n const mobileNavigationReference = useId();\n const mobileNavigationId = `navbar-mobile-navigation-${mobileNavigationReference}`;\n const mobileNavigationAnchorName = `--navbar-mobile-menu-trigger-${mobileNavigationReference.replace(/[^a-zA-Z0-9_-]/g, '-')}`;\n const linkClassName = [\n baseLinkClassName,\n linkWeight === 'bold' ? 'font-bold' : 'font-semibold',\n linkTreatment === 'underlined' ? underlinedLinkClassName : '',\n ]\n .filter(Boolean)\n .join(' ');\n const containerWidthClassName = width === 'wide' ? 'max-w-7xl' : 'max-w-5xl';\n const layerClassName = layer === 'above' ? 'z-50' : 'z-40';\n const renderLinks = (className: string) =>\n items.map((item) => (\n <Fragment key={item.href}>\n {renderLink ? (\n renderLink({\n children: item.label,\n className,\n href: item.href,\n })\n ) : (\n <a className={className} href={item.href}>\n {item.label}\n </a>\n )}\n </Fragment>\n ));\n\n return (\n <header\n className={`sticky top-0 ${layerClassName} border-b border-border-subtle bg-surface-page/90 text-label font-semibold backdrop-blur-lg`}\n >\n <div\n className={`mx-auto flex min-h-20 ${containerWidthClassName} items-center justify-between gap-4 px-shell`}\n >\n <div className=\"shrink-0\">{brand}</div>\n <nav aria-label={navigationLabel} className=\"hidden items-center gap-6 lg:flex\">\n {renderLinks(linkClassName)}\n </nav>\n {actions}\n {mobileNavigation ? (\n <div className=\"relative md:hidden\">\n <button\n aria-controls={mobileNavigationId}\n aria-label={mobileNavigation.menuLabel}\n className=\"flex size-12 cursor-pointer items-center justify-center rounded-control bg-action-primary text-content-inverse shadow-card transition-[translate,scale,background-color] duration-200 ease-brand active:scale-[0.97] focus-visible:outline-none focus-visible:shadow-focus\"\n popoverTarget={mobileNavigationId}\n popoverTargetAction=\"toggle\"\n style={{anchorName: mobileNavigationAnchorName}}\n type=\"button\"\n >\n <span className=\"sr-only\">{mobileNavigation.menuLabel}</span>\n <LuMenu aria-hidden=\"true\" className=\"size-6\" />\n </button>\n <nav\n aria-label={mobileNavigation.navigationLabel}\n className=\"fixed top-[4.5rem] right-[1.25rem] m-0 w-72 rounded-card border border-border-subtle bg-surface-raised p-3 shadow-float [@supports(position-anchor:--navbar-mobile-menu-trigger)]:absolute [@supports(position-anchor:--navbar-mobile-menu-trigger)]:top-[calc(anchor(bottom)+0.5rem)] [@supports(position-anchor:--navbar-mobile-menu-trigger)]:right-auto [@supports(position-anchor:--navbar-mobile-menu-trigger)]:left-[anchor(right)] [@supports(position-anchor:--navbar-mobile-menu-trigger)]:-translate-x-full\"\n id={mobileNavigationId}\n popover=\"auto\"\n style={{positionAnchor: mobileNavigationAnchorName}}\n >\n {renderLinks(mobileLinkClassName)}\n {mobileNavigation.actions ? (\n <div className=\"mt-2 space-y-3 border-t border-border-subtle pt-3\">\n {mobileNavigation.actions}\n </div>\n ) : null}\n </nav>\n </div>\n ) : null}\n </div>\n </header>\n );\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs","useId","jsx","jsxs"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1e99/components",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Reusable React components built with Tailwind CSS.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"prepare": "husky",
|
|
31
31
|
"typecheck": "tsc --noEmit",
|
|
32
32
|
"test": "vitest run --project unit",
|
|
33
|
+
"test:utils": "jest",
|
|
33
34
|
"test:watch": "vitest --project unit",
|
|
34
35
|
"storybook": "storybook dev -p 6006",
|
|
35
36
|
"build-storybook": "storybook build",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@eslint/js": "^9.39.5",
|
|
48
|
+
"@jest/globals": "30.5.2",
|
|
47
49
|
"@storybook/addon-a11y": "^10.6.3",
|
|
48
50
|
"@storybook/addon-vitest": "^10.6.3",
|
|
49
51
|
"@storybook/react-vite": "^10.6.3",
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
63
65
|
"globals": "^16.5.0",
|
|
64
66
|
"husky": "^9.1.7",
|
|
67
|
+
"jest": "30.5.2",
|
|
65
68
|
"jsdom": "^28.0.0",
|
|
66
69
|
"playwright": "1.64.1",
|
|
67
70
|
"react": "^19.2.4",
|
|
@@ -69,6 +72,7 @@
|
|
|
69
72
|
"react-icons": "5.7.0",
|
|
70
73
|
"storybook": "^10.6.3",
|
|
71
74
|
"tailwindcss": "^4.3.3",
|
|
75
|
+
"ts-jest": "29.4.15",
|
|
72
76
|
"tsup": "^9.0.0",
|
|
73
77
|
"typescript": "^5.9.3",
|
|
74
78
|
"typescript-eslint": "^8.72.0",
|