@medialane/ui 0.65.1 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,7 @@ module.exports = __toCommonJS(launchpad_filter_bar_exports);
25
25
  var import_jsx_runtime = require("react/jsx-runtime");
26
26
  var import_lucide_react = require("lucide-react");
27
27
  var import_cn = require("../utils/cn.js");
28
+ var import_launchpad_services = require("../data/launchpad-services.js");
28
29
  function LaunchpadFilterBar({
29
30
  query,
30
31
  onQueryChange,
@@ -33,6 +34,9 @@ function LaunchpadFilterBar({
33
34
  onToggleGroup,
34
35
  resultCount
35
36
  }) {
37
+ const countFor = (key) => import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter(
38
+ (d) => d.group === key && d.status === "live" && (query.trim() === "" || `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase()))
39
+ ).length;
36
40
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col sm:flex-row gap-3 sm:items-center", children: [
37
41
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex-1 sm:max-w-xs", children: [
38
42
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" }),
@@ -59,17 +63,21 @@ function LaunchpadFilterBar({
59
63
  ] }),
60
64
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-2 flex-1", children: groups.map((group) => {
61
65
  const active = activeGroups.has(group.key);
62
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
66
+ const count = countFor(group.key);
67
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
63
68
  "button",
64
69
  {
65
70
  type: "button",
66
71
  onClick: () => onToggleGroup(group.key),
67
72
  "aria-pressed": active,
68
73
  className: (0, import_cn.cn)(
69
- "h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border",
74
+ "h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border inline-flex items-center gap-1.5",
70
75
  active ? "bg-gradient-to-r from-brand-purple to-brand-blue text-white border-transparent shadow-sm" : "bg-card text-muted-foreground border-border active:bg-muted/60 sm:hover:bg-muted/40"
71
76
  ),
72
- children: group.title
77
+ children: [
78
+ group.title,
79
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("tabular-nums", active ? "text-white/70" : "text-muted-foreground/60"), children: count })
80
+ ]
73
81
  },
74
82
  group.key
75
83
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/launchpad-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport type { ServiceGroup, ServiceGroupDefinition } from \"../data/launchpad-services.js\";\n\nexport interface LaunchpadFilterBarProps {\n query: string;\n onQueryChange: (value: string) => void;\n groups: ServiceGroupDefinition[];\n activeGroups: Set<ServiceGroup>;\n onToggleGroup: (key: ServiceGroup) => void;\n resultCount: number;\n}\n\n/**\n * Search + group-filter bar sitting above the grouped launchpad sections.\n * Fully controlled — all state (query, active groups) lives in the caller\n * (`LaunchpadGroupedSections`) so the grid below can react to the same\n * state without prop-drilling through this component.\n */\nexport function LaunchpadFilterBar({\n query,\n onQueryChange,\n groups,\n activeGroups,\n onToggleGroup,\n resultCount,\n}: LaunchpadFilterBarProps) {\n return (\n <div className=\"flex flex-col sm:flex-row gap-3 sm:items-center\">\n <div className=\"relative flex-1 sm:max-w-xs\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none\" />\n <input\n type=\"text\"\n value={query}\n onChange={(e) => onQueryChange(e.target.value)}\n placeholder=\"Search services\"\n className=\"w-full h-10 rounded-full border border-border bg-card pl-9 pr-9 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-brand-purple/40 focus:border-brand-purple/40\"\n />\n {query && (\n <button\n type=\"button\"\n onClick={() => onQueryChange(\"\")}\n aria-label=\"Clear search\"\n className=\"absolute right-2.5 top-1/2 -translate-y-1/2 h-5 w-5 flex items-center justify-center rounded-full text-muted-foreground hover:text-foreground transition-colors\"\n >\n <X className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap gap-2 flex-1\">\n {groups.map((group) => {\n const active = activeGroups.has(group.key);\n return (\n <button\n key={group.key}\n type=\"button\"\n onClick={() => onToggleGroup(group.key)}\n aria-pressed={active}\n className={cn(\n \"h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border\",\n active\n ? \"bg-gradient-to-r from-brand-purple to-brand-blue text-white border-transparent shadow-sm\"\n : \"bg-card text-muted-foreground border-border active:bg-muted/60 sm:hover:bg-muted/40\",\n )}\n >\n {group.title}\n </button>\n );\n })}\n </div>\n\n <p className=\"text-xs text-muted-foreground whitespace-nowrap sm:pl-2\">\n {resultCount} {resultCount === 1 ? \"service\" : \"services\"}\n </p>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA+BM;AA7BN,0BAA0B;AAC1B,gBAAmB;AAkBZ,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,SACE,6CAAC,SAAI,WAAU,mDACb;AAAA,iDAAC,SAAI,WAAU,+BACb;AAAA,kDAAC,8BAAO,WAAU,8FAA6F;AAAA,MAC/G;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAK;AAAA,UAC7C,aAAY;AAAA,UACZ,WAAU;AAAA;AAAA,MACZ;AAAA,MACC,SACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,MAAM,cAAc,EAAE;AAAA,UAC/B,cAAW;AAAA,UACX,WAAU;AAAA,UAEV,sDAAC,yBAAE,WAAU,eAAc;AAAA;AAAA,MAC7B;AAAA,OAEJ;AAAA,IAEA,4CAAC,SAAI,WAAU,+BACZ,iBAAO,IAAI,CAAC,UAAU;AACrB,YAAM,SAAS,aAAa,IAAI,MAAM,GAAG;AACzC,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,SAAS,MAAM,cAAc,MAAM,GAAG;AAAA,UACtC,gBAAc;AAAA,UACd,eAAW;AAAA,YACT;AAAA,YACA,SACI,6FACA;AAAA,UACN;AAAA,UAEC,gBAAM;AAAA;AAAA,QAXF,MAAM;AAAA,MAYb;AAAA,IAEJ,CAAC,GACH;AAAA,IAEA,6CAAC,OAAE,WAAU,2DACV;AAAA;AAAA,MAAY;AAAA,MAAE,gBAAgB,IAAI,YAAY;AAAA,OACjD;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/launchpad-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport {\n LAUNCHPAD_SERVICE_DEFINITIONS,\n type ServiceGroup,\n type ServiceGroupDefinition,\n} from \"../data/launchpad-services.js\";\n\nexport interface LaunchpadFilterBarProps {\n query: string;\n onQueryChange: (value: string) => void;\n groups: ServiceGroupDefinition[];\n activeGroups: Set<ServiceGroup>;\n onToggleGroup: (key: ServiceGroup) => void;\n resultCount: number;\n}\n\n/**\n * Search + group-filter bar above the launchpad grid. Fully controlled — all\n * state (query, active groups) lives in the caller so the grid below reacts\n * to the same state. Each pill shows the live count of services it matches\n * under the current search query.\n */\nexport function LaunchpadFilterBar({\n query,\n onQueryChange,\n groups,\n activeGroups,\n onToggleGroup,\n resultCount,\n}: LaunchpadFilterBarProps) {\n const countFor = (key: ServiceGroup) =>\n LAUNCHPAD_SERVICE_DEFINITIONS.filter(\n (d) =>\n d.group === key &&\n d.status === \"live\" &&\n (query.trim() === \"\" ||\n `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase())),\n ).length;\n\n return (\n <div className=\"flex flex-col sm:flex-row gap-3 sm:items-center\">\n <div className=\"relative flex-1 sm:max-w-xs\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none\" />\n <input\n type=\"text\"\n value={query}\n onChange={(e) => onQueryChange(e.target.value)}\n placeholder=\"Search services\"\n className=\"w-full h-10 rounded-full border border-border bg-card pl-9 pr-9 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-brand-purple/40 focus:border-brand-purple/40\"\n />\n {query && (\n <button\n type=\"button\"\n onClick={() => onQueryChange(\"\")}\n aria-label=\"Clear search\"\n className=\"absolute right-2.5 top-1/2 -translate-y-1/2 h-5 w-5 flex items-center justify-center rounded-full text-muted-foreground hover:text-foreground transition-colors\"\n >\n <X className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap gap-2 flex-1\">\n {groups.map((group) => {\n const active = activeGroups.has(group.key);\n const count = countFor(group.key);\n return (\n <button\n key={group.key}\n type=\"button\"\n onClick={() => onToggleGroup(group.key)}\n aria-pressed={active}\n className={cn(\n \"h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border inline-flex items-center gap-1.5\",\n active\n ? \"bg-gradient-to-r from-brand-purple to-brand-blue text-white border-transparent shadow-sm\"\n : \"bg-card text-muted-foreground border-border active:bg-muted/60 sm:hover:bg-muted/40\",\n )}\n >\n {group.title}\n <span className={cn(\"tabular-nums\", active ? \"text-white/70\" : \"text-muted-foreground/60\")}>\n {count}\n </span>\n </button>\n );\n })}\n </div>\n\n <p className=\"text-xs text-muted-foreground whitespace-nowrap sm:pl-2\">\n {resultCount} {resultCount === 1 ? \"service\" : \"services\"}\n </p>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA4CM;AA1CN,0BAA0B;AAC1B,gBAAmB;AACnB,gCAIO;AAiBA,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,WAAW,CAAC,QAChB,wDAA8B;AAAA,IAC5B,CAAC,MACC,EAAE,UAAU,OACZ,EAAE,WAAW,WACZ,MAAM,KAAK,MAAM,MAChB,GAAG,EAAE,KAAK,IAAI,EAAE,KAAK,IAAI,EAAE,QAAQ,GAAG,YAAY,EAAE,SAAS,MAAM,KAAK,EAAE,YAAY,CAAC;AAAA,EAC7F,EAAE;AAEJ,SACE,6CAAC,SAAI,WAAU,mDACb;AAAA,iDAAC,SAAI,WAAU,+BACb;AAAA,kDAAC,8BAAO,WAAU,8FAA6F;AAAA,MAC/G;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAK;AAAA,UAC7C,aAAY;AAAA,UACZ,WAAU;AAAA;AAAA,MACZ;AAAA,MACC,SACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,MAAM,cAAc,EAAE;AAAA,UAC/B,cAAW;AAAA,UACX,WAAU;AAAA,UAEV,sDAAC,yBAAE,WAAU,eAAc;AAAA;AAAA,MAC7B;AAAA,OAEJ;AAAA,IAEA,4CAAC,SAAI,WAAU,+BACZ,iBAAO,IAAI,CAAC,UAAU;AACrB,YAAM,SAAS,aAAa,IAAI,MAAM,GAAG;AACzC,YAAM,QAAQ,SAAS,MAAM,GAAG;AAChC,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,SAAS,MAAM,cAAc,MAAM,GAAG;AAAA,UACtC,gBAAc;AAAA,UACd,eAAW;AAAA,YACT;AAAA,YACA,SACI,6FACA;AAAA,UACN;AAAA,UAEC;AAAA,kBAAM;AAAA,YACP,4CAAC,UAAK,eAAW,cAAG,gBAAgB,SAAS,kBAAkB,0BAA0B,GACtF,iBACH;AAAA;AAAA;AAAA,QAdK,MAAM;AAAA,MAeb;AAAA,IAEJ,CAAC,GACH;AAAA,IAEA,6CAAC,OAAE,WAAU,2DACV;AAAA;AAAA,MAAY;AAAA,MAAE,gBAAgB,IAAI,YAAY;AAAA,OACjD;AAAA,KACF;AAEJ;","names":[]}
@@ -11,10 +11,10 @@ interface LaunchpadFilterBarProps {
11
11
  resultCount: number;
12
12
  }
13
13
  /**
14
- * Search + group-filter bar sitting above the grouped launchpad sections.
15
- * Fully controlled — all state (query, active groups) lives in the caller
16
- * (`LaunchpadGroupedSections`) so the grid below can react to the same
17
- * state without prop-drilling through this component.
14
+ * Search + group-filter bar above the launchpad grid. Fully controlled — all
15
+ * state (query, active groups) lives in the caller so the grid below reacts
16
+ * to the same state. Each pill shows the live count of services it matches
17
+ * under the current search query.
18
18
  */
19
19
  declare function LaunchpadFilterBar({ query, onQueryChange, groups, activeGroups, onToggleGroup, resultCount, }: LaunchpadFilterBarProps): react_jsx_runtime.JSX.Element;
20
20
 
@@ -11,10 +11,10 @@ interface LaunchpadFilterBarProps {
11
11
  resultCount: number;
12
12
  }
13
13
  /**
14
- * Search + group-filter bar sitting above the grouped launchpad sections.
15
- * Fully controlled — all state (query, active groups) lives in the caller
16
- * (`LaunchpadGroupedSections`) so the grid below can react to the same
17
- * state without prop-drilling through this component.
14
+ * Search + group-filter bar above the launchpad grid. Fully controlled — all
15
+ * state (query, active groups) lives in the caller so the grid below reacts
16
+ * to the same state. Each pill shows the live count of services it matches
17
+ * under the current search query.
18
18
  */
19
19
  declare function LaunchpadFilterBar({ query, onQueryChange, groups, activeGroups, onToggleGroup, resultCount, }: LaunchpadFilterBarProps): react_jsx_runtime.JSX.Element;
20
20
 
@@ -2,6 +2,9 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Search, X } from "lucide-react";
4
4
  import { cn } from "../utils/cn.js";
5
+ import {
6
+ LAUNCHPAD_SERVICE_DEFINITIONS
7
+ } from "../data/launchpad-services.js";
5
8
  function LaunchpadFilterBar({
6
9
  query,
7
10
  onQueryChange,
@@ -10,6 +13,9 @@ function LaunchpadFilterBar({
10
13
  onToggleGroup,
11
14
  resultCount
12
15
  }) {
16
+ const countFor = (key) => LAUNCHPAD_SERVICE_DEFINITIONS.filter(
17
+ (d) => d.group === key && d.status === "live" && (query.trim() === "" || `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase()))
18
+ ).length;
13
19
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row gap-3 sm:items-center", children: [
14
20
  /* @__PURE__ */ jsxs("div", { className: "relative flex-1 sm:max-w-xs", children: [
15
21
  /* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" }),
@@ -36,17 +42,21 @@ function LaunchpadFilterBar({
36
42
  ] }),
37
43
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 flex-1", children: groups.map((group) => {
38
44
  const active = activeGroups.has(group.key);
39
- return /* @__PURE__ */ jsx(
45
+ const count = countFor(group.key);
46
+ return /* @__PURE__ */ jsxs(
40
47
  "button",
41
48
  {
42
49
  type: "button",
43
50
  onClick: () => onToggleGroup(group.key),
44
51
  "aria-pressed": active,
45
52
  className: cn(
46
- "h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border",
53
+ "h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border inline-flex items-center gap-1.5",
47
54
  active ? "bg-gradient-to-r from-brand-purple to-brand-blue text-white border-transparent shadow-sm" : "bg-card text-muted-foreground border-border active:bg-muted/60 sm:hover:bg-muted/40"
48
55
  ),
49
- children: group.title
56
+ children: [
57
+ group.title,
58
+ /* @__PURE__ */ jsx("span", { className: cn("tabular-nums", active ? "text-white/70" : "text-muted-foreground/60"), children: count })
59
+ ]
50
60
  },
51
61
  group.key
52
62
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/launchpad-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport type { ServiceGroup, ServiceGroupDefinition } from \"../data/launchpad-services.js\";\n\nexport interface LaunchpadFilterBarProps {\n query: string;\n onQueryChange: (value: string) => void;\n groups: ServiceGroupDefinition[];\n activeGroups: Set<ServiceGroup>;\n onToggleGroup: (key: ServiceGroup) => void;\n resultCount: number;\n}\n\n/**\n * Search + group-filter bar sitting above the grouped launchpad sections.\n * Fully controlled — all state (query, active groups) lives in the caller\n * (`LaunchpadGroupedSections`) so the grid below can react to the same\n * state without prop-drilling through this component.\n */\nexport function LaunchpadFilterBar({\n query,\n onQueryChange,\n groups,\n activeGroups,\n onToggleGroup,\n resultCount,\n}: LaunchpadFilterBarProps) {\n return (\n <div className=\"flex flex-col sm:flex-row gap-3 sm:items-center\">\n <div className=\"relative flex-1 sm:max-w-xs\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none\" />\n <input\n type=\"text\"\n value={query}\n onChange={(e) => onQueryChange(e.target.value)}\n placeholder=\"Search services\"\n className=\"w-full h-10 rounded-full border border-border bg-card pl-9 pr-9 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-brand-purple/40 focus:border-brand-purple/40\"\n />\n {query && (\n <button\n type=\"button\"\n onClick={() => onQueryChange(\"\")}\n aria-label=\"Clear search\"\n className=\"absolute right-2.5 top-1/2 -translate-y-1/2 h-5 w-5 flex items-center justify-center rounded-full text-muted-foreground hover:text-foreground transition-colors\"\n >\n <X className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap gap-2 flex-1\">\n {groups.map((group) => {\n const active = activeGroups.has(group.key);\n return (\n <button\n key={group.key}\n type=\"button\"\n onClick={() => onToggleGroup(group.key)}\n aria-pressed={active}\n className={cn(\n \"h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border\",\n active\n ? \"bg-gradient-to-r from-brand-purple to-brand-blue text-white border-transparent shadow-sm\"\n : \"bg-card text-muted-foreground border-border active:bg-muted/60 sm:hover:bg-muted/40\",\n )}\n >\n {group.title}\n </button>\n );\n })}\n </div>\n\n <p className=\"text-xs text-muted-foreground whitespace-nowrap sm:pl-2\">\n {resultCount} {resultCount === 1 ? \"service\" : \"services\"}\n </p>\n </div>\n );\n}\n"],"mappings":";AA+BM,SACE,KADF;AA7BN,SAAS,QAAQ,SAAS;AAC1B,SAAS,UAAU;AAkBZ,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,SACE,qBAAC,SAAI,WAAU,mDACb;AAAA,yBAAC,SAAI,WAAU,+BACb;AAAA,0BAAC,UAAO,WAAU,8FAA6F;AAAA,MAC/G;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAK;AAAA,UAC7C,aAAY;AAAA,UACZ,WAAU;AAAA;AAAA,MACZ;AAAA,MACC,SACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,MAAM,cAAc,EAAE;AAAA,UAC/B,cAAW;AAAA,UACX,WAAU;AAAA,UAEV,8BAAC,KAAE,WAAU,eAAc;AAAA;AAAA,MAC7B;AAAA,OAEJ;AAAA,IAEA,oBAAC,SAAI,WAAU,+BACZ,iBAAO,IAAI,CAAC,UAAU;AACrB,YAAM,SAAS,aAAa,IAAI,MAAM,GAAG;AACzC,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,SAAS,MAAM,cAAc,MAAM,GAAG;AAAA,UACtC,gBAAc;AAAA,UACd,WAAW;AAAA,YACT;AAAA,YACA,SACI,6FACA;AAAA,UACN;AAAA,UAEC,gBAAM;AAAA;AAAA,QAXF,MAAM;AAAA,MAYb;AAAA,IAEJ,CAAC,GACH;AAAA,IAEA,qBAAC,OAAE,WAAU,2DACV;AAAA;AAAA,MAAY;AAAA,MAAE,gBAAgB,IAAI,YAAY;AAAA,OACjD;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/launchpad-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport {\n LAUNCHPAD_SERVICE_DEFINITIONS,\n type ServiceGroup,\n type ServiceGroupDefinition,\n} from \"../data/launchpad-services.js\";\n\nexport interface LaunchpadFilterBarProps {\n query: string;\n onQueryChange: (value: string) => void;\n groups: ServiceGroupDefinition[];\n activeGroups: Set<ServiceGroup>;\n onToggleGroup: (key: ServiceGroup) => void;\n resultCount: number;\n}\n\n/**\n * Search + group-filter bar above the launchpad grid. Fully controlled — all\n * state (query, active groups) lives in the caller so the grid below reacts\n * to the same state. Each pill shows the live count of services it matches\n * under the current search query.\n */\nexport function LaunchpadFilterBar({\n query,\n onQueryChange,\n groups,\n activeGroups,\n onToggleGroup,\n resultCount,\n}: LaunchpadFilterBarProps) {\n const countFor = (key: ServiceGroup) =>\n LAUNCHPAD_SERVICE_DEFINITIONS.filter(\n (d) =>\n d.group === key &&\n d.status === \"live\" &&\n (query.trim() === \"\" ||\n `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase())),\n ).length;\n\n return (\n <div className=\"flex flex-col sm:flex-row gap-3 sm:items-center\">\n <div className=\"relative flex-1 sm:max-w-xs\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none\" />\n <input\n type=\"text\"\n value={query}\n onChange={(e) => onQueryChange(e.target.value)}\n placeholder=\"Search services\"\n className=\"w-full h-10 rounded-full border border-border bg-card pl-9 pr-9 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-brand-purple/40 focus:border-brand-purple/40\"\n />\n {query && (\n <button\n type=\"button\"\n onClick={() => onQueryChange(\"\")}\n aria-label=\"Clear search\"\n className=\"absolute right-2.5 top-1/2 -translate-y-1/2 h-5 w-5 flex items-center justify-center rounded-full text-muted-foreground hover:text-foreground transition-colors\"\n >\n <X className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap gap-2 flex-1\">\n {groups.map((group) => {\n const active = activeGroups.has(group.key);\n const count = countFor(group.key);\n return (\n <button\n key={group.key}\n type=\"button\"\n onClick={() => onToggleGroup(group.key)}\n aria-pressed={active}\n className={cn(\n \"h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border inline-flex items-center gap-1.5\",\n active\n ? \"bg-gradient-to-r from-brand-purple to-brand-blue text-white border-transparent shadow-sm\"\n : \"bg-card text-muted-foreground border-border active:bg-muted/60 sm:hover:bg-muted/40\",\n )}\n >\n {group.title}\n <span className={cn(\"tabular-nums\", active ? \"text-white/70\" : \"text-muted-foreground/60\")}>\n {count}\n </span>\n </button>\n );\n })}\n </div>\n\n <p className=\"text-xs text-muted-foreground whitespace-nowrap sm:pl-2\">\n {resultCount} {resultCount === 1 ? \"service\" : \"services\"}\n </p>\n </div>\n );\n}\n"],"mappings":";AA4CM,SACE,KADF;AA1CN,SAAS,QAAQ,SAAS;AAC1B,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,OAGK;AAiBA,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,WAAW,CAAC,QAChB,8BAA8B;AAAA,IAC5B,CAAC,MACC,EAAE,UAAU,OACZ,EAAE,WAAW,WACZ,MAAM,KAAK,MAAM,MAChB,GAAG,EAAE,KAAK,IAAI,EAAE,KAAK,IAAI,EAAE,QAAQ,GAAG,YAAY,EAAE,SAAS,MAAM,KAAK,EAAE,YAAY,CAAC;AAAA,EAC7F,EAAE;AAEJ,SACE,qBAAC,SAAI,WAAU,mDACb;AAAA,yBAAC,SAAI,WAAU,+BACb;AAAA,0BAAC,UAAO,WAAU,8FAA6F;AAAA,MAC/G;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAK;AAAA,UAC7C,aAAY;AAAA,UACZ,WAAU;AAAA;AAAA,MACZ;AAAA,MACC,SACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,MAAM,cAAc,EAAE;AAAA,UAC/B,cAAW;AAAA,UACX,WAAU;AAAA,UAEV,8BAAC,KAAE,WAAU,eAAc;AAAA;AAAA,MAC7B;AAAA,OAEJ;AAAA,IAEA,oBAAC,SAAI,WAAU,+BACZ,iBAAO,IAAI,CAAC,UAAU;AACrB,YAAM,SAAS,aAAa,IAAI,MAAM,GAAG;AACzC,YAAM,QAAQ,SAAS,MAAM,GAAG;AAChC,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,SAAS,MAAM,cAAc,MAAM,GAAG;AAAA,UACtC,gBAAc;AAAA,UACd,WAAW;AAAA,YACT;AAAA,YACA,SACI,6FACA;AAAA,UACN;AAAA,UAEC;AAAA,kBAAM;AAAA,YACP,oBAAC,UAAK,WAAW,GAAG,gBAAgB,SAAS,kBAAkB,0BAA0B,GACtF,iBACH;AAAA;AAAA;AAAA,QAdK,MAAM;AAAA,MAeb;AAAA,IAEJ,CAAC,GACH;AAAA,IAEA,qBAAC,OAAE,WAAU,2DACV;AAAA;AAAA,MAAY;AAAA,MAAE,gBAAgB,IAAI,YAAY;AAAA,OACjD;AAAA,KACF;AAEJ;","names":[]}
@@ -29,6 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
30
  var launchpad_services_exports = {};
31
31
  __export(launchpad_services_exports, {
32
+ GROUP_ACCENTS: () => GROUP_ACCENTS,
32
33
  LaunchpadGroupedSections: () => LaunchpadGroupedSections,
33
34
  LaunchpadServiceCard: () => LaunchpadServiceCard,
34
35
  SERVICE_HUES: () => SERVICE_HUES,
@@ -42,135 +43,83 @@ var import_framer_motion = require("framer-motion");
42
43
  var import_lucide_react = require("lucide-react");
43
44
  var import_cn = require("../utils/cn.js");
44
45
  var import_launchpad_services = require("../data/launchpad-services.js");
45
- const DEFAULT_HUE = {
46
- text: "text-sky-600 dark:text-sky-400",
47
- solid: "bg-sky-600",
48
- border: "border-sky-500/30 dark:border-sky-400/25",
49
- tint: "bg-sky-500/10"
46
+ const DEFAULT_ACCENT = {
47
+ text: "text-muted-foreground",
48
+ tint: "bg-muted/60",
49
+ hoverBorder: "sm:hover:border-border"
50
+ };
51
+ const GROUP_ACCENTS = {
52
+ "nfts": { text: "text-brand-blue", tint: "bg-brand-blue/10", hoverBorder: "sm:hover:border-brand-blue/40" },
53
+ "limited-editions": { text: "text-brand-purple", tint: "bg-brand-purple/10", hoverBorder: "sm:hover:border-brand-purple/40" },
54
+ "community": { text: "text-emerald-600 dark:text-emerald-400", tint: "bg-emerald-500/10", hoverBorder: "sm:hover:border-emerald-500/40" },
55
+ "coins": { text: "text-brand-orange", tint: "bg-brand-orange/10", hoverBorder: "sm:hover:border-brand-orange/40" },
56
+ "claims": { text: "text-brand-rose", tint: "bg-brand-rose/10", hoverBorder: "sm:hover:border-brand-rose/40" },
57
+ "coming-soon": DEFAULT_ACCENT
50
58
  };
51
59
  const SERVICE_HUES = {
52
- // Single Editions blue + green
53
- "mint-ip-asset": { text: "text-blue-600 dark:text-blue-400", solid: "bg-blue-600", border: "border-blue-500/30 dark:border-blue-400/25", tint: "bg-blue-500/10" },
54
- "create-collection": { text: "text-green-600 dark:text-green-400", solid: "bg-green-600", border: "border-green-500/30 dark:border-green-400/25", tint: "bg-green-500/10" },
55
- // Limited Editions — purple + red
56
- "ip-collection-1155": { text: "text-purple-600 dark:text-purple-400", solid: "bg-purple-600", border: "border-purple-500/30 dark:border-purple-400/25", tint: "bg-purple-500/10" },
57
- "mint-editions": { text: "text-red-600 dark:text-red-400", solid: "bg-red-600", border: "border-red-500/30 dark:border-red-400/25", tint: "bg-red-500/10" },
58
- // Creator Coins & Memecoins — yellow + orange
60
+ "nfts": { text: "text-blue-600 dark:text-blue-400", solid: "bg-blue-600", border: "border-blue-500/30 dark:border-blue-400/25", tint: "bg-blue-500/10" },
61
+ "limited-editions": { text: "text-purple-600 dark:text-purple-400", solid: "bg-purple-600", border: "border-purple-500/30 dark:border-purple-400/25", tint: "bg-purple-500/10" },
59
62
  "creator-coins": { text: "text-yellow-600 dark:text-yellow-400", solid: "bg-yellow-600", border: "border-yellow-500/30 dark:border-yellow-400/25", tint: "bg-yellow-500/10" },
60
63
  "claim-memecoin": { text: "text-orange-600 dark:text-orange-400", solid: "bg-orange-600", border: "border-orange-500/30 dark:border-orange-400/25", tint: "bg-orange-500/10" },
61
64
  "collection-drop": { text: "text-orange-600 dark:text-orange-400", solid: "bg-orange-600", border: "border-orange-500/30 dark:border-orange-400/25", tint: "bg-orange-500/10" },
62
65
  "pop-protocol": { text: "text-emerald-600 dark:text-emerald-400", solid: "bg-emerald-600", border: "border-emerald-500/30 dark:border-emerald-400/25", tint: "bg-emerald-500/10" },
66
+ "ip-tickets": { text: "text-teal-600 dark:text-teal-400", solid: "bg-teal-600", border: "border-teal-500/30 dark:border-teal-400/25", tint: "bg-teal-500/10" },
63
67
  "remix-asset": { text: "text-indigo-600 dark:text-indigo-400", solid: "bg-indigo-600", border: "border-indigo-500/30 dark:border-indigo-400/25", tint: "bg-indigo-500/10" },
64
68
  "claim-username": { text: "text-violet-600 dark:text-violet-400", solid: "bg-violet-600", border: "border-violet-500/30 dark:border-violet-400/25", tint: "bg-violet-500/10" },
65
69
  "claim-collection": { text: "text-cyan-600 dark:text-cyan-400", solid: "bg-cyan-600", border: "border-cyan-500/30 dark:border-cyan-400/25", tint: "bg-cyan-500/10" },
66
70
  "claim-collection-name": { text: "text-pink-600 dark:text-pink-400", solid: "bg-pink-600", border: "border-pink-500/30 dark:border-pink-400/25", tint: "bg-pink-500/10" }
67
71
  };
68
- function LaunchpadServiceCard({ def, override = {}, featured = false, index = 0 }) {
69
- const { key, icon: Icon, title, browseLinkLabel, features } = def;
72
+ const GROUP_TITLE_BY_KEY = Object.fromEntries(
73
+ import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((g) => [g.key, g.title])
74
+ );
75
+ function LaunchpadServiceCard({ def, override = {}, index = 0 }) {
76
+ const { icon: Icon, title, group } = def;
70
77
  const status = override.status ?? def.status;
71
78
  const blurb = override.blurb ?? def.blurb;
72
- const { href, browseHref } = override;
79
+ const { href } = override;
73
80
  const reduceMotion = (0, import_framer_motion.useReducedMotion)();
74
81
  const live = status === "live";
75
- const hue = SERVICE_HUES[key] ?? DEFAULT_HUE;
82
+ const accent = GROUP_ACCENTS[group] ?? DEFAULT_ACCENT;
83
+ const body = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
84
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between gap-3", children: [
85
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("flex h-10 w-10 items-center justify-center rounded-xl", live ? accent.tint : "bg-muted/40"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: (0, import_cn.cn)("h-5 w-5", live ? accent.text : "text-muted-foreground/50") }) }),
86
+ live ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("text-[10px] font-semibold uppercase tracking-widest", accent.text), children: GROUP_TITLE_BY_KEY[group] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex items-center gap-1 text-[11px] font-medium text-muted-foreground/60", children: [
87
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Lock, { className: "h-3 w-3" }),
88
+ "Coming soon"
89
+ ] })
90
+ ] }),
91
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1", children: [
92
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { className: "flex items-center gap-1.5 text-lg font-bold tracking-tight leading-snug", children: [
93
+ title,
94
+ live && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, { className: "h-4 w-4 shrink-0 text-muted-foreground/40 transition-transform duration-200 sm:group-hover:translate-x-0.5 motion-reduce:transform-none" })
95
+ ] }),
96
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: (0, import_cn.cn)("text-sm leading-relaxed", live ? "text-muted-foreground" : "text-muted-foreground/60"), children: blurb })
97
+ ] })
98
+ ] });
76
99
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
77
100
  import_framer_motion.motion.div,
78
101
  {
79
102
  layout: !reduceMotion,
80
- initial: { opacity: 0, y: reduceMotion ? 0 : 16 },
81
- whileInView: { opacity: 1, y: 0 },
103
+ initial: { opacity: 0, y: reduceMotion ? 0 : 12 },
104
+ animate: { opacity: 1, y: 0 },
82
105
  exit: { opacity: 0 },
83
- viewport: { once: true, margin: "-40px" },
84
- transition: { duration: 0.45, delay: index * 0.06, ease: [0.25, 0.46, 0.45, 0.94] },
85
- className: (0, import_cn.cn)("flex", featured && "sm:col-span-2"),
86
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
87
- "div",
106
+ transition: { duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] },
107
+ className: "flex",
108
+ children: live && href ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
109
+ import_link.default,
88
110
  {
111
+ href,
89
112
  className: (0, import_cn.cn)(
90
- "group relative flex flex-1 flex-col overflow-hidden min-h-[200px] rounded-2xl border bg-card transition-all duration-300 ease-out",
91
- live ? (0, import_cn.cn)("active:scale-[0.985] sm:hover:-translate-y-1 motion-reduce:transform-none motion-reduce:transition-none", hue.border, "sm:hover:shadow-md sm:hover:shadow-black/5 dark:sm:hover:shadow-black/20") : "border-border/30 opacity-75"
113
+ "group flex flex-1 flex-col gap-3 rounded-2xl border border-border/60 bg-card p-5",
114
+ "transition-colors duration-200 active:scale-[0.99] motion-reduce:transform-none",
115
+ accent.hoverBorder
92
116
  ),
93
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex flex-col flex-1 p-5 sm:p-6 gap-3 sm:gap-4", children: [
94
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-start justify-between gap-3", children: [
95
- live ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("flex h-12 w-12 items-center justify-center rounded-xl", hue.tint), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: (0, import_cn.cn)("h-6 w-6", hue.text) }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: "h-8 w-8 shrink-0 text-muted-foreground/50" }),
96
- !live && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex items-center gap-1 text-[11px] font-medium text-muted-foreground/60 pt-1", children: [
97
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Lock, { className: "h-3 w-3" }),
98
- "Coming soon"
99
- ] })
100
- ] }),
101
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1.5", children: [
102
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-xl sm:text-2xl font-black tracking-tight leading-snug", children: title }),
103
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: (0, import_cn.cn)("text-sm leading-relaxed", live ? "text-muted-foreground" : "text-muted-foreground/60", !featured && "max-w-[36ch]"), children: blurb })
104
- ] }),
105
- live && features.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-1.5", children: features.slice(0, 2).map((feature) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
106
- "span",
107
- {
108
- className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-muted/40 border border-border/30 text-xs font-medium text-muted-foreground",
109
- children: [
110
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Check, { className: (0, import_cn.cn)("h-3 w-3 shrink-0", hue.text) }),
111
- feature
112
- ]
113
- },
114
- feature
115
- )) }),
116
- live && href && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href, "aria-label": `${def.cta} \u2014 ${title}`, className: "absolute inset-0 z-10" }),
117
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-auto pt-1 flex items-end justify-between gap-3", children: [
118
- live && browseHref && browseLinkLabel ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
119
- import_link.default,
120
- {
121
- href: browseHref,
122
- className: "relative z-20 inline-flex items-center gap-1 text-xs font-medium text-muted-foreground active:text-foreground sm:hover:text-foreground transition-colors",
123
- children: [
124
- browseLinkLabel,
125
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, { className: "h-3 w-3" })
126
- ]
127
- }
128
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
129
- live && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
130
- "span",
131
- {
132
- className: (0, import_cn.cn)(
133
- "inline-flex items-center gap-2 h-10 px-5 rounded-full",
134
- "text-sm font-semibold text-white",
135
- hue.solid
136
- ),
137
- children: [
138
- def.cta,
139
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, { className: "h-4 w-4 transition-transform duration-300 sm:group-hover:translate-x-0.5 motion-reduce:transform-none" })
140
- ]
141
- }
142
- )
143
- ] })
144
- ] })
117
+ children: body
145
118
  }
146
- )
119
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-1 flex-col gap-3 rounded-2xl border border-border/30 bg-card p-5 opacity-75", children: body })
147
120
  }
148
121
  );
149
122
  }
150
- const GROUP_TITLE_ACCENTS = {
151
- "coins": "bg-gradient-to-r from-yellow-500 via-amber-500 to-orange-500 bg-clip-text text-transparent",
152
- "community": "bg-gradient-to-r from-emerald-500 to-green-600 bg-clip-text text-transparent"
153
- };
154
- function GroupHeader({ group }) {
155
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
156
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: (0, import_cn.cn)("text-3xl sm:text-4xl font-black tracking-tight", GROUP_TITLE_ACCENTS[group.key]), children: group.title }),
157
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-base sm:text-lg text-muted-foreground leading-relaxed max-w-2xl", children: group.tagline })
158
- ] });
159
- }
160
- const POP_STEPS = [
161
- { title: "Create your event badge", body: "Name it, add your artwork, and set who can claim \u2014 ready in minutes." },
162
- { title: "Share the claim link", body: "Your community claims for free \u2014 one badge per person, no faking." },
163
- { title: "It stays with them forever", body: "Badges can't be sold or transferred \u2014 lasting proof they were there." }
164
- ];
165
- function PopHowItWorks() {
166
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-col justify-center gap-5 sm:gap-6 px-2 py-4 sm:py-0 sm:pl-6", children: POP_STEPS.map((step, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex gap-4", children: [
167
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-sm font-bold", children: i + 1 }),
168
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-0.5", children: [
169
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-semibold", children: step.title }),
170
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-sm text-muted-foreground leading-relaxed", children: step.body })
171
- ] })
172
- ] }, step.title)) });
173
- }
174
123
  function ComingSoonStrip({ group, defs }) {
175
124
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "rounded-2xl border border-border/40 p-5", children: [
176
125
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-semibold text-sm", children: group.title }),
@@ -212,19 +161,15 @@ function useLaunchpadFilter() {
212
161
  return { query, setQuery, activeGroups, toggleGroup, filterableGroups, totalMatches, clear };
213
162
  }
214
163
  function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilters, className }) {
215
- const matches = (def) => {
216
- if (activeGroups.size > 0 && !activeGroups.has(def.group)) return false;
217
- if (def.status !== "live") return false;
218
- if (query.trim() === "") return true;
219
- const haystack = `${def.title} ${def.blurb} ${def.subtitle}`.toLowerCase();
220
- return haystack.includes(query.trim().toLowerCase());
221
- };
222
- const totalMatches = (0, import_react.useMemo)(
223
- () => import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter(matches).length,
224
- // eslint-disable-next-line react-hooks/exhaustive-deps
225
- [query, activeGroups]
164
+ const inActiveGroup = (d) => activeGroups.size === 0 || activeGroups.has(d.group);
165
+ const inSearch = (d) => query.trim() === "" || `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase());
166
+ const groupOrder = Object.fromEntries(import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((g, i) => [g.key, i]));
167
+ const liveDefs = import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter((d) => d.group !== "coming-soon" && d.status === "live" && inActiveGroup(d) && inSearch(d)).sort((a, b) => (groupOrder[a.group] ?? 99) - (groupOrder[b.group] ?? 99));
168
+ const comingSoonGroup = import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.find((g) => g.key === "coming-soon");
169
+ const comingSoonDefs = import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter(
170
+ (d) => d.group === "coming-soon" && inActiveGroup(d) && inSearch(d)
226
171
  );
227
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_cn.cn)("space-y-8 sm:space-y-10", className), children: totalMatches === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-center py-16 space-y-3", children: [
172
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_cn.cn)("space-y-8", className), children: liveDefs.length === 0 && comingSoonDefs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-center py-16 space-y-3", children: [
228
173
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-lg font-semibold", children: "No services match" }),
229
174
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-sm text-muted-foreground", children: "Try a different search or clear your filters." }),
230
175
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -236,42 +181,14 @@ function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilte
236
181
  children: "Clear filters"
237
182
  }
238
183
  )
239
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "space-y-20 sm:space-y-28", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.AnimatePresence, { children: import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((group) => {
240
- if (group.key === "coming-soon") {
241
- const inActiveGroup = (d) => activeGroups.size === 0 || activeGroups.has(d.group);
242
- const inSearch = (d) => query.trim() === "" || `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase());
243
- const comingSoonDefs = import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter(
244
- (d) => d.group === group.key && inActiveGroup(d) && inSearch(d)
245
- );
246
- if (comingSoonDefs.length === 0) return null;
247
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComingSoonStrip, { group, defs: comingSoonDefs }, group.key);
248
- }
249
- const defs = import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter((d) => d.group === group.key && matches(d));
250
- if (defs.length === 0) return null;
251
- const showPopHowItWorks = group.key === "community" && defs.some((d) => d.key === "pop-protocol");
252
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
253
- import_framer_motion.motion.div,
254
- {
255
- layout: true,
256
- initial: { opacity: 0, y: 8 },
257
- animate: { opacity: 1, y: 0 },
258
- exit: { opacity: 0 },
259
- transition: { duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] },
260
- className: "space-y-7 sm:space-y-10",
261
- children: [
262
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GroupHeader, { group }),
263
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5", children: [
264
- defs.map((def, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LaunchpadServiceCard, { def, override: overrides[def.key], index: i }, def.key)),
265
- showPopHowItWorks && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PopHowItWorks, {})
266
- ] })
267
- ]
268
- },
269
- group.key
270
- );
271
- }) }) }) });
184
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.motion.div, { layout: true, className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.AnimatePresence, { mode: "popLayout", children: liveDefs.map((def, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LaunchpadServiceCard, { def, override: overrides[def.key], index: i }, def.key)) }) }),
186
+ comingSoonGroup && comingSoonDefs.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComingSoonStrip, { group: comingSoonGroup, defs: comingSoonDefs })
187
+ ] }) });
272
188
  }
273
189
  // Annotate the CommonJS export names for ESM import in node:
274
190
  0 && (module.exports = {
191
+ GROUP_ACCENTS,
275
192
  LaunchpadGroupedSections,
276
193
  LaunchpadServiceCard,
277
194
  SERVICE_HUES,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/launchpad-services.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Launchpad grouped services — the single source for the /launchpad page UI\n * in medialane-io and medialane-starknet.\n *\n * Card philosophy (creator-first redesign 2026-06-10; flat card pass\n * 2026-07-05): the whole card is the action. One title, one creator-language\n * sentence (def.blurb), one unique hue per service. The hue lives in a soft\n * icon-tile tint and a static border — no ambient aurora glow, no blurred\n * light-leaks, no gradient wash behind the card. Those read as washed-out\n * noise on light backgrounds and don't match the rest of the design system\n * (flat surfaces, solid accents). A staggered entrance reveal and a quiet\n * border/lift microinteraction on hover are still here; hover is desktop\n * polish only (gated to sm+, degraded under reduced-motion).\n *\n * Filter state (search + group pills) lives in `useLaunchpadFilter()` so a\n * page can render `LaunchpadFilterBar` wherever it wants (e.g. right under\n * the h1) while `LaunchpadGroupedSections` — now fully controlled — renders\n * only the grid reacting to that same state.\n *\n * Apps own: hrefs + per-app rollout status flips. Everything else lives here.\n */\n\nimport { useMemo, useState } from \"react\";\nimport Link from \"next/link\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { Lock, ArrowRight, Check } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport {\n LAUNCHPAD_SERVICE_DEFINITIONS,\n LAUNCHPAD_SERVICE_GROUPS,\n type ServiceDefinition,\n type ServiceGroup,\n type ServiceGroupDefinition,\n type ServiceStatus,\n} from \"../data/launchpad-services.js\";\n\n// ── Per-app injection points ─────────────────────────────────────────────────\n\nexport interface ServiceOverride {\n /** Primary destination — the whole card links here (required for live services) */\n href?: string;\n /** Secondary browse link href (pairs with the def's browseLinkLabel) */\n browseHref?: string;\n /** Per-app rollout flips (e.g. coins live on one app first) */\n status?: ServiceStatus;\n /** Per-app one-liner override (rarely needed) */\n blurb?: string;\n}\n\nexport type ServiceOverrides = Record<string, ServiceOverride>;\n\n// ── One unique hue per service — never repeated inside a group ───────────────\n\ninterface ServiceHue {\n /** icon glyph tint (600 for light surfaces, 400 for dark) */\n text: string;\n /** solid fill — CTA button background */\n solid: string;\n /** static card border (visible on both themes, not just on hover) */\n border: string;\n /** soft icon-tile background (10% tint, both themes) */\n tint: string;\n}\n\nconst DEFAULT_HUE: ServiceHue = {\n text: \"text-sky-600 dark:text-sky-400\",\n solid: \"bg-sky-600\",\n border: \"border-sky-500/30 dark:border-sky-400/25\",\n tint: \"bg-sky-500/10\",\n};\n\nexport const SERVICE_HUES: Record<string, ServiceHue> = {\n // Single Editions — blue + green\n \"mint-ip-asset\": { text: \"text-blue-600 dark:text-blue-400\", solid: \"bg-blue-600\", border: \"border-blue-500/30 dark:border-blue-400/25\", tint: \"bg-blue-500/10\" },\n \"create-collection\": { text: \"text-green-600 dark:text-green-400\", solid: \"bg-green-600\", border: \"border-green-500/30 dark:border-green-400/25\", tint: \"bg-green-500/10\" },\n // Limited Editions — purple + red\n \"ip-collection-1155\": { text: \"text-purple-600 dark:text-purple-400\", solid: \"bg-purple-600\", border: \"border-purple-500/30 dark:border-purple-400/25\", tint: \"bg-purple-500/10\" },\n \"mint-editions\": { text: \"text-red-600 dark:text-red-400\", solid: \"bg-red-600\", border: \"border-red-500/30 dark:border-red-400/25\", tint: \"bg-red-500/10\" },\n // Creator Coins & Memecoins — yellow + orange\n \"creator-coins\": { text: \"text-yellow-600 dark:text-yellow-400\", solid: \"bg-yellow-600\", border: \"border-yellow-500/30 dark:border-yellow-400/25\", tint: \"bg-yellow-500/10\" },\n \"claim-memecoin\": { text: \"text-orange-600 dark:text-orange-400\", solid: \"bg-orange-600\", border: \"border-orange-500/30 dark:border-orange-400/25\", tint: \"bg-orange-500/10\" },\n \"collection-drop\": { text: \"text-orange-600 dark:text-orange-400\", solid: \"bg-orange-600\", border: \"border-orange-500/30 dark:border-orange-400/25\", tint: \"bg-orange-500/10\" },\n \"pop-protocol\": { text: \"text-emerald-600 dark:text-emerald-400\", solid: \"bg-emerald-600\", border: \"border-emerald-500/30 dark:border-emerald-400/25\", tint: \"bg-emerald-500/10\" },\n \"remix-asset\": { text: \"text-indigo-600 dark:text-indigo-400\", solid: \"bg-indigo-600\", border: \"border-indigo-500/30 dark:border-indigo-400/25\", tint: \"bg-indigo-500/10\" },\n \"claim-username\": { text: \"text-violet-600 dark:text-violet-400\", solid: \"bg-violet-600\", border: \"border-violet-500/30 dark:border-violet-400/25\", tint: \"bg-violet-500/10\" },\n \"claim-collection\": { text: \"text-cyan-600 dark:text-cyan-400\", solid: \"bg-cyan-600\", border: \"border-cyan-500/30 dark:border-cyan-400/25\", tint: \"bg-cyan-500/10\" },\n \"claim-collection-name\": { text: \"text-pink-600 dark:text-pink-400\", solid: \"bg-pink-600\", border: \"border-pink-500/30 dark:border-pink-400/25\", tint: \"bg-pink-500/10\" },\n};\n\n// ── Service card — the whole card is the action ─────────────────────────────\n\nexport interface LaunchpadServiceCardProps {\n def: ServiceDefinition;\n override?: ServiceOverride;\n /** Showcase layout — spans the full grid width (e.g. POP Protocol) */\n featured?: boolean;\n /** Grid position — drives the staggered entrance reveal. */\n index?: number;\n}\n\nexport function LaunchpadServiceCard({ def, override = {}, featured = false, index = 0 }: LaunchpadServiceCardProps) {\n const { key, icon: Icon, title, browseLinkLabel, features } = def;\n const status = override.status ?? def.status;\n const blurb = override.blurb ?? def.blurb;\n const { href, browseHref } = override;\n const reduceMotion = useReducedMotion();\n\n const live = status === \"live\";\n const hue = SERVICE_HUES[key] ?? DEFAULT_HUE;\n\n return (\n <motion.div\n layout={!reduceMotion}\n initial={{ opacity: 0, y: reduceMotion ? 0 : 16 }}\n whileInView={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0 }}\n viewport={{ once: true, margin: \"-40px\" }}\n transition={{ duration: 0.45, delay: index * 0.06, ease: [0.25, 0.46, 0.45, 0.94] }}\n className={cn(\"flex\", featured && \"sm:col-span-2\")}\n >\n <div\n className={cn(\n \"group relative flex flex-1 flex-col overflow-hidden min-h-[200px] rounded-2xl border bg-card transition-all duration-300 ease-out\",\n live\n ? cn(\"active:scale-[0.985] sm:hover:-translate-y-1 motion-reduce:transform-none motion-reduce:transition-none\", hue.border, \"sm:hover:shadow-md sm:hover:shadow-black/5 dark:sm:hover:shadow-black/20\")\n : \"border-border/30 opacity-75\",\n )}\n >\n <div className=\"relative flex flex-col flex-1 p-5 sm:p-6 gap-3 sm:gap-4\">\n <div className=\"flex items-start justify-between gap-3\">\n {live ? (\n <span className={cn(\"flex h-12 w-12 items-center justify-center rounded-xl\", hue.tint)}>\n <Icon className={cn(\"h-6 w-6\", hue.text)} />\n </span>\n ) : (\n <Icon className=\"h-8 w-8 shrink-0 text-muted-foreground/50\" />\n )}\n {!live && (\n <span className=\"flex items-center gap-1 text-[11px] font-medium text-muted-foreground/60 pt-1\">\n <Lock className=\"h-3 w-3\" />\n Coming soon\n </span>\n )}\n </div>\n\n <div className=\"space-y-1.5\">\n <h3 className=\"text-xl sm:text-2xl font-black tracking-tight leading-snug\">{title}</h3>\n <p className={cn(\"text-sm leading-relaxed\", live ? \"text-muted-foreground\" : \"text-muted-foreground/60\", !featured && \"max-w-[36ch]\")}>\n {blurb}\n </p>\n </div>\n\n {/* Feature showcase — plain-language chips, capped at 2 for card density */}\n {live && features.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\">\n {features.slice(0, 2).map((feature) => (\n <span\n key={feature}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-muted/40 border border-border/30 text-xs font-medium text-muted-foreground\"\n >\n <Check className={cn(\"h-3 w-3 shrink-0\", hue.text)} />\n {feature}\n </span>\n ))}\n </div>\n )}\n\n {/* Stretched link — the whole card is the action; the button is the visual cue */}\n {live && href && <Link href={href} aria-label={`${def.cta} — ${title}`} className=\"absolute inset-0 z-10\" />}\n\n <div className=\"mt-auto pt-1 flex items-end justify-between gap-3\">\n {live && browseHref && browseLinkLabel ? (\n <Link\n href={browseHref}\n className=\"relative z-20 inline-flex items-center gap-1 text-xs font-medium text-muted-foreground active:text-foreground sm:hover:text-foreground transition-colors\"\n >\n {browseLinkLabel}\n <ArrowRight className=\"h-3 w-3\" />\n </Link>\n ) : (\n <span />\n )}\n {live && (\n <span\n className={cn(\n \"inline-flex items-center gap-2 h-10 px-5 rounded-full\",\n \"text-sm font-semibold text-white\",\n hue.solid,\n )}\n >\n {def.cta}\n <ArrowRight className=\"h-4 w-4 transition-transform duration-300 sm:group-hover:translate-x-0.5 motion-reduce:transform-none\" />\n </span>\n )}\n </div>\n </div>\n </div>\n </motion.div>\n );\n}\n\n// ── Group sections ───────────────────────────────────────────────────────────\n\n/** Vivid gradient titles give special sections identity without boxing them in\n * (no panels inside panels — color carries the distinction, space does the rest). */\nconst GROUP_TITLE_ACCENTS: Partial<Record<ServiceGroup, string>> = {\n \"coins\": \"bg-gradient-to-r from-yellow-500 via-amber-500 to-orange-500 bg-clip-text text-transparent\",\n \"community\": \"bg-gradient-to-r from-emerald-500 to-green-600 bg-clip-text text-transparent\",\n};\n\nfunction GroupHeader({ group }: { group: ServiceGroupDefinition }) {\n return (\n <div className=\"space-y-2\">\n <h2 className={cn(\"text-3xl sm:text-4xl font-black tracking-tight\", GROUP_TITLE_ACCENTS[group.key])}>\n {group.title}\n </h2>\n <p className=\"text-base sm:text-lg text-muted-foreground leading-relaxed max-w-2xl\">{group.tagline}</p>\n </div>\n );\n}\n\n/** POP Protocol companion column — open how-it-works steps, no extra panel. */\nconst POP_STEPS = [\n { title: \"Create your event badge\", body: \"Name it, add your artwork, and set who can claim — ready in minutes.\" },\n { title: \"Share the claim link\", body: \"Your community claims for free — one badge per person, no faking.\" },\n { title: \"It stays with them forever\", body: \"Badges can't be sold or transferred — lasting proof they were there.\" },\n];\n\nfunction PopHowItWorks() {\n return (\n <div className=\"flex flex-col justify-center gap-5 sm:gap-6 px-2 py-4 sm:py-0 sm:pl-6\">\n {POP_STEPS.map((step, i) => (\n <div key={step.title} className=\"flex gap-4\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-sm font-bold\">\n {i + 1}\n </span>\n <div className=\"space-y-0.5\">\n <p className=\"font-semibold\">{step.title}</p>\n <p className=\"text-sm text-muted-foreground leading-relaxed\">{step.body}</p>\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction ComingSoonStrip({ group, defs }: { group: ServiceGroupDefinition; defs: ServiceDefinition[] }) {\n return (\n <div className=\"rounded-2xl border border-border/40 p-5\">\n <p className=\"font-semibold text-sm\">{group.title}</p>\n <p className=\"text-sm text-muted-foreground mt-0.5\">{group.tagline}</p>\n <div className=\"flex flex-wrap gap-2 mt-4\">\n {defs.map(({ key, icon: Icon, title }) => (\n <div key={key} className=\"flex items-center gap-2 px-3 py-2 rounded-full bg-muted/30 border border-border/25\">\n <Icon className=\"h-3.5 w-3.5 text-muted-foreground/60\" />\n <span className=\"text-xs font-medium text-muted-foreground\">{title}</span>\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * Owns the search + group-filter state for the launchpad page. A page renders\n * `LaunchpadFilterBar` wherever it wants (e.g. right under the h1) wired to\n * this hook's fields, then passes `query`/`activeGroups` straight through to\n * `LaunchpadGroupedSections` so the grid reacts to the same state.\n */\nexport function useLaunchpadFilter() {\n const [query, setQuery] = useState(\"\");\n const [activeGroups, setActiveGroups] = useState<Set<ServiceGroup>>(new Set());\n\n const filterableGroups = LAUNCHPAD_SERVICE_GROUPS.filter((g) => g.key !== \"coming-soon\");\n\n const toggleGroup = (key: ServiceGroup) => {\n setActiveGroups((prev) => {\n const next = new Set(prev);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n return next;\n });\n };\n\n const matches = (def: ServiceDefinition): boolean => {\n if (activeGroups.size > 0 && !activeGroups.has(def.group)) return false;\n if (def.status !== \"live\") return false;\n if (query.trim() === \"\") return true;\n const haystack = `${def.title} ${def.blurb} ${def.subtitle}`.toLowerCase();\n return haystack.includes(query.trim().toLowerCase());\n };\n\n const totalMatches = useMemo(\n () => LAUNCHPAD_SERVICE_DEFINITIONS.filter(matches).length,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [query, activeGroups],\n );\n\n const clear = () => { setQuery(\"\"); setActiveGroups(new Set()); };\n\n return { query, setQuery, activeGroups, toggleGroup, filterableGroups, totalMatches, clear };\n}\n\nexport interface LaunchpadGroupedSectionsProps {\n /** Per-app hrefs / rollout flips, keyed by service key. */\n overrides: ServiceOverrides;\n /** Current search query — from `useLaunchpadFilter()`. */\n query: string;\n /** Current active group filters — from `useLaunchpadFilter()`. */\n activeGroups: Set<ServiceGroup>;\n /** Reset both — from `useLaunchpadFilter()`. */\n onClearFilters: () => void;\n className?: string;\n}\n\n/** The full grouped launchpad services grid — section order comes from\n * LAUNCHPAD_SERVICE_GROUPS; cards from LAUNCHPAD_SERVICE_DEFINITIONS.\n * Fully controlled by `useLaunchpadFilter()` state passed in from the page. */\nexport function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilters, className }: LaunchpadGroupedSectionsProps) {\n const matches = (def: ServiceDefinition): boolean => {\n if (activeGroups.size > 0 && !activeGroups.has(def.group)) return false;\n if (def.status !== \"live\") return false;\n if (query.trim() === \"\") return true;\n const haystack = `${def.title} ${def.blurb} ${def.subtitle}`.toLowerCase();\n return haystack.includes(query.trim().toLowerCase());\n };\n\n const totalMatches = useMemo(\n () => LAUNCHPAD_SERVICE_DEFINITIONS.filter(matches).length,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [query, activeGroups],\n );\n\n return (\n <div className={cn(\"space-y-8 sm:space-y-10\", className)}>\n {totalMatches === 0 ? (\n <div className=\"text-center py-16 space-y-3\">\n <p className=\"text-lg font-semibold\">No services match</p>\n <p className=\"text-sm text-muted-foreground\">Try a different search or clear your filters.</p>\n <button\n type=\"button\"\n onClick={onClearFilters}\n className=\"inline-flex items-center h-9 px-4 rounded-full text-sm font-semibold bg-primary text-primary-foreground\"\n >\n Clear filters\n </button>\n </div>\n ) : (\n <div className=\"space-y-20 sm:space-y-28\">\n <AnimatePresence>\n {LAUNCHPAD_SERVICE_GROUPS.map((group) => {\n if (group.key === \"coming-soon\") {\n // Coming-soon items are never \"live\" by definition, so they can't\n // go through matches() (which requires status === \"live\") — only\n // the group/search filters apply here, not the live-status gate.\n const inActiveGroup = (d: ServiceDefinition) => activeGroups.size === 0 || activeGroups.has(d.group);\n const inSearch = (d: ServiceDefinition) =>\n query.trim() === \"\" ||\n `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase());\n const comingSoonDefs = LAUNCHPAD_SERVICE_DEFINITIONS.filter(\n (d) => d.group === group.key && inActiveGroup(d) && inSearch(d),\n );\n if (comingSoonDefs.length === 0) return null;\n return <ComingSoonStrip key={group.key} group={group} defs={comingSoonDefs} />;\n }\n\n const defs = LAUNCHPAD_SERVICE_DEFINITIONS.filter((d) => d.group === group.key && matches(d));\n if (defs.length === 0) return null;\n\n const showPopHowItWorks = group.key === \"community\" && defs.some((d) => d.key === \"pop-protocol\");\n\n return (\n <motion.div\n key={group.key}\n layout\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] }}\n className=\"space-y-7 sm:space-y-10\"\n >\n <GroupHeader group={group} />\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5\">\n {defs.map((def, i) => (\n <LaunchpadServiceCard key={def.key} def={def} override={overrides[def.key]} index={i} />\n ))}\n {showPopHowItWorks && <PopHowItWorks />}\n </div>\n </motion.div>\n );\n })}\n </AnimatePresence>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Link"]}
1
+ {"version":3,"sources":["../../src/components/launchpad-services.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Launchpad services — the single source for the /launchpad page UI in\n * medialane-io and medialane-starknet.\n *\n * One dynamic grid (2026-07-13 redesign): one card per service, no group\n * sections. Groups exist as tags — a small accent label on the card and the\n * filter pills above the grid. The card is a single link to the service's own\n * page (its complete control surface); it carries no second link, no feature\n * chips, and no per-service color identity. Five group accents are the only\n * color voice, applied to the icon tile and the tag text.\n *\n * Filter state (search + group pills) lives in `useLaunchpadFilter()` so a\n * page can render `LaunchpadFilterBar` wherever it wants while\n * `LaunchpadGroupedSections` — fully controlled — renders the grid reacting\n * to that same state.\n *\n * Apps own: hrefs + per-app rollout status flips. Everything else lives here.\n */\n\nimport { useMemo, useState } from \"react\";\nimport Link from \"next/link\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { Lock, ArrowRight } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport {\n LAUNCHPAD_SERVICE_DEFINITIONS,\n LAUNCHPAD_SERVICE_GROUPS,\n type ServiceDefinition,\n type ServiceGroup,\n type ServiceGroupDefinition,\n type ServiceStatus,\n} from \"../data/launchpad-services.js\";\n\n// ── Per-app injection points ─────────────────────────────────────────────────\n\nexport interface ServiceOverride {\n /** Primary destination — the whole card links here (required for live services) */\n href?: string;\n /** Legacy secondary link — the redesigned card renders a single link, so this is unused. */\n browseHref?: string;\n /** Per-app rollout flips (e.g. coins live on one app first) */\n status?: ServiceStatus;\n /** Per-app one-liner override (rarely needed) */\n blurb?: string;\n}\n\nexport type ServiceOverrides = Record<string, ServiceOverride>;\n\n// ── Group accents — the grid's only color voice ─────────────────────────────\n\ninterface GroupAccent {\n /** icon glyph + tag text tint */\n text: string;\n /** soft icon-tile background */\n tint: string;\n /** card border tint on desktop hover (polish only) */\n hoverBorder: string;\n}\n\nconst DEFAULT_ACCENT: GroupAccent = {\n text: \"text-muted-foreground\",\n tint: \"bg-muted/60\",\n hoverBorder: \"sm:hover:border-border\",\n};\n\nexport const GROUP_ACCENTS: Record<ServiceGroup, GroupAccent> = {\n \"nfts\": { text: \"text-brand-blue\", tint: \"bg-brand-blue/10\", hoverBorder: \"sm:hover:border-brand-blue/40\" },\n \"limited-editions\": { text: \"text-brand-purple\", tint: \"bg-brand-purple/10\", hoverBorder: \"sm:hover:border-brand-purple/40\" },\n \"community\": { text: \"text-emerald-600 dark:text-emerald-400\", tint: \"bg-emerald-500/10\", hoverBorder: \"sm:hover:border-emerald-500/40\" },\n \"coins\": { text: \"text-brand-orange\", tint: \"bg-brand-orange/10\", hoverBorder: \"sm:hover:border-brand-orange/40\" },\n \"claims\": { text: \"text-brand-rose\", tint: \"bg-brand-rose/10\", hoverBorder: \"sm:hover:border-brand-rose/40\" },\n \"coming-soon\": DEFAULT_ACCENT,\n};\n\n/** Legacy per-service hue table — kept for LaunchpadStrip (homepage). The grid\n * no longer uses it. */\ninterface ServiceHue {\n text: string;\n solid: string;\n border: string;\n tint: string;\n}\n\nexport const SERVICE_HUES: Record<string, ServiceHue> = {\n \"nfts\": { text: \"text-blue-600 dark:text-blue-400\", solid: \"bg-blue-600\", border: \"border-blue-500/30 dark:border-blue-400/25\", tint: \"bg-blue-500/10\" },\n \"limited-editions\": { text: \"text-purple-600 dark:text-purple-400\", solid: \"bg-purple-600\", border: \"border-purple-500/30 dark:border-purple-400/25\", tint: \"bg-purple-500/10\" },\n \"creator-coins\": { text: \"text-yellow-600 dark:text-yellow-400\", solid: \"bg-yellow-600\", border: \"border-yellow-500/30 dark:border-yellow-400/25\", tint: \"bg-yellow-500/10\" },\n \"claim-memecoin\": { text: \"text-orange-600 dark:text-orange-400\", solid: \"bg-orange-600\", border: \"border-orange-500/30 dark:border-orange-400/25\", tint: \"bg-orange-500/10\" },\n \"collection-drop\": { text: \"text-orange-600 dark:text-orange-400\", solid: \"bg-orange-600\", border: \"border-orange-500/30 dark:border-orange-400/25\", tint: \"bg-orange-500/10\" },\n \"pop-protocol\": { text: \"text-emerald-600 dark:text-emerald-400\", solid: \"bg-emerald-600\", border: \"border-emerald-500/30 dark:border-emerald-400/25\", tint: \"bg-emerald-500/10\" },\n \"ip-tickets\": { text: \"text-teal-600 dark:text-teal-400\", solid: \"bg-teal-600\", border: \"border-teal-500/30 dark:border-teal-400/25\", tint: \"bg-teal-500/10\" },\n \"remix-asset\": { text: \"text-indigo-600 dark:text-indigo-400\", solid: \"bg-indigo-600\", border: \"border-indigo-500/30 dark:border-indigo-400/25\", tint: \"bg-indigo-500/10\" },\n \"claim-username\": { text: \"text-violet-600 dark:text-violet-400\", solid: \"bg-violet-600\", border: \"border-violet-500/30 dark:border-violet-400/25\", tint: \"bg-violet-500/10\" },\n \"claim-collection\": { text: \"text-cyan-600 dark:text-cyan-400\", solid: \"bg-cyan-600\", border: \"border-cyan-500/30 dark:border-cyan-400/25\", tint: \"bg-cyan-500/10\" },\n \"claim-collection-name\": { text: \"text-pink-600 dark:text-pink-400\", solid: \"bg-pink-600\", border: \"border-pink-500/30 dark:border-pink-400/25\", tint: \"bg-pink-500/10\" },\n};\n\nconst GROUP_TITLE_BY_KEY = Object.fromEntries(\n LAUNCHPAD_SERVICE_GROUPS.map((g) => [g.key, g.title]),\n) as Record<ServiceGroup, string>;\n\n// ── Service card — one link, one accent, title + one sentence ────────────────\n\nexport interface LaunchpadServiceCardProps {\n def: ServiceDefinition;\n override?: ServiceOverride;\n /** Grid position — drives the staggered entrance reveal. */\n index?: number;\n}\n\nexport function LaunchpadServiceCard({ def, override = {}, index = 0 }: LaunchpadServiceCardProps) {\n const { icon: Icon, title, group } = def;\n const status = override.status ?? def.status;\n const blurb = override.blurb ?? def.blurb;\n const { href } = override;\n const reduceMotion = useReducedMotion();\n\n const live = status === \"live\";\n const accent = GROUP_ACCENTS[group] ?? DEFAULT_ACCENT;\n\n const body = (\n <>\n <div className=\"flex items-center justify-between gap-3\">\n <span className={cn(\"flex h-10 w-10 items-center justify-center rounded-xl\", live ? accent.tint : \"bg-muted/40\")}>\n <Icon className={cn(\"h-5 w-5\", live ? accent.text : \"text-muted-foreground/50\")} />\n </span>\n {live ? (\n <span className={cn(\"text-[10px] font-semibold uppercase tracking-widest\", accent.text)}>\n {GROUP_TITLE_BY_KEY[group]}\n </span>\n ) : (\n <span className=\"flex items-center gap-1 text-[11px] font-medium text-muted-foreground/60\">\n <Lock className=\"h-3 w-3\" />\n Coming soon\n </span>\n )}\n </div>\n <div className=\"space-y-1\">\n <h3 className=\"flex items-center gap-1.5 text-lg font-bold tracking-tight leading-snug\">\n {title}\n {live && (\n <ArrowRight className=\"h-4 w-4 shrink-0 text-muted-foreground/40 transition-transform duration-200 sm:group-hover:translate-x-0.5 motion-reduce:transform-none\" />\n )}\n </h3>\n <p className={cn(\"text-sm leading-relaxed\", live ? \"text-muted-foreground\" : \"text-muted-foreground/60\")}>\n {blurb}\n </p>\n </div>\n </>\n );\n\n return (\n <motion.div\n layout={!reduceMotion}\n initial={{ opacity: 0, y: reduceMotion ? 0 : 12 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] }}\n className=\"flex\"\n >\n {live && href ? (\n <Link\n href={href}\n className={cn(\n \"group flex flex-1 flex-col gap-3 rounded-2xl border border-border/60 bg-card p-5\",\n \"transition-colors duration-200 active:scale-[0.99] motion-reduce:transform-none\",\n accent.hoverBorder,\n )}\n >\n {body}\n </Link>\n ) : (\n <div className=\"flex flex-1 flex-col gap-3 rounded-2xl border border-border/30 bg-card p-5 opacity-75\">\n {body}\n </div>\n )}\n </motion.div>\n );\n}\n\n// ── Coming-soon strip ────────────────────────────────────────────────────────\n\nfunction ComingSoonStrip({ group, defs }: { group: ServiceGroupDefinition; defs: ServiceDefinition[] }) {\n return (\n <div className=\"rounded-2xl border border-border/40 p-5\">\n <p className=\"font-semibold text-sm\">{group.title}</p>\n <p className=\"text-sm text-muted-foreground mt-0.5\">{group.tagline}</p>\n <div className=\"flex flex-wrap gap-2 mt-4\">\n {defs.map(({ key, icon: Icon, title }) => (\n <div key={key} className=\"flex items-center gap-2 px-3 py-2 rounded-full bg-muted/30 border border-border/25\">\n <Icon className=\"h-3.5 w-3.5 text-muted-foreground/60\" />\n <span className=\"text-xs font-medium text-muted-foreground\">{title}</span>\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n// ── Filter state ─────────────────────────────────────────────────────────────\n\n/**\n * Owns the search + group-filter state for the launchpad page. A page renders\n * `LaunchpadFilterBar` wherever it wants (e.g. right under the h1) wired to\n * this hook's fields, then passes `query`/`activeGroups` straight through to\n * `LaunchpadGroupedSections` so the grid reacts to the same state.\n */\nexport function useLaunchpadFilter() {\n const [query, setQuery] = useState(\"\");\n const [activeGroups, setActiveGroups] = useState<Set<ServiceGroup>>(new Set());\n\n const filterableGroups = LAUNCHPAD_SERVICE_GROUPS.filter((g) => g.key !== \"coming-soon\");\n\n const toggleGroup = (key: ServiceGroup) => {\n setActiveGroups((prev) => {\n const next = new Set(prev);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n return next;\n });\n };\n\n const matches = (def: ServiceDefinition): boolean => {\n if (activeGroups.size > 0 && !activeGroups.has(def.group)) return false;\n if (def.status !== \"live\") return false;\n if (query.trim() === \"\") return true;\n const haystack = `${def.title} ${def.blurb} ${def.subtitle}`.toLowerCase();\n return haystack.includes(query.trim().toLowerCase());\n };\n\n const totalMatches = useMemo(\n () => LAUNCHPAD_SERVICE_DEFINITIONS.filter(matches).length,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [query, activeGroups],\n );\n\n const clear = () => { setQuery(\"\"); setActiveGroups(new Set()); };\n\n return { query, setQuery, activeGroups, toggleGroup, filterableGroups, totalMatches, clear };\n}\n\n// ── The grid ─────────────────────────────────────────────────────────────────\n\nexport interface LaunchpadGroupedSectionsProps {\n /** Per-app hrefs / rollout flips, keyed by service key. */\n overrides: ServiceOverrides;\n /** Current search query — from `useLaunchpadFilter()`. */\n query: string;\n /** Current active group filters — from `useLaunchpadFilter()`. */\n activeGroups: Set<ServiceGroup>;\n /** Reset both — from `useLaunchpadFilter()`. */\n onClearFilters: () => void;\n className?: string;\n}\n\n/** The full launchpad services grid — one dynamic grid of all live services,\n * ordered by group, plus the coming-soon strip. Fully controlled by\n * `useLaunchpadFilter()` state passed in from the page. */\nexport function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilters, className }: LaunchpadGroupedSectionsProps) {\n const inActiveGroup = (d: ServiceDefinition) => activeGroups.size === 0 || activeGroups.has(d.group);\n const inSearch = (d: ServiceDefinition) =>\n query.trim() === \"\" ||\n `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase());\n\n const groupOrder = Object.fromEntries(LAUNCHPAD_SERVICE_GROUPS.map((g, i) => [g.key, i]));\n\n const liveDefs = LAUNCHPAD_SERVICE_DEFINITIONS\n .filter((d) => d.group !== \"coming-soon\" && d.status === \"live\" && inActiveGroup(d) && inSearch(d))\n .sort((a, b) => (groupOrder[a.group] ?? 99) - (groupOrder[b.group] ?? 99));\n\n const comingSoonGroup = LAUNCHPAD_SERVICE_GROUPS.find((g) => g.key === \"coming-soon\");\n const comingSoonDefs = LAUNCHPAD_SERVICE_DEFINITIONS.filter(\n (d) => d.group === \"coming-soon\" && inActiveGroup(d) && inSearch(d),\n );\n\n return (\n <div className={cn(\"space-y-8\", className)}>\n {liveDefs.length === 0 && comingSoonDefs.length === 0 ? (\n <div className=\"text-center py-16 space-y-3\">\n <p className=\"text-lg font-semibold\">No services match</p>\n <p className=\"text-sm text-muted-foreground\">Try a different search or clear your filters.</p>\n <button\n type=\"button\"\n onClick={onClearFilters}\n className=\"inline-flex items-center h-9 px-4 rounded-full text-sm font-semibold bg-primary text-primary-foreground\"\n >\n Clear filters\n </button>\n </div>\n ) : (\n <>\n <motion.div layout className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n <AnimatePresence mode=\"popLayout\">\n {liveDefs.map((def, i) => (\n <LaunchpadServiceCard key={def.key} def={def} override={overrides[def.key]} index={i} />\n ))}\n </AnimatePresence>\n </motion.div>\n {comingSoonGroup && comingSoonDefs.length > 0 && (\n <ComingSoonStrip group={comingSoonGroup} defs={comingSoonDefs} />\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Link"]}