@mediaradar/design-components 0.1.0

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/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @mediaradar/design-components
2
+
3
+ Real, shared React component primitives — the canonical implementation for
4
+ components genuinely common across consuming apps (`insights-studio-prototype`,
5
+ `mr-clientapps-insights-studio`), published as a real npm package so there's
6
+ structurally only one implementation instead of two that can silently drift.
7
+
8
+ **Status: spike/pilot.** One component (`InfoTip`) so far, not yet published
9
+ to the real npm registry, not yet consumed by `mr-clientapps-insights-studio`
10
+ at all. See `gh-design-system`'s open spike PR for the current state.
11
+
12
+ ---
13
+
14
+ ## What belongs here — and what doesn't
15
+
16
+ Not every shared-feeling component should move here. Two failure modes to
17
+ watch for, in opposite directions:
18
+
19
+ **Doesn't belong here: a concept that only exists in one app.** If
20
+ `mr-clientapps-insights-studio` has nothing resembling a component, or has
21
+ something meaningfully more sophisticated for the same job (its own
22
+ `FilterDrawer`/`FilterPanel` system vs. a prototype's simpler filter
23
+ components, for example), forcing it into a shared package is a downgrade
24
+ for whichever app had the better version, not an improvement. Leave it
25
+ where it is.
26
+
27
+ **The escape hatch — a genuine conflict discovered mid-migration.** Sometimes
28
+ a component looks like a good candidate until reconciliation actually starts,
29
+ and it turns out the two apps need behavior that's genuinely incompatible —
30
+ not just different-by-convenience, but conflicting requirements neither side
31
+ can drop. When that happens, **pull the component back out of this package**
32
+ rather than forcing a compromise that serves neither app well. A component
33
+ that ends up split by `if (isMrClientapps)`-style branching inside a
34
+ "shared" primitive has stopped being shared in any meaningful sense — it's
35
+ just drift with extra indirection. Keeping it separate, in each app's own
36
+ tree, is the honest outcome in that case, not a failure of the process.
37
+
38
+ This hasn't happened yet — `InfoTip`'s reconciliation (hover-trigger support
39
+ from `insights-studio-prototype`, z-index layering awareness from
40
+ `mr-clientapps-insights-studio`) turned out to be a clean, additive merge,
41
+ not a conflict. But the difference between "additive merge" and "genuine
42
+ conflict" is a judgment call every future migration needs to make freshly,
43
+ not assume away because the first one went smoothly.
44
+
45
+ **When it's genuinely additive but app-specific**, prefer these over
46
+ forking, in order:
47
+ 1. **A prop with a default matching current behavior** — see `InfoTip`'s
48
+ own `trigger` prop: `mr-clientapps-insights-studio` never has to know it
49
+ exists, since the default preserves exactly what it already had.
50
+ 2. **Composition** — an app wraps the shared primitive in its own local
51
+ component to add app-specific concerns (analytics, entitlements) the
52
+ shared primitive shouldn't know about. The shared component stays
53
+ strictly presentational.
54
+
55
+ ## Reconciling two existing implementations
56
+
57
+ When a component already exists separately in both apps (as opposed to
58
+ being net-new), verify differences empirically before picking a winner —
59
+ don't assume one app's version is "more correct." `InfoTip`'s reconciliation
60
+ found real, verified divergence in both directions: `mr-clientapps`' bare
61
+ `outline` utility turned out to rely on a Tailwind v4 implementation
62
+ fallback rather than the intended `outline-solid` syntax (confirmed by
63
+ actually compiling both with the real installed Tailwind, not assumed), its
64
+ `shadow-2` didn't exist anywhere in `@mediaradar/design-tokens` at all (a
65
+ soon-to-be-stale local name), and its z-index value turned out to be in a
66
+ completely different numeric scale than this package's own semantic
67
+ `z-tooltip` token — using the "proper" shared token there would have
68
+ actually broken the thing its own code comment was trying to guarantee.
69
+ None of that would have surfaced without checking directly.
70
+
71
+ ## Peer dependencies
72
+
73
+ `react`/`react-dom` are peer dependencies, never bundled — a bundled React
74
+ would sit alongside a consuming app's own instance instead of using it,
75
+ breaking hooks/context silently. `@mediaradar/design-tokens` is also a peer
76
+ dependency, even though nothing here literally imports from it — components
77
+ use Tailwind class names that only resolve correctly once a consuming app
78
+ already has that package's Tailwind theme wired up. Declaring it as a peer
79
+ (rather than not declaring it at all) means `npm install` at least warns if
80
+ it's missing, instead of the component silently rendering unstyled.
package/dist/index.cjs ADDED
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ InfoTip: () => InfoTip
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/InfoTip.tsx
28
+ var import_react = require("react");
29
+ var import_react_dom = require("react-dom");
30
+ var import_lucide_react = require("lucide-react");
31
+ var import_jsx_runtime = require("react/jsx-runtime");
32
+ function InfoTip({ text, size = 11, label = "More information", trigger = "click" }) {
33
+ const [open, setOpen] = (0, import_react.useState)(false);
34
+ const [pos, setPos] = (0, import_react.useState)(null);
35
+ const btnRef = (0, import_react.useRef)(null);
36
+ (0, import_react.useEffect)(() => {
37
+ if (!open) return;
38
+ const onDown = (e) => {
39
+ if (!btnRef.current?.contains(e.target)) setOpen(false);
40
+ };
41
+ const onKey = (e) => e.key === "Escape" && setOpen(false);
42
+ window.addEventListener("mousedown", onDown);
43
+ window.addEventListener("keydown", onKey);
44
+ return () => {
45
+ window.removeEventListener("mousedown", onDown);
46
+ window.removeEventListener("keydown", onKey);
47
+ };
48
+ }, [open]);
49
+ const place = () => {
50
+ if (!btnRef.current) return;
51
+ const r = btnRef.current.getBoundingClientRect();
52
+ const width = Math.min(260, window.innerWidth - 24);
53
+ const left = Math.max(12, Math.min(r.left, window.innerWidth - width - 12));
54
+ setPos({ top: r.bottom + 6, left, width });
55
+ };
56
+ const show = () => {
57
+ place();
58
+ setOpen(true);
59
+ };
60
+ const toggle = (e) => {
61
+ e.stopPropagation();
62
+ if (!open) show();
63
+ else setOpen(false);
64
+ };
65
+ const hoverProps = trigger === "hover" ? {
66
+ onMouseEnter: show,
67
+ onMouseLeave: () => setOpen(false),
68
+ onFocus: show,
69
+ onBlur: () => setOpen(false)
70
+ } : {};
71
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
72
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
73
+ "button",
74
+ {
75
+ ref: btnRef,
76
+ type: "button",
77
+ onClick: toggle,
78
+ "aria-label": label,
79
+ "aria-expanded": open,
80
+ ...hoverProps,
81
+ className: "inline-flex cursor-pointer items-center text-text-tertiary transition-colors hover:text-text-primary focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus",
82
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Info, { size, "aria-hidden": "true" })
83
+ }
84
+ ),
85
+ open && pos && (0, import_react_dom.createPortal)(
86
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
87
+ "div",
88
+ {
89
+ role: "tooltip",
90
+ style: { position: "fixed", top: pos.top, left: pos.left, width: pos.width },
91
+ className: "z-[90] rounded-control border border-border bg-surface px-3 py-2 text-[12px] font-normal not-italic normal-case leading-relaxed tracking-normal text-text-secondary shadow-overlay",
92
+ children: text
93
+ }
94
+ ),
95
+ document.body
96
+ )
97
+ ] });
98
+ }
99
+ // Annotate the CommonJS export names for ESM import in node:
100
+ 0 && (module.exports = {
101
+ InfoTip
102
+ });
103
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/InfoTip.tsx"],"sourcesContent":["export { InfoTip } from \"./InfoTip\";\nexport type { InfoTipProps } from \"./InfoTip\";\n","\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Info } from \"lucide-react\";\n\n/**\n * @pattern InfoTip\n * @description Info tooltip — a small info glyph that opens a portalled,\n * viewport-clamped bubble. Mirrors the Publisher Intel header affordance so\n * page subtitles can collapse into the eyebrow.\n * @tokens text-text-tertiary, rounded-control, border-border, bg-surface,\n * shadow-overlay, text-text-secondary, focus-visible:outline-border-focus,\n * hover:text-text-primary, tracking-normal\n * @dependencies lucide-react\n * @behavior 'click' mode toggles on click only. 'hover' mode also opens on\n * keyboard focus and closes on blur, so it stays reachable without a mouse —\n * not just a decorative hover effect. Both modes close on Escape and on any\n * outside click. The bubble is portaled to document.body and repositions\n * itself to stay clear of the right viewport edge.\n * @usage\n * ```tsx\n * <InfoTip text=\"Trims time periods to data that has finalized across the selected media formats.\" />\n * <InfoTip text=\"Hover variant\" trigger=\"hover\" />\n * ```\n */\nexport interface InfoTipProps {\n /** The bubble's content. */\n text: string;\n /** Glyph size in pixels. */\n size?: number;\n /** Accessible label for the trigger button. */\n label?: string;\n /**\n * 'click' toggles the bubble (default); 'hover' reveals it on hover, and\n * also on keyboard focus so it stays reachable without a pointer.\n */\n trigger?: \"click\" | \"hover\";\n}\n\nexport function InfoTip({ text, size = 11, label = \"More information\", trigger = \"click\" }: InfoTipProps) {\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState<{ top: number; left: number; width: number } | null>(null);\n const btnRef = useRef<HTMLButtonElement>(null);\n\n useEffect(() => {\n if (!open) return;\n const onDown = (e: MouseEvent) => {\n if (!btnRef.current?.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setOpen(false);\n window.addEventListener(\"mousedown\", onDown);\n window.addEventListener(\"keydown\", onKey);\n return () => {\n window.removeEventListener(\"mousedown\", onDown);\n window.removeEventListener(\"keydown\", onKey);\n };\n }, [open]);\n\n // Position the bubble under the glyph, clamped within the viewport so it\n // never squeezes against the right edge.\n const place = () => {\n if (!btnRef.current) return;\n const r = btnRef.current.getBoundingClientRect();\n const width = Math.min(260, window.innerWidth - 24);\n const left = Math.max(12, Math.min(r.left, window.innerWidth - width - 12));\n setPos({ top: r.bottom + 6, left, width });\n };\n\n const show = () => {\n place();\n setOpen(true);\n };\n\n const toggle = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (!open) show();\n else setOpen(false);\n };\n\n // Hover trigger also opens on keyboard focus so it stays reachable.\n const hoverProps =\n trigger === \"hover\"\n ? {\n onMouseEnter: show,\n onMouseLeave: () => setOpen(false),\n onFocus: show,\n onBlur: () => setOpen(false),\n }\n : {};\n\n return (\n <>\n <button\n ref={btnRef}\n type=\"button\"\n onClick={toggle}\n aria-label={label}\n aria-expanded={open}\n {...hoverProps}\n className=\"inline-flex cursor-pointer items-center text-text-tertiary transition-colors hover:text-text-primary focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus\"\n >\n <Info size={size} aria-hidden=\"true\" />\n </button>\n {open &&\n pos &&\n createPortal(\n <div\n role=\"tooltip\"\n style={{ position: \"fixed\", top: pos.top, left: pos.left, width: pos.width }}\n // z-[90]: deliberately an arbitrary value, not @mediaradar/design-tokens'\n // semantic z-tooltip (1200) -- checked directly: mr-clientapps-insights-\n // studio's real toast layer is a literal z-[100] (ToastProvider.tsx),\n // completely unrelated in scale to design-tokens' 1000+ semantic z-index\n // tokens (z-dropdown/z-modal/z-tooltip). Using the shared token here\n // would render this bubble ABOVE that app's toasts -- the opposite of\n // the original intent (a tooltip should never cover a notification).\n // 90 sits above mr-clientapps' modals (below its own z-[100] toasts) as\n // of this writing -- portals to document.body, becoming a sibling of any\n // open dialog rather than a child of it, so a lower z-index would render\n // invisibly behind the dialog that triggered it. RE-VERIFY this number\n // against any NEW consuming app's own z-index scale before trusting it;\n // this is a leaf component making an assumption about global stacking\n // context it can't fully see.\n className=\"z-[90] rounded-control border border-border bg-surface px-3 py-2 text-[12px] font-normal not-italic normal-case leading-relaxed tracking-normal text-text-secondary shadow-overlay\"\n >\n {text}\n </div>,\n document.body\n )}\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,mBAA4C;AAC5C,uBAA6B;AAC7B,0BAAqB;AAwFjB;AApDG,SAAS,QAAQ,EAAE,MAAM,OAAO,IAAI,QAAQ,oBAAoB,UAAU,QAAQ,GAAiB;AACxG,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,KAAK;AACtC,QAAM,CAAC,KAAK,MAAM,QAAI,uBAA8D,IAAI;AACxF,QAAM,aAAS,qBAA0B,IAAI;AAE7C,8BAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,CAAC,MAAkB;AAChC,UAAI,CAAC,OAAO,SAAS,SAAS,EAAE,MAAc,EAAG,SAAQ,KAAK;AAAA,IAChE;AACA,UAAM,QAAQ,CAAC,MAAqB,EAAE,QAAQ,YAAY,QAAQ,KAAK;AACvE,WAAO,iBAAiB,aAAa,MAAM;AAC3C,WAAO,iBAAiB,WAAW,KAAK;AACxC,WAAO,MAAM;AACX,aAAO,oBAAoB,aAAa,MAAM;AAC9C,aAAO,oBAAoB,WAAW,KAAK;AAAA,IAC7C;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAIT,QAAM,QAAQ,MAAM;AAClB,QAAI,CAAC,OAAO,QAAS;AACrB,UAAM,IAAI,OAAO,QAAQ,sBAAsB;AAC/C,UAAM,QAAQ,KAAK,IAAI,KAAK,OAAO,aAAa,EAAE;AAClD,UAAM,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,MAAM,OAAO,aAAa,QAAQ,EAAE,CAAC;AAC1E,WAAO,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,MAAM,CAAC;AAAA,EAC3C;AAEA,QAAM,OAAO,MAAM;AACjB,UAAM;AACN,YAAQ,IAAI;AAAA,EACd;AAEA,QAAM,SAAS,CAAC,MAAwB;AACtC,MAAE,gBAAgB;AAClB,QAAI,CAAC,KAAM,MAAK;AAAA,QACX,SAAQ,KAAK;AAAA,EACpB;AAGA,QAAM,aACJ,YAAY,UACR;AAAA,IACE,cAAc;AAAA,IACd,cAAc,MAAM,QAAQ,KAAK;AAAA,IACjC,SAAS;AAAA,IACT,QAAQ,MAAM,QAAQ,KAAK;AAAA,EAC7B,IACA,CAAC;AAEP,SACE,4EACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAS;AAAA,QACT,cAAY;AAAA,QACZ,iBAAe;AAAA,QACd,GAAG;AAAA,QACJ,WAAU;AAAA,QAEV,sDAAC,4BAAK,MAAY,eAAY,QAAO;AAAA;AAAA,IACvC;AAAA,IACC,QACC,WACA;AAAA,MACE;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO,EAAE,UAAU,SAAS,KAAK,IAAI,KAAK,MAAM,IAAI,MAAM,OAAO,IAAI,MAAM;AAAA,UAe3E,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
@@ -0,0 +1,38 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * @pattern InfoTip
5
+ * @description Info tooltip — a small info glyph that opens a portalled,
6
+ * viewport-clamped bubble. Mirrors the Publisher Intel header affordance so
7
+ * page subtitles can collapse into the eyebrow.
8
+ * @tokens text-text-tertiary, rounded-control, border-border, bg-surface,
9
+ * shadow-overlay, text-text-secondary, focus-visible:outline-border-focus,
10
+ * hover:text-text-primary, tracking-normal
11
+ * @dependencies lucide-react
12
+ * @behavior 'click' mode toggles on click only. 'hover' mode also opens on
13
+ * keyboard focus and closes on blur, so it stays reachable without a mouse —
14
+ * not just a decorative hover effect. Both modes close on Escape and on any
15
+ * outside click. The bubble is portaled to document.body and repositions
16
+ * itself to stay clear of the right viewport edge.
17
+ * @usage
18
+ * ```tsx
19
+ * <InfoTip text="Trims time periods to data that has finalized across the selected media formats." />
20
+ * <InfoTip text="Hover variant" trigger="hover" />
21
+ * ```
22
+ */
23
+ interface InfoTipProps {
24
+ /** The bubble's content. */
25
+ text: string;
26
+ /** Glyph size in pixels. */
27
+ size?: number;
28
+ /** Accessible label for the trigger button. */
29
+ label?: string;
30
+ /**
31
+ * 'click' toggles the bubble (default); 'hover' reveals it on hover, and
32
+ * also on keyboard focus so it stays reachable without a pointer.
33
+ */
34
+ trigger?: "click" | "hover";
35
+ }
36
+ declare function InfoTip({ text, size, label, trigger }: InfoTipProps): react.JSX.Element;
37
+
38
+ export { InfoTip, type InfoTipProps };
@@ -0,0 +1,38 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * @pattern InfoTip
5
+ * @description Info tooltip — a small info glyph that opens a portalled,
6
+ * viewport-clamped bubble. Mirrors the Publisher Intel header affordance so
7
+ * page subtitles can collapse into the eyebrow.
8
+ * @tokens text-text-tertiary, rounded-control, border-border, bg-surface,
9
+ * shadow-overlay, text-text-secondary, focus-visible:outline-border-focus,
10
+ * hover:text-text-primary, tracking-normal
11
+ * @dependencies lucide-react
12
+ * @behavior 'click' mode toggles on click only. 'hover' mode also opens on
13
+ * keyboard focus and closes on blur, so it stays reachable without a mouse —
14
+ * not just a decorative hover effect. Both modes close on Escape and on any
15
+ * outside click. The bubble is portaled to document.body and repositions
16
+ * itself to stay clear of the right viewport edge.
17
+ * @usage
18
+ * ```tsx
19
+ * <InfoTip text="Trims time periods to data that has finalized across the selected media formats." />
20
+ * <InfoTip text="Hover variant" trigger="hover" />
21
+ * ```
22
+ */
23
+ interface InfoTipProps {
24
+ /** The bubble's content. */
25
+ text: string;
26
+ /** Glyph size in pixels. */
27
+ size?: number;
28
+ /** Accessible label for the trigger button. */
29
+ label?: string;
30
+ /**
31
+ * 'click' toggles the bubble (default); 'hover' reveals it on hover, and
32
+ * also on keyboard focus so it stays reachable without a pointer.
33
+ */
34
+ trigger?: "click" | "hover";
35
+ }
36
+ declare function InfoTip({ text, size, label, trigger }: InfoTipProps): react.JSX.Element;
37
+
38
+ export { InfoTip, type InfoTipProps };
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ // src/InfoTip.tsx
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { Info } from "lucide-react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ function InfoTip({ text, size = 11, label = "More information", trigger = "click" }) {
7
+ const [open, setOpen] = useState(false);
8
+ const [pos, setPos] = useState(null);
9
+ const btnRef = useRef(null);
10
+ useEffect(() => {
11
+ if (!open) return;
12
+ const onDown = (e) => {
13
+ if (!btnRef.current?.contains(e.target)) setOpen(false);
14
+ };
15
+ const onKey = (e) => e.key === "Escape" && setOpen(false);
16
+ window.addEventListener("mousedown", onDown);
17
+ window.addEventListener("keydown", onKey);
18
+ return () => {
19
+ window.removeEventListener("mousedown", onDown);
20
+ window.removeEventListener("keydown", onKey);
21
+ };
22
+ }, [open]);
23
+ const place = () => {
24
+ if (!btnRef.current) return;
25
+ const r = btnRef.current.getBoundingClientRect();
26
+ const width = Math.min(260, window.innerWidth - 24);
27
+ const left = Math.max(12, Math.min(r.left, window.innerWidth - width - 12));
28
+ setPos({ top: r.bottom + 6, left, width });
29
+ };
30
+ const show = () => {
31
+ place();
32
+ setOpen(true);
33
+ };
34
+ const toggle = (e) => {
35
+ e.stopPropagation();
36
+ if (!open) show();
37
+ else setOpen(false);
38
+ };
39
+ const hoverProps = trigger === "hover" ? {
40
+ onMouseEnter: show,
41
+ onMouseLeave: () => setOpen(false),
42
+ onFocus: show,
43
+ onBlur: () => setOpen(false)
44
+ } : {};
45
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
46
+ /* @__PURE__ */ jsx(
47
+ "button",
48
+ {
49
+ ref: btnRef,
50
+ type: "button",
51
+ onClick: toggle,
52
+ "aria-label": label,
53
+ "aria-expanded": open,
54
+ ...hoverProps,
55
+ className: "inline-flex cursor-pointer items-center text-text-tertiary transition-colors hover:text-text-primary focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus",
56
+ children: /* @__PURE__ */ jsx(Info, { size, "aria-hidden": "true" })
57
+ }
58
+ ),
59
+ open && pos && createPortal(
60
+ /* @__PURE__ */ jsx(
61
+ "div",
62
+ {
63
+ role: "tooltip",
64
+ style: { position: "fixed", top: pos.top, left: pos.left, width: pos.width },
65
+ className: "z-[90] rounded-control border border-border bg-surface px-3 py-2 text-[12px] font-normal not-italic normal-case leading-relaxed tracking-normal text-text-secondary shadow-overlay",
66
+ children: text
67
+ }
68
+ ),
69
+ document.body
70
+ )
71
+ ] });
72
+ }
73
+ export {
74
+ InfoTip
75
+ };
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/InfoTip.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Info } from \"lucide-react\";\n\n/**\n * @pattern InfoTip\n * @description Info tooltip — a small info glyph that opens a portalled,\n * viewport-clamped bubble. Mirrors the Publisher Intel header affordance so\n * page subtitles can collapse into the eyebrow.\n * @tokens text-text-tertiary, rounded-control, border-border, bg-surface,\n * shadow-overlay, text-text-secondary, focus-visible:outline-border-focus,\n * hover:text-text-primary, tracking-normal\n * @dependencies lucide-react\n * @behavior 'click' mode toggles on click only. 'hover' mode also opens on\n * keyboard focus and closes on blur, so it stays reachable without a mouse —\n * not just a decorative hover effect. Both modes close on Escape and on any\n * outside click. The bubble is portaled to document.body and repositions\n * itself to stay clear of the right viewport edge.\n * @usage\n * ```tsx\n * <InfoTip text=\"Trims time periods to data that has finalized across the selected media formats.\" />\n * <InfoTip text=\"Hover variant\" trigger=\"hover\" />\n * ```\n */\nexport interface InfoTipProps {\n /** The bubble's content. */\n text: string;\n /** Glyph size in pixels. */\n size?: number;\n /** Accessible label for the trigger button. */\n label?: string;\n /**\n * 'click' toggles the bubble (default); 'hover' reveals it on hover, and\n * also on keyboard focus so it stays reachable without a pointer.\n */\n trigger?: \"click\" | \"hover\";\n}\n\nexport function InfoTip({ text, size = 11, label = \"More information\", trigger = \"click\" }: InfoTipProps) {\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState<{ top: number; left: number; width: number } | null>(null);\n const btnRef = useRef<HTMLButtonElement>(null);\n\n useEffect(() => {\n if (!open) return;\n const onDown = (e: MouseEvent) => {\n if (!btnRef.current?.contains(e.target as Node)) setOpen(false);\n };\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setOpen(false);\n window.addEventListener(\"mousedown\", onDown);\n window.addEventListener(\"keydown\", onKey);\n return () => {\n window.removeEventListener(\"mousedown\", onDown);\n window.removeEventListener(\"keydown\", onKey);\n };\n }, [open]);\n\n // Position the bubble under the glyph, clamped within the viewport so it\n // never squeezes against the right edge.\n const place = () => {\n if (!btnRef.current) return;\n const r = btnRef.current.getBoundingClientRect();\n const width = Math.min(260, window.innerWidth - 24);\n const left = Math.max(12, Math.min(r.left, window.innerWidth - width - 12));\n setPos({ top: r.bottom + 6, left, width });\n };\n\n const show = () => {\n place();\n setOpen(true);\n };\n\n const toggle = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (!open) show();\n else setOpen(false);\n };\n\n // Hover trigger also opens on keyboard focus so it stays reachable.\n const hoverProps =\n trigger === \"hover\"\n ? {\n onMouseEnter: show,\n onMouseLeave: () => setOpen(false),\n onFocus: show,\n onBlur: () => setOpen(false),\n }\n : {};\n\n return (\n <>\n <button\n ref={btnRef}\n type=\"button\"\n onClick={toggle}\n aria-label={label}\n aria-expanded={open}\n {...hoverProps}\n className=\"inline-flex cursor-pointer items-center text-text-tertiary transition-colors hover:text-text-primary focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-border-focus\"\n >\n <Info size={size} aria-hidden=\"true\" />\n </button>\n {open &&\n pos &&\n createPortal(\n <div\n role=\"tooltip\"\n style={{ position: \"fixed\", top: pos.top, left: pos.left, width: pos.width }}\n // z-[90]: deliberately an arbitrary value, not @mediaradar/design-tokens'\n // semantic z-tooltip (1200) -- checked directly: mr-clientapps-insights-\n // studio's real toast layer is a literal z-[100] (ToastProvider.tsx),\n // completely unrelated in scale to design-tokens' 1000+ semantic z-index\n // tokens (z-dropdown/z-modal/z-tooltip). Using the shared token here\n // would render this bubble ABOVE that app's toasts -- the opposite of\n // the original intent (a tooltip should never cover a notification).\n // 90 sits above mr-clientapps' modals (below its own z-[100] toasts) as\n // of this writing -- portals to document.body, becoming a sibling of any\n // open dialog rather than a child of it, so a lower z-index would render\n // invisibly behind the dialog that triggered it. RE-VERIFY this number\n // against any NEW consuming app's own z-index scale before trusting it;\n // this is a leaf component making an assumption about global stacking\n // context it can't fully see.\n className=\"z-[90] rounded-control border border-border bg-surface px-3 py-2 text-[12px] font-normal not-italic normal-case leading-relaxed tracking-normal text-text-secondary shadow-overlay\"\n >\n {text}\n </div>,\n document.body\n )}\n </>\n );\n}\n"],"mappings":";AAEA,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AAwFjB,mBAUI,KAVJ;AApDG,SAAS,QAAQ,EAAE,MAAM,OAAO,IAAI,QAAQ,oBAAoB,UAAU,QAAQ,GAAiB;AACxG,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,KAAK,MAAM,IAAI,SAA8D,IAAI;AACxF,QAAM,SAAS,OAA0B,IAAI;AAE7C,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,CAAC,MAAkB;AAChC,UAAI,CAAC,OAAO,SAAS,SAAS,EAAE,MAAc,EAAG,SAAQ,KAAK;AAAA,IAChE;AACA,UAAM,QAAQ,CAAC,MAAqB,EAAE,QAAQ,YAAY,QAAQ,KAAK;AACvE,WAAO,iBAAiB,aAAa,MAAM;AAC3C,WAAO,iBAAiB,WAAW,KAAK;AACxC,WAAO,MAAM;AACX,aAAO,oBAAoB,aAAa,MAAM;AAC9C,aAAO,oBAAoB,WAAW,KAAK;AAAA,IAC7C;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAIT,QAAM,QAAQ,MAAM;AAClB,QAAI,CAAC,OAAO,QAAS;AACrB,UAAM,IAAI,OAAO,QAAQ,sBAAsB;AAC/C,UAAM,QAAQ,KAAK,IAAI,KAAK,OAAO,aAAa,EAAE;AAClD,UAAM,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,MAAM,OAAO,aAAa,QAAQ,EAAE,CAAC;AAC1E,WAAO,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,MAAM,CAAC;AAAA,EAC3C;AAEA,QAAM,OAAO,MAAM;AACjB,UAAM;AACN,YAAQ,IAAI;AAAA,EACd;AAEA,QAAM,SAAS,CAAC,MAAwB;AACtC,MAAE,gBAAgB;AAClB,QAAI,CAAC,KAAM,MAAK;AAAA,QACX,SAAQ,KAAK;AAAA,EACpB;AAGA,QAAM,aACJ,YAAY,UACR;AAAA,IACE,cAAc;AAAA,IACd,cAAc,MAAM,QAAQ,KAAK;AAAA,IACjC,SAAS;AAAA,IACT,QAAQ,MAAM,QAAQ,KAAK;AAAA,EAC7B,IACA,CAAC;AAEP,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAS;AAAA,QACT,cAAY;AAAA,QACZ,iBAAe;AAAA,QACd,GAAG;AAAA,QACJ,WAAU;AAAA,QAEV,8BAAC,QAAK,MAAY,eAAY,QAAO;AAAA;AAAA,IACvC;AAAA,IACC,QACC,OACA;AAAA,MACE;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO,EAAE,UAAU,SAAS,KAAK,IAAI,KAAK,MAAM,IAAI,MAAM,OAAO,IAAI,MAAM;AAAA,UAe3E,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@mediaradar/design-components",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "peerDependencies": {
27
+ "react": "^18.0.0 || ^19.0.0",
28
+ "react-dom": "^18.0.0 || ^19.0.0",
29
+ "@mediaradar/design-tokens": ">=2.0.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "@mediaradar/design-tokens": {
33
+ "optional": false
34
+ }
35
+ },
36
+ "dependencies": {
37
+ "lucide-react": "^1.17.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/react": "^19.2.6",
41
+ "@types/react-dom": "^19.2.6",
42
+ "react": "^19.2.6",
43
+ "react-dom": "^19.2.6",
44
+ "tsup": "^8.3.5",
45
+ "typescript": "^5.7.2"
46
+ },
47
+ "author": "",
48
+ "license": "Apache-2.0",
49
+ "keywords": [
50
+ "design-components",
51
+ "mediaradar"
52
+ ],
53
+ "description": "MediaRadar shared React component primitives -- the canonical implementation for components genuinely common across consuming apps (insights-studio-prototype, mr-clientapps-insights-studio). Not everything belongs here -- see the pilot notes in this package's README before adding a new component."
54
+ }