@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 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;
@@ -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 jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
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__ */ jsx3(Fragment, { children: renderLink ? renderLink({
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__ */ jsx3("a", { className, href: item.href, children: item.label }) }, item.href));
191
- return /* @__PURE__ */ jsx3(
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__ */ jsxs3(
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__ */ jsx3("div", { className: "shrink-0", children: brand }),
201
- /* @__PURE__ */ jsx3("nav", { "aria-label": navigationLabel, className: "hidden items-center gap-6 lg:flex", children: renderLinks(linkClassName) }),
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__ */ jsxs3("details", { className: "relative md:hidden", children: [
204
- /* @__PURE__ */ jsxs3(
205
- "summary",
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 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",
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__ */ jsx3("span", { className: "sr-only", children: mobileNavigation.menuLabel }),
211
- /* @__PURE__ */ jsx3(LuMenu, { "aria-hidden": "true", className: "size-6" })
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__ */ jsxs3(
302
+ /* @__PURE__ */ jsxs4(
216
303
  "nav",
217
304
  {
218
305
  "aria-label": mobileNavigation.navigationLabel,
219
- className: "absolute top-14 right-0 w-72 rounded-card border border-border-subtle bg-surface-raised p-3 shadow-float",
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__ */ jsx3("div", { className: "mt-2 space-y-3 border-t border-border-subtle pt-3", children: mobileNavigation.actions }) : null
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",
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",