@brendanbank/atrium-host-bundle-utils 0.15.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/LICENCE.md ADDED
@@ -0,0 +1,25 @@
1
+ # BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Brendan Bank
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # `@brendanbank/atrium-host-bundle-utils`
2
+
3
+ Runtime helpers, Vite preset, and React hooks for [atrium](https://github.com/brendanbank/atrium)
4
+ host bundles. Wraps the dual-tree mount pattern so a host's
5
+ `main.tsx` collapses to ~10 lines of registration calls.
6
+
7
+ ```tsx
8
+ import {
9
+ AtriumRegistry,
10
+ makeWrapperElement,
11
+ } from '@brendanbank/atrium-host-bundle-utils';
12
+ import { MyWidget } from './MyWidget';
13
+
14
+ const reg = window.__ATRIUM_REGISTRY__ as AtriumRegistry;
15
+ reg.registerHomeWidget({
16
+ key: 'my-card',
17
+ render: () => makeWrapperElement(<MyWidget />),
18
+ });
19
+ ```
20
+
21
+ ## Installation
22
+
23
+ The package is published on **GitHub Packages**, not the public npm
24
+ registry. Add an `.npmrc` to your host project that points the
25
+ `@brendanbank` scope at GitHub Packages and supplies a token with
26
+ `read:packages` scope:
27
+
28
+ ```
29
+ # .npmrc
30
+ @brendanbank:registry=https://npm.pkg.github.com
31
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
32
+ ```
33
+
34
+ In CI, `GITHUB_TOKEN` is provided automatically by Actions; locally,
35
+ generate a [classic PAT](https://github.com/settings/tokens) with
36
+ `read:packages` and export it. Then:
37
+
38
+ ```bash
39
+ pnpm add @brendanbank/atrium-host-bundle-utils \
40
+ @brendanbank/atrium-host-types
41
+ ```
42
+
43
+ ## Three subpath entry-points
44
+
45
+ | Import | What you get |
46
+ | ---------------------------------------------------------- | ---------------------------------------------------------- |
47
+ | `@brendanbank/atrium-host-bundle-utils` | `makeWrapperElement`, `mountInside`, `unmountInside`, types |
48
+ | `@brendanbank/atrium-host-bundle-utils/vite` | `hostBundleConfig({ entry })` — returns a Vite config |
49
+ | `@brendanbank/atrium-host-bundle-utils/react` | `useMe`, `usePerm`, `useRole`, `useUserContext`, `<AtriumProvider>` |
50
+
51
+ ## Vite preset
52
+
53
+ ```ts
54
+ // vite.config.ts
55
+ import { hostBundleConfig } from '@brendanbank/atrium-host-bundle-utils/vite';
56
+
57
+ export default hostBundleConfig({ entry: 'src/main.tsx' });
58
+ ```
59
+
60
+ The factory returns a complete Vite library-mode config: emits a
61
+ single `dist/main.js` that atrium's loader dynamic-imports, inlines
62
+ imported `.css` via runtime `<style>` tags, and defines
63
+ `process.env.NODE_ENV` so the externalised React + TanStack Query
64
+ references resolve. Pass `extraConfig` to layer additional plugins
65
+ or overrides on top.
66
+
67
+ `vite` and `vite-plugin-css-injected-by-js` are declared as optional
68
+ peer deps; install them as devDependencies in the host's
69
+ `frontend/package.json` only when building the bundle.
70
+
71
+ ## React hooks
72
+
73
+ ```tsx
74
+ import {
75
+ AtriumProvider,
76
+ useMe,
77
+ usePerm,
78
+ useRole,
79
+ } from '@brendanbank/atrium-host-bundle-utils/react';
80
+
81
+ function CommissionsPage() {
82
+ const { data: me, isLoading } = useMe();
83
+ const hasPerm = usePerm();
84
+ if (isLoading) return <Loader />;
85
+ if (hasPerm('commission.view.all')) return <Admin />;
86
+ return <SelfCommissions userId={me?.id} />;
87
+ }
88
+
89
+ function App() {
90
+ return (
91
+ <QueryClientProvider client={hostQueryClient}>
92
+ <AtriumProvider apiBase="/api">
93
+ <CommissionsPage />
94
+ </AtriumProvider>
95
+ </QueryClientProvider>
96
+ );
97
+ }
98
+ ```
99
+
100
+ `<AtriumProvider>` reads from your existing `<QueryClientProvider>`
101
+ by default — no second QueryClient. Pass `client={hostQueryClient}`
102
+ to wrap one inline; pass `fetchUserContext={...}` to inject a custom
103
+ fetcher (useful for tests or hosts that want axios-shaped retry).
104
+
105
+ ## Versioning
106
+
107
+ The package version tracks atrium's image version. Pin `^0.15` for
108
+ "compatible with atrium 0.15.x"; bump together with the atrium image
109
+ to pick up new registry slots and SDK helpers.
110
+
111
+ ## See also
112
+
113
+ - [`@brendanbank/atrium-host-types`](../host-types/) — the typed
114
+ registry declarations this package re-exports.
115
+ - [`docs/published-images.md`](../../docs/published-images.md) — the
116
+ full host-extension contract.
117
+ - [`examples/hello-world/`](../../examples/hello-world/) — the
118
+ canonical worked example exercising every extension slot through
119
+ these helpers.
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /** Translate a shared atrium key against the running atrium image's
2
+ * i18next instance.
3
+ *
4
+ * ```ts
5
+ * import { __atrium_t__ } from '@brendanbank/atrium-host-bundle-utils';
6
+ *
7
+ * <Button>{__atrium_t__('common.save')}</Button>
8
+ * <Text>{__atrium_t__('common.welcome', { name: me.full_name })}</Text>
9
+ * ```
10
+ *
11
+ * - Missing atrium image (pre-0.14): returns the literal key.
12
+ * - Missing key: returns the literal key (not empty string), so a
13
+ * typo is visible in the rendered output.
14
+ * - Missing translation in the active locale: falls back to English
15
+ * via i18next's ``fallbackLng``. */
16
+ export declare function __atrium_t__(key: string, vars?: Record<string, string | number>): string;
17
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAkDA;;;;;;;;;;;;;;uCAcuC;AACvC,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GACrC,MAAM,CAWR"}
package/dist/i18n.js ADDED
@@ -0,0 +1,32 @@
1
+ // Copyright (c) 2026 Brendan Bank
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /** Translate a shared atrium key against the running atrium image's
4
+ * i18next instance.
5
+ *
6
+ * ```ts
7
+ * import { __atrium_t__ } from '@brendanbank/atrium-host-bundle-utils';
8
+ *
9
+ * <Button>{__atrium_t__('common.save')}</Button>
10
+ * <Text>{__atrium_t__('common.welcome', { name: me.full_name })}</Text>
11
+ * ```
12
+ *
13
+ * - Missing atrium image (pre-0.14): returns the literal key.
14
+ * - Missing key: returns the literal key (not empty string), so a
15
+ * typo is visible in the rendered output.
16
+ * - Missing translation in the active locale: falls back to English
17
+ * via i18next's ``fallbackLng``. */
18
+ export function __atrium_t__(key, vars) {
19
+ if (typeof window === 'undefined')
20
+ return key;
21
+ const i18n = window
22
+ .__atrium_i18n__;
23
+ if (!i18n || typeof i18n.t !== 'function')
24
+ return key;
25
+ // ``defaultValue: key`` is the explicit "missing key → key" rule.
26
+ // i18next's default behaviour already returns the key when the
27
+ // lookup misses, but spelling it out makes the intent unambiguous
28
+ // and survives any future i18next config change in atrium.
29
+ const out = i18n.t(key, { ...vars, defaultValue: key });
30
+ return typeof out === 'string' ? out : key;
31
+ }
32
+ //# sourceMappingURL=i18n.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,wCAAwC;AAiDxC;;;;;;;;;;;;;;uCAcuC;AACvC,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,IAAsC;IAEtC,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,GAAG,CAAC;IAC9C,MAAM,IAAI,GAAI,MAAuD;SAClE,eAAe,CAAC;IACnB,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,CAAC,KAAK,UAAU;QAAE,OAAO,GAAG,CAAC;IACtD,kEAAkE;IAClE,+DAA+D;IAC/D,kEAAkE;IAClE,2DAA2D;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;IACxD,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Atrium host-bundle runtime helpers.
3
+ *
4
+ * Atrium loads each host's frontend as a single ES module via
5
+ * `import(system.host_bundle_url)` after the SPA boots. The host
6
+ * bundle calls `window.__ATRIUM_REGISTRY__.register*(...)` at import
7
+ * time. The elements registered are atrium-React elements (created
8
+ * via `window.React.createElement`) whose ref callback mounts the
9
+ * host's *own* React tree inside via `react-dom/client`'s
10
+ * `createRoot`. Two React trees coexist in the DOM:
11
+ *
12
+ * - atrium's React owns the page shell + the wrapper `<div>`
13
+ * - the host's React owns the subtree (Mantine, hook state, query
14
+ * cache)
15
+ *
16
+ * `makeWrapperElement` and `mountInside` package up the dual-tree
17
+ * pattern so the host's `main.tsx` becomes ten lines of registration
18
+ * calls — see `examples/hello-world/frontend/src/main.tsx` for the
19
+ * canonical use.
20
+ *
21
+ * Type re-exports below let a host that only needs the runtime
22
+ * helpers add a single dep instead of two.
23
+ */
24
+ import type { ReactElement } from 'react';
25
+ export { __atrium_t__ } from './i18n';
26
+ export type { AdminUserRow, AdminTab, AtriumEvent, AtriumEventHandler, AtriumNotification, AtriumRegistry, HomeWidget, HomeWidgetWidth, LocaleOverlay, NavItem, NotificationKindRenderer, ProfileItem, ProfileSlot, RouteEntry, UserContext, } from '@brendanbank/atrium-host-types';
27
+ /** Mount `child` inside `el` using a fresh React root, idempotent on
28
+ * the same `el`/`child` pair.
29
+ *
30
+ * Behaviour:
31
+ *
32
+ * - First call: `createRoot(el).render(child)` and remember the root.
33
+ * - Same `el` + same `child` reference: no-op (handles `<StrictMode>`
34
+ * double-invoke and ref-callback re-fires on remount).
35
+ * - Same `el` + different `child` reference: re-renders into the
36
+ * existing root — cheaper than unmount/remount and preserves
37
+ * component state where the new child's tree matches.
38
+ *
39
+ * Hosts use this directly only when they want to manage their own
40
+ * ref semantics. The common case is `makeWrapperElement(child)`
41
+ * below, which closes over a single child and tracks the mounted
42
+ * node via the ref callback's `null` signal so unmount is exact. */
43
+ export declare function mountInside(el: HTMLElement, child: ReactElement): void;
44
+ /** Unmount the React root previously attached to `el` by `mountInside`.
45
+ * No-op when there is no tracked root. Called automatically by the
46
+ * ref callback returned from `makeWrapperElement`; hosts only call
47
+ * this directly when managing their own ref semantics. */
48
+ export declare function unmountInside(el: HTMLElement): void;
49
+ /** Build an atrium-React element whose ref callback mounts `child` via
50
+ * the host's own React. Returns a `ReactElement` so it slots
51
+ * directly into the registry option-bag types (`render: () =>
52
+ * makeWrapperElement(<MyWidget />)`).
53
+ *
54
+ * Each call captures the supplied `child` in a closure and tracks the
55
+ * mounted DOM node so the React unmount fires when atrium drops the
56
+ * wrapper (route swap, tab change). The closure-style tracking is the
57
+ * correct fix for issue #31 — using a module-level WeakMap alone
58
+ * meant a child swap on a reused DOM node could leave the previous
59
+ * child's tree mounted; pinning state to one closure-per-call
60
+ * guarantees one wrapper element owns one mount.
61
+ *
62
+ * Note: the returned object is created via *atrium's* React, not the
63
+ * host bundle's. The two trees only meet at this `<div>`; everything
64
+ * inside the ref callback runs under the host's React. The
65
+ * `ReactElement` cast is safe because atrium's React exposes the
66
+ * same element shape — the type system can't tell them apart, and
67
+ * the registry consumer treats either as opaque.
68
+ *
69
+ * Tabler icons (and other hooks-free SVG components) can be passed
70
+ * directly to `window.React.createElement(...)` — they don't need
71
+ * the wrapper. The wrapper is only required for trees that use
72
+ * hooks, context, or anything that depends on the host's React copy. */
73
+ export declare function makeWrapperElement(child: ReactElement): ReactElement;
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAG1C,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,YAAY,EACV,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,UAAU,EACV,eAAe,EACf,aAAa,EACb,OAAO,EACP,wBAAwB,EACxB,WAAW,EACX,WAAW,EACX,UAAU,EACV,WAAW,GACZ,MAAM,gCAAgC,CAAC;AA2CxC;;;;;;;;;;;;;;;qEAeqE;AACrE,wBAAgB,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAWtE;AAED;;;2DAG2D;AAC3D,wBAAgB,aAAa,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,CAKnD;AAED;;;;;;;;;;;;;;;;;;;;;;;yEAuByE;AACzE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAsBpE"}
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ // Copyright (c) 2026 Brendan Bank
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ import { createRoot } from 'react-dom/client';
4
+ export { __atrium_t__ } from './i18n';
5
+ /** Resolve the React instance atrium exposes on `window.React`. Throws
6
+ * with a clear message when missing — the host bundle's import-time
7
+ * side-effects are running before the SPA mounted, which means the
8
+ * bundle is being loaded outside atrium's own loader. */
9
+ function getAtriumReact() {
10
+ if (typeof window === 'undefined') {
11
+ throw new Error('[atrium-host-bundle-utils] makeWrapperElement requires a browser ' +
12
+ 'environment; called from a non-window context');
13
+ }
14
+ const r = window.React;
15
+ if (!r || typeof r.createElement !== 'function') {
16
+ throw new Error('[atrium-host-bundle-utils] window.React is missing — atrium SPA ' +
17
+ 'must mount and expose its React on window.React before the host ' +
18
+ 'bundle is imported. Check that the bundle is being loaded via ' +
19
+ "atrium's host_bundle_url loader and not by the host's own SPA.");
20
+ }
21
+ return r;
22
+ }
23
+ // WeakMap so a DOM element going away (host route swap, profile card
24
+ // re-render) doesn't pin the React root in memory. The entry is GC'd
25
+ // alongside the element.
26
+ const mounted = new WeakMap();
27
+ /** Mount `child` inside `el` using a fresh React root, idempotent on
28
+ * the same `el`/`child` pair.
29
+ *
30
+ * Behaviour:
31
+ *
32
+ * - First call: `createRoot(el).render(child)` and remember the root.
33
+ * - Same `el` + same `child` reference: no-op (handles `<StrictMode>`
34
+ * double-invoke and ref-callback re-fires on remount).
35
+ * - Same `el` + different `child` reference: re-renders into the
36
+ * existing root — cheaper than unmount/remount and preserves
37
+ * component state where the new child's tree matches.
38
+ *
39
+ * Hosts use this directly only when they want to manage their own
40
+ * ref semantics. The common case is `makeWrapperElement(child)`
41
+ * below, which closes over a single child and tracks the mounted
42
+ * node via the ref callback's `null` signal so unmount is exact. */
43
+ export function mountInside(el, child) {
44
+ const existing = mounted.get(el);
45
+ if (existing) {
46
+ if (existing.child === child)
47
+ return;
48
+ existing.root.render(child);
49
+ existing.child = child;
50
+ return;
51
+ }
52
+ const root = createRoot(el);
53
+ root.render(child);
54
+ mounted.set(el, { root, child });
55
+ }
56
+ /** Unmount the React root previously attached to `el` by `mountInside`.
57
+ * No-op when there is no tracked root. Called automatically by the
58
+ * ref callback returned from `makeWrapperElement`; hosts only call
59
+ * this directly when managing their own ref semantics. */
60
+ export function unmountInside(el) {
61
+ const existing = mounted.get(el);
62
+ if (!existing)
63
+ return;
64
+ existing.root.unmount();
65
+ mounted.delete(el);
66
+ }
67
+ /** Build an atrium-React element whose ref callback mounts `child` via
68
+ * the host's own React. Returns a `ReactElement` so it slots
69
+ * directly into the registry option-bag types (`render: () =>
70
+ * makeWrapperElement(<MyWidget />)`).
71
+ *
72
+ * Each call captures the supplied `child` in a closure and tracks the
73
+ * mounted DOM node so the React unmount fires when atrium drops the
74
+ * wrapper (route swap, tab change). The closure-style tracking is the
75
+ * correct fix for issue #31 — using a module-level WeakMap alone
76
+ * meant a child swap on a reused DOM node could leave the previous
77
+ * child's tree mounted; pinning state to one closure-per-call
78
+ * guarantees one wrapper element owns one mount.
79
+ *
80
+ * Note: the returned object is created via *atrium's* React, not the
81
+ * host bundle's. The two trees only meet at this `<div>`; everything
82
+ * inside the ref callback runs under the host's React. The
83
+ * `ReactElement` cast is safe because atrium's React exposes the
84
+ * same element shape — the type system can't tell them apart, and
85
+ * the registry consumer treats either as opaque.
86
+ *
87
+ * Tabler icons (and other hooks-free SVG components) can be passed
88
+ * directly to `window.React.createElement(...)` — they don't need
89
+ * the wrapper. The wrapper is only required for trees that use
90
+ * hooks, context, or anything that depends on the host's React copy. */
91
+ export function makeWrapperElement(child) {
92
+ const AtriumReact = getAtriumReact();
93
+ let mountedNode = null;
94
+ let mountedRoot = null;
95
+ return AtriumReact.createElement('div', {
96
+ ref: (el) => {
97
+ // React calls the ref with `null` on unmount and again with the
98
+ // node on mount. Same node twice in a row is the StrictMode /
99
+ // remount case — skip.
100
+ if (el === mountedNode)
101
+ return;
102
+ if (mountedRoot && mountedNode) {
103
+ mountedRoot.unmount();
104
+ mountedRoot = null;
105
+ }
106
+ mountedNode = el;
107
+ if (el) {
108
+ mountedRoot = createRoot(el);
109
+ mountedRoot.render(child);
110
+ }
111
+ },
112
+ });
113
+ }
114
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,wCAAwC;AA0BxC,OAAO,EAAE,UAAU,EAAa,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AA4BtC;;;0DAG0D;AAC1D,SAAS,cAAc;IACrB,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CACb,mEAAmE;YACjE,+CAA+C,CAClD,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAI,MAAmD,CAAC,KAAK,CAAC;IACrE,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CACb,kEAAkE;YAChE,kEAAkE;YAClE,gEAAgE;YAChE,gEAAgE,CACnE,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAOD,qEAAqE;AACrE,qEAAqE;AACrE,yBAAyB;AACzB,MAAM,OAAO,GAAG,IAAI,OAAO,EAA2B,CAAC;AAEvD;;;;;;;;;;;;;;;qEAeqE;AACrE,MAAM,UAAU,WAAW,CAAC,EAAe,EAAE,KAAmB;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACjC,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,QAAQ,CAAC,KAAK,KAAK,KAAK;YAAE,OAAO;QACrC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC;QACvB,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACnB,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;2DAG2D;AAC3D,MAAM,UAAU,aAAa,CAAC,EAAe;IAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACjC,IAAI,CAAC,QAAQ;QAAE,OAAO;IACtB,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;IACxB,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;yEAuByE;AACzE,MAAM,UAAU,kBAAkB,CAAC,KAAmB;IACpD,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IACrC,IAAI,WAAW,GAAuB,IAAI,CAAC;IAC3C,IAAI,WAAW,GAAgB,IAAI,CAAC;IAEpC,OAAO,WAAW,CAAC,aAAa,CAAC,KAAK,EAAE;QACtC,GAAG,EAAE,CAAC,EAAsB,EAAE,EAAE;YAC9B,gEAAgE;YAChE,8DAA8D;YAC9D,uBAAuB;YACvB,IAAI,EAAE,KAAK,WAAW;gBAAE,OAAO;YAC/B,IAAI,WAAW,IAAI,WAAW,EAAE,CAAC;gBAC/B,WAAW,CAAC,OAAO,EAAE,CAAC;gBACtB,WAAW,GAAG,IAAI,CAAC;YACrB,CAAC;YACD,WAAW,GAAG,EAAE,CAAC;YACjB,IAAI,EAAE,EAAE,CAAC;gBACP,WAAW,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC;gBAC7B,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;KACF,CAAiB,CAAC;AACrB,CAAC"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * React hooks + provider for atrium host bundles.
3
+ *
4
+ * One TanStack Query subscription serves the whole host tree:
5
+ * `useMe()` fetches `/users/me/context` once and shares the result
6
+ * with every `usePerm()`, `useRole()`, and `useUserContext()` caller.
7
+ * The hooks read the API base URL from `<AtriumProvider>` (default
8
+ * `'/api'`) so a host serving its bundle on a different prefix can
9
+ * point them at the right origin without forking the package.
10
+ *
11
+ * The hooks reuse the host's enclosing `<QueryClientProvider>` if one
12
+ * is already mounted; pass `client={hostQueryClient}` to
13
+ * `<AtriumProvider>` only if you want this provider to wrap the
14
+ * QueryClient too. Two QueryClients (atrium's + the host's) is the
15
+ * intended state — atrium clears its cache on logout via `qc.clear()`,
16
+ * and a shared client would lose host queries the user still wants.
17
+ */
18
+ import { type ReactNode } from 'react';
19
+ import { type QueryClient, type UseQueryResult } from '@tanstack/react-query';
20
+ import type { UserContext } from '@brendanbank/atrium-host-types';
21
+ export type { UserContext } from '@brendanbank/atrium-host-types';
22
+ export { __atrium_t__ } from '../i18n';
23
+ interface AtriumContextValue {
24
+ apiBase: string;
25
+ fetchUserContext: () => Promise<UserContext | null>;
26
+ }
27
+ export interface AtriumProviderProps {
28
+ /** API base URL the hooks prefix on every fetch. Default `'/api'` —
29
+ * matches atrium's convention of mounting the API behind `/api/*`
30
+ * on the same origin as the SPA. */
31
+ apiBase?: string;
32
+ /** Optional TanStack QueryClient. When supplied the provider wraps
33
+ * children in a `<QueryClientProvider>`; otherwise the hooks
34
+ * inherit the caller's enclosing one. Use this only when the host
35
+ * hasn't already set up its own provider. */
36
+ client?: QueryClient;
37
+ /** Override the fetcher for `/users/me/context`. Useful for tests
38
+ * and for hosts that want to layer an axios interceptor or extra
39
+ * headers on top. The default uses `fetch` with `credentials:
40
+ * 'include'`. */
41
+ fetchUserContext?: () => Promise<UserContext | null>;
42
+ children: ReactNode;
43
+ }
44
+ /** Provider that supplies `apiBase` and (optionally) a QueryClient
45
+ * to the atrium hooks below. Hosts that already wrap their tree in
46
+ * `<QueryClientProvider>` can omit `client` — the hooks use the
47
+ * caller's existing QueryClient. */
48
+ export declare function AtriumProvider({ apiBase, client, fetchUserContext, children, }: AtriumProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<AtriumContextValue | null>> | import("react").FunctionComponentElement<import("@tanstack/react-query").QueryClientProviderProps>;
49
+ /** Stable cache key for the `/users/me/context` query. Exported so
50
+ * hosts can `queryClient.invalidateQueries({ queryKey: ME_QUERY_KEY })`
51
+ * after a flow that changed the user's roles or permissions. */
52
+ export declare const ME_QUERY_KEY: readonly ["atrium", "me"];
53
+ /** Fetch the signed-in user's RBAC context. Returns the standard
54
+ * TanStack Query result so callers can surface loading and error
55
+ * states — `data` is `null` when the user is not signed in (atrium
56
+ * responded 401/403) and the typed `UserContext` otherwise.
57
+ *
58
+ * Cache is shared with `usePerm` / `useRole` / `useUserContext` —
59
+ * one network request per host tree. Stale time is 60s, matching
60
+ * atrium's own `useMe` so the host's cache invalidation rhythm
61
+ * doesn't fight atrium's. */
62
+ export declare function useMe(): UseQueryResult<UserContext | null>;
63
+ /** Alias for `useMe()` — present because issue #40 documents both
64
+ * names. The shape is identical; pick whichever reads better at the
65
+ * call site. */
66
+ export declare function useUserContext(): UseQueryResult<UserContext | null>;
67
+ /** Returns a stable predicate `(code) => boolean` over the signed-in
68
+ * user's permissions. While `useMe` is still loading or if the user
69
+ * is signed out, the predicate returns `false` for every code — so
70
+ * callers default to "not allowed" which is the safe fallback for
71
+ * RBAC gating.
72
+ *
73
+ * Returning a function (not a boolean) is deliberate: a single
74
+ * call site can check several codes against one query subscription,
75
+ * matching the pattern in issue #40:
76
+ *
77
+ * ```tsx
78
+ * const hasPerm = usePerm();
79
+ * if (hasPerm('commission.view.all')) return <Admin />;
80
+ * if (hasPerm('commission.view.own')) return <SelfCommissions />;
81
+ * ```
82
+ */
83
+ export declare function usePerm(): (code: string) => boolean;
84
+ /** Convenience: does the signed-in user hold the named role? Returns
85
+ * `false` while loading and when signed out, mirroring `usePerm`. */
86
+ export declare function useRole(code: string): boolean;
87
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAElE,YAAY,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,UAAU,kBAAkB;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CACrD;AAqBD,MAAM,WAAW,mBAAmB;IAClC;;yCAEqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;kDAG8C;IAC9C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;sBAGkB;IAClB,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACrD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;qCAGqC;AACrC,wBAAgB,cAAc,CAAC,EAC7B,OAA0B,EAC1B,MAAM,EACN,gBAAgB,EAChB,QAAQ,GACT,EAAE,mBAAmB,2MAYrB;AAED;;iEAEiE;AACjE,eAAO,MAAM,YAAY,2BAA4B,CAAC;AAsBtD;;;;;;;;8BAQ8B;AAC9B,wBAAgB,KAAK,IAAI,cAAc,CAAC,WAAW,GAAG,IAAI,CAAC,CAQ1D;AAED;;iBAEiB;AACjB,wBAAgB,cAAc,IAAI,cAAc,CAAC,WAAW,GAAG,IAAI,CAAC,CAEnE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,IAAI,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAMnD;AAED;sEACsE;AACtE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG7C"}
@@ -0,0 +1,125 @@
1
+ // Copyright (c) 2026 Brendan Bank
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /**
4
+ * React hooks + provider for atrium host bundles.
5
+ *
6
+ * One TanStack Query subscription serves the whole host tree:
7
+ * `useMe()` fetches `/users/me/context` once and shares the result
8
+ * with every `usePerm()`, `useRole()`, and `useUserContext()` caller.
9
+ * The hooks read the API base URL from `<AtriumProvider>` (default
10
+ * `'/api'`) so a host serving its bundle on a different prefix can
11
+ * point them at the right origin without forking the package.
12
+ *
13
+ * The hooks reuse the host's enclosing `<QueryClientProvider>` if one
14
+ * is already mounted; pass `client={hostQueryClient}` to
15
+ * `<AtriumProvider>` only if you want this provider to wrap the
16
+ * QueryClient too. Two QueryClients (atrium's + the host's) is the
17
+ * intended state — atrium clears its cache on logout via `qc.clear()`,
18
+ * and a shared client would lose host queries the user still wants.
19
+ */
20
+ import { createContext, createElement, useCallback, useContext, useMemo, } from 'react';
21
+ import { QueryClientProvider, useQuery, } from '@tanstack/react-query';
22
+ export { __atrium_t__ } from '../i18n';
23
+ const DEFAULT_API_BASE = '/api';
24
+ async function defaultFetchUserContext(apiBase) {
25
+ const res = await fetch(`${apiBase}/users/me/context`, {
26
+ credentials: 'include',
27
+ headers: { Accept: 'application/json' },
28
+ });
29
+ if (res.status === 401 || res.status === 403)
30
+ return null;
31
+ if (!res.ok) {
32
+ const body = await res.text().catch(() => '');
33
+ throw new Error(`/users/me/context: ${res.status} ${body}`.trim());
34
+ }
35
+ return (await res.json());
36
+ }
37
+ const AtriumContext = createContext(null);
38
+ /** Provider that supplies `apiBase` and (optionally) a QueryClient
39
+ * to the atrium hooks below. Hosts that already wrap their tree in
40
+ * `<QueryClientProvider>` can omit `client` — the hooks use the
41
+ * caller's existing QueryClient. */
42
+ export function AtriumProvider({ apiBase = DEFAULT_API_BASE, client, fetchUserContext, children, }) {
43
+ const value = useMemo(() => {
44
+ const fetcher = fetchUserContext ?? (() => defaultFetchUserContext(apiBase));
45
+ return { apiBase, fetchUserContext: fetcher };
46
+ }, [apiBase, fetchUserContext]);
47
+ const inner = createElement(AtriumContext.Provider, { value }, children);
48
+ if (client) {
49
+ return createElement(QueryClientProvider, { client }, inner);
50
+ }
51
+ return inner;
52
+ }
53
+ /** Stable cache key for the `/users/me/context` query. Exported so
54
+ * hosts can `queryClient.invalidateQueries({ queryKey: ME_QUERY_KEY })`
55
+ * after a flow that changed the user's roles or permissions. */
56
+ export const ME_QUERY_KEY = ['atrium', 'me'];
57
+ // Module-level fallback so hooks work without an explicit
58
+ // `<AtriumProvider>` — convenient for hosts that don't need to
59
+ // override the API base or the fetcher. Lazily-allocated so the
60
+ // import has no side effects.
61
+ let DEFAULT_CTX = null;
62
+ function defaultCtx() {
63
+ if (!DEFAULT_CTX) {
64
+ DEFAULT_CTX = {
65
+ apiBase: DEFAULT_API_BASE,
66
+ fetchUserContext: () => defaultFetchUserContext(DEFAULT_API_BASE),
67
+ };
68
+ }
69
+ return DEFAULT_CTX;
70
+ }
71
+ function useAtriumContextOrDefault() {
72
+ const ctx = useContext(AtriumContext);
73
+ return ctx ?? defaultCtx();
74
+ }
75
+ /** Fetch the signed-in user's RBAC context. Returns the standard
76
+ * TanStack Query result so callers can surface loading and error
77
+ * states — `data` is `null` when the user is not signed in (atrium
78
+ * responded 401/403) and the typed `UserContext` otherwise.
79
+ *
80
+ * Cache is shared with `usePerm` / `useRole` / `useUserContext` —
81
+ * one network request per host tree. Stale time is 60s, matching
82
+ * atrium's own `useMe` so the host's cache invalidation rhythm
83
+ * doesn't fight atrium's. */
84
+ export function useMe() {
85
+ const { fetchUserContext } = useAtriumContextOrDefault();
86
+ return useQuery({
87
+ queryKey: ME_QUERY_KEY,
88
+ queryFn: fetchUserContext,
89
+ staleTime: 60_000,
90
+ retry: false,
91
+ });
92
+ }
93
+ /** Alias for `useMe()` — present because issue #40 documents both
94
+ * names. The shape is identical; pick whichever reads better at the
95
+ * call site. */
96
+ export function useUserContext() {
97
+ return useMe();
98
+ }
99
+ /** Returns a stable predicate `(code) => boolean` over the signed-in
100
+ * user's permissions. While `useMe` is still loading or if the user
101
+ * is signed out, the predicate returns `false` for every code — so
102
+ * callers default to "not allowed" which is the safe fallback for
103
+ * RBAC gating.
104
+ *
105
+ * Returning a function (not a boolean) is deliberate: a single
106
+ * call site can check several codes against one query subscription,
107
+ * matching the pattern in issue #40:
108
+ *
109
+ * ```tsx
110
+ * const hasPerm = usePerm();
111
+ * if (hasPerm('commission.view.all')) return <Admin />;
112
+ * if (hasPerm('commission.view.own')) return <SelfCommissions />;
113
+ * ```
114
+ */
115
+ export function usePerm() {
116
+ const { data: me } = useMe();
117
+ return useCallback((code) => (me?.permissions ?? []).includes(code), [me?.permissions]);
118
+ }
119
+ /** Convenience: does the signed-in user hold the named role? Returns
120
+ * `false` while loading and when signed out, mirroring `usePerm`. */
121
+ export function useRole(code) {
122
+ const { data: me } = useMe();
123
+ return (me?.roles ?? []).includes(code);
124
+ }
125
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ export interface HostBundleConfigOptions {
2
+ /** Path to the host bundle's entry. Resolved against the directory
3
+ * containing the `vite.config.ts` that calls this factory. */
4
+ entry: string;
5
+ /** Output filename for the built bundle. Default `'main.js'` —
6
+ * matches atrium's documented `system.host_bundle_url` of
7
+ * `/host/main.js`. */
8
+ fileName?: string;
9
+ /** Output directory inside the host's frontend project. Default
10
+ * `'dist'` — matches Vite's default and the `COPY --from=frontend-builder
11
+ * /app/dist /opt/atrium/static/host` Dockerfile pattern. */
12
+ outDir?: string;
13
+ /** Include sourcemaps in the build. Default `true` — they are tiny
14
+ * next to the bundle and make production debugging tractable. */
15
+ sourcemap?: boolean;
16
+ /** Override the JS target. Default `'es2022'`. */
17
+ target?: string;
18
+ /** Extra Vite config to merge on top of the defaults. Plugins are
19
+ * appended; everything else uses Vite's standard merge semantics. */
20
+ extraConfig?: Record<string, unknown>;
21
+ }
22
+ /** Build a Vite config for an atrium host bundle.
23
+ *
24
+ * Usage in the host's `vite.config.ts`:
25
+ *
26
+ * ```ts
27
+ * import { hostBundleConfig } from '@brendanbank/atrium-host-bundle-utils/vite';
28
+ *
29
+ * export default hostBundleConfig({ entry: 'src/main.tsx' });
30
+ * ```
31
+ *
32
+ * The factory is async because it lazy-imports `vite` and the css
33
+ * injection plugin so they can stay optional peer deps.
34
+ */
35
+ export declare function hostBundleConfig(options: HostBundleConfigOptions): Promise<Record<string, unknown>>;
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAwCA,MAAM,WAAW,uBAAuB;IACtC;mEAC+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd;;2BAEuB;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;iEAE6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;sEACkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;0EACsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAiDlC"}
@@ -0,0 +1,85 @@
1
+ // Copyright (c) 2026 Brendan Bank
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /**
4
+ * Vite preset for atrium host bundles.
5
+ *
6
+ * Imported only by `vite.config.ts`, never by runtime code, so Vite
7
+ * stays out of the host's production bundle. Vite + the css-injection
8
+ * plugin are declared as optional peer deps; install them only when
9
+ * building (typical: as devDependencies in the host's
10
+ * `frontend/package.json`).
11
+ *
12
+ * The preset returns a complete Vite config that:
13
+ *
14
+ * - Builds in library mode (`build.lib`) emitting a single ES module
15
+ * at `dist/main.js`. Atrium's loader does
16
+ * `import(system.host_bundle_url)` with that URL — no IIFE / UMD
17
+ * needed.
18
+ * - Inlines every imported `.css` via a runtime `<style>` tag
19
+ * (`vite-plugin-css-injected-by-js`). Without this Vite extracts
20
+ * CSS to a sibling `main.css` that atrium's dynamic-import never
21
+ * fetches, and Mantine / FullCalendar / any rich-text editor ships
22
+ * silently unstyled.
23
+ * - Defines `process.env.NODE_ENV` so the externalised React +
24
+ * TanStack Query references resolve in lib-mode builds (Vite's
25
+ * app-mode auto-replaces this; lib-mode does not).
26
+ *
27
+ * Hosts can override anything by passing `extraConfig` (merged into
28
+ * the returned object) — typical use: a host needing a SVG plugin or
29
+ * a dev-only proxy. The defaults match the canonical
30
+ * self-contained-bundle pattern documented in
31
+ * `docs/published-images.md`.
32
+ */
33
+ import { resolve } from 'node:path';
34
+ /** Build a Vite config for an atrium host bundle.
35
+ *
36
+ * Usage in the host's `vite.config.ts`:
37
+ *
38
+ * ```ts
39
+ * import { hostBundleConfig } from '@brendanbank/atrium-host-bundle-utils/vite';
40
+ *
41
+ * export default hostBundleConfig({ entry: 'src/main.tsx' });
42
+ * ```
43
+ *
44
+ * The factory is async because it lazy-imports `vite` and the css
45
+ * injection plugin so they can stay optional peer deps.
46
+ */
47
+ export async function hostBundleConfig(options) {
48
+ const { entry, fileName = 'main.js', outDir = 'dist', sourcemap = true, target = 'es2022', extraConfig = {}, } = options;
49
+ const [vite, cssInjectedByJs] = await Promise.all([
50
+ import('vite'),
51
+ import('vite-plugin-css-injected-by-js'),
52
+ ]);
53
+ const mergeConfig = vite.mergeConfig;
54
+ const cssPluginFactory = (cssInjectedByJs.default ??
55
+ cssInjectedByJs);
56
+ // Build the config as a plain object — `defineConfig` is only a
57
+ // typing helper, and its strict `Plugin` typing fights the
58
+ // dynamically-imported CSS plugin. The merged config is what Vite
59
+ // actually consumes at build time.
60
+ const base = {
61
+ // Lib-mode builds don't auto-replace `process.env.NODE_ENV` the
62
+ // way Vite's app-mode builds do, and several deps (React internals,
63
+ // TanStack Query) reference it. Without these defines the bundle
64
+ // throws `ReferenceError: process is not defined` the moment the
65
+ // SPA dynamic-imports it.
66
+ define: {
67
+ 'process.env.NODE_ENV': JSON.stringify('production'),
68
+ 'process.env': '{}',
69
+ },
70
+ plugins: [cssPluginFactory()],
71
+ build: {
72
+ target,
73
+ lib: {
74
+ entry: resolve(process.cwd(), entry),
75
+ formats: ['es'],
76
+ fileName: () => fileName,
77
+ },
78
+ outDir,
79
+ emptyOutDir: true,
80
+ sourcemap,
81
+ },
82
+ };
83
+ return mergeConfig(base, extraConfig);
84
+ }
85
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,wCAAwC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6BpC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAgC;IAEhC,MAAM,EACJ,KAAK,EACL,QAAQ,GAAG,SAAS,EACpB,MAAM,GAAG,MAAM,EACf,SAAS,GAAG,IAAI,EAChB,MAAM,GAAG,QAAQ,EACjB,WAAW,GAAG,EAAE,GACjB,GAAG,OAAO,CAAC;IAEZ,MAAM,CAAC,IAAI,EAAE,eAAe,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC;QACd,MAAM,CAAC,gCAAgC,CAAC;KACzC,CAAC,CAAC;IAEH,MAAM,WAAW,GAAI,IAA6D,CAAC,WAAW,CAAC;IAC/F,MAAM,gBAAgB,GACpB,CAAE,eAA0D,CAAC,OAAO;QACjE,eAA4C,CAAkB,CAAC;IAEpE,gEAAgE;IAChE,2DAA2D;IAC3D,kEAAkE;IAClE,mCAAmC;IACnC,MAAM,IAAI,GAA4B;QACpC,gEAAgE;QAChE,oEAAoE;QACpE,iEAAiE;QACjE,iEAAiE;QACjE,0BAA0B;QAC1B,MAAM,EAAE;YACN,sBAAsB,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC;YACpD,aAAa,EAAE,IAAI;SACpB;QACD,OAAO,EAAE,CAAC,gBAAgB,EAAE,CAAC;QAC7B,KAAK,EAAE;YACL,MAAM;YACN,GAAG,EAAE;gBACH,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,CAAC;gBACpC,OAAO,EAAE,CAAC,IAAI,CAAC;gBACf,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ;aACzB;YACD,MAAM;YACN,WAAW,EAAE,IAAI;YACjB,SAAS;SACV;KACF,CAAC;IAEF,OAAO,WAAW,CAAC,IAAI,EAAE,WAAW,CAA4B,CAAC;AACnE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@brendanbank/atrium-host-bundle-utils",
3
+ "version": "0.15.0",
4
+ "description": "Runtime helpers, Vite preset, and React hooks for atrium host bundles. Wraps the dual-tree mount pattern, the host-bundle Vite library config, and the /users/me/context hook so a host's main.tsx is ~10 lines of registration calls.",
5
+ "license": "BSD-2-Clause",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./vite": {
19
+ "types": "./dist/vite/index.d.ts",
20
+ "default": "./dist/vite/index.js"
21
+ },
22
+ "./react": {
23
+ "types": "./dist/react/index.d.ts",
24
+ "default": "./dist/react/index.js"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "@brendanbank/atrium-host-types": "0.15.0"
29
+ },
30
+ "peerDependencies": {
31
+ "@tanstack/react-query": "^5.0.0",
32
+ "react": "^18.0.0 || ^19.0.0",
33
+ "react-dom": "^18.0.0 || ^19.0.0",
34
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
35
+ "vite-plugin-css-injected-by-js": "^3.0.0"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "vite": {
39
+ "optional": true
40
+ },
41
+ "vite-plugin-css-injected-by-js": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "devDependencies": {
46
+ "@tanstack/react-query": "^5.100.5",
47
+ "@testing-library/jest-dom": "^6.9.1",
48
+ "@testing-library/react": "^16.3.2",
49
+ "@types/node": "^25.6.0",
50
+ "@types/react": "^19.2.14",
51
+ "@types/react-dom": "^19.2.3",
52
+ "jsdom": "^29.0.2",
53
+ "msw": "^2.7.0",
54
+ "react": "^19.2.5",
55
+ "react-dom": "^19.2.5",
56
+ "typescript": "^6.0.3",
57
+ "vite": "^8.0.10",
58
+ "vite-plugin-css-injected-by-js": "^3.5.2",
59
+ "vitest": "^4.1.5"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/brendanbank/atrium.git",
67
+ "directory": "packages/host-bundle-utils"
68
+ },
69
+ "scripts": {
70
+ "build": "tsc -p tsconfig.json",
71
+ "typecheck": "tsc -p tsconfig.json --noEmit",
72
+ "test": "vitest run",
73
+ "test:watch": "vitest",
74
+ "clean": "rm -rf dist"
75
+ }
76
+ }