@bytesbrains/weblocks 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.
Files changed (109) hide show
  1. package/AGENT.md +214 -0
  2. package/CATALOG.md +387 -0
  3. package/CHANGELOG.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +294 -0
  6. package/catalog.json +1850 -0
  7. package/lib/blocks/about.d.ts +2 -0
  8. package/lib/blocks/about.js +50 -0
  9. package/lib/blocks/accordion.d.ts +2 -0
  10. package/lib/blocks/accordion.js +51 -0
  11. package/lib/blocks/announcementBar.d.ts +2 -0
  12. package/lib/blocks/announcementBar.js +56 -0
  13. package/lib/blocks/appShell.d.ts +2 -0
  14. package/lib/blocks/appShell.js +54 -0
  15. package/lib/blocks/auth.d.ts +2 -0
  16. package/lib/blocks/auth.js +82 -0
  17. package/lib/blocks/blogList.d.ts +2 -0
  18. package/lib/blocks/blogList.js +81 -0
  19. package/lib/blocks/blogPost.d.ts +2 -0
  20. package/lib/blocks/blogPost.js +60 -0
  21. package/lib/blocks/carousel.d.ts +2 -0
  22. package/lib/blocks/carousel.js +56 -0
  23. package/lib/blocks/contactDetails.d.ts +2 -0
  24. package/lib/blocks/contactDetails.js +50 -0
  25. package/lib/blocks/contactForm.d.ts +2 -0
  26. package/lib/blocks/contactForm.js +100 -0
  27. package/lib/blocks/cta.d.ts +2 -0
  28. package/lib/blocks/cta.js +40 -0
  29. package/lib/blocks/divider.d.ts +2 -0
  30. package/lib/blocks/divider.js +28 -0
  31. package/lib/blocks/faq.d.ts +2 -0
  32. package/lib/blocks/faq.js +52 -0
  33. package/lib/blocks/features.d.ts +2 -0
  34. package/lib/blocks/features.js +58 -0
  35. package/lib/blocks/feed.d.ts +2 -0
  36. package/lib/blocks/feed.js +81 -0
  37. package/lib/blocks/footer.d.ts +2 -0
  38. package/lib/blocks/footer.js +40 -0
  39. package/lib/blocks/gallery.d.ts +2 -0
  40. package/lib/blocks/gallery.js +64 -0
  41. package/lib/blocks/hero.d.ts +2 -0
  42. package/lib/blocks/hero.js +48 -0
  43. package/lib/blocks/heroApp.d.ts +2 -0
  44. package/lib/blocks/heroApp.js +65 -0
  45. package/lib/blocks/logos.d.ts +2 -0
  46. package/lib/blocks/logos.js +59 -0
  47. package/lib/blocks/map.d.ts +2 -0
  48. package/lib/blocks/map.js +43 -0
  49. package/lib/blocks/nav.d.ts +2 -0
  50. package/lib/blocks/nav.js +39 -0
  51. package/lib/blocks/newsletter.d.ts +2 -0
  52. package/lib/blocks/newsletter.js +56 -0
  53. package/lib/blocks/pricing.d.ts +2 -0
  54. package/lib/blocks/pricing.js +89 -0
  55. package/lib/blocks/prose.d.ts +5 -0
  56. package/lib/blocks/prose.js +46 -0
  57. package/lib/blocks/richText.d.ts +2 -0
  58. package/lib/blocks/richText.js +46 -0
  59. package/lib/blocks/services.d.ts +2 -0
  60. package/lib/blocks/services.js +65 -0
  61. package/lib/blocks/sidebar.d.ts +2 -0
  62. package/lib/blocks/sidebar.js +58 -0
  63. package/lib/blocks/socialLinks.d.ts +2 -0
  64. package/lib/blocks/socialLinks.js +53 -0
  65. package/lib/blocks/spacer.d.ts +2 -0
  66. package/lib/blocks/spacer.js +25 -0
  67. package/lib/blocks/split.d.ts +2 -0
  68. package/lib/blocks/split.js +67 -0
  69. package/lib/blocks/stats.d.ts +2 -0
  70. package/lib/blocks/stats.js +64 -0
  71. package/lib/blocks/steps.d.ts +2 -0
  72. package/lib/blocks/steps.js +63 -0
  73. package/lib/blocks/tabs.d.ts +2 -0
  74. package/lib/blocks/tabs.js +57 -0
  75. package/lib/blocks/team.d.ts +2 -0
  76. package/lib/blocks/team.js +94 -0
  77. package/lib/blocks/testimonials.d.ts +2 -0
  78. package/lib/blocks/testimonials.js +54 -0
  79. package/lib/blocks/timeline.d.ts +2 -0
  80. package/lib/blocks/timeline.js +53 -0
  81. package/lib/blocks/video.d.ts +2 -0
  82. package/lib/blocks/video.js +61 -0
  83. package/lib/catalog.d.ts +26 -0
  84. package/lib/catalog.js +62 -0
  85. package/lib/generate.d.ts +35 -0
  86. package/lib/generate.js +160 -0
  87. package/lib/index.d.ts +19 -0
  88. package/lib/index.js +11 -0
  89. package/lib/ops.d.ts +85 -0
  90. package/lib/ops.js +248 -0
  91. package/lib/presets.d.ts +16 -0
  92. package/lib/presets.js +50 -0
  93. package/lib/pwa.d.ts +30 -0
  94. package/lib/pwa.js +79 -0
  95. package/lib/registry.d.ts +53 -0
  96. package/lib/registry.js +81 -0
  97. package/lib/render.d.ts +8 -0
  98. package/lib/render.js +105 -0
  99. package/lib/runtime.d.ts +68 -0
  100. package/lib/runtime.js +46 -0
  101. package/lib/schema.d.ts +67 -0
  102. package/lib/schema.js +150 -0
  103. package/lib/tokens.d.ts +37 -0
  104. package/lib/tokens.js +136 -0
  105. package/lib/types.d.ts +108 -0
  106. package/lib/types.js +10 -0
  107. package/lib/validate.d.ts +10 -0
  108. package/lib/validate.js +48 -0
  109. package/package.json +65 -0
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const about: BlockSpec;
@@ -0,0 +1,50 @@
1
+ /** `about` — story / mission prose with an optional side image. Static brick. */
2
+ import { escapeAttr, escapeHtml, sanitizeUrl } from '../schema.js';
3
+ const schema = {
4
+ eyebrow: { kind: 'string', default: '', max: 60 },
5
+ title: { kind: 'string', default: 'About us', max: 120 },
6
+ body: { kind: 'string', default: '', max: 1200 },
7
+ image: { kind: 'string', default: '', max: 500 },
8
+ imageAlt: { kind: 'string', default: '', max: 200 },
9
+ imageSide: { kind: 'enum', values: ['left', 'right'], default: 'right' },
10
+ };
11
+ const css = `
12
+ .blk-about{padding:var(--space-lg) var(--space);background:var(--bg);color:var(--text)}
13
+ .blk-about .wrap{max-width:1080px;margin:0 auto;display:grid;grid-template-columns:1fr 1fr;gap:calc(var(--space)*2);align-items:center}
14
+ .blk-about.no-img .wrap{grid-template-columns:min(64ch,100%);justify-content:center;text-align:center}
15
+ .blk-about.img-left .media{order:-1}
16
+ .blk-about .eyebrow{color:var(--accent);font-weight:700;letter-spacing:.04em;text-transform:uppercase;font-size:var(--fs-base);margin:0 0 .4em}
17
+ .blk-about h2{font-size:var(--fs-xl);margin:0 0 .4em;font-weight:800}
18
+ .blk-about p.body{color:var(--muted);font-size:var(--fs-lg);margin:0;white-space:pre-line}
19
+ .blk-about .media img{width:100%;border-radius:var(--radius);display:block;object-fit:cover}
20
+ @media(max-width:760px){.blk-about .wrap{grid-template-columns:1fr;text-align:left}.blk-about.img-left .media{order:0}}
21
+ `.trim();
22
+ function render(config) {
23
+ const eyebrow = config.eyebrow;
24
+ const title = config.title;
25
+ const body = config.body;
26
+ const image = sanitizeUrl(config.image);
27
+ const hasImg = image !== '#' && !!config.image;
28
+ const alt = config.imageAlt;
29
+ const side = config.imageSide;
30
+ const text = `<div class="text">
31
+ ${eyebrow ? `<p class="eyebrow">${escapeHtml(eyebrow)}</p>` : ''}
32
+ ${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
33
+ ${body ? `<p class="body">${escapeHtml(body)}</p>` : ''}
34
+ </div>`;
35
+ const media = hasImg ? `<div class="media"><img src="${escapeAttr(image)}" alt="${escapeAttr(alt)}" loading="lazy"></div>` : '';
36
+ const cls = `blk-about${hasImg ? (side === 'left' ? ' img-left' : '') : ' no-img'}`;
37
+ return `<section class="${cls}" aria-label="${escapeAttr(title || 'About')}">
38
+ <div class="wrap">
39
+ ${text}
40
+ ${media}
41
+ </div>
42
+ </section>`;
43
+ }
44
+ export const about = {
45
+ type: 'about',
46
+ description: 'A story/mission section: eyebrow, title, prose body, and an optional side image (choose which side).',
47
+ schema,
48
+ css,
49
+ render,
50
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const accordion: BlockSpec;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `accordion` — generic disclosure panels built on native `<details>/<summary>`
3
+ * (no JavaScript, no island). FAQ is a specialization of this pattern; use this
4
+ * for any collapsible content. Static brick.
5
+ */
6
+ import { escapeAttr, escapeHtml } from '../schema.js';
7
+ const schema = {
8
+ title: { kind: 'string', default: '', max: 120 },
9
+ items: {
10
+ kind: 'array', max: 24,
11
+ of: {
12
+ kind: 'object',
13
+ fields: {
14
+ heading: { kind: 'string', required: true, default: '', max: 200 },
15
+ body: { kind: 'string', default: '', max: 1200 },
16
+ },
17
+ },
18
+ },
19
+ };
20
+ const css = `
21
+ .blk-accordion{padding:var(--space-lg) var(--space);background:var(--bg);color:var(--text)}
22
+ .blk-accordion .wrap{max-width:760px;margin:0 auto}
23
+ .blk-accordion h2{font-size:var(--fs-xl);margin:0 0 var(--space);font-weight:800;text-align:center}
24
+ .blk-accordion details{border:1px solid color-mix(in srgb,var(--text) 15%,transparent);border-radius:var(--radius);background:var(--surface);margin-bottom:.6em;overflow:hidden}
25
+ .blk-accordion summary{cursor:pointer;list-style:none;padding:.9em 1em;font-weight:700;display:flex;justify-content:space-between;align-items:center;gap:1em}
26
+ .blk-accordion summary::-webkit-details-marker{display:none}
27
+ .blk-accordion summary::after{content:"+";color:var(--primary);font-weight:800;font-size:1.2em;transition:transform var(--motion)}
28
+ .blk-accordion details[open] summary::after{content:"–"}
29
+ .blk-accordion .body{padding:0 1em 1em;color:var(--muted);line-height:1.6;white-space:pre-line}
30
+ `.trim();
31
+ function render(config) {
32
+ const title = config.title;
33
+ const items = (config.items ?? []).filter((it) => it && it.heading);
34
+ const panels = items.map((it) => `<details>
35
+ <summary>${escapeHtml(it.heading)}</summary>
36
+ ${it.body ? `<div class="body">${escapeHtml(it.body)}</div>` : ''}
37
+ </details>`).join('\n ');
38
+ return `<section class="blk-accordion" aria-label="${escapeAttr(title || 'Accordion')}">
39
+ <div class="wrap">
40
+ ${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
41
+ ${panels}
42
+ </div>
43
+ </section>`;
44
+ }
45
+ export const accordion = {
46
+ type: 'accordion',
47
+ description: 'Generic collapsible disclosure panels (native details/summary, no JavaScript); each panel has a heading and body.',
48
+ schema,
49
+ css,
50
+ render,
51
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const announcementBar: BlockSpec;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `announcement-bar` — dismissible top strip (promo / notice). Static markup;
3
+ * the dismiss button is a client "island" (declared via `island`). Renders
4
+ * fine without JS — the strip simply stays put.
5
+ */
6
+ import { escapeAttr, escapeHtml, sanitizeUrl } from '../schema.js';
7
+ const schema = {
8
+ text: { kind: 'string', required: true, default: '', max: 200 },
9
+ linkLabel: { kind: 'string', default: '', max: 40 },
10
+ href: { kind: 'string', default: '', max: 500 },
11
+ tone: { kind: 'enum', values: ['info', 'promo', 'warning'], default: 'info' },
12
+ dismissible: { kind: 'boolean', default: true },
13
+ };
14
+ const css = `
15
+ .blk-announcement-bar{width:100%;background:var(--surface);color:var(--text);font-family:var(--font);font-size:var(--fs-base);border-bottom:1px solid color-mix(in srgb, var(--text) 15%, transparent)}
16
+ .blk-announcement-bar.tone-info{background:color-mix(in srgb, var(--primary) 12%, var(--surface))}
17
+ .blk-announcement-bar.tone-promo{background:color-mix(in srgb, var(--accent) 16%, var(--surface))}
18
+ .blk-announcement-bar.tone-warning{background:color-mix(in srgb, var(--accent) 22%, var(--surface))}
19
+ .blk-announcement-bar .wrap{max-width:1080px;margin:0 auto;display:flex;align-items:center;justify-content:center;gap:.6em;padding:.55em var(--space);text-align:center}
20
+ .blk-announcement-bar .msg{margin:0}
21
+ .blk-announcement-bar a{color:var(--primary);font-weight:700;text-decoration:underline;text-underline-offset:2px}
22
+ .blk-announcement-bar a:hover,.blk-announcement-bar a:focus-visible{color:var(--accent)}
23
+ .blk-announcement-bar .close{flex:0 0 auto;margin-left:auto;background:none;border:0;color:inherit;font-size:1.2em;line-height:1;cursor:pointer;padding:.15em .4em;border-radius:var(--radius);opacity:.7;transition:opacity var(--motion),background var(--motion)}
24
+ .blk-announcement-bar .close:hover,.blk-announcement-bar .close:focus-visible{opacity:1;background:color-mix(in srgb, var(--text) 12%, transparent)}
25
+ @media(max-width:440px){.blk-announcement-bar .wrap{flex-wrap:wrap;gap:.3em;font-size:calc(var(--fs-base)*.95)}}
26
+ `.trim();
27
+ const TONES = new Set(['info', 'promo', 'warning']);
28
+ function render(config) {
29
+ const text = config.text;
30
+ // Nothing to announce → render an empty but valid landmark (total render).
31
+ const linkLabel = config.linkLabel;
32
+ const href = escapeAttr(sanitizeUrl(config.href));
33
+ const hasLink = !!linkLabel && href !== '#';
34
+ const toneRaw = config.tone;
35
+ const tone = TONES.has(toneRaw) ? toneRaw : 'info';
36
+ const dismissible = config.dismissible;
37
+ const link = hasLink ? ` <a href="${href}">${escapeHtml(linkLabel)}</a>` : '';
38
+ const close = dismissible
39
+ ? `<button type="button" class="close" data-wl-dismiss aria-label="Dismiss">&times;</button>`
40
+ : '';
41
+ return `<section class="blk-announcement-bar tone-${tone}" aria-label="Announcement">
42
+ <div class="wrap">
43
+ <p class="msg">${escapeHtml(text)}${link}</p>
44
+ ${close}
45
+ </div>
46
+ </section>`;
47
+ }
48
+ export const announcementBar = {
49
+ type: 'announcement-bar',
50
+ description: 'A dismissible full-width strip for a short promo or notice, with an optional inline link and info/promo/warning tone.',
51
+ schema,
52
+ css,
53
+ render,
54
+ // Dismiss is JS-enhanced; the strip renders and reads fine with no JS.
55
+ island: 'announcement-bar',
56
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const appShell: BlockSpec;
@@ -0,0 +1,54 @@
1
+ /** `app-shell` — persistent bottom tab bar (app-like nav). Static brick. */
2
+ import { escapeAttr, escapeHtml, sanitizeUrl } from '../schema.js';
3
+ const schema = {
4
+ brand: { kind: 'string', default: '', max: 60 },
5
+ tabs: {
6
+ kind: 'array',
7
+ max: 6,
8
+ of: {
9
+ kind: 'object',
10
+ fields: {
11
+ label: { kind: 'string', required: true, default: '', max: 40 },
12
+ href: { kind: 'string', default: '', max: 500 },
13
+ icon: { kind: 'string', default: '', max: 8 }, // emoji or a single glyph
14
+ },
15
+ },
16
+ },
17
+ };
18
+ const css = `
19
+ .blk-app-shell{position:sticky;bottom:0;left:0;right:0;z-index:20;background:var(--surface);color:var(--text);border-top:1px solid color-mix(in srgb, var(--text) 15%, transparent);font-family:var(--font)}
20
+ .blk-app-shell .wrap{max-width:720px;margin:0 auto;display:flex;align-items:stretch;justify-content:space-around;gap:.2em}
21
+ .blk-app-shell .brand{display:none}
22
+ .blk-app-shell a{flex:1 1 0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.15em;padding:.5em .3em;text-decoration:none;color:var(--muted);font-size:var(--fs-base);text-align:center;border-radius:var(--radius);transition:color var(--motion),background var(--motion)}
23
+ .blk-app-shell a:hover,.blk-app-shell a:focus-visible{color:var(--primary);background:color-mix(in srgb, var(--primary) 10%, transparent)}
24
+ .blk-app-shell .ico{font-size:1.4em;line-height:1}
25
+ .blk-app-shell .lbl{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
26
+ @media(max-width:440px){.blk-app-shell a{font-size:calc(var(--fs-base)*.9);padding:.45em .2em}.blk-app-shell .ico{font-size:1.3em}}
27
+ `.trim();
28
+ function tabItem(t) {
29
+ // Drop tabs with no label rather than emit an empty target (total render).
30
+ if (!t.label)
31
+ return '';
32
+ const href = escapeAttr(sanitizeUrl(t.href || '#'));
33
+ return `<a href="${href}">
34
+ ${t.icon ? `<span class="ico" aria-hidden="true">${escapeHtml(t.icon)}</span>` : ''}
35
+ <span class="lbl">${escapeHtml(t.label)}</span>
36
+ </a>`;
37
+ }
38
+ function render(config) {
39
+ const brand = config.brand;
40
+ const tabs = (config.tabs ?? []).map(tabItem).filter(Boolean);
41
+ return `<nav class="blk-app-shell" aria-label="${escapeAttr(brand || 'App navigation')}">
42
+ <div class="wrap">
43
+ ${brand ? `<span class="brand">${escapeHtml(brand)}</span>` : ''}
44
+ ${tabs.join('\n ')}
45
+ </div>
46
+ </nav>`;
47
+ }
48
+ export const appShell = {
49
+ type: 'app-shell',
50
+ description: 'A persistent bottom tab bar for app-like navigation, each tab an optional icon plus a label linking to a view.',
51
+ schema,
52
+ css,
53
+ render,
54
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const auth: BlockSpec;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `auth` — provider-agnostic sign in / sign up. Powered brick (§6): each provider
3
+ * button and the optional email form target the host's `auth.start` capability
4
+ * (the block carries `data-wl-provider` so one runtime route serves them all).
5
+ * With no runtime wired it renders inert-but-valid. The engine bundles NO auth
6
+ * backend — identity is entirely the host's concern.
7
+ */
8
+ import { escapeAttr, escapeHtml } from '../schema.js';
9
+ import { NOOP_RUNTIME, safeMethod } from '../runtime.js';
10
+ const CAPABILITY = 'auth.start';
11
+ const schema = {
12
+ mode: { kind: 'enum', values: ['signin', 'signup'], default: 'signin' },
13
+ title: { kind: 'string', default: '', max: 120 },
14
+ providers: {
15
+ kind: 'array', max: 6,
16
+ of: {
17
+ kind: 'object',
18
+ fields: {
19
+ label: { kind: 'string', required: true, default: '', max: 40 },
20
+ provider: { kind: 'string', default: '', max: 40 }, // e.g. google, github, email
21
+ },
22
+ },
23
+ },
24
+ showEmail: { kind: 'boolean', default: true },
25
+ };
26
+ const css = `
27
+ .blk-auth{padding:var(--space-lg) var(--space);background:var(--bg);color:var(--text)}
28
+ .blk-auth .wrap{max-width:400px;margin:0 auto;background:var(--surface);border-radius:var(--radius);padding:calc(var(--space)*1.6);text-align:center}
29
+ .blk-auth h2{font-size:var(--fs-lg);margin:0 0 var(--space);font-weight:800}
30
+ .blk-auth .providers{display:flex;flex-direction:column;gap:.5em}
31
+ .blk-auth .provider{font:inherit;font-weight:600;cursor:pointer;text-decoration:none;display:block;padding:.7em 1em;border-radius:var(--radius);border:1px solid color-mix(in srgb,var(--text) 18%,transparent);background:var(--bg);color:var(--text);transition:border-color var(--motion)}
32
+ .blk-auth .provider:hover{border-color:var(--primary)}
33
+ .blk-auth .provider[disabled]{opacity:.55;cursor:not-allowed}
34
+ .blk-auth .sep{display:flex;align-items:center;gap:.6em;color:var(--muted);font-size:var(--fs-base);margin:var(--space) 0}
35
+ .blk-auth .sep::before,.blk-auth .sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,var(--text) 15%,transparent)}
36
+ .blk-auth form{display:grid;gap:.5em}
37
+ .blk-auth input{font:inherit;color:inherit;background:var(--bg);border:1px solid color-mix(in srgb,var(--text) 18%,transparent);border-radius:var(--radius);padding:.7em .8em}
38
+ .blk-auth button{font:inherit;font-weight:700;cursor:pointer;border:0;border-radius:var(--radius);padding:.7em 1em;background:var(--primary);color:var(--on-primary)}
39
+ .blk-auth button[disabled]{opacity:.55;cursor:not-allowed}
40
+ .blk-auth .note{margin:var(--space) 0 0;font-size:var(--fs-base);color:var(--muted)}
41
+ `.trim();
42
+ function render(config, _tokens, ctx) {
43
+ const id = ctx?.id ?? 'auth';
44
+ const runtime = ctx?.runtime ?? NOOP_RUNTIME;
45
+ const mode = config.mode === 'signup' ? 'signup' : 'signin';
46
+ const title = config.title || (mode === 'signup' ? 'Create your account' : 'Sign in');
47
+ const providers = config.providers ?? [];
48
+ const showEmail = config.showEmail !== false;
49
+ const action = runtime.resolve(CAPABILITY, id);
50
+ const inert = action ? '' : ' data-wl-inert="true"';
51
+ // `action.url` comes from the host adapter; it is escaped here, but the host
52
+ // is responsible for returning a legitimate endpoint (it is trusted config).
53
+ const buttons = providers.filter((p) => p && p.label).map((p) => {
54
+ const prov = escapeAttr(p.provider || p.label);
55
+ return action
56
+ ? `<form method="${safeMethod(action.method)}" action="${escapeAttr(action.url)}" data-wl-capability="${CAPABILITY}" data-wl-block="${escapeAttr(id)}" data-wl-provider="${prov}"><input type="hidden" name="provider" value="${prov}"><button class="provider" type="submit">${escapeHtml(p.label)}</button></form>`
57
+ : `<button class="provider" type="button" data-wl-capability="${CAPABILITY}" data-wl-provider="${prov}" disabled>${escapeHtml(p.label)}</button>`;
58
+ }).join('\n ');
59
+ const email = showEmail ? `
60
+ ${buttons ? '<div class="sep">or</div>' : ''}
61
+ <form method="${safeMethod(action?.method)}" action="${action ? escapeAttr(action.url) : '#'}" data-wl-capability="${CAPABILITY}" data-wl-block="${escapeAttr(id)}" data-wl-provider="email"${inert}>
62
+ <input type="email" name="email" placeholder="you@example.com" aria-label="Email address" required>
63
+ <button type="submit"${action ? '' : ' disabled'}>Continue with email</button>
64
+ </form>` : '';
65
+ return `<section class="blk-auth" aria-label="${escapeAttr(title)}">
66
+ <div class="wrap">
67
+ <h2>${escapeHtml(title)}</h2>
68
+ ${buttons ? `<div class="providers">\n ${buttons}\n </div>` : ''}
69
+ ${email}
70
+ ${action ? '' : '<p class="note">Connect an auth runtime to enable sign in.</p>'}
71
+ </div>
72
+ </section>`;
73
+ }
74
+ export const auth = {
75
+ type: 'auth',
76
+ description: 'A provider-agnostic sign in / sign up panel: social provider buttons and an optional email form that start auth via a host runtime.',
77
+ schema,
78
+ css,
79
+ render,
80
+ island: 'auth',
81
+ runtime: { capabilities: [CAPABILITY] },
82
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const blogList: BlockSpec;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `blog-list` — a post index as a card grid. The post list is static config for
3
+ * now (a host can later feed it from a store via the runtime contract). Static
4
+ * brick.
5
+ */
6
+ import { escapeAttr, escapeHtml, sanitizeUrl } from '../schema.js';
7
+ const schema = {
8
+ title: { kind: 'string', default: '', max: 120 },
9
+ columns: { kind: 'int', oneOf: [2, 3], default: 3 },
10
+ posts: {
11
+ kind: 'array', max: 24,
12
+ of: {
13
+ kind: 'object',
14
+ fields: {
15
+ title: { kind: 'string', required: true, default: '', max: 160 },
16
+ excerpt: { kind: 'string', default: '', max: 280 },
17
+ href: { kind: 'string', default: '', max: 500 },
18
+ image: { kind: 'string', default: '', max: 500 },
19
+ date: { kind: 'string', default: '', max: 40 },
20
+ tag: { kind: 'string', default: '', max: 40 },
21
+ },
22
+ },
23
+ },
24
+ };
25
+ const css = `
26
+ .blk-blog-list{padding:var(--space-lg) var(--space);background:var(--bg);color:var(--text)}
27
+ .blk-blog-list .wrap{max-width:1120px;margin:0 auto}
28
+ .blk-blog-list h2{font-size:var(--fs-xl);margin:0 0 var(--space-lg);font-weight:800;text-align:center}
29
+ .blk-blog-list .grid{display:grid;grid-template-columns:repeat(var(--cols,3),1fr);gap:calc(var(--space)*1.4)}
30
+ .blk-blog-list .card{display:flex;flex-direction:column;background:var(--surface);border-radius:var(--radius);overflow:hidden}
31
+ .blk-blog-list a.card{text-decoration:none;color:inherit;transition:transform var(--motion)}
32
+ .blk-blog-list a.card:hover{transform:translateY(-3px)}
33
+ .blk-blog-list .cover{aspect-ratio:16/9;object-fit:cover;width:100%;background:color-mix(in srgb,var(--muted) 15%,transparent)}
34
+ .blk-blog-list .body{padding:calc(var(--space)*1.1);display:flex;flex-direction:column;gap:.4em}
35
+ .blk-blog-list .meta{display:flex;gap:.6em;color:var(--muted);font-size:var(--fs-base)}
36
+ .blk-blog-list .tag{color:var(--accent);font-weight:700}
37
+ .blk-blog-list h3{margin:0;font-size:var(--fs-lg);font-weight:700;line-height:1.3}
38
+ .blk-blog-list p{margin:0;color:var(--muted);line-height:1.5}
39
+ @media(max-width:760px){.blk-blog-list .grid{grid-template-columns:1fr 1fr}}
40
+ @media(max-width:440px){.blk-blog-list .grid{grid-template-columns:1fr}}
41
+ `.trim();
42
+ function card(p) {
43
+ if (!p.title)
44
+ return '';
45
+ const href = sanitizeUrl(p.href);
46
+ const linked = href !== '#' && !!p.href;
47
+ const img = sanitizeUrl(p.image);
48
+ const cover = (img !== '#' && p.image) ? `<img class="cover" src="${escapeAttr(img)}" alt="${escapeAttr(p.title)}" loading="lazy">` : '';
49
+ const meta = (p.tag || p.date)
50
+ ? `<div class="meta">${p.tag ? `<span class="tag">${escapeHtml(p.tag)}</span>` : ''}${p.date ? `<span>${escapeHtml(p.date)}</span>` : ''}</div>`
51
+ : '';
52
+ const inner = `${cover}
53
+ <div class="body">
54
+ ${meta}
55
+ <h3>${escapeHtml(p.title)}</h3>
56
+ ${p.excerpt ? `<p>${escapeHtml(p.excerpt)}</p>` : ''}
57
+ </div>`;
58
+ return linked
59
+ ? `<a class="card" href="${escapeAttr(href)}">${inner}</a>`
60
+ : `<article class="card">${inner}</article>`;
61
+ }
62
+ function render(config) {
63
+ const title = config.title;
64
+ const columns = Number.isInteger(config.columns) ? config.columns : 3;
65
+ const posts = (config.posts ?? []).map(card).filter(Boolean);
66
+ return `<section class="blk-blog-list" aria-label="${escapeAttr(title || 'Posts')}">
67
+ <div class="wrap">
68
+ ${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
69
+ <div class="grid" style="--cols:${columns}">
70
+ ${posts.join('\n ')}
71
+ </div>
72
+ </div>
73
+ </section>`;
74
+ }
75
+ export const blogList = {
76
+ type: 'blog-list',
77
+ description: 'A post index as a grid of cards, each with an optional cover image, tag, date, title, and excerpt linking to the post.',
78
+ schema,
79
+ css,
80
+ render,
81
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const blogPost: BlockSpec;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `blog-post` — a single article layout. The body is TYPED content blocks
3
+ * (heading / paragraph / quote / bullet), never raw HTML, so it stays safe and
4
+ * editable. Static brick.
5
+ */
6
+ import { escapeAttr, escapeHtml, sanitizeUrl } from '../schema.js';
7
+ import { renderProse } from './prose.js';
8
+ const schema = {
9
+ title: { kind: 'string', required: true, default: '', max: 200 },
10
+ date: { kind: 'string', default: '', max: 40 },
11
+ author: { kind: 'string', default: '', max: 80 },
12
+ cover: { kind: 'string', default: '', max: 500 },
13
+ coverAlt: { kind: 'string', default: '', max: 200 },
14
+ body: {
15
+ kind: 'array', max: 120,
16
+ of: {
17
+ kind: 'object',
18
+ fields: {
19
+ kind: { kind: 'enum', values: ['heading', 'paragraph', 'quote', 'bullet'], default: 'paragraph' },
20
+ text: { kind: 'string', required: true, default: '', max: 2000 },
21
+ },
22
+ },
23
+ },
24
+ };
25
+ const css = `
26
+ .blk-blog-post{padding:var(--space-lg) var(--space);background:var(--bg);color:var(--text)}
27
+ .blk-blog-post article{max-width:68ch;margin:0 auto}
28
+ .blk-blog-post .cover{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius);margin:0 0 var(--space)}
29
+ .blk-blog-post h1{font-size:var(--fs-xl);margin:0 0 .3em;font-weight:800;line-height:1.15}
30
+ .blk-blog-post .meta{color:var(--muted);font-size:var(--fs-base);margin:0 0 var(--space-lg)}
31
+ .blk-blog-post h2{font-size:var(--fs-lg);margin:1.2em 0 .3em;font-weight:700}
32
+ .blk-blog-post p{margin:0 0 1em;font-size:var(--fs-lg);line-height:1.7}
33
+ .blk-blog-post blockquote{margin:1.2em 0;padding:.4em 0 .4em 1em;border-left:3px solid var(--accent);color:var(--muted);font-style:italic}
34
+ .blk-blog-post ul{margin:0 0 1em;padding-left:1.4em}
35
+ .blk-blog-post li{margin:.3em 0;font-size:var(--fs-lg);line-height:1.6}
36
+ `.trim();
37
+ function render(config) {
38
+ const title = config.title;
39
+ const date = config.date;
40
+ const author = config.author;
41
+ const cover = sanitizeUrl(config.cover);
42
+ const hasCover = cover !== '#' && !!config.cover;
43
+ const body = renderProse(config.body);
44
+ const meta = [author && `By ${escapeHtml(author)}`, date && escapeHtml(date)].filter(Boolean).join(' · ');
45
+ return `<section class="blk-blog-post" aria-label="${escapeAttr(title || 'Article')}">
46
+ <article>
47
+ ${hasCover ? `<img class="cover" src="${escapeAttr(cover)}" alt="${escapeAttr(config.coverAlt)}" loading="lazy">` : ''}
48
+ ${title ? `<h1>${escapeHtml(title)}</h1>` : ''}
49
+ ${meta ? `<p class="meta">${meta}</p>` : ''}
50
+ ${body}
51
+ </article>
52
+ </section>`;
53
+ }
54
+ export const blogPost = {
55
+ type: 'blog-post',
56
+ description: 'A single article layout with an optional cover, title, author/date, and a typed body (headings, paragraphs, quotes, bullets).',
57
+ schema,
58
+ css,
59
+ render,
60
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const carousel: BlockSpec;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `carousel` — a horizontal scroll-snap track of slides. Works fully without JS
3
+ * (native scroll-snap); the `carousel` island can add arrows/autoplay. Static-
4
+ * first: no behaviour ships unless the island is present.
5
+ */
6
+ import { escapeAttr, escapeHtml, sanitizeUrl } from '../schema.js';
7
+ const schema = {
8
+ title: { kind: 'string', default: '', max: 120 },
9
+ items: {
10
+ kind: 'array', max: 20,
11
+ of: {
12
+ kind: 'object',
13
+ fields: {
14
+ src: { kind: 'string', default: '', max: 500 },
15
+ alt: { kind: 'string', default: '', max: 200 },
16
+ caption: { kind: 'string', default: '', max: 200 },
17
+ },
18
+ },
19
+ },
20
+ autoplay: { kind: 'boolean', default: false },
21
+ };
22
+ const css = `
23
+ .blk-carousel{padding:var(--space-lg) var(--space);background:var(--bg)}
24
+ .blk-carousel .track{display:flex;gap:var(--space);overflow-x:auto;scroll-snap-type:x mandatory;max-width:1120px;margin:0 auto;padding-bottom:.5em}
25
+ .blk-carousel .slide{flex:0 0 min(88%,720px);scroll-snap-align:center;background:var(--surface);border-radius:var(--radius);overflow:hidden}
26
+ .blk-carousel img{display:block;width:100%;aspect-ratio:16/9;object-fit:cover}
27
+ .blk-carousel figcaption{padding:.6em .8em;color:var(--muted);font-size:var(--fs-base)}
28
+ .blk-carousel .track::-webkit-scrollbar{height:8px}
29
+ `.trim();
30
+ function slide(it) {
31
+ const src = sanitizeUrl(it.src);
32
+ if (src === '#' || !it.src)
33
+ return '';
34
+ return `<figure class="slide">
35
+ <img src="${escapeAttr(src)}" alt="${escapeAttr(it.alt)}" loading="lazy">
36
+ ${it.caption ? `<figcaption>${escapeHtml(it.caption)}</figcaption>` : ''}
37
+ </figure>`;
38
+ }
39
+ function render(config) {
40
+ const title = config.title;
41
+ const items = (config.items ?? []).map(slide).filter(Boolean);
42
+ const autoplay = config.autoplay === true;
43
+ return `<section class="blk-carousel" aria-label="${escapeAttr(title || 'Carousel')}"${autoplay ? ' data-wl-autoplay="true"' : ''}>
44
+ <div class="track">
45
+ ${items.join('\n ')}
46
+ </div>
47
+ </section>`;
48
+ }
49
+ export const carousel = {
50
+ type: 'carousel',
51
+ description: 'A horizontal, swipeable carousel of image slides with optional captions; scrolls natively and can be enhanced with arrows/autoplay.',
52
+ schema,
53
+ css,
54
+ render,
55
+ island: 'carousel',
56
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const contactDetails: BlockSpec;
@@ -0,0 +1,50 @@
1
+ /** `contact-details` — address / phone / email / hours. Static brick. */
2
+ import { escapeAttr, escapeHtml, sanitizeUrl } from '../schema.js';
3
+ const schema = {
4
+ title: { kind: 'string', default: 'Get in touch', max: 120 },
5
+ address: { kind: 'string', default: '', max: 200 },
6
+ phone: { kind: 'string', default: '', max: 40 },
7
+ email: { kind: 'string', default: '', max: 120 },
8
+ hours: { kind: 'string', default: '', max: 160 },
9
+ };
10
+ const css = `
11
+ .blk-contact{padding:var(--space-lg) var(--space);background:var(--surface);color:var(--text)}
12
+ .blk-contact .wrap{max-width:760px;margin:0 auto}
13
+ .blk-contact h2{font-size:var(--fs-xl);margin:0 0 var(--space);font-weight:800}
14
+ .blk-contact dl{display:grid;grid-template-columns:auto 1fr;gap:.5em 1.2em;margin:0}
15
+ .blk-contact dt{color:var(--muted);font-weight:600}
16
+ .blk-contact dd{margin:0}
17
+ .blk-contact a{color:var(--primary);text-decoration:none}
18
+ `.trim();
19
+ function row(label, value, href) {
20
+ if (!value)
21
+ return '';
22
+ const v = href ? `<a href="${escapeAttr(sanitizeUrl(href))}">${escapeHtml(value)}</a>` : escapeHtml(value);
23
+ return `<dt>${escapeHtml(label)}</dt><dd>${v}</dd>`;
24
+ }
25
+ function render(config) {
26
+ const title = config.title;
27
+ const address = config.address;
28
+ const phone = config.phone;
29
+ const email = config.email;
30
+ const hours = config.hours;
31
+ const rows = [
32
+ row('Address', address),
33
+ row('Phone', phone, phone ? `tel:${phone.replace(/[^+\d]/g, '')}` : undefined),
34
+ row('Email', email, email ? `mailto:${email}` : undefined),
35
+ row('Hours', hours),
36
+ ].filter(Boolean).join('\n ');
37
+ return `<section class="blk-contact" aria-label="${escapeAttr(title || 'Contact')}">
38
+ <div class="wrap">
39
+ ${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
40
+ <dl>
41
+ ${rows}
42
+ </dl>
43
+ </div>
44
+ </section>`;
45
+ }
46
+ export const contactDetails = {
47
+ type: 'contact-details',
48
+ description: 'A contact info block: address, phone, email, and opening hours (each optional). Read-only — no form.',
49
+ schema, css, render,
50
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const contactForm: BlockSpec;