create-bearminion 1.2.0 → 1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # create-bearminion
2
2
 
3
+ ## 1.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 755bad0: The Media section's video cover is a responsive image (a WebP `srcset` from 480 to 1920 pixels wide), so a phone no longer downloads the full-size poster, and it loads at once instead of lazily when the section is one of the first two on the page, where it is often the largest thing on screen.
8
+ - 0a2301e: The package pages link to the new website and documentation: the npm homepage is https://bearminion.com, and the READMEs (including the template's) point to https://docs.bearminion.com instead of Markdown files on GitHub.
9
+
3
10
  ## 1.2.0
4
11
 
5
12
  ### Minor Changes
package/README.md CHANGED
@@ -58,8 +58,10 @@ The [default BearMinion template](https://github.com/langehelp-software/bearmini
58
58
 
59
59
  ## Learn more
60
60
 
61
+ - [bearminion.com](https://bearminion.com), with a demo of the editor
62
+ - [Getting started](https://docs.bearminion.com/getting-started/): create, edit and put your site online
63
+ - [Documentation](https://docs.bearminion.com)
61
64
  - [BearMinion on GitHub](https://github.com/langehelp-software/bearminion)
62
- - [Documentation](https://github.com/langehelp-software/bearminion/blob/main/docs/README.md)
63
65
 
64
66
  ## License
65
67
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "create-bearminion",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Create a BearMinion site: an Astro website you edit by clicking on it.",
5
5
  "license": "MIT",
6
6
  "author": "Jakub Lange",
7
- "homepage": "https://github.com/langehelp-software/bearminion#readme",
7
+ "homepage": "https://bearminion.com",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/langehelp-software/bearminion.git",
@@ -200,7 +200,7 @@ A variant change, a new, duplicated, moved, or deleted section, and a sidebar va
200
200
 
201
201
  Header links nest up to three levels (`MAX_NAV_DEPTH`). Desktop navigation uses hover and focus flyouts. The mobile menu uses an accordion. The link picker lists pages, `#anchor` on the current page, `/page#anchor` on other pages, and a custom URL.
202
202
 
203
- The API is `/__bearminion/api` on localhost. Saving `content/*.json` asks the editor to reload. If the draft is still dirty, the editor asks to save, discard, or cancel first. Unsaved drafts are kept in `localStorage` until save or discard. An upload does not reload by itself, so the draft can preview the new file before save. Request and response shapes, with curl examples, are in the [Dev API docs](https://github.com/langehelp-software/bearminion/blob/main/docs/dev-api.md), and every sidebar tab is described in [the editor docs](https://github.com/langehelp-software/bearminion/blob/main/docs/editor.md).
203
+ The API is `/__bearminion/api` on localhost. Saving `content/*.json` asks the editor to reload. If the draft is still dirty, the editor asks to save, discard, or cancel first. Unsaved drafts are kept in `localStorage` until save or discard. An upload does not reload by itself, so the draft can preview the new file before save. Request and response shapes, with curl examples, are in the [Dev API docs](https://docs.bearminion.com/dev-api/), and every sidebar tab is described in [the editor docs](https://docs.bearminion.com/editor/).
204
204
 
205
205
  ```bash
206
206
  pnpm dev
@@ -19,7 +19,7 @@
19
19
  "@fontsource/jetbrains-mono": "^5.3.0",
20
20
  "@tailwindcss/vite": "^4.1.12",
21
21
  "astro": "^7.3.8",
22
- "bearminion": "^1.2.0",
22
+ "bearminion": "^1.2.1",
23
23
  "class-variance-authority": "^0.7.1",
24
24
  "sharp": "^0.35.5",
25
25
  "tailwind-merge": "^3.7.0",
@@ -29,6 +29,8 @@ const player = {
29
29
  image: props.image,
30
30
  label: props.label,
31
31
  imageField: field('image.alt'),
32
+ // The first two sections are usually on screen at load, where a lazy cover would hold back the largest paint.
33
+ eager: index <= 1,
32
34
  }
33
35
  ---
34
36
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import type { Image } from 'bearminion'
3
- import { imageUrl } from '../../../lib/image-url'
3
+ import { imageSrcset, imageUrl } from '../../../lib/image-url'
4
4
  import { cn } from '../../../lib/cn'
5
5
  import MediaFrame from '../../molecules/MediaFrame.astro'
6
6
  import type { MediaProps } from './definition'
@@ -15,10 +15,12 @@ interface Props {
15
15
  label?: string
16
16
  /** `data-field` path for the image, so a click on the frame opens the image picker. */
17
17
  imageField: string
18
+ /** Near the top of the page: the cover loads at once instead of lazily, since it is likely the largest thing on screen. */
19
+ eager?: boolean
18
20
  class?: string
19
21
  }
20
22
 
21
- const { kind, aspect, video, poster, image, label, imageField, class: className } = Astro.props
23
+ const { kind, aspect, video, poster, image, label, imageField, eager = false, class: className } = Astro.props
22
24
  const ASPECT = { '16:9': 'aspect-video', '4:3': 'aspect-[4/3]', '21:9': 'aspect-[21/9]', '1:1': 'aspect-square' } as const
23
25
  const frame = cn(
24
26
  'relative w-full min-h-48 overflow-hidden rounded-[clamp(1rem,2cqi,1.5rem)] bg-surface shadow-[inset_0_0_0_1px_var(--line),0_40px_80px_-48px_oklch(0.2_0.02_255/0.35)]',
@@ -29,6 +31,9 @@ const source = kind === 'video' && video ? parseMediaSource(video) : null
29
31
  const hasImage = kind === 'image' && Boolean(image?.src) && image?.src !== 'placeholder' && !image?.hidden
30
32
  const empty = kind === 'video' ? !source : !hasImage
31
33
  const cover = source ? ((await imageUrl(poster)) ?? (source.kind === 'youtube' ? source.thumbnail : undefined)) : undefined
34
+ const coverSrcset = source ? await imageSrcset(poster) : undefined
35
+ // The frame is at most 68rem wide and the full width of a phone.
36
+ const coverSizes = '(min-width: 72rem) 68rem, 100vw'
32
37
  const name = label || 'Video'
33
38
  ---
34
39
 
@@ -68,7 +73,17 @@ const name = label || 'Video'
68
73
  data-label={name}
69
74
  data-poster={source.kind === 'file' ? cover : undefined}
70
75
  >
71
- {cover && <img src={cover} alt="" class="absolute inset-0 size-full object-cover" loading="lazy" decoding="async" />}
76
+ {cover && (
77
+ <img
78
+ src={cover}
79
+ srcset={coverSrcset}
80
+ sizes={coverSrcset ? coverSizes : undefined}
81
+ alt=""
82
+ class="absolute inset-0 size-full object-cover"
83
+ loading={eager ? 'eager' : 'lazy'}
84
+ decoding="async"
85
+ />
86
+ )}
72
87
  <button
73
88
  type="button"
74
89
  class="group absolute inset-0 grid cursor-pointer place-items-center bg-[oklch(0.15_0.01_255/0.12)] transition-colors hover:bg-[oklch(0.15_0.01_255/0.22)] focus-visible:outline-2 focus-visible:outline-offset-[-4px] focus-visible:outline-accent"
@@ -7,6 +7,23 @@ import { isRemote, resolveLocalAsset } from './assets'
7
7
  * optimized to WebP; a file uploaded in this dev session is served by the editor's dev route; a remote URL is used as
8
8
  * it is. A missing file in `src/assets` fails the build, like a missing `<Image>`.
9
9
  */
10
+ /** Widths for a responsive `srcset`; none wider than the file itself. */
11
+ const SRCSET_WIDTHS = [480, 768, 1080, 1440, 1920]
12
+
13
+ /**
14
+ * An image as a WebP `srcset`, for a plain `<img>` that is not an Astro `<Image>`, such as a video's cover. Undefined
15
+ * when the image is not a file in `src/assets` (a remote URL or a dev upload): then `imageUrl` alone is used.
16
+ */
17
+ export async function imageSrcset(image: Image | undefined): Promise<string | undefined> {
18
+ if (!image || image.hidden || !image.src || image.src === 'placeholder' || isRemote(image.src)) return undefined
19
+ const asset = resolveLocalAsset(image.src.replace(/^\//, ''))
20
+ if (!asset) return undefined
21
+ const widths = SRCSET_WIDTHS.filter((width) => width < asset.width)
22
+ widths.push(Math.min(asset.width, SRCSET_WIDTHS.at(-1) ?? asset.width))
23
+ const sources = await Promise.all(widths.map(async (width) => `${(await getImage({ src: asset, width, format: 'webp' })).src} ${width}w`))
24
+ return sources.join(', ')
25
+ }
26
+
10
27
  export async function imageUrl(image: Image | undefined, maxWidth = 1920): Promise<string | undefined> {
11
28
  if (!image || image.hidden || !image.src || image.src === 'placeholder') return undefined
12
29
  if (isRemote(image.src)) return image.src