@yoamigo.com/core 0.3.0 → 0.3.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/dist/prod.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { a as ContentStore, E as ContentStoreMode, C as ContentStoreProvider, d as MarkdownText, e as MarkdownTextProps, P as PageInfo, b as StaticImage, c as StaticImageProps, M as StaticText, S as StaticTextProps, b as YaImage, c as YaImageProps, M as YaText, S as YaTextProps, u as useContentStore } from './MarkdownText-BUTYfqXS.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
- import React from 'react';
3
+ import React, { CSSProperties, ReactNode } from 'react';
4
4
  export { Link, LinkProps, NavigateFunction, Router, RouterProps, useNavigate } from './router.js';
5
5
  export { Route, Switch, useParams } from 'wouter';
6
6
  export { A as AssetResolverFn, C as ContentRegistry, c as contentRegistry, a as getAllContent, g as getContent, h as hasContent, r as registerContent, b as resolveAssetUrl, s as setAssetResolver } from './asset-resolver-BnIvDkVv.js';
@@ -33,4 +33,98 @@ interface SafeHtmlProps {
33
33
  }
34
34
  declare function SafeHtml({ content, className }: SafeHtmlProps): react_jsx_runtime.JSX.Element;
35
35
 
36
- export { SafeHtml, type SafeHtmlProps, StaticLink, type StaticLinkProps, StaticLink as YaLink, type StaticLinkProps as YaLinkProps };
36
+ interface BackgroundImageConfig {
37
+ src: string;
38
+ objectFit?: 'cover' | 'contain' | 'fill';
39
+ objectPosition?: string;
40
+ focalPoint?: {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ }
45
+ interface OverlayConfig {
46
+ color: string;
47
+ opacity: number;
48
+ }
49
+ interface BackgroundConfig {
50
+ type: 'none' | 'color' | 'image';
51
+ backgroundColor?: string;
52
+ backgroundImage?: BackgroundImageConfig;
53
+ overlay?: OverlayConfig;
54
+ }
55
+ interface StaticContainerProps {
56
+ fieldId: string;
57
+ className?: string;
58
+ style?: CSSProperties;
59
+ as?: 'section' | 'div' | 'article' | 'header' | 'footer' | 'main' | 'aside';
60
+ children: ReactNode;
61
+ /** Fallback background config if not in store */
62
+ defaultBackground?: BackgroundConfig;
63
+ }
64
+ /**
65
+ * Parse background config from content store
66
+ */
67
+ declare function parseBackgroundConfig(value: string): BackgroundConfig;
68
+ /**
69
+ * Serialize background config for storage
70
+ */
71
+ declare function serializeBackgroundConfig(config: BackgroundConfig): string;
72
+ declare function StaticContainer({ fieldId, className, style, as: Tag, children, defaultBackground, }: StaticContainerProps): react_jsx_runtime.JSX.Element;
73
+
74
+ /**
75
+ * StaticVideo Component - Production version of YaVideo
76
+ *
77
+ * Renders video statically (uploaded, YouTube, or Vimeo embeds).
78
+ * No editing capabilities - pure display.
79
+ */
80
+ interface VideoFieldValue {
81
+ /** Video source type */
82
+ type: 'upload' | 'youtube' | 'vimeo';
83
+ /** Video URL (for upload) or video ID (for embeds) */
84
+ src: string;
85
+ /** Poster image URL */
86
+ poster?: string;
87
+ /** Autoplay video (requires muted for browser policy) */
88
+ autoplay?: boolean;
89
+ /** Mute video audio */
90
+ muted?: boolean;
91
+ /** Loop video playback */
92
+ loop?: boolean;
93
+ /** Show video controls */
94
+ controls?: boolean;
95
+ /** Play inline on mobile (prevent fullscreen hijack) */
96
+ playsinline?: boolean;
97
+ /** Preload strategy */
98
+ preload?: 'none' | 'metadata' | 'auto';
99
+ /** CSS object-fit */
100
+ objectFit?: 'cover' | 'contain' | 'fill';
101
+ /** CSS aspect-ratio (e.g., "16/9") */
102
+ aspectRatio?: string;
103
+ /** Start playback at this time (seconds) */
104
+ startTime?: number;
105
+ /** End playback at this time (seconds) */
106
+ endTime?: number;
107
+ }
108
+ interface StaticVideoProps {
109
+ fieldId: string;
110
+ className?: string;
111
+ /** Default aspect ratio from props */
112
+ aspectRatio?: string;
113
+ /** Default object-fit */
114
+ objectFit?: 'cover' | 'contain' | 'fill';
115
+ /** Loading strategy */
116
+ loading?: 'lazy' | 'eager';
117
+ /** Default video value (used when nothing in content store) */
118
+ defaultValue?: VideoFieldValue;
119
+ /** Fallback for backward compatibility (deprecated: use defaultValue) */
120
+ fallbackSrc?: string;
121
+ /** Fallback poster image */
122
+ fallbackPoster?: string;
123
+ }
124
+ /**
125
+ * Serialize video field value for storage
126
+ */
127
+ declare function serializeVideoValue(value: VideoFieldValue): string;
128
+ declare function StaticVideo({ fieldId, className, aspectRatio: propAspectRatio, objectFit: propObjectFit, loading, defaultValue, fallbackSrc, fallbackPoster, }: StaticVideoProps): react_jsx_runtime.JSX.Element;
129
+
130
+ export { type BackgroundConfig, type BackgroundImageConfig, type OverlayConfig, SafeHtml, type SafeHtmlProps, StaticContainer, type StaticContainerProps, StaticLink, type StaticLinkProps, StaticVideo, type StaticVideoProps, type VideoFieldValue, StaticContainer as YaContainer, type StaticContainerProps as YaContainerProps, StaticLink as YaLink, type StaticLinkProps as YaLinkProps, StaticVideo as YaVideo, type StaticVideoProps as YaVideoProps, parseBackgroundConfig, serializeBackgroundConfig, serializeVideoValue };
package/dist/prod.js CHANGED
@@ -298,12 +298,301 @@ function MarkdownText({ content, className }) {
298
298
  return /* @__PURE__ */ jsx6("span", { className, children: elements });
299
299
  }
300
300
 
301
+ // src/components/StaticContainer.tsx
302
+ import { jsx as jsx7 } from "react/jsx-runtime";
303
+ function parseBackgroundConfig(value) {
304
+ if (!value) {
305
+ return { type: "none" };
306
+ }
307
+ try {
308
+ const parsed = JSON.parse(value);
309
+ if (typeof parsed === "object" && parsed.type) {
310
+ return parsed;
311
+ }
312
+ } catch {
313
+ }
314
+ return { type: "none" };
315
+ }
316
+ function serializeBackgroundConfig(config) {
317
+ return JSON.stringify(config);
318
+ }
319
+ function getObjectPosition2(imageConfig) {
320
+ if (imageConfig.focalPoint) {
321
+ return `${imageConfig.focalPoint.x}% ${imageConfig.focalPoint.y}%`;
322
+ }
323
+ return imageConfig.objectPosition || "50% 50%";
324
+ }
325
+ function StaticContainer({
326
+ fieldId,
327
+ className,
328
+ style,
329
+ as: Tag = "section",
330
+ children,
331
+ defaultBackground
332
+ }) {
333
+ const { getValue } = useContentStore();
334
+ const rawValue = getValue(fieldId);
335
+ const backgroundConfig = rawValue ? parseBackgroundConfig(rawValue) : defaultBackground || { type: "none" };
336
+ const hasBackground = backgroundConfig.type !== "none";
337
+ const backgroundStyles = {};
338
+ if (backgroundConfig.type === "color" && backgroundConfig.backgroundColor) {
339
+ backgroundStyles.backgroundColor = backgroundConfig.backgroundColor;
340
+ }
341
+ if (backgroundConfig.type === "image" && backgroundConfig.backgroundImage) {
342
+ const img = backgroundConfig.backgroundImage;
343
+ const resolvedSrc = resolveAssetUrl(img.src);
344
+ backgroundStyles.backgroundImage = `url(${resolvedSrc})`;
345
+ backgroundStyles.backgroundSize = img.objectFit || "cover";
346
+ backgroundStyles.backgroundPosition = getObjectPosition2(img);
347
+ backgroundStyles.backgroundRepeat = "no-repeat";
348
+ }
349
+ const overlayCustomProps = {};
350
+ if (backgroundConfig.overlay) {
351
+ overlayCustomProps["--ya-overlay-color"] = backgroundConfig.overlay.color;
352
+ overlayCustomProps["--ya-overlay-opacity"] = backgroundConfig.overlay.opacity;
353
+ }
354
+ return /* @__PURE__ */ jsx7(
355
+ Tag,
356
+ {
357
+ className: `ya-container ${hasBackground ? "ya-container-has-overlay" : ""} ${className || ""}`,
358
+ style: {
359
+ ...backgroundStyles,
360
+ ...overlayCustomProps,
361
+ ...style
362
+ },
363
+ "data-field-id": fieldId,
364
+ children
365
+ }
366
+ );
367
+ }
368
+
369
+ // src/components/StaticVideo.tsx
370
+ import { useCallback, useEffect, useRef, useState } from "react";
371
+ import { jsx as jsx8 } from "react/jsx-runtime";
372
+ function parseVideoValue(value) {
373
+ if (!value) {
374
+ return { type: "upload", src: "" };
375
+ }
376
+ try {
377
+ const parsed = JSON.parse(value);
378
+ if (typeof parsed === "object" && parsed.src) {
379
+ return {
380
+ type: parsed.type || "upload",
381
+ ...parsed
382
+ };
383
+ }
384
+ } catch {
385
+ }
386
+ return { type: "upload", src: value };
387
+ }
388
+ function serializeVideoValue(value) {
389
+ return JSON.stringify(value);
390
+ }
391
+ function buildYouTubeEmbedUrl(videoId, value) {
392
+ const params = new URLSearchParams({
393
+ rel: "0",
394
+ modestbranding: "1"
395
+ });
396
+ if (value.autoplay) params.set("autoplay", "1");
397
+ if (value.muted) params.set("mute", "1");
398
+ if (value.loop) {
399
+ params.set("loop", "1");
400
+ params.set("playlist", videoId);
401
+ }
402
+ if (value.controls === false) params.set("controls", "0");
403
+ if (value.startTime) params.set("start", String(Math.floor(value.startTime)));
404
+ if (value.endTime) params.set("end", String(Math.floor(value.endTime)));
405
+ return `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
406
+ }
407
+ function buildVimeoEmbedUrl(videoId, value) {
408
+ const params = new URLSearchParams({
409
+ title: "0",
410
+ byline: "0",
411
+ portrait: "0"
412
+ });
413
+ if (value.autoplay) params.set("autoplay", "1");
414
+ if (value.muted) params.set("muted", "1");
415
+ if (value.loop) params.set("loop", "1");
416
+ if (value.controls === false) params.set("controls", "0");
417
+ let url = `https://player.vimeo.com/video/${videoId}?${params.toString()}`;
418
+ if (value.startTime) {
419
+ url += `#t=${Math.floor(value.startTime)}s`;
420
+ }
421
+ return url;
422
+ }
423
+ function StaticVideo({
424
+ fieldId,
425
+ className,
426
+ aspectRatio: propAspectRatio,
427
+ objectFit: propObjectFit,
428
+ loading = "lazy",
429
+ defaultValue,
430
+ fallbackSrc,
431
+ fallbackPoster
432
+ }) {
433
+ const { getValue } = useContentStore();
434
+ const containerRef = useRef(null);
435
+ const videoRef = useRef(null);
436
+ const [isInView, setIsInView] = useState(loading === "eager");
437
+ const rawValue = getValue(fieldId);
438
+ const parsedValue = parseVideoValue(rawValue);
439
+ const videoData = parsedValue.src ? parsedValue : defaultValue || parsedValue;
440
+ const src = videoData.src || fallbackSrc || "";
441
+ const poster = videoData.poster || fallbackPoster || "";
442
+ const objectFit = videoData.objectFit || propObjectFit || "cover";
443
+ const aspectRatio = videoData.aspectRatio || propAspectRatio || "16/9";
444
+ const autoplay = videoData.autoplay ?? false;
445
+ const muted = videoData.muted ?? false;
446
+ const loop = videoData.loop ?? false;
447
+ const controls = videoData.controls ?? true;
448
+ const playsinline = videoData.playsinline ?? true;
449
+ const preload = videoData.preload ?? "metadata";
450
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
451
+ useEffect(() => {
452
+ const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
453
+ setPrefersReducedMotion(mediaQuery.matches);
454
+ const handleChange = (e) => {
455
+ setPrefersReducedMotion(e.matches);
456
+ };
457
+ mediaQuery.addEventListener("change", handleChange);
458
+ return () => mediaQuery.removeEventListener("change", handleChange);
459
+ }, []);
460
+ const effectiveAutoplay = autoplay && !prefersReducedMotion;
461
+ useEffect(() => {
462
+ if (loading === "eager" || isInView) return;
463
+ const observer = new IntersectionObserver(
464
+ (entries) => {
465
+ if (entries[0]?.isIntersecting) {
466
+ setIsInView(true);
467
+ observer.disconnect();
468
+ }
469
+ },
470
+ { rootMargin: "200px" }
471
+ );
472
+ if (containerRef.current) {
473
+ observer.observe(containerRef.current);
474
+ }
475
+ return () => observer.disconnect();
476
+ }, [loading, isInView]);
477
+ const handleKeyDown = useCallback(
478
+ (e) => {
479
+ if ((e.key === " " || e.key === "Enter") && videoData.type === "upload" && controls) {
480
+ e.preventDefault();
481
+ const video = videoRef.current;
482
+ if (video) {
483
+ if (video.paused) {
484
+ video.play();
485
+ } else {
486
+ video.pause();
487
+ }
488
+ }
489
+ }
490
+ },
491
+ [videoData.type, controls]
492
+ );
493
+ const renderVideo = () => {
494
+ if (!src) return null;
495
+ if (!isInView && loading === "lazy") {
496
+ return /* @__PURE__ */ jsx8("div", { className: "ya-video-placeholder", style: { aspectRatio } });
497
+ }
498
+ if (videoData.type === "youtube" && src) {
499
+ const embedUrl = buildYouTubeEmbedUrl(src, videoData);
500
+ return /* @__PURE__ */ jsx8(
501
+ "iframe",
502
+ {
503
+ src: embedUrl,
504
+ className,
505
+ style: {
506
+ width: "100%",
507
+ height: "100%",
508
+ border: "none",
509
+ aspectRatio,
510
+ objectFit
511
+ },
512
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
513
+ allowFullScreen: true,
514
+ loading
515
+ }
516
+ );
517
+ }
518
+ if (videoData.type === "vimeo" && src) {
519
+ const embedUrl = buildVimeoEmbedUrl(src, videoData);
520
+ return /* @__PURE__ */ jsx8(
521
+ "iframe",
522
+ {
523
+ src: embedUrl,
524
+ className,
525
+ style: {
526
+ width: "100%",
527
+ height: "100%",
528
+ border: "none",
529
+ aspectRatio,
530
+ objectFit
531
+ },
532
+ allow: "autoplay; fullscreen; picture-in-picture",
533
+ allowFullScreen: true,
534
+ loading
535
+ }
536
+ );
537
+ }
538
+ const resolvedSrc = resolveAssetUrl(src);
539
+ const resolvedPoster = poster ? resolveAssetUrl(poster) : void 0;
540
+ return /* @__PURE__ */ jsx8(
541
+ "video",
542
+ {
543
+ ref: videoRef,
544
+ src: resolvedSrc,
545
+ poster: resolvedPoster,
546
+ className,
547
+ style: {
548
+ objectFit,
549
+ aspectRatio
550
+ },
551
+ autoPlay: effectiveAutoplay,
552
+ muted,
553
+ loop,
554
+ controls,
555
+ playsInline: playsinline,
556
+ preload,
557
+ onLoadedMetadata: (e) => {
558
+ if (videoData.startTime) {
559
+ e.currentTarget.currentTime = videoData.startTime;
560
+ }
561
+ },
562
+ onTimeUpdate: (e) => {
563
+ if (videoData.endTime && e.currentTarget.currentTime >= videoData.endTime) {
564
+ if (loop) {
565
+ e.currentTarget.currentTime = videoData.startTime || 0;
566
+ } else {
567
+ e.currentTarget.pause();
568
+ }
569
+ }
570
+ }
571
+ }
572
+ );
573
+ };
574
+ return /* @__PURE__ */ jsx8(
575
+ "div",
576
+ {
577
+ ref: containerRef,
578
+ className: `ya-video-wrapper ${className || ""}`,
579
+ style: { aspectRatio },
580
+ "data-field-id": fieldId,
581
+ tabIndex: videoData.type === "upload" && controls ? 0 : void 0,
582
+ role: videoData.type === "upload" && controls ? "application" : void 0,
583
+ "aria-label": videoData.type === "upload" && controls ? "Video player. Press Space or Enter to play/pause." : void 0,
584
+ onKeyDown: handleKeyDown,
585
+ children: renderVideo()
586
+ }
587
+ );
588
+ }
589
+
301
590
  // src/router/Link.tsx
302
591
  import { Link as WouterLink } from "wouter";
303
- import { jsx as jsx7 } from "react/jsx-runtime";
592
+ import { jsx as jsx9 } from "react/jsx-runtime";
304
593
  function Link({ to, href, children, className, onClick, replace, ...props }) {
305
594
  const target = href ?? to ?? "/";
306
- return /* @__PURE__ */ jsx7(WouterLink, { href: target, className, onClick, replace, ...props, children });
595
+ return /* @__PURE__ */ jsx9(WouterLink, { href: target, className, onClick, replace, ...props, children });
307
596
  }
308
597
 
309
598
  // src/router/useNavigate.ts
@@ -322,7 +611,7 @@ function useNavigate() {
322
611
 
323
612
  // src/router/Router.tsx
324
613
  import { Router as WouterRouter } from "wouter";
325
- import { jsx as jsx8 } from "react/jsx-runtime";
614
+ import { jsx as jsx10 } from "react/jsx-runtime";
326
615
  function detectBasename() {
327
616
  if (typeof window === "undefined") return "";
328
617
  const sessionMatch = window.location.pathname.match(/^\/session\/[^/]+/);
@@ -337,7 +626,7 @@ function detectBasename() {
337
626
  }
338
627
  function Router({ children, base }) {
339
628
  const basename = base ?? detectBasename();
340
- return /* @__PURE__ */ jsx8(WouterRouter, { base: basename, children });
629
+ return /* @__PURE__ */ jsx10(WouterRouter, { base: basename, children });
341
630
  }
342
631
 
343
632
  // src/router/index.ts
@@ -349,19 +638,26 @@ export {
349
638
  Route,
350
639
  Router,
351
640
  SafeHtml,
641
+ StaticContainer,
352
642
  MpImage as StaticImage,
353
643
  StaticLink,
354
644
  MpText as StaticText,
645
+ StaticVideo,
355
646
  Switch,
647
+ StaticContainer as YaContainer,
356
648
  MpImage as YaImage,
357
649
  StaticLink as YaLink,
358
650
  MpText as YaText,
651
+ StaticVideo as YaVideo,
359
652
  contentRegistry,
360
653
  getAllContent,
361
654
  getContent,
362
655
  hasContent,
656
+ parseBackgroundConfig,
363
657
  registerContent,
364
658
  resolveAssetUrl,
659
+ serializeBackgroundConfig,
660
+ serializeVideoValue,
365
661
  setAssetResolver,
366
662
  useContentStore,
367
663
  useNavigate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoamigo.com/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Core components, router, and utilities for YoAmigo templates",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",