activeadmin-react 0.1.0.alpha1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 92cba0113794ff8883e09e5bf1cfad4b498c705bf97fdabcf2c461c2d320e04c
4
+ data.tar.gz: 54dbdaed377bcd530612b4077ad9364b167b7a268438b09d8d038ff15d798052
5
+ SHA512:
6
+ metadata.gz: 3aead5075fda511f7d0ce83ae644860eb2831432c93c99aeab3a68b5b8c07d513049237d1826db73b2bd79971d45845db449e4a113f09d874e8d709893ce95ee
7
+ data.tar.gz: dff36285667c02f65f3a52fe0f24ef0f52a5428e8be2640dd142f837b4cb42b4eeb799a3f7a18e119ea4a49c0c3c9ccda73bd16054b1ad6f5d18f157a1b2c87c
data/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ <!-- CHANGELOG.md -->
2
+
3
+ # Changelog
4
+
5
+ All notable changes to ActiveAdmin React are recorded here. During ordinary pre-1.0 development, PATCH releases contain fixes and small compatible improvements; MINOR releases may contain new capabilities, meaningful API evolution, and documented breaking changes.
6
+
7
+ ## 0.1.0.alpha1 — 2026-09-05
8
+
9
+ First integrated prerelease for Rodeo dogfooding. Public Ruby and JavaScript contracts
10
+ remain unstable during the 0.x line. This alpha includes the capabilities below and
11
+ real Chromium coverage for engine rendering, Turbo cleanup/remount, server fallbacks,
12
+ and live Action Cable reconnect/replay. Supported runtime dependencies are Ruby >= 3.2,
13
+ Rails >= 8.0 and < 9, and ActiveAdmin >= 4.0.0.beta22 and < 5; React 18/19 is supplied
14
+ by the host.
15
+
16
+ ### Added
17
+
18
+ - Arbre-native `react_component` islands with deterministic markup, JSON-safe props, caller-owned HTML attributes, and server-rendered fallback content.
19
+ - A build-tool-neutral React 18/19 runtime with explicit component registration, multiple islands per page, Turbo lifecycle cleanup, and duplicate-registration protection.
20
+ - A validated Action Cable operation protocol with reconnect replay, duplicate and out-of-order suppression, terminal states, authenticated cancellation commands, and accessible status attributes.
21
+ - Explicit Rails engine contribution contracts with ownership, namespaces, surfaces, deterministic diagnostics, collision errors, and RBS signatures.
22
+ - An ActiveAdmin 4 dummy host, focused local validation commands, independent CI quality gates, packaged JavaScript and RBS files, and clean-install package verification.
23
+
24
+ ### Security
25
+
26
+ - Props reject unsupported objects and non-finite floats instead of serializing arbitrary values.
27
+ - Mount points reserve their runtime data attributes and render no inline JavaScript.
28
+ - Action Cable examples keep user and tenant authorization on the server, and cancellation uses same-origin requests with Rails CSRF tokens.
29
+
30
+
31
+ Stan Carver II
32
+ Made in Texas 🤠
33
+ https://stancarver.com
data/LICENSE.txt ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stan Carver II
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,203 @@
1
+ <!-- README.md -->
2
+
3
+ # ActiveAdmin React
4
+
5
+ React islands for ActiveAdmin, with an Arbre-native Ruby API and optional asynchronous integrations.
6
+
7
+ ActiveAdmin React keeps administrative pages Rails-first and server-rendered while making React available for interactions that benefit from a client-side component. The project is on the ordinary pre-1.0 `0.x` line, so documented breaking changes may ship in a MINOR release while Rodeo dogfooding helps stabilize the public contracts.
8
+
9
+ ## Installation and compatibility
10
+
11
+ Add the gem to a Rails application that uses ActiveAdmin:
12
+
13
+ ```ruby
14
+ gem "activeadmin-react", "0.1.0.alpha1", require: "active_admin/react"
15
+ ```
16
+
17
+ Then run `bundle install`. The gem requires Ruby 3.2 or newer, Rails 8.x, and ActiveAdmin `4.0.0.beta22` or newer within the 4.x line. The JavaScript runtime uses the React 18/19 `createRoot` API; the host supplies `react` and `react-dom` and remains responsible for compiling and serving browser assets.
18
+
19
+ ## Render an island from Arbre
20
+
21
+ The `react_component` helper is available inside ActiveAdmin's Arbre DSL:
22
+
23
+ ```ruby
24
+ ActiveAdmin.register Order do
25
+ show do
26
+ panel "Order activity" do
27
+ react_component(
28
+ "OrdersTable",
29
+ props: { order_id: resource.id },
30
+ fallback: -> { "Order activity is available without JavaScript." },
31
+ class: "orders-table"
32
+ )
33
+ end
34
+ end
35
+ end
36
+ ```
37
+
38
+ Props may contain `nil`, booleans, strings, integers, finite floats, symbols, dates, times, arrays, and hashes with string or symbol keys. Symbols become strings and date/time values become ISO 8601 strings. Unsupported values, non-finite floats, invalid component names, and malformed `data` attributes raise `ArgumentError` before markup is rendered.
39
+
40
+ The mount owns the `data-react-component` and `data-react-props` attributes. Other HTML and data attributes remain caller-owned. A callable fallback receives polite `status` semantics by default and stays in the page until React mounts successfully.
41
+
42
+ ## Register and start components
43
+
44
+ The packaged JavaScript entrypoint is `app/javascript/active_admin/react/index.js`. Configure the host's Vite, esbuild, or equivalent resolver so `active_admin/react` points to that file inside the installed gem. For example, Vite can derive the gem root with `bundle show activeadmin-react`:
45
+
46
+ ```js
47
+ import { execFileSync } from "node:child_process"
48
+ import { resolve } from "node:path"
49
+ import { defineConfig } from "vite"
50
+
51
+ const gemRoot = execFileSync("bundle", ["show", "activeadmin-react"], {
52
+ encoding: "utf8"
53
+ }).trim()
54
+
55
+ export default defineConfig({
56
+ resolve: {
57
+ alias: {
58
+ "active_admin/react": resolve(gemRoot, "app/javascript/active_admin/react/index.js")
59
+ }
60
+ }
61
+ })
62
+ ```
63
+
64
+ Register every component before starting the runtime:
65
+
66
+ ```js
67
+ import { registerComponent, start } from "active_admin/react"
68
+ import OrdersTable from "./components/OrdersTable"
69
+
70
+ registerComponent("OrdersTable", OrdersTable)
71
+ start()
72
+ ```
73
+
74
+ `start()` mounts every `[data-react-component]` island, mounts newly rendered pages after `turbo:load`, and unmounts roots before Turbo caches the page. Repeated calls are safe. `stop()` removes the Turbo listeners and unmounts tracked roots. Duplicate component names, unknown components, and malformed JSON props fail loudly.
75
+
76
+ The shipped modules use package-style relative imports intended for a JavaScript build tool. Copying the directory directly into an importmap or serving it to browsers without a resolver is not currently a supported integration path.
77
+
78
+ ## Engine contributions
79
+
80
+ Requiring `active_admin/react` does not scan Rails engines, eager-load models, or require third-party adapters. An engine exposes a small adapter and the host installs it explicitly:
81
+
82
+ ```ruby
83
+ module CommerceEngine
84
+ module ActiveAdminReact
85
+ module_function
86
+
87
+ def install!
88
+ ActiveAdmin::React::Contributions.register(
89
+ "OrdersTable",
90
+ namespace: "commerce.admin",
91
+ owner: "CommerceEngine",
92
+ source: "commerce_engine/active_admin_react",
93
+ surfaces: %i[component page],
94
+ description: "Interactive order review"
95
+ )
96
+ end
97
+ end
98
+ end
99
+ ```
100
+
101
+ `name`, `namespace`, `owner`, and `source` are required. `surfaces` defaults to `[:component]`; additional keywords become diagnostic metadata. Component names are globally unique. Conflicts identify both owners, namespaces, and sources.
102
+
103
+ `ActiveAdmin::React::Contributions.diagnostics` returns entries sorted by namespace, component name, and owner. An installer can query `ActiveAdmin::React::Contributions.registry.registered?("OrdersTable")` before registering inside a reload hook. `ActiveAdmin::React::Contributions.reset!` creates a fresh registry for test isolation. Metadata hashes, arrays, sets, and strings are recursively copied and frozen during registration, so later changes to caller-owned values cannot alter registered state and diagnostics cannot mutate it. Other metadata values must be immutable objects supplied by the contributor.
104
+
105
+ ## Asynchronous Action Cable operations
106
+
107
+ Action Cable transports operation state; application jobs and services own the expensive work. Each event uses a server-owned operation identifier, idempotency key, and monotonic sequence:
108
+
109
+ ```json
110
+ {
111
+ "operation_id": "report-123",
112
+ "idempotency_key": "report-123:7",
113
+ "sequence": 7,
114
+ "state": "running",
115
+ "progress": 60,
116
+ "message": "Rendering pages",
117
+ "result": null,
118
+ "result_metadata": null,
119
+ "error": null,
120
+ "occurred_at": "2026-09-03T18:42:00Z"
121
+ }
122
+ ```
123
+
124
+ States are `pending`, `queued`, `running`, `retrying`, `completed`, `failed`, and `cancelled`; the last three are terminal. `OperationState` ignores duplicates, out-of-order events, events for another operation, and updates after a terminal event. `subscribeToOperation` validates envelopes and resumes after the last applied sequence:
125
+
126
+ ```js
127
+ import { OperationState, subscribeToOperation } from "active_admin/react"
128
+
129
+ const operationState = new OperationState({ operationId })
130
+ const subscription = subscribeToOperation({
131
+ consumer,
132
+ channel: "OperationsChannel",
133
+ params: { operation_id: operationId },
134
+ operationState,
135
+ onEvent: (_event, current) => setOperation(current),
136
+ onProtocolError: (error) => reportProtocolError(error)
137
+ })
138
+
139
+ return () => subscription.unsubscribe()
140
+ ```
141
+
142
+ Use `operationAccessibility(operationState)` on the visible status container. Active work returns a polite, busy `status`; successful and cancelled work returns a polite, non-busy status; failures return an assertive `alert`.
143
+
144
+ Cancellation is an authenticated application command rather than a Cable event:
145
+
146
+ ```js
147
+ import { requestOperationCancellation } from "active_admin/react"
148
+
149
+ await requestOperationCancellation({
150
+ url: `/admin/operations/${operationId}/cancel`,
151
+ operationId,
152
+ csrfToken: document.querySelector('meta[name="csrf-token"]')?.content
153
+ })
154
+ ```
155
+
156
+ The helper sends only `operation_id` with same-origin credentials. The endpoint must load and authorize the operation in the current user and tenant context before asking the owning job or service to cancel it. Channels must apply the same server-side authorization before streaming or replaying events.
157
+
158
+ ## CSP, CSRF, and fallbacks
159
+
160
+ The gem emits no inline scripts and does not create CSP nonces. Compile and serve React assets through the host's normal CSP-aware pipeline. Keep CSRF tokens in Rails-managed forms or meta tags rather than component props. Avoid placing credentials or sensitive tenant context in props because mount data is visible in HTML.
161
+
162
+ Meaningful fallback content keeps the page usable when JavaScript is disabled or an asset fails. Keep progress text visible, associate progress bars with their labels, and never communicate state through color alone.
163
+
164
+ ## Development and testing
165
+
166
+ Install pinned runtimes and dependencies, then run the complete local quality gate:
167
+
168
+ ```sh
169
+ bin/setup
170
+ bin/test
171
+ ```
172
+
173
+ Focused checks remain directly runnable:
174
+
175
+ ```sh
176
+ mise exec -- bundle exec rake spec
177
+ mise exec -- bundle exec rake rubocop
178
+ mise exec -- bundle exec rake rbs
179
+ mise exec -- npm run test:js
180
+ mise exec -- bundle exec rspec spec/integration
181
+ ```
182
+
183
+ `bin/test` includes the dummy ActiveAdmin 4 integration host through the full RSpec suite. `bin/package` builds the gem, verifies its contents, installs it and its dependencies into an isolated gem home, and proves Rails loads the installed copy. These validation commands never publish.
184
+
185
+ Ruby APIs live under `lib/`, browser modules under `app/javascript/`, RBS signatures under `sig/`, the dummy host under `spec/dummy/`, JavaScript tests under `test/javascript/`, and maintainer documentation under `docs/`.
186
+
187
+ ## Troubleshooting
188
+
189
+ - `Unknown React component` means the exact, case-sensitive mount name was not registered before `start()` ran.
190
+ - Prop errors mean the Arbre call received a value outside the documented grammar. Keep credentials, tenant identifiers, and CSRF tokens out of props.
191
+ - Duplicate registration errors identify competing engine owners and sources. Rename the component or remove the duplicate adapter.
192
+ - Repeated mounts usually mean both the host and `start()` own Turbo lifecycle listeners. Keep one lifecycle owner and call `stop()` before replacing it.
193
+ - Rejected Cable subscriptions belong at the server authorization boundary. Never trust client-provided user or tenant parameters.
194
+ - Browser resolution errors usually mean the host alias does not point to the packaged `app/javascript/active_admin/react/index.js` or the build tool is not resolving relative imports.
195
+
196
+ ## Documentation and license
197
+
198
+ See the [documentation index](docs/README.md), [release process](docs/releasing.md), [release policy](RELEASES.md), and [changelog](CHANGELOG.md). ActiveAdmin React is available under the [MIT License](LICENSE.txt). Source, issues, and pull requests live in the [GitHub repository](https://github.com/scarver2/activeadmin-react).
199
+
200
+
201
+ Stan Carver II
202
+ Made in Texas 🤠
203
+ https://stancarver.com
data/RELEASES.md ADDED
@@ -0,0 +1,53 @@
1
+ <!-- RELEASES.md -->
2
+
3
+ # Release Policy
4
+
5
+ ActiveAdmin React uses capability-gated Semantic Versioning. There are no calendar
6
+ commitments: quality gates, Rodeo dogfooding, community feedback, and ActiveAdmin 4
7
+ maturity determine when a release is ready.
8
+
9
+ ## Pre-1.0 versions
10
+
11
+ Development uses ordinary `0.MINOR.PATCH` versions, with explicit `0.MINOR.PATCH.alphaN`
12
+ and `0.MINOR.PATCH.betaN` prereleases when authorized. The first integrated prerelease
13
+ is `0.1.0.alpha1`. Prerelease numbers start at 1 and have no leading zeroes.
14
+
15
+ - Increment PATCH for fixes and small backward-compatible improvements.
16
+ - Increment MINOR for new capabilities, meaningful API evolution, and documented breaking
17
+ changes while the public API remains unstable under Semantic Versioning's `0.y.z` rules.
18
+ - Keep Rodeo-specific business behavior outside the gem. Rodeo dogfooding supplies the
19
+ primary evidence for whether generally useful contracts are ready to stabilize.
20
+ - Track ActiveAdmin 4 closely and consider generally useful ActiveAdmin or Arbre fixes for
21
+ upstream contribution instead of permanent private patches.
22
+
23
+ ## Path to 1.0
24
+
25
+ Move to `1.0.0.rc1` only when Rodeo dogfooding indicates that the Ruby API, JavaScript
26
+ adapter protocol, security guidance, packaging, and compatibility policy are ready to
27
+ stabilize. Publish additional candidates as `1.0.0.rcN` when needed, then publish `1.0.0`
28
+ after only release-blocking defects remain.
29
+
30
+ The stable release guarantees an Arbre-native mounting API, deterministic React lifecycle,
31
+ documented React and ActiveAdmin compatibility, Action Cable-friendly asynchronous
32
+ integration, engine contribution contracts, CSP/CSRF guidance, and semantic versioning for
33
+ public APIs.
34
+
35
+ ## Release mechanics
36
+
37
+ 1. Merge the entire reviewed stack into `master`.
38
+ 2. Ensure CI is green at the exact release commit.
39
+ 3. Update `ActiveAdmin::React::VERSION` and release notes.
40
+ 4. Tag that exact commit with the matching `v0.MINOR.PATCH`, optional `.alphaN` or
41
+ `.betaN` suffix, or future `v1.0.0.rcN` tag. Numeric components have no leading zeroes.
42
+ 5. Let GitHub Actions publish through RubyGems Trusted Publishing and the `release`
43
+ environment.
44
+ 6. Verify the gem is installable and its provenance is visible on RubyGems.org.
45
+ 7. Create or complete the corresponding GitHub Release.
46
+
47
+ Never publish from an unreviewed working tree or store a long-lived RubyGems API key when
48
+ Trusted Publishing is available.
49
+
50
+
51
+ Stan Carver II
52
+ Made in Texas 🤠
53
+ https://stancarver.com
@@ -0,0 +1,84 @@
1
+ // app/javascript/active_admin/react/cable.js
2
+
3
+ import { normalizeEvent, validateOperationEvent } from "./protocol"
4
+
5
+ export function subscribeToOperation({
6
+ consumer,
7
+ channel,
8
+ params = {},
9
+ operationState = null,
10
+ strict = true,
11
+ resume = true,
12
+ onEvent,
13
+ onIgnoredEvent,
14
+ onProtocolError,
15
+ onConnected,
16
+ onDisconnected,
17
+ onRejected
18
+ }) {
19
+ if (!consumer?.subscriptions?.create) throw new Error("consumer is required")
20
+ if (typeof channel !== "string" || channel.trim().length === 0) throw new Error("channel is required")
21
+
22
+ const identifier = { channel, ...params }
23
+ let subscription
24
+
25
+ subscription = consumer.subscriptions.create(identifier, {
26
+ connected() {
27
+ const resumeFrom = operationState?.lastSequence ?? null
28
+ if (resume && resumeFrom !== null && typeof subscription?.perform === "function") {
29
+ subscription.perform("resume", { after_sequence: resumeFrom })
30
+ }
31
+ onConnected?.({ resumeFrom })
32
+ },
33
+ disconnected(details) {
34
+ onDisconnected?.(details)
35
+ },
36
+ rejected() {
37
+ onRejected?.()
38
+ },
39
+ received(event) {
40
+ try {
41
+ const normalized = strict ? validateOperationEvent(event) : normalizeEvent(event)
42
+ const outcome = operationState?.applyEvent(normalized)
43
+ if (outcome && !outcome.applied) {
44
+ onIgnoredEvent?.(normalized, outcome.reason)
45
+ return
46
+ }
47
+ onEvent?.(normalized, outcome?.value ?? normalized)
48
+ } catch (error) {
49
+ if (onProtocolError) onProtocolError(error, event)
50
+ else throw error
51
+ }
52
+ }
53
+ })
54
+
55
+ return subscription
56
+ }
57
+
58
+ export async function requestOperationCancellation({ url, operationId, csrfToken, fetchImpl = globalThis.fetch }) {
59
+ if (typeof url !== "string" || url.length === 0) throw new Error("url is required")
60
+ if (typeof operationId !== "string" || operationId.length === 0) throw new Error("operationId is required")
61
+ if (typeof fetchImpl !== "function") throw new Error("fetch is required")
62
+
63
+ const headers = { Accept: "application/json", "Content-Type": "application/json" }
64
+ if (csrfToken) headers["X-CSRF-Token"] = csrfToken
65
+
66
+ const response = await fetchImpl(url, {
67
+ method: "POST",
68
+ credentials: "same-origin",
69
+ headers,
70
+ body: JSON.stringify({ operation_id: operationId })
71
+ })
72
+ const payload = await response.json()
73
+ if (!response.ok) throw new OperationCancellationError(response.status, payload)
74
+ return payload
75
+ }
76
+
77
+ export class OperationCancellationError extends Error {
78
+ constructor(status, response) {
79
+ super(`Operation cancellation failed with HTTP ${status}`)
80
+ this.name = "OperationCancellationError"
81
+ this.status = status
82
+ this.response = response
83
+ }
84
+ }
@@ -0,0 +1,13 @@
1
+ // app/javascript/active_admin/react/index.js
2
+
3
+ export { clearComponents, registerComponent, resolveComponent } from "./registry"
4
+ export { mountAll, mountElement, start, stop, unmountAll, unmountElement } from "./runtime"
5
+ export { OperationCancellationError, requestOperationCancellation, subscribeToOperation } from "./cable"
6
+ export { OperationState, operationAccessibility } from "./operation"
7
+ export {
8
+ normalizeEvent,
9
+ OPERATION_STATES,
10
+ OperationProtocolError,
11
+ TERMINAL_OPERATION_STATES,
12
+ validateOperationEvent
13
+ } from "./protocol"
@@ -0,0 +1,73 @@
1
+ // app/javascript/active_admin/react/operation.js
2
+
3
+ import { normalizeEvent, TERMINAL_OPERATION_STATES } from "./protocol"
4
+
5
+ export class OperationState {
6
+ constructor(initial = {}) {
7
+ this.value = {
8
+ operationId: initial.operationId ?? initial.operation_id ?? null,
9
+ id: initial.id ?? null,
10
+ idempotencyKey: initial.idempotencyKey ?? initial.idempotency_key ?? initial.id ?? null,
11
+ sequence: initial.sequence ?? null,
12
+ state: initial.state ?? "pending",
13
+ progress: initial.progress ?? null,
14
+ message: initial.message ?? null,
15
+ result: initial.result ?? null,
16
+ resultMetadata: initial.resultMetadata ?? initial.result_metadata ?? null,
17
+ error: initial.error ?? null,
18
+ occurredAt: initial.occurredAt ?? null
19
+ }
20
+ this.seen = new Set()
21
+ if (this.value.idempotencyKey) this.seen.add(this.value.idempotencyKey)
22
+ }
23
+
24
+ apply(event) {
25
+ return this.applyEvent(event).value
26
+ }
27
+
28
+ applyEvent(event) {
29
+ const normalized = normalizeEvent(event)
30
+ const key = normalized.idempotencyKey
31
+
32
+ if (key && this.seen.has(key)) return this.ignored("duplicate")
33
+ if (this.operationMismatch(normalized)) return this.ignored("operation_mismatch")
34
+ if (this.outOfOrder(normalized)) return this.ignored("out_of_order")
35
+ if (this.terminal()) return this.ignored("terminal")
36
+
37
+ if (key) this.seen.add(key)
38
+ this.value = { ...this.value, ...normalized }
39
+ return { applied: true, reason: null, value: this.value }
40
+ }
41
+
42
+ get lastSequence() {
43
+ return this.value.sequence
44
+ }
45
+
46
+ ignored(reason) {
47
+ return { applied: false, reason, value: this.value }
48
+ }
49
+
50
+ operationMismatch(event) {
51
+ return Boolean(this.value.operationId && event.operationId && this.value.operationId !== event.operationId)
52
+ }
53
+
54
+ outOfOrder(event) {
55
+ return event.sequence !== null && this.lastSequence !== null && event.sequence <= this.lastSequence
56
+ }
57
+
58
+ terminal() {
59
+ return TERMINAL_OPERATION_STATES.includes(this.value.state)
60
+ }
61
+ }
62
+
63
+ export function operationAccessibility(operation) {
64
+ const value = operation instanceof OperationState ? operation.value : operation
65
+ const state = value?.state ?? "pending"
66
+
67
+ if (state === "failed") return { role: "alert", "aria-live": "assertive", "aria-busy": false }
68
+ return {
69
+ role: "status",
70
+ "aria-live": "polite",
71
+ "aria-busy": !TERMINAL_OPERATION_STATES.includes(state)
72
+ }
73
+ }
@@ -0,0 +1,102 @@
1
+ // app/javascript/active_admin/react/protocol.js
2
+
3
+ export const OPERATION_STATES = Object.freeze([
4
+ "pending",
5
+ "queued",
6
+ "running",
7
+ "retrying",
8
+ "completed",
9
+ "failed",
10
+ "cancelled"
11
+ ])
12
+
13
+ export const TERMINAL_OPERATION_STATES = Object.freeze(["completed", "failed", "cancelled"])
14
+
15
+ export class OperationProtocolError extends Error {
16
+ constructor(issues) {
17
+ super(`Invalid operation event: ${issues.join(", ")}`)
18
+ this.name = "OperationProtocolError"
19
+ this.issues = issues
20
+ }
21
+ }
22
+
23
+ export function normalizeEvent(event = {}) {
24
+ const source = event && typeof event === "object" && !Array.isArray(event) ? event : {}
25
+ const idempotencyKey = source.idempotency_key ?? source.idempotencyKey ?? source.id ?? null
26
+
27
+ return {
28
+ operationId: source.operation_id ?? source.operationId ?? null,
29
+ id: source.event_id ?? source.eventId ?? source.id ?? idempotencyKey,
30
+ idempotencyKey,
31
+ sequence: normalizeSequence(source.sequence),
32
+ state: normalizeState(source.state),
33
+ progress: clampProgress(source.progress),
34
+ message: source.message ?? null,
35
+ result: source.result ?? null,
36
+ resultMetadata: source.result_metadata ?? source.resultMetadata ?? null,
37
+ error: normalizeError(source.error),
38
+ occurredAt: source.occurred_at ?? source.occurredAt ?? null
39
+ }
40
+ }
41
+
42
+ export function validateOperationEvent(event) {
43
+ const issues = []
44
+
45
+ if (!event || typeof event !== "object" || Array.isArray(event)) {
46
+ throw new OperationProtocolError(["event must be an object"])
47
+ }
48
+
49
+ const normalized = normalizeEvent(event)
50
+ if (!nonEmptyString(normalized.operationId)) issues.push("operation_id is required")
51
+ if (!nonEmptyString(normalized.idempotencyKey)) issues.push("idempotency_key is required")
52
+ if (!validSequence(event.sequence)) issues.push("sequence must be a non-negative integer")
53
+ if (!OPERATION_STATES.includes(normalized.state)) issues.push(`state must be one of ${OPERATION_STATES.join("/")}`)
54
+ if (!validProgress(event.progress)) issues.push("progress must be between 0 and 100")
55
+ if (normalized.state === "failed" && normalized.error === null) issues.push("failed events require an error")
56
+
57
+ if (issues.length > 0) throw new OperationProtocolError(issues)
58
+ return normalized
59
+ }
60
+
61
+ function clampProgress(value) {
62
+ if (value === null || value === undefined) return null
63
+ const numeric = Number(value)
64
+ if (!Number.isFinite(numeric)) return null
65
+ return Math.min(100, Math.max(0, numeric))
66
+ }
67
+
68
+ function nonEmptyString(value) {
69
+ return typeof value === "string" && value.trim().length > 0
70
+ }
71
+
72
+ function normalizeError(error) {
73
+ if (error === null || error === undefined) return null
74
+ if (typeof error === "string") return { code: null, message: error, retryable: false, details: null }
75
+ if (typeof error !== "object" || Array.isArray(error)) return null
76
+
77
+ return {
78
+ code: error.code ?? null,
79
+ message: error.message ?? null,
80
+ retryable: error.retryable === true,
81
+ details: error.details ?? null
82
+ }
83
+ }
84
+
85
+ function normalizeSequence(value) {
86
+ const numeric = Number(value)
87
+ return Number.isInteger(numeric) && numeric >= 0 ? numeric : null
88
+ }
89
+
90
+ function normalizeState(value) {
91
+ return typeof value === "string" ? value.toLowerCase() : "unknown"
92
+ }
93
+
94
+ function validProgress(value) {
95
+ if (value === null || value === undefined) return true
96
+ const numeric = Number(value)
97
+ return Number.isFinite(numeric) && numeric >= 0 && numeric <= 100
98
+ }
99
+
100
+ function validSequence(value) {
101
+ return Number.isInteger(value) && value >= 0
102
+ }
@@ -0,0 +1,18 @@
1
+ const components = new Map()
2
+
3
+ export function registerComponent(name, component) {
4
+ if (!name || !component) throw new Error("name and component are required")
5
+
6
+ const key = String(name)
7
+ if (components.has(key)) throw new Error(`component already registered: ${key}`)
8
+
9
+ components.set(key, component)
10
+ }
11
+
12
+ export function resolveComponent(name) {
13
+ return components.get(String(name))
14
+ }
15
+
16
+ export function clearComponents() {
17
+ components.clear()
18
+ }
@@ -0,0 +1,73 @@
1
+ import React from "react"
2
+ import { createRoot } from "react-dom/client"
3
+ import { resolveComponent } from "./registry"
4
+
5
+ const roots = new Map()
6
+ const selector = "[data-react-component]"
7
+ let started = false
8
+
9
+ function elementsWithin(root) {
10
+ const elements = [...root.querySelectorAll(selector)]
11
+ if (typeof root.matches === "function" && root.matches(selector)) elements.unshift(root)
12
+ return elements
13
+ }
14
+
15
+ function mountOnTurboLoad() {
16
+ mountAll()
17
+ }
18
+
19
+ function unmountBeforeTurboCache() {
20
+ unmountAll()
21
+ }
22
+
23
+ function propsFor(element) {
24
+ const raw = element.dataset.reactProps || "{}"
25
+ return JSON.parse(raw)
26
+ }
27
+
28
+ export function mountElement(element) {
29
+ if (roots.has(element)) return roots.get(element)
30
+
31
+ const Component = resolveComponent(element.dataset.reactComponent)
32
+ if (!Component) throw new Error(`Unknown React component: ${element.dataset.reactComponent}`)
33
+
34
+ const root = createRoot(element)
35
+ root.render(React.createElement(Component, propsFor(element)))
36
+ roots.set(element, root)
37
+ return root
38
+ }
39
+
40
+ export function mountAll(root = document) {
41
+ elementsWithin(root).forEach(mountElement)
42
+ }
43
+
44
+ export function unmountElement(element) {
45
+ const root = roots.get(element)
46
+ if (!root) return
47
+ root.unmount()
48
+ roots.delete(element)
49
+ }
50
+
51
+ export function unmountAll(root = document) {
52
+ for (const element of roots.keys()) {
53
+ if (root === document || element === root || root.contains(element)) unmountElement(element)
54
+ }
55
+ }
56
+
57
+ export function start() {
58
+ if (started) return
59
+
60
+ mountAll()
61
+ document.addEventListener("turbo:load", mountOnTurboLoad)
62
+ document.addEventListener("turbo:before-cache", unmountBeforeTurboCache)
63
+ started = true
64
+ }
65
+
66
+ export function stop() {
67
+ if (!started) return
68
+
69
+ unmountAll()
70
+ document.removeEventListener("turbo:load", mountOnTurboLoad)
71
+ document.removeEventListener("turbo:before-cache", unmountBeforeTurboCache)
72
+ started = false
73
+ }
data/docs/README.md ADDED
@@ -0,0 +1,16 @@
1
+ <!-- docs/README.md -->
2
+
3
+ # Documentation
4
+
5
+ - [Project README](../README.md) — installation, compatibility, public APIs, supported asset integration, security, development, and troubleshooting.
6
+ - [Release process](releasing.md) — maintainer validation, RubyGems Trusted Publishing, failure handling, and post-release verification.
7
+ - [Release policy](../RELEASES.md) — ordinary pre-1.0 versioning and the path to `1.0.0.rcN` and `1.0.0`.
8
+ - [Changelog](../CHANGELOG.md) — release notes for shipped and upcoming versions.
9
+ - [MIT License](../LICENSE.txt) — terms for using and distributing ActiveAdmin React.
10
+
11
+ Source, issues, and pull requests are available in the [GitHub repository](https://github.com/scarver2/activeadmin-react).
12
+
13
+
14
+ Stan Carver II
15
+ Made in Texas 🤠
16
+ https://stancarver.com
data/docs/releasing.md ADDED
@@ -0,0 +1,59 @@
1
+ <!-- docs/releasing.md -->
2
+
3
+ # Releasing ActiveAdmin React
4
+
5
+ ActiveAdmin React uses reviewed tags and RubyGems Trusted Publishing. Local commands and continuous integration validate a release candidate; only the protected GitHub Actions release job publishes it. Never add a long-lived `RUBYGEMS_API_KEY`, `GEM_HOST_API_KEY`, or RubyGems credentials file to the repository or GitHub secrets.
6
+
7
+ ## One-time trusted publisher setup
8
+
9
+ Configure the `activeadmin-react` trusted publisher on RubyGems.org with these exact claims:
10
+
11
+ - GitHub owner: `scarver2`
12
+ - Repository: `activeadmin-react`
13
+ - Workflow filename: `release.yml`
14
+ - Environment: `release`
15
+
16
+ Create the matching GitHub `release` environment and restrict deployment to authorized maintainers and release refs. Require reviewer approval when more than one person can initiate a release. The publish job uses GitHub OIDC to exchange its identity for a short-lived credential scoped by these claims.
17
+
18
+ ## Validation does not publish
19
+
20
+ Prepare a release only after the complete pull-request stack has merged into `master`. Update `ActiveAdmin::React::VERSION` and `CHANGELOG.md` together, then verify a clean local checkout of the exact `origin/master` commit:
21
+
22
+ ```sh
23
+ bin/setup
24
+ bin/test
25
+ bin/package
26
+ ```
27
+
28
+ `bin/test` runs the Ruby specs, RuboCop, RBS validation, JavaScript tests, and dummy-host integration coverage. `bin/package` builds the gem, inspects the packaged file list, installs it with dependencies in an isolated gem home, and proves Rails loads that installed copy. Neither command tags a commit, contacts RubyGems publishing APIs, or publishes a gem.
29
+
30
+ Before tagging, confirm every required GitHub Actions quality and package job is green for the exact `master` commit. Inspect the generated gem contents and release notes; do not treat a successful local build as approval to publish.
31
+
32
+ ## Publishing
33
+
34
+ Create a tag that exactly matches the gem version with a leading `v`: `v0.MINOR.PATCH`, or an authorized prerelease `v0.MINOR.PATCH.alphaN` or `v0.MINOR.PATCH.betaN`. The first integrated prerelease is `v0.1.0.alpha1`. When Rodeo dogfooding indicates readiness to converge on 1.0, use `v1.0.0.rcN`. All numeric components have no leading zeroes, and prerelease `N` starts at 1. Other suffixes, `0.x` release candidates, `1.0.0` alpha/beta versions, and build metadata are rejected by the guard.
35
+
36
+ Push the tag only from the reviewed `master` commit. The tag-triggered `.github/workflows/release.yml` job checks out that commit and publishes through the protected `release` environment and RubyGems Trusted Publishing. Approving that environment deployment authorizes publication; local validation never does.
37
+
38
+ ## Release checklist
39
+
40
+ 1. Merge the complete reviewed stack into `master` from the bottom up.
41
+ 2. Update the version and changelog in a reviewed pull request.
42
+ 3. Confirm the exact resulting `master` commit passes every CI quality and package gate.
43
+ 4. Run `bin/test` and `bin/package` from a clean checkout of that commit.
44
+ 5. Create the matching allowed release tag at that exact commit and push it once.
45
+ 6. Review and approve the protected `release` environment deployment.
46
+ 7. Verify the version, dependencies, checksum, and provenance on RubyGems.org.
47
+ 8. Install the published gem in a clean host and require `active_admin/react`.
48
+ 9. Create or complete the matching GitHub Release from the reviewed changelog entry.
49
+
50
+ ## Failure and recovery
51
+
52
+ Stop when validation, tag identity, environment approval, or OIDC claim verification fails. Do not bypass a failed gate, move a published tag, overwrite a released version, or fall back to a long-lived RubyGems token. Correct the source through another reviewed pull request, increment the version, and publish a new tag.
53
+
54
+ RubyGems versions are immutable. Yank a version only when its artifact is unsafe or fundamentally unusable, then document the reason in both the changelog and GitHub Release. For ordinary defects, leave the prior release available and publish a corrected PATCH version.
55
+
56
+
57
+ Stan Carver II
58
+ Made in Texas 🤠
59
+ https://stancarver.com
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module React
5
+ # Adds the Arbre DSL entry point for rendering a React island.
6
+ module Arbre
7
+ def react_component(component, props: {}, fallback: nil, **html)
8
+ mount = ActiveAdmin::React::Mount.new(component, props: props, fallback: fallback, html: html)
9
+ attributes = mount.attributes
10
+ if fallback.respond_to?(:call)
11
+ attributes['aria-live'] = 'polite' unless attributes.key?('aria-live') || attributes.key?(:'aria-live')
12
+ attributes['role'] = 'status' unless attributes.key?('role') || attributes.key?(:role)
13
+ end
14
+
15
+ div(**attributes) do
16
+ text_node fallback.call if fallback.respond_to?(:call)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # lib/active_admin/react/contributions.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveAdmin
5
+ module React
6
+ # Coordinates engine-owned React component contributions.
7
+ module Contributions
8
+ module_function
9
+
10
+ def registry
11
+ @registry ||= Registry.new
12
+ end
13
+
14
+ def register(name, **attributes)
15
+ registry.register(name, **attributes)
16
+ end
17
+
18
+ def diagnostics
19
+ registry.diagnostics
20
+ end
21
+
22
+ def reset!
23
+ @registry = Registry.new
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'date'
5
+
6
+ module ActiveAdmin
7
+ module React
8
+ # Builds immutable HTML attributes for a React island and retains its fallback.
9
+ class Mount
10
+ DATA_COMPONENT = 'react-component'
11
+ DATA_PROPS = 'react-props'
12
+ COMPONENT_PATTERN = /\A[A-Za-z][A-Za-z0-9._-]*\z/
13
+
14
+ def initialize(component, props: {}, fallback: nil, html: {})
15
+ @component = normalize_component(component)
16
+ @props = normalize_props(props)
17
+ @fallback = fallback
18
+ @html = html.dup
19
+ end
20
+
21
+ def attributes
22
+ data = html_data.merge(
23
+ DATA_COMPONENT => @component,
24
+ DATA_PROPS => props_json
25
+ )
26
+
27
+ @html.merge(data: data)
28
+ end
29
+
30
+ attr_reader :fallback
31
+
32
+ private
33
+
34
+ def normalize_component(component)
35
+ return component.to_s if component.is_a?(String) && component.match?(COMPONENT_PATTERN)
36
+ return component.to_s if component.is_a?(Symbol) && component.to_s.match?(COMPONENT_PATTERN)
37
+
38
+ raise ArgumentError, 'component must be a non-empty Ruby identifier'
39
+ end
40
+
41
+ def html_data
42
+ data = @html.fetch(:data, {})
43
+ raise ArgumentError, 'html data must be a Hash' unless data.is_a?(Hash)
44
+
45
+ data.dup
46
+ end
47
+
48
+ def props_json
49
+ @props_json ||= JSON.generate(@props)
50
+ end
51
+
52
+ # Keep the accepted prop grammar explicit so unsupported objects fail closed.
53
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
54
+ def normalize_props(value)
55
+ case value
56
+ when NilClass, TrueClass, FalseClass, String, Integer, Symbol
57
+ value.is_a?(Symbol) ? value.to_s : value
58
+ when Float
59
+ raise ArgumentError, 'props cannot contain a non-finite Float' unless value.finite?
60
+
61
+ value
62
+ when DateTime, Time
63
+ value.iso8601(6)
64
+ when Date
65
+ value.iso8601
66
+ when Array
67
+ value.map { |item| normalize_props(item) }
68
+ when Hash
69
+ value.to_h do |key, item|
70
+ unless key.is_a?(String) || key.is_a?(Symbol)
71
+ raise ArgumentError, 'props Hash keys must be Strings or Symbols'
72
+ end
73
+
74
+ [key.to_s, normalize_props(item)]
75
+ end
76
+ else
77
+ raise ArgumentError, "unsupported prop type: #{value.class}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,134 @@
1
+ # lib/active_admin/react/registry.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveAdmin
5
+ module React
6
+ # Stores namespaced component contributions with copied, immutable metadata.
7
+ class Registry
8
+ include Enumerable
9
+
10
+ Entry = Data.define(:name, :namespace, :source, :owner, :surfaces, :metadata) do
11
+ def provenance
12
+ "#{owner.inspect} in namespace #{namespace.inspect} from #{source.inspect}"
13
+ end
14
+ end
15
+
16
+ def initialize
17
+ @entries = {}
18
+ end
19
+
20
+ def register(name, **attributes)
21
+ entry = build_entry(name, attributes)
22
+ raise_conflict(entry) if @entries.key?(entry.name)
23
+
24
+ @entries[entry.name] = entry
25
+ end
26
+
27
+ def fetch(name)
28
+ @entries.fetch(name.to_s)
29
+ end
30
+
31
+ def each(&)
32
+ ordered_entries.each(&)
33
+ end
34
+
35
+ def to_a
36
+ ordered_entries
37
+ end
38
+
39
+ def registered?(name)
40
+ @entries.key?(name.to_s.strip)
41
+ end
42
+
43
+ def diagnostics
44
+ ordered_entries.map do |entry|
45
+ {
46
+ name: entry.name,
47
+ namespace: entry.namespace,
48
+ owner: entry.owner,
49
+ source: entry.source,
50
+ surfaces: entry.surfaces,
51
+ metadata: entry.metadata
52
+ }.freeze
53
+ end.freeze
54
+ end
55
+
56
+ private
57
+
58
+ def build_entry(name, attributes)
59
+ surfaces = attributes.delete(:surfaces) { [:component] }
60
+ Entry.new(
61
+ name: normalize_required(name, :name),
62
+ namespace: normalize_attribute(attributes, :namespace),
63
+ source: normalize_attribute(attributes, :source),
64
+ owner: normalize_attribute(attributes, :owner),
65
+ surfaces: normalize_surfaces(surfaces),
66
+ metadata: immutable_metadata(attributes)
67
+ ).freeze
68
+ end
69
+
70
+ def immutable_metadata(value, ancestors = Set.new)
71
+ return value.dup.freeze if value.is_a?(String)
72
+ return value unless metadata_container?(value)
73
+
74
+ object_id = value.object_id
75
+ raise ActiveAdmin::React::Error, 'metadata must not contain cyclic containers' if ancestors.include?(object_id)
76
+
77
+ nested_ancestors = ancestors.dup.add(object_id)
78
+ return immutable_hash(value, nested_ancestors) if value.is_a?(Hash)
79
+ return immutable_array(value, nested_ancestors) if value.is_a?(Array)
80
+
81
+ immutable_set(value, nested_ancestors)
82
+ end
83
+
84
+ def immutable_hash(value, ancestors)
85
+ value.each_with_object({}) do |(key, nested_value), copy|
86
+ copy[immutable_metadata(key, ancestors)] = immutable_metadata(nested_value, ancestors)
87
+ end.freeze
88
+ end
89
+
90
+ def immutable_array(value, ancestors)
91
+ value.map { |nested_value| immutable_metadata(nested_value, ancestors) }.freeze
92
+ end
93
+
94
+ def immutable_set(value, ancestors)
95
+ value.each_with_object(Set.new) do |nested_value, copy|
96
+ copy << immutable_metadata(nested_value, ancestors)
97
+ end.freeze
98
+ end
99
+
100
+ def metadata_container?(value)
101
+ value.is_a?(Hash) || value.is_a?(Array) || value.is_a?(Set)
102
+ end
103
+
104
+ def normalize_attribute(attributes, field)
105
+ normalize_required(attributes.delete(field), field)
106
+ end
107
+
108
+ def normalize_required(value, field)
109
+ normalized = value.to_s.strip
110
+ raise ActiveAdmin::React::Error, "#{field} must be present" if normalized.empty?
111
+
112
+ normalized
113
+ end
114
+
115
+ def normalize_surfaces(surfaces)
116
+ normalized = Array(surfaces).map { |surface| normalize_required(surface, :surface).to_sym }.uniq.sort.freeze
117
+ raise ActiveAdmin::React::Error, 'surfaces must not be empty' if normalized.empty?
118
+
119
+ normalized
120
+ end
121
+
122
+ def ordered_entries
123
+ @entries.values.sort_by { |entry| [entry.namespace, entry.name, entry.owner] }.freeze
124
+ end
125
+
126
+ def raise_conflict(incoming)
127
+ existing = @entries.fetch(incoming.name)
128
+ message = "component #{incoming.name.inspect} is owned by #{existing.provenance}; " \
129
+ "attempted owner #{incoming.provenance}"
130
+ raise ActiveAdmin::React::Error, message
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,8 @@
1
+ # lib/active_admin/react/version.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveAdmin
5
+ module React
6
+ VERSION = '0.1.0.alpha1'
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_admin'
4
+ require_relative 'react/arbre'
5
+ require_relative 'react/contributions'
6
+ require_relative 'react/mount'
7
+ require_relative 'react/registry'
8
+ require_relative 'react/version'
9
+
10
+ module ActiveAdmin
11
+ module React
12
+ class Error < StandardError; end
13
+ end
14
+ end
15
+
16
+ Arbre::Element.include(ActiveAdmin::React::Arbre)
@@ -0,0 +1,32 @@
1
+ # sig/active_admin/react.rbs
2
+
3
+ module ActiveAdmin
4
+ module React
5
+ class Registry
6
+ class Entry
7
+ attr_reader name: String
8
+ attr_reader namespace: String
9
+ attr_reader source: String
10
+ attr_reader owner: String
11
+ attr_reader surfaces: Array[Symbol]
12
+ attr_reader metadata: Hash[Symbol, untyped]
13
+ end
14
+
15
+ def initialize: () -> void
16
+ def register: (String | Symbol name, namespace: String | Symbol, source: String | Symbol, owner: untyped, ?surfaces: Array[String | Symbol], **untyped metadata) -> Entry
17
+ def fetch: (String | Symbol name) -> Entry
18
+ def each: () { (Entry) -> void } -> Array[Entry]
19
+ | () -> Enumerator[Entry, Array[Entry]]
20
+ def to_a: () -> Array[Entry]
21
+ def registered?: (String | Symbol name) -> bool
22
+ def diagnostics: () -> Array[Hash[Symbol, untyped]]
23
+ end
24
+
25
+ module Contributions
26
+ def self.registry: () -> Registry
27
+ def self.register: (String | Symbol name, namespace: String | Symbol, source: String | Symbol, owner: untyped, ?surfaces: Array[String | Symbol], **untyped metadata) -> Registry::Entry
28
+ def self.diagnostics: () -> Array[Hash[Symbol, untyped]]
29
+ def self.reset!: () -> Registry
30
+ end
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activeadmin-react
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha1
5
+ platform: ruby
6
+ authors:
7
+ - Stan Carver II
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activeadmin
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 4.0.0.beta22
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '5'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 4.0.0.beta22
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '5'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rails
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '8.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '8.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
52
+ description: 'An Arbre-native bridge for mounting optional React components inside
53
+ ActiveAdmin while keeping ActiveAdmin Rails-first and server-rendered.
54
+
55
+ '
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - CHANGELOG.md
61
+ - LICENSE.txt
62
+ - README.md
63
+ - RELEASES.md
64
+ - app/javascript/active_admin/react/cable.js
65
+ - app/javascript/active_admin/react/index.js
66
+ - app/javascript/active_admin/react/operation.js
67
+ - app/javascript/active_admin/react/protocol.js
68
+ - app/javascript/active_admin/react/registry.js
69
+ - app/javascript/active_admin/react/runtime.js
70
+ - docs/README.md
71
+ - docs/releasing.md
72
+ - lib/active_admin/react.rb
73
+ - lib/active_admin/react/arbre.rb
74
+ - lib/active_admin/react/contributions.rb
75
+ - lib/active_admin/react/mount.rb
76
+ - lib/active_admin/react/registry.rb
77
+ - lib/active_admin/react/version.rb
78
+ - sig/active_admin/react.rbs
79
+ homepage: https://github.com/scarver2/activeadmin-react
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ bug_tracker_uri: https://github.com/scarver2/activeadmin-react/issues
84
+ changelog_uri: https://github.com/scarver2/activeadmin-react/blob/v0.1.0.alpha1/CHANGELOG.md
85
+ documentation_uri: https://github.com/scarver2/activeadmin-react/blob/v0.1.0.alpha1/README.md
86
+ homepage_uri: https://github.com/scarver2/activeadmin-react
87
+ rubygems_mfa_required: 'true'
88
+ source_code_uri: https://github.com/scarver2/activeadmin-react/tree/v0.1.0.alpha1
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3.2'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 4.0.16
104
+ specification_version: 4
105
+ summary: React islands for ActiveAdmin
106
+ test_files: []