spine-framework-cortex 0.1.6 → 0.1.9

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/LICENSE.md ADDED
@@ -0,0 +1,13 @@
1
+ # Spine Framework Internal Use License 1.0.0
2
+
3
+ **Source-available. Free for internal use. Commercial rights reserved.**
4
+
5
+ Copyright © 2026 Dahl Ventures Inc. All rights reserved.
6
+
7
+ This software is licensed under the Spine Framework Internal Use License 1.0.0.
8
+
9
+ For full license terms, see: https://github.com/spine-framework/spine-framework/blob/main/LICENSE.md
10
+
11
+ **Summary:** You may use this software for internal business use only. Commercial use, resale, SaaS offering, white-labeling, and distribution require a separate Commercial License.
12
+
13
+ Contact: webmaster@spine-framework.com
@@ -8,8 +8,9 @@
8
8
  */
9
9
 
10
10
  import * as React from "react"
11
- import { useNavigate, useLocation } from "react-router-dom"
11
+ import { Link, useNavigate, useLocation } from "react-router-dom"
12
12
  import { useAuth } from "@core/contexts/AuthContext"
13
+ import { useCurrentApp } from "@core/contexts/AppContext"
13
14
  import {
14
15
  LayoutDashboard,
15
16
  Building2,
@@ -35,26 +36,30 @@ import {
35
36
  SidebarRail,
36
37
  } from "@core/components/ui/sidebar"
37
38
 
38
- const crmItems = [
39
- { title: "Dashboard", url: "/cortex/dashboard", icon: LayoutDashboard },
40
- { title: "Accounts", url: "/cortex/crm/accounts", icon: Building2 },
41
- { title: "Contacts", url: "/cortex/crm/contacts", icon: Users },
42
- { title: "Deals", url: "/cortex/crm/deals", icon: Handshake },
43
- { title: "Health", url: "/cortex/crm/health", icon: Heart },
44
- { title: "Activity", url: "/cortex/crm/activity", icon: Activity },
45
- ]
46
-
47
- const opsItems = [
48
- { title: "Support", url: "/cortex/support", icon: Headphones },
49
- { title: "Community", url: "/cortex/community", icon: Users },
50
- { title: "Knowledge Base", url: "/cortex/kb", icon: BookOpen },
51
- { title: "Courses", url: "/cortex/courses", icon: GraduationCap },
52
- ]
53
-
54
39
  export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
55
40
  const navigate = useNavigate()
56
41
  const location = useLocation()
57
42
  const { user } = useAuth()
43
+ const app = useCurrentApp()
44
+
45
+ // Normalize route_prefix: '/' becomes '' so all paths are /dashboard, /crm/accounts, etc.
46
+ const base = app.route_prefix === '/' ? '' : (app.route_prefix || '')
47
+
48
+ const crmItems = [
49
+ { title: "Dashboard", url: `${base}/dashboard`, icon: LayoutDashboard },
50
+ { title: "Accounts", url: `${base}/crm/accounts`, icon: Building2 },
51
+ { title: "Contacts", url: `${base}/crm/contacts`, icon: Users },
52
+ { title: "Deals", url: `${base}/crm/deals`, icon: Handshake },
53
+ { title: "Health", url: `${base}/crm/health`, icon: Heart },
54
+ { title: "Activity", url: `${base}/crm/activity`, icon: Activity },
55
+ ]
56
+
57
+ const opsItems = [
58
+ { title: "Support", url: `${base}/support`, icon: Headphones },
59
+ { title: "Community", url: `${base}/community`, icon: Users },
60
+ { title: "Knowledge Base", url: `${base}/kb`, icon: BookOpen },
61
+ { title: "Courses", url: `${base}/courses`, icon: GraduationCap },
62
+ ]
58
63
 
59
64
  const isActive = (url: string) => location.pathname.startsWith(url)
60
65
 
@@ -63,7 +68,7 @@ export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>
63
68
  <SidebarHeader>
64
69
  <SidebarMenu>
65
70
  <SidebarMenuItem>
66
- <SidebarMenuButton size="lg" onClick={() => navigate("/cortex/dashboard")}>
71
+ <SidebarMenuButton size="lg" onClick={() => navigate(`${base}/dashboard`)}>
67
72
  <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
68
73
  <span className="text-sm font-bold">Cx</span>
69
74
  </div>
@@ -87,10 +92,10 @@ export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>
87
92
  asChild
88
93
  isActive={isActive(item.url)}
89
94
  >
90
- <a href={item.url}>
95
+ <Link to={item.url}>
91
96
  <item.icon className="h-4 w-4" />
92
97
  <span>{item.title}</span>
93
- </a>
98
+ </Link>
94
99
  </SidebarMenuButton>
95
100
  </SidebarMenuItem>
96
101
  ))}
@@ -108,10 +113,10 @@ export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>
108
113
  asChild
109
114
  isActive={isActive(item.url)}
110
115
  >
111
- <a href={item.url}>
116
+ <Link to={item.url}>
112
117
  <item.icon className="h-4 w-4" />
113
118
  <span>{item.title}</span>
114
- </a>
119
+ </Link>
115
120
  </SidebarMenuButton>
116
121
  </SidebarMenuItem>
117
122
  ))}
package/index.tsx CHANGED
@@ -4,6 +4,7 @@ import { LoadingSpinner } from '@core/components/ui/LoadingSpinner'
4
4
  import { AppShell } from '@core/components/layout/AppShell'
5
5
  import { CortexSidebar } from './components/CortexSidebar'
6
6
  import { TooltipProvider } from '@core/components/ui/tooltip'
7
+ import { useCurrentApp } from '@core/contexts/AppContext'
7
8
 
8
9
  const CortexDashboard = lazy(() => import('./pages/CortexDashboard'))
9
10
 
@@ -40,13 +41,21 @@ const Fallback = <div className="min-h-[400px] flex items-center justify-center"
40
41
 
41
42
  function CortexLayout() {
42
43
  const location = useLocation()
44
+ const app = useCurrentApp()
45
+
46
+ // Normalize route_prefix: '/' → '' so paths are /dashboard, /crm/accounts, etc.
47
+ const base = app.route_prefix === '/' ? '' : (app.route_prefix || '')
48
+ const prefixDepth = base.split('/').filter(Boolean).length
49
+
43
50
  const segments = location.pathname.split('/').filter(Boolean)
44
- const breadcrumbs: { title: string; url?: string }[] = [{ title: 'Cortex', url: '/cortex/dashboard' }]
45
- if (segments[1] && segments[1] !== 'dashboard') {
46
- breadcrumbs.push({ title: segments[1].charAt(0).toUpperCase() + segments[1].slice(1) })
51
+ const appSegments = segments.slice(prefixDepth) // strips the prefix segments
52
+
53
+ const breadcrumbs: { title: string; url?: string }[] = [{ title: 'Cortex', url: `${base}/dashboard` }]
54
+ if (appSegments[0] && appSegments[0] !== 'dashboard') {
55
+ breadcrumbs.push({ title: appSegments[0].charAt(0).toUpperCase() + appSegments[0].slice(1) })
47
56
  }
48
- if (segments[2]) {
49
- breadcrumbs.push({ title: segments[2].charAt(0).toUpperCase() + segments[2].slice(1) })
57
+ if (appSegments[1]) {
58
+ breadcrumbs.push({ title: appSegments[1].charAt(0).toUpperCase() + appSegments[1].slice(1) })
50
59
  }
51
60
 
52
61
  return (
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "spine-framework-cortex",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "Cortex — AI-powered support, CRM, and knowledge base app for Spine Framework",
5
5
  "type": "module",
6
- "license": "MIT",
6
+ "license": "SEE LICENSE IN LICENSE.md",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/art-mojo-admin/spine",
9
+ "url": "https://github.com/spine-framework/cortex",
10
10
  "directory": "custom/apps/cortex"
11
11
  },
12
12
  "peerDependencies": {
@@ -20,7 +20,8 @@
20
20
  "components/",
21
21
  "functions/",
22
22
  "lib/",
23
- "README.md"
23
+ "README.md",
24
+ "LICENSE.md"
24
25
  ],
25
26
  "spine": {
26
27
  "type": "app",
@@ -7,7 +7,7 @@ import { Skeleton } from '@core/components/ui/skeleton'
7
7
  import { Button } from '@core/components/ui/button'
8
8
  import { ScrollArea } from '@core/components/ui/scroll-area'
9
9
  import { Separator } from '@core/components/ui/separator'
10
- import { ArrowLeft, User, Ticket, Handshake, Activity, Heart, Funnel, TrendingUp, Target, Clock } from 'lucide-react'
10
+ import { ArrowLeft, User, Ticket, Handshake, Activity, Heart, Filter, TrendingUp, Target, Clock } from 'lucide-react'
11
11
 
12
12
  interface Account {
13
13
  id: string
@@ -140,7 +140,7 @@ function ActivityTab({ accountId }: { accountId: string }) {
140
140
  )
141
141
  }
142
142
 
143
- function FunnelTab({ account }: { account: Account }) {
143
+ function FilterTab({ account }: { account: Account }) {
144
144
  const stage = account.data?.lifecycle_stage
145
145
  const score = account.data?.lead_score ?? 0
146
146
  const temp = account.data?.temperature
@@ -217,7 +217,7 @@ function FunnelTab({ account }: { account: Account }) {
217
217
  {queue?.pending_opportunity_id && (
218
218
  <div className="border rounded-lg p-4 bg-yellow-50 border-yellow-200">
219
219
  <div className="flex items-center gap-2">
220
- <Funnel className="h-4 w-4 text-yellow-600" />
220
+ <Filter className="h-4 w-4 text-yellow-600" />
221
221
  <span className="font-medium text-sm">Opportunity in Queue</span>
222
222
  </div>
223
223
  <p className="text-xs text-muted-foreground mt-1">
@@ -317,7 +317,7 @@ function FunnelTab({ account }: { account: Account }) {
317
317
  {/* Empty State */}
318
318
  {!stage && !score && !temp && !ratings && !queue?.pending_opportunity_id && (
319
319
  <div className="text-center py-12 text-muted-foreground">
320
- <Funnel className="h-10 w-10 mx-auto mb-3 opacity-30" />
320
+ <Filter className="h-10 w-10 mx-auto mb-3 opacity-30" />
321
321
  <p className="text-sm">No funnel data yet.</p>
322
322
  <p className="text-xs mt-1">Signals will appear here as they are processed.</p>
323
323
  </div>
@@ -364,7 +364,7 @@ export default function AccountDetailPage() {
364
364
  <TabsList className="h-9 bg-transparent p-0 gap-4">
365
365
  {[
366
366
  { value: 'people', label: 'People', icon: User },
367
- { value: 'funnel', label: 'Funnel', icon: Funnel },
367
+ { value: 'funnel', label: 'Funnel', icon: Filter },
368
368
  { value: 'tickets', label: 'Tickets', icon: Ticket },
369
369
  { value: 'deals', label: 'Deals', icon: Handshake },
370
370
  { value: 'health', label: 'Health', icon: Heart },
@@ -381,7 +381,7 @@ export default function AccountDetailPage() {
381
381
 
382
382
  <ScrollArea className="flex-1">
383
383
  <TabsContent value="people" className="mt-0"><PeopleTab accountId={id!} /></TabsContent>
384
- <TabsContent value="funnel" className="mt-0"><FunnelTab account={account} /></TabsContent>
384
+ <TabsContent value="funnel" className="mt-0"><FilterTab account={account} /></TabsContent>
385
385
  <TabsContent value="tickets" className="mt-0"><ItemsTab accountId={id!} typeSlug="support_ticket" emptyText="No tickets." /></TabsContent>
386
386
  <TabsContent value="deals" className="mt-0"><ItemsTab accountId={id!} typeSlug="deal" emptyText="No deals." /></TabsContent>
387
387
  <TabsContent value="health" className="mt-0"><ItemsTab accountId={id!} typeSlug="csm_health" emptyText="No health records." /></TabsContent>
package/seed/types.json CHANGED
@@ -777,5 +777,59 @@
777
777
  "functionality": null
778
778
  },
779
779
  "validation_schema": {}
780
+ },
781
+ {
782
+ "kind": "item",
783
+ "slug": "support_ticket",
784
+ "name": "Support Ticket",
785
+ "description": "Customer support request or issue",
786
+ "icon": "ticket",
787
+ "color": "#F59E0B",
788
+ "ownership": "tenant",
789
+ "is_active": true,
790
+ "design_schema": {
791
+ "fields": {
792
+ "title": { "data_type": "text", "label": "Title", "required": true, "permissions": { "system_admin": ["read", "write"], "support": ["read", "write"], "member": ["read", "write"] } },
793
+ "description": { "data_type": "richtext", "label": "Description", "required": true, "permissions": { "system_admin": ["read", "write"], "support": ["read", "write"], "member": ["read", "write"] } },
794
+ "status": { "data_type": "select", "label": "Status", "required": true, "options": [{"label": "Open", "value": "open"}, {"label": "In Progress", "value": "in_progress"}, {"label": "Resolved", "value": "resolved"}, {"label": "Closed", "value": "closed"}], "permissions": { "system_admin": ["read", "write"], "support": ["read", "write"], "member": ["read"] } },
795
+ "priority": { "data_type": "select", "label": "Priority", "required": false, "options": [{"label": "Low", "value": "low"}, {"label": "Medium", "value": "medium"}, {"label": "High", "value": "high"}, {"label": "Urgent", "value": "urgent"}], "permissions": { "system_admin": ["read", "write"], "support": ["read", "write"], "member": ["read"] } },
796
+ "channel": { "data_type": "select", "label": "Channel", "required": false, "options": [{"label": "Email", "value": "email"}, {"label": "Chat", "value": "chat"}, {"label": "Portal", "value": "portal"}, {"label": "Phone", "value": "phone"}], "permissions": { "system_admin": ["read", "write"], "support": ["read", "write"], "member": ["read"] } },
797
+ "assignee_id": { "data_type": "uuid", "label": "Assigned To", "required": false, "permissions": { "system_admin": ["read", "write"], "support": ["read", "write"], "member": ["read"] } },
798
+ "resolved_at": { "data_type": "datetime", "label": "Resolved At", "required": false, "permissions": { "system_admin": ["read", "write"], "support": ["read", "write"], "member": ["read"] } },
799
+ "satisfaction_score": { "data_type": "number", "label": "Satisfaction Score", "required": false, "validation": { "min": 1, "max": 5 }, "permissions": { "system_admin": ["read", "write"], "support": ["read"], "member": ["read", "write"] } }
800
+ },
801
+ "views": {
802
+ "default_list": {
803
+ "type": "list",
804
+ "display": "table",
805
+ "label": "Support Tickets",
806
+ "fields": {
807
+ "title": { "sortable": true, "display_type": "text" },
808
+ "status": { "sortable": true, "display_type": "badge" },
809
+ "priority": { "sortable": true, "display_type": "badge" },
810
+ "channel": { "sortable": false, "display_type": "badge" }
811
+ },
812
+ "default_sort": { "field": "created_at", "direction": "desc" }
813
+ },
814
+ "default_detail": {
815
+ "type": "detail",
816
+ "label": "Support Ticket",
817
+ "sections": [
818
+ { "title": "Ticket", "fields": { "title": { "display_type": "input" }, "description": { "display_type": "richtext" }, "status": { "display_type": "select" }, "priority": { "display_type": "select" } } },
819
+ { "title": "Assignment", "fields": { "assignee_id": { "display_type": "text" }, "channel": { "display_type": "select" }, "resolved_at": { "display_type": "timestamp" } } },
820
+ { "title": "Feedback", "fields": { "satisfaction_score": { "display_type": "number" } } }
821
+ ]
822
+ }
823
+ },
824
+ "record_permissions": { "system_admin": ["create", "read", "update", "delete"], "support": ["create", "read", "update"], "member": ["create", "read"] },
825
+ "functionality": null
826
+ },
827
+ "validation_schema": {
828
+ "fields": {
829
+ "title": { "data_type": "text", "required": true },
830
+ "description": { "data_type": "richtext", "required": true },
831
+ "status": { "data_type": "text", "required": true }
832
+ }
833
+ }
780
834
  }
781
835
  ]