@igstack/app-catalog-frontend-core 0.4.0 → 0.6.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.
Files changed (41) hide show
  1. package/dist/esm/__tests__/integration/harness/given.d.ts +5 -1
  2. package/dist/esm/modules/appCatalog/context/AppCatalogContext.js +1 -1
  3. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +1 -13
  4. package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
  5. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.d.ts +18 -0
  6. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js +26 -0
  7. package/dist/esm/modules/appCatalog/ui/components/MarkdownText.js.map +1 -0
  8. package/dist/esm/modules/appCatalog/ui/components/PersonBadge.js +1 -1
  9. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +1 -1
  10. package/dist/esm/modules/appCatalog/ui/components/TierVariantsSection.js +1 -1
  11. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +3 -1
  12. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +5 -2
  13. package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
  14. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.d.ts +3 -1
  15. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +17 -12
  16. package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
  17. package/dist/esm/routeTree.gen.d.ts +18 -4
  18. package/dist/esm/routeTree.gen.js +11 -4
  19. package/dist/esm/routeTree.gen.js.map +1 -1
  20. package/dist/esm/routes/_layout/app.$slug.d.ts +3 -0
  21. package/dist/esm/routes/_layout/app._slug.js +21 -0
  22. package/dist/esm/routes/_layout/app._slug.js.map +1 -0
  23. package/dist/esm/routes/_layout/index.d.ts +0 -1
  24. package/dist/esm/routes/_layout/index.js +0 -1
  25. package/dist/esm/routes/_layout/index.js.map +1 -1
  26. package/package.json +3 -3
  27. package/src/__tests__/integration/appDeepLink.integration.test.ts +55 -0
  28. package/src/__tests__/integration/harness/given.tsx +7 -3
  29. package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +1 -18
  30. package/src/modules/appCatalog/ui/components/MarkdownText.tsx +42 -0
  31. package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +10 -3
  32. package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +18 -15
  33. package/src/routeTree.gen.ts +21 -2
  34. package/src/routes/_layout/app.$slug.tsx +23 -0
  35. package/src/routes/_layout/index.tsx +0 -1
  36. package/dist/esm/__tests__/modules/gallery/EscapeDismissal.integration.test.d.ts +0 -0
  37. package/dist/esm/modules/appCatalog/ui/components/AppDetailModal.d.ts +0 -7
  38. package/src/__tests__/modules/appCatalog/ScreenshotPreview.test.tsx +0 -63
  39. package/src/__tests__/modules/gallery/EscapeDismissal.integration.test.tsx +0 -118
  40. package/src/modules/appCatalog/ui/components/AppDetailModal.tsx +0 -365
  41. /package/dist/esm/__tests__/{modules/appCatalog/ScreenshotPreview.test.d.ts → integration/appDeepLink.integration.test.d.ts} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useNavigate } from "@tanstack/react-router";
2
3
  import { X } from "lucide-react";
3
4
  import { useDeferredValue, useState, useEffect, useMemo } from "react";
4
5
  import { Button } from "../../../../ui/button.js";
@@ -6,20 +7,19 @@ import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyCont
6
7
  import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
7
8
  import { useAppClickHistory } from "../../hooks/useAppClickHistory.js";
8
9
  import { useAppCounts } from "../../hooks/useAppCounts.js";
9
- import { useUrlSyncedState } from "../../hooks/useUrlSyncedState.js";
10
10
  import { searchResources } from "../../utils/searchApps.js";
11
11
  import { OnboardingCard } from "../components/OnboardingCard.js";
12
12
  import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
13
13
  import { FilterBar } from "../filters/FilterBar.js";
14
14
  import { AppCatalogGrid } from "../grid/AppCatalogGrid.js";
15
- function AppCatalogPage() {
15
+ function AppCatalogPage({
16
+ selectedSlug
17
+ } = {}) {
16
18
  const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext();
17
19
  const { state: filterState, actions } = useAppCatalogFilters();
18
20
  const { getTopApps } = useAppClickHistory();
19
- const [selectedAppSlug, setSelectedAppSlug] = useUrlSyncedState({
20
- key: "app",
21
- defaultValue: void 0
22
- });
21
+ const navigate = useNavigate();
22
+ const selectedAppSlug = selectedSlug;
23
23
  const searchValue = filterState.searchValue;
24
24
  const setSearchValue = actions.setSearchValue;
25
25
  const deferredSearchValue = useDeferredValue(searchValue);
@@ -72,17 +72,21 @@ function AppCatalogPage() {
72
72
  searchValue: deferredSearchValue
73
73
  });
74
74
  useEffect(() => {
75
- if (filteredApps.length === 1 && filteredApps[0]) {
76
- setSelectedAppSlug(filteredApps[0].slug);
75
+ if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {
76
+ void navigate({
77
+ to: "/app/$slug",
78
+ params: { slug: filteredApps[0].slug },
79
+ replace: true
80
+ });
77
81
  }
78
- }, [filteredApps, setSelectedAppSlug]);
82
+ }, [filteredApps, selectedAppSlug, navigate]);
79
83
  const handleAppClick = (app) => {
80
- setSelectedAppSlug(app.slug);
84
+ void navigate({ to: "/app/$slug", params: { slug: app.slug } });
81
85
  };
82
86
  const handleClearFilters = () => {
83
87
  setSearchValue("");
84
88
  actions.clearAllFilters();
85
- setSelectedAppSlug(void 0);
89
+ void navigate({ to: "/" });
86
90
  };
87
91
  const totalAppsCount = useMemo(() => {
88
92
  let count = rootResources.length;
@@ -121,7 +125,7 @@ function AppCatalogPage() {
121
125
  variant: "outline",
122
126
  onClick: () => {
123
127
  setSearchValue("");
124
- setSelectedAppSlug(void 0);
128
+ void navigate({ to: "/" });
125
129
  },
126
130
  className: "gap-2",
127
131
  children: [
@@ -137,6 +141,7 @@ function AppCatalogPage() {
137
141
  selectedAppSlug,
138
142
  groupingDefinition,
139
143
  onAppClick: handleAppClick,
144
+ onClosePanel: () => void navigate({ to: "/" }),
140
145
  hasSearch: !!deferredSearchValue,
141
146
  searchQuery: deferredSearchValue,
142
147
  totalAppsCount,
@@ -1 +1 @@
1
- {"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { X } from 'lucide-react'\nimport { useDeferredValue, useEffect, useMemo, useState } from 'react'\nimport { Button } from '~/ui/button'\nimport {\n Empty,\n EmptyContent,\n EmptyDescription,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n} from '~/ui/empty'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useAppCounts } from '../../hooks/useAppCounts'\nimport { useUrlSyncedState } from '../../hooks/useUrlSyncedState'\nimport { searchResources } from '../../utils/searchApps'\nimport { OnboardingCard } from '../components/OnboardingCard'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { FilterBar } from '../filters/FilterBar'\nimport { AppCatalogGrid } from '../grid/AppCatalogGrid'\n\nexport function AppCatalogPage() {\n const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const { getTopApps } = useAppClickHistory()\n\n // URL-synced state\n const [selectedAppSlug, setSelectedAppSlug] = useUrlSyncedState<\n string | undefined\n >({\n key: 'app',\n defaultValue: undefined,\n })\n\n // Search value from context (URL-synced in AppCatalogFiltersContext)\n const searchValue = filterState.searchValue\n const setSearchValue = actions.setSearchValue\n\n // Defer the search value used for filtering to avoid blocking the input\n const deferredSearchValue = useDeferredValue(searchValue)\n\n // State for top apps (loaded async)\n const [topAppSlugs, setTopAppSlugs] = useState<string[]>([])\n\n // Load top apps on mount to calculate recent count\n useEffect(() => {\n void getTopApps(10).then(setTopAppSlugs)\n }, [getTopApps])\n\n // Get root resources for filtering (children handled internally by searchResources)\n const rootResources = useMemo(\n () => resources.filter((r) => !r.parentSlug),\n [resources],\n )\n\n const filteredApps = useMemo(() => {\n let result = rootResources\n\n // Step 1: Filter deprecated apps (if not showing them)\n if (!filterState.showDeprecated) {\n result = result.filter((app) => !app.deprecated)\n }\n\n // Step 2: Apply recent mode or tag filters\n if (filterState.recentMode) {\n // Filter to top 10 most clicked apps\n result = result.filter((app) => topAppSlugs.includes(app.slug))\n } else if (Object.keys(filterState.tagFilters).length > 0) {\n // Apply tag filters (AND condition)\n result = result.filter((app) => {\n return Object.entries(filterState.tagFilters).every(\n ([prefix, value]) => {\n const fullTag = `${prefix}:${value}`\n return app.tags?.some(\n (tag) => tag.toLowerCase() === fullTag.toLowerCase(),\n )\n },\n )\n })\n }\n\n // Step 3: Apply search (using deferred value)\n // Pass all resources so children contribute to parent scoring\n const childResources = resources.filter((r) => r.parentSlug)\n result = searchResources(\n [...result, ...childResources],\n deferredSearchValue,\n )\n\n return result\n }, [\n rootResources,\n resources,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\n\n // Calculate counts for FilterBar\n const { allCount, recentCount, deprecatedCount } = useAppCounts({\n apps: rootResources,\n topAppSlugs,\n searchValue: deferredSearchValue,\n })\n\n // Auto-open details when only 1 result\n useEffect(() => {\n if (filteredApps.length === 1 && filteredApps[0]) {\n setSelectedAppSlug(filteredApps[0].slug)\n }\n }, [filteredApps, setSelectedAppSlug])\n\n const handleAppClick = (app: Resource) => {\n setSelectedAppSlug(app.slug)\n }\n\n const handleClearFilters = () => {\n setSearchValue('')\n actions.clearAllFilters()\n setSelectedAppSlug(undefined)\n }\n\n // Calculate total apps count (respecting showDeprecated setting)\n const totalAppsCount = useMemo(() => {\n let count = rootResources.length\n if (!filterState.showDeprecated) {\n count = rootResources.filter((app) => !app.deprecated).length\n }\n return count\n }, [rootResources, filterState.showDeprecated])\n\n if (isLoadingApps) {\n return <div className=\"py-6 text-muted-foreground\">Loading…</div>\n }\n\n // Use first tag definition for grouping\n const groupingDefinition = tagsDefinitions[0]\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0\">\n <div className=\"shrink-0\">\n <OnboardingCard />\n </div>\n\n <div className=\"shrink-0\">\n <FilterBar\n totalCount={allCount}\n recentCount={recentCount}\n deprecatedCount={deprecatedCount}\n apps={rootResources}\n />\n </div>\n\n <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 ? (\n <Empty>\n <EmptyHeader>\n <EmptyMedia variant=\"icon\">\n <X className=\"h-6 w-6\" />\n </EmptyMedia>\n <EmptyTitle>\n No apps found{searchValue && ` for \"${searchValue}\"`}\n </EmptyTitle>\n <EmptyDescription>\n Try adjusting your search or filters\n </EmptyDescription>\n </EmptyHeader>\n <EmptyContent>\n {searchValue && (\n <Button\n variant=\"outline\"\n onClick={() => {\n setSearchValue('')\n setSelectedAppSlug(undefined)\n }}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear search\n </Button>\n )}\n </EmptyContent>\n </Empty>\n ) : (\n <AppCatalogGrid\n apps={filteredApps}\n selectedAppSlug={selectedAppSlug}\n groupingDefinition={groupingDefinition}\n onAppClick={handleAppClick}\n hasSearch={!!deferredSearchValue}\n searchQuery={deferredSearchValue}\n totalAppsCount={totalAppsCount}\n onClearFilters={handleClearFilters}\n />\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AppCatalogPage.js","sources":["../../../../../../src/modules/appCatalog/ui/pages/AppCatalogPage.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useNavigate } from '@tanstack/react-router'\nimport { X } from 'lucide-react'\nimport { useDeferredValue, useEffect, useMemo, useState } from 'react'\nimport { Button } from '~/ui/button'\nimport {\n Empty,\n EmptyContent,\n EmptyDescription,\n EmptyHeader,\n EmptyMedia,\n EmptyTitle,\n} from '~/ui/empty'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { useAppCounts } from '../../hooks/useAppCounts'\nimport { searchResources } from '../../utils/searchApps'\nimport { OnboardingCard } from '../components/OnboardingCard'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { FilterBar } from '../filters/FilterBar'\nimport { AppCatalogGrid } from '../grid/AppCatalogGrid'\n\nexport function AppCatalogPage({\n selectedSlug,\n}: { selectedSlug?: string } = {}) {\n const { resources, isLoadingApps, tagsDefinitions } = useAppCatalogContext()\n const { state: filterState, actions } = useAppCatalogFilters()\n const { getTopApps } = useAppClickHistory()\n const navigate = useNavigate()\n\n // Selected app comes from the route path (/app/<slug>); undefined = nothing open\n const selectedAppSlug = selectedSlug\n\n // Search value from context (URL-synced in AppCatalogFiltersContext)\n const searchValue = filterState.searchValue\n const setSearchValue = actions.setSearchValue\n\n // Defer the search value used for filtering to avoid blocking the input\n const deferredSearchValue = useDeferredValue(searchValue)\n\n // State for top apps (loaded async)\n const [topAppSlugs, setTopAppSlugs] = useState<string[]>([])\n\n // Load top apps on mount to calculate recent count\n useEffect(() => {\n void getTopApps(10).then(setTopAppSlugs)\n }, [getTopApps])\n\n // Get root resources for filtering (children handled internally by searchResources)\n const rootResources = useMemo(\n () => resources.filter((r) => !r.parentSlug),\n [resources],\n )\n\n const filteredApps = useMemo(() => {\n let result = rootResources\n\n // Step 1: Filter deprecated apps (if not showing them)\n if (!filterState.showDeprecated) {\n result = result.filter((app) => !app.deprecated)\n }\n\n // Step 2: Apply recent mode or tag filters\n if (filterState.recentMode) {\n // Filter to top 10 most clicked apps\n result = result.filter((app) => topAppSlugs.includes(app.slug))\n } else if (Object.keys(filterState.tagFilters).length > 0) {\n // Apply tag filters (AND condition)\n result = result.filter((app) => {\n return Object.entries(filterState.tagFilters).every(\n ([prefix, value]) => {\n const fullTag = `${prefix}:${value}`\n return app.tags?.some(\n (tag) => tag.toLowerCase() === fullTag.toLowerCase(),\n )\n },\n )\n })\n }\n\n // Step 3: Apply search (using deferred value)\n // Pass all resources so children contribute to parent scoring\n const childResources = resources.filter((r) => r.parentSlug)\n result = searchResources(\n [...result, ...childResources],\n deferredSearchValue,\n )\n\n return result\n }, [\n rootResources,\n resources,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\n\n // Calculate counts for FilterBar\n const { allCount, recentCount, deprecatedCount } = useAppCounts({\n apps: rootResources,\n topAppSlugs,\n searchValue: deferredSearchValue,\n })\n\n // Auto-open details when only 1 result\n useEffect(() => {\n if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {\n void navigate({\n to: '/app/$slug',\n params: { slug: filteredApps[0].slug },\n replace: true,\n })\n }\n }, [filteredApps, selectedAppSlug, navigate])\n\n const handleAppClick = (app: Resource) => {\n void navigate({ to: '/app/$slug', params: { slug: app.slug } })\n }\n\n const handleClearFilters = () => {\n setSearchValue('')\n actions.clearAllFilters()\n void navigate({ to: '/' })\n }\n\n // Calculate total apps count (respecting showDeprecated setting)\n const totalAppsCount = useMemo(() => {\n let count = rootResources.length\n if (!filterState.showDeprecated) {\n count = rootResources.filter((app) => !app.deprecated).length\n }\n return count\n }, [rootResources, filterState.showDeprecated])\n\n if (isLoadingApps) {\n return <div className=\"py-6 text-muted-foreground\">Loading…</div>\n }\n\n // Use first tag definition for grouping\n const groupingDefinition = tagsDefinitions[0]\n\n return (\n <div className=\"flex flex-col flex-1 min-h-0\">\n <div className=\"shrink-0\">\n <OnboardingCard />\n </div>\n\n <div className=\"shrink-0\">\n <FilterBar\n totalCount={allCount}\n recentCount={recentCount}\n deprecatedCount={deprecatedCount}\n apps={rootResources}\n />\n </div>\n\n <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 ? (\n <Empty>\n <EmptyHeader>\n <EmptyMedia variant=\"icon\">\n <X className=\"h-6 w-6\" />\n </EmptyMedia>\n <EmptyTitle>\n No apps found{searchValue && ` for \"${searchValue}\"`}\n </EmptyTitle>\n <EmptyDescription>\n Try adjusting your search or filters\n </EmptyDescription>\n </EmptyHeader>\n <EmptyContent>\n {searchValue && (\n <Button\n variant=\"outline\"\n onClick={() => {\n setSearchValue('')\n void navigate({ to: '/' })\n }}\n className=\"gap-2\"\n >\n <X className=\"h-4 w-4\" />\n Clear search\n </Button>\n )}\n </EmptyContent>\n </Empty>\n ) : (\n <AppCatalogGrid\n apps={filteredApps}\n selectedAppSlug={selectedAppSlug}\n groupingDefinition={groupingDefinition}\n onAppClick={handleAppClick}\n onClosePanel={() => void navigate({ to: '/' })}\n hasSearch={!!deferredSearchValue}\n searchQuery={deferredSearchValue}\n totalAppsCount={totalAppsCount}\n onClearFilters={handleClearFilters}\n />\n )}\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -3,10 +3,10 @@ import { Route as LayoutRouteImport } from './routes/_layout.js';
3
3
  import { Route as LayoutIndexRouteImport } from './routes/_layout/index.js';
4
4
  import { Route as AuthCallbackRouteImport } from './routes/auth.callback';
5
5
  import { Route as LayoutLoginRouteImport } from './routes/_layout/login.js';
6
+ import { Route as LayoutAppSlugRouteImport } from './routes/_layout/app.$slug';
6
7
  import { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index';
7
8
  declare const LayoutRoute: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>;
8
9
  declare const LayoutIndexRoute: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/", "/", "/_layout/", "/_layout/", import('zod').ZodObject<{
9
- app: import('zod').ZodOptional<import('zod').ZodString>;
10
10
  filterTag: import('zod').ZodOptional<import('zod').ZodString>;
11
11
  }, import('zod/v4/core').$strip>, import('@tanstack/router-core').ResolveParams<"/">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
12
12
  appCatalogLoader: import('./modules/appCatalog/routeLoader.js').AppCatalogLoaderReturn;
@@ -16,6 +16,9 @@ declare const AuthCallbackRoute: import('@tanstack/router-core').Route<import('@
16
16
  code: string | undefined;
17
17
  }, import('@tanstack/router-core').ResolveParams<"/auth/callback">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>;
18
18
  declare const LayoutLoginRoute: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/login", "/login", "/_layout/login", "/_layout/login", undefined, import('@tanstack/router-core').ResolveParams<"/login">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>;
19
+ declare const LayoutAppSlugRoute: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/app/$slug", "/app/$slug", "/_layout/app/$slug", "/_layout/app/$slug", undefined, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
20
+ appCatalogLoader: import('./modules/appCatalog/routeLoader.js').AppCatalogLoaderReturn;
21
+ }>, unknown, unknown, unknown, unknown, undefined>;
19
22
  declare const LayoutCatalogAppsIndexRoute: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('./types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/catalog/apps", "/catalog/apps/", "/_layout/catalog/apps/", "/_layout/catalog/apps/", import('zod').ZodObject<{
20
23
  app: import('zod').ZodOptional<import('zod').ZodString>;
21
24
  q: import('zod').ZodOptional<import('zod').ZodString>;
@@ -24,12 +27,14 @@ export interface FileRoutesByFullPath {
24
27
  '/': typeof LayoutIndexRoute;
25
28
  '/login': typeof LayoutLoginRoute;
26
29
  '/auth/callback': typeof AuthCallbackRoute;
30
+ '/app/$slug': typeof LayoutAppSlugRoute;
27
31
  '/catalog/apps/': typeof LayoutCatalogAppsIndexRoute;
28
32
  }
29
33
  export interface FileRoutesByTo {
30
34
  '/login': typeof LayoutLoginRoute;
31
35
  '/auth/callback': typeof AuthCallbackRoute;
32
36
  '/': typeof LayoutIndexRoute;
37
+ '/app/$slug': typeof LayoutAppSlugRoute;
33
38
  '/catalog/apps': typeof LayoutCatalogAppsIndexRoute;
34
39
  }
35
40
  export interface FileRoutesById {
@@ -38,14 +43,15 @@ export interface FileRoutesById {
38
43
  '/_layout/login': typeof LayoutLoginRoute;
39
44
  '/auth/callback': typeof AuthCallbackRoute;
40
45
  '/_layout/': typeof LayoutIndexRoute;
46
+ '/_layout/app/$slug': typeof LayoutAppSlugRoute;
41
47
  '/_layout/catalog/apps/': typeof LayoutCatalogAppsIndexRoute;
42
48
  }
43
49
  export interface FileRouteTypes {
44
50
  fileRoutesByFullPath: FileRoutesByFullPath;
45
- fullPaths: '/' | '/login' | '/auth/callback' | '/catalog/apps/';
51
+ fullPaths: '/' | '/login' | '/auth/callback' | '/app/$slug' | '/catalog/apps/';
46
52
  fileRoutesByTo: FileRoutesByTo;
47
- to: '/login' | '/auth/callback' | '/' | '/catalog/apps';
48
- id: '__root__' | '/_layout' | '/_layout/login' | '/auth/callback' | '/_layout/' | '/_layout/catalog/apps/';
53
+ to: '/login' | '/auth/callback' | '/' | '/app/$slug' | '/catalog/apps';
54
+ id: '__root__' | '/_layout' | '/_layout/login' | '/auth/callback' | '/_layout/' | '/_layout/app/$slug' | '/_layout/catalog/apps/';
49
55
  fileRoutesById: FileRoutesById;
50
56
  }
51
57
  export interface RootRouteChildren {
@@ -82,6 +88,13 @@ declare module '@tanstack/react-router' {
82
88
  preLoaderRoute: typeof LayoutLoginRouteImport;
83
89
  parentRoute: typeof LayoutRoute;
84
90
  };
91
+ '/_layout/app/$slug': {
92
+ id: '/_layout/app/$slug';
93
+ path: '/app/$slug';
94
+ fullPath: '/app/$slug';
95
+ preLoaderRoute: typeof LayoutAppSlugRouteImport;
96
+ parentRoute: typeof LayoutRoute;
97
+ };
85
98
  '/_layout/catalog/apps/': {
86
99
  id: '/_layout/catalog/apps/';
87
100
  path: '/catalog/apps';
@@ -94,6 +107,7 @@ declare module '@tanstack/react-router' {
94
107
  interface LayoutRouteChildren {
95
108
  LayoutLoginRoute: typeof LayoutLoginRoute;
96
109
  LayoutIndexRoute: typeof LayoutIndexRoute;
110
+ LayoutAppSlugRoute: typeof LayoutAppSlugRoute;
97
111
  LayoutCatalogAppsIndexRoute: typeof LayoutCatalogAppsIndexRoute;
98
112
  }
99
113
  declare const LayoutRouteChildren: LayoutRouteChildren;
@@ -1,14 +1,15 @@
1
1
  import { Route } from "./routes/__root.js";
2
2
  import { Route as Route$2 } from "./routes/_layout.js";
3
- import { Route as Route$4 } from "./routes/_layout/index.js";
3
+ import { Route as Route$5 } from "./routes/_layout/index.js";
4
4
  import { Route as Route$1 } from "./routes/auth.callback.js";
5
- import { Route as Route$5 } from "./routes/_layout/login.js";
5
+ import { Route as Route$6 } from "./routes/_layout/login.js";
6
+ import { Route as Route$4 } from "./routes/_layout/app._slug.js";
6
7
  import { Route as Route$3 } from "./routes/_layout/catalog.apps.index.js";
7
8
  const LayoutRoute = Route$2.update({
8
9
  id: "/_layout",
9
10
  getParentRoute: () => Route
10
11
  });
11
- const LayoutIndexRoute = Route$4.update({
12
+ const LayoutIndexRoute = Route$5.update({
12
13
  id: "/",
13
14
  path: "/",
14
15
  getParentRoute: () => LayoutRoute
@@ -18,11 +19,16 @@ const AuthCallbackRoute = Route$1.update({
18
19
  path: "/auth/callback",
19
20
  getParentRoute: () => Route
20
21
  });
21
- const LayoutLoginRoute = Route$5.update({
22
+ const LayoutLoginRoute = Route$6.update({
22
23
  id: "/login",
23
24
  path: "/login",
24
25
  getParentRoute: () => LayoutRoute
25
26
  });
27
+ const LayoutAppSlugRoute = Route$4.update({
28
+ id: "/app/$slug",
29
+ path: "/app/$slug",
30
+ getParentRoute: () => LayoutRoute
31
+ });
26
32
  const LayoutCatalogAppsIndexRoute = Route$3.update({
27
33
  id: "/catalog/apps/",
28
34
  path: "/catalog/apps/",
@@ -31,6 +37,7 @@ const LayoutCatalogAppsIndexRoute = Route$3.update({
31
37
  const LayoutRouteChildren = {
32
38
  LayoutLoginRoute,
33
39
  LayoutIndexRoute,
40
+ LayoutAppSlugRoute,
34
41
  LayoutCatalogAppsIndexRoute
35
42
  };
36
43
  const LayoutRouteWithChildren = LayoutRoute._addFileChildren(LayoutRouteChildren);
@@ -1 +1 @@
1
- {"version":3,"file":"routeTree.gen.js","sources":["../../src/routeTree.gen.ts"],"sourcesContent":["/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { Route as rootRouteImport } from './routes/__root'\nimport { Route as LayoutRouteImport } from './routes/_layout'\nimport { Route as LayoutIndexRouteImport } from './routes/_layout/index'\nimport { Route as AuthCallbackRouteImport } from './routes/auth.callback'\nimport { Route as LayoutLoginRouteImport } from './routes/_layout/login'\nimport { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index'\n\nconst LayoutRoute = LayoutRouteImport.update({\n id: '/_layout',\n getParentRoute: () => rootRouteImport,\n} as any)\nconst LayoutIndexRoute = LayoutIndexRouteImport.update({\n id: '/',\n path: '/',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst AuthCallbackRoute = AuthCallbackRouteImport.update({\n id: '/auth/callback',\n path: '/auth/callback',\n getParentRoute: () => rootRouteImport,\n} as any)\nconst LayoutLoginRoute = LayoutLoginRouteImport.update({\n id: '/login',\n path: '/login',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst LayoutCatalogAppsIndexRoute = LayoutCatalogAppsIndexRouteImport.update({\n id: '/catalog/apps/',\n path: '/catalog/apps/',\n getParentRoute: () => LayoutRoute,\n} as any)\n\nexport interface FileRoutesByFullPath {\n '/': typeof LayoutIndexRoute\n '/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/catalog/apps/': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRoutesByTo {\n '/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/': typeof LayoutIndexRoute\n '/catalog/apps': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRoutesById {\n __root__: typeof rootRouteImport\n '/_layout': typeof LayoutRouteWithChildren\n '/_layout/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/_layout/': typeof LayoutIndexRoute\n '/_layout/catalog/apps/': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRouteTypes {\n fileRoutesByFullPath: FileRoutesByFullPath\n fullPaths: '/' | '/login' | '/auth/callback' | '/catalog/apps/'\n fileRoutesByTo: FileRoutesByTo\n to: '/login' | '/auth/callback' | '/' | '/catalog/apps'\n id:\n | '__root__'\n | '/_layout'\n | '/_layout/login'\n | '/auth/callback'\n | '/_layout/'\n | '/_layout/catalog/apps/'\n fileRoutesById: FileRoutesById\n}\nexport interface RootRouteChildren {\n LayoutRoute: typeof LayoutRouteWithChildren\n AuthCallbackRoute: typeof AuthCallbackRoute\n}\n\ndeclare module '@tanstack/react-router' {\n interface FileRoutesByPath {\n '/_layout': {\n id: '/_layout'\n path: ''\n fullPath: '/'\n preLoaderRoute: typeof LayoutRouteImport\n parentRoute: typeof rootRouteImport\n }\n '/_layout/': {\n id: '/_layout/'\n path: '/'\n fullPath: '/'\n preLoaderRoute: typeof LayoutIndexRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/auth/callback': {\n id: '/auth/callback'\n path: '/auth/callback'\n fullPath: '/auth/callback'\n preLoaderRoute: typeof AuthCallbackRouteImport\n parentRoute: typeof rootRouteImport\n }\n '/_layout/login': {\n id: '/_layout/login'\n path: '/login'\n fullPath: '/login'\n preLoaderRoute: typeof LayoutLoginRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/_layout/catalog/apps/': {\n id: '/_layout/catalog/apps/'\n path: '/catalog/apps'\n fullPath: '/catalog/apps/'\n preLoaderRoute: typeof LayoutCatalogAppsIndexRouteImport\n parentRoute: typeof LayoutRoute\n }\n }\n}\n\ninterface LayoutRouteChildren {\n LayoutLoginRoute: typeof LayoutLoginRoute\n LayoutIndexRoute: typeof LayoutIndexRoute\n LayoutCatalogAppsIndexRoute: typeof LayoutCatalogAppsIndexRoute\n}\n\nconst LayoutRouteChildren: LayoutRouteChildren = {\n LayoutLoginRoute: LayoutLoginRoute,\n LayoutIndexRoute: LayoutIndexRoute,\n LayoutCatalogAppsIndexRoute: LayoutCatalogAppsIndexRoute,\n}\n\nconst LayoutRouteWithChildren =\n LayoutRoute._addFileChildren(LayoutRouteChildren)\n\nconst rootRouteChildren: RootRouteChildren = {\n LayoutRoute: LayoutRouteWithChildren,\n AuthCallbackRoute: AuthCallbackRoute,\n}\nexport const routeTree = rootRouteImport\n ._addFileChildren(rootRouteChildren)\n ._addFileTypes<FileRouteTypes>()\n"],"names":["LayoutRouteImport","rootRouteImport","LayoutIndexRouteImport","AuthCallbackRouteImport","LayoutLoginRouteImport","LayoutCatalogAppsIndexRouteImport"],"mappings":";;;;;;AAiBA,MAAM,cAAcA,QAAkB,OAAO;AAAA,EAC3C,IAAI;AAAA,EACJ,gBAAgB,MAAMC;AACxB,CAAQ;AACR,MAAM,mBAAmBC,QAAuB,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,oBAAoBC,QAAwB,OAAO;AAAA,EACvD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAMF;AACxB,CAAQ;AACR,MAAM,mBAAmBG,QAAuB,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,8BAA8BC,QAAkC,OAAO;AAAA,EAC3E,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AAuFR,MAAM,sBAA2C;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,0BACJ,YAAY,iBAAiB,mBAAmB;AAElD,MAAM,oBAAuC;AAAA,EAC3C,aAAa;AAAA,EACb;AACF;AACO,MAAM,YAAYJ,MACtB,iBAAiB,iBAAiB,EAClC,cAAA;"}
1
+ {"version":3,"file":"routeTree.gen.js","sources":["../../src/routeTree.gen.ts"],"sourcesContent":["/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { Route as rootRouteImport } from './routes/__root'\nimport { Route as LayoutRouteImport } from './routes/_layout'\nimport { Route as LayoutIndexRouteImport } from './routes/_layout/index'\nimport { Route as AuthCallbackRouteImport } from './routes/auth.callback'\nimport { Route as LayoutLoginRouteImport } from './routes/_layout/login'\nimport { Route as LayoutAppSlugRouteImport } from './routes/_layout/app.$slug'\nimport { Route as LayoutCatalogAppsIndexRouteImport } from './routes/_layout/catalog.apps.index'\n\nconst LayoutRoute = LayoutRouteImport.update({\n id: '/_layout',\n getParentRoute: () => rootRouteImport,\n} as any)\nconst LayoutIndexRoute = LayoutIndexRouteImport.update({\n id: '/',\n path: '/',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst AuthCallbackRoute = AuthCallbackRouteImport.update({\n id: '/auth/callback',\n path: '/auth/callback',\n getParentRoute: () => rootRouteImport,\n} as any)\nconst LayoutLoginRoute = LayoutLoginRouteImport.update({\n id: '/login',\n path: '/login',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst LayoutAppSlugRoute = LayoutAppSlugRouteImport.update({\n id: '/app/$slug',\n path: '/app/$slug',\n getParentRoute: () => LayoutRoute,\n} as any)\nconst LayoutCatalogAppsIndexRoute = LayoutCatalogAppsIndexRouteImport.update({\n id: '/catalog/apps/',\n path: '/catalog/apps/',\n getParentRoute: () => LayoutRoute,\n} as any)\n\nexport interface FileRoutesByFullPath {\n '/': typeof LayoutIndexRoute\n '/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/app/$slug': typeof LayoutAppSlugRoute\n '/catalog/apps/': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRoutesByTo {\n '/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/': typeof LayoutIndexRoute\n '/app/$slug': typeof LayoutAppSlugRoute\n '/catalog/apps': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRoutesById {\n __root__: typeof rootRouteImport\n '/_layout': typeof LayoutRouteWithChildren\n '/_layout/login': typeof LayoutLoginRoute\n '/auth/callback': typeof AuthCallbackRoute\n '/_layout/': typeof LayoutIndexRoute\n '/_layout/app/$slug': typeof LayoutAppSlugRoute\n '/_layout/catalog/apps/': typeof LayoutCatalogAppsIndexRoute\n}\nexport interface FileRouteTypes {\n fileRoutesByFullPath: FileRoutesByFullPath\n fullPaths: '/' | '/login' | '/auth/callback' | '/app/$slug' | '/catalog/apps/'\n fileRoutesByTo: FileRoutesByTo\n to: '/login' | '/auth/callback' | '/' | '/app/$slug' | '/catalog/apps'\n id:\n | '__root__'\n | '/_layout'\n | '/_layout/login'\n | '/auth/callback'\n | '/_layout/'\n | '/_layout/app/$slug'\n | '/_layout/catalog/apps/'\n fileRoutesById: FileRoutesById\n}\nexport interface RootRouteChildren {\n LayoutRoute: typeof LayoutRouteWithChildren\n AuthCallbackRoute: typeof AuthCallbackRoute\n}\n\ndeclare module '@tanstack/react-router' {\n interface FileRoutesByPath {\n '/_layout': {\n id: '/_layout'\n path: ''\n fullPath: '/'\n preLoaderRoute: typeof LayoutRouteImport\n parentRoute: typeof rootRouteImport\n }\n '/_layout/': {\n id: '/_layout/'\n path: '/'\n fullPath: '/'\n preLoaderRoute: typeof LayoutIndexRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/auth/callback': {\n id: '/auth/callback'\n path: '/auth/callback'\n fullPath: '/auth/callback'\n preLoaderRoute: typeof AuthCallbackRouteImport\n parentRoute: typeof rootRouteImport\n }\n '/_layout/login': {\n id: '/_layout/login'\n path: '/login'\n fullPath: '/login'\n preLoaderRoute: typeof LayoutLoginRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/_layout/app/$slug': {\n id: '/_layout/app/$slug'\n path: '/app/$slug'\n fullPath: '/app/$slug'\n preLoaderRoute: typeof LayoutAppSlugRouteImport\n parentRoute: typeof LayoutRoute\n }\n '/_layout/catalog/apps/': {\n id: '/_layout/catalog/apps/'\n path: '/catalog/apps'\n fullPath: '/catalog/apps/'\n preLoaderRoute: typeof LayoutCatalogAppsIndexRouteImport\n parentRoute: typeof LayoutRoute\n }\n }\n}\n\ninterface LayoutRouteChildren {\n LayoutLoginRoute: typeof LayoutLoginRoute\n LayoutIndexRoute: typeof LayoutIndexRoute\n LayoutAppSlugRoute: typeof LayoutAppSlugRoute\n LayoutCatalogAppsIndexRoute: typeof LayoutCatalogAppsIndexRoute\n}\n\nconst LayoutRouteChildren: LayoutRouteChildren = {\n LayoutLoginRoute: LayoutLoginRoute,\n LayoutIndexRoute: LayoutIndexRoute,\n LayoutAppSlugRoute: LayoutAppSlugRoute,\n LayoutCatalogAppsIndexRoute: LayoutCatalogAppsIndexRoute,\n}\n\nconst LayoutRouteWithChildren =\n LayoutRoute._addFileChildren(LayoutRouteChildren)\n\nconst rootRouteChildren: RootRouteChildren = {\n LayoutRoute: LayoutRouteWithChildren,\n AuthCallbackRoute: AuthCallbackRoute,\n}\nexport const routeTree = rootRouteImport\n ._addFileChildren(rootRouteChildren)\n ._addFileTypes<FileRouteTypes>()\n"],"names":["LayoutRouteImport","rootRouteImport","LayoutIndexRouteImport","AuthCallbackRouteImport","LayoutLoginRouteImport","LayoutAppSlugRouteImport","LayoutCatalogAppsIndexRouteImport"],"mappings":";;;;;;;AAkBA,MAAM,cAAcA,QAAkB,OAAO;AAAA,EAC3C,IAAI;AAAA,EACJ,gBAAgB,MAAMC;AACxB,CAAQ;AACR,MAAM,mBAAmBC,QAAuB,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,oBAAoBC,QAAwB,OAAO;AAAA,EACvD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAMF;AACxB,CAAQ;AACR,MAAM,mBAAmBG,QAAuB,OAAO;AAAA,EACrD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,qBAAqBC,QAAyB,OAAO;AAAA,EACzD,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AACR,MAAM,8BAA8BC,QAAkC,OAAO;AAAA,EAC3E,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,MAAM;AACxB,CAAQ;AAmGR,MAAM,sBAA2C;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,0BACJ,YAAY,iBAAiB,mBAAmB;AAElD,MAAM,oBAAuC;AAAA,EAC3C,aAAa;AAAA,EACb;AACF;AACO,MAAM,YAAYL,MACtB,iBAAiB,iBAAiB,EAClC,cAAA;"}
@@ -0,0 +1,3 @@
1
+ export declare const Route: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('../../types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/app/$slug", "/app/$slug", "/_layout/app/$slug", "/_layout/app/$slug", undefined, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
2
+ appCatalogLoader: import('~/modules/appCatalog/routeLoader').AppCatalogLoaderReturn;
3
+ }>, unknown, unknown, unknown, unknown, undefined>;
@@ -0,0 +1,21 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createFileRoute } from "@tanstack/react-router";
3
+ import { appCatalogRouteLoader } from "../../modules/appCatalog/routeLoader.js";
4
+ import { AppCatalogLayout } from "../../modules/appCatalog/ui/layout/AppCatalogLayout.js";
5
+ import { AppCatalogPage } from "../../modules/appCatalog/ui/pages/AppCatalogPage.js";
6
+ const Route = createFileRoute("/_layout/app/$slug")({
7
+ component: RouteComponent,
8
+ async loader() {
9
+ const appCatalogLoader = await appCatalogRouteLoader();
10
+ return { appCatalogLoader };
11
+ }
12
+ });
13
+ function RouteComponent() {
14
+ const { queryClient, trpcClient } = Route.useRouteContext();
15
+ const { slug } = Route.useParams();
16
+ return /* @__PURE__ */ jsx(AppCatalogLayout, { queryClient, trpcClient, children: /* @__PURE__ */ jsx(AppCatalogPage, { selectedSlug: slug }) });
17
+ }
18
+ export {
19
+ Route
20
+ };
21
+ //# sourceMappingURL=app._slug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app._slug.js","sources":["../../../../src/routes/_layout/app.$slug.tsx"],"sourcesContent":["import { createFileRoute } from '@tanstack/react-router'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\nexport const Route = createFileRoute('/_layout/app/$slug')({\n component: RouteComponent,\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return { appCatalogLoader }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n const { slug } = Route.useParams()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage selectedSlug={slug} />\n </AppCatalogLayout>\n )\n}\n"],"names":[],"mappings":";;;;;AAKO,MAAM,QAAQ,gBAAgB,oBAAoB,EAAE;AAAA,EACzD,WAAW;AAAA,EACX,MAAM,SAAS;AACb,UAAM,mBAAmB,MAAM,sBAAA;AAC/B,WAAO,EAAE,iBAAA;AAAA,EACX;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAC1C,QAAM,EAAE,KAAA,IAAS,MAAM,UAAA;AAEvB,SACE,oBAAC,oBAAiB,aAA0B,YAC1C,8BAAC,gBAAA,EAAe,cAAc,MAAM,EAAA,CACtC;AAEJ;"}
@@ -1,6 +1,5 @@
1
1
  import { z } from 'zod';
2
2
  export declare const Route: import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/router-core').Route<import('@tanstack/react-router').Register, import('@tanstack/react-router').RootRoute<import('@tanstack/react-router').Register, undefined, import('../../types/types.js').AcRouterContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_layout", "/_layout", undefined, import('@tanstack/router-core').ResolveParams<"">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/", "/", "/_layout/", "/_layout/", z.ZodObject<{
3
- app: z.ZodOptional<z.ZodString>;
4
3
  filterTag: z.ZodOptional<z.ZodString>;
5
4
  }, z.core.$strip>, import('@tanstack/router-core').ResolveParams<"/">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
6
5
  appCatalogLoader: import('~/modules/appCatalog/routeLoader').AppCatalogLoaderReturn;
@@ -5,7 +5,6 @@ import { AppCatalogLayout } from "../../modules/appCatalog/ui/layout/AppCatalogL
5
5
  import { AppCatalogPage } from "../../modules/appCatalog/ui/pages/AppCatalogPage.js";
6
6
  import { object, string } from "../../node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js";
7
7
  const searchSchema = object({
8
- app: string().optional(),
9
8
  filterTag: string().optional()
10
9
  });
11
10
  const Route = createFileRoute("/_layout/")({
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/routes/_layout/index.tsx"],"sourcesContent":["import { createFileRoute } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\nconst searchSchema = z.object({\n app: z.string().optional(),\n filterTag: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return {\n appCatalogLoader,\n }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAMA,MAAM,eAAeA,OAAS;AAAA,EAC5B,KAAKC,OAAE,EAAS,SAAA;AAAA,EAChB,WAAWA,OAAE,EAAS,SAAA;AACxB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,WAAW,EAAE;AAAA,EAChD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,MAAM,SAAS;AACb,UAAM,mBAAmB,MAAM,sBAAA;AAC/B,WAAO;AAAA,MACL;AAAA,IAAA;AAAA,EAEJ;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAE1C,6BACG,kBAAA,EAAiB,aAA0B,YAC1C,UAAA,oBAAC,kBAAe,GAClB;AAEJ;"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/routes/_layout/index.tsx"],"sourcesContent":["import { createFileRoute } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\nconst searchSchema = z.object({\n filterTag: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return {\n appCatalogLoader,\n }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <AppCatalogPage />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAMA,MAAM,eAAeA,OAAS;AAAA,EAC5B,WAAWC,OAAE,EAAS,SAAA;AACxB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,WAAW,EAAE;AAAA,EAChD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,MAAM,SAAS;AACb,UAAM,mBAAmB,MAAM,sBAAA;AAC/B,WAAO;AAAA,MACL;AAAA,IAAA;AAAA,EAEJ;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAE1C,6BACG,kBAAA,EAAiB,aAA0B,YAC1C,UAAA,oBAAC,kBAAe,GAClB;AAEJ;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-frontend-core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Frontend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -134,8 +134,8 @@
134
134
  "vite-plugin-static-copy": "^3.1.4",
135
135
  "vite-plugin-svgr": "^4.2.0",
136
136
  "vitest": "^4.1.2",
137
- "@igstack/app-catalog-backend-core": "0.4.0",
138
- "@igstack/app-catalog-shared-core": "0.4.0"
137
+ "@igstack/app-catalog-backend-core": "0.6.0",
138
+ "@igstack/app-catalog-shared-core": "0.6.0"
139
139
  },
140
140
  "peerDependencies": {
141
141
  "react": "19.1.2",
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { fireEvent, waitFor } from '@testing-library/react'
3
+ import '@testing-library/jest-dom/vitest'
4
+
5
+ import { given } from './harness/given'
6
+ import { magazine } from './mock-backend/magazines'
7
+
8
+ describe('App deep-link routing', () => {
9
+ it('opens the app detail when navigating directly to /app/<slug>', async () => {
10
+ const { ui } = await given(magazine.full(), { initialRoute: '/app/jira' })
11
+ await waitFor(() => {
12
+ expect(ui.catalog.isDetailPanelOpen()).toBe(true)
13
+ })
14
+ expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
15
+ })
16
+
17
+ it('navigates to /app/<slug> when selecting an app, and pushes history', async () => {
18
+ const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
19
+ await ui.catalog.openApp('Jira')
20
+ await waitFor(() => {
21
+ expect(router.state.location.pathname).toBe('/app/jira')
22
+ })
23
+ // push (not replace): catalog '/' is still in history, Back returns to it
24
+ expect(router.history.canGoBack()).toBe(true)
25
+ })
26
+
27
+ it('renders the catalog with nothing open for an unknown slug', async () => {
28
+ const { ui } = await given(magazine.full(), {
29
+ initialRoute: '/app/does-not-exist',
30
+ })
31
+ await waitFor(() => {
32
+ expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
33
+ })
34
+ expect(ui.catalog.isDetailPanelOpen()).toBe(false)
35
+ })
36
+
37
+ it('returns to / when the detail panel is closed', async () => {
38
+ const { ui, router } = await given(magazine.full(), { initialRoute: '/' })
39
+ await ui.catalog.openApp('Jira')
40
+ await waitFor(() => expect(ui.catalog.isDetailPanelOpen()).toBe(true))
41
+ fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
42
+ await waitFor(() => {
43
+ expect(ui.catalog.isDetailPanelOpen()).toBe(false)
44
+ })
45
+ expect(router.state.location.pathname).toBe('/')
46
+ })
47
+
48
+ it('ignores legacy ?app= query param (no detail opens)', async () => {
49
+ const { ui } = await given(magazine.full(), { initialRoute: '/?app=jira' })
50
+ await waitFor(() => {
51
+ expect(ui.catalog.getTableData().length).toBeGreaterThan(0)
52
+ })
53
+ expect(ui.catalog.isDetailPanelOpen()).toBe(false)
54
+ })
55
+ })
@@ -32,6 +32,7 @@ import { GalleryTools } from '../tools/GalleryTools'
32
32
  export interface GivenResult {
33
33
  ui: UiTools
34
34
  backend: MockBackendVerifier
35
+ router: ReturnType<typeof createAcRouter>
35
36
  }
36
37
 
37
38
  export interface UiTools {
@@ -57,7 +58,10 @@ export async function cleanupTestResources(): Promise<void> {
57
58
  }
58
59
  }
59
60
 
60
- export async function given(magazine: Magazine): Promise<GivenResult> {
61
+ export async function given(
62
+ magazine: Magazine,
63
+ opts: { initialRoute?: string } = {},
64
+ ): Promise<GivenResult> {
61
65
  // Clean up any previous resources
62
66
  await cleanupTestResources()
63
67
  resetConfigurerCounter()
@@ -126,7 +130,7 @@ export async function given(magazine: Magazine): Promise<GivenResult> {
126
130
 
127
131
  const router = createAcRouter({
128
132
  history: createMemoryHistory({
129
- initialEntries: ['/catalog/apps'],
133
+ initialEntries: [opts.initialRoute ?? '/catalog/apps'],
130
134
  }),
131
135
  context: {
132
136
  queryClient,
@@ -187,5 +191,5 @@ export async function given(magazine: Magazine): Promise<GivenResult> {
187
191
  globalError: () => getGlobalError(),
188
192
  }
189
193
 
190
- return { ui, backend }
194
+ return { ui, backend, router }
191
195
  }
@@ -20,6 +20,7 @@ import {
20
20
  TableHeader,
21
21
  TableRow,
22
22
  } from '~/ui/table'
23
+ import { MarkdownLink } from './MarkdownText'
23
24
  import { PersonBadge } from './PersonBadge'
24
25
 
25
26
  // Constants
@@ -30,24 +31,6 @@ interface AccessRequestSectionProps {
30
31
  approvalMethods: AppApprovalMethod[]
31
32
  }
32
33
 
33
- // Component for rendering markdown links with security attributes
34
- const MarkdownLink = ({
35
- href,
36
- children,
37
- }: {
38
- href?: string
39
- children?: React.ReactNode
40
- }) => (
41
- <a
42
- href={href}
43
- target="_blank"
44
- rel="noopener noreferrer"
45
- className="text-primary hover:underline"
46
- >
47
- {children}
48
- </a>
49
- )
50
-
51
34
  // Helper function for approval method icons
52
35
  function getApprovalMethodIcon(
53
36
  type: 'service' | 'personTeam' | 'custom' | 'noAccessRequired' | 'unknown',
@@ -0,0 +1,42 @@
1
+ import type React from 'react'
2
+ import ReactMarkdown from 'react-markdown'
3
+
4
+ /**
5
+ * Renders a markdown link with security attributes (opens in a new tab,
6
+ * no referrer/opener leakage). Shared so all catalog text fields render
7
+ * links identically.
8
+ */
9
+ export const MarkdownLink = ({
10
+ href,
11
+ children,
12
+ }: {
13
+ href?: string
14
+ children?: React.ReactNode
15
+ }) => (
16
+ <a
17
+ href={href}
18
+ target="_blank"
19
+ rel="noopener noreferrer"
20
+ className="text-primary hover:underline"
21
+ >
22
+ {children}
23
+ </a>
24
+ )
25
+
26
+ /**
27
+ * Renders user-facing catalog text (descriptions, comments, prompts) as
28
+ * markdown so links become clickable. Links use {@link MarkdownLink}.
29
+ */
30
+ export function MarkdownText({
31
+ children,
32
+ className,
33
+ }: {
34
+ children: string
35
+ className?: string
36
+ }) {
37
+ return (
38
+ <span className={className}>
39
+ <ReactMarkdown components={{ a: MarkdownLink }}>{children}</ReactMarkdown>
40
+ </span>
41
+ )
42
+ }
@@ -31,6 +31,7 @@ import {
31
31
  import { AccessRequestSection } from '../components/AccessRequestSection'
32
32
  import { useUser } from '~/modules/auth'
33
33
  import { InlineEditableField } from '../components/InlineEditableField'
34
+ import { MarkdownText } from '../components/MarkdownText'
34
35
  import { ScreenshotGallery } from '../components/ScreenshotGallery'
35
36
  import { useUpdateApp } from '../../hooks/useUpdateApp'
36
37
  import { useAppCatalogContext } from '../../context/AppCatalogContext'
@@ -54,6 +55,8 @@ export interface AppCatalogGridProps {
54
55
  totalAppsCount?: number
55
56
  /** Callback to clear all filters and search */
56
57
  onClearFilters?: () => void
58
+ /** Called when the user closes the detail panel (Esc or X) — e.g. to navigate back to the catalog URL */
59
+ onClosePanel?: () => void
57
60
  }
58
61
 
59
62
  function getIconUrl(iconName: string): string {
@@ -395,10 +398,12 @@ function AppDetails({
395
398
  placeholder="Description"
396
399
  className="min-h-[4rem] resize-y text-sm text-muted-foreground"
397
400
  />
401
+ ) : app.description ? (
402
+ <MarkdownText className="prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0">
403
+ {app.description}
404
+ </MarkdownText>
398
405
  ) : (
399
- <p className="text-sm text-muted-foreground">
400
- {app.description || '—'}
401
- </p>
406
+ <p className="text-sm text-muted-foreground">—</p>
402
407
  )}
403
408
  </div>
404
409
 
@@ -694,6 +699,7 @@ export function AppCatalogGrid({
694
699
  searchQuery,
695
700
  totalAppsCount,
696
701
  onClearFilters,
702
+ onClosePanel,
697
703
  }: AppCatalogGridProps) {
698
704
  const selectedApp = selectedAppSlug
699
705
  ? apps.find((a) => a.slug === selectedAppSlug)
@@ -859,6 +865,7 @@ export function AppCatalogGrid({
859
865
 
860
866
  const handleClosePanel = () => {
861
867
  setHasUserClosed(true)
868
+ onClosePanel?.()
862
869
  }
863
870
 
864
871
  return (