@startsimpli/ui 0.4.50 → 0.4.52
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/package.json +2 -1
- package/src/catalog/catalog.ts +55 -0
- package/src/components/document-editor/DocumentEditor.tsx +476 -0
- package/src/components/document-editor/__tests__/DocumentEditor.test.tsx +125 -0
- package/src/components/document-editor/__tests__/sections.test.ts +107 -0
- package/src/components/document-editor/index.ts +20 -0
- package/src/components/document-editor/sections.ts +170 -0
- package/src/components/document-editor/types.ts +32 -0
- package/src/components/health/AttentionFeed.tsx +95 -0
- package/src/components/health/DeliveryHealth.tsx +127 -0
- package/src/components/health/HealthCard.tsx +108 -0
- package/src/components/health/PipelineHealth.tsx +99 -0
- package/src/components/health/SourceFreshness.tsx +88 -0
- package/src/components/health/__tests__/health.test.tsx +142 -0
- package/src/components/health/health-status.ts +79 -0
- package/src/components/health/index.ts +24 -0
- package/src/components/index.ts +4 -0
- package/src/components/navigation/__tests__/grouped-nav.test.tsx +69 -0
- package/src/components/navigation/grouped-nav.tsx +192 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { ChevronDown } from 'lucide-react'
|
|
6
|
+
import { cn } from '../../lib/utils'
|
|
7
|
+
|
|
8
|
+
export interface GroupedNavLink {
|
|
9
|
+
href: string
|
|
10
|
+
label: string
|
|
11
|
+
icon?: React.ReactNode
|
|
12
|
+
/** Optional trailing slot (count pill, status dot…). */
|
|
13
|
+
badge?: React.ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GroupedNavGroup {
|
|
17
|
+
label: string
|
|
18
|
+
icon?: React.ReactNode
|
|
19
|
+
items: GroupedNavLink[]
|
|
20
|
+
/** Start expanded (default true). Ignored while a child is active — then forced open. */
|
|
21
|
+
defaultOpen?: boolean
|
|
22
|
+
/** Show a chevron toggle (default true). false = a static section heading. */
|
|
23
|
+
collapsible?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A top-level link, or a named group of links. Discriminated by `items`. */
|
|
27
|
+
export type GroupedNavEntry = GroupedNavLink | GroupedNavGroup
|
|
28
|
+
|
|
29
|
+
export interface GroupedNavRenderLinkArgs {
|
|
30
|
+
link: GroupedNavLink
|
|
31
|
+
active: boolean
|
|
32
|
+
className: string
|
|
33
|
+
/** The pre-composed inner content (icon + label + badge). */
|
|
34
|
+
content: React.ReactNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GroupedNavProps {
|
|
38
|
+
items: GroupedNavEntry[]
|
|
39
|
+
/** The current path; drives active state. */
|
|
40
|
+
activeHref?: string
|
|
41
|
+
/** Override active matching (default: exact, or `activeHref` under `href/`). */
|
|
42
|
+
isActive?: (href: string, activeHref?: string) => boolean
|
|
43
|
+
/**
|
|
44
|
+
* Render a link — pass `next/link` here in a Next app. Default renders a plain
|
|
45
|
+
* `<a>` so the primitive stays framework-agnostic.
|
|
46
|
+
*/
|
|
47
|
+
renderLink?: (args: GroupedNavRenderLinkArgs) => React.ReactNode
|
|
48
|
+
className?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isGroup(entry: GroupedNavEntry): entry is GroupedNavGroup {
|
|
52
|
+
return Array.isArray((entry as GroupedNavGroup).items)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultIsActive(href: string, activeHref?: string): boolean {
|
|
56
|
+
if (!activeHref) return false
|
|
57
|
+
if (href === '/') return activeHref === '/'
|
|
58
|
+
return activeHref === href || activeHref.startsWith(href + '/')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const defaultRenderLink = ({ link, active, className, content }: GroupedNavRenderLinkArgs) => (
|
|
62
|
+
<a href={link.href} className={className} aria-current={active ? 'page' : undefined}>
|
|
63
|
+
{content}
|
|
64
|
+
</a>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
function linkClasses(active: boolean, indented: boolean) {
|
|
68
|
+
return cn(
|
|
69
|
+
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
|
70
|
+
indented && 'pl-9',
|
|
71
|
+
active
|
|
72
|
+
? 'bg-primary-600 text-white'
|
|
73
|
+
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900',
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A grouped sidebar navigation list: a mix of top-level links and collapsible
|
|
79
|
+
* groups (e.g. Dashboard + a "Content" group of the three content types). Purely
|
|
80
|
+
* the `<nav>` list — the app shell owns the logo, user footer, and chrome. Active
|
|
81
|
+
* state and link rendering are injectable so it drops into any router.
|
|
82
|
+
*/
|
|
83
|
+
export function GroupedNav({
|
|
84
|
+
items,
|
|
85
|
+
activeHref,
|
|
86
|
+
isActive = defaultIsActive,
|
|
87
|
+
renderLink = defaultRenderLink,
|
|
88
|
+
className,
|
|
89
|
+
}: GroupedNavProps) {
|
|
90
|
+
return (
|
|
91
|
+
<nav className={cn('space-y-1', className)} aria-label="Primary">
|
|
92
|
+
{items.map((entry, idx) =>
|
|
93
|
+
isGroup(entry) ? (
|
|
94
|
+
<NavGroup
|
|
95
|
+
key={`group-${entry.label}-${idx}`}
|
|
96
|
+
group={entry}
|
|
97
|
+
activeHref={activeHref}
|
|
98
|
+
isActive={isActive}
|
|
99
|
+
renderLink={renderLink}
|
|
100
|
+
/>
|
|
101
|
+
) : (
|
|
102
|
+
<NavLinkRow
|
|
103
|
+
key={entry.href}
|
|
104
|
+
link={entry}
|
|
105
|
+
active={isActive(entry.href, activeHref)}
|
|
106
|
+
indented={false}
|
|
107
|
+
renderLink={renderLink}
|
|
108
|
+
/>
|
|
109
|
+
),
|
|
110
|
+
)}
|
|
111
|
+
</nav>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function NavLinkRow({
|
|
116
|
+
link,
|
|
117
|
+
active,
|
|
118
|
+
indented,
|
|
119
|
+
renderLink,
|
|
120
|
+
}: {
|
|
121
|
+
link: GroupedNavLink
|
|
122
|
+
active: boolean
|
|
123
|
+
indented: boolean
|
|
124
|
+
renderLink: NonNullable<GroupedNavProps['renderLink']>
|
|
125
|
+
}) {
|
|
126
|
+
const content = (
|
|
127
|
+
<>
|
|
128
|
+
{link.icon ? <span className="flex-shrink-0 w-5 h-5 flex items-center justify-center">{link.icon}</span> : null}
|
|
129
|
+
<span className="flex-1 truncate">{link.label}</span>
|
|
130
|
+
{link.badge ? <span className="flex-shrink-0">{link.badge}</span> : null}
|
|
131
|
+
</>
|
|
132
|
+
)
|
|
133
|
+
return <>{renderLink({ link, active, className: linkClasses(active, indented), content })}</>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function NavGroup({
|
|
137
|
+
group,
|
|
138
|
+
activeHref,
|
|
139
|
+
isActive,
|
|
140
|
+
renderLink,
|
|
141
|
+
}: {
|
|
142
|
+
group: GroupedNavGroup
|
|
143
|
+
activeHref?: string
|
|
144
|
+
isActive: NonNullable<GroupedNavProps['isActive']>
|
|
145
|
+
renderLink: NonNullable<GroupedNavProps['renderLink']>
|
|
146
|
+
}) {
|
|
147
|
+
const collapsible = group.collapsible ?? true
|
|
148
|
+
const hasActiveChild = group.items.some((l) => isActive(l.href, activeHref))
|
|
149
|
+
const [open, setOpen] = useState(group.defaultOpen ?? true)
|
|
150
|
+
const expanded = hasActiveChild || open
|
|
151
|
+
|
|
152
|
+
const links = expanded
|
|
153
|
+
? group.items.map((link) => (
|
|
154
|
+
<NavLinkRow
|
|
155
|
+
key={link.href}
|
|
156
|
+
link={link}
|
|
157
|
+
active={isActive(link.href, activeHref)}
|
|
158
|
+
indented
|
|
159
|
+
renderLink={renderLink}
|
|
160
|
+
/>
|
|
161
|
+
))
|
|
162
|
+
: null
|
|
163
|
+
|
|
164
|
+
const heading = (
|
|
165
|
+
<span className="flex items-center gap-2 min-w-0">
|
|
166
|
+
{group.icon ? <span className="flex-shrink-0 w-5 h-5 flex items-center justify-center">{group.icon}</span> : null}
|
|
167
|
+
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wider truncate">{group.label}</span>
|
|
168
|
+
</span>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="pt-3 first:pt-0">
|
|
173
|
+
{collapsible ? (
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
onClick={() => setOpen((o) => !o)}
|
|
177
|
+
aria-expanded={expanded}
|
|
178
|
+
className="w-full flex items-center justify-between gap-2 px-3 py-1.5 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
179
|
+
>
|
|
180
|
+
{heading}
|
|
181
|
+
<ChevronDown
|
|
182
|
+
className={cn('w-4 h-4 text-gray-400 transition-transform flex-shrink-0', expanded ? '' : '-rotate-90')}
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
/>
|
|
185
|
+
</button>
|
|
186
|
+
) : (
|
|
187
|
+
<div className="px-3 py-1.5">{heading}</div>
|
|
188
|
+
)}
|
|
189
|
+
{links && <div className="mt-1 space-y-1">{links}</div>}
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|