@burdenoff/microfe-devportal 2026.1108.2 → 2026.1109.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"SectionNav.js","names":["OrgSwitcher","useDevportal","isSeller","useIsSellerSection","currentOrgId","activePhysicalOrgId","activeOrgId","setCurrentOrgId","setActivePhysicalOrgId","setActiveOrgId","tr","useTr","open","setOpen","useState","ref","useRef","useGetMyOrganizationsQuery","orgs","data","org","useEffect","handler","label","o","createOrgPath","orgSectionPath","v","cn","Building2","ChevronDown","Check","navigate","basePath","Plus"],"sources":["../../../src/devportal/components/SectionNav.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useLocation } from \"react-router\";\nimport {\n LayoutDashboard,\n AppWindow,\n Building2,\n Share2,\n DollarSign,\n BookOpen,\n Library,\n User,\n Settings,\n ChevronDown,\n Check,\n Plus,\n} from \"lucide-react\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n useIsSellerSection,\n orgSectionPath,\n} from \"../hooks/useIsSellerSection\";\nimport {\n useGetMyOrganizationsQuery,\n type GetMyOrganizationsQuery,\n} from \"../hooks/useGraphQL\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\n\ntype OrgItem = GetMyOrganizationsQuery[\"myDeveloperOrganizations\"][number];\n\ninterface SectionDef {\n path: string;\n i18nKey: string;\n fallback: string;\n icon: typeof LayoutDashboard;\n /** Additional sub-paths that should mark this section as active. */\n matchPrefixes?: string[];\n}\n\nconst SECTIONS: ReadonlyArray<SectionDef> = [\n {\n path: \"\",\n i18nKey: \"devportal.nav.dashboard\",\n fallback: \"Dashboard\",\n icon: LayoutDashboard,\n },\n {\n path: \"apps\",\n i18nKey: \"devportal.nav.apps\",\n fallback: \"Apps\",\n icon: AppWindow,\n },\n {\n path: \"organizations\",\n i18nKey: \"devportal.nav.organizations\",\n fallback: \"Organizations\",\n icon: Building2,\n },\n {\n path: \"delegations\",\n i18nKey: \"devportal.nav.delegations\",\n fallback: \"Delegations\",\n icon: Share2,\n },\n {\n path: \"revenue\",\n i18nKey: \"devportal.nav.revenue\",\n fallback: \"Revenue\",\n icon: DollarSign,\n },\n {\n path: \"docs\",\n i18nKey: \"devportal.nav.docs\",\n fallback: \"Docs\",\n icon: BookOpen,\n },\n {\n path: \"resources\",\n i18nKey: \"devportal.nav.resources\",\n fallback: \"Resources\",\n icon: Library,\n },\n {\n path: \"publisher/profile\",\n i18nKey: \"devportal.nav.publisher\",\n fallback: \"Publisher\",\n icon: User,\n matchPrefixes: [\"publisher\"],\n },\n {\n path: \"settings\",\n i18nKey: \"devportal.nav.settings\",\n fallback: \"Settings\",\n icon: Settings,\n },\n];\n\nfunction joinBaseAndPath(basePath: string | undefined, path: string): string {\n const base = (basePath ?? \"\").replace(/\\/$/, \"\");\n if (!path) return base || \"/\";\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction isSectionActive(\n pathname: string,\n basePath: string | undefined,\n section: SectionDef,\n): boolean {\n const sectionUrl = joinBaseAndPath(basePath, section.path);\n // Index/dashboard is active only when exactly matched (or with empty extra).\n if (section.path === \"\") {\n return pathname === sectionUrl || pathname === `${sectionUrl}/`;\n }\n const candidates = [section.path, ...(section.matchPrefixes ?? [])];\n return candidates.some((prefix) => {\n const url = joinBaseAndPath(basePath, prefix);\n return pathname === url || pathname.startsWith(`${url}/`);\n });\n}\n\n/**\n * Org context switcher — can be embedded standalone or inside SectionNav.\n *\n * Shared between the regular devportal and the seller portal: filters the\n * org list to PHYSICAL-kind orgs under /devportal/seller/** and DIGITAL-kind\n * everywhere else, and reads/writes the matching active-org slot\n * (activePhysicalOrgId vs activeOrgId) so this switcher and\n * useSellerOrganization (used by the item-creation flow) always agree on\n * which org is current. See DeveloperOrgKind (BOFF-7285).\n */\nexport const OrgSwitcher: FC = () => {\n const {\n activeOrgId,\n setActiveOrgId,\n activePhysicalOrgId,\n setActivePhysicalOrgId,\n navigate,\n basePath,\n } = useDevportal();\n const isSeller = useIsSellerSection();\n const currentOrgId = isSeller ? activePhysicalOrgId : activeOrgId;\n const setCurrentOrgId = isSeller ? setActivePhysicalOrgId : setActiveOrgId;\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n\n const { data } = useGetMyOrganizationsQuery({ fetchPolicy: \"cache-first\" });\n const allOrgs: OrgItem[] = data?.myDeveloperOrganizations ?? [];\n const orgs = allOrgs.filter((org) =>\n isSeller ? org.kind === \"PHYSICAL\" : org.kind !== \"PHYSICAL\",\n );\n\n // Auto-select when there's exactly one matching org and nothing is stored yet\n useEffect(() => {\n if (!currentOrgId && orgs.length === 1 && orgs[0]?.id) {\n setCurrentOrgId(orgs[0].id);\n }\n }, [orgs, currentOrgId, setCurrentOrgId]);\n\n // Close on outside click\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n if (open) document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [open]);\n\n const activeOrg = orgs.find((o) => o.id === currentOrgId);\n const label =\n activeOrg?.name ??\n (orgs.length === 0\n ? tr(\"devportal.nav.noOrg\", \"No organization\")\n : tr(\"devportal.nav.selectOrg\", \"Select org\"));\n\n const createOrgPath = orgSectionPath(isSeller, \"new\");\n\n return (\n <div ref={ref} className=\"relative flex-shrink-0\">\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors\",\n \"bg-bg-surface text-text-primary border-border-subtle hover:bg-accent\",\n )}\n aria-label={tr(\"devportal.nav.switchOrg\", \"Switch organization\")}\n >\n <Building2 size={13} className=\"text-primary flex-shrink-0\" />\n <span className=\"max-w-[140px] truncate\">{label}</span>\n <ChevronDown\n size={13}\n className={cn(\"transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div className=\"absolute right-0 top-full z-50 mt-1 min-w-[200px] rounded-md border border-border-subtle bg-popover shadow-lg py-1\">\n {orgs.map((org) => (\n <button\n key={org.id}\n type=\"button\"\n onClick={() => {\n setCurrentOrgId(org.id);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors hover:bg-accent\",\n org.id === currentOrgId\n ? \"text-primary font-medium\"\n : \"text-text-primary\",\n )}\n >\n <Check\n size={13}\n className={cn(\n \"flex-shrink-0\",\n org.id === currentOrgId\n ? \"opacity-100 text-primary\"\n : \"opacity-0\",\n )}\n />\n <span className=\"truncate\">{org.name}</span>\n </button>\n ))}\n {orgs.length > 0 && (\n <div className=\"my-1 border-t border-border-subtle\" />\n )}\n <button\n type=\"button\"\n onClick={() => {\n setOpen(false);\n navigate?.(`${basePath ?? \"\"}${createOrgPath}`);\n }}\n className=\"flex w-full items-center gap-2 px-3 py-2 text-sm text-left text-primary transition-colors hover:bg-accent\"\n >\n <Plus size={13} className=\"flex-shrink-0\" />\n <span className=\"truncate\">\n {tr(\"devportal.nav.newOrganization\", \"New organization\")}\n </span>\n </button>\n </div>\n )}\n </div>\n );\n};\n\n/**\n * Horizontal section nav rendered above the page content on every devportal\n * route. Gives developers a one-click jump between all top-level sections so\n * routes like OAuth, Delegations, Analytics, Revenue, Docs, Resources,\n * Publisher Profile, and Settings are reachable without typing URLs.\n *\n * Renders nothing when `basePath` is undefined (standalone/dev mode) so it\n * doesn't conflict with whatever harness wraps the MFE there.\n */\nexport const SectionNav: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { pathname } = useLocation();\n const tr = useTr();\n\n if (!basePath || !navigate) return null;\n\n const handleClick = (path: string) => {\n navigate(joinBaseAndPath(basePath, path));\n };\n\n return (\n <nav\n aria-label={tr(\"devportal.nav.sections\", \"Developer portal sections\")}\n className=\"border-b border-border-subtle bg-bg-surface/40 px-4 py-2\"\n data-testid=\"devportal-section-nav\"\n >\n <div className=\"flex items-center gap-2\">\n <ul className=\"flex flex-1 flex-wrap gap-1\">\n {SECTIONS.map((section) => {\n const active = isSectionActive(pathname, basePath, section);\n const Icon = section.icon;\n return (\n <li key={section.path}>\n <button\n type=\"button\"\n onClick={() => handleClick(section.path)}\n aria-current={active ? \"page\" : undefined}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors\",\n active\n ? \"bg-action-primary-bg text-action-primary-text\"\n : \"text-text-secondary hover:bg-accent hover:text-text-primary\",\n )}\n >\n <Icon size={14} />\n <span>{tr(section.i18nKey, section.fallback)}</span>\n </button>\n </li>\n );\n })}\n </ul>\n <OrgSwitcher />\n </div>\n </nav>\n );\n};\n\n// Exported for unit tests that want to assert against the canonical list.\nexport const __SECTION_NAV_SECTIONS = SECTIONS;\n"],"mappings":";;;;;;;;;;;AAoIA,IAAaA,UAAwB;CACnC,IAAM,EACJ,gBACA,mBACA,wBACA,2BACA,aACA,gBACEC,EAAa,GACXC,IAAWC,EAAmB,GAC9BC,IAAeF,IAAWG,IAAsBC,GAChDC,IAAkBL,IAAWM,IAAyBC,GACtDC,IAAKC,EAAM,GACX,CAACC,GAAMC,KAAWC,EAAS,EAAK,GAChCC,IAAMC,EAAuB,IAAI,GAEjC,EAAE,YAASC,EAA2B,EAAE,aAAa,cAAc,CAAC,GAEpEC,KADqBC,GAAM,4BAA4B,CAAC,EAAA,CACzC,QAAQ,MAC3BjB,IAAWkB,EAAI,SAAS,aAAaA,EAAI,SAAS,UACpD;CAUA,AAPAC,QAAgB;EACd,AAAI,CAACjB,KAAgBc,EAAK,WAAW,KAAKA,EAAK,EAAE,EAAE,MACjDX,EAAgBW,EAAK,EAAE,CAAC,EAAE;CAE9B,GAAG;EAACA;EAAMd;EAAcG;CAAe,CAAC,GAGxCc,QAAgB;EACd,IAAMC,KAAW,MAAkB;GACjC,AAAIP,EAAI,WAAW,CAACA,EAAI,QAAQ,SAAS,EAAE,MAAc,KACvDF,EAAQ,EAAK;EAEjB;EAEA,OADID,KAAM,SAAS,iBAAiB,aAAaU,CAAO,SAC3C,SAAS,oBAAoB,aAAaA,CAAO;CAChE,GAAG,CAACV,CAAI,CAAC;CAGT,IAAMW,IADYL,EAAK,MAAM,MAAMM,EAAE,OAAOpB,CAE1C,CAAA,EAAW,SACVc,EAAK,WAAW,IACbR,EAAG,uBAAuB,iBAAiB,IAC3CA,EAAG,2BAA2B,YAAY,IAE1Ce,IAAgBC,EAAexB,GAAU,KAAK;CAEpD,OACE,kBAAC,OAAD;EAAU;EAAK,WAAU;EAAzB,UAAA,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAeW,GAAS,MAAM,CAACc,CAAC;GAChC,WAAWC,EACT,wGACA,sEACF;GACA,cAAYlB,EAAG,2BAA2B,qBAAqB;GAPjE,UAAA;IASE,kBAACmB,GAAD;KAAW,MAAM;KAAI,WAAU;IAA8B,CAAA;IAC7D,kBAAC,QAAD;KAAM,WAAU;KAA0B,UAAAN;IAAY,CAAA;IACtD,kBAACO,GAAD;KACE,MAAM;KACN,WAAWF,EAAG,wBAAwBhB,KAAQ,YAAY;IAC3D,CAAA;GACK;EAEP,CAAA,GAAAA,KACC,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACGM,EAAK,KAAK,MACT,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe;MAEb,AADAX,EAAgBa,EAAI,EAAE,GACtBP,EAAQ,EAAK;KACf;KACA,WAAWe,EACT,gGACAR,EAAI,OAAOhB,IACP,6BACA,mBACN;KAZF,UAAA,CAcE,kBAAC2B,GAAD;MACE,MAAM;MACN,WAAWH,EACT,iBACAR,EAAI,OAAOhB,IACP,6BACA,WACN;KACD,CAAA,GACD,kBAAC,QAAD;MAAM,WAAU;MAAY,UAAAgB,EAAI;KAAW,CAAA,CACrC;IAvBD,GAAAA,EAAI,EAuBH,CACT;IACAF,EAAK,SAAS,KACb,kBAAC,OAAD,EAAK,WAAU,qCAAsC,CAAA;IAEvD,kBAAC,UAAD;KACE,MAAK;KACL,eAAe;MAEb,AADAL,EAAQ,EAAK,GACbmB,IAAW,GAAGC,KAAY,KAAKR,GAAe;KAChD;KACA,WAAU;KANZ,UAAA,CAQE,kBAACS,GAAD;MAAM,MAAM;MAAI,WAAU;KAAiB,CAAA,GAC3C,kBAAC,QAAD;MAAM,WAAU;MACb,UAAAxB,EAAG,iCAAiC,kBAAkB;KACnD,CAAA,CACA;;GACL;EAEJ,CAAA,CAAA;;AAET"}
1
+ {"version":3,"file":"SectionNav.js","names":["OrgSwitcher","useDevportal","isSeller","useIsSellerSection","currentOrgId","activePhysicalOrgId","activeOrgId","setCurrentOrgId","setActivePhysicalOrgId","setActiveOrgId","tr","useTr","open","setOpen","useState","ref","useRef","useGetMyOrganizationsQuery","orgs","data","org","useEffect","handler","label","o","createOrgPath","orgSectionPath","v","cn","Building2","ChevronDown","Check","navigate","basePath","Plus"],"sources":["../../../src/devportal/components/SectionNav.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useLocation } from \"react-router\";\nimport {\n LayoutDashboard,\n AppWindow,\n Blocks,\n Building2,\n Share2,\n DollarSign,\n BookOpen,\n Library,\n User,\n Settings,\n ChevronDown,\n Check,\n Plus,\n} from \"lucide-react\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n useIsSellerSection,\n orgSectionPath,\n} from \"../hooks/useIsSellerSection\";\nimport {\n useGetMyOrganizationsQuery,\n type GetMyOrganizationsQuery,\n} from \"../hooks/useGraphQL\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\n\ntype OrgItem = GetMyOrganizationsQuery[\"myDeveloperOrganizations\"][number];\n\ninterface SectionDef {\n path: string;\n i18nKey: string;\n fallback: string;\n icon: typeof LayoutDashboard;\n /** Additional sub-paths that should mark this section as active. */\n matchPrefixes?: string[];\n}\n\nconst SECTIONS: ReadonlyArray<SectionDef> = [\n {\n path: \"\",\n i18nKey: \"devportal.nav.dashboard\",\n fallback: \"Dashboard\",\n icon: LayoutDashboard,\n },\n {\n path: \"apps\",\n i18nKey: \"devportal.nav.apps\",\n fallback: \"Apps\",\n icon: AppWindow,\n },\n {\n path: \"timecampus-applets\",\n i18nKey: \"devportal.nav.timecampusApplets\",\n fallback: \"TimeCampus Applets\",\n icon: Blocks,\n },\n {\n path: \"organizations\",\n i18nKey: \"devportal.nav.organizations\",\n fallback: \"Organizations\",\n icon: Building2,\n },\n {\n path: \"delegations\",\n i18nKey: \"devportal.nav.delegations\",\n fallback: \"Delegations\",\n icon: Share2,\n },\n {\n path: \"revenue\",\n i18nKey: \"devportal.nav.revenue\",\n fallback: \"Revenue\",\n icon: DollarSign,\n },\n {\n path: \"docs\",\n i18nKey: \"devportal.nav.docs\",\n fallback: \"Docs\",\n icon: BookOpen,\n },\n {\n path: \"resources\",\n i18nKey: \"devportal.nav.resources\",\n fallback: \"Resources\",\n icon: Library,\n },\n {\n path: \"publisher/profile\",\n i18nKey: \"devportal.nav.publisher\",\n fallback: \"Publisher\",\n icon: User,\n matchPrefixes: [\"publisher\"],\n },\n {\n path: \"settings\",\n i18nKey: \"devportal.nav.settings\",\n fallback: \"Settings\",\n icon: Settings,\n },\n];\n\nfunction joinBaseAndPath(basePath: string | undefined, path: string): string {\n const base = (basePath ?? \"\").replace(/\\/$/, \"\");\n if (!path) return base || \"/\";\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction isSectionActive(\n pathname: string,\n basePath: string | undefined,\n section: SectionDef,\n): boolean {\n const sectionUrl = joinBaseAndPath(basePath, section.path);\n // Index/dashboard is active only when exactly matched (or with empty extra).\n if (section.path === \"\") {\n return pathname === sectionUrl || pathname === `${sectionUrl}/`;\n }\n const candidates = [section.path, ...(section.matchPrefixes ?? [])];\n return candidates.some((prefix) => {\n const url = joinBaseAndPath(basePath, prefix);\n return pathname === url || pathname.startsWith(`${url}/`);\n });\n}\n\n/**\n * Org context switcher — can be embedded standalone or inside SectionNav.\n *\n * Shared between the regular devportal and the seller portal: filters the\n * org list to PHYSICAL-kind orgs under /devportal/seller/** and DIGITAL-kind\n * everywhere else, and reads/writes the matching active-org slot\n * (activePhysicalOrgId vs activeOrgId) so this switcher and\n * useSellerOrganization (used by the item-creation flow) always agree on\n * which org is current. See DeveloperOrgKind (BOFF-7285).\n */\nexport const OrgSwitcher: FC = () => {\n const {\n activeOrgId,\n setActiveOrgId,\n activePhysicalOrgId,\n setActivePhysicalOrgId,\n navigate,\n basePath,\n } = useDevportal();\n const isSeller = useIsSellerSection();\n const currentOrgId = isSeller ? activePhysicalOrgId : activeOrgId;\n const setCurrentOrgId = isSeller ? setActivePhysicalOrgId : setActiveOrgId;\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n\n const { data } = useGetMyOrganizationsQuery({ fetchPolicy: \"cache-first\" });\n const allOrgs: OrgItem[] = data?.myDeveloperOrganizations ?? [];\n const orgs = allOrgs.filter((org) =>\n isSeller ? org.kind === \"PHYSICAL\" : org.kind !== \"PHYSICAL\",\n );\n\n // Auto-select when there's exactly one matching org and nothing is stored yet\n useEffect(() => {\n if (!currentOrgId && orgs.length === 1 && orgs[0]?.id) {\n setCurrentOrgId(orgs[0].id);\n }\n }, [orgs, currentOrgId, setCurrentOrgId]);\n\n // Close on outside click\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n if (open) document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [open]);\n\n const activeOrg = orgs.find((o) => o.id === currentOrgId);\n const label =\n activeOrg?.name ??\n (orgs.length === 0\n ? tr(\"devportal.nav.noOrg\", \"No organization\")\n : tr(\"devportal.nav.selectOrg\", \"Select org\"));\n\n const createOrgPath = orgSectionPath(isSeller, \"new\");\n\n return (\n <div ref={ref} className=\"relative flex-shrink-0\">\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors\",\n \"bg-bg-surface text-text-primary border-border-subtle hover:bg-accent\",\n )}\n aria-label={tr(\"devportal.nav.switchOrg\", \"Switch organization\")}\n >\n <Building2 size={13} className=\"text-primary flex-shrink-0\" />\n <span className=\"max-w-[140px] truncate\">{label}</span>\n <ChevronDown\n size={13}\n className={cn(\"transition-transform\", open && \"rotate-180\")}\n />\n </button>\n\n {open && (\n <div className=\"absolute right-0 top-full z-50 mt-1 min-w-[200px] rounded-md border border-border-subtle bg-popover shadow-lg py-1\">\n {orgs.map((org) => (\n <button\n key={org.id}\n type=\"button\"\n onClick={() => {\n setCurrentOrgId(org.id);\n setOpen(false);\n }}\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors hover:bg-accent\",\n org.id === currentOrgId\n ? \"text-primary font-medium\"\n : \"text-text-primary\",\n )}\n >\n <Check\n size={13}\n className={cn(\n \"flex-shrink-0\",\n org.id === currentOrgId\n ? \"opacity-100 text-primary\"\n : \"opacity-0\",\n )}\n />\n <span className=\"truncate\">{org.name}</span>\n </button>\n ))}\n {orgs.length > 0 && (\n <div className=\"my-1 border-t border-border-subtle\" />\n )}\n <button\n type=\"button\"\n onClick={() => {\n setOpen(false);\n navigate?.(`${basePath ?? \"\"}${createOrgPath}`);\n }}\n className=\"flex w-full items-center gap-2 px-3 py-2 text-sm text-left text-primary transition-colors hover:bg-accent\"\n >\n <Plus size={13} className=\"flex-shrink-0\" />\n <span className=\"truncate\">\n {tr(\"devportal.nav.newOrganization\", \"New organization\")}\n </span>\n </button>\n </div>\n )}\n </div>\n );\n};\n\n/**\n * Horizontal section nav rendered above the page content on every devportal\n * route. Gives developers a one-click jump between all top-level sections so\n * routes like OAuth, Delegations, Analytics, Revenue, Docs, Resources,\n * Publisher Profile, and Settings are reachable without typing URLs.\n *\n * Renders nothing when `basePath` is undefined (standalone/dev mode) so it\n * doesn't conflict with whatever harness wraps the MFE there.\n */\nexport const SectionNav: FC = () => {\n const { navigate, basePath } = useDevportal();\n const { pathname } = useLocation();\n const tr = useTr();\n\n if (!basePath || !navigate) return null;\n\n const handleClick = (path: string) => {\n navigate(joinBaseAndPath(basePath, path));\n };\n\n return (\n <nav\n aria-label={tr(\"devportal.nav.sections\", \"Developer portal sections\")}\n className=\"border-b border-border-subtle bg-bg-surface/40 px-4 py-2\"\n data-testid=\"devportal-section-nav\"\n >\n <div className=\"flex items-center gap-2\">\n <ul className=\"flex flex-1 flex-wrap gap-1\">\n {SECTIONS.map((section) => {\n const active = isSectionActive(pathname, basePath, section);\n const Icon = section.icon;\n return (\n <li key={section.path}>\n <button\n type=\"button\"\n onClick={() => handleClick(section.path)}\n aria-current={active ? \"page\" : undefined}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors\",\n active\n ? \"bg-action-primary-bg text-action-primary-text\"\n : \"text-text-secondary hover:bg-accent hover:text-text-primary\",\n )}\n >\n <Icon size={14} />\n <span>{tr(section.i18nKey, section.fallback)}</span>\n </button>\n </li>\n );\n })}\n </ul>\n <OrgSwitcher />\n </div>\n </nav>\n );\n};\n\n// Exported for unit tests that want to assert against the canonical list.\nexport const __SECTION_NAV_SECTIONS = SECTIONS;\n"],"mappings":";;;;;;;;;;;AA2IA,IAAaA,UAAwB;CACnC,IAAM,EACJ,gBACA,mBACA,wBACA,2BACA,aACA,gBACEC,EAAa,GACXC,IAAWC,EAAmB,GAC9BC,IAAeF,IAAWG,IAAsBC,GAChDC,IAAkBL,IAAWM,IAAyBC,GACtDC,IAAKC,EAAM,GACX,CAACC,GAAMC,KAAWC,EAAS,EAAK,GAChCC,IAAMC,EAAuB,IAAI,GAEjC,EAAE,YAASC,EAA2B,EAAE,aAAa,cAAc,CAAC,GAEpEC,KADqBC,GAAM,4BAA4B,CAAC,EAAA,CACzC,QAAQ,MAC3BjB,IAAWkB,EAAI,SAAS,aAAaA,EAAI,SAAS,UACpD;CAUA,AAPAC,QAAgB;EACd,AAAI,CAACjB,KAAgBc,EAAK,WAAW,KAAKA,EAAK,EAAE,EAAE,MACjDX,EAAgBW,EAAK,EAAE,CAAC,EAAE;CAE9B,GAAG;EAACA;EAAMd;EAAcG;CAAe,CAAC,GAGxCc,QAAgB;EACd,IAAMC,KAAW,MAAkB;GACjC,AAAIP,EAAI,WAAW,CAACA,EAAI,QAAQ,SAAS,EAAE,MAAc,KACvDF,EAAQ,EAAK;EAEjB;EAEA,OADID,KAAM,SAAS,iBAAiB,aAAaU,CAAO,SAC3C,SAAS,oBAAoB,aAAaA,CAAO;CAChE,GAAG,CAACV,CAAI,CAAC;CAGT,IAAMW,IADYL,EAAK,MAAM,MAAMM,EAAE,OAAOpB,CAE1C,CAAA,EAAW,SACVc,EAAK,WAAW,IACbR,EAAG,uBAAuB,iBAAiB,IAC3CA,EAAG,2BAA2B,YAAY,IAE1Ce,IAAgBC,EAAexB,GAAU,KAAK;CAEpD,OACE,kBAAC,OAAD;EAAU;EAAK,WAAU;EAAzB,UAAA,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAeW,GAAS,MAAM,CAACc,CAAC;GAChC,WAAWC,EACT,wGACA,sEACF;GACA,cAAYlB,EAAG,2BAA2B,qBAAqB;GAPjE,UAAA;IASE,kBAACmB,GAAD;KAAW,MAAM;KAAI,WAAU;IAA8B,CAAA;IAC7D,kBAAC,QAAD;KAAM,WAAU;KAA0B,UAAAN;IAAY,CAAA;IACtD,kBAACO,GAAD;KACE,MAAM;KACN,WAAWF,EAAG,wBAAwBhB,KAAQ,YAAY;IAC3D,CAAA;GACK;EAEP,CAAA,GAAAA,KACC,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACGM,EAAK,KAAK,MACT,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe;MAEb,AADAX,EAAgBa,EAAI,EAAE,GACtBP,EAAQ,EAAK;KACf;KACA,WAAWe,EACT,gGACAR,EAAI,OAAOhB,IACP,6BACA,mBACN;KAZF,UAAA,CAcE,kBAAC2B,GAAD;MACE,MAAM;MACN,WAAWH,EACT,iBACAR,EAAI,OAAOhB,IACP,6BACA,WACN;KACD,CAAA,GACD,kBAAC,QAAD;MAAM,WAAU;MAAY,UAAAgB,EAAI;KAAW,CAAA,CACrC;IAvBD,GAAAA,EAAI,EAuBH,CACT;IACAF,EAAK,SAAS,KACb,kBAAC,OAAD,EAAK,WAAU,qCAAsC,CAAA;IAEvD,kBAAC,UAAD;KACE,MAAK;KACL,eAAe;MAEb,AADAL,EAAQ,EAAK,GACbmB,IAAW,GAAGC,KAAY,KAAKR,GAAe;KAChD;KACA,WAAU;KANZ,UAAA,CAQE,kBAACS,GAAD;MAAM,MAAM;MAAI,WAAU;KAAiB,CAAA,GAC3C,kBAAC,QAAD;MAAM,WAAU;MACb,UAAAxB,EAAG,iCAAiC,kBAAkB;KACnD,CAAA,CACA;;GACL;EAEJ,CAAA,CAAA;;AAET"}
@@ -0,0 +1,2 @@
1
+ import { FC } from 'react';
2
+ export declare const TimeCampusAppletsPage: FC;
@@ -0,0 +1,490 @@
1
+ import { PageLayout as e, PageSection as t } from "../components/PageLayout.js";
2
+ import { EmptyState as n } from "../components/EmptyState.js";
3
+ import { LoadingState as r } from "../components/LoadingState.js";
4
+ import { StatusBadge as i } from "../components/StatusBadge.js";
5
+ import { useDevportal as a } from "../providers/DevportalProvider.js";
6
+ import { useTr as o } from "../utils/i18n.js";
7
+ import { TIMECAMPUS_APPLETS_DOCS_URL as s, TIMECAMPUS_MANIFEST_DOCS_URL as c, TIMECAMPUS_SAMPLE_WIDGET_URL as l, canAddVersion as u, canEditVersion as d, canWithdrawFromMarketplace as f, errorText as p, formatDate as m, manifestForNextVersion as h, manifestText as g, marketplaceOffer as _, parseManifestText as v, starterManifest as y, versionAction as b } from "../utils/timecampusApplets.js";
8
+ import { useDevportalAddTimeCampusAppletVersionMutation as x, useDevportalCreateTimeCampusAppletMutation as S, useDevportalSubmitTimeCampusAppletToMarketplaceMutation as C, useDevportalSubmitTimeCampusAppletVersionMutation as w, useDevportalTimeCampusAppletsQuery as T, useDevportalUpdateTimeCampusAppletVersionMutation as E, useDevportalValidateTimeCampusAppletManifestLazyQuery as D, useDevportalWithdrawTimeCampusAppletFromMarketplaceMutation as O, useDevportalWithdrawTimeCampusAppletVersionMutation as k, useMyWorkspacesQuery as A } from "../../generated/wspace-operations.js";
9
+ import { useState as j } from "react";
10
+ import { BookOpen as M, CheckCircle2 as N, ExternalLink as P, FilePlus2 as F, Globe as I, Pencil as L, Puzzle as R, Send as z, Undo2 as B, Upload as V } from "lucide-react";
11
+ import { Fragment as H, jsx as U, jsxs as W } from "react/jsx-runtime";
12
+ import { GlassCard as G } from "@burdenoff/fe-libs/ui";
13
+ import { useSafeGatewayClient as K } from "@burdenoff/fe-libs/shared/graphql";
14
+ // src/devportal/pages/TimeCampusAppletsPage.tsx
15
+ var q = ["DevportalTimeCampusApplets"], J = "inline-flex items-center justify-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50", Y = "inline-flex items-center justify-center gap-1.5 rounded-md border border-border-subtle bg-bg-surface px-3 py-1.5 text-sm font-medium text-text-primary hover:bg-bg-surface-hover disabled:opacity-50";
16
+ function X(e) {
17
+ return e.kind === "new" ? "new" : e.kind === "version" ? `version-${e.appletId}` : `edit-${e.versionId}`;
18
+ }
19
+ var ee = ({ target: e, initialText: t, onClose: n, onSaved: r }) => {
20
+ let i = o(), a = K("workspace") ?? void 0, [s, l] = j(t), [u, d] = j([]), [f, m] = j(!1), [h, { loading: g }] = D({
21
+ client: a,
22
+ fetchPolicy: "no-cache"
23
+ }), [_, { loading: y }] = S({
24
+ client: a,
25
+ refetchQueries: q,
26
+ awaitRefetchQueries: !0
27
+ }), [b, { loading: C }] = x({
28
+ client: a,
29
+ refetchQueries: q,
30
+ awaitRefetchQueries: !0
31
+ }), [w, { loading: T }] = E({
32
+ client: a,
33
+ refetchQueries: q,
34
+ awaitRefetchQueries: !0
35
+ }), O = y || C || T, k = () => {
36
+ let e = v(s);
37
+ return e.ok ? e.manifest : (d([e.error]), m(!1), null);
38
+ }, A = async () => {
39
+ let e = k();
40
+ if (e) try {
41
+ let { data: t } = await h({ variables: { manifest: e } }), n = t?.validateTimeCampusAppletManifest;
42
+ d(n?.errors ?? []), m(n?.valid === !0);
43
+ } catch (e) {
44
+ d([p(e)]), m(!1);
45
+ }
46
+ }, M = async () => {
47
+ let t = k();
48
+ if (t) try {
49
+ e.kind === "new" ? (await _({ variables: { manifest: t } }), r(i("devportal.timecampusApplets.registered", "Applet registered as a draft. Submit it for review when it is ready."))) : e.kind === "edit" ? (await w({ variables: {
50
+ versionId: e.versionId,
51
+ manifest: t
52
+ } }), r(i("devportal.timecampusApplets.versionUpdated", "Manifest saved. The version is a draft."))) : (await b({ variables: {
53
+ appletId: e.appletId,
54
+ manifest: t
55
+ } }), r(i("devportal.timecampusApplets.versionAdded", "New version saved as a draft.")));
56
+ } catch (e) {
57
+ d([p(e)]), m(!1);
58
+ }
59
+ }, F = e.kind === "new" ? i("devportal.timecampusApplets.registerTitle", "Register an applet") : e.kind === "edit" ? i("devportal.timecampusApplets.editVersionTitle", "Edit v{{version}} of {{name}}", {
60
+ name: e.appletName,
61
+ version: e.version
62
+ }) : i("devportal.timecampusApplets.newVersionTitle", "Upload a new version of {{name}}", { name: e.appletName });
63
+ return /* @__PURE__ */ W(G, {
64
+ className: "flex flex-col gap-3 p-4",
65
+ "data-testid": "tc-manifest-editor",
66
+ children: [
67
+ /* @__PURE__ */ W("div", {
68
+ className: "flex flex-wrap items-center justify-between gap-2",
69
+ children: [/* @__PURE__ */ U("h2", {
70
+ className: "text-base font-semibold text-text-primary",
71
+ children: F
72
+ }), /* @__PURE__ */ W("a", {
73
+ href: c,
74
+ target: "_blank",
75
+ rel: "noopener noreferrer",
76
+ className: "inline-flex items-center gap-1 text-sm text-primary hover:underline",
77
+ children: [i("devportal.timecampusApplets.manifestReference", "Manifest reference"), /* @__PURE__ */ U(P, {
78
+ className: "h-3.5 w-3.5",
79
+ "aria-hidden": "true"
80
+ })]
81
+ })]
82
+ }),
83
+ /* @__PURE__ */ W("label", {
84
+ className: "flex flex-col gap-1 text-sm text-text-secondary",
85
+ children: [i("devportal.timecampusApplets.manifestLabel", "Manifest (JSON)"), /* @__PURE__ */ U("textarea", {
86
+ value: s,
87
+ onChange: (e) => {
88
+ l(e.target.value), m(!1);
89
+ },
90
+ spellCheck: !1,
91
+ rows: 18,
92
+ className: "w-full rounded-md border border-border-subtle bg-bg-sunken p-3 font-mono text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg",
93
+ "data-testid": "tc-manifest-text"
94
+ })]
95
+ }),
96
+ u.length > 0 && /* @__PURE__ */ U("ul", {
97
+ className: "list-disc space-y-1 rounded-md bg-status-error-bg-subtle py-2 pl-6 pr-3 text-xs text-status-error-text",
98
+ role: "alert",
99
+ "data-testid": "tc-manifest-problems",
100
+ children: u.map((e, t) => /* @__PURE__ */ U("li", { children: e }, `${t}-${e}`))
101
+ }),
102
+ f && u.length === 0 && /* @__PURE__ */ W("p", {
103
+ className: "flex items-center gap-1.5 text-xs text-status-success-text",
104
+ "data-testid": "tc-manifest-valid",
105
+ children: [/* @__PURE__ */ U(N, {
106
+ className: "h-4 w-4",
107
+ "aria-hidden": "true"
108
+ }), i("devportal.timecampusApplets.manifestValid", "The manifest is valid.")]
109
+ }),
110
+ /* @__PURE__ */ W("div", {
111
+ className: "flex flex-wrap justify-end gap-2",
112
+ children: [
113
+ /* @__PURE__ */ U("button", {
114
+ type: "button",
115
+ className: Y,
116
+ onClick: n,
117
+ children: i("devportal.cancel", "Cancel")
118
+ }),
119
+ /* @__PURE__ */ W("button", {
120
+ type: "button",
121
+ className: Y,
122
+ onClick: () => void A(),
123
+ disabled: g || O,
124
+ children: [/* @__PURE__ */ U(N, {
125
+ className: "h-4 w-4",
126
+ "aria-hidden": "true"
127
+ }), i("devportal.timecampusApplets.validate", "Validate")]
128
+ }),
129
+ /* @__PURE__ */ W("button", {
130
+ type: "button",
131
+ className: J,
132
+ onClick: () => void M(),
133
+ disabled: O,
134
+ "data-testid": "tc-manifest-save",
135
+ children: [/* @__PURE__ */ U(V, {
136
+ className: "h-4 w-4",
137
+ "aria-hidden": "true"
138
+ }), e.kind === "new" ? i("devportal.timecampusApplets.register", "Register applet") : i("devportal.timecampusApplets.uploadVersion", "Upload version")]
139
+ })
140
+ ]
141
+ })
142
+ ]
143
+ });
144
+ }, te = ({ applet: e, busy: t, onNewVersion: n, onEditVersion: r, onSubmitVersion: a, onWithdrawVersion: s, onOffer: c, onWithdrawOffer: l }) => {
145
+ let p = o(), h = e.latestVersion, g = b(e), v = _(e), y = e.marketplace, x = y?.latestSubmission;
146
+ return /* @__PURE__ */ W(G, {
147
+ className: "flex flex-col gap-4 p-4",
148
+ "data-testid": `tc-applet-${e.key}`,
149
+ children: [/* @__PURE__ */ W("div", {
150
+ className: "flex flex-wrap items-start justify-between gap-3",
151
+ children: [/* @__PURE__ */ W("div", {
152
+ className: "min-w-0",
153
+ children: [
154
+ /* @__PURE__ */ W("div", {
155
+ className: "flex flex-wrap items-center gap-2",
156
+ children: [/* @__PURE__ */ U("h3", {
157
+ className: "text-base font-semibold text-text-primary",
158
+ children: e.name
159
+ }), /* @__PURE__ */ U(i, { status: e.status })]
160
+ }),
161
+ /* @__PURE__ */ U("p", {
162
+ className: "mt-0.5 font-mono text-xs text-text-tertiary",
163
+ children: e.key
164
+ }),
165
+ /* @__PURE__ */ U("p", {
166
+ className: "mt-1 text-sm text-text-secondary",
167
+ children: e.description
168
+ })
169
+ ]
170
+ }), /* @__PURE__ */ W("button", {
171
+ type: "button",
172
+ className: Y,
173
+ onClick: n,
174
+ disabled: t || !u(e),
175
+ title: u(e) ? void 0 : p("devportal.timecampusApplets.openVersionHint", "Finish or withdraw the open version first."),
176
+ children: [/* @__PURE__ */ U(F, {
177
+ className: "h-4 w-4",
178
+ "aria-hidden": "true"
179
+ }), p("devportal.timecampusApplets.newVersion", "New version")]
180
+ })]
181
+ }), /* @__PURE__ */ W("div", {
182
+ className: "grid gap-3 md:grid-cols-2",
183
+ children: [/* @__PURE__ */ W("div", {
184
+ className: "flex flex-col gap-2 rounded-md border border-border-subtle bg-bg-sunken/40 p-3",
185
+ "data-testid": `tc-applet-version-${e.key}`,
186
+ children: [
187
+ /* @__PURE__ */ W("div", {
188
+ className: "flex flex-wrap items-center gap-2",
189
+ children: [/* @__PURE__ */ W("p", {
190
+ className: "text-sm font-medium text-text-primary",
191
+ children: [
192
+ p("devportal.timecampusApplets.latestVersion", "Latest version"),
193
+ " ",
194
+ "v",
195
+ h.version
196
+ ]
197
+ }), /* @__PURE__ */ U(i, { status: h.status })]
198
+ }),
199
+ e.liveVersion && e.liveVersion.id !== h.id && /* @__PURE__ */ U("p", {
200
+ className: "text-xs text-text-secondary",
201
+ children: p("devportal.timecampusApplets.liveInWorkspace", "v{{version}} is live in your workspace.", { version: e.liveVersion.version })
202
+ }),
203
+ h.submittedAt && /* @__PURE__ */ W("p", {
204
+ className: "text-xs text-text-secondary",
205
+ children: [
206
+ p("devportal.timecampusApplets.submitted", "Submitted"),
207
+ " ",
208
+ m(h.submittedAt)
209
+ ]
210
+ }),
211
+ h.reviewNotes && /* @__PURE__ */ W("p", {
212
+ className: "text-xs text-text-secondary",
213
+ children: [
214
+ /* @__PURE__ */ W("span", {
215
+ className: "font-medium text-text-primary",
216
+ children: [p("devportal.timecampusApplets.reviewNotes", "Review notes"), ":"]
217
+ }),
218
+ " ",
219
+ h.reviewNotes
220
+ ]
221
+ }),
222
+ h.suspendedReason && /* @__PURE__ */ U("p", {
223
+ className: "text-xs text-status-warning-text",
224
+ children: h.suspendedReason
225
+ }),
226
+ d(e) && /* @__PURE__ */ W("button", {
227
+ type: "button",
228
+ className: Y,
229
+ onClick: r,
230
+ disabled: t,
231
+ "data-testid": `tc-edit-version-${e.key}`,
232
+ children: [/* @__PURE__ */ U(L, {
233
+ className: "h-4 w-4",
234
+ "aria-hidden": "true"
235
+ }), p("devportal.timecampusApplets.editManifest", "Edit manifest")]
236
+ }),
237
+ g.kind === "submit" && /* @__PURE__ */ W("button", {
238
+ type: "button",
239
+ className: J,
240
+ onClick: () => a(g.versionId),
241
+ disabled: t,
242
+ children: [/* @__PURE__ */ U(z, {
243
+ className: "h-4 w-4",
244
+ "aria-hidden": "true"
245
+ }), p("devportal.timecampusApplets.submitForReview", "Submit for review")]
246
+ }),
247
+ g.kind === "withdraw" && /* @__PURE__ */ W("button", {
248
+ type: "button",
249
+ className: Y,
250
+ onClick: () => s(g.versionId),
251
+ disabled: t,
252
+ children: [/* @__PURE__ */ U(B, {
253
+ className: "h-4 w-4",
254
+ "aria-hidden": "true"
255
+ }), p("devportal.timecampusApplets.withdrawReview", "Withdraw from review")]
256
+ })
257
+ ]
258
+ }), /* @__PURE__ */ W("div", {
259
+ className: "flex flex-col gap-2 rounded-md border border-border-subtle bg-bg-sunken/40 p-3",
260
+ "data-testid": `tc-applet-marketplace-${e.key}`,
261
+ children: [
262
+ /* @__PURE__ */ W("div", {
263
+ className: "flex flex-wrap items-center gap-2",
264
+ children: [
265
+ /* @__PURE__ */ U(I, {
266
+ className: "h-4 w-4 text-text-tertiary",
267
+ "aria-hidden": "true"
268
+ }),
269
+ /* @__PURE__ */ U("p", {
270
+ className: "text-sm font-medium text-text-primary",
271
+ children: p("devportal.timecampusApplets.marketplace", "Public marketplace")
272
+ }),
273
+ y ? /* @__PURE__ */ U(i, { status: y.status }) : /* @__PURE__ */ U(i, { status: "NOT_LISTED" })
274
+ ]
275
+ }),
276
+ y?.liveSubmission && /* @__PURE__ */ U("p", {
277
+ className: "text-xs text-text-secondary",
278
+ children: p("devportal.timecampusApplets.certifiedLive", "v{{version}} is certified and live in the marketplace.", { version: y.liveSubmission.version })
279
+ }),
280
+ x && /* @__PURE__ */ W("p", {
281
+ className: "flex flex-wrap items-center gap-2 text-xs text-text-secondary",
282
+ children: [p("devportal.timecampusApplets.latestSubmission", "Latest submission v{{version}}", { version: x.version }), /* @__PURE__ */ U(i, { status: x.status })]
283
+ }),
284
+ x?.reviewNotes && /* @__PURE__ */ W("p", {
285
+ className: "text-xs text-text-secondary",
286
+ children: [
287
+ /* @__PURE__ */ W("span", {
288
+ className: "font-medium text-text-primary",
289
+ children: [p("devportal.timecampusApplets.certificationNotes", "Certification notes"), ":"]
290
+ }),
291
+ " ",
292
+ x.reviewNotes
293
+ ]
294
+ }),
295
+ y?.suspendedReason && /* @__PURE__ */ U("p", {
296
+ className: "text-xs text-status-warning-text",
297
+ children: y.suspendedReason
298
+ }),
299
+ f(e) ? /* @__PURE__ */ W("button", {
300
+ type: "button",
301
+ className: Y,
302
+ onClick: l,
303
+ disabled: t,
304
+ children: [/* @__PURE__ */ U(B, {
305
+ className: "h-4 w-4",
306
+ "aria-hidden": "true"
307
+ }), p("devportal.timecampusApplets.withdrawSubmission", "Withdraw submission")]
308
+ }) : v.canOffer ? /* @__PURE__ */ W("button", {
309
+ type: "button",
310
+ className: J,
311
+ onClick: c,
312
+ disabled: t,
313
+ children: [/* @__PURE__ */ U(I, {
314
+ className: "h-4 w-4",
315
+ "aria-hidden": "true"
316
+ }), p("devportal.timecampusApplets.offer", "Offer v{{version}} to the marketplace", { version: v.version })]
317
+ }) : /* @__PURE__ */ U("p", {
318
+ className: "text-xs text-text-tertiary",
319
+ children: v.reason
320
+ })
321
+ ]
322
+ })]
323
+ })]
324
+ });
325
+ }, ne = () => {
326
+ let e = o(), t = [
327
+ {
328
+ href: c,
329
+ label: e("devportal.timecampusApplets.docsManifest", "Applet manifest reference"),
330
+ icon: M
331
+ },
332
+ {
333
+ href: l,
334
+ label: e("devportal.timecampusApplets.docsSample", "Sample widget (view source)"),
335
+ icon: R
336
+ },
337
+ {
338
+ href: s,
339
+ label: e("devportal.timecampusApplets.docsApplets", "How applets work in TimeCampus"),
340
+ icon: I
341
+ }
342
+ ];
343
+ return /* @__PURE__ */ U("ul", {
344
+ className: "flex flex-wrap gap-2",
345
+ "data-testid": "tc-applet-resources",
346
+ children: t.map(({ href: e, label: t, icon: n }) => /* @__PURE__ */ U("li", { children: /* @__PURE__ */ W("a", {
347
+ href: e,
348
+ target: "_blank",
349
+ rel: "noopener noreferrer",
350
+ className: Y,
351
+ children: [
352
+ /* @__PURE__ */ U(n, {
353
+ className: "h-4 w-4",
354
+ "aria-hidden": "true"
355
+ }),
356
+ t,
357
+ /* @__PURE__ */ U(P, {
358
+ className: "h-3.5 w-3.5",
359
+ "aria-hidden": "true"
360
+ })
361
+ ]
362
+ }) }, e))
363
+ });
364
+ }, Z = () => {
365
+ let i = o(), { workspaceId: s } = a(), l = K("workspace"), u = l ?? void 0, d = !!s && !!l, [f, m] = j(null), _ = f && f.workspaceId === s ? f : null, v = (e, t) => {
366
+ s && (x(null), m({
367
+ target: e,
368
+ text: t,
369
+ workspaceId: s
370
+ }));
371
+ }, [b, x] = j(null), { data: S, loading: E, error: D } = T({
372
+ client: u,
373
+ skip: !d,
374
+ fetchPolicy: "cache-and-network"
375
+ }), { data: M } = A({
376
+ client: u,
377
+ skip: !d,
378
+ variables: { pagination: {
379
+ limit: 100,
380
+ offset: 0
381
+ } }
382
+ }), N = (M?.myWorkspaces?.items ?? []).find((e) => e.id === s)?.name ?? null, P = {
383
+ client: u,
384
+ refetchQueries: q,
385
+ awaitRefetchQueries: !0
386
+ }, [I, L] = w(P), [z, B] = k(P), [V, G] = C(P), [Y, Z] = O(P), re = L.loading || B.loading || G.loading || Z.loading, Q = async (e, t) => {
387
+ x(null);
388
+ try {
389
+ await e(), x({
390
+ tone: "success",
391
+ text: t
392
+ });
393
+ } catch (e) {
394
+ x({
395
+ tone: "error",
396
+ text: p(e)
397
+ });
398
+ }
399
+ }, $ = S?.timeCampusMyThirdPartyApplets ?? [];
400
+ return /* @__PURE__ */ W(e, {
401
+ title: i("devportal.timecampusApplets.title", "TimeCampus applets"),
402
+ description: i("devportal.timecampusApplets.description", "Build widgets for TimeCampus, publish them in your workspace and offer them to every TimeCampus workspace."),
403
+ purpose: i("devportal.timecampusApplets.purpose", "Register an applet from a JSON manifest, upload new versions, send each version to your workspace's reviewers, and once it is live offer it to the public marketplace. The TimeCampus platform team certifies every marketplace version before other workspaces can install it."),
404
+ purposeLearnMoreHref: c,
405
+ actions: d ? /* @__PURE__ */ W("button", {
406
+ type: "button",
407
+ className: J,
408
+ onClick: () => v({ kind: "new" }, y()),
409
+ "data-testid": "tc-register-applet",
410
+ children: [/* @__PURE__ */ U(F, {
411
+ className: "h-4 w-4",
412
+ "aria-hidden": "true"
413
+ }), i("devportal.timecampusApplets.register", "Register applet")]
414
+ }) : void 0,
415
+ children: [/* @__PURE__ */ U(t, {
416
+ title: i("devportal.timecampusApplets.resources", "Build an applet"),
417
+ children: /* @__PURE__ */ U(ne, {})
418
+ }), d ? /* @__PURE__ */ W(H, { children: [
419
+ /* @__PURE__ */ U("p", {
420
+ className: "text-sm text-text-secondary",
421
+ "data-testid": "tc-workspace",
422
+ children: N ? i("devportal.timecampusApplets.inWorkspace", "Your applets in {{workspace}}. Switch workspace with the context switcher.", { workspace: N }) : i("devportal.timecampusApplets.inActiveWorkspace", "Your applets in the active workspace. Switch workspace with the context switcher.")
423
+ }),
424
+ b && /* @__PURE__ */ U("p", {
425
+ role: b.tone === "error" ? "alert" : "status",
426
+ className: b.tone === "error" ? "rounded-md bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text" : "rounded-md bg-status-success-bg-subtle px-3 py-2 text-sm text-status-success-text",
427
+ "data-testid": "tc-feedback",
428
+ children: b.text
429
+ }),
430
+ _ && /* @__PURE__ */ U(ee, {
431
+ target: _.target,
432
+ initialText: _.text,
433
+ onClose: () => m(null),
434
+ onSaved: (e) => {
435
+ m(null), x({
436
+ tone: "success",
437
+ text: e
438
+ });
439
+ }
440
+ }, `${_.workspaceId}-${X(_.target)}`),
441
+ /* @__PURE__ */ U(t, {
442
+ title: i("devportal.timecampusApplets.yourApplets", "Your applets"),
443
+ "data-testid": "tc-applet-list",
444
+ children: E && $.length === 0 ? /* @__PURE__ */ U(r, { message: i("devportal.timecampusApplets.loading", "Loading your applets...") }) : D && $.length === 0 ? /* @__PURE__ */ U("p", {
445
+ role: "alert",
446
+ className: "rounded-md bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
447
+ children: p(D)
448
+ }) : $.length === 0 ? /* @__PURE__ */ U(n, {
449
+ icon: /* @__PURE__ */ U(R, {
450
+ className: "h-8 w-8",
451
+ "aria-hidden": "true"
452
+ }),
453
+ title: i("devportal.timecampusApplets.emptyTitle", "No applets yet"),
454
+ description: i("devportal.timecampusApplets.empty", "Register your first applet. The starter manifest points at the sample widget, so it validates as is.")
455
+ }) : /* @__PURE__ */ U("div", {
456
+ className: "flex flex-col gap-4",
457
+ children: $.map((e) => /* @__PURE__ */ U(te, {
458
+ applet: e,
459
+ busy: re,
460
+ onNewVersion: () => v({
461
+ kind: "version",
462
+ appletId: e.id,
463
+ appletName: e.name
464
+ }, h(e.latestVersion.manifest)),
465
+ onEditVersion: () => v({
466
+ kind: "edit",
467
+ versionId: e.latestVersion.id,
468
+ appletName: e.name,
469
+ version: e.latestVersion.version
470
+ }, g(e.latestVersion.manifest)),
471
+ onSubmitVersion: (e) => void Q(() => I({ variables: { versionId: e } }), i("devportal.timecampusApplets.submittedForReview", "Sent to your workspace's reviewers.")),
472
+ onWithdrawVersion: (e) => void Q(() => z({ variables: { versionId: e } }), i("devportal.timecampusApplets.withdrawnFromReview", "Withdrawn from review. The version is a draft again.")),
473
+ onOffer: () => void Q(() => V({ variables: { appletId: e.id } }), i("devportal.timecampusApplets.offered", "Sent to the TimeCampus platform team for certification.")),
474
+ onWithdrawOffer: () => void Q(() => Y({ variables: { appletId: e.id } }), i("devportal.timecampusApplets.offerWithdrawn", "Marketplace submission withdrawn."))
475
+ }, e.id))
476
+ })
477
+ })
478
+ ] }) : /* @__PURE__ */ U(n, {
479
+ icon: /* @__PURE__ */ U(R, {
480
+ className: "h-8 w-8",
481
+ "aria-hidden": "true"
482
+ }),
483
+ title: i("devportal.timecampusApplets.noWorkspaceTitle", "Choose a workspace"),
484
+ description: i("devportal.timecampusApplets.noWorkspace", "TimeCampus applets belong to a workspace. Pick the workspace you develop in with the context switcher at the top of the page.")
485
+ })]
486
+ });
487
+ };
488
+ export { Z as TimeCampusAppletsPage };
489
+
490
+ //# sourceMappingURL=TimeCampusAppletsPage.js.map