@flamingo-stack/openframe-frontend-core 0.0.745 → 0.0.747
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/build/export-map.json +1 -1
- package/dist/{chunk-PM5ELQBL.cjs → chunk-2CEQGGRN.cjs} +32 -32
- package/dist/{chunk-6IQDQQ56.cjs → chunk-4AWI6CCU.cjs} +38 -38
- package/dist/{chunk-75BF7YJR.cjs → chunk-4DT5YXTM.cjs} +13 -13
- package/dist/{chunk-YJRMCFX3.cjs → chunk-4MGZYY6U.cjs} +7 -7
- package/dist/{chunk-HVR7DPAY.cjs → chunk-6RTHHU3S.cjs} +4 -4
- package/dist/{chunk-DJO7YMT3.cjs → chunk-7Y5U3Y4S.cjs} +194 -194
- package/dist/{chunk-GAGNKWOC.cjs → chunk-BBM2ICOY.cjs} +2 -2
- package/dist/{chunk-4N7KGOBS.cjs → chunk-BEC43GHL.cjs} +3 -3
- package/dist/{chunk-GOTEXQZN.cjs → chunk-C7NTL2GV.cjs} +80 -80
- package/dist/{chunk-NFINQ7IZ.cjs → chunk-FCQCEQBY.cjs} +6 -6
- package/dist/{chunk-URVIPSAJ.cjs → chunk-HNLI4OU5.cjs} +14 -14
- package/dist/{chunk-HMSCUL4R.cjs → chunk-HTMHDEIV.cjs} +4 -4
- package/dist/{chunk-3LX5AGZO.cjs → chunk-IJA3OZWF.cjs} +24 -24
- package/dist/{chunk-ABLUMGCI.cjs → chunk-JHQNUWFM.cjs} +83 -33
- package/dist/{chunk-RIGSZEXS.cjs → chunk-JPSMPXCH.cjs} +90 -90
- package/dist/{chunk-UY6A6TEY.cjs → chunk-L2IQESTQ.cjs} +2 -2
- package/dist/{chunk-M4FKV2U5.cjs → chunk-LLAKQT3B.cjs} +12 -12
- package/dist/{chunk-GQ2TRIOV.cjs → chunk-MA7T36JY.cjs} +35 -35
- package/dist/{chunk-7KHQHYXL.cjs → chunk-O6QQ44I3.cjs} +18 -18
- package/dist/{chunk-LJ46XYYP.cjs → chunk-P7KTAXXE.cjs} +41 -27
- package/dist/{chunk-FNIFUCXU.cjs → chunk-PIQRV4EQ.cjs} +5 -5
- package/dist/{chunk-OWGLRE74.cjs → chunk-PSIKUDUV.cjs} +8 -8
- package/dist/{chunk-4A6OF4HI.cjs → chunk-PSSGIZXZ.cjs} +44 -44
- package/dist/{chunk-YIOWV7LS.cjs → chunk-QPSQ5WLG.cjs} +2 -2
- package/dist/{chunk-QES5NV4T.cjs → chunk-R3FXJXSQ.cjs} +1040 -1010
- package/dist/{chunk-7PQOZLVP.cjs → chunk-TYW7NBP7.cjs} +20 -20
- package/dist/{chunk-U2O5YQRP.cjs → chunk-U6XWGK53.cjs} +33 -33
- package/dist/{chunk-GEEKJ7SH.cjs → chunk-WY4FKKCV.cjs} +74 -74
- package/dist/{chunk-JGZEHBBD.cjs → chunk-YIRX6C7V.cjs} +10 -10
- package/dist/{chunk-5TCIRWHL.cjs → chunk-YRULLK7N.cjs} +5 -5
- package/dist/{cloud-tenants-DECPWIYQ.cjs → cloud-tenants-QJZIF2JS.cjs} +13 -13
- package/dist/components/case-studies/index.cjs +31 -29
- package/dist/components/case-studies/share-experience-section.d.ts.map +1 -1
- package/dist/components/case-studies/share-experience-section.js +6 -5
- package/dist/components/chat/index.cjs +9 -9
- package/dist/components/contact/index.cjs +14 -14
- package/dist/components/docs/index.cjs +17 -17
- package/dist/components/embeds/index.cjs +14 -14
- package/dist/components/faq/index.cjs +15 -15
- package/dist/components/features/index.cjs +13 -13
- package/dist/components/help-center-pages/index.cjs +116 -116
- package/dist/components/index.cjs +121 -125
- package/dist/components/layout/page-heading.d.ts +16 -0
- package/dist/components/layout/page-heading.d.ts.map +1 -1
- package/dist/components/layout/page-heading.js +16 -2
- package/dist/components/meeting-scheduler/index.cjs +63 -63
- package/dist/components/navigation/index.cjs +16 -16
- package/dist/components/onboarding-guides/index.cjs +16 -16
- package/dist/components/product-demo/index.cjs +9 -9
- package/dist/components/related-content/index.cjs +7 -7
- package/dist/components/tickets/index.cjs +19 -19
- package/dist/components/ui/filter-checkbox-item.d.ts +8 -2
- package/dist/components/ui/filter-checkbox-item.d.ts.map +1 -1
- package/dist/components/ui/filter-checkbox-item.js +14 -4
- package/dist/components/ui/index.cjs +15 -13
- package/dist/components/ui/layout-hold.d.ts +4 -3
- package/dist/components/ui/layout-hold.d.ts.map +1 -1
- package/dist/components/ui/layout-hold.js +29 -9
- package/dist/components/unified-filter-logic.js +1 -1
- package/dist/components/vendor-tag.d.ts +12 -0
- package/dist/components/vendor-tag.d.ts.map +1 -1
- package/dist/components/vendor-tag.js +50 -47
- package/dist/{devices-HJKDRWQX.cjs → devices-V55KQCAL.cjs} +14 -14
- package/dist/{embeddable-chat-3SKFBO2P.cjs → embeddable-chat-5CDR4I6E.cjs} +80 -80
- package/dist/index.cjs +22 -18
- package/dist/{knowledge-NH2CGY2C.cjs → knowledge-S3VGDRKR.cjs} +14 -14
- package/dist/{logs-KZYIDYQK.cjs → logs-K5E2BGZE.cjs} +21 -21
- package/dist/{policies-UAVTPIKM.cjs → policies-BWU4FRFE.cjs} +17 -17
- package/dist/{remote-session-N4PJB5FW.cjs → remote-session-IYT4LWJC.cjs} +7 -7
- package/dist/{scripts-FRPY43KU.cjs → scripts-25UPGXZU.cjs} +20 -20
- package/dist/{software-update-AEJJNLX5.cjs → software-update-F6UC7ULZ.cjs} +17 -17
- package/dist/{tickets-board-O753S4ZB.cjs → tickets-board-4MT6ZB4F.cjs} +8 -8
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +2 -3
- package/dist/utils/index.cjs +74 -24
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +8 -6
- package/dist/utils/vendor-classification.d.ts +43 -17
- package/dist/utils/vendor-classification.d.ts.map +1 -1
- package/dist/utils/vendor-classification.js +73 -22
- package/package.json +1 -1
- package/src/components/case-studies/share-experience-section.tsx +9 -6
- package/src/components/layout/page-heading.tsx +52 -11
- package/src/components/ui/filter-checkbox-item.tsx +21 -5
- package/src/components/ui/layout-hold.tsx +38 -10
- package/src/components/unified-filter-logic.tsx +1 -1
- package/src/components/vendor-tag.tsx +73 -69
- package/src/utils/format.ts +2 -3
- package/src/utils/index.ts +7 -5
- package/src/utils/vendor-classification.ts +113 -34
- package/dist/utils/format-text-stub.d.ts +0 -5
- package/dist/utils/format-text-stub.d.ts.map +0 -1
- package/dist/utils/format-text-stub.js +0 -20
- package/src/utils/format-text-stub.ts +0 -17
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, type RefObject, useLayoutEffect, useRef, useState } from 'react';
|
|
2
4
|
import { cn } from '../../utils/cn';
|
|
5
|
+
import { SkeletonText } from './skeleton';
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Holding a layout while its content is read, the two ways that are right at
|
|
@@ -10,7 +13,8 @@ import { cn } from '../../utils/cn';
|
|
|
10
13
|
*
|
|
11
14
|
* - `TextHold`: an inline stand-in exactly as wide as the words it is given
|
|
12
15
|
* (same font, same letter spacing, because it IS those words, not shown).
|
|
13
|
-
* `bar` draws
|
|
16
|
+
* `bar` draws a `SkeletonText` of as many lines as the words take; `blank`
|
|
17
|
+
* only keeps their space.
|
|
14
18
|
* - `LayoutHold`: a block as tall as the real layout rendered inside it, kept
|
|
15
19
|
* invisible and inert: the layout's own height, at any width.
|
|
16
20
|
*
|
|
@@ -19,27 +23,51 @@ import { cn } from '../../utils/cn';
|
|
|
19
23
|
export interface TextHoldProps {
|
|
20
24
|
/** The words whose width is held (the content's own, or a stand-in of its length). */
|
|
21
25
|
children: ReactNode;
|
|
22
|
-
/** `bar`: a
|
|
26
|
+
/** `bar`: a `SkeletonText` of as many lines as the words take. `blank`: the space only. */
|
|
23
27
|
variant?: 'bar' | 'blank';
|
|
24
28
|
className?: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export function TextHold({ children, variant = 'bar', className }: TextHoldProps) {
|
|
32
|
+
const hold = useRef<HTMLSpanElement>(null);
|
|
33
|
+
const words = useRef<HTMLSpanElement>(null);
|
|
34
|
+
const lines = useLineCount(hold, words, variant === 'bar');
|
|
28
35
|
return (
|
|
29
36
|
<span
|
|
37
|
+
ref={hold}
|
|
30
38
|
aria-hidden
|
|
31
|
-
className={cn(
|
|
32
|
-
variant === 'bar' ? 'animate-pulse rounded bg-ods-border text-transparent' : 'invisible',
|
|
33
|
-
// A hold is never selectable or clickable, whatever it wraps.
|
|
34
|
-
'pointer-events-none select-none',
|
|
35
|
-
className,
|
|
36
|
-
)}
|
|
39
|
+
className={cn('pointer-events-none select-none', variant === 'bar' && 'relative inline-block', className)}
|
|
37
40
|
>
|
|
38
|
-
{
|
|
41
|
+
<span ref={words} className="invisible">
|
|
42
|
+
{children}
|
|
43
|
+
</span>
|
|
44
|
+
{lines > 0 ? (
|
|
45
|
+
// `justify-around`: each `Skeleton` sits in the middle of the line it stands for.
|
|
46
|
+
<SkeletonText lines={lines} className="absolute inset-0 flex flex-col justify-around space-y-0" />
|
|
47
|
+
) : null}
|
|
39
48
|
</span>
|
|
40
49
|
);
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
/** How many lines the words take (one box per line), kept current as the width changes. */
|
|
53
|
+
function useLineCount(
|
|
54
|
+
hold: RefObject<HTMLSpanElement | null>,
|
|
55
|
+
words: RefObject<HTMLSpanElement | null>,
|
|
56
|
+
enabled: boolean,
|
|
57
|
+
): number {
|
|
58
|
+
const [lines, setLines] = useState(0);
|
|
59
|
+
useLayoutEffect(() => {
|
|
60
|
+
const box = hold.current;
|
|
61
|
+
if (!enabled || !box) return undefined;
|
|
62
|
+
const measure = () => setLines(words.current?.getClientRects().length ?? 0);
|
|
63
|
+
measure();
|
|
64
|
+
const observer = new ResizeObserver(measure);
|
|
65
|
+
observer.observe(box);
|
|
66
|
+
return () => observer.disconnect();
|
|
67
|
+
}, [hold, words, enabled]);
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
export interface LayoutHoldProps {
|
|
44
72
|
/** The real layout, rendered with the data at hand. */
|
|
45
73
|
children: ReactNode;
|
|
@@ -397,7 +397,7 @@ export function useUnifiedFiltering(config: FilterConfig) {
|
|
|
397
397
|
};
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
import { formatClassification, formatPricingModel } from '../utils/format
|
|
400
|
+
import { formatClassification, formatPricingModel } from '../utils/format';
|
|
401
401
|
|
|
402
402
|
// Helper functions for filter labels
|
|
403
403
|
function getFilterLabel(filterKey: string): string {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Boxes,
|
|
3
|
+
import { Boxes, Database, DollarSign, Hand, Plug, ShieldCheck, Sparkles } from 'lucide-react';
|
|
4
|
+
import { createElement, type ReactNode } from 'react';
|
|
4
5
|
import { cn } from '../utils/cn';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type OpenFrameClassification,
|
|
9
|
-
} from '../utils/vendor-classification';
|
|
10
|
-
import { OpenframeLogoIcon } from './icons-v2-generated/logos/openframe-logo-icon';
|
|
6
|
+
import { formatClassification } from '../utils/format';
|
|
7
|
+
import { type VendorClassificationIconConfig, vendorClassificationCopy } from '../utils/vendor-classification';
|
|
8
|
+
import { resolveIcon } from './chat/utils/icon-library';
|
|
11
9
|
|
|
12
10
|
export interface VendorTagProps {
|
|
13
11
|
type:
|
|
@@ -36,12 +34,69 @@ export interface VendorTagProps {
|
|
|
36
34
|
accentColor?: string;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const TONE_CLASS: Readonly<Record<VendorClassificationIconConfig['tone'], string>> = {
|
|
38
|
+
accent: 'text-ods-text-on-accent',
|
|
39
|
+
neutral: 'text-ods-text-secondary',
|
|
40
|
+
warning: 'text-ods-warning',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A classification's icon, from its config (`VENDOR_CLASSIFICATION_COPY[value].icon`): the
|
|
45
|
+
* icon is resolved by NAME through the shared resolver, never imported here. A license icon
|
|
46
|
+
* sits on a small tile (the accent tile for an `accent` tone); an OpenFrame mark and a
|
|
47
|
+
* lifecycle icon are drawn bare in their tone. A value with no icon draws nothing.
|
|
48
|
+
*/
|
|
49
|
+
export function VendorClassificationIcon({
|
|
50
|
+
classification,
|
|
51
|
+
accentColor,
|
|
52
|
+
className,
|
|
53
|
+
}: {
|
|
54
|
+
classification: string;
|
|
55
|
+
/** The accent tile's colour, for a host that tints its hover accent (a vendor card). */
|
|
56
|
+
accentColor?: string;
|
|
57
|
+
className?: string;
|
|
58
|
+
}) {
|
|
59
|
+
const copy = vendorClassificationCopy(classification);
|
|
60
|
+
if (!copy?.icon) return null;
|
|
61
|
+
// The resolver returns one component per name, loaded on demand (drawn through `createElement`, as `NamedIcon` does).
|
|
62
|
+
const icon = resolveIcon(copy.icon.name);
|
|
63
|
+
if (copy.group !== 'license') {
|
|
64
|
+
const bareClass = copy.icon.tone === 'accent' ? 'text-ods-accent' : TONE_CLASS[copy.icon.tone];
|
|
65
|
+
return createElement(icon, {
|
|
66
|
+
size: copy.group === 'openframe' ? 16 : 12,
|
|
67
|
+
className: cn('shrink-0', bareClass, className),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const accent = copy.icon.tone === 'accent';
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
'flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm',
|
|
75
|
+
accent ? 'bg-ods-accent' : 'bg-ods-border',
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
style={accent && accentColor ? { backgroundColor: accentColor } : undefined}
|
|
79
|
+
>
|
|
80
|
+
{createElement(icon, { size: 10, className: TONE_CLASS[copy.icon.tone] })}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A classification tag, derived from the value the server sends and its config: the label
|
|
87
|
+
* (an OpenFrame value's one word), the configured icon, and the warning text colour for a
|
|
88
|
+
* lifecycle value. Any other value is its title-cased label with no icon.
|
|
89
|
+
*/
|
|
90
|
+
function classificationTag(
|
|
91
|
+
value: string,
|
|
92
|
+
text?: string,
|
|
93
|
+
accentColor?: string,
|
|
94
|
+
): { text: string; icon: ReactNode; textColor?: string } {
|
|
95
|
+
const copy = vendorClassificationCopy(value);
|
|
41
96
|
return {
|
|
42
|
-
text: text ||
|
|
43
|
-
textColor: undefined,
|
|
44
|
-
icon: <
|
|
97
|
+
text: text || (copy?.group === 'openframe' ? copy.word : undefined) || copy?.label || formatClassification(value),
|
|
98
|
+
textColor: copy?.group === 'lifecycle' ? 'text-ods-warning' : undefined,
|
|
99
|
+
icon: <VendorClassificationIcon classification={value} accentColor={accentColor} />,
|
|
45
100
|
};
|
|
46
101
|
}
|
|
47
102
|
|
|
@@ -53,7 +108,7 @@ export function VendorTag({ type, text, className = '', hidden = false, size = '
|
|
|
53
108
|
);
|
|
54
109
|
|
|
55
110
|
// Get display text and styling based on type
|
|
56
|
-
const getTagContent = () => {
|
|
111
|
+
const getTagContent = (): { text: string; icon: ReactNode; textColor?: string } => {
|
|
57
112
|
switch (type) {
|
|
58
113
|
case 'placeholder':
|
|
59
114
|
return {
|
|
@@ -86,26 +141,9 @@ export function VendorTag({ type, text, className = '', hidden = false, size = '
|
|
|
86
141
|
),
|
|
87
142
|
};
|
|
88
143
|
case 'open-source':
|
|
89
|
-
return
|
|
90
|
-
text: text || 'Open Source',
|
|
91
|
-
icon: (
|
|
92
|
-
<div
|
|
93
|
-
className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent"
|
|
94
|
-
style={accentColor ? { backgroundColor: accentColor } : undefined}
|
|
95
|
-
>
|
|
96
|
-
<Code width={10} height={10} className="text-ods-text-on-accent" />
|
|
97
|
-
</div>
|
|
98
|
-
),
|
|
99
|
-
};
|
|
144
|
+
return classificationTag('open_source', text, accentColor);
|
|
100
145
|
case 'commercial':
|
|
101
|
-
return
|
|
102
|
-
text: text || 'Commercial Vendor',
|
|
103
|
-
icon: (
|
|
104
|
-
<div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border">
|
|
105
|
-
<DollarSign width={10} height={10} className="text-ods-text-secondary" />
|
|
106
|
-
</div>
|
|
107
|
-
),
|
|
108
|
-
};
|
|
146
|
+
return classificationTag('commercial', text);
|
|
109
147
|
// platform-capability tags — same neutral icon-box chrome as 'commercial'
|
|
110
148
|
case 'api':
|
|
111
149
|
return {
|
|
@@ -188,46 +226,12 @@ export function VendorTag({ type, text, className = '', hidden = false, size = '
|
|
|
188
226
|
</div>
|
|
189
227
|
),
|
|
190
228
|
};
|
|
191
|
-
case 'classification':
|
|
192
|
-
|
|
193
|
-
const classificationType = text?.toLowerCase();
|
|
194
|
-
|
|
195
|
-
if (classificationType === 'open_source') {
|
|
196
|
-
return {
|
|
197
|
-
text: 'Open Source',
|
|
198
|
-
icon: (
|
|
199
|
-
<div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent">
|
|
200
|
-
<Code width={10} height={10} className="text-ods-text-on-accent" />
|
|
201
|
-
</div>
|
|
202
|
-
),
|
|
203
|
-
};
|
|
204
|
-
} else if (classificationType === 'commercial') {
|
|
205
|
-
return {
|
|
206
|
-
text: 'Commercial Vendor',
|
|
207
|
-
icon: (
|
|
208
|
-
<div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border">
|
|
209
|
-
<DollarSign width={10} height={10} className="text-ods-text-secondary" />
|
|
210
|
-
</div>
|
|
211
|
-
),
|
|
212
|
-
};
|
|
213
|
-
} else if (isOpenFrameClassification(classificationType)) {
|
|
214
|
-
return openFrameTag(classificationType);
|
|
215
|
-
} else {
|
|
216
|
-
// Fallback for unknown classification types
|
|
217
|
-
return {
|
|
218
|
-
text: text || 'Classification',
|
|
219
|
-
icon: (
|
|
220
|
-
<div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent">
|
|
221
|
-
<span className="text-[8px] font-bold text-ods-text-on-accent">C</span>
|
|
222
|
-
</div>
|
|
223
|
-
),
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
229
|
+
case 'classification':
|
|
230
|
+
return classificationTag((text ?? '').toLowerCase(), undefined, accentColor);
|
|
227
231
|
case 'openframe_selected':
|
|
228
232
|
case 'openframe_connect':
|
|
229
233
|
case 'openframe_shipped':
|
|
230
|
-
return
|
|
234
|
+
return classificationTag(type, text);
|
|
231
235
|
default:
|
|
232
236
|
return {
|
|
233
237
|
text: text || type,
|
package/src/utils/format.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ProgramInstant } from './program-instant';
|
|
6
|
-
import {
|
|
6
|
+
import { vendorClassificationCopy } from './vendor-classification';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Format a date to a human-readable string
|
|
@@ -941,8 +941,7 @@ export function stripHtml(html: string): string {
|
|
|
941
941
|
* curated mapping.
|
|
942
942
|
*/
|
|
943
943
|
export function formatClassification(classification: string): string {
|
|
944
|
-
|
|
945
|
-
return formatUnderscoreText(classification);
|
|
944
|
+
return vendorClassificationCopy(classification)?.label ?? formatUnderscoreText(classification);
|
|
946
945
|
}
|
|
947
946
|
|
|
948
947
|
/**
|
package/src/utils/index.ts
CHANGED
|
@@ -21,13 +21,15 @@ export * from './ai-providers';
|
|
|
21
21
|
export * from './model-token-rates';
|
|
22
22
|
export { TRIAL_TERMS, trialLabels, trialLengthLabel } from './trial';
|
|
23
23
|
export {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
VENDOR_CLASSIFICATION_COPY,
|
|
25
|
+
vendorClassificationCopy,
|
|
26
|
+
vendorClassificationGroup,
|
|
27
|
+
compareVendorClassifications,
|
|
27
28
|
isOpenFrameClassification,
|
|
28
29
|
type VendorClassificationValue,
|
|
29
|
-
type
|
|
30
|
-
type
|
|
30
|
+
type VendorClassificationGroup,
|
|
31
|
+
type VendorClassificationCopy,
|
|
32
|
+
type VendorClassificationIconConfig,
|
|
31
33
|
} from './vendor-classification';
|
|
32
34
|
// SVG path constants — re-exported here (server-safe) because icons-v2 has "use client"
|
|
33
35
|
export { PLAY_ICON_PATH } from '../components/icons-v2-generated/media-playback/play-icon';
|
|
@@ -1,59 +1,138 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vendor classifications:
|
|
2
|
+
* Vendor classifications: whatever `vendor_classification.classification` holds on the
|
|
3
|
+
* server. The database owns the vocabulary (its CHECK constraint is the list), the catalog
|
|
4
|
+
* builds its filters from the rows that exist, and nothing here limits which values are valid.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* This module only ENRICHES the values we want to present specially: a curated label, a
|
|
7
|
+
* tooltip description and a group. A value with no entry still works everywhere, labelled
|
|
8
|
+
* by title case. To present a new value better, add an entry here; to make it exist, add it
|
|
9
|
+
* to the database.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
* - `
|
|
10
|
-
* - `
|
|
11
|
+
* Groups:
|
|
12
|
+
* - `license`: how the software is licensed.
|
|
13
|
+
* - `lifecycle`: where the product is in its life (a tag and a filter; a card's one tag stays the license).
|
|
14
|
+
* - `openframe`: drawn with the OpenFrame mark and ONE word (`word`).
|
|
11
15
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
] as const;
|
|
19
|
-
|
|
20
|
-
export type VendorClassificationValue = (typeof VENDOR_CLASSIFICATIONS)[number];
|
|
21
|
-
|
|
22
|
-
export type OpenFrameClassification = Extract<VendorClassificationValue, `openframe_${string}`>;
|
|
23
|
-
|
|
24
|
-
export interface OpenFrameClassificationCopy {
|
|
25
|
-
/** The word drawn beside the OpenFrame mark. */
|
|
26
|
-
word: string;
|
|
27
|
-
/** The full name, for plain-text surfaces (a select option, an aria label). */
|
|
16
|
+
export type VendorClassificationValue = string;
|
|
17
|
+
|
|
18
|
+
export type VendorClassificationGroup = 'license' | 'lifecycle' | 'openframe';
|
|
19
|
+
|
|
20
|
+
export interface VendorClassificationCopy {
|
|
21
|
+
/** The label on a tag, a filter and a plain-text surface. */
|
|
28
22
|
label: string;
|
|
29
23
|
/** What the classification means, for a tooltip. */
|
|
30
24
|
description: string;
|
|
25
|
+
group: VendorClassificationGroup;
|
|
26
|
+
/** OpenFrame group only: the one word drawn beside the OpenFrame mark. */
|
|
27
|
+
word?: string;
|
|
28
|
+
/** The icon drawn beside the label, by name (resolved by the shared icon resolver), and its tone. */
|
|
29
|
+
icon?: VendorClassificationIconConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A classification's icon, as data: `name` is any name the shared resolver knows (an alias such
|
|
34
|
+
* as `openframe-logo`, an icon of the design set by its kebab name, or a lucide name). `tone`:
|
|
35
|
+
* `accent` (the brand colour), `neutral` (secondary text on a tile) or `warning`.
|
|
36
|
+
*/
|
|
37
|
+
export interface VendorClassificationIconConfig {
|
|
38
|
+
name: string;
|
|
39
|
+
tone: 'accent' | 'neutral' | 'warning';
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
/** In display order: a filter list shows OpenFrame first, then license, then lifecycle. */
|
|
43
|
+
export const VENDOR_CLASSIFICATION_COPY: Readonly<Record<string, VendorClassificationCopy>> = {
|
|
34
44
|
openframe_selected: {
|
|
35
|
-
word: 'Selected',
|
|
36
45
|
label: 'OpenFrame Selected',
|
|
46
|
+
word: 'Selected',
|
|
37
47
|
description:
|
|
38
48
|
'OpenFrame is a unified platform that integrates multiple open-source IT and security tools into a single dashboard for MSPs. This filter shows vendors selected by our team for their excellence, community support, and MSP-specific value.',
|
|
49
|
+
group: 'openframe',
|
|
50
|
+
icon: { name: 'openframe-logo', tone: 'accent' },
|
|
51
|
+
},
|
|
52
|
+
openframe_connect: {
|
|
53
|
+
label: 'OpenFrame Connect',
|
|
54
|
+
word: 'Connect',
|
|
55
|
+
description:
|
|
56
|
+
'Services OpenFrame connects to and manages for every client, such as Microsoft 365 and Google Workspace, beside devices and tickets.',
|
|
57
|
+
group: 'openframe',
|
|
58
|
+
icon: { name: 'openframe-logo', tone: 'accent' },
|
|
39
59
|
},
|
|
40
60
|
openframe_shipped: {
|
|
41
|
-
word: 'Shipped',
|
|
42
61
|
label: 'OpenFrame Shipped',
|
|
62
|
+
word: 'Shipped',
|
|
43
63
|
description: 'Tools and OpenFrame products that are in the released OpenFrame product today, not on the roadmap.',
|
|
64
|
+
group: 'openframe',
|
|
65
|
+
icon: { name: 'openframe-logo', tone: 'accent' },
|
|
44
66
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
67
|
+
open_source: {
|
|
68
|
+
label: 'Open Source',
|
|
69
|
+
description: 'Released under an OSI-approved open-source license.',
|
|
70
|
+
group: 'license',
|
|
71
|
+
icon: { name: 'code', tone: 'accent' },
|
|
72
|
+
},
|
|
73
|
+
source_available: {
|
|
74
|
+
label: 'Source-available',
|
|
48
75
|
description:
|
|
49
|
-
'
|
|
76
|
+
'The source code is public, but the license restricts how it may be used (for example SSPL, BSL, the Elastic License, or limits on company size or on offering it as a service).',
|
|
77
|
+
group: 'license',
|
|
78
|
+
icon: { name: 'file-code', tone: 'neutral' },
|
|
79
|
+
},
|
|
80
|
+
commercial: {
|
|
81
|
+
label: 'Commercial',
|
|
82
|
+
description: 'Proprietary software sold by the vendor.',
|
|
83
|
+
group: 'license',
|
|
84
|
+
icon: { name: 'dollar', tone: 'neutral' },
|
|
85
|
+
},
|
|
86
|
+
end_of_life: {
|
|
87
|
+
label: 'End of life',
|
|
88
|
+
description: 'The vendor has discontinued or retired the product. No new releases.',
|
|
89
|
+
group: 'lifecycle',
|
|
90
|
+
icon: { name: 'alert-triangle', tone: 'warning' },
|
|
91
|
+
},
|
|
92
|
+
end_of_support: {
|
|
93
|
+
label: 'End of support',
|
|
94
|
+
description: 'The vendor no longer provides support or security fixes, though the product can still be obtained.',
|
|
95
|
+
group: 'lifecycle',
|
|
96
|
+
icon: { name: 'clock', tone: 'warning' },
|
|
97
|
+
},
|
|
98
|
+
unmaintained: {
|
|
99
|
+
label: 'Unmaintained',
|
|
100
|
+
description: 'No release for years and no official end-of-life notice.',
|
|
101
|
+
group: 'lifecycle',
|
|
102
|
+
icon: { name: 'pause-circle', tone: 'warning' },
|
|
50
103
|
},
|
|
51
104
|
};
|
|
52
105
|
|
|
53
|
-
|
|
54
|
-
|
|
106
|
+
/** The enrichment of a classification, or undefined for a value we do not present specially. */
|
|
107
|
+
export function vendorClassificationCopy(value: string | null | undefined): VendorClassificationCopy | undefined {
|
|
108
|
+
if (!value || !Object.prototype.hasOwnProperty.call(VENDOR_CLASSIFICATION_COPY, value)) return undefined;
|
|
109
|
+
return VENDOR_CLASSIFICATION_COPY[value];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The group of a classification, or undefined for a value with no enrichment. */
|
|
113
|
+
export function vendorClassificationGroup(value: string | null | undefined): VendorClassificationGroup | undefined {
|
|
114
|
+
return vendorClassificationCopy(value)?.group;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A classification drawn with the OpenFrame mark (it has a `word`). */
|
|
118
|
+
export function isOpenFrameClassification(value: string | null | undefined): boolean {
|
|
119
|
+
return vendorClassificationGroup(value) === 'openframe';
|
|
55
120
|
}
|
|
56
121
|
|
|
57
|
-
|
|
58
|
-
|
|
122
|
+
const DISPLAY_ORDER = Object.keys(VENDOR_CLASSIFICATION_COPY);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Sort order for a list of classifications (a filter list): the enriched values in their
|
|
126
|
+
* display order (OpenFrame Selected, Connect, Shipped, then license, then lifecycle), then
|
|
127
|
+
* every other value the server holds, alphabetically.
|
|
128
|
+
*/
|
|
129
|
+
export function compareVendorClassifications(a: string, b: string): number {
|
|
130
|
+
const ia = DISPLAY_ORDER.indexOf(a);
|
|
131
|
+
const ib = DISPLAY_ORDER.indexOf(b);
|
|
132
|
+
if (ia !== -1 || ib !== -1) {
|
|
133
|
+
if (ia === -1) return 1;
|
|
134
|
+
if (ib === -1) return -1;
|
|
135
|
+
return ia - ib;
|
|
136
|
+
}
|
|
137
|
+
return a.localeCompare(b);
|
|
59
138
|
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export declare function formatText(text: string): string;
|
|
2
|
-
export declare function truncateText(text: string, maxLength: number): string;
|
|
3
|
-
export declare function formatClassification(classification: string): string;
|
|
4
|
-
export declare function formatPricingModel(pricing: string): string;
|
|
5
|
-
//# sourceMappingURL=format-text-stub.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"format-text-stub.d.ts","sourceRoot":"","sources":["../../src/utils/format-text-stub.ts"],"names":[],"mappings":"AACA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D"}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
function formatText(text) {
|
|
3
|
-
return text;
|
|
4
|
-
}
|
|
5
|
-
function truncateText(text, maxLength) {
|
|
6
|
-
if (text.length <= maxLength) return text;
|
|
7
|
-
return text.slice(0, maxLength) + "...";
|
|
8
|
-
}
|
|
9
|
-
function formatClassification(classification) {
|
|
10
|
-
return classification.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
11
|
-
}
|
|
12
|
-
function formatPricingModel(pricing) {
|
|
13
|
-
return pricing.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
14
|
-
}
|
|
15
|
-
export {
|
|
16
|
-
formatClassification,
|
|
17
|
-
formatPricingModel,
|
|
18
|
-
formatText,
|
|
19
|
-
truncateText
|
|
20
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Stub for format-text utilities
|
|
2
|
-
export function formatText(text: string): string {
|
|
3
|
-
return text;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function truncateText(text: string, maxLength: number): string {
|
|
7
|
-
if (text.length <= maxLength) return text;
|
|
8
|
-
return text.slice(0, maxLength) + '...';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function formatClassification(classification: string): string {
|
|
12
|
-
return classification.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function formatPricingModel(pricing: string): string {
|
|
16
|
-
return pricing.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
17
|
-
}
|