@1e99/components 0.0.0-stage → 0.1.0

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
@@ -1,3 +1,261 @@
1
- # Temporary Holding Version
1
+ # @1e99/components
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Componentes React compartilhados pelos produtos 1e99. `Navbar`,
4
+ `GoogleSignInButton` e `Selector` foram extraídos fielmente de
5
+ `domain-website` e `curriculo`, sem introduzir dependência de Next.js,
6
+ autenticação, i18n ou geração de PDF.
7
+
8
+ ## Instalação futura
9
+
10
+ Após a primeira publicação pública, instale o pacote no app consumidor:
11
+
12
+ ```sh
13
+ pnpm add @1e99/components
14
+ ```
15
+
16
+ O pacote declara `react`, `react-dom`, `react-icons` e `tailwindcss` como
17
+ peer dependencies. Os apps 1e99 já usam `react-icons` 5.7.0; ele fornece os
18
+ ícones Google e Lucide que o pacote renderiza, sem duplicar SVGs manualmente.
19
+
20
+ ### Tailwind CSS 4
21
+
22
+ O Tailwind 4 não lê automaticamente classes presentes em `node_modules`.
23
+ Adicione esta diretiva à folha CSS que já contém `@import "tailwindcss"`, com
24
+ o caminho relativo àquela folha:
25
+
26
+ ```css
27
+ @source "../../node_modules/@1e99/components/dist";
28
+ ```
29
+
30
+ Nos consumidores atuais, a alteração futura será em:
31
+
32
+ - `domain-website/src/app/globals.css`;
33
+ - `curriculo/src/app/brand.css`.
34
+
35
+ Os dois projetos já fornecem os tokens usados pela navbar, incluindo
36
+ `surface-page`, `content-primary`, `content-secondary`, `border-subtle`,
37
+ `action-primary`, `surface-raised`, `content-inverse`, `shadow-card`,
38
+ `shadow-float`, `shadow-focus`, `ease-brand`, `shell`, `control` e `card`.
39
+ O pacote não redefine esses tokens nem importa CSS global no aplicativo.
40
+
41
+ ## Uso
42
+
43
+ ```tsx
44
+ import {useState} from "react";
45
+ import {
46
+ GoogleSignInButton,
47
+ Navbar,
48
+ Selector,
49
+ } from "@1e99/components";
50
+
51
+ type LocaleOption = {
52
+ label: string;
53
+ languageTag: string;
54
+ value: string;
55
+ };
56
+
57
+ const localeOptions = [
58
+ { value: "pt", label: "Português", languageTag: "pt-BR" },
59
+ { value: "en", label: "English", languageTag: "en" },
60
+ { value: "es", label: "Español", languageTag: "es" },
61
+ ] satisfies readonly LocaleOption[];
62
+
63
+ function Header() {
64
+ const [locale, setLocale] = useState("pt");
65
+
66
+ return (
67
+ <Navbar
68
+ brand={<a href="/"><BrandLogo /></a>}
69
+ items={[
70
+ { href: "/apps", label: "Apps" },
71
+ { href: "/about", label: "Sobre" },
72
+ ]}
73
+ navigationLabel="Navegação principal"
74
+ renderLink={({ href, children, className }) => (
75
+ <Link className={className} href={href}>{children}</Link>
76
+ )}
77
+ actions={
78
+ <div className="hidden items-center gap-3 md:flex">
79
+ <Selector
80
+ label={t("language")}
81
+ onValueChange={setLocale}
82
+ options={localeOptions}
83
+ renderOption={({ option }) => (
84
+ <span lang={option.languageTag}>{option.label}</span>
85
+ )}
86
+ renderTrigger={({ selectedOption, value }) => (
87
+ <span lang={selectedOption?.languageTag}>
88
+ {selectedOption?.label ?? value}
89
+ </span>
90
+ )}
91
+ value={locale}
92
+ />
93
+ <GoogleSignInButton
94
+ label={t("signInWithGoogle")}
95
+ onClick={startGoogleLogin}
96
+ />
97
+ </div>
98
+ }
99
+ width="content"
100
+ layer="above"
101
+ linkTreatment="underlined"
102
+ linkWeight="semibold"
103
+ mobileNavigation={{
104
+ menuLabel: t("openNavigation"),
105
+ navigationLabel: t("mobileNavigation"),
106
+ actions: <MobileAccountActions />,
107
+ }}
108
+ />
109
+ );
110
+ }
111
+ ```
112
+
113
+ | Prop | Opções | Uso nos projetos existentes |
114
+ | --- | --- | --- |
115
+ | `width` | `content` (padrão), `wide` | domain-website / curriculo |
116
+ | `layer` | `standard` (padrão), `above` | curriculo / domain-website |
117
+ | `linkTreatment` | `plain` (padrão), `underlined` | curriculo / domain-website |
118
+ | `linkWeight` | `semibold` (padrão), `bold` | domain-website / curriculo |
119
+ | `mobileNavigation` | ausente ou objeto de disclosure | curriculo / domain-website |
120
+
121
+ `brand` e `actions` são slots e, junto da navegação desktop, permanecem como
122
+ regiões irmãs no container — o mesmo layout dos dois headers fonte. O objeto
123
+ `mobileNavigation` reproduz o menu do domain (botão azul de 48px e painel
124
+ elevado); ele fica ausente no perfil do currículo. `renderLink` recebe `href`,
125
+ `children` e `className`, permitindo usar `next/link`, `next-intl/link` ou
126
+ outro roteador sem acoplar o pacote a ele.
127
+
128
+ ### Controles de header
129
+
130
+ `GoogleSignInButton` é o único tratamento visual para entrar com Google e
131
+ segue o botão do `domain-website` em todos os usos. `label` é obrigatório e
132
+ deve receber a string já traduzida pelo consumidor; também aceita atributos
133
+ comportamentais nativos de botão (`onClick`, `disabled`, `aria-*`). Não aceita
134
+ variante nem `className`; autenticação e estado de carregamento são do
135
+ consumidor.
136
+
137
+ `Selector` é um disclosure genérico para uma coleção controlada. Cada opção
138
+ tem apenas `value` estável e `label` como contrato-base; o consumidor pode
139
+ acrescentar campos ao tipar sua própria coleção, como `languageTag`, descrição
140
+ ou ícone. `value` e `onValueChange` são controlados pelo consumidor: ao clicar
141
+ em uma opção, o componente informa o novo valor e fecha o disclosure, mas não
142
+ navega nem atualiza estado por conta própria.
143
+
144
+ `renderOption` recebe `option`, `isSelected` e `select`; ele renderiza somente
145
+ o conteúdo interno do botão que o pacote controla. `renderTrigger` recebe
146
+ `selectedOption` e `value` para personalizar o conteúdo do gatilho. Dessa
147
+ forma, atributos como `lang` ficam no conteúdo do aplicativo consumidor, não
148
+ no tipo genérico do pacote. O componente preserva botões com `aria-pressed`,
149
+ `select-none`, as densidades `standard` (padrão), `compact` e
150
+ `compact-mobile` e `whiteSurface`. Sem `triggerAriaLabel`, o nome acessível do
151
+ gatilho vem do conteúdo selecionado visível; use essa prop quando o consumidor
152
+ precisar de um nome mais descritivo e atualize-o junto do valor controlado.
153
+
154
+ `Navbar` não recebe props de idioma nem de autenticação. `items`, `actions` e
155
+ `mobileNavigation.actions` são os pontos de composição para qualquer controle
156
+ ou CTA do produto. No Storybook, `Navigation/Navbar` inclui o playground de
157
+ referência com o logo oficial fixo; seus args serializáveis controlam itens,
158
+ seletor, botão Google e menu móvel. O seletor e o botão também possuem stories
159
+ próprias em `Controls/Selector` e `Controls/GoogleSignInButton`.
160
+
161
+ ## Desenvolvimento e testes
162
+
163
+ ```sh
164
+ pnpm lint
165
+ pnpm lint:fix
166
+ pnpm format
167
+ pnpm storybook
168
+ pnpm test
169
+ pnpm test:stories
170
+ pnpm build
171
+ pnpm build-storybook
172
+ pnpm pack:check
173
+ ```
174
+
175
+ `pnpm lint` aplica o padrão Google para TypeScript e React; `pnpm lint:fix`
176
+ corrige automaticamente o que for seguro, e `pnpm format` é seu atalho. O
177
+ Husky executa `pnpm lint` e `pnpm test` antes de cada commit local, portanto
178
+ ambos devem passar antes de o histórico ser criado.
179
+
180
+ `pnpm test` roda testes de contrato em Vitest + Testing Library. As stories
181
+ exibem o playground configurável da navbar, o seletor genérico e o botão
182
+ Google, incluindo o SVG oficial somente como asset de demonstração do
183
+ Storybook. Elas são verificadas em Chromium por `pnpm test:stories`, com os
184
+ addons oficiais de acessibilidade e Vitest.
185
+
186
+ Em uma máquina Linux nova, instale as dependências do Chromium uma vez antes
187
+ do teste de stories:
188
+
189
+ ```sh
190
+ pnpm exec playwright install chromium
191
+ sudo pnpm exec playwright install-deps chromium
192
+ ```
193
+
194
+ ## Organização do código
195
+
196
+ Cada componente é uma unidade em `src/components/<nome>/`: implementação,
197
+ testes e story (quando houver) ficam juntos. `src/index.ts` é o único ponto de
198
+ entrada público; mudar a organização interna não altera como os aplicativos
199
+ importam `@1e99/components`. Testes transversais ficam em `test/`.
200
+
201
+ ## Publicação
202
+
203
+ O destino recomendado é o npm público, como `@1e99/components`: ele evita
204
+ configuração de registry e token em cada consumidor. GitHub Packages é uma
205
+ alternativa para distribuição privada, mas exige registry e token GitHub nos
206
+ aplicativos.
207
+
208
+ `.github/workflows/ci.yml` valida pull requests e pushes para `main` com lint,
209
+ tipos, testes, build, Storybook e prévia do tarball. `.github/workflows/publish.yml`
210
+ repete essas verificações e publica somente quando uma tag `vX.Y.Z` corresponde
211
+ exatamente à versão em `package.json` e aponta para um commit ancestral de
212
+ `main`. Os actions usados nos workflows estão fixados em commits revisáveis.
213
+
214
+ ### Primeiro release pelo CI
215
+
216
+ O bootstrap usa temporariamente o secret de repositório `NPM_TOKEN`. Crie-o no
217
+ npm como token granular, com **Bypass two-factor authentication**, permissão
218
+ **Read and write (publish and stage)** limitada ao escopo `@1e99` e expiração
219
+ curta. Salve-o em GitHub → `1e99/components` → Settings → Secrets and variables
220
+ → Actions como `NPM_TOKEN`. Nunca grave ou compartilhe esse valor por chat,
221
+ issues, commits ou logs.
222
+
223
+ Como `@1e99/components` ainda não existe antes do primeiro publish, o escopo
224
+ inteiro é uma exceção temporária de bootstrap: revogue o token assim que OIDC
225
+ for configurado. O npm anuncia a remoção do publish direto com tokens granular
226
+ de bypass de 2FA em janeiro de 2027; se o primeiro release for adiado além
227
+ disso, use o fluxo de staged publishing do npm em vez de manter um token.
228
+
229
+ Antes de criar a tag pública, confirme a política de licença: o pacote está
230
+ como `UNLICENSED` e essa decisão precisa de aprovação explícita para publicação
231
+ pública.
232
+
233
+ Envie os commits e a tag correspondente à versão:
234
+
235
+ ```sh
236
+ git push origin main
237
+ git tag v0.1.0
238
+ git push origin v0.1.0
239
+ ```
240
+
241
+ O workflow recusa a tag se `package.json` não estiver em `0.1.0` e publica com
242
+ `publishConfig.access: public` se todas as verificações passarem.
243
+ Configure também regras do GitHub para exigir o CI antes de merge em `main` e
244
+ restringir a criação e atualização de tags `v*` a mantenedores autorizados.
245
+
246
+ ### Migrar para Trusted Publishing
247
+
248
+ Depois que a primeira release for publicada, habilite 2FA na conta npm e abra
249
+ as configurações de `@1e99/components` no npm. Em **Trusted Publisher**, escolha
250
+ GitHub Actions e informe organização `1e99`, repositório `components` e arquivo
251
+ de workflow `publish.yml`; permita publicação direta com `npm publish`.
252
+
253
+ Em seguida, apague o secret `NPM_TOKEN` do GitHub e revogue o token no npm. As
254
+ tags futuras usarão OIDC com credenciais efêmeras do GitHub Actions e gerarão
255
+ provenance automaticamente. Para uma próxima versão, atualize a versão e envie
256
+ a tag criada pelo gerenciador:
257
+
258
+ ```sh
259
+ pnpm version patch
260
+ git push origin main --follow-tags
261
+ ```
@@ -0,0 +1,71 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode } from 'react';
3
+
4
+ type GoogleSignInButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'className' | 'type'> & {
5
+ label: ReactNode;
6
+ };
7
+ declare function GoogleSignInButton({ label, ...buttonProps }: GoogleSignInButtonProps): react.JSX.Element;
8
+
9
+ type SelectorOption = {
10
+ label: ReactNode;
11
+ value: string;
12
+ };
13
+ type SelectorDensity = 'compact' | 'compact-mobile' | 'standard';
14
+ type SelectorRenderOptionContext<TOption extends SelectorOption> = {
15
+ isSelected: boolean;
16
+ option: TOption;
17
+ select: () => void;
18
+ };
19
+ type SelectorRenderTriggerContext<TOption extends SelectorOption> = {
20
+ selectedOption: TOption | undefined;
21
+ value: string;
22
+ };
23
+ type SelectorBaseProps = {
24
+ label: string;
25
+ triggerAriaLabel?: string;
26
+ value: string;
27
+ };
28
+ type SelectorProps<TOption extends SelectorOption = SelectorOption> = SelectorBaseProps & {
29
+ density?: SelectorDensity;
30
+ onValueChange: (value: string) => void;
31
+ options: readonly TOption[];
32
+ renderOption?: (context: SelectorRenderOptionContext<TOption>) => ReactNode;
33
+ renderTrigger?: (context: SelectorRenderTriggerContext<TOption>) => ReactNode;
34
+ whiteSurface?: boolean;
35
+ };
36
+ declare function Selector<TOption extends SelectorOption = SelectorOption>({ density, label, onValueChange, options, renderOption, renderTrigger, triggerAriaLabel, value, whiteSurface, }: SelectorProps<TOption>): react.JSX.Element;
37
+
38
+ type NavbarItem = {
39
+ href: string;
40
+ label: ReactNode;
41
+ };
42
+ type NavbarLinkRenderer = (props: {
43
+ children: ReactNode;
44
+ className: string;
45
+ href: string;
46
+ }) => ReactNode;
47
+ type NavbarWidth = 'content' | 'wide';
48
+ type NavbarLinkTreatment = 'plain' | 'underlined';
49
+ type NavbarLinkWeight = 'bold' | 'semibold';
50
+ type NavbarLayer = 'above' | 'standard';
51
+ type NavbarMobileNavigation = {
52
+ actions?: ReactNode;
53
+ menuLabel: string;
54
+ navigationLabel: string;
55
+ };
56
+ type NavbarProps = {
57
+ actions?: ReactNode;
58
+ brand: ReactNode;
59
+ items: readonly NavbarItem[];
60
+ layer?: NavbarLayer;
61
+ linkTreatment?: NavbarLinkTreatment;
62
+ linkWeight?: NavbarLinkWeight;
63
+ mobileNavigation?: NavbarMobileNavigation;
64
+ navigationLabel: string;
65
+ renderLink?: NavbarLinkRenderer;
66
+ width?: NavbarWidth;
67
+ };
68
+ declare function Navbar({ actions, brand, items, layer, linkTreatment, linkWeight, mobileNavigation, navigationLabel, renderLink, width, }: NavbarProps): react.JSX.Element;
69
+
70
+ export { GoogleSignInButton, Navbar, Selector };
71
+ export type { GoogleSignInButtonProps, NavbarItem, NavbarLayer, NavbarLinkRenderer, NavbarLinkTreatment, NavbarLinkWeight, NavbarMobileNavigation, NavbarProps, NavbarWidth, SelectorDensity, SelectorOption, SelectorProps, SelectorRenderOptionContext, SelectorRenderTriggerContext };
package/dist/index.js ADDED
@@ -0,0 +1,189 @@
1
+ // src/components/google-sign-in-button/google-sign-in-button.tsx
2
+ import { FcGoogle } from "react-icons/fc";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ var buttonClassName = "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";
5
+ function GoogleSignInButton({ label, ...buttonProps }) {
6
+ return /* @__PURE__ */ jsxs("button", { ...buttonProps, className: buttonClassName, type: "button", children: [
7
+ /* @__PURE__ */ jsx(
8
+ FcGoogle,
9
+ {
10
+ "aria-hidden": "true",
11
+ className: "size-5 shrink-0",
12
+ "data-testid": "google-mark"
13
+ }
14
+ ),
15
+ label
16
+ ] });
17
+ }
18
+
19
+ // src/components/selector/selector.tsx
20
+ import { LuChevronDown } from "react-icons/lu";
21
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
22
+ var standardTriggerClassName = "flex w-full cursor-pointer list-none 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 [&::-webkit-details-marker]:hidden";
23
+ var optionClassName = "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";
24
+ function closeSelectorDisclosure(element) {
25
+ element.closest("details")?.removeAttribute("open");
26
+ }
27
+ function getNavigationLayout(density) {
28
+ if (density === "compact") {
29
+ return { trigger: "h-12 gap-2", wrapper: "w-auto" };
30
+ }
31
+ if (density === "compact-mobile") {
32
+ return {
33
+ trigger: "h-12 gap-2 px-3 md:gap-4 md:px-4 md:min-w-32",
34
+ wrapper: "w-auto"
35
+ };
36
+ }
37
+ return { trigger: "min-h-12 gap-4 px-4 md:min-w-32", wrapper: "w-full md:w-auto" };
38
+ }
39
+ function SelectorTrigger({
40
+ densityClassName,
41
+ renderTrigger,
42
+ selectedOption,
43
+ triggerAriaLabel,
44
+ value
45
+ }) {
46
+ const triggerContent = renderTrigger?.({ selectedOption, value }) ?? selectedOption?.label ?? value;
47
+ return /* @__PURE__ */ jsxs2("summary", { "aria-label": triggerAriaLabel, className: `${standardTriggerClassName} ${densityClassName}`, children: [
48
+ /* @__PURE__ */ jsx2("span", { children: triggerContent }),
49
+ /* @__PURE__ */ jsx2(
50
+ LuChevronDown,
51
+ {
52
+ "aria-hidden": "true",
53
+ className: "selector-disclosure-arrow size-5 shrink-0 text-content-secondary transition-transform duration-200 ease-brand motion-reduce:transition-none group-open:rotate-180"
54
+ }
55
+ )
56
+ ] });
57
+ }
58
+ function Selector({
59
+ density = "standard",
60
+ label,
61
+ onValueChange,
62
+ options,
63
+ renderOption,
64
+ renderTrigger,
65
+ triggerAriaLabel,
66
+ value,
67
+ whiteSurface = false
68
+ }) {
69
+ const selectedOption = options.find((option) => option.value === value);
70
+ const layout = getNavigationLayout(density);
71
+ const surfaceClassName = whiteSurface ? "bg-white" : "bg-surface-raised";
72
+ return /* @__PURE__ */ jsxs2("details", { className: `selector-disclosure group relative ${layout.wrapper}`, children: [
73
+ /* @__PURE__ */ jsx2(
74
+ SelectorTrigger,
75
+ {
76
+ densityClassName: layout.trigger,
77
+ renderTrigger,
78
+ selectedOption,
79
+ triggerAriaLabel,
80
+ value
81
+ }
82
+ ),
83
+ /* @__PURE__ */ jsx2(
84
+ "ul",
85
+ {
86
+ "aria-label": label,
87
+ 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`,
88
+ children: options.map((option) => {
89
+ const isSelected = option.value === value;
90
+ const select = () => onValueChange(option.value);
91
+ const optionContent = renderOption?.({ isSelected, option, select }) ?? option.label;
92
+ return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(
93
+ "button",
94
+ {
95
+ "aria-pressed": isSelected,
96
+ className: `${optionClassName} text-left ${isSelected ? "bg-brand-cloud text-content-primary" : ""}`,
97
+ onClick: (event) => {
98
+ select();
99
+ closeSelectorDisclosure(event.currentTarget);
100
+ },
101
+ type: "button",
102
+ children: optionContent
103
+ }
104
+ ) }, option.value);
105
+ })
106
+ }
107
+ )
108
+ ] });
109
+ }
110
+
111
+ // src/components/navbar/navbar.tsx
112
+ import { Fragment } from "react";
113
+ import { LuMenu } from "react-icons/lu";
114
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
115
+ 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";
116
+ 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";
117
+ 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";
118
+ function Navbar({
119
+ actions,
120
+ brand,
121
+ items,
122
+ layer = "standard",
123
+ linkTreatment = "plain",
124
+ linkWeight = "semibold",
125
+ mobileNavigation,
126
+ navigationLabel,
127
+ renderLink,
128
+ width = "content"
129
+ }) {
130
+ const linkClassName = [
131
+ baseLinkClassName,
132
+ linkWeight === "bold" ? "font-bold" : "font-semibold",
133
+ linkTreatment === "underlined" ? underlinedLinkClassName : ""
134
+ ].filter(Boolean).join(" ");
135
+ const containerWidthClassName = width === "wide" ? "max-w-7xl" : "max-w-5xl";
136
+ const layerClassName = layer === "above" ? "z-50" : "z-40";
137
+ const renderLinks = (className) => items.map((item) => /* @__PURE__ */ jsx3(Fragment, { children: renderLink ? renderLink({
138
+ children: item.label,
139
+ className,
140
+ href: item.href
141
+ }) : /* @__PURE__ */ jsx3("a", { className, href: item.href, children: item.label }) }, item.href));
142
+ return /* @__PURE__ */ jsx3(
143
+ "header",
144
+ {
145
+ className: `sticky top-0 ${layerClassName} border-b border-border-subtle bg-surface-page/90 text-label font-semibold backdrop-blur-lg`,
146
+ children: /* @__PURE__ */ jsxs3(
147
+ "div",
148
+ {
149
+ className: `mx-auto flex min-h-20 ${containerWidthClassName} items-center justify-between gap-4 px-shell`,
150
+ children: [
151
+ /* @__PURE__ */ jsx3("div", { className: "shrink-0", children: brand }),
152
+ /* @__PURE__ */ jsx3("nav", { "aria-label": navigationLabel, className: "hidden items-center gap-6 lg:flex", children: renderLinks(linkClassName) }),
153
+ actions,
154
+ mobileNavigation ? /* @__PURE__ */ jsxs3("details", { className: "relative md:hidden", children: [
155
+ /* @__PURE__ */ jsxs3(
156
+ "summary",
157
+ {
158
+ "aria-label": mobileNavigation.menuLabel,
159
+ 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",
160
+ children: [
161
+ /* @__PURE__ */ jsx3("span", { className: "sr-only", children: mobileNavigation.menuLabel }),
162
+ /* @__PURE__ */ jsx3(LuMenu, { "aria-hidden": "true", className: "size-6" })
163
+ ]
164
+ }
165
+ ),
166
+ /* @__PURE__ */ jsxs3(
167
+ "nav",
168
+ {
169
+ "aria-label": mobileNavigation.navigationLabel,
170
+ className: "absolute top-14 right-0 w-72 rounded-card border border-border-subtle bg-surface-raised p-3 shadow-float",
171
+ children: [
172
+ renderLinks(mobileLinkClassName),
173
+ mobileNavigation.actions ? /* @__PURE__ */ jsx3("div", { className: "mt-2 space-y-3 border-t border-border-subtle pt-3", children: mobileNavigation.actions }) : null
174
+ ]
175
+ }
176
+ )
177
+ ] }) : null
178
+ ]
179
+ }
180
+ )
181
+ }
182
+ );
183
+ }
184
+ export {
185
+ GoogleSignInButton,
186
+ Navbar,
187
+ Selector
188
+ };
189
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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} 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 list-none 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 [&::-webkit-details-marker]:hidden';\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 closeSelectorDisclosure(element: HTMLElement) {\n element.closest('details')?.removeAttribute('open');\n}\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 renderTrigger,\n selectedOption,\n triggerAriaLabel,\n value,\n}: {\n densityClassName: 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 <summary aria-label={triggerAriaLabel} className={`${standardTriggerClassName} ${densityClassName}`}>\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 group-open:rotate-180\"\n />\n </summary>\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 selectedOption = options.find((option) => option.value === value);\n const layout = getNavigationLayout(density);\n const surfaceClassName = whiteSurface ? 'bg-white' : 'bg-surface-raised';\n\n return (\n <details className={`selector-disclosure group relative ${layout.wrapper}`}>\n <SelectorTrigger\n densityClassName={layout.trigger}\n renderTrigger={renderTrigger}\n selectedOption={selectedOption}\n triggerAriaLabel={triggerAriaLabel}\n value={value}\n />\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 >\n {options.map((option) => {\n const isSelected = option.value === value;\n const select = () => onValueChange(option.value);\n const optionContent = renderOption?.({isSelected, option, select}) ??\n 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={(event) => {\n select();\n closeSelectorDisclosure(event.currentTarget);\n }}\n type=\"button\"\n >\n {optionContent}\n </button>\n </li>\n );\n })}\n </ul>\n </details>\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"]}
package/package.json CHANGED
@@ -1,6 +1,81 @@
1
1
  {
2
2
  "name": "@1e99/components",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Shared React components for 1e99 applications.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/1e99/components.git"
8
+ },
9
+ "license": "UNLICENSED",
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "format": "pnpm lint:fix",
27
+ "lint": "eslint . --max-warnings 0",
28
+ "lint:fix": "eslint . --fix --max-warnings 0",
29
+ "prepare": "husky",
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "vitest run --project unit",
32
+ "test:watch": "vitest --project unit",
33
+ "storybook": "storybook dev -p 6006",
34
+ "build-storybook": "storybook build",
35
+ "test:stories": "vitest run --project storybook",
36
+ "pack:check": "pnpm build && npm pack --dry-run"
37
+ },
38
+ "peerDependencies": {
39
+ "react": "^19.0.0",
40
+ "react-dom": "^19.0.0",
41
+ "react-icons": "^5.7.0",
42
+ "tailwindcss": "^4.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@eslint/js": "^9.39.5",
46
+ "@storybook/addon-a11y": "^10.6.3",
47
+ "@storybook/addon-vitest": "^10.6.3",
48
+ "@storybook/react-vite": "^10.6.3",
49
+ "@tailwindcss/vite": "^4.3.3",
50
+ "@testing-library/jest-dom": "^6.9.1",
51
+ "@testing-library/react": "^16.3.2",
52
+ "@testing-library/user-event": "^14.6.1",
53
+ "@types/node": "26.6.5",
54
+ "@types/react": "^19.2.14",
55
+ "@types/react-dom": "^19.2.3",
56
+ "@vitejs/plugin-react": "^5.1.4",
57
+ "@vitest/browser-playwright": "5.0.5",
58
+ "eslint": "^9.39.5",
59
+ "eslint-config-google": "^0.14.0",
60
+ "eslint-plugin-react": "^7.37.5",
61
+ "eslint-plugin-react-hooks": "^7.1.1",
62
+ "globals": "^16.5.0",
63
+ "husky": "^9.1.7",
64
+ "jsdom": "^28.0.0",
65
+ "playwright": "1.64.1",
66
+ "react": "^19.2.4",
67
+ "react-dom": "^19.2.4",
68
+ "react-icons": "5.7.0",
69
+ "storybook": "^10.6.3",
70
+ "tailwindcss": "^4.3.3",
71
+ "tsup": "^9.0.0",
72
+ "typescript": "^5.9.3",
73
+ "typescript-eslint": "^8.72.0",
74
+ "vite": "^8.3.5",
75
+ "vitest": "^5.0.5"
76
+ },
77
+ "packageManager": "pnpm@12.9.1",
78
+ "publishConfig": {
79
+ "access": "public"
80
+ }
81
+ }