@bapla/site-theme 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RK Création (SAS FR)
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,3 +1,170 @@
1
- # Temporary Holding Version
1
+ # @bapla/site-theme
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
+ Le thème de départ des sites hébergés par Bapla Websites
4
+ (`docs/features/websites.md` §7.5) : le thème Tailwind tiré de l'Apparence,
5
+ le rendu des blocs standards, des médias, du texte riche et des menus. React
6
+ 18 ou 19, Tailwind CSS 4, ni Astro ni Next : les routes viendront dans
7
+ `@bapla/site-astro` et `@bapla/site-next`.
8
+
9
+ Public sur npmjs, sous licence MIT : `npm install @bapla/site-theme`, sans
10
+ jeton — le build d'un site sur Coolify l'installe comme n'importe quel
11
+ paquet.
12
+
13
+ ## Le thème
14
+
15
+ L'API de lecture sert l'Apparence du site, défauts résolus
16
+ (`ResolvedAppearance`). Au build, et à chaque revalidation, le site en écrit
17
+ la feuille :
18
+
19
+ ```ts
20
+ import { fontImports, themeCss } from '@bapla/site-theme'
21
+
22
+ const appearance = await delivery.appearance()
23
+ await writeFile('src/styles/theme.css', themeCss(appearance))
24
+ await writeFile('src/styles/fonts.ts', fontImports(appearance).map((p) => `import '${p}'`).join('\n'))
25
+ ```
26
+
27
+ ```css
28
+ /* src/styles/site.css */
29
+ @import "tailwindcss";
30
+ @import "@bapla/site-theme/base.css";
31
+ @import "./theme.css";
32
+ ```
33
+
34
+ `themeCss` produit un bloc `@theme` (`--color-primary-50…950` et
35
+ `secondary`, `accent`, `neutral` ; `--color-background`, `--color-text` ;
36
+ `--font-body`, `--font-heading` ; `--text-h1…h6`, `--text-p` avec leur
37
+ interligne, leur espacement et leur graisse ; `--radius-base`) : les classes
38
+ `bg-primary-600`, `font-heading`, `text-h2`, `rounded-base` existent. En mode
39
+ `both`, le sombre suit `prefers-color-scheme` et se lit dans la rampe
40
+ `neutral`. Une valeur qui n'est ni une couleur ni une longueur est écartée.
41
+
42
+ ## Les polices
43
+
44
+ **Auto-hébergées, toujours** : jamais d'appel à Google Fonts ni à un CDN depuis
45
+ la page. Le site installe les paquets Fontsource de ses polices, et
46
+ `fontImports` dit lesquels importer :
47
+
48
+ ```sh
49
+ npm install @fontsource-variable/inter @fontsource-variable/playfair-display
50
+ ```
51
+
52
+ Une police sans version variable : `fontImports(appearance, { variable: false })`
53
+ donne `@fontsource/<id>/<graisse>.css` pour les graisses utilisées.
54
+
55
+ ## Les blocs
56
+
57
+ ```tsx
58
+ import { Blocks, BlocksProvider } from '@bapla/site-theme'
59
+
60
+ <BlocksProvider resolveLink={(id) => paths[id] ?? null} resolveMedia={(id) => media[id] ?? null}>
61
+ <Blocks blocks={node.data.body} />
62
+ </BlocksProvider>
63
+ ```
64
+
65
+ Les quatorze blocs standards fonctionnent sans une ligne du thème : section,
66
+ grille (colonnes égales, autant que la largeur minimale en permet, ou chaque
67
+ colonne sa largeur en douzièmes ou en pourcentages ; empilement sous un point
68
+ de rupture), colonne, texte, média, carrousel, onglets, accordéon (au clavier,
69
+ ARIA), bouton, intégration (YouTube, Vimeo, Dailymotion, cartes ; une autre
70
+ adresse devient un lien), séparateur, espace. Un type inconnu ne rend rien.
71
+
72
+ Un lien qui ne se résout pas n'est pas suivi : un id de node que
73
+ `resolveLink` ne connaît pas, une adresse qui n'est ni un chemin, ni une
74
+ ancre, ni `mailto:`, `tel:`, `http(s)`.
75
+
76
+ ### Remplacer ou ajouter un bloc
77
+
78
+ Un thème importe le composant de base et le réécrit, ou l'habille :
79
+
80
+ ```tsx
81
+ import { BlocksProvider, Grid, type BlockProps } from '@bapla/site-theme'
82
+
83
+ function MyGrid(props: BlockProps) {
84
+ return <div className="my-grid"><Grid {...props} /></div>
85
+ }
86
+
87
+ function Hero({ props }: BlockProps<{ title?: string }>) {
88
+ return <h1 className="text-h1 text-primary-700">{props.title}</h1>
89
+ }
90
+
91
+ <BlocksProvider components={{ grid: MyGrid, hero: Hero }}>…</BlocksProvider>
92
+ ```
93
+
94
+ Les composants de base portent des classes `bapla-*` et des attributs
95
+ `data-*` (`.bapla-grid[data-gap='lg']`), stylés dans `base.css`, en
96
+ `@layer components` : une utilitaire Tailwind ou une règle du thème l'emporte
97
+ sans `!important`.
98
+
99
+ ## Les médias
100
+
101
+ Un média se sert **sur le domaine du site**, `/_media/<web_med_…>/<nom>`, avec
102
+ `?w=&h=&fit=&fm=&q=` : le site relaie à l'app, qui lit le stockage. `<Media>`
103
+ donne un `srcset` (320 à 1920, jamais plus large que l'original) et place le
104
+ point focal en `object-position`.
105
+
106
+ ### Le compagnon d'images
107
+
108
+ Hébergé, un site a son **imgproxy sur `/_img`** (`websites.md` §5.9) : les
109
+ images transformées y passent, par des **URL signées** avec la clé du site,
110
+ que l'hébergement lui donne (`BAPLA_IMAGES_KEY`, `BAPLA_IMAGES_SALT`,
111
+ `BAPLA_IMAGES_PATH`). La signature se fait **sur le serveur seulement** :
112
+
113
+ ```tsx
114
+ import { BlocksProvider } from '@bapla/site-theme'
115
+ import { imgproxyUrlsFromEnv } from '@bapla/site-theme/server'
116
+
117
+ <BlocksProvider imageUrl={imgproxyUrlsFromEnv()} imageSizes={{ banner: { w: 1600, h: 500, fit: 'cover' } }}>
118
+ ```
119
+
120
+ Sans ces variables (le développement), les URL restent celles de `/_media`.
121
+ SVG, GIF et vidéos ne passent jamais par le compagnon.
122
+
123
+ ### `<Image>`
124
+
125
+ ```tsx
126
+ <Image media={photo} size="card" /> // 300×300 recadré, 1x et 2x
127
+ <Image media={photo} w={640} h={360} fit="cover" /> // une boîte directe
128
+ <Image media={photo} size="card" w={500} /> // le nom, w remplacé
129
+ <Image media={hero} w={1600} widths={[480, 960, 1600]} sizes="100vw" formats={['avif', 'webp']} priority />
130
+ ```
131
+
132
+ - **Tailles nommées** : `thumbnail` (150×150), `card` (300×300), `medium`
133
+ (768 de large), `large` (1280) ; un site en ajoute ou en remplace par
134
+ `imageSizes` du `BlocksProvider`. Un paramètre direct l'emporte sur le nom.
135
+ - **`<img>`** : la boîte en 1x et 2x (2x seulement si l'original suffit),
136
+ `width` et `height` posés pour la mise en page ; avec `widths` et `sizes`,
137
+ un `srcset` en largeurs.
138
+ - **`<picture>`** avec `formats` : une source par format moderne, l'`<img>`
139
+ au format de l'original en repli.
140
+ - Jamais agrandie ; `cover` recadre autour du point focal ; `priority` charge
141
+ tout de suite (`fetchpriority="high"`).
142
+
143
+ ## Les menus
144
+
145
+ `<NavBar items={menu.items} brand={…} />` : la barre, ses sous-menus (motif
146
+ « disclosure » : des liens, et un bouton qui montre les enfants), les
147
+ méga-menus (`panel` : un arbre de blocs, où `menu_links` liste les enfants de
148
+ l'élément), le menu mobile sous `md`. Échap ferme et rend le focus, sortir de
149
+ la barre ferme tout. `<Breadcrumb items={…} />` pour le fil d'Ariane.
150
+
151
+ La forme d'un élément de menu (`src/menu/types.ts`) est **provisoire** : le
152
+ modèle des menus est encore en discussion (`websites.md` §5.8).
153
+
154
+ ## Les libellés
155
+
156
+ Français par défaut ; un site dans une autre langue passe les siens :
157
+ `<BlocksProvider labels={{ menu: 'Menu', next: 'Next', … }}>`.
158
+
159
+ ## Développer
160
+
161
+ ```sh
162
+ npm run build -w @bapla/site-theme
163
+ npm test -w @bapla/site-theme
164
+ ```
165
+
166
+ ## Publier
167
+
168
+ Une étiquette `site-theme/vX.Y.Z`, égale à la version de `package.json`,
169
+ publie sur npmjs (`publish:site-theme` dans `.gitlab-ci.yml`, avec la
170
+ variable `NPM_TOKEN`).
package/dist/base.css ADDED
@@ -0,0 +1,331 @@
1
+ /*
2
+ * @bapla/site-theme — the base style of a site.
3
+ *
4
+ * Imported after Tailwind and the theme of the Appearance:
5
+ *
6
+ * @import "tailwindcss";
7
+ * @import "@bapla/site-theme/base.css";
8
+ * (then the generated `themeCss(appearance)`)
9
+ *
10
+ * Everything sits in `@layer components` (and `base` for elements), so a
11
+ * Tailwind utility or a theme's own rule wins without `!important`. Colours
12
+ * read the variables of the Appearance, with a fallback for a site that has
13
+ * not generated them yet.
14
+ */
15
+
16
+ @layer base {
17
+ *,
18
+ *::before,
19
+ *::after {
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ html {
24
+ -webkit-text-size-adjust: 100%;
25
+ text-rendering: optimizeLegibility;
26
+ }
27
+
28
+ body {
29
+ margin: 0;
30
+ background-color: var(--color-background, #ffffff);
31
+ color: var(--color-text, #1f2937);
32
+ font-family: var(--font-body, ui-sans-serif, system-ui, sans-serif);
33
+ line-height: 1.6;
34
+ }
35
+
36
+ h1, h2, h3, h4, h5, h6 {
37
+ font-family: var(--font-heading, inherit);
38
+ line-height: 1.2;
39
+ margin: 0 0 0.5em;
40
+ text-wrap: balance;
41
+ }
42
+
43
+ p, ul, ol, blockquote, pre, figure, table {
44
+ margin: 0 0 1em;
45
+ }
46
+
47
+ img, video, iframe {
48
+ display: block;
49
+ max-width: 100%;
50
+ }
51
+
52
+ img, video {
53
+ height: auto;
54
+ }
55
+
56
+ a {
57
+ color: var(--color-primary-700, #1d4ed8);
58
+ text-underline-offset: 0.15em;
59
+ }
60
+
61
+ a:hover {
62
+ color: var(--color-primary-800, #1e40af);
63
+ }
64
+
65
+ :focus-visible {
66
+ outline: 2px solid var(--color-primary-600, #2563eb);
67
+ outline-offset: 2px;
68
+ border-radius: 2px;
69
+ }
70
+
71
+ [hidden] {
72
+ display: none !important;
73
+ }
74
+
75
+ @media (prefers-reduced-motion: reduce) {
76
+ *, *::before, *::after {
77
+ animation-duration: 0.01ms !important;
78
+ transition-duration: 0.01ms !important;
79
+ scroll-behavior: auto !important;
80
+ }
81
+ }
82
+ }
83
+
84
+ @layer components {
85
+ /* Layout ---------------------------------------------------------------- */
86
+
87
+ .bapla-container {
88
+ width: 100%;
89
+ max-width: var(--bapla-container, 72rem);
90
+ margin-inline: auto;
91
+ padding-inline: 1rem;
92
+ }
93
+
94
+ .bapla-section[data-width='narrow'] { --bapla-container: 46rem; }
95
+ .bapla-section[data-width='wide'] { --bapla-container: 90rem; }
96
+ .bapla-section[data-width='full'] { --bapla-container: none; }
97
+ .bapla-section[data-width='full'] > .bapla-container { padding-inline: 0; }
98
+
99
+ .bapla-section[data-padding='none'] { padding-block: 0; }
100
+ .bapla-section[data-padding='sm'] { padding-block: 1.5rem; }
101
+ .bapla-section[data-padding='md'] { padding-block: 3rem; }
102
+ .bapla-section[data-padding='lg'] { padding-block: 6rem; }
103
+
104
+ .bapla-section[data-background='muted'] { background-color: var(--color-neutral-100, #f3f4f6); }
105
+ .bapla-section[data-background='accent'] { background-color: var(--color-primary-50, #eff6ff); }
106
+ .bapla-section[data-background='dark'] {
107
+ background-color: var(--color-neutral-900, #111827);
108
+ color: var(--color-neutral-50, #f9fafb);
109
+ }
110
+ .bapla-section[data-background='dark'] a { color: var(--color-primary-300, #93c5fd); }
111
+ .bapla-section[data-background='image'] {
112
+ background-size: cover;
113
+ background-position: center;
114
+ }
115
+
116
+ .bapla-grid {
117
+ display: grid;
118
+ grid-template-columns: var(--bapla-grid-cols, minmax(0, 1fr));
119
+ gap: var(--bapla-gap, 1.5rem);
120
+ }
121
+
122
+ .bapla-grid[data-gap='none'] { --bapla-gap: 0; }
123
+ .bapla-grid[data-gap='sm'] { --bapla-gap: 0.75rem; }
124
+ .bapla-grid[data-gap='md'] { --bapla-gap: 1.5rem; }
125
+ .bapla-grid[data-gap='lg'] { --bapla-gap: 3rem; }
126
+
127
+ .bapla-grid[data-align='start'] { align-items: start; }
128
+ .bapla-grid[data-align='center'] { align-items: center; }
129
+ .bapla-grid[data-align='end'] { align-items: end; }
130
+ .bapla-grid[data-align='stretch'] { align-items: stretch; }
131
+
132
+ .bapla-column {
133
+ min-width: 0;
134
+ grid-column: var(--bapla-col-span, auto);
135
+ }
136
+
137
+ .bapla-column[data-align='start'] { align-self: start; }
138
+ .bapla-column[data-align='center'] { align-self: center; }
139
+ .bapla-column[data-align='end'] { align-self: end; }
140
+
141
+ /* Stacked into one column below its breakpoint. */
142
+ @media (max-width: 1023.98px) {
143
+ .bapla-grid[data-stack='lg'] { grid-template-columns: minmax(0, 1fr); }
144
+ .bapla-grid[data-stack='lg'] > .bapla-column { grid-column: auto; }
145
+ }
146
+ @media (max-width: 767.98px) {
147
+ .bapla-grid[data-stack='md'] { grid-template-columns: minmax(0, 1fr); }
148
+ .bapla-grid[data-stack='md'] > .bapla-column { grid-column: auto; }
149
+ }
150
+ @media (max-width: 639.98px) {
151
+ .bapla-grid[data-stack='sm'] { grid-template-columns: minmax(0, 1fr); }
152
+ .bapla-grid[data-stack='sm'] > .bapla-column { grid-column: auto; }
153
+ }
154
+
155
+ .bapla-divider {
156
+ border: 0;
157
+ border-top: 1px solid var(--color-neutral-200, #e5e7eb);
158
+ margin-block: 1.5rem;
159
+ }
160
+ .bapla-divider[data-size='none'] { margin-block: 0; }
161
+ .bapla-divider[data-size='sm'] { margin-block: 0.75rem; }
162
+ .bapla-divider[data-size='lg'] { margin-block: 3rem; }
163
+
164
+ .bapla-spacer { height: 2rem; }
165
+ .bapla-spacer[data-size='sm'] { height: 1rem; }
166
+ .bapla-spacer[data-size='lg'] { height: 4rem; }
167
+ .bapla-spacer[data-size='xl'] { height: 8rem; }
168
+
169
+ /* Content --------------------------------------------------------------- */
170
+
171
+ .bapla-richtext > :last-child { margin-bottom: 0; }
172
+ .bapla-richtext blockquote {
173
+ border-left: 3px solid var(--color-primary-300, #93c5fd);
174
+ padding-left: 1rem;
175
+ color: var(--color-neutral-700, #374151);
176
+ }
177
+ .bapla-richtext pre {
178
+ overflow-x: auto;
179
+ padding: 1rem;
180
+ border-radius: var(--radius-base, 0.5rem);
181
+ background-color: var(--color-neutral-100, #f3f4f6);
182
+ }
183
+ .bapla-table { overflow-x: auto; }
184
+ .bapla-table table { border-collapse: collapse; width: 100%; }
185
+ .bapla-table th, .bapla-table td {
186
+ border: 1px solid var(--color-neutral-200, #e5e7eb);
187
+ padding: 0.5rem 0.75rem;
188
+ text-align: left;
189
+ }
190
+
191
+ .bapla-figure { margin: 0; }
192
+ .bapla-figure .bapla-media { width: 100%; border-radius: var(--radius-base, 0.5rem); }
193
+ .bapla-figure[data-aspect='16:9'] .bapla-media { aspect-ratio: 16 / 9; }
194
+ .bapla-figure[data-aspect='4:3'] .bapla-media { aspect-ratio: 4 / 3; }
195
+ .bapla-figure[data-aspect='1:1'] .bapla-media { aspect-ratio: 1; }
196
+ .bapla-figure[data-aspect='3:4'] .bapla-media { aspect-ratio: 3 / 4; }
197
+ .bapla-figure figcaption {
198
+ margin-top: 0.5rem;
199
+ font-size: 0.875em;
200
+ color: var(--color-neutral-600, #4b5563);
201
+ }
202
+
203
+ .bapla-button {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ gap: 0.5em;
208
+ padding: 0.625em 1.25em;
209
+ border: 2px solid transparent;
210
+ border-radius: var(--radius-base, 0.5rem);
211
+ font-weight: 600;
212
+ line-height: 1.2;
213
+ text-decoration: none;
214
+ cursor: pointer;
215
+ transition: background-color 150ms, color 150ms, border-color 150ms;
216
+ }
217
+ .bapla-button[data-size='sm'] { font-size: 0.875rem; }
218
+ .bapla-button[data-size='lg'] { font-size: 1.125rem; }
219
+ .bapla-button[data-variant='primary'] { background-color: var(--color-primary-600, #2563eb); color: #ffffff; }
220
+ .bapla-button[data-variant='primary']:hover { background-color: var(--color-primary-700, #1d4ed8); color: #ffffff; }
221
+ .bapla-button[data-variant='secondary'] { background-color: var(--color-secondary-600, #4b5563); color: #ffffff; }
222
+ .bapla-button[data-variant='secondary']:hover { background-color: var(--color-secondary-700, #374151); color: #ffffff; }
223
+ .bapla-button[data-variant='outline'] { border-color: currentColor; color: var(--color-primary-700, #1d4ed8); background: transparent; }
224
+ .bapla-button[data-variant='link'] { padding-inline: 0; color: var(--color-primary-700, #1d4ed8); text-decoration: underline; }
225
+ .bapla-button[aria-disabled='true'] { opacity: 0.6; cursor: default; }
226
+
227
+ .bapla-embed { position: relative; aspect-ratio: 16 / 9; }
228
+ .bapla-embed[data-aspect='4:3'] { aspect-ratio: 4 / 3; }
229
+ .bapla-embed[data-aspect='1:1'] { aspect-ratio: 1; }
230
+ .bapla-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
231
+
232
+ /* Interactive ----------------------------------------------------------- */
233
+
234
+ .bapla-carousel { position: relative; }
235
+ .bapla-carousel-controls { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 0.5rem; }
236
+ .bapla-carousel-controls button,
237
+ .bapla-tablist [role='tab'],
238
+ .bapla-accordion-item button,
239
+ .bapla-nav-toggle,
240
+ .bapla-nav-disclosure {
241
+ font: inherit;
242
+ color: inherit;
243
+ background: none;
244
+ border: 0;
245
+ cursor: pointer;
246
+ }
247
+ .bapla-carousel-controls button {
248
+ padding: 0.375rem 0.75rem;
249
+ border: 1px solid var(--color-neutral-300, #d1d5db);
250
+ border-radius: var(--radius-base, 0.5rem);
251
+ background-color: var(--color-background, #ffffff);
252
+ }
253
+ .bapla-carousel-dots { display: flex; justify-content: center; gap: 0.5rem; margin-top: 0.75rem; }
254
+ .bapla-carousel-dots button {
255
+ width: 0.75rem;
256
+ height: 0.75rem;
257
+ padding: 0;
258
+ border-radius: 9999px;
259
+ border: 0;
260
+ background-color: var(--color-neutral-300, #d1d5db);
261
+ cursor: pointer;
262
+ }
263
+ .bapla-carousel-dots button[aria-current='true'] { background-color: var(--color-primary-600, #2563eb); }
264
+
265
+ .bapla-tablist { display: flex; gap: 0.25rem; border-bottom: 1px solid var(--color-neutral-200, #e5e7eb); margin-bottom: 1rem; overflow-x: auto; }
266
+ .bapla-tabs[data-alignment='center'] .bapla-tablist { justify-content: center; }
267
+ .bapla-tabs[data-alignment='stretch'] [role='tab'] { flex: 1; }
268
+ .bapla-tablist [role='tab'] { padding: 0.625rem 1rem; border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; }
269
+ .bapla-tablist [role='tab'][aria-selected='true'] { border-bottom-color: var(--color-primary-600, #2563eb); color: var(--color-primary-700, #1d4ed8); font-weight: 600; }
270
+
271
+ .bapla-accordion-item { border-bottom: 1px solid var(--color-neutral-200, #e5e7eb); }
272
+ .bapla-accordion-item h3 { margin: 0; font-size: inherit; }
273
+ .bapla-accordion-item button { display: flex; width: 100%; justify-content: space-between; padding: 1rem 0; text-align: left; font-weight: 600; }
274
+ .bapla-accordion-item button::after { content: '+'; }
275
+ .bapla-accordion-item button[aria-expanded='true']::after { content: '−'; }
276
+ .bapla-accordion-panel { padding-bottom: 1rem; }
277
+
278
+ /* Navigation ------------------------------------------------------------ */
279
+
280
+ .bapla-nav { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; }
281
+ .bapla-nav-brand { margin-right: auto; }
282
+ .bapla-nav ul { list-style: none; margin: 0; padding: 0; }
283
+ .bapla-nav-list { display: flex; align-items: center; gap: 0.25rem; }
284
+ .bapla-nav-item { position: relative; display: flex; flex-wrap: wrap; align-items: center; }
285
+ .bapla-nav-link,
286
+ .bapla-nav-disclosure { display: inline-flex; align-items: center; padding: 0.5rem 0.75rem; color: inherit; text-decoration: none; border-radius: var(--radius-base, 0.5rem); }
287
+ .bapla-nav-link:hover,
288
+ .bapla-nav-disclosure:hover { background-color: var(--color-neutral-100, #f3f4f6); }
289
+ .bapla-nav-item[data-active='true'] > .bapla-nav-link,
290
+ .bapla-nav-link[aria-current='page'] { color: var(--color-primary-700, #1d4ed8); font-weight: 600; }
291
+ .bapla-nav-caret { width: 0.5em; height: 0.5em; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(45deg) translateY(-25%); }
292
+ .bapla-nav-heading { display: block; padding: 0.5rem 0.75rem; font-weight: 600; font-size: 0.875em; color: var(--color-neutral-600, #4b5563); }
293
+ .bapla-nav-toggle { display: none; padding: 0.5rem 0.75rem; border: 1px solid var(--color-neutral-300, #d1d5db); border-radius: var(--radius-base, 0.5rem); }
294
+
295
+ .bapla-nav-submenu,
296
+ .bapla-mega {
297
+ position: absolute;
298
+ top: 100%;
299
+ left: 0;
300
+ z-index: 50;
301
+ min-width: 14rem;
302
+ padding: 0.5rem;
303
+ background-color: var(--color-background, #ffffff);
304
+ border: 1px solid var(--color-neutral-200, #e5e7eb);
305
+ border-radius: var(--radius-base, 0.5rem);
306
+ box-shadow: 0 10px 30px -10px rgb(0 0 0 / 0.2);
307
+ }
308
+ .bapla-nav-submenu .bapla-nav-submenu { position: static; border: 0; box-shadow: none; padding: 0 0 0 0.75rem; }
309
+ .bapla-nav-submenu .bapla-nav-item { display: block; }
310
+ .bapla-nav-submenu .bapla-nav-link { display: flex; }
311
+ .bapla-nav-item[data-kind='panel'] { position: static; }
312
+ .bapla-mega { left: 0; right: 0; padding: 1.5rem; }
313
+
314
+ .bapla-menu-links,
315
+ .bapla-menu-links ul { list-style: none; margin: 0; padding: 0; }
316
+ .bapla-menu-links a { display: block; padding: 0.25rem 0; text-decoration: none; color: inherit; }
317
+ .bapla-menu-links a:hover { color: var(--color-primary-700, #1d4ed8); }
318
+ .bapla-menu-heading { display: block; font-weight: 600; margin-top: 0.5rem; }
319
+
320
+ @media (max-width: 767.98px) {
321
+ .bapla-nav-toggle { display: inline-flex; }
322
+ .bapla-nav-list { display: none; flex-basis: 100%; flex-direction: column; align-items: stretch; }
323
+ .bapla-nav-list[data-open='true'] { display: flex; }
324
+ .bapla-nav-submenu,
325
+ .bapla-mega { position: static; box-shadow: none; border: 0; padding: 0 0 0 1rem; min-width: 0; }
326
+ }
327
+
328
+ .bapla-breadcrumb ol { display: flex; flex-wrap: wrap; list-style: none; margin: 0; padding: 0; font-size: 0.875em; }
329
+ .bapla-breadcrumb li + li::before { content: '/'; margin-inline: 0.5rem; color: var(--color-neutral-400, #9ca3af); }
330
+ .bapla-breadcrumb [aria-current='page'] { color: var(--color-neutral-600, #4b5563); }
331
+ }
@@ -0,0 +1,69 @@
1
+ // src/media/urls.ts
2
+ var MEDIA_WIDTHS = [320, 480, 640, 768, 1024, 1280, 1536, 1920];
3
+ var MEDIA_ID = /^web_med_[0-9a-f-]{36}$/;
4
+ function mediaUrl(media, transform = {}) {
5
+ const base = media.url !== "" ? media.url : `/_media/${media.id}/${encodeURIComponent(media.filename)}`;
6
+ const params = new URLSearchParams();
7
+ for (const key of ["w", "h", "fit", "fm", "q"]) {
8
+ const value = transform[key];
9
+ if (value !== void 0) params.set(key, String(value));
10
+ }
11
+ const query = params.toString();
12
+ return query === "" ? base : `${base}${base.includes("?") ? "&" : "?"}${query}`;
13
+ }
14
+ function readMedia(value, resolve) {
15
+ if (typeof value === "string") return MEDIA_ID.test(value) ? resolve(value) : null;
16
+ if (value === null || typeof value !== "object") return null;
17
+ const media = value;
18
+ if (typeof media.id !== "string" || typeof media.url !== "string") return null;
19
+ if (!media.url.startsWith("/") || media.url.startsWith("//")) return null;
20
+ return {
21
+ id: media.id,
22
+ url: media.url,
23
+ filename: typeof media.filename === "string" ? media.filename : "",
24
+ mime: typeof media.mime === "string" ? media.mime : "",
25
+ width: typeof media.width === "number" ? media.width : null,
26
+ height: typeof media.height === "number" ? media.height : null,
27
+ alt: typeof media.alt === "string" ? media.alt : null,
28
+ focal_point: media.focal_point ?? null
29
+ };
30
+ }
31
+ function objectPosition(focal) {
32
+ if (!focal) return void 0;
33
+ const pct = (v) => `${Math.round(Math.min(1, Math.max(0, v)) * 100)}%`;
34
+ return `${pct(focal.x)} ${pct(focal.y)}`;
35
+ }
36
+
37
+ // src/media/imgproxy.ts
38
+ function imgproxyPath(media, transform = {}) {
39
+ const options = [];
40
+ const fit = transform.fit ?? "inside";
41
+ if (transform.w !== void 0 || transform.h !== void 0) {
42
+ const kind = fit === "cover" ? "fill" : "fit";
43
+ options.push(`rs:${kind}:${Math.round(transform.w ?? 0)}:${Math.round(transform.h ?? 0)}:0`);
44
+ }
45
+ if (fit === "cover") {
46
+ const focal = media.focal_point;
47
+ options.push(focal ? `g:fp:${number(focal.x)}:${number(focal.y)}` : "g:ce");
48
+ }
49
+ options.push(`q:${transform.q ?? 80}`);
50
+ let path = `/${options.join("/")}/plain/${media.id}/${encodeURIComponent(media.filename)}`;
51
+ if (transform.fm !== void 0) path += `@${transform.fm === "jpeg" ? "jpg" : transform.fm}`;
52
+ return path;
53
+ }
54
+ function isTransformable(media) {
55
+ return ["image/jpeg", "image/png", "image/webp", "image/avif"].includes(media.mime);
56
+ }
57
+ function number(value) {
58
+ const fixed = Math.min(1, Math.max(0, value)).toFixed(4);
59
+ return fixed.replace(/0+$/, "").replace(/\.$/, "") || "0";
60
+ }
61
+
62
+ export {
63
+ MEDIA_WIDTHS,
64
+ mediaUrl,
65
+ readMedia,
66
+ objectPosition,
67
+ imgproxyPath,
68
+ isTransformable
69
+ };