@carbonid1/design-system 5.7.6 → 5.7.8

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.
@@ -3,17 +3,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { computeTooltipPlacement, } from '../helpers/tooltipPlacement/tooltipPlacement';
4
4
  import { Kbd } from '../Kbd/Kbd';
5
5
  import { cloneElement, useCallback, useEffect, useLayoutEffect, useRef, useState, } from 'react';
6
+ import { createPortal } from 'react-dom';
6
7
  const DEFAULT_DELAY = 200;
7
8
  export const VIEWPORT_MARGIN = 8;
8
- // Must stay in sync with the mb-2 / mt-2 (0.5rem = 8px) classes below the flip
9
- // math subtracts this gap, so a mismatch would mis-predict whether a side fits.
9
+ // Gap in px between the trigger and the tooltip box, baked into the fixed `top`
10
+ // offset below; the flip math subtracts it when deciding whether a side fits.
10
11
  const TRIGGER_GAP = 8;
11
12
  // Position before paint so an edge tooltip never flashes centered-then-shifted.
12
13
  // useLayoutEffect warns and no-ops during SSR, so fall back to useEffect there.
13
14
  const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
14
15
  export const Tooltip = ({ label, shortcut, position = 'top', delay = DEFAULT_DELAY, maxWidth, disabled, className, children, }) => {
15
16
  const [visible, setVisible] = useState(false);
16
- const [placement, setPlacement] = useState({ side: position, offsetX: 0 });
17
+ const [box, setBox] = useState(null);
17
18
  const timeoutRef = useRef(null);
18
19
  const wrapperRef = useRef(null);
19
20
  const tooltipRef = useRef(null);
@@ -71,28 +72,50 @@ export const Tooltip = ({ label, shortcut, position = 'top', delay = DEFAULT_DEL
71
72
  if (!wrapper || !tooltip)
72
73
  return;
73
74
  const trigger = wrapper.getBoundingClientRect();
74
- const box = tooltip.getBoundingClientRect();
75
- const next = computeTooltipPlacement({
75
+ const tip = tooltip.getBoundingClientRect();
76
+ const placement = computeTooltipPlacement({
76
77
  trigger,
77
- tooltip: { width: box.width, height: box.height },
78
+ tooltip: { width: tip.width, height: tip.height },
78
79
  viewport: { width: window.innerWidth, height: window.innerHeight },
79
80
  preferredSide: position,
80
81
  margin: VIEWPORT_MARGIN,
81
82
  gap: TRIGGER_GAP,
82
83
  });
83
- // Keep the same object when nothing moved so a show or a no-op resize
84
- // doesn't trigger a redundant re-render (the common centered case is a no-op).
85
- setPlacement(prev => (prev.side === next.side && prev.offsetX === next.offsetX ? prev : next));
84
+ const left = trigger.left + trigger.width / 2;
85
+ const top = placement.side === 'top' ? trigger.top - TRIGGER_GAP : trigger.bottom + TRIGGER_GAP;
86
+ // Keep the same object when nothing moved so a no-op resize/scroll doesn't
87
+ // trigger a redundant re-render (the common centered, still case is a no-op).
88
+ setBox(prev => prev &&
89
+ prev.side === placement.side &&
90
+ prev.offsetX === placement.offsetX &&
91
+ prev.left === left &&
92
+ prev.top === top
93
+ ? prev
94
+ : { ...placement, left, top });
86
95
  };
87
96
  measure();
97
+ // Portaled and fixed, the box no longer rides the trigger's scroll container,
98
+ // so re-anchor on scroll (capture, to catch any scrolling ancestor) and resize.
88
99
  window.addEventListener('resize', measure, { passive: true });
89
- return () => window.removeEventListener('resize', measure);
100
+ window.addEventListener('scroll', measure, { capture: true, passive: true });
101
+ return () => {
102
+ window.removeEventListener('resize', measure);
103
+ window.removeEventListener('scroll', measure, { capture: true });
104
+ };
90
105
  }, [visible, position]);
91
- const positionClasses = placement.side === 'top' ? 'bottom-full mb-2' : 'top-full mt-2';
92
- return (_jsxs("div", { ref: wrapperRef, className: `relative inline-flex${className ? ` ${className}` : ''}`, onMouseEnter: show, onMouseLeave: hide, onPointerDown: hide, onFocus: show, onBlur: hide, children: [cloneElement(children, {
106
+ const boxStyle = {
107
+ position: 'fixed',
108
+ left: box?.left ?? 0,
109
+ top: box?.top ?? 0,
110
+ transform: `translate(calc(-50% + ${box?.offsetX ?? 0}px), ${box?.side === 'top' ? '-100%' : '0'})`,
111
+ // Hidden until measured so the first frame never paints at the un-anchored origin.
112
+ visibility: box ? 'visible' : 'hidden',
113
+ ...(maxWidth ? { maxWidth } : {}),
114
+ };
115
+ return (_jsxs("div", { ref: wrapperRef, className: `inline-flex${className ? ` ${className}` : ''}`, onMouseEnter: show, onMouseLeave: hide, onPointerDown: hide, onFocus: show, onBlur: hide, children: [cloneElement(children, {
93
116
  'aria-label': children.props['aria-label'] ?? label,
94
- }), visible && !disabled && (_jsxs("div", { ref: tooltipRef, role: "tooltip", style: {
95
- transform: `translateX(calc(-50% + ${placement.offsetX}px))`,
96
- ...(maxWidth ? { maxWidth } : {}),
97
- }, className: `absolute left-1/2 z-50 ${maxWidth ? 'w-max whitespace-normal' : 'whitespace-nowrap'} bg-foreground text-background pointer-events-none flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs shadow-lg ${positionClasses}`, children: [label, shortcut && (_jsx(Kbd, { keys: shortcut, size: "sm", className: "bg-background/15 border-transparent" }))] }))] }));
117
+ }), visible &&
118
+ !disabled &&
119
+ typeof document !== 'undefined' &&
120
+ createPortal(_jsxs("div", { ref: tooltipRef, role: "tooltip", style: boxStyle, className: `bg-foreground text-background pointer-events-none z-50 flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs shadow-lg ${maxWidth ? 'w-max whitespace-normal' : 'whitespace-nowrap'}`, children: [label, shortcut && (_jsx(Kbd, { keys: shortcut, size: "sm", className: "bg-background/15 border-transparent" }))] }), document.body)] }));
98
121
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbonid1/design-system",
3
- "version": "5.7.6",
3
+ "version": "5.7.8",
4
4
  "description": "Shared React UI primitives + design tokens (themes, postcss config)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { mkdir, rm, symlink } from 'node:fs/promises'
2
+ import { lstat, mkdir, rm, symlink } from 'node:fs/promises'
3
3
  import { dirname, join, resolve } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
 
@@ -15,13 +15,37 @@ if (
15
15
  }
16
16
 
17
17
  const consumerRoot = process.env.INIT_CWD || process.cwd()
18
- const skillDest = join(consumerRoot, '.claude', 'skills', 'design-system')
18
+ const skillDestinations = [
19
+ join(consumerRoot, '.claude', 'skills', 'design-system'),
20
+ join(consumerRoot, '.agents', 'skills', 'design-system'),
21
+ ]
22
+
23
+ async function removeExistingSkillLink(skillDest) {
24
+ try {
25
+ const stat = await lstat(skillDest)
26
+ if (!stat.isSymbolicLink()) {
27
+ console.warn(`[@carbonid1/design-system] skipped existing non-symlink skill: ${skillDest}`)
28
+ return false
29
+ }
30
+ } catch (err) {
31
+ if (err.code !== 'ENOENT') {
32
+ throw err
33
+ }
34
+ }
19
35
 
20
- try {
21
- await mkdir(dirname(skillDest), { recursive: true })
22
36
  await rm(skillDest, { recursive: true, force: true })
23
- await symlink(SKILL_SRC, skillDest, 'dir')
24
- console.log(`[@carbonid1/design-system] linked skill → ${skillDest}`)
25
- } catch (err) {
26
- console.warn(`[@carbonid1/design-system] could not link skill: ${err.message}`)
37
+ return true
38
+ }
39
+
40
+ for (const skillDest of skillDestinations) {
41
+ try {
42
+ await mkdir(dirname(skillDest), { recursive: true })
43
+ if (!(await removeExistingSkillLink(skillDest))) {
44
+ continue
45
+ }
46
+ await symlink(SKILL_SRC, skillDest, 'dir')
47
+ console.log(`[@carbonid1/design-system] linked skill → ${skillDest}`)
48
+ } catch (err) {
49
+ console.warn(`[@carbonid1/design-system] could not link skill: ${err.message}`)
50
+ }
27
51
  }