@ossy/app 1.40.2 → 1.40.3

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.
@@ -0,0 +1,220 @@
1
+ import { templatesContentOneOf } from './template-to-json-schema.js'
2
+
3
+ const workspaceIdProp = {
4
+ workspaceId: {
5
+ type: 'string',
6
+ description: 'Workspace id (defaults to request workspace header)',
7
+ },
8
+ }
9
+
10
+ /** @type {Record<string, (ctx: { schemas: object[] }) => object>} */
11
+ const ENVELOPES = {
12
+ '@ossy/resources/actions/list': () => ({
13
+ type: 'object',
14
+ properties: {
15
+ ...workspaceIdProp,
16
+ location: { type: 'string', description: 'Folder path, e.g. /test/' },
17
+ query: { type: 'object', description: 'Optional query filters merged with location' },
18
+ },
19
+ }),
20
+
21
+ '@ossy/resources/actions/get': () => ({
22
+ type: 'object',
23
+ required: ['resourceId'],
24
+ properties: {
25
+ ...workspaceIdProp,
26
+ resourceId: { type: 'string' },
27
+ },
28
+ }),
29
+
30
+ '@ossy/resources/actions/search': () => ({
31
+ type: 'object',
32
+ properties: {
33
+ ...workspaceIdProp,
34
+ location: { type: 'string' },
35
+ name: { type: 'string' },
36
+ type: { type: 'string', description: 'Resource template id or mime type' },
37
+ },
38
+ }),
39
+
40
+ '@ossy/resources/actions/create': ({ schemas }) => {
41
+ const contentSchema = templatesContentOneOf(schemas)
42
+ return {
43
+ type: 'object',
44
+ required: ['location', 'name', 'type'],
45
+ properties: {
46
+ ...workspaceIdProp,
47
+ location: { type: 'string', description: 'Parent folder path, e.g. /test/' },
48
+ name: { type: 'string' },
49
+ type: {
50
+ type: 'string',
51
+ description: 'directory | mime type (binary) | registered resource template id',
52
+ },
53
+ size: {
54
+ type: 'number',
55
+ description: 'Byte size — required when type is a mime type (binary upload)',
56
+ },
57
+ ...(contentSchema
58
+ ? {
59
+ content: {
60
+ description: 'Document payload when type is a resource template id',
61
+ ...contentSchema,
62
+ },
63
+ }
64
+ : {}),
65
+ },
66
+ }
67
+ },
68
+
69
+ '@ossy/resources/actions/update-content': ({ schemas }) => {
70
+ const contentSchema = templatesContentOneOf(schemas)
71
+ return {
72
+ type: 'object',
73
+ required: ['resourceId', 'content'],
74
+ properties: {
75
+ ...workspaceIdProp,
76
+ resourceId: { type: 'string' },
77
+ ...(contentSchema
78
+ ? { content: contentSchema }
79
+ : { content: { type: 'object' } }),
80
+ },
81
+ }
82
+ },
83
+
84
+ '@ossy/resources/actions/delete': () => ({
85
+ type: 'object',
86
+ required: ['resourceId'],
87
+ properties: {
88
+ ...workspaceIdProp,
89
+ resourceId: { type: 'string' },
90
+ },
91
+ }),
92
+
93
+ '@ossy/resources/actions/upload-named-version': () => ({
94
+ type: 'object',
95
+ required: ['resourceId', 'namedVersion', 'type', 'size'],
96
+ properties: {
97
+ ...workspaceIdProp,
98
+ resourceId: { type: 'string' },
99
+ namedVersion: { type: 'string', description: 'Version label, e.g. thumbnail' },
100
+ type: { type: 'string', description: 'Mime type' },
101
+ size: { type: 'number' },
102
+ },
103
+ }),
104
+
105
+ '@ossy/resources/actions/update-name': () => ({
106
+ type: 'object',
107
+ required: ['resourceId', 'name'],
108
+ properties: {
109
+ ...workspaceIdProp,
110
+ resourceId: { type: 'string' },
111
+ name: { type: 'string' },
112
+ },
113
+ }),
114
+
115
+ '@ossy/resources/actions/update-location': () => ({
116
+ type: 'object',
117
+ required: ['resourceId', 'target'],
118
+ properties: {
119
+ ...workspaceIdProp,
120
+ resourceId: { type: 'string' },
121
+ target: { type: 'string', description: 'Destination folder path' },
122
+ },
123
+ }),
124
+
125
+ '@ossy/resources/actions/update-access': () => ({
126
+ type: 'object',
127
+ required: ['resourceId', 'access'],
128
+ properties: {
129
+ ...workspaceIdProp,
130
+ resourceId: { type: 'string' },
131
+ access: { type: 'string', enum: ['public', 'workspace', 'restricted'] },
132
+ },
133
+ }),
134
+
135
+ '@ossy/booking/actions/get-services': () => ({
136
+ type: 'object',
137
+ properties: {
138
+ providerSlug: { type: 'string' },
139
+ workspaceId: { type: 'string' },
140
+ includeInactive: { type: 'boolean' },
141
+ includeContact: { type: 'boolean' },
142
+ },
143
+ }),
144
+
145
+ '@ossy/booking/actions/get-available-slots': () => ({
146
+ type: 'object',
147
+ properties: {
148
+ providerSlug: { type: 'string' },
149
+ workspaceId: { type: 'string' },
150
+ serviceId: { type: 'string' },
151
+ duration: { type: 'number', description: 'Slot duration in minutes' },
152
+ from: { type: 'number', description: 'Range start (Unix ms)' },
153
+ to: { type: 'number', description: 'Range end (Unix ms)' },
154
+ },
155
+ }),
156
+
157
+ '@ossy/booking/actions/create': () => ({
158
+ type: 'object',
159
+ required: ['startAt', 'duration', 'clientName', 'clientEmail'],
160
+ properties: {
161
+ providerSlug: { type: 'string' },
162
+ workspaceId: { type: 'string' },
163
+ serviceId: { type: 'string' },
164
+ startAt: { type: 'number', description: 'Slot start (Unix ms)' },
165
+ duration: { type: 'number', description: 'Duration in minutes' },
166
+ clientName: { type: 'string' },
167
+ clientEmail: { type: 'string', format: 'email' },
168
+ clientMessage: { type: 'string' },
169
+ },
170
+ }),
171
+
172
+ '@ossy/workspaces/actions/get-schemas': () => ({
173
+ type: 'object',
174
+ properties: { ...workspaceIdProp },
175
+ }),
176
+
177
+ '@ossy/workspaces/actions/import-schemas': () => ({
178
+ type: 'object',
179
+ required: ['schemas'],
180
+ properties: {
181
+ ...workspaceIdProp,
182
+ schemas: {
183
+ type: 'array',
184
+ items: { type: 'object' },
185
+ description: 'Workspace-imported schema definitions',
186
+ },
187
+ },
188
+ }),
189
+
190
+ '@ossy/users/actions/create-api-token': () => ({
191
+ type: 'object',
192
+ required: ['name', 'description'],
193
+ properties: {
194
+ name: { type: 'string' },
195
+ description: { type: 'string' },
196
+ },
197
+ }),
198
+ }
199
+
200
+ /**
201
+ * @param {string} actionId
202
+ * @param {{ schemas?: object[] }} [ctx]
203
+ * @returns {object}
204
+ */
205
+ export function buildActionInputSchema (actionId, ctx = {}) {
206
+ const schemas = ctx.schemas || []
207
+ const builder = ENVELOPES[actionId]
208
+ if (builder) {
209
+ return builder({ schemas })
210
+ }
211
+
212
+ return {
213
+ type: 'object',
214
+ properties: {
215
+ ...workspaceIdProp,
216
+ payload: { type: 'object', description: 'Action-specific payload' },
217
+ },
218
+ additionalProperties: true,
219
+ }
220
+ }
@@ -0,0 +1,36 @@
1
+ import { buildCapabilities, capabilitiesToActionsSchema } from './build-capabilities.js'
2
+ import { taskIdFromActionId } from '@ossy/schema'
3
+
4
+ /**
5
+ * @param {Array<{ id: string, access?: string, package?: string }>} actions
6
+ * @param {{ schemas?: object[], tasks?: object[], taskCatalog?: object[], taskGraphEdges?: object[], generatedAt?: string }} [options]
7
+ * @returns {{ version: number, generatedAt: string, actions: Array<object> }}
8
+ */
9
+ export function buildActionsSchema (actions = [], options = {}) {
10
+ const capabilities = buildCapabilities(
11
+ {
12
+ actions,
13
+ tasks: options.tasks || actions.flatMap(a => {
14
+ try {
15
+ return [{ id: taskIdFromActionId(a.id) }]
16
+ } catch {
17
+ return []
18
+ }
19
+ }),
20
+ schemas: options.schemas || [],
21
+ taskCatalog: options.taskCatalog || [],
22
+ taskGraphEdges: options.taskGraphEdges || [],
23
+ },
24
+ { generatedAt: options.generatedAt },
25
+ )
26
+ return capabilitiesToActionsSchema(capabilities, actions)
27
+ }
28
+
29
+ export {
30
+ buildCapabilities,
31
+ buildCapabilitiesGraph,
32
+ capabilitiesTaskTopologyResource,
33
+ capabilitiesToActionsSchema,
34
+ projectTaskForCapabilities,
35
+ TASK_TOPOLOGY_RESOURCE_URI,
36
+ } from './build-capabilities.js'
@@ -0,0 +1,190 @@
1
+ import { actionIdToToolName, humanizeActionId } from './action-id-to-tool-name.js'
2
+ import { buildActionInputSchema } from './build-action-input-schema.js'
3
+ import { taskIdFromActionId } from '@ossy/schema'
4
+
5
+ const MCP_ACCESS = new Set(['workspace', 'public'])
6
+
7
+ /** MCP resource URI for read-only task topology (tasks + graph edges). */
8
+ export const TASK_TOPOLOGY_RESOURCE_URI = 'ossy://capabilities/task-topology'
9
+
10
+ /**
11
+ * @param {object} task
12
+ */
13
+ export function projectTaskForCapabilities (task) {
14
+ return {
15
+ id: task.id,
16
+ package: task.package ?? null,
17
+ moduleId: task.moduleId ?? null,
18
+ triggers: task.triggers ?? [],
19
+ schedule: task.schedule ?? null,
20
+ inputSchemaIds: task.inputSchemaIds ?? [],
21
+ actionIds: task.actionIds ?? [],
22
+ primaryActionId: task.primaryActionId ?? null,
23
+ outputs: task.outputs ?? [],
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Structured trigger → task edges for agent planning (ADR 0011).
29
+ *
30
+ * @param {object[]} taskCatalog
31
+ */
32
+ export function buildCapabilitiesGraph (taskCatalog = []) {
33
+ /** @type {Array<{ kind: string, from: object, to: { taskId: string }, label?: string }>} */
34
+ const edges = []
35
+
36
+ for (const task of taskCatalog) {
37
+ for (const trigger of task.triggers ?? []) {
38
+ if (trigger.kind === 'on_action' && trigger.action) {
39
+ edges.push({
40
+ kind: 'on_action',
41
+ from: { kind: 'action', actionId: trigger.action },
42
+ to: { taskId: task.id },
43
+ label: trigger.action,
44
+ })
45
+ }
46
+ if (trigger.kind === 'on_event' && trigger.type) {
47
+ edges.push({
48
+ kind: 'on_event',
49
+ from: {
50
+ kind: 'trigger',
51
+ type: trigger.type,
52
+ ...(trigger.event ? { event: trigger.event } : {}),
53
+ },
54
+ to: { taskId: task.id },
55
+ label: [trigger.type, trigger.event].filter(Boolean).join(' · '),
56
+ })
57
+ }
58
+ if (trigger.kind === 'on_schedule' && trigger.schedule) {
59
+ edges.push({
60
+ kind: 'on_schedule',
61
+ from: { kind: 'schedule', schedule: trigger.schedule },
62
+ to: { taskId: task.id },
63
+ label: trigger.schedule,
64
+ })
65
+ }
66
+ }
67
+ if (task.primaryActionId) {
68
+ edges.push({
69
+ kind: 'primary',
70
+ from: { kind: 'action', actionId: task.primaryActionId },
71
+ to: { taskId: task.id },
72
+ label: 'invoke',
73
+ })
74
+ }
75
+ }
76
+
77
+ return { edges }
78
+ }
79
+
80
+ /**
81
+ * Build agent capabilities (MCP tools + resource template catalog) from manifest data.
82
+ *
83
+ * @param {{
84
+ * actions?: Array<{ id: string, access?: string, package?: string }>,
85
+ * tasks?: Array<{ id: string }>,
86
+ * schemas?: Array<object>,
87
+ * taskCatalog?: object[],
88
+ * taskGraphEdges?: object[],
89
+ * }} manifest
90
+ * @param {{ generatedAt?: string }} [options]
91
+ */
92
+ export function buildCapabilities (manifest, options = {}) {
93
+ const actions = manifest.actions || []
94
+ const tasks = manifest.tasks || []
95
+ const schemas = manifest.schemas || []
96
+ const taskCatalog = manifest.taskCatalog || []
97
+ const taskIds = new Set(tasks.map(t => t.id))
98
+
99
+ const tools = actions
100
+ .filter(action => {
101
+ try {
102
+ return taskIds.has(taskIdFromActionId(action.id))
103
+ } catch {
104
+ return false
105
+ }
106
+ })
107
+ .filter(action => MCP_ACCESS.has(action.access || 'authenticated'))
108
+ .map(action => ({
109
+ actionId: action.id,
110
+ name: actionIdToToolName(action.id),
111
+ title: humanizeActionId(action.id),
112
+ description: `Invoke ${action.id}`,
113
+ access: action.access || 'authenticated',
114
+ package: action.package,
115
+ channels: ['sdk', 'api', 'mcp'],
116
+ inputSchema: buildActionInputSchema(action.id, { schemas }),
117
+ }))
118
+ .sort((a, b) => a.actionId.localeCompare(b.actionId))
119
+
120
+ const projectedTasks = taskCatalog.map(projectTaskForCapabilities)
121
+
122
+ return {
123
+ version: 2,
124
+ generatedAt: options.generatedAt || new Date().toISOString(),
125
+ tools,
126
+ schemas: schemas.map(t => ({
127
+ id: t.id,
128
+ name: t.name,
129
+ package: t.package,
130
+ fields: t.fields,
131
+ })),
132
+ tasks: projectedTasks,
133
+ graph: buildCapabilitiesGraph(taskCatalog),
134
+ /** Legacy string-edge projection kept for UI parity — prefer `graph.edges`. */
135
+ taskGraphEdges: manifest.taskGraphEdges ?? [],
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Payload exposed via MCP resource {@link TASK_TOPOLOGY_RESOURCE_URI}.
141
+ *
142
+ * @param {ReturnType<typeof buildCapabilities>} capabilities
143
+ */
144
+ export function capabilitiesTaskTopologyResource (capabilities) {
145
+ return {
146
+ version: capabilities.version,
147
+ generatedAt: capabilities.generatedAt,
148
+ tasks: capabilities.tasks ?? [],
149
+ graph: capabilities.graph ?? { edges: [] },
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Legacy actions.schema.json shape for package detail / docs.
155
+ *
156
+ * @param {ReturnType<typeof buildCapabilities>} capabilities
157
+ * @param {Array<{ id: string, access?: string, package?: string }>} [allActions]
158
+ */
159
+ export function capabilitiesToActionsSchema (capabilities, allActions) {
160
+ const toolById = Object.fromEntries(capabilities.tools.map(t => [t.actionId, t]))
161
+ const actions = (allActions || capabilities.tools.map(t => ({
162
+ id: t.actionId,
163
+ access: t.access,
164
+ package: t.package,
165
+ })))
166
+
167
+ return {
168
+ version: capabilities.version,
169
+ generatedAt: capabilities.generatedAt,
170
+ actions: actions.map(action => {
171
+ const tool = toolById[action.id]
172
+ const inputSchema = tool?.inputSchema
173
+ ?? buildActionInputSchema(action.id, {
174
+ schemas: capabilities.schemas,
175
+ })
176
+ const hasKnownEnvelope = inputSchema?.properties && !inputSchema.properties.payload
177
+
178
+ return {
179
+ id: action.id,
180
+ access: action.access ?? 'authenticated',
181
+ package: action.package,
182
+ channels: tool ? tool.channels : ['sdk', 'api'],
183
+ ...(tool?.title ? { title: tool.title } : {}),
184
+ ...(tool?.description ? { description: tool.description } : {}),
185
+ ...(tool?.name ? { mcpTool: tool.name } : {}),
186
+ ...(hasKnownEnvelope ? { inputSchema } : {}),
187
+ }
188
+ }).sort((a, b) => a.id.localeCompare(b.id)),
189
+ }
190
+ }
@@ -32,7 +32,7 @@ export function buildManifestSummary (manifest) {
32
32
  apis: [],
33
33
  actions: [],
34
34
  components: [],
35
- resourceTemplates: [],
35
+ schemas: [],
36
36
  tasks: [],
37
37
  integrations: [],
38
38
  emails: [],
@@ -69,15 +69,21 @@ export function buildManifestSummary (manifest) {
69
69
  add(component.package, 'components', { id: component.id })
70
70
  }
71
71
 
72
- for (const template of manifest.resourceTemplates || []) {
73
- add(template.package, 'resourceTemplates', {
72
+ for (const template of manifest.schemas || []) {
73
+ add(template.package, 'schemas', {
74
74
  id: template.id,
75
75
  ...(template.title ? { title: template.title } : {}),
76
76
  })
77
77
  }
78
78
 
79
- for (const task of manifest.tasks || []) {
80
- add(task.package, 'tasks', { id: task.id })
79
+ for (const task of manifest.taskCatalog || manifest.tasks || []) {
80
+ add(task.package, 'tasks', {
81
+ id: task.id,
82
+ ...(task.moduleId ? { moduleId: task.moduleId } : {}),
83
+ ...(task.triggers ? { triggers: task.triggers } : {}),
84
+ ...(task.inputSchemaIds ? { inputSchemaIds: task.inputSchemaIds } : {}),
85
+ ...(task.primaryActionId ? { primaryActionId: task.primaryActionId } : {}),
86
+ })
81
87
  }
82
88
 
83
89
  for (const integration of manifest.integrations || []) {
@@ -0,0 +1 @@
1
+ export { extractTaskCatalogEntry, buildTaskCatalogEdges } from '@ossy/schema'
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Map resource template field types to JSON Schema (draft-07 subset).
3
+ *
4
+ * @param {{ name: string, type: string, options?: string[] }} field
5
+ * @returns {object}
6
+ */
7
+ export function fieldToJsonSchema (field) {
8
+ const type = field?.type?.trim?.() || 'text'
9
+
10
+ switch (type) {
11
+ case 'number':
12
+ case 'timestamp':
13
+ return { type: 'number', description: `${field.name} (Unix ms for timestamp)` }
14
+ case 'boolean':
15
+ return { type: 'boolean' }
16
+ case 'select':
17
+ return {
18
+ type: 'string',
19
+ ...(Array.isArray(field.options) && field.options.length
20
+ ? { enum: field.options }
21
+ : {}),
22
+ }
23
+ case 'multiselect':
24
+ return {
25
+ type: 'array',
26
+ items: {
27
+ type: 'string',
28
+ ...(Array.isArray(field.options) && field.options.length
29
+ ? { enum: field.options }
30
+ : {}),
31
+ },
32
+ }
33
+ case 'file':
34
+ case 'image':
35
+ return {
36
+ type: 'object',
37
+ properties: { resourceId: { type: 'string' } },
38
+ required: ['resourceId'],
39
+ description: 'Reference to an uploaded binary resource',
40
+ }
41
+ case 'date-range':
42
+ return {
43
+ type: 'object',
44
+ properties: {
45
+ start: { type: 'number' },
46
+ end: { type: 'number' },
47
+ },
48
+ required: ['start', 'end'],
49
+ }
50
+ case 'date-ranges':
51
+ return {
52
+ type: 'array',
53
+ items: {
54
+ type: 'object',
55
+ properties: {
56
+ start: { type: 'number' },
57
+ end: { type: 'number' },
58
+ },
59
+ required: ['start', 'end'],
60
+ },
61
+ }
62
+ case 'email':
63
+ return { type: 'string', format: 'email' }
64
+ case 'textarea':
65
+ case 'richtext':
66
+ case 'text':
67
+ case 'date':
68
+ default:
69
+ return { type: 'string' }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @param {{ id: string, name?: string, fields?: Array<object> }} template
75
+ * @returns {object}
76
+ */
77
+ export function templateToJsonSchema (template) {
78
+ const properties = {}
79
+ for (const field of template.fields || []) {
80
+ if (!field?.name) continue
81
+ properties[field.name] = fieldToJsonSchema(field)
82
+ }
83
+
84
+ return {
85
+ title: template.id,
86
+ type: 'object',
87
+ properties,
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @param {Array<{ id: string, fields?: Array<object> }>} templates
93
+ * @returns {object | undefined}
94
+ */
95
+ export function templatesContentOneOf (templates) {
96
+ const branches = (templates || [])
97
+ .filter(t => t?.id && Array.isArray(t.fields))
98
+ .map(templateToJsonSchema)
99
+
100
+ if (!branches.length) return undefined
101
+
102
+ return { oneOf: branches }
103
+ }
package/src/shell/App.jsx CHANGED
@@ -1,7 +1,8 @@
1
1
  import React from 'react'
2
2
  import { SDK } from '@ossy/sdk'
3
3
  import { WorkspaceProvider } from '@ossy/sdk-react'
4
- import { Theme, ComponentSlotsProvider, LocaleProvider } from '@ossy/design-system'
4
+ import { Theme, ComponentSlotsProvider, LocaleProvider, DEFAULT_FORM_FIELD_SLOTS } from '@ossy/design-system'
5
+ import { SchemasReadBootstrap } from '@ossy/workspaces/SchemasReadBootstrap'
5
6
  import { ThemeEditor } from './ThemeEditor.jsx'
6
7
  import { defaultAppSettings } from './AppSettings.jsx'
7
8
  import { Router } from '@ossy/router-react'
@@ -16,7 +17,8 @@ export const App = ({ children, language, messages, fallbackMessages, ..._appSet
16
17
 
17
18
  const sdk = SDK.of({
18
19
  apiUrl: appSettings.apiUrl,
19
- workspaceId: appSettings.workspaceId
20
+ workspaceId: appSettings.workspaceId,
21
+ actionRoutes: appSettings.actionRoutes,
20
22
  })
21
23
 
22
24
  return (
@@ -28,9 +30,10 @@ export const App = ({ children, language, messages, fallbackMessages, ..._appSet
28
30
  supportedLanguages={appSettings.supportedLanguages}
29
31
  >
30
32
  <AppContext.Provider value={contextSettings}>
31
- <ComponentSlotsProvider slots={components || {}}>
33
+ <ComponentSlotsProvider slots={{ ...DEFAULT_FORM_FIELD_SLOTS, ...(components || {}) }}>
32
34
  <Theme theme={appSettings.theme} themes={appSettings.themes}>
33
35
  <WorkspaceProvider sdk={sdk}>
36
+ <SchemasReadBootstrap schemas={appSettings.schemas} />
34
37
  <Router {...appSettings} pages={appSettings.pages || []}>
35
38
  {children}
36
39
  {appSettings.devMode && <ThemeEditor />}
@@ -25,7 +25,18 @@ export function defaultAppSettings() {
25
25
  faviconHref: undefined,
26
26
  /** When true, main app sidebar is collapsed to icons (from server cookie / app-settings). */
27
27
  sidebarPrimaryCollapsed: false,
28
+ /** Cached workspace shell snapshot (from user-app-settings cookie; refreshed client-side). */
29
+ workspaceName: undefined,
30
+ workspaceServices: undefined,
31
+ workspaces: undefined,
28
32
  /** Grouped manifest entries by npm package — for dev tooling. */
29
33
  manifestSummary: undefined,
34
+ /** Merged system + workspace schemas (SSR bootstrap for client validation). */
35
+ schemas: undefined,
36
+ /** Task catalog from `app build` (ADR 0011) — for automation topology UI. */
37
+ taskCatalog: undefined,
38
+ taskGraphEdges: undefined,
39
+ /** Action id → HTTP route map for sdk.invoke transport routing. */
40
+ actionRoutes: undefined,
30
41
  }
31
42
  }
@@ -57,36 +57,40 @@ export const DevPagesPanel = () => {
57
57
 
58
58
  return (
59
59
  <View gap="m">
60
- <Text variant="title-tertiary">Dev</Text>
60
+ <Text variant="heading-tertiary" text="design-system.dev.pagesPanel.title" />
61
61
 
62
62
  <View gap="xs" style={currentPageStyles}>
63
- <Text variant="title-tertiary" style={{ marginBottom: 'var(--space-xs)' }}>
64
- Current page
65
- </Text>
63
+ <Text
64
+ variant="heading-tertiary"
65
+ text="design-system.dev.pagesPanel.currentPage"
66
+ style={{ marginBottom: 'var(--space-xs)' }}
67
+ />
66
68
  <Text style={monoStyle}>
67
- <strong>id:</strong> {app?.pageId || '—'}
69
+ <strong><Text as="span" text="design-system.dev.pagesPanel.id" />:</strong> {app?.pageId || '—'}
68
70
  </Text>
69
71
  <Text style={monoStyle}>
70
- <strong>path:</strong> {currentPathLabel}
72
+ <strong><Text as="span" text="design-system.dev.pagesPanel.path" />:</strong> {currentPathLabel}
71
73
  </Text>
72
74
  <Text style={monoStyle}>
73
- <strong>url:</strong> {currentUrl}
75
+ <strong><Text as="span" text="design-system.dev.pagesPanel.url" />:</strong> {currentUrl}
74
76
  </Text>
75
77
  {packageDetailHref && currentPage?.package && (
76
78
  <Button
77
79
  variant="link"
78
80
  href={packageDetailHref}
81
+ label="design-system.dev.pagesPanel.viewPackage"
82
+ params={{ package: currentPage.package }}
79
83
  style={{ alignSelf: 'flex-start', padding: 0, height: 'auto' }}
80
- >
81
- View package ({currentPage.package})
82
- </Button>
84
+ />
83
85
  )}
84
86
  </View>
85
87
 
86
88
  <View gap="s">
87
- <Text variant="title-tertiary">
88
- Pages ({pages.length})
89
- </Text>
89
+ <Text
90
+ variant="heading-tertiary"
91
+ text="design-system.dev.pagesPanel.pages"
92
+ params={{ count: pages.length }}
93
+ />
90
94
 
91
95
  {groups.map(({ key, label, pages: groupPages }) => (
92
96
  <View key={key} gap="xs">