@medialane/ui 0.73.2 → 0.73.3
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.
- package/dist/components/launchpad-services.cjs +9 -9
- package/dist/components/launchpad-services.cjs.map +1 -1
- package/dist/components/launchpad-services.d.cts +5 -3
- package/dist/components/launchpad-services.d.ts +5 -3
- package/dist/components/launchpad-services.js +9 -9
- package/dist/components/launchpad-services.js.map +1 -1
- package/dist/medialane.css +23 -0
- package/package.json +1 -1
|
@@ -47,14 +47,14 @@ var import_launchpad_services = require("../data/launchpad-services.js");
|
|
|
47
47
|
const DEFAULT_SLICE = {
|
|
48
48
|
tint: "bg-muted/60",
|
|
49
49
|
text: "text-muted-foreground/60",
|
|
50
|
-
|
|
50
|
+
border: "border-border/60"
|
|
51
51
|
};
|
|
52
52
|
const GROUP_SLICES = {
|
|
53
|
-
"nfts": { tint: "bg-brand-blue/10", text: "text-brand-blue",
|
|
54
|
-
"limited-editions": { tint: "bg-brand-indigo/10", text: "text-brand-indigo",
|
|
55
|
-
"coins": { tint: "bg-brand-purple/10", text: "text-brand-purple",
|
|
56
|
-
"community": { tint: "bg-brand-rose/10", text: "text-brand-rose",
|
|
57
|
-
"claims": { tint: "bg-brand-orange/10", text: "text-brand-orange",
|
|
53
|
+
"nfts": { tint: "bg-brand-blue/10", text: "text-brand-blue", border: "border-brand-blue/30" },
|
|
54
|
+
"limited-editions": { tint: "bg-brand-indigo/10", text: "text-brand-indigo", border: "border-brand-indigo/30" },
|
|
55
|
+
"coins": { tint: "bg-brand-purple/10", text: "text-brand-purple", border: "border-brand-purple/30" },
|
|
56
|
+
"community": { tint: "bg-brand-rose/10", text: "text-brand-rose", border: "border-brand-rose/30" },
|
|
57
|
+
"claims": { tint: "bg-brand-orange/10", text: "text-brand-orange", border: "border-brand-orange/30" },
|
|
58
58
|
"coming-soon": DEFAULT_SLICE
|
|
59
59
|
};
|
|
60
60
|
const SERVICE_HUES = {
|
|
@@ -118,9 +118,9 @@ function LaunchpadServiceCard({ def, override = {}, index = 0 }) {
|
|
|
118
118
|
{
|
|
119
119
|
href,
|
|
120
120
|
className: (0, import_cn.cn)(
|
|
121
|
-
"group flex flex-1 flex-col rounded-3xl border
|
|
122
|
-
"
|
|
123
|
-
slice.
|
|
121
|
+
"group flex flex-1 flex-col rounded-3xl border bg-card p-6 sm:aspect-[4/5]",
|
|
122
|
+
"ml-grad-hover-border",
|
|
123
|
+
slice.border
|
|
124
124
|
),
|
|
125
125
|
children: body
|
|
126
126
|
}
|
|
@@ -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 { 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 /** Live per-user fact shown as a quiet chip on the card (e.g. \"3 collections\").\n * Apps pass real counts for the signed-in creator; omit when zero/signed out. */\n meta?: string;\n}\n\nexport type ServiceOverrides = Record<string, ServiceOverride>;\n\n// ── Group accents — the grid's only color voice ─────────────────────────────\n\n/** Each group owns one hue of the Medialane spectrum (blue → indigo → purple →\n * rose → orange in group order), applied flat: icon-tile tint + glyph, the\n * use-case markers, and the desktop hover border. No gradients, no labels. */\ninterface GroupSlice {\n /** soft icon-tile background */\n tint: string;\n /** icon glyph + use-case marker tint */\n text: string;\n /** card border tint on desktop hover (polish only) */\n hoverBorder: string;\n}\n\nconst DEFAULT_SLICE: GroupSlice = {\n tint: \"bg-muted/60\",\n text: \"text-muted-foreground/60\",\n hoverBorder: \"sm:hover:border-border\",\n};\n\nexport const GROUP_SLICES: Record<ServiceGroup, GroupSlice> = {\n \"nfts\": { tint: \"bg-brand-blue/10\", text: \"text-brand-blue\", hoverBorder: \"sm:hover:border-brand-blue/50\" },\n \"limited-editions\": { tint: \"bg-brand-indigo/10\", text: \"text-brand-indigo\", hoverBorder: \"sm:hover:border-brand-indigo/50\" },\n \"coins\": { tint: \"bg-brand-purple/10\", text: \"text-brand-purple\", hoverBorder: \"sm:hover:border-brand-purple/50\" },\n \"community\": { tint: \"bg-brand-rose/10\", text: \"text-brand-rose\", hoverBorder: \"sm:hover:border-brand-rose/50\" },\n \"claims\": { tint: \"bg-brand-orange/10\", text: \"text-brand-orange\", hoverBorder: \"sm:hover:border-brand-orange/50\" },\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\n/** Brand tokens only ([[brand-tokens-only]]) — one hue per service, drawn from\n * the brand spectrum. */\nexport const SERVICE_HUES: Record<string, ServiceHue> = {\n \"nfts\": { text: \"text-brand-blue\", solid: \"bg-brand-blue\", border: \"border-brand-blue/30\", tint: \"bg-brand-blue/10\" },\n \"limited-editions\": { text: \"text-brand-indigo\", solid: \"bg-brand-indigo\", border: \"border-brand-indigo/30\", tint: \"bg-brand-indigo/10\" },\n \"creator-coins\": { text: \"text-brand-price\", solid: \"bg-brand-price\", border: \"border-brand-price/30\", tint: \"bg-brand-price/10\" },\n \"claim-memecoin\": { text: \"text-brand-orange\", solid: \"bg-brand-orange\", border: \"border-brand-orange/30\", tint: \"bg-brand-orange/10\" },\n \"collection-drop\": { text: \"text-brand-purple\", solid: \"bg-brand-purple\", border: \"border-brand-purple/30\", tint: \"bg-brand-purple/10\" },\n \"pop-protocol\": { text: \"text-brand-rose\", solid: \"bg-brand-rose\", border: \"border-brand-rose/30\", tint: \"bg-brand-rose/10\" },\n \"ip-tickets\": { text: \"text-brand-orange\", solid: \"bg-brand-orange\", border: \"border-brand-orange/30\", tint: \"bg-brand-orange/10\" },\n \"remix-asset\": { text: \"text-brand-indigo\", solid: \"bg-brand-indigo\", border: \"border-brand-indigo/30\", tint: \"bg-brand-indigo/10\" },\n \"claim-username\": { text: \"text-brand-purple\", solid: \"bg-brand-purple\", border: \"border-brand-purple/30\", tint: \"bg-brand-purple/10\" },\n \"claim-collection\": { text: \"text-brand-blue\", solid: \"bg-brand-blue\", border: \"border-brand-blue/30\", tint: \"bg-brand-blue/10\" },\n \"claim-collection-name\": { text: \"text-brand-rose\", solid: \"bg-brand-rose\", border: \"border-brand-rose/30\", tint: \"bg-brand-rose/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 tile + live per-user fact */}\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\", live ? slice.tint : \"bg-muted/40\")}>\n <Icon className={cn(\"h-7 w-7\", live ? slice.text : \"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 ) : override.meta ? (\n <span className=\"text-xs font-semibold tabular-nums px-2.5 py-1 rounded-full bg-muted/50 text-foreground/80\">\n {override.meta}\n </span>\n ) : null}\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.text)} />\n {feature}\n </li>\n ))}\n </ul>\n )}\n\n {/* The action — quiet verb, no pill */}\n {live && (\n <div className=\"pt-5\">\n <span className=\"inline-flex items-center gap-1.5 text-sm font-semibold\">\n {cta}\n <ArrowRight className=\"h-4 w-4 transition-transform duration-200 sm:group-hover:translate-x-0.5 motion-reduce:transform-none\" />\n </span>\n </div>\n )}\n </>\n );\n\n return (\n <motion.div\n layout={!reduceMotion}\n initial={{ opacity: 0, y: reduceMotion ? 0 : 16 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0 }}\n whileTap={live ? { scale: 0.97 } : undefined}\n transition={{ duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] }}\n className=\"flex\"\n >\n {live && href ? (\n <Link\n href={href}\n className={cn(\n \"group flex flex-1 flex-col rounded-3xl border border-border/60 bg-card p-6 sm:aspect-[4/5]\",\n \"transition-colors duration-200\",\n slice.hoverBorder,\n )}\n >\n {body}\n </Link>\n ) : (\n <div className=\"flex flex-1 flex-col rounded-3xl border border-border/30 bg-card p-6 sm:aspect-[4/5] opacity-75\">\n {body}\n </div>\n )}\n </motion.div>\n );\n}\n\n// ── Coming-soon strip ────────────────────────────────────────────────────────\n\nfunction ComingSoonStrip({ group, defs }: { group: ServiceGroupDefinition; defs: ServiceDefinition[] }) {\n return (\n <div className=\"rounded-2xl border border-border/40 p-5\">\n <p className=\"font-semibold text-sm\">{group.title}</p>\n <p className=\"text-sm text-muted-foreground mt-0.5\">{group.tagline}</p>\n <div className=\"flex flex-wrap gap-2 mt-4\">\n {defs.map(({ key, icon: Icon, title }) => (\n <div key={key} className=\"flex items-center gap-2 px-3 py-2 rounded-full bg-muted/30 border border-border/25\">\n <Icon className=\"h-3.5 w-3.5 text-muted-foreground/60\" />\n <span className=\"text-xs font-medium text-muted-foreground\">{title}</span>\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n// ── Filter state ─────────────────────────────────────────────────────────────\n\n/** Shared search predicate — group titles count as search terms so a query\n * like \"editions\" or \"community\" finds the whole group. */\nexport function serviceMatchesQuery(def: ServiceDefinition, query: string): boolean {\n if (query.trim() === \"\") return true;\n const haystack = `${def.title} ${def.blurb} ${def.subtitle} ${GROUP_TITLE_BY_KEY[def.group] ?? \"\"}`.toLowerCase();\n return haystack.includes(query.trim().toLowerCase());\n}\n\n/** Sync filter state into the URL (?q=…&groups=…) via replaceState — the\n * filtered launchpad becomes shareable and survives reload, without touching\n * the router (this package is router-agnostic). */\nfunction syncFilterUrl(query: string, groups: Set<ServiceGroup>) {\n if (typeof window === \"undefined\") return;\n const params = new URLSearchParams(window.location.search);\n if (query.trim()) params.set(\"q\", query.trim());\n else params.delete(\"q\");\n if (groups.size > 0) params.set(\"groups\", [...groups].join(\",\"));\n else params.delete(\"groups\");\n const qs = params.toString();\n window.history.replaceState(null, \"\", qs ? `${window.location.pathname}?${qs}` : window.location.pathname);\n}\n\n/**\n * Owns the search + group-filter state for the launchpad page. A page renders\n * `LaunchpadFilterBar` wherever it wants (e.g. right under the h1) wired to\n * this hook's fields, then passes `query`/`activeGroups` straight through to\n * `LaunchpadGroupedSections` so the grid reacts to the same state.\n * State is mirrored into the URL (?q=…&groups=…) so a filtered view can be\n * shared and survives reload.\n */\nexport function useLaunchpadFilter() {\n const [query, setQueryState] = useState(\"\");\n const [activeGroups, setActiveGroups] = useState<Set<ServiceGroup>>(new Set());\n\n // Hydrate once from the URL (client only).\n useEffect(() => {\n const params = new URLSearchParams(window.location.search);\n const q = params.get(\"q\");\n if (q) setQueryState(q);\n const g = params.get(\"groups\");\n if (g) {\n const groups = g.split(\",\").filter((k): k is ServiceGroup => GROUP_KEYS.has(k as ServiceGroup));\n if (groups.length > 0) setActiveGroups(new Set(groups));\n }\n }, []);\n\n const filterableGroups = LAUNCHPAD_SERVICE_GROUPS.filter((g) => g.key !== \"coming-soon\");\n\n const setQuery = (value: string) => {\n setQueryState(value);\n syncFilterUrl(value, activeGroups);\n };\n\n const toggleGroup = (key: ServiceGroup) => {\n setActiveGroups((prev) => {\n const next = new Set(prev);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n syncFilterUrl(query, next);\n return next;\n });\n };\n\n const matches = (def: ServiceDefinition): boolean => {\n if (activeGroups.size > 0 && !activeGroups.has(def.group)) return false;\n if (def.status !== \"live\") return false;\n return serviceMatchesQuery(def, query);\n };\n\n const totalMatches = useMemo(\n () => LAUNCHPAD_SERVICE_DEFINITIONS.filter(matches).length,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [query, activeGroups],\n );\n\n const clear = () => {\n setQueryState(\"\");\n setActiveGroups(new Set());\n syncFilterUrl(\"\", new Set());\n };\n\n return { query, setQuery, activeGroups, toggleGroup, filterableGroups, totalMatches, clear };\n}\n\n// ── The grid ─────────────────────────────────────────────────────────────────\n\nexport interface LaunchpadGroupedSectionsProps {\n /** Per-app hrefs / rollout flips, keyed by service key. */\n overrides: ServiceOverrides;\n /** Current search query — from `useLaunchpadFilter()`. */\n query: string;\n /** Current active group filters — from `useLaunchpadFilter()`. */\n activeGroups: Set<ServiceGroup>;\n /** Reset both — from `useLaunchpadFilter()`. */\n onClearFilters: () => void;\n className?: string;\n}\n\n/** The full launchpad services grid — one dynamic grid of all live services,\n * ordered by group, plus the coming-soon strip. Fully controlled by\n * `useLaunchpadFilter()` state passed in from the page. */\nexport function LaunchpadGroupedSections({ overrides, query, activeGroups, onClearFilters, className }: LaunchpadGroupedSectionsProps) {\n const inActiveGroup = (d: ServiceDefinition) => activeGroups.size === 0 || activeGroups.has(d.group);\n const inSearch = (d: ServiceDefinition) => serviceMatchesQuery(d, query);\n\n const groupOrder = Object.fromEntries(LAUNCHPAD_SERVICE_GROUPS.map((g, i) => [g.key, i]));\n\n const liveDefs = LAUNCHPAD_SERVICE_DEFINITIONS\n .filter((d) => d.group !== \"coming-soon\" && d.status === \"live\" && inActiveGroup(d) && inSearch(d))\n .sort((a, b) => (groupOrder[a.group] ?? 99) - (groupOrder[b.group] ?? 99));\n\n const comingSoonGroup = LAUNCHPAD_SERVICE_GROUPS.find((g) => g.key === \"coming-soon\");\n const comingSoonDefs = LAUNCHPAD_SERVICE_DEFINITIONS.filter(\n (d) => d.group === \"coming-soon\" && inActiveGroup(d) && inSearch(d),\n );\n\n return (\n <div className={cn(\"space-y-8\", className)}>\n {liveDefs.length === 0 && comingSoonDefs.length === 0 ? (\n <div className=\"text-center py-16 space-y-3\">\n <p className=\"text-lg font-semibold\">No services match</p>\n <p className=\"text-sm text-muted-foreground\">Try a different search or clear your filters.</p>\n <button\n type=\"button\"\n onClick={onClearFilters}\n className=\"inline-flex items-center h-9 px-4 rounded-full text-sm font-semibold bg-primary text-primary-foreground\"\n >\n Clear filters\n </button>\n </div>\n ) : (\n <>\n <motion.div layout className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5 sm:gap-6\">\n <AnimatePresence mode=\"popLayout\">\n {liveDefs.map((def, i) => (\n <LaunchpadServiceCard key={def.key} def={def} override={overrides[def.key]} index={i} />\n ))}\n </AnimatePresence>\n </motion.div>\n {comingSoonGroup && comingSoonDefs.length > 0 && (\n <ComingSoonStrip group={comingSoonGroup} defs={comingSoonDefs} />\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Link"]}
|
|
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 /** Live per-user fact shown as a quiet chip on the card (e.g. \"3 collections\").\n * Apps pass real counts for the signed-in creator; omit when zero/signed out. */\n meta?: string;\n}\n\nexport type ServiceOverrides = Record<string, ServiceOverride>;\n\n// ── Group accents — the grid's only color voice ─────────────────────────────\n\n/** Each group owns one hue of the Medialane spectrum (blue → indigo → purple →\n * rose → orange in group order), applied flat: icon-tile tint + glyph, the\n * use-case markers, and the card's resting hairline. On hover-capable\n * devices the full-spectrum gradient ring fades in over the hairline\n * (.ml-grad-hover-border — polish only, never carries information). */\ninterface GroupSlice {\n /** soft icon-tile background */\n tint: string;\n /** icon glyph + use-case marker tint */\n text: string;\n /** resting card hairline in the group's hue */\n border: string;\n}\n\nconst DEFAULT_SLICE: GroupSlice = {\n tint: \"bg-muted/60\",\n text: \"text-muted-foreground/60\",\n border: \"border-border/60\",\n};\n\nexport const GROUP_SLICES: Record<ServiceGroup, GroupSlice> = {\n \"nfts\": { tint: \"bg-brand-blue/10\", text: \"text-brand-blue\", border: \"border-brand-blue/30\" },\n \"limited-editions\": { tint: \"bg-brand-indigo/10\", text: \"text-brand-indigo\", border: \"border-brand-indigo/30\" },\n \"coins\": { tint: \"bg-brand-purple/10\", text: \"text-brand-purple\", border: \"border-brand-purple/30\" },\n \"community\": { tint: \"bg-brand-rose/10\", text: \"text-brand-rose\", border: \"border-brand-rose/30\" },\n \"claims\": { tint: \"bg-brand-orange/10\", text: \"text-brand-orange\", border: \"border-brand-orange/30\" },\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\n/** Brand tokens only ([[brand-tokens-only]]) — one hue per service, drawn from\n * the brand spectrum. */\nexport const SERVICE_HUES: Record<string, ServiceHue> = {\n \"nfts\": { text: \"text-brand-blue\", solid: \"bg-brand-blue\", border: \"border-brand-blue/30\", tint: \"bg-brand-blue/10\" },\n \"limited-editions\": { text: \"text-brand-indigo\", solid: \"bg-brand-indigo\", border: \"border-brand-indigo/30\", tint: \"bg-brand-indigo/10\" },\n \"creator-coins\": { text: \"text-brand-price\", solid: \"bg-brand-price\", border: \"border-brand-price/30\", tint: \"bg-brand-price/10\" },\n \"claim-memecoin\": { text: \"text-brand-orange\", solid: \"bg-brand-orange\", border: \"border-brand-orange/30\", tint: \"bg-brand-orange/10\" },\n \"collection-drop\": { text: \"text-brand-purple\", solid: \"bg-brand-purple\", border: \"border-brand-purple/30\", tint: \"bg-brand-purple/10\" },\n \"pop-protocol\": { text: \"text-brand-rose\", solid: \"bg-brand-rose\", border: \"border-brand-rose/30\", tint: \"bg-brand-rose/10\" },\n \"ip-tickets\": { text: \"text-brand-orange\", solid: \"bg-brand-orange\", border: \"border-brand-orange/30\", tint: \"bg-brand-orange/10\" },\n \"remix-asset\": { text: \"text-brand-indigo\", solid: \"bg-brand-indigo\", border: \"border-brand-indigo/30\", tint: \"bg-brand-indigo/10\" },\n \"claim-username\": { text: \"text-brand-purple\", solid: \"bg-brand-purple\", border: \"border-brand-purple/30\", tint: \"bg-brand-purple/10\" },\n \"claim-collection\": { text: \"text-brand-blue\", solid: \"bg-brand-blue\", border: \"border-brand-blue/30\", tint: \"bg-brand-blue/10\" },\n \"claim-collection-name\": { text: \"text-brand-rose\", solid: \"bg-brand-rose\", border: \"border-brand-rose/30\", tint: \"bg-brand-rose/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 tile + live per-user fact */}\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\", live ? slice.tint : \"bg-muted/40\")}>\n <Icon className={cn(\"h-7 w-7\", live ? slice.text : \"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 ) : override.meta ? (\n <span className=\"text-xs font-semibold tabular-nums px-2.5 py-1 rounded-full bg-muted/50 text-foreground/80\">\n {override.meta}\n </span>\n ) : null}\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.text)} />\n {feature}\n </li>\n ))}\n </ul>\n )}\n\n {/* The action — quiet verb, no pill */}\n {live && (\n <div className=\"pt-5\">\n <span className=\"inline-flex items-center gap-1.5 text-sm font-semibold\">\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 bg-card p-6 sm:aspect-[4/5]\",\n \"ml-grad-hover-border\",\n slice.border,\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuII;AAlHJ,mBAA6C;AAC7C,kBAAiB;AACjB,2BAA0D;AAC1D,0BAAwC;AACxC,gBAAmB;AACnB,gCAOO;AAoCP,MAAM,gBAA4B;AAAA,EAChC,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AACV;AAEO,MAAM,eAAiD;AAAA,EAC5D,QAAQ,EAAE,MAAM,oBAAoB,MAAM,mBAAmB,QAAQ,uBAAuB;AAAA,EAC5F,oBAAoB,EAAE,MAAM,sBAAsB,MAAM,qBAAqB,QAAQ,yBAAyB;AAAA,EAC9G,SAAS,EAAE,MAAM,sBAAsB,MAAM,qBAAqB,QAAQ,yBAAyB;AAAA,EACnG,aAAa,EAAE,MAAM,oBAAoB,MAAM,mBAAmB,QAAQ,uBAAuB;AAAA,EACjG,UAAU,EAAE,MAAM,sBAAsB,MAAM,qBAAqB,QAAQ,yBAAyB;AAAA,EACpG,eAAe;AACjB;AAaO,MAAM,eAA2C;AAAA,EACtD,QAAQ,EAAE,MAAM,mBAAmB,OAAO,iBAAiB,QAAQ,wBAAwB,MAAM,mBAAmB;AAAA,EACpH,oBAAoB,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACxI,iBAAiB,EAAE,MAAM,oBAAoB,OAAO,kBAAkB,QAAQ,yBAAyB,MAAM,oBAAoB;AAAA,EACjI,kBAAkB,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACtI,mBAAmB,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACvI,gBAAgB,EAAE,MAAM,mBAAmB,OAAO,iBAAiB,QAAQ,wBAAwB,MAAM,mBAAmB;AAAA,EAC5H,cAAc,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EAClI,eAAe,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACnI,kBAAkB,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACtI,oBAAoB,EAAE,MAAM,mBAAmB,OAAO,iBAAiB,QAAQ,wBAAwB,MAAM,mBAAmB;AAAA,EAChI,yBAAyB,EAAE,MAAM,mBAAmB,OAAO,iBAAiB,QAAQ,wBAAwB,MAAM,mBAAmB;AACvI;AAEA,MAAM,qBAAqB,OAAO;AAAA,EAChC,mDAAyB,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC;AACtD;AAEA,MAAM,aAAa,IAAI,IAAI,mDAAyB,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAW9D,SAAS,qBAAqB,EAAE,KAAK,WAAW,CAAC,GAAG,QAAQ,EAAE,GAA8B;AACjG,QAAM,EAAE,MAAM,MAAM,OAAO,OAAO,UAAU,IAAI,IAAI;AACpD,QAAM,SAAS,SAAS,UAAU,IAAI;AACtC,QAAM,QAAQ,SAAS,SAAS,IAAI;AACpC,QAAM,EAAE,KAAK,IAAI;AACjB,QAAM,mBAAe,uCAAiB;AAEtC,QAAM,OAAO,WAAW;AACxB,QAAM,QAAQ,aAAa,KAAK,KAAK;AAErC,QAAM,OACJ,4EAEE;AAAA,iDAAC,SAAI,WAAU,0CACb;AAAA,kDAAC,UAAK,eAAW,cAAG,0DAA0D,OAAO,MAAM,OAAO,aAAa,GAC7G,sDAAC,QAAK,eAAW,cAAG,WAAW,OAAO,MAAM,OAAO,0BAA0B,GAAG,GAClF;AAAA,MACC,CAAC,OACA,6CAAC,UAAK,WAAU,iFACd;AAAA,oDAAC,4BAAK,WAAU,WAAU;AAAA,QAAE;AAAA,SAE9B,IACE,SAAS,OACX,4CAAC,UAAK,WAAU,8FACb,mBAAS,MACZ,IACE;AAAA,OACN;AAAA,IAGA,6CAAC,SAAI,WAAU,0BACb;AAAA,kDAAC,QAAG,WAAU,mDAAmD,iBAAM;AAAA,MACvE,4CAAC,OAAE,eAAW,cAAG,2BAA2B,OAAO,0BAA0B,0BAA0B,GACpG,iBACH;AAAA,OACF;AAAA,IAGC,QAAQ,SAAS,SAAS,KACzB,4CAAC,QAAG,WAAU,oBACX,mBAAS,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,YACzB,6CAAC,QAAiB,WAAU,wEAC1B;AAAA,kDAAC,6BAAM,eAAW,cAAG,8BAA8B,MAAM,IAAI,GAAG;AAAA,MAC/D;AAAA,SAFM,OAGT,CACD,GACH;AAAA,IAID,QACC,4CAAC,SAAI,WAAU,QACb,uDAAC,UAAK,WAAU,0DACb;AAAA;AAAA,MACD,4CAAC,kCAAW,WAAU,yGAAwG;AAAA,OAChI,GACF;AAAA,KAEJ;AAGF,SACE;AAAA,IAAC,4BAAO;AAAA,IAAP;AAAA,MACC,QAAQ,CAAC;AAAA,MACT,SAAS,EAAE,SAAS,GAAG,GAAG,eAAe,IAAI,GAAG;AAAA,MAChD,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAC5B,MAAM,EAAE,SAAS,EAAE;AAAA,MACnB,UAAU,OAAO,EAAE,OAAO,KAAK,IAAI;AAAA,MACnC,YAAY,EAAE,UAAU,MAAM,OAAO,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM,MAAM,IAAI,EAAE;AAAA,MAClF,WAAU;AAAA,MAET,kBAAQ,OACP;AAAA,QAAC,YAAAA;AAAA,QAAA;AAAA,UACC;AAAA,UACA,eAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,MAAM;AAAA,UACR;AAAA,UAEC;AAAA;AAAA,MACH,IAEA,4CAAC,SAAI,WAAU,mGACZ,gBACH;AAAA;AAAA,EAEJ;AAEJ;AAIA,SAAS,gBAAgB,EAAE,OAAO,KAAK,GAAiE;AACtG,SACE,6CAAC,SAAI,WAAU,2CACb;AAAA,gDAAC,OAAE,WAAU,yBAAyB,gBAAM,OAAM;AAAA,IAClD,4CAAC,OAAE,WAAU,wCAAwC,gBAAM,SAAQ;AAAA,IACnE,4CAAC,SAAI,WAAU,6BACZ,eAAK,IAAI,CAAC,EAAE,KAAK,MAAM,MAAM,MAAM,MAClC,6CAAC,SAAc,WAAU,sFACvB;AAAA,kDAAC,QAAK,WAAU,wCAAuC;AAAA,MACvD,4CAAC,UAAK,WAAU,6CAA6C,iBAAM;AAAA,SAF3D,GAGV,CACD,GACH;AAAA,KACF;AAEJ;AAMO,SAAS,oBAAoB,KAAwB,OAAwB;AAClF,MAAI,MAAM,KAAK,MAAM,GAAI,QAAO;AAChC,QAAM,WAAW,GAAG,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,QAAQ,IAAI,mBAAmB,IAAI,KAAK,KAAK,EAAE,GAAG,YAAY;AAChH,SAAO,SAAS,SAAS,MAAM,KAAK,EAAE,YAAY,CAAC;AACrD;AAKA,SAAS,cAAc,OAAe,QAA2B;AAC/D,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,MAAI,MAAM,KAAK,EAAG,QAAO,IAAI,KAAK,MAAM,KAAK,CAAC;AAAA,MACzC,QAAO,OAAO,GAAG;AACtB,MAAI,OAAO,OAAO,EAAG,QAAO,IAAI,UAAU,CAAC,GAAG,MAAM,EAAE,KAAK,GAAG,CAAC;AAAA,MAC1D,QAAO,OAAO,QAAQ;AAC3B,QAAM,KAAK,OAAO,SAAS;AAC3B,SAAO,QAAQ,aAAa,MAAM,IAAI,KAAK,GAAG,OAAO,SAAS,QAAQ,IAAI,EAAE,KAAK,OAAO,SAAS,QAAQ;AAC3G;AAUO,SAAS,qBAAqB;AACnC,QAAM,CAAC,OAAO,aAAa,QAAI,uBAAS,EAAE;AAC1C,QAAM,CAAC,cAAc,eAAe,QAAI,uBAA4B,oBAAI,IAAI,CAAC;AAG7E,8BAAU,MAAM;AACd,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,UAAM,IAAI,OAAO,IAAI,GAAG;AACxB,QAAI,EAAG,eAAc,CAAC;AACtB,UAAM,IAAI,OAAO,IAAI,QAAQ;AAC7B,QAAI,GAAG;AACL,YAAM,SAAS,EAAE,MAAM,GAAG,EAAE,OAAO,CAAC,MAAyB,WAAW,IAAI,CAAiB,CAAC;AAC9F,UAAI,OAAO,SAAS,EAAG,iBAAgB,IAAI,IAAI,MAAM,CAAC;AAAA,IACxD;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,mBAAmB,mDAAyB,OAAO,CAAC,MAAM,EAAE,QAAQ,aAAa;AAEvF,QAAM,WAAW,CAAC,UAAkB;AAClC,kBAAc,KAAK;AACnB,kBAAc,OAAO,YAAY;AAAA,EACnC;AAEA,QAAM,cAAc,CAAC,QAAsB;AACzC,oBAAgB,CAAC,SAAS;AACxB,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,UAAI,KAAK,IAAI,GAAG,EAAG,MAAK,OAAO,GAAG;AAAA,UAC7B,MAAK,IAAI,GAAG;AACjB,oBAAc,OAAO,IAAI;AACzB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,QAAM,UAAU,CAAC,QAAoC;AACnD,QAAI,aAAa,OAAO,KAAK,CAAC,aAAa,IAAI,IAAI,KAAK,EAAG,QAAO;AAClE,QAAI,IAAI,WAAW,OAAQ,QAAO;AAClC,WAAO,oBAAoB,KAAK,KAAK;AAAA,EACvC;AAEA,QAAM,mBAAe;AAAA,IACnB,MAAM,wDAA8B,OAAO,OAAO,EAAE;AAAA;AAAA,IAEpD,CAAC,OAAO,YAAY;AAAA,EACtB;AAEA,QAAM,QAAQ,MAAM;AAClB,kBAAc,EAAE;AAChB,oBAAgB,oBAAI,IAAI,CAAC;AACzB,kBAAc,IAAI,oBAAI,IAAI,CAAC;AAAA,EAC7B;AAEA,SAAO,EAAE,OAAO,UAAU,cAAc,aAAa,kBAAkB,cAAc,MAAM;AAC7F;AAmBO,SAAS,yBAAyB,EAAE,WAAW,OAAO,cAAc,gBAAgB,UAAU,GAAkC;AACrI,QAAM,gBAAgB,CAAC,MAAyB,aAAa,SAAS,KAAK,aAAa,IAAI,EAAE,KAAK;AACnG,QAAM,WAAW,CAAC,MAAyB,oBAAoB,GAAG,KAAK;AAEvE,QAAM,aAAa,OAAO,YAAY,mDAAyB,IAAI,CAAC,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAExF,QAAM,WAAW,wDACd,OAAO,CAAC,MAAM,EAAE,UAAU,iBAAiB,EAAE,WAAW,UAAU,cAAc,CAAC,KAAK,SAAS,CAAC,CAAC,EACjG,KAAK,CAAC,GAAG,OAAO,WAAW,EAAE,KAAK,KAAK,OAAO,WAAW,EAAE,KAAK,KAAK,GAAG;AAE3E,QAAM,kBAAkB,mDAAyB,KAAK,CAAC,MAAM,EAAE,QAAQ,aAAa;AACpF,QAAM,iBAAiB,wDAA8B;AAAA,IACnD,CAAC,MAAM,EAAE,UAAU,iBAAiB,cAAc,CAAC,KAAK,SAAS,CAAC;AAAA,EACpE;AAEA,SACE,4CAAC,SAAI,eAAW,cAAG,aAAa,SAAS,GACtC,mBAAS,WAAW,KAAK,eAAe,WAAW,IAClD,6CAAC,SAAI,WAAU,+BACb;AAAA,gDAAC,OAAE,WAAU,yBAAwB,+BAAiB;AAAA,IACtD,4CAAC,OAAE,WAAU,iCAAgC,2DAA6C;AAAA,IAC1F;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QACX;AAAA;AAAA,IAED;AAAA,KACF,IAEA,4EACE;AAAA,gDAAC,4BAAO,KAAP,EAAW,QAAM,MAAC,WAAU,gFAC3B,sDAAC,wCAAgB,MAAK,aACnB,mBAAS,IAAI,CAAC,KAAK,MAClB,4CAAC,wBAAmC,KAAU,UAAU,UAAU,IAAI,GAAG,GAAG,OAAO,KAAxD,IAAI,GAAuD,CACvF,GACH,GACF;AAAA,IACC,mBAAmB,eAAe,SAAS,KAC1C,4CAAC,mBAAgB,OAAO,iBAAiB,MAAM,gBAAgB;AAAA,KAEnE,GAEJ;AAEJ;","names":["Link"]}
|
|
@@ -18,14 +18,16 @@ interface ServiceOverride {
|
|
|
18
18
|
type ServiceOverrides = Record<string, ServiceOverride>;
|
|
19
19
|
/** Each group owns one hue of the Medialane spectrum (blue → indigo → purple →
|
|
20
20
|
* rose → orange in group order), applied flat: icon-tile tint + glyph, the
|
|
21
|
-
* use-case markers, and the
|
|
21
|
+
* use-case markers, and the card's resting hairline. On hover-capable
|
|
22
|
+
* devices the full-spectrum gradient ring fades in over the hairline
|
|
23
|
+
* (.ml-grad-hover-border — polish only, never carries information). */
|
|
22
24
|
interface GroupSlice {
|
|
23
25
|
/** soft icon-tile background */
|
|
24
26
|
tint: string;
|
|
25
27
|
/** icon glyph + use-case marker tint */
|
|
26
28
|
text: string;
|
|
27
|
-
/** card
|
|
28
|
-
|
|
29
|
+
/** resting card hairline in the group's hue */
|
|
30
|
+
border: string;
|
|
29
31
|
}
|
|
30
32
|
declare const GROUP_SLICES: Record<ServiceGroup, GroupSlice>;
|
|
31
33
|
/** Legacy per-service hue table — kept for LaunchpadStrip (homepage). The grid
|
|
@@ -18,14 +18,16 @@ interface ServiceOverride {
|
|
|
18
18
|
type ServiceOverrides = Record<string, ServiceOverride>;
|
|
19
19
|
/** Each group owns one hue of the Medialane spectrum (blue → indigo → purple →
|
|
20
20
|
* rose → orange in group order), applied flat: icon-tile tint + glyph, the
|
|
21
|
-
* use-case markers, and the
|
|
21
|
+
* use-case markers, and the card's resting hairline. On hover-capable
|
|
22
|
+
* devices the full-spectrum gradient ring fades in over the hairline
|
|
23
|
+
* (.ml-grad-hover-border — polish only, never carries information). */
|
|
22
24
|
interface GroupSlice {
|
|
23
25
|
/** soft icon-tile background */
|
|
24
26
|
tint: string;
|
|
25
27
|
/** icon glyph + use-case marker tint */
|
|
26
28
|
text: string;
|
|
27
|
-
/** card
|
|
28
|
-
|
|
29
|
+
/** resting card hairline in the group's hue */
|
|
30
|
+
border: string;
|
|
29
31
|
}
|
|
30
32
|
declare const GROUP_SLICES: Record<ServiceGroup, GroupSlice>;
|
|
31
33
|
/** Legacy per-service hue table — kept for LaunchpadStrip (homepage). The grid
|
|
@@ -12,14 +12,14 @@ import {
|
|
|
12
12
|
const DEFAULT_SLICE = {
|
|
13
13
|
tint: "bg-muted/60",
|
|
14
14
|
text: "text-muted-foreground/60",
|
|
15
|
-
|
|
15
|
+
border: "border-border/60"
|
|
16
16
|
};
|
|
17
17
|
const GROUP_SLICES = {
|
|
18
|
-
"nfts": { tint: "bg-brand-blue/10", text: "text-brand-blue",
|
|
19
|
-
"limited-editions": { tint: "bg-brand-indigo/10", text: "text-brand-indigo",
|
|
20
|
-
"coins": { tint: "bg-brand-purple/10", text: "text-brand-purple",
|
|
21
|
-
"community": { tint: "bg-brand-rose/10", text: "text-brand-rose",
|
|
22
|
-
"claims": { tint: "bg-brand-orange/10", text: "text-brand-orange",
|
|
18
|
+
"nfts": { tint: "bg-brand-blue/10", text: "text-brand-blue", border: "border-brand-blue/30" },
|
|
19
|
+
"limited-editions": { tint: "bg-brand-indigo/10", text: "text-brand-indigo", border: "border-brand-indigo/30" },
|
|
20
|
+
"coins": { tint: "bg-brand-purple/10", text: "text-brand-purple", border: "border-brand-purple/30" },
|
|
21
|
+
"community": { tint: "bg-brand-rose/10", text: "text-brand-rose", border: "border-brand-rose/30" },
|
|
22
|
+
"claims": { tint: "bg-brand-orange/10", text: "text-brand-orange", border: "border-brand-orange/30" },
|
|
23
23
|
"coming-soon": DEFAULT_SLICE
|
|
24
24
|
};
|
|
25
25
|
const SERVICE_HUES = {
|
|
@@ -83,9 +83,9 @@ function LaunchpadServiceCard({ def, override = {}, index = 0 }) {
|
|
|
83
83
|
{
|
|
84
84
|
href,
|
|
85
85
|
className: cn(
|
|
86
|
-
"group flex flex-1 flex-col rounded-3xl border
|
|
87
|
-
"
|
|
88
|
-
slice.
|
|
86
|
+
"group flex flex-1 flex-col rounded-3xl border bg-card p-6 sm:aspect-[4/5]",
|
|
87
|
+
"ml-grad-hover-border",
|
|
88
|
+
slice.border
|
|
89
89
|
),
|
|
90
90
|
children: body
|
|
91
91
|
}
|
|
@@ -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 { 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 /** Live per-user fact shown as a quiet chip on the card (e.g. \"3 collections\").\n * Apps pass real counts for the signed-in creator; omit when zero/signed out. */\n meta?: string;\n}\n\nexport type ServiceOverrides = Record<string, ServiceOverride>;\n\n// ── Group accents — the grid's only color voice ─────────────────────────────\n\n/** Each group owns one hue of the Medialane spectrum (blue → indigo → purple →\n * rose → orange in group order), applied flat: icon-tile tint + glyph, the\n * use-case markers, and the desktop hover border. No gradients, no labels. */\ninterface GroupSlice {\n /** soft icon-tile background */\n tint: string;\n /** icon glyph + use-case marker tint */\n text: string;\n /** card border tint on desktop hover (polish only) */\n hoverBorder: string;\n}\n\nconst DEFAULT_SLICE: GroupSlice = {\n tint: \"bg-muted/60\",\n text: \"text-muted-foreground/60\",\n hoverBorder: \"sm:hover:border-border\",\n};\n\nexport const GROUP_SLICES: Record<ServiceGroup, GroupSlice> = {\n \"nfts\": { tint: \"bg-brand-blue/10\", text: \"text-brand-blue\", hoverBorder: \"sm:hover:border-brand-blue/50\" },\n \"limited-editions\": { tint: \"bg-brand-indigo/10\", text: \"text-brand-indigo\", hoverBorder: \"sm:hover:border-brand-indigo/50\" },\n \"coins\": { tint: \"bg-brand-purple/10\", text: \"text-brand-purple\", hoverBorder: \"sm:hover:border-brand-purple/50\" },\n \"community\": { tint: \"bg-brand-rose/10\", text: \"text-brand-rose\", hoverBorder: \"sm:hover:border-brand-rose/50\" },\n \"claims\": { tint: \"bg-brand-orange/10\", text: \"text-brand-orange\", hoverBorder: \"sm:hover:border-brand-orange/50\" },\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\n/** Brand tokens only ([[brand-tokens-only]]) — one hue per service, drawn from\n * the brand spectrum. */\nexport const SERVICE_HUES: Record<string, ServiceHue> = {\n \"nfts\": { text: \"text-brand-blue\", solid: \"bg-brand-blue\", border: \"border-brand-blue/30\", tint: \"bg-brand-blue/10\" },\n \"limited-editions\": { text: \"text-brand-indigo\", solid: \"bg-brand-indigo\", border: \"border-brand-indigo/30\", tint: \"bg-brand-indigo/10\" },\n \"creator-coins\": { text: \"text-brand-price\", solid: \"bg-brand-price\", border: \"border-brand-price/30\", tint: \"bg-brand-price/10\" },\n \"claim-memecoin\": { text: \"text-brand-orange\", solid: \"bg-brand-orange\", border: \"border-brand-orange/30\", tint: \"bg-brand-orange/10\" },\n \"collection-drop\": { text: \"text-brand-purple\", solid: \"bg-brand-purple\", border: \"border-brand-purple/30\", tint: \"bg-brand-purple/10\" },\n \"pop-protocol\": { text: \"text-brand-rose\", solid: \"bg-brand-rose\", border: \"border-brand-rose/30\", tint: \"bg-brand-rose/10\" },\n \"ip-tickets\": { text: \"text-brand-orange\", solid: \"bg-brand-orange\", border: \"border-brand-orange/30\", tint: \"bg-brand-orange/10\" },\n \"remix-asset\": { text: \"text-brand-indigo\", solid: \"bg-brand-indigo\", border: \"border-brand-indigo/30\", tint: \"bg-brand-indigo/10\" },\n \"claim-username\": { text: \"text-brand-purple\", solid: \"bg-brand-purple\", border: \"border-brand-purple/30\", tint: \"bg-brand-purple/10\" },\n \"claim-collection\": { text: \"text-brand-blue\", solid: \"bg-brand-blue\", border: \"border-brand-blue/30\", tint: \"bg-brand-blue/10\" },\n \"claim-collection-name\": { text: \"text-brand-rose\", solid: \"bg-brand-rose\", border: \"border-brand-rose/30\", tint: \"bg-brand-rose/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 tile + live per-user fact */}\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\", live ? slice.tint : \"bg-muted/40\")}>\n <Icon className={cn(\"h-7 w-7\", live ? slice.text : \"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 ) : override.meta ? (\n <span className=\"text-xs font-semibold tabular-nums px-2.5 py-1 rounded-full bg-muted/50 text-foreground/80\">\n {override.meta}\n </span>\n ) : null}\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.text)} />\n {feature}\n </li>\n ))}\n </ul>\n )}\n\n {/* The action — quiet verb, no pill */}\n {live && (\n <div className=\"pt-5\">\n <span className=\"inline-flex items-center gap-1.5 text-sm font-semibold\">\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":";AAqII,mBAIM,KAGA,YAPN;AAhHJ,SAAS,WAAW,SAAS,gBAAgB;AAC7C,OAAO,UAAU;AACjB,SAAS,iBAAiB,QAAQ,wBAAwB;AAC1D,SAAS,MAAM,YAAY,aAAa;AACxC,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AAkCP,MAAM,gBAA4B;AAAA,EAChC,MAAM;AAAA,EACN,MAAM;AAAA,EACN,aAAa;AACf;AAEO,MAAM,eAAiD;AAAA,EAC5D,QAAQ,EAAE,MAAM,oBAAoB,MAAM,mBAAmB,aAAa,gCAAgC;AAAA,EAC1G,oBAAoB,EAAE,MAAM,sBAAsB,MAAM,qBAAqB,aAAa,kCAAkC;AAAA,EAC5H,SAAS,EAAE,MAAM,sBAAsB,MAAM,qBAAqB,aAAa,kCAAkC;AAAA,EACjH,aAAa,EAAE,MAAM,oBAAoB,MAAM,mBAAmB,aAAa,gCAAgC;AAAA,EAC/G,UAAU,EAAE,MAAM,sBAAsB,MAAM,qBAAqB,aAAa,kCAAkC;AAAA,EAClH,eAAe;AACjB;AAaO,MAAM,eAA2C;AAAA,EACtD,QAAQ,EAAE,MAAM,mBAAmB,OAAO,iBAAiB,QAAQ,wBAAwB,MAAM,mBAAmB;AAAA,EACpH,oBAAoB,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACxI,iBAAiB,EAAE,MAAM,oBAAoB,OAAO,kBAAkB,QAAQ,yBAAyB,MAAM,oBAAoB;AAAA,EACjI,kBAAkB,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACtI,mBAAmB,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACvI,gBAAgB,EAAE,MAAM,mBAAmB,OAAO,iBAAiB,QAAQ,wBAAwB,MAAM,mBAAmB;AAAA,EAC5H,cAAc,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EAClI,eAAe,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACnI,kBAAkB,EAAE,MAAM,qBAAqB,OAAO,mBAAmB,QAAQ,0BAA0B,MAAM,qBAAqB;AAAA,EACtI,oBAAoB,EAAE,MAAM,mBAAmB,OAAO,iBAAiB,QAAQ,wBAAwB,MAAM,mBAAmB;AAAA,EAChI,yBAAyB,EAAE,MAAM,mBAAmB,OAAO,iBAAiB,QAAQ,wBAAwB,MAAM,mBAAmB;AACvI;AAEA,MAAM,qBAAqB,OAAO;AAAA,EAChC,yBAAyB,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC;AACtD;AAEA,MAAM,aAAa,IAAI,IAAI,yBAAyB,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AAW9D,SAAS,qBAAqB,EAAE,KAAK,WAAW,CAAC,GAAG,QAAQ,EAAE,GAA8B;AACjG,QAAM,EAAE,MAAM,MAAM,OAAO,OAAO,UAAU,IAAI,IAAI;AACpD,QAAM,SAAS,SAAS,UAAU,IAAI;AACtC,QAAM,QAAQ,SAAS,SAAS,IAAI;AACpC,QAAM,EAAE,KAAK,IAAI;AACjB,QAAM,eAAe,iBAAiB;AAEtC,QAAM,OAAO,WAAW;AACxB,QAAM,QAAQ,aAAa,KAAK,KAAK;AAErC,QAAM,OACJ,iCAEE;AAAA,yBAAC,SAAI,WAAU,0CACb;AAAA,0BAAC,UAAK,WAAW,GAAG,0DAA0D,OAAO,MAAM,OAAO,aAAa,GAC7G,8BAAC,QAAK,WAAW,GAAG,WAAW,OAAO,MAAM,OAAO,0BAA0B,GAAG,GAClF;AAAA,MACC,CAAC,OACA,qBAAC,UAAK,WAAU,iFACd;AAAA,4BAAC,QAAK,WAAU,WAAU;AAAA,QAAE;AAAA,SAE9B,IACE,SAAS,OACX,oBAAC,UAAK,WAAU,8FACb,mBAAS,MACZ,IACE;AAAA,OACN;AAAA,IAGA,qBAAC,SAAI,WAAU,0BACb;AAAA,0BAAC,QAAG,WAAU,mDAAmD,iBAAM;AAAA,MACvE,oBAAC,OAAE,WAAW,GAAG,2BAA2B,OAAO,0BAA0B,0BAA0B,GACpG,iBACH;AAAA,OACF;AAAA,IAGC,QAAQ,SAAS,SAAS,KACzB,oBAAC,QAAG,WAAU,oBACX,mBAAS,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,YACzB,qBAAC,QAAiB,WAAU,wEAC1B;AAAA,0BAAC,SAAM,WAAW,GAAG,8BAA8B,MAAM,IAAI,GAAG;AAAA,MAC/D;AAAA,SAFM,OAGT,CACD,GACH;AAAA,IAID,QACC,oBAAC,SAAI,WAAU,QACb,+BAAC,UAAK,WAAU,0DACb;AAAA;AAAA,MACD,oBAAC,cAAW,WAAU,yGAAwG;AAAA,OAChI,GACF;AAAA,KAEJ;AAGF,SACE;AAAA,IAAC,OAAO;AAAA,IAAP;AAAA,MACC,QAAQ,CAAC;AAAA,MACT,SAAS,EAAE,SAAS,GAAG,GAAG,eAAe,IAAI,GAAG;AAAA,MAChD,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAC5B,MAAM,EAAE,SAAS,EAAE;AAAA,MACnB,UAAU,OAAO,EAAE,OAAO,KAAK,IAAI;AAAA,MACnC,YAAY,EAAE,UAAU,MAAM,OAAO,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM,MAAM,IAAI,EAAE;AAAA,MAClF,WAAU;AAAA,MAET,kBAAQ,OACP;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,MAAM;AAAA,UACR;AAAA,UAEC;AAAA;AAAA,MACH,IAEA,oBAAC,SAAI,WAAU,mGACZ,gBACH;AAAA;AAAA,EAEJ;AAEJ;AAIA,SAAS,gBAAgB,EAAE,OAAO,KAAK,GAAiE;AACtG,SACE,qBAAC,SAAI,WAAU,2CACb;AAAA,wBAAC,OAAE,WAAU,yBAAyB,gBAAM,OAAM;AAAA,IAClD,oBAAC,OAAE,WAAU,wCAAwC,gBAAM,SAAQ;AAAA,IACnE,oBAAC,SAAI,WAAU,6BACZ,eAAK,IAAI,CAAC,EAAE,KAAK,MAAM,MAAM,MAAM,MAClC,qBAAC,SAAc,WAAU,sFACvB;AAAA,0BAAC,QAAK,WAAU,wCAAuC;AAAA,MACvD,oBAAC,UAAK,WAAU,6CAA6C,iBAAM;AAAA,SAF3D,GAGV,CACD,GACH;AAAA,KACF;AAEJ;AAMO,SAAS,oBAAoB,KAAwB,OAAwB;AAClF,MAAI,MAAM,KAAK,MAAM,GAAI,QAAO;AAChC,QAAM,WAAW,GAAG,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,QAAQ,IAAI,mBAAmB,IAAI,KAAK,KAAK,EAAE,GAAG,YAAY;AAChH,SAAO,SAAS,SAAS,MAAM,KAAK,EAAE,YAAY,CAAC;AACrD;AAKA,SAAS,cAAc,OAAe,QAA2B;AAC/D,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,MAAI,MAAM,KAAK,EAAG,QAAO,IAAI,KAAK,MAAM,KAAK,CAAC;AAAA,MACzC,QAAO,OAAO,GAAG;AACtB,MAAI,OAAO,OAAO,EAAG,QAAO,IAAI,UAAU,CAAC,GAAG,MAAM,EAAE,KAAK,GAAG,CAAC;AAAA,MAC1D,QAAO,OAAO,QAAQ;AAC3B,QAAM,KAAK,OAAO,SAAS;AAC3B,SAAO,QAAQ,aAAa,MAAM,IAAI,KAAK,GAAG,OAAO,SAAS,QAAQ,IAAI,EAAE,KAAK,OAAO,SAAS,QAAQ;AAC3G;AAUO,SAAS,qBAAqB;AACnC,QAAM,CAAC,OAAO,aAAa,IAAI,SAAS,EAAE;AAC1C,QAAM,CAAC,cAAc,eAAe,IAAI,SAA4B,oBAAI,IAAI,CAAC;AAG7E,YAAU,MAAM;AACd,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,UAAM,IAAI,OAAO,IAAI,GAAG;AACxB,QAAI,EAAG,eAAc,CAAC;AACtB,UAAM,IAAI,OAAO,IAAI,QAAQ;AAC7B,QAAI,GAAG;AACL,YAAM,SAAS,EAAE,MAAM,GAAG,EAAE,OAAO,CAAC,MAAyB,WAAW,IAAI,CAAiB,CAAC;AAC9F,UAAI,OAAO,SAAS,EAAG,iBAAgB,IAAI,IAAI,MAAM,CAAC;AAAA,IACxD;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,mBAAmB,yBAAyB,OAAO,CAAC,MAAM,EAAE,QAAQ,aAAa;AAEvF,QAAM,WAAW,CAAC,UAAkB;AAClC,kBAAc,KAAK;AACnB,kBAAc,OAAO,YAAY;AAAA,EACnC;AAEA,QAAM,cAAc,CAAC,QAAsB;AACzC,oBAAgB,CAAC,SAAS;AACxB,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,UAAI,KAAK,IAAI,GAAG,EAAG,MAAK,OAAO,GAAG;AAAA,UAC7B,MAAK,IAAI,GAAG;AACjB,oBAAc,OAAO,IAAI;AACzB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,QAAM,UAAU,CAAC,QAAoC;AACnD,QAAI,aAAa,OAAO,KAAK,CAAC,aAAa,IAAI,IAAI,KAAK,EAAG,QAAO;AAClE,QAAI,IAAI,WAAW,OAAQ,QAAO;AAClC,WAAO,oBAAoB,KAAK,KAAK;AAAA,EACvC;AAEA,QAAM,eAAe;AAAA,IACnB,MAAM,8BAA8B,OAAO,OAAO,EAAE;AAAA;AAAA,IAEpD,CAAC,OAAO,YAAY;AAAA,EACtB;AAEA,QAAM,QAAQ,MAAM;AAClB,kBAAc,EAAE;AAChB,oBAAgB,oBAAI,IAAI,CAAC;AACzB,kBAAc,IAAI,oBAAI,IAAI,CAAC;AAAA,EAC7B;AAEA,SAAO,EAAE,OAAO,UAAU,cAAc,aAAa,kBAAkB,cAAc,MAAM;AAC7F;AAmBO,SAAS,yBAAyB,EAAE,WAAW,OAAO,cAAc,gBAAgB,UAAU,GAAkC;AACrI,QAAM,gBAAgB,CAAC,MAAyB,aAAa,SAAS,KAAK,aAAa,IAAI,EAAE,KAAK;AACnG,QAAM,WAAW,CAAC,MAAyB,oBAAoB,GAAG,KAAK;AAEvE,QAAM,aAAa,OAAO,YAAY,yBAAyB,IAAI,CAAC,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAExF,QAAM,WAAW,8BACd,OAAO,CAAC,MAAM,EAAE,UAAU,iBAAiB,EAAE,WAAW,UAAU,cAAc,CAAC,KAAK,SAAS,CAAC,CAAC,EACjG,KAAK,CAAC,GAAG,OAAO,WAAW,EAAE,KAAK,KAAK,OAAO,WAAW,EAAE,KAAK,KAAK,GAAG;AAE3E,QAAM,kBAAkB,yBAAyB,KAAK,CAAC,MAAM,EAAE,QAAQ,aAAa;AACpF,QAAM,iBAAiB,8BAA8B;AAAA,IACnD,CAAC,MAAM,EAAE,UAAU,iBAAiB,cAAc,CAAC,KAAK,SAAS,CAAC;AAAA,EACpE;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GACtC,mBAAS,WAAW,KAAK,eAAe,WAAW,IAClD,qBAAC,SAAI,WAAU,+BACb;AAAA,wBAAC,OAAE,WAAU,yBAAwB,+BAAiB;AAAA,IACtD,oBAAC,OAAE,WAAU,iCAAgC,2DAA6C;AAAA,IAC1F;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QACX;AAAA;AAAA,IAED;AAAA,KACF,IAEA,iCACE;AAAA,wBAAC,OAAO,KAAP,EAAW,QAAM,MAAC,WAAU,gFAC3B,8BAAC,mBAAgB,MAAK,aACnB,mBAAS,IAAI,CAAC,KAAK,MAClB,oBAAC,wBAAmC,KAAU,UAAU,UAAU,IAAI,GAAG,GAAG,OAAO,KAAxD,IAAI,GAAuD,CACvF,GACH,GACF;AAAA,IACC,mBAAmB,eAAe,SAAS,KAC1C,oBAAC,mBAAgB,OAAO,iBAAiB,MAAM,gBAAgB;AAAA,KAEnE,GAEJ;AAEJ;","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 /** Live per-user fact shown as a quiet chip on the card (e.g. \"3 collections\").\n * Apps pass real counts for the signed-in creator; omit when zero/signed out. */\n meta?: string;\n}\n\nexport type ServiceOverrides = Record<string, ServiceOverride>;\n\n// ── Group accents — the grid's only color voice ─────────────────────────────\n\n/** Each group owns one hue of the Medialane spectrum (blue → indigo → purple →\n * rose → orange in group order), applied flat: icon-tile tint + glyph, the\n * use-case markers, and the card's resting hairline. On hover-capable\n * devices the full-spectrum gradient ring fades in over the hairline\n * (.ml-grad-hover-border — polish only, never carries information). */\ninterface GroupSlice {\n /** soft icon-tile background */\n tint: string;\n /** icon glyph + use-case marker tint */\n text: string;\n /** resting card hairline in the group's hue */\n border: string;\n}\n\nconst DEFAULT_SLICE: GroupSlice = {\n tint: \"bg-muted/60\",\n text: \"text-muted-foreground/60\",\n border: \"border-border/60\",\n};\n\nexport const GROUP_SLICES: Record<ServiceGroup, GroupSlice> = {\n \"nfts\": { tint: \"bg-brand-blue/10\", text: \"text-brand-blue\", border: \"border-brand-blue/30\" },\n \"limited-editions\": { tint: \"bg-brand-indigo/10\", text: \"text-brand-indigo\", border: \"border-brand-indigo/30\" },\n \"coins\": { tint: \"bg-brand-purple/10\", text: \"text-brand-purple\", border: \"border-brand-purple/30\" },\n \"community\": { tint: \"bg-brand-rose/10\", text: \"text-brand-rose\", border: \"border-brand-rose/30\" },\n \"claims\": { tint: \"bg-brand-orange/10\", text: \"text-brand-orange\", border: \"border-brand-orange/30\" },\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\n/** Brand tokens only ([[brand-tokens-only]]) — one hue per service, drawn from\n * the brand spectrum. */\nexport const SERVICE_HUES: Record<string, ServiceHue> = {\n \"nfts\": { text: \"text-brand-blue\", solid: \"bg-brand-blue\", border: \"border-brand-blue/30\", tint: \"bg-brand-blue/10\" },\n \"limited-editions\": { text: \"text-brand-indigo\", solid: \"bg-brand-indigo\", border: \"border-brand-indigo/30\", tint: \"bg-brand-indigo/10\" },\n \"creator-coins\": { text: \"text-brand-price\", solid: \"bg-brand-price\", border: \"border-brand-price/30\", tint: \"bg-brand-price/10\" },\n \"claim-memecoin\": { text: \"text-brand-orange\", solid: \"bg-brand-orange\", border: \"border-brand-orange/30\", tint: \"bg-brand-orange/10\" },\n \"collection-drop\": { text: \"text-brand-purple\", solid: \"bg-brand-purple\", border: \"border-brand-purple/30\", tint: \"bg-brand-purple/10\" },\n \"pop-protocol\": { text: \"text-brand-rose\", solid: \"bg-brand-rose\", border: \"border-brand-rose/30\", tint: \"bg-brand-rose/10\" },\n \"ip-tickets\": { text: \"text-brand-orange\", solid: \"bg-brand-orange\", border: \"border-brand-orange/30\", tint: \"bg-brand-orange/10\" },\n \"remix-asset\": { text: \"text-brand-indigo\", solid: \"bg-brand-indigo\", border: \"border-brand-indigo/30\", tint: \"bg-brand-indigo/10\" },\n \"claim-username\": { text: \"text-brand-purple\", solid: \"bg-brand-purple\", border: \"border-brand-purple/30\", tint: \"bg-brand-purple/10\" },\n \"claim-collection\": { text: \"text-brand-blue\", solid: \"bg-brand-blue\", border: \"border-brand-blue/30\", tint: \"bg-brand-blue/10\" },\n \"claim-collection-name\": { text: \"text-brand-rose\", solid: \"bg-brand-rose\", border: \"border-brand-rose/30\", tint: \"bg-brand-rose/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 tile + live per-user fact */}\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\", live ? slice.tint : \"bg-muted/40\")}>\n <Icon className={cn(\"h-7 w-7\", live ? slice.text : \"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 ) : override.meta ? (\n <span className=\"text-xs font-semibold tabular-nums px-2.5 py-1 rounded-full bg-muted/50 text-foreground/80\">\n {override.meta}\n </span>\n ) : null}\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.text)} />\n {feature}\n </li>\n ))}\n </ul>\n )}\n\n {/* The action — quiet verb, no pill */}\n {live && (\n <div className=\"pt-5\">\n <span className=\"inline-flex items-center gap-1.5 text-sm font-semibold\">\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 bg-card p-6 sm:aspect-[4/5]\",\n \"ml-grad-hover-border\",\n slice.border,\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":[]}
|
package/dist/medialane.css
CHANGED
|
@@ -208,6 +208,29 @@ input[type="number"] { -moz-appearance: textfield; }
|
|
|
208
208
|
pointer-events: none;
|
|
209
209
|
}
|
|
210
210
|
.ml-gbtn:hover::before { padding: 2px; animation-duration: 1.6s; }
|
|
211
|
+
|
|
212
|
+
/* ── Hover-only branded gradient border (launchpad cards) ───────────────── */
|
|
213
|
+
/* The resting state keeps the card's own hairline; on hover-capable devices */
|
|
214
|
+
/* the full-spectrum ring fades in over it. Decoration only — never carries */
|
|
215
|
+
/* information (mobile users lose nothing). */
|
|
216
|
+
.ml-grad-hover-border { position: relative; }
|
|
217
|
+
.ml-grad-hover-border::before {
|
|
218
|
+
content: '';
|
|
219
|
+
position: absolute;
|
|
220
|
+
inset: -1px;
|
|
221
|
+
border-radius: inherit;
|
|
222
|
+
padding: 1.5px;
|
|
223
|
+
background: linear-gradient(120deg, #3b7bff, #8a5cf6, #f6608f, #fb8b46);
|
|
224
|
+
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
225
|
+
-webkit-mask-composite: xor;
|
|
226
|
+
mask-composite: exclude;
|
|
227
|
+
opacity: 0;
|
|
228
|
+
transition: opacity 0.25s ease;
|
|
229
|
+
pointer-events: none;
|
|
230
|
+
}
|
|
231
|
+
@media (hover: hover) {
|
|
232
|
+
.ml-grad-hover-border:hover::before { opacity: 1; }
|
|
233
|
+
}
|
|
211
234
|
@keyframes mlBorder {
|
|
212
235
|
0% { background-position: 0% 50%; }
|
|
213
236
|
100% { background-position: 220% 50%; }
|