@burdenoff/microfe-vibecontrols 2026.1003.2 → 2026.1005.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.
- package/dist/components/assistant/assistantProduct.js +55 -3
- package/dist/components/assistant/assistantProduct.js.map +1 -1
- package/dist/components/assistant/recordAdapters.js +65 -0
- package/dist/components/assistant/recordAdapters.js.map +1 -0
- package/dist/pages/TemplatesPage.js +94 -75
- package/dist/pages/TemplatesPage.js.map +1 -1
- package/dist/pages/VibesPage.js +309 -247
- package/dist/pages/VibesPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,7 +2,8 @@ import { ASSISTANT_MODES as e } from "../../types/assistant.js";
|
|
|
2
2
|
import { PRODUCT as t, reportAssistantTokenUsage as n } from "../../services/assistantApi.js";
|
|
3
3
|
import { describeCurrentPage as r, gatherPageContext as i } from "../../utils/pageContext.js";
|
|
4
4
|
import { buildPromptForMode as a } from "../../utils/assistantPrompt.js";
|
|
5
|
-
|
|
5
|
+
import { VIBECONTROLS_RECORD_ADAPTERS as o } from "./recordAdapters.js";
|
|
6
|
+
var s = {
|
|
6
7
|
product: t,
|
|
7
8
|
modes: e,
|
|
8
9
|
describeCurrentPage: r,
|
|
@@ -13,11 +14,62 @@ var o = {
|
|
|
13
14
|
attachments: "/VibeControls/AI Assistant/Attachments",
|
|
14
15
|
artifacts: "/VibeControls/AI Assistant/Artifacts"
|
|
15
16
|
},
|
|
16
|
-
localUiCommands: {
|
|
17
|
+
localUiCommands: {
|
|
18
|
+
themeWords: "mode-only",
|
|
19
|
+
entityRoutes: [
|
|
20
|
+
{
|
|
21
|
+
id: "vibecontrols-vibe",
|
|
22
|
+
pattern: "/vibecontrols/vibes/:id",
|
|
23
|
+
params: { id: {
|
|
24
|
+
kind: "id",
|
|
25
|
+
exclude: ["new"]
|
|
26
|
+
} },
|
|
27
|
+
visibleWith: "vibecontrols-vibes",
|
|
28
|
+
noun: {
|
|
29
|
+
key: "entity.vibe",
|
|
30
|
+
fallback: "vibe"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "vibecontrols-agent",
|
|
35
|
+
pattern: "/vibecontrols/agents/:id",
|
|
36
|
+
params: { id: {
|
|
37
|
+
kind: "id",
|
|
38
|
+
exclude: ["new"]
|
|
39
|
+
} },
|
|
40
|
+
visibleWith: "vibecontrols-agents",
|
|
41
|
+
noun: {
|
|
42
|
+
key: "entity.agent",
|
|
43
|
+
fallback: "agent"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "vibecontrols-session",
|
|
48
|
+
pattern: "/vibecontrols/sessions/:id",
|
|
49
|
+
params: { id: { kind: "id" } },
|
|
50
|
+
visibleWith: "vibecontrols-sessions",
|
|
51
|
+
noun: {
|
|
52
|
+
key: "entity.session",
|
|
53
|
+
fallback: "session"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "vibecontrols-connections",
|
|
58
|
+
pattern: "/integrations/connections",
|
|
59
|
+
params: {},
|
|
60
|
+
visibleWith: "integrations",
|
|
61
|
+
noun: {
|
|
62
|
+
key: "entity.connections",
|
|
63
|
+
fallback: "Connected Systems"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
records: o
|
|
68
|
+
},
|
|
17
69
|
reportTurnTimings: !0,
|
|
18
70
|
writeApproval: "proposal-v1"
|
|
19
71
|
};
|
|
20
72
|
//#endregion
|
|
21
|
-
export {
|
|
73
|
+
export { s as VIBECONTROLS_ASSISTANT_PRODUCT };
|
|
22
74
|
|
|
23
75
|
//# sourceMappingURL=assistantProduct.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistantProduct.js","names":[],"sources":["../../../src/components/assistant/assistantProduct.ts"],"sourcesContent":["/**\n * VibeControls' answers to the shared assistant's product seam, as one value.\n *\n * ★★ Every field of fe-libs' `AssistantProductProvider` is OPTIONAL, and a\n * missing one does not throw — it silently degrades. No `gatherPageContext`\n * sends every turn with no page context; no `buildPrompt` drops the\n * confirm-before-mutate guardrail and the language directive; no\n * `reportTokenUsage` meters nothing; no `storageFolders` starts a second folder\n * beside the one holding every saved attachment; no `writeApproval` keeps the\n * legacy write flow.\n *\n * So the value is typed `Required<…>` over the provider's own props: forgetting\n * a field, or fe-libs adding one, is a type error in this file rather than a\n * quiet regression in production. The wrapper spreads it into the provider, and\n * `assistantProduct.test.tsx` reads it back through fe-libs' real context.\n */\n\nimport type { LocalUiCommandOptions } from '@burdenoff/fe-libs/shared/assistant/localUiCommands';\nimport type { AssistantProductProviderProps } from '@burdenoff/fe-libs/shared/assistant/product';\nimport { ASSISTANT_MODES } from '@/types/assistant';\nimport { PRODUCT, reportAssistantTokenUsage } from '@/services/assistantApi';\nimport { describeCurrentPage, gatherPageContext } from '@/utils/pageContext';\nimport { buildPromptForMode } from '@/utils/assistantPrompt';\n\n/**\n * Where this product's assistant files live.\n *\n * ★★ These strings are LOAD-BEARING once users have saved anything. They are the\n * real folders in the workspace file library holding every attachment and\n * artifact. Changing them does not fail — it starts a second folder beside the\n * first and orphans everything in the original.\n *\n * ★ Note the capitalisation: `/VibeControls/…`, while the product id is\n * `vibecontrols`. That mismatch is exactly why fe-libs cannot derive this and\n * why it is supplied here.\n */\nexport const STORAGE_FOLDERS = {\n attachments: '/VibeControls/AI Assistant/Attachments',\n artifacts: '/VibeControls/AI Assistant/Artifacts',\n} as const;\n\n/**\n * How the panel reads a UI command it carries out itself (BOFF-7802), for\n * VibeControls.\n *\n * ★★ `mode-only` because VibeControls' CONTENT has themes of its own. A docs\n * site has a Light/Dark theme (`DocsSettingsPanel`), and a VibeDeck has a colour\n * theme (`VibeDeckForm`). So \"use the dark theme\" on either page is a request\n * about that site or deck, and must reach the model rather than flip the app.\n * fe-libs names this exact case (`LocalUiCommandOptions.themeWords`). A miss\n * costs only a turn; a false match changes the app when the user meant their\n * content. \"<name> mode\" (\"dark mode\"), and a theme qualified as the app's\n * (\"change the app theme to dark\"), still change the app locally, with no turn.\n *\n * ★ Its own exported constant so the seam test can pin the field by identity,\n * like every other field on the seam value.\n */\nexport const LOCAL_UI_COMMANDS: LocalUiCommandOptions = {
|
|
1
|
+
{"version":3,"file":"assistantProduct.js","names":[],"sources":["../../../src/components/assistant/assistantProduct.ts"],"sourcesContent":["/**\n * VibeControls' answers to the shared assistant's product seam, as one value.\n *\n * ★★ Every field of fe-libs' `AssistantProductProvider` is OPTIONAL, and a\n * missing one does not throw — it silently degrades. No `gatherPageContext`\n * sends every turn with no page context; no `buildPrompt` drops the\n * confirm-before-mutate guardrail and the language directive; no\n * `reportTokenUsage` meters nothing; no `storageFolders` starts a second folder\n * beside the one holding every saved attachment; no `writeApproval` keeps the\n * legacy write flow.\n *\n * So the value is typed `Required<…>` over the provider's own props: forgetting\n * a field, or fe-libs adding one, is a type error in this file rather than a\n * quiet regression in production. The wrapper spreads it into the provider, and\n * `assistantProduct.test.tsx` reads it back through fe-libs' real context.\n */\n\nimport type { LocalUiCommandOptions } from '@burdenoff/fe-libs/shared/assistant/localUiCommands';\nimport type { AssistantEntityRoute } from '@burdenoff/fe-libs/shared/assistant/uiActions/entityRoutes';\nimport type { AssistantProductProviderProps } from '@burdenoff/fe-libs/shared/assistant/product';\nimport { ASSISTANT_MODES } from '@/types/assistant';\nimport { PRODUCT, reportAssistantTokenUsage } from '@/services/assistantApi';\nimport { describeCurrentPage, gatherPageContext } from '@/utils/pageContext';\nimport { buildPromptForMode } from '@/utils/assistantPrompt';\nimport { VIBECONTROLS_RECORD_ADAPTERS } from './recordAdapters';\n\n/**\n * Where this product's assistant files live.\n *\n * ★★ These strings are LOAD-BEARING once users have saved anything. They are the\n * real folders in the workspace file library holding every attachment and\n * artifact. Changing them does not fail — it starts a second folder beside the\n * first and orphans everything in the original.\n *\n * ★ Note the capitalisation: `/VibeControls/…`, while the product id is\n * `vibecontrols`. That mismatch is exactly why fe-libs cannot derive this and\n * why it is supplied here.\n */\nexport const STORAGE_FOLDERS = {\n attachments: '/VibeControls/AI Assistant/Attachments',\n artifacts: '/VibeControls/AI Assistant/Artifacts',\n} as const;\n\n/**\n * The pages of ONE record that no sidebar entry names (BOFF-8060 N1b), so \"take me\n * there\" can open them from a link in the assistant's own reply.\n *\n * ★★ `exclude` lists the router's STATIC siblings of each `:id` segment\n * (`VibeControlsRoutes.tsx`): `vibes/new` redirects to the create dialog, so it is\n * not a vibe. `entityRoutes.test.ts` renders every template through the real\n * route table and checks every static sibling against them.\n *\n * ★ `/integrations/connections` is the app shell's Connected Systems page, shown\n * to whoever can see the Integrations entry.\n */\nexport const ENTITY_ROUTES: readonly AssistantEntityRoute[] = [\n {\n id: 'vibecontrols-vibe',\n pattern: '/vibecontrols/vibes/:id',\n params: { id: { kind: 'id', exclude: ['new'] } },\n visibleWith: 'vibecontrols-vibes',\n noun: { key: 'entity.vibe', fallback: 'vibe' },\n },\n {\n id: 'vibecontrols-agent',\n pattern: '/vibecontrols/agents/:id',\n params: { id: { kind: 'id', exclude: ['new'] } },\n visibleWith: 'vibecontrols-agents',\n noun: { key: 'entity.agent', fallback: 'agent' },\n },\n {\n id: 'vibecontrols-session',\n pattern: '/vibecontrols/sessions/:id',\n params: { id: { kind: 'id' } },\n visibleWith: 'vibecontrols-sessions',\n noun: { key: 'entity.session', fallback: 'session' },\n },\n {\n id: 'vibecontrols-connections',\n pattern: '/integrations/connections',\n params: {},\n visibleWith: 'integrations',\n noun: { key: 'entity.connections', fallback: 'Connected Systems' },\n },\n];\n\n/**\n * How the panel reads a UI command it carries out itself (BOFF-7802), for\n * VibeControls.\n *\n * ★★ `mode-only` because VibeControls' CONTENT has themes of its own. A docs\n * site has a Light/Dark theme (`DocsSettingsPanel`), and a VibeDeck has a colour\n * theme (`VibeDeckForm`). So \"use the dark theme\" on either page is a request\n * about that site or deck, and must reach the model rather than flip the app.\n * fe-libs names this exact case (`LocalUiCommandOptions.themeWords`). A miss\n * costs only a turn; a false match changes the app when the user meant their\n * content. \"<name> mode\" (\"dark mode\"), and a theme qualified as the app's\n * (\"change the app theme to dark\"), still change the app locally, with no turn.\n *\n * ★ Its own exported constant so the seam test can pin the field by identity,\n * like every other field on the seam value.\n */\nexport const LOCAL_UI_COMMANDS: LocalUiCommandOptions = {\n themeWords: 'mode-only',\n entityRoutes: ENTITY_ROUTES,\n // BOFF-8060 N1b: \"open the Staging vibe\" / agent, by name.\n records: VIBECONTROLS_RECORD_ADAPTERS,\n};\n\n/**\n * Every seam field, required: the provider's props minus its children, and\n * minus the one answer VibeControls deliberately does not give.\n *\n * ★★ `confirmWrite` is omitted so a HIGH-risk write proposal is confirmed in\n * fe-libs' shared `AssistantWriteConfirmDialog` (BOFF-7803 5a). That is the\n * documented default when a product has no dialog of its own, and VibeControls\n * has none. A product dialog must meet the whole contract fe-libs sets for it\n * (readback, target count, consequences, the exact confirm label, sensitive\n * values masked, never `window.confirm`). The shared one already does, so a\n * VibeControls copy would only be a second place for that to go wrong.\n */\nexport type VibeControlsAssistantProduct = Required<\n Omit<AssistantProductProviderProps, 'children' | 'confirmWrite'>\n>;\n\nexport const VIBECONTROLS_ASSISTANT_PRODUCT: VibeControlsAssistantProduct = {\n product: PRODUCT,\n modes: ASSISTANT_MODES,\n describeCurrentPage,\n gatherPageContext,\n buildPrompt: buildPromptForMode,\n reportTokenUsage: reportAssistantTokenUsage,\n storageFolders: STORAGE_FOLDERS,\n localUiCommands: LOCAL_UI_COMMANDS,\n /**\n * ★ On. That is fe-libs' own default (on unless `false`, BOFF-7801) and what\n * VibeControls does today: this seam never set the field, so its turns have\n * been reported by default. The reports carry timings and coarse dimensions\n * only, and the proposal flow's latency is what the rollout measures.\n */\n reportTurnTimings: true,\n /**\n * ★★★ VibeControls asks for the proposal flow (BOFF-7803 5a). At\n * `'proposal-v1'` fe-libs provisions Ask and Take action (`api-calls`)\n * through the `assistant:use`-gated `provisionAssistantSandbox`, requests the\n * proposal flow for Take action, and lets a spoken Take-action turn through\n * when the sandbox's own `/health` also says v1. Every change then waits for\n * the button on its card; the sandbox and wspace-sandbox-svc decide whether\n * it runs. Build (`vibe-plugins`) is not affected: it is still provisioned\n * through `createSandbox`, exactly as before.\n *\n * ★★ That provisioning path has NO `createSandbox` fallback: while\n * wspace-sandbox-svc answers it with 503 (its session relay switched off),\n * the assistant cannot start Ask or Take action at all. This flag reaches\n * users only when vibecontrols-app pins the microfe version that carries it,\n * so the app bump is the release switch.\n */\n writeApproval: 'proposal-v1',\n};\n"],"mappings":";;;;;AA6HA,IAAa,IAA+D;CAC1E,SAAS;CACT,OAAO;CACP;CACA;CACA,aAAa;CACb,kBAAkB;CAClB,gBA9F6B;EAC7B,aAAa;EACb,WAAW;EACZ;CA4FC,iBA/BsD;EACtD,YAAY;EACZ,cAjD4D;GAC5D;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI;KAAE,MAAM;KAAM,SAAS,CAAC,MAAM;KAAE,EAAE;IAChD,aAAa;IACb,MAAM;KAAE,KAAK;KAAe,UAAU;KAAQ;IAC/C;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI;KAAE,MAAM;KAAM,SAAS,CAAC,MAAM;KAAE,EAAE;IAChD,aAAa;IACb,MAAM;KAAE,KAAK;KAAgB,UAAU;KAAS;IACjD;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI,EAAE,MAAM,MAAM,EAAE;IAC9B,aAAa;IACb,MAAM;KAAE,KAAK;KAAkB,UAAU;KAAW;IACrD;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE;IACV,aAAa;IACb,MAAM;KAAE,KAAK;KAAsB,UAAU;KAAqB;IACnE;GACF;EAsBC,SAAS;EACV;CAiCC,mBAAmB;CAiBnB,eAAe;CAChB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getStoredWorkspaceId as e, graphqlFetch as t } from "@burdenoff/fe-libs/shared/graphql";
|
|
2
|
+
var n = "\n query AssistantFindVibes($pagination: PaginationInput) {\n vibes(pagination: $pagination) {\n items { id name workspaceId archivedAt deletedAt }\n }\n }\n", r = "\n query AssistantFindAgents($pagination: PaginationInput) {\n vibecontrolsAgents(pagination: $pagination) {\n id name hostname workspaceId deletedAt\n }\n }\n", i = {
|
|
3
|
+
graphqlFetch: t,
|
|
4
|
+
getWorkspaceId: e
|
|
5
|
+
}, a = (e) => typeof e == "string" && e.trim().length > 0 ? e.trim() : null, o = (e) => e.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
6
|
+
function s(e, t) {
|
|
7
|
+
let n = o(e);
|
|
8
|
+
if (!n) return !1;
|
|
9
|
+
let r = o(t).split(" ");
|
|
10
|
+
return n.split(" ").every((e) => r.some((t) => t.startsWith(e)));
|
|
11
|
+
}
|
|
12
|
+
async function c(e, t, n, r, i, a, o) {
|
|
13
|
+
let c = e.getWorkspaceId();
|
|
14
|
+
if (!c) return [];
|
|
15
|
+
let l = await e.graphqlFetch({
|
|
16
|
+
gateway: "workspace",
|
|
17
|
+
query: t,
|
|
18
|
+
operationName: n,
|
|
19
|
+
variables: { pagination: { limit: 200 } },
|
|
20
|
+
workspaceToken: !0,
|
|
21
|
+
workspaceId: c,
|
|
22
|
+
organizationId: !0,
|
|
23
|
+
signal: a,
|
|
24
|
+
suppressGlobalErrorEvent: !0
|
|
25
|
+
});
|
|
26
|
+
if (l.errors?.length || !l.data) return [];
|
|
27
|
+
let u = r(l.data);
|
|
28
|
+
if (!Array.isArray(u)) return [];
|
|
29
|
+
let d = [];
|
|
30
|
+
for (let e of u) {
|
|
31
|
+
if (!e || typeof e.id != "string" || e.id.length === 0 || typeof e.workspaceId == "string" && e.workspaceId !== c || e.deletedAt || e.archivedAt) continue;
|
|
32
|
+
let t = o(e);
|
|
33
|
+
t === null || !s(i, t) || d.push({
|
|
34
|
+
params: { id: e.id },
|
|
35
|
+
name: t
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return d;
|
|
39
|
+
}
|
|
40
|
+
function l(e = i) {
|
|
41
|
+
return [{
|
|
42
|
+
id: "vibecontrols-vibe",
|
|
43
|
+
nouns: ["vibe", "vibes"],
|
|
44
|
+
noun: {
|
|
45
|
+
key: "entity.vibe",
|
|
46
|
+
fallback: "vibe"
|
|
47
|
+
},
|
|
48
|
+
entityRouteId: "vibecontrols-vibe",
|
|
49
|
+
find: (t, r) => c(e, n, "AssistantFindVibes", (e) => e.vibes?.items, t, r, (e) => a(e.name))
|
|
50
|
+
}, {
|
|
51
|
+
id: "vibecontrols-agent",
|
|
52
|
+
nouns: ["agent", "agents"],
|
|
53
|
+
noun: {
|
|
54
|
+
key: "entity.agent",
|
|
55
|
+
fallback: "agent"
|
|
56
|
+
},
|
|
57
|
+
entityRouteId: "vibecontrols-agent",
|
|
58
|
+
find: (t, n) => c(e, r, "AssistantFindAgents", (e) => e.vibecontrolsAgents, t, n, (e) => a(e.name) ?? a(e.hostname))
|
|
59
|
+
}];
|
|
60
|
+
}
|
|
61
|
+
var u = l();
|
|
62
|
+
//#endregion
|
|
63
|
+
export { u as VIBECONTROLS_RECORD_ADAPTERS };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=recordAdapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordAdapters.js","names":[],"sources":["../../../src/components/assistant/recordAdapters.ts"],"sourcesContent":["/**\n * VibeControls' named-record adapters for the shared assistant (BOFF-8060 N1b):\n * \"open the Staging Tunnel vibe\", \"open the build-box agent\".\n *\n * An adapter only LOOKS A RECORD UP BY NAME through the READ API the product's\n * own pages already use (`vibes`, `vibecontrolsAgents`), as the signed-in person:\n * the workspace token is theirs and the gateway's `@rbac` filters the list, so a\n * record they may not read never comes back. The adapter produces route params,\n * never a path: fe-libs fills the entity route declared in `assistantProduct.ts`\n * (`ENTITY_ROUTES`) and re-validates it, and only for a person who can see the\n * route's sidebar entry.\n *\n * ★ Neither list query has a server-side `search`, so the adapter reads the same\n * first page the product's own pickers read (`LIST_LIMIT`) and matches the name\n * HERE. It must filter itself: fe-libs caps what an adapter may return, and an\n * unfiltered page would let the wanted record fall off the end.\n *\n * ★ Raw operation strings (the product's other non-hook reads do the same):\n * `recordAdapters.test.ts` validates both against the committed workspace schema.\n *\n * ★ Nothing here writes, opens a form, or touches the DOM.\n */\nimport type {\n AssistantRecordAdapter,\n AssistantRecordMatch,\n} from '@burdenoff/fe-libs/shared/assistant/uiActions/recordNames';\nimport { getStoredWorkspaceId, graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\n/** The page size the product's own vibe picker reads (EntityVibeLinkControl). */\nexport const LIST_LIMIT = 200;\n\nexport const LIST_VIBES_FOR_ASSISTANT = `\n query AssistantFindVibes($pagination: PaginationInput) {\n vibes(pagination: $pagination) {\n items { id name workspaceId archivedAt deletedAt }\n }\n }\n`;\n\nexport const LIST_AGENTS_FOR_ASSISTANT = `\n query AssistantFindAgents($pagination: PaginationInput) {\n vibecontrolsAgents(pagination: $pagination) {\n id name hostname workspaceId deletedAt\n }\n }\n`;\n\n/** The two things the adapters need from the app; injectable so tests run without a network. */\nexport interface RecordAdapterDeps {\n graphqlFetch: typeof graphqlFetch;\n getWorkspaceId: () => string | null;\n}\nconst LIVE_DEPS: RecordAdapterDeps = { graphqlFetch, getWorkspaceId: getStoredWorkspaceId };\n\ninterface RawNode {\n id?: unknown;\n name?: unknown;\n hostname?: unknown;\n workspaceId?: unknown;\n archivedAt?: unknown;\n deletedAt?: unknown;\n}\n\nconst text = (value: unknown): string | null =>\n typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;\n\nconst normalise = (value: string): string =>\n value\n .normalize('NFKC')\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s]/gu, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\n/** Every word said is the start of a word in the name (the same rule fe-libs plans with). */\nexport function nameFits(said: string, name: string): boolean {\n const wanted = normalise(said);\n if (!wanted) return false;\n const words = normalise(name).split(' ');\n return wanted.split(' ').every((word) => words.some((candidate) => candidate.startsWith(word)));\n}\n\nasync function findByName(\n deps: RecordAdapterDeps,\n query: string,\n operationName: string,\n read: (data: Record<string, unknown>) => unknown,\n name: string,\n signal: AbortSignal,\n label: (node: RawNode) => string | null\n): Promise<readonly AssistantRecordMatch[]> {\n const workspaceId = deps.getWorkspaceId();\n if (!workspaceId) return [];\n const result = await deps.graphqlFetch<Record<string, unknown>>({\n gateway: 'workspace',\n query,\n operationName,\n variables: { pagination: { limit: LIST_LIMIT } },\n // The person's own workspace token, minted for THIS workspace.\n workspaceToken: true,\n workspaceId,\n organizationId: true,\n signal,\n suppressGlobalErrorEvent: true,\n });\n if (result.errors?.length || !result.data) return [];\n const rows = read(result.data);\n if (!Array.isArray(rows)) return [];\n const matches: AssistantRecordMatch[] = [];\n for (const row of rows as Array<RawNode | null>) {\n if (!row || typeof row.id !== 'string' || row.id.length === 0) continue;\n // A list answered for another workspace (a switch mid-request) is not used.\n if (typeof row.workspaceId === 'string' && row.workspaceId !== workspaceId) continue;\n if (row.deletedAt || row.archivedAt) continue;\n const shown = label(row);\n if (shown === null || !nameFits(name, shown)) continue;\n matches.push({ params: { id: row.id }, name: shown });\n }\n return matches;\n}\n\nexport function createVibeControlsRecordAdapters(\n deps: RecordAdapterDeps = LIVE_DEPS\n): readonly AssistantRecordAdapter[] {\n const vibes: AssistantRecordAdapter = {\n id: 'vibecontrols-vibe',\n nouns: ['vibe', 'vibes'],\n noun: { key: 'entity.vibe', fallback: 'vibe' },\n entityRouteId: 'vibecontrols-vibe',\n find: (name, signal) =>\n findByName(\n deps,\n LIST_VIBES_FOR_ASSISTANT,\n 'AssistantFindVibes',\n (data) => (data.vibes as { items?: unknown } | null | undefined)?.items,\n name,\n signal,\n (node) => text(node.name)\n ),\n };\n const agents: AssistantRecordAdapter = {\n id: 'vibecontrols-agent',\n nouns: ['agent', 'agents'],\n noun: { key: 'entity.agent', fallback: 'agent' },\n entityRouteId: 'vibecontrols-agent',\n find: (name, signal) =>\n findByName(\n deps,\n LIST_AGENTS_FOR_ASSISTANT,\n 'AssistantFindAgents',\n (data) => data.vibecontrolsAgents,\n name,\n signal,\n // An agent with no display name is called by its host.\n (node) => text(node.name) ?? text(node.hostname)\n ),\n };\n return [vibes, agents];\n}\n\nexport const VIBECONTROLS_RECORD_ADAPTERS: readonly AssistantRecordAdapter[] =\n createVibeControlsRecordAdapters();\n"],"mappings":";AA+BA,IAAa,IAA2B,8KAQ3B,IAA4B,gLAanC,IAA+B;CAAE;CAAc,gBAAgB;CAAsB,EAWrF,KAAQ,MACZ,OAAO,KAAU,YAAY,EAAM,MAAM,CAAC,SAAS,IAAI,EAAM,MAAM,GAAG,MAElE,KAAa,MACjB,EACG,UAAU,OAAO,CACjB,aAAa,CACb,QAAQ,qBAAqB,IAAI,CACjC,QAAQ,QAAQ,IAAI,CACpB,MAAM;AAGX,SAAgB,EAAS,GAAc,GAAuB;CAC5D,IAAM,IAAS,EAAU,EAAK;AAC9B,KAAI,CAAC,EAAQ,QAAO;CACpB,IAAM,IAAQ,EAAU,EAAK,CAAC,MAAM,IAAI;AACxC,QAAO,EAAO,MAAM,IAAI,CAAC,OAAO,MAAS,EAAM,MAAM,MAAc,EAAU,WAAW,EAAK,CAAC,CAAC;;AAGjG,eAAe,EACb,GACA,GACA,GACA,GACA,GACA,GACA,GAC0C;CAC1C,IAAM,IAAc,EAAK,gBAAgB;AACzC,KAAI,CAAC,EAAa,QAAO,EAAE;CAC3B,IAAM,IAAS,MAAM,EAAK,aAAsC;EAC9D,SAAS;EACT;EACA;EACA,WAAW,EAAE,YAAY,EAAE,OAAA,KAAmB,EAAE;EAEhD,gBAAgB;EAChB;EACA,gBAAgB;EAChB;EACA,0BAA0B;EAC3B,CAAC;AACF,KAAI,EAAO,QAAQ,UAAU,CAAC,EAAO,KAAM,QAAO,EAAE;CACpD,IAAM,IAAO,EAAK,EAAO,KAAK;AAC9B,KAAI,CAAC,MAAM,QAAQ,EAAK,CAAE,QAAO,EAAE;CACnC,IAAM,IAAkC,EAAE;AAC1C,MAAK,IAAM,KAAO,GAA+B;AAI/C,MAHI,CAAC,KAAO,OAAO,EAAI,MAAO,YAAY,EAAI,GAAG,WAAW,KAExD,OAAO,EAAI,eAAgB,YAAY,EAAI,gBAAgB,KAC3D,EAAI,aAAa,EAAI,WAAY;EACrC,IAAM,IAAQ,EAAM,EAAI;AACpB,QAAU,QAAQ,CAAC,EAAS,GAAM,EAAM,IAC5C,EAAQ,KAAK;GAAE,QAAQ,EAAE,IAAI,EAAI,IAAI;GAAE,MAAM;GAAO,CAAC;;AAEvD,QAAO;;AAGT,SAAgB,EACd,IAA0B,GACS;AAkCnC,QAAO,CAjC+B;EACpC,IAAI;EACJ,OAAO,CAAC,QAAQ,QAAQ;EACxB,MAAM;GAAE,KAAK;GAAe,UAAU;GAAQ;EAC9C,eAAe;EACf,OAAO,GAAM,MACX,EACE,GACA,GACA,uBACC,MAAU,EAAK,OAAkD,OAClE,GACA,IACC,MAAS,EAAK,EAAK,KAAK,CAC1B;EACJ,EACsC;EACrC,IAAI;EACJ,OAAO,CAAC,SAAS,SAAS;EAC1B,MAAM;GAAE,KAAK;GAAgB,UAAU;GAAS;EAChD,eAAe;EACf,OAAO,GAAM,MACX,EACE,GACA,GACA,wBACC,MAAS,EAAK,oBACf,GACA,IAEC,MAAS,EAAK,EAAK,KAAK,IAAI,EAAK,EAAK,SAAS,CACjD;EACJ,CACqB;;AAGxB,IAAa,IACX,GAAkC"}
|
|
@@ -7,11 +7,11 @@ import { SearchInput as a } from "../components/shared/SearchInput.js";
|
|
|
7
7
|
import { FilterDropdown as o } from "../components/shared/FilterDropdown.js";
|
|
8
8
|
import { GetHelpButton as s } from "../components/shared/GetHelpButton.js";
|
|
9
9
|
import "../components/shared/index.js";
|
|
10
|
-
import { VIBE_PERMISSIONS as
|
|
11
|
-
import { PageSkeleton as
|
|
12
|
-
import { TemplateCard as
|
|
10
|
+
import { VIBE_PERMISSIONS as ee } from "../constants/permissions.js";
|
|
11
|
+
import { PageSkeleton as c } from "../components/shared/PageSkeleton.js";
|
|
12
|
+
import { TemplateCard as l } from "../components/templates/TemplateCard.js";
|
|
13
13
|
import "../components/templates/index.js";
|
|
14
|
-
import { useCallback as
|
|
14
|
+
import { useCallback as u, useEffect as d, useMemo as f, useState as p } from "react";
|
|
15
15
|
import { useI18n as m } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
16
16
|
import { FileCode as h, RefreshCw as g } from "lucide-react";
|
|
17
17
|
import { usePermissions as _ } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
@@ -19,63 +19,82 @@ import { AnimatePresence as v } from "framer-motion";
|
|
|
19
19
|
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
20
20
|
import { PagePurpose as x } from "@burdenoff/fe-libs/ui";
|
|
21
21
|
import { MotionItem as S, MotionList as C, MotionPage as w } from "@burdenoff/fe-libs/shared/motion";
|
|
22
|
+
import { useAssistantPageControls as T } from "@burdenoff/fe-libs/shared/assistant/appControls/useAssistantControl";
|
|
23
|
+
import { enumFilter as E, filtersOf as D, searchOf as O } from "@burdenoff/fe-libs/shared/assistant/appControls/pageAdapters";
|
|
22
24
|
//#region src/pages/TemplatesPage.tsx
|
|
23
|
-
function
|
|
24
|
-
let
|
|
25
|
-
let r =
|
|
25
|
+
function k() {
|
|
26
|
+
let k = t(), { t: A } = m(), { hasPermission: j, isLoading: M } = _(), N = j(ee.TEMPLATE_READ), P = u((e, t, n) => {
|
|
27
|
+
let r = A(e, n), i = r === e ? t : r;
|
|
26
28
|
return n ? Object.entries(n).reduce((e, [t, n]) => e.replace(`{{${t}}}`, String(n)), i) : i;
|
|
27
|
-
}, [
|
|
29
|
+
}, [A]), F = f(() => [
|
|
28
30
|
{
|
|
29
31
|
value: "all",
|
|
30
|
-
label:
|
|
32
|
+
label: P("template.allCategories", "All Categories")
|
|
31
33
|
},
|
|
32
34
|
{
|
|
33
35
|
value: "STARTER",
|
|
34
|
-
label:
|
|
36
|
+
label: P("template.categoryStarter", "Starter")
|
|
35
37
|
},
|
|
36
38
|
{
|
|
37
39
|
value: "MICROSERVICE",
|
|
38
|
-
label:
|
|
40
|
+
label: P("template.categoryMicroservice", "Microservice")
|
|
39
41
|
},
|
|
40
42
|
{
|
|
41
43
|
value: "FRONTEND",
|
|
42
|
-
label:
|
|
44
|
+
label: P("template.categoryFrontend", "Frontend")
|
|
43
45
|
},
|
|
44
46
|
{
|
|
45
47
|
value: "BACKEND",
|
|
46
|
-
label:
|
|
48
|
+
label: P("template.categoryBackend", "Backend")
|
|
47
49
|
},
|
|
48
50
|
{
|
|
49
51
|
value: "FULLSTACK",
|
|
50
|
-
label:
|
|
52
|
+
label: P("template.categoryFullstack", "Fullstack")
|
|
51
53
|
},
|
|
52
54
|
{
|
|
53
55
|
value: "INFRASTRUCTURE",
|
|
54
|
-
label:
|
|
56
|
+
label: P("template.categoryInfrastructure", "Infrastructure")
|
|
55
57
|
},
|
|
56
58
|
{
|
|
57
59
|
value: "LIBRARY",
|
|
58
|
-
label:
|
|
60
|
+
label: P("template.categoryLibrary", "Library")
|
|
59
61
|
},
|
|
60
62
|
{
|
|
61
63
|
value: "CUSTOM",
|
|
62
|
-
label:
|
|
64
|
+
label: P("template.categoryCustom", "Custom")
|
|
63
65
|
}
|
|
64
|
-
], [
|
|
66
|
+
], [P]), [I, L] = p(""), [R, z] = p("all"), [B, V] = p([]), [H, U] = p(!1);
|
|
67
|
+
T(f(() => ({
|
|
68
|
+
page: P("template.assistantPage", "Templates"),
|
|
69
|
+
filters: D([E({
|
|
70
|
+
id: "category",
|
|
71
|
+
label: P("template.assistantCategory", "Category"),
|
|
72
|
+
options: F,
|
|
73
|
+
neutral: "all",
|
|
74
|
+
value: R,
|
|
75
|
+
set: z
|
|
76
|
+
})]),
|
|
77
|
+
search: O(L)
|
|
78
|
+
}), [
|
|
79
|
+
P,
|
|
80
|
+
F,
|
|
81
|
+
R
|
|
82
|
+
]));
|
|
83
|
+
let { data: W, loading: G, error: K, refetch: q, fetchMore: J } = e({
|
|
65
84
|
variables: {
|
|
66
85
|
filter: {
|
|
67
|
-
category:
|
|
68
|
-
search:
|
|
86
|
+
category: R === "all" ? void 0 : R,
|
|
87
|
+
search: I || void 0
|
|
69
88
|
},
|
|
70
89
|
pagination: {
|
|
71
90
|
limit: 50,
|
|
72
91
|
offset: 0
|
|
73
92
|
}
|
|
74
93
|
},
|
|
75
|
-
skip:
|
|
94
|
+
skip: M || !N,
|
|
76
95
|
fetchPolicy: "cache-and-network",
|
|
77
96
|
notifyOnNetworkStatusChange: !0
|
|
78
|
-
}),
|
|
97
|
+
}), Y = u((e) => e.map((e) => ({
|
|
79
98
|
id: e.id,
|
|
80
99
|
name: e.name,
|
|
81
100
|
description: e.description || null,
|
|
@@ -86,17 +105,17 @@ function T() {
|
|
|
86
105
|
createdAt: e.createdAt,
|
|
87
106
|
updatedAt: e.updatedAt
|
|
88
107
|
})), []);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}, [
|
|
92
|
-
let
|
|
93
|
-
if (!(
|
|
94
|
-
|
|
108
|
+
d(() => {
|
|
109
|
+
V(Y(W?.vibecontrolsTemplates?.items ?? []));
|
|
110
|
+
}, [W?.vibecontrolsTemplates?.items, Y]);
|
|
111
|
+
let X = W?.vibecontrolsTemplates?.total ?? 0, Z = B.length < X, Q = B, $ = B, te = u(async () => {
|
|
112
|
+
if (!(H || !Z)) {
|
|
113
|
+
U(!0);
|
|
95
114
|
try {
|
|
96
|
-
await
|
|
115
|
+
await J({
|
|
97
116
|
variables: { pagination: {
|
|
98
117
|
limit: 50,
|
|
99
|
-
offset:
|
|
118
|
+
offset: B.length
|
|
100
119
|
} },
|
|
101
120
|
updateQuery: (e, { fetchMoreResult: t }) => {
|
|
102
121
|
if (!t?.vibecontrolsTemplates) return e;
|
|
@@ -113,49 +132,49 @@ function T() {
|
|
|
113
132
|
}
|
|
114
133
|
});
|
|
115
134
|
} finally {
|
|
116
|
-
|
|
135
|
+
U(!1);
|
|
117
136
|
}
|
|
118
137
|
}
|
|
119
138
|
}, [
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
]),
|
|
125
|
-
|
|
139
|
+
J,
|
|
140
|
+
B.length,
|
|
141
|
+
H,
|
|
142
|
+
Z
|
|
143
|
+
]), ne = (e) => {
|
|
144
|
+
k(`/templates/${e.id}`);
|
|
126
145
|
};
|
|
127
|
-
return
|
|
146
|
+
return M ? /* @__PURE__ */ y("div", {
|
|
128
147
|
className: "flex items-center justify-center min-h-[400px]",
|
|
129
148
|
children: /* @__PURE__ */ y("p", {
|
|
130
149
|
className: "text-text-secondary",
|
|
131
150
|
children: "Loading permissions…"
|
|
132
151
|
})
|
|
133
|
-
}) :
|
|
152
|
+
}) : N ? K && Q.length === 0 ? /* @__PURE__ */ y(w, {
|
|
134
153
|
className: "space-y-6 px-6 lg:px-8 py-6 bg-bg-canvas min-h-screen",
|
|
135
154
|
children: /* @__PURE__ */ y(r, {
|
|
136
|
-
title:
|
|
137
|
-
message:
|
|
138
|
-
onRetry: () =>
|
|
155
|
+
title: P("template.failedLoad", "Failed to load templates"),
|
|
156
|
+
message: K.message,
|
|
157
|
+
onRetry: () => q()
|
|
139
158
|
})
|
|
140
159
|
}) : /* @__PURE__ */ b(w, {
|
|
141
160
|
className: "space-y-6 px-6 lg:px-8 py-6 bg-bg-canvas min-h-screen",
|
|
142
161
|
children: [
|
|
143
162
|
/* @__PURE__ */ y(n, {
|
|
144
|
-
title:
|
|
145
|
-
description:
|
|
163
|
+
title: P("template.title", "Templates"),
|
|
164
|
+
description: X > 0 ? P("template.description", "{{total}} ready-made blueprints — pick one to scaffold a new component instantly", { total: Q.length }) : P("template.descriptionEmpty", "Reusable blueprints your team publishes from components — pick one to scaffold a new component in seconds"),
|
|
146
165
|
actions: /* @__PURE__ */ y("button", {
|
|
147
166
|
type: "button",
|
|
148
|
-
onClick: () =>
|
|
149
|
-
disabled:
|
|
167
|
+
onClick: () => q(),
|
|
168
|
+
disabled: G,
|
|
150
169
|
className: "flex items-center gap-2 px-3 py-2 text-text-secondary hover:bg-bg-sunken rounded-button transition-colors disabled:opacity-50",
|
|
151
|
-
title:
|
|
152
|
-
"aria-label":
|
|
153
|
-
children: /* @__PURE__ */ y(g, { className: `size-4 ${
|
|
170
|
+
title: P("common.refresh", "Refresh"),
|
|
171
|
+
"aria-label": P("common.refresh", "Refresh"),
|
|
172
|
+
children: /* @__PURE__ */ y(g, { className: `size-4 ${G ? "animate-spin" : ""}` })
|
|
154
173
|
})
|
|
155
174
|
}),
|
|
156
175
|
/* @__PURE__ */ y(x, {
|
|
157
176
|
icon: /* @__PURE__ */ y(h, { className: "size-4" }),
|
|
158
|
-
children:
|
|
177
|
+
children: P("template.purpose", "Templates are reusable blueprints your team publishes from working components — a saved starting point complete with structure and config. Pick a template to scaffold a new component in seconds instead of wiring it up by hand, so everyone starts from the same proven setup.")
|
|
159
178
|
}),
|
|
160
179
|
/* @__PURE__ */ b("div", {
|
|
161
180
|
className: "flex items-center gap-2 sm:gap-3 flex-wrap",
|
|
@@ -163,60 +182,60 @@ function T() {
|
|
|
163
182
|
"data-tour": "vibecontrols-template-search",
|
|
164
183
|
className: "flex-1 sm:w-64 sm:flex-none",
|
|
165
184
|
children: /* @__PURE__ */ y(a, {
|
|
166
|
-
value:
|
|
167
|
-
onChange:
|
|
168
|
-
placeholder:
|
|
185
|
+
value: I,
|
|
186
|
+
onChange: L,
|
|
187
|
+
placeholder: P("template.searchPlaceholder", "Search templates..."),
|
|
169
188
|
className: "w-full"
|
|
170
189
|
})
|
|
171
190
|
}), /* @__PURE__ */ y("div", {
|
|
172
191
|
"data-tour": "vibecontrols-template-category-filter",
|
|
173
192
|
children: /* @__PURE__ */ y(o, {
|
|
174
|
-
label:
|
|
175
|
-
value:
|
|
176
|
-
options:
|
|
177
|
-
onChange:
|
|
193
|
+
label: P("template.filterCategory", "Category"),
|
|
194
|
+
value: R,
|
|
195
|
+
options: F,
|
|
196
|
+
onChange: z
|
|
178
197
|
})
|
|
179
198
|
})]
|
|
180
199
|
}),
|
|
181
200
|
/* @__PURE__ */ b("p", {
|
|
182
201
|
className: "text-sm text-text-secondary",
|
|
183
|
-
children: [
|
|
184
|
-
filtered:
|
|
185
|
-
total:
|
|
186
|
-
}),
|
|
202
|
+
children: [P("template.showingResults", "Showing {{filtered}} of {{total}} templates", {
|
|
203
|
+
filtered: $.length,
|
|
204
|
+
total: X
|
|
205
|
+
}), G && /* @__PURE__ */ y("span", {
|
|
187
206
|
className: "ml-2 text-text-muted",
|
|
188
|
-
children:
|
|
207
|
+
children: P("common.refreshing", "(refreshing...)")
|
|
189
208
|
})]
|
|
190
209
|
}),
|
|
191
|
-
|
|
210
|
+
G && Q.length === 0 ? /* @__PURE__ */ y(c, {
|
|
192
211
|
variant: "cards",
|
|
193
212
|
count: 6
|
|
194
|
-
}) :
|
|
213
|
+
}) : $.length > 0 ? /* @__PURE__ */ y(v, {
|
|
195
214
|
mode: "wait",
|
|
196
215
|
children: /* @__PURE__ */ y(C, {
|
|
197
216
|
stagger: .04,
|
|
198
217
|
className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4",
|
|
199
218
|
"data-tour": "vibecontrols-template-grid",
|
|
200
|
-
children:
|
|
219
|
+
children: $.map((e) => /* @__PURE__ */ y(S, { children: /* @__PURE__ */ y(l, {
|
|
201
220
|
template: e,
|
|
202
|
-
onOpen:
|
|
221
|
+
onOpen: ne
|
|
203
222
|
}) }, e.id))
|
|
204
223
|
}, "template-grid")
|
|
205
224
|
}) : /* @__PURE__ */ y(i, {
|
|
206
225
|
icon: /* @__PURE__ */ y(h, { className: "size-8 text-text-secondary" }),
|
|
207
|
-
illustration:
|
|
208
|
-
title:
|
|
209
|
-
description:
|
|
210
|
-
action: !
|
|
226
|
+
illustration: I || R !== "all" ? "empty-search" : "empty-data",
|
|
227
|
+
title: I || R !== "all" ? P("template.emptyNoResults", "No templates found") : P("template.emptyNoTemplates", "No templates yet"),
|
|
228
|
+
description: I || R !== "all" ? P("template.emptyAdjustSearch", "Try adjusting your search or filters") : P("template.emptyCreate", "No templates yet. To create one: open any component → click the ⋯ menu → \"Publish as Template\". It will appear here and anyone in your workspace can use it as a starting point."),
|
|
229
|
+
action: !I && R === "all" ? /* @__PURE__ */ y(s, { prompt: "What is a Template in VibeControls and how do I create one?" }) : void 0
|
|
211
230
|
}),
|
|
212
|
-
|
|
231
|
+
Z && $.length > 0 && /* @__PURE__ */ y("div", {
|
|
213
232
|
className: "flex justify-center pt-2",
|
|
214
233
|
children: /* @__PURE__ */ y("button", {
|
|
215
234
|
type: "button",
|
|
216
|
-
onClick:
|
|
217
|
-
disabled:
|
|
235
|
+
onClick: te,
|
|
236
|
+
disabled: H,
|
|
218
237
|
className: "flex items-center gap-2 px-4 py-2 bg-bg-sunken text-text-primary rounded-button hover:bg-action-secondary-bg-hover transition-colors disabled:opacity-50",
|
|
219
|
-
children:
|
|
238
|
+
children: H ? P("common.loading", "Loading...") : P("common.loadMore", "Load more")
|
|
220
239
|
})
|
|
221
240
|
})
|
|
222
241
|
]
|
|
@@ -235,6 +254,6 @@ function T() {
|
|
|
235
254
|
});
|
|
236
255
|
}
|
|
237
256
|
//#endregion
|
|
238
|
-
export {
|
|
257
|
+
export { k as TemplatesPage };
|
|
239
258
|
|
|
240
259
|
//# sourceMappingURL=TemplatesPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TemplatesPage.js","names":[],"sources":["../../src/pages/TemplatesPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect } from 'react';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '../constants/permissions';\nimport { FileCode, RefreshCw } from 'lucide-react';\nimport { TemplateCard } from '@/components/templates';\nimport { PageSkeleton } from '@/components/shared/PageSkeleton';\nimport type { TemplateCardData } from '@/components/templates';\nimport {\n PageHeader,\n SearchInput,\n FilterDropdown,\n EmptyState,\n ErrorState,\n GetHelpButton,\n} from '@/components/shared';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { AnimatePresence } from 'framer-motion';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useListTemplatesQuery } from '@/generated/wspace-operations';\n\nexport function TemplatesPage() {\n const navigate = useVibeNavigate();\n const { t } = useI18n();\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const canReadTemplate = hasPermission(VIBE_PERMISSIONS.TEMPLATE_READ);\n\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key, params);\n const template = translated === key ? fallback : translated;\n if (!params) return template;\n return Object.entries(params).reduce(\n (acc, [paramKey, paramValue]) => acc.replace(`{{${paramKey}}}`, String(paramValue)),\n template\n );\n },\n [t]\n );\n\n const categoryOptions = [\n { value: 'all', label: tr('template.allCategories', 'All Categories') },\n { value: 'STARTER', label: tr('template.categoryStarter', 'Starter') },\n { value: 'MICROSERVICE', label: tr('template.categoryMicroservice', 'Microservice') },\n { value: 'FRONTEND', label: tr('template.categoryFrontend', 'Frontend') },\n { value: 'BACKEND', label: tr('template.categoryBackend', 'Backend') },\n { value: 'FULLSTACK', label: tr('template.categoryFullstack', 'Fullstack') },\n { value: 'INFRASTRUCTURE', label: tr('template.categoryInfrastructure', 'Infrastructure') },\n { value: 'LIBRARY', label: tr('template.categoryLibrary', 'Library') },\n { value: 'CUSTOM', label: tr('template.categoryCustom', 'Custom') },\n ];\n\n const PAGE_SIZE = 50;\n const [searchQuery, setSearchQuery] = useState('');\n const [categoryFilter, setCategoryFilter] = useState('all');\n const [accumulated, setAccumulated] = useState<TemplateCardData[]>([]);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const { data, loading, error, refetch, fetchMore } = useListTemplatesQuery({\n variables: {\n filter: {\n category: categoryFilter !== 'all' ? categoryFilter : undefined,\n search: searchQuery || undefined,\n },\n pagination: { limit: PAGE_SIZE, offset: 0 },\n },\n skip: permissionsLoading || !canReadTemplate,\n fetchPolicy: 'cache-and-network',\n notifyOnNetworkStatusChange: true,\n });\n\n const mapItems = useCallback(\n (items: ReadonlyArray<Record<string, unknown>>): TemplateCardData[] =>\n items.map((item) => ({\n id: item.id as string,\n name: item.name as string,\n description: (item.description as string) || null,\n category: (item.category as string) || null,\n cloneCount: (item.cloneCount as number) || 0,\n variableCount: Array.isArray(item.variables) ? (item.variables as unknown[]).length : 0,\n tags: (item.tags as string[]) || [],\n createdAt: item.createdAt as string,\n updatedAt: item.updatedAt as string,\n })),\n []\n );\n\n useEffect(() => {\n const items = data?.vibecontrolsTemplates?.items ?? [];\n setAccumulated(mapItems(items as ReadonlyArray<Record<string, unknown>>));\n }, [data?.vibecontrolsTemplates?.items, mapItems]);\n\n const total = data?.vibecontrolsTemplates?.total ?? 0;\n const hasMore = accumulated.length < total;\n const templates = accumulated;\n const filteredTemplates = accumulated;\n\n const handleLoadMore = useCallback(async () => {\n if (loadingMore || !hasMore) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: accumulated.length } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult?.vibecontrolsTemplates) return prev;\n const prevList = prev.vibecontrolsTemplates;\n const nextList = fetchMoreResult.vibecontrolsTemplates;\n return {\n ...prev,\n vibecontrolsTemplates: {\n ...nextList,\n items: [...(prevList?.items ?? []), ...nextList.items],\n total: nextList.total,\n hasMore: nextList.hasMore,\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n }, [fetchMore, accumulated.length, loadingMore, hasMore]);\n\n const handleTemplateSelect = (tmpl: TemplateCardData) => {\n navigate(`/templates/${tmpl.id}`);\n };\n\n if (permissionsLoading)\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <p className=\"text-text-secondary\">Loading permissions…</p>\n </div>\n );\n if (!canReadTemplate)\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary\">\n You don't have permission to access this page.\n </p>\n </div>\n </div>\n );\n\n if (error && templates.length === 0) {\n return (\n <MotionPage className=\"space-y-6 px-6 lg:px-8 py-6 bg-bg-canvas min-h-screen\">\n <ErrorState\n title={tr('template.failedLoad', 'Failed to load templates')}\n message={error.message}\n onRetry={() => refetch()}\n />\n </MotionPage>\n );\n }\n\n return (\n <MotionPage className=\"space-y-6 px-6 lg:px-8 py-6 bg-bg-canvas min-h-screen\">\n <PageHeader\n title={tr('template.title', 'Templates')}\n description={\n total > 0\n ? tr(\n 'template.description',\n '{{total}} ready-made blueprints — pick one to scaffold a new component instantly',\n {\n total: templates.length,\n }\n )\n : tr(\n 'template.descriptionEmpty',\n 'Reusable blueprints your team publishes from components — pick one to scaffold a new component in seconds'\n )\n }\n actions={\n <button\n type=\"button\"\n onClick={() => refetch()}\n disabled={loading}\n className=\"flex items-center gap-2 px-3 py-2 text-text-secondary hover:bg-bg-sunken rounded-button transition-colors disabled:opacity-50\"\n title={tr('common.refresh', 'Refresh')}\n aria-label={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n </button>\n }\n />\n\n <PagePurpose icon={<FileCode className=\"size-4\" />}>\n {tr(\n 'template.purpose',\n 'Templates are reusable blueprints your team publishes from working components — a saved starting point complete with structure and config. Pick a template to scaffold a new component in seconds instead of wiring it up by hand, so everyone starts from the same proven setup.'\n )}\n </PagePurpose>\n\n {/* Filters — SearchInput / FilterDropdown are custom components that don't\n forward rest props onto a DOM node, so the tour anchors live on\n wrapper <div>s. */}\n <div className=\"flex items-center gap-2 sm:gap-3 flex-wrap\">\n <div data-tour=\"vibecontrols-template-search\" className=\"flex-1 sm:w-64 sm:flex-none\">\n <SearchInput\n value={searchQuery}\n onChange={setSearchQuery}\n placeholder={tr('template.searchPlaceholder', 'Search templates...')}\n className=\"w-full\"\n />\n </div>\n <div data-tour=\"vibecontrols-template-category-filter\">\n <FilterDropdown\n label={tr('template.filterCategory', 'Category')}\n value={categoryFilter}\n options={categoryOptions}\n onChange={setCategoryFilter}\n />\n </div>\n </div>\n\n {/* Results count */}\n <p className=\"text-sm text-text-secondary\">\n {tr('template.showingResults', 'Showing {{filtered}} of {{total}} templates', {\n filtered: filteredTemplates.length,\n total,\n })}\n {loading && (\n <span className=\"ml-2 text-text-muted\">{tr('common.refreshing', '(refreshing...)')}</span>\n )}\n </p>\n\n {/* Grid */}\n {loading && templates.length === 0 ? (\n <PageSkeleton variant=\"cards\" count={6} />\n ) : filteredTemplates.length > 0 ? (\n <AnimatePresence mode=\"wait\">\n <MotionList\n key=\"template-grid\"\n stagger={0.04}\n className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\"\n data-tour=\"vibecontrols-template-grid\"\n >\n {filteredTemplates.map((tmpl) => (\n <MotionItem key={tmpl.id}>\n <TemplateCard template={tmpl} onOpen={handleTemplateSelect} />\n </MotionItem>\n ))}\n </MotionList>\n </AnimatePresence>\n ) : (\n <EmptyState\n icon={<FileCode className=\"size-8 text-text-secondary\" />}\n illustration={searchQuery || categoryFilter !== 'all' ? 'empty-search' : 'empty-data'}\n title={\n searchQuery || categoryFilter !== 'all'\n ? tr('template.emptyNoResults', 'No templates found')\n : tr('template.emptyNoTemplates', 'No templates yet')\n }\n description={\n searchQuery || categoryFilter !== 'all'\n ? tr('template.emptyAdjustSearch', 'Try adjusting your search or filters')\n : tr(\n 'template.emptyCreate',\n 'No templates yet. To create one: open any component → click the ⋯ menu → \"Publish as Template\". It will appear here and anyone in your workspace can use it as a starting point.'\n )\n }\n action={\n !searchQuery && categoryFilter === 'all' ? (\n <GetHelpButton prompt=\"What is a Template in VibeControls and how do I create one?\" />\n ) : undefined\n }\n />\n )}\n\n {/* Load more */}\n {hasMore && filteredTemplates.length > 0 && (\n <div className=\"flex justify-center pt-2\">\n <button\n type=\"button\"\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"flex items-center gap-2 px-4 py-2 bg-bg-sunken text-text-primary rounded-button hover:bg-action-secondary-bg-hover transition-colors disabled:opacity-50\"\n >\n {loadingMore ? tr('common.loading', 'Loading...') : tr('common.loadMore', 'Load more')}\n </button>\n </div>\n )}\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TemplatesPage.js","names":[],"sources":["../../src/pages/TemplatesPage.tsx"],"sourcesContent":["import { useAssistantPageControls } from '@burdenoff/fe-libs/shared/assistant/appControls/useAssistantControl';\nimport {\n enumFilter,\n filtersOf,\n searchOf,\n} from '@burdenoff/fe-libs/shared/assistant/appControls/pageAdapters';\nimport { useState, useCallback, useEffect, useMemo } from 'react';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '../constants/permissions';\nimport { FileCode, RefreshCw } from 'lucide-react';\nimport { TemplateCard } from '@/components/templates';\nimport { PageSkeleton } from '@/components/shared/PageSkeleton';\nimport type { TemplateCardData } from '@/components/templates';\nimport {\n PageHeader,\n SearchInput,\n FilterDropdown,\n EmptyState,\n ErrorState,\n GetHelpButton,\n} from '@/components/shared';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { AnimatePresence } from 'framer-motion';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useListTemplatesQuery } from '@/generated/wspace-operations';\n\nexport function TemplatesPage() {\n const navigate = useVibeNavigate();\n const { t } = useI18n();\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const canReadTemplate = hasPermission(VIBE_PERMISSIONS.TEMPLATE_READ);\n\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key, params);\n const template = translated === key ? fallback : translated;\n if (!params) return template;\n return Object.entries(params).reduce(\n (acc, [paramKey, paramValue]) => acc.replace(`{{${paramKey}}}`, String(paramValue)),\n template\n );\n },\n [t]\n );\n\n const categoryOptions = useMemo(\n () => [\n { value: 'all', label: tr('template.allCategories', 'All Categories') },\n { value: 'STARTER', label: tr('template.categoryStarter', 'Starter') },\n { value: 'MICROSERVICE', label: tr('template.categoryMicroservice', 'Microservice') },\n { value: 'FRONTEND', label: tr('template.categoryFrontend', 'Frontend') },\n { value: 'BACKEND', label: tr('template.categoryBackend', 'Backend') },\n { value: 'FULLSTACK', label: tr('template.categoryFullstack', 'Fullstack') },\n { value: 'INFRASTRUCTURE', label: tr('template.categoryInfrastructure', 'Infrastructure') },\n { value: 'LIBRARY', label: tr('template.categoryLibrary', 'Library') },\n { value: 'CUSTOM', label: tr('template.categoryCustom', 'Custom') },\n ],\n [tr]\n );\n\n const PAGE_SIZE = 50;\n const [searchQuery, setSearchQuery] = useState('');\n const [categoryFilter, setCategoryFilter] = useState('all');\n const [accumulated, setAccumulated] = useState<TemplateCardData[]>([]);\n const [loadingMore, setLoadingMore] = useState(false);\n\n // BOFF-8060: what the assistant may do on this page — the SAME setters the\n // category dropdown and the search box call.\n const assistantControls = useMemo(\n () => ({\n page: tr('template.assistantPage', 'Templates'),\n filters: filtersOf([\n enumFilter<string>({\n id: 'category',\n label: tr('template.assistantCategory', 'Category'),\n options: categoryOptions,\n neutral: 'all',\n value: categoryFilter,\n set: setCategoryFilter,\n }),\n ]),\n search: searchOf(setSearchQuery),\n }),\n [tr, categoryOptions, categoryFilter]\n );\n useAssistantPageControls(assistantControls);\n\n const { data, loading, error, refetch, fetchMore } = useListTemplatesQuery({\n variables: {\n filter: {\n category: categoryFilter !== 'all' ? categoryFilter : undefined,\n search: searchQuery || undefined,\n },\n pagination: { limit: PAGE_SIZE, offset: 0 },\n },\n skip: permissionsLoading || !canReadTemplate,\n fetchPolicy: 'cache-and-network',\n notifyOnNetworkStatusChange: true,\n });\n\n const mapItems = useCallback(\n (items: ReadonlyArray<Record<string, unknown>>): TemplateCardData[] =>\n items.map((item) => ({\n id: item.id as string,\n name: item.name as string,\n description: (item.description as string) || null,\n category: (item.category as string) || null,\n cloneCount: (item.cloneCount as number) || 0,\n variableCount: Array.isArray(item.variables) ? (item.variables as unknown[]).length : 0,\n tags: (item.tags as string[]) || [],\n createdAt: item.createdAt as string,\n updatedAt: item.updatedAt as string,\n })),\n []\n );\n\n useEffect(() => {\n const items = data?.vibecontrolsTemplates?.items ?? [];\n setAccumulated(mapItems(items as ReadonlyArray<Record<string, unknown>>));\n }, [data?.vibecontrolsTemplates?.items, mapItems]);\n\n const total = data?.vibecontrolsTemplates?.total ?? 0;\n const hasMore = accumulated.length < total;\n const templates = accumulated;\n const filteredTemplates = accumulated;\n\n const handleLoadMore = useCallback(async () => {\n if (loadingMore || !hasMore) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: accumulated.length } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult?.vibecontrolsTemplates) return prev;\n const prevList = prev.vibecontrolsTemplates;\n const nextList = fetchMoreResult.vibecontrolsTemplates;\n return {\n ...prev,\n vibecontrolsTemplates: {\n ...nextList,\n items: [...(prevList?.items ?? []), ...nextList.items],\n total: nextList.total,\n hasMore: nextList.hasMore,\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n }, [fetchMore, accumulated.length, loadingMore, hasMore]);\n\n const handleTemplateSelect = (tmpl: TemplateCardData) => {\n navigate(`/templates/${tmpl.id}`);\n };\n\n if (permissionsLoading)\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <p className=\"text-text-secondary\">Loading permissions…</p>\n </div>\n );\n if (!canReadTemplate)\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Access Denied</h2>\n <p className=\"text-sm text-text-secondary\">\n You don't have permission to access this page.\n </p>\n </div>\n </div>\n );\n\n if (error && templates.length === 0) {\n return (\n <MotionPage className=\"space-y-6 px-6 lg:px-8 py-6 bg-bg-canvas min-h-screen\">\n <ErrorState\n title={tr('template.failedLoad', 'Failed to load templates')}\n message={error.message}\n onRetry={() => refetch()}\n />\n </MotionPage>\n );\n }\n\n return (\n <MotionPage className=\"space-y-6 px-6 lg:px-8 py-6 bg-bg-canvas min-h-screen\">\n <PageHeader\n title={tr('template.title', 'Templates')}\n description={\n total > 0\n ? tr(\n 'template.description',\n '{{total}} ready-made blueprints — pick one to scaffold a new component instantly',\n {\n total: templates.length,\n }\n )\n : tr(\n 'template.descriptionEmpty',\n 'Reusable blueprints your team publishes from components — pick one to scaffold a new component in seconds'\n )\n }\n actions={\n <button\n type=\"button\"\n onClick={() => refetch()}\n disabled={loading}\n className=\"flex items-center gap-2 px-3 py-2 text-text-secondary hover:bg-bg-sunken rounded-button transition-colors disabled:opacity-50\"\n title={tr('common.refresh', 'Refresh')}\n aria-label={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n </button>\n }\n />\n\n <PagePurpose icon={<FileCode className=\"size-4\" />}>\n {tr(\n 'template.purpose',\n 'Templates are reusable blueprints your team publishes from working components — a saved starting point complete with structure and config. Pick a template to scaffold a new component in seconds instead of wiring it up by hand, so everyone starts from the same proven setup.'\n )}\n </PagePurpose>\n\n {/* Filters — SearchInput / FilterDropdown are custom components that don't\n forward rest props onto a DOM node, so the tour anchors live on\n wrapper <div>s. */}\n <div className=\"flex items-center gap-2 sm:gap-3 flex-wrap\">\n <div data-tour=\"vibecontrols-template-search\" className=\"flex-1 sm:w-64 sm:flex-none\">\n <SearchInput\n value={searchQuery}\n onChange={setSearchQuery}\n placeholder={tr('template.searchPlaceholder', 'Search templates...')}\n className=\"w-full\"\n />\n </div>\n <div data-tour=\"vibecontrols-template-category-filter\">\n <FilterDropdown\n label={tr('template.filterCategory', 'Category')}\n value={categoryFilter}\n options={categoryOptions}\n onChange={setCategoryFilter}\n />\n </div>\n </div>\n\n {/* Results count */}\n <p className=\"text-sm text-text-secondary\">\n {tr('template.showingResults', 'Showing {{filtered}} of {{total}} templates', {\n filtered: filteredTemplates.length,\n total,\n })}\n {loading && (\n <span className=\"ml-2 text-text-muted\">{tr('common.refreshing', '(refreshing...)')}</span>\n )}\n </p>\n\n {/* Grid */}\n {loading && templates.length === 0 ? (\n <PageSkeleton variant=\"cards\" count={6} />\n ) : filteredTemplates.length > 0 ? (\n <AnimatePresence mode=\"wait\">\n <MotionList\n key=\"template-grid\"\n stagger={0.04}\n className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\"\n data-tour=\"vibecontrols-template-grid\"\n >\n {filteredTemplates.map((tmpl) => (\n <MotionItem key={tmpl.id}>\n <TemplateCard template={tmpl} onOpen={handleTemplateSelect} />\n </MotionItem>\n ))}\n </MotionList>\n </AnimatePresence>\n ) : (\n <EmptyState\n icon={<FileCode className=\"size-8 text-text-secondary\" />}\n illustration={searchQuery || categoryFilter !== 'all' ? 'empty-search' : 'empty-data'}\n title={\n searchQuery || categoryFilter !== 'all'\n ? tr('template.emptyNoResults', 'No templates found')\n : tr('template.emptyNoTemplates', 'No templates yet')\n }\n description={\n searchQuery || categoryFilter !== 'all'\n ? tr('template.emptyAdjustSearch', 'Try adjusting your search or filters')\n : tr(\n 'template.emptyCreate',\n 'No templates yet. To create one: open any component → click the ⋯ menu → \"Publish as Template\". It will appear here and anyone in your workspace can use it as a starting point.'\n )\n }\n action={\n !searchQuery && categoryFilter === 'all' ? (\n <GetHelpButton prompt=\"What is a Template in VibeControls and how do I create one?\" />\n ) : undefined\n }\n />\n )}\n\n {/* Load more */}\n {hasMore && filteredTemplates.length > 0 && (\n <div className=\"flex justify-center pt-2\">\n <button\n type=\"button\"\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"flex items-center gap-2 px-4 py-2 bg-bg-sunken text-text-primary rounded-button hover:bg-action-secondary-bg-hover transition-colors disabled:opacity-50\"\n >\n {loadingMore ? tr('common.loading', 'Loading...') : tr('common.loadMore', 'Load more')}\n </button>\n </div>\n )}\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|