@igstack/app-catalog-frontend-core 0.10.1 → 0.11.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.
- package/dist/esm/__tests__/integration/harness/given.d.ts +1 -0
- package/dist/esm/__tests__/integration/skewWarning.integration.test.d.ts +0 -0
- package/dist/esm/__tests__/integration/tools/AppDetailTools.d.ts +5 -0
- package/dist/esm/__tests__/integration/tools/CatalogTools.d.ts +15 -1
- package/dist/esm/__tests__/modules/appCatalog/utils/resolveHelpers.test.d.ts +1 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.d.ts +40 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js +40 -0
- package/dist/esm/modules/appCatalog/hooks/useSessionSyncedState.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.d.ts +17 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js +61 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessPrerequisiteChain.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js +9 -2
- package/dist/esm/modules/appCatalog/ui/components/AccessRequestSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js +1 -1
- package/dist/esm/modules/appCatalog/ui/components/OnboardingCard.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.d.ts +9 -1
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +12 -3
- package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js +3 -2
- package/dist/esm/modules/appCatalog/ui/context/AppCatalogFiltersContext.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.d.ts +5 -0
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js +56 -10
- package/dist/esm/modules/appCatalog/ui/grid/AppCatalogGrid.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.d.ts +12 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js +48 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherDetailPanel.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.d.ts +19 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js +355 -0
- package/dist/esm/modules/appCatalog/ui/launcher/LauncherHome.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.d.ts +13 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js +45 -0
- package/dist/esm/modules/appCatalog/ui/launcher/ResourceIcon.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.d.ts +4 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.js +41 -0
- package/dist/esm/modules/appCatalog/ui/launcher/monogram.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js +40 -6
- package/dist/esm/modules/appCatalog/ui/pages/AppCatalogPage.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.d.ts +1 -1
- package/dist/esm/modules/appCatalog/utils/markdownToPlainText.js.map +1 -1
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.d.ts +10 -0
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js +17 -0
- package/dist/esm/modules/appCatalog/utils/resolveHelpers.js.map +1 -1
- package/dist/esm/routeTree.gen.d.ts +1 -1
- package/dist/esm/routes/_layout/app.$slug.d.ts +1 -1
- package/dist/esm/routes/_layout/app._slug.js +4 -3
- package/dist/esm/routes/_layout/app._slug.js.map +1 -1
- package/dist/esm/routes/_layout/catalog.apps.index.js +2 -1
- package/dist/esm/routes/_layout/catalog.apps.index.js.map +1 -1
- package/dist/esm/routes/_layout/index.js +2 -1
- package/dist/esm/routes/_layout/index.js.map +1 -1
- package/dist/esm/routes/_layout/service-desks.js +2 -1
- package/dist/esm/routes/_layout/service-desks.js.map +1 -1
- package/dist/esm/ui/components/header/Header.js +1 -1
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/index.css +73 -66
- package/package.json +4 -4
- package/src/__tests__/integration/appCatalog.integration.test.ts +106 -0
- package/src/__tests__/integration/appDeepLink.integration.test.ts +50 -24
- package/src/__tests__/integration/crossRefLinkListView.integration.test.ts +8 -8
- package/src/__tests__/integration/harness/given.tsx +9 -1
- package/src/__tests__/integration/replacementLink.integration.test.ts +6 -2
- package/src/__tests__/integration/searchDeprecatedFallback.integration.test.ts +21 -16
- package/src/__tests__/integration/setup/testSetup.ts +1 -0
- package/src/__tests__/integration/skewWarning.integration.test.ts +56 -0
- package/src/__tests__/integration/tools/AppDetailTools.ts +13 -0
- package/src/__tests__/integration/tools/CatalogTools.ts +60 -11
- package/src/__tests__/modules/appCatalog/utils/markdownToPlainText.test.ts +5 -5
- package/src/__tests__/modules/appCatalog/utils/resolveHelpers.test.ts +79 -0
- package/src/index.css +73 -66
- package/src/modules/appCatalog/hooks/useSessionSyncedState.ts +83 -0
- package/src/modules/appCatalog/ui/components/AccessPrerequisiteChain.tsx +89 -0
- package/src/modules/appCatalog/ui/components/AccessRequestSection.tsx +31 -2
- package/src/modules/appCatalog/ui/components/OnboardingCard.tsx +1 -1
- package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +22 -1
- package/src/modules/appCatalog/ui/context/AppCatalogFiltersContext.tsx +11 -5
- package/src/modules/appCatalog/ui/grid/AppCatalogGrid.tsx +68 -10
- package/src/modules/appCatalog/ui/launcher/LauncherDetailPanel.tsx +55 -0
- package/src/modules/appCatalog/ui/launcher/LauncherHome.tsx +497 -0
- package/src/modules/appCatalog/ui/launcher/ResourceIcon.tsx +54 -0
- package/src/modules/appCatalog/ui/launcher/monogram.ts +41 -0
- package/src/modules/appCatalog/ui/pages/AppCatalogPage.tsx +75 -13
- package/src/modules/appCatalog/utils/markdownToPlainText.ts +1 -1
- package/src/modules/appCatalog/utils/resolveHelpers.ts +31 -0
- package/src/routes/_layout/app.$slug.tsx +7 -4
- package/src/routes/_layout/catalog.apps.index.tsx +4 -1
- package/src/routes/_layout/index.tsx +5 -1
- package/src/routes/_layout/service-desks.tsx +4 -1
- package/src/ui/components/header/Header.tsx +3 -1
|
@@ -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 { 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 // Dev-only skew warning: data arrived (resources > 0) but nothing is top-level\n // (rootResources === 0). That fingerprints a frontend/backend-core version skew\n // or a stale service worker — NOT a normal empty search/filter (those still have\n // rootResources). Guarded to dev so prod users never see it.\n useEffect(() => {\n if (\n import.meta.env.DEV &&\n !isLoadingApps &&\n resources.length > 0 &&\n rootResources.length === 0\n ) {\n console.warn(\n `[app-catalog] Loaded ${resources.length} resources but 0 are top-level — ` +\n `likely a frontend/backend-core version skew or a stale service worker. ` +\n `Check the version footer (be X / fe Y) and hard-reload.`,\n )\n }\n }, [isLoadingApps, resources.length, rootResources.length])\n\n const { filteredApps, didDeprecatedFallback } = useMemo(() => {\n const childResources = resources.filter((r) => r.parentSlug)\n\n // Apply recent mode / tag filters / search over a base set of root apps.\n const runPipeline = (base: typeof rootResources) => {\n let result = base\n\n // 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 // Apply search (using deferred value). Pass all resources so children\n // contribute to parent scoring.\n return searchResources(\n [...result, ...childResources],\n deferredSearchValue,\n )\n }\n\n // Matches among non-deprecated (\"active\") apps only.\n const activeMatches = runPipeline(\n rootResources.filter((app) => !app.deprecated),\n )\n\n // #11: derive the fallback purely from the data (not the toggle), so it\n // stays stable after the toggle auto-enables below — otherwise the notice\n // would flicker off as soon as showDeprecated flips to true.\n // Fallback applies when there's a query, no active matches, but deprecated\n // apps DO match.\n const hasQuery = deferredSearchValue.trim() !== ''\n if (hasQuery && activeMatches.length === 0) {\n const allMatches = runPipeline(rootResources)\n if (allMatches.length > 0) {\n return { filteredApps: allMatches, didDeprecatedFallback: true }\n }\n }\n\n // Normal display: include deprecated only when the toggle is on.\n const result = filterState.showDeprecated\n ? runPipeline(rootResources)\n : activeMatches\n return { filteredApps: result, didDeprecatedFallback: false }\n }, [\n rootResources,\n resources,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\n\n // #11: when the search fell back to deprecated results, enable the\n // \"Show Deprecated Apps\" toggle so the state is visible and consistent.\n // Runs as an effect (not in render) to avoid a side effect during useMemo.\n useEffect(() => {\n if (didDeprecatedFallback && !filterState.showDeprecated) {\n actions.setShowDeprecated(true)\n }\n }, [didDeprecatedFallback, filterState.showDeprecated, actions])\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. Carry the *current* search value into\n // the URL (`q`) as part of this navigation so the search input and focus\n // survive the route change (#10). We must inject `searchValue` explicitly\n // rather than rely on `(prev) => prev`: this child effect runs before the\n // provider's async state→URL sync effect, so at nav time the URL does not yet\n // hold `q` and the query would otherwise be lost on remount.\n useEffect(() => {\n if (filteredApps.length === 1 && filteredApps[0] && !selectedAppSlug) {\n void navigate({\n to: '/app/$slug',\n params: { slug: filteredApps[0].slug },\n search: (prev) => ({\n ...prev,\n q: searchValue === '' ? undefined : searchValue,\n }),\n replace: true,\n })\n }\n }, [filteredApps, selectedAppSlug, navigate, searchValue])\n\n // #22: alias → canonical redirect. When an app is renamed its old slug is\n // kept in `aliases[]`. If the URL slug matches no canonical slug but does\n // match some app's alias, redirect (replace) to that app's canonical slug so\n // old bookmarks resolve instead of showing a blank catalog. Guard on a real\n // canonical miss so we never fight the normal detail-open path. Note: this is\n // a client-side SPA redirect (replace), not an HTTP 301 — see #22.\n useEffect(() => {\n if (!selectedAppSlug || resources.length === 0) return\n const canonical = resources.some((r) => r.slug === selectedAppSlug)\n if (canonical) return\n const aliased = resources.find((r) => r.aliases?.includes(selectedAppSlug))\n if (aliased) {\n void navigate({\n to: '/app/$slug',\n params: { slug: aliased.slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [selectedAppSlug, resources, navigate])\n\n const handleAppClick = (app: Resource) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: app.slug },\n search: (prev) => prev,\n })\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 {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"shrink-0 px-1 pb-2 text-sm text-muted-foreground\"\n >\n {`No active apps${\n searchValue ? ` for \"${searchValue}\"` : ''\n } — showing deprecated matches.`}\n </div>\n )}\n\n <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 && !selectedAppSlug ? (\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":["result"],"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'\nimport { LauncherHome } from '../launcher/LauncherHome'\nimport { LauncherDetailPanel } from '../launcher/LauncherDetailPanel'\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 // Dev-only skew warning: data arrived (resources > 0) but nothing is top-level\n // (rootResources === 0). That fingerprints a frontend/backend-core version skew\n // or a stale service worker — NOT a normal empty search/filter (those still have\n // rootResources). Guarded to dev so prod users never see it.\n useEffect(() => {\n if (\n import.meta.env.DEV &&\n !isLoadingApps &&\n resources.length > 0 &&\n rootResources.length === 0\n ) {\n console.warn(\n `[app-catalog] Loaded ${resources.length} resources but 0 are top-level — ` +\n `likely a frontend/backend-core version skew or a stale service worker. ` +\n `Check the version footer (be X / fe Y) and hard-reload.`,\n )\n }\n }, [isLoadingApps, resources.length, rootResources.length])\n\n const { filteredApps, didDeprecatedFallback } = useMemo(() => {\n const childResources = resources.filter((r) => r.parentSlug)\n\n // Apply recent mode / tag filters / search over a base set of root apps.\n const runPipeline = (base: typeof rootResources) => {\n let result = base\n\n // 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 // Apply search (using deferred value). Pass all resources so children\n // contribute to parent scoring.\n return searchResources(\n [...result, ...childResources],\n deferredSearchValue,\n )\n }\n\n // Matches among non-deprecated (\"active\") apps only.\n const activeMatches = runPipeline(\n rootResources.filter((app) => !app.deprecated),\n )\n\n // #11: derive the fallback purely from the data (not the toggle), so it\n // stays stable after the toggle auto-enables below — otherwise the notice\n // would flicker off as soon as showDeprecated flips to true.\n // Fallback applies when there's a query, no active matches, but deprecated\n // apps DO match.\n const hasQuery = deferredSearchValue.trim() !== ''\n if (hasQuery && activeMatches.length === 0) {\n const allMatches = runPipeline(rootResources)\n if (allMatches.length > 0) {\n return { filteredApps: allMatches, didDeprecatedFallback: true }\n }\n }\n\n // Normal display: include deprecated only when the toggle is on.\n const result = filterState.showDeprecated\n ? runPipeline(rootResources)\n : activeMatches\n return { filteredApps: result, didDeprecatedFallback: false }\n }, [\n rootResources,\n resources,\n deferredSearchValue,\n filterState.recentMode,\n filterState.tagFilters,\n filterState.showDeprecated,\n topAppSlugs,\n ])\n\n // #11: when the search fell back to deprecated results, enable the\n // \"Show Deprecated Apps\" toggle so the state is visible and consistent.\n // Runs as an effect (not in render) to avoid a side effect during useMemo.\n useEffect(() => {\n if (didDeprecatedFallback && !filterState.showDeprecated) {\n actions.setShowDeprecated(true)\n }\n }, [didDeprecatedFallback, filterState.showDeprecated, actions])\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 — ONLY in the legacy grid path\n // (recent/tag filters active). In the launcher (#38) typing shows the\n // search-morph results list; auto-navigating to /app/<slug> on a single\n // match would yank the user out of the launcher into the old grid+panel\n // (reported bug: searching \"biom\" jumped straight to an app-detail page).\n // The morph already renders the single match as a keyboard-selectable row —\n // the user presses ↵ or clicks to open it.\n const legacyGridActive =\n filterState.recentMode || Object.keys(filterState.tagFilters).length > 0\n useEffect(() => {\n if (\n legacyGridActive &&\n filteredApps.length === 1 &&\n filteredApps[0] &&\n !selectedAppSlug\n ) {\n void navigate({\n to: '/app/$slug',\n params: { slug: filteredApps[0].slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [legacyGridActive, filteredApps, selectedAppSlug, navigate])\n\n // #22: alias → canonical redirect. When an app is renamed its old slug is\n // kept in `aliases[]`. If the URL slug matches no canonical slug but does\n // match some app's alias, redirect (replace) to that app's canonical slug so\n // old bookmarks resolve instead of showing a blank catalog. Guard on a real\n // canonical miss so we never fight the normal detail-open path. Note: this is\n // a client-side SPA redirect (replace), not an HTTP 301 — see ig-umbrella#22.\n useEffect(() => {\n if (!selectedAppSlug || resources.length === 0) return\n const canonical = resources.some((r) => r.slug === selectedAppSlug)\n if (canonical) return\n const aliased = resources.find((r) => r.aliases?.includes(selectedAppSlug))\n if (aliased) {\n void navigate({\n to: '/app/$slug',\n params: { slug: aliased.slug },\n search: (prev) => prev,\n replace: true,\n })\n }\n }, [selectedAppSlug, resources, navigate])\n\n const handleAppClick = (app: Resource) => {\n void navigate({\n to: '/app/$slug',\n params: { slug: app.slug },\n search: (prev) => prev,\n })\n }\n\n const handleLaunch = (app: Resource) => {\n if (app.appUrl) window.open(app.appUrl, '_blank', 'noopener,noreferrer')\n }\n\n // Adaptive-home launcher (#38): shown whenever no structural filter\n // (recentMode / tagFilters) is active. A selected app no longer forces the\n // old grid — instead the launcher stays as the backdrop and the app detail\n // renders in a slide-over panel (#38 item B). The legacy grid+split-pane is\n // reserved for the recent/tag filter views.\n const showLauncherHome =\n !filterState.recentMode && Object.keys(filterState.tagFilters).length === 0\n\n // The app whose detail slide-over is open over the launcher backdrop.\n const launcherSelectedApp = useMemo(\n () =>\n selectedAppSlug\n ? (resources.find((r) => r.slug === selectedAppSlug) ?? null)\n : null,\n [selectedAppSlug, resources],\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 // Adaptive-home launcher view (#38): the discovery spine or the search-morph\n // results list (handled inside LauncherHome). Owns vertical scroll —\n // MainLayout is h-screen/overflow-hidden, so the launcher must scroll\n // internally to reveal the full Browse-all list as the user scrolls.\n if (showLauncherHome) {\n return (\n <div className=\"flex-1 min-h-0 overflow-y-auto\">\n <LauncherHome\n apps={rootResources.filter(\n (a) => filterState.showDeprecated || !a.deprecated,\n )}\n // All resources incl. children — so search matches sub-resources\n // (e.g. an AWS account) and surfaces their parent, preserving the\n // existing cross-sub-resource search behavior.\n allResources={resources}\n searchValue={searchValue}\n onSearchChange={setSearchValue}\n onAppClick={handleAppClick}\n onLaunch={handleLaunch}\n totalCount={totalAppsCount}\n />\n {/* #38 item B: app detail as a slide-over over the launcher backdrop,\n instead of dropping into the old grid + split-pane. */}\n {launcherSelectedApp && (\n <LauncherDetailPanel\n app={launcherSelectedApp}\n onClose={() => void navigate({ to: '/' })}\n onAppClick={handleAppClick}\n />\n )}\n </div>\n )\n }\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 {didDeprecatedFallback && (\n <div\n role=\"status\"\n className=\"shrink-0 px-1 pb-2 text-sm text-muted-foreground\"\n >\n {`No active apps${\n searchValue ? ` for \"${searchValue}\"` : ''\n } — showing deprecated matches.`}\n </div>\n )}\n\n <div className=\"flex-1 min-h-0\">\n {filteredApps.length === 0 && !selectedAppSlug ? (\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":["result"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Reduce a short markdown string to its visible plain text.
|
|
3
3
|
*
|
|
4
4
|
* Descriptions may contain markdown — notably cross-reference links like
|
|
5
|
-
* `[
|
|
5
|
+
* `[Signatera Portal](/app/signatera-portal)` (#25). The detail panel renders
|
|
6
6
|
* those as interactive links via {@link MarkdownText}, but in compact / clamped
|
|
7
7
|
* surfaces (grid list preview, table row, filter combobox, sub-resources) an
|
|
8
8
|
* interactive link would break `line-clamp` and can't be reliably highlighted
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdownToPlainText.js","sources":["../../../../../src/modules/appCatalog/utils/markdownToPlainText.ts"],"sourcesContent":["/**\n * Reduce a short markdown string to its visible plain text.\n *\n * Descriptions may contain markdown — notably cross-reference links like\n * `[
|
|
1
|
+
{"version":3,"file":"markdownToPlainText.js","sources":["../../../../../src/modules/appCatalog/utils/markdownToPlainText.ts"],"sourcesContent":["/**\n * Reduce a short markdown string to its visible plain text.\n *\n * Descriptions may contain markdown — notably cross-reference links like\n * `[Signatera Portal](/app/signatera-portal)` (#25). The detail panel renders\n * those as interactive links via {@link MarkdownText}, but in compact / clamped\n * surfaces (grid list preview, table row, filter combobox, sub-resources) an\n * interactive link would break `line-clamp` and can't be reliably highlighted\n * for search. There we show the *text* only — so `[Name](/app/slug)` shows\n * \"Name\", never the raw bracket/paren syntax.\n *\n * This is deliberately a small, allocation-cheap regex pass (not a full\n * markdown parser): it covers the inline constructs that actually appear in\n * catalog descriptions — links, images, emphasis, and inline code.\n */\nexport function markdownToPlainText(input: string): string {\n if (!input) return ''\n\n return (\n input\n // Images:  → alt (must run before links)\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Links: [text](url) → text\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // Bold / italic markers: **x**, __x__, *x*, _x_ → x\n .replace(/(\\*\\*|__)(.*?)\\1/g, '$2')\n .replace(/(\\*|_)(.*?)\\1/g, '$2')\n // Inline code: `x` → x\n .replace(/`([^`]*)`/g, '$1')\n )\n}\n"],"names":[],"mappings":"AAeO,SAAS,oBAAoB,OAAuB;AACzD,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,MAEG,QAAQ,2BAA2B,IAAI,EAEvC,QAAQ,0BAA0B,IAAI,EAEtC,QAAQ,qBAAqB,IAAI,EACjC,QAAQ,kBAAkB,IAAI,EAE9B,QAAQ,cAAc,IAAI;AAEjC;"}
|
|
@@ -3,5 +3,15 @@ export declare function getPersonBySlug(persons: Person[], slug: string): Person
|
|
|
3
3
|
export declare function getGroupBySlug(groups: Group[], slug: string): Group | undefined;
|
|
4
4
|
export declare function getChildResources(resources: Resource[], parentSlug: string): Resource[];
|
|
5
5
|
export declare function getRootResources(resources: Resource[]): Resource[];
|
|
6
|
+
/** The immediate parent of a resource (undefined for a root or if missing). */
|
|
7
|
+
export declare function getParentResource(resources: Resource[], resource: Pick<Resource, 'parentSlug'>): Resource | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The prerequisite chain for a resource: its ancestors that carry an access
|
|
10
|
+
* policy, ordered ROOT → nearest parent. Used to surface the two-step access
|
|
11
|
+
* pattern ("get <parent> access first, then this resource"), which the domain
|
|
12
|
+
* model computes from the tree but users can't see otherwise (#38 item D).
|
|
13
|
+
* Guards against cycles.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getAccessPrerequisiteChain(resources: Resource[], resource: Pick<Resource, 'parentSlug'>): Resource[];
|
|
6
16
|
/** @deprecated Use getChildResources instead */
|
|
7
17
|
export declare const getSubResourcesForApp: typeof getChildResources;
|
|
@@ -7,9 +7,26 @@ function getGroupBySlug(groups, slug) {
|
|
|
7
7
|
function getChildResources(resources, parentSlug) {
|
|
8
8
|
return resources.filter((r) => r.parentSlug === parentSlug);
|
|
9
9
|
}
|
|
10
|
+
function getParentResource(resources, resource) {
|
|
11
|
+
if (!resource.parentSlug) return void 0;
|
|
12
|
+
return resources.find((r) => r.slug === resource.parentSlug);
|
|
13
|
+
}
|
|
14
|
+
function getAccessPrerequisiteChain(resources, resource) {
|
|
15
|
+
const chain = [];
|
|
16
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17
|
+
let current = getParentResource(resources, resource);
|
|
18
|
+
while (current && !seen.has(current.slug)) {
|
|
19
|
+
seen.add(current.slug);
|
|
20
|
+
if (current.accessRequest) chain.unshift(current);
|
|
21
|
+
current = getParentResource(resources, current);
|
|
22
|
+
}
|
|
23
|
+
return chain;
|
|
24
|
+
}
|
|
10
25
|
export {
|
|
26
|
+
getAccessPrerequisiteChain,
|
|
11
27
|
getChildResources,
|
|
12
28
|
getGroupBySlug,
|
|
29
|
+
getParentResource,
|
|
13
30
|
getPersonBySlug
|
|
14
31
|
};
|
|
15
32
|
//# sourceMappingURL=resolveHelpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveHelpers.js","sources":["../../../../../src/modules/appCatalog/utils/resolveHelpers.ts"],"sourcesContent":["import type { Group, Person, Resource } from '@igstack/app-catalog-backend-core'\n\nexport function getPersonBySlug(\n persons: Person[],\n slug: string,\n): Person | undefined {\n return persons.find((p) => p.slug === slug)\n}\n\nexport function getGroupBySlug(\n groups: Group[],\n slug: string,\n): Group | undefined {\n return groups.find((g) => g.slug === slug)\n}\n\nexport function getChildResources(\n resources: Resource[],\n parentSlug: string,\n): Resource[] {\n return resources.filter((r) => r.parentSlug === parentSlug)\n}\n\nexport function getRootResources(resources: Resource[]): Resource[] {\n return resources.filter((r) => !r.parentSlug)\n}\n\n/** @deprecated Use getChildResources instead */\nexport const getSubResourcesForApp = getChildResources\n"],"names":[],"mappings":"AAEO,SAAS,gBACd,SACA,MACoB;AACpB,SAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC5C;AAEO,SAAS,eACd,QACA,MACmB;AACnB,SAAO,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC3C;AAEO,SAAS,kBACd,WACA,YACY;AACZ,SAAO,UAAU,OAAO,CAAC,MAAM,EAAE,eAAe,UAAU;AAC5D;"}
|
|
1
|
+
{"version":3,"file":"resolveHelpers.js","sources":["../../../../../src/modules/appCatalog/utils/resolveHelpers.ts"],"sourcesContent":["import type { Group, Person, Resource } from '@igstack/app-catalog-backend-core'\n\nexport function getPersonBySlug(\n persons: Person[],\n slug: string,\n): Person | undefined {\n return persons.find((p) => p.slug === slug)\n}\n\nexport function getGroupBySlug(\n groups: Group[],\n slug: string,\n): Group | undefined {\n return groups.find((g) => g.slug === slug)\n}\n\nexport function getChildResources(\n resources: Resource[],\n parentSlug: string,\n): Resource[] {\n return resources.filter((r) => r.parentSlug === parentSlug)\n}\n\nexport function getRootResources(resources: Resource[]): Resource[] {\n return resources.filter((r) => !r.parentSlug)\n}\n\n/** The immediate parent of a resource (undefined for a root or if missing). */\nexport function getParentResource(\n resources: Resource[],\n resource: Pick<Resource, 'parentSlug'>,\n): Resource | undefined {\n if (!resource.parentSlug) return undefined\n return resources.find((r) => r.slug === resource.parentSlug)\n}\n\n/**\n * The prerequisite chain for a resource: its ancestors that carry an access\n * policy, ordered ROOT → nearest parent. Used to surface the two-step access\n * pattern (\"get <parent> access first, then this resource\"), which the domain\n * model computes from the tree but users can't see otherwise (#38 item D).\n * Guards against cycles.\n */\nexport function getAccessPrerequisiteChain(\n resources: Resource[],\n resource: Pick<Resource, 'parentSlug'>,\n): Resource[] {\n const chain: Resource[] = []\n const seen = new Set<string>()\n let current = getParentResource(resources, resource)\n while (current && !seen.has(current.slug)) {\n seen.add(current.slug)\n if (current.accessRequest) chain.unshift(current)\n current = getParentResource(resources, current)\n }\n return chain\n}\n\n/** @deprecated Use getChildResources instead */\nexport const getSubResourcesForApp = getChildResources\n"],"names":[],"mappings":"AAEO,SAAS,gBACd,SACA,MACoB;AACpB,SAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC5C;AAEO,SAAS,eACd,QACA,MACmB;AACnB,SAAO,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC3C;AAEO,SAAS,kBACd,WACA,YACY;AACZ,SAAO,UAAU,OAAO,CAAC,MAAM,EAAE,eAAe,UAAU;AAC5D;AAOO,SAAS,kBACd,WACA,UACsB;AACtB,MAAI,CAAC,SAAS,WAAY,QAAO;AACjC,SAAO,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,UAAU;AAC7D;AASO,SAAS,2BACd,WACA,UACY;AACZ,QAAM,QAAoB,CAAA;AAC1B,QAAM,2BAAW,IAAA;AACjB,MAAI,UAAU,kBAAkB,WAAW,QAAQ;AACnD,SAAO,WAAW,CAAC,KAAK,IAAI,QAAQ,IAAI,GAAG;AACzC,SAAK,IAAI,QAAQ,IAAI;AACrB,QAAI,QAAQ,cAAe,OAAM,QAAQ,OAAO;AAChD,cAAU,kBAAkB,WAAW,OAAO;AAAA,EAChD;AACA,SAAO;AACT;"}
|
|
@@ -24,11 +24,11 @@ declare const LayoutServiceDesksRoute: import('@tanstack/router-core').Route<imp
|
|
|
24
24
|
}>, unknown, unknown, unknown, unknown, undefined>;
|
|
25
25
|
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>;
|
|
26
26
|
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", import('zod').ZodObject<{
|
|
27
|
-
q: import('zod').ZodOptional<import('zod').ZodString>;
|
|
28
27
|
filterTag: import('zod').ZodOptional<import('zod').ZodString>;
|
|
29
28
|
recent: import('zod').ZodOptional<import('zod').ZodString>;
|
|
30
29
|
filters: import('zod').ZodOptional<import('zod').ZodString>;
|
|
31
30
|
deprecated: import('zod').ZodOptional<import('zod').ZodString>;
|
|
31
|
+
q: import('zod').ZodOptional<import('zod').ZodString>;
|
|
32
32
|
}, import('zod/v4/core').$strip>, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
|
|
33
33
|
appCatalogLoader: import('./modules/appCatalog/routeLoader.js').AppCatalogLoaderReturn;
|
|
34
34
|
}>, unknown, unknown, unknown, unknown, undefined>;
|
|
@@ -1,10 +1,10 @@
|
|
|
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>, "/app/$slug", "/app/$slug", "/_layout/app/$slug", "/_layout/app/$slug", z.ZodObject<{
|
|
3
|
-
q: z.ZodOptional<z.ZodString>;
|
|
4
3
|
filterTag: z.ZodOptional<z.ZodString>;
|
|
5
4
|
recent: z.ZodOptional<z.ZodString>;
|
|
6
5
|
filters: z.ZodOptional<z.ZodString>;
|
|
7
6
|
deprecated: z.ZodOptional<z.ZodString>;
|
|
7
|
+
q: z.ZodOptional<z.ZodString>;
|
|
8
8
|
}, z.core.$strip>, import('@tanstack/router-core').ResolveParams<"/app/$slug">, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, import('@tanstack/router-core').AnyContext, {}, () => Promise<{
|
|
9
9
|
appCatalogLoader: import('~/modules/appCatalog/routeLoader').AppCatalogLoaderReturn;
|
|
10
10
|
}>, unknown, unknown, unknown, unknown, undefined>;
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
|
3
3
|
import { appCatalogRouteLoader } from "../../modules/appCatalog/routeLoader.js";
|
|
4
4
|
import { AppCatalogLayout } from "../../modules/appCatalog/ui/layout/AppCatalogLayout.js";
|
|
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
|
-
q: string().optional(),
|
|
9
8
|
filterTag: string().optional(),
|
|
10
9
|
recent: string().optional(),
|
|
11
10
|
filters: string().optional(),
|
|
12
|
-
deprecated: string().optional()
|
|
11
|
+
deprecated: string().optional(),
|
|
12
|
+
q: string().optional()
|
|
13
13
|
});
|
|
14
14
|
const Route = createFileRoute("/_layout/app/$slug")({
|
|
15
15
|
component: RouteComponent,
|
|
16
16
|
validateSearch: searchSchema,
|
|
17
|
+
search: { middlewares: [stripSearchParams(["q"])] },
|
|
17
18
|
async loader() {
|
|
18
19
|
const appCatalogLoader = await appCatalogRouteLoader();
|
|
19
20
|
return { appCatalogLoader };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app._slug.js","sources":["../../../../src/routes/_layout/app.$slug.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\n// Preserve the URL-synced filter params (see AppCatalogFiltersContext) on this\n// route so they aren't stripped on navigation
|
|
1
|
+
{"version":3,"file":"app._slug.js","sources":["../../../../src/routes/_layout/app.$slug.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } 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\n// Preserve the URL-synced filter params (see AppCatalogFiltersContext) on this\n// route so they aren't stripped on navigation. `q` (the search text) is the\n// exception: as of #27 it lives in sessionStorage, not the URL. It stays\n// declared here so an incoming legacy `?q=` still validates, but the\n// `stripSearchParams` middleware removes it so shared app links stay clean.\nconst searchSchema = z.object({\n filterTag: z.string().optional(),\n recent: z.string().optional(),\n filters: z.string().optional(),\n deprecated: z.string().optional(),\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/app/$slug')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\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":["z.object","z.string"],"mappings":";;;;;;AAWA,MAAM,eAAeA,OAAS;AAAA,EAC5B,WAAWC,OAAE,EAAS,SAAA;AAAA,EACtB,QAAQA,OAAE,EAAS,SAAA;AAAA,EACnB,SAASA,OAAE,EAAS,SAAA;AAAA,EACpB,YAAYA,OAAE,EAAS,SAAA;AAAA,EACvB,GAAGA,OAAE,EAAS,SAAA;AAChB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,oBAAoB,EAAE;AAAA,EACzD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,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,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
|
3
3
|
import { catalogRouteLoader } from "../../modules/appCatalog/catalogRouteLoader.js";
|
|
4
4
|
import { AppCatalogLayout } from "../../modules/appCatalog/ui/layout/AppCatalogLayout.js";
|
|
5
5
|
import { AppCatalogPage } from "../../modules/appCatalog/ui/pages/AppCatalogPage.js";
|
|
@@ -11,6 +11,7 @@ const searchSchema = object({
|
|
|
11
11
|
const Route = createFileRoute("/_layout/catalog/apps/")({
|
|
12
12
|
component: RouteComponent,
|
|
13
13
|
validateSearch: searchSchema,
|
|
14
|
+
search: { middlewares: [stripSearchParams(["q"])] },
|
|
14
15
|
async loader(ctx) {
|
|
15
16
|
await catalogRouteLoader(ctx);
|
|
16
17
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.apps.index.js","sources":["../../../../src/routes/_layout/catalog.apps.index.tsx"],"sourcesContent":["import { createFileRoute } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { catalogRouteLoader } from '~/modules/appCatalog/catalogRouteLoader'\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 q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/catalog/apps/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n async loader(ctx) {\n await catalogRouteLoader(ctx)\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":";;;;;;
|
|
1
|
+
{"version":3,"file":"catalog.apps.index.js","sources":["../../../../src/routes/_layout/catalog.apps.index.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { catalogRouteLoader } from '~/modules/appCatalog/catalogRouteLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { AppCatalogPage } from '~/modules/appCatalog/ui/pages/AppCatalogPage'\n\n// #27: `q` kept declared for legacy links but stripped from the URL (search\n// text lives in sessionStorage now).\nconst searchSchema = z.object({\n app: z.string().optional(),\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/catalog/apps/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader(ctx) {\n await catalogRouteLoader(ctx)\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":";;;;;;AAQA,MAAM,eAAeA,OAAS;AAAA,EAC5B,KAAKC,OAAE,EAAS,SAAA;AAAA,EAChB,GAAGA,OAAE,EAAS,SAAA;AAChB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,wBAAwB,EAAE;AAAA,EAC7D,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,MAAM,OAAO,KAAK;AAChB,UAAM,mBAAmB,GAAG;AAAA,EAC9B;AACF,CAAC;AAED,SAAS,iBAAiB;AACxB,QAAM,EAAE,aAAa,eAAe,MAAM,gBAAA;AAE1C,6BACG,kBAAA,EAAiB,aAA0B,YAC1C,UAAA,oBAAC,kBAAe,GAClB;AAEJ;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
|
3
3
|
import { appCatalogRouteLoader } from "../../modules/appCatalog/routeLoader.js";
|
|
4
4
|
import { AppCatalogLayout } from "../../modules/appCatalog/ui/layout/AppCatalogLayout.js";
|
|
5
5
|
import { AppCatalogPage } from "../../modules/appCatalog/ui/pages/AppCatalogPage.js";
|
|
@@ -11,6 +11,7 @@ const searchSchema = object({
|
|
|
11
11
|
const Route = createFileRoute("/_layout/")({
|
|
12
12
|
component: RouteComponent,
|
|
13
13
|
validateSearch: searchSchema,
|
|
14
|
+
search: { middlewares: [stripSearchParams(["q"])] },
|
|
14
15
|
async loader() {
|
|
15
16
|
const appCatalogLoader = await appCatalogRouteLoader();
|
|
16
17
|
return {
|
|
@@ -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 filterTag: z.string().optional(),\n q: 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":";;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/routes/_layout/index.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } 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\n// #27: `q` (search text) no longer belongs in the URL — it lives in\n// sessionStorage now. Keep it declared so incoming `?q=` validates, but the\n// `stripSearchParams` middleware removes it so shared/bookmarked links stay clean.\nconst searchSchema = z.object({\n filterTag: z.string().optional(),\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\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":";;;;;;AASA,MAAM,eAAeA,OAAS;AAAA,EAC5B,WAAWC,OAAE,EAAS,SAAA;AAAA,EACtB,GAAGA,OAAE,EAAS,SAAA;AAChB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,WAAW,EAAE;AAAA,EAChD,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,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,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
|
3
3
|
import { appCatalogRouteLoader } from "../../modules/appCatalog/routeLoader.js";
|
|
4
4
|
import { AppCatalogLayout } from "../../modules/appCatalog/ui/layout/AppCatalogLayout.js";
|
|
5
5
|
import { ServiceDesksPage } from "../../modules/appCatalog/ui/pages/ServiceDesksPage.js";
|
|
@@ -10,6 +10,7 @@ const searchSchema = object({
|
|
|
10
10
|
const Route = createFileRoute("/_layout/service-desks")({
|
|
11
11
|
component: RouteComponent,
|
|
12
12
|
validateSearch: searchSchema,
|
|
13
|
+
search: { middlewares: [stripSearchParams(["q"])] },
|
|
13
14
|
async loader() {
|
|
14
15
|
const appCatalogLoader = await appCatalogRouteLoader();
|
|
15
16
|
return { appCatalogLoader };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-desks.js","sources":["../../../../src/routes/_layout/service-desks.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 { ServiceDesksPage } from '~/modules/appCatalog/ui/pages/ServiceDesksPage'\n\nconst searchSchema = z.object({\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/service-desks')({\n component: RouteComponent,\n validateSearch: searchSchema,\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return { appCatalogLoader }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <ServiceDesksPage />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"service-desks.js","sources":["../../../../src/routes/_layout/service-desks.tsx"],"sourcesContent":["import { createFileRoute, stripSearchParams } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { appCatalogRouteLoader } from '~/modules/appCatalog/routeLoader'\nimport { AppCatalogLayout } from '~/modules/appCatalog/ui/layout/AppCatalogLayout'\nimport { ServiceDesksPage } from '~/modules/appCatalog/ui/pages/ServiceDesksPage'\n\n// #27: `q` kept declared for legacy links but stripped from the URL (search\n// text lives in sessionStorage now).\nconst searchSchema = z.object({\n q: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/_layout/service-desks')({\n component: RouteComponent,\n validateSearch: searchSchema,\n search: { middlewares: [stripSearchParams(['q'])] },\n async loader() {\n const appCatalogLoader = await appCatalogRouteLoader()\n return { appCatalogLoader }\n },\n})\n\nfunction RouteComponent() {\n const { queryClient, trpcClient } = Route.useRouteContext()\n\n return (\n <AppCatalogLayout queryClient={queryClient} trpcClient={trpcClient}>\n <ServiceDesksPage />\n </AppCatalogLayout>\n )\n}\n"],"names":["z.object","z.string"],"mappings":";;;;;;AAQA,MAAM,eAAeA,OAAS;AAAA,EAC5B,GAAGC,OAAE,EAAS,SAAA;AAChB,CAAC;AAEM,MAAM,QAAQ,gBAAgB,wBAAwB,EAAE;AAAA,EAC7D,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAA;AAAA,EAChD,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;AAE1C,6BACG,kBAAA,EAAiB,aAA0B,YAC1C,UAAA,oBAAC,oBAAiB,GACpB;AAEJ;"}
|
|
@@ -102,7 +102,7 @@ function Header({ middle }) {
|
|
|
102
102
|
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsx(Link, { to: "/", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
103
103
|
/* @__PURE__ */ jsx(SvgAppCatalog, { className: "h-16 w-16" }),
|
|
104
104
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
105
|
-
/* @__PURE__ */ jsx("span", { className: "text-lg font-
|
|
105
|
+
/* @__PURE__ */ jsx("span", { className: "font-serif text-lg font-semibold", children: "App Catalog" }),
|
|
106
106
|
(appCatalogContextMaybe == null ? void 0 : appCatalogContextMaybe.versions) && /* @__PURE__ */ jsx(VersionDisplay, { versions: appCatalogContextMaybe.versions })
|
|
107
107
|
] })
|
|
108
108
|
] }) }) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Header.js","sources":["../../../../../src/ui/components/header/Header.tsx"],"sourcesContent":["import type {\n AppVersionInfo,\n VersionInfo,\n} from '@igstack/app-catalog-backend-core'\nimport { Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { LogOut } from 'lucide-react'\nimport React, { use } from 'react'\nimport AppCatalogLogo from '~/assets/app-catalog.svg?react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { ThemeSwitcher } from '~/components/ThemeSwitcher'\nimport { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Button } from '~/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '~/ui/dropdown-menu'\nimport { Skeleton } from '~/ui/skeleton'\n\nfunction ShaSuffix({ version }: { version: VersionInfo }) {\n if (!version.sha) return null\n if (version.shaUrl) {\n return (\n <>\n {' '}\n <a\n href={version.shaUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n onClick={(e) => e.stopPropagation()}\n >\n ({version.sha})\n </a>\n </>\n )\n }\n return <span className=\"text-muted-foreground\"> ({version.sha})</span>\n}\n\nfunction VersionItem({\n label,\n version,\n}: {\n label: string\n version: VersionInfo\n}) {\n // Outer element is a <span> (not <a>) so the optional SHA link can be a\n // sibling anchor without producing invalid nested <a> markup.\n return (\n <span className=\"text-xs text-muted-foreground\" title={label}>\n {label}:{' '}\n {version.url ? (\n <a\n href={version.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:text-primary/80 transition-colors font-medium\"\n onClick={(e) => e.stopPropagation()}\n >\n {version.displayName}\n </a>\n ) : (\n version.displayName\n )}\n <ShaSuffix version={version} />\n </span>\n )\n}\n\nexport function VersionDisplay({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-0.5\">\n {versions.backend && (\n <VersionItem label=\"Pipeline\" version={versions.backend} />\n )}\n {versions.coreVersion && (\n <VersionItem label=\"Core\" version={versions.coreVersion} />\n )}\n {versions.frontend && (\n <VersionItem label=\"FE\" version={versions.frontend} />\n )}\n </div>\n )\n}\n\nexport interface HeaderProps {\n middle?: React.ReactNode\n}\n\nexport function Header({ middle }: HeaderProps) {\n const { isLoading } = useAuth()\n const isAuthenticated = useIsAuthenticated()\n const user = useUser()\n const { logout, devLogin } = useAuthActions()\n const { open: openLoginModal } = useAuthModal()\n const appCatalogContextMaybe = use(AppCatalogContext)\n const trpc = useTRPC()\n const { data: providersData } = useQuery(\n trpc.auth.getProviders.queryOptions(),\n )\n\n const handleLogout = async () => {\n try {\n await logout()\n } catch (error) {\n console.error('Logout failed:', error)\n }\n }\n\n const handleLoginClick = () => {\n // Preserve the current URL for redirect after login\n const currentUrl = window.location.pathname + window.location.search\n openLoginModal(currentUrl)\n }\n\n return (\n <div className=\"flex items-center mb-4 pb-4 gap-4 border-b\">\n <div className=\"flex items-center gap-4\">\n <Link to=\"/\">\n <div className=\"flex items-center gap-2\">\n <AppCatalogLogo className=\"h-16 w-16\" />\n <div className=\"flex flex-col\">\n <span className=\"text-lg font-bold\">App Catalog</span>\n {appCatalogContextMaybe?.versions && (\n <VersionDisplay versions={appCatalogContextMaybe.versions} />\n )}\n </div>\n </div>\n </Link>\n </div>\n {middle && <div className=\"flex-1\">{middle}</div>}\n <div className=\"flex items-center gap-3 ml-auto\">\n <ThemeSwitcher />\n {isLoading ? (\n <Skeleton className=\"w-8 h-8 rounded-full\" />\n ) : !isAuthenticated ? (\n <div className=\"flex items-center gap-2\">\n {providersData?.devLoginEnabled && (\n <button\n type=\"button\"\n onClick={devLogin}\n className=\"text-xs font-medium px-2 py-1 rounded bg-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 cursor-pointer\"\n >\n DEV Login\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleLoginClick}\n className=\"text-sm font-medium text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n Login\n </button>\n </div>\n ) : (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-8 h-8 rounded-full p-0 hover:bg-accent hover:text-accent-foreground cursor-pointer\"\n aria-label={`User menu for ${user?.name || 'User'}`}\n data-testid=\"user-avatar-button\"\n >\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-full h-full rounded-full bg-primary text-primary-foreground text-xs font-semibold\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <div className=\"px-2 py-3 flex items-center gap-3\">\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-10 h-10 rounded-full object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-primary text-primary-foreground text-sm font-semibold shrink-0\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-xs text-muted-foreground truncate\">\n @{user?.email?.split('@')[0] || 'user'}\n </p>\n <p className=\"text-sm font-medium truncate\">\n {user?.name || 'User'}\n </p>\n </div>\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleLogout}>\n <LogOut className=\"h-4 w-4 mr-2\" />\n <span>Sign out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )\n}\n"],"names":["AppCatalogLogo"],"mappings":";;;;;;;;;;;;;;;AA6BA,SAAS,UAAU,EAAE,WAAqC;AACxD,MAAI,CAAC,QAAQ,IAAK,QAAO;AACzB,MAAI,QAAQ,QAAQ;AAClB,WACE,qBAAA,UAAA,EACG,UAAA;AAAA,MAAA;AAAA,MACD;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,QAAQ;AAAA,UACd,QAAO;AAAA,UACP,KAAI;AAAA,UACJ,WAAU;AAAA,UACV,SAAS,CAAC,MAAM,EAAE,gBAAA;AAAA,UACnB,UAAA;AAAA,YAAA;AAAA,YACG,QAAQ;AAAA,YAAI;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IAChB,GACF;AAAA,EAEJ;AACA,SAAO,qBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA;AAAA,IAAA;AAAA,IAAG,QAAQ;AAAA,IAAI;AAAA,EAAA,GAAC;AACjE;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AACF,GAGG;AAGD,SACE,qBAAC,QAAA,EAAK,WAAU,iCAAgC,OAAO,OACpD,UAAA;AAAA,IAAA;AAAA,IAAM;AAAA,IAAE;AAAA,IACR,QAAQ,MACP;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAM,QAAQ;AAAA,QACd,QAAO;AAAA,QACP,KAAI;AAAA,QACJ,WAAU;AAAA,QACV,SAAS,CAAC,MAAM,EAAE,gBAAA;AAAA,QAEjB,UAAA,QAAQ;AAAA,MAAA;AAAA,IAAA,IAGX,QAAQ;AAAA,IAEV,oBAAC,aAAU,QAAA,CAAkB;AAAA,EAAA,GAC/B;AAEJ;AAEO,SAAS,eAAe,EAAE,YAA0C;;AACzE,QAAM,YACJ,cAAS,YAAT,mBAAkB,iBAAgB,WAAW,CAAC,SAAS;AAEzD,MAAI,SAAS;AACX,+BACG,QAAA,EAAK,WAAU,iCAAgC,OAAM,SAAQ,UAAA,SAE9D;AAAA,EAEJ;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,yBACZ,UAAA;AAAA,IAAA,SAAS,WACR,oBAAC,aAAA,EAAY,OAAM,YAAW,SAAS,SAAS,SAAS;AAAA,IAE1D,SAAS,eACR,oBAAC,aAAA,EAAY,OAAM,QAAO,SAAS,SAAS,aAAa;AAAA,IAE1D,SAAS,YACR,oBAAC,aAAA,EAAY,OAAM,MAAK,SAAS,SAAS,SAAA,CAAU;AAAA,EAAA,GAExD;AAEJ;AAMO,SAAS,OAAO,EAAE,UAAuB;;AAC9C,QAAM,EAAE,UAAA,IAAc,QAAA;AACtB,QAAM,kBAAkB,mBAAA;AACxB,QAAM,OAAO,QAAA;AACb,QAAM,EAAE,QAAQ,SAAA,IAAa,eAAA;AAC7B,QAAM,EAAE,MAAM,eAAA,IAAmB,aAAA;AACjC,QAAM,yBAAyB,IAAI,iBAAiB;AACpD,QAAM,OAAO,QAAA;AACb,QAAM,EAAE,MAAM,cAAA,IAAkB;AAAA,IAC9B,KAAK,KAAK,aAAa,aAAA;AAAA,EAAa;AAGtC,QAAM,eAAe,YAAY;AAC/B,QAAI;AACF,YAAM,OAAA;AAAA,IACR,SAAS,OAAO;AACd,cAAQ,MAAM,kBAAkB,KAAK;AAAA,IACvC;AAAA,EACF;AAEA,QAAM,mBAAmB,MAAM;AAE7B,UAAM,aAAa,OAAO,SAAS,WAAW,OAAO,SAAS;AAC9D,mBAAe,UAAU;AAAA,EAC3B;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,8CACb,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,2BACb,UAAA,oBAAC,MAAA,EAAK,IAAG,KACP,UAAA,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,MAAA,oBAACA,eAAA,EAAe,WAAU,YAAA,CAAY;AAAA,MACtC,qBAAC,OAAA,EAAI,WAAU,iBACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,qBAAoB,UAAA,eAAW;AAAA,SAC9C,iEAAwB,aACvB,oBAAC,gBAAA,EAAe,UAAU,uBAAuB,SAAA,CAAU;AAAA,MAAA,EAAA,CAE/D;AAAA,IAAA,EAAA,CACF,GACF,GACF;AAAA,IACC,UAAU,oBAAC,OAAA,EAAI,WAAU,UAAU,UAAA,QAAO;AAAA,IAC3C,qBAAC,OAAA,EAAI,WAAU,mCACb,UAAA;AAAA,MAAA,oBAAC,eAAA,EAAc;AAAA,MACd,YACC,oBAAC,UAAA,EAAS,WAAU,uBAAA,CAAuB,IACzC,CAAC,kBACH,qBAAC,OAAA,EAAI,WAAU,2BACZ,UAAA;AAAA,SAAA,+CAAe,oBACd;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YACX,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAIH;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YACX,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MAED,EAAA,CACF,yBAEC,cAAA,EACC,UAAA;AAAA,QAAA,oBAAC,qBAAA,EAAoB,SAAO,MAC1B,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV,cAAY,kBAAiB,6BAAM,SAAQ,MAAM;AAAA,YACjD,eAAY;AAAA,YAEX,wCAAM,SACL;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,KAAK,KAAK;AAAA,gBACV,KAAK,KAAK,QAAQ;AAAA,gBAClB,WAAU;AAAA,cAAA;AAAA,YAAA,IAGZ,oBAAC,OAAA,EAAI,WAAU,wHACX,YAAA,6BAAM,UAAQ,6BAAM,UAAS,KAAK,OAAO,CAAC,EAAE,cAAY,CAC5D;AAAA,UAAA;AAAA,QAAA,GAGN;AAAA,QACA,qBAAC,qBAAA,EAAoB,OAAM,OAAM,WAAU,QACzC,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,qCACZ,UAAA;AAAA,aAAA,6BAAM,SACL;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,KAAK,KAAK;AAAA,gBACV,KAAK,KAAK,QAAQ;AAAA,gBAClB,WAAU;AAAA,cAAA;AAAA,YAAA,IAGZ,oBAAC,OAAA,EAAI,WAAU,6HACX,YAAA,6BAAM,UAAQ,6BAAM,UAAS,KAAK,OAAO,CAAC,EAAE,eAChD;AAAA,YAEF,qBAAC,OAAA,EAAI,WAAU,yBACb,UAAA;AAAA,cAAA,qBAAC,KAAA,EAAE,WAAU,0CAAyC,UAAA;AAAA,gBAAA;AAAA,kBAClD,kCAAM,UAAN,mBAAa,MAAM,KAAK,OAAM;AAAA,cAAA,GAClC;AAAA,kCACC,KAAA,EAAE,WAAU,gCACV,WAAA,6BAAM,SAAQ,OAAA,CACjB;AAAA,YAAA,EAAA,CACF;AAAA,UAAA,GACF;AAAA,8BACC,uBAAA,EAAsB;AAAA,UACvB,qBAAC,kBAAA,EAAiB,SAAS,cACzB,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAO,WAAU,eAAA,CAAe;AAAA,YACjC,oBAAC,UAAK,UAAA,WAAA,CAAQ;AAAA,UAAA,EAAA,CAChB;AAAA,QAAA,EAAA,CACF;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"Header.js","sources":["../../../../../src/ui/components/header/Header.tsx"],"sourcesContent":["import type {\n AppVersionInfo,\n VersionInfo,\n} from '@igstack/app-catalog-backend-core'\nimport { Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { LogOut } from 'lucide-react'\nimport React, { use } from 'react'\nimport AppCatalogLogo from '~/assets/app-catalog.svg?react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport { ThemeSwitcher } from '~/components/ThemeSwitcher'\nimport { AppCatalogContext } from '~/modules/appCatalog/context/AppCatalogContext'\nimport {\n useAuth,\n useAuthActions,\n useIsAuthenticated,\n useUser,\n} from '~/modules/auth'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Button } from '~/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '~/ui/dropdown-menu'\nimport { Skeleton } from '~/ui/skeleton'\n\nfunction ShaSuffix({ version }: { version: VersionInfo }) {\n if (!version.sha) return null\n if (version.shaUrl) {\n return (\n <>\n {' '}\n <a\n href={version.shaUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-primary\"\n onClick={(e) => e.stopPropagation()}\n >\n ({version.sha})\n </a>\n </>\n )\n }\n return <span className=\"text-muted-foreground\"> ({version.sha})</span>\n}\n\nfunction VersionItem({\n label,\n version,\n}: {\n label: string\n version: VersionInfo\n}) {\n // Outer element is a <span> (not <a>) so the optional SHA link can be a\n // sibling anchor without producing invalid nested <a> markup.\n return (\n <span className=\"text-xs text-muted-foreground\" title={label}>\n {label}:{' '}\n {version.url ? (\n <a\n href={version.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:text-primary/80 transition-colors font-medium\"\n onClick={(e) => e.stopPropagation()}\n >\n {version.displayName}\n </a>\n ) : (\n version.displayName\n )}\n <ShaSuffix version={version} />\n </span>\n )\n}\n\nexport function VersionDisplay({ versions }: { versions: AppVersionInfo }) {\n const isLocal =\n versions.backend?.displayName === 'local' && !versions.coreVersion\n\n if (isLocal) {\n return (\n <span className=\"text-xs text-muted-foreground\" title=\"Local\">\n local\n </span>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-0.5\">\n {versions.backend && (\n <VersionItem label=\"Pipeline\" version={versions.backend} />\n )}\n {versions.coreVersion && (\n <VersionItem label=\"Core\" version={versions.coreVersion} />\n )}\n {versions.frontend && (\n <VersionItem label=\"FE\" version={versions.frontend} />\n )}\n </div>\n )\n}\n\nexport interface HeaderProps {\n middle?: React.ReactNode\n}\n\nexport function Header({ middle }: HeaderProps) {\n const { isLoading } = useAuth()\n const isAuthenticated = useIsAuthenticated()\n const user = useUser()\n const { logout, devLogin } = useAuthActions()\n const { open: openLoginModal } = useAuthModal()\n const appCatalogContextMaybe = use(AppCatalogContext)\n const trpc = useTRPC()\n const { data: providersData } = useQuery(\n trpc.auth.getProviders.queryOptions(),\n )\n\n const handleLogout = async () => {\n try {\n await logout()\n } catch (error) {\n console.error('Logout failed:', error)\n }\n }\n\n const handleLoginClick = () => {\n // Preserve the current URL for redirect after login\n const currentUrl = window.location.pathname + window.location.search\n openLoginModal(currentUrl)\n }\n\n return (\n <div className=\"flex items-center mb-4 pb-4 gap-4 border-b\">\n <div className=\"flex items-center gap-4\">\n <Link to=\"/\">\n <div className=\"flex items-center gap-2\">\n <AppCatalogLogo className=\"h-16 w-16\" />\n <div className=\"flex flex-col\">\n <span className=\"font-serif text-lg font-semibold\">\n App Catalog\n </span>\n {appCatalogContextMaybe?.versions && (\n <VersionDisplay versions={appCatalogContextMaybe.versions} />\n )}\n </div>\n </div>\n </Link>\n </div>\n {middle && <div className=\"flex-1\">{middle}</div>}\n <div className=\"flex items-center gap-3 ml-auto\">\n <ThemeSwitcher />\n {isLoading ? (\n <Skeleton className=\"w-8 h-8 rounded-full\" />\n ) : !isAuthenticated ? (\n <div className=\"flex items-center gap-2\">\n {providersData?.devLoginEnabled && (\n <button\n type=\"button\"\n onClick={devLogin}\n className=\"text-xs font-medium px-2 py-1 rounded bg-amber-500/20 text-amber-600 dark:text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 cursor-pointer\"\n >\n DEV Login\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleLoginClick}\n className=\"text-sm font-medium text-muted-foreground hover:text-foreground cursor-pointer\"\n >\n Login\n </button>\n </div>\n ) : (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-8 h-8 rounded-full p-0 hover:bg-accent hover:text-accent-foreground cursor-pointer\"\n aria-label={`User menu for ${user?.name || 'User'}`}\n data-testid=\"user-avatar-button\"\n >\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-full h-full rounded-full bg-primary text-primary-foreground text-xs font-semibold\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <div className=\"px-2 py-3 flex items-center gap-3\">\n {user?.image ? (\n <img\n src={user.image}\n alt={user.name || 'User'}\n className=\"w-10 h-10 rounded-full object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex items-center justify-center w-10 h-10 rounded-full bg-primary text-primary-foreground text-sm font-semibold shrink-0\">\n {(user?.name || user?.email || '?').charAt(0).toUpperCase()}\n </div>\n )}\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-xs text-muted-foreground truncate\">\n @{user?.email?.split('@')[0] || 'user'}\n </p>\n <p className=\"text-sm font-medium truncate\">\n {user?.name || 'User'}\n </p>\n </div>\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleLogout}>\n <LogOut className=\"h-4 w-4 mr-2\" />\n <span>Sign out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n )\n}\n"],"names":["AppCatalogLogo"],"mappings":"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"}
|
package/dist/index.css
CHANGED
|
@@ -1,52 +1,59 @@
|
|
|
1
1
|
/* @import url(./modules/resourceJump/ui/JumpMainButton.css); */
|
|
2
2
|
@import 'tailwindcss';
|
|
3
|
+
/* Warm, crafted type to match the hand-drawn logo (see issue #31 redesign):
|
|
4
|
+
Fraunces = characterful display serif, Nunito Sans = friendly readable body. */
|
|
5
|
+
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Nunito+Sans:wght@400;500;600;700;800&display=swap');
|
|
3
6
|
/* Other module imports this css, and we need to tell tailwind to scan our tsx files */
|
|
4
7
|
@source ".";
|
|
5
8
|
|
|
6
9
|
/* @import 'tw-animate-css'; */
|
|
7
10
|
|
|
8
11
|
@layer base {
|
|
12
|
+
/* Warm "crayon" palette derived from the hand-drawn app-catalog logo
|
|
13
|
+
(orange #dc6827, amber #eca832, coral #e4724a, blue #3782c2, green #65a443)
|
|
14
|
+
on a cream paper background. Replaces the previous generic purple theme
|
|
15
|
+
that read as an AI template. See issue #31. */
|
|
9
16
|
:root {
|
|
10
|
-
--background: oklch(0.
|
|
11
|
-
--foreground: oklch(0.
|
|
12
|
-
--card: oklch(
|
|
13
|
-
--card-foreground: oklch(0.
|
|
14
|
-
--popover: oklch(
|
|
15
|
-
--popover-foreground: oklch(0.
|
|
16
|
-
--primary: oklch(0.
|
|
17
|
-
--primary-foreground: oklch(
|
|
18
|
-
--secondary: oklch(0.
|
|
19
|
-
--secondary-foreground: oklch(0.
|
|
20
|
-
--muted: oklch(0.
|
|
21
|
-
--muted-foreground: oklch(0.
|
|
22
|
-
--accent: oklch(0.
|
|
23
|
-
--accent-foreground: oklch(0.
|
|
17
|
+
--background: oklch(0.9825 0.0125 84); /* warm cream paper */
|
|
18
|
+
--foreground: oklch(0.28 0.02 55); /* warm near-black ink */
|
|
19
|
+
--card: oklch(0.995 0.004 84);
|
|
20
|
+
--card-foreground: oklch(0.28 0.02 55);
|
|
21
|
+
--popover: oklch(0.995 0.004 84);
|
|
22
|
+
--popover-foreground: oklch(0.28 0.02 55);
|
|
23
|
+
--primary: oklch(0.62 0.17 45); /* logo orange #dc6827 */
|
|
24
|
+
--primary-foreground: oklch(0.99 0.01 84);
|
|
25
|
+
--secondary: oklch(0.94 0.018 80); /* warm sunk panel */
|
|
26
|
+
--secondary-foreground: oklch(0.36 0.03 55);
|
|
27
|
+
--muted: oklch(0.955 0.012 82);
|
|
28
|
+
--muted-foreground: oklch(0.52 0.025 60);
|
|
29
|
+
--accent: oklch(0.93 0.05 70); /* soft amber accent */
|
|
30
|
+
--accent-foreground: oklch(0.36 0.04 55);
|
|
24
31
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
|
25
32
|
--destructive-foreground: oklch(1 0 0);
|
|
26
|
-
--border: oklch(0.
|
|
27
|
-
--input: oklch(0.
|
|
28
|
-
--ring: oklch(0.
|
|
29
|
-
--chart-1: oklch(0.
|
|
30
|
-
--chart-2: oklch(0.
|
|
31
|
-
--chart-3: oklch(0.
|
|
32
|
-
--chart-4: oklch(0.
|
|
33
|
-
--chart-5: oklch(0.
|
|
34
|
-
--sidebar: oklch(0.
|
|
35
|
-
--sidebar-foreground: oklch(0.
|
|
36
|
-
--sidebar-primary: oklch(0.
|
|
37
|
-
--sidebar-primary-foreground: oklch(
|
|
38
|
-
--sidebar-accent: oklch(0.
|
|
39
|
-
--sidebar-accent-foreground: oklch(0.
|
|
40
|
-
--sidebar-border: oklch(0.
|
|
41
|
-
--sidebar-ring: oklch(0.
|
|
33
|
+
--border: oklch(0.9 0.018 80); /* warm hairline */
|
|
34
|
+
--input: oklch(0.9 0.018 80);
|
|
35
|
+
--ring: oklch(0.62 0.17 45);
|
|
36
|
+
--chart-1: oklch(0.62 0.17 45); /* orange */
|
|
37
|
+
--chart-2: oklch(0.62 0.14 230); /* blue #3782c2 */
|
|
38
|
+
--chart-3: oklch(0.68 0.14 135); /* green #65a443 */
|
|
39
|
+
--chart-4: oklch(0.75 0.14 75); /* amber #eca832 */
|
|
40
|
+
--chart-5: oklch(0.66 0.15 20); /* coral #e4724a */
|
|
41
|
+
--sidebar: oklch(0.955 0.012 82);
|
|
42
|
+
--sidebar-foreground: oklch(0.28 0.02 55);
|
|
43
|
+
--sidebar-primary: oklch(0.62 0.17 45);
|
|
44
|
+
--sidebar-primary-foreground: oklch(0.99 0.01 84);
|
|
45
|
+
--sidebar-accent: oklch(0.93 0.05 70);
|
|
46
|
+
--sidebar-accent-foreground: oklch(0.36 0.04 55);
|
|
47
|
+
--sidebar-border: oklch(0.9 0.018 80);
|
|
48
|
+
--sidebar-ring: oklch(0.62 0.17 45);
|
|
42
49
|
--font-sans:
|
|
43
|
-
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
50
|
+
'Nunito Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
44
51
|
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
45
|
-
--font-serif:
|
|
52
|
+
--font-serif: 'Fraunces', Georgia, serif;
|
|
46
53
|
--font-mono:
|
|
47
54
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
|
48
55
|
'Courier New', monospace;
|
|
49
|
-
--radius: 0.
|
|
56
|
+
--radius: 0.875rem; /* rounder corners to match the friendly logo */
|
|
50
57
|
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
|
51
58
|
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
|
52
59
|
--shadow-sm:
|
|
@@ -73,44 +80,44 @@
|
|
|
73
80
|
--eh-context-border: #ff7926;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
/*
|
|
83
|
+
/* Warm-dark equivalent of the crayon palette (see issue #31). */
|
|
77
84
|
.dark {
|
|
78
|
-
--background:
|
|
79
|
-
--foreground: oklch(0.
|
|
80
|
-
--card: oklch(0.
|
|
81
|
-
--card-foreground: oklch(0.
|
|
82
|
-
--popover: oklch(0.
|
|
83
|
-
--popover-foreground: oklch(0.
|
|
84
|
-
--primary: oklch(0.
|
|
85
|
-
--primary-foreground: oklch(0.
|
|
86
|
-
--secondary: oklch(0.
|
|
87
|
-
--secondary-foreground: oklch(0.
|
|
88
|
-
--muted: oklch(0.
|
|
89
|
-
--muted-foreground: oklch(0.
|
|
90
|
-
--accent: oklch(0.
|
|
91
|
-
--accent-foreground: oklch(0.
|
|
85
|
+
--background: oklch(0.19 0.012 60); /* warm charcoal, not blue-gray */
|
|
86
|
+
--foreground: oklch(0.93 0.012 84);
|
|
87
|
+
--card: oklch(0.23 0.014 58);
|
|
88
|
+
--card-foreground: oklch(0.93 0.012 84);
|
|
89
|
+
--popover: oklch(0.23 0.014 58);
|
|
90
|
+
--popover-foreground: oklch(0.93 0.012 84);
|
|
91
|
+
--primary: oklch(0.7 0.16 48); /* brightened logo orange for dark */
|
|
92
|
+
--primary-foreground: oklch(0.2 0.012 55);
|
|
93
|
+
--secondary: oklch(0.3 0.016 58);
|
|
94
|
+
--secondary-foreground: oklch(0.9 0.012 84);
|
|
95
|
+
--muted: oklch(0.26 0.014 58);
|
|
96
|
+
--muted-foreground: oklch(0.72 0.018 70);
|
|
97
|
+
--accent: oklch(0.34 0.03 60);
|
|
98
|
+
--accent-foreground: oklch(0.9 0.012 84);
|
|
92
99
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
|
93
100
|
--destructive-foreground: oklch(0.2077 0.0398 265.7549);
|
|
94
|
-
--border: oklch(0.
|
|
95
|
-
--input: oklch(0.
|
|
96
|
-
--ring: oklch(0.
|
|
97
|
-
--chart-1: oklch(0.
|
|
98
|
-
--chart-2: oklch(0.
|
|
99
|
-
--chart-3: oklch(0.
|
|
100
|
-
--chart-4: oklch(0.
|
|
101
|
-
--chart-5: oklch(0.
|
|
102
|
-
--sidebar: oklch(0.
|
|
103
|
-
--sidebar-foreground: oklch(0.
|
|
104
|
-
--sidebar-primary: oklch(0.
|
|
105
|
-
--sidebar-primary-foreground: oklch(0.
|
|
106
|
-
--sidebar-accent: oklch(0.
|
|
107
|
-
--sidebar-accent-foreground: oklch(0.
|
|
108
|
-
--sidebar-border: oklch(0.
|
|
109
|
-
--sidebar-ring: oklch(0.
|
|
101
|
+
--border: oklch(0.36 0.016 58);
|
|
102
|
+
--input: oklch(0.36 0.016 58);
|
|
103
|
+
--ring: oklch(0.7 0.16 48);
|
|
104
|
+
--chart-1: oklch(0.7 0.16 48);
|
|
105
|
+
--chart-2: oklch(0.66 0.13 230);
|
|
106
|
+
--chart-3: oklch(0.7 0.14 135);
|
|
107
|
+
--chart-4: oklch(0.78 0.14 75);
|
|
108
|
+
--chart-5: oklch(0.68 0.15 20);
|
|
109
|
+
--sidebar: oklch(0.23 0.014 58);
|
|
110
|
+
--sidebar-foreground: oklch(0.93 0.012 84);
|
|
111
|
+
--sidebar-primary: oklch(0.7 0.16 48);
|
|
112
|
+
--sidebar-primary-foreground: oklch(0.2 0.012 55);
|
|
113
|
+
--sidebar-accent: oklch(0.34 0.03 60);
|
|
114
|
+
--sidebar-accent-foreground: oklch(0.9 0.012 84);
|
|
115
|
+
--sidebar-border: oklch(0.36 0.016 58);
|
|
116
|
+
--sidebar-ring: oklch(0.7 0.16 48);
|
|
110
117
|
--font-sans:
|
|
111
|
-
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
118
|
+
'Nunito Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
112
119
|
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
113
|
-
--font-serif:
|
|
120
|
+
--font-serif: 'Fraunces', Georgia, serif;
|
|
114
121
|
--font-mono:
|
|
115
122
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
|
116
123
|
'Courier New', monospace;
|