@signal9/era-ui 22.0.0 → 23.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/era-ui.css +1 -1
  3. package/dist/eslint/index.d.ts +55 -0
  4. package/dist/eslint/index.js +77 -0
  5. package/dist/eslint/rules-core.d.ts +14 -0
  6. package/dist/eslint/rules-core.js +111 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/os/desktop.svelte +42 -11
  9. package/dist/os/desktop.svelte.d.ts +16 -0
  10. package/dist/ui/accordion/accordion-trigger.svelte +2 -2
  11. package/dist/ui/accordion/accordion-trigger.svelte.d.ts +2 -2
  12. package/dist/ui/alert-dialog/alert-dialog-content.svelte +6 -2
  13. package/dist/ui/alert-dialog/alert-dialog-content.svelte.d.ts +2 -2
  14. package/dist/ui/alert-dialog/alert-dialog-root.svelte +33 -0
  15. package/dist/ui/alert-dialog/alert-dialog-root.svelte.d.ts +8 -0
  16. package/dist/ui/alert-dialog/alert-dialog-trigger.svelte +8 -0
  17. package/dist/ui/alert-dialog/index.d.ts +1 -1
  18. package/dist/ui/alert-dialog/index.js +1 -1
  19. package/dist/ui/combobox/combobox-trigger.svelte +2 -2
  20. package/dist/ui/combobox/combobox-trigger.svelte.d.ts +2 -2
  21. package/dist/ui/context-menu/context-menu-content.svelte +6 -2
  22. package/dist/ui/context-menu/context-menu-content.svelte.d.ts +2 -2
  23. package/dist/ui/context-menu/context-menu-root.svelte +33 -0
  24. package/dist/ui/context-menu/context-menu-root.svelte.d.ts +8 -0
  25. package/dist/ui/context-menu/context-menu-sub-content.svelte +6 -2
  26. package/dist/ui/context-menu/context-menu-sub-content.svelte.d.ts +2 -2
  27. package/dist/ui/context-menu/context-menu-sub-trigger.svelte +2 -2
  28. package/dist/ui/context-menu/context-menu-sub-trigger.svelte.d.ts +2 -2
  29. package/dist/ui/context-menu/context-menu-trigger.svelte +8 -0
  30. package/dist/ui/context-menu/index.d.ts +1 -1
  31. package/dist/ui/context-menu/index.js +1 -1
  32. package/dist/ui/date-picker/date-picker-trigger.svelte +2 -2
  33. package/dist/ui/date-picker/date-picker-trigger.svelte.d.ts +2 -2
  34. package/dist/ui/date-range-picker/date-range-picker-trigger.svelte +2 -2
  35. package/dist/ui/date-range-picker/date-range-picker-trigger.svelte.d.ts +2 -2
  36. package/dist/ui/dialog/dialog-content.svelte +16 -2
  37. package/dist/ui/dialog/dialog-content.svelte.d.ts +2 -2
  38. package/dist/ui/dialog/dialog-root.svelte +43 -0
  39. package/dist/ui/dialog/dialog-root.svelte.d.ts +8 -0
  40. package/dist/ui/dialog/dialog-trigger.svelte +10 -0
  41. package/dist/ui/dialog/index.d.ts +1 -1
  42. package/dist/ui/dialog/index.js +1 -1
  43. package/dist/ui/dropdown-menu/dropdown-menu-content.svelte +6 -2
  44. package/dist/ui/dropdown-menu/dropdown-menu-content.svelte.d.ts +2 -2
  45. package/dist/ui/dropdown-menu/dropdown-menu-root.svelte +33 -0
  46. package/dist/ui/dropdown-menu/dropdown-menu-root.svelte.d.ts +8 -0
  47. package/dist/ui/dropdown-menu/dropdown-menu-sub-content.svelte +6 -2
  48. package/dist/ui/dropdown-menu/dropdown-menu-sub-content.svelte.d.ts +2 -2
  49. package/dist/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +2 -2
  50. package/dist/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte.d.ts +2 -2
  51. package/dist/ui/dropdown-menu/dropdown-menu-trigger.svelte +10 -2
  52. package/dist/ui/dropdown-menu/dropdown-menu-trigger.svelte.d.ts +2 -2
  53. package/dist/ui/dropdown-menu/index.d.ts +1 -1
  54. package/dist/ui/dropdown-menu/index.js +1 -1
  55. package/dist/ui/index.d.ts +1 -0
  56. package/dist/ui/index.js +1 -0
  57. package/dist/ui/link-preview/index.d.ts +1 -1
  58. package/dist/ui/link-preview/index.js +1 -1
  59. package/dist/ui/link-preview/link-preview-content.svelte +6 -2
  60. package/dist/ui/link-preview/link-preview-content.svelte.d.ts +2 -2
  61. package/dist/ui/link-preview/link-preview-root.svelte +33 -0
  62. package/dist/ui/link-preview/link-preview-root.svelte.d.ts +8 -0
  63. package/dist/ui/link-preview/link-preview-trigger.svelte +8 -0
  64. package/dist/ui/menubar/menubar-content.svelte +6 -2
  65. package/dist/ui/menubar/menubar-content.svelte.d.ts +2 -2
  66. package/dist/ui/menubar/menubar-root.svelte +26 -1
  67. package/dist/ui/menubar/menubar-root.svelte.d.ts +1 -1
  68. package/dist/ui/menubar/menubar-sub-content.svelte +6 -2
  69. package/dist/ui/menubar/menubar-sub-content.svelte.d.ts +2 -2
  70. package/dist/ui/menubar/menubar-sub-trigger.svelte +2 -2
  71. package/dist/ui/menubar/menubar-sub-trigger.svelte.d.ts +2 -2
  72. package/dist/ui/nav/index.d.ts +2 -0
  73. package/dist/ui/nav/index.js +2 -0
  74. package/dist/ui/nav/nav-bar.svelte +51 -0
  75. package/dist/ui/nav/nav-bar.svelte.d.ts +21 -0
  76. package/dist/ui/nav/nav-item.svelte +79 -0
  77. package/dist/ui/nav/nav-item.svelte.d.ts +29 -0
  78. package/dist/ui/provider/axes.d.ts +14 -1
  79. package/dist/ui/provider/axis-relay.svelte.d.ts +34 -0
  80. package/dist/ui/provider/axis-relay.svelte.js +109 -0
  81. package/dist/ui/select/select-content.svelte +2 -2
  82. package/dist/ui/select/select-content.svelte.d.ts +2 -2
  83. package/dist/ui/select/select-trigger.svelte +2 -2
  84. package/dist/ui/select/select-trigger.svelte.d.ts +2 -2
  85. package/dist/ui/sheet/sheet-content.svelte +8 -2
  86. package/dist/ui/sheet/sheet-content.svelte.d.ts +2 -2
  87. package/dist/ui/step/step-content.svelte +2 -2
  88. package/dist/ui/step/step-content.svelte.d.ts +2 -2
  89. package/dist/ui/step/step-summary.svelte +2 -2
  90. package/dist/ui/step/step-summary.svelte.d.ts +2 -2
  91. package/dist/ui/tooltip/index.d.ts +1 -1
  92. package/dist/ui/tooltip/index.js +1 -1
  93. package/dist/ui/tooltip/tooltip-content.svelte +6 -2
  94. package/dist/ui/tooltip/tooltip-content.svelte.d.ts +2 -2
  95. package/dist/ui/tooltip/tooltip-root.svelte +33 -0
  96. package/dist/ui/tooltip/tooltip-root.svelte.d.ts +8 -0
  97. package/dist/ui/tooltip/tooltip-trigger.svelte +8 -0
  98. package/dist/utils/index.d.ts +36 -3
  99. package/package.json +10 -5
@@ -0,0 +1,77 @@
1
+ /*
2
+ * ESLINT RULES FOR CONSUMING ERA.
3
+ *
4
+ * era's failure mode is silently wrong, never loudly broken. An unknown class
5
+ * renders as nothing; an arbitrary value renders as SOMETHING, which is worse,
6
+ * because it looks fine on the density you wrote it at and quietly falls off
7
+ * the ladder on the other three. Neither shows up in a type check, a test, or a
8
+ * screenshot taken at one setting.
9
+ *
10
+ * These rules catch the two shapes that account for nearly all of it, and both
11
+ * were proposed by a consumer who had hit them repeatedly rather than invented
12
+ * here:
13
+ *
14
+ * era/no-arbitrary-tier h-[24px] / h-(--era-h-control) where h-control is
15
+ * the name for exactly that value.
16
+ * era/no-hardcoded-motion duration-150 / ease-in-out, which pin timing that
17
+ * the motion axis is supposed to own — including at
18
+ * prefers-reduced-motion, where they refuse to stop.
19
+ *
20
+ * THE VOCABULARY IS IMPORTED, NOT RESTATED. TIERS and SPACING come from
21
+ * utils/tw-merge-config.js, the same lists cn() is built from and that
22
+ * check-twmerge verifies against the stylesheet. A second copy here would be
23
+ * the exact hand-maintained duplicate this repo keeps having to delete — and it
24
+ * would go stale in the direction that matters, telling consumers a rung exists
25
+ * that does not.
26
+ */
27
+ import { TIERS, SPACING } from '../utils/tw-merge-config.js';
28
+ import { findArbitraryTiers as findTiers, findHardcodedMotion } from './rules-core.js';
29
+ /** era's real vocabulary, bound once. */
30
+ const VOCAB = { TIERS, SPACING };
31
+ /** Arbitrary/var-form lengths where era has a name, against era's own lists. */
32
+ export const findArbitraryTiers = (value) => findTiers(value, VOCAB);
33
+ export { findHardcodedMotion };
34
+ function makeRule(description, find) {
35
+ return {
36
+ meta: { type: 'problem', docs: { description }, schema: [] },
37
+ create(ctx) {
38
+ const check = (node, text) => {
39
+ for (const r of find(text))
40
+ ctx.report({ node, message: r.message });
41
+ };
42
+ return {
43
+ Literal(node) {
44
+ if (typeof node.value === 'string')
45
+ check(node, node.value);
46
+ },
47
+ TemplateElement(node) {
48
+ const cooked = node.value?.cooked;
49
+ if (typeof cooked === 'string')
50
+ check(node, cooked);
51
+ },
52
+ SvelteLiteral(node) {
53
+ if (typeof node.value === 'string')
54
+ check(node, node.value);
55
+ }
56
+ };
57
+ }
58
+ };
59
+ }
60
+ export const rules = {
61
+ 'no-arbitrary-tier': makeRule('Use era’s named rungs instead of arbitrary or var-form lengths.', findArbitraryTiers),
62
+ 'no-hardcoded-motion': makeRule('Take transition timing from the motion axis, not from a literal.', findHardcodedMotion)
63
+ };
64
+ const plugin = {
65
+ meta: { name: '@sig-nine/era-ui/eslint' },
66
+ rules,
67
+ configs: {}
68
+ };
69
+ /** Flat-config preset: both rules as errors. */
70
+ plugin.configs.recommended = {
71
+ plugins: { era: plugin },
72
+ rules: {
73
+ 'era/no-arbitrary-tier': 'error',
74
+ 'era/no-hardcoded-motion': 'error'
75
+ }
76
+ };
77
+ export default plugin;
@@ -0,0 +1,14 @@
1
+ /** One flagged class, with the offset that lets a host report it precisely. */
2
+ export interface Report {
3
+ index: number;
4
+ length: number;
5
+ message: string;
6
+ }
7
+ export interface Vocabulary {
8
+ TIERS: string[];
9
+ SPACING: string[];
10
+ }
11
+ /** Every offending token in one class string, with its offset for a precise report. */
12
+ export declare function findArbitraryTiers(value: string, vocab: Vocabulary): Report[];
13
+ /** Hardcoded timing, which the motion axis is supposed to own. */
14
+ export declare function findHardcodedMotion(value: string): Report[];
@@ -0,0 +1,111 @@
1
+ /*
2
+ * THE PURE RULE LOGIC — string in, reports out.
3
+ *
4
+ * Split from index.ts for one reason: the vocabulary is PASSED IN rather than
5
+ * imported here. index.ts wires it to the real TIERS/SPACING (the same lists
6
+ * cn() is built from), and scripts/check-eslint-rules.ts wires the same lists
7
+ * itself, so the self-test exercises exactly what ships without needing to
8
+ * resolve the package's `.js`-suffixed internal imports from source.
9
+ *
10
+ * No cross-directory imports live in this file, and that is deliberate.
11
+ */
12
+ /** Prefixes whose value is a tier height/width. */
13
+ const TIER_PREFIXES = ['h', 'min-h', 'max-h', 'w', 'min-w', 'max-w', 'size', 'rounded'];
14
+ /** Prefixes whose value is on the --spacing-* scale. */
15
+ const SPACING_PREFIXES = [
16
+ 'p',
17
+ 'px',
18
+ 'py',
19
+ 'pt',
20
+ 'pr',
21
+ 'pb',
22
+ 'pl',
23
+ 'm',
24
+ 'mx',
25
+ 'my',
26
+ 'mt',
27
+ 'mr',
28
+ 'mb',
29
+ 'ml',
30
+ 'gap',
31
+ 'gap-x',
32
+ 'gap-y',
33
+ 'top',
34
+ 'right',
35
+ 'bottom',
36
+ 'left',
37
+ 'inset',
38
+ 'inset-x',
39
+ 'inset-y'
40
+ ];
41
+ /** `--era-h-control` -> `control`, so the var form can be named back. */
42
+ function tierFromToken(token, TIERS) {
43
+ const m = token.match(/^--era-(?:h|rd)-([a-z]+)$/);
44
+ return m && TIERS.includes(m[1]) ? m[1] : null;
45
+ }
46
+ function spacingFromToken(token, SPACING) {
47
+ const m = token.match(/^--era-(.+)$/);
48
+ if (!m)
49
+ return null;
50
+ const name = m[1] === 'gap' ? 'gutter' : m[1];
51
+ return SPACING.includes(name) ? name : null;
52
+ }
53
+ /** Every offending token in one class string, with its offset for a precise report. */
54
+ export function findArbitraryTiers(value, vocab) {
55
+ const { TIERS, SPACING } = vocab;
56
+ const out = [];
57
+ for (const m of value.matchAll(/(?:^|\s)((?:[a-z-]+:)*)([a-z-]+)-(\[[^\]]+\]|\([^)]+\))/g)) {
58
+ const cls = m[0].trimStart();
59
+ const index = m.index + (m[0].length - cls.length);
60
+ const [, variants, prefix, raw] = m;
61
+ const inner = raw.slice(1, -1);
62
+ // The paren form is era's own shorthand for a token — it is not wrong, it
63
+ // is just unnamed when a name exists.
64
+ const named = tierFromToken(inner, TIERS) ?? spacingFromToken(inner, SPACING);
65
+ if (named) {
66
+ const suggestion = `${variants}${prefix}-${named}`;
67
+ if ((TIER_PREFIXES.includes(prefix) && TIERS.includes(named)) ||
68
+ (SPACING_PREFIXES.includes(prefix) && SPACING.includes(named))) {
69
+ out.push({
70
+ index,
71
+ length: cls.length,
72
+ message: `\`${cls}\` names a value era already has a name for — use \`${suggestion}\`. The named form is what tailwind-merge can resolve, so a consumer overriding it actually wins.`
73
+ });
74
+ continue;
75
+ }
76
+ }
77
+ // A bare ABSOLUTE length in a tier or spacing position is the real hazard:
78
+ // it is correct at the one density it was written at and wrong at the
79
+ // other three.
80
+ //
81
+ // Absolute only — px/rem/em, and a unitless 0. A percentage, ch, vh or fr
82
+ // is not on the ladder and era has no rung to offer instead, so flagging
83
+ // `w-[40%]` or `max-w-[65ch]` would be telling a consumer to replace
84
+ // correct code with something that does not exist. Both were false
85
+ // positives in the first draft, caught by this rule's own self-test.
86
+ const isLadderLength = /^-?\d+(?:\.\d+)?(?:px|rem|em)?$/.test(inner);
87
+ if (isLadderLength && (TIER_PREFIXES.includes(prefix) || SPACING_PREFIXES.includes(prefix))) {
88
+ out.push({
89
+ index,
90
+ length: cls.length,
91
+ message: `\`${cls}\` hardcodes a length on an axis era scales. It is correct at the density you wrote it at and wrong at the other three — use a rung (${prefix}-${TIERS.join(' / ')}) or a spacing name.`
92
+ });
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+ /** Hardcoded timing, which the motion axis is supposed to own. */
98
+ export function findHardcodedMotion(value) {
99
+ const out = [];
100
+ for (const m of value.matchAll(/(?:^|\s)((?:[a-z-]+:)*)(duration-\d+|duration-\[[^\]]+\]|ease-(?:linear|in|out|in-out)|ease-\[[^\]]+\])/g)) {
101
+ const cls = (m[1] ?? '') + m[2];
102
+ const index = m.index + (m[0].length - cls.length);
103
+ const fix = m[2].startsWith('duration') ? 'duration-base' : 'ease-base';
104
+ out.push({
105
+ index,
106
+ length: cls.length,
107
+ message: `\`${cls}\` pins timing the motion axis owns — use \`${fix}\`. Hardcoded timing ignores data-motion="instant", which is also what prefers-reduced-motion resolves to, so this animates for people who asked it not to.`
108
+ });
109
+ }
110
+ return out;
111
+ }
package/dist/index.d.ts CHANGED
@@ -29,6 +29,6 @@
29
29
  * moment it exists, with no export map to maintain — and it means no component
30
30
  * can ever be pulled in by accident, because nothing re-exports them together.
31
31
  */
32
- export { cn, tv, hotkeys, keys, parseKeybinding, matchKeyBindingPress, createKeybindingsHandler, formatKeybinding, isApplePlatform, modKey, type KeyBindingMap, type KeyBindingPress, type KeyBindingOptions, type PartProps, type VariantProps } from './utils/index.js';
32
+ export { cn, tv, hotkeys, keys, parseKeybinding, matchKeyBindingPress, createKeybindingsHandler, formatKeybinding, isApplePlatform, modKey, type KeyBindingMap, type KeyBindingPress, type KeyBindingOptions, type PartProps, type OwnedPartProps, type VariantProps } from './utils/index.js';
33
33
  export { modes, defaultMode, surfaces, defaultSurface, surfaceRegistry, corners, defaultCorners, fonts, defaultFont, motions, defaultMotion, axisRegistry, axisKeys, type Mode, type Surface, type SurfaceMeta, type Corners, type Font, type Motion, type AxisKey, type AxisMeta, type AxisLevel } from './ui/provider/index.js';
34
34
  export { Mode as ModeProvider } from './ui/mode/index.js';
@@ -9,6 +9,7 @@
9
9
  let {
10
10
  pm: wmProp,
11
11
  notifications: notifProp,
12
+ embedded = false,
12
13
  children,
13
14
  class: className,
14
15
  ...restProps
@@ -18,6 +19,22 @@
18
19
  pm?: PaneManager;
19
20
  /** The notification service, likewise shared via context. */
20
21
  notifications?: NotificationService;
22
+ /**
23
+ * This Desktop is a WIDGET inside an ordinary page, not the viewport.
24
+ *
25
+ * A full-viewport Desktop takes the document with it: it locks
26
+ * <html>/<body> against scrolling and fences pinch-zoom, both of which are
27
+ * correct when the workspace IS the page and hostile when it is a 26rem
28
+ * box in the middle of a marketing site. Shipped exactly that way — a
29
+ * consumer embedded one and their whole page stopped scrolling.
30
+ *
31
+ * Named for the situation rather than the mechanism. `lockScroll={false}`
32
+ * was the obvious spelling and is the wrong one: more than one behaviour
33
+ * differs for an embedded Desktop, so a mechanism-named boolean needs a
34
+ * second boolean the first time that is true — and a consumer would have
35
+ * to know which mechanisms to disable rather than what they are building.
36
+ */
37
+ embedded?: boolean;
21
38
  /** Chrome + wallpaper: place <Taskbar>, <Toaster>, <Launcher>,
22
39
  * <NotificationCenter> and any desktop widgets here. */
23
40
  children?: Snippet;
@@ -68,16 +85,23 @@
68
85
  untrack(() => pm.relayout());
69
86
  });
70
87
 
71
- // Document scroll lock: the Desktop IS the viewport, so the page itself must
72
- // never scroll, rubber-band, or pull-to-refresh — on a phone those drag the
73
- // whole workspace around, which is meaningless for a fixed surface. Lock
74
- // <html>/<body> for the Desktop's lifetime and restore their prior inline
75
- // values on unmount, so a consumer's ordinary page is untouched before and
76
- // after the workspace mounts. Scrolling INSIDE panes and menus is unaffected
77
- // those are their own overflow-auto containers with overscroll-behavior.
78
- // overscroll-behavior:none is the specific killer for iOS pull-to-refresh and
79
- // the elastic bounce; overflow:hidden + height stop any residual page scroll.
88
+ // Document scroll lock: a full-viewport Desktop IS the viewport, so the page
89
+ // itself must never scroll, rubber-band, or pull-to-refresh — on a phone
90
+ // those drag the whole workspace around, which is meaningless for a fixed
91
+ // surface. Lock <html>/<body> for the Desktop's lifetime and restore their
92
+ // prior inline values on unmount. Scrolling INSIDE panes and menus is
93
+ // unaffected those are their own overflow-auto containers with
94
+ // overscroll-behavior. overscroll-behavior:none is the specific killer for
95
+ // iOS pull-to-refresh and the elastic bounce; overflow:hidden + height stop
96
+ // any residual page scroll.
97
+ //
98
+ // NOT WHEN EMBEDDED. This comment used to say the consumer's ordinary page is
99
+ // untouched "before and after the workspace mounts", which is true and reads
100
+ // as broader than it is — DURING was the whole problem. A Desktop embedded as
101
+ // a widget in a scrolling page froze that page for its entire lifetime, and
102
+ // nothing in the API suggested a component would take the document with it.
80
103
  $effect(() => {
104
+ if (embedded) return;
81
105
  const els = [document.documentElement, document.body];
82
106
  const lock = { overflow: 'hidden', overscrollBehavior: 'none', height: '100%' } as const;
83
107
  const keys = Object.keys(lock) as (keyof typeof lock)[];
@@ -93,9 +117,16 @@
93
117
  // chrome away and strand panes). Safari fires non-standard gesture* events
94
118
  // for pinch/rotate; other engines surface it as a multi-touch touchmove.
95
119
  // Cancel both — non-passive, so preventDefault actually stops the gesture.
96
- // (Single-finger pane dragging runs on pointer events and is unaffected; the
97
- // page can't scroll anyway now, see the scroll lock above.)
120
+ // (Single-finger pane dragging runs on pointer events and is unaffected; a
121
+ // full-viewport Desktop cannot scroll the page anyway, see the scroll lock.)
122
+ //
123
+ // ALSO NOT WHEN EMBEDDED, and this half is an accessibility question rather
124
+ // than a layout one. Pinch-zoom is how people with low vision read a page;
125
+ // swallowing it because the finger landed on a widget takes that away from
126
+ // the whole document. The parenthetical above is the tell — it assumed the
127
+ // page could not scroll, which stops being true the moment the lock is off.
98
128
  $effect(() => {
129
+ if (embedded) return;
99
130
  const el = ref;
100
131
  if (!el) return;
101
132
  const stop = (e: Event) => e.preventDefault();
@@ -8,6 +8,22 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
8
8
  pm?: PaneManager;
9
9
  /** The notification service, likewise shared via context. */
10
10
  notifications?: NotificationService;
11
+ /**
12
+ * This Desktop is a WIDGET inside an ordinary page, not the viewport.
13
+ *
14
+ * A full-viewport Desktop takes the document with it: it locks
15
+ * <html>/<body> against scrolling and fences pinch-zoom, both of which are
16
+ * correct when the workspace IS the page and hostile when it is a 26rem
17
+ * box in the middle of a marketing site. Shipped exactly that way — a
18
+ * consumer embedded one and their whole page stopped scrolling.
19
+ *
20
+ * Named for the situation rather than the mechanism. `lockScroll={false}`
21
+ * was the obvious spelling and is the wrong one: more than one behaviour
22
+ * differs for an embedded Desktop, so a mechanism-named boolean needs a
23
+ * second boolean the first time that is true — and a consumer would have
24
+ * to know which mechanisms to disable rather than what they are building.
25
+ */
26
+ embedded?: boolean;
11
27
  /** Chrome + wallpaper: place <Taskbar>, <Toaster>, <Launcher>,
12
28
  * <NotificationCenter> and any desktop widgets here. */
13
29
  children?: Snippet;
@@ -1,14 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { Accordion } from 'bits-ui';
3
3
  import ChevronDown from '@lucide/svelte/icons/chevron-down';
4
- import { cn, type PartProps } from '../../utils/index.js';
4
+ import { cn, type OwnedPartProps } from '../../utils/index.js';
5
5
 
6
6
  let {
7
7
  ref = $bindable(null),
8
8
  children,
9
9
  class: className,
10
10
  ...restProps
11
- }: PartProps<Accordion.TriggerProps> = $props();
11
+ }: OwnedPartProps<Accordion.TriggerProps> = $props();
12
12
  </script>
13
13
 
14
14
  <Accordion.Trigger
@@ -1,5 +1,5 @@
1
1
  import { Accordion } from 'bits-ui';
2
- import { type PartProps } from '../../utils/index.js';
3
- declare const AccordionTrigger: import("svelte").Component<PartProps<Accordion.TriggerProps>, {}, "ref">;
2
+ import { type OwnedPartProps } from '../../utils/index.js';
3
+ declare const AccordionTrigger: import("svelte").Component<OwnedPartProps<Accordion.TriggerProps>, {}, "ref">;
4
4
  type AccordionTrigger = ReturnType<typeof AccordionTrigger>;
5
5
  export default AccordionTrigger;
@@ -1,19 +1,23 @@
1
1
  <script lang="ts">
2
+ import { getAxisRelay } from '../provider/axis-relay.svelte.js';
2
3
  import { AlertDialog } from 'bits-ui';
3
- import { cn, type PartProps } from '../../utils/index.js';
4
+ import { cn, type OwnedPartProps } from '../../utils/index.js';
4
5
 
5
6
  let {
6
7
  ref = $bindable(null),
7
8
  children,
8
9
  class: className,
9
10
  ...restProps
10
- }: PartProps<AlertDialog.ContentProps> = $props();
11
+ }: OwnedPartProps<AlertDialog.ContentProps> = $props();
12
+
13
+ const relay = getAxisRelay();
11
14
  </script>
12
15
 
13
16
  <AlertDialog.Portal>
14
17
  <AlertDialog.Overlay class="fixed inset-0 z-overlay bg-overlay" />
15
18
  <AlertDialog.Content
16
19
  bind:ref
20
+ {...relay?.attrs}
17
21
  class={cn(
18
22
  'fixed top-1/2 left-1/2 z-overlay flex w-full max-w-md -translate-x-1/2 -translate-y-1/2 flex-col gap-gutter rounded-bar border border-divider bg-well p-card shadow-lg glass-blur',
19
23
  className
@@ -1,4 +1,4 @@
1
- import { type PartProps } from '../../utils/index.js';
2
- declare const AlertDialogContent: import("svelte").Component<PartProps<import("bits-ui").DialogContentProps>, {}, "ref">;
1
+ import { type OwnedPartProps } from '../../utils/index.js';
2
+ declare const AlertDialogContent: import("svelte").Component<OwnedPartProps<import("bits-ui").DialogContentProps>, {}, "ref">;
3
3
  type AlertDialogContent = ReturnType<typeof AlertDialogContent>;
4
4
  export default AlertDialogContent;
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { AlertDialog } from 'bits-ui';
4
+ import { setAxisRelay } from '../provider/axis-relay.svelte.js';
5
+
6
+ /*
7
+ * era owns Root ONLY to open the axis channel — it renders no DOM, adds no
8
+ * behaviour and forwards everything. There has to BE a component here: a
9
+ * re-export (`export const Root = AlertDialog.Root`) has nowhere to put a context,
10
+ * which is exactly why the axes could not cross the portal before.
11
+ *
12
+ * `open` is re-declared bindable because a spread cannot carry a two-way
13
+ * binding, so wrapping a primitive breaks `bind:open` at every call site
14
+ * unless the wrapper states it.
15
+ */
16
+ let {
17
+ open = $bindable(false),
18
+ children,
19
+ ...restProps
20
+ }: AlertDialog.RootProps & { children?: Snippet } = $props();
21
+
22
+ const relay = setAxisRelay();
23
+
24
+ // Capture on OPEN, not on mount: bits-ui mounts content eagerly and toggles
25
+ // it, so a mount-time read runs once at page load and never again.
26
+ $effect(() => {
27
+ if (open) relay.refresh();
28
+ });
29
+ </script>
30
+
31
+ <AlertDialog.Root bind:open {...restProps}>
32
+ {@render children?.()}
33
+ </AlertDialog.Root>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { AlertDialog } from 'bits-ui';
3
+ type $$ComponentProps = AlertDialog.RootProps & {
4
+ children?: Snippet;
5
+ };
6
+ declare const AlertDialogRoot: import("svelte").Component<$$ComponentProps, {}, "open">;
7
+ type AlertDialogRoot = ReturnType<typeof AlertDialogRoot>;
8
+ export default AlertDialogRoot;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { getAxisRelay } from '../provider/axis-relay.svelte.js';
2
3
  import type { Snippet } from 'svelte';
3
4
  import { AlertDialog, type WithoutChildrenOrChild } from 'bits-ui';
4
5
  import { Button, type ButtonVariants } from '../button';
@@ -17,6 +18,13 @@
17
18
  ButtonVariants & {
18
19
  children?: Snippet;
19
20
  } = $props();
21
+
22
+ // The trigger is the last node still inside the subtree, so it is the
23
+ // anchor the axes are read FROM when the content opens.
24
+ const relay = getAxisRelay();
25
+ $effect(() => {
26
+ relay?.register(ref);
27
+ });
20
28
  </script>
21
29
 
22
30
  <AlertDialog.Trigger {...restProps}>
@@ -1,5 +1,5 @@
1
1
  import { AlertDialog } from 'bits-ui';
2
- export declare const Root: typeof AlertDialog.Root;
2
+ export { default as Root } from './alert-dialog-root.svelte';
3
3
  export declare const Portal: typeof AlertDialog.Portal;
4
4
  export { default as Trigger } from './alert-dialog-trigger.svelte';
5
5
  export { default as Content } from './alert-dialog-content.svelte';
@@ -1,5 +1,5 @@
1
1
  import { AlertDialog } from 'bits-ui';
2
- export const Root = AlertDialog.Root;
2
+ export { default as Root } from './alert-dialog-root.svelte';
3
3
  export const Portal = AlertDialog.Portal;
4
4
  export { default as Trigger } from './alert-dialog-trigger.svelte';
5
5
  export { default as Content } from './alert-dialog-content.svelte';
@@ -1,14 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { Combobox } from 'bits-ui';
3
3
  import ChevronDown from '@lucide/svelte/icons/chevron-down';
4
- import { cn, type PartProps } from '../../utils/index.js';
4
+ import { cn, type OwnedPartProps } from '../../utils/index.js';
5
5
 
6
6
  let {
7
7
  ref = $bindable(null),
8
8
  children,
9
9
  class: className,
10
10
  ...restProps
11
- }: PartProps<Combobox.TriggerProps> = $props();
11
+ }: OwnedPartProps<Combobox.TriggerProps> = $props();
12
12
  </script>
13
13
 
14
14
  <Combobox.Trigger
@@ -1,5 +1,5 @@
1
1
  import { Combobox } from 'bits-ui';
2
- import { type PartProps } from '../../utils/index.js';
3
- declare const ComboboxTrigger: import("svelte").Component<PartProps<Combobox.TriggerProps>, {}, "ref">;
2
+ import { type OwnedPartProps } from '../../utils/index.js';
3
+ declare const ComboboxTrigger: import("svelte").Component<OwnedPartProps<Combobox.TriggerProps>, {}, "ref">;
4
4
  type ComboboxTrigger = ReturnType<typeof ComboboxTrigger>;
5
5
  export default ComboboxTrigger;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
+ import { getAxisRelay } from '../provider/axis-relay.svelte.js';
2
3
  import { ContextMenu } from 'bits-ui';
3
- import { cn, type PartProps } from '../../utils/index.js';
4
+ import { cn, type OwnedPartProps } from '../../utils/index.js';
4
5
 
5
6
  let {
6
7
  ref = $bindable(null),
@@ -8,12 +9,15 @@
8
9
  class: className,
9
10
  sideOffset = 4,
10
11
  ...restProps
11
- }: PartProps<ContextMenu.ContentProps> = $props();
12
+ }: OwnedPartProps<ContextMenu.ContentProps> = $props();
13
+
14
+ const relay = getAxisRelay();
12
15
  </script>
13
16
 
14
17
  <ContextMenu.Portal>
15
18
  <ContextMenu.Content
16
19
  bind:ref
20
+ {...relay?.attrs}
17
21
  align="start"
18
22
  // p-menu, SURFACE-owned on purpose: 0 on flat — rows run the
19
23
  // panel's full width, flush, and the first row sits directly under the
@@ -1,4 +1,4 @@
1
- import { type PartProps } from '../../utils/index.js';
2
- declare const ContextMenuContent: import("svelte").Component<PartProps<import("bits-ui").DropdownMenuContentProps>, {}, "ref">;
1
+ import { type OwnedPartProps } from '../../utils/index.js';
2
+ declare const ContextMenuContent: import("svelte").Component<OwnedPartProps<import("bits-ui").DropdownMenuContentProps>, {}, "ref">;
3
3
  type ContextMenuContent = ReturnType<typeof ContextMenuContent>;
4
4
  export default ContextMenuContent;
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { ContextMenu } from 'bits-ui';
4
+ import { setAxisRelay } from '../provider/axis-relay.svelte.js';
5
+
6
+ /*
7
+ * era owns Root ONLY to open the axis channel — it renders no DOM, adds no
8
+ * behaviour and forwards everything. There has to BE a component here: a
9
+ * re-export (`export const Root = ContextMenu.Root`) has nowhere to put a context,
10
+ * which is exactly why the axes could not cross the portal before.
11
+ *
12
+ * `open` is re-declared bindable because a spread cannot carry a two-way
13
+ * binding, so wrapping a primitive breaks `bind:open` at every call site
14
+ * unless the wrapper states it.
15
+ */
16
+ let {
17
+ open = $bindable(false),
18
+ children,
19
+ ...restProps
20
+ }: ContextMenu.RootProps & { children?: Snippet } = $props();
21
+
22
+ const relay = setAxisRelay();
23
+
24
+ // Capture on OPEN, not on mount: bits-ui mounts content eagerly and toggles
25
+ // it, so a mount-time read runs once at page load and never again.
26
+ $effect(() => {
27
+ if (open) relay.refresh();
28
+ });
29
+ </script>
30
+
31
+ <ContextMenu.Root bind:open {...restProps}>
32
+ {@render children?.()}
33
+ </ContextMenu.Root>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { ContextMenu } from 'bits-ui';
3
+ type $$ComponentProps = ContextMenu.RootProps & {
4
+ children?: Snippet;
5
+ };
6
+ declare const ContextMenuRoot: import("svelte").Component<$$ComponentProps, {}, "open">;
7
+ type ContextMenuRoot = ReturnType<typeof ContextMenuRoot>;
8
+ export default ContextMenuRoot;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
+ import { getAxisRelay } from '../provider/axis-relay.svelte.js';
2
3
  import { ContextMenu } from 'bits-ui';
3
- import { cn, type PartProps } from '../../utils/index.js';
4
+ import { cn, type OwnedPartProps } from '../../utils/index.js';
4
5
 
5
6
  let {
6
7
  ref = $bindable(null),
@@ -8,12 +9,15 @@
8
9
  class: className,
9
10
  sideOffset = 4,
10
11
  ...restProps
11
- }: PartProps<ContextMenu.SubContentProps> = $props();
12
+ }: OwnedPartProps<ContextMenu.SubContentProps> = $props();
13
+
14
+ const relay = getAxisRelay();
12
15
  </script>
13
16
 
14
17
  <ContextMenu.Portal>
15
18
  <ContextMenu.SubContent
16
19
  bind:ref
20
+ {...relay?.attrs}
17
21
  class={cn(
18
22
  'z-overlay w-max overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
19
23
  className
@@ -1,5 +1,5 @@
1
1
  import { ContextMenu } from 'bits-ui';
2
- import { type PartProps } from '../../utils/index.js';
3
- declare const ContextMenuSubContent: import("svelte").Component<PartProps<ContextMenu.SubContentProps>, {}, "ref">;
2
+ import { type OwnedPartProps } from '../../utils/index.js';
3
+ declare const ContextMenuSubContent: import("svelte").Component<OwnedPartProps<ContextMenu.SubContentProps>, {}, "ref">;
4
4
  type ContextMenuSubContent = ReturnType<typeof ContextMenuSubContent>;
5
5
  export default ContextMenuSubContent;
@@ -1,14 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { ContextMenu } from 'bits-ui';
3
3
  import ChevronRight from '@lucide/svelte/icons/chevron-right';
4
- import { cn, type PartProps } from '../../utils/index.js';
4
+ import { cn, type OwnedPartProps } from '../../utils/index.js';
5
5
 
6
6
  let {
7
7
  ref = $bindable(null),
8
8
  children,
9
9
  class: className,
10
10
  ...restProps
11
- }: PartProps<ContextMenu.SubTriggerProps> = $props();
11
+ }: OwnedPartProps<ContextMenu.SubTriggerProps> = $props();
12
12
  </script>
13
13
 
14
14
  <ContextMenu.SubTrigger
@@ -1,5 +1,5 @@
1
1
  import { ContextMenu } from 'bits-ui';
2
- import { type PartProps } from '../../utils/index.js';
3
- declare const ContextMenuSubTrigger: import("svelte").Component<PartProps<ContextMenu.SubTriggerProps>, {}, "ref">;
2
+ import { type OwnedPartProps } from '../../utils/index.js';
3
+ declare const ContextMenuSubTrigger: import("svelte").Component<OwnedPartProps<ContextMenu.SubTriggerProps>, {}, "ref">;
4
4
  type ContextMenuSubTrigger = ReturnType<typeof ContextMenuSubTrigger>;
5
5
  export default ContextMenuSubTrigger;