@1e99/components 0.1.4 → 0.2.2

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 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. As stories são
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;
@@ -22,7 +36,9 @@ type SelectorRenderTriggerContext<TOption extends SelectorOption> = {
22
36
  };
23
37
  type SelectorBaseProps = {
24
38
  label: string;
39
+ showTriggerIndicator?: boolean;
25
40
  triggerAriaLabel?: string;
41
+ triggerClassName?: string;
26
42
  value: string;
27
43
  };
28
44
  type SelectorProps<TOption extends SelectorOption = SelectorOption> = SelectorBaseProps & {
@@ -33,7 +49,7 @@ type SelectorProps<TOption extends SelectorOption = SelectorOption> = SelectorBa
33
49
  renderTrigger?: (context: SelectorRenderTriggerContext<TOption>) => ReactNode;
34
50
  whiteSurface?: boolean;
35
51
  };
36
- declare function Selector<TOption extends SelectorOption = SelectorOption>({ density, label, onValueChange, options, renderOption, renderTrigger, triggerAriaLabel, value, whiteSurface, }: SelectorProps<TOption>): react.JSX.Element;
52
+ declare function Selector<TOption extends SelectorOption = SelectorOption>({ density, label, onValueChange, options, renderOption, renderTrigger, showTriggerIndicator, triggerAriaLabel, triggerClassName, value, whiteSurface, }: SelectorProps<TOption>): react.JSX.Element;
37
53
 
38
54
  type NavbarItem = {
39
55
  href: string;
@@ -67,5 +83,5 @@ type NavbarProps = {
67
83
  };
68
84
  declare function Navbar({ actions, brand, items, layer, linkTreatment, linkWeight, mobileNavigation, navigationLabel, renderLink, width, }: NavbarProps): react.JSX.Element;
69
85
 
70
- export { GoogleSignInButton, Navbar, Selector };
71
- export type { GoogleSignInButtonProps, NavbarItem, NavbarLayer, NavbarLinkRenderer, NavbarLinkTreatment, NavbarLinkWeight, NavbarMobileNavigation, NavbarProps, NavbarWidth, SelectorDensity, SelectorOption, SelectorProps, SelectorRenderOptionContext, SelectorRenderTriggerContext };
86
+ export { AccountControl, GoogleSignInButton, Navbar, Selector, getInitials };
87
+ 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";
@@ -41,7 +51,9 @@ function SelectorTrigger({
41
51
  optionListId,
42
52
  renderTrigger,
43
53
  selectedOption,
54
+ showTriggerIndicator,
44
55
  triggerAriaLabel,
56
+ triggerClassName,
45
57
  value
46
58
  }) {
47
59
  const triggerContent = renderTrigger?.({ selectedOption, value }) ?? selectedOption?.label ?? value;
@@ -51,18 +63,18 @@ function SelectorTrigger({
51
63
  "aria-controls": optionListId,
52
64
  "aria-expanded": isOpen,
53
65
  "aria-label": triggerAriaLabel,
54
- className: `${standardTriggerClassName} ${densityClassName}`,
66
+ className: triggerClassName ?? `${standardTriggerClassName} ${densityClassName}`,
55
67
  onClick,
56
68
  type: "button",
57
69
  children: [
58
70
  /* @__PURE__ */ jsx2("span", { children: triggerContent }),
59
- /* @__PURE__ */ jsx2(
71
+ showTriggerIndicator ? /* @__PURE__ */ jsx2(
60
72
  LuChevronDown,
61
73
  {
62
74
  "aria-hidden": "true",
63
75
  className: `selector-disclosure-arrow size-5 shrink-0 text-content-secondary transition-transform duration-200 ease-brand motion-reduce:transition-none ${isOpen ? "rotate-180" : ""}`
64
76
  }
65
- )
77
+ ) : null
66
78
  ]
67
79
  }
68
80
  );
@@ -74,7 +86,9 @@ function Selector({
74
86
  options,
75
87
  renderOption,
76
88
  renderTrigger,
89
+ showTriggerIndicator = true,
77
90
  triggerAriaLabel,
91
+ triggerClassName,
78
92
  value,
79
93
  whiteSurface = false
80
94
  }) {
@@ -127,7 +141,9 @@ function Selector({
127
141
  optionListId,
128
142
  renderTrigger,
129
143
  selectedOption,
144
+ showTriggerIndicator,
130
145
  triggerAriaLabel,
146
+ triggerClassName,
131
147
  value
132
148
  }
133
149
  ),
@@ -157,10 +173,71 @@ function Selector({
157
173
  ] });
158
174
  }
159
175
 
176
+ // src/components/account-control/account-control.tsx
177
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
178
+ function AccountControl({
179
+ email,
180
+ fallbackIdentity,
181
+ menuAriaLabel,
182
+ menuText,
183
+ name,
184
+ onSignOut,
185
+ signOutAriaLabel,
186
+ signOutText
187
+ }) {
188
+ const normalizedName = name?.trim();
189
+ const identity = normalizedName || email || fallbackIdentity;
190
+ const initials = getInitials(identity);
191
+ const showEmail = Boolean(email && email !== identity);
192
+ const options = [
193
+ { label: identity, value: "account" },
194
+ { label: signOutText, value: "sign-out" }
195
+ ];
196
+ return /* @__PURE__ */ jsx3(
197
+ Selector,
198
+ {
199
+ label: menuText,
200
+ onValueChange: (value) => {
201
+ if (value === "sign-out") {
202
+ onSignOut();
203
+ }
204
+ },
205
+ options,
206
+ renderOption: ({ option }) => option.value === "account" ? /* @__PURE__ */ jsxs3("span", { "data-account-identity": true, className: "flex min-w-0 flex-col items-start py-1", children: [
207
+ /* @__PURE__ */ jsx3(
208
+ "span",
209
+ {
210
+ className: "w-full break-words text-ui font-semibold text-content-primary",
211
+ title: identity,
212
+ children: identity
213
+ }
214
+ ),
215
+ showEmail ? /* @__PURE__ */ jsx3(
216
+ "span",
217
+ {
218
+ className: "mt-1 w-full break-all text-label text-content-secondary",
219
+ title: email ?? void 0,
220
+ children: email
221
+ }
222
+ ) : null
223
+ ] }) : /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-3", children: [
224
+ /* @__PURE__ */ jsx3(LuLogOut, { "aria-hidden": "true", className: "size-5 shrink-0" }),
225
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: option.label }),
226
+ /* @__PURE__ */ jsx3("span", { className: "sr-only", children: signOutAriaLabel })
227
+ ] }),
228
+ renderTrigger: () => /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: initials }),
229
+ showTriggerIndicator: false,
230
+ triggerAriaLabel: `${initials} ${menuAriaLabel}`,
231
+ triggerClassName: "flex size-10 cursor-pointer select-none items-center justify-center rounded-full bg-brand-yellow font-data text-label font-semibold text-brand-navy-dark transition-colors duration-200 ease-brand hover:bg-brand-yellow/85 focus-visible:outline-none focus-visible:shadow-focus",
232
+ value: "account"
233
+ }
234
+ );
235
+ }
236
+
160
237
  // src/components/navbar/navbar.tsx
161
238
  import { Fragment, useId as useId2 } from "react";
162
239
  import { LuMenu } from "react-icons/lu";
163
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
240
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
164
241
  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
242
  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
243
  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";
@@ -186,25 +263,25 @@ function Navbar({
186
263
  ].filter(Boolean).join(" ");
187
264
  const containerWidthClassName = width === "wide" ? "max-w-7xl" : "max-w-5xl";
188
265
  const layerClassName = layer === "above" ? "z-50" : "z-40";
189
- const renderLinks = (className) => items.map((item) => /* @__PURE__ */ jsx3(Fragment, { children: renderLink ? renderLink({
266
+ const renderLinks = (className) => items.map((item) => /* @__PURE__ */ jsx4(Fragment, { children: renderLink ? renderLink({
190
267
  children: item.label,
191
268
  className,
192
269
  href: item.href
193
- }) : /* @__PURE__ */ jsx3("a", { className, href: item.href, children: item.label }) }, item.href));
194
- return /* @__PURE__ */ jsx3(
270
+ }) : /* @__PURE__ */ jsx4("a", { className, href: item.href, children: item.label }) }, item.href));
271
+ return /* @__PURE__ */ jsx4(
195
272
  "header",
196
273
  {
197
274
  className: `sticky top-0 ${layerClassName} border-b border-border-subtle bg-surface-page/90 text-label font-semibold backdrop-blur-lg`,
198
- children: /* @__PURE__ */ jsxs3(
275
+ children: /* @__PURE__ */ jsxs4(
199
276
  "div",
200
277
  {
201
278
  className: `mx-auto flex min-h-20 ${containerWidthClassName} items-center justify-between gap-4 px-shell`,
202
279
  children: [
203
- /* @__PURE__ */ jsx3("div", { className: "shrink-0", children: brand }),
204
- /* @__PURE__ */ jsx3("nav", { "aria-label": navigationLabel, className: "hidden items-center gap-6 lg:flex", children: renderLinks(linkClassName) }),
280
+ /* @__PURE__ */ jsx4("div", { className: "shrink-0", children: brand }),
281
+ /* @__PURE__ */ jsx4("nav", { "aria-label": navigationLabel, className: "hidden items-center gap-6 lg:flex", children: renderLinks(linkClassName) }),
205
282
  actions,
206
- mobileNavigation ? /* @__PURE__ */ jsxs3("div", { className: "relative md:hidden", children: [
207
- /* @__PURE__ */ jsxs3(
283
+ mobileNavigation ? /* @__PURE__ */ jsxs4("div", { className: "relative md:hidden", children: [
284
+ /* @__PURE__ */ jsxs4(
208
285
  "button",
209
286
  {
210
287
  "aria-controls": mobileNavigationId,
@@ -215,12 +292,12 @@ function Navbar({
215
292
  style: { anchorName: mobileNavigationAnchorName },
216
293
  type: "button",
217
294
  children: [
218
- /* @__PURE__ */ jsx3("span", { className: "sr-only", children: mobileNavigation.menuLabel }),
219
- /* @__PURE__ */ jsx3(LuMenu, { "aria-hidden": "true", className: "size-6" })
295
+ /* @__PURE__ */ jsx4("span", { className: "sr-only", children: mobileNavigation.menuLabel }),
296
+ /* @__PURE__ */ jsx4(LuMenu, { "aria-hidden": "true", className: "size-6" })
220
297
  ]
221
298
  }
222
299
  ),
223
- /* @__PURE__ */ jsxs3(
300
+ /* @__PURE__ */ jsxs4(
224
301
  "nav",
225
302
  {
226
303
  "aria-label": mobileNavigation.navigationLabel,
@@ -230,7 +307,7 @@ function Navbar({
230
307
  style: { positionAnchor: mobileNavigationAnchorName },
231
308
  children: [
232
309
  renderLinks(mobileLinkClassName),
233
- mobileNavigation.actions ? /* @__PURE__ */ jsx3("div", { className: "mt-2 space-y-3 border-t border-border-subtle pt-3", children: mobileNavigation.actions }) : null
310
+ mobileNavigation.actions ? /* @__PURE__ */ jsx4("div", { className: "mt-2 space-y-3 border-t border-border-subtle pt-3", children: mobileNavigation.actions }) : null
234
311
  ]
235
312
  }
236
313
  )
@@ -242,8 +319,10 @@ function Navbar({
242
319
  );
243
320
  }
244
321
  export {
322
+ AccountControl,
245
323
  GoogleSignInButton,
246
324
  Navbar,
247
- Selector
325
+ Selector,
326
+ getInitials
248
327
  };
249
328
  //# 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, 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":";AACA,SAAQ,gBAAe;AAgBnB,SACE,KADF;AAPJ,IAAM,kBACJ;AAEK,SAAS,mBACZ,EAAC,OAAO,GAAG,YAAW,GACxB;AACA,SACE,qBAAC,YAAQ,GAAG,aAAa,WAAW,iBAAiB,MAAK,UACxD;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAU;AAAA,QACV,eAAY;AAAA;AAAA,IACd;AAAA,IACC;AAAA,KACH;AAEJ;;;AC1BA,SAAwB,WAAW,OAAO,QAAQ,gBAAe;AACjE,SAAQ,qBAAoB;AAkFxB,SAQE,OAAAA,MARF,QAAAC,aAAA;AA5CJ,IAAM,2BACJ;AAEF,IAAM,kBACJ;AAEF,SAAS,oBAAoB,SAA0B;AACrD,MAAI,YAAY,WAAW;AACzB,WAAO,EAAC,SAAS,cAAc,SAAS,SAAQ;AAAA,EAClD;AAEA,MAAI,YAAY,kBAAkB;AAChC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,IACX;AAAA,EACF;AAEA,SAAO,EAAC,SAAS,mCAAmC,SAAS,mBAAkB;AACjF;AAEA,SAAS,gBAAgD;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AACD,QAAM,iBAAiB,gBAAgB,EAAC,gBAAgB,MAAK,CAAC,KAC5D,gBAAgB,SAAS;AAE3B,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,iBAAe;AAAA,MACf,iBAAe;AAAA,MACf,cAAY;AAAA,MACZ,WAAW,GAAG,wBAAwB,IAAI,gBAAgB;AAAA,MAC1D;AAAA,MACA,MAAK;AAAA,MAEL;AAAA,wBAAAD,KAAC,UAAM,0BAAe;AAAA,QACtB,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW,+IAA+I,SAAS,eAAe,EAAE;AAAA;AAAA,QACtL;AAAA;AAAA;AAAA,EACF;AAEJ;AAEO,SAAS,SAA0D;AAAA,EACxE,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AACjB,GAA2B;AACzB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,kBAAkB,mBAAmB,IAC1C,SAAiC,IAAI;AACvC,QAAM,eAAe,MAAM;AAC3B,QAAM,6BAA6B,OAA+B,IAAI;AACtE,QAAM,yBAAyB,OAAO,aAAa;AACnD,QAAM,oBAAoB,OAAuB,IAAI;AACrD,QAAM,iBAAiB,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACtE,QAAM,SAAS,oBAAoB,OAAO;AAC1C,QAAM,mBAAmB,eAAe,aAAa;AAErD,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AAEA,UAAM,gCAAgC,CAAC,UAAwB;AAC7D,YAAM,kBAAkB,kBAAkB;AAE1C,UACE,CAAC,mBACD,EAAE,MAAM,kBAAkB,SAC1B,gBAAgB,SAAS,MAAM,MAAM,GACrC;AACA;AAAA,MACF;AAEA,gBAAU,KAAK;AAAA,IACjB;AAEA,aAAS,iBAAiB,eAAe,+BAA+B,IAAI;AAE5E,WAAO,MAAM;AACX,eAAS,oBAAoB,eAAe,+BAA+B,IAAI;AAAA,IACjF;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,YAAU,MAAM;AACd,2BAAuB,UAAU;AAAA,EACnC,GAAG,CAAC,aAAa,CAAC;AAElB,YAAU,MAAM;AACd,QACE,UACA,qBAAqB,QACrB,qBAAqB,2BAA2B,SAChD;AACA;AAAA,IACF;AAEA,+BAA2B,UAAU;AACrC,2BAAuB,QAAQ,iBAAiB,KAAK;AAAA,EACvD,GAAG,CAAC,QAAQ,gBAAgB,CAAC;AAE7B,QAAM,eAAe,CAAC,cAAsB;AAC1C,cAAU,KAAK;AACf,wBAAoB,EAAC,OAAO,UAAS,CAAC;AAAA,EACxC;AAEA,SACE,gBAAAC,MAAC,SAAI,KAAK,mBAAmB,WAAW,gCAAgC,OAAO,OAAO,IACpF;AAAA,oBAAAD;AAAA,MAAC;AAAA;AAAA,QACC,kBAAkB,OAAO;AAAA,QACzB;AAAA,QACA,SAAS,MAAM,UAAU,CAAC,gBAAgB,CAAC,WAAW;AAAA,QACtD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACC,SACC,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAY;AAAA,QACZ,WAAW,2EAA2E,gBAAgB;AAAA,QACtG,IAAI;AAAA,QAEH,kBAAQ,IAAI,CAAC,WAAW;AACvB,gBAAM,aAAa,OAAO,UAAU;AACpC,gBAAM,SAAS,MAAM,aAAa,OAAO,KAAK;AAC9C,gBAAM,gBACJ,eAAe,EAAC,YAAY,QAAQ,OAAM,CAAC,KAAK,OAAO;AAEzD,iBACE,gBAAAA,KAAC,QACC,0BAAAA;AAAA,YAAC;AAAA;AAAA,cACC,gBAAc;AAAA,cACd,WAAW,GAAG,eAAe,cAAc,aAAa,wCAAwC,EAAE;AAAA,cAClG,SAAS;AAAA,cACT,MAAK;AAAA,cAEJ;AAAA;AAAA,UACH,KARO,OAAO,KAShB;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH,IACE;AAAA,KACN;AAEJ;;;ACnNA,SAAQ,UAA0B,SAAAE,cAAY;AAC9C,SAAQ,cAAa;AA+EX,gBAAAC,MAqBE,QAAAC,aArBF;AA3CV,IAAM,oBACJ;AAEF,IAAM,0BACJ;AAEF,IAAM,sBACJ;AAEK,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAAgB;AACd,QAAM,4BAA4BF,OAAM;AACxC,QAAM,qBAAqB,4BAA4B,yBAAyB;AAChF,QAAM,6BAA6B,gCAAgC,0BAA0B,QAAQ,mBAAmB,GAAG,CAAC;AAC5H,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA,eAAe,SAAS,cAAc;AAAA,IACtC,kBAAkB,eAAe,0BAA0B;AAAA,EAC7D,EACK,OAAO,OAAO,EACd,KAAK,GAAG;AACb,QAAM,0BAA0B,UAAU,SAAS,cAAc;AACjE,QAAM,iBAAiB,UAAU,UAAU,SAAS;AACpD,QAAM,cAAc,CAAC,cACnB,MAAM,IAAI,CAAC,SACT,gBAAAC,KAAC,YACE,uBACC,WAAW;AAAA,IACT,UAAU,KAAK;AAAA,IACf;AAAA,IACA,MAAM,KAAK;AAAA,EACb,CAAC,IAED,gBAAAA,KAAC,OAAE,WAAsB,MAAM,KAAK,MACjC,eAAK,OACR,KAVW,KAAK,IAYpB,CACD;AAEH,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,gBAAgB,cAAc;AAAA,MAEzC,0BAAAC;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,yBAAyB,uBAAuB;AAAA,UAE3D;AAAA,4BAAAD,KAAC,SAAI,WAAU,YAAY,iBAAM;AAAA,YACjC,gBAAAA,KAAC,SAAI,cAAY,iBAAiB,WAAU,qCACzC,sBAAY,aAAa,GAC5B;AAAA,YACC;AAAA,YACA,mBACC,gBAAAC,MAAC,SAAI,WAAU,sBACb;AAAA,8BAAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,iBAAe;AAAA,kBACf,cAAY,iBAAiB;AAAA,kBAC7B,WAAU;AAAA,kBACV,eAAe;AAAA,kBACf,qBAAoB;AAAA,kBACpB,OAAO,EAAC,YAAY,2BAA0B;AAAA,kBAC9C,MAAK;AAAA,kBAEL;AAAA,oCAAAD,KAAC,UAAK,WAAU,WAAW,2BAAiB,WAAU;AAAA,oBACtD,gBAAAA,KAAC,UAAO,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA;AAAA,cAChD;AAAA,cACA,gBAAAC;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAY,iBAAiB;AAAA,kBAC7B,WAAU;AAAA,kBACV,IAAI;AAAA,kBACJ,SAAQ;AAAA,kBACR,OAAO,EAAC,gBAAgB,2BAA0B;AAAA,kBAEjD;AAAA,gCAAY,mBAAmB;AAAA,oBAC/B,iBAAiB,UAChB,gBAAAD,KAAC,SAAI,WAAU,qDACZ,2BAAiB,SACpB,IACE;AAAA;AAAA;AAAA,cACN;AAAA,eACF,IACE;AAAA;AAAA;AAAA,MACN;AAAA;AAAA,EACF;AAEJ;","names":["jsx","jsxs","useId","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 break-words 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 break-all 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 aria-hidden=\"true\">{initials}</span>\n )}\n showTriggerIndicator={false}\n triggerAriaLabel={`${initials} ${menuAriaLabel}`}\n triggerClassName=\"flex size-10 cursor-pointer select-none items-center justify-center rounded-full bg-brand-yellow font-data text-label font-semibold text-brand-navy-dark transition-colors duration-200 ease-brand hover:bg-brand-yellow/85 focus-visible:outline-none focus-visible:shadow-focus\"\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 showTriggerIndicator?: boolean;\n triggerAriaLabel?: string;\n triggerClassName?: 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 showTriggerIndicator,\n triggerAriaLabel,\n triggerClassName,\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 showTriggerIndicator: boolean;\n triggerAriaLabel?: string;\n triggerClassName?: 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={triggerClassName ?? `${standardTriggerClassName} ${densityClassName}`}\n onClick={onClick}\n type=\"button\"\n >\n <span>{triggerContent}</span>\n {showTriggerIndicator ? (\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 ) : null}\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 showTriggerIndicator = true,\n triggerAriaLabel,\n triggerClassName,\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 showTriggerIndicator={showTriggerIndicator}\n triggerAriaLabel={triggerAriaLabel}\n triggerClassName={triggerClassName}\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":";AACA,SAAQ,gBAAe;AAgBnB,SACE,KADF;AAPJ,IAAM,kBACJ;AAEK,SAAS,mBACZ,EAAC,OAAO,GAAG,YAAW,GACxB;AACA,SACE,qBAAC,YAAQ,GAAG,aAAa,WAAW,iBAAiB,MAAK,UACxD;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAU;AAAA,QACV,eAAY;AAAA;AAAA,IACd;AAAA,IACC;AAAA,KACH;AAEJ;;;AC1BA,SAAQ,gBAAe;;;ACAhB,SAAS,YAAY,OAAe;AACzC,QAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO;AACtD,QAAM,WAAW,MAAM,SAAS,IAAI,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,EAAE,CAAE,IAAI;AAEhE,SACE,SACK,IAAI,CAAC,SAAS,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC,EACjC,KAAK,EAAE,EACP,YAAY,EACZ,MAAM,GAAG,CAAC,KAAK;AAExB;;;ACXA,SAAwB,WAAW,OAAO,QAAQ,gBAAe;AACjE,SAAQ,qBAAoB;AAwFxB,SAQE,OAAAA,MARF,QAAAC,aAAA;AAhDJ,IAAM,2BACJ;AAEF,IAAM,kBACJ;AAEF,SAAS,oBAAoB,SAA0B;AACrD,MAAI,YAAY,WAAW;AACzB,WAAO,EAAC,SAAS,cAAc,SAAS,SAAQ;AAAA,EAClD;AAEA,MAAI,YAAY,kBAAkB;AAChC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS;AAAA,IACX;AAAA,EACF;AAEA,SAAO,EAAC,SAAS,mCAAmC,SAAS,mBAAkB;AACjF;AAEA,SAAS,gBAAgD;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAWG;AACD,QAAM,iBAAiB,gBAAgB,EAAC,gBAAgB,MAAK,CAAC,KAC5D,gBAAgB,SAAS;AAE3B,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,iBAAe;AAAA,MACf,iBAAe;AAAA,MACf,cAAY;AAAA,MACZ,WAAW,oBAAoB,GAAG,wBAAwB,IAAI,gBAAgB;AAAA,MAC9E;AAAA,MACA,MAAK;AAAA,MAEL;AAAA,wBAAAD,KAAC,UAAM,0BAAe;AAAA,QACrB,uBACC,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW,+IAA+I,SAAS,eAAe,EAAE;AAAA;AAAA,QACtL,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;AAEO,SAAS,SAA0D;AAAA,EACxE,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,uBAAuB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AACjB,GAA2B;AACzB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,kBAAkB,mBAAmB,IAC1C,SAAiC,IAAI;AACvC,QAAM,eAAe,MAAM;AAC3B,QAAM,6BAA6B,OAA+B,IAAI;AACtE,QAAM,yBAAyB,OAAO,aAAa;AACnD,QAAM,oBAAoB,OAAuB,IAAI;AACrD,QAAM,iBAAiB,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACtE,QAAM,SAAS,oBAAoB,OAAO;AAC1C,QAAM,mBAAmB,eAAe,aAAa;AAErD,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AAEA,UAAM,gCAAgC,CAAC,UAAwB;AAC7D,YAAM,kBAAkB,kBAAkB;AAE1C,UACE,CAAC,mBACD,EAAE,MAAM,kBAAkB,SAC1B,gBAAgB,SAAS,MAAM,MAAM,GACrC;AACA;AAAA,MACF;AAEA,gBAAU,KAAK;AAAA,IACjB;AAEA,aAAS,iBAAiB,eAAe,+BAA+B,IAAI;AAE5E,WAAO,MAAM;AACX,eAAS,oBAAoB,eAAe,+BAA+B,IAAI;AAAA,IACjF;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,YAAU,MAAM;AACd,2BAAuB,UAAU;AAAA,EACnC,GAAG,CAAC,aAAa,CAAC;AAElB,YAAU,MAAM;AACd,QACE,UACA,qBAAqB,QACrB,qBAAqB,2BAA2B,SAChD;AACA;AAAA,IACF;AAEA,+BAA2B,UAAU;AACrC,2BAAuB,QAAQ,iBAAiB,KAAK;AAAA,EACvD,GAAG,CAAC,QAAQ,gBAAgB,CAAC;AAE7B,QAAM,eAAe,CAAC,cAAsB;AAC1C,cAAU,KAAK;AACf,wBAAoB,EAAC,OAAO,UAAS,CAAC;AAAA,EACxC;AAEA,SACE,gBAAAC,MAAC,SAAI,KAAK,mBAAmB,WAAW,gCAAgC,OAAO,OAAO,IACpF;AAAA,oBAAAD;AAAA,MAAC;AAAA;AAAA,QACC,kBAAkB,OAAO;AAAA,QACzB;AAAA,QACA,SAAS,MAAM,UAAU,CAAC,gBAAgB,CAAC,WAAW;AAAA,QACtD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACC,SACC,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAY;AAAA,QACZ,WAAW,2EAA2E,gBAAgB;AAAA,QACtG,IAAI;AAAA,QAEH,kBAAQ,IAAI,CAAC,WAAW;AACvB,gBAAM,aAAa,OAAO,UAAU;AACpC,gBAAM,SAAS,MAAM,aAAa,OAAO,KAAK;AAC9C,gBAAM,gBACJ,eAAe,EAAC,YAAY,QAAQ,OAAM,CAAC,KAAK,OAAO;AAEzD,iBACE,gBAAAA,KAAC,QACC,0BAAAA;AAAA,YAAC;AAAA;AAAA,cACC,gBAAc;AAAA,cACd,WAAW,GAAG,eAAe,cAAc,aAAa,wCAAwC,EAAE;AAAA,cAClG,SAAS;AAAA,cACT,MAAK;AAAA,cAEJ;AAAA;AAAA,UACH,KARO,OAAO,KAShB;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH,IACE;AAAA,KACN;AAEJ;;;AFnLQ,SACE,OAAAE,MADF,QAAAC,aAAA;AA7BD,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,iBAAiB,MAAM,KAAK;AAClC,QAAM,WAAW,kBAAkB,SAAS;AAC5C,QAAM,WAAW,YAAY,QAAQ;AACrC,QAAM,YAAY,QAAQ,SAAS,UAAU,QAAQ;AACrD,QAAM,UAAU;AAAA,IACd,EAAC,OAAO,UAAU,OAAO,UAAS;AAAA,IAClC,EAAC,OAAO,aAAa,OAAO,WAAU;AAAA,EACxC;AAEA,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,MACP,eAAe,CAAC,UAAU;AACxB,YAAI,UAAU,YAAY;AACxB,oBAAU;AAAA,QACZ;AAAA,MACF;AAAA,MACA;AAAA,MACA,cAAc,CAAC,EAAC,OAAM,MAAM,OAAO,UAAU,YAC3C,gBAAAC,MAAC,UAAK,yBAAqB,MAAC,WAAU,0CACpC;AAAA,wBAAAD;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO;AAAA,YAEN;AAAA;AAAA,QACH;AAAA,QACC,YACC,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,SAAS;AAAA,YAEf;AAAA;AAAA,QACH,IACE;AAAA,SACN,IAEA,gBAAAC,MAAC,UAAK,WAAU,2BACd;AAAA,wBAAAD,KAAC,YAAS,eAAY,QAAO,WAAU,mBAAkB;AAAA,QACzD,gBAAAA,KAAC,UAAK,eAAY,QAAQ,iBAAO,OAAM;AAAA,QACvC,gBAAAA,KAAC,UAAK,WAAU,WAAW,4BAAiB;AAAA,SAC9C;AAAA,MAEF,eAAe,MACb,gBAAAA,KAAC,UAAK,eAAY,QAAQ,oBAAS;AAAA,MAErC,sBAAsB;AAAA,MACtB,kBAAkB,GAAG,QAAQ,IAAI,aAAa;AAAA,MAC9C,kBAAiB;AAAA,MACjB,OAAM;AAAA;AAAA,EACR;AAEJ;;;AG5EA,SAAQ,UAA0B,SAAAE,cAAY;AAC9C,SAAQ,cAAa;AA+EX,gBAAAC,MAqBE,QAAAC,aArBF;AA3CV,IAAM,oBACJ;AAEF,IAAM,0BACJ;AAEF,IAAM,sBACJ;AAEK,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAAgB;AACd,QAAM,4BAA4BF,OAAM;AACxC,QAAM,qBAAqB,4BAA4B,yBAAyB;AAChF,QAAM,6BAA6B,gCAAgC,0BAA0B,QAAQ,mBAAmB,GAAG,CAAC;AAC5H,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA,eAAe,SAAS,cAAc;AAAA,IACtC,kBAAkB,eAAe,0BAA0B;AAAA,EAC7D,EACK,OAAO,OAAO,EACd,KAAK,GAAG;AACb,QAAM,0BAA0B,UAAU,SAAS,cAAc;AACjE,QAAM,iBAAiB,UAAU,UAAU,SAAS;AACpD,QAAM,cAAc,CAAC,cACnB,MAAM,IAAI,CAAC,SACT,gBAAAC,KAAC,YACE,uBACC,WAAW;AAAA,IACT,UAAU,KAAK;AAAA,IACf;AAAA,IACA,MAAM,KAAK;AAAA,EACb,CAAC,IAED,gBAAAA,KAAC,OAAE,WAAsB,MAAM,KAAK,MACjC,eAAK,OACR,KAVW,KAAK,IAYpB,CACD;AAEH,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,gBAAgB,cAAc;AAAA,MAEzC,0BAAAC;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,yBAAyB,uBAAuB;AAAA,UAE3D;AAAA,4BAAAD,KAAC,SAAI,WAAU,YAAY,iBAAM;AAAA,YACjC,gBAAAA,KAAC,SAAI,cAAY,iBAAiB,WAAU,qCACzC,sBAAY,aAAa,GAC5B;AAAA,YACC;AAAA,YACA,mBACC,gBAAAC,MAAC,SAAI,WAAU,sBACb;AAAA,8BAAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,iBAAe;AAAA,kBACf,cAAY,iBAAiB;AAAA,kBAC7B,WAAU;AAAA,kBACV,eAAe;AAAA,kBACf,qBAAoB;AAAA,kBACpB,OAAO,EAAC,YAAY,2BAA0B;AAAA,kBAC9C,MAAK;AAAA,kBAEL;AAAA,oCAAAD,KAAC,UAAK,WAAU,WAAW,2BAAiB,WAAU;AAAA,oBACtD,gBAAAA,KAAC,UAAO,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA;AAAA,cAChD;AAAA,cACA,gBAAAC;AAAA,gBAAC;AAAA;AAAA,kBACC,cAAY,iBAAiB;AAAA,kBAC7B,WAAU;AAAA,kBACV,IAAI;AAAA,kBACJ,SAAQ;AAAA,kBACR,OAAO,EAAC,gBAAgB,2BAA0B;AAAA,kBAEjD;AAAA,gCAAY,mBAAmB;AAAA,oBAC/B,iBAAiB,UAChB,gBAAAD,KAAC,SAAI,WAAU,qDACZ,2BAAiB,SACpB,IACE;AAAA;AAAA;AAAA,cACN;AAAA,eACF,IACE;AAAA;AAAA;AAAA,MACN;AAAA;AAAA,EACF;AAEJ;","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.4",
3
+ "version": "0.2.2",
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",