@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.
@@ -1,4 +1,3 @@
1
- import * as React from 'react';
2
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
2
  import { ServiceStatus, ServiceGroup, ServiceDefinition, ServiceGroupDefinition } from '../data/launchpad-services.cjs';
4
3
  import 'lucide-react';
@@ -14,15 +13,20 @@ interface ServiceOverride {
14
13
  blurb?: string;
15
14
  }
16
15
  type ServiceOverrides = Record<string, ServiceOverride>;
17
- interface GroupAccent {
18
- /** icon glyph + tag text tint */
19
- text: string;
20
- /** soft icon-tile background */
21
- tint: string;
16
+ /** The deck is the brand spectrum: each group owns one slice of the Medialane
17
+ * gradient (blue indigo purple → rose → orange), so the grid read in
18
+ * group order draws the full spectrum across the launchpad. The slice colors
19
+ * the icon chip, the use-case markers, and the desktop hover border — never
20
+ * text labels. */
21
+ interface GroupSlice {
22
+ /** icon chip fill — a two-stop slice of the brand gradient */
23
+ chip: string;
24
+ /** use-case marker tint */
25
+ marker: string;
22
26
  /** card border tint on desktop hover (polish only) */
23
27
  hoverBorder: string;
24
28
  }
25
- declare const GROUP_ACCENTS: Record<ServiceGroup, GroupAccent>;
29
+ declare const GROUP_SLICES: Record<ServiceGroup, GroupSlice>;
26
30
  /** Legacy per-service hue table — kept for LaunchpadStrip (homepage). The grid
27
31
  * no longer uses it. */
28
32
  interface ServiceHue {
@@ -39,15 +43,20 @@ interface LaunchpadServiceCardProps {
39
43
  index?: number;
40
44
  }
41
45
  declare function LaunchpadServiceCard({ def, override, index }: LaunchpadServiceCardProps): react_jsx_runtime.JSX.Element;
46
+ /** Shared search predicate — group titles count as search terms so a query
47
+ * like "editions" or "community" finds the whole group. */
48
+ declare function serviceMatchesQuery(def: ServiceDefinition, query: string): boolean;
42
49
  /**
43
50
  * Owns the search + group-filter state for the launchpad page. A page renders
44
51
  * `LaunchpadFilterBar` wherever it wants (e.g. right under the h1) wired to
45
52
  * this hook's fields, then passes `query`/`activeGroups` straight through to
46
53
  * `LaunchpadGroupedSections` so the grid reacts to the same state.
54
+ * State is mirrored into the URL (?q=…&groups=…) so a filtered view can be
55
+ * shared and survives reload.
47
56
  */
48
57
  declare function useLaunchpadFilter(): {
49
58
  query: string;
50
- setQuery: React.Dispatch<React.SetStateAction<string>>;
59
+ setQuery: (value: string) => void;
51
60
  activeGroups: Set<ServiceGroup>;
52
61
  toggleGroup: (key: ServiceGroup) => void;
53
62
  filterableGroups: ServiceGroupDefinition[];
@@ -70,4 +79,4 @@ interface LaunchpadGroupedSectionsProps {
70
79
  * `useLaunchpadFilter()` state passed in from the page. */
71
80
  declare function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilters, className }: LaunchpadGroupedSectionsProps): react_jsx_runtime.JSX.Element;
72
81
 
73
- export { GROUP_ACCENTS, LaunchpadGroupedSections, type LaunchpadGroupedSectionsProps, LaunchpadServiceCard, type LaunchpadServiceCardProps, SERVICE_HUES, type ServiceOverride, type ServiceOverrides, useLaunchpadFilter };
82
+ export { GROUP_SLICES, LaunchpadGroupedSections, type LaunchpadGroupedSectionsProps, LaunchpadServiceCard, type LaunchpadServiceCardProps, SERVICE_HUES, type ServiceOverride, type ServiceOverrides, serviceMatchesQuery, useLaunchpadFilter };
@@ -1,4 +1,3 @@
1
- import * as React from 'react';
2
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
2
  import { ServiceStatus, ServiceGroup, ServiceDefinition, ServiceGroupDefinition } from '../data/launchpad-services.js';
4
3
  import 'lucide-react';
@@ -14,15 +13,20 @@ interface ServiceOverride {
14
13
  blurb?: string;
15
14
  }
16
15
  type ServiceOverrides = Record<string, ServiceOverride>;
17
- interface GroupAccent {
18
- /** icon glyph + tag text tint */
19
- text: string;
20
- /** soft icon-tile background */
21
- tint: string;
16
+ /** The deck is the brand spectrum: each group owns one slice of the Medialane
17
+ * gradient (blue indigo purple → rose → orange), so the grid read in
18
+ * group order draws the full spectrum across the launchpad. The slice colors
19
+ * the icon chip, the use-case markers, and the desktop hover border — never
20
+ * text labels. */
21
+ interface GroupSlice {
22
+ /** icon chip fill — a two-stop slice of the brand gradient */
23
+ chip: string;
24
+ /** use-case marker tint */
25
+ marker: string;
22
26
  /** card border tint on desktop hover (polish only) */
23
27
  hoverBorder: string;
24
28
  }
25
- declare const GROUP_ACCENTS: Record<ServiceGroup, GroupAccent>;
29
+ declare const GROUP_SLICES: Record<ServiceGroup, GroupSlice>;
26
30
  /** Legacy per-service hue table — kept for LaunchpadStrip (homepage). The grid
27
31
  * no longer uses it. */
28
32
  interface ServiceHue {
@@ -39,15 +43,20 @@ interface LaunchpadServiceCardProps {
39
43
  index?: number;
40
44
  }
41
45
  declare function LaunchpadServiceCard({ def, override, index }: LaunchpadServiceCardProps): react_jsx_runtime.JSX.Element;
46
+ /** Shared search predicate — group titles count as search terms so a query
47
+ * like "editions" or "community" finds the whole group. */
48
+ declare function serviceMatchesQuery(def: ServiceDefinition, query: string): boolean;
42
49
  /**
43
50
  * Owns the search + group-filter state for the launchpad page. A page renders
44
51
  * `LaunchpadFilterBar` wherever it wants (e.g. right under the h1) wired to
45
52
  * this hook's fields, then passes `query`/`activeGroups` straight through to
46
53
  * `LaunchpadGroupedSections` so the grid reacts to the same state.
54
+ * State is mirrored into the URL (?q=…&groups=…) so a filtered view can be
55
+ * shared and survives reload.
47
56
  */
48
57
  declare function useLaunchpadFilter(): {
49
58
  query: string;
50
- setQuery: React.Dispatch<React.SetStateAction<string>>;
59
+ setQuery: (value: string) => void;
51
60
  activeGroups: Set<ServiceGroup>;
52
61
  toggleGroup: (key: ServiceGroup) => void;
53
62
  filterableGroups: ServiceGroupDefinition[];
@@ -70,4 +79,4 @@ interface LaunchpadGroupedSectionsProps {
70
79
  * `useLaunchpadFilter()` state passed in from the page. */
71
80
  declare function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilters, className }: LaunchpadGroupedSectionsProps): react_jsx_runtime.JSX.Element;
72
81
 
73
- export { GROUP_ACCENTS, LaunchpadGroupedSections, type LaunchpadGroupedSectionsProps, LaunchpadServiceCard, type LaunchpadServiceCardProps, SERVICE_HUES, type ServiceOverride, type ServiceOverrides, useLaunchpadFilter };
82
+ export { GROUP_SLICES, LaunchpadGroupedSections, type LaunchpadGroupedSectionsProps, LaunchpadServiceCard, type LaunchpadServiceCardProps, SERVICE_HUES, type ServiceOverride, type ServiceOverrides, serviceMatchesQuery, useLaunchpadFilter };
@@ -1,26 +1,46 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useMemo, useState } from "react";
3
+ import { useEffect, useMemo, useState } from "react";
4
4
  import Link from "next/link";
5
5
  import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
6
- import { Lock, ArrowRight } from "lucide-react";
6
+ import { Lock, ArrowRight, Check } from "lucide-react";
7
7
  import { cn } from "../utils/cn.js";
8
8
  import {
9
9
  LAUNCHPAD_SERVICE_DEFINITIONS,
10
10
  LAUNCHPAD_SERVICE_GROUPS
11
11
  } from "../data/launchpad-services.js";
12
- const DEFAULT_ACCENT = {
13
- text: "text-muted-foreground",
14
- tint: "bg-muted/60",
12
+ const DEFAULT_SLICE = {
13
+ chip: "bg-gradient-to-br from-muted to-muted",
14
+ marker: "text-muted-foreground/60",
15
15
  hoverBorder: "sm:hover:border-border"
16
16
  };
17
- const GROUP_ACCENTS = {
18
- "nfts": { text: "text-brand-blue", tint: "bg-brand-blue/10", hoverBorder: "sm:hover:border-brand-blue/40" },
19
- "limited-editions": { text: "text-brand-purple", tint: "bg-brand-purple/10", hoverBorder: "sm:hover:border-brand-purple/40" },
20
- "community": { text: "text-emerald-600 dark:text-emerald-400", tint: "bg-emerald-500/10", hoverBorder: "sm:hover:border-emerald-500/40" },
21
- "coins": { text: "text-brand-orange", tint: "bg-brand-orange/10", hoverBorder: "sm:hover:border-brand-orange/40" },
22
- "claims": { text: "text-brand-rose", tint: "bg-brand-rose/10", hoverBorder: "sm:hover:border-brand-rose/40" },
23
- "coming-soon": DEFAULT_ACCENT
17
+ const GROUP_SLICES = {
18
+ "nfts": {
19
+ chip: "bg-gradient-to-br from-brand-blue to-brand-indigo",
20
+ marker: "text-brand-blue",
21
+ hoverBorder: "sm:hover:border-brand-blue/50"
22
+ },
23
+ "limited-editions": {
24
+ chip: "bg-gradient-to-br from-brand-indigo to-brand-purple",
25
+ marker: "text-brand-indigo",
26
+ hoverBorder: "sm:hover:border-brand-indigo/50"
27
+ },
28
+ "coins": {
29
+ chip: "bg-gradient-to-br from-brand-purple to-brand-rose",
30
+ marker: "text-brand-purple",
31
+ hoverBorder: "sm:hover:border-brand-purple/50"
32
+ },
33
+ "community": {
34
+ chip: "bg-gradient-to-br from-brand-rose to-brand-orange",
35
+ marker: "text-brand-rose",
36
+ hoverBorder: "sm:hover:border-brand-rose/50"
37
+ },
38
+ "claims": {
39
+ chip: "bg-gradient-to-br from-brand-orange to-brand-price",
40
+ marker: "text-brand-orange",
41
+ hoverBorder: "sm:hover:border-brand-orange/50"
42
+ },
43
+ "coming-soon": DEFAULT_SLICE
24
44
  };
25
45
  const SERVICE_HUES = {
26
46
  "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" },
@@ -38,37 +58,44 @@ const SERVICE_HUES = {
38
58
  const GROUP_TITLE_BY_KEY = Object.fromEntries(
39
59
  LAUNCHPAD_SERVICE_GROUPS.map((g) => [g.key, g.title])
40
60
  );
61
+ const GROUP_KEYS = new Set(LAUNCHPAD_SERVICE_GROUPS.map((g) => g.key));
41
62
  function LaunchpadServiceCard({ def, override = {}, index = 0 }) {
42
- const { icon: Icon, title, group } = def;
63
+ const { icon: Icon, title, group, features, cta } = def;
43
64
  const status = override.status ?? def.status;
44
65
  const blurb = override.blurb ?? def.blurb;
45
66
  const { href } = override;
46
67
  const reduceMotion = useReducedMotion();
47
68
  const live = status === "live";
48
- const accent = GROUP_ACCENTS[group] ?? DEFAULT_ACCENT;
69
+ const slice = GROUP_SLICES[group] ?? DEFAULT_SLICE;
49
70
  const body = /* @__PURE__ */ jsxs(Fragment, { children: [
50
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
51
- /* @__PURE__ */ jsx("span", { className: cn("flex h-10 w-10 items-center justify-center rounded-xl", live ? accent.tint : "bg-muted/40"), children: /* @__PURE__ */ jsx(Icon, { className: cn("h-5 w-5", live ? accent.text : "text-muted-foreground/50") }) }),
52
- live ? /* @__PURE__ */ jsx("span", { className: cn("text-[10px] font-semibold uppercase tracking-widest", accent.text), children: GROUP_TITLE_BY_KEY[group] }) : /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-[11px] font-medium text-muted-foreground/60", children: [
71
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
72
+ /* @__PURE__ */ jsx("span", { className: cn("flex h-14 w-14 items-center justify-center rounded-2xl shadow-sm", live ? slice.chip : "bg-muted/40"), children: /* @__PURE__ */ jsx(Icon, { className: cn("h-7 w-7", live ? "text-white" : "text-muted-foreground/50") }) }),
73
+ !live && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-[11px] font-medium text-muted-foreground/60 pt-1", children: [
53
74
  /* @__PURE__ */ jsx(Lock, { className: "h-3 w-3" }),
54
75
  "Coming soon"
55
76
  ] })
56
77
  ] }),
57
- /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
58
- /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-1.5 text-lg font-bold tracking-tight leading-snug", children: [
59
- title,
60
- live && /* @__PURE__ */ jsx(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" })
61
- ] }),
78
+ /* @__PURE__ */ jsxs("div", { className: "mt-auto space-y-2 pt-6", children: [
79
+ /* @__PURE__ */ jsx("h3", { className: "text-2xl font-bold tracking-tight leading-tight", children: title }),
62
80
  /* @__PURE__ */ jsx("p", { className: cn("text-sm leading-relaxed", live ? "text-muted-foreground" : "text-muted-foreground/60"), children: blurb })
63
- ] })
81
+ ] }),
82
+ live && features.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-1.5 pt-4", children: features.slice(0, 3).map((feature) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2 text-xs text-muted-foreground leading-relaxed", children: [
83
+ /* @__PURE__ */ jsx(Check, { className: cn("h-3.5 w-3.5 shrink-0 mt-px", slice.marker) }),
84
+ feature
85
+ ] }, feature)) }),
86
+ live && /* @__PURE__ */ jsx("div", { className: "pt-5", children: /* @__PURE__ */ 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: [
87
+ cta,
88
+ /* @__PURE__ */ jsx(ArrowRight, { className: "h-4 w-4 transition-transform duration-200 sm:group-hover:translate-x-0.5 motion-reduce:transform-none" })
89
+ ] }) })
64
90
  ] });
65
91
  return /* @__PURE__ */ jsx(
66
92
  motion.div,
67
93
  {
68
94
  layout: !reduceMotion,
69
- initial: { opacity: 0, y: reduceMotion ? 0 : 12 },
95
+ initial: { opacity: 0, y: reduceMotion ? 0 : 16 },
70
96
  animate: { opacity: 1, y: 0 },
71
97
  exit: { opacity: 0 },
98
+ whileTap: live ? { scale: 0.97 } : void 0,
72
99
  transition: { duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] },
73
100
  className: "flex",
74
101
  children: live && href ? /* @__PURE__ */ jsx(
@@ -76,13 +103,13 @@ function LaunchpadServiceCard({ def, override = {}, index = 0 }) {
76
103
  {
77
104
  href,
78
105
  className: cn(
79
- "group flex flex-1 flex-col gap-3 rounded-2xl border border-border/60 bg-card p-5",
80
- "transition-colors duration-200 active:scale-[0.99] motion-reduce:transform-none",
81
- accent.hoverBorder
106
+ "group flex flex-1 flex-col rounded-3xl border border-border/60 bg-card p-6 sm:aspect-[4/5]",
107
+ "transition-colors duration-200",
108
+ slice.hoverBorder
82
109
  ),
83
110
  children: body
84
111
  }
85
- ) : /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-3 rounded-2xl border border-border/30 bg-card p-5 opacity-75", children: body })
112
+ ) : /* @__PURE__ */ 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 })
86
113
  }
87
114
  );
88
115
  }
@@ -96,24 +123,52 @@ function ComingSoonStrip({ group, defs }) {
96
123
  ] }, key)) })
97
124
  ] });
98
125
  }
126
+ function serviceMatchesQuery(def, query) {
127
+ if (query.trim() === "") return true;
128
+ const haystack = `${def.title} ${def.blurb} ${def.subtitle} ${GROUP_TITLE_BY_KEY[def.group] ?? ""}`.toLowerCase();
129
+ return haystack.includes(query.trim().toLowerCase());
130
+ }
131
+ function syncFilterUrl(query, groups) {
132
+ if (typeof window === "undefined") return;
133
+ const params = new URLSearchParams(window.location.search);
134
+ if (query.trim()) params.set("q", query.trim());
135
+ else params.delete("q");
136
+ if (groups.size > 0) params.set("groups", [...groups].join(","));
137
+ else params.delete("groups");
138
+ const qs = params.toString();
139
+ window.history.replaceState(null, "", qs ? `${window.location.pathname}?${qs}` : window.location.pathname);
140
+ }
99
141
  function useLaunchpadFilter() {
100
- const [query, setQuery] = useState("");
142
+ const [query, setQueryState] = useState("");
101
143
  const [activeGroups, setActiveGroups] = useState(/* @__PURE__ */ new Set());
144
+ useEffect(() => {
145
+ const params = new URLSearchParams(window.location.search);
146
+ const q = params.get("q");
147
+ if (q) setQueryState(q);
148
+ const g = params.get("groups");
149
+ if (g) {
150
+ const groups = g.split(",").filter((k) => GROUP_KEYS.has(k));
151
+ if (groups.length > 0) setActiveGroups(new Set(groups));
152
+ }
153
+ }, []);
102
154
  const filterableGroups = LAUNCHPAD_SERVICE_GROUPS.filter((g) => g.key !== "coming-soon");
155
+ const setQuery = (value) => {
156
+ setQueryState(value);
157
+ syncFilterUrl(value, activeGroups);
158
+ };
103
159
  const toggleGroup = (key) => {
104
160
  setActiveGroups((prev) => {
105
161
  const next = new Set(prev);
106
162
  if (next.has(key)) next.delete(key);
107
163
  else next.add(key);
164
+ syncFilterUrl(query, next);
108
165
  return next;
109
166
  });
110
167
  };
111
168
  const matches = (def) => {
112
169
  if (activeGroups.size > 0 && !activeGroups.has(def.group)) return false;
113
170
  if (def.status !== "live") return false;
114
- if (query.trim() === "") return true;
115
- const haystack = `${def.title} ${def.blurb} ${def.subtitle}`.toLowerCase();
116
- return haystack.includes(query.trim().toLowerCase());
171
+ return serviceMatchesQuery(def, query);
117
172
  };
118
173
  const totalMatches = useMemo(
119
174
  () => LAUNCHPAD_SERVICE_DEFINITIONS.filter(matches).length,
@@ -121,14 +176,15 @@ function useLaunchpadFilter() {
121
176
  [query, activeGroups]
122
177
  );
123
178
  const clear = () => {
124
- setQuery("");
179
+ setQueryState("");
125
180
  setActiveGroups(/* @__PURE__ */ new Set());
181
+ syncFilterUrl("", /* @__PURE__ */ new Set());
126
182
  };
127
183
  return { query, setQuery, activeGroups, toggleGroup, filterableGroups, totalMatches, clear };
128
184
  }
129
185
  function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilters, className }) {
130
186
  const inActiveGroup = (d) => activeGroups.size === 0 || activeGroups.has(d.group);
131
- const inSearch = (d) => query.trim() === "" || `${d.title} ${d.blurb} ${d.subtitle}`.toLowerCase().includes(query.trim().toLowerCase());
187
+ const inSearch = (d) => serviceMatchesQuery(d, query);
132
188
  const groupOrder = Object.fromEntries(LAUNCHPAD_SERVICE_GROUPS.map((g, i) => [g.key, i]));
133
189
  const liveDefs = 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));
134
190
  const comingSoonGroup = LAUNCHPAD_SERVICE_GROUPS.find((g) => g.key === "coming-soon");
@@ -148,15 +204,16 @@ function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilte
148
204
  }
149
205
  )
150
206
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
151
- /* @__PURE__ */ jsx(motion.div, { layout: true, className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "popLayout", children: liveDefs.map((def, i) => /* @__PURE__ */ jsx(LaunchpadServiceCard, { def, override: overrides[def.key], index: i }, def.key)) }) }),
207
+ /* @__PURE__ */ jsx(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__ */ jsx(AnimatePresence, { mode: "popLayout", children: liveDefs.map((def, i) => /* @__PURE__ */ jsx(LaunchpadServiceCard, { def, override: overrides[def.key], index: i }, def.key)) }) }),
152
208
  comingSoonGroup && comingSoonDefs.length > 0 && /* @__PURE__ */ jsx(ComingSoonStrip, { group: comingSoonGroup, defs: comingSoonDefs })
153
209
  ] }) });
154
210
  }
155
211
  export {
156
- GROUP_ACCENTS,
212
+ GROUP_SLICES,
157
213
  LaunchpadGroupedSections,
158
214
  LaunchpadServiceCard,
159
215
  SERVICE_HUES,
216
+ serviceMatchesQuery,
160
217
  useLaunchpadFilter
161
218
  };
162
219
  //# sourceMappingURL=launchpad-services.js.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":[]}
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":[]}
@@ -48,24 +48,6 @@ function useNavCommandMenu() {
48
48
  close: () => document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE))
49
49
  };
50
50
  }
51
- const RECENT_KEY = "ml.nav.recent";
52
- const RECENT_MAX = 3;
53
- function readRecents() {
54
- try {
55
- const raw = localStorage.getItem(RECENT_KEY);
56
- const ids = raw ? JSON.parse(raw) : [];
57
- return Array.isArray(ids) ? ids.filter((x) => typeof x === "string") : [];
58
- } catch {
59
- return [];
60
- }
61
- }
62
- function pushRecent(id) {
63
- try {
64
- const next = [id, ...readRecents().filter((x) => x !== id)].slice(0, RECENT_MAX);
65
- localStorage.setItem(RECENT_KEY, JSON.stringify(next));
66
- } catch {
67
- }
68
- }
69
51
  function Kbd({ children, className }) {
70
52
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
71
53
  "kbd",
@@ -141,13 +123,11 @@ const GROUP_HEADING_CLASSES = (0, import_cn.cn)(
141
123
  function NavCommandMenu({ commands, trigger, accountSlot, footerSlot }) {
142
124
  const [open, setOpen] = React.useState(false);
143
125
  const [query, setQuery] = React.useState("");
144
- const [recentIds, setRecentIds] = React.useState([]);
145
126
  const router = (0, import_navigation.useRouter)();
146
127
  const inputRef = React.useRef(null);
147
128
  React.useEffect(() => {
148
129
  if (!open) return;
149
130
  setQuery("");
150
- setRecentIds(readRecents());
151
131
  const t = setTimeout(() => inputRef.current?.focus(), 60);
152
132
  return () => clearTimeout(t);
153
133
  }, [open]);
@@ -172,19 +152,12 @@ function NavCommandMenu({ commands, trigger, accountSlot, footerSlot }) {
172
152
  }, []);
173
153
  const runCommand = React.useCallback(
174
154
  (cmd) => {
175
- pushRecent(cmd.id);
176
155
  setOpen(false);
177
156
  if (cmd.href) router.push(cmd.href);
178
157
  else cmd.action?.();
179
158
  },
180
159
  [router]
181
160
  );
182
- const recentItems = React.useMemo(() => {
183
- if (recentIds.length === 0) return [];
184
- const all = new Map(commands.flatMap((g) => g.items.map((it) => [it.id, it])));
185
- return recentIds.map((id) => all.get(id)).filter((it) => Boolean(it));
186
- }, [recentIds, commands]);
187
- const showRecents = query === "" && recentItems.length > 0;
188
161
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
189
162
  trigger,
190
163
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.AnimatePresence, { children: open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
@@ -251,19 +224,10 @@ function NavCommandMenu({ commands, trigger, accountSlot, footerSlot }) {
251
224
  ] }),
252
225
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_cmdk.Command.List, { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain p-2", children: [
253
226
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_cmdk.Command.Empty, { className: "py-8 text-center text-sm text-muted-foreground", children: "No results found." }),
254
- showRecents && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_cmdk.Command.Group, { heading: "Recent", className: GROUP_HEADING_CLASSES, children: recentItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
255
- CommandRow,
256
- {
257
- item,
258
- primary: false,
259
- onSelect: () => runCommand(item)
260
- },
261
- `recent-${item.id}`
262
- )) }),
263
227
  commands.map((group, i) => {
264
228
  const primary = !group.heading;
265
229
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(React.Fragment, { children: [
266
- (i > 0 || showRecents) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_cmdk.Command.Separator, { className: "my-1.5 h-px bg-border/40" }),
230
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_cmdk.Command.Separator, { className: "my-1.5 h-px bg-border/40" }),
267
231
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_cmdk.Command.Group, { heading: group.heading, className: GROUP_HEADING_CLASSES, children: group.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
268
232
  CommandRow,
269
233
  {