@1e99/components 0.1.0 → 0.1.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/LICENSE +21 -0
- package/README.md +57 -188
- package/dist/index.js +61 -39
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 1e99
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,42 +1,32 @@
|
|
|
1
1
|
# @1e99/components
|
|
2
2
|
|
|
3
|
-
Componentes React
|
|
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.
|
|
3
|
+
Componentes React reutilizáveis, estilizados com Tailwind CSS.
|
|
7
4
|
|
|
8
|
-
## Instalação
|
|
9
|
-
|
|
10
|
-
Após a primeira publicação pública, instale o pacote no app consumidor:
|
|
5
|
+
## Instalação
|
|
11
6
|
|
|
12
7
|
```sh
|
|
13
8
|
pnpm add @1e99/components
|
|
14
9
|
```
|
|
15
10
|
|
|
16
|
-
O pacote declara `react`, `react-dom`, `react-icons` e `tailwindcss` como
|
|
17
|
-
|
|
18
|
-
ícones Google e Lucide que o pacote renderiza, sem duplicar SVGs manualmente.
|
|
11
|
+
O pacote declara `react`, `react-dom`, `react-icons` e `tailwindcss` como peer
|
|
12
|
+
dependencies.
|
|
19
13
|
|
|
20
14
|
### Tailwind CSS 4
|
|
21
15
|
|
|
22
|
-
O Tailwind 4 não
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
O Tailwind 4 não examina automaticamente classes em `node_modules`. Na folha
|
|
17
|
+
CSS que importa o Tailwind, adicione uma diretiva `@source` cujo caminho seja
|
|
18
|
+
relativo a ela:
|
|
25
19
|
|
|
26
20
|
```css
|
|
21
|
+
@import "tailwindcss";
|
|
27
22
|
@source "../../node_modules/@1e99/components/dist";
|
|
28
23
|
```
|
|
29
24
|
|
|
30
|
-
|
|
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
|
|
25
|
+
Os componentes usam tokens Tailwind do tema do aplicativo, como
|
|
36
26
|
`surface-page`, `content-primary`, `content-secondary`, `border-subtle`,
|
|
37
27
|
`action-primary`, `surface-raised`, `content-inverse`, `shadow-card`,
|
|
38
28
|
`shadow-float`, `shadow-focus`, `ease-brand`, `shell`, `control` e `card`.
|
|
39
|
-
O pacote não
|
|
29
|
+
O pacote não cria tokens nem importa CSS global.
|
|
40
30
|
|
|
41
31
|
## Uso
|
|
42
32
|
|
|
@@ -48,117 +38,65 @@ import {
|
|
|
48
38
|
Selector,
|
|
49
39
|
} from "@1e99/components";
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
label:
|
|
53
|
-
|
|
54
|
-
|
|
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[];
|
|
41
|
+
const languageOptions = [
|
|
42
|
+
{label: "Português", value: "pt"},
|
|
43
|
+
{label: "English", value: "en"},
|
|
44
|
+
] as const;
|
|
62
45
|
|
|
63
46
|
function Header() {
|
|
64
|
-
const [
|
|
47
|
+
const [language, setLanguage] = useState("pt");
|
|
65
48
|
|
|
66
49
|
return (
|
|
67
50
|
<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
51
|
actions={
|
|
78
52
|
<div className="hidden items-center gap-3 md:flex">
|
|
79
53
|
<Selector
|
|
80
|
-
label=
|
|
81
|
-
onValueChange={
|
|
82
|
-
options={
|
|
83
|
-
|
|
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}
|
|
54
|
+
label="Idioma"
|
|
55
|
+
onValueChange={setLanguage}
|
|
56
|
+
options={languageOptions}
|
|
57
|
+
value={language}
|
|
92
58
|
/>
|
|
93
59
|
<GoogleSignInButton
|
|
94
|
-
label=
|
|
95
|
-
onClick={
|
|
60
|
+
label="Entrar com Google"
|
|
61
|
+
onClick={() => undefined}
|
|
96
62
|
/>
|
|
97
63
|
</div>
|
|
98
64
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
navigationLabel: t("mobileNavigation"),
|
|
106
|
-
actions: <MobileAccountActions />,
|
|
107
|
-
}}
|
|
65
|
+
brand={<a aria-label="Início" href="/">Marca</a>}
|
|
66
|
+
items={[
|
|
67
|
+
{href: "/products", label: "Produtos"},
|
|
68
|
+
{href: "/about", label: "Sobre"},
|
|
69
|
+
]}
|
|
70
|
+
navigationLabel="Navegação principal"
|
|
108
71
|
/>
|
|
109
72
|
);
|
|
110
73
|
}
|
|
111
74
|
```
|
|
112
75
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
76
|
+
`Navbar` mantém marca, itens de navegação e ações como regiões independentes.
|
|
77
|
+
Use `renderLink` para integrar um roteador próprio sem acoplar o pacote a uma
|
|
78
|
+
biblioteca de navegação. Os itens podem ser adicionados ou removidos, e
|
|
79
|
+
`actions` e `mobileNavigation.actions` aceitam qualquer nó React.
|
|
129
80
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
81
|
+
| Prop | Opções | Finalidade |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `width` | `content` (padrão), `wide` | Largura máxima da navegação |
|
|
84
|
+
| `layer` | `standard` (padrão), `above` | Camada de empilhamento |
|
|
85
|
+
| `linkTreatment` | `plain` (padrão), `underlined` | Aparência dos links |
|
|
86
|
+
| `linkWeight` | `semibold` (padrão), `bold` | Peso dos links |
|
|
87
|
+
| `mobileNavigation` | ausente ou objeto de disclosure | Navegação móvel opcional |
|
|
143
88
|
|
|
144
|
-
`
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
89
|
+
`GoogleSignInButton` recebe uma `label` obrigatória e os atributos nativos de
|
|
90
|
+
um botão, como `onClick`, `disabled` e `aria-*`. Autenticação, estado de
|
|
91
|
+
carregamento e traduções pertencem ao aplicativo que o utiliza.
|
|
153
92
|
|
|
154
|
-
`
|
|
155
|
-
`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
próprias em `Controls/Selector` e `Controls/GoogleSignInButton`.
|
|
93
|
+
`Selector` é controlado: `value` determina a opção visível e
|
|
94
|
+
`onValueChange` informa uma nova escolha. As opções exigem somente `value` e
|
|
95
|
+
`label`, mas podem ser estendidas com outros campos e apresentadas por
|
|
96
|
+
`renderOption` e `renderTrigger`. O componente não navega nem muda estado por
|
|
97
|
+
conta própria.
|
|
160
98
|
|
|
161
|
-
## Desenvolvimento
|
|
99
|
+
## Desenvolvimento
|
|
162
100
|
|
|
163
101
|
```sh
|
|
164
102
|
pnpm lint
|
|
@@ -172,90 +110,21 @@ pnpm build-storybook
|
|
|
172
110
|
pnpm pack:check
|
|
173
111
|
```
|
|
174
112
|
|
|
175
|
-
`pnpm lint`
|
|
176
|
-
|
|
177
|
-
|
|
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:
|
|
113
|
+
Husky executa `pnpm lint` e `pnpm test` antes de cada commit. As stories são
|
|
114
|
+
verificadas em Chromium por `pnpm test:stories`; em uma máquina Linux nova,
|
|
115
|
+
instale as dependências uma vez:
|
|
188
116
|
|
|
189
117
|
```sh
|
|
190
118
|
pnpm exec playwright install chromium
|
|
191
119
|
sudo pnpm exec playwright install-deps chromium
|
|
192
120
|
```
|
|
193
121
|
|
|
194
|
-
|
|
122
|
+
Cada componente fica em `src/components/<nome>/`, junto dos seus testes e
|
|
123
|
+
stories. `src/index.ts` é o ponto de entrada público, e testes transversais
|
|
124
|
+
ficam em `test/`.
|
|
195
125
|
|
|
196
|
-
|
|
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/`.
|
|
126
|
+
## Licença
|
|
200
127
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
```
|
|
128
|
+
Este pacote é licenciado sob a [MIT License](LICENSE). Você pode usar, copiar,
|
|
129
|
+
modificar, distribuir e sublicenciar o software, inclusive comercialmente,
|
|
130
|
+
desde que preserve o aviso de copyright e a licença.
|
package/dist/index.js
CHANGED
|
@@ -17,13 +17,11 @@ function GoogleSignInButton({ label, ...buttonProps }) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// src/components/selector/selector.tsx
|
|
20
|
+
import { useCallback, useEffect, useState } from "react";
|
|
20
21
|
import { LuChevronDown } from "react-icons/lu";
|
|
21
22
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
22
23
|
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
24
|
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
25
|
function getNavigationLayout(density) {
|
|
28
26
|
if (density === "compact") {
|
|
29
27
|
return { trigger: "h-12 gap-2", wrapper: "w-auto" };
|
|
@@ -66,46 +64,70 @@ function Selector({
|
|
|
66
64
|
value,
|
|
67
65
|
whiteSurface = false
|
|
68
66
|
}) {
|
|
67
|
+
const [selectorElement, setSelectorElement] = useState(null);
|
|
69
68
|
const selectedOption = options.find((option) => option.value === value);
|
|
70
69
|
const layout = getNavigationLayout(density);
|
|
71
70
|
const surfaceClassName = whiteSurface ? "bg-white" : "bg-surface-raised";
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
})
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!selectorElement) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const closeWhenPointerStartsOutside = (event) => {
|
|
76
|
+
if (!selectorElement.open || !(event.target instanceof Node) || selectorElement.contains(event.target)) {
|
|
77
|
+
return;
|
|
106
78
|
}
|
|
107
|
-
|
|
108
|
-
|
|
79
|
+
selectorElement.removeAttribute("open");
|
|
80
|
+
};
|
|
81
|
+
document.addEventListener("pointerdown", closeWhenPointerStartsOutside, true);
|
|
82
|
+
return () => {
|
|
83
|
+
document.removeEventListener("pointerdown", closeWhenPointerStartsOutside, true);
|
|
84
|
+
};
|
|
85
|
+
}, [selectorElement]);
|
|
86
|
+
const selectOption = useCallback((option) => {
|
|
87
|
+
selectorElement?.removeAttribute("open");
|
|
88
|
+
onValueChange(option.value);
|
|
89
|
+
}, [onValueChange, selectorElement]);
|
|
90
|
+
return /* @__PURE__ */ jsxs2(
|
|
91
|
+
"details",
|
|
92
|
+
{
|
|
93
|
+
ref: setSelectorElement,
|
|
94
|
+
className: `selector-disclosure group relative ${layout.wrapper}`,
|
|
95
|
+
children: [
|
|
96
|
+
/* @__PURE__ */ jsx2(
|
|
97
|
+
SelectorTrigger,
|
|
98
|
+
{
|
|
99
|
+
densityClassName: layout.trigger,
|
|
100
|
+
renderTrigger,
|
|
101
|
+
selectedOption,
|
|
102
|
+
triggerAriaLabel,
|
|
103
|
+
value
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
/* @__PURE__ */ jsx2(
|
|
107
|
+
"ul",
|
|
108
|
+
{
|
|
109
|
+
"aria-label": label,
|
|
110
|
+
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`,
|
|
111
|
+
children: options.map((option) => {
|
|
112
|
+
const isSelected = option.value === value;
|
|
113
|
+
const select = () => selectOption(option);
|
|
114
|
+
const optionContent = renderOption?.({ isSelected, option, select }) ?? option.label;
|
|
115
|
+
return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(
|
|
116
|
+
"button",
|
|
117
|
+
{
|
|
118
|
+
"aria-pressed": isSelected,
|
|
119
|
+
className: `${optionClassName} text-left ${isSelected ? "bg-brand-cloud text-content-primary" : ""}`,
|
|
120
|
+
onClick: select,
|
|
121
|
+
type: "button",
|
|
122
|
+
children: optionContent
|
|
123
|
+
}
|
|
124
|
+
) }, option.value);
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
);
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
// src/components/navbar/navbar.tsx
|
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} 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"]}
|
|
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, useCallback, useEffect, 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 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 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 [selectorElement, setSelectorElement] =\n useState<HTMLDetailsElement | null>(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 (!selectorElement) {\n return;\n }\n\n const closeWhenPointerStartsOutside = (event: PointerEvent) => {\n if (\n !selectorElement.open ||\n !(event.target instanceof Node) ||\n selectorElement.contains(event.target)\n ) {\n return;\n }\n\n selectorElement.removeAttribute('open');\n };\n\n document.addEventListener('pointerdown', closeWhenPointerStartsOutside, true);\n\n return () => {\n document.removeEventListener('pointerdown', closeWhenPointerStartsOutside, true);\n };\n }, [selectorElement]);\n\n const selectOption = useCallback((option: TOption) => {\n selectorElement?.removeAttribute('open');\n onValueChange(option.value);\n }, [onValueChange, selectorElement]);\n\n return (\n <details\n ref={setSelectorElement}\n className={`selector-disclosure group relative ${layout.wrapper}`}\n >\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 = () => selectOption(option);\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={select}\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,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1e99/components",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Reusable React components built with Tailwind CSS.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/1e99/components.git"
|
|
8
8
|
},
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "MIT",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"sideEffects": false,
|
|
12
12
|
"main": "./dist/index.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
+
"LICENSE",
|
|
22
23
|
"README.md"
|
|
23
24
|
],
|
|
24
25
|
"scripts": {
|