PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / admin / analytics / components / common / ui.js

ui.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/admin/analytics/components/common/ui.js

293 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React from "react";
2 import { __ } from "@wordpress/i18n";
3 import { I } from "./icons";
4 import { Sparkline } from "./charts";
5
6 export function cn( ...c ) { return c.filter( Boolean ).join( " " ); }
7
8 export const fmt = ( n ) => ( Number( n ) || 0 ).toLocaleString();
9 export const fmtCompact = ( n ) => {
10 n = Number( n ) || 0;
11 if ( n < 1000 ) return String( n );
12 for ( const u of [ { v: 1e9, s: "B" }, { v: 1e6, s: "M" }, { v: 1e3, s: "K" } ] ) {
13 if ( n >= u.v ) return ( n / u.v ).toFixed( n % u.v === 0 ? 0 : 1 ).replace( /\.0$/, "" ) + u.s;
14 }
15 return String( n );
16 };
17
18 export function Btn( { kind = "ghost", size = "md", icon, children, className, ...rest } ) {
19 return (
20 <button className={ cn( "bda-btn", kind, size === "sm" && "sm", className ) } { ...rest }>
21 { icon }{ children }
22 </button>
23 );
24 }
25
26 export function Badge( { children, color = "slate", className } ) {
27 return <span className={ cn( "bda-badge", color, className ) }>{ children }</span>;
28 }
29
30 /* Design-system "Pro" badge — same markup quickbuilder renders in Settings
31 (crown glyph from the betterdocs `btd-icon` font + amber tokens). */
32 export function ProBadge( { className } ) {
33 return (
34 <span className={ cn( "wprf-badge", className ) }>
35 <span className="wprf-badge-icon"><i className="btd-icon btd-crown" /></span>
36 <span className="wprf-badge-item">{ __( "Pro", "betterdocs" ) }</span>
37 </span>
38 );
39 }
40
41 export function Card( { children, className, title, action, padded = true } ) {
42 return (
43 <div className={ cn( "bda-card", className ) }>
44 { ( title || action ) && (
45 <div className="bda-card-head">
46 { typeof title === "string" ? <h3 className="bda-card-title">{ title }</h3> : title }
47 { action }
48 </div>
49 ) }
50 <div className={ padded ? "bda-card-body" : "bda-card-body flush" }>{ children }</div>
51 </div>
52 );
53 }
54
55 export function Avatar( { author, size = 24 } ) {
56 if ( ! author ) return null;
57 return (
58 <span className="bda-avatar" style={ { width: size, height: size, fontSize: Math.max( 9, size * 0.42 ), background: `hsl(${ author.hue }, 62%, 50%)` } }>
59 { author.initials }
60 </span>
61 );
62 }
63
64 export function Trend( { value, suffix = "%" } ) {
65 if ( value == null ) return null;
66 const up = value >= 0;
67 return (
68 <span className={ cn( "bda-trend", up ? "up" : "down" ) }>
69 { up ? <I.ArrowUp size={ 11 } strokeWidth={ 2.5 } /> : <I.ArrowDown size={ 11 } strokeWidth={ 2.5 } /> }
70 { Math.abs( value ).toFixed( 1 ) }{ suffix }
71 </span>
72 );
73 }
74
75 export function HelpBar( { score } ) {
76 const color = score >= 80 ? "#00b884" : score >= 60 ? "#F59E0B" : "#EF4444";
77 return (
78 <div className="bda-helpbar">
79 <div className="bda-helpbar-track"><div className="bda-helpbar-fill" style={ { width: `${ score }%`, background: color } } /></div>
80 <div className="bda-helpbar-val">{ score }%</div>
81 </div>
82 );
83 }
84
85 export function KpiCard( { label, value, spark, color = "#00b884", icon, delta, dir } ) {
86 const up = dir > 0;
87 return (
88 <div className="bda-kpi">
89 <div className="bda-kpi-top">
90 <div className="bda-kpi-label">{ label }</div>
91 { icon && <div className="bda-kpi-ico" style={ { background: color + "1A", color } }>{ icon }</div> }
92 </div>
93 <div className="bda-kpi-row">
94 <div>
95 <div className="bda-kpi-value">{ value }</div>
96 { delta != null && (
97 <div className={ cn( "bda-trend", up ? "up" : "down" ) } style={ { marginTop: 8 } }>
98 { up ? <I.ArrowUp size={ 12 } strokeWidth={ 2.5 } /> : <I.ArrowDown size={ 12 } strokeWidth={ 2.5 } /> }
99 <span>{ Math.abs( delta ).toFixed( 1 ) }%</span>
100 <span style={ { color: "var(--text-color-500)", fontWeight: 400, marginLeft: 4 } }>vs prev</span>
101 </div>
102 ) }
103 </div>
104 { spark && <Sparkline points={ spark } color={ color } width={ 92 } height={ 36 } /> }
105 </div>
106 </div>
107 );
108 }
109
110 export function Dropdown( { label, value, options, onChange, icon, align = "left", minWidth = 180 } ) {
111 const [ open, setOpen ] = React.useState( false );
112 const ref = React.useRef( null );
113 React.useEffect( () => {
114 const h = ( e ) => { if ( ref.current && ! ref.current.contains( e.target ) ) setOpen( false ); };
115 document.addEventListener( "mousedown", h );
116 return () => document.removeEventListener( "mousedown", h );
117 }, [] );
118 const selected = options.find( ( o ) => o.value === value ) || options[ 0 ];
119 return (
120 <div ref={ ref } className="bda-dd">
121 <button className="bda-dd-btn" onClick={ () => setOpen( ( o ) => ! o ) }>
122 { icon }
123 { label && <span className="lbl">{ label }:</span> }
124 <span className="val">{ selected?.label }</span>
125 <I.ChevronDown size={ 14 } style={ { color: "var(--text-color-500)" } } />
126 </button>
127 { open && (
128 <div className={ cn( "bda-dd-menu", align === "right" && "right" ) } style={ { minWidth } }>
129 { options.map( ( o ) => (
130 <button key={ o.value } className={ cn( "bda-dd-opt", o.value === value && "sel" ) }
131 onClick={ () => { onChange( o.value ); setOpen( false ); } }>
132 <span>{ o.label }</span>
133 { o.value === value && <I.Check size={ 14 } style={ { color: "#00b884" } } /> }
134 </button>
135 ) ) }
136 </div>
137 ) }
138 </div>
139 );
140 }
141
142 const DATE_PRESETS = [
143 { value: "7", label: __( "Last 7 days", "betterdocs" ) },
144 { value: "14", label: __( "Last 14 days", "betterdocs" ) },
145 { value: "30", label: __( "Last 30 days", "betterdocs" ) }
146 ];
147 export function DateRange( { value, onChange } ) {
148 return <Dropdown icon={ <I.Calendar size={ 14 } style={ { color: "var(--text-color-600)" } } /> } value={ String( value ) } options={ DATE_PRESETS } onChange={ ( v ) => onChange( parseInt( v, 10 ) ) } minWidth={ 170 } />;
149 }
150
151 export function Pagination( { page, totalPages, onPage, perPage = 20, total } ) {
152 if ( totalPages <= 1 ) return (
153 <div className="bda-pag"><div className="bda-pag-info">{ total } { __( "items", "betterdocs" ) }</div></div>
154 );
155 const pages = [];
156 for ( let i = 1; i <= totalPages; i++ ) {
157 if ( i === 1 || i === totalPages || Math.abs( i - page ) <= 1 ) pages.push( i );
158 else if ( pages[ pages.length - 1 ] !== "…" ) pages.push( "…" );
159 }
160 return (
161 <div className="bda-pag">
162 <div className="bda-pag-info">
163 { __( "Showing", "betterdocs" ) } <b>{ ( page - 1 ) * perPage + 1 }–{ Math.min( page * perPage, total ) }</b> { __( "of", "betterdocs" ) } <b>{ total }</b>
164 </div>
165 <div className="bda-pag-btns">
166 <button disabled={ page <= 1 } onClick={ () => onPage( page - 1 ) }><I.ChevronLeft size={ 16 } /></button>
167 { pages.map( ( p, i ) => p === "…"
168 ? <span key={ i } style={ { width: 32, textAlign: "center", color: "var(--text-color-500)" } }>…</span>
169 : <button key={ i } className={ p === page ? "cur" : "" } onClick={ () => onPage( p ) }>{ p }</button> ) }
170 <button disabled={ page >= totalPages } onClick={ () => onPage( page + 1 ) }><I.ChevronRight size={ 16 } /></button>
171 </div>
172 </div>
173 );
174 }
175
176 /* ── Empty / error / loading states (Claude design "States gallery") ──
177 illustration: "data" | "search" | "error" */
178 export function EmptyArt( { kind = "data" } ) {
179 if ( kind === "search" ) {
180 return (
181 <svg width="120" height="100" viewBox="0 0 120 100" fill="none" aria-hidden="true">
182 <ellipse cx="60" cy="92" rx="44" ry="4" fill="#EDF0F3" />
183 <rect x="22" y="20" width="60" height="58" rx="6" fill="#fff" stroke="#d1fadf" strokeWidth="1.5" />
184 <rect x="30" y="32" width="44" height="2.5" rx="1.25" fill="#d1fadf" />
185 <rect x="30" y="40" width="32" height="2.5" rx="1.25" fill="#EDF0F3" />
186 <rect x="30" y="48" width="36" height="2.5" rx="1.25" fill="#EDF0F3" />
187 <circle cx="84" cy="62" r="14" fill="#fff" stroke="#00b884" strokeWidth="2.5" />
188 <line x1="94" y1="72" x2="104" y2="82" stroke="#00b884" strokeWidth="3" strokeLinecap="round" />
189 </svg>
190 );
191 }
192 if ( kind === "error" ) {
193 return (
194 <svg width="120" height="100" viewBox="0 0 120 100" fill="none" aria-hidden="true">
195 <ellipse cx="60" cy="92" rx="44" ry="4" fill="#FEE2E2" />
196 <path d="M60 20 L96 80 H24 Z" fill="#FEF2F2" stroke="#EF4444" strokeWidth="2" strokeLinejoin="round" />
197 <line x1="60" y1="40" x2="60" y2="60" stroke="#EF4444" strokeWidth="3" strokeLinecap="round" />
198 <circle cx="60" cy="70" r="2.5" fill="#EF4444" />
199 </svg>
200 );
201 }
202 return (
203 <svg width="120" height="100" viewBox="0 0 120 100" fill="none" aria-hidden="true">
204 <ellipse cx="60" cy="92" rx="44" ry="4" fill="#EDF0F3" />
205 <rect x="22" y="22" width="76" height="58" rx="8" fill="#fff" stroke="#d1fadf" strokeWidth="1.5" />
206 <path d="M30 66 L42 54 L52 62 L66 46 L78 56 L90 44" stroke="#00b884" strokeWidth="2.2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
207 <circle cx="42" cy="54" r="2.5" fill="#00b884" /><circle cx="52" cy="62" r="2.5" fill="#00b884" />
208 <circle cx="66" cy="46" r="2.5" fill="#00b884" /><circle cx="78" cy="56" r="2.5" fill="#00b884" /><circle cx="90" cy="44" r="2.5" fill="#00b884" />
209 <rect x="30" y="30" width="22" height="3" rx="1.5" fill="#d1fadf" />
210 <rect x="30" y="36" width="14" height="2.5" rx="1.25" fill="#EDF0F3" />
211 </svg>
212 );
213 }
214
215 export function EmptyState( { illustration = "data", title, subtitle, actionLabel, onAction } ) {
216 return (
217 <div className="bda-empty">
218 <EmptyArt kind={ illustration } />
219 <h3>{ title }</h3>
220 { subtitle && <p>{ subtitle }</p> }
221 { actionLabel && (
222 <button className="bda-empty-action" onClick={ onAction }>
223 { actionLabel } <I.ArrowRight size={ 13 } />
224 </button>
225 ) }
226 </div>
227 );
228 }
229
230 export function ErrorState( { title, subtitle, code, onRetry } ) {
231 return (
232 <div className="bda-empty">
233 <EmptyArt kind="error" />
234 <h3>{ title || __( "Couldn't load this section", "betterdocs" ) }</h3>
235 <p>{ subtitle || __( "We hit a snag fetching this data. This is usually temporary — try again, or check that the analytics service is reachable.", "betterdocs" ) }</p>
236 { onRetry && (
237 <div className="bda-empty-actions">
238 <button className="bda-btn primary" onClick={ onRetry }><I.RefreshCw size={ 13 } />{ __( "Try again", "betterdocs" ) }</button>
239 </div>
240 ) }
241 { code && <code className="bda-empty-code">{ code }</code> }
242 </div>
243 );
244 }
245
246 export function SkeletonLine( { w = "100%", h = 10, className, style } ) {
247 return <div className={ cn( "bda-skel", className ) } style={ { width: w, height: h, ...style } } />;
248 }
249
250 export function KpiSkeleton() {
251 return (
252 <div className="bda-kpi">
253 <SkeletonLine w="50%" h={ 10 } style={ { marginBottom: 12 } } />
254 <div className="bda-kpi-row">
255 <div>
256 <SkeletonLine w={ 120 } h={ 26 } style={ { marginBottom: 8 } } />
257 <SkeletonLine w={ 80 } h={ 10 } />
258 </div>
259 <SkeletonLine w={ 90 } h={ 36 } />
260 </div>
261 </div>
262 );
263 }
264
265 export function TableRowSkeleton( { cols = 6 } ) {
266 return (
267 <tr>
268 { Array.from( { length: cols } ).map( ( _, i ) => (
269 <td key={ i }><SkeletonLine w={ i === 0 ? "70%" : "55%" } h={ 10 } /></td>
270 ) ) }
271 </tr>
272 );
273 }
274
275 export function Skeleton( { lines = 4 } ) {
276 return (
277 <div style={ { display: "flex", flexDirection: "column", gap: 12, padding: 8 } }>
278 { Array.from( { length: lines } ).map( ( _, i ) => (
279 <SkeletonLine key={ i } h={ 12 } w={ i === 0 ? "70%" : "100%" } />
280 ) ) }
281 </div>
282 );
283 }
284
285 /* Deterministic avatar descriptor from a name. */
286 export function authorFromName( name ) {
287 const n = String( name || "?" ).trim();
288 let hue = 0;
289 for ( let i = 0; i < n.length; i++ ) hue = ( hue * 31 + n.charCodeAt( i ) ) % 360;
290 const initials = n.split( /\s+/ ).slice( 0, 2 ).map( ( p ) => p[ 0 ] || "" ).join( "" ).toUpperCase() || "?";
291 return { hue, initials, name: n };
292 }
293