@promakeai/cli 0.9.9 → 0.9.10

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 (137) hide show
  1. package/README.md +111 -111
  2. package/dist/index.js +142 -142
  3. package/dist/registry/about-page.json +3 -3
  4. package/dist/registry/about-section.json +4 -4
  5. package/dist/registry/animations.json +2 -2
  6. package/dist/registry/announcement-bar.json +4 -4
  7. package/dist/registry/api.json +1 -1
  8. package/dist/registry/auth-core.json +2 -2
  9. package/dist/registry/bento-grid-section.json +4 -4
  10. package/dist/registry/blog-core.json +5 -5
  11. package/dist/registry/blog-list-page.json +4 -4
  12. package/dist/registry/blog-section.json +4 -4
  13. package/dist/registry/cards-carousel-section.json +4 -4
  14. package/dist/registry/cart-drawer.json +4 -4
  15. package/dist/registry/cart-page.json +4 -4
  16. package/dist/registry/case-study-page.json +3 -3
  17. package/dist/registry/category-section.json +4 -4
  18. package/dist/registry/checkout-page.json +4 -4
  19. package/dist/registry/coming-soon-page-minimal.json +4 -4
  20. package/dist/registry/coming-soon-page.json +4 -4
  21. package/dist/registry/contact-info-grid.json +4 -4
  22. package/dist/registry/contact-page-centered.json +4 -4
  23. package/dist/registry/contact-page-map-overlay.json +3 -3
  24. package/dist/registry/contact-page-map-split.json +3 -3
  25. package/dist/registry/contact-page-split.json +4 -4
  26. package/dist/registry/contact-page.json +4 -4
  27. package/dist/registry/content-section.json +4 -4
  28. package/dist/registry/cookie-consent.json +4 -4
  29. package/dist/registry/cookies-page.json +3 -3
  30. package/dist/registry/cta-section.json +3 -3
  31. package/dist/registry/ecommerce-core.json +8 -8
  32. package/dist/registry/empty-page.json +3 -3
  33. package/dist/registry/faq-categorized.json +4 -4
  34. package/dist/registry/faq-simple.json +4 -4
  35. package/dist/registry/favorites-blog-block.json +1 -1
  36. package/dist/registry/favorites-blog-page.json +4 -4
  37. package/dist/registry/favorites-ecommerce-block.json +1 -1
  38. package/dist/registry/favorites-ecommerce-page.json +4 -4
  39. package/dist/registry/feature-section.json +3 -3
  40. package/dist/registry/featured-products.json +4 -4
  41. package/dist/registry/footer-detailed.json +4 -4
  42. package/dist/registry/footer-minimal.json +3 -3
  43. package/dist/registry/footer.json +3 -3
  44. package/dist/registry/forgot-password-page-split.json +4 -4
  45. package/dist/registry/forgot-password-page.json +4 -4
  46. package/dist/registry/google-adsense.json +4 -4
  47. package/dist/registry/google-map.json +2 -2
  48. package/dist/registry/header-centered-pill.json +4 -4
  49. package/dist/registry/header-ecommerce.json +4 -4
  50. package/dist/registry/header-mega.json +4 -4
  51. package/dist/registry/header-minimal.json +4 -4
  52. package/dist/registry/header-simple.json +3 -3
  53. package/dist/registry/hero-carousel.json +3 -3
  54. package/dist/registry/hero-cta.json +4 -4
  55. package/dist/registry/hero-gradient.json +4 -4
  56. package/dist/registry/hero-grid.json +4 -4
  57. package/dist/registry/hero-profile.json +3 -3
  58. package/dist/registry/hero.json +3 -3
  59. package/dist/registry/index.json +103 -103
  60. package/dist/registry/landing-page-app.json +3 -3
  61. package/dist/registry/landing-page-saas.json +3 -3
  62. package/dist/registry/login-page-split.json +4 -4
  63. package/dist/registry/login-page.json +4 -4
  64. package/dist/registry/logo-cloud.json +4 -4
  65. package/dist/registry/masonry-grid.json +3 -3
  66. package/dist/registry/my-orders-page.json +4 -4
  67. package/dist/registry/newsletter-section.json +4 -4
  68. package/dist/registry/order-card-compact.json +3 -3
  69. package/dist/registry/order-confirmation-page.json +4 -4
  70. package/dist/registry/order-detail-block.json +1 -1
  71. package/dist/registry/orders-list-block.json +1 -1
  72. package/dist/registry/payment-success-block.json +2 -2
  73. package/dist/registry/portfolio-page.json +4 -4
  74. package/dist/registry/post-card.json +4 -4
  75. package/dist/registry/post-detail-block.json +4 -4
  76. package/dist/registry/post-detail-page.json +4 -4
  77. package/dist/registry/pricing-card.json +3 -3
  78. package/dist/registry/pricing-page.json +4 -4
  79. package/dist/registry/pricing-section.json +4 -4
  80. package/dist/registry/privacy-page.json +3 -3
  81. package/dist/registry/product-card-detailed.json +4 -4
  82. package/dist/registry/product-card-hover.json +4 -4
  83. package/dist/registry/product-card.json +4 -4
  84. package/dist/registry/product-detail-block.json +2 -2
  85. package/dist/registry/product-detail-page.json +4 -4
  86. package/dist/registry/product-detail-section.json +4 -4
  87. package/dist/registry/product-quick-view.json +4 -4
  88. package/dist/registry/products-page.json +4 -4
  89. package/dist/registry/reading-progress.json +4 -4
  90. package/dist/registry/register-page-split.json +4 -4
  91. package/dist/registry/register-page.json +4 -4
  92. package/dist/registry/related-posts-block.json +1 -1
  93. package/dist/registry/related-products-block.json +2 -2
  94. package/dist/registry/reset-password-page-split.json +4 -4
  95. package/dist/registry/reset-password-page.json +4 -4
  96. package/dist/registry/service-card.json +1 -1
  97. package/dist/registry/share-buttons.json +4 -4
  98. package/dist/registry/skill-card.json +1 -1
  99. package/dist/registry/team-page.json +4 -4
  100. package/dist/registry/terms-page.json +3 -3
  101. package/dist/registry/testimonials-carousel.json +4 -4
  102. package/dist/registry/testimonials-grid.json +4 -4
  103. package/dist/registry/timeline-section.json +4 -4
  104. package/dist/registry/verify-email-page.json +4 -4
  105. package/dist/registry/video-hero.json +4 -4
  106. package/dist/registry/youtube-embed.json +4 -4
  107. package/package.json +1 -1
  108. package/template/.env +5 -5
  109. package/template/README.md +54 -54
  110. package/template/eslint.config.js +41 -41
  111. package/template/index.html +237 -237
  112. package/template/package.json +96 -96
  113. package/template/public/_redirects +1 -1
  114. package/template/public/robots.txt +14 -14
  115. package/template/scripts/init-db.ts +18 -18
  116. package/template/src/App.tsx +21 -21
  117. package/template/src/components/FormField.tsx +48 -48
  118. package/template/src/components/FormFileInput.tsx +75 -75
  119. package/template/src/components/GoogleAnalytics.tsx +34 -34
  120. package/template/src/components/LanguageSwitcher.tsx +53 -53
  121. package/template/src/components/MetriaAnalytics.tsx +68 -68
  122. package/template/src/components/PasswordInput.tsx +60 -60
  123. package/template/src/components/ScriptInjector.tsx +62 -62
  124. package/template/src/components/Stack.tsx +39 -39
  125. package/template/src/constants/constants.json +71 -71
  126. package/template/src/db/index.ts +21 -21
  127. package/template/src/db/provider.tsx +106 -106
  128. package/template/src/db/schema.json +278 -278
  129. package/template/src/db/types.ts +195 -195
  130. package/template/src/hooks/use-debounced-value.ts +12 -12
  131. package/template/src/hooks/use-page-title.ts +55 -55
  132. package/template/src/lang/index.ts +90 -90
  133. package/template/src/lib/api.ts +345 -345
  134. package/template/src/lib/env.ts +19 -19
  135. package/template/src/router.tsx +14 -14
  136. package/template/src/vite-env.d.ts +1 -1
  137. package/template/vite.config.ts +194 -199
@@ -1,53 +1,53 @@
1
- import { useTranslation } from "react-i18next";
2
- import { Button } from "@/components/ui/button";
3
- import {
4
- DropdownMenu,
5
- DropdownMenuContent,
6
- DropdownMenuItem,
7
- DropdownMenuTrigger,
8
- } from "@/components/ui/dropdown-menu";
9
- import { changeLanguage } from "@/lang";
10
- import { cn } from "@/lib/utils";
11
- import constants from "@/constants/constants.json";
12
-
13
- interface LanguageSwitcherProps {
14
- className?: string;
15
- style?: React.CSSProperties;
16
- }
17
-
18
- const languages: Record<string, string> =
19
- constants?.site?.availableLanguages || {};
20
-
21
- export function LanguageSwitcher({ className, style }: LanguageSwitcherProps) {
22
- const { i18n } = useTranslation();
23
- const currentLang = i18n.language;
24
-
25
- return (
26
- <DropdownMenu>
27
- <DropdownMenuTrigger asChild>
28
- <Button
29
- variant="ghost"
30
- size="sm"
31
- className={cn("h-9 px-2 text-sm font-medium", className)}
32
- style={style}
33
- >
34
- {languages?.[currentLang] || currentLang.toUpperCase()}
35
- </Button>
36
- </DropdownMenuTrigger>
37
- <DropdownMenuContent align="end">
38
- {Object.entries(languages).map(([lang, label]) => (
39
- <DropdownMenuItem
40
- key={lang}
41
- onClick={() => changeLanguage(lang)}
42
- className={cn(
43
- currentLang === lang ? "bg-accent" : "",
44
- "hover:text-primary focus:text-primary",
45
- )}
46
- >
47
- {label || lang.toUpperCase()}
48
- </DropdownMenuItem>
49
- ))}
50
- </DropdownMenuContent>
51
- </DropdownMenu>
52
- );
53
- }
1
+ import { useTranslation } from "react-i18next";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ } from "@/components/ui/dropdown-menu";
9
+ import { changeLanguage } from "@/lang";
10
+ import { cn } from "@/lib/utils";
11
+ import constants from "@/constants/constants.json";
12
+
13
+ interface LanguageSwitcherProps {
14
+ className?: string;
15
+ style?: React.CSSProperties;
16
+ }
17
+
18
+ const languages: Record<string, string> =
19
+ constants?.site?.availableLanguages || {};
20
+
21
+ export function LanguageSwitcher({ className, style }: LanguageSwitcherProps) {
22
+ const { i18n } = useTranslation();
23
+ const currentLang = i18n.language;
24
+
25
+ return (
26
+ <DropdownMenu>
27
+ <DropdownMenuTrigger asChild>
28
+ <Button
29
+ variant="ghost"
30
+ size="sm"
31
+ className={cn("h-9 px-2 text-sm font-medium", className)}
32
+ style={style}
33
+ >
34
+ {languages?.[currentLang] || currentLang.toUpperCase()}
35
+ </Button>
36
+ </DropdownMenuTrigger>
37
+ <DropdownMenuContent align="end">
38
+ {Object.entries(languages).map(([lang, label]) => (
39
+ <DropdownMenuItem
40
+ key={lang}
41
+ onClick={() => changeLanguage(lang)}
42
+ className={cn(
43
+ currentLang === lang ? "bg-accent" : "",
44
+ "hover:text-primary focus:text-primary",
45
+ )}
46
+ >
47
+ {label || lang.toUpperCase()}
48
+ </DropdownMenuItem>
49
+ ))}
50
+ </DropdownMenuContent>
51
+ </DropdownMenu>
52
+ );
53
+ }
@@ -1,68 +1,68 @@
1
- import { useEffect, useRef } from 'react';
2
- const DEFAULT_TRACKER_SCRIPT_URL =
3
- 'https://cdn.jsdelivr.net/npm/@litemetrics/tracker@latest/dist/litemetrics.global.js';
4
-
5
- function pickFirstNonEmpty(...values: Array<string | undefined>): string | undefined {
6
- return values.find((value) => typeof value === 'string' && value.trim().length > 0);
7
- }
8
-
9
- export const analyticsConfig = {
10
- siteId: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SITE_ID, import.meta.env.VITE_METRIA_SITE_ID),
11
- serverUrl: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SERVER_URL, import.meta.env.VITE_METRIA_SERVER_URL),
12
- trackerScriptUrl: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SCRIPT_URL) ?? DEFAULT_TRACKER_SCRIPT_URL,
13
- };
14
-
15
- export const isAnalyticsEnabled = Boolean(analyticsConfig.siteId && analyticsConfig.serverUrl);
16
-
17
- type TrackerGlobal = {
18
- createTracker: (config: { siteId: string; endpoint: string }) => void;
19
- };
20
-
21
- declare global {
22
- interface Window {
23
- Litemetrics?: TrackerGlobal;
24
- }
25
- }
26
-
27
- function stripTrailingSlashes(value: string): string {
28
- return value.replace(/\/+$/, '');
29
- }
30
-
31
- function ensureHttps(value: string): string {
32
- return value.replace(/^http:\/\//i, 'https://');
33
- }
34
-
35
- export function MetriaAnalytics() {
36
- const injected = useRef(false);
37
-
38
- useEffect(() => {
39
- if (!isAnalyticsEnabled || !analyticsConfig.siteId || !analyticsConfig.serverUrl || injected.current) {
40
- return;
41
- }
42
- if (document.querySelector('[data-injected="litemetrics-tracker"]')) return;
43
-
44
- injected.current = true;
45
-
46
- const normalizedServerUrl = ensureHttps(stripTrailingSlashes(analyticsConfig.serverUrl));
47
- const script = document.createElement('script');
48
- script.async = true;
49
- script.src = analyticsConfig.trackerScriptUrl;
50
- script.setAttribute('data-injected', 'litemetrics-tracker');
51
- script.onload = () => {
52
- if (document.querySelector('[data-injected="litemetrics-init"]')) return;
53
-
54
- window.Litemetrics?.createTracker({
55
- siteId: analyticsConfig.siteId!,
56
- endpoint: `${normalizedServerUrl}/api/collect`,
57
- });
58
-
59
- const marker = document.createElement('meta');
60
- marker.setAttribute('data-injected', 'litemetrics-init');
61
- document.head.appendChild(marker);
62
- };
63
-
64
- document.head.appendChild(script);
65
- }, []);
66
-
67
- return null;
68
- }
1
+ import { useEffect, useRef } from 'react';
2
+ const DEFAULT_TRACKER_SCRIPT_URL =
3
+ 'https://cdn.jsdelivr.net/npm/@litemetrics/tracker@latest/dist/litemetrics.global.js';
4
+
5
+ function pickFirstNonEmpty(...values: Array<string | undefined>): string | undefined {
6
+ return values.find((value) => typeof value === 'string' && value.trim().length > 0);
7
+ }
8
+
9
+ export const analyticsConfig = {
10
+ siteId: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SITE_ID, import.meta.env.VITE_METRIA_SITE_ID),
11
+ serverUrl: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SERVER_URL, import.meta.env.VITE_METRIA_SERVER_URL),
12
+ trackerScriptUrl: pickFirstNonEmpty(import.meta.env.VITE_LITEMETRICS_SCRIPT_URL) ?? DEFAULT_TRACKER_SCRIPT_URL,
13
+ };
14
+
15
+ export const isAnalyticsEnabled = Boolean(analyticsConfig.siteId && analyticsConfig.serverUrl);
16
+
17
+ type TrackerGlobal = {
18
+ createTracker: (config: { siteId: string; endpoint: string }) => void;
19
+ };
20
+
21
+ declare global {
22
+ interface Window {
23
+ Litemetrics?: TrackerGlobal;
24
+ }
25
+ }
26
+
27
+ function stripTrailingSlashes(value: string): string {
28
+ return value.replace(/\/+$/, '');
29
+ }
30
+
31
+ function ensureHttps(value: string): string {
32
+ return value.replace(/^http:\/\//i, 'https://');
33
+ }
34
+
35
+ export function MetriaAnalytics() {
36
+ const injected = useRef(false);
37
+
38
+ useEffect(() => {
39
+ if (!isAnalyticsEnabled || !analyticsConfig.siteId || !analyticsConfig.serverUrl || injected.current) {
40
+ return;
41
+ }
42
+ if (document.querySelector('[data-injected="litemetrics-tracker"]')) return;
43
+
44
+ injected.current = true;
45
+
46
+ const normalizedServerUrl = ensureHttps(stripTrailingSlashes(analyticsConfig.serverUrl));
47
+ const script = document.createElement('script');
48
+ script.async = true;
49
+ script.src = analyticsConfig.trackerScriptUrl;
50
+ script.setAttribute('data-injected', 'litemetrics-tracker');
51
+ script.onload = () => {
52
+ if (document.querySelector('[data-injected="litemetrics-init"]')) return;
53
+
54
+ window.Litemetrics?.createTracker({
55
+ siteId: analyticsConfig.siteId!,
56
+ endpoint: `${normalizedServerUrl}/api/collect`,
57
+ });
58
+
59
+ const marker = document.createElement('meta');
60
+ marker.setAttribute('data-injected', 'litemetrics-init');
61
+ document.head.appendChild(marker);
62
+ };
63
+
64
+ document.head.appendChild(script);
65
+ }, []);
66
+
67
+ return null;
68
+ }
@@ -1,61 +1,61 @@
1
- import { useState } from "react";
2
- import { Input } from "@/components/ui/input";
3
- import { Eye, EyeOff } from "lucide-react";
4
-
5
- interface PasswordInputProps {
6
- id?: string;
7
- name?: string;
8
- value: string;
9
- onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
10
- placeholder?: string;
11
- required?: boolean;
12
- autoComplete?: string;
13
- className?: string;
14
- disabled?: boolean;
15
- minLength?: number;
16
- }
17
-
18
- export function PasswordInput({
19
- id = "password",
20
- name = "password",
21
- value,
22
- onChange,
23
- placeholder = "Enter password",
24
- required = false,
25
- autoComplete = "current-password",
26
- disabled = false,
27
- minLength = undefined,
28
- className = "",
29
- }: PasswordInputProps) {
30
- const [showPassword, setShowPassword] = useState(false);
31
-
32
- return (
33
- <div className="relative">
34
- <Input
35
- id={id}
36
- name={name}
37
- type={showPassword ? "text" : "password"}
38
- value={value}
39
- onChange={onChange}
40
- placeholder={placeholder}
41
- required={required}
42
- className={`mt-1 pr-10 ${className}`}
43
- autoComplete={autoComplete}
44
- disabled={disabled}
45
- minLength={minLength}
46
- />
47
- <button
48
- type="button"
49
- onClick={() => setShowPassword(!showPassword)}
50
- className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
51
- aria-label={showPassword ? "Hide password" : "Show password"}
52
- >
53
- {showPassword ? (
54
- <EyeOff className="w-4 h-4" />
55
- ) : (
56
- <Eye className="w-4 h-4" />
57
- )}
58
- </button>
59
- </div>
60
- );
1
+ import { useState } from "react";
2
+ import { Input } from "@/components/ui/input";
3
+ import { Eye, EyeOff } from "lucide-react";
4
+
5
+ interface PasswordInputProps {
6
+ id?: string;
7
+ name?: string;
8
+ value: string;
9
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
10
+ placeholder?: string;
11
+ required?: boolean;
12
+ autoComplete?: string;
13
+ className?: string;
14
+ disabled?: boolean;
15
+ minLength?: number;
16
+ }
17
+
18
+ export function PasswordInput({
19
+ id = "password",
20
+ name = "password",
21
+ value,
22
+ onChange,
23
+ placeholder = "Enter password",
24
+ required = false,
25
+ autoComplete = "current-password",
26
+ disabled = false,
27
+ minLength = undefined,
28
+ className = "",
29
+ }: PasswordInputProps) {
30
+ const [showPassword, setShowPassword] = useState(false);
31
+
32
+ return (
33
+ <div className="relative">
34
+ <Input
35
+ id={id}
36
+ name={name}
37
+ type={showPassword ? "text" : "password"}
38
+ value={value}
39
+ onChange={onChange}
40
+ placeholder={placeholder}
41
+ required={required}
42
+ className={`mt-1 pr-10 ${className}`}
43
+ autoComplete={autoComplete}
44
+ disabled={disabled}
45
+ minLength={minLength}
46
+ />
47
+ <button
48
+ type="button"
49
+ onClick={() => setShowPassword(!showPassword)}
50
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
51
+ aria-label={showPassword ? "Hide password" : "Show password"}
52
+ >
53
+ {showPassword ? (
54
+ <EyeOff className="w-4 h-4" />
55
+ ) : (
56
+ <Eye className="w-4 h-4" />
57
+ )}
58
+ </button>
59
+ </div>
60
+ );
61
61
  }
@@ -1,62 +1,62 @@
1
- import { useEffect, useRef } from "react";
2
- import constants from "@/constants/constants.json";
3
-
4
- function injectScript(html: string, target: "head" | "body", position: "start" | "end", marker: string) {
5
- if (!html) return;
6
-
7
- // Check if already injected
8
- if (document.querySelector(`[data-injected="${marker}"]`)) return;
9
-
10
- const container = document.createElement("div");
11
- container.innerHTML = html;
12
-
13
- const targetElement = target === "head" ? document.head : document.body;
14
-
15
- // Create wrapper with marker
16
- const wrapper = document.createDocumentFragment();
17
-
18
- Array.from(container.childNodes).forEach((node) => {
19
- if (node.nodeName === "SCRIPT") {
20
- // Recreate script for execution
21
- const script = node as HTMLScriptElement;
22
- const newScript = document.createElement("script");
23
- newScript.setAttribute("data-injected", marker);
24
- Array.from(script.attributes).forEach((attr) => {
25
- newScript.setAttribute(attr.name, attr.value);
26
- });
27
- if (script.innerHTML) {
28
- newScript.innerHTML = script.innerHTML;
29
- }
30
- wrapper.appendChild(newScript);
31
- } else {
32
- const clone = node.cloneNode(true) as HTMLElement;
33
- if (clone.setAttribute) {
34
- clone.setAttribute("data-injected", marker);
35
- }
36
- wrapper.appendChild(clone);
37
- }
38
- });
39
-
40
- if (position === "start") {
41
- targetElement.insertBefore(wrapper, targetElement.firstChild);
42
- } else {
43
- targetElement.appendChild(wrapper);
44
- }
45
- }
46
-
47
- export function ScriptInjector() {
48
- const injected = useRef(false);
49
- const { headStart, headEnd, bodyStart, bodyEnd } = constants.scripts;
50
-
51
- useEffect(() => {
52
- if (injected.current) return;
53
- injected.current = true;
54
-
55
- injectScript(headStart, "head", "start", "head-start");
56
- injectScript(headEnd, "head", "end", "head-end");
57
- injectScript(bodyStart, "body", "start", "body-start");
58
- injectScript(bodyEnd, "body", "end", "body-end");
59
- }, []);
60
-
61
- return null;
62
- }
1
+ import { useEffect, useRef } from "react";
2
+ import constants from "@/constants/constants.json";
3
+
4
+ function injectScript(html: string, target: "head" | "body", position: "start" | "end", marker: string) {
5
+ if (!html) return;
6
+
7
+ // Check if already injected
8
+ if (document.querySelector(`[data-injected="${marker}"]`)) return;
9
+
10
+ const container = document.createElement("div");
11
+ container.innerHTML = html;
12
+
13
+ const targetElement = target === "head" ? document.head : document.body;
14
+
15
+ // Create wrapper with marker
16
+ const wrapper = document.createDocumentFragment();
17
+
18
+ Array.from(container.childNodes).forEach((node) => {
19
+ if (node.nodeName === "SCRIPT") {
20
+ // Recreate script for execution
21
+ const script = node as HTMLScriptElement;
22
+ const newScript = document.createElement("script");
23
+ newScript.setAttribute("data-injected", marker);
24
+ Array.from(script.attributes).forEach((attr) => {
25
+ newScript.setAttribute(attr.name, attr.value);
26
+ });
27
+ if (script.innerHTML) {
28
+ newScript.innerHTML = script.innerHTML;
29
+ }
30
+ wrapper.appendChild(newScript);
31
+ } else {
32
+ const clone = node.cloneNode(true) as HTMLElement;
33
+ if (clone.setAttribute) {
34
+ clone.setAttribute("data-injected", marker);
35
+ }
36
+ wrapper.appendChild(clone);
37
+ }
38
+ });
39
+
40
+ if (position === "start") {
41
+ targetElement.insertBefore(wrapper, targetElement.firstChild);
42
+ } else {
43
+ targetElement.appendChild(wrapper);
44
+ }
45
+ }
46
+
47
+ export function ScriptInjector() {
48
+ const injected = useRef(false);
49
+ const { headStart, headEnd, bodyStart, bodyEnd } = constants.scripts;
50
+
51
+ useEffect(() => {
52
+ if (injected.current) return;
53
+ injected.current = true;
54
+
55
+ injectScript(headStart, "head", "start", "head-start");
56
+ injectScript(headEnd, "head", "end", "head-end");
57
+ injectScript(bodyStart, "body", "start", "body-start");
58
+ injectScript(bodyEnd, "body", "end", "body-end");
59
+ }, []);
60
+
61
+ return null;
62
+ }
@@ -1,39 +1,39 @@
1
- import clsx from "clsx";
2
- import { Children } from "react";
3
-
4
- type StackProps = {
5
- as?: keyof HTMLElementTagNameMap; // "div", "ul", "section" vs can be any valid HTML element
6
- gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; //Tailwind spacing scale
7
- direction?: "vertical" | "horizontal";
8
- className?: string;
9
- children: React.ReactNode;
10
- wrapChildren?: boolean; // If true, wraps each child in a Stack with the same gap
11
- childGap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; // Gap for wrapped children
12
- };
13
-
14
- export function Stack({
15
- as: Component = "div",
16
- gap = 2,
17
- direction = "vertical",
18
- className,
19
- children,
20
- wrapChildren = false,
21
- childGap = 2,
22
- }: StackProps) {
23
- const gapClass =
24
- direction === "vertical" ? `space-y-${gap}` : `space-x-${gap}`;
25
-
26
- const content = wrapChildren
27
- ? Children.map(children, (child, index) => (
28
- <Stack key={index} gap={childGap} direction={direction}>
29
- {child}
30
- </Stack>
31
- ))
32
- : children;
33
-
34
- return (
35
- <Component className={clsx(gapClass, className)}>
36
- {content}
37
- </Component>
38
- );
39
- }
1
+ import clsx from "clsx";
2
+ import { Children } from "react";
3
+
4
+ type StackProps = {
5
+ as?: keyof HTMLElementTagNameMap; // "div", "ul", "section" vs can be any valid HTML element
6
+ gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; //Tailwind spacing scale
7
+ direction?: "vertical" | "horizontal";
8
+ className?: string;
9
+ children: React.ReactNode;
10
+ wrapChildren?: boolean; // If true, wraps each child in a Stack with the same gap
11
+ childGap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; // Gap for wrapped children
12
+ };
13
+
14
+ export function Stack({
15
+ as: Component = "div",
16
+ gap = 2,
17
+ direction = "vertical",
18
+ className,
19
+ children,
20
+ wrapChildren = false,
21
+ childGap = 2,
22
+ }: StackProps) {
23
+ const gapClass =
24
+ direction === "vertical" ? `space-y-${gap}` : `space-x-${gap}`;
25
+
26
+ const content = wrapChildren
27
+ ? Children.map(children, (child, index) => (
28
+ <Stack key={index} gap={childGap} direction={direction}>
29
+ {child}
30
+ </Stack>
31
+ ))
32
+ : children;
33
+
34
+ return (
35
+ <Component className={clsx(gapClass, className)}>
36
+ {content}
37
+ </Component>
38
+ );
39
+ }