@fayz-ai/core 0.1.6
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/app/render.d.ts +16 -0
- package/dist/app/render.d.ts.map +1 -0
- package/dist/blocks/index.d.ts +25 -0
- package/dist/blocks/index.d.ts.map +1 -0
- package/dist/chunk-2NTHNWEH.cjs +32 -0
- package/dist/chunk-2NTHNWEH.cjs.map +1 -0
- package/dist/chunk-5EAZOMVP.js +1609 -0
- package/dist/chunk-5EAZOMVP.js.map +1 -0
- package/dist/chunk-5TKIZVVJ.cjs +16 -0
- package/dist/chunk-5TKIZVVJ.cjs.map +1 -0
- package/dist/chunk-7RTUHSKY.js +3 -0
- package/dist/chunk-7RTUHSKY.js.map +1 -0
- package/dist/chunk-AXTWKF5X.cjs +38 -0
- package/dist/chunk-AXTWKF5X.cjs.map +1 -0
- package/dist/chunk-EGWWQ7AT.cjs +355 -0
- package/dist/chunk-EGWWQ7AT.cjs.map +1 -0
- package/dist/chunk-KAYQFSR6.js +298 -0
- package/dist/chunk-KAYQFSR6.js.map +1 -0
- package/dist/chunk-LSVBDYFQ.cjs +1639 -0
- package/dist/chunk-LSVBDYFQ.cjs.map +1 -0
- package/dist/chunk-SNQW6TRF.cjs +658 -0
- package/dist/chunk-SNQW6TRF.cjs.map +1 -0
- package/dist/chunk-WKZVSKEU.js +26 -0
- package/dist/chunk-WKZVSKEU.js.map +1 -0
- package/dist/chunk-Y2C2FBQP.js +35 -0
- package/dist/chunk-Y2C2FBQP.js.map +1 -0
- package/dist/chunk-YSBXGHAM.js +643 -0
- package/dist/chunk-YSBXGHAM.js.map +1 -0
- package/dist/data/archetype.d.ts +13 -0
- package/dist/data/archetype.d.ts.map +1 -0
- package/dist/data/cached.d.ts +19 -0
- package/dist/data/cached.d.ts.map +1 -0
- package/dist/data/fayz-api.d.ts +19 -0
- package/dist/data/fayz-api.d.ts.map +1 -0
- package/dist/data/index.cjs +32 -0
- package/dist/data/index.cjs.map +1 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +3 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/mock.d.ts +7 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/resolve.d.ts +22 -0
- package/dist/data/resolve.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +18 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +23 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/entity/index.cjs +28 -0
- package/dist/entity/index.cjs.map +1 -0
- package/dist/entity/index.d.ts +3 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/index.js +3 -0
- package/dist/entity/index.js.map +1 -0
- package/dist/entity/registry.d.ts +20 -0
- package/dist/entity/registry.d.ts.map +1 -0
- package/dist/events/index.d.ts +16 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/i18n/index.cjs +44 -0
- package/dist/i18n/index.cjs.map +1 -0
- package/dist/i18n/index.d.ts +21 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/shell-translations.d.ts +2 -0
- package/dist/i18n/shell-translations.d.ts.map +1 -0
- package/dist/index.cjs +911 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +596 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cache.d.ts +21 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/csv.d.ts +14 -0
- package/dist/lib/csv.d.ts.map +1 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/manifest/index.d.ts +80 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/plugin/createSafeDataProvider.d.ts +14 -0
- package/dist/plugin/createSafeDataProvider.d.ts.map +1 -0
- package/dist/plugin/index.cjs +32 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.ts +2 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +3 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/runtime.d.ts +31 -0
- package/dist/plugin/runtime.d.ts.map +1 -0
- package/dist/registry/index.d.ts +88 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/router/index.cjs +16 -0
- package/dist/router/index.cjs.map +1 -0
- package/dist/router/index.d.ts +8 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +3 -0
- package/dist/router/index.js.map +1 -0
- package/dist/runtime/index.cjs +16 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/oauth.d.ts +96 -0
- package/dist/runtime/oauth.d.ts.map +1 -0
- package/dist/tenant/index.d.ts +3 -0
- package/dist/tenant/index.d.ts.map +1 -0
- package/dist/types/auth.d.ts +27 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/billing.d.ts +17 -0
- package/dist/types/billing.d.ts.map +1 -0
- package/dist/types/crud.d.ts +107 -0
- package/dist/types/crud.d.ts.map +1 -0
- package/dist/types/entities.d.ts +134 -0
- package/dist/types/entities.d.ts.map +1 -0
- package/dist/types/i18n.d.ts +8 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/index.cjs +4 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/org.d.ts +96 -0
- package/dist/types/org.d.ts.map +1 -0
- package/dist/types/permissions.d.ts +25 -0
- package/dist/types/permissions.d.ts.map +1 -0
- package/dist/types/plugins.d.ts +314 -0
- package/dist/types/plugins.d.ts.map +1 -0
- package/dist/types/theme.d.ts +16 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/app/render.tsx +69 -0
- package/src/blocks/index.tsx +68 -0
- package/src/data/archetype.ts +210 -0
- package/src/data/cached.ts +55 -0
- package/src/data/fayz-api.ts +200 -0
- package/src/data/index.ts +8 -0
- package/src/data/mock.ts +84 -0
- package/src/data/resolve.ts +108 -0
- package/src/data/supabase.ts +169 -0
- package/src/data/types.ts +20 -0
- package/src/entity/index.ts +2 -0
- package/src/entity/registry.ts +42 -0
- package/src/events/index.tsx +82 -0
- package/src/i18n/index.ts +250 -0
- package/src/i18n/shell-translations.ts +1391 -0
- package/src/index.ts +132 -0
- package/src/lib/cache.ts +118 -0
- package/src/lib/csv.ts +54 -0
- package/src/lib/format.ts +58 -0
- package/src/manifest/app-manifest.schema.json +103 -0
- package/src/manifest/index.ts +394 -0
- package/src/plugin/createSafeDataProvider.ts +30 -0
- package/src/plugin/index.ts +1 -0
- package/src/plugin/runtime.ts +313 -0
- package/src/registry/index.ts +163 -0
- package/src/router/index.ts +29 -0
- package/src/runtime/index.ts +15 -0
- package/src/runtime/oauth.ts +219 -0
- package/src/tenant/index.ts +18 -0
- package/src/types/auth.ts +26 -0
- package/src/types/billing.ts +17 -0
- package/src/types/crud.ts +108 -0
- package/src/types/entities.ts +142 -0
- package/src/types/i18n.ts +7 -0
- package/src/types/index.ts +10 -0
- package/src/types/org.ts +94 -0
- package/src/types/permissions.ts +32 -0
- package/src/types/plugins.ts +363 -0
- package/src/types/theme.ts +17 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { DataProvider, CrudQuery, CrudResult } from './types'
|
|
2
|
+
|
|
3
|
+
export interface SupabaseProviderConfig {
|
|
4
|
+
schema?: string
|
|
5
|
+
tenantId?: string | (() => string | undefined)
|
|
6
|
+
tenantIdColumn?: string
|
|
7
|
+
searchColumns?: string[]
|
|
8
|
+
selectColumns?: string
|
|
9
|
+
columnMap?: Record<string, string>
|
|
10
|
+
filters?: Record<string, string>
|
|
11
|
+
defaults?: Record<string, unknown>
|
|
12
|
+
supabaseClient?: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function camelToSnake(str: string): string {
|
|
16
|
+
return str.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function snakeToCamel(str: string): string {
|
|
20
|
+
return str.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mapRow<T>(row: Record<string, unknown>, columnMap?: Record<string, string>): T {
|
|
24
|
+
const result: Record<string, unknown> = {}
|
|
25
|
+
for (const [key, value] of Object.entries(row)) {
|
|
26
|
+
const camelKey = columnMap?.[key] ?? snakeToCamel(key)
|
|
27
|
+
result[camelKey] = value
|
|
28
|
+
}
|
|
29
|
+
return result as T
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mapEntity(entity: Record<string, unknown>, columnMap?: Record<string, string>): Record<string, unknown> {
|
|
33
|
+
const reverseMap: Record<string, string> = {}
|
|
34
|
+
if (columnMap) {
|
|
35
|
+
for (const [snake, camel] of Object.entries(columnMap)) {
|
|
36
|
+
reverseMap[camel] = snake
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const result: Record<string, unknown> = {}
|
|
40
|
+
for (const [key, value] of Object.entries(entity)) {
|
|
41
|
+
if (key === 'id' || key === 'createdAt' || key === 'updatedAt' || key === 'tenantId') continue
|
|
42
|
+
const snakeKey = reverseMap[key] ?? camelToSnake(key)
|
|
43
|
+
result[snakeKey] = value
|
|
44
|
+
}
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Global Supabase client registry — set once via createFayzApp
|
|
49
|
+
let _globalSupabaseClient: unknown | null = null
|
|
50
|
+
|
|
51
|
+
export function setGlobalSupabaseClient(client: unknown): void {
|
|
52
|
+
_globalSupabaseClient = client
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getSupabaseClientOptional(): unknown | null {
|
|
56
|
+
return _globalSupabaseClient
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createSupabaseProvider<T extends { id: string }>(
|
|
60
|
+
table: string,
|
|
61
|
+
config?: SupabaseProviderConfig,
|
|
62
|
+
): DataProvider<T> {
|
|
63
|
+
const schema = config?.schema ?? 'public'
|
|
64
|
+
const tenantIdCol = config?.tenantIdColumn ?? 'tenant_id'
|
|
65
|
+
const searchCols = config?.searchColumns ?? []
|
|
66
|
+
const selectCols = config?.selectColumns ?? '*'
|
|
67
|
+
const columnMap = config?.columnMap
|
|
68
|
+
|
|
69
|
+
function resolveTenantId(): string | undefined {
|
|
70
|
+
if (!config?.tenantId) return undefined
|
|
71
|
+
return typeof config.tenantId === 'function' ? config.tenantId() : config.tenantId
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getClient() {
|
|
75
|
+
const supabase = (config?.supabaseClient ?? _globalSupabaseClient) as {
|
|
76
|
+
schema: (s: string) => unknown
|
|
77
|
+
from: (t: string) => unknown
|
|
78
|
+
} | null
|
|
79
|
+
if (!supabase) throw new Error(`[@fayz-ai/core] Supabase client not available. Call createFayzApp with a provider first.`)
|
|
80
|
+
return schema === 'public' ? (supabase as { from: (t: string) => unknown }) : (supabase.schema(schema) as { from: (t: string) => unknown })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
async list(query: CrudQuery): Promise<CrudResult<T>> {
|
|
85
|
+
let q = (getClient().from(table) as { select: (...args: unknown[]) => unknown })
|
|
86
|
+
.select(selectCols, { count: 'exact' }) as Record<string, unknown>
|
|
87
|
+
|
|
88
|
+
const tenantId = resolveTenantId()
|
|
89
|
+
if (tenantId) q = (q as { eq: (col: string, val: string) => unknown }).eq(tenantIdCol, tenantId) as Record<string, unknown>
|
|
90
|
+
|
|
91
|
+
if (config?.filters) {
|
|
92
|
+
for (const [col, val] of Object.entries(config.filters)) {
|
|
93
|
+
q = (q as { eq: (c: string, v: string) => unknown }).eq(col, val) as Record<string, unknown>
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Faceted / dynamic filters from the query (equality, mapped to columns).
|
|
98
|
+
if (query.filters) {
|
|
99
|
+
for (const [key, val] of Object.entries(query.filters)) {
|
|
100
|
+
if (val == null || val === '') continue
|
|
101
|
+
const col = columnMap?.[key] ?? camelToSnake(key)
|
|
102
|
+
q = (q as { eq: (c: string, v: unknown) => unknown }).eq(col, val) as Record<string, unknown>
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (query.search && searchCols.length > 0) {
|
|
107
|
+
const term = `%${query.search}%`
|
|
108
|
+
const orClause = searchCols.map((col) => `${camelToSnake(col)}.ilike.${term}`).join(',')
|
|
109
|
+
q = (q as { or: (c: string) => unknown }).or(orClause) as Record<string, unknown>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (query.sortBy) {
|
|
113
|
+
q = (q as { order: (col: string, opts: unknown) => unknown })
|
|
114
|
+
.order(camelToSnake(query.sortBy), { ascending: query.sortDir !== 'desc' }) as Record<string, unknown>
|
|
115
|
+
} else {
|
|
116
|
+
q = (q as { order: (col: string, opts: unknown) => unknown })
|
|
117
|
+
.order('created_at', { ascending: false }) as Record<string, unknown>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const page = query.page ?? 1
|
|
121
|
+
const pageSize = query.pageSize ?? 50
|
|
122
|
+
const from = (page - 1) * pageSize
|
|
123
|
+
q = (q as { range: (f: number, t: number) => unknown }).range(from, from + pageSize - 1) as Record<string, unknown>
|
|
124
|
+
|
|
125
|
+
const { data, error, count } = await q as { data: Record<string, unknown>[] | null; error: unknown; count: number | null }
|
|
126
|
+
if (error) throw error
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
data: (data ?? []).map((row) => mapRow<T>(row, columnMap)),
|
|
130
|
+
total: count ?? 0,
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async create(data) {
|
|
135
|
+
const row = mapEntity(data as Record<string, unknown>, columnMap)
|
|
136
|
+
const tenantId = resolveTenantId()
|
|
137
|
+
if (tenantId) row[tenantIdCol] = tenantId
|
|
138
|
+
if (config?.defaults) {
|
|
139
|
+
for (const [col, val] of Object.entries(config.defaults)) {
|
|
140
|
+
if (row[col] === undefined) row[col] = val
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { data: created, error } = (await (getClient().from(table) as {
|
|
145
|
+
insert: (r: unknown) => { select: () => { single: () => Promise<{ data: unknown; error: unknown }> } }
|
|
146
|
+
}).insert(row).select().single()) as { data: Record<string, unknown> | null; error: unknown }
|
|
147
|
+
|
|
148
|
+
if (error) throw error
|
|
149
|
+
return mapRow<T>(created!, columnMap)
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async update(id, data) {
|
|
153
|
+
const row = mapEntity(data as Record<string, unknown>, columnMap)
|
|
154
|
+
const { data: updated, error } = (await (getClient().from(table) as {
|
|
155
|
+
update: (r: unknown) => { eq: (c: string, v: string) => { select: () => { single: () => Promise<{ data: unknown; error: unknown }> } } }
|
|
156
|
+
}).update(row).eq('id', id).select().single()) as { data: Record<string, unknown> | null; error: unknown }
|
|
157
|
+
|
|
158
|
+
if (error) throw error
|
|
159
|
+
return mapRow<T>(updated!, columnMap)
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async remove(id) {
|
|
163
|
+
const { error } = (await (getClient().from(table) as {
|
|
164
|
+
delete: () => { eq: (c: string, v: string) => Promise<{ error: unknown }> }
|
|
165
|
+
}).delete().eq('id', id)) as { error: unknown }
|
|
166
|
+
if (error) throw error
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CrudQuery {
|
|
2
|
+
search?: string
|
|
3
|
+
sortBy?: string
|
|
4
|
+
sortDir?: 'asc' | 'desc'
|
|
5
|
+
page?: number
|
|
6
|
+
pageSize?: number
|
|
7
|
+
filters?: Record<string, unknown>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CrudResult<T> {
|
|
11
|
+
data: T[]
|
|
12
|
+
total: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DataProvider<T extends { id: string } = { id: string }> {
|
|
16
|
+
list(query: CrudQuery): Promise<CrudResult<T>>
|
|
17
|
+
create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'>): Promise<T>
|
|
18
|
+
update(id: string, data: Partial<T>): Promise<T>
|
|
19
|
+
remove(id: string): Promise<void>
|
|
20
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { EntityDef } from '../types/crud'
|
|
2
|
+
import type { EntityArchetype } from '../types/entities'
|
|
3
|
+
|
|
4
|
+
export interface RegisteredEntity {
|
|
5
|
+
entityKey: string
|
|
6
|
+
label: string
|
|
7
|
+
labelPlural: string
|
|
8
|
+
icon?: string
|
|
9
|
+
fields: EntityDef['fields']
|
|
10
|
+
source: 'app' | 'plugin'
|
|
11
|
+
archetype?: EntityArchetype
|
|
12
|
+
pluginId?: string
|
|
13
|
+
pluginName?: string
|
|
14
|
+
pluginIcon?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const entityRegistry = new Map<string, RegisteredEntity>()
|
|
18
|
+
|
|
19
|
+
export function registerEntity(entry: RegisteredEntity): void {
|
|
20
|
+
entityRegistry.set(entry.entityKey, entry)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getEntityByKey(key: string): RegisteredEntity | undefined {
|
|
24
|
+
return entityRegistry.get(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAllEntities(): RegisteredEntity[] {
|
|
28
|
+
return Array.from(entityRegistry.values())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function clearEntityRegistry(): void {
|
|
32
|
+
entityRegistry.clear()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function deriveEntityKey(entityDef: EntityDef): string {
|
|
36
|
+
const name = entityDef.name.toLowerCase().replace(/\s+/g, '-')
|
|
37
|
+
if (entityDef.data?.archetype && entityDef.data?.archetypeKind) {
|
|
38
|
+
return `${entityDef.data.archetype}:${entityDef.data.archetypeKind}`
|
|
39
|
+
}
|
|
40
|
+
if (entityDef.data?.archetype) return entityDef.data.archetype
|
|
41
|
+
return name
|
|
42
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Event bus — the substrate for cross-plugin behavior. Replaces the bespoke
|
|
5
|
+
// `window.dispatchEvent('agenda:open-booking')` hack and the imperative
|
|
6
|
+
// financial bridge: plugins emit named events, others subscribe, and the
|
|
7
|
+
// AppManifest can declare event→action bindings as data. Plugins describe the
|
|
8
|
+
// events they emit via PluginManifest.events (see types/plugins) so the
|
|
9
|
+
// platform can introspect and wire automations.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export type EventHandler<T = unknown> = (payload: T) => void
|
|
13
|
+
|
|
14
|
+
export interface EventBus {
|
|
15
|
+
emit<T = unknown>(event: string, payload?: T): void
|
|
16
|
+
on<T = unknown>(event: string, handler: EventHandler<T>): () => void
|
|
17
|
+
once<T = unknown>(event: string, handler: EventHandler<T>): () => void
|
|
18
|
+
off<T = unknown>(event: string, handler: EventHandler<T>): void
|
|
19
|
+
clear(event?: string): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createEventBus(): EventBus {
|
|
23
|
+
const handlers = new Map<string, Set<EventHandler>>()
|
|
24
|
+
const bus: EventBus = {
|
|
25
|
+
emit(event, payload) {
|
|
26
|
+
const set = handlers.get(event)
|
|
27
|
+
if (!set) return
|
|
28
|
+
// Snapshot so handlers can subscribe/unsubscribe during dispatch.
|
|
29
|
+
for (const handler of Array.from(set)) {
|
|
30
|
+
try {
|
|
31
|
+
handler(payload)
|
|
32
|
+
} catch (err) {
|
|
33
|
+
// A misbehaving subscriber must not break the emitter or its peers.
|
|
34
|
+
console.error(`[fayz] event handler for "${event}" threw:`, err)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
on(event, handler) {
|
|
39
|
+
let set = handlers.get(event)
|
|
40
|
+
if (!set) {
|
|
41
|
+
set = new Set()
|
|
42
|
+
handlers.set(event, set)
|
|
43
|
+
}
|
|
44
|
+
set.add(handler as EventHandler)
|
|
45
|
+
return () => bus.off(event, handler)
|
|
46
|
+
},
|
|
47
|
+
once(event, handler) {
|
|
48
|
+
const wrapped: EventHandler = (payload) => {
|
|
49
|
+
bus.off(event, wrapped)
|
|
50
|
+
;(handler as EventHandler)(payload)
|
|
51
|
+
}
|
|
52
|
+
return bus.on(event, wrapped)
|
|
53
|
+
},
|
|
54
|
+
off(event, handler) {
|
|
55
|
+
handlers.get(event)?.delete(handler as EventHandler)
|
|
56
|
+
},
|
|
57
|
+
clear(event) {
|
|
58
|
+
if (event) handlers.delete(event)
|
|
59
|
+
else handlers.clear()
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
return bus
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The app-wide default bus. One per app runtime (repo-per-app). */
|
|
66
|
+
export const eventBus: EventBus = createEventBus()
|
|
67
|
+
|
|
68
|
+
/** Subscribe to an event for the lifetime of the component. The latest handler
|
|
69
|
+
* is always invoked, so closures over fresh props/state work without resubscribing. */
|
|
70
|
+
export function useOnEvent<T = unknown>(
|
|
71
|
+
event: string,
|
|
72
|
+
handler: EventHandler<T>,
|
|
73
|
+
deps: React.DependencyList = [],
|
|
74
|
+
): void {
|
|
75
|
+
const ref = React.useRef(handler)
|
|
76
|
+
ref.current = handler
|
|
77
|
+
React.useEffect(
|
|
78
|
+
() => eventBus.on<T>(event, (payload) => ref.current(payload)),
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
[event, ...deps],
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { shellTranslations } from './shell-translations'
|
|
3
|
+
|
|
4
|
+
export interface I18nConfig {
|
|
5
|
+
defaultLocale: string
|
|
6
|
+
supported: string[]
|
|
7
|
+
translations: Record<string, Record<string, string>>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const I18nContext = React.createContext<I18nConfig>({
|
|
11
|
+
defaultLocale: 'en',
|
|
12
|
+
supported: ['en'],
|
|
13
|
+
translations: {},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const I18nProvider = I18nContext.Provider
|
|
17
|
+
|
|
18
|
+
export function useI18nConfig(): I18nConfig {
|
|
19
|
+
return React.useContext(I18nContext)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useTranslation(): (key: string, params?: Record<string, string | number>) => string {
|
|
23
|
+
const config = React.useContext(I18nContext)
|
|
24
|
+
const localeStore = useLocaleStore()
|
|
25
|
+
const locale = localeStore.locale
|
|
26
|
+
|
|
27
|
+
return React.useCallback(
|
|
28
|
+
(key: string, params?: Record<string, string | number>): string => {
|
|
29
|
+
const localizedTranslations = config.translations[locale] ?? {}
|
|
30
|
+
const fallback = config.translations[config.defaultLocale] ?? {}
|
|
31
|
+
// Global fallback lets plugins register their locales independently of any
|
|
32
|
+
// I18nProvider — needed when a de-bridged plugin runs under a host shell
|
|
33
|
+
// that doesn't mount @fayz-ai/core's I18nProvider (incremental de-bridge), and
|
|
34
|
+
// for manifest apps where plugins self-register translations.
|
|
35
|
+
const gLocale = _globalTranslations[locale] ?? {}
|
|
36
|
+
const gFallback = _globalTranslations[config.defaultLocale] ?? {}
|
|
37
|
+
let raw = localizedTranslations[key] ?? gLocale[key] ?? fallback[key] ?? gFallback[key] ?? key
|
|
38
|
+
|
|
39
|
+
if (params) {
|
|
40
|
+
for (const [k, v] of Object.entries(params)) {
|
|
41
|
+
raw = raw.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), String(v))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return raw
|
|
45
|
+
},
|
|
46
|
+
[config, locale],
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Minimal locale store (avoids circular dep with stores package)
|
|
51
|
+
interface LocaleStore {
|
|
52
|
+
locale: string
|
|
53
|
+
setLocale: (locale: string) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let _localeStore: LocaleStore = {
|
|
57
|
+
locale: 'en',
|
|
58
|
+
setLocale: (l) => { _localeStore.locale = l },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function setLocaleStore(store: LocaleStore): void {
|
|
62
|
+
_localeStore = store
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function useLocaleStore(): LocaleStore {
|
|
66
|
+
const [, rerender] = React.useReducer((n: number) => n + 1, 0)
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
const prev = _localeStore.setLocale
|
|
69
|
+
_localeStore.setLocale = (l) => {
|
|
70
|
+
prev(l)
|
|
71
|
+
rerender()
|
|
72
|
+
}
|
|
73
|
+
return () => { _localeStore.setLocale = prev }
|
|
74
|
+
}, [])
|
|
75
|
+
return _localeStore
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getCurrentLocale(): string {
|
|
79
|
+
return _localeStore.locale
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function setCurrentLocale(locale: string): void {
|
|
83
|
+
_localeStore.setLocale(locale)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Core built-in translations
|
|
87
|
+
export const coreTranslations: Record<string, Record<string, string>> = {
|
|
88
|
+
en: {
|
|
89
|
+
'common.save': 'Save',
|
|
90
|
+
'common.saveChanges': 'Save Changes',
|
|
91
|
+
'common.saving': 'Saving...',
|
|
92
|
+
'common.cancel': 'Cancel',
|
|
93
|
+
'common.delete': 'Delete',
|
|
94
|
+
'common.edit': 'Edit',
|
|
95
|
+
'common.close': 'Close',
|
|
96
|
+
'common.confirm': 'Confirm',
|
|
97
|
+
'common.discard': 'Discard',
|
|
98
|
+
'common.back': 'Back',
|
|
99
|
+
'common.formIncomplete': 'Fill in the required fields before saving.',
|
|
100
|
+
'saveBar.unsaved': 'Unsaved changes',
|
|
101
|
+
'common.add': 'Add',
|
|
102
|
+
'common.loading': 'Loading...',
|
|
103
|
+
'common.error': 'Something went wrong',
|
|
104
|
+
'common.noResults': 'No results found.',
|
|
105
|
+
'common.search': 'Search',
|
|
106
|
+
'common.actions': 'Actions',
|
|
107
|
+
'common.continue': 'Continue',
|
|
108
|
+
'common.done': 'Done',
|
|
109
|
+
'common.yes': 'Yes',
|
|
110
|
+
'common.no': 'No',
|
|
111
|
+
'common.active': 'Active',
|
|
112
|
+
'common.inactive': 'Inactive',
|
|
113
|
+
'common.overview': 'Overview',
|
|
114
|
+
'common.settings': 'Settings',
|
|
115
|
+
'common.plugins': 'Plugins',
|
|
116
|
+
'common.new': 'New',
|
|
117
|
+
'common.filter': 'Filter',
|
|
118
|
+
'common.export': 'Export',
|
|
119
|
+
'common.import': 'Import',
|
|
120
|
+
'common.preview': 'Preview',
|
|
121
|
+
'common.total': 'Total',
|
|
122
|
+
'crud.newItem': 'New {{name}}',
|
|
123
|
+
'crud.editItem': 'Edit {{name}}',
|
|
124
|
+
'crud.deleteConfirm': 'Delete {{name}}?',
|
|
125
|
+
'crud.deleteMessage': 'This action cannot be undone.',
|
|
126
|
+
'crud.created': '{{name}} created',
|
|
127
|
+
'crud.updated': '{{name}} updated',
|
|
128
|
+
'crud.deleted': '{{name}} deleted',
|
|
129
|
+
'crud.noItems': 'No {{namePlural}} yet.',
|
|
130
|
+
'auth.login.title': 'Sign in',
|
|
131
|
+
'auth.login.subtitle': 'Enter your credentials to access your account',
|
|
132
|
+
'auth.login.emailLabel': 'Email',
|
|
133
|
+
'auth.login.passwordLabel': 'Password',
|
|
134
|
+
'auth.login.submit': 'Sign in',
|
|
135
|
+
'auth.login.forgotPassword': 'Forgot password?',
|
|
136
|
+
'auth.login.noAccount': "Don't have an account?",
|
|
137
|
+
'auth.login.signUpLink': 'Sign up',
|
|
138
|
+
'auth.signup.title': 'Create account',
|
|
139
|
+
'auth.signup.submit': 'Create account',
|
|
140
|
+
'auth.signup.hasAccount': 'Already have an account?',
|
|
141
|
+
'auth.signup.signInLink': 'Sign in',
|
|
142
|
+
'settings.title': 'Settings',
|
|
143
|
+
'settings.company': 'Company',
|
|
144
|
+
'settings.branding': 'Branding',
|
|
145
|
+
'settings.security': 'Security',
|
|
146
|
+
'settings.team': 'Team',
|
|
147
|
+
'settings.billing': 'Billing',
|
|
148
|
+
'settings.locations': 'Locations',
|
|
149
|
+
'billing.title': 'Plans & Billing',
|
|
150
|
+
'billing.currentPlan': 'Current Plan',
|
|
151
|
+
'billing.upgrade': 'Upgrade',
|
|
152
|
+
'billing.manage': 'Manage Subscription',
|
|
153
|
+
},
|
|
154
|
+
'pt-BR': {
|
|
155
|
+
'common.save': 'Salvar',
|
|
156
|
+
'common.saveChanges': 'Salvar Alterações',
|
|
157
|
+
'common.saving': 'Salvando...',
|
|
158
|
+
'common.cancel': 'Cancelar',
|
|
159
|
+
'common.delete': 'Excluir',
|
|
160
|
+
'common.edit': 'Editar',
|
|
161
|
+
'common.close': 'Fechar',
|
|
162
|
+
'common.confirm': 'Confirmar',
|
|
163
|
+
'common.discard': 'Descartar',
|
|
164
|
+
'common.back': 'Voltar',
|
|
165
|
+
'common.formIncomplete': 'Preencha os campos obrigatórios antes de salvar.',
|
|
166
|
+
'saveBar.unsaved': 'Alterações não salvas',
|
|
167
|
+
'common.add': 'Adicionar',
|
|
168
|
+
'common.loading': 'Carregando...',
|
|
169
|
+
'common.error': 'Algo deu errado',
|
|
170
|
+
'common.noResults': 'Nenhum resultado encontrado.',
|
|
171
|
+
'common.search': 'Buscar',
|
|
172
|
+
'common.actions': 'Ações',
|
|
173
|
+
'common.continue': 'Continuar',
|
|
174
|
+
'common.done': 'Concluído',
|
|
175
|
+
'common.yes': 'Sim',
|
|
176
|
+
'common.no': 'Não',
|
|
177
|
+
'common.active': 'Ativo',
|
|
178
|
+
'common.inactive': 'Inativo',
|
|
179
|
+
'common.overview': 'Visão Geral',
|
|
180
|
+
'common.settings': 'Configurações',
|
|
181
|
+
'common.plugins': 'Plugins',
|
|
182
|
+
'common.new': 'Novo',
|
|
183
|
+
'common.filter': 'Filtrar',
|
|
184
|
+
'common.export': 'Exportar',
|
|
185
|
+
'common.import': 'Importar',
|
|
186
|
+
'common.preview': 'Pré-visualizar',
|
|
187
|
+
'common.total': 'Total',
|
|
188
|
+
'crud.newItem': 'Novo {{name}}',
|
|
189
|
+
'crud.editItem': 'Editar {{name}}',
|
|
190
|
+
'crud.deleteConfirm': 'Excluir {{name}}?',
|
|
191
|
+
'crud.deleteMessage': 'Esta ação não pode ser desfeita.',
|
|
192
|
+
'crud.created': '{{name}} criado',
|
|
193
|
+
'crud.updated': '{{name}} atualizado',
|
|
194
|
+
'crud.deleted': '{{name}} excluído',
|
|
195
|
+
'crud.noItems': 'Nenhum {{namePlural}} ainda.',
|
|
196
|
+
'auth.login.title': 'Entrar',
|
|
197
|
+
'auth.login.subtitle': 'Digite suas credenciais para acessar sua conta',
|
|
198
|
+
'auth.login.emailLabel': 'E-mail',
|
|
199
|
+
'auth.login.passwordLabel': 'Senha',
|
|
200
|
+
'auth.login.submit': 'Entrar',
|
|
201
|
+
'auth.login.forgotPassword': 'Esqueceu a senha?',
|
|
202
|
+
'auth.login.noAccount': 'Não tem uma conta?',
|
|
203
|
+
'auth.login.signUpLink': 'Cadastrar',
|
|
204
|
+
'auth.signup.title': 'Criar conta',
|
|
205
|
+
'auth.signup.submit': 'Criar conta',
|
|
206
|
+
'auth.signup.hasAccount': 'Já tem uma conta?',
|
|
207
|
+
'auth.signup.signInLink': 'Entrar',
|
|
208
|
+
'settings.title': 'Configurações',
|
|
209
|
+
'settings.company': 'Empresa',
|
|
210
|
+
'settings.branding': 'Identidade Visual',
|
|
211
|
+
'settings.security': 'Segurança',
|
|
212
|
+
'settings.team': 'Equipe',
|
|
213
|
+
'settings.billing': 'Faturamento',
|
|
214
|
+
'settings.locations': 'Localizações',
|
|
215
|
+
'billing.title': 'Planos e Faturamento',
|
|
216
|
+
'billing.currentPlan': 'Plano Atual',
|
|
217
|
+
'billing.upgrade': 'Fazer Upgrade',
|
|
218
|
+
'billing.manage': 'Gerenciar Assinatura',
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Global translation registry — plugins merge their locales here so keys resolve
|
|
223
|
+
// regardless of which shell mounts the I18nProvider.
|
|
224
|
+
let _globalTranslations: Record<string, Record<string, string>> = {}
|
|
225
|
+
|
|
226
|
+
export function registerTranslations(translations?: Record<string, Record<string, string>>): void {
|
|
227
|
+
if (!translations) return
|
|
228
|
+
_globalTranslations = mergeTranslations(_globalTranslations, translations)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function mergeTranslations(
|
|
232
|
+
base: Record<string, Record<string, string>>,
|
|
233
|
+
...overrides: Array<Record<string, Record<string, string>> | undefined>
|
|
234
|
+
): Record<string, Record<string, string>> {
|
|
235
|
+
const result = { ...base }
|
|
236
|
+
for (const override of overrides) {
|
|
237
|
+
if (!override) continue
|
|
238
|
+
for (const [locale, translations] of Object.entries(override)) {
|
|
239
|
+
result[locale] = { ...(result[locale] ?? {}), ...translations }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Seed the global fallback with core's own translations + the shared shell /
|
|
246
|
+
// CRUD / framework translations (extracted from saas-core's central i18n), so
|
|
247
|
+
// the native CRUD engine, framework UI and de-bridged plugins resolve their
|
|
248
|
+
// keys even under a host shell that does not mount @fayz-ai/core's I18nProvider.
|
|
249
|
+
registerTranslations(coreTranslations)
|
|
250
|
+
registerTranslations(shellTranslations)
|