dash_kit 2.0.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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +170 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/builds/dash_kit/dashboard_grid.css +1 -0
  6. data/app/assets/builds/dash_kit/dashboard_grid.js +5 -0
  7. data/app/assets/javascripts/dash_kit/index.js +2 -0
  8. data/app/assets/stylesheets/dash_kit/application.css +15 -0
  9. data/app/controllers/dash_kit/application_controller.rb +6 -0
  10. data/app/controllers/dash_kit/dashboards_controller.rb +139 -0
  11. data/app/controllers/dash_kit/widget_definitions_controller.rb +33 -0
  12. data/app/controllers/dash_kit/widgets_controller.rb +39 -0
  13. data/app/helpers/dash_kit/application_helper.rb +4 -0
  14. data/app/helpers/dash_kit/dashboard_helper.rb +130 -0
  15. data/app/javascript/dash_kit/dashboard_grid.jsx +10 -0
  16. data/app/models/concerns/dash_kit/widget_management.rb +16 -0
  17. data/app/models/dash_kit/application_record.rb +5 -0
  18. data/app/models/dash_kit/dashboard.rb +53 -0
  19. data/app/models/dash_kit/widget_definition.rb +9 -0
  20. data/app/views/dash_kit/dashboards/_widgets.html.erb +1 -0
  21. data/app/views/dash_kit/dashboards/edit.html.erb +21 -0
  22. data/app/views/dash_kit/dashboards/index.html.erb +34 -0
  23. data/app/views/dash_kit/dashboards/new.html.erb +28 -0
  24. data/app/views/dash_kit/widget_definitions/_missing_renderer.html.erb +3 -0
  25. data/app/views/layouts/dash_kit/application.html.erb +17 -0
  26. data/config/importmap.rb +1 -0
  27. data/config/routes.rb +16 -0
  28. data/lib/dash_kit/configuration_backfill.rb +27 -0
  29. data/lib/dash_kit/engine.rb +19 -0
  30. data/lib/dash_kit/reference/guide.md +289 -0
  31. data/lib/dash_kit/reference.rb +11 -0
  32. data/lib/dash_kit/renderer_registry.rb +21 -0
  33. data/lib/dash_kit/the_local/agents/dash_kit-develop.md +298 -0
  34. data/lib/dash_kit/the_local/agents/dash_kit-info.md +298 -0
  35. data/lib/dash_kit/the_local/agents/dash_kit-install.md +298 -0
  36. data/lib/dash_kit/the_local.rb +34 -0
  37. data/lib/dash_kit/version.rb +3 -0
  38. data/lib/dash_kit/widget_registry.rb +69 -0
  39. data/lib/dash_kit.rb +72 -0
  40. data/lib/generators/dash_kit/install_generator.rb +79 -0
  41. data/lib/generators/dash_kit/templates/create_dash_kit_dashboards.rb.tt +20 -0
  42. data/lib/generators/dash_kit/templates/create_dash_kit_widget_definitions.rb.tt +12 -0
  43. data/lib/generators/dash_kit/templates/dash_kit.rb.tt +16 -0
  44. data/lib/tasks/dash_kit_tasks.rake +9 -0
  45. metadata +147 -0
@@ -0,0 +1,298 @@
1
+ ---
2
+ name: dash_kit-info
3
+ description: Use to learn what DashKit is, what it does and does not do, and its architecture (widget registry, Dashboard, helpers, routes).
4
+ tools: Read
5
+ ---
6
+
7
+ You explain DashKit, answering only from the reference: what it is, what it deliberately is not, its widget registry, the Dashboard model, the WidgetManagement concern, the renderer registry and runtime widget builder (WidgetDefinition records built from a source and visualization), the view helpers, the engine routes, and the Turbo lazy-load data flow that hands both registered and built widgets the dashboard's filter_state. You make no changes.
8
+
9
+ ## DashKit
10
+
11
+ DashKit is a Rails engine gem for composable, configurable dashboards. It is a
12
+ presentation and interaction layer: you register widgets, persist per-owner
13
+ configuration (visibility, order, filters), and render dashboards without
14
+ coupling the UI to any specific data backend.
15
+
16
+ ### What DashKit is
17
+
18
+ - A Rails engine (`DashKit::Engine`) with an isolated namespace, mounted in the
19
+ host app.
20
+ - A widget registry: dashboard types map to the widgets available on them.
21
+ - A persistence layer for user preferences (which widgets show, in what order,
22
+ with which filters) via an ActiveRecord `Dashboard` model.
23
+ - A rendering layer that lazy-loads each widget through a Turbo Frame and
24
+ refreshes on configuration change.
25
+
26
+ A dashboard renders two kinds of widget side by side:
27
+
28
+ - **Registered widgets** — declared up front in `DashKit.configure`. The
29
+ developer fixes the set of widgets a dashboard type can show.
30
+ - **Widget definitions (built widgets)** — created at runtime by a host-app
31
+ *user* through the settings modal and persisted as records. The host opens a
32
+ small builder UI (a `source` and a `visualization`) and DashKit renders each
33
+ built widget through a host-registered renderer partial.
34
+
35
+ ### What DashKit is not
36
+
37
+ - It does **not** define metrics or their meaning.
38
+ - It does **not** query databases or fetch data. Widgets declare what they need
39
+ through a partial; the host app supplies the data.
40
+ - It does **not** validate filter meaning or enforce business rules.
41
+ - It assumes no specific backend or analytics engine.
42
+
43
+ DashKit treats datasets as opaque structures it can render consistently, so a
44
+ host can replace or evolve its backend without rewriting dashboards.
45
+
46
+ ### Architecture
47
+
48
+ The layers, all under the `DashKit` namespace:
49
+
50
+ - **WidgetRegistry** (`lib/dash_kit/widget_registry.rb`) — a global registry
51
+ mapping a dashboard type to its available widgets. `DashKit.registry` returns
52
+ the singleton; `DashKit.configure { |r| ... }` yields it. Each
53
+ `register(:type)` block uses a `DashboardBuilder` whose `widget(key, label:,
54
+ partial:, **options)` declares one widget and assigns it a position by
55
+ declaration order.
56
+ - **Dashboard** (`app/models/dash_kit/dashboard.rb`) — the ActiveRecord model
57
+ (table `dash_kit_dashboards`) persisting per-owner configuration:
58
+ `widget_order`, `hidden_widgets`, `widget_settings`, and `filter_state`, plus
59
+ `name`, `dashboard_type`, `visibility` (`private` or `account`),
60
+ `role_default_for`, and `active`. It `belongs_to :owner, polymorphic: true`
61
+ and optionally an `account`. Owners may have many named dashboards;
62
+ `for_owner(owner)` and `for_account(account)` scope them, and `activate!` /
63
+ `duplicate!` manage the active one.
64
+ - **WidgetManagement** (`app/models/concerns/dash_kit/widget_management.rb`) — a
65
+ concern mixed into `Dashboard` providing `ordered_visible_widgets`,
66
+ `available_widgets`, `widget_visible?`, `toggle_widget`, `move_widget_up`,
67
+ `move_widget_down`, and `update_filter`.
68
+ - **WidgetDefinition** (`app/models/dash_kit/widget_definition.rb`) — the
69
+ ActiveRecord model (table `dash_kit_widget_definitions`) for a widget a user
70
+ built at runtime. It `belongs_to :dashboard` and carries `source` (string),
71
+ `visualization` (string), and `options` (jsonb, default `{}`). `Dashboard
72
+ has_many :widget_definitions, -> { order(:id) }, dependent: :destroy`.
73
+ - **RendererRegistry** (`lib/dash_kit/renderer_registry.rb`) — a global registry
74
+ mapping a visualization name to the host partial that draws it.
75
+ `DashKit.register_renderer(:visualization_name, partial: "path/to/partial")`
76
+ registers one; `DashKit.renderer_for(visualization)` looks it up;
77
+ `DashKit.visualizations` lists the registered names; `DashKit.reset_renderers!`
78
+ clears them. A built widget whose visualization has no registered renderer
79
+ falls back to the `dash_kit/widget_definitions/missing_renderer` partial.
80
+ - **Sources** — the host declares what a viewer may build from with
81
+ `DashKit.available_sources_for = ->(viewer) { [...] }`, a lambda returning the
82
+ list of sources offered in the builder. `DashKit.available_sources(viewer)`
83
+ calls it. It defaults to `NO_SOURCES` (`->(_viewer) { [] }`), so the builder UI
84
+ stays hidden until the host sets it.
85
+ - **DashboardHelper** (`app/helpers/dash_kit/dashboard_helper.rb`) — view
86
+ helpers: `dash_kit_render_widgets(config:)`, `dash_kit_widget_frame`,
87
+ `dash_kit_settings_modal(config:)`, `dash_kit_settings_button_attributes`,
88
+ `dash_kit_loading_skeleton`, and, for built widgets,
89
+ `dash_kit_available_sources`, `dash_kit_visualizations`, and
90
+ `dash_kit_widget_definition_frame(definition)`. `dash_kit_render_widgets`
91
+ renders the registered widgets in order, then appends one lazy Turbo Frame per
92
+ `widget_definition`.
93
+
94
+ ### Data flow
95
+
96
+ UI events -> dashboard update -> Turbo refresh -> widgets re-render via
97
+ lazy-loaded Turbo Frames. A widget is never rendered inline; it loads through a
98
+ `<turbo-frame loading="lazy">` pointing at the widget's own route. The frame
99
+ carries its dashboard's id, so the widget partial is handed the dashboard's
100
+ `filter_state` to render against (DashKit passes the blob; the host interprets
101
+ it).
102
+
103
+ Built widgets follow the same flow. Each `widget_definition` lazy-loads through
104
+ its own `<turbo-frame>` at `/widget_definitions/:id`;
105
+ `WidgetDefinitionsController#show` finds the definition scoped to the current
106
+ owner's dashboards, looks up `renderer_for(definition.visualization)`, and
107
+ renders that host partial with `source:` (the definition's source), `options:`
108
+ (its options hash), and `filter_state: definition.dashboard.filter_state`. So
109
+ **both** registered widgets and built widgets receive the dashboard's
110
+ `filter_state` as a local — DashKit passes the opaque blob and the host partial
111
+ reads it to shape its own data or stats request.
112
+
113
+ ### Routes (mounted at `/dash_kit`)
114
+
115
+ The engine routes live in `config/routes.rb`:
116
+
117
+ - `GET /widgets/:id` — render an individual registered widget
118
+ - `GET /widget_definitions/:id` — render an individual built widget
119
+ - `resources :dashboards` (`index`, `new`, `create`, `edit`, `update`,
120
+ `destroy`) plus member `select`, `duplicate`, `toggle_widget`, `move_widget`,
121
+ `reorder`, and `save_filters` — the dashboard CRUD plus the widget
122
+ visibility/order/filter actions.
123
+ - Dashboard member `create_definition`, `update_definition`, and
124
+ `destroy_definition` — create, update, and delete a built widget. They accept
125
+ `widget_definition[source, visualization, options]` (strong params
126
+ `permit(:source, :visualization, options: {})`) and are guarded by
127
+ `require_editable!`.
128
+
129
+ ### JavaScript
130
+
131
+ Stimulus controllers ship via importmap (no Node.js build step). The
132
+ `SortableListController` handles client-side widget toggles and drag reordering,
133
+ batching saves when the settings modal closes. It depends on `sortablejs`.
134
+
135
+ ### Widget builder (built widgets)
136
+
137
+ Beyond the registered widgets a developer declares, DashKit lets a host-app
138
+ *user* build their own widgets at runtime and persist them as
139
+ `WidgetDefinition` records. Two host-supplied pieces turn the builder on:
140
+
141
+ - **Sources** — set `DashKit.available_sources_for = ->(viewer) { [...] }` to
142
+ return the list of sources a viewer may build from. Until the host sets this
143
+ it defaults to `NO_SOURCES` and the builder UI stays hidden. The settings
144
+ modal only renders the "Build a widget" form when
145
+ `dash_kit_available_sources.any?`.
146
+ - **Renderers** — call `DashKit.register_renderer(:visualization, partial:
147
+ "path/to/partial")` for each visualization the builder offers, and write each
148
+ renderer partial. A built widget whose visualization has no registered
149
+ renderer falls back to `dash_kit/widget_definitions/missing_renderer`.
150
+
151
+ In the UI, the settings modal's "Build a widget" form is a `source` select
152
+ (from `available_sources`) and a `visualization` select (from `visualizations`);
153
+ it POSTs to `create_definition`. Once saved, `dash_kit_render_widgets` appends a
154
+ lazy Turbo Frame for the new definition, which loads `/widget_definitions/:id`
155
+ and renders the matching renderer partial with `source`, `options`, and
156
+ `filter_state` — the same `filter_state` contract registered widgets get. A
157
+ renderer partial reads `filter_state` to shape its own data request; DashKit
158
+ passes the opaque blob and never interprets it.
159
+
160
+ ### Installation in a host app
161
+
162
+ Add the gem to the host's `Gemfile` (until it is on RubyGems, use a git source):
163
+
164
+ ```ruby
165
+ gem "dash_kit", github: "DYB-Development/dash_kit"
166
+ ```
167
+
168
+ Then run the install generator and migrate:
169
+
170
+ ```bash
171
+ bundle install
172
+ rails generate dash_kit:install
173
+ rails db:migrate
174
+ ```
175
+
176
+ The generator creates the migrations for `dash_kit_dashboards` and
177
+ `dash_kit_widget_definitions`, the `config/initializers/dash_kit.rb`
178
+ initializer, and mounts the engine with `mount DashKit::Engine => "/dash_kit"`.
179
+ The following manual steps remain:
180
+
181
+ 1. **Add the association to the owner model** (the model that owns dashboards,
182
+ e.g. `Account`):
183
+
184
+ ```ruby
185
+ has_many :dash_kit_dashboards, class_name: "DashKit::Dashboard",
186
+ as: :owner, dependent: :destroy
187
+ ```
188
+
189
+ 2. **Pin sortablejs** in `config/importmap.rb`:
190
+
191
+ ```ruby
192
+ pin "sortablejs"
193
+ ```
194
+
195
+ 3. **Register the Stimulus controllers** in
196
+ `app/javascript/controllers/index.js`:
197
+
198
+ ```js
199
+ import { registerControllers as registerDashKitControllers } from "dash_kit/index"
200
+ registerDashKitControllers(application)
201
+ ```
202
+
203
+ 4. **Configure the initializer** `config/initializers/dash_kit.rb`:
204
+
205
+ ```ruby
206
+ DashKit.parent_controller = "ApplicationController"
207
+ DashKit.current_owner_method = :current_account
208
+
209
+ DashKit.configure do |config|
210
+ config.register(:home) do |d|
211
+ d.widget :on_deck, label: "On Deck", partial: "widgets/home/on_deck"
212
+ d.widget :tasks, label: "Tasks", partial: "widgets/home/tasks"
213
+ end
214
+ end
215
+ ```
216
+
217
+ - `parent_controller` is the controller DashKit's controllers inherit from,
218
+ giving them the host's authentication and helpers. It defaults to
219
+ `DashKit::ApplicationController`.
220
+ - `current_owner_method` is the method DashKit calls to scope dashboards
221
+ to the current owner (e.g. `:current_account`). It defaults to `nil` and
222
+ must be set.
223
+
224
+ 5. **Create a dashboard controller and view** that look up the dashboard and
225
+ render the widgets:
226
+
227
+ ```ruby
228
+ class DashboardController < ApplicationController
229
+ def show
230
+ @dashboard = DashKit::Dashboard.for_owner(current_account)
231
+ .find_or_create_by!(dashboard_type: "home", name: "Home") do |dashboard|
232
+ dashboard.widget_order = DashKit.registry.default_widget_order(:home)
233
+ end
234
+ end
235
+ end
236
+ ```
237
+
238
+ ```erb
239
+ <%= dash_kit_settings_button_attributes %>
240
+ <%= dash_kit_settings_modal(config: @dashboard) %>
241
+ <%= dash_kit_render_widgets(config: @dashboard) %>
242
+ ```
243
+
244
+ 6. **(Optional) Enable the widget builder.** To let users build their own
245
+ widgets, set the sources lambda and register a renderer per visualization in
246
+ the initializer, then write each renderer partial:
247
+
248
+ ```ruby
249
+ DashKit.available_sources_for = ->(viewer) { viewer.account.reports }
250
+ DashKit.register_renderer(:bar_chart, partial: "widgets/renderers/bar_chart")
251
+ DashKit.register_renderer(:stat, partial: "widgets/renderers/stat")
252
+ ```
253
+
254
+ Each renderer partial receives `source`, `options`, and `filter_state` as
255
+ locals. Until `available_sources_for` returns a non-empty list the builder UI
256
+ stays hidden.
257
+
258
+ ### Conventions a consuming app must follow
259
+
260
+ - **Register every dashboard type and its widgets** in
261
+ `DashKit.configure`; a widget that is not registered will not render. Each
262
+ `widget` needs a `label` and a `partial`. Declaration order is the default
263
+ display order.
264
+ - **Supply the data yourself.** A widget's partial is responsible for fetching
265
+ and rendering its own data; DashKit only renders the partial inside a lazy
266
+ Turbo Frame. Keep partials self-contained around the owner DashKit scopes to.
267
+ This holds for registered widgets and for built-widget renderer partials
268
+ alike — both receive `filter_state` and read it to shape their own request.
269
+ - **Register a renderer for every visualization the builder offers.** A built
270
+ widget renders through the partial registered with
271
+ `DashKit.register_renderer`; one with no registered renderer falls back to the
272
+ `missing_renderer` partial. Set `DashKit.available_sources_for` to expose the
273
+ builder UI in the first place.
274
+ - **Persist built widgets through the engine routes**
275
+ (`create_definition` / `update_definition` / `destroy_definition`), not by
276
+ writing `WidgetDefinition` rows by hand.
277
+ - **Scope to the current owner.** Always look up dashboards through
278
+ `DashKit::Dashboard.for_owner(owner)` and set `current_owner_method` so
279
+ DashKit never leaks one owner's dashboard to another. `Dashboard` is
280
+ polymorphic on `owner`.
281
+ - **Persist preferences through the model, not by hand.** Use the
282
+ `WidgetManagement` methods (`toggle_widget`, `move_widget_up`,
283
+ `move_widget_down`, `update_filter`) or the engine routes; do not write
284
+ `widget_order` / `hidden_widgets` / `filter_state` directly.
285
+ - **Let widgets lazy-load.** Render through `dash_kit_render_widgets` /
286
+ `dash_kit_widget_frame` so each widget loads in its own Turbo Frame; do not
287
+ inline widget bodies.
288
+ - **Mobile-first, Tailwind, dark mode.** The shipped UI is styled with Tailwind
289
+ and supports dark mode; host styling should match. The primary surface is
290
+ often a native webview.
291
+
292
+ ### Requirements
293
+
294
+ - Ruby >= 3.2, Rails >= 7.1.
295
+ - `turbo-rails` for Turbo Frames and Stream refreshes.
296
+ - `keystone_ui` (a ViewComponent-based UI gem) for shared UI components.
297
+ - `sortablejs`, pinned via importmap, for drag reordering.
298
+
@@ -0,0 +1,298 @@
1
+ ---
2
+ name: dash_kit-install
3
+ description: Use to set DashKit up in a host Rails app: add the gem, run the install generator, mount the engine, configure the initializer, wire the owner association, importmap, and Stimulus controllers.
4
+ tools: Read, Edit, Write, Bash
5
+ ---
6
+
7
+ You install DashKit into a host Rails app, following only the reference's installation steps. You add the gem, run the install generator, migrate, mount the engine, then complete the manual wiring: the polymorphic owner association, the Stimulus controller registration, and the initializer (parent_controller, current_owner_method, and registered dashboards/widgets). You set up a dashboard controller and view that scope to the current owner. You do not invent configuration the reference does not describe.
8
+
9
+ ## DashKit
10
+
11
+ DashKit is a Rails engine gem for composable, configurable dashboards. It is a
12
+ presentation and interaction layer: you register widgets, persist per-owner
13
+ configuration (visibility, order, filters), and render dashboards without
14
+ coupling the UI to any specific data backend.
15
+
16
+ ### What DashKit is
17
+
18
+ - A Rails engine (`DashKit::Engine`) with an isolated namespace, mounted in the
19
+ host app.
20
+ - A widget registry: dashboard types map to the widgets available on them.
21
+ - A persistence layer for user preferences (which widgets show, in what order,
22
+ with which filters) via an ActiveRecord `Dashboard` model.
23
+ - A rendering layer that lazy-loads each widget through a Turbo Frame and
24
+ refreshes on configuration change.
25
+
26
+ A dashboard renders two kinds of widget side by side:
27
+
28
+ - **Registered widgets** — declared up front in `DashKit.configure`. The
29
+ developer fixes the set of widgets a dashboard type can show.
30
+ - **Widget definitions (built widgets)** — created at runtime by a host-app
31
+ *user* through the settings modal and persisted as records. The host opens a
32
+ small builder UI (a `source` and a `visualization`) and DashKit renders each
33
+ built widget through a host-registered renderer partial.
34
+
35
+ ### What DashKit is not
36
+
37
+ - It does **not** define metrics or their meaning.
38
+ - It does **not** query databases or fetch data. Widgets declare what they need
39
+ through a partial; the host app supplies the data.
40
+ - It does **not** validate filter meaning or enforce business rules.
41
+ - It assumes no specific backend or analytics engine.
42
+
43
+ DashKit treats datasets as opaque structures it can render consistently, so a
44
+ host can replace or evolve its backend without rewriting dashboards.
45
+
46
+ ### Architecture
47
+
48
+ The layers, all under the `DashKit` namespace:
49
+
50
+ - **WidgetRegistry** (`lib/dash_kit/widget_registry.rb`) — a global registry
51
+ mapping a dashboard type to its available widgets. `DashKit.registry` returns
52
+ the singleton; `DashKit.configure { |r| ... }` yields it. Each
53
+ `register(:type)` block uses a `DashboardBuilder` whose `widget(key, label:,
54
+ partial:, **options)` declares one widget and assigns it a position by
55
+ declaration order.
56
+ - **Dashboard** (`app/models/dash_kit/dashboard.rb`) — the ActiveRecord model
57
+ (table `dash_kit_dashboards`) persisting per-owner configuration:
58
+ `widget_order`, `hidden_widgets`, `widget_settings`, and `filter_state`, plus
59
+ `name`, `dashboard_type`, `visibility` (`private` or `account`),
60
+ `role_default_for`, and `active`. It `belongs_to :owner, polymorphic: true`
61
+ and optionally an `account`. Owners may have many named dashboards;
62
+ `for_owner(owner)` and `for_account(account)` scope them, and `activate!` /
63
+ `duplicate!` manage the active one.
64
+ - **WidgetManagement** (`app/models/concerns/dash_kit/widget_management.rb`) — a
65
+ concern mixed into `Dashboard` providing `ordered_visible_widgets`,
66
+ `available_widgets`, `widget_visible?`, `toggle_widget`, `move_widget_up`,
67
+ `move_widget_down`, and `update_filter`.
68
+ - **WidgetDefinition** (`app/models/dash_kit/widget_definition.rb`) — the
69
+ ActiveRecord model (table `dash_kit_widget_definitions`) for a widget a user
70
+ built at runtime. It `belongs_to :dashboard` and carries `source` (string),
71
+ `visualization` (string), and `options` (jsonb, default `{}`). `Dashboard
72
+ has_many :widget_definitions, -> { order(:id) }, dependent: :destroy`.
73
+ - **RendererRegistry** (`lib/dash_kit/renderer_registry.rb`) — a global registry
74
+ mapping a visualization name to the host partial that draws it.
75
+ `DashKit.register_renderer(:visualization_name, partial: "path/to/partial")`
76
+ registers one; `DashKit.renderer_for(visualization)` looks it up;
77
+ `DashKit.visualizations` lists the registered names; `DashKit.reset_renderers!`
78
+ clears them. A built widget whose visualization has no registered renderer
79
+ falls back to the `dash_kit/widget_definitions/missing_renderer` partial.
80
+ - **Sources** — the host declares what a viewer may build from with
81
+ `DashKit.available_sources_for = ->(viewer) { [...] }`, a lambda returning the
82
+ list of sources offered in the builder. `DashKit.available_sources(viewer)`
83
+ calls it. It defaults to `NO_SOURCES` (`->(_viewer) { [] }`), so the builder UI
84
+ stays hidden until the host sets it.
85
+ - **DashboardHelper** (`app/helpers/dash_kit/dashboard_helper.rb`) — view
86
+ helpers: `dash_kit_render_widgets(config:)`, `dash_kit_widget_frame`,
87
+ `dash_kit_settings_modal(config:)`, `dash_kit_settings_button_attributes`,
88
+ `dash_kit_loading_skeleton`, and, for built widgets,
89
+ `dash_kit_available_sources`, `dash_kit_visualizations`, and
90
+ `dash_kit_widget_definition_frame(definition)`. `dash_kit_render_widgets`
91
+ renders the registered widgets in order, then appends one lazy Turbo Frame per
92
+ `widget_definition`.
93
+
94
+ ### Data flow
95
+
96
+ UI events -> dashboard update -> Turbo refresh -> widgets re-render via
97
+ lazy-loaded Turbo Frames. A widget is never rendered inline; it loads through a
98
+ `<turbo-frame loading="lazy">` pointing at the widget's own route. The frame
99
+ carries its dashboard's id, so the widget partial is handed the dashboard's
100
+ `filter_state` to render against (DashKit passes the blob; the host interprets
101
+ it).
102
+
103
+ Built widgets follow the same flow. Each `widget_definition` lazy-loads through
104
+ its own `<turbo-frame>` at `/widget_definitions/:id`;
105
+ `WidgetDefinitionsController#show` finds the definition scoped to the current
106
+ owner's dashboards, looks up `renderer_for(definition.visualization)`, and
107
+ renders that host partial with `source:` (the definition's source), `options:`
108
+ (its options hash), and `filter_state: definition.dashboard.filter_state`. So
109
+ **both** registered widgets and built widgets receive the dashboard's
110
+ `filter_state` as a local — DashKit passes the opaque blob and the host partial
111
+ reads it to shape its own data or stats request.
112
+
113
+ ### Routes (mounted at `/dash_kit`)
114
+
115
+ The engine routes live in `config/routes.rb`:
116
+
117
+ - `GET /widgets/:id` — render an individual registered widget
118
+ - `GET /widget_definitions/:id` — render an individual built widget
119
+ - `resources :dashboards` (`index`, `new`, `create`, `edit`, `update`,
120
+ `destroy`) plus member `select`, `duplicate`, `toggle_widget`, `move_widget`,
121
+ `reorder`, and `save_filters` — the dashboard CRUD plus the widget
122
+ visibility/order/filter actions.
123
+ - Dashboard member `create_definition`, `update_definition`, and
124
+ `destroy_definition` — create, update, and delete a built widget. They accept
125
+ `widget_definition[source, visualization, options]` (strong params
126
+ `permit(:source, :visualization, options: {})`) and are guarded by
127
+ `require_editable!`.
128
+
129
+ ### JavaScript
130
+
131
+ Stimulus controllers ship via importmap (no Node.js build step). The
132
+ `SortableListController` handles client-side widget toggles and drag reordering,
133
+ batching saves when the settings modal closes. It depends on `sortablejs`.
134
+
135
+ ### Widget builder (built widgets)
136
+
137
+ Beyond the registered widgets a developer declares, DashKit lets a host-app
138
+ *user* build their own widgets at runtime and persist them as
139
+ `WidgetDefinition` records. Two host-supplied pieces turn the builder on:
140
+
141
+ - **Sources** — set `DashKit.available_sources_for = ->(viewer) { [...] }` to
142
+ return the list of sources a viewer may build from. Until the host sets this
143
+ it defaults to `NO_SOURCES` and the builder UI stays hidden. The settings
144
+ modal only renders the "Build a widget" form when
145
+ `dash_kit_available_sources.any?`.
146
+ - **Renderers** — call `DashKit.register_renderer(:visualization, partial:
147
+ "path/to/partial")` for each visualization the builder offers, and write each
148
+ renderer partial. A built widget whose visualization has no registered
149
+ renderer falls back to `dash_kit/widget_definitions/missing_renderer`.
150
+
151
+ In the UI, the settings modal's "Build a widget" form is a `source` select
152
+ (from `available_sources`) and a `visualization` select (from `visualizations`);
153
+ it POSTs to `create_definition`. Once saved, `dash_kit_render_widgets` appends a
154
+ lazy Turbo Frame for the new definition, which loads `/widget_definitions/:id`
155
+ and renders the matching renderer partial with `source`, `options`, and
156
+ `filter_state` — the same `filter_state` contract registered widgets get. A
157
+ renderer partial reads `filter_state` to shape its own data request; DashKit
158
+ passes the opaque blob and never interprets it.
159
+
160
+ ### Installation in a host app
161
+
162
+ Add the gem to the host's `Gemfile` (until it is on RubyGems, use a git source):
163
+
164
+ ```ruby
165
+ gem "dash_kit", github: "DYB-Development/dash_kit"
166
+ ```
167
+
168
+ Then run the install generator and migrate:
169
+
170
+ ```bash
171
+ bundle install
172
+ rails generate dash_kit:install
173
+ rails db:migrate
174
+ ```
175
+
176
+ The generator creates the migrations for `dash_kit_dashboards` and
177
+ `dash_kit_widget_definitions`, the `config/initializers/dash_kit.rb`
178
+ initializer, and mounts the engine with `mount DashKit::Engine => "/dash_kit"`.
179
+ The following manual steps remain:
180
+
181
+ 1. **Add the association to the owner model** (the model that owns dashboards,
182
+ e.g. `Account`):
183
+
184
+ ```ruby
185
+ has_many :dash_kit_dashboards, class_name: "DashKit::Dashboard",
186
+ as: :owner, dependent: :destroy
187
+ ```
188
+
189
+ 2. **Pin sortablejs** in `config/importmap.rb`:
190
+
191
+ ```ruby
192
+ pin "sortablejs"
193
+ ```
194
+
195
+ 3. **Register the Stimulus controllers** in
196
+ `app/javascript/controllers/index.js`:
197
+
198
+ ```js
199
+ import { registerControllers as registerDashKitControllers } from "dash_kit/index"
200
+ registerDashKitControllers(application)
201
+ ```
202
+
203
+ 4. **Configure the initializer** `config/initializers/dash_kit.rb`:
204
+
205
+ ```ruby
206
+ DashKit.parent_controller = "ApplicationController"
207
+ DashKit.current_owner_method = :current_account
208
+
209
+ DashKit.configure do |config|
210
+ config.register(:home) do |d|
211
+ d.widget :on_deck, label: "On Deck", partial: "widgets/home/on_deck"
212
+ d.widget :tasks, label: "Tasks", partial: "widgets/home/tasks"
213
+ end
214
+ end
215
+ ```
216
+
217
+ - `parent_controller` is the controller DashKit's controllers inherit from,
218
+ giving them the host's authentication and helpers. It defaults to
219
+ `DashKit::ApplicationController`.
220
+ - `current_owner_method` is the method DashKit calls to scope dashboards
221
+ to the current owner (e.g. `:current_account`). It defaults to `nil` and
222
+ must be set.
223
+
224
+ 5. **Create a dashboard controller and view** that look up the dashboard and
225
+ render the widgets:
226
+
227
+ ```ruby
228
+ class DashboardController < ApplicationController
229
+ def show
230
+ @dashboard = DashKit::Dashboard.for_owner(current_account)
231
+ .find_or_create_by!(dashboard_type: "home", name: "Home") do |dashboard|
232
+ dashboard.widget_order = DashKit.registry.default_widget_order(:home)
233
+ end
234
+ end
235
+ end
236
+ ```
237
+
238
+ ```erb
239
+ <%= dash_kit_settings_button_attributes %>
240
+ <%= dash_kit_settings_modal(config: @dashboard) %>
241
+ <%= dash_kit_render_widgets(config: @dashboard) %>
242
+ ```
243
+
244
+ 6. **(Optional) Enable the widget builder.** To let users build their own
245
+ widgets, set the sources lambda and register a renderer per visualization in
246
+ the initializer, then write each renderer partial:
247
+
248
+ ```ruby
249
+ DashKit.available_sources_for = ->(viewer) { viewer.account.reports }
250
+ DashKit.register_renderer(:bar_chart, partial: "widgets/renderers/bar_chart")
251
+ DashKit.register_renderer(:stat, partial: "widgets/renderers/stat")
252
+ ```
253
+
254
+ Each renderer partial receives `source`, `options`, and `filter_state` as
255
+ locals. Until `available_sources_for` returns a non-empty list the builder UI
256
+ stays hidden.
257
+
258
+ ### Conventions a consuming app must follow
259
+
260
+ - **Register every dashboard type and its widgets** in
261
+ `DashKit.configure`; a widget that is not registered will not render. Each
262
+ `widget` needs a `label` and a `partial`. Declaration order is the default
263
+ display order.
264
+ - **Supply the data yourself.** A widget's partial is responsible for fetching
265
+ and rendering its own data; DashKit only renders the partial inside a lazy
266
+ Turbo Frame. Keep partials self-contained around the owner DashKit scopes to.
267
+ This holds for registered widgets and for built-widget renderer partials
268
+ alike — both receive `filter_state` and read it to shape their own request.
269
+ - **Register a renderer for every visualization the builder offers.** A built
270
+ widget renders through the partial registered with
271
+ `DashKit.register_renderer`; one with no registered renderer falls back to the
272
+ `missing_renderer` partial. Set `DashKit.available_sources_for` to expose the
273
+ builder UI in the first place.
274
+ - **Persist built widgets through the engine routes**
275
+ (`create_definition` / `update_definition` / `destroy_definition`), not by
276
+ writing `WidgetDefinition` rows by hand.
277
+ - **Scope to the current owner.** Always look up dashboards through
278
+ `DashKit::Dashboard.for_owner(owner)` and set `current_owner_method` so
279
+ DashKit never leaks one owner's dashboard to another. `Dashboard` is
280
+ polymorphic on `owner`.
281
+ - **Persist preferences through the model, not by hand.** Use the
282
+ `WidgetManagement` methods (`toggle_widget`, `move_widget_up`,
283
+ `move_widget_down`, `update_filter`) or the engine routes; do not write
284
+ `widget_order` / `hidden_widgets` / `filter_state` directly.
285
+ - **Let widgets lazy-load.** Render through `dash_kit_render_widgets` /
286
+ `dash_kit_widget_frame` so each widget loads in its own Turbo Frame; do not
287
+ inline widget bodies.
288
+ - **Mobile-first, Tailwind, dark mode.** The shipped UI is styled with Tailwind
289
+ and supports dark mode; host styling should match. The primary surface is
290
+ often a native webview.
291
+
292
+ ### Requirements
293
+
294
+ - Ruby >= 3.2, Rails >= 7.1.
295
+ - `turbo-rails` for Turbo Frames and Stream refreshes.
296
+ - `keystone_ui` (a ViewComponent-based UI gem) for shared UI components.
297
+ - `sortablejs`, pinned via importmap, for drag reordering.
298
+
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dash_kit/reference"
4
+
5
+ begin
6
+ require "the_local"
7
+
8
+ TheLocal.register(
9
+ "dash_kit",
10
+ prefix: "dash_kit",
11
+ scope: "composable, configurable Rails dashboards and widgets",
12
+ agents_dir: File.expand_path("the_local/agents", __dir__)
13
+ ) do |c|
14
+ c.agent "info",
15
+ description: "Use to learn what DashKit is, what it does and does not do, and its architecture (widget registry, Dashboard, helpers, routes).",
16
+ tools: "Read",
17
+ body: "You explain DashKit, answering only from the reference: what it is, what it deliberately is not, its widget registry, the Dashboard model, the WidgetManagement concern, the renderer registry and runtime widget builder (WidgetDefinition records built from a source and visualization), the view helpers, the engine routes, and the Turbo lazy-load data flow that hands both registered and built widgets the dashboard's filter_state. You make no changes.",
18
+ knowledge: DashKit::Reference.content
19
+
20
+ c.agent "install",
21
+ description: "Use to set DashKit up in a host Rails app: add the gem, run the install generator, mount the engine, configure the initializer, wire the owner association, importmap, and Stimulus controllers.",
22
+ tools: "Read, Edit, Write, Bash",
23
+ body: "You install DashKit into a host Rails app, following only the reference's installation steps. You add the gem, run the install generator, migrate, mount the engine, then complete the manual wiring: the polymorphic owner association, the Stimulus controller registration, and the initializer (parent_controller, current_owner_method, and registered dashboards/widgets). You set up a dashboard controller and view that scope to the current owner. You do not invent configuration the reference does not describe.",
24
+ knowledge: DashKit::Reference.content
25
+
26
+ c.agent "develop",
27
+ description: "Use to build dashboards and widgets with DashKit in a host app: register widget types, write widget partials, render and lazy-load them, and persist per-owner visibility, order, and filters correctly.",
28
+ tools: "Read, Edit, Write, Bash",
29
+ body: "You build dashboards and widgets with DashKit, following the reference's conventions. You register dashboard types and widgets in DashKit.configure, write self-contained widget partials that fetch their own data, render through dash_kit_render_widgets so widgets lazy-load in Turbo Frames, and always scope dashboards to the current owner via Dashboard.for_owner. You enable the runtime widget builder by setting DashKit.available_sources_for and registering a renderer per visualization with DashKit.register_renderer, writing renderer partials that read filter_state just like registered widgets, and persisting built widgets through the create_definition/update_definition/destroy_definition routes rather than writing WidgetDefinition rows by hand. You persist preferences through the WidgetManagement methods and engine routes rather than writing JSON columns by hand. You never make DashKit query data or enforce metric semantics — those belong to the host.",
30
+ knowledge: DashKit::Reference.content
31
+ end
32
+ rescue LoadError
33
+ # the_local is a build-time-only dependency; the gem works without it.
34
+ end
@@ -0,0 +1,3 @@
1
+ module DashKit
2
+ VERSION = "2.0.0"
3
+ end