@burdenoff/website-sdk 0.0.0-dev → 2026.511.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.
Files changed (91) hide show
  1. package/dist/analytics/index.js +0 -1
  2. package/dist/analytics/index.js.map +1 -1
  3. package/dist/analytics/index.mjs +0 -1
  4. package/dist/analytics/index.mjs.map +1 -1
  5. package/dist/client/index.d.mts +2 -5
  6. package/dist/client/index.d.ts +2 -5
  7. package/dist/client/index.js +2 -11
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/client/index.mjs +2 -11
  10. package/dist/client/index.mjs.map +1 -1
  11. package/dist/components/contact.d.mts +2 -77
  12. package/dist/components/contact.d.ts +2 -77
  13. package/dist/components/contact.js +9 -112
  14. package/dist/components/contact.js.map +1 -1
  15. package/dist/components/contact.mjs +10 -112
  16. package/dist/components/contact.mjs.map +1 -1
  17. package/dist/components/newsletter.js +6 -17
  18. package/dist/components/newsletter.js.map +1 -1
  19. package/dist/components/newsletter.mjs +6 -17
  20. package/dist/components/newsletter.mjs.map +1 -1
  21. package/dist/components/pricing.d.mts +0 -38
  22. package/dist/components/pricing.d.ts +0 -38
  23. package/dist/components/pricing.js +84 -267
  24. package/dist/components/pricing.js.map +1 -1
  25. package/dist/components/pricing.mjs +84 -267
  26. package/dist/components/pricing.mjs.map +1 -1
  27. package/dist/components/waitlist.js +2 -6
  28. package/dist/components/waitlist.js.map +1 -1
  29. package/dist/components/waitlist.mjs +2 -6
  30. package/dist/components/waitlist.mjs.map +1 -1
  31. package/dist/content/index.d.mts +9 -181
  32. package/dist/content/index.d.ts +9 -181
  33. package/dist/content/index.js +16 -2177
  34. package/dist/content/index.js.map +1 -1
  35. package/dist/content/index.mjs +6 -2141
  36. package/dist/content/index.mjs.map +1 -1
  37. package/dist/index.d.mts +5 -43
  38. package/dist/index.d.ts +5 -43
  39. package/dist/index.js +373 -3919
  40. package/dist/index.js.map +1 -1
  41. package/dist/index.mjs +378 -3905
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/product/index.d.mts +0 -8
  44. package/dist/product/index.d.ts +0 -8
  45. package/dist/product/index.js +4 -24
  46. package/dist/product/index.js.map +1 -1
  47. package/dist/product/index.mjs +4 -24
  48. package/dist/product/index.mjs.map +1 -1
  49. package/package.json +3 -5
  50. package/dist/components/electron-download.d.mts +0 -32
  51. package/dist/components/electron-download.d.ts +0 -32
  52. package/dist/components/electron-download.js +0 -131
  53. package/dist/components/electron-download.js.map +0 -1
  54. package/dist/components/electron-download.mjs +0 -108
  55. package/dist/components/electron-download.mjs.map +0 -1
  56. package/dist/components/launch-countdown.d.mts +0 -15
  57. package/dist/components/launch-countdown.d.ts +0 -15
  58. package/dist/components/launch-countdown.js +0 -89
  59. package/dist/components/launch-countdown.js.map +0 -1
  60. package/dist/components/launch-countdown.mjs +0 -87
  61. package/dist/components/launch-countdown.mjs.map +0 -1
  62. package/dist/components/partners.d.mts +0 -88
  63. package/dist/components/partners.d.ts +0 -88
  64. package/dist/components/partners.js +0 -1169
  65. package/dist/components/partners.js.map +0 -1
  66. package/dist/components/partners.mjs +0 -1143
  67. package/dist/components/partners.mjs.map +0 -1
  68. package/dist/components/problems-solved.d.mts +0 -37
  69. package/dist/components/problems-solved.d.ts +0 -37
  70. package/dist/components/problems-solved.js +0 -157
  71. package/dist/components/problems-solved.js.map +0 -1
  72. package/dist/components/problems-solved.mjs +0 -135
  73. package/dist/components/problems-solved.mjs.map +0 -1
  74. package/dist/components/resource-links.d.mts +0 -28
  75. package/dist/components/resource-links.d.ts +0 -28
  76. package/dist/components/resource-links.js +0 -229
  77. package/dist/components/resource-links.js.map +0 -1
  78. package/dist/components/resource-links.mjs +0 -227
  79. package/dist/components/resource-links.mjs.map +0 -1
  80. package/dist/components/social-links.d.mts +0 -37
  81. package/dist/components/social-links.d.ts +0 -37
  82. package/dist/components/social-links.js +0 -268
  83. package/dist/components/social-links.js.map +0 -1
  84. package/dist/components/social-links.mjs +0 -266
  85. package/dist/components/social-links.mjs.map +0 -1
  86. package/dist/components/website-switcher.d.mts +0 -27
  87. package/dist/components/website-switcher.d.ts +0 -27
  88. package/dist/components/website-switcher.js +0 -168
  89. package/dist/components/website-switcher.js.map +0 -1
  90. package/dist/components/website-switcher.mjs +0 -146
  91. package/dist/components/website-switcher.mjs.map +0 -1
@@ -1,168 +0,0 @@
1
- 'use strict';
2
-
3
- var React = require('react');
4
- var DropdownMenu = require('@radix-ui/react-dropdown-menu');
5
- var lucideReact = require('lucide-react');
6
- var clsx = require('clsx');
7
- var tailwindMerge = require('tailwind-merge');
8
- var jsxRuntime = require('react/jsx-runtime');
9
-
10
- function _interopNamespace(e) {
11
- if (e && e.__esModule) return e;
12
- var n = Object.create(null);
13
- if (e) {
14
- Object.keys(e).forEach(function (k) {
15
- if (k !== 'default') {
16
- var d = Object.getOwnPropertyDescriptor(e, k);
17
- Object.defineProperty(n, k, d.get ? d : {
18
- enumerable: true,
19
- get: function () { return e[k]; }
20
- });
21
- }
22
- });
23
- }
24
- n.default = e;
25
- return Object.freeze(n);
26
- }
27
-
28
- var React__namespace = /*#__PURE__*/_interopNamespace(React);
29
- var DropdownMenu__namespace = /*#__PURE__*/_interopNamespace(DropdownMenu);
30
-
31
- function cn(...inputs) {
32
- return tailwindMerge.twMerge(clsx.clsx(inputs));
33
- }
34
- var Input = React__namespace.forwardRef(
35
- ({ className, type, ...props }, ref) => {
36
- return /* @__PURE__ */ jsxRuntime.jsx(
37
- "input",
38
- {
39
- type,
40
- className: cn(
41
- "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
42
- className
43
- ),
44
- ref,
45
- ...props
46
- }
47
- );
48
- }
49
- );
50
- Input.displayName = "Input";
51
- function normalizeSearch(value) {
52
- return value.toLowerCase().trim();
53
- }
54
- function filterProducts(products, query) {
55
- const normalized = normalizeSearch(query);
56
- if (!normalized) return products;
57
- return products.filter((p) => normalizeSearch(p.name).includes(normalized));
58
- }
59
- function WebsiteSwitcher({
60
- products,
61
- triggerLabel = "Explore products",
62
- searchPlaceholder = "Find a product\u2026",
63
- emptyText = "No products match your search.",
64
- className
65
- }) {
66
- const [open, setOpen] = React.useState(false);
67
- const [query, setQuery] = React.useState("");
68
- const filtered = React.useMemo(
69
- () => filterProducts(products, query),
70
- [products, query]
71
- );
72
- return /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu__namespace.Root, { open, onOpenChange: setOpen, children: [
73
- /* @__PURE__ */ jsxRuntime.jsxs(
74
- DropdownMenu__namespace.Trigger,
75
- {
76
- className: cn(
77
- "inline-flex items-center gap-1.5 text-sm font-medium transition-colors",
78
- "text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-md",
79
- className
80
- ),
81
- "aria-label": triggerLabel,
82
- children: [
83
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { className: "h-4 w-4", "aria-hidden": "true" }),
84
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: triggerLabel }),
85
- /* @__PURE__ */ jsxRuntime.jsx(
86
- lucideReact.ChevronDown,
87
- {
88
- className: cn(
89
- "h-3.5 w-3.5 transition-transform duration-200",
90
- open && "rotate-180"
91
- ),
92
- "aria-hidden": "true"
93
- }
94
- )
95
- ]
96
- }
97
- ),
98
- /* @__PURE__ */ jsxRuntime.jsx(DropdownMenu__namespace.Portal, { children: /* @__PURE__ */ jsxRuntime.jsxs(
99
- DropdownMenu__namespace.Content,
100
- {
101
- className: "z-50 min-w-[16rem] max-w-[20rem] rounded-lg border border-border/60 bg-popover p-2 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2",
102
- sideOffset: 8,
103
- align: "end",
104
- avoidCollisions: true,
105
- children: [
106
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-2 pb-2 pt-1", children: [
107
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-muted-foreground mb-2", children: "Burdenoff ecosystem" }),
108
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
109
- /* @__PURE__ */ jsxRuntime.jsx(
110
- lucideReact.Search,
111
- {
112
- className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground",
113
- "aria-hidden": "true"
114
- }
115
- ),
116
- /* @__PURE__ */ jsxRuntime.jsx(
117
- Input,
118
- {
119
- type: "search",
120
- value: query,
121
- onChange: (e) => setQuery(e.target.value),
122
- placeholder: searchPlaceholder,
123
- className: "h-8 pl-8 text-sm",
124
- "aria-label": searchPlaceholder,
125
- onKeyDown: (e) => {
126
- if (e.key === "Escape") {
127
- setQuery("");
128
- }
129
- }
130
- }
131
- )
132
- ] })
133
- ] }),
134
- /* @__PURE__ */ jsxRuntime.jsx(DropdownMenu__namespace.Separator, { className: "my-1 h-px bg-border/60" }),
135
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-[min(60vh,24rem)] overflow-y-auto py-1", children: filtered.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: emptyText }) : filtered.map((product) => /* @__PURE__ */ jsxRuntime.jsx(DropdownMenu__namespace.Item, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
136
- "a",
137
- {
138
- href: product.websiteUrl,
139
- target: "_blank",
140
- rel: "noopener noreferrer",
141
- className: cn(
142
- "flex items-center gap-3 rounded-md px-2 py-2 text-sm outline-none",
143
- "text-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
144
- ),
145
- children: [
146
- product.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx(
147
- "img",
148
- {
149
- src: product.logoUrl,
150
- alt: "",
151
- "aria-hidden": "true",
152
- className: "h-5 w-5 shrink-0 rounded object-contain"
153
- }
154
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-primary/10 text-[10px] font-bold text-primary", children: product.name.charAt(0) }),
155
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: product.name })
156
- ]
157
- }
158
- ) }, product.slug)) }),
159
- /* @__PURE__ */ jsxRuntime.jsx(DropdownMenu__namespace.Arrow, { className: "fill-popover" })
160
- ]
161
- }
162
- ) })
163
- ] });
164
- }
165
-
166
- exports.WebsiteSwitcher = WebsiteSwitcher;
167
- //# sourceMappingURL=website-switcher.js.map
168
- //# sourceMappingURL=website-switcher.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/utils/cn.ts","../../src/ui/input.tsx","../../src/components/website-switcher.tsx"],"names":["twMerge","clsx","React","jsx","useState","useMemo","jsxs","DropdownMenu","Globe","ChevronDown","Search"],"mappings":"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","file":"website-switcher.js","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import * as React from \"react\";\n\nimport { cn } from \"../utils/cn\";\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n className,\n )}\n ref={ref}\n {...props}\n />\n );\n },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n","\"use client\";\n\n/**\n * WebsiteSwitcher Component\n *\n * A dropdown menu for navigating between Burdenoff ecosystem product websites.\n * Includes a search filter and renders each product with its logo and name.\n *\n * @example\n * ```tsx\n * import { WebsiteSwitcher } from '@burdenoff/website-sdk'\n *\n * <WebsiteSwitcher\n * products={[\n * { slug: 'vibecontrols', name: 'VibeControls', logoUrl: '/vibe.svg', websiteUrl: 'https://vibecontrols.com' },\n * { slug: 'fluidgrids', name: 'FluidGrids', logoUrl: '/fg.svg', websiteUrl: 'https://fluidgrids.com' },\n * ]}\n * />\n * ```\n */\n\nimport { useMemo, useState } from \"react\";\n\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { ChevronDown, Globe, Search } from \"lucide-react\";\n\nimport { Input } from \"../ui/input\";\nimport { cn } from \"../utils/cn\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WebsiteSwitcherProduct {\n /** Stable identifier for the product. */\n slug: string;\n /** Human-readable product name. */\n name: string;\n /** URL to the product logo (absolute or relative). */\n logoUrl?: string | null;\n /** Product marketing site URL. */\n websiteUrl: string;\n}\n\nexport interface WebsiteSwitcherProps {\n /** All Burdenoff ecosystem products to display. */\n products: WebsiteSwitcherProduct[];\n /** Accessible label for the trigger. */\n triggerLabel?: string;\n /** Placeholder text for the search input. */\n searchPlaceholder?: string;\n /** Empty-state text when filtering returns no results. */\n emptyText?: string;\n /** Optional additional CSS class for the trigger. */\n className?: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction normalizeSearch(value: string): string {\n return value.toLowerCase().trim();\n}\n\nfunction filterProducts(\n products: WebsiteSwitcherProduct[],\n query: string,\n): WebsiteSwitcherProduct[] {\n const normalized = normalizeSearch(query);\n if (!normalized) return products;\n return products.filter((p) => normalizeSearch(p.name).includes(normalized));\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function WebsiteSwitcher({\n products,\n triggerLabel = \"Explore products\",\n searchPlaceholder = \"Find a product…\",\n emptyText = \"No products match your search.\",\n className,\n}: WebsiteSwitcherProps) {\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n\n const filtered = useMemo(\n () => filterProducts(products, query),\n [products, query],\n );\n\n return (\n <DropdownMenu.Root open={open} onOpenChange={setOpen}>\n <DropdownMenu.Trigger\n className={cn(\n \"inline-flex items-center gap-1.5 text-sm font-medium transition-colors\",\n \"text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-md\",\n className,\n )}\n aria-label={triggerLabel}\n >\n <Globe className=\"h-4 w-4\" aria-hidden=\"true\" />\n <span className=\"hidden sm:inline\">{triggerLabel}</span>\n <ChevronDown\n className={cn(\n \"h-3.5 w-3.5 transition-transform duration-200\",\n open && \"rotate-180\",\n )}\n aria-hidden=\"true\"\n />\n </DropdownMenu.Trigger>\n\n <DropdownMenu.Portal>\n <DropdownMenu.Content\n className=\"z-50 min-w-[16rem] max-w-[20rem] rounded-lg border border-border/60 bg-popover p-2 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2\"\n sideOffset={8}\n align=\"end\"\n avoidCollisions\n >\n <div className=\"px-2 pb-2 pt-1\">\n <p className=\"text-xs font-medium text-muted-foreground mb-2\">\n Burdenoff ecosystem\n </p>\n <div className=\"relative\">\n <Search\n className=\"absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground\"\n aria-hidden=\"true\"\n />\n <Input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={searchPlaceholder}\n className=\"h-8 pl-8 text-sm\"\n aria-label={searchPlaceholder}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") {\n setQuery(\"\");\n }\n }}\n />\n </div>\n </div>\n\n <DropdownMenu.Separator className=\"my-1 h-px bg-border/60\" />\n\n <div className=\"max-h-[min(60vh,24rem)] overflow-y-auto py-1\">\n {filtered.length === 0 ? (\n <div className=\"px-3 py-4 text-center text-sm text-muted-foreground\">\n {emptyText}\n </div>\n ) : (\n filtered.map((product) => (\n <DropdownMenu.Item key={product.slug} asChild>\n <a\n href={product.websiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={cn(\n \"flex items-center gap-3 rounded-md px-2 py-2 text-sm outline-none\",\n \"text-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n )}\n >\n {product.logoUrl ? (\n <img\n src={product.logoUrl}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"h-5 w-5 shrink-0 rounded object-contain\"\n />\n ) : (\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-primary/10 text-[10px] font-bold text-primary\">\n {product.name.charAt(0)}\n </span>\n )}\n <span className=\"truncate\">{product.name}</span>\n </a>\n </DropdownMenu.Item>\n ))\n )}\n </div>\n\n <DropdownMenu.Arrow className=\"fill-popover\" />\n </DropdownMenu.Content>\n </DropdownMenu.Portal>\n </DropdownMenu.Root>\n );\n}\n"]}
@@ -1,146 +0,0 @@
1
- import * as React from 'react';
2
- import { useState, useMemo } from 'react';
3
- import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
4
- import { Globe, ChevronDown, Search } from 'lucide-react';
5
- import { clsx } from 'clsx';
6
- import { twMerge } from 'tailwind-merge';
7
- import { jsx, jsxs } from 'react/jsx-runtime';
8
-
9
- function cn(...inputs) {
10
- return twMerge(clsx(inputs));
11
- }
12
- var Input = React.forwardRef(
13
- ({ className, type, ...props }, ref) => {
14
- return /* @__PURE__ */ jsx(
15
- "input",
16
- {
17
- type,
18
- className: cn(
19
- "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
20
- className
21
- ),
22
- ref,
23
- ...props
24
- }
25
- );
26
- }
27
- );
28
- Input.displayName = "Input";
29
- function normalizeSearch(value) {
30
- return value.toLowerCase().trim();
31
- }
32
- function filterProducts(products, query) {
33
- const normalized = normalizeSearch(query);
34
- if (!normalized) return products;
35
- return products.filter((p) => normalizeSearch(p.name).includes(normalized));
36
- }
37
- function WebsiteSwitcher({
38
- products,
39
- triggerLabel = "Explore products",
40
- searchPlaceholder = "Find a product\u2026",
41
- emptyText = "No products match your search.",
42
- className
43
- }) {
44
- const [open, setOpen] = useState(false);
45
- const [query, setQuery] = useState("");
46
- const filtered = useMemo(
47
- () => filterProducts(products, query),
48
- [products, query]
49
- );
50
- return /* @__PURE__ */ jsxs(DropdownMenu.Root, { open, onOpenChange: setOpen, children: [
51
- /* @__PURE__ */ jsxs(
52
- DropdownMenu.Trigger,
53
- {
54
- className: cn(
55
- "inline-flex items-center gap-1.5 text-sm font-medium transition-colors",
56
- "text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-md",
57
- className
58
- ),
59
- "aria-label": triggerLabel,
60
- children: [
61
- /* @__PURE__ */ jsx(Globe, { className: "h-4 w-4", "aria-hidden": "true" }),
62
- /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: triggerLabel }),
63
- /* @__PURE__ */ jsx(
64
- ChevronDown,
65
- {
66
- className: cn(
67
- "h-3.5 w-3.5 transition-transform duration-200",
68
- open && "rotate-180"
69
- ),
70
- "aria-hidden": "true"
71
- }
72
- )
73
- ]
74
- }
75
- ),
76
- /* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsxs(
77
- DropdownMenu.Content,
78
- {
79
- className: "z-50 min-w-[16rem] max-w-[20rem] rounded-lg border border-border/60 bg-popover p-2 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2",
80
- sideOffset: 8,
81
- align: "end",
82
- avoidCollisions: true,
83
- children: [
84
- /* @__PURE__ */ jsxs("div", { className: "px-2 pb-2 pt-1", children: [
85
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-2", children: "Burdenoff ecosystem" }),
86
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
87
- /* @__PURE__ */ jsx(
88
- Search,
89
- {
90
- className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground",
91
- "aria-hidden": "true"
92
- }
93
- ),
94
- /* @__PURE__ */ jsx(
95
- Input,
96
- {
97
- type: "search",
98
- value: query,
99
- onChange: (e) => setQuery(e.target.value),
100
- placeholder: searchPlaceholder,
101
- className: "h-8 pl-8 text-sm",
102
- "aria-label": searchPlaceholder,
103
- onKeyDown: (e) => {
104
- if (e.key === "Escape") {
105
- setQuery("");
106
- }
107
- }
108
- }
109
- )
110
- ] })
111
- ] }),
112
- /* @__PURE__ */ jsx(DropdownMenu.Separator, { className: "my-1 h-px bg-border/60" }),
113
- /* @__PURE__ */ jsx("div", { className: "max-h-[min(60vh,24rem)] overflow-y-auto py-1", children: filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: emptyText }) : filtered.map((product) => /* @__PURE__ */ jsx(DropdownMenu.Item, { asChild: true, children: /* @__PURE__ */ jsxs(
114
- "a",
115
- {
116
- href: product.websiteUrl,
117
- target: "_blank",
118
- rel: "noopener noreferrer",
119
- className: cn(
120
- "flex items-center gap-3 rounded-md px-2 py-2 text-sm outline-none",
121
- "text-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
122
- ),
123
- children: [
124
- product.logoUrl ? /* @__PURE__ */ jsx(
125
- "img",
126
- {
127
- src: product.logoUrl,
128
- alt: "",
129
- "aria-hidden": "true",
130
- className: "h-5 w-5 shrink-0 rounded object-contain"
131
- }
132
- ) : /* @__PURE__ */ jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-primary/10 text-[10px] font-bold text-primary", children: product.name.charAt(0) }),
133
- /* @__PURE__ */ jsx("span", { className: "truncate", children: product.name })
134
- ]
135
- }
136
- ) }, product.slug)) }),
137
- /* @__PURE__ */ jsx(DropdownMenu.Arrow, { className: "fill-popover" })
138
- ]
139
- }
140
- ) })
141
- ] });
142
- }
143
-
144
- export { WebsiteSwitcher };
145
- //# sourceMappingURL=website-switcher.mjs.map
146
- //# sourceMappingURL=website-switcher.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/utils/cn.ts","../../src/ui/input.tsx","../../src/components/website-switcher.tsx"],"names":["jsx"],"mappings":"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","file":"website-switcher.mjs","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import * as React from \"react\";\n\nimport { cn } from \"../utils/cn\";\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n className,\n )}\n ref={ref}\n {...props}\n />\n );\n },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n","\"use client\";\n\n/**\n * WebsiteSwitcher Component\n *\n * A dropdown menu for navigating between Burdenoff ecosystem product websites.\n * Includes a search filter and renders each product with its logo and name.\n *\n * @example\n * ```tsx\n * import { WebsiteSwitcher } from '@burdenoff/website-sdk'\n *\n * <WebsiteSwitcher\n * products={[\n * { slug: 'vibecontrols', name: 'VibeControls', logoUrl: '/vibe.svg', websiteUrl: 'https://vibecontrols.com' },\n * { slug: 'fluidgrids', name: 'FluidGrids', logoUrl: '/fg.svg', websiteUrl: 'https://fluidgrids.com' },\n * ]}\n * />\n * ```\n */\n\nimport { useMemo, useState } from \"react\";\n\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { ChevronDown, Globe, Search } from \"lucide-react\";\n\nimport { Input } from \"../ui/input\";\nimport { cn } from \"../utils/cn\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WebsiteSwitcherProduct {\n /** Stable identifier for the product. */\n slug: string;\n /** Human-readable product name. */\n name: string;\n /** URL to the product logo (absolute or relative). */\n logoUrl?: string | null;\n /** Product marketing site URL. */\n websiteUrl: string;\n}\n\nexport interface WebsiteSwitcherProps {\n /** All Burdenoff ecosystem products to display. */\n products: WebsiteSwitcherProduct[];\n /** Accessible label for the trigger. */\n triggerLabel?: string;\n /** Placeholder text for the search input. */\n searchPlaceholder?: string;\n /** Empty-state text when filtering returns no results. */\n emptyText?: string;\n /** Optional additional CSS class for the trigger. */\n className?: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction normalizeSearch(value: string): string {\n return value.toLowerCase().trim();\n}\n\nfunction filterProducts(\n products: WebsiteSwitcherProduct[],\n query: string,\n): WebsiteSwitcherProduct[] {\n const normalized = normalizeSearch(query);\n if (!normalized) return products;\n return products.filter((p) => normalizeSearch(p.name).includes(normalized));\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function WebsiteSwitcher({\n products,\n triggerLabel = \"Explore products\",\n searchPlaceholder = \"Find a product…\",\n emptyText = \"No products match your search.\",\n className,\n}: WebsiteSwitcherProps) {\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n\n const filtered = useMemo(\n () => filterProducts(products, query),\n [products, query],\n );\n\n return (\n <DropdownMenu.Root open={open} onOpenChange={setOpen}>\n <DropdownMenu.Trigger\n className={cn(\n \"inline-flex items-center gap-1.5 text-sm font-medium transition-colors\",\n \"text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-md\",\n className,\n )}\n aria-label={triggerLabel}\n >\n <Globe className=\"h-4 w-4\" aria-hidden=\"true\" />\n <span className=\"hidden sm:inline\">{triggerLabel}</span>\n <ChevronDown\n className={cn(\n \"h-3.5 w-3.5 transition-transform duration-200\",\n open && \"rotate-180\",\n )}\n aria-hidden=\"true\"\n />\n </DropdownMenu.Trigger>\n\n <DropdownMenu.Portal>\n <DropdownMenu.Content\n className=\"z-50 min-w-[16rem] max-w-[20rem] rounded-lg border border-border/60 bg-popover p-2 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2\"\n sideOffset={8}\n align=\"end\"\n avoidCollisions\n >\n <div className=\"px-2 pb-2 pt-1\">\n <p className=\"text-xs font-medium text-muted-foreground mb-2\">\n Burdenoff ecosystem\n </p>\n <div className=\"relative\">\n <Search\n className=\"absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground\"\n aria-hidden=\"true\"\n />\n <Input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={searchPlaceholder}\n className=\"h-8 pl-8 text-sm\"\n aria-label={searchPlaceholder}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") {\n setQuery(\"\");\n }\n }}\n />\n </div>\n </div>\n\n <DropdownMenu.Separator className=\"my-1 h-px bg-border/60\" />\n\n <div className=\"max-h-[min(60vh,24rem)] overflow-y-auto py-1\">\n {filtered.length === 0 ? (\n <div className=\"px-3 py-4 text-center text-sm text-muted-foreground\">\n {emptyText}\n </div>\n ) : (\n filtered.map((product) => (\n <DropdownMenu.Item key={product.slug} asChild>\n <a\n href={product.websiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={cn(\n \"flex items-center gap-3 rounded-md px-2 py-2 text-sm outline-none\",\n \"text-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n )}\n >\n {product.logoUrl ? (\n <img\n src={product.logoUrl}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"h-5 w-5 shrink-0 rounded object-contain\"\n />\n ) : (\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-primary/10 text-[10px] font-bold text-primary\">\n {product.name.charAt(0)}\n </span>\n )}\n <span className=\"truncate\">{product.name}</span>\n </a>\n </DropdownMenu.Item>\n ))\n )}\n </div>\n\n <DropdownMenu.Arrow className=\"fill-popover\" />\n </DropdownMenu.Content>\n </DropdownMenu.Portal>\n </DropdownMenu.Root>\n );\n}\n"]}