@medialane/ui 0.66.0 → 0.68.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.
@@ -23,9 +23,11 @@ __export(launchpad_filter_bar_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(launchpad_filter_bar_exports);
25
25
  var import_jsx_runtime = require("react/jsx-runtime");
26
+ var import_react = require("react");
26
27
  var import_lucide_react = require("lucide-react");
27
28
  var import_cn = require("../utils/cn.js");
28
- var import_launchpad_services = require("../data/launchpad-services.js");
29
+ var import_launchpad_services = require("./launchpad-services.js");
30
+ var import_launchpad_services2 = require("../data/launchpad-services.js");
29
31
  function LaunchpadFilterBar({
30
32
  query,
31
33
  onQueryChange,
@@ -34,8 +36,20 @@ function LaunchpadFilterBar({
34
36
  onToggleGroup,
35
37
  resultCount
36
38
  }) {
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
+ const inputRef = (0, import_react.useRef)(null);
40
+ (0, import_react.useEffect)(() => {
41
+ const onKeyDown = (e) => {
42
+ if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey) return;
43
+ const target = e.target;
44
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) return;
45
+ e.preventDefault();
46
+ inputRef.current?.focus();
47
+ };
48
+ window.addEventListener("keydown", onKeyDown);
49
+ return () => window.removeEventListener("keydown", onKeyDown);
50
+ }, []);
51
+ const countFor = (key) => import_launchpad_services2.LAUNCHPAD_SERVICE_DEFINITIONS.filter(
52
+ (d) => d.group === key && d.status === "live" && (0, import_launchpad_services.serviceMatchesQuery)(d, query)
39
53
  ).length;
40
54
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col sm:flex-row gap-3 sm:items-center", children: [
41
55
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex-1 sm:max-w-xs", children: [
@@ -43,6 +57,7 @@ function LaunchpadFilterBar({
43
57
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
44
58
  "input",
45
59
  {
60
+ ref: inputRef,
46
61
  type: "text",
47
62
  value: query,
48
63
  onChange: (e) => onQueryChange(e.target.value),
@@ -50,7 +65,7 @@ function LaunchpadFilterBar({
50
65
  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"
51
66
  }
52
67
  ),
53
- query && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
68
+ query ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
54
69
  "button",
55
70
  {
56
71
  type: "button",
@@ -59,7 +74,7 @@ function LaunchpadFilterBar({
59
74
  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",
60
75
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-3.5 w-3.5" })
61
76
  }
62
- )
77
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("kbd", { className: "absolute right-3 top-1/2 -translate-y-1/2 hidden sm:flex h-5 w-5 items-center justify-center rounded border border-border bg-muted/40 text-[11px] font-medium text-muted-foreground pointer-events-none", children: "/" })
63
78
  ] }),
64
79
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-2 flex-1", children: groups.map((group) => {
65
80
  const active = activeGroups.has(group.key);
@@ -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 {\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":[]}
1
+ {"version":3,"sources":["../../src/components/launchpad-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { serviceMatchesQuery } from \"./launchpad-services.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. Pressing \"/\" anywhere on the page focuses\n * the search input.\n */\nexport function LaunchpadFilterBar({\n query,\n onQueryChange,\n groups,\n activeGroups,\n onToggleGroup,\n resultCount,\n}: LaunchpadFilterBarProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key !== \"/\" || e.metaKey || e.ctrlKey || e.altKey) return;\n const target = e.target as HTMLElement | null;\n if (target && (target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\" || target.isContentEditable)) return;\n e.preventDefault();\n inputRef.current?.focus();\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, []);\n\n const countFor = (key: ServiceGroup) =>\n LAUNCHPAD_SERVICE_DEFINITIONS.filter(\n (d) => d.group === key && d.status === \"live\" && serviceMatchesQuery(d, query),\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 ref={inputRef}\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 <kbd className=\"absolute right-3 top-1/2 -translate-y-1/2 hidden sm:flex h-5 w-5 items-center justify-center rounded border border-border bg-muted/40 text-[11px] font-medium text-muted-foreground pointer-events-none\">\n /\n </kbd>\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;AAyDM;AAvDN,mBAAkC;AAClC,0BAA0B;AAC1B,gBAAmB;AACnB,gCAAoC;AACpC,IAAAA,6BAIO;AAkBA,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,eAAW,qBAAyB,IAAI;AAE9C,8BAAU,MAAM;AACd,UAAM,YAAY,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,OAAQ;AACzD,YAAM,SAAS,EAAE;AACjB,UAAI,WAAW,OAAO,YAAY,WAAW,OAAO,YAAY,cAAc,OAAO,mBAAoB;AACzG,QAAE,eAAe;AACjB,eAAS,SAAS,MAAM;AAAA,IAC1B;AACA,WAAO,iBAAiB,WAAW,SAAS;AAC5C,WAAO,MAAM,OAAO,oBAAoB,WAAW,SAAS;AAAA,EAC9D,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW,CAAC,QAChB,yDAA8B;AAAA,IAC5B,CAAC,MAAM,EAAE,UAAU,OAAO,EAAE,WAAW,cAAU,+CAAoB,GAAG,KAAK;AAAA,EAC/E,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,KAAK;AAAA,UACL,MAAK;AAAA,UACL,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAK;AAAA,UAC7C,aAAY;AAAA,UACZ,WAAU;AAAA;AAAA,MACZ;AAAA,MACC,QACC;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,IAEA,4CAAC,SAAI,WAAU,2MAA0M,eAEzN;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":["import_launchpad_services"]}
@@ -14,7 +14,8 @@ interface LaunchpadFilterBarProps {
14
14
  * Search + group-filter bar above the launchpad grid. Fully controlled — all
15
15
  * state (query, active groups) lives in the caller so the grid below reacts
16
16
  * to the same state. Each pill shows the live count of services it matches
17
- * under the current search query.
17
+ * under the current search query. Pressing "/" anywhere on the page focuses
18
+ * the search input.
18
19
  */
19
20
  declare function LaunchpadFilterBar({ query, onQueryChange, groups, activeGroups, onToggleGroup, resultCount, }: LaunchpadFilterBarProps): react_jsx_runtime.JSX.Element;
20
21
 
@@ -14,7 +14,8 @@ interface LaunchpadFilterBarProps {
14
14
  * Search + group-filter bar above the launchpad grid. Fully controlled — all
15
15
  * state (query, active groups) lives in the caller so the grid below reacts
16
16
  * to the same state. Each pill shows the live count of services it matches
17
- * under the current search query.
17
+ * under the current search query. Pressing "/" anywhere on the page focuses
18
+ * the search input.
18
19
  */
19
20
  declare function LaunchpadFilterBar({ query, onQueryChange, groups, activeGroups, onToggleGroup, resultCount, }: LaunchpadFilterBarProps): react_jsx_runtime.JSX.Element;
20
21
 
@@ -1,7 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
3
4
  import { Search, X } from "lucide-react";
4
5
  import { cn } from "../utils/cn.js";
6
+ import { serviceMatchesQuery } from "./launchpad-services.js";
5
7
  import {
6
8
  LAUNCHPAD_SERVICE_DEFINITIONS
7
9
  } from "../data/launchpad-services.js";
@@ -13,8 +15,20 @@ function LaunchpadFilterBar({
13
15
  onToggleGroup,
14
16
  resultCount
15
17
  }) {
18
+ const inputRef = useRef(null);
19
+ useEffect(() => {
20
+ const onKeyDown = (e) => {
21
+ if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey) return;
22
+ const target = e.target;
23
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) return;
24
+ e.preventDefault();
25
+ inputRef.current?.focus();
26
+ };
27
+ window.addEventListener("keydown", onKeyDown);
28
+ return () => window.removeEventListener("keydown", onKeyDown);
29
+ }, []);
16
30
  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()))
31
+ (d) => d.group === key && d.status === "live" && serviceMatchesQuery(d, query)
18
32
  ).length;
19
33
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row gap-3 sm:items-center", children: [
20
34
  /* @__PURE__ */ jsxs("div", { className: "relative flex-1 sm:max-w-xs", children: [
@@ -22,6 +36,7 @@ function LaunchpadFilterBar({
22
36
  /* @__PURE__ */ jsx(
23
37
  "input",
24
38
  {
39
+ ref: inputRef,
25
40
  type: "text",
26
41
  value: query,
27
42
  onChange: (e) => onQueryChange(e.target.value),
@@ -29,7 +44,7 @@ function LaunchpadFilterBar({
29
44
  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"
30
45
  }
31
46
  ),
32
- query && /* @__PURE__ */ jsx(
47
+ query ? /* @__PURE__ */ jsx(
33
48
  "button",
34
49
  {
35
50
  type: "button",
@@ -38,7 +53,7 @@ function LaunchpadFilterBar({
38
53
  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",
39
54
  children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
40
55
  }
41
- )
56
+ ) : /* @__PURE__ */ jsx("kbd", { className: "absolute right-3 top-1/2 -translate-y-1/2 hidden sm:flex h-5 w-5 items-center justify-center rounded border border-border bg-muted/40 text-[11px] font-medium text-muted-foreground pointer-events-none", children: "/" })
42
57
  ] }),
43
58
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 flex-1", children: groups.map((group) => {
44
59
  const active = activeGroups.has(group.key);
@@ -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 {\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":[]}
1
+ {"version":3,"sources":["../../src/components/launchpad-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { Search, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { serviceMatchesQuery } from \"./launchpad-services.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. Pressing \"/\" anywhere on the page focuses\n * the search input.\n */\nexport function LaunchpadFilterBar({\n query,\n onQueryChange,\n groups,\n activeGroups,\n onToggleGroup,\n resultCount,\n}: LaunchpadFilterBarProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key !== \"/\" || e.metaKey || e.ctrlKey || e.altKey) return;\n const target = e.target as HTMLElement | null;\n if (target && (target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\" || target.isContentEditable)) return;\n e.preventDefault();\n inputRef.current?.focus();\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, []);\n\n const countFor = (key: ServiceGroup) =>\n LAUNCHPAD_SERVICE_DEFINITIONS.filter(\n (d) => d.group === key && d.status === \"live\" && serviceMatchesQuery(d, query),\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 ref={inputRef}\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 <kbd className=\"absolute right-3 top-1/2 -translate-y-1/2 hidden sm:flex h-5 w-5 items-center justify-center rounded border border-border bg-muted/40 text-[11px] font-medium text-muted-foreground pointer-events-none\">\n /\n </kbd>\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":";AAyDM,SACE,KADF;AAvDN,SAAS,WAAW,cAAc;AAClC,SAAS,QAAQ,SAAS;AAC1B,SAAS,UAAU;AACnB,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,OAGK;AAkBA,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,WAAW,OAAyB,IAAI;AAE9C,YAAU,MAAM;AACd,UAAM,YAAY,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,OAAQ;AACzD,YAAM,SAAS,EAAE;AACjB,UAAI,WAAW,OAAO,YAAY,WAAW,OAAO,YAAY,cAAc,OAAO,mBAAoB;AACzG,QAAE,eAAe;AACjB,eAAS,SAAS,MAAM;AAAA,IAC1B;AACA,WAAO,iBAAiB,WAAW,SAAS;AAC5C,WAAO,MAAM,OAAO,oBAAoB,WAAW,SAAS;AAAA,EAC9D,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW,CAAC,QAChB,8BAA8B;AAAA,IAC5B,CAAC,MAAM,EAAE,UAAU,OAAO,EAAE,WAAW,UAAU,oBAAoB,GAAG,KAAK;AAAA,EAC/E,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,KAAK;AAAA,UACL,MAAK;AAAA,UACL,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAK;AAAA,UAC7C,aAAY;AAAA,UACZ,WAAU;AAAA;AAAA,MACZ;AAAA,MACC,QACC;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,IAEA,oBAAC,SAAI,WAAU,2MAA0M,eAEzN;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,10 +29,11 @@ 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
+ GROUP_SLICES: () => GROUP_SLICES,
33
33
  LaunchpadGroupedSections: () => LaunchpadGroupedSections,
34
34
  LaunchpadServiceCard: () => LaunchpadServiceCard,
35
35
  SERVICE_HUES: () => SERVICE_HUES,
36
+ serviceMatchesQuery: () => serviceMatchesQuery,
36
37
  useLaunchpadFilter: () => useLaunchpadFilter
37
38
  });
38
39
  module.exports = __toCommonJS(launchpad_services_exports);
@@ -43,18 +44,38 @@ var import_framer_motion = require("framer-motion");
43
44
  var import_lucide_react = require("lucide-react");
44
45
  var import_cn = require("../utils/cn.js");
45
46
  var import_launchpad_services = require("../data/launchpad-services.js");
46
- const DEFAULT_ACCENT = {
47
- text: "text-muted-foreground",
48
- tint: "bg-muted/60",
47
+ const DEFAULT_SLICE = {
48
+ chip: "bg-gradient-to-br from-muted to-muted",
49
+ marker: "text-muted-foreground/60",
49
50
  hoverBorder: "sm:hover:border-border"
50
51
  };
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
52
+ const GROUP_SLICES = {
53
+ "nfts": {
54
+ chip: "bg-gradient-to-br from-brand-blue to-brand-indigo",
55
+ marker: "text-brand-blue",
56
+ hoverBorder: "sm:hover:border-brand-blue/50"
57
+ },
58
+ "limited-editions": {
59
+ chip: "bg-gradient-to-br from-brand-indigo to-brand-purple",
60
+ marker: "text-brand-indigo",
61
+ hoverBorder: "sm:hover:border-brand-indigo/50"
62
+ },
63
+ "coins": {
64
+ chip: "bg-gradient-to-br from-brand-purple to-brand-rose",
65
+ marker: "text-brand-purple",
66
+ hoverBorder: "sm:hover:border-brand-purple/50"
67
+ },
68
+ "community": {
69
+ chip: "bg-gradient-to-br from-brand-rose to-brand-orange",
70
+ marker: "text-brand-rose",
71
+ hoverBorder: "sm:hover:border-brand-rose/50"
72
+ },
73
+ "claims": {
74
+ chip: "bg-gradient-to-br from-brand-orange to-brand-price",
75
+ marker: "text-brand-orange",
76
+ hoverBorder: "sm:hover:border-brand-orange/50"
77
+ },
78
+ "coming-soon": DEFAULT_SLICE
58
79
  };
59
80
  const SERVICE_HUES = {
60
81
  "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" },
@@ -72,37 +93,44 @@ const SERVICE_HUES = {
72
93
  const GROUP_TITLE_BY_KEY = Object.fromEntries(
73
94
  import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((g) => [g.key, g.title])
74
95
  );
96
+ const GROUP_KEYS = new Set(import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((g) => g.key));
75
97
  function LaunchpadServiceCard({ def, override = {}, index = 0 }) {
76
- const { icon: Icon, title, group } = def;
98
+ const { icon: Icon, title, group, features, cta } = def;
77
99
  const status = override.status ?? def.status;
78
100
  const blurb = override.blurb ?? def.blurb;
79
101
  const { href } = override;
80
102
  const reduceMotion = (0, import_framer_motion.useReducedMotion)();
81
103
  const live = status === "live";
82
- const accent = GROUP_ACCENTS[group] ?? DEFAULT_ACCENT;
104
+ const slice = GROUP_SLICES[group] ?? DEFAULT_SLICE;
83
105
  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: [
106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-start justify-between gap-3", children: [
107
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("flex h-14 w-14 items-center justify-center rounded-2xl shadow-sm", live ? slice.chip : "bg-muted/40"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: (0, import_cn.cn)("h-7 w-7", live ? "text-white" : "text-muted-foreground/50") }) }),
108
+ !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: [
87
109
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Lock, { className: "h-3 w-3" }),
88
110
  "Coming soon"
89
111
  ] })
90
112
  ] }),
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
- ] }),
113
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-auto space-y-2 pt-6", children: [
114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-2xl font-bold tracking-tight leading-tight", children: title }),
96
115
  /* @__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
- ] })
116
+ ] }),
117
+ live && features.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "space-y-1.5 pt-4", children: features.slice(0, 3).map((feature) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: "flex items-start gap-2 text-xs text-muted-foreground leading-relaxed", children: [
118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Check, { className: (0, import_cn.cn)("h-3.5 w-3.5 shrink-0 mt-px", slice.marker) }),
119
+ feature
120
+ ] }, feature)) }),
121
+ live && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pt-5", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-2 h-10 px-5 rounded-full text-sm font-semibold text-white bg-gradient-to-r from-brand-blue via-brand-purple to-brand-rose", children: [
122
+ cta,
123
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, { className: "h-4 w-4 transition-transform duration-200 sm:group-hover:translate-x-0.5 motion-reduce:transform-none" })
124
+ ] }) })
98
125
  ] });
99
126
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
100
127
  import_framer_motion.motion.div,
101
128
  {
102
129
  layout: !reduceMotion,
103
- initial: { opacity: 0, y: reduceMotion ? 0 : 12 },
130
+ initial: { opacity: 0, y: reduceMotion ? 0 : 16 },
104
131
  animate: { opacity: 1, y: 0 },
105
132
  exit: { opacity: 0 },
133
+ whileTap: live ? { scale: 0.97 } : void 0,
106
134
  transition: { duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] },
107
135
  className: "flex",
108
136
  children: live && href ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -110,13 +138,13 @@ function LaunchpadServiceCard({ def, override = {}, index = 0 }) {
110
138
  {
111
139
  href,
112
140
  className: (0, import_cn.cn)(
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
141
+ "group flex flex-1 flex-col rounded-3xl border border-border/60 bg-card p-6 sm:aspect-[4/5]",
142
+ "transition-colors duration-200",
143
+ slice.hoverBorder
116
144
  ),
117
145
  children: body
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
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-1 flex-col rounded-3xl border border-border/30 bg-card p-6 sm:aspect-[4/5] opacity-75", children: body })
120
148
  }
121
149
  );
122
150
  }
@@ -130,24 +158,52 @@ function ComingSoonStrip({ group, defs }) {
130
158
  ] }, key)) })
131
159
  ] });
132
160
  }
161
+ function serviceMatchesQuery(def, query) {
162
+ if (query.trim() === "") return true;
163
+ const haystack = `${def.title} ${def.blurb} ${def.subtitle} ${GROUP_TITLE_BY_KEY[def.group] ?? ""}`.toLowerCase();
164
+ return haystack.includes(query.trim().toLowerCase());
165
+ }
166
+ function syncFilterUrl(query, groups) {
167
+ if (typeof window === "undefined") return;
168
+ const params = new URLSearchParams(window.location.search);
169
+ if (query.trim()) params.set("q", query.trim());
170
+ else params.delete("q");
171
+ if (groups.size > 0) params.set("groups", [...groups].join(","));
172
+ else params.delete("groups");
173
+ const qs = params.toString();
174
+ window.history.replaceState(null, "", qs ? `${window.location.pathname}?${qs}` : window.location.pathname);
175
+ }
133
176
  function useLaunchpadFilter() {
134
- const [query, setQuery] = (0, import_react.useState)("");
177
+ const [query, setQueryState] = (0, import_react.useState)("");
135
178
  const [activeGroups, setActiveGroups] = (0, import_react.useState)(/* @__PURE__ */ new Set());
179
+ (0, import_react.useEffect)(() => {
180
+ const params = new URLSearchParams(window.location.search);
181
+ const q = params.get("q");
182
+ if (q) setQueryState(q);
183
+ const g = params.get("groups");
184
+ if (g) {
185
+ const groups = g.split(",").filter((k) => GROUP_KEYS.has(k));
186
+ if (groups.length > 0) setActiveGroups(new Set(groups));
187
+ }
188
+ }, []);
136
189
  const filterableGroups = import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.filter((g) => g.key !== "coming-soon");
190
+ const setQuery = (value) => {
191
+ setQueryState(value);
192
+ syncFilterUrl(value, activeGroups);
193
+ };
137
194
  const toggleGroup = (key) => {
138
195
  setActiveGroups((prev) => {
139
196
  const next = new Set(prev);
140
197
  if (next.has(key)) next.delete(key);
141
198
  else next.add(key);
199
+ syncFilterUrl(query, next);
142
200
  return next;
143
201
  });
144
202
  };
145
203
  const matches = (def) => {
146
204
  if (activeGroups.size > 0 && !activeGroups.has(def.group)) return false;
147
205
  if (def.status !== "live") return false;
148
- if (query.trim() === "") return true;
149
- const haystack = `${def.title} ${def.blurb} ${def.subtitle}`.toLowerCase();
150
- return haystack.includes(query.trim().toLowerCase());
206
+ return serviceMatchesQuery(def, query);
151
207
  };
152
208
  const totalMatches = (0, import_react.useMemo)(
153
209
  () => import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter(matches).length,
@@ -155,14 +211,15 @@ function useLaunchpadFilter() {
155
211
  [query, activeGroups]
156
212
  );
157
213
  const clear = () => {
158
- setQuery("");
214
+ setQueryState("");
159
215
  setActiveGroups(/* @__PURE__ */ new Set());
216
+ syncFilterUrl("", /* @__PURE__ */ new Set());
160
217
  };
161
218
  return { query, setQuery, activeGroups, toggleGroup, filterableGroups, totalMatches, clear };
162
219
  }
163
220
  function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilters, className }) {
164
221
  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());
222
+ const inSearch = (d) => serviceMatchesQuery(d, query);
166
223
  const groupOrder = Object.fromEntries(import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((g, i) => [g.key, i]));
167
224
  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
225
  const comingSoonGroup = import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.find((g) => g.key === "coming-soon");
@@ -182,16 +239,17 @@ function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilte
182
239
  }
183
240
  )
184
241
  ] }) : /* @__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)) }) }),
242
+ /* @__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 xl:grid-cols-4 gap-5 sm:gap-6", 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
243
  comingSoonGroup && comingSoonDefs.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComingSoonStrip, { group: comingSoonGroup, defs: comingSoonDefs })
187
244
  ] }) });
188
245
  }
189
246
  // Annotate the CommonJS export names for ESM import in node:
190
247
  0 && (module.exports = {
191
- GROUP_ACCENTS,
248
+ GROUP_SLICES,
192
249
  LaunchpadGroupedSections,
193
250
  LaunchpadServiceCard,
194
251
  SERVICE_HUES,
252
+ serviceMatchesQuery,
195
253
  useLaunchpadFilter
196
254
  });
197
255
  //# sourceMappingURL=launchpad-services.cjs.map
@@ -1 +1 @@
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"]}
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 { useEffect, 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 /** 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\n/** The deck is the brand spectrum: each group owns one slice of the Medialane\n * gradient (blue → indigo → purple → rose → orange), so the grid read in\n * group order draws the full spectrum across the launchpad. The slice colors\n * the icon chip, the use-case markers, and the desktop hover border — never\n * text labels. */\ninterface GroupSlice {\n /** icon chip fill — a two-stop slice of the brand gradient */\n chip: string;\n /** use-case marker tint */\n marker: string;\n /** card border tint on desktop hover (polish only) */\n hoverBorder: string;\n}\n\nconst DEFAULT_SLICE: GroupSlice = {\n chip: \"bg-gradient-to-br from-muted to-muted\",\n marker: \"text-muted-foreground/60\",\n hoverBorder: \"sm:hover:border-border\",\n};\n\nexport const GROUP_SLICES: Record<ServiceGroup, GroupSlice> = {\n \"nfts\": {\n chip: \"bg-gradient-to-br from-brand-blue to-brand-indigo\",\n marker: \"text-brand-blue\",\n hoverBorder: \"sm:hover:border-brand-blue/50\",\n },\n \"limited-editions\": {\n chip: \"bg-gradient-to-br from-brand-indigo to-brand-purple\",\n marker: \"text-brand-indigo\",\n hoverBorder: \"sm:hover:border-brand-indigo/50\",\n },\n \"coins\": {\n chip: \"bg-gradient-to-br from-brand-purple to-brand-rose\",\n marker: \"text-brand-purple\",\n hoverBorder: \"sm:hover:border-brand-purple/50\",\n },\n \"community\": {\n chip: \"bg-gradient-to-br from-brand-rose to-brand-orange\",\n marker: \"text-brand-rose\",\n hoverBorder: \"sm:hover:border-brand-rose/50\",\n },\n \"claims\": {\n chip: \"bg-gradient-to-br from-brand-orange to-brand-price\",\n marker: \"text-brand-orange\",\n hoverBorder: \"sm:hover:border-brand-orange/50\",\n },\n \"coming-soon\": DEFAULT_SLICE,\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\nconst GROUP_KEYS = new Set(LAUNCHPAD_SERVICE_GROUPS.map((g) => g.key));\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, features, cta } = 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 slice = GROUP_SLICES[group] ?? DEFAULT_SLICE;\n\n const body = (\n <>\n {/* Icon chip — the group's slice of the brand spectrum */}\n <div className=\"flex items-start justify-between gap-3\">\n <span className={cn(\"flex h-14 w-14 items-center justify-center rounded-2xl shadow-sm\", live ? slice.chip : \"bg-muted/40\")}>\n <Icon className={cn(\"h-7 w-7\", live ? \"text-white\" : \"text-muted-foreground/50\")} />\n </span>\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 {/* Title + one sentence */}\n <div className=\"mt-auto space-y-2 pt-6\">\n <h3 className=\"text-2xl font-bold tracking-tight leading-tight\">{title}</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 {/* Use cases */}\n {live && features.length > 0 && (\n <ul className=\"space-y-1.5 pt-4\">\n {features.slice(0, 3).map((feature) => (\n <li key={feature} className=\"flex items-start gap-2 text-xs text-muted-foreground leading-relaxed\">\n <Check className={cn(\"h-3.5 w-3.5 shrink-0 mt-px\", slice.marker)} />\n {feature}\n </li>\n ))}\n </ul>\n )}\n\n {/* The action — one vivid full-spectrum pill, identical on every card */}\n {live && (\n <div className=\"pt-5\">\n <span className=\"inline-flex items-center gap-2 h-10 px-5 rounded-full text-sm font-semibold text-white bg-gradient-to-r from-brand-blue via-brand-purple to-brand-rose\">\n {cta}\n <ArrowRight className=\"h-4 w-4 transition-transform duration-200 sm:group-hover:translate-x-0.5 motion-reduce:transform-none\" />\n </span>\n </div>\n )}\n </>\n );\n\n return (\n <motion.div\n layout={!reduceMotion}\n initial={{ opacity: 0, y: reduceMotion ? 0 : 16 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0 }}\n whileTap={live ? { scale: 0.97 } : undefined}\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 rounded-3xl border border-border/60 bg-card p-6 sm:aspect-[4/5]\",\n \"transition-colors duration-200\",\n slice.hoverBorder,\n )}\n >\n {body}\n </Link>\n ) : (\n <div className=\"flex flex-1 flex-col rounded-3xl border border-border/30 bg-card p-6 sm:aspect-[4/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/** Shared search predicate — group titles count as search terms so a query\n * like \"editions\" or \"community\" finds the whole group. */\nexport function serviceMatchesQuery(def: ServiceDefinition, query: string): boolean {\n if (query.trim() === \"\") return true;\n const haystack = `${def.title} ${def.blurb} ${def.subtitle} ${GROUP_TITLE_BY_KEY[def.group] ?? \"\"}`.toLowerCase();\n return haystack.includes(query.trim().toLowerCase());\n}\n\n/** Sync filter state into the URL (?q=…&groups=…) via replaceState — the\n * filtered launchpad becomes shareable and survives reload, without touching\n * the router (this package is router-agnostic). */\nfunction syncFilterUrl(query: string, groups: Set<ServiceGroup>) {\n if (typeof window === \"undefined\") return;\n const params = new URLSearchParams(window.location.search);\n if (query.trim()) params.set(\"q\", query.trim());\n else params.delete(\"q\");\n if (groups.size > 0) params.set(\"groups\", [...groups].join(\",\"));\n else params.delete(\"groups\");\n const qs = params.toString();\n window.history.replaceState(null, \"\", qs ? `${window.location.pathname}?${qs}` : window.location.pathname);\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 * State is mirrored into the URL (?q=…&groups=…) so a filtered view can be\n * shared and survives reload.\n */\nexport function useLaunchpadFilter() {\n const [query, setQueryState] = useState(\"\");\n const [activeGroups, setActiveGroups] = useState<Set<ServiceGroup>>(new Set());\n\n // Hydrate once from the URL (client only).\n useEffect(() => {\n const params = new URLSearchParams(window.location.search);\n const q = params.get(\"q\");\n if (q) setQueryState(q);\n const g = params.get(\"groups\");\n if (g) {\n const groups = g.split(\",\").filter((k): k is ServiceGroup => GROUP_KEYS.has(k as ServiceGroup));\n if (groups.length > 0) setActiveGroups(new Set(groups));\n }\n }, []);\n\n const filterableGroups = LAUNCHPAD_SERVICE_GROUPS.filter((g) => g.key !== \"coming-soon\");\n\n const setQuery = (value: string) => {\n setQueryState(value);\n syncFilterUrl(value, activeGroups);\n };\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 syncFilterUrl(query, next);\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 return serviceMatchesQuery(def, query);\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 = () => {\n setQueryState(\"\");\n setActiveGroups(new Set());\n syncFilterUrl(\"\", new Set());\n };\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) => serviceMatchesQuery(d, query);\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 xl:grid-cols-4 gap-5 sm:gap-6\">\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"]}