@srcroot/ui 0.0.65 → 1.0.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/README.md CHANGED
@@ -1,146 +1,171 @@
1
1
  # @srcroot/ui
2
2
 
3
- A UI library with polymorphic, accessible React components.
4
- This library provides a collection of re-usable components that you can copy and paste into your apps.
3
+ A UI library with polymorphic, accessible React components for Next.js, Vite, and other React frameworks.
5
4
 
6
5
  ## Features
7
6
 
8
- - **Polymorphic**: Most components support an `as` prop (e.g., render a `Button` as an `a` tag).
9
- - **Accessible**: Built on standard HTML elements and WAI-ARIA patterns.
10
- - **Copy/Paste**: Not a dependency you install, but code you own.
11
- - **Styled**: Beautiful defaults using Tailwind CSS and `class-variance-authority`.
7
+ - **Framework-Aware**: Automatically detects your framework (Next.js, Vite, etc.) and uses the right component variants
8
+ - **Polymorphic**: Most components support an `as` prop (e.g., render a `Button` as an `a` tag)
9
+ - **Accessible**: Built on standard HTML elements and WAI-ARIA patterns
10
+ - **Copy/Paste**: Components are copied directly into your project — you own the code
11
+ - **Styled**: Beautiful defaults using Tailwind CSS and `class-variance-authority`
12
+ - **Multi-Platform**: Supports npm, Yarn, pnpm, Bun, and Deno
12
13
 
13
- ## Installation
14
-
15
- This library is distributed via a CLI that initializes your project and adds components directly to your source code.
14
+ ## Quick Start
16
15
 
17
16
  ### 1. Initialize
18
17
 
19
- Run the `init` command to set up the necessary dependencies and structural files (like `cn` utility) in your project.
20
-
21
18
  ```bash
22
19
  npx @srcroot/ui init
23
20
  ```
24
21
 
25
- This will ask a few questions to configure your project structure (e.g., where to put components).
22
+ This sets up your project with the `cn` utility, theme configuration, and creates the necessary structure.
26
23
 
27
24
  ### 2. Add Components
28
25
 
29
- Use the `add` command to install components. You can do this in three ways:
30
-
31
- **Interactive Mode:**
32
- Run without arguments to select components from a list.
33
26
  ```bash
27
+ # Interactive mode
34
28
  npx @srcroot/ui add
35
- ```
36
29
 
37
- **Specific Components:**
38
- Add one or more components by name.
39
- ```bash
30
+ # Specific components
40
31
  npx @srcroot/ui add button card input
41
- ```
42
32
 
43
- **Add All:**
44
- Install every available component at once.
45
- ```bash
33
+ # Add all components
46
34
  npx @srcroot/ui add --all
47
35
  ```
48
36
 
49
- This will copy the component files to your `components/ui` directory and install any necessary peer dependencies (like `react-icons` or `clsx`).
37
+ Components are installed to `src/components/ui/` (or `components/ui/` depending on your structure).
50
38
 
51
- ### 3. Usage
39
+ ## Supported Package Managers
52
40
 
53
- Import components directly from your project folder:
41
+ The CLI automatically detects and uses your package manager:
54
42
 
55
- ```tsx
56
- import { Button } from "@/components/ui/button"
57
-
58
- export default function Home() {
59
- return (
60
- <Button variant="destructive" onClick={() => alert("Clicked!")}>
61
- Click Me
62
- </Button>
63
- )
64
- }
65
- ```
43
+ - **npm** - `npm install`
44
+ - **Yarn** - `yarn add`
45
+ - **pnpm** - `pnpm add`
46
+ - **Bun** - `bun add`
47
+ - **Deno** - `deno add -A`
48
+
49
+ ## Framework Detection
50
+
51
+ The CLI automatically detects your framework:
52
+
53
+ | Framework | Detection | Notes |
54
+ |-----------|-----------|-------|
55
+ | Next.js | `next.config.ts/js/mjs` or `src/app` directory | Uses `next/script` for analytics |
56
+ | Vite | `vite.config.ts/js/mjs` | Uses `useEffect` for analytics, localStorage for themes |
66
57
 
67
58
  ## Available Components
68
59
 
69
- run `npx @srcroot/ui list` to see all available components.
60
+ Run `npx @srcroot/ui list` to see all available components.
61
+
62
+ ### Analytics
63
+ - `google-analytics` - Google Analytics 4 integration
64
+ - `google-tag-manager` - Google Tag Manager container
65
+ - `meta-pixel` - Meta/Facebook Pixel tracking
66
+ - `microsoft-clarity` - Microsoft Clarity analytics
67
+ - `tiktok-pixel` - TikTok Pixel tracking
70
68
 
71
69
  ### Core
72
- - `button` - Polymorphic button with variants.
73
- - `badge` - Status indicators.
74
- - `avatar` - User profile images with fallbacks.
75
- - `separator` - Visual divider.
76
- - `button-group` - Attached or spaced button sets.
70
+ - `button` - Polymorphic button with variants
71
+ - `badge` - Status indicators
72
+ - `avatar` - User profile images with fallbacks
73
+ - `separator` - Visual divider
74
+ - `button-group` - Attached or spaced button sets
75
+ - `slot` - Polymorphic slot for component composition
77
76
 
78
77
  ### Forms
79
- - `input` - Basic text input.
80
- - `textarea` - Multi-line text input.
81
- - `checkbox` - Toggle selection.
82
- - `radio` - Single selection from list.
83
- - `switch` - Toggle switch.
84
- - `select` - Dropdown selection.
85
- - `slider` - Range input.
86
- - `otp-input` - One-time password verification.
87
- - `search` - Search input with debounce support.
88
- - `calendar` - Date and range picker.
78
+ - `input` - Basic text input
79
+ - `textarea` - Multi-line text input
80
+ - `checkbox` - Toggle selection
81
+ - `radio` - Single selection from list
82
+ - `switch` - Toggle switch
83
+ - `select` - Dropdown selection
84
+ - `slider` - Range input
85
+ - `otp-input` - One-time password verification
86
+ - `search` - Search input with debounce
87
+ - `calendar` - Date and range picker
88
+ - `date-picker` - Date selection component
89
+ - `form-field` - Form field wrapper with label and validation
90
+ - `input-group` - Input with attached elements
89
91
 
90
92
  ### Layout
91
- - `card` - Content container with header/content/footer.
92
- - `container` - Centered layout wrapper.
93
- - `aspect-ratio` - Maintain element proportions.
93
+ - `card` - Content container with header/content/footer
94
+ - `container` - Centered layout wrapper
95
+ - `aspect-ratio` - Maintain element proportions
96
+ - `resizable` - Resizable panel groups
94
97
 
95
98
  ### Data Display
96
- - `text` - Polymorphic typography component.
97
- - `label` - Accessible form label.
98
- - `table` - Responsive data table.
99
- - `accordion` - Collapsible content sections.
100
- - `collapsible` - Expandable panel.
101
- - `tabs` - Tabbed content switcher.
102
- - `progress` - Progress bar.
103
- - `skeleton` - Loading placeholder state.
104
- - `image` - Enhanced img with fallback and loading state.
105
- - `carousel` - Content slider with autoplay.
99
+ - `text` - Polymorphic typography
100
+ - `label` - Accessible form label
101
+ - `table` - Responsive data table
102
+ - `table-of-contents` - Auto-generated TOC
103
+ - `accordion` - Collapsible content sections
104
+ - `collapsible` - Expandable panel
105
+ - `tabs` - Tabbed content switcher
106
+ - `progress` - Progress bar
107
+ - `skeleton` - Loading placeholder
108
+ - `image` - Enhanced image with fallback
109
+ - `carousel` - Content slider with autoplay
110
+ - `marquee` - Scrolling marquee animation
106
111
 
107
112
  ### Feedback
108
- - `loading-spinner` - SVG spinner with variants.
109
- - `star-rating` - Interactive rating component.
110
- - `toast` - Transient notifications.
111
- - `alert` - Critical information banner.
113
+ - `loading-spinner` - SVG spinner with variants
114
+ - `star-rating` - Interactive rating component
115
+ - `toast` - Transient notifications
116
+ - `alert` - Critical information banner
117
+ - `scroll-to-top` - Scroll to top button
118
+ - `scroll-animation` - Scroll-triggered animations
112
119
 
113
120
  ### Overlays
114
- - `dialog` - Modal dialog.
115
- - `alert-dialog` - Modal for confirming actions.
116
- - `sheet` - Side-panel overlay.
117
- - `popover` - Content appearing over trigger.
118
- - `tooltip` - Hover information.
119
- - `dropdown-menu` - Menu for actions/navigation.
121
+ - `dialog` - Modal dialog
122
+ - `alert-dialog` - Modal for confirming actions
123
+ - `sheet` - Side-panel overlay
124
+ - `popover` - Content appearing over trigger
125
+ - `tooltip` - Hover information
126
+ - `dropdown-menu` - Menu for actions/navigation
127
+ - `context-menu` - Right-click context menu
120
128
 
121
129
  ### Navigation
122
- - `breadcrumb` - Navigation trail.
123
- - `pagination` - Page navigation controls.
130
+ - `breadcrumb` - Navigation trail
131
+ - `pagination` - Page navigation controls
132
+ - `sidebar` - Collapsible sidebar with navigation
133
+ - `menubar` - Menu bar component
134
+
135
+ ### Specialized
136
+ - `chatbot` - Chat interface widget
137
+ - `chart` - Recharts-based chart component
138
+ - `map` - Google Maps embed
139
+ - `combobox` - Searchable dropdown (Command menu pattern)
140
+ - `command` - Command menu (Cmdk-style)
141
+ - `file-upload` - Drag-and-drop file upload
142
+ - `hover-card` - Hover-triggered card
143
+ - `native-select` - Native select wrapper
144
+ - `patterns` - Decorative background patterns
145
+ - `theme-switcher` - Light/dark/system theme toggle
146
+ - `whatsapp` - WhatsApp floating button
147
+ - `floating-dock` - macOS-style floating dock
124
148
 
125
149
  ## Polymorphism
126
150
 
127
- Our components accept an `as` prop to change the underlying HTML element while maintaining styles and behavior.
151
+ Components accept an `as` prop to change the underlying HTML element:
128
152
 
129
153
  ```tsx
130
- // Renders as an <a> tag but looks like a button
131
- <Button as="a" href="/login">
132
- Login
133
- </Button>
134
-
135
- // Renders as a specialized text variant
136
- <Text as="h1" variant="h1">
137
- Page Title
138
- </Text>
154
+ <Button as="a" href="/login">Login</Button>
155
+
156
+ <Text as="h1" variant="h1">Page Title</Text>
139
157
  ```
140
158
 
141
- ## Local Development
159
+ ## CLI Options
142
160
 
143
- To run the documentation/playground locally:
161
+ ```
162
+ npx @srcroot/ui init Initialize project structure
163
+ npx @srcroot/ui add [comps] Add component(s)
164
+ npx @srcroot/ui add --all Add all components
165
+ npx @srcroot/ui list List available components
166
+ ```
167
+
168
+ ## Local Development
144
169
 
145
170
  ```bash
146
171
  cd examples/playground
@@ -148,4 +173,4 @@ npm install
148
173
  npm run dev
149
174
  ```
150
175
 
151
- Visit `http://localhost:3001` to view the component showcase.
176
+ Visit `http://localhost:3001` to view the component showcase.
package/dist/index.js CHANGED
@@ -101,15 +101,13 @@ var ThemeService = class {
101
101
  };
102
102
 
103
103
  // src/cli/utils/templates.ts
104
- var TAILWIND_CONFIG = `import type { Config } from "tailwindcss"
104
+ function getTailwindConfig(framework) {
105
+ const contentPaths = framework === "vite" ? ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"] : ["./src/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}"];
106
+ return `import type { Config } from "tailwindcss"
105
107
 
106
108
  const config: Config = {
107
109
  darkMode: ["class"],
108
- content: [
109
- "./src/**/*.{js,ts,jsx,tsx,mdx}",
110
- "./app/**/*.{js,ts,jsx,tsx,mdx}",
111
- "./components/**/*.{js,ts,jsx,tsx,mdx}",
112
- ],
110
+ content: ${JSON.stringify(contentPaths, null, 6).replace(/\n/g, "\n ").replace(/\[$/, " [").replace(/\]$/, " ]")},
113
111
  theme: {
114
112
  extend: {
115
113
  colors: {
@@ -173,12 +171,15 @@ const config: Config = {
173
171
 
174
172
  export default config
175
173
  `;
174
+ }
175
+ var TAILWIND_CONFIG = getTailwindConfig("nextjs");
176
176
 
177
177
  // src/cli/utils/get-package-manager.ts
178
178
  import fs3 from "fs";
179
179
  import path3 from "path";
180
180
  function getPackageManager(cwd) {
181
181
  const dir = cwd || process.cwd();
182
+ if (fs3.existsSync(path3.join(dir, "deno.json")) || fs3.existsSync(path3.join(dir, "deno.jsonc"))) return "deno";
182
183
  if (fs3.existsSync(path3.join(dir, "bun.lockb"))) return "bun";
183
184
  if (fs3.existsSync(path3.join(dir, "pnpm-lock.yaml"))) return "pnpm";
184
185
  if (fs3.existsSync(path3.join(dir, "yarn.lock"))) return "yarn";
@@ -189,6 +190,8 @@ function getPackageManager(cwd) {
189
190
  if (userAgent.startsWith("pnpm")) return "pnpm";
190
191
  if (userAgent.startsWith("bun")) return "bun";
191
192
  }
193
+ const denoAgent = process.env.DENO_ENV;
194
+ if (denoAgent) return "deno";
192
195
  return "npm";
193
196
  }
194
197
 
@@ -261,10 +264,23 @@ var ProjectInitializer = class {
261
264
  process.exit(1);
262
265
  }
263
266
  }
267
+ detectFramework(cwd) {
268
+ if (fs5.existsSync(path5.join(cwd, "next.config.ts")) || fs5.existsSync(path5.join(cwd, "next.config.js")) || fs5.existsSync(path5.join(cwd, "next.config.mjs"))) {
269
+ return "nextjs";
270
+ }
271
+ if (fs5.existsSync(path5.join(cwd, "vite.config.ts")) || fs5.existsSync(path5.join(cwd, "vite.config.js")) || fs5.existsSync(path5.join(cwd, "vite.config.mjs"))) {
272
+ return "vite";
273
+ }
274
+ if (fs5.existsSync(path5.join(cwd, "src", "app"))) {
275
+ return "nextjs";
276
+ }
277
+ return "unknown";
278
+ }
264
279
  async detectConfiguration() {
265
280
  const cwd = path5.resolve(this.options.cwd);
266
281
  const packageManager = getPackageManager(cwd);
267
282
  const installCmd = packageManager === "npm" ? "install" : "add";
283
+ const framework = this.detectFramework(cwd);
268
284
  const pkg = await fs5.readJson(path5.join(cwd, "package.json"));
269
285
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
270
286
  const tailwindVersion = allDeps["tailwindcss"] || "";
@@ -303,7 +319,8 @@ var ProjectInitializer = class {
303
319
  hasPagesDir,
304
320
  libDir,
305
321
  componentsDir,
306
- globalsPath
322
+ globalsPath,
323
+ framework
307
324
  };
308
325
  }
309
326
  async promptUser() {
@@ -370,7 +387,7 @@ export function cn(...inputs: ClassValue[]) {
370
387
  if (!cfg.isTailwind4) {
371
388
  spinner.start("Setting up Tailwind config...");
372
389
  const tailwindConfigPath = path5.join(cfg.cwd, "tailwind.config.ts");
373
- await fs5.writeFile(tailwindConfigPath, TAILWIND_CONFIG);
390
+ await fs5.writeFile(tailwindConfigPath, getTailwindConfig(cfg.framework));
374
391
  spinner.succeed(`Created tailwind.config.ts`);
375
392
  }
376
393
  const packageInfo = getPackageInfo();
@@ -924,14 +941,123 @@ var REGISTRY = {
924
941
 
925
942
  // src/cli/services/component-adder.ts
926
943
  var __dirname4 = path6.dirname(fileURLToPath5(import.meta.url));
944
+ var FRAMEWORK_SPECIFIC_COMPONENTS = {
945
+ "google-analytics": "analytics/google-analytics.vite.tsx",
946
+ "google-tag-manager": "analytics/google-tag-manager.vite.tsx",
947
+ "meta-pixel": "analytics/meta-pixel.vite.tsx",
948
+ "microsoft-clarity": "analytics/microsoft-clarity.vite.tsx",
949
+ "tiktok-pixel": "analytics/tiktok-pixel.vite.tsx",
950
+ "theme-switcher": "ui/theme-switcher.vite.tsx"
951
+ };
927
952
  var ComponentAdder = class {
928
953
  cwd;
929
954
  options;
955
+ framework = "unknown";
930
956
  constructor(cwd, options) {
931
957
  this.cwd = cwd;
932
958
  this.options = options;
959
+ this.framework = this.detectFramework();
960
+ }
961
+ detectFramework() {
962
+ if (fs6.existsSync(path6.join(this.cwd, "next.config.ts")) || fs6.existsSync(path6.join(this.cwd, "next.config.js")) || fs6.existsSync(path6.join(this.cwd, "next.config.mjs"))) {
963
+ return "nextjs";
964
+ }
965
+ if (fs6.existsSync(path6.join(this.cwd, "vite.config.ts")) || fs6.existsSync(path6.join(this.cwd, "vite.config.js")) || fs6.existsSync(path6.join(this.cwd, "vite.config.mjs"))) {
966
+ return "vite";
967
+ }
968
+ if (fs6.existsSync(path6.join(this.cwd, "src", "app"))) {
969
+ return "nextjs";
970
+ }
971
+ return "unknown";
972
+ }
973
+ transformForVite(name, content) {
974
+ content = content.replace(/^"use client"[;\n\r]*/m, "");
975
+ content = content.replace(/import\s*{\s*useTheme\s*}\s*from\s*"next-themes"\s*;?\n?/g, "");
976
+ content = content.replace(/import\s*{\s*ThemeProvider\s*}\s*from\s*"next-themes"\s*;?\n?/g, "");
977
+ content = content.replace(/import\s*{\s*cn\s*}\s*from\s*"@\/lib\/utils"\s*;?\n?/g, 'import { cn } from "../../lib/utils"\n');
978
+ content = content.replace(/import\s*{\s*Slot\s*}\s*from\s*"@\/components\/ui\/slot"\s*;?\n?/g, 'import { Slot } from "./slot"\n');
979
+ const otherUiImports = [
980
+ "accordion",
981
+ "alert-dialog",
982
+ "alert",
983
+ "aspect-ratio",
984
+ "avatar",
985
+ "badge",
986
+ "breadcrumb",
987
+ "button",
988
+ "button-group",
989
+ "calendar",
990
+ "card",
991
+ "carousel",
992
+ "chart",
993
+ "chatbot",
994
+ "checkbox",
995
+ "collapsible",
996
+ "combobox",
997
+ "command",
998
+ "container",
999
+ "context-menu",
1000
+ "date-picker",
1001
+ "dialog",
1002
+ "drawer",
1003
+ "dropdown-menu",
1004
+ "empty-state",
1005
+ "file-upload",
1006
+ "floating-dock",
1007
+ "form-field",
1008
+ "hover-card",
1009
+ "image",
1010
+ "input",
1011
+ "input-group",
1012
+ "kbd",
1013
+ "label",
1014
+ "loading-spinner",
1015
+ "map",
1016
+ "marquee",
1017
+ "menubar",
1018
+ "native-select",
1019
+ "otp-input",
1020
+ "pagination",
1021
+ "patterns",
1022
+ "popover",
1023
+ "progress",
1024
+ "radio",
1025
+ "resizable",
1026
+ "scroll-animation",
1027
+ "scroll-area",
1028
+ "scroll-to-top",
1029
+ "search",
1030
+ "select",
1031
+ "separator",
1032
+ "sheet",
1033
+ "sidebar",
1034
+ "skeleton",
1035
+ "slider",
1036
+ "star-rating",
1037
+ "switch",
1038
+ "table",
1039
+ "table-of-contents",
1040
+ "tabs",
1041
+ "text",
1042
+ "textarea",
1043
+ "toast",
1044
+ "toggle",
1045
+ "toggle-group",
1046
+ "tooltip",
1047
+ "whatsapp"
1048
+ ];
1049
+ for (const comp of otherUiImports) {
1050
+ const regex = new RegExp(`from\\s+"@/components/ui/${comp}"`, "g");
1051
+ content = content.replace(regex, `from "./${comp}"`);
1052
+ }
1053
+ return content.trimStart();
933
1054
  }
934
1055
  async add(components) {
1056
+ if (this.framework === "vite") {
1057
+ logger.info("Detected Vite project - using Vite-compatible components");
1058
+ } else if (this.framework === "nextjs") {
1059
+ logger.info("Detected Next.js project - using Next.js components");
1060
+ }
935
1061
  components = await this.resolveComponents(components);
936
1062
  const { valid, invalid } = this.validateComponents(components);
937
1063
  if (invalid.length > 0) {
@@ -1023,9 +1149,23 @@ var ComponentAdder = class {
1023
1149
  async installPackages(packages) {
1024
1150
  const packageManager = getPackageManager(this.cwd);
1025
1151
  const spinner = ora2("Installing dependencies...").start();
1026
- const installCmd = packageManager === "npm" ? "install" : "add";
1152
+ let installCmd;
1153
+ switch (packageManager) {
1154
+ case "deno":
1155
+ installCmd = ["add", "-A", ...packages];
1156
+ break;
1157
+ case "bun":
1158
+ installCmd = ["add", ...packages];
1159
+ break;
1160
+ case "pnpm":
1161
+ case "yarn":
1162
+ installCmd = ["add", ...packages];
1163
+ break;
1164
+ default:
1165
+ installCmd = ["install", ...packages];
1166
+ }
1027
1167
  try {
1028
- await execa2(packageManager, [installCmd, ...packages], {
1168
+ await execa2(packageManager, installCmd, {
1029
1169
  cwd: this.cwd
1030
1170
  });
1031
1171
  spinner.succeed("Dependencies installed");
@@ -1104,12 +1244,26 @@ Please manually install: ${packages.join(" ")}`);
1104
1244
  }
1105
1245
  spinner.start("Adding components...");
1106
1246
  }
1107
- const registryPath = path6.resolve(getRegistryPath(), comp.file);
1247
+ let registryPath = path6.resolve(getRegistryPath(), comp.file);
1248
+ let content = "";
1249
+ if (this.framework === "vite") {
1250
+ const viteVariant = FRAMEWORK_SPECIFIC_COMPONENTS[name];
1251
+ if (viteVariant) {
1252
+ const vitePath = path6.resolve(getRegistryPath(), viteVariant);
1253
+ if (fs6.existsSync(vitePath)) {
1254
+ registryPath = vitePath;
1255
+ spinner.info(`Using Vite-specific ${fileName}`);
1256
+ }
1257
+ }
1258
+ }
1108
1259
  if (!fs6.existsSync(registryPath)) {
1109
1260
  spinner.warn(`Registry file not found for ${name}: ${registryPath}`);
1110
1261
  continue;
1111
1262
  }
1112
- const content = await fs6.readFile(registryPath, "utf-8");
1263
+ content = await fs6.readFile(registryPath, "utf-8");
1264
+ if (this.framework === "vite") {
1265
+ content = this.transformForVite(name, content);
1266
+ }
1113
1267
  await fs6.writeFile(targetPath, content);
1114
1268
  addedCount++;
1115
1269
  if (components.length > 10) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srcroot/ui",
3
- "version": "0.0.65",
3
+ "version": "1.0.1",
4
4
  "description": "A UI library with polymorphic, accessible React components",
5
5
  "author": "Shifaul Islam",
6
6
  "license": "MIT",
@@ -71,4 +71,4 @@
71
71
  "optional": true
72
72
  }
73
73
  }
74
- }
74
+ }
@@ -0,0 +1,35 @@
1
+ import { useEffect } from "react"
2
+ import type { FC } from "react"
3
+
4
+ interface GoogleAnalyticsProps {
5
+ gaIds: string[];
6
+ }
7
+
8
+ const GoogleAnalytics: FC<GoogleAnalyticsProps> = ({ gaIds }) => {
9
+ useEffect(() => {
10
+ if (gaIds.length === 0) return
11
+
12
+ const gtmScript = document.createElement("script")
13
+ gtmScript.async = true
14
+ gtmScript.src = `https://www.googletagmanager.com/gtag/js?id=${gaIds[0]}`
15
+ document.head.appendChild(gtmScript)
16
+
17
+ const inlineScript = document.createElement("script")
18
+ inlineScript.innerHTML = `
19
+ window.dataLayer = window.dataLayer || [];
20
+ function gtag(){dataLayer.push(arguments);}
21
+ gtag('js', new Date());
22
+ ${gaIds.map((id) => `gtag('config', '${id}', { page_path: window.location.pathname });`).join("\n")}
23
+ `
24
+ document.head.appendChild(inlineScript)
25
+
26
+ return () => {
27
+ document.head.removeChild(gtmScript)
28
+ document.head.removeChild(inlineScript)
29
+ }
30
+ }, [gaIds])
31
+
32
+ return null
33
+ }
34
+
35
+ export default GoogleAnalytics
@@ -0,0 +1,50 @@
1
+ import { useEffect } from "react"
2
+ import type { FC, ReactNode } from "react"
3
+
4
+ interface GTMContainer {
5
+ gtmId: string;
6
+ tagServerUrl?: string;
7
+ }
8
+
9
+ interface GoogleTagManagerProps {
10
+ containers: GTMContainer[];
11
+ }
12
+
13
+ const GoogleTagManager: FC<GoogleTagManagerProps> = ({ containers }) => {
14
+ useEffect(() => {
15
+ const defaultServer = "https://www.googletagmanager.com"
16
+
17
+ const scriptsMap = containers.reduce((map, container) => {
18
+ const server = container.tagServerUrl || defaultServer;
19
+ if (!map.has(server)) {
20
+ map.set(server, []);
21
+ }
22
+ map.get(server)!.push(container.gtmId);
23
+ return map;
24
+ }, new Map<string, string[]>()); const scriptElements: HTMLScriptElement[] = []
25
+
26
+ Array.from(scriptsMap.entries()).forEach(([server, ids]) => {
27
+ ids.forEach((id) => {
28
+ const script = document.createElement("script")
29
+ script.innerHTML = `
30
+ (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s);j.async=true;j.src="${server}/gtm.js?"+i;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${id}');
31
+ `
32
+ script.id = `gtm-script-${server}-${id}`
33
+ document.head.appendChild(script)
34
+ scriptElements.push(script)
35
+ })
36
+ })
37
+
38
+ return () => {
39
+ scriptElements.forEach((script) => {
40
+ if (document.head.contains(script)) {
41
+ document.head.removeChild(script)
42
+ }
43
+ })
44
+ }
45
+ }, [containers])
46
+
47
+ return null
48
+ }
49
+
50
+ export default GoogleTagManager
@@ -0,0 +1,38 @@
1
+ import { useEffect } from "react"
2
+ import type { FC } from "react"
3
+
4
+ interface MetaPixelProps {
5
+ pixelIds: string[];
6
+ }
7
+
8
+ const MetaPixel: FC<MetaPixelProps> = ({ pixelIds }) => {
9
+ useEffect(() => {
10
+ if (pixelIds.length === 0) return
11
+
12
+ const script = document.createElement("script")
13
+ script.innerHTML = `
14
+ !function(f,b,e,v,n,t,s)
15
+ {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
16
+ n.callMethod.apply(n,arguments):n.queue.push(arguments)};
17
+ if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
18
+ n.queue=[];t=b.createElement(e);t.async=!0;
19
+ t.src=v;s=b.getElementsByTagName(e)[0];
20
+ s.parentNode.insertBefore(t,s)}(window, document,'script',
21
+ 'https://connect.facebook.net/en_US/fbevents.js');
22
+ ${pixelIds.map((id) => `fbq('init', '${id}');`).join("\n")}
23
+ fbq('track', 'PageView');
24
+ `
25
+ script.id = "fb-script-multi"
26
+ document.head.appendChild(script)
27
+
28
+ return () => {
29
+ if (document.head.contains(script)) {
30
+ document.head.removeChild(script)
31
+ }
32
+ }
33
+ }, [pixelIds])
34
+
35
+ return null
36
+ }
37
+
38
+ export default MetaPixel
@@ -0,0 +1,36 @@
1
+ import { useEffect } from "react"
2
+ import type { FC } from "react"
3
+
4
+ interface MicrosoftClarityProps {
5
+ clarityIds: string[];
6
+ }
7
+
8
+ const MicrosoftClarity: FC<MicrosoftClarityProps> = ({ clarityIds }) => {
9
+ useEffect(() => {
10
+ clarityIds.forEach((id) => {
11
+ const script = document.createElement("script")
12
+ script.innerHTML = `
13
+ (function(c,l,a,r,i,t,y){
14
+ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
15
+ t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
16
+ y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
17
+ })(window, document, "clarity", "script", "${id}");
18
+ `
19
+ script.id = `microsoft-clarity-init-${id}`
20
+ document.head.appendChild(script)
21
+ })
22
+
23
+ return () => {
24
+ clarityIds.forEach((id) => {
25
+ const script = document.getElementById(`microsoft-clarity-init-${id}`)
26
+ if (script && document.head.contains(script)) {
27
+ document.head.removeChild(script)
28
+ }
29
+ })
30
+ }
31
+ }, [clarityIds])
32
+
33
+ return null
34
+ }
35
+
36
+ export default MicrosoftClarity
@@ -0,0 +1,39 @@
1
+ import { useEffect } from "react"
2
+ import type { FC } from "react"
3
+
4
+ interface TikTokPixelProps {
5
+ pixelIds: string[];
6
+ }
7
+
8
+ const TikTokPixel: FC<TikTokPixelProps> = ({ pixelIds }) => {
9
+ useEffect(() => {
10
+ const script = document.createElement("script")
11
+ script.innerHTML = `
12
+ !function (w, d, t) {
13
+ w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];
14
+ ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie","holdConsent","revokeConsent","grantConsent"],
15
+ ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
16
+ for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);
17
+ ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},
18
+ ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js";
19
+ ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};
20
+ var s=document.createElement("script");s.type="text/javascript",s.async=!0,s.src=r+"?sdkid="+e+"&lib="+t;
21
+ var p=document.getElementsByTagName("script")[0];p.parentNode.insertBefore(s,p)};
22
+ ${pixelIds.map((id) => `ttq.load('${id}');`).join("\n")}
23
+ ttq.page();
24
+ }(window, document, 'ttq');
25
+ `
26
+ script.id = "tiktok-script-multi"
27
+ document.head.appendChild(script)
28
+
29
+ return () => {
30
+ if (document.head.contains(script)) {
31
+ document.head.removeChild(script)
32
+ }
33
+ }
34
+ }, [pixelIds])
35
+
36
+ return null
37
+ }
38
+
39
+ export default TikTokPixel
@@ -0,0 +1,73 @@
1
+ import * as React from "react"
2
+ import { FiSun, FiMoon, FiMonitor } from "react-icons/fi"
3
+ import { cn } from "../lib/utils"
4
+
5
+ interface ThemeSwitcherProps {
6
+ onThemeChange?: (theme: string) => void
7
+ className?: string
8
+ }
9
+
10
+ export function ThemeSwitcher({ onThemeChange, className }: ThemeSwitcherProps) {
11
+ const [theme, setThemeState] = React.useState<string>("light")
12
+ const [mounted, setMounted] = React.useState(false)
13
+
14
+ React.useEffect(() => {
15
+ setMounted(true)
16
+ const stored = localStorage.getItem("theme") || "light"
17
+ setThemeState(stored)
18
+ }, [])
19
+
20
+ const setTheme = (newTheme: string) => {
21
+ setThemeState(newTheme)
22
+ localStorage.setItem("theme", newTheme)
23
+ if (newTheme === "dark") {
24
+ document.documentElement.classList.add("dark")
25
+ } else {
26
+ document.documentElement.classList.remove("dark")
27
+ }
28
+ onThemeChange?.(newTheme)
29
+ }
30
+
31
+ const toggleTheme = () => {
32
+ if (theme === "light") {
33
+ setTheme("dark")
34
+ } else if (theme === "dark") {
35
+ setTheme("system")
36
+ } else {
37
+ setTheme("light")
38
+ }
39
+ }
40
+
41
+ const getIcon = () => {
42
+ if (!mounted) {
43
+ return <FiSun className="mr-2 h-4 w-4" />
44
+ }
45
+ if (theme === "system") {
46
+ return <FiMonitor className="mr-2 h-4 w-4" />
47
+ }
48
+ if (theme === "dark") {
49
+ return <FiMoon className="mr-2 h-4 w-4" />
50
+ }
51
+ return <FiSun className="mr-2 h-4 w-4" />
52
+ }
53
+
54
+ const getLabel = () => {
55
+ if (!mounted) return "Theme"
56
+ if (theme === "system") return "System"
57
+ if (theme === "dark") return "Dark"
58
+ return "Light"
59
+ }
60
+
61
+ return (
62
+ <button
63
+ onClick={toggleTheme}
64
+ className={cn(
65
+ "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer",
66
+ className
67
+ )}
68
+ >
69
+ {getIcon()}
70
+ <span>Theme: {getLabel()}</span>
71
+ </button>
72
+ )
73
+ }