@medialane/ui 0.65.1 → 0.67.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,8 +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");
29
+ var import_launchpad_services = require("./launchpad-services.js");
30
+ var import_launchpad_services2 = require("../data/launchpad-services.js");
28
31
  function LaunchpadFilterBar({
29
32
  query,
30
33
  onQueryChange,
@@ -33,12 +36,28 @@ function LaunchpadFilterBar({
33
36
  onToggleGroup,
34
37
  resultCount
35
38
  }) {
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)
53
+ ).length;
36
54
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col sm:flex-row gap-3 sm:items-center", children: [
37
55
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex-1 sm:max-w-xs", children: [
38
56
  /* @__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" }),
39
57
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
40
58
  "input",
41
59
  {
60
+ ref: inputRef,
42
61
  type: "text",
43
62
  value: query,
44
63
  onChange: (e) => onQueryChange(e.target.value),
@@ -46,7 +65,7 @@ function LaunchpadFilterBar({
46
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"
47
66
  }
48
67
  ),
49
- query && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
68
+ query ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
50
69
  "button",
51
70
  {
52
71
  type: "button",
@@ -55,21 +74,25 @@ function LaunchpadFilterBar({
55
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",
56
75
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-3.5 w-3.5" })
57
76
  }
58
- )
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: "/" })
59
78
  ] }),
60
79
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-2 flex-1", children: groups.map((group) => {
61
80
  const active = activeGroups.has(group.key);
62
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
81
+ const count = countFor(group.key);
82
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
63
83
  "button",
64
84
  {
65
85
  type: "button",
66
86
  onClick: () => onToggleGroup(group.key),
67
87
  "aria-pressed": active,
68
88
  className: (0, import_cn.cn)(
69
- "h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border",
89
+ "h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border inline-flex items-center gap-1.5",
70
90
  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
91
  ),
72
- children: group.title
92
+ children: [
93
+ group.title,
94
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("tabular-nums", active ? "text-white/70" : "text-muted-foreground/60"), children: count })
95
+ ]
73
96
  },
74
97
  group.key
75
98
  );
@@ -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 { 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"]}
@@ -11,10 +11,11 @@ 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. 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
 
@@ -11,10 +11,11 @@ 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. 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,12 @@
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";
7
+ import {
8
+ LAUNCHPAD_SERVICE_DEFINITIONS
9
+ } from "../data/launchpad-services.js";
5
10
  function LaunchpadFilterBar({
6
11
  query,
7
12
  onQueryChange,
@@ -10,12 +15,28 @@ function LaunchpadFilterBar({
10
15
  onToggleGroup,
11
16
  resultCount
12
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
+ }, []);
30
+ const countFor = (key) => LAUNCHPAD_SERVICE_DEFINITIONS.filter(
31
+ (d) => d.group === key && d.status === "live" && serviceMatchesQuery(d, query)
32
+ ).length;
13
33
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row gap-3 sm:items-center", children: [
14
34
  /* @__PURE__ */ jsxs("div", { className: "relative flex-1 sm:max-w-xs", children: [
15
35
  /* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" }),
16
36
  /* @__PURE__ */ jsx(
17
37
  "input",
18
38
  {
39
+ ref: inputRef,
19
40
  type: "text",
20
41
  value: query,
21
42
  onChange: (e) => onQueryChange(e.target.value),
@@ -23,7 +44,7 @@ function LaunchpadFilterBar({
23
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"
24
45
  }
25
46
  ),
26
- query && /* @__PURE__ */ jsx(
47
+ query ? /* @__PURE__ */ jsx(
27
48
  "button",
28
49
  {
29
50
  type: "button",
@@ -32,21 +53,25 @@ function LaunchpadFilterBar({
32
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",
33
54
  children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
34
55
  }
35
- )
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: "/" })
36
57
  ] }),
37
58
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 flex-1", children: groups.map((group) => {
38
59
  const active = activeGroups.has(group.key);
39
- return /* @__PURE__ */ jsx(
60
+ const count = countFor(group.key);
61
+ return /* @__PURE__ */ jsxs(
40
62
  "button",
41
63
  {
42
64
  type: "button",
43
65
  onClick: () => onToggleGroup(group.key),
44
66
  "aria-pressed": active,
45
67
  className: cn(
46
- "h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border",
68
+ "h-8 px-3.5 rounded-full text-xs font-semibold transition-colors border inline-flex items-center gap-1.5",
47
69
  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
70
  ),
49
- children: group.title
71
+ children: [
72
+ group.title,
73
+ /* @__PURE__ */ jsx("span", { className: cn("tabular-nums", active ? "text-white/70" : "text-muted-foreground/60"), children: count })
74
+ ]
50
75
  },
51
76
  group.key
52
77
  );
@@ -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 { 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,9 +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
33
  LaunchpadGroupedSections: () => LaunchpadGroupedSections,
33
34
  LaunchpadServiceCard: () => LaunchpadServiceCard,
34
35
  SERVICE_HUES: () => SERVICE_HUES,
36
+ serviceMatchesQuery: () => serviceMatchesQuery,
35
37
  useLaunchpadFilter: () => useLaunchpadFilter
36
38
  });
37
39
  module.exports = __toCommonJS(launchpad_services_exports);
@@ -42,135 +44,84 @@ var import_framer_motion = require("framer-motion");
42
44
  var import_lucide_react = require("lucide-react");
43
45
  var import_cn = require("../utils/cn.js");
44
46
  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"
47
+ const DEFAULT_ACCENT = {
48
+ text: "text-muted-foreground",
49
+ tint: "bg-muted/60",
50
+ hoverBorder: "sm:hover:border-border"
51
+ };
52
+ const GROUP_ACCENTS = {
53
+ "nfts": { text: "text-brand-blue", tint: "bg-brand-blue/10", hoverBorder: "sm:hover:border-brand-blue/40" },
54
+ "limited-editions": { text: "text-brand-purple", tint: "bg-brand-purple/10", hoverBorder: "sm:hover:border-brand-purple/40" },
55
+ "community": { text: "text-emerald-600 dark:text-emerald-400", tint: "bg-emerald-500/10", hoverBorder: "sm:hover:border-emerald-500/40" },
56
+ "coins": { text: "text-brand-orange", tint: "bg-brand-orange/10", hoverBorder: "sm:hover:border-brand-orange/40" },
57
+ "claims": { text: "text-brand-rose", tint: "bg-brand-rose/10", hoverBorder: "sm:hover:border-brand-rose/40" },
58
+ "coming-soon": DEFAULT_ACCENT
50
59
  };
51
60
  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
61
+ "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" },
62
+ "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
63
  "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
64
  "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
65
  "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
66
  "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" },
67
+ "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
68
  "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
69
  "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
70
  "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
71
  "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
72
  };
68
- function LaunchpadServiceCard({ def, override = {}, featured = false, index = 0 }) {
69
- const { key, icon: Icon, title, browseLinkLabel, features } = def;
73
+ const GROUP_TITLE_BY_KEY = Object.fromEntries(
74
+ import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((g) => [g.key, g.title])
75
+ );
76
+ const GROUP_KEYS = new Set(import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((g) => g.key));
77
+ function LaunchpadServiceCard({ def, override = {}, index = 0 }) {
78
+ const { icon: Icon, title, group } = def;
70
79
  const status = override.status ?? def.status;
71
80
  const blurb = override.blurb ?? def.blurb;
72
- const { href, browseHref } = override;
81
+ const { href } = override;
73
82
  const reduceMotion = (0, import_framer_motion.useReducedMotion)();
74
83
  const live = status === "live";
75
- const hue = SERVICE_HUES[key] ?? DEFAULT_HUE;
84
+ const accent = GROUP_ACCENTS[group] ?? DEFAULT_ACCENT;
85
+ const body = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
86
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-start justify-between gap-3", children: [
87
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("flex h-12 w-12 items-center justify-center rounded-2xl", live ? accent.tint : "bg-muted/40"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: (0, import_cn.cn)("h-6 w-6", live ? accent.text : "text-muted-foreground/50") }) }),
88
+ !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: [
89
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Lock, { className: "h-3 w-3" }),
90
+ "Coming soon"
91
+ ] })
92
+ ] }),
93
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1.5 mt-auto", children: [
94
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { className: "flex items-center gap-1.5 text-xl font-bold tracking-tight leading-snug", children: [
95
+ title,
96
+ 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" })
97
+ ] }),
98
+ /* @__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 })
99
+ ] })
100
+ ] });
76
101
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
77
102
  import_framer_motion.motion.div,
78
103
  {
79
104
  layout: !reduceMotion,
80
- initial: { opacity: 0, y: reduceMotion ? 0 : 16 },
81
- whileInView: { opacity: 1, y: 0 },
105
+ initial: { opacity: 0, y: reduceMotion ? 0 : 12 },
106
+ animate: { opacity: 1, y: 0 },
82
107
  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",
108
+ transition: { duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] },
109
+ className: "flex",
110
+ children: live && href ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
111
+ import_link.default,
88
112
  {
113
+ href,
89
114
  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"
115
+ "group flex flex-1 flex-col gap-5 rounded-2xl border border-border/60 bg-card p-6 min-h-[200px]",
116
+ "transition-colors duration-200 active:scale-[0.99] motion-reduce:transform-none",
117
+ accent.hoverBorder
92
118
  ),
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
- ] })
119
+ children: body
145
120
  }
146
- )
121
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-1 flex-col gap-5 rounded-2xl border border-border/30 bg-card p-6 min-h-[200px] opacity-75", children: body })
147
122
  }
148
123
  );
149
124
  }
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
125
  function ComingSoonStrip({ group, defs }) {
175
126
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "rounded-2xl border border-border/40 p-5", children: [
176
127
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-semibold text-sm", children: group.title }),
@@ -181,24 +132,52 @@ function ComingSoonStrip({ group, defs }) {
181
132
  ] }, key)) })
182
133
  ] });
183
134
  }
135
+ function serviceMatchesQuery(def, query) {
136
+ if (query.trim() === "") return true;
137
+ const haystack = `${def.title} ${def.blurb} ${def.subtitle} ${GROUP_TITLE_BY_KEY[def.group] ?? ""}`.toLowerCase();
138
+ return haystack.includes(query.trim().toLowerCase());
139
+ }
140
+ function syncFilterUrl(query, groups) {
141
+ if (typeof window === "undefined") return;
142
+ const params = new URLSearchParams(window.location.search);
143
+ if (query.trim()) params.set("q", query.trim());
144
+ else params.delete("q");
145
+ if (groups.size > 0) params.set("groups", [...groups].join(","));
146
+ else params.delete("groups");
147
+ const qs = params.toString();
148
+ window.history.replaceState(null, "", qs ? `${window.location.pathname}?${qs}` : window.location.pathname);
149
+ }
184
150
  function useLaunchpadFilter() {
185
- const [query, setQuery] = (0, import_react.useState)("");
151
+ const [query, setQueryState] = (0, import_react.useState)("");
186
152
  const [activeGroups, setActiveGroups] = (0, import_react.useState)(/* @__PURE__ */ new Set());
153
+ (0, import_react.useEffect)(() => {
154
+ const params = new URLSearchParams(window.location.search);
155
+ const q = params.get("q");
156
+ if (q) setQueryState(q);
157
+ const g = params.get("groups");
158
+ if (g) {
159
+ const groups = g.split(",").filter((k) => GROUP_KEYS.has(k));
160
+ if (groups.length > 0) setActiveGroups(new Set(groups));
161
+ }
162
+ }, []);
187
163
  const filterableGroups = import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.filter((g) => g.key !== "coming-soon");
164
+ const setQuery = (value) => {
165
+ setQueryState(value);
166
+ syncFilterUrl(value, activeGroups);
167
+ };
188
168
  const toggleGroup = (key) => {
189
169
  setActiveGroups((prev) => {
190
170
  const next = new Set(prev);
191
171
  if (next.has(key)) next.delete(key);
192
172
  else next.add(key);
173
+ syncFilterUrl(query, next);
193
174
  return next;
194
175
  });
195
176
  };
196
177
  const matches = (def) => {
197
178
  if (activeGroups.size > 0 && !activeGroups.has(def.group)) return false;
198
179
  if (def.status !== "live") return false;
199
- if (query.trim() === "") return true;
200
- const haystack = `${def.title} ${def.blurb} ${def.subtitle}`.toLowerCase();
201
- return haystack.includes(query.trim().toLowerCase());
180
+ return serviceMatchesQuery(def, query);
202
181
  };
203
182
  const totalMatches = (0, import_react.useMemo)(
204
183
  () => import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter(matches).length,
@@ -206,25 +185,22 @@ function useLaunchpadFilter() {
206
185
  [query, activeGroups]
207
186
  );
208
187
  const clear = () => {
209
- setQuery("");
188
+ setQueryState("");
210
189
  setActiveGroups(/* @__PURE__ */ new Set());
190
+ syncFilterUrl("", /* @__PURE__ */ new Set());
211
191
  };
212
192
  return { query, setQuery, activeGroups, toggleGroup, filterableGroups, totalMatches, clear };
213
193
  }
214
194
  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]
195
+ const inActiveGroup = (d) => activeGroups.size === 0 || activeGroups.has(d.group);
196
+ const inSearch = (d) => serviceMatchesQuery(d, query);
197
+ const groupOrder = Object.fromEntries(import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.map((g, i) => [g.key, i]));
198
+ 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));
199
+ const comingSoonGroup = import_launchpad_services.LAUNCHPAD_SERVICE_GROUPS.find((g) => g.key === "coming-soon");
200
+ const comingSoonDefs = import_launchpad_services.LAUNCHPAD_SERVICE_DEFINITIONS.filter(
201
+ (d) => d.group === "coming-soon" && inActiveGroup(d) && inSearch(d)
226
202
  );
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: [
203
+ 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
204
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-lg font-semibold", children: "No services match" }),
229
205
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-sm text-muted-foreground", children: "Try a different search or clear your filters." }),
230
206
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -236,45 +212,18 @@ function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilte
236
212
  children: "Clear filters"
237
213
  }
238
214
  )
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
- }) }) }) });
215
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
216
+ /* @__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-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)) }) }),
217
+ comingSoonGroup && comingSoonDefs.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComingSoonStrip, { group: comingSoonGroup, defs: comingSoonDefs })
218
+ ] }) });
272
219
  }
273
220
  // Annotate the CommonJS export names for ESM import in node:
274
221
  0 && (module.exports = {
222
+ GROUP_ACCENTS,
275
223
  LaunchpadGroupedSections,
276
224
  LaunchpadServiceCard,
277
225
  SERVICE_HUES,
226
+ serviceMatchesQuery,
278
227
  useLaunchpadFilter
279
228
  });
280
229
  //# sourceMappingURL=launchpad-services.cjs.map