@bapla/site-theme 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -2
- package/dist/base.css +331 -0
- package/dist/chunk-RYCW4YWV.js +69 -0
- package/dist/imgproxy-rioGvvyv.d.ts +189 -0
- package/dist/index.d.ts +220 -0
- package/dist/index.js +904 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +31 -0
- package/package.json +65 -4
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
|
-
#
|
|
1
|
+
# @bapla/site-theme
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
};
|