@publier/shell 0.2.0 → 0.4.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.
@@ -4,7 +4,7 @@ import { navConfig, type NavVariant } from 'virtual:publier-nav-config';
4
4
  import type { TopNavProps } from './types.ts';
5
5
 
6
6
  const EMPTY: NavVariant = { brand: '', logo: null, tabs: [], links: [], cta: null };
7
- const { variant = 'default', currentPath = '', variantConfig } = Astro.props as TopNavProps;
7
+ const { variant = 'default', currentPath = '', variantConfig, navVariant } = Astro.props as TopNavProps;
8
8
  const v = variantConfig ?? navConfig.variants[variant] ?? navConfig.variants.default ?? EMPTY;
9
9
  const [searchSlotHtml, themeSlotHtml, mobileSlotHtml] = await Promise.all([
10
10
  Astro.slots.has('search') ? Astro.slots.render('search') : Promise.resolve(null),
@@ -12,7 +12,7 @@ const [searchSlotHtml, themeSlotHtml, mobileSlotHtml] = await Promise.all([
12
12
  Astro.slots.has('mobile') ? Astro.slots.render('mobile') : Promise.resolve(null),
13
13
  ]);
14
14
  const html = shellRenderTopNav(JSON.stringify({
15
- variant, currentPath, v,
15
+ variant, navVariant, currentPath, v,
16
16
  searchEnabled: navConfig.searchEnabled,
17
17
  searchSlotHtml, themeSlotHtml, mobileSlotHtml,
18
18
  }));
@@ -479,12 +479,16 @@ export interface TopNavProps {
479
479
  variantConfig?: import('virtual:publier-nav-config').NavVariant;
480
480
  /** Current page path for active highlighting. Pass `Astro.url.pathname`. */
481
481
  currentPath?: string;
482
+ /** Navigation style: `"mega"` renders group panels; anything else is the simple dropdown nav. */
483
+ navVariant?: string;
482
484
  }
483
485
 
484
486
  export interface TopNavMobileProps {
485
487
  variant?: string;
486
488
  variantConfig?: import('virtual:publier-nav-config').NavVariant;
487
489
  currentPath?: string;
490
+ /** Same value as `TopNavProps.navVariant`. */
491
+ navVariant?: string;
488
492
  }
489
493
 
490
494
  // ---------------------------------------------------------------------------
@@ -12,6 +12,7 @@ import { feeds } from 'virtual:publier-feeds';
12
12
  import { fonts } from 'virtual:publier-fonts';
13
13
  import { navConfig, type NavVariant } from 'virtual:publier-nav-config';
14
14
  import { site } from 'virtual:publier-site';
15
+ import { fullPreset, variants } from 'virtual:publier-theme-variants';
15
16
 
16
17
  interface Props { title: string; description?: string; nav?: string | false; navConfig?: NavVariant; class?: string; lang?: string }
17
18
  const { title, description, nav: navProp, navConfig: resolvedNav, class: className, lang = Astro.currentLocale ?? 'en' } = Astro.props;
@@ -28,6 +29,10 @@ if (navProp !== undefined) {
28
29
  : (JSON.parse(matched) as string);
29
30
  }
30
31
 
32
+ const htmlHooks = fullPreset
33
+ ? Object.fromEntries([['data-full-preset', ''], ...Object.entries(variants).map(([k, v]) => [`data-variant-${k}`, v])])
34
+ : {};
35
+
31
36
  const emailProtectionStart = '<!--email_off-->';
32
37
  const emailProtectionEnd = '<!--/email_off-->';
33
38
  const storageKey = announcement?.storageKey ?? 'publier-announcement';
@@ -38,7 +43,7 @@ const [head, bodyOpen, bodyClose] = JSON.parse(shellRenderBaseLayout(JSON.string
38
43
  }))) as [string, string, string];
39
44
  ---
40
45
  <!doctype html>
41
- <html lang={lang}>
46
+ <html lang={lang} {...htmlHooks}>
42
47
  <head>
43
48
  <Fragment set:html={head} />
44
49
  <Font cssVariable="--publier-font-builtin-sans" preload />
@@ -59,10 +64,10 @@ const [head, bodyOpen, bodyClose] = JSON.parse(shellRenderBaseLayout(JSON.string
59
64
  <slot name="skip" />
60
65
  <Fragment set:html={bodyOpen} />
61
66
  {navVariant !== false && (
62
- <TopNav variant={navVariant} variantConfig={resolvedNav} currentPath={currentPath}>
67
+ <TopNav variant={navVariant} variantConfig={resolvedNav} currentPath={currentPath} navVariant={variants.nav}>
63
68
  {navConfig.searchEnabled && <SearchButton slot="search" />}
64
69
  <ThemeToggle slot="theme" default={appearance.default} strict={appearance.strict} />
65
- <TopNavMobile slot="mobile" variant={navVariant} variantConfig={resolvedNav} currentPath={currentPath} />
70
+ <TopNavMobile slot="mobile" variant={navVariant} variantConfig={resolvedNav} currentPath={currentPath} navVariant={variants.nav} />
66
71
  </TopNav>
67
72
  )}
68
73
  <slot />
@@ -193,3 +193,122 @@
193
193
  --color-pl-sidebar-active-fg: var(--color-primary);
194
194
  --color-pl-sidebar-border: var(--color-pl-border);
195
195
  }
196
+
197
+ /* ─── Full-preset base layer ────────────────────────────────
198
+ * Every element a full preset can set, with neutral defaults, so a preset or
199
+ * `theme.yaml` can override any one of them and the colour-only presets keep
200
+ * looking as they do today. Cascade: this base, then the preset's theme file,
201
+ * then `theme.yaml` overrides, then `customCss`.
202
+ *
203
+ * The consuming rules below apply only when the document carries
204
+ * `data-full-preset` (a full preset, or a `theme.yaml` that sets one of these
205
+ * sections). Renderers emit the same markup either way; presets switch
206
+ * components through `data-variant-*` hooks on <html>. */
207
+ @layer publier-theme {
208
+ :root {
209
+ --publier-radius-button: var(--radius-field, 0.5rem);
210
+ --publier-radius-control: var(--radius-field, 0.5rem);
211
+ --publier-radius-card: var(--radius-box, 0.75rem);
212
+ --publier-radius-panel: var(--radius-box, 0.75rem);
213
+ --publier-radius-chip: var(--radius-selector, 0.5rem);
214
+
215
+ --publier-space-xxs: 0.125rem;
216
+ --publier-space-xs: 0.25rem;
217
+ --publier-space-sm: 0.5rem;
218
+ --publier-space-md: 0.75rem;
219
+ --publier-space-base: 1rem;
220
+ --publier-space-lg: 1.5rem;
221
+ --publier-space-xl: 2rem;
222
+ --publier-space-xxl: 3rem;
223
+ --publier-space-section: 4rem;
224
+
225
+ --publier-elevation-flat: none;
226
+ --publier-elevation-raised: var(--shadow-sm);
227
+ --publier-elevation-overlay: var(--shadow-lg);
228
+
229
+ --publier-motion-duration-fast: 100ms;
230
+ --publier-motion-duration-base: 150ms;
231
+ --publier-motion-duration-slow: 240ms;
232
+ --publier-motion-easing: cubic-bezier(0.4, 0, 0.2, 1);
233
+ }
234
+
235
+ html[data-full-preset] {
236
+ --publier-transition-duration: var(--publier-motion-duration-base);
237
+ --publier-transition-timing: var(--publier-motion-easing);
238
+ --publier-card-radius: var(--publier-radius-card);
239
+ --publier-control-radius: var(--publier-radius-control);
240
+ --publier-surface-radius: var(--publier-radius-panel);
241
+ }
242
+
243
+ html[data-full-preset] .prose :where(h1) {
244
+ font-size: var(--publier-scale-display-lg-size);
245
+ font-weight: var(--publier-scale-display-lg-weight);
246
+ line-height: var(--publier-scale-display-lg-line-height);
247
+ letter-spacing: var(--publier-scale-display-lg-tracking);
248
+ }
249
+ html[data-full-preset] .prose :where(h2) {
250
+ font-size: var(--publier-scale-heading-lg-size);
251
+ font-weight: var(--publier-scale-heading-lg-weight);
252
+ line-height: var(--publier-scale-heading-lg-line-height);
253
+ letter-spacing: var(--publier-scale-heading-lg-tracking);
254
+ }
255
+ html[data-full-preset] .prose :where(h3) {
256
+ font-size: var(--publier-scale-heading-md-size);
257
+ font-weight: var(--publier-scale-heading-md-weight);
258
+ line-height: var(--publier-scale-heading-md-line-height);
259
+ letter-spacing: var(--publier-scale-heading-md-tracking);
260
+ }
261
+ html[data-full-preset] .prose :where(h4) {
262
+ font-size: var(--publier-scale-heading-sm-size);
263
+ font-weight: var(--publier-scale-heading-sm-weight);
264
+ line-height: var(--publier-scale-heading-sm-line-height);
265
+ }
266
+ html[data-full-preset] .prose :where(p, li) {
267
+ font-size: var(--publier-scale-body-md-size);
268
+ line-height: var(--publier-scale-body-md-line-height);
269
+ }
270
+ html[data-full-preset] .prose :where(.lead) {
271
+ font-size: var(--publier-scale-body-lg-size);
272
+ line-height: var(--publier-scale-body-lg-line-height);
273
+ }
274
+ html[data-full-preset] .btn {
275
+ font-size: var(--publier-scale-label-size);
276
+ font-weight: var(--publier-scale-label-weight);
277
+ border-radius: var(--publier-radius-button);
278
+ }
279
+ html[data-full-preset] .badge {
280
+ border-radius: var(--publier-radius-chip);
281
+ }
282
+ html[data-full-preset] .card {
283
+ border-radius: var(--publier-radius-card);
284
+ }
285
+ html[data-full-preset] :where(.dropdown-content, .menu.dropdown-content, [popover]) {
286
+ box-shadow: var(--publier-elevation-overlay);
287
+ border-radius: var(--publier-radius-card);
288
+ }
289
+ html[data-full-preset] :where(section, .publier-section) {
290
+ --publier-section-gap: var(--publier-space-section);
291
+ }
292
+
293
+ /* Component variants. */
294
+ html[data-full-preset][data-variant-button="pill"] .btn {
295
+ border-radius: 9999px;
296
+ }
297
+ html[data-full-preset][data-variant-button="square"] .btn {
298
+ border-radius: 0;
299
+ }
300
+ html[data-full-preset][data-variant-card="elevated"] .card {
301
+ box-shadow: var(--publier-elevation-raised);
302
+ border-color: transparent;
303
+ }
304
+ html[data-full-preset][data-variant-card="filled"] .card {
305
+ background-color: var(--color-pl-muted-bg);
306
+ border-color: transparent;
307
+ }
308
+ html[data-full-preset][data-variant-card="hairline"] .card {
309
+ border: 1px solid var(--color-pl-border);
310
+ }
311
+ html[data-full-preset][data-variant-card="hairline"] .card:hover {
312
+ box-shadow: var(--publier-elevation-raised);
313
+ }
314
+ }
@@ -21,6 +21,13 @@ declare module 'virtual:publier-appearance' {
21
21
  }
22
22
 
23
23
  declare module 'virtual:publier-theme-css';
24
+
25
+ declare module 'virtual:publier-theme-variants' {
26
+ import type { VariantSet } from './tailwind/presets.ts';
27
+ /** True for a full preset, or when `theme.yaml` sets a non-colour element. */
28
+ export const fullPreset: boolean;
29
+ export const variants: VariantSet;
30
+ }
24
31
  declare module 'virtual:publier-custom-css';
25
32
 
26
33
  declare module 'virtual:publier-nav-config' {
@@ -29,6 +36,21 @@ declare module 'virtual:publier-nav-config' {
29
36
  href: string;
30
37
  external?: boolean;
31
38
  description?: string;
39
+ /** Mega menu: local path or https URL of a small decorative image. */
40
+ icon?: string;
41
+ }
42
+
43
+ /** Mega menu: a titled sub-group of a group; sub-groups nest up to three levels. */
44
+ export interface NavGroup {
45
+ label: string;
46
+ items?: NavItem[];
47
+ groups?: NavGroup[];
48
+ }
49
+
50
+ /** Mega menu: the column shared by every group's panel (featured links). */
51
+ export interface NavAside {
52
+ label?: string;
53
+ items: NavItem[];
32
54
  }
33
55
 
34
56
  /** A plain link, or a labelled group of one-level navigation items. */
@@ -37,6 +59,14 @@ declare module 'virtual:publier-nav-config' {
37
59
  href?: string;
38
60
  external?: boolean;
39
61
  items?: NavItem[];
62
+ /** Mega menu: titled sub-groups (a tree). */
63
+ groups?: NavGroup[];
64
+ /** Mega menu: sub-group columns on desktop, 1 to 4 (default 2). */
65
+ columns?: 1 | 2 | 3 | 4;
66
+ /** Mega menu: links under the group's body. */
67
+ footer?: NavItem[];
68
+ /** Mega menu: show a filter field in the group's panel. */
69
+ search?: boolean;
40
70
  }
41
71
 
42
72
  export interface NavCta {
@@ -69,6 +99,8 @@ declare module 'virtual:publier-nav-config' {
69
99
  links: NavLink[];
70
100
  /** Primary CTA button. Null when absent. */
71
101
  cta: NavCta | null;
102
+ /** Mega menu: featured links shown in every group's panel. */
103
+ aside?: NavAside;
72
104
  }
73
105
 
74
106
  /**
@@ -1 +0,0 @@
1
- const e=[`almond`,`aspen`,`catppuccin`,`dark`,`dusk`,`emerald`,`enterprise`,`light`,`maple`,`neutral`,`ocean`,`purple`,`ruby`,`solar`,`vitepress`];function t(){return e}function n(t){return e.includes(t)}export{n,t as r,e as t};