bsmnt 0.11.0 → 0.12.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 (50) hide show
  1. package/package.json +1 -1
  2. package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +36 -4
  3. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +7 -1
  4. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +7 -1
  5. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
  6. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  7. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  8. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  9. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +72 -34
  10. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  11. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  12. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +3 -0
  13. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +2 -0
  14. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
  15. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +7 -1
  16. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +36 -4
  17. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  18. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  19. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  20. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +72 -34
  21. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  22. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  23. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +3 -0
  24. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +2 -0
  25. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +5 -0
  26. package/src/templates/next-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
  27. package/src/templates/next-default/biome.json +2 -1
  28. package/src/templates/next-default/components/ui/image/index.tsx +20 -12
  29. package/src/templates/next-default/next.config.ts +10 -4
  30. package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
  31. package/src/templates/next-experiments/biome.json +2 -1
  32. package/src/templates/next-experiments/components/ui/image/index.tsx +20 -12
  33. package/src/templates/next-experiments/next.config.ts +10 -4
  34. package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -0
  35. package/src/templates/next-pagebuilder/biome.json +2 -1
  36. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +5 -0
  37. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  38. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +20 -12
  39. package/src/templates/next-pagebuilder/next.config.ts +4 -3
  40. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
  41. package/src/templates/next-pagebuilder-cache/biome.json +2 -1
  42. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +5 -0
  43. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  44. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +21 -12
  45. package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
  46. package/src/templates/next-pagebuilder-cache/next.config.ts +4 -3
  47. package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
  48. package/src/templates/next-webgl/biome.json +2 -1
  49. package/src/templates/next-webgl/components/ui/image/index.tsx +20 -12
  50. package/src/templates/next-webgl/next.config.ts +10 -4
@@ -98,7 +98,6 @@ const getFinalPlaceholder = (
98
98
  * Enhanced Image component with responsive sizing and automatic optimizations.
99
99
  *
100
100
  * Always use this component instead of next/image directly. Provides:
101
- * - Automatic responsive sizes generation
102
101
  * - Smart blur placeholders with aspect ratio support
103
102
  * - Performance optimizations (lazy loading by default)
104
103
  * - Priority flag for LCP images
@@ -147,14 +146,14 @@ export function Image({
147
146
  style,
148
147
  className,
149
148
  objectFit = "cover",
150
- quality = 90,
149
+ quality = 75,
151
150
  alt = "",
152
151
  fill,
153
152
  block = !fill,
154
- width = block ? 1 : undefined,
155
- height = block ? 1 : undefined,
156
- mobileSize = "100vw",
157
- desktopSize = "100vw",
153
+ width,
154
+ height,
155
+ mobileSize,
156
+ desktopSize,
158
157
  sizes,
159
158
  src,
160
159
  unoptimized,
@@ -164,9 +163,18 @@ export function Image({
164
163
  priority = false,
165
164
  ...props
166
165
  }: ImageProps) {
167
- // Generate responsive sizes if not provided
168
- const finalSizes =
169
- sizes || buildImageSizes({ base: mobileSize, desktop: desktopSize })
166
+ const responsiveSizes =
167
+ mobileSize || desktopSize
168
+ ? buildImageSizes({
169
+ base: mobileSize ?? "100vw",
170
+ desktop: desktopSize ?? mobileSize ?? "100vw",
171
+ })
172
+ : undefined
173
+
174
+ const hasIntrinsicSize = width !== undefined && height !== undefined
175
+ const finalSizes = sizes ?? responsiveSizes ?? (hasIntrinsicSize ? undefined : "100vw")
176
+ const finalWidth = width ?? (block ? 1 : undefined)
177
+ const finalHeight = height ?? (block ? 1 : undefined)
170
178
 
171
179
  // Early return after hooks
172
180
  if (!src) return null
@@ -194,8 +202,8 @@ export function Image({
194
202
  <NextImage
195
203
  ref={ref}
196
204
  fill={!block}
197
- {...(width !== undefined && { width })}
198
- {...(height !== undefined && { height })}
205
+ {...(finalWidth !== undefined && { width: finalWidth })}
206
+ {...(finalHeight !== undefined && { height: finalHeight })}
199
207
  priority={priority}
200
208
  quality={quality}
201
209
  alt={alt}
@@ -204,7 +212,7 @@ export function Image({
204
212
  ...style,
205
213
  }}
206
214
  className={cn(className, block && "block w-full")}
207
- sizes={finalSizes}
215
+ {...(finalSizes && { sizes: finalSizes })}
208
216
  src={src}
209
217
  unoptimized={unoptimized || isSvg}
210
218
  draggable={false}
@@ -77,7 +77,6 @@ const nextConfig: NextConfig = {
77
77
  ],
78
78
  },
79
79
  images: {
80
- dangerouslyAllowSVG: true,
81
80
  remotePatterns: [
82
81
  {
83
82
  protocol: "https",
@@ -85,9 +84,11 @@ const nextConfig: NextConfig = {
85
84
  },
86
85
  ],
87
86
  minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
88
- qualities: [90],
87
+ qualities: [75, 90, 100],
89
88
  formats: ["image/avif", "image/webp"],
90
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
89
+ deviceSizes: [
90
+ 640, 750, 828, 1080, 1200, 1320, 1620, 1920, 2048, 2560, 3024, 3456, 3840,
91
+ ],
91
92
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
92
93
  },
93
94
  headers: async () => [
@@ -0,0 +1,11 @@
1
+ language js
2
+
3
+ or {
4
+ `<Image $attrs />`,
5
+ `<SanityImage $attrs />`
6
+ } as $image where {
7
+ $attrs <: contains `fill`,
8
+ ! $attrs <: contains `fill={false}`,
9
+ ! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
10
+ register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
11
+ }
@@ -247,7 +247,8 @@
247
247
  "plugins": [
248
248
  "./.biome/plugins/no-anchor-element.grit",
249
249
  "./.biome/plugins/no-unnecessary-forwardref.grit",
250
- "./.biome/plugins/no-relative-parent-imports.grit"
250
+ "./.biome/plugins/no-relative-parent-imports.grit",
251
+ "./.biome/plugins/no-fill-without-sizes.grit"
251
252
  ],
252
253
  "vcs": {
253
254
  "clientKind": "git",
@@ -28,6 +28,9 @@ export function ArticleContent({ block }: ArticleContentProps) {
28
28
  <SanityImage
29
29
  image={author.avatar}
30
30
  alt={author.name || ""}
31
+ width={40}
32
+ height={40}
33
+ maxWidth={80}
31
34
  className="size-10 rounded-full object-cover"
32
35
  />
33
36
  ) : null}
@@ -50,6 +53,8 @@ export function ArticleContent({ block }: ArticleContentProps) {
50
53
  image={thumbnail}
51
54
  alt={thumbnail.alt || ""}
52
55
  priority
56
+ quality={90}
57
+ sizes="(min-width: 896px) 896px, 100vw"
53
58
  className="aspect-video w-full object-cover"
54
59
  />
55
60
  </div>
@@ -14,6 +14,7 @@ export const RelatedPostItem = ({ post }: { post: ArticleRelatedPost }) => {
14
14
  <SanityImage
15
15
  image={post.metadata.image}
16
16
  alt={post.title || ""}
17
+ sizes="(min-width: 768px) 768px, 100vw"
17
18
  className="aspect-[16/9] w-full object-cover transition-transform duration-300 group-hover:scale-105"
18
19
  />
19
20
  </div>
@@ -9,6 +9,7 @@
9
9
  import cn from "clsx"
10
10
  import NextImage, { type ImageProps as NextImageProps } from "next/image"
11
11
  import type { CSSProperties, Ref } from "react"
12
+ import { buildImageSizes } from "@/lib/utils/image-sizes"
12
13
 
13
14
  /**
14
15
  * Enhanced Image component props extending Next.js Image.
@@ -97,7 +98,6 @@ const getFinalPlaceholder = (
97
98
  * Enhanced Image component with responsive sizing and automatic optimizations.
98
99
  *
99
100
  * Always use this component instead of next/image directly. Provides:
100
- * - Automatic responsive sizes generation
101
101
  * - Smart blur placeholders with aspect ratio support
102
102
  * - Performance optimizations (lazy loading by default)
103
103
  * - Priority flag for LCP images
@@ -146,14 +146,14 @@ export function Image({
146
146
  style,
147
147
  className,
148
148
  objectFit = "cover",
149
- quality = 90,
149
+ quality = 75,
150
150
  alt = "",
151
151
  fill,
152
152
  block = !fill,
153
- width = block ? 1 : undefined,
154
- height = block ? 1 : undefined,
155
- mobileSize = "100vw",
156
- desktopSize = "100vw",
153
+ width,
154
+ height,
155
+ mobileSize,
156
+ desktopSize,
157
157
  sizes,
158
158
  src,
159
159
  unoptimized,
@@ -163,9 +163,18 @@ export function Image({
163
163
  priority = false,
164
164
  ...props
165
165
  }: ImageProps) {
166
- // Generate responsive sizes if not provided
167
- const finalSizes =
168
- sizes || `(max-width: 1440px) ${mobileSize}, ${desktopSize}`
166
+ const responsiveSizes =
167
+ mobileSize || desktopSize
168
+ ? buildImageSizes({
169
+ base: mobileSize ?? "100vw",
170
+ desktop: desktopSize ?? mobileSize ?? "100vw",
171
+ })
172
+ : undefined
173
+
174
+ const hasIntrinsicSize = width !== undefined && height !== undefined
175
+ const finalSizes = sizes ?? responsiveSizes ?? (hasIntrinsicSize ? undefined : "100vw")
176
+ const finalWidth = width ?? (block ? 1 : undefined)
177
+ const finalHeight = height ?? (block ? 1 : undefined)
169
178
 
170
179
  // Early return after hooks
171
180
  if (!src) return null
@@ -193,8 +202,8 @@ export function Image({
193
202
  <NextImage
194
203
  ref={ref}
195
204
  fill={!block}
196
- {...(width !== undefined && { width })}
197
- {...(height !== undefined && { height })}
205
+ {...(finalWidth !== undefined && { width: finalWidth })}
206
+ {...(finalHeight !== undefined && { height: finalHeight })}
198
207
  priority={priority}
199
208
  quality={quality}
200
209
  alt={alt}
@@ -203,7 +212,7 @@ export function Image({
203
212
  ...style,
204
213
  }}
205
214
  className={cn(className, block && "block w-full")}
206
- sizes={finalSizes}
215
+ {...(finalSizes && { sizes: finalSizes })}
207
216
  src={src}
208
217
  unoptimized={unoptimized || isSvg}
209
218
  draggable={false}
@@ -0,0 +1,72 @@
1
+ /** Mirrors the theme's `--breakpoint-*` scale, which clears Tailwind's defaults. */
2
+ export const BASEMENT_BREAKPOINTS = {
3
+ mobile: 640,
4
+ tablet: 768,
5
+ "tablet-lg": 1024,
6
+ desktop: 1440,
7
+ "desktop-large": 1920,
8
+ } as const
9
+
10
+ export type BreakpointMap = Readonly<Record<string, number>>
11
+
12
+ /** Mirrors the element's variants: `grid-cols-1 tablet:grid-cols-2` → `{ base: 1, tablet: 2 }`. */
13
+ export type ByBreakpoint<
14
+ T,
15
+ B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
16
+ > = { base: T } & Partial<Record<Extract<keyof B, string>, T>>
17
+
18
+ function columnWidth(columns: number): string {
19
+ return `${Math.round(100 / columns)}vw`
20
+ }
21
+
22
+ /**
23
+ * Widest first, since `sizes` takes the first matching condition and the last
24
+ * entry must be bare. `min-width` matches the Tailwind variant's boundary; a
25
+ * `max-width: <next> - 1` one would leave a fractional-pixel gap.
26
+ */
27
+ function toSizes(
28
+ base: string,
29
+ overrides: Readonly<Record<string, string | undefined>>,
30
+ scale: BreakpointMap
31
+ ): string {
32
+ const conditions: string[] = []
33
+
34
+ for (const [name, min] of Object.entries(scale).sort(
35
+ ([, a], [, b]) => b - a
36
+ )) {
37
+ const value = overrides[name]
38
+ if (value !== undefined) {
39
+ conditions.push(`(min-width: ${min}px) ${value}`)
40
+ }
41
+ }
42
+
43
+ return [...conditions, base].join(", ")
44
+ }
45
+
46
+ /** Builds `sizes` from the CSS length the image renders at per breakpoint. */
47
+ export function buildImageSizes<
48
+ B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
49
+ >(widths: ByBreakpoint<string, B>, breakpoints?: B): string {
50
+ const { base, ...overrides } = widths
51
+ return toSizes(base, overrides, breakpoints ?? BASEMENT_BREAKPOINTS)
52
+ }
53
+
54
+ /**
55
+ * Builds `sizes` from a grid's column count per breakpoint. Rounded: `sizes`
56
+ * only steers which srcset candidate the browser picks, and gaps make the real
57
+ * column narrower than `100 / N` anyway.
58
+ */
59
+ export function buildGridImageSizes<
60
+ B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
61
+ >(columns: ByBreakpoint<number, B>, breakpoints?: B): string {
62
+ const { base, ...overrides } = columns
63
+ const widths: Record<string, string> = {}
64
+
65
+ for (const [name, count] of Object.entries(overrides)) {
66
+ if (count !== undefined) {
67
+ widths[name] = columnWidth(count)
68
+ }
69
+ }
70
+
71
+ return toSizes(columnWidth(base), widths, breakpoints ?? BASEMENT_BREAKPOINTS)
72
+ }
@@ -82,7 +82,6 @@ const nextConfig: NextConfig = {
82
82
  ],
83
83
  },
84
84
  images: {
85
- dangerouslyAllowSVG: true,
86
85
  remotePatterns: [
87
86
  {
88
87
  protocol: "https",
@@ -90,9 +89,11 @@ const nextConfig: NextConfig = {
90
89
  },
91
90
  ],
92
91
  minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
93
- qualities: [90],
92
+ qualities: [75, 90, 100],
94
93
  formats: ["image/avif", "image/webp"],
95
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
94
+ deviceSizes: [
95
+ 640, 750, 828, 1080, 1200, 1320, 1620, 1920, 2048, 2560, 3024, 3456, 3840,
96
+ ],
96
97
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
97
98
  },
98
99
  headers: async () => [
@@ -0,0 +1,11 @@
1
+ language js
2
+
3
+ or {
4
+ `<Image $attrs />`,
5
+ `<SanityImage $attrs />`
6
+ } as $image where {
7
+ $attrs <: contains `fill`,
8
+ ! $attrs <: contains `fill={false}`,
9
+ ! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
10
+ register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
11
+ }
@@ -234,7 +234,8 @@
234
234
  "plugins": [
235
235
  "./.biome/plugins/no-anchor-element.grit",
236
236
  "./.biome/plugins/no-unnecessary-forwardref.grit",
237
- "./.biome/plugins/no-relative-parent-imports.grit"
237
+ "./.biome/plugins/no-relative-parent-imports.grit",
238
+ "./.biome/plugins/no-fill-without-sizes.grit"
238
239
  ],
239
240
  "vcs": {
240
241
  "clientKind": "git",
@@ -98,7 +98,6 @@ const getFinalPlaceholder = (
98
98
  * Enhanced Image component with responsive sizing and automatic optimizations.
99
99
  *
100
100
  * Always use this component instead of next/image directly. Provides:
101
- * - Automatic responsive sizes generation
102
101
  * - Smart blur placeholders with aspect ratio support
103
102
  * - Performance optimizations (lazy loading by default)
104
103
  * - Priority flag for LCP images
@@ -147,14 +146,14 @@ export function Image({
147
146
  style,
148
147
  className,
149
148
  objectFit = "cover",
150
- quality = 90,
149
+ quality = 75,
151
150
  alt = "",
152
151
  fill,
153
152
  block = !fill,
154
- width = block ? 1 : undefined,
155
- height = block ? 1 : undefined,
156
- mobileSize = "100vw",
157
- desktopSize = "100vw",
153
+ width,
154
+ height,
155
+ mobileSize,
156
+ desktopSize,
158
157
  sizes,
159
158
  src,
160
159
  unoptimized,
@@ -164,9 +163,18 @@ export function Image({
164
163
  priority = false,
165
164
  ...props
166
165
  }: ImageProps) {
167
- // Generate responsive sizes if not provided
168
- const finalSizes =
169
- sizes || buildImageSizes({ base: mobileSize, desktop: desktopSize })
166
+ const responsiveSizes =
167
+ mobileSize || desktopSize
168
+ ? buildImageSizes({
169
+ base: mobileSize ?? "100vw",
170
+ desktop: desktopSize ?? mobileSize ?? "100vw",
171
+ })
172
+ : undefined
173
+
174
+ const hasIntrinsicSize = width !== undefined && height !== undefined
175
+ const finalSizes = sizes ?? responsiveSizes ?? (hasIntrinsicSize ? undefined : "100vw")
176
+ const finalWidth = width ?? (block ? 1 : undefined)
177
+ const finalHeight = height ?? (block ? 1 : undefined)
170
178
 
171
179
  // Early return after hooks
172
180
  if (!src) return null
@@ -194,8 +202,8 @@ export function Image({
194
202
  <NextImage
195
203
  ref={ref}
196
204
  fill={!block}
197
- {...(width !== undefined && { width })}
198
- {...(height !== undefined && { height })}
205
+ {...(finalWidth !== undefined && { width: finalWidth })}
206
+ {...(finalHeight !== undefined && { height: finalHeight })}
199
207
  priority={priority}
200
208
  quality={quality}
201
209
  alt={alt}
@@ -204,7 +212,7 @@ export function Image({
204
212
  ...style,
205
213
  }}
206
214
  className={cn(className, block && "block w-full")}
207
- sizes={finalSizes}
215
+ {...(finalSizes && { sizes: finalSizes })}
208
216
  src={src}
209
217
  unoptimized={unoptimized || isSvg}
210
218
  draggable={false}
@@ -77,12 +77,18 @@ const nextConfig: NextConfig = {
77
77
  ],
78
78
  },
79
79
  images: {
80
- dangerouslyAllowSVG: true,
81
- remotePatterns: [],
80
+ remotePatterns: [
81
+ {
82
+ protocol: "https",
83
+ hostname: "cdn.sanity.io",
84
+ },
85
+ ],
82
86
  minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
83
- qualities: [90],
87
+ qualities: [75, 90, 100],
84
88
  formats: ["image/avif", "image/webp"],
85
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
89
+ deviceSizes: [
90
+ 640, 750, 828, 1080, 1200, 1320, 1620, 1920, 2048, 2560, 3024, 3456, 3840,
91
+ ],
86
92
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
87
93
  },
88
94
  headers: async () => [