@olwiba/ui 0.1.8 → 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/dist/index.d.ts +5 -1
- package/dist/index.js +194 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/marketing/ComparisonSection.tsx +8 -19
- package/src/marketing/ContactSection.tsx +181 -102
- package/src/marketing/FeatureMarqueeSection.tsx +6 -5
- package/src/marketing/Footer.tsx +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Check, Minus } from 'lucide-react';
|
|
4
3
|
import { cn, useUIVariant } from '@olwiba/cn';
|
|
5
4
|
import { SectionTitle } from './SectionTitle';
|
|
6
5
|
import { StaggerChildren } from '../motion/StaggerChildren';
|
|
@@ -45,9 +44,9 @@ export function ComparisonSection({
|
|
|
45
44
|
<div
|
|
46
45
|
key={col.label}
|
|
47
46
|
className={cn(
|
|
48
|
-
'flex flex-col rounded-2xl border p-6',
|
|
47
|
+
'flex h-full flex-col rounded-2xl border p-6',
|
|
49
48
|
col.highlighted
|
|
50
|
-
? 'border-
|
|
49
|
+
? 'border-primary bg-primary/10 ring-1 ring-primary/20'
|
|
51
50
|
: 'border-border bg-muted/40',
|
|
52
51
|
)}
|
|
53
52
|
>
|
|
@@ -55,26 +54,16 @@ export function ComparisonSection({
|
|
|
55
54
|
<p
|
|
56
55
|
className={cn(
|
|
57
56
|
'text-xs font-semibold uppercase tracking-widest',
|
|
58
|
-
col.highlighted ? 'text-
|
|
57
|
+
col.highlighted ? 'text-primary/80' : 'text-muted-foreground',
|
|
59
58
|
)}
|
|
60
59
|
>
|
|
61
60
|
{col.label}
|
|
62
61
|
</p>
|
|
63
|
-
<p
|
|
64
|
-
className={cn(
|
|
65
|
-
'mt-2 text-2xl font-bold tracking-tight',
|
|
66
|
-
col.highlighted ? 'text-background' : 'text-foreground',
|
|
67
|
-
)}
|
|
68
|
-
>
|
|
62
|
+
<p className="mt-2 text-2xl font-bold tracking-tight text-foreground">
|
|
69
63
|
{col.cost}
|
|
70
64
|
</p>
|
|
71
65
|
{col.costDetail && (
|
|
72
|
-
<p
|
|
73
|
-
className={cn(
|
|
74
|
-
'mt-1 text-sm',
|
|
75
|
-
col.highlighted ? 'text-background/60' : 'text-muted-foreground',
|
|
76
|
-
)}
|
|
77
|
-
>
|
|
66
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
78
67
|
{col.costDetail}
|
|
79
68
|
</p>
|
|
80
69
|
)}
|
|
@@ -84,14 +73,14 @@ export function ComparisonSection({
|
|
|
84
73
|
{col.items.map((item) => (
|
|
85
74
|
<li key={item} className="flex items-start gap-2.5">
|
|
86
75
|
{col.highlighted ? (
|
|
87
|
-
<
|
|
76
|
+
<span className="mt-0.5 shrink-0 text-sm leading-none">⚡</span>
|
|
88
77
|
) : (
|
|
89
|
-
<
|
|
78
|
+
<span className="mt-0.5 shrink-0 text-sm leading-none">❌</span>
|
|
90
79
|
)}
|
|
91
80
|
<span
|
|
92
81
|
className={cn(
|
|
93
82
|
'text-sm leading-snug',
|
|
94
|
-
col.highlighted ? 'text-
|
|
83
|
+
col.highlighted ? 'text-foreground' : 'text-muted-foreground',
|
|
95
84
|
)}
|
|
96
85
|
>
|
|
97
86
|
{item}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import { Badge, Button,
|
|
4
|
+
import { ChevronDown, Mail, MessageSquare, Send, type LucideIcon } from 'lucide-react';
|
|
5
|
+
import { Badge, Button, Input, Label, Textarea } from '@olwiba/cn';
|
|
6
6
|
|
|
7
7
|
export type ContactInfoItem = {
|
|
8
8
|
label: string;
|
|
@@ -22,9 +22,13 @@ export interface ContactSectionProps {
|
|
|
22
22
|
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
{
|
|
27
|
-
{
|
|
25
|
+
const PHONE_PREFIXES = [
|
|
26
|
+
{ label: 'US +1', value: '+1-US' },
|
|
27
|
+
{ label: 'NZ +64', value: '+64-NZ' },
|
|
28
|
+
{ label: 'AU +61', value: '+61-AU' },
|
|
29
|
+
{ label: 'UK +44', value: '+44-UK' },
|
|
30
|
+
{ label: 'CA +1', value: '+1-CA' },
|
|
31
|
+
{ label: 'EU', value: '+0-EU' },
|
|
28
32
|
];
|
|
29
33
|
|
|
30
34
|
const defaults = {
|
|
@@ -41,7 +45,7 @@ export function ContactSection({
|
|
|
41
45
|
badge = defaults.badge,
|
|
42
46
|
title = defaults.title,
|
|
43
47
|
description = defaults.description,
|
|
44
|
-
contactInfo
|
|
48
|
+
contactInfo,
|
|
45
49
|
privacyHref,
|
|
46
50
|
successTitle = defaults.successTitle,
|
|
47
51
|
successDescription = defaults.successDescription,
|
|
@@ -58,137 +62,212 @@ export function ContactSection({
|
|
|
58
62
|
|
|
59
63
|
return (
|
|
60
64
|
<section className="overflow-hidden rounded-2xl border bg-card">
|
|
61
|
-
<div className="
|
|
62
|
-
|
|
63
|
-
{/* Left panel */}
|
|
64
|
-
<div className="relative overflow-hidden bg-primary px-8 py-12 text-primary-foreground">
|
|
65
|
-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,hsl(var(--primary-foreground)/0.12),transparent_55%)]" />
|
|
66
|
-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_left,hsl(var(--primary-foreground)/0.06),transparent_60%)]" />
|
|
67
|
-
<div className="relative flex h-full flex-col gap-8">
|
|
68
|
-
<div>
|
|
69
|
-
<Badge
|
|
70
|
-
variant="secondary"
|
|
71
|
-
className="mb-4 bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/20"
|
|
72
|
-
>
|
|
73
|
-
{badge}
|
|
74
|
-
</Badge>
|
|
75
|
-
<h2 className="text-3xl font-semibold leading-tight">{title}</h2>
|
|
76
|
-
<p className="mt-3 text-sm leading-relaxed text-primary-foreground/70">{description}</p>
|
|
77
|
-
</div>
|
|
65
|
+
<div className="px-6 py-14 sm:px-10 sm:py-20">
|
|
78
66
|
|
|
79
|
-
|
|
67
|
+
{/* Header */}
|
|
68
|
+
<div className="mx-auto max-w-2xl text-center">
|
|
69
|
+
{badge && (
|
|
70
|
+
<div className="mb-4 flex justify-center">
|
|
71
|
+
<Badge variant="secondary">{badge}</Badge>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
<h2 className="text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
75
|
+
{title}
|
|
76
|
+
</h2>
|
|
77
|
+
<p className="mt-4 text-lg text-muted-foreground">{description}</p>
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
{contactInfo && contactInfo.length > 0 && (
|
|
80
|
+
<div className="mt-8 flex flex-wrap justify-center gap-6">
|
|
82
81
|
{contactInfo.map(({ icon: Icon = Mail, label, value }) => (
|
|
83
|
-
<div key={label} className="flex items-
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
</div>
|
|
87
|
-
<div>
|
|
88
|
-
<div className="text-xs font-medium uppercase tracking-wider text-primary-foreground/50">{label}</div>
|
|
89
|
-
<div className="mt-0.5 text-sm font-medium">{value}</div>
|
|
90
|
-
</div>
|
|
82
|
+
<div key={label} className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
83
|
+
<Icon className="size-4 shrink-0" />
|
|
84
|
+
<span>{value}</span>
|
|
91
85
|
</div>
|
|
92
86
|
))}
|
|
93
87
|
</div>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
94
90
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
{/* Body */}
|
|
92
|
+
{submitted ? (
|
|
93
|
+
<div className="mx-auto mt-16 max-w-xl text-center">
|
|
94
|
+
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
|
95
|
+
<MessageSquare className="size-6" />
|
|
99
96
|
</div>
|
|
97
|
+
<h3 className="mt-5 text-lg font-semibold">{successTitle}</h3>
|
|
98
|
+
<p className="mt-2 text-sm text-muted-foreground">{successDescription}</p>
|
|
99
|
+
<Button variant="outline" size="sm" className="mt-6" onClick={() => setSubmitted(false)}>
|
|
100
|
+
{sendAnotherLabel}
|
|
101
|
+
</Button>
|
|
100
102
|
</div>
|
|
101
|
-
|
|
103
|
+
) : (
|
|
104
|
+
<form onSubmit={handleSubmit} className="mx-auto mt-16 max-w-xl sm:mt-20">
|
|
105
|
+
{/* Honeypot */}
|
|
106
|
+
<input
|
|
107
|
+
type="text"
|
|
108
|
+
name="_hp"
|
|
109
|
+
tabIndex={-1}
|
|
110
|
+
autoComplete="off"
|
|
111
|
+
aria-hidden="true"
|
|
112
|
+
className="hidden"
|
|
113
|
+
/>
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{submitted ? (
|
|
106
|
-
<div className="flex h-full min-h-[360px] flex-col items-center justify-center gap-5 text-center">
|
|
107
|
-
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
|
108
|
-
<MessageSquare className="size-6" />
|
|
109
|
-
</div>
|
|
115
|
+
<div className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2">
|
|
116
|
+
{/* First name */}
|
|
110
117
|
<div>
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
124
|
-
<div className="space-y-1.5">
|
|
125
|
-
<Label htmlFor="contact-first">First name</Label>
|
|
126
|
-
<Input id="contact-first" name="firstName" placeholder="Olivia" required />
|
|
127
|
-
</div>
|
|
128
|
-
<div className="space-y-1.5">
|
|
129
|
-
<Label htmlFor="contact-last">Last name</Label>
|
|
130
|
-
<Input id="contact-last" name="lastName" placeholder="Reed" required />
|
|
118
|
+
<Label htmlFor="contact-first" className="block text-sm/6 font-semibold">
|
|
119
|
+
First name
|
|
120
|
+
</Label>
|
|
121
|
+
<div className="mt-2.5">
|
|
122
|
+
<Input
|
|
123
|
+
id="contact-first"
|
|
124
|
+
name="firstName"
|
|
125
|
+
autoComplete="given-name"
|
|
126
|
+
placeholder="First"
|
|
127
|
+
required
|
|
128
|
+
/>
|
|
131
129
|
</div>
|
|
132
130
|
</div>
|
|
133
131
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<Input
|
|
132
|
+
{/* Last name */}
|
|
133
|
+
<div>
|
|
134
|
+
<Label htmlFor="contact-last" className="block text-sm/6 font-semibold">
|
|
135
|
+
Last name
|
|
136
|
+
</Label>
|
|
137
|
+
<div className="mt-2.5">
|
|
138
|
+
<Input
|
|
139
|
+
id="contact-last"
|
|
140
|
+
name="lastName"
|
|
141
|
+
autoComplete="family-name"
|
|
142
|
+
placeholder="Last"
|
|
143
|
+
required
|
|
144
|
+
/>
|
|
141
145
|
</div>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Company */}
|
|
149
|
+
<div className="sm:col-span-2">
|
|
150
|
+
<Label htmlFor="contact-company" className="block text-sm/6 font-semibold">
|
|
151
|
+
Company{' '}
|
|
152
|
+
<span className="font-normal text-muted-foreground">(optional)</span>
|
|
153
|
+
</Label>
|
|
154
|
+
<div className="mt-2.5">
|
|
155
|
+
<Input
|
|
156
|
+
id="contact-company"
|
|
157
|
+
name="company"
|
|
158
|
+
autoComplete="organization"
|
|
159
|
+
placeholder="Acme Inc."
|
|
160
|
+
/>
|
|
148
161
|
</div>
|
|
149
162
|
</div>
|
|
150
163
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<
|
|
164
|
+
{/* Email */}
|
|
165
|
+
<div className="sm:col-span-2">
|
|
166
|
+
<Label htmlFor="contact-email" className="block text-sm/6 font-semibold">
|
|
167
|
+
Email
|
|
168
|
+
</Label>
|
|
169
|
+
<div className="mt-2.5">
|
|
170
|
+
<Input
|
|
171
|
+
id="contact-email"
|
|
172
|
+
name="email"
|
|
173
|
+
type="email"
|
|
174
|
+
autoComplete="email"
|
|
175
|
+
placeholder="you@company.com"
|
|
176
|
+
required
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
154
179
|
</div>
|
|
155
180
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<
|
|
181
|
+
{/* Phone */}
|
|
182
|
+
<div className="sm:col-span-2">
|
|
183
|
+
<Label htmlFor="contact-phone" className="block text-sm/6 font-semibold">
|
|
184
|
+
Phone number{' '}
|
|
185
|
+
<span className="font-normal text-muted-foreground">(optional)</span>
|
|
186
|
+
</Label>
|
|
187
|
+
<div className="mt-2.5 flex overflow-hidden rounded-md border border-input bg-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
|
188
|
+
<div className="relative flex shrink-0 items-center border-r border-input">
|
|
189
|
+
<select
|
|
190
|
+
name="countryCode"
|
|
191
|
+
aria-label="Country code"
|
|
192
|
+
defaultValue="+1-US"
|
|
193
|
+
className="h-full appearance-none bg-transparent py-2 pl-3 pr-7 text-sm text-foreground focus:outline-none"
|
|
194
|
+
>
|
|
195
|
+
{PHONE_PREFIXES.map(({ label, value }) => (
|
|
196
|
+
<option key={value} value={value}>
|
|
197
|
+
{label}
|
|
198
|
+
</option>
|
|
199
|
+
))}
|
|
200
|
+
</select>
|
|
201
|
+
<ChevronDown className="pointer-events-none absolute right-1.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
202
|
+
</div>
|
|
203
|
+
<input
|
|
204
|
+
id="contact-phone"
|
|
205
|
+
name="phone"
|
|
206
|
+
type="tel"
|
|
207
|
+
placeholder="123-456-7890"
|
|
208
|
+
className="min-w-0 grow bg-transparent px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
159
211
|
</div>
|
|
160
212
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
{/* Message */}
|
|
214
|
+
<div className="sm:col-span-2">
|
|
215
|
+
<Label htmlFor="contact-message" className="block text-sm/6 font-semibold">
|
|
216
|
+
Message
|
|
217
|
+
</Label>
|
|
218
|
+
<div className="mt-2.5">
|
|
219
|
+
<Textarea
|
|
220
|
+
id="contact-message"
|
|
221
|
+
name="message"
|
|
222
|
+
rows={4}
|
|
223
|
+
placeholder="Tell us what you're working on..."
|
|
224
|
+
className="resize-none"
|
|
225
|
+
required
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
170
228
|
</div>
|
|
171
229
|
|
|
230
|
+
{/* Privacy toggle */}
|
|
172
231
|
{privacyHref && (
|
|
173
|
-
<div className="flex
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
232
|
+
<div className="flex gap-x-4 sm:col-span-2">
|
|
233
|
+
<div className="flex h-6 items-center">
|
|
234
|
+
<div className="group relative inline-flex h-5 w-8 shrink-0 cursor-pointer rounded-full border border-input bg-muted p-px transition-colors duration-200 ease-in-out focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 has-[:checked]:border-primary has-[:checked]:bg-primary">
|
|
235
|
+
<span className="size-4 rounded-full bg-background shadow-sm transition-transform duration-200 ease-in-out group-has-[:checked]:translate-x-3" />
|
|
236
|
+
<input
|
|
237
|
+
id="contact-privacy"
|
|
238
|
+
name="privacy"
|
|
239
|
+
type="checkbox"
|
|
240
|
+
required
|
|
241
|
+
aria-label="Agree to privacy policy"
|
|
242
|
+
className="absolute inset-0 size-full cursor-pointer appearance-none focus:outline-none"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
<Label
|
|
247
|
+
htmlFor="contact-privacy"
|
|
248
|
+
className="cursor-pointer text-sm font-normal leading-snug text-muted-foreground"
|
|
249
|
+
>
|
|
250
|
+
By selecting this, you agree to our{' '}
|
|
251
|
+
<a
|
|
252
|
+
href={privacyHref}
|
|
253
|
+
className="font-medium text-foreground underline-offset-2 hover:underline"
|
|
254
|
+
>
|
|
178
255
|
privacy policy
|
|
179
256
|
</a>
|
|
180
257
|
.
|
|
181
258
|
</Label>
|
|
182
259
|
</div>
|
|
183
260
|
)}
|
|
261
|
+
</div>
|
|
184
262
|
|
|
263
|
+
<div className="mt-10">
|
|
185
264
|
<Button type="submit" className="w-full">
|
|
186
|
-
|
|
265
|
+
Let's talk
|
|
187
266
|
<Send className="ml-2 size-4" />
|
|
188
267
|
</Button>
|
|
189
|
-
</
|
|
190
|
-
|
|
191
|
-
|
|
268
|
+
</div>
|
|
269
|
+
</form>
|
|
270
|
+
)}
|
|
192
271
|
|
|
193
272
|
</div>
|
|
194
273
|
</section>
|
|
@@ -63,11 +63,8 @@ export function FeatureMarqueeSection({
|
|
|
63
63
|
{rows.map((row, i) => {
|
|
64
64
|
const direction = row.direction ?? (i % 2 === 0 ? 'left' : 'right');
|
|
65
65
|
const reversed = direction === 'right';
|
|
66
|
-
// For right-scroll rows: reverse source order so item 0 starts on the right
|
|
67
|
-
// and items enter from the right in natural reading order.
|
|
68
|
-
const source = reversed ? [...row.items].reverse() : row.items;
|
|
69
66
|
// Repeat enough that one set exceeds 4K viewport (~3840px).
|
|
70
|
-
const set = Array.from({ length: 4 }, () =>
|
|
67
|
+
const set = Array.from({ length: 4 }, () => row.items).flat();
|
|
71
68
|
const items = [...set, ...set];
|
|
72
69
|
|
|
73
70
|
return (
|
|
@@ -87,7 +84,7 @@ export function FeatureMarqueeSection({
|
|
|
87
84
|
pauseOnHover && 'group-hover:[animation-play-state:paused]',
|
|
88
85
|
)}
|
|
89
86
|
style={{
|
|
90
|
-
animation:
|
|
87
|
+
animation: `${reversed ? 'feature-marquee-right' : 'feature-marquee-left'} ${duration} linear infinite`,
|
|
91
88
|
}}
|
|
92
89
|
>
|
|
93
90
|
{items.map((item, j) => {
|
|
@@ -114,6 +111,10 @@ export function FeatureMarqueeSection({
|
|
|
114
111
|
from { transform: translateX(0); }
|
|
115
112
|
to { transform: translateX(-50%); }
|
|
116
113
|
}
|
|
114
|
+
@keyframes feature-marquee-right {
|
|
115
|
+
from { transform: translateX(-50%); }
|
|
116
|
+
to { transform: translateX(0); }
|
|
117
|
+
}
|
|
117
118
|
`}</style>
|
|
118
119
|
</section>
|
|
119
120
|
);
|
package/src/marketing/Footer.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
|
+
import { StatusIndicator } from '@olwiba/cn';
|
|
4
5
|
import type { AppShellRenderLink } from '../app/AppShell';
|
|
5
6
|
|
|
6
7
|
export interface FooterProps {
|
|
@@ -8,6 +9,7 @@ export interface FooterProps {
|
|
|
8
9
|
navLinks?: Array<{ label: string; href: string }>;
|
|
9
10
|
socialLinks?: Array<{ label: string; href: string; icon: ReactNode }>;
|
|
10
11
|
legal?: string;
|
|
12
|
+
status?: { label: string; operational?: boolean };
|
|
11
13
|
renderLink?: AppShellRenderLink;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -20,6 +22,7 @@ export function Footer({
|
|
|
20
22
|
navLinks,
|
|
21
23
|
socialLinks,
|
|
22
24
|
legal,
|
|
25
|
+
status,
|
|
23
26
|
renderLink = defaultRenderLink,
|
|
24
27
|
}: FooterProps) {
|
|
25
28
|
const brandHref = brand.href ?? '/';
|
|
@@ -77,6 +80,21 @@ export function Footer({
|
|
|
77
80
|
<p className="mt-10 text-center text-sm/6 text-muted-foreground/70">
|
|
78
81
|
{copyrightText}
|
|
79
82
|
</p>
|
|
83
|
+
|
|
84
|
+
{/* Status */}
|
|
85
|
+
{status && (
|
|
86
|
+
<div className="mt-4 flex justify-center">
|
|
87
|
+
<div className="inline-flex items-center rounded-full border bg-muted/40 px-3 py-1 text-xs text-muted-foreground">
|
|
88
|
+
<StatusIndicator
|
|
89
|
+
pulse={status.operational !== false}
|
|
90
|
+
dotClassName={status.operational !== false ? 'text-emerald-500' : 'text-destructive'}
|
|
91
|
+
size="sm"
|
|
92
|
+
>
|
|
93
|
+
{status.label}
|
|
94
|
+
</StatusIndicator>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
80
98
|
</div>
|
|
81
99
|
</footer>
|
|
82
100
|
);
|