@flamingo-stack/openframe-frontend-core 0.0.744 → 0.0.746
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-LJ46XYYP.cjs → chunk-2ZVNXD74.cjs} +24 -24
- package/dist/{chunk-FTYQS6VI.cjs → chunk-3AUTORMB.cjs} +13 -13
- package/dist/{chunk-2JRCJV3M.cjs → chunk-3KVB5FQZ.cjs} +5 -5
- package/dist/{chunk-2UBR53AN.cjs → chunk-3OAACQQG.cjs} +4 -4
- package/dist/{chunk-QSO56JZ2.cjs → chunk-5X3F2KL7.cjs} +90 -90
- package/dist/{chunk-AT6ZI4CO.cjs → chunk-7AKFKPBV.cjs} +12 -12
- package/dist/{chunk-4N7KGOBS.cjs → chunk-B24U6KNJ.cjs} +3 -3
- package/dist/{chunk-H62FNZGG.cjs → chunk-DJ4YDVBF.cjs} +18 -18
- package/dist/{chunk-QZYF2YQ3.cjs → chunk-FAN73OTP.cjs} +6 -6
- package/dist/{chunk-HJ4KGA2M.cjs → chunk-GPSHFEF2.cjs} +1084 -890
- package/dist/{chunk-OC2LBQQJ.cjs → chunk-GSFANY2L.cjs} +80 -80
- package/dist/{chunk-URVIPSAJ.cjs → chunk-HNLI4OU5.cjs} +14 -14
- package/dist/{chunk-PYWHDPVC.cjs → chunk-IRKUFXVV.cjs} +38 -38
- package/dist/{chunk-ABLUMGCI.cjs → chunk-JHQNUWFM.cjs} +83 -33
- package/dist/{chunk-4A6OF4HI.cjs → chunk-KVPSV4ZI.cjs} +44 -44
- package/dist/{chunk-UY6A6TEY.cjs → chunk-L2IQESTQ.cjs} +2 -2
- package/dist/{chunk-YIOWV7LS.cjs → chunk-NZMQIAQC.cjs} +2 -2
- package/dist/{chunk-YJRMCFX3.cjs → chunk-O4J4NIMB.cjs} +7 -7
- package/dist/{chunk-GAGNKWOC.cjs → chunk-OWABNVDV.cjs} +2 -2
- package/dist/{chunk-HTQJYN47.cjs → chunk-PLBJ3XVC.cjs} +32 -32
- package/dist/{chunk-OWGLRE74.cjs → chunk-PSIKUDUV.cjs} +8 -8
- package/dist/{chunk-NURAL5HD.cjs → chunk-SDDZHPVK.cjs} +24 -24
- package/dist/{chunk-HHL23RFC.cjs → chunk-SLODGX6D.cjs} +35 -35
- package/dist/{chunk-7YDF3QBN.cjs → chunk-TILK7SC4.cjs} +320 -207
- package/dist/{chunk-U2O5YQRP.cjs → chunk-U6XWGK53.cjs} +33 -33
- package/dist/{chunk-JGZEHBBD.cjs → chunk-VZGCSVH7.cjs} +10 -10
- package/dist/{chunk-35IQLGV3.cjs → chunk-X4Y74M5K.cjs} +74 -74
- package/dist/{chunk-HMSUYWSB.cjs → chunk-XQJOYPPN.cjs} +4 -4
- package/dist/{chunk-65LJNDR2.cjs → chunk-YKF66VQY.cjs} +20 -20
- package/dist/{chunk-5TCIRWHL.cjs → chunk-YRULLK7N.cjs} +5 -5
- package/dist/{cloud-tenants-JPXAXSPE.cjs → cloud-tenants-OKUFSNPW.cjs} +13 -13
- package/dist/components/case-studies/index.cjs +25 -25
- 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/embeds/reddit-embed-client.d.ts +3 -1
- package/dist/components/embeds/reddit-embed-client.d.ts.map +1 -1
- package/dist/components/embeds/reddit-embed-client.js +5 -3
- package/dist/components/embeds/twitter-embed-client.d.ts +3 -1
- package/dist/components/embeds/twitter-embed-client.d.ts.map +1 -1
- package/dist/components/embeds/twitter-embed-client.js +5 -3
- 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 +123 -125
- 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 +17 -13
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/markdown/base-components.js +3 -2
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -1
- package/dist/components/ui/markdown/rich/embed-overrides.js +11 -8
- package/dist/components/ui/markdown-editor.d.ts.map +1 -1
- package/dist/components/ui/markdown-editor.js +4 -0
- package/dist/components/ui/markdown-link-command.d.ts +37 -0
- package/dist/components/ui/markdown-link-command.d.ts.map +1 -0
- package/dist/components/ui/markdown-link-command.js +180 -0
- 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-ONC4JOVH.cjs → devices-URCDMYFU.cjs} +14 -14
- package/dist/{embeddable-chat-7ZEGCDVF.cjs → embeddable-chat-B35GB5B6.cjs} +80 -80
- package/dist/index.cjs +36 -18
- package/dist/{knowledge-G7XU5YST.cjs → knowledge-5MW2PREQ.cjs} +14 -14
- package/dist/{logs-5B7NY4G6.cjs → logs-LJJK5CK4.cjs} +21 -21
- package/dist/{policies-OYR3SLJN.cjs → policies-6TCEWQYG.cjs} +17 -17
- package/dist/{remote-session-N4PJB5FW.cjs → remote-session-IYT4LWJC.cjs} +7 -7
- package/dist/{scripts-6CWHTY6U.cjs → scripts-ABB2P6PT.cjs} +20 -20
- package/dist/{software-update-AJPIY4SQ.cjs → software-update-4427ZRMO.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 +193 -28
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +9 -6
- package/dist/utils/markdown-link.d.ts +44 -0
- package/dist/utils/markdown-link.d.ts.map +1 -0
- package/dist/utils/markdown-link.js +83 -0
- package/dist/utils/markdown-to-plain.d.ts +3 -16
- package/dist/utils/markdown-to-plain.d.ts.map +1 -1
- package/dist/utils/markdown-to-plain.js +33 -3
- 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 +3 -1
- package/src/components/embeds/reddit-embed-client.tsx +7 -3
- package/src/components/embeds/twitter-embed-client.tsx +7 -3
- package/src/components/ui/filter-checkbox-item.tsx +21 -5
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/markdown/base-components.tsx +5 -2
- package/src/components/ui/markdown/rich/embed-overrides.tsx +12 -9
- package/src/components/ui/markdown-editor.tsx +6 -0
- package/src/components/ui/markdown-link-command.tsx +252 -0
- 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 +8 -5
- package/src/utils/markdown-link.ts +148 -0
- package/src/utils/markdown-to-plain.ts +64 -22
- 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,14 +1,44 @@
|
|
|
1
|
+
import { toText } from "hast-util-to-text";
|
|
2
|
+
import rehypeRaw from "rehype-raw";
|
|
3
|
+
import { remark } from "remark";
|
|
4
|
+
import remarkBreaks from "remark-breaks";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import remarkRehype from "remark-rehype";
|
|
7
|
+
import { SKIP, visit } from "unist-util-visit";
|
|
1
8
|
function stripInlineMarkdown(s) {
|
|
2
9
|
return s.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1");
|
|
3
10
|
}
|
|
11
|
+
const toHtmlTree = remark().use(remarkGfm).use(remarkBreaks).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw);
|
|
12
|
+
const NOT_PROSE = /* @__PURE__ */ new Set(["pre", "img"]);
|
|
13
|
+
const TABLE_SECTIONS = /* @__PURE__ */ new Set(["thead", "tbody", "tfoot"]);
|
|
14
|
+
function proseTree(markdown) {
|
|
15
|
+
const tree = toHtmlTree.runSync(toHtmlTree.parse(markdown));
|
|
16
|
+
visit(tree, "element", (node, index, parent) => {
|
|
17
|
+
if (NOT_PROSE.has(node.tagName) && parent && index !== void 0) {
|
|
18
|
+
parent.children.splice(index, 1);
|
|
19
|
+
return [SKIP, index];
|
|
20
|
+
}
|
|
21
|
+
if (TABLE_SECTIONS.has(node.tagName) && parent && index !== void 0) {
|
|
22
|
+
const followedBySection = parent.children.slice(index + 1).some((sibling) => sibling.type === "element" && TABLE_SECTIONS.has(sibling.tagName));
|
|
23
|
+
const rows = node.children.filter(
|
|
24
|
+
(child) => child.type === "element" && child.tagName === "tr"
|
|
25
|
+
);
|
|
26
|
+
const lastRow = rows[rows.length - 1];
|
|
27
|
+
if (followedBySection && lastRow) {
|
|
28
|
+
lastRow.children.push({ type: "element", tagName: "br", properties: {}, children: [] });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return void 0;
|
|
32
|
+
});
|
|
33
|
+
return tree;
|
|
34
|
+
}
|
|
4
35
|
function markdownToPlainText(markdown, options = {}) {
|
|
5
36
|
const { mode = "article", maxChars } = options;
|
|
6
|
-
let out =
|
|
7
|
-
out = stripInlineMarkdown(out).replace(/^#+\s*/gm, "").replace(/^\s*[-*+]\s+/gm, "");
|
|
37
|
+
let out = toText(proseTree(markdown));
|
|
8
38
|
if (mode === "preview") {
|
|
9
39
|
out = out.replace(/\s+/g, " ").trim();
|
|
10
40
|
} else {
|
|
11
|
-
out = out.replace(/\n{3,}/g, "\n\n").trim();
|
|
41
|
+
out = out.replace(/[^\S\n]*\n[^\S\n]*/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
12
42
|
}
|
|
13
43
|
if (maxChars != null && out.length > maxChars) {
|
|
14
44
|
out = out.slice(0, maxChars);
|
|
@@ -1,26 +1,52 @@
|
|
|
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
|
-
export type
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
/** The word drawn beside the OpenFrame mark. */
|
|
17
|
-
word: string;
|
|
18
|
-
/** The full name, for plain-text surfaces (a select option, an aria label). */
|
|
16
|
+
export type VendorClassificationValue = string;
|
|
17
|
+
export type VendorClassificationGroup = 'license' | 'lifecycle' | 'openframe';
|
|
18
|
+
export interface VendorClassificationCopy {
|
|
19
|
+
/** The label on a tag, a filter and a plain-text surface. */
|
|
19
20
|
label: string;
|
|
20
21
|
/** What the classification means, for a tooltip. */
|
|
21
22
|
description: string;
|
|
23
|
+
group: VendorClassificationGroup;
|
|
24
|
+
/** OpenFrame group only: the one word drawn beside the OpenFrame mark. */
|
|
25
|
+
word?: string;
|
|
26
|
+
/** The icon drawn beside the label, by name (resolved by the shared icon resolver), and its tone. */
|
|
27
|
+
icon?: VendorClassificationIconConfig;
|
|
22
28
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
/**
|
|
30
|
+
* A classification's icon, as data: `name` is any name the shared resolver knows (an alias such
|
|
31
|
+
* as `openframe-logo`, an icon of the design set by its kebab name, or a lucide name). `tone`:
|
|
32
|
+
* `accent` (the brand colour), `neutral` (secondary text on a tile) or `warning`.
|
|
33
|
+
*/
|
|
34
|
+
export interface VendorClassificationIconConfig {
|
|
35
|
+
name: string;
|
|
36
|
+
tone: 'accent' | 'neutral' | 'warning';
|
|
37
|
+
}
|
|
38
|
+
/** In display order: a filter list shows OpenFrame first, then license, then lifecycle. */
|
|
39
|
+
export declare const VENDOR_CLASSIFICATION_COPY: Readonly<Record<string, VendorClassificationCopy>>;
|
|
40
|
+
/** The enrichment of a classification, or undefined for a value we do not present specially. */
|
|
41
|
+
export declare function vendorClassificationCopy(value: string | null | undefined): VendorClassificationCopy | undefined;
|
|
42
|
+
/** The group of a classification, or undefined for a value with no enrichment. */
|
|
43
|
+
export declare function vendorClassificationGroup(value: string | null | undefined): VendorClassificationGroup | undefined;
|
|
44
|
+
/** A classification drawn with the OpenFrame mark (it has a `word`). */
|
|
45
|
+
export declare function isOpenFrameClassification(value: string | null | undefined): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Sort order for a list of classifications (a filter list): the enriched values in their
|
|
48
|
+
* display order (OpenFrame Selected, Connect, Shipped, then license, then lifecycle), then
|
|
49
|
+
* every other value the server holds, alphabetically.
|
|
50
|
+
*/
|
|
51
|
+
export declare function compareVendorClassifications(a: string, b: string): number;
|
|
26
52
|
//# sourceMappingURL=vendor-classification.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vendor-classification.d.ts","sourceRoot":"","sources":["../../src/utils/vendor-classification.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"vendor-classification.d.ts","sourceRoot":"","sources":["../../src/utils/vendor-classification.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAE/C,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAE9E,MAAM,WAAW,wBAAwB;IACvC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,yBAAyB,CAAC;IACjC,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qGAAqG;IACrG,IAAI,CAAC,EAAE,8BAA8B,CAAC;CACvC;AAED;;;;GAIG;AACH,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;CACxC;AAED,2FAA2F;AAC3F,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CA6DzF,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,wBAAwB,GAAG,SAAS,CAG/G;AAED,kFAAkF;AAClF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,yBAAyB,GAAG,SAAS,CAEjH;AAED,wEAAwE;AACxE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEnF;AAID;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CASzE"}
|
|
@@ -1,36 +1,87 @@
|
|
|
1
|
-
const
|
|
2
|
-
"open_source",
|
|
3
|
-
"commercial",
|
|
4
|
-
"openframe_selected",
|
|
5
|
-
"openframe_shipped",
|
|
6
|
-
"openframe_connect"
|
|
7
|
-
];
|
|
8
|
-
const OPENFRAME_CLASSIFICATIONS = {
|
|
1
|
+
const VENDOR_CLASSIFICATION_COPY = {
|
|
9
2
|
openframe_selected: {
|
|
10
|
-
word: "Selected",
|
|
11
3
|
label: "OpenFrame Selected",
|
|
12
|
-
|
|
4
|
+
word: "Selected",
|
|
5
|
+
description: "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.",
|
|
6
|
+
group: "openframe",
|
|
7
|
+
icon: { name: "openframe-logo", tone: "accent" }
|
|
8
|
+
},
|
|
9
|
+
openframe_connect: {
|
|
10
|
+
label: "OpenFrame Connect",
|
|
11
|
+
word: "Connect",
|
|
12
|
+
description: "Services OpenFrame connects to and manages for every client, such as Microsoft 365 and Google Workspace, beside devices and tickets.",
|
|
13
|
+
group: "openframe",
|
|
14
|
+
icon: { name: "openframe-logo", tone: "accent" }
|
|
13
15
|
},
|
|
14
16
|
openframe_shipped: {
|
|
15
|
-
word: "Shipped",
|
|
16
17
|
label: "OpenFrame Shipped",
|
|
17
|
-
|
|
18
|
+
word: "Shipped",
|
|
19
|
+
description: "Tools and OpenFrame products that are in the released OpenFrame product today, not on the roadmap.",
|
|
20
|
+
group: "openframe",
|
|
21
|
+
icon: { name: "openframe-logo", tone: "accent" }
|
|
18
22
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
open_source: {
|
|
24
|
+
label: "Open Source",
|
|
25
|
+
description: "Released under an OSI-approved open-source license.",
|
|
26
|
+
group: "license",
|
|
27
|
+
icon: { name: "code", tone: "accent" }
|
|
28
|
+
},
|
|
29
|
+
source_available: {
|
|
30
|
+
label: "Source-available",
|
|
31
|
+
description: "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).",
|
|
32
|
+
group: "license",
|
|
33
|
+
icon: { name: "file-code", tone: "neutral" }
|
|
34
|
+
},
|
|
35
|
+
commercial: {
|
|
36
|
+
label: "Commercial",
|
|
37
|
+
description: "Proprietary software sold by the vendor.",
|
|
38
|
+
group: "license",
|
|
39
|
+
icon: { name: "dollar", tone: "neutral" }
|
|
40
|
+
},
|
|
41
|
+
end_of_life: {
|
|
42
|
+
label: "End of life",
|
|
43
|
+
description: "The vendor has discontinued or retired the product. No new releases.",
|
|
44
|
+
group: "lifecycle",
|
|
45
|
+
icon: { name: "alert-triangle", tone: "warning" }
|
|
46
|
+
},
|
|
47
|
+
end_of_support: {
|
|
48
|
+
label: "End of support",
|
|
49
|
+
description: "The vendor no longer provides support or security fixes, though the product can still be obtained.",
|
|
50
|
+
group: "lifecycle",
|
|
51
|
+
icon: { name: "clock", tone: "warning" }
|
|
52
|
+
},
|
|
53
|
+
unmaintained: {
|
|
54
|
+
label: "Unmaintained",
|
|
55
|
+
description: "No release for years and no official end-of-life notice.",
|
|
56
|
+
group: "lifecycle",
|
|
57
|
+
icon: { name: "pause-circle", tone: "warning" }
|
|
23
58
|
}
|
|
24
59
|
};
|
|
25
|
-
function
|
|
26
|
-
|
|
60
|
+
function vendorClassificationCopy(value) {
|
|
61
|
+
if (!value || !Object.prototype.hasOwnProperty.call(VENDOR_CLASSIFICATION_COPY, value)) return void 0;
|
|
62
|
+
return VENDOR_CLASSIFICATION_COPY[value];
|
|
63
|
+
}
|
|
64
|
+
function vendorClassificationGroup(value) {
|
|
65
|
+
return vendorClassificationCopy(value)?.group;
|
|
27
66
|
}
|
|
28
67
|
function isOpenFrameClassification(value) {
|
|
29
|
-
return
|
|
68
|
+
return vendorClassificationGroup(value) === "openframe";
|
|
69
|
+
}
|
|
70
|
+
const DISPLAY_ORDER = Object.keys(VENDOR_CLASSIFICATION_COPY);
|
|
71
|
+
function compareVendorClassifications(a, b) {
|
|
72
|
+
const ia = DISPLAY_ORDER.indexOf(a);
|
|
73
|
+
const ib = DISPLAY_ORDER.indexOf(b);
|
|
74
|
+
if (ia !== -1 || ib !== -1) {
|
|
75
|
+
if (ia === -1) return 1;
|
|
76
|
+
if (ib === -1) return -1;
|
|
77
|
+
return ia - ib;
|
|
78
|
+
}
|
|
79
|
+
return a.localeCompare(b);
|
|
30
80
|
}
|
|
31
81
|
export {
|
|
32
|
-
|
|
33
|
-
|
|
82
|
+
VENDOR_CLASSIFICATION_COPY,
|
|
83
|
+
compareVendorClassifications,
|
|
34
84
|
isOpenFrameClassification,
|
|
35
|
-
|
|
85
|
+
vendorClassificationCopy,
|
|
86
|
+
vendorClassificationGroup
|
|
36
87
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flamingo-stack/openframe-frontend-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.746",
|
|
4
4
|
"description": "Shared design system and components for all Flamingo platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -480,6 +480,7 @@
|
|
|
480
480
|
"framer-motion": "^12.23.12",
|
|
481
481
|
"globals": "^16.4.0",
|
|
482
482
|
"graphql": "^16.12.0",
|
|
483
|
+
"hast-util-to-text": "^4.0.2",
|
|
483
484
|
"libphonenumber-js": "^1.12.40",
|
|
484
485
|
"lucide-react": "^0.454.0",
|
|
485
486
|
"mermaid": "^11.14.0",
|
|
@@ -504,6 +505,7 @@
|
|
|
504
505
|
"remark": "^15.0.1",
|
|
505
506
|
"remark-breaks": "^4.0.0",
|
|
506
507
|
"remark-gfm": "^4.0.1",
|
|
508
|
+
"remark-rehype": "^11.1.2",
|
|
507
509
|
"sonner": "^1.2.4",
|
|
508
510
|
"tailwind-merge": "^3.4.0",
|
|
509
511
|
"tailwindcss-animate": "^1.0.7",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import Link from '../../embed-shims/next-link';
|
|
5
5
|
import { formatLargeNumber } from '../../utils/format';
|
|
6
|
+
import { renderedLinkRel } from '../../utils/markdown-link';
|
|
6
7
|
import type { MediaItem as CarouselMediaItem } from '../../utils/media-carousel-utils-stub';
|
|
7
8
|
import { socialCache } from '../../utils/social-embed-cache';
|
|
8
9
|
import { MediaCarousel } from '../media-carousel';
|
|
@@ -159,10 +160,13 @@ type RedditListingResponse = Array<{ data?: { children?: Array<{ data?: RedditPo
|
|
|
159
160
|
|
|
160
161
|
interface RedditEmbedProps {
|
|
161
162
|
url: string;
|
|
163
|
+
/** The `rel` the author wrote on the post's link (`nofollow` …), kept on the card's links to the post. */
|
|
164
|
+
rel?: string;
|
|
162
165
|
maxWidth?: number;
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
export function RedditEmbedClient({ url, maxWidth = 700 }: RedditEmbedProps) {
|
|
168
|
+
export function RedditEmbedClient({ url, maxWidth = 700, rel }: RedditEmbedProps) {
|
|
169
|
+
const postLinkRel = renderedLinkRel(rel, true);
|
|
166
170
|
const { redditProxyUrl } = useRichMarkdownRuntime();
|
|
167
171
|
const [redditData, setRedditData] = useState<RedditPost | null>(null);
|
|
168
172
|
const [loading, setLoading] = useState(true);
|
|
@@ -261,7 +265,7 @@ export function RedditEmbedClient({ url, maxWidth = 700 }: RedditEmbedProps) {
|
|
|
261
265
|
<Link
|
|
262
266
|
href={url}
|
|
263
267
|
target="_blank"
|
|
264
|
-
rel=
|
|
268
|
+
rel={postLinkRel}
|
|
265
269
|
className="inline-flex items-center space-x-2 rounded-md bg-[#FF4500] px-4 py-2 text-white transition-colors text-h6 hover:bg-[#E03D00]"
|
|
266
270
|
>
|
|
267
271
|
<RedditIcon />
|
|
@@ -562,7 +566,7 @@ export function RedditEmbedClient({ url, maxWidth = 700 }: RedditEmbedProps) {
|
|
|
562
566
|
<Link
|
|
563
567
|
href={url}
|
|
564
568
|
target="_blank"
|
|
565
|
-
rel=
|
|
569
|
+
rel={postLinkRel}
|
|
566
570
|
className="inline-flex items-center space-x-2 text-ods-accent transition-colors text-h6 hover:opacity-80"
|
|
567
571
|
>
|
|
568
572
|
<ExternalLinkIcon />
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import Image from '../../embed-shims/next-image';
|
|
5
5
|
import Link from '../../embed-shims/next-link';
|
|
6
|
+
import { renderedLinkRel } from '../../utils/markdown-link';
|
|
6
7
|
import { socialCache } from '../../utils/social-embed-cache';
|
|
7
8
|
import { XLogoIcon } from '../icons-v2-generated/brand-logos/x-logo-icon';
|
|
8
9
|
import { TwitterContainer } from './embed-container';
|
|
@@ -71,11 +72,14 @@ interface TwitterOEmbedResponse {
|
|
|
71
72
|
|
|
72
73
|
interface TwitterEmbedProps {
|
|
73
74
|
url: string;
|
|
75
|
+
/** The `rel` the author wrote on the post's link (`nofollow` …), kept on the card's links to the post. */
|
|
76
|
+
rel?: string;
|
|
74
77
|
tweetId?: string;
|
|
75
78
|
maxWidth?: number;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
export function TwitterEmbedClient({ url, tweetId, maxWidth = 700 }: TwitterEmbedProps) {
|
|
81
|
+
export function TwitterEmbedClient({ url, tweetId, maxWidth = 700, rel }: TwitterEmbedProps) {
|
|
82
|
+
const postLinkRel = renderedLinkRel(rel, true);
|
|
79
83
|
const { twitterProxyUrl } = useRichMarkdownRuntime();
|
|
80
84
|
const [tweetData, setTweetData] = useState<TwitterOEmbedResponse | null>(null);
|
|
81
85
|
const [fetchLoading, setLoading] = useState(true);
|
|
@@ -179,7 +183,7 @@ export function TwitterEmbedClient({ url, tweetId, maxWidth = 700 }: TwitterEmbe
|
|
|
179
183
|
<Link
|
|
180
184
|
href={url}
|
|
181
185
|
target="_blank"
|
|
182
|
-
rel=
|
|
186
|
+
rel={postLinkRel}
|
|
183
187
|
className="inline-flex items-center space-x-2 rounded-md bg-ods-bg-surface px-4 py-2 text-ods-text-primary transition-colors text-h6 hover:bg-ods-bg-active"
|
|
184
188
|
>
|
|
185
189
|
<XIcon />
|
|
@@ -318,7 +322,7 @@ export function TwitterEmbedClient({ url, tweetId, maxWidth = 700 }: TwitterEmbe
|
|
|
318
322
|
<Link
|
|
319
323
|
href={url}
|
|
320
324
|
target="_blank"
|
|
321
|
-
rel=
|
|
325
|
+
rel={postLinkRel}
|
|
322
326
|
className="inline-flex items-center space-x-2 text-ods-accent transition-colors text-h6 hover:opacity-80"
|
|
323
327
|
>
|
|
324
328
|
<ExternalLinkIcon />
|
|
@@ -1,30 +1,46 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
4
5
|
import { cn } from '../../utils/cn';
|
|
5
6
|
import { CheckboxCheckmarkIcon } from '../icons-v2-generated/signs-and-symbols/checkbox-checkmark-icon';
|
|
6
7
|
|
|
7
8
|
export interface FilterCheckboxItemProps {
|
|
8
|
-
label
|
|
9
|
+
/** Text, or a richer label (an icon with a word and a tooltip). */
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
/** The accessible name when `label` is not plain text. */
|
|
12
|
+
ariaLabel?: string;
|
|
9
13
|
checked: boolean;
|
|
10
14
|
onChange: (checked: boolean) => void;
|
|
11
15
|
count?: number;
|
|
16
|
+
/** Ignore clicks (e.g. while a navigation started by this filter is pending). */
|
|
17
|
+
disabled?: boolean;
|
|
12
18
|
className?: string;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
export function FilterCheckboxItem({
|
|
21
|
+
export function FilterCheckboxItem({
|
|
22
|
+
label,
|
|
23
|
+
ariaLabel,
|
|
24
|
+
checked,
|
|
25
|
+
onChange,
|
|
26
|
+
count,
|
|
27
|
+
disabled,
|
|
28
|
+
className,
|
|
29
|
+
}: FilterCheckboxItemProps) {
|
|
16
30
|
return (
|
|
17
31
|
<div
|
|
18
32
|
className={cn(
|
|
19
33
|
'flex items-center gap-3 border-b border-ods-border bg-ods-card last:border-b-0',
|
|
20
|
-
'cursor-pointer transition-colors hover:bg-ods-bg-hover',
|
|
34
|
+
disabled ? 'cursor-wait opacity-70' : 'cursor-pointer transition-colors hover:bg-ods-bg-hover',
|
|
21
35
|
count !== undefined ? 'px-4 py-3' : 'p-3',
|
|
22
36
|
className,
|
|
23
37
|
)}
|
|
24
|
-
onClick={() => onChange(!checked)}
|
|
38
|
+
onClick={() => !disabled && onChange(!checked)}
|
|
25
39
|
>
|
|
26
40
|
<CheckboxPrimitive.Root
|
|
27
41
|
checked={checked}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
aria-label={ariaLabel ?? (typeof label === 'string' ? label : undefined)}
|
|
28
44
|
onCheckedChange={c => onChange(c === true)}
|
|
29
45
|
className={cn(
|
|
30
46
|
'h-6 w-6 shrink-0 rounded-[6px] border-2',
|
|
@@ -37,7 +53,7 @@ export function FilterCheckboxItem({ label, checked, onChange, count, className
|
|
|
37
53
|
<CheckboxCheckmarkIcon size={10} />
|
|
38
54
|
</CheckboxPrimitive.Indicator>
|
|
39
55
|
</CheckboxPrimitive.Root>
|
|
40
|
-
<span className="flex-1 text-ods-text-primary text-h4">{label}</span>
|
|
56
|
+
<span className="min-w-0 flex-1 text-ods-text-primary text-h4">{label}</span>
|
|
41
57
|
{count !== undefined && (
|
|
42
58
|
<span className="shrink-0 text-ods-text-secondary text-h5">{count.toLocaleString()}</span>
|
|
43
59
|
)}
|
|
@@ -14,6 +14,7 @@ import { useAuthedImageSrc } from '../../../hooks/use-authed-image-src';
|
|
|
14
14
|
import type { ResolveLinkResult } from '../../../types/doc-source';
|
|
15
15
|
import { cn } from '../../../utils/cn';
|
|
16
16
|
import { slugifyHeadingText } from '../../../utils/markdown-heading-id';
|
|
17
|
+
import { renderedLinkRel } from '../../../utils/markdown-link';
|
|
17
18
|
import { getHashTargetElement, navigateSamePageHash, HUB_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav';
|
|
18
19
|
import { extractText, resolveFallbackHeadingId, useAssignedHeadingIds, useHeadingId } from './heading-ids';
|
|
19
20
|
import { MermaidDiagram } from './mermaid-diagram';
|
|
@@ -250,7 +251,7 @@ export function buildBaseComponents({
|
|
|
250
251
|
),
|
|
251
252
|
|
|
252
253
|
// --- links ---
|
|
253
|
-
a: ({ href, children, className: linkClassName }: MdRenderProps<'a'>) => {
|
|
254
|
+
a: ({ href, children, className: linkClassName, rel: authoredRel }: MdRenderProps<'a'>) => {
|
|
254
255
|
const isBroken = href !== undefined && brokenLinks.includes(href);
|
|
255
256
|
const isInternalDocLink =
|
|
256
257
|
propCurrentPath !== undefined &&
|
|
@@ -339,7 +340,9 @@ export function buildBaseComponents({
|
|
|
339
340
|
href={href}
|
|
340
341
|
className={`relative text-ods-accent no-underline transition-colors duration-200 after:absolute after:-bottom-0.5 after:left-0 after:h-0.5 after:w-0 after:bg-ods-accent after:transition-all after:duration-300 after:content-[''] hover:after:w-full ${linkClassName || ''}`}
|
|
341
342
|
target={href?.startsWith('http') ? '_blank' : undefined}
|
|
342
|
-
rel=
|
|
343
|
+
// The author's SEO tokens (`<a rel="nofollow">`, the editor's Nofollow choice) are kept;
|
|
344
|
+
// every other authored token is dropped, and a new-tab link always gets noopener noreferrer.
|
|
345
|
+
rel={renderedLinkRel(authoredRel, href?.startsWith('http') ?? false)}
|
|
343
346
|
onClick={inPageAnchorId !== null ? handleAnchorClick : undefined}
|
|
344
347
|
>
|
|
345
348
|
{children}
|
|
@@ -22,15 +22,18 @@ import { Video } from '../../../features/video';
|
|
|
22
22
|
import { buildStandardLeafRenderers, hasRenderableSrc, type MdRenderProps } from '../base-components';
|
|
23
23
|
import type { TextSizeElement } from '../text-size';
|
|
24
24
|
|
|
25
|
-
/** Depth-first search of a hast node for the first `<a href>` matching `hostRe
|
|
26
|
-
function
|
|
25
|
+
/** Depth-first search of a hast node for the first `<a href>` matching `hostRe`, with the `rel` written on it. */
|
|
26
|
+
function findFirstLink(node: ElementContent | undefined, hostRe: RegExp): { href: string; rel?: string } | null {
|
|
27
27
|
if (!node || node.type !== 'element') return null;
|
|
28
28
|
if (node.tagName === 'a') {
|
|
29
|
-
const href = node.properties
|
|
30
|
-
if (typeof href === 'string' && hostRe.test(href))
|
|
29
|
+
const { href, rel } = node.properties;
|
|
30
|
+
if (typeof href === 'string' && hostRe.test(href)) {
|
|
31
|
+
const relText = Array.isArray(rel) ? rel.join(' ') : typeof rel === 'string' ? rel : undefined;
|
|
32
|
+
return { href, rel: relText };
|
|
33
|
+
}
|
|
31
34
|
}
|
|
32
35
|
for (const child of node.children) {
|
|
33
|
-
const found =
|
|
36
|
+
const found = findFirstLink(child, hostRe);
|
|
34
37
|
if (found) return found;
|
|
35
38
|
}
|
|
36
39
|
return null;
|
|
@@ -202,12 +205,12 @@ export function buildRichEmbedOverrides({
|
|
|
202
205
|
const { node, className } = bqProps;
|
|
203
206
|
const classNames: string = Array.isArray(className) ? className.join(' ') : (className ?? '');
|
|
204
207
|
if (classNames.includes('reddit-embed-bq')) {
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
208
|
+
const post = findFirstLink(node, /reddit\.com/);
|
|
209
|
+
if (post) return <RedditEmbedClient url={post.href} rel={post.rel} />;
|
|
207
210
|
}
|
|
208
211
|
if (classNames.includes('twitter-tweet')) {
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
212
|
+
const tweet = findFirstLink(node, /(?:twitter\.com|x\.com)\/[^/]+\/status\//);
|
|
213
|
+
if (tweet) return <TwitterEmbedClient url={tweet.href} rel={tweet.rel} />;
|
|
211
214
|
}
|
|
212
215
|
return standard.blockquote(bqProps);
|
|
213
216
|
},
|
|
@@ -7,6 +7,7 @@ import { useRef, useCallback, useState, useEffect } from 'react';
|
|
|
7
7
|
import dynamic from '../../embed-shims/next-dynamic';
|
|
8
8
|
import { cn } from '../../utils/cn';
|
|
9
9
|
import { IMAGE_FILE_ACCEPT, fileMediaType } from '../../utils/media-type';
|
|
10
|
+
import { useMarkdownLinkCommand } from './markdown-link-command';
|
|
10
11
|
|
|
11
12
|
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
|
|
12
13
|
|
|
@@ -135,6 +136,8 @@ export function MarkdownEditor({
|
|
|
135
136
|
const [isUploading, setIsUploading] = useState(false);
|
|
136
137
|
const [uploadProgress, setUploadProgress] = useState('');
|
|
137
138
|
const [defaultExtraCommands, setDefaultExtraCommands] = useState<ICommand[]>([]);
|
|
139
|
+
// The toolbar's link button opens the shared link dialog (text, URL, Follow / Nofollow).
|
|
140
|
+
const { commands: toolbarCommands, linkDialog } = useMarkdownLinkCommand();
|
|
138
141
|
|
|
139
142
|
useEffect(() => {
|
|
140
143
|
let cancelled = false;
|
|
@@ -352,6 +355,7 @@ export function MarkdownEditor({
|
|
|
352
355
|
visibleDragbar={true}
|
|
353
356
|
height={height}
|
|
354
357
|
minHeight={minHeight}
|
|
358
|
+
commands={toolbarCommands}
|
|
355
359
|
extraCommands={extraCommands}
|
|
356
360
|
style={
|
|
357
361
|
{
|
|
@@ -387,6 +391,8 @@ export function MarkdownEditor({
|
|
|
387
391
|
|
|
388
392
|
{uploadProgress && <p className="mt-1 text-ods-text-secondary text-h6">{uploadProgress}</p>}
|
|
389
393
|
|
|
394
|
+
{linkDialog}
|
|
395
|
+
|
|
390
396
|
<MarkdownEditorStyles />
|
|
391
397
|
</div>
|
|
392
398
|
);
|