@nospt/backstage-plugin-apigee 1.1.0-rc2 → 1.1.0-rc3

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.

Potentially problematic release.


This version of @nospt/backstage-plugin-apigee might be problematic. Click here for more details.

Files changed (47) hide show
  1. package/README.md +19 -19
  2. package/config.d.ts +25 -25
  3. package/dist/components/ApiCard.esm.js +41 -104
  4. package/dist/components/ApiCard.esm.js.map +1 -1
  5. package/dist/components/ApiCard.module.css.esm.js +8 -0
  6. package/dist/components/ApiCard.module.css.esm.js.map +1 -0
  7. package/dist/components/ApiCardGrid.esm.js +7 -21
  8. package/dist/components/ApiCardGrid.esm.js.map +1 -1
  9. package/dist/components/ApiCardGrid.module.css.esm.js +8 -0
  10. package/dist/components/ApiCardGrid.module.css.esm.js.map +1 -0
  11. package/dist/components/ApigeeApiCatalogPage.esm.js +71 -57
  12. package/dist/components/ApigeeApiCatalogPage.esm.js.map +1 -1
  13. package/dist/components/ApigeeNavContent.esm.js +2 -2
  14. package/dist/components/ApigeeNavContent.esm.js.map +1 -1
  15. package/dist/components/CountCards.esm.js +99 -0
  16. package/dist/components/CountCards.esm.js.map +1 -0
  17. package/dist/components/CountCards.module.css.esm.js +8 -0
  18. package/dist/components/CountCards.module.css.esm.js.map +1 -0
  19. package/dist/components/FilterToolbar.esm.js +63 -133
  20. package/dist/components/FilterToolbar.esm.js.map +1 -1
  21. package/dist/components/FilterToolbar.module.css.esm.js +8 -0
  22. package/dist/components/FilterToolbar.module.css.esm.js.map +1 -0
  23. package/dist/components/SearchBar.esm.js +7 -35
  24. package/dist/components/SearchBar.esm.js.map +1 -1
  25. package/dist/components/apiCardViewModel.esm.js.map +1 -1
  26. package/dist/components/apigeeFacetFilters.esm.js.map +1 -1
  27. package/dist/components/apigeeOrgFilter.esm.js.map +1 -1
  28. package/dist/components/apigeeSearchParams.esm.js +14 -1
  29. package/dist/components/apigeeSearchParams.esm.js.map +1 -1
  30. package/dist/components/categoryIcons.esm.js +12 -22
  31. package/dist/components/categoryIcons.esm.js.map +1 -1
  32. package/dist/components/facetModel.esm.js +30 -10
  33. package/dist/components/facetModel.esm.js.map +1 -1
  34. package/dist/components/grouping.esm.js.map +1 -1
  35. package/dist/components/states.esm.js +19 -49
  36. package/dist/components/states.esm.js.map +1 -1
  37. package/dist/components/useApigeeFilters.esm.js +18 -2
  38. package/dist/components/useApigeeFilters.esm.js.map +1 -1
  39. package/dist/components/useDebouncedValue.esm.js.map +1 -1
  40. package/dist/constants.esm.js.map +1 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
  43. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
  44. package/dist/plugin.esm.js +2 -2
  45. package/dist/plugin.esm.js.map +1 -1
  46. package/dist/routes.esm.js.map +1 -1
  47. package/package.json +17 -15
@@ -1 +1 @@
1
- {"version":3,"file":"ApigeeApiCatalogPage.esm.js","sources":["../../src/components/ApigeeApiCatalogPage.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { Content, ContentHeader, Header, Page } from '@backstage/core-components';\r\nimport { configApiRef, useApi } from '@backstage/core-plugin-api';\r\nimport {\r\n EntityListProvider,\r\n useEntityList,\r\n type DefaultEntityFilters,\r\n} from '@backstage/plugin-catalog-react';\r\nimport { Button, ButtonGroup, makeStyles } from '@material-ui/core';\r\nimport ViewAgenda from '@material-ui/icons/ViewAgenda';\r\nimport ViewModule from '@material-ui/icons/ViewModule';\r\nimport { useEffect, useMemo, useState } from 'react';\r\nimport { toApiCardViewModel } from './apiCardViewModel';\r\nimport { ApiCardGrid, type ViewMode } from './ApiCardGrid';\r\nimport {\r\n ApiKindFilter,\r\n isApigeeEntity,\r\n type ApiSearchFilter,\r\n type FieldFacetFilter,\r\n} from './apigeeFacetFilters';\r\nimport { ApigeeOrgFilter } from './apigeeOrgFilter';\r\nimport { isActive } from './apigeeSearchParams';\r\nimport { deriveFacets } from './facetModel';\r\nimport { FilterToolbar } from './FilterToolbar';\r\nimport { SearchBar } from './SearchBar';\r\nimport {\r\n ApiCatalogEmptyState,\r\n ApiCatalogError,\r\n NoMatchState,\r\n SkeletonGrid,\r\n} from './states';\r\nimport { useApigeeFilters } from './useApigeeFilters';\r\n\r\n/**\r\n * Entity-list filters registered on the Apigee catalog page. `apigeeKind` (the seam-free\r\n * `ApiKindFilter`) and `apigeeOrg` (the dual-key is-Apigee gate) are the backend filters applied\r\n * once by {@link ApplyApigeeFilters}; the facet + search filters\r\n * (`apigeeLifecycle`/`apigeeOrgFacet`/`apigeeOwner`/`apigeeCategory`/`apigeeDepartment`/`apigeeSearch`)\r\n * are driven from the URL by `useApigeeFilters`. Exported so the hook can import the filter contract.\r\n */\r\nexport type ApigeeFilters = DefaultEntityFilters & {\r\n apigeeKind?: ApiKindFilter;\r\n apigeeOrg?: ApigeeOrgFilter;\r\n apigeeLifecycle?: FieldFacetFilter;\r\n apigeeOrgFacet?: FieldFacetFilter;\r\n apigeeOwner?: FieldFacetFilter;\r\n apigeeCategory?: FieldFacetFilter;\r\n apigeeDepartment?: FieldFacetFilter;\r\n apigeeSearch?: ApiSearchFilter;\r\n};\r\n\r\nconst useStyles = makeStyles({\r\n toggleGroup: {\r\n marginLeft: 'auto',\r\n },\r\n});\r\n\r\n/**\r\n * Registers the Apigee backend filters on the shared EntityListProvider ONCE: the seam-free\r\n * `ApiKindFilter` (kind=api with NO `toQueryValue`, so the provider's URL writer stays a no-op and\r\n * `useSearchParams` owns a clean URL) plus the dual-key `ApigeeOrgFilter` is-Apigee gate. The facet\r\n * + search filters are applied separately, URL-driven, by `useApigeeFilters`. Rendered inside the\r\n * provider; renders nothing itself.\r\n */\r\nfunction ApplyApigeeFilters() {\r\n const { updateFilters } = useEntityList<ApigeeFilters>();\r\n useEffect(() => {\r\n updateFilters({\r\n apigeeKind: new ApiKindFilter(),\r\n apigeeOrg: new ApigeeOrgFilter(),\r\n });\r\n }, [updateFilters]);\r\n return null;\r\n}\r\n\r\n/**\r\n * The page body: reads the filtered entity list, maps to view-models, and branches on\r\n * loading/empty/error. Exposes a Flat/Grouped view toggle (component state only, no view\r\n * URL param — that is a Phase 6 seam). (UI-01, UI-05, UI-11)\r\n */\r\nexport function ApigeeCatalogContent() {\r\n const classes = useStyles();\r\n const { entities, backendEntities, loading, error } =\r\n useEntityList<ApigeeFilters>();\r\n const { state, inputValue, setInputValue, toggleFacet, clearFacet, clearAll } =\r\n useApigeeFilters();\r\n const configApi = useApi(configApiRef);\r\n\r\n const overrides = useMemo<Record<string, string> | undefined>(() => {\r\n const config = configApi.getOptionalConfig('apigee.catalog.categoryIcons');\r\n if (!config) {\r\n return undefined;\r\n }\r\n const out: Record<string, string> = {};\r\n for (const key of config.keys()) {\r\n const value = config.getOptionalString(key);\r\n if (value) {\r\n out[key] = value;\r\n }\r\n }\r\n return Object.keys(out).length > 0 ? out : undefined;\r\n }, [configApi]);\r\n\r\n const orgLabels = useMemo<Record<string, string> | undefined>(() => {\r\n const config = configApi.getOptionalConfig('apigee.catalog.orgLabels');\r\n if (!config) {\r\n return undefined;\r\n }\r\n const out: Record<string, string> = {};\r\n for (const key of config.keys()) {\r\n const value = config.getOptionalString(key);\r\n if (value) {\r\n out[key] = value;\r\n }\r\n }\r\n return Object.keys(out).length > 0 ? out : undefined;\r\n }, [configApi]);\r\n\r\n const vms = useMemo(() => entities.map(toApiCardViewModel), [entities]);\r\n const facets = useMemo(\r\n () => deriveFacets(backendEntities.filter(isApigeeEntity), overrides, orgLabels),\r\n [backendEntities, overrides, orgLabels],\r\n );\r\n const active = isActive(state);\r\n const [viewMode, setViewMode] = useState<ViewMode>('flat');\r\n\r\n const renderBody = () => {\r\n if (error) {\r\n return <ApiCatalogError error={error} />;\r\n }\r\n if (loading) {\r\n return <SkeletonGrid />;\r\n }\r\n if (vms.length === 0) {\r\n return active ? (\r\n <NoMatchState onClearAll={clearAll} />\r\n ) : (\r\n <ApiCatalogEmptyState />\r\n );\r\n }\r\n return <ApiCardGrid vms={vms} viewMode={viewMode} overrides={overrides} />;\r\n };\r\n\r\n return (\r\n <Page themeId=\"apis\">\r\n <Header\r\n title=\"API Catalog\"\r\n subtitle=\"Browse APIs discovered from Apigee & API Hub\"\r\n />\r\n <Content>\r\n <ContentHeader title={`${vms.length} APIs`}>\r\n <div\r\n role=\"group\"\r\n aria-label=\"API view mode\"\r\n className={classes.toggleGroup}\r\n >\r\n <ButtonGroup size=\"small\">\r\n <Button\r\n startIcon={<ViewModule />}\r\n aria-label=\"Flat grid view\"\r\n aria-pressed={viewMode === 'flat'}\r\n color={viewMode === 'flat' ? 'primary' : 'default'}\r\n onClick={() => setViewMode('flat')}\r\n >\r\n Flat\r\n </Button>\r\n <Button\r\n startIcon={<ViewAgenda />}\r\n aria-label=\"Group by category\"\r\n aria-pressed={viewMode === 'grouped'}\r\n color={viewMode === 'grouped' ? 'primary' : 'default'}\r\n onClick={() => setViewMode('grouped')}\r\n >\r\n Grouped\r\n </Button>\r\n </ButtonGroup>\r\n </div>\r\n </ContentHeader>\r\n {!loading &&\r\n !error &&\r\n backendEntities.filter(isApigeeEntity).length > 0 ? (\r\n <FilterToolbar\r\n facets={facets}\r\n selected={state.selected}\r\n onToggleFacet={toggleFacet}\r\n onClearFacet={clearFacet}\r\n onClearAll={clearAll}\r\n active={active}\r\n resultCount={vms.length}\r\n searchBar={\r\n <SearchBar value={inputValue} onChange={setInputValue} />\r\n }\r\n />\r\n ) : null}\r\n {renderBody()}\r\n </Content>\r\n </Page>\r\n );\r\n}\r\n\r\n/**\r\n * Route target for `/apigee` (05-06). Wraps the content in an `EntityListProvider` and\r\n * registers the Apigee filters — the shared filter context Phase 6's facets + search reuse.\r\n */\r\nexport function ApigeeApiCatalogPage() {\r\n return (\r\n <EntityListProvider>\r\n <ApplyApigeeFilters />\r\n <ApigeeCatalogContent />\r\n </EntityListProvider>\r\n );\r\n}\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ApigeeApiCatalogPage.esm.js","sources":["../../src/components/ApigeeApiCatalogPage.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { Content, ContentHeader, Page } from '@backstage/core-components';\nimport { configApiRef, useApi } from '@backstage/core-plugin-api';\nimport {\n EntityListProvider,\n useEntityList,\n type DefaultEntityFilters,\n} from '@backstage/plugin-catalog-react';\nimport { ToggleButton } from '@backstage/ui';\nimport { RiLayoutGridLine, RiLayoutRowLine } from '@remixicon/react';\nimport { useEffect, useMemo, useState } from 'react';\nimport { toApiCardViewModel } from './apiCardViewModel';\nimport { ApiCardGrid, type ViewMode } from './ApiCardGrid';\nimport {\n ApiKindFilter,\n isApigeeEntity,\n type ApiSearchFilter,\n type FieldFacetFilter,\n} from './apigeeFacetFilters';\nimport { ApigeeOrgFilter } from './apigeeOrgFilter';\nimport { isActive } from './apigeeSearchParams';\nimport { CountCards } from './CountCards';\nimport { deriveCounts, deriveFacets } from './facetModel';\nimport { FilterToolbar } from './FilterToolbar';\nimport { SearchBar } from './SearchBar';\nimport {\n ApiCatalogEmptyState,\n ApiCatalogError,\n NoMatchState,\n SkeletonGrid,\n} from './states';\nimport { useApigeeFilters } from './useApigeeFilters';\n\n/**\n * Entity-list filters registered on the Apigee catalog page. `apigeeKind` (the seam-free\n * `ApiKindFilter`) and `apigeeOrg` (the dual-key is-Apigee gate) are the backend filters applied\n * once by {@link ApplyApigeeFilters}; the facet + search filters\n * (`apigeeLifecycle`/`apigeeOrgFacet`/`apigeeOwner`/`apigeeCategory`/`apigeeDepartment`/`apigeeSearch`)\n * are driven from the URL by `useApigeeFilters`. Exported so the hook can import the filter contract.\n */\nexport type ApigeeFilters = DefaultEntityFilters & {\n apigeeKind?: ApiKindFilter;\n apigeeOrg?: ApigeeOrgFilter;\n apigeeLifecycle?: FieldFacetFilter;\n apigeeOrgFacet?: FieldFacetFilter;\n apigeeOwner?: FieldFacetFilter;\n apigeeCategory?: FieldFacetFilter;\n apigeeDepartment?: FieldFacetFilter;\n apigeeSearch?: ApiSearchFilter;\n};\n\n/**\n * Registers the Apigee backend filters on the shared EntityListProvider ONCE: the seam-free\n * `ApiKindFilter` (kind=api with NO `toQueryValue`, so the provider's URL writer stays a no-op and\n * `useSearchParams` owns a clean URL) plus the dual-key `ApigeeOrgFilter` is-Apigee gate. The facet\n * + search filters are applied separately, URL-driven, by `useApigeeFilters`. Rendered inside the\n * provider; renders nothing itself.\n */\nfunction ApplyApigeeFilters() {\n const { updateFilters } = useEntityList<ApigeeFilters>();\n useEffect(() => {\n updateFilters({\n apigeeKind: new ApiKindFilter(),\n apigeeOrg: new ApigeeOrgFilter(),\n });\n }, [updateFilters]);\n return null;\n}\n\n/**\n * The page body: reads the filtered entity list, maps to view-models, and branches on\n * loading/empty/error. Exposes a Flat/Grouped view toggle (component state only, no view\n * URL param — that is a Phase 6 seam). (UI-01, UI-05, UI-11)\n */\nexport function ApigeeCatalogContent() {\n const { entities, backendEntities, loading, error } =\n useEntityList<ApigeeFilters>();\n const {\n state,\n inputValue,\n setInputValue,\n toggleFacet,\n selectOnlyFacet,\n clearFacet,\n clearAll,\n } = useApigeeFilters();\n const configApi = useApi(configApiRef);\n\n const overrides = useMemo<Record<string, string> | undefined>(() => {\n const config = configApi.getOptionalConfig('apigee.catalog.categoryIcons');\n if (!config) {\n return undefined;\n }\n const out: Record<string, string> = {};\n for (const key of config.keys()) {\n const value = config.getOptionalString(key);\n if (value) {\n out[key] = value;\n }\n }\n return Object.keys(out).length > 0 ? out : undefined;\n }, [configApi]);\n\n const orgLabels = useMemo<Record<string, string> | undefined>(() => {\n const config = configApi.getOptionalConfig('apigee.catalog.orgLabels');\n if (!config) {\n return undefined;\n }\n const out: Record<string, string> = {};\n for (const key of config.keys()) {\n const value = config.getOptionalString(key);\n if (value) {\n out[key] = value;\n }\n }\n return Object.keys(out).length > 0 ? out : undefined;\n }, [configApi]);\n\n const apigeeEntities = useMemo(\n () => backendEntities.filter(isApigeeEntity),\n [backendEntities],\n );\n const vms = useMemo(() => entities.map(toApiCardViewModel), [entities]);\n const facets = useMemo(\n () => deriveFacets(apigeeEntities, overrides, orgLabels),\n [apigeeEntities, overrides, orgLabels],\n );\n // Static full-catalog counts: the cards stay fixed as filters apply; only the grid narrows.\n const counts = useMemo(\n () => deriveCounts(apigeeEntities, ['org', 'lifecycle', 'department'], orgLabels),\n [apigeeEntities, orgLabels],\n );\n const active = isActive(state);\n const [viewMode, setViewMode] = useState<ViewMode>('flat');\n\n const renderBody = () => {\n if (error) {\n return <ApiCatalogError error={error} />;\n }\n if (loading) {\n return <SkeletonGrid />;\n }\n if (vms.length === 0) {\n return active ? (\n <NoMatchState onClearAll={clearAll} />\n ) : (\n <ApiCatalogEmptyState />\n );\n }\n return <ApiCardGrid vms={vms} viewMode={viewMode} overrides={overrides} />;\n };\n\n return (\n <Page themeId=\"apis\">\n <Content>\n <ContentHeader title=\"APIs\">\n <div\n role=\"group\"\n aria-label=\"API view mode\"\n style={{\n marginLeft: 'auto',\n display: 'inline-flex',\n gap: 'var(--bui-space-1)',\n }}\n >\n <ToggleButton\n aria-label=\"Flat grid view\"\n isSelected={viewMode === 'flat'}\n onChange={() => setViewMode('flat')}\n iconStart={<RiLayoutGridLine />}\n >\n Flat\n </ToggleButton>\n <ToggleButton\n aria-label=\"Group by category\"\n isSelected={viewMode === 'grouped'}\n onChange={() => setViewMode('grouped')}\n iconStart={<RiLayoutRowLine />}\n >\n Grouped\n </ToggleButton>\n </div>\n </ContentHeader>\n {!loading && !error && apigeeEntities.length > 0 ? (\n <>\n <CountCards\n total={apigeeEntities.length}\n counts={counts}\n selected={state.selected}\n onSelectFacet={selectOnlyFacet}\n />\n <FilterToolbar\n facets={facets}\n selected={state.selected}\n onToggleFacet={toggleFacet}\n onClearFacet={clearFacet}\n onClearAll={clearAll}\n active={active}\n resultCount={vms.length}\n searchBar={\n <SearchBar value={inputValue} onChange={setInputValue} />\n }\n />\n </>\n ) : null}\n {renderBody()}\n </Content>\n </Page>\n );\n}\n\n/**\n * Route target for `/apigee` (05-06). Wraps the content in an `EntityListProvider` and\n * registers the Apigee filters — the shared filter context Phase 6's facets + search reuse.\n */\nexport function ApigeeApiCatalogPage() {\n return (\n <EntityListProvider>\n <ApplyApigeeFilters />\n <ApigeeCatalogContent />\n </EntityListProvider>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { Sidebar, SidebarItem, SidebarSubmenu, SidebarSubmenuItem } from '@backstage/core-components';
3
- import LayersIcon from '@material-ui/icons/Layers';
3
+ import { RiStackLine } from '@remixicon/react';
4
4
 
5
5
  const ApigeeNavContent = ({ navItems }) => {
6
6
  const allItems = navItems.rest();
@@ -20,7 +20,7 @@ const ApigeeNavContent = ({ navItems }) => {
20
20
  },
21
21
  item.node.spec.id
22
22
  )),
23
- apigeeItems.length > 0 && /* @__PURE__ */ jsx(SidebarItem, { icon: LayersIcon, text: "API Hub", children: /* @__PURE__ */ jsx(SidebarSubmenu, { title: "API Hub", children: apigeeItems.map((item) => /* @__PURE__ */ jsx(
23
+ apigeeItems.length > 0 && /* @__PURE__ */ jsx(SidebarItem, { icon: () => /* @__PURE__ */ jsx(RiStackLine, {}), text: "API Hub", children: /* @__PURE__ */ jsx(SidebarSubmenu, { title: "API Hub", children: apigeeItems.map((item) => /* @__PURE__ */ jsx(
24
24
  SidebarSubmenuItem,
25
25
  {
26
26
  title: item.title,
@@ -1 +1 @@
1
- {"version":3,"file":"ApigeeNavContent.esm.js","sources":["../../src/components/ApigeeNavContent.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n Sidebar,\r\n SidebarItem,\r\n SidebarSubmenu,\r\n SidebarSubmenuItem,\r\n} from '@backstage/core-components';\r\nimport type {\r\n NavContentComponent,\r\n NavContentNavItem,\r\n} from '@backstage/plugin-app-react';\r\nimport LayersIcon from '@material-ui/icons/Layers';\r\nimport React from 'react';\r\n\r\n/**\r\n * Renders the app nav bar with an expandable \"API Hub\" group for all apigee\r\n * plugin pages, while rendering any other plugins' pages as flat sidebar items.\r\n *\r\n * To activate, add `apigeeNavModule` to your app's `createApp({ features })`.\r\n */\r\nexport const ApigeeNavContent: NavContentComponent = ({ navItems }) => {\r\n const allItems = navItems.rest();\r\n const apigeeItems = allItems.filter(\r\n (item: NavContentNavItem) =>\r\n item.node.spec.plugin.pluginId === 'apigee',\r\n );\r\n const otherItems = allItems.filter(\r\n (item: NavContentNavItem) =>\r\n item.node.spec.plugin.pluginId !== 'apigee',\r\n );\r\n\r\n return (\r\n <Sidebar>\r\n {otherItems.map((item: NavContentNavItem) => (\r\n <SidebarItem\r\n key={item.node.spec.id}\r\n to={item.href}\r\n // item.icon is IconElement (JSX); SidebarItem.icon is IconComponent.\r\n // Wrapping in a render-prop is the documented pattern from AppNav.\r\n icon={(_props: object) => item.icon as React.ReactElement}\r\n text={item.title}\r\n />\r\n ))}\r\n {apigeeItems.length > 0 && (\r\n <SidebarItem icon={LayersIcon} text=\"API Hub\">\r\n <SidebarSubmenu title=\"API Hub\">\r\n {apigeeItems.map((item: NavContentNavItem) => (\r\n <SidebarSubmenuItem\r\n key={item.node.spec.id}\r\n title={item.title}\r\n to={item.href}\r\n />\r\n ))}\r\n </SidebarSubmenu>\r\n </SidebarItem>\r\n )}\r\n </Sidebar>\r\n );\r\n};\r\n"],"names":[],"mappings":";;;;AAoBO,MAAM,gBAAA,GAAwC,CAAC,EAAE,QAAA,EAAS,KAAM;AACrE,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,EAAK;AAC/B,EAAA,MAAM,cAAc,QAAA,CAAS,MAAA;AAAA,IAC3B,CAAC,IAAA,KACC,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,OAAO,QAAA,KAAa;AAAA,GACvC;AACA,EAAA,MAAM,aAAa,QAAA,CAAS,MAAA;AAAA,IAC1B,CAAC,IAAA,KACC,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,OAAO,QAAA,KAAa;AAAA,GACvC;AAEA,EAAA,4BACG,OAAA,EAAA,EACE,QAAA,EAAA;AAAA,IAAA,UAAA,CAAW,GAAA,CAAI,CAAC,IAAA,qBACf,GAAA;AAAA,MAAC,WAAA;AAAA,MAAA;AAAA,QAEC,IAAI,IAAA,CAAK,IAAA;AAAA,QAGT,IAAA,EAAM,CAAC,MAAA,KAAmB,IAAA,CAAK,IAAA;AAAA,QAC/B,MAAM,IAAA,CAAK;AAAA,OAAA;AAAA,MALN,IAAA,CAAK,KAAK,IAAA,CAAK;AAAA,KAOvB,CAAA;AAAA,IACA,YAAY,MAAA,GAAS,CAAA,oBACpB,GAAA,CAAC,WAAA,EAAA,EAAY,MAAM,UAAA,EAAY,IAAA,EAAK,SAAA,EAClC,QAAA,kBAAA,GAAA,CAAC,kBAAe,KAAA,EAAM,SAAA,EACnB,QAAA,EAAA,WAAA,CAAY,GAAA,CAAI,CAAC,IAAA,qBAChB,GAAA;AAAA,MAAC,kBAAA;AAAA,MAAA;AAAA,QAEC,OAAO,IAAA,CAAK,KAAA;AAAA,QACZ,IAAI,IAAA,CAAK;AAAA,OAAA;AAAA,MAFJ,IAAA,CAAK,KAAK,IAAA,CAAK;AAAA,KAIvB,GACH,CAAA,EACF;AAAA,GAAA,EAEJ,CAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"ApigeeNavContent.esm.js","sources":["../../src/components/ApigeeNavContent.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport {\n Sidebar,\n SidebarItem,\n SidebarSubmenu,\n SidebarSubmenuItem,\n} from '@backstage/core-components';\nimport type {\n NavContentComponent,\n NavContentNavItem,\n} from '@backstage/plugin-app-react';\nimport { RiStackLine } from '@remixicon/react';\nimport React from 'react';\n\n/**\n * Renders the app nav bar with an expandable \"API Hub\" group for all apigee\n * plugin pages, while rendering any other plugins' pages as flat sidebar items.\n *\n * To activate, add `apigeeNavModule` to your app's `createApp({ features })`.\n */\nexport const ApigeeNavContent: NavContentComponent = ({ navItems }) => {\n const allItems = navItems.rest();\n const apigeeItems = allItems.filter(\n (item: NavContentNavItem) =>\n item.node.spec.plugin.pluginId === 'apigee',\n );\n const otherItems = allItems.filter(\n (item: NavContentNavItem) =>\n item.node.spec.plugin.pluginId !== 'apigee',\n );\n\n return (\n <Sidebar>\n {otherItems.map((item: NavContentNavItem) => (\n <SidebarItem\n key={item.node.spec.id}\n to={item.href}\n // item.icon is IconElement (JSX); SidebarItem.icon is IconComponent.\n // Wrapping in a render-prop is the documented pattern from AppNav.\n icon={(_props: object) => item.icon as React.ReactElement}\n text={item.title}\n />\n ))}\n {apigeeItems.length > 0 && (\n <SidebarItem icon={() => <RiStackLine />} text=\"API Hub\">\n <SidebarSubmenu title=\"API Hub\">\n {apigeeItems.map((item: NavContentNavItem) => (\n <SidebarSubmenuItem\n key={item.node.spec.id}\n title={item.title}\n to={item.href}\n />\n ))}\n </SidebarSubmenu>\n </SidebarItem>\n )}\n </Sidebar>\n );\n};\n"],"names":[],"mappings":";;;;AAoBO,MAAM,gBAAA,GAAwC,CAAC,EAAE,QAAA,EAAS,KAAM;AACrE,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,EAAK;AAC/B,EAAA,MAAM,cAAc,QAAA,CAAS,MAAA;AAAA,IAC3B,CAAC,IAAA,KACC,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,OAAO,QAAA,KAAa;AAAA,GACvC;AACA,EAAA,MAAM,aAAa,QAAA,CAAS,MAAA;AAAA,IAC1B,CAAC,IAAA,KACC,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,OAAO,QAAA,KAAa;AAAA,GACvC;AAEA,EAAA,4BACG,OAAA,EAAA,EACE,QAAA,EAAA;AAAA,IAAA,UAAA,CAAW,GAAA,CAAI,CAAC,IAAA,qBACf,GAAA;AAAA,MAAC,WAAA;AAAA,MAAA;AAAA,QAEC,IAAI,IAAA,CAAK,IAAA;AAAA,QAGT,IAAA,EAAM,CAAC,MAAA,KAAmB,IAAA,CAAK,IAAA;AAAA,QAC/B,MAAM,IAAA,CAAK;AAAA,OAAA;AAAA,MALN,IAAA,CAAK,KAAK,IAAA,CAAK;AAAA,KAOvB,CAAA;AAAA,IACA,YAAY,MAAA,GAAS,CAAA,wBACnB,WAAA,EAAA,EAAY,IAAA,EAAM,sBAAM,GAAA,CAAC,WAAA,EAAA,EAAY,GAAI,IAAA,EAAK,SAAA,EAC7C,8BAAC,cAAA,EAAA,EAAe,KAAA,EAAM,WACnB,QAAA,EAAA,WAAA,CAAY,GAAA,CAAI,CAAC,IAAA,qBAChB,GAAA;AAAA,MAAC,kBAAA;AAAA,MAAA;AAAA,QAEC,OAAO,IAAA,CAAK,KAAA;AAAA,QACZ,IAAI,IAAA,CAAK;AAAA,OAAA;AAAA,MAFJ,IAAA,CAAK,KAAK,IAAA,CAAK;AAAA,KAIvB,GACH,CAAA,EACF;AAAA,GAAA,EAEJ,CAAA;AAEJ;;;;"}
@@ -0,0 +1,99 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { Text } from '@backstage/ui';
3
+ import styles from './CountCards.module.css.esm.js';
4
+
5
+ function CountCards({
6
+ total,
7
+ counts,
8
+ selected,
9
+ onSelectFacet
10
+ }) {
11
+ return /* @__PURE__ */ jsxs("div", { className: styles.row, children: [
12
+ /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": "APIs", className: styles.card, children: [
13
+ /* @__PURE__ */ jsx(
14
+ Text,
15
+ {
16
+ as: "p",
17
+ variant: "title-large",
18
+ weight: "bold",
19
+ className: styles.total,
20
+ children: total
21
+ }
22
+ ),
23
+ /* @__PURE__ */ jsx(
24
+ Text,
25
+ {
26
+ as: "h3",
27
+ variant: "body-x-small",
28
+ color: "secondary",
29
+ className: styles.cardTitle,
30
+ children: "APIs"
31
+ }
32
+ )
33
+ ] }),
34
+ counts.map((count) => {
35
+ const heading = `By ${count.label.toLowerCase()}`;
36
+ return /* @__PURE__ */ jsxs(
37
+ "div",
38
+ {
39
+ role: "group",
40
+ "aria-label": heading,
41
+ className: styles.card,
42
+ children: [
43
+ /* @__PURE__ */ jsx(
44
+ Text,
45
+ {
46
+ as: "h3",
47
+ variant: "body-x-small",
48
+ color: "secondary",
49
+ className: styles.cardTitle,
50
+ children: heading
51
+ }
52
+ ),
53
+ count.buckets.length === 0 ? /* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", children: "No data" }) : /* @__PURE__ */ jsx("div", { className: styles.list, children: count.buckets.map((bucket) => {
54
+ const isSelected = selected[count.key].includes(bucket.value);
55
+ return /* @__PURE__ */ jsxs(
56
+ "button",
57
+ {
58
+ type: "button",
59
+ className: `${styles.bucket} ${isSelected ? styles.bucketSelected : ""}`,
60
+ "aria-pressed": isSelected,
61
+ "aria-label": `${bucket.label}, ${bucket.count} ${bucket.count === 1 ? "API" : "APIs"}`,
62
+ onClick: () => onSelectFacet(count.key, bucket.value),
63
+ children: [
64
+ /* @__PURE__ */ jsx(
65
+ Text,
66
+ {
67
+ as: "span",
68
+ variant: "body-small",
69
+ truncate: true,
70
+ title: bucket.label,
71
+ className: styles.bucketLabel,
72
+ children: bucket.label
73
+ }
74
+ ),
75
+ /* @__PURE__ */ jsx(
76
+ Text,
77
+ {
78
+ as: "span",
79
+ variant: "body-small",
80
+ weight: "bold",
81
+ className: styles.bucketCount,
82
+ children: bucket.count
83
+ }
84
+ )
85
+ ]
86
+ },
87
+ bucket.value
88
+ );
89
+ }) })
90
+ ]
91
+ },
92
+ count.key
93
+ );
94
+ })
95
+ ] });
96
+ }
97
+
98
+ export { CountCards };
99
+ //# sourceMappingURL=CountCards.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CountCards.esm.js","sources":["../../src/components/CountCards.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { Text } from '@backstage/ui';\nimport type { DerivedCount, FacetKey } from './facetModel';\nimport styles from './CountCards.module.css';\n\nexport interface CountCardsProps {\n /** Total APIs in the Apigee catalog (static snapshot — not the filtered count). */\n total: number;\n /** Per-facet breakdowns (org / lifecycle / department) from `deriveCounts`. */\n counts: DerivedCount[];\n /** Currently selected values per facet (from the URL filter state). */\n selected: Record<FacetKey, string[]>;\n /** Select a single facet value — clicking filters the catalog to exactly that value, replacing any other card selection. */\n onSelectFacet: (key: FacetKey, value: string) => void;\n}\n\n/**\n * Summary stat row shown above the filter toolbar: a prominent total-APIs card plus one\n * breakdown card per facet (org / lifecycle / department). Counts are a fixed snapshot of the\n * full Apigee catalog — they do NOT change as filters apply. Each bucket is a single-select link:\n * clicking filters the grid to exactly that value (replacing any other card selection), and the\n * active value is highlighted. Purely presentational.\n */\nexport function CountCards({\n total,\n counts,\n selected,\n onSelectFacet,\n}: CountCardsProps) {\n return (\n <div className={styles.row}>\n <div role=\"group\" aria-label=\"APIs\" className={styles.card}>\n <Text\n as=\"p\"\n variant=\"title-large\"\n weight=\"bold\"\n className={styles.total}\n >\n {total}\n </Text>\n <Text\n as=\"h3\"\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.cardTitle}\n >\n APIs\n </Text>\n </div>\n {counts.map(count => {\n const heading = `By ${count.label.toLowerCase()}`;\n return (\n <div\n key={count.key}\n role=\"group\"\n aria-label={heading}\n className={styles.card}\n >\n <Text\n as=\"h3\"\n variant=\"body-x-small\"\n color=\"secondary\"\n className={styles.cardTitle}\n >\n {heading}\n </Text>\n {count.buckets.length === 0 ? (\n <Text variant=\"body-small\" color=\"secondary\">\n No data\n </Text>\n ) : (\n <div className={styles.list}>\n {count.buckets.map(bucket => {\n const isSelected = selected[count.key].includes(bucket.value);\n return (\n <button\n key={bucket.value}\n type=\"button\"\n className={`${styles.bucket} ${\n isSelected ? styles.bucketSelected : ''\n }`}\n aria-pressed={isSelected}\n aria-label={`${bucket.label}, ${bucket.count} ${\n bucket.count === 1 ? 'API' : 'APIs'\n }`}\n onClick={() => onSelectFacet(count.key, bucket.value)}\n >\n <Text\n as=\"span\"\n variant=\"body-small\"\n truncate\n title={bucket.label}\n className={styles.bucketLabel}\n >\n {bucket.label}\n </Text>\n <Text\n as=\"span\"\n variant=\"body-small\"\n weight=\"bold\"\n className={styles.bucketCount}\n >\n {bucket.count}\n </Text>\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import styleInject from '../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
+
3
+ var css_248z = ".CountCards_row__c9b5cbf79d {\n display: flex;\n flex-wrap: wrap;\n gap: var(--bui-space-4);\n margin-bottom: var(--bui-space-4);\n}\n\n.CountCards_card__c9b5cbf79d {\n flex: 1 1 200px;\n min-width: 200px;\n padding: var(--bui-space-4);\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-3);\n background-color: var(--bui-bg-neutral-1);\n}\n\n.CountCards_total__c9b5cbf79d {\n line-height: 1.1;\n}\n\n.CountCards_cardTitle__c9b5cbf79d {\n margin-bottom: var(--bui-space-2);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.CountCards_list__c9b5cbf79d {\n max-height: 160px;\n overflow-y: auto;\n}\n\n.CountCards_bucket__c9b5cbf79d {\n display: flex;\n gap: var(--bui-space-2);\n align-items: baseline;\n justify-content: space-between;\n width: 100%;\n padding: var(--bui-space-1) var(--bui-space-2);\n border: none;\n border-radius: var(--bui-radius-2);\n background: none;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.CountCards_bucket__c9b5cbf79d:hover {\n background-color: var(--bui-bg-neutral-1-hover);\n}\n\n.CountCards_bucket__c9b5cbf79d:focus-visible {\n outline: 2px solid var(--bui-ring);\n outline-offset: -2px;\n}\n\n.CountCards_bucketSelected__c9b5cbf79d {\n background-color: var(--bui-bg-neutral-1-pressed);\n}\n\n.CountCards_bucketLabel__c9b5cbf79d {\n flex: 1;\n min-width: 0;\n}\n\n.CountCards_bucketCount__c9b5cbf79d {\n font-variant-numeric: tabular-nums;\n}\n";
4
+ var styles = {"row":"CountCards_row__c9b5cbf79d","card":"CountCards_card__c9b5cbf79d","total":"CountCards_total__c9b5cbf79d","cardTitle":"CountCards_cardTitle__c9b5cbf79d","list":"CountCards_list__c9b5cbf79d","bucket":"CountCards_bucket__c9b5cbf79d","bucketSelected":"CountCards_bucketSelected__c9b5cbf79d","bucketLabel":"CountCards_bucketLabel__c9b5cbf79d","bucketCount":"CountCards_bucketCount__c9b5cbf79d"};
5
+ styleInject(css_248z);
6
+
7
+ export { styles as default };
8
+ //# sourceMappingURL=CountCards.module.css.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CountCards.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -1,156 +1,87 @@
1
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { useState } from 'react';
3
- import { Box, Button, makeStyles, Chip, Menu, FormGroup, FormControlLabel, Checkbox } from '@material-ui/core';
4
- import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
5
- import Clear from '@material-ui/icons/Clear';
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState, useRef, useEffect } from 'react';
3
+ import { Button, Checkbox } from '@backstage/ui';
4
+ import { RiCloseLine, RiArrowDropDownLine } from '@remixicon/react';
5
+ import styles from './FilterToolbar.module.css.esm.js';
6
6
 
7
- const useStyles = makeStyles((theme) => ({
8
- toolbar: {
9
- display: "flex",
10
- flexWrap: "wrap",
11
- alignItems: "center",
12
- gap: theme.spacing(1),
13
- marginBottom: theme.spacing(2)
14
- },
15
- control: {
16
- "&:focus-visible": {
17
- outline: `2px solid ${theme.palette.primary.main}`,
18
- outlineOffset: 2
19
- }
20
- },
21
- clearAll: {
22
- marginLeft: "auto"
23
- },
24
- countChip: {
25
- marginLeft: theme.spacing(0.5)
26
- },
27
- formGroup: {
28
- padding: theme.spacing(0, 2)
29
- },
30
- menuFooter: {
31
- display: "flex",
32
- justifyContent: "flex-end",
33
- padding: theme.spacing(0.5, 1)
34
- },
35
- optionLabel: {
36
- display: "flex",
37
- alignItems: "center",
38
- gap: theme.spacing(0.5)
39
- },
40
- optionIcon: {
41
- fontSize: 16
42
- },
43
- srOnly: {
44
- position: "absolute",
45
- width: 1,
46
- height: 1,
47
- padding: 0,
48
- margin: -1,
49
- overflow: "hidden",
50
- clip: "rect(0 0 0 0)",
51
- whiteSpace: "nowrap",
52
- border: 0
53
- }
54
- }));
55
7
  function FacetDropdown({
56
8
  facet,
57
9
  selected,
58
10
  onToggle,
59
11
  onClear
60
12
  }) {
61
- const classes = useStyles();
62
- const [anchorEl, setAnchorEl] = useState(null);
63
- const open = Boolean(anchorEl);
13
+ const [open, setOpen] = useState(false);
14
+ const ref = useRef(null);
64
15
  const menuId = `facet-menu-${facet.key}`;
65
16
  const count = selected.length;
66
- return /* @__PURE__ */ jsxs(Fragment, { children: [
17
+ useEffect(() => {
18
+ if (!open) {
19
+ return void 0;
20
+ }
21
+ const onDocMouseDown = (event) => {
22
+ if (ref.current && !ref.current.contains(event.target)) {
23
+ setOpen(false);
24
+ }
25
+ };
26
+ const onKeyDown = (event) => {
27
+ if (event.key === "Escape") {
28
+ setOpen(false);
29
+ }
30
+ };
31
+ document.addEventListener("mousedown", onDocMouseDown);
32
+ document.addEventListener("keydown", onKeyDown);
33
+ return () => {
34
+ document.removeEventListener("mousedown", onDocMouseDown);
35
+ document.removeEventListener("keydown", onKeyDown);
36
+ };
37
+ }, [open]);
38
+ return /* @__PURE__ */ jsxs("div", { ref, className: styles.facet, children: [
67
39
  /* @__PURE__ */ jsxs(
68
40
  Button,
69
41
  {
70
- className: classes.control,
71
- variant: "outlined",
42
+ variant: count > 0 ? "primary" : "secondary",
72
43
  size: "small",
73
- disabled: !facet.enabled,
74
- endIcon: /* @__PURE__ */ jsx(ArrowDropDown, {}),
75
- color: count > 0 ? "primary" : "default",
44
+ isDisabled: !facet.enabled,
45
+ iconEnd: /* @__PURE__ */ jsx(RiArrowDropDownLine, {}),
76
46
  "aria-haspopup": "true",
77
47
  "aria-expanded": open,
78
48
  "aria-controls": open ? menuId : void 0,
79
49
  "aria-label": count > 0 ? `${facet.label} filter, ${count} selected` : `${facet.label} filter`,
80
- onClick: (event) => setAnchorEl(event.currentTarget),
50
+ onPress: () => setOpen((prev) => !prev),
81
51
  children: [
82
52
  facet.label,
83
- count > 0 ? /* @__PURE__ */ jsx(
84
- Chip,
85
- {
86
- className: classes.countChip,
87
- size: "small",
88
- color: "primary",
89
- label: count
90
- }
91
- ) : null
53
+ count > 0 ? /* @__PURE__ */ jsx("span", { className: styles.countChip, children: count }) : null
92
54
  ]
93
55
  }
94
56
  ),
95
- /* @__PURE__ */ jsxs(
96
- Menu,
57
+ open ? /* @__PURE__ */ jsxs(
58
+ "div",
97
59
  {
98
60
  id: menuId,
99
- anchorEl,
100
- open,
101
- onClose: () => setAnchorEl(null),
102
- getContentAnchorEl: null,
103
- anchorOrigin: { vertical: "bottom", horizontal: "left" },
104
- transformOrigin: { vertical: "top", horizontal: "left" },
61
+ className: styles.menu,
62
+ role: "group",
63
+ "aria-label": `${facet.label} options`,
105
64
  children: [
106
- /* @__PURE__ */ jsx(
107
- FormGroup,
108
- {
109
- className: classes.formGroup,
110
- role: "group",
111
- "aria-label": `${facet.label} options`,
112
- children: facet.options.map((option) => {
113
- const OptionIcon = facet.icons?.[option.value];
114
- return /* @__PURE__ */ jsx(
115
- FormControlLabel,
116
- {
117
- control: /* @__PURE__ */ jsx(
118
- Checkbox,
119
- {
120
- color: "primary",
121
- checked: selected.includes(option.value),
122
- onChange: () => onToggle(option.value)
123
- }
124
- ),
125
- label: OptionIcon ? /* @__PURE__ */ jsxs("span", { className: classes.optionLabel, children: [
126
- /* @__PURE__ */ jsx(
127
- OptionIcon,
128
- {
129
- className: classes.optionIcon,
130
- "aria-hidden": true
131
- }
132
- ),
133
- option.label
134
- ] }) : option.label
135
- },
136
- option.value
137
- );
138
- })
139
- }
140
- ),
141
- count > 0 ? /* @__PURE__ */ jsx("div", { className: classes.menuFooter, children: /* @__PURE__ */ jsx(
142
- Button,
143
- {
144
- className: classes.control,
145
- variant: "text",
146
- size: "small",
147
- onClick: onClear,
148
- children: "Clear"
149
- }
150
- ) }) : null
65
+ facet.options.map((option) => {
66
+ const OptionIcon = facet.icons?.[option.value];
67
+ return /* @__PURE__ */ jsx(
68
+ Checkbox,
69
+ {
70
+ className: styles.option,
71
+ isSelected: selected.includes(option.value),
72
+ onChange: () => onToggle(option.value),
73
+ children: OptionIcon ? /* @__PURE__ */ jsxs("span", { className: styles.optionLabel, children: [
74
+ /* @__PURE__ */ jsx(OptionIcon, { "aria-hidden": true, size: 16 }),
75
+ option.label
76
+ ] }) : option.label
77
+ },
78
+ option.value
79
+ );
80
+ }),
81
+ count > 0 ? /* @__PURE__ */ jsx("div", { className: styles.menuFooter, children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "small", onPress: onClear, children: "Clear" }) }) : null
151
82
  ]
152
83
  }
153
- )
84
+ ) : null
154
85
  ] });
155
86
  }
156
87
  function FilterToolbar({
@@ -163,9 +94,8 @@ function FilterToolbar({
163
94
  resultCount,
164
95
  searchBar
165
96
  }) {
166
- const classes = useStyles();
167
97
  return /* @__PURE__ */ jsxs("div", { role: "search", "aria-label": "Filter and search APIs", children: [
168
- /* @__PURE__ */ jsxs(Box, { className: classes.toolbar, children: [
98
+ /* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
169
99
  facets.map((facet) => /* @__PURE__ */ jsx(
170
100
  FacetDropdown,
171
101
  {
@@ -180,16 +110,16 @@ function FilterToolbar({
180
110
  active ? /* @__PURE__ */ jsx(
181
111
  Button,
182
112
  {
183
- className: `${classes.control} ${classes.clearAll}`,
184
- variant: "text",
113
+ className: styles.clearAll,
114
+ variant: "tertiary",
185
115
  size: "small",
186
- startIcon: /* @__PURE__ */ jsx(Clear, {}),
187
- onClick: onClearAll,
116
+ iconStart: /* @__PURE__ */ jsx(RiCloseLine, {}),
117
+ onPress: onClearAll,
188
118
  children: "Clear all"
189
119
  }
190
120
  ) : null
191
121
  ] }),
192
- /* @__PURE__ */ jsx("span", { className: classes.srOnly, "aria-live": "polite", children: `${resultCount} APIs match your filters` })
122
+ /* @__PURE__ */ jsx("span", { className: styles.srOnly, "aria-live": "polite", children: `${resultCount} APIs match your filters` })
193
123
  ] });
194
124
  }
195
125
 
@@ -1 +1 @@
1
- {"version":3,"file":"FilterToolbar.esm.js","sources":["../../src/components/FilterToolbar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport { useState, type ReactNode } from 'react';\r\nimport {\r\n Box,\r\n Button,\r\n Checkbox,\r\n Chip,\r\n FormControlLabel,\r\n FormGroup,\r\n Menu,\r\n makeStyles,\r\n} from '@material-ui/core';\r\nimport ArrowDropDown from '@material-ui/icons/ArrowDropDown';\r\nimport Clear from '@material-ui/icons/Clear';\r\nimport type { DerivedFacet, FacetKey } from './facetModel';\r\n\r\nexport interface FilterToolbarProps {\r\n /** Derived facets from `deriveFacets()`; ALL render (revised D-05) — one with no values is disabled. */\r\n facets: DerivedFacet[];\r\n /** Current selections per facet (hydrated from the URL state in 06-04). */\r\n selected: Record<FacetKey, string[]>;\r\n onToggleFacet: (key: FacetKey, value: string) => void;\r\n /** Per-facet menu-footer \"Clear\" (clears just that facet). */\r\n onClearFacet: (key: FacetKey) => void;\r\n /** Toolbar \"Clear all\" (resets every facet + search). */\r\n onClearAll: () => void;\r\n /** `isActive(state)` — gates the \"Clear all\" affordance. */\r\n active: boolean;\r\n /** Filtered result count for the polite live region (UI-07). */\r\n resultCount: number;\r\n /** `SearchBar` mounted inline in the toolbar row. */\r\n searchBar: ReactNode;\r\n}\r\n\r\nconst useStyles = makeStyles(theme => ({\r\n toolbar: {\r\n display: 'flex',\r\n flexWrap: 'wrap',\r\n alignItems: 'center',\r\n gap: theme.spacing(1),\r\n marginBottom: theme.spacing(2),\r\n },\r\n control: {\r\n '&:focus-visible': {\r\n outline: `2px solid ${theme.palette.primary.main}`,\r\n outlineOffset: 2,\r\n },\r\n },\r\n clearAll: {\r\n marginLeft: 'auto',\r\n },\r\n countChip: {\r\n marginLeft: theme.spacing(0.5),\r\n },\r\n formGroup: {\r\n padding: theme.spacing(0, 2),\r\n },\r\n menuFooter: {\r\n display: 'flex',\r\n justifyContent: 'flex-end',\r\n padding: theme.spacing(0.5, 1),\r\n },\r\n optionLabel: {\r\n display: 'flex',\r\n alignItems: 'center',\r\n gap: theme.spacing(0.5),\r\n },\r\n optionIcon: {\r\n fontSize: 16,\r\n },\r\n srOnly: {\r\n position: 'absolute',\r\n width: 1,\r\n height: 1,\r\n padding: 0,\r\n margin: -1,\r\n overflow: 'hidden',\r\n clip: 'rect(0 0 0 0)',\r\n whiteSpace: 'nowrap',\r\n border: 0,\r\n },\r\n}));\r\n\r\n/**\r\n * A single multi-select facet as an outlined menu-button (verified MUI v4 pattern —\r\n * 06-RESEARCH Pattern 7). The `Menu` sets `getContentAnchorEl={null}` so it bottom-anchors,\r\n * and its options are `FormControlLabel`+`Checkbox` inside a `FormGroup` (not clickable menu\r\n * rows) so toggling a checkbox keeps the menu OPEN for multi-select. All dynamic text (`facet.label`,\r\n * `option.label`) renders as React children only — never raw HTML (T-06-03-01).\r\n */\r\nfunction FacetDropdown({\r\n facet,\r\n selected,\r\n onToggle,\r\n onClear,\r\n}: {\r\n facet: DerivedFacet;\r\n selected: string[];\r\n onToggle: (value: string) => void;\r\n onClear: () => void;\r\n}) {\r\n const classes = useStyles();\r\n const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);\r\n const open = Boolean(anchorEl);\r\n const menuId = `facet-menu-${facet.key}`;\r\n const count = selected.length;\r\n\r\n return (\r\n <>\r\n <Button\r\n className={classes.control}\r\n variant=\"outlined\"\r\n size=\"small\"\r\n disabled={!facet.enabled}\r\n endIcon={<ArrowDropDown />}\r\n color={count > 0 ? 'primary' : 'default'}\r\n aria-haspopup=\"true\"\r\n aria-expanded={open}\r\n aria-controls={open ? menuId : undefined}\r\n aria-label={\r\n count > 0\r\n ? `${facet.label} filter, ${count} selected`\r\n : `${facet.label} filter`\r\n }\r\n onClick={event => setAnchorEl(event.currentTarget)}\r\n >\r\n {facet.label}\r\n {count > 0 ? (\r\n <Chip\r\n className={classes.countChip}\r\n size=\"small\"\r\n color=\"primary\"\r\n label={count}\r\n />\r\n ) : null}\r\n </Button>\r\n <Menu\r\n id={menuId}\r\n anchorEl={anchorEl}\r\n open={open}\r\n onClose={() => setAnchorEl(null)}\r\n getContentAnchorEl={null}\r\n anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}\r\n transformOrigin={{ vertical: 'top', horizontal: 'left' }}\r\n >\r\n <FormGroup\r\n className={classes.formGroup}\r\n role=\"group\"\r\n aria-label={`${facet.label} options`}\r\n >\r\n {facet.options.map(option => {\r\n const OptionIcon = facet.icons?.[option.value];\r\n return (\r\n <FormControlLabel\r\n key={option.value}\r\n control={\r\n <Checkbox\r\n color=\"primary\"\r\n checked={selected.includes(option.value)}\r\n onChange={() => onToggle(option.value)}\r\n />\r\n }\r\n label={\r\n OptionIcon ? (\r\n <span className={classes.optionLabel}>\r\n <OptionIcon\r\n className={classes.optionIcon}\r\n aria-hidden\r\n />\r\n {option.label}\r\n </span>\r\n ) : (\r\n option.label\r\n )\r\n }\r\n />\r\n );\r\n })}\r\n </FormGroup>\r\n {count > 0 ? (\r\n <div className={classes.menuFooter}>\r\n <Button\r\n className={classes.control}\r\n variant=\"text\"\r\n size=\"small\"\r\n onClick={onClear}\r\n >\r\n Clear\r\n </Button>\r\n </div>\r\n ) : null}\r\n </Menu>\r\n </>\r\n );\r\n}\r\n\r\n/**\r\n * Presentational Phase 6 toolbar (D-01 top toolbar, D-02 multi-select dropdowns). Renders EVERY\r\n * facet in `FACET_ORDER` (revised D-05 — a facet with no values renders disabled), the inline\r\n * `searchBar` slot, a conditional right-aligned \"Clear all\", and a polite `aria-live` result-count\r\n * region. Purely props-driven — no data fetching and no URL/provider access (that wiring is 06-04).\r\n * (UI-06, UI-07)\r\n */\r\nexport function FilterToolbar({\r\n facets,\r\n selected,\r\n onToggleFacet,\r\n onClearFacet,\r\n onClearAll,\r\n active,\r\n resultCount,\r\n searchBar,\r\n}: FilterToolbarProps) {\r\n const classes = useStyles();\r\n\r\n return (\r\n <div role=\"search\" aria-label=\"Filter and search APIs\">\r\n <Box className={classes.toolbar}>\r\n {facets.map(facet => (\r\n <FacetDropdown\r\n key={facet.key}\r\n facet={facet}\r\n selected={selected[facet.key]}\r\n onToggle={value => onToggleFacet(facet.key, value)}\r\n onClear={() => onClearFacet(facet.key)}\r\n />\r\n ))}\r\n {searchBar}\r\n {active ? (\r\n <Button\r\n className={`${classes.control} ${classes.clearAll}`}\r\n variant=\"text\"\r\n size=\"small\"\r\n startIcon={<Clear />}\r\n onClick={onClearAll}\r\n >\r\n Clear all\r\n </Button>\r\n ) : null}\r\n </Box>\r\n <span className={classes.srOnly} aria-live=\"polite\">\r\n {`${resultCount} APIs match your filters`}\r\n </span>\r\n </div>\r\n );\r\n}\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FilterToolbar.esm.js","sources":["../../src/components/FilterToolbar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Button, Checkbox } from '@backstage/ui';\nimport { RiArrowDropDownLine, RiCloseLine } from '@remixicon/react';\nimport type { DerivedFacet, FacetKey } from './facetModel';\nimport styles from './FilterToolbar.module.css';\n\nexport interface FilterToolbarProps {\n /** Derived facets from `deriveFacets()`; ALL render (revised D-05) — one with no values is disabled. */\n facets: DerivedFacet[];\n /** Current selections per facet (hydrated from the URL state in 06-04). */\n selected: Record<FacetKey, string[]>;\n onToggleFacet: (key: FacetKey, value: string) => void;\n /** Per-facet menu-footer \"Clear\" (clears just that facet). */\n onClearFacet: (key: FacetKey) => void;\n /** Toolbar \"Clear all\" (resets every facet + search). */\n onClearAll: () => void;\n /** `isActive(state)` — gates the \"Clear all\" affordance. */\n active: boolean;\n /** Filtered result count for the polite live region (UI-07). */\n resultCount: number;\n /** `SearchBar` mounted inline in the toolbar row. */\n searchBar: ReactNode;\n}\n\n/**\n * A single multi-select facet as an outlined disclosure button. Options are @backstage/ui\n * `Checkbox`es in a popover that stays OPEN while toggling (multi-select); the popover closes on\n * outside-click or Escape. All dynamic text (`facet.label`, `option.label`) renders as React\n * children only — never raw HTML (T-06-03-01).\n */\nfunction FacetDropdown({\n facet,\n selected,\n onToggle,\n onClear,\n}: {\n facet: DerivedFacet;\n selected: string[];\n onToggle: (value: string) => void;\n onClear: () => void;\n}) {\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n const menuId = `facet-menu-${facet.key}`;\n const count = selected.length;\n\n useEffect(() => {\n if (!open) {\n return undefined;\n }\n const onDocMouseDown = (event: MouseEvent) => {\n if (ref.current && !ref.current.contains(event.target as Node)) {\n setOpen(false);\n }\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', onDocMouseDown);\n document.addEventListener('keydown', onKeyDown);\n return () => {\n document.removeEventListener('mousedown', onDocMouseDown);\n document.removeEventListener('keydown', onKeyDown);\n };\n }, [open]);\n\n return (\n <div ref={ref} className={styles.facet}>\n <Button\n variant={count > 0 ? 'primary' : 'secondary'}\n size=\"small\"\n isDisabled={!facet.enabled}\n iconEnd={<RiArrowDropDownLine />}\n aria-haspopup=\"true\"\n aria-expanded={open}\n aria-controls={open ? menuId : undefined}\n aria-label={\n count > 0\n ? `${facet.label} filter, ${count} selected`\n : `${facet.label} filter`\n }\n onPress={() => setOpen(prev => !prev)}\n >\n {facet.label}\n {count > 0 ? <span className={styles.countChip}>{count}</span> : null}\n </Button>\n {open ? (\n <div\n id={menuId}\n className={styles.menu}\n role=\"group\"\n aria-label={`${facet.label} options`}\n >\n {facet.options.map(option => {\n const OptionIcon = facet.icons?.[option.value];\n return (\n <Checkbox\n key={option.value}\n className={styles.option}\n isSelected={selected.includes(option.value)}\n onChange={() => onToggle(option.value)}\n >\n {OptionIcon ? (\n <span className={styles.optionLabel}>\n <OptionIcon aria-hidden size={16} />\n {option.label}\n </span>\n ) : (\n option.label\n )}\n </Checkbox>\n );\n })}\n {count > 0 ? (\n <div className={styles.menuFooter}>\n <Button variant=\"tertiary\" size=\"small\" onPress={onClear}>\n Clear\n </Button>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Presentational Phase 6 toolbar (D-01 top toolbar, D-02 multi-select dropdowns). Renders EVERY\n * facet in `FACET_ORDER` (revised D-05 — a facet with no values renders disabled), the inline\n * `searchBar` slot, a conditional right-aligned \"Clear all\", and a polite `aria-live` result-count\n * region. Purely props-driven — no data fetching and no URL/provider access (that wiring is 06-04).\n * (UI-06, UI-07)\n */\nexport function FilterToolbar({\n facets,\n selected,\n onToggleFacet,\n onClearFacet,\n onClearAll,\n active,\n resultCount,\n searchBar,\n}: FilterToolbarProps) {\n return (\n <div role=\"search\" aria-label=\"Filter and search APIs\">\n <div className={styles.toolbar}>\n {facets.map(facet => (\n <FacetDropdown\n key={facet.key}\n facet={facet}\n selected={selected[facet.key]}\n onToggle={value => onToggleFacet(facet.key, value)}\n onClear={() => onClearFacet(facet.key)}\n />\n ))}\n {searchBar}\n {active ? (\n <Button\n className={styles.clearAll}\n variant=\"tertiary\"\n size=\"small\"\n iconStart={<RiCloseLine />}\n onPress={onClearAll}\n >\n Clear all\n </Button>\n ) : null}\n </div>\n <span className={styles.srOnly} aria-live=\"polite\">\n {`${resultCount} APIs match your filters`}\n </span>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;AA+BA,SAAS,aAAA,CAAc;AAAA,EACrB,KAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,EAKG;AACD,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAS,KAAK,CAAA;AACtC,EAAA,MAAM,GAAA,GAAM,OAAuB,IAAI,CAAA;AACvC,EAAA,MAAM,MAAA,GAAS,CAAA,WAAA,EAAc,KAAA,CAAM,GAAG,CAAA,CAAA;AACtC,EAAA,MAAM,QAAQ,QAAA,CAAS,MAAA;AAEvB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,MAAM,cAAA,GAAiB,CAAC,KAAA,KAAsB;AAC5C,MAAA,IAAI,GAAA,CAAI,WAAW,CAAC,GAAA,CAAI,QAAQ,QAAA,CAAS,KAAA,CAAM,MAAc,CAAA,EAAG;AAC9D,QAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,MACf;AAAA,IACF,CAAA;AACA,IAAA,MAAM,SAAA,GAAY,CAAC,KAAA,KAAyB;AAC1C,MAAA,IAAI,KAAA,CAAM,QAAQ,QAAA,EAAU;AAC1B,QAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,MACf;AAAA,IACF,CAAA;AACA,IAAA,QAAA,CAAS,gBAAA,CAAiB,aAAa,cAAc,CAAA;AACrD,IAAA,QAAA,CAAS,gBAAA,CAAiB,WAAW,SAAS,CAAA;AAC9C,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,mBAAA,CAAoB,aAAa,cAAc,CAAA;AACxD,MAAA,QAAA,CAAS,mBAAA,CAAoB,WAAW,SAAS,CAAA;AAAA,IACnD,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,GAAA,EAAU,SAAA,EAAW,OAAO,KAAA,EAC/B,QAAA,EAAA;AAAA,oBAAA,IAAA;AAAA,MAAC,MAAA;AAAA,MAAA;AAAA,QACC,OAAA,EAAS,KAAA,GAAQ,CAAA,GAAI,SAAA,GAAY,WAAA;AAAA,QACjC,IAAA,EAAK,OAAA;AAAA,QACL,UAAA,EAAY,CAAC,KAAA,CAAM,OAAA;AAAA,QACnB,OAAA,sBAAU,mBAAA,EAAA,EAAoB,CAAA;AAAA,QAC9B,eAAA,EAAc,MAAA;AAAA,QACd,eAAA,EAAe,IAAA;AAAA,QACf,eAAA,EAAe,OAAO,MAAA,GAAS,MAAA;AAAA,QAC/B,YAAA,EACE,KAAA,GAAQ,CAAA,GACJ,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,SAAA,EAAY,KAAK,CAAA,SAAA,CAAA,GAC/B,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,OAAA,CAAA;AAAA,QAEpB,OAAA,EAAS,MAAM,OAAA,CAAQ,CAAA,IAAA,KAAQ,CAAC,IAAI,CAAA;AAAA,QAEnC,QAAA,EAAA;AAAA,UAAA,KAAA,CAAM,KAAA;AAAA,UACN,KAAA,GAAQ,oBAAI,GAAA,CAAC,MAAA,EAAA,EAAK,WAAW,MAAA,CAAO,SAAA,EAAY,iBAAM,CAAA,GAAU;AAAA;AAAA;AAAA,KACnE;AAAA,IACC,IAAA,mBACC,IAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,EAAA,EAAI,MAAA;AAAA,QACJ,WAAW,MAAA,CAAO,IAAA;AAAA,QAClB,IAAA,EAAK,OAAA;AAAA,QACL,YAAA,EAAY,CAAA,EAAG,KAAA,CAAM,KAAK,CAAA,QAAA,CAAA;AAAA,QAEzB,QAAA,EAAA;AAAA,UAAA,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,MAAA,KAAU;AAC3B,YAAA,MAAM,UAAA,GAAa,KAAA,CAAM,KAAA,GAAQ,MAAA,CAAO,KAAK,CAAA;AAC7C,YAAA,uBACE,GAAA;AAAA,cAAC,QAAA;AAAA,cAAA;AAAA,gBAEC,WAAW,MAAA,CAAO,MAAA;AAAA,gBAClB,UAAA,EAAY,QAAA,CAAS,QAAA,CAAS,MAAA,CAAO,KAAK,CAAA;AAAA,gBAC1C,QAAA,EAAU,MAAM,QAAA,CAAS,MAAA,CAAO,KAAK,CAAA;AAAA,gBAEpC,QAAA,EAAA,UAAA,mBACC,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,OAAO,WAAA,EACtB,QAAA,EAAA;AAAA,kCAAA,GAAA,CAAC,UAAA,EAAA,EAAW,aAAA,EAAW,IAAA,EAAC,IAAA,EAAM,EAAA,EAAI,CAAA;AAAA,kBACjC,MAAA,CAAO;AAAA,iBAAA,EACV,IAEA,MAAA,CAAO;AAAA,eAAA;AAAA,cAXJ,MAAA,CAAO;AAAA,aAad;AAAA,UAEJ,CAAC,CAAA;AAAA,UACA,QAAQ,CAAA,mBACP,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,OAAO,UAAA,EACrB,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAO,OAAA,EAAQ,YAAW,IAAA,EAAK,OAAA,EAAQ,SAAS,OAAA,EAAS,QAAA,EAAA,OAAA,EAE1D,GACF,CAAA,GACE;AAAA;AAAA;AAAA,KACN,GACE;AAAA,GAAA,EACN,CAAA;AAEJ;AASO,SAAS,aAAA,CAAc;AAAA,EAC5B,MAAA;AAAA,EACA,QAAA;AAAA,EACA,aAAA;AAAA,EACA,YAAA;AAAA,EACA,UAAA;AAAA,EACA,MAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,IAAA,EAAK,QAAA,EAAS,cAAW,wBAAA,EAC5B,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,MAAA,CAAO,OAAA,EACpB,QAAA,EAAA;AAAA,MAAA,MAAA,CAAO,IAAI,CAAA,KAAA,qBACV,GAAA;AAAA,QAAC,aAAA;AAAA,QAAA;AAAA,UAEC,KAAA;AAAA,UACA,QAAA,EAAU,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA;AAAA,UAC5B,QAAA,EAAU,CAAA,KAAA,KAAS,aAAA,CAAc,KAAA,CAAM,KAAK,KAAK,CAAA;AAAA,UACjD,OAAA,EAAS,MAAM,YAAA,CAAa,KAAA,CAAM,GAAG;AAAA,SAAA;AAAA,QAJhC,KAAA,CAAM;AAAA,OAMd,CAAA;AAAA,MACA,SAAA;AAAA,MACA,MAAA,mBACC,GAAA;AAAA,QAAC,MAAA;AAAA,QAAA;AAAA,UACC,WAAW,MAAA,CAAO,QAAA;AAAA,UAClB,OAAA,EAAQ,UAAA;AAAA,UACR,IAAA,EAAK,OAAA;AAAA,UACL,SAAA,sBAAY,WAAA,EAAA,EAAY,CAAA;AAAA,UACxB,OAAA,EAAS,UAAA;AAAA,UACV,QAAA,EAAA;AAAA;AAAA,OAED,GACE;AAAA,KAAA,EACN,CAAA;AAAA,oBACA,GAAA,CAAC,UAAK,SAAA,EAAW,MAAA,CAAO,QAAQ,WAAA,EAAU,QAAA,EACvC,QAAA,EAAA,CAAA,EAAG,WAAW,CAAA,wBAAA,CAAA,EACjB;AAAA,GAAA,EACF,CAAA;AAEJ;;;;"}
@@ -0,0 +1,8 @@
1
+ import styleInject from '../node_modules_dist/style-inject/dist/style-inject.es.esm.js';
2
+
3
+ var css_248z = ".FilterToolbar_toolbar__a9218d3e6c {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--bui-space-2);\n margin-bottom: var(--bui-space-4);\n}\n\n.FilterToolbar_facet__a9218d3e6c {\n position: relative;\n}\n\n.FilterToolbar_countChip__a9218d3e6c {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 18px;\n height: 18px;\n margin-left: var(--bui-space-1);\n padding: 0 var(--bui-space-1);\n border-radius: var(--bui-radius-full);\n background-color: var(--bui-bg-solid);\n color: var(--bui-fg-solid);\n font-size: var(--bui-font-size-1);\n font-variant-numeric: tabular-nums;\n}\n\n.FilterToolbar_menu__a9218d3e6c {\n position: absolute;\n top: calc(100% + var(--bui-space-1));\n left: 0;\n z-index: 10;\n min-width: 200px;\n padding: var(--bui-space-2) 0;\n border: 1px solid var(--bui-border-1);\n border-radius: var(--bui-radius-3);\n background-color: var(--bui-bg-app);\n box-shadow: var(--bui-shadow);\n}\n\n.FilterToolbar_option__a9218d3e6c {\n padding: var(--bui-space-1) var(--bui-space-3);\n}\n\n.FilterToolbar_optionLabel__a9218d3e6c {\n display: inline-flex;\n align-items: center;\n gap: var(--bui-space-1);\n}\n\n.FilterToolbar_menuFooter__a9218d3e6c {\n display: flex;\n justify-content: flex-end;\n padding: var(--bui-space-1) var(--bui-space-2) 0;\n}\n\n.FilterToolbar_clearAll__a9218d3e6c {\n margin-left: auto;\n}\n\n.FilterToolbar_srOnly__a9218d3e6c {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n}\n";
4
+ var styles = {"toolbar":"FilterToolbar_toolbar__a9218d3e6c","facet":"FilterToolbar_facet__a9218d3e6c","countChip":"FilterToolbar_countChip__a9218d3e6c","menu":"FilterToolbar_menu__a9218d3e6c","option":"FilterToolbar_option__a9218d3e6c","optionLabel":"FilterToolbar_optionLabel__a9218d3e6c","menuFooter":"FilterToolbar_menuFooter__a9218d3e6c","clearAll":"FilterToolbar_clearAll__a9218d3e6c","srOnly":"FilterToolbar_srOnly__a9218d3e6c"};
5
+ styleInject(css_248z);
6
+
7
+ export { styles as default };
8
+ //# sourceMappingURL=FilterToolbar.module.css.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilterToolbar.module.css.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -1,45 +1,17 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { TextField, InputAdornment, IconButton, makeStyles } from '@material-ui/core';
3
- import Clear from '@material-ui/icons/Clear';
4
- import Search from '@material-ui/icons/Search';
2
+ import { SearchField } from '@backstage/ui';
3
+ import { RiSearchLine } from '@remixicon/react';
5
4
 
6
- const useStyles = makeStyles((theme) => ({
7
- field: {
8
- minWidth: 240,
9
- flex: "1 1 auto",
10
- "& input:focus-visible": {
11
- outline: `2px solid ${theme.palette.primary.main}`,
12
- outlineOffset: 2
13
- }
14
- },
15
- searchIcon: {
16
- color: theme.palette.text.secondary
17
- }
18
- }));
19
5
  function SearchBar({ value, onChange }) {
20
- const classes = useStyles();
21
6
  return /* @__PURE__ */ jsx(
22
- TextField,
7
+ SearchField,
23
8
  {
24
- className: classes.field,
25
- variant: "outlined",
26
- size: "small",
9
+ "aria-label": "Search APIs",
27
10
  placeholder: "Search APIs\u2026",
28
11
  value,
29
- onChange: (event) => onChange(event.target.value),
30
- inputProps: { "aria-label": "Search APIs" },
31
- InputProps: {
32
- startAdornment: /* @__PURE__ */ jsx(InputAdornment, { position: "start", children: /* @__PURE__ */ jsx(Search, { className: classes.searchIcon }) }),
33
- endAdornment: value ? /* @__PURE__ */ jsx(InputAdornment, { position: "end", children: /* @__PURE__ */ jsx(
34
- IconButton,
35
- {
36
- size: "small",
37
- "aria-label": "Clear search",
38
- onClick: () => onChange(""),
39
- children: /* @__PURE__ */ jsx(Clear, {})
40
- }
41
- ) }) : void 0
42
- }
12
+ onChange,
13
+ icon: /* @__PURE__ */ jsx(RiSearchLine, {}),
14
+ style: { flex: "1 1 auto", minWidth: 240 }
43
15
  }
44
16
  );
45
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SearchBar.esm.js","sources":["../../src/components/SearchBar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\r\nimport {\r\n IconButton,\r\n InputAdornment,\r\n TextField,\r\n makeStyles,\r\n} from '@material-ui/core';\r\nimport Clear from '@material-ui/icons/Clear';\r\nimport Search from '@material-ui/icons/Search';\r\n\r\nexport interface SearchBarProps {\r\n /** Controlled input value — echoes keystrokes instantly (no debounce here). */\r\n value: string;\r\n /** Reports every change; the 200 ms trailing debounce lives in `useApigeeFilters` (06-04). */\r\n onChange: (next: string) => void;\r\n}\r\n\r\nconst useStyles = makeStyles(theme => ({\r\n field: {\r\n minWidth: 240,\r\n flex: '1 1 auto',\r\n '& input:focus-visible': {\r\n outline: `2px solid ${theme.palette.primary.main}`,\r\n outlineOffset: 2,\r\n },\r\n },\r\n searchIcon: {\r\n color: theme.palette.text.secondary,\r\n },\r\n}));\r\n\r\n/**\r\n * Stateless, controlled outlined search field (UI-SPEC §Component Inventory #2). Holds NO state\r\n * and NO debounce — it purely echoes `value` and reports changes; the 200 ms trailing debounce\r\n * lives in the `useApigeeFilters` hook (06-04). Leading search icon, and a Clear\r\n * (✕) button shown only when `value` is non-empty. No spinner (UI-SPEC Assumption 9). The `value`\r\n * renders only as a controlled prop (React auto-escapes) — never raw HTML (T-06-03-01).\r\n */\r\nexport function SearchBar({ value, onChange }: SearchBarProps) {\r\n const classes = useStyles();\r\n\r\n return (\r\n <TextField\r\n className={classes.field}\r\n variant=\"outlined\"\r\n size=\"small\"\r\n placeholder=\"Search APIs…\"\r\n value={value}\r\n onChange={event => onChange(event.target.value)}\r\n inputProps={{ 'aria-label': 'Search APIs' }}\r\n InputProps={{\r\n startAdornment: (\r\n <InputAdornment position=\"start\">\r\n <Search className={classes.searchIcon} />\r\n </InputAdornment>\r\n ),\r\n endAdornment: value ? (\r\n <InputAdornment position=\"end\">\r\n <IconButton\r\n size=\"small\"\r\n aria-label=\"Clear search\"\r\n onClick={() => onChange('')}\r\n >\r\n <Clear />\r\n </IconButton>\r\n </InputAdornment>\r\n ) : undefined,\r\n }}\r\n />\r\n );\r\n}\r\n"],"names":[],"mappings":";;;;;AAiBA,MAAM,SAAA,GAAY,WAAW,CAAA,KAAA,MAAU;AAAA,EACrC,KAAA,EAAO;AAAA,IACL,QAAA,EAAU,GAAA;AAAA,IACV,IAAA,EAAM,UAAA;AAAA,IACN,uBAAA,EAAyB;AAAA,MACvB,OAAA,EAAS,CAAA,UAAA,EAAa,KAAA,CAAM,OAAA,CAAQ,QAAQ,IAAI,CAAA,CAAA;AAAA,MAChD,aAAA,EAAe;AAAA;AACjB,GACF;AAAA,EACA,UAAA,EAAY;AAAA,IACV,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK;AAAA;AAE9B,CAAA,CAAE,CAAA;AASK,SAAS,SAAA,CAAU,EAAE,KAAA,EAAO,QAAA,EAAS,EAAmB;AAC7D,EAAA,MAAM,UAAU,SAAA,EAAU;AAE1B,EAAA,uBACE,GAAA;AAAA,IAAC,SAAA;AAAA,IAAA;AAAA,MACC,WAAW,OAAA,CAAQ,KAAA;AAAA,MACnB,OAAA,EAAQ,UAAA;AAAA,MACR,IAAA,EAAK,OAAA;AAAA,MACL,WAAA,EAAY,mBAAA;AAAA,MACZ,KAAA;AAAA,MACA,QAAA,EAAU,CAAA,KAAA,KAAS,QAAA,CAAS,KAAA,CAAM,OAAO,KAAK,CAAA;AAAA,MAC9C,UAAA,EAAY,EAAE,YAAA,EAAc,aAAA,EAAc;AAAA,MAC1C,UAAA,EAAY;AAAA,QACV,cAAA,kBACE,GAAA,CAAC,cAAA,EAAA,EAAe,QAAA,EAAS,OAAA,EACvB,8BAAC,MAAA,EAAA,EAAO,SAAA,EAAW,OAAA,CAAQ,UAAA,EAAY,CAAA,EACzC,CAAA;AAAA,QAEF,YAAA,EAAc,KAAA,mBACZ,GAAA,CAAC,cAAA,EAAA,EAAe,UAAS,KAAA,EACvB,QAAA,kBAAA,GAAA;AAAA,UAAC,UAAA;AAAA,UAAA;AAAA,YACC,IAAA,EAAK,OAAA;AAAA,YACL,YAAA,EAAW,cAAA;AAAA,YACX,OAAA,EAAS,MAAM,QAAA,CAAS,EAAE,CAAA;AAAA,YAE1B,8BAAC,KAAA,EAAA,EAAM;AAAA;AAAA,WAEX,CAAA,GACE;AAAA;AACN;AAAA,GACF;AAEJ;;;;"}
1
+ {"version":3,"file":"SearchBar.esm.js","sources":["../../src/components/SearchBar.tsx"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\nimport { SearchField } from '@backstage/ui';\nimport { RiSearchLine } from '@remixicon/react';\n\nexport interface SearchBarProps {\n /** Controlled input value — echoes keystrokes instantly (no debounce here). */\n value: string;\n /** Reports every change; the 200 ms trailing debounce lives in `useApigeeFilters` (06-04). */\n onChange: (next: string) => void;\n}\n\n/**\n * Stateless, controlled search field (UI-SPEC §Component Inventory #2). Holds NO state\n * and NO debounce — it purely echoes `value` and reports changes; the 200 ms trailing debounce\n * lives in the `useApigeeFilters` hook (06-04). Leading search icon plus a built-in Clear (✕)\n * button that @backstage/ui `SearchField` shows only when `value` is non-empty. The `value`\n * renders only as a controlled prop (React auto-escapes) — never raw HTML (T-06-03-01).\n */\nexport function SearchBar({ value, onChange }: SearchBarProps) {\n return (\n <SearchField\n aria-label=\"Search APIs\"\n placeholder=\"Search APIs…\"\n value={value}\n onChange={onChange}\n icon={<RiSearchLine />}\n style={{ flex: '1 1 auto', minWidth: 240 }}\n />\n );\n}\n"],"names":[],"mappings":";;;;AAkBO,SAAS,SAAA,CAAU,EAAE,KAAA,EAAO,QAAA,EAAS,EAAmB;AAC7D,EAAA,uBACE,GAAA;AAAA,IAAC,WAAA;AAAA,IAAA;AAAA,MACC,YAAA,EAAW,aAAA;AAAA,MACX,WAAA,EAAY,mBAAA;AAAA,MACZ,KAAA;AAAA,MACA,QAAA;AAAA,MACA,IAAA,sBAAO,YAAA,EAAA,EAAa,CAAA;AAAA,MACpB,KAAA,EAAO,EAAE,IAAA,EAAM,UAAA,EAAY,UAAU,GAAA;AAAI;AAAA,GAC3C;AAEJ;;;;"}