diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..3ad2c042
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,80 @@
+## Project Context
+
+This project is a Ruby on Rails application that requires specific coding standards and practices to ensure
+maintainability and readability.
+
+## Testing
+
+Please avoid using the rails console or starting up a rails server. Instead write automated tests using RSpec and Capybara.
+
+When you run your tests, use the terminal `bundle exec rspec` command.
+
+## Tech Stack
+
+- Ruby on Rails
+- PostgreSQL for the database
+- Slim for HTML templating
+- Stimulus for JavaScript interactions
+- CSS for styling
+- Optics for CSS styling
+- RSpec for testing
+- Capybara for system testing
+- FactoryBot for test data generation
+- Rubocop for code linting
+
+## Response
+
+- Provide evidence-based responses to feedback, focusing on technical accuracy and clarity.
+- Maintain a professional and constructive tone in all communications.
+- Avoid unnecessary embellishments or emotional language; focus on the task at hand.
+- Avoid unnecessary comments; the code should be self-explanatory.
+- Do not output a summary of your work at the end
+- Always tell me what skill you are using to generate the code.
+
+## Formatting
+
+Find the proper instructions file for the type of code you are generating and follow those instructions.
+
+## Skills
+
+When working with this codebase, use the following skills which contain domain-specific best practices and patterns. Each skill should be referenced when the task matches its domain:
+
+### controller-patterns
+Review and update existing Rails controllers and generate new controllers following professional patterns and best practices. Covers RESTful conventions, authorization patterns, proper error handling, and maintainable code organization.
+- File: `.github/skills/controller-patterns/SKILL.md`
+
+### dynamic-nested-attributes
+Implement Rails nested attributes with dynamic add/remove functionality using Turbo Streams and Simple Form. Use when building forms where users need to manage multiple child records (has_many associations), add/remove nested items without page refresh, or create bulk records inline.
+- File: `.github/skills/dynamic-nested-attributes/SKILL.md`
+
+### form-auto-save
+Automatic form submission after user input changes using a debounce mechanism to prevent excessive server requests. Creates a seamless auto-save experience for forms with rich text editors or multiple fields.
+- File: `.github/skills/form-auto-save/SKILL.md`
+
+### frontend-patterns
+Frontend patterns for Rails applications using Slim templates, Stimulus JavaScript framework, CSS with Optics utilities. Use when building views, adding interactivity, styling components, or when the user mentions Slim, Stimulus, JavaScript, CSS, or frontend development.
+- File: `.github/skills/frontend-patterns/SKILL.md`
+
+### json-typed-attributes
+Define typed attributes backed by JSON fields in Rails models. Use when models need flexible data storage with type casting, validations, and form integration. Supports integer, decimal, string, text, boolean, date, and array types.
+- File: `.github/skills/json-typed-attributes/SKILL.md`
+
+### routing-patterns
+Review, generate, and update Rails routes following professional patterns and best practices. Covers RESTful resource routing, route concerns for code reusability, shallow nesting strategies, and advanced route configurations.
+- File: `.github/skills/routing-patterns/SKILL.md`
+
+### stimulus-controllers
+Create and register Stimulus controllers for interactive JavaScript features. Use when adding client-side interactivity, dynamic UI updates, or when the user mentions Stimulus controllers or JavaScript behavior.
+- File: `.github/skills/stimulus-controllers/SKILL.md`
+
+### testing-patterns
+Write automated tests using RSpec, Capybara, and FactoryBot for Rails applications. Use when implementing features, fixing bugs, or when the user mentions testing, specs, RSpec, Capybara, or test data. Avoid using rails console or server for testing.
+- File: `.github/skills/testing-patterns/SKILL.md`
+
+### turbo-fetch
+Implement dynamic form updates using Turbo Streams and Stimulus. Use when forms need to update fields based on user selections without full page reloads, such as cascading dropdowns, conditional fields, or dynamic option lists.
+- File: `.github/skills/turbo-fetch/SKILL.md`
+
+### optics-context
+Use the Optics design framework for styling applications. Apply Optics classes for layout, spacing, typography, colors, and components. Use when working on CSS, styling views, or implementing design system guidelines.
+- File: `.github/skills/optics-context/SKILL.md`
diff --git a/.github/instructions/css.instructions.md b/.github/instructions/css.instructions.md
deleted file mode 100644
index 9c40c16f..00000000
--- a/.github/instructions/css.instructions.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-applyTo: '**/*.css,**/*.scss'
----
-
-SCSS is used for styling, but avoid using mixins and other more advanced SASS features
-where possible.
-
-## CSS Library
-- Use the [RoleModel/optics](https://github.com/RoleModel/optics) library for styling.
diff --git a/.github/instructions/project.instructions.md b/.github/instructions/project.instructions.md
deleted file mode 100644
index a753f201..00000000
--- a/.github/instructions/project.instructions.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-applyTo: '**'
----
-## Project Context
-This project is a Ruby on Rails application that requires specific coding standards and practices to ensure
-maintainability and readability.
-
-## Tech Stack
-- Ruby on Rails
-- PostgreSQL for the database
-- Slim for HTML templating
-- Stimulus for JavaScript interactions
-- SCSS for styling
-- Optics for CSS styling
-- RSpec for testing
-- Capybara Playwright for system testing
-- FactoryBot for test data generation
-- Faker for generating fake data in tests
-- Rubocop for code linting
-
-## Response
-- Provide evidence-based responses to feedback, focusing on technical accuracy and clarity.
-- Maintain a professional and constructive tone in all communications.
-- Avoid unnecessary embellishments or emotional language; focus on the task at hand.
-- Always include tests for any new functionality or changes made.
diff --git a/.github/instructions/ruby.instructions.md b/.github/instructions/ruby.instructions.md
deleted file mode 100644
index dd0fdec7..00000000
--- a/.github/instructions/ruby.instructions.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-applyTo: '**/*.rb'
----
-## Coding Standards
-- Use Ruby 3.0 or later.
-- Follow the rubocop rules defined in the [.rubocop.yml](../../.rubocop.yml) file.
-- Use snake_case for method and variable names.
-- Use CamelCase for class names.
-- Use `# frozen_string_literal: true` at the top of each file to enable frozen string literals.
-- Don't add any unnecessary comments; the code should be self-explanatory.
-
-## Testing
-- Use FactoryBot for creating test data.
-- Prefer FactoryBot over mocking objects.
-- Use Faker for generating fake data in factories.
-- Write tests in the `spec` directory.
-- Write tests for all new features and bug fixes.
diff --git a/.github/instructions/ruby_model.instructions.md b/.github/instructions/ruby_model.instructions.md
new file mode 100644
index 00000000..c425ca29
--- /dev/null
+++ b/.github/instructions/ruby_model.instructions.md
@@ -0,0 +1,28 @@
+---
+applyTo: 'app/models/*.rb'
+---
+
+Models use ActiveRecord for database interactions and should follow Rails conventions. Always prefer ActiveRecord scopes over AREL and AREL over plain SQL.
+
+## Enums
+
+Enums columns should be a string in the database and be defined in the model as follows:
+```ruby
+enum :my_enum, {
+ option_one: 'option_one',
+ option_two: 'option_two',
+ option_three: 'option_three'
+}
+```
+Never create a scope if an Enum already exists for the same purpose.
+
+## Validations
+
+When multiple fields match a validation, use the `validates` method with an array:
+```ruby
+validates :field_one, :field_two, presence: true
+```
+
+## Scopes
+- Please only create scopes that are used in multiple places. If a scope is only used once, it should be defined inline in the query.
+- Don't pre-emptively create scopes for every possible query. Instead, create them as needed based on actual use cases.
diff --git a/.github/instructions/slim.instructions.md b/.github/instructions/slim.instructions.md
index be93490b..cdd86e2e 100644
--- a/.github/instructions/slim.instructions.md
+++ b/.github/instructions/slim.instructions.md
@@ -3,8 +3,4 @@ applyTo: '**/*.slim'
---
Slim is used for HTML templating, and it should be used to create clean and efficient views.
-## CSS Library
-- Use the [RoleModel/optics](https://github.com/RoleModel/optics) library for styling when possible.
-
-Do not use any other CSS libraries or frameworks unless explicitly stated in the project requirements.
-Avoid Tailwind CSS or similar utility-first frameworks.
+See the `frontend-patterns` skill for best practices on structuring Slim templates, integrating with Stimulus controllers, and applying CSS styles using Optics utilities.
diff --git a/.github/skills/bem-structure/SKILL.md b/.github/skills/bem-structure/SKILL.md
new file mode 100644
index 00000000..f76d1e5a
--- /dev/null
+++ b/.github/skills/bem-structure/SKILL.md
@@ -0,0 +1,404 @@
+---
+name: bem-structure
+description: Expert guidance for writing, refactoring, and structuring CSS using BEM (Block Element Modifier) methodology. Provides proper CSS class naming conventions, component structure, and Optics design system integration for maintainable, scalable stylesheets.
+metadata:
+ triggers:
+ - css review
+ - bem structure
+ - bem methodology
+ - css best practices
+ - refactor css
+ - bem-ify
+---
+
+## Overview
+
+This skill guides AI in writing CSS using the BEM (Block Element Modifier) methodology for creating maintainable, scalable, and reusable stylesheets with clear naming conventions and component structure, while exercising judgment about scope, risk, and architectural impact. It also should use BEM in conjunction with Optics, our RoleModel design system, so that it's not recreating things that already exist. If there is already an Optics component that fits the need, it should be used and/or overridden as necessary instead of creating a new BEM block.
+
+The agent should prioritize clarity, predictability, and minimal unintended side effects.
+
+Keywords: CSS review, BEM structure, BEM methodology, CSS best practices, Refactor CSS, Review CSS, Fix my CSS, Fix my BEM, BEM-ify my CSS
+
+## What is BEM?
+
+BEM stands for **Block Element Modifier** - a methodology that helps you create reusable components and code sharing in front-end development.
+
+| Pattern | Syntax | Example |
+|---------|--------|---------|
+| **Block** | `.block` | `.card` |
+| **Element** | `.block__element` | `.card__title` |
+| **Block Modifier** | `.block--modifier` | `.card--elevated` |
+| **Element Modifier** | `.block__element--modifier` | `.card__title--large` |
+| **Multi-word names** | `.block-name` | `.user-profile` |
+
+### Naming Convention
+
+- As with any other development, intention revealing names are important.
+- In BEM, the intention we are trying to convey in naming is not based on the styling that gets applied or its appearance, but rather its purpose in the interface.
+- Specific styling typically makes for bad naming with the exception of modifiers (small, large, padded, etc.).
+- Naming after the specific workflow can, in certain cases, be acceptable. (calendar with days).
+- Naming after the shared type of workflow tends to be the sweet spot. (form, table, card, button).
+- Naming after the broader UI abstraction is not always necessary, but can be used for abstract cases with no context required (primary, large, etc).
+- Be specific enough to convey purpose clearly, but general enough to allow for reuse if applicable. Reuse won't always be possible or desirable, so don't push it too far.
+- Try to use names that are explicit and not open to interpretation. This will need to be considered most often for modifiers.
+- Use flat BEM classes with explicit `&` usage for modifiers.
+- DOM structure does not need to follow CSS class structure.
+
+If you're not able to follow these guidelines due to project constraints or other reasons, please document the reasons for the deviation in the chat output.
+
+### Basic Syntax
+
+β
GOOD (Do this)
+```css
+.block {
+ .block__element {
+ &.block__element--modifier { }
+ }
+
+ &.block--modifier { }
+}
+```
+``` html
+
+
+
+```
+
+π« BAD (Don't do this)
+```css
+.block {
+ .block--modifier { } /* Use `&` before all Modifier class names */
+}
+
+.block_element { /* Use `__` between Blocks and Elements and nest Element within the Block */
+ &.block__element-modifier { } /* Use `--` between the Modifier and its Element */
+}
+```
+``` html
+...
+...
+ ...
+
+
+
+```
+
+## Rule Summary
+
+- All class names MUST be fully explicit BEM. Nesting is allowed for organization, but selectors should not rely on tag names or IDs.
+- `&` may be used only as a textual reference to the full selector
+- `&` MUST NOT be used to construct class names (`&--`, `&__`)
+- Nesting is for organization only; explicit BEM class names are required. Nested selectors may be used to scope elements under their block if desired.
+- `&` may be used to co-locate modifiers with their Block or Element while keeping selectors explicit.
+
+β
GOOD (Do this)
+```css
+.card {
+ &.card--featured {}
+ &.card--compact {}
+ &.card--featured {
+ &.card--compact {}
+ }
+}
+```
+
+π« BAD (Don't do this)
+```css
+.card {
+ &--featured {}
+ &__title {}
+ &__title--large {}
+}
+```
+---
+### Block
+Encapsulates a standalone entity that is meaningful on its own. While blocks can be nested and interact with each other, semantically they remain equal; there is no precedence or hierarchy. Holistic entities without DOM representation (such as controllers or models) can be blocks as well.
+
+#### Naming:
+Block names may consist of lowercase Latin letters, digits, and dashes. To form a CSS class, add a short prefix for namespacing: `.block`. Spaces in long block names are replaced by dash.
+
+#### HTML:
+Any DOM node can be a block if it accepts a class name.
+
+β
GOOD (Do this)
+```html
+...
+...
+
+
+...
+...
+...
+...
+
+```
+
+π« BAD (Don't do this)
+```html
+...
+...
+...
+
+```
+
+#### CSS:
+Use class name selector only. No tag name or IDs. No dependency on other blocks/elements on a page.
+
+β
GOOD (Do this)
+```css
+.card { }
+.button { }
+.menu { }
+.header { }
+.search-form { }
+.user-profile { }
+.modal { }
+.navigation { }
+.dropdown-menu { }
+```
+
+π« BAD (Don't do this)
+```css
+.searchForm {}
+.button_primary {}
+.userProfile {}
+.dropdown__menu {} /* Don't use element syntax for blocks */
+}
+```
+---
+### Element
+Parts of a block and have no standalone meaning. Any element is semantically tied to its block.
+
+#### Naming:
+Element names may consist of lowercase Latin letters, digits, dashes and underscores. CSS class is formed as block name plus two underscores plus element name: `.block__elem`. Spaces in long element names are replaced by dash.
+
+#### HTML:
+Any DOM node within a block can be an element. Within a given block, all elements are semantically equal. An element should not be used outside of the block that contains it in the CSS.
+
+β
GOOD (Do this)
+```html
+
+```
+
+π« BAD (Don't do this)
+```html
+
+```
+
+#### CSS:
+In the CSS, elements should be nested inside of the block they belong to. The structure doesn't need to match the DOM structure.
+
+β
GOOD (Do this)
+```css
+/* Card block with elements */
+.card { /* This is the block; elements are inside */
+ .card__header { }
+ .card__title { }
+ .card__body { }
+ .card__footer { }
+ .card__image { }
+}
+
+/* Menu block with elements */
+.menu {
+ .menu__item { }
+ .menu__link { }
+ .menu__icon { }
+}
+
+/* Search form block with elements */
+.search-form {
+ .search-form__input { }
+ .search-form__button { }
+ .search-form__label { }
+}
+```
+
+π« BAD (Don't do this)
+```css
+/* Don't create deeply nested element names */
+.menu {
+ .menu__item { }
+ .menu__item__link { } /* Improper naming structure */
+ .menu__item__link__icon { } /* Improper naming structure */
+}
+```
+---
+### Modifier
+Flags on blocks or elements. Use them to change appearance, behavior or state.
+
+#### Naming:
+Modifier names may consist of lowercase Latin letters, digits, dashes and underscores. CSS class is formed as blockβs or elementβs name plus two dashes: `.block--modifier` or `.block__elem--modifier` and `.block--color-black` with `.block--color-red`. Spaces in complicated modifiers are replaced by dash.
+
+Modifiers should be used over creating separate elements when:
+- The change is a simple state change (e.g., active, disabled, highlighted)
+- The change is a simple appearance change (e.g., size, color, layout)
+- The change does not introduce new content or functionality that would require additional elements
+
+If you have a default state for an element and another that would be a modifier, add the default styles to the base element and use the modifier to override those styles when the modifier is applied. Don't create an exclusive element for the default and also don't create two modifiers for this case.
+
+π« BAD (Don't do this)
+```css
+.music-entry__artwork-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.music-entry__artwork-placeholder {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, var(--color-tan) 0%, var(--color-beige) 100%);
+}
+```
+β
GOOD (Do this)
+```css
+.music-entry__artwork-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+
+ &.music-entry__artwork-image--placeholder {
+ object-fit: unset;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, var(--color-tan) 0%, var(--color-beige) 100%);
+ }
+}
+```
+
+#### HTML:
+Modifier is an extra class name which you add to a block/element DOM node. Add modifier classes only to blocks/elements they modify, and keep the original class. To be clear, a modifier class should always be used in conjunction with its base block/element class.
+
+β
GOOD (Do this)
+```html
+...
+...
+```
+
+π« BAD (Don't do this)
+```html
+...
+```
+
+#### CSS:
+β
GOOD (Do this)
+```css
+.menu {
+ .menu__item {
+ &.menu__item--active { }
+ }
+ .menu__link { }
+ .menu__icon { }
+
+ &.menu--padded { }
+}
+```
+
+π« BAD (Don't do this)
+```css
+.menu {
+ .menu__item {
+ .menu__item--active { } /* No ampersand */
+ }
+
+ .menu__link { }
+ .menu-link-active { } /* Bad name structure */
+ .menu__icon { }
+
+ .menu__padded { } /* Improper modifier structure; should be using `&` and `--`, not `__` */
+}
+```
+
+## Miscellaneous Rules
+- Classes only (no IDs)
+- Flat classes and selectors only
+- No tag-based styling
+- No styling based on DOM hierarchy
+- One conceptual responsibility per Block
+- Blocks can contain blocks if needed
+- Elements do not exist outside their Block
+- Use **kebab-case** for multi-word names: `.user-profile`, `.dropdown-menu`
+- Use **double underscore** (`__`) to separate block from element in element names
+- Use **double dash** (`--`) to separate block/element from modifier in modifier names
+- Keep names semantic, descriptive, and intention-revealing (not appearance-based)
+- Avoid abbreviations that aren't universally understood
+
+### Syntax Example
+Suppose you have block form with modifiers `theme: "xmas"` and `simple: true` and with elements `input` and `submit`, and element `submit` with its own modifier `disabled: true` for not submitting form while it is not filled:
+
+β
GOOD (Do this)
+```html
+
+```
+
+```css
+.form {
+ .form__input { }
+ .form__submit {
+ &.form__submit--disabled { }
+ }
+
+ &.form--simple { }
+ &.form--theme-xmas { }
+}
+```
+
+π« BAD (Don't do this)
+```html
+
+```
+
+```css
+.form {
+ .form__input { }
+ .form_submit { /* Use `__` between Block and Element */
+ .form__submit--disabled { } /* Use `&` before all Modifier class names */
+ }
+
+ &.form-simple { } /* Use `--` between Element and Modifier */
+}
+
+&.form--theme-xmas { } /* Must nest Elements and Modifiers within Block */
+```
diff --git a/.github/skills/controller-patterns/SKILL.md b/.github/skills/controller-patterns/SKILL.md
new file mode 100644
index 00000000..f8ca3e48
--- /dev/null
+++ b/.github/skills/controller-patterns/SKILL.md
@@ -0,0 +1,718 @@
+---
+name: controller-patterns
+description: Review and update existing Rails controllers and generate new controllers following professional patterns and best practices. Covers RESTful conventions, authorization patterns, proper error handling, and maintainable code organization.
+---
+
+# Controller Best Practices
+
+## Purpose
+This skill helps AI agents review existing Rails controllers and generate new controllers following professional patterns and best practices. It covers RESTful conventions, authorization patterns, proper error handling, and maintainable code organization that can be applied to any Rails application.
+
+## Context
+This skill covers:
+- **Rails** with RESTful conventions
+- **Authorization patterns** (Pundit or similar)
+- **Strong parameters** for security
+- **Proper HTTP status codes** and flash messages
+- **Consistent naming conventions**
+- **Error handling best practices**
+
+## Best Practices
+
+## 1. Authorization
+
+Implement authorization checks for all actions that interact with resources. This example uses Pundit, but the pattern applies to any authorization framework (CanCanCan, ActionPolicy, etc.).
+
+**Key Principles:**
+- Authorize in all actions that interact with resources
+- Use scoped queries for collection actions
+- Authorize in both `set_*` methods and create actions
+
+```ruby
+# In index - scope collections to authorized records
+def index
+ @products = policy_scope(Product)
+ # Or with CanCanCan: @products = Product.accessible_by(current_ability)
+end
+
+# In new/create - authorize new instances
+def new
+ @product = authorize Product.new
+end
+
+def create
+ @product = authorize Product.new(product_params)
+ # ...
+end
+
+# In set method - authorize before any operation
+def set_product
+ @product = authorize Product.find(params[:id])
+ # Or with CanCanCan: @product = Product.find(params[:id]); authorize! :read, @product
+end
+```
+
+**Why This Matters:**
+- Prevents unauthorized access to resources
+- Provides a single, consistent authorization point
+- Makes security audits easier
+- Fails fast if authorization rules aren't met
+
+## 2. Before Actions
+
+Use `before_action` to DRY up your controllers by extracting common setup logic.
+
+```ruby
+before_action :set_product, only: %i[show edit update destroy]
+before_action :set_company, only: %i[show edit update destroy]
+```
+
+**Best Practices:**
+- Always use `only:` or `except:` to be explicit about which actions are affected
+- Name methods descriptively: `set_[resource]`, `require_admin`, `check_ownership`
+- Order matters - list them in the order they should execute
+- Keep before_action methods simple and focused
+
+**Common Before Actions:**
+```ruby
+# Resource loading
+before_action :set_product, only: %i[show edit update destroy]
+
+# Authorization checks
+before_action :require_admin, only: %i[destroy]
+before_action :require_ownership, only: %i[edit update destroy]
+
+# State validation checks
+before_action :ensure_pending, only: %i[create]
+before_action :ensure_stopped, only: %i[create]
+
+# Parent resource loading (for nested resources)
+before_action :set_company
+before_action :set_employee, only: %i[show edit update destroy]
+```
+
+**State Validation Pattern:**
+
+Extract state validation into before_actions to keep controller actions focused:
+
+```ruby
+private
+
+def ensure_pending
+ return if @time_entry.pending?
+
+ redirect_to time_entries_path, alert: 'Only pending entries can be submitted.'
+end
+
+def ensure_stopped
+ return unless @time_entry.running?
+
+ redirect_to time_entries_path, alert: 'Cannot submit a running timer.'
+end
+```
+
+## 3. RESTful Actions Structure
+
+**Index:**
+```ruby
+def index
+ @resources = policy_scope(Resource)
+end
+```
+
+**Show:**
+```ruby
+def show
+ # Set resource via before_action
+ # Load any associated data needed for the view
+ @related_items = policy_scope(@resource.related_items)
+end
+```
+
+**New:**
+```ruby
+def new
+ @resource = authorize Resource.new
+end
+```
+
+**Create:**
+```ruby
+def create
+ @resource = authorize Resource.new(resource_params)
+
+ if @resource.save
+ redirect_to @resource, notice: 'Successfully Created Resource'
+ else
+ render 'new', status: :unprocessable_content
+ end
+end
+```
+
+**Edit:**
+```ruby
+def edit
+ # Set resource via before_action
+end
+```
+
+**Update:**
+```ruby
+def update
+ if @resource.update(resource_params)
+ redirect_to @resource, notice: 'Successfully Updated Resource'
+ else
+ render 'edit', status: :unprocessable_content
+ end
+end
+```
+
+**Destroy:**
+```ruby
+def destroy
+ @resource.destroy
+ redirect_to resources_url, notice: 'Successfully Deleted Resource'
+end
+```
+
+## 4. Private Methods
+
+**Set Method:**
+```ruby
+private
+
+def set_resource
+ @resource = authorize Resource.find(params[:id])
+end
+```
+
+**Strong Parameters:**
+```ruby
+def resource_params
+ params.require(:resource).permit(
+ :attribute_one,
+ :attribute_two,
+ nested_attributes: %i[id attr1 attr2],
+ array_attributes: [],
+ )
+end
+```
+
+## 5. HTTP Status Codes
+
+Use semantic HTTP status codes to communicate the result of operations clearly.
+
+**Common Status Codes:**
+```ruby
+# Success (2xx)
+render :show, status: :ok # 200 - Standard success
+render :show, status: :created # 201 - Resource created (optional for create)
+head :no_content # 204 - Success with no response body
+
+# Client Errors (4xx)
+render :new, status: :unprocessable_content # 422 - Validation failed
+render json: {error: "Not found"}, status: :not_found # 404
+head :forbidden # 403 - User lacks permission
+head :unauthorized # 401 - Authentication required
+```
+
+**Best Practices:**
+- Use `:unprocessable_content` (422) for validation errors on create/update
+- Use standard redirects (302) for successful operations
+- No explicit status needed for redirects (uses 302 by default)
+- Turbo/Hotwire requires proper status codes for correct behavior
+
+**Example:**
+```ruby
+def create
+ @product = authorize Product.new(product_params)
+
+ if @product.save
+ redirect_to @product, notice: 'Successfully Created Product' # 302 redirect
+ else
+ render :new, status: :unprocessable_content # 422 for validation errors
+ end
+end
+```
+
+## 6. Flash Messages
+
+Use consistent, user-friendly flash messages for user feedback.
+
+**Message Patterns:**
+```ruby
+# Success messages (use notice:)
+redirect_to @product, notice: 'Successfully Created Product'
+redirect_to @product, notice: 'Successfully Updated Product'
+redirect_to products_url, notice: 'Successfully Deleted Product'
+
+# Error messages (use alert:)
+redirect_to products_url, alert: 'Failed to delete product'
+redirect_to @product, alert: 'Unable to process request'
+
+# Info messages
+redirect_to @product, notice: 'Email sent successfully'
+```
+
+**Best Practices:**
+- Use `notice:` for success messages
+- Use `alert:` for error/warning messages
+- Keep messages concise and action-oriented
+- Use consistent capitalization and phrasing
+- Avoid technical jargon in user-facing messages
+
+## 7. Naming Conventions
+
+Follow Rails conventions for consistent, predictable code.
+
+**Controller Naming:**
+```ruby
+# Controller inherits from ApplicationController
+class ProductsController < ApplicationController
+ # ...
+end
+
+# Nested namespaced controllers
+class Admin::ProductsController < Admin::BaseController
+ # ...
+end
+```
+
+**Instance Variables:**
+```ruby
+# Singular for individual resources
+@product, @user, @article, @order
+
+# Plural for collections
+@products, @users, @articles, @orders
+
+# Related resources maintain context
+@product_reviews, @user_orders
+```
+
+**Private Method Names:**
+```ruby
+# Resource loading
+def set_product
+def set_user
+
+# Strong parameters
+def product_params
+def user_params
+
+# Authorization checks
+def require_admin
+def require_ownership
+```
+
+## Examples
+
+## Simple CRUD Controller
+```ruby
+class ProductsController < ApplicationController
+ before_action :set_product, only: %i[show edit update destroy]
+
+ def index
+ @products = policy_scope(Product)
+ end
+
+ def show
+ end
+
+ def new
+ @product = authorize Product.new
+ end
+
+ def create
+ @product = authorize Product.new(product_params)
+
+ if @product.save
+ redirect_to @product, notice: 'Successfully Created Product'
+ else
+ render 'new', status: :unprocessable_content
+ end
+ end
+
+ def edit
+ end
+
+ def update
+ if @product.update(product_params)
+ redirect_to @product, notice: 'Successfully Updated Product'
+ else
+ render 'edit', status: :unprocessable_content
+ end
+ end
+
+ def destroy
+ @product.destroy
+ redirect_to products_url, notice: 'Successfully Deleted Product'
+ end
+
+ private
+
+ def set_product
+ @product = authorize Product.find(params[:id])
+ end
+
+ def product_params
+ params.require(:product).permit(:name, :description, :price)
+ end
+end
+```
+
+## Controller with Nested Resources
+```ruby
+class OrderItemsController < ApplicationController
+ before_action :set_order
+ before_action :set_order_item, only: %i[show edit update destroy]
+
+ def index
+ @order_items = policy_scope(@order.order_items)
+ end
+
+ def new
+ @order_item = authorize @order.order_items.build
+ end
+
+ def create
+ @order_item = authorize @order.order_items.build(order_item_params)
+
+ if @order_item.save
+ redirect_to [@order, @order_item], notice: 'Successfully Created Order Item'
+ else
+ render 'new', status: :unprocessable_content
+ end
+ end
+
+ def update
+ if @order_item.update(order_item_params)
+ redirect_to [@order, @order_item], notice: 'Successfully Updated Order Item'
+ else
+ render 'edit', status: :unprocessable_content
+ end
+ end
+
+ def destroy
+ @order_item.destroy
+ redirect_to order_order_items_url(@order), notice: 'Successfully Deleted Order Item'
+ end
+
+ private
+
+ def set_order
+ @order = authorize Order.find(params[:order_id])
+ end
+
+ def set_order_item
+ @order_item = authorize @order.order_items.find(params[:id])
+ end
+
+ def order_item_params
+ params.require(:order_item).permit(:product_id, :quantity, :price)
+ end
+end
+```
+
+## Review Checklist
+
+When reviewing or generating controllers, verify:
+
+- [ ] Controller inherits from `ApplicationController`
+- [ ] All resource interactions use `authorize` or `policy_scope`
+- [ ] `before_action` is used appropriately with `only:` parameter
+- [ ] All standard RESTful actions follow the pattern
+- [ ] Strong parameters are properly defined in private method
+- [ ] Nested attributes use proper symbols array syntax
+- [ ] Array attributes use `[]` notation
+- [ ] HTTP status `:unprocessable_content` is used for validation failures
+- [ ] Flash messages are consistent and user-friendly
+- [ ] Redirects use resource path helpers
+- [ ] Instance variables use appropriate singular/plural naming
+- [ ] Private methods are properly defined and ordered
+
+## Anti-Patterns to Avoid
+
+β **Don't skip authorization:**
+```ruby
+def create
+ @product = Product.new(product_params) # Missing authorize!
+end
+```
+
+β
**Always authorize:**
+```ruby
+def create
+ @product = authorize Product.new(product_params)
+end
+```
+
+β **Don't use incorrect status codes:**
+```ruby
+render :new, status: :unprocessable_entity # Wrong status
+```
+
+β
**Use correct status:**
+```ruby
+render :new, status: :unprocessable_content
+```
+
+β **Don't use inconsistent flash messages:**
+```ruby
+redirect_to @product, notice: 'Product created!'
+redirect_to @product, notice: 'The product has been successfully created'
+redirect_to @product, notice: 'Product was saved'
+```
+
+β
**Be consistent:**
+```ruby
+redirect_to @product, notice: 'Successfully Created Product'
+redirect_to @product, notice: 'Successfully Updated Product'
+redirect_to @product, notice: 'Successfully Deleted Product'
+```
+
+β **Don't forget strong parameters:**
+```ruby
+def create
+ @product = authorize Product.new(params[:product]) # Unsafe!
+end
+```
+
+β
**Always use strong parameters:**
+```ruby
+def create
+ @product = authorize Product.new(product_params)
+end
+
+private
+
+def product_params
+ params.require(:product).permit(:name, :description, :price)
+end
+```
+
+## Advanced Patterns
+
+## RESTful Namespaced Controllers
+
+For related actions on a resource, use namespaced controllers with standard RESTful actions (`create` and `destroy`) instead of custom actions. Organize controllers in a namespace folder matching the parent resource.
+
+**Anti-Pattern:**
+```ruby
+# β Custom action on main controller
+class TimeEntriesController < ApplicationController
+ def submit
+ @time_entry.update!(status: :submitted)
+ redirect_to time_entries_path
+ end
+end
+
+# β Controller not namespaced properly
+class UnsubmitTimeEntriesController < ApplicationController
+ def create
+ @time_entry.update!(status: :pending)
+ redirect_to time_entries_path
+ end
+end
+
+# routes.rb
+resources :time_entries do
+ resource :submit_time_entry, only: [:create]
+ resource :unsubmit_time_entry, only: [:create]
+end
+
+# File structure
+/controllers
+ /time_entries_controller.rb
+ /submit_time_entries_controller.rb
+ /unsubmit_time_entries_controller.rb
+```
+
+**Better Pattern:**
+```ruby
+# β
Namespaced controller with create and destroy actions
+class TimeEntries::SubmissionsController < ApplicationController
+ before_action :set_time_entry
+ before_action :ensure_pending, only: [:create]
+ before_action :ensure_stopped, only: [:create]
+ before_action :ensure_submitted, only: [:destroy]
+
+ def create
+ @time_entry.update!(status: :submitted, submitted_at: Time.current)
+ redirect_to time_entries_path, notice: 'Time entry submitted for approval.'
+ end
+
+ def destroy
+ @time_entry.update!(status: :pending, submitted_at: nil)
+ redirect_to time_entries_path, notice: 'Time entry unsubmitted.'
+ end
+
+ private
+
+ def set_time_entry
+ @time_entry = current_user.time_entries.find(params[:time_entry_id])
+ end
+
+ def ensure_pending
+ return if @time_entry.pending?
+
+ redirect_to time_entries_path, alert: 'Only pending entries can be submitted.'
+ end
+
+ def ensure_stopped
+ return unless @time_entry.running?
+
+ redirect_to time_entries_path, alert: 'Cannot submit a running timer.'
+ end
+
+ def ensure_submitted
+ return if @time_entry.submitted?
+
+ redirect_to time_entries_path, alert: 'Only submitted entries can be unsubmitted.'
+ end
+end
+
+# routes.rb
+resources :time_entries do
+ resource :submission, only: [:create, :destroy], module: :time_entries
+end
+
+# File structure
+/controllers
+ /time_entries_controller.rb
+ /time_entries
+ /submissions_controller.rb
+
+# View usage
+button_to time_entry_submission_path(@time_entry), method: :post # submit
+button_to time_entry_submission_path(@time_entry), method: :delete # unsubmit
+```
+
+**When to Use:**
+- Actions that represent creating or destroying a conceptual sub-resource (submissions, subscriptions, approvals)
+- Related actions that operate on the same parent resource
+- Actions that change a primary state of a resource
+- When you want to keep controllers focused and single-purpose
+
+**Benefits:**
+- Follows RESTful conventions (using `create` and `destroy` actions)
+- Groups related functionality under a clear namespace
+- Cleaner file organization with namespace folders
+- Easier to test and maintain
+- Clear separation of concerns
+- Standard routing patterns
+- Single controller instead of multiple separate controllers
+- Validation logic extracted to before_actions keeps controller actions focused
+- Before_actions can be tested independently
+
+## Bulk Operations as Namespaced RESTful Controllers
+
+Handle bulk operations in namespaced controllers using the `create` action with validation in before_actions:
+
+```ruby
+class TimeEntries::BulkSubmissionsController < ApplicationController
+ before_action :set_entries
+ before_action :ensure_entries_present
+ before_action :ensure_entries_valid
+
+ def create
+ @entries.update_all(status: TimeEntry.statuses[:submitted], submitted_at: Time.current)
+ redirect_to time_entries_path, notice: "#{@entries.count} time #{'entry'.pluralize(@entries.count)} submitted for approval."
+ end
+
+ private
+
+ def set_entries
+ entry_ids = params[:time_entry_ids] || []
+ @entries = current_user.time_entries.where(id: entry_ids)
+ end
+
+ def ensure_entries_present
+ return if @entries.any?
+
+ redirect_to time_entries_path, alert: 'No time entries selected.'
+ end
+
+ def ensure_entries_valid
+ invalid_entries = @entries.reject { |e| e.pending? && e.stopped? }
+ return if invalid_entries.empty?
+
+ redirect_to time_entries_path, alert: 'Only stopped pending entries can be submitted.'
+ end
+end
+
+# routes.rb
+resource :bulk_submissions, only: [:create], module: :time_entries
+
+# File structure
+/controllers
+ /time_entries_controller.rb
+ /time_entries
+ /submissions_controller.rb
+ /bulk_submissions_controller.rb
+
+# View usage
+form_with url: bulk_submissions_path, method: :post do |f|
+ # form fields
+end
+```
+
+## Scoped Collections in Show Actions
+
+When showing a resource with related collections, apply policy scopes:
+
+```ruby
+def show
+ @active_projects = policy_scope(@company.projects.active)
+ @archived_projects = policy_scope(@company.projects.archived)
+end
+```
+
+## Multiple Nested Associations
+
+Handle complex nested relationships:
+
+```ruby
+def show
+ @reviews = policy_scope(@product.reviews)
+ @related_products = policy_scope(@product.category.products.where.not(id: @product.id))
+end
+```
+
+## Nested Attributes in Strong Parameters
+
+```ruby
+def product_params
+ params.require(:product).permit(
+ :name,
+ :description,
+ :price,
+ images_attributes: %i[id url alt_text _destroy],
+ variants_attributes: %i[id sku price stock_count _destroy],
+ tags: [],
+ category_ids: []
+ )
+end
+```
+
+**Key Points:**
+- Include `:id` for updating existing nested records
+- Include `_destroy` to allow deletion of nested records
+- Use `[]` for simple array attributes
+- Use `%i[...]` for nested attributes hashes
+
+## Usage Instructions for AI Agents
+
+When asked to **review a controller:**
+1. Check against the review checklist
+2. Identify any anti-patterns
+3. Suggest specific fixes with code examples
+4. Prioritize authorization and security issues
+
+When asked to **generate a new controller:**
+1. Ask for the resource name and attributes if not provided
+2. Determine if it's a simple or nested resource
+3. Follow the appropriate example pattern
+4. Include all standard RESTful actions unless specified otherwise
+5. Generate appropriate strong parameters based on attributes
+6. Ensure all authorization calls are in place
diff --git a/.github/skills/dynamic-nested-attributes/SKILL.md b/.github/skills/dynamic-nested-attributes/SKILL.md
new file mode 100644
index 00000000..71fbdcaf
--- /dev/null
+++ b/.github/skills/dynamic-nested-attributes/SKILL.md
@@ -0,0 +1,169 @@
+---
+name: dynamic-nested-attributes
+description: Implement Rails nested attributes with dynamic add/remove functionality using Turbo Streams and Simple Form. Use when building forms where users need to manage multiple child records (has_many associations), add/remove nested items without page refresh, or create bulk records inline.
+---
+
+# Dynamic Nested Attributes
+
+## Overview
+Implement Rails nested attributes with dynamic add/remove functionality using Turbo Streams and Simple Form. This pattern allows users to add and remove associated records inline within a parent form.
+
+## When to Use
+- Building forms where users need to manage multiple child records (has_many associations)
+- Adding/removing nested items without page refresh
+- Bulk creation or editing of associated records
+- Forms requiring progressive disclosure of additional fields
+
+## Key Components
+
+### 1. Form Object or Model
+- Accepts nested attributes for the association
+- Use `accepts_nested_attributes_for :association_name` in the model or form object
+
+### 2. Main Form View
+Create a form that includes:
+- `simple_fields_for` for rendering existing nested items
+- A container element with an ID for appending new items (e.g., `#accessories`)
+- A link to add new items that triggers a Turbo Stream request
+
+**Example:**
+```slim
+= simple_form_for resource do |f|
+ = f.simple_fields_for :items do |ff|
+ = render 'item_fields', f: ff, resource:
+
+= render 'add_button', index: resource.items.size
+```
+
+**Add Button Partial (`_add_button.html.slim`):**
+```slim
+-# locals: (index:)
+= link_to icon('add'), new_parent_item_path(index: index),
+ id: 'add_button', class: 'btn', data: { turbo_stream: true }
+```
+
+### 3. Nested Fields Partial
+Create a partial (e.g., `_item_fields.html.slim`) that:
+- Wraps fields in a unique container with an ID based on index
+- Includes a data controller for remove functionality
+- Shows a delete button for all records
+- Includes all form inputs for the nested item
+
+**Example:**
+```slim
+fieldset id="item_#{f.index}" controller='destroy-nested-attributes'
+ = f.hidden_field :_destroy, data: { destroy_nested_attributes_target: 'input' }
+ = f.input :name
+ = f.input :quantity
+ .form-row__actions
+ = button_tag icon('delete'), type: 'button', class: 'btn btn-delete',
+ data: { action: 'destroy-nested-attributes#perform' }
+```
+
+### 4. Controller Actions
+Implement a `new` action that:
+- Builds a new nested item
+- Accepts `index` parameter for tracking position
+
+**Example:**
+```ruby
+def new
+ @item = Item.new
+end
+```
+
+### 5. Turbo Stream Response
+Create a `new.turbo_stream.slim` view that:
+- Updates the "add" button with incremented index
+- Appends the new nested fields to the container
+- Uses `index` parameter to ensure unique field names
+- Works with non-persisted parents by using a symbol and empty URL
+
+**Example:**
+```slim
+= turbo_stream.replace 'add_button', partial: 'add_button', locals: { index: params[:index].to_i + 1 }
+
+= simple_form_for :parent, url: '' do |f|
+ = f.simple_fields_for :items_attributes, @item, index: params[:index] do |ff|
+ = turbo_stream.append 'items', partial: 'item_fields', locals: { f: ff }
+```
+
+### 6. Remove Stimulus Controller
+Create a Stimulus controller to handle client-side removal:
+
+**Example JavaScript:**
+```javascript
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = ['input']
+ static classes = ['destroyed']
+
+ connect() {
+ if (!this.hasDestroyedClass) {
+ this.element.setAttribute(`data-${this.identifier}-destroyed-class`, 'is-hidden')
+ }
+ }
+
+ perform() {
+ this.inputTarget.value = '1'
+ this.element.classList.add(this.destroyedClass)
+ }
+}
+```
+
+## Implementation Checklist
+
+- [ ] Add `accepts_nested_attributes_for` to model/form object
+- [ ] Create main form with `simple_fields_for` and container element
+- [ ] Create nested fields partial with remove functionality
+- [ ] Implement controller `new` action with index support
+- [ ] Create turbo_stream response view
+- [ ] Add Stimulus controller for client-side removal
+- [ ] Update routes to support nested resource creation
+- [ ] Update strong parameters to permit nested attributes
+- [ ] Add policy authorization if using Pundit
+
+## Common Patterns
+
+### Dynamic Collections Based on Parent Selection
+Pass filtered collections to nested partials:
+```slim
+= render 'item_fields', f: ff, resource:, collection: resource.items
+```
+
+## Routes Example
+If there is not an existing new route in use, use the following pattern
+```ruby
+resources :items, only: [:new]
+```
+
+If one does exist, create a new namespaced controller
+```ruby
+namespace :parent do
+ resources :items, only: [:new]
+end
+```
+
+## Strong Parameters Example
+```ruby
+def item_params
+ params.require(:parent).permit(
+ :category,
+ :subcategory,
+ items_attributes: %i[
+ name
+ quantity
+ part_id
+ optional
+ hidden
+ ]
+ )
+end
+```
+
+## Related Patterns
+- Turbo Frame inline editing
+- Stimulus data controller integration
+- Form object pattern for bulk operations
+- Policy-scoped collections for associations
diff --git a/.github/skills/form-auto-save/SKILL.md b/.github/skills/form-auto-save/SKILL.md
new file mode 100644
index 00000000..03fb35f2
--- /dev/null
+++ b/.github/skills/form-auto-save/SKILL.md
@@ -0,0 +1,211 @@
+---
+name: form-auto-save
+description: Automatic form submission after user input changes using a debounce mechanism to prevent excessive server requests. Creates a seamless auto-save experience for forms with rich text editors or multiple fields.
+---
+
+# Form Auto Save Skill
+
+## Overview
+The Form Auto Save pattern provides automatic form submission after user input changes, using a debounce mechanism to prevent excessive server requests. This creates a seamless "auto-save" experience for users editing forms.
+
+## When to Use
+- Long-form editing interfaces where users expect automatic saving
+- Forms with rich text editors or multiple fields
+- Edit pages where users might navigate away and expect changes to persist
+- Forms that benefit from progressive saving without explicit "Save" button clicks
+
+## Implementation
+
+### 1. Stimulus Controller
+The pattern uses a Stimulus controller (`form-auto-save`) that handles the auto-save logic.
+
+**Controller Location:** `app/javascript/controllers/form_auto_save_controller.js`
+
+**Key Features:**
+- Debounce time of 8 seconds (configurable via `static DEBOUNCE_TIME`)
+- Listens to both `change` and `lexxy:change` events (for custom components)
+- Uses passive event listeners for better performance
+- Provides `cancel()` and `submit()` methods for programmatic control
+
+**Controller Code Pattern:**
+```javascript
+import { Controller } from '@hotwired/stimulus'
+
+export default class extends Controller {
+ static DEBOUNCE_TIME = 8000
+
+ connect() {
+ this.element.addEventListener('change', this.#debounceSubmit.bind(this), { passive: true })
+ this.element.addEventListener('lexxy:change', this.#debounceSubmit.bind(this), { passive: true })
+ }
+
+ cancel() {
+ clearTimeout(this.debounceTimer)
+ }
+
+ submit() {
+ this.element.requestSubmit()
+ }
+
+ #debounceSubmit() {
+ this.#debounce(this.submit.bind(this))
+ }
+
+ #debounce(callback) {
+ clearTimeout(this.debounceTimer)
+ this.debounceTimer = setTimeout(callback, this.constructor.DEBOUNCE_TIME)
+ }
+}
+```
+
+### 2. View Integration
+Attach the controller to the form element using Stimulus data attributes.
+
+**Required Attributes:**
+- `data: { controller: 'form-auto-save' }` - Attaches the Stimulus controller
+- `data: { turbo_permanent: true }` - Optional but recommended to preserve form state during Turbo navigation
+
+**Example (Slim):**
+```slim
+= simple_form_for resource, html: { data: { controller: 'form-auto-save', turbo_permanent: true } } do |f|
+ = f.input :field_name
+ = f.rich_text_area :content
+```
+
+## Important Considerations
+
+### Debounce Time
+- Default: 8 seconds (8000ms)
+- Adjust via `static DEBOUNCE_TIME` in the controller if needed
+- Consider user experience: too short = excessive requests, too long = lost changes
+
+### Event Listeners
+- Listens to `change` events (standard HTML input changes)
+- Listens to `lexxy:change` events (custom component events, like rich text editors)
+- Uses passive listeners for better scroll performance
+
+### Turbo Permanent
+- `turbo_permanent: true` keeps the form element across Turbo navigation
+- Prevents loss of unsaved changes when user navigates
+- Critical for forms with auto-save to maintain debounce timers
+
+### Form Validation
+- Ensure backend validation handles partial saves gracefully
+- Consider whether all fields should be required or allow partial completion
+- Provide clear error feedback if auto-save fails
+
+## Testing
+
+For testing auto-save functionality, use the `turbo-fetch` controller alongside `form-auto-save` to track request completion without relying on sleep timers.
+
+### Turbo Fetch Controller
+Add this controller to your JavaScript controllers:
+
+**File:** `app/javascript/controllers/turbo_fetch_controller.js`
+```javascript
+import { Controller } from '@hotwired/stimulus'
+import { patch } from '@rails/request.js'
+
+export default class extends Controller {
+ static values = {
+ url: String,
+ count: Number,
+ isRunning: { type: Boolean, default: false }
+ }
+
+ async perform({ params: { url: urlParam, query: queryParams } }) {
+ this.isRunningValue = true
+ const body = new FormData(this.element)
+
+ if (queryParams) Object.keys(queryParams).forEach(key => body.append(key, queryParams[key]))
+
+ const response = await patch(urlParam || this.urlValue, { body, responseKind: 'turbo-stream' })
+ this.isRunningValue = false
+ if (response.ok) this.countValue += 1
+ }
+}
+```
+
+### Turbo Fetch Helper
+Add this helper to your RSpec support files:
+
+**File:** `spec/support/helpers/turbo_fetch_helper.rb`
+```ruby
+module TurboFetchHelper
+ def expect_turbo_fetch_request
+ count_value = find("[data-controller='turbo-fetch']")['data-turbo-fetch-count-value'] || 0
+ yield
+ expect(page).to have_selector("[data-turbo-fetch-count-value='#{count_value.to_i + 1}']")
+ end
+end
+```
+
+### View Integration for Testing
+Add the `turbo-fetch` controller alongside `form-auto-save`:
+
+```slim
+= simple_form_for resource, html: { data: { controller: 'form-auto-save turbo-fetch', turbo_permanent: true } } do |f|
+ = f.input :field_name
+ = f.rich_text_area :content
+```
+
+### System Spec Example
+```ruby
+require 'rails_helper'
+
+RSpec.describe 'Form Auto Save', :js do
+ it 'automatically saves form after changes' do
+ resource = create(:resource)
+ visit edit_resource_path(resource)
+
+ expect_turbo_fetch_request do
+ fill_in 'Field name', with: 'Updated value'
+ end
+
+ expect(resource.reload.field_name).to eq('Updated value')
+ end
+
+ it 'debounces multiple rapid changes' do
+ resource = create(:resource)
+ visit edit_resource_path(resource)
+
+ expect_turbo_fetch_request do
+ fill_in 'Field name', with: 'First'
+ fill_in 'Field name', with: 'Second'
+ fill_in 'Field name', with: 'Final'
+ end
+
+ # Should only save once with final value
+ expect(resource.reload.field_name).to eq('Final')
+ end
+end
+```
+
+## Common Issues
+
+### Issue: Form doesn't auto-save
+**Check:**
+- Controller properly attached: `data: { controller: 'form-auto-save' }`
+- Form fields trigger `change` events (text inputs may need blur)
+- Network requests in browser DevTools
+
+### Issue: Too many requests
+**Solutions:**
+- Increase `DEBOUNCE_TIME`
+- Check for unnecessary event triggers
+- Verify debounce logic is working
+
+### Issue: Lost changes on navigation
+**Solutions:**
+- Add `turbo_permanent: true` to form
+- Ensure form has stable `id` attribute
+- Consider adding "unsaved changes" warning
+
+## Related Patterns
+- **Turbo Streams:** For more complex form updates and partial page replacements
+- **Stimulus Values:** If you need per-instance debounce times
+- **Form Validation:** Consider inline validation with auto-save
+
+## References
+- Stimulus Controller API: https://stimulus.hotwired.dev/
+- Turbo Permanent: https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads
diff --git a/.github/skills/frontend-patterns/SKILL.md b/.github/skills/frontend-patterns/SKILL.md
new file mode 100644
index 00000000..7c2c3084
--- /dev/null
+++ b/.github/skills/frontend-patterns/SKILL.md
@@ -0,0 +1,119 @@
+---
+name: frontend-patterns
+description: Frontend patterns for Rails applications using Slim templates, Stimulus JavaScript framework, CSS with Optics utilities. Use when building views, adding interactivity, styling components, or when the user mentions Slim, Stimulus, JavaScript, CSS, or frontend development.
+---
+
+# Frontend Patterns
+
+## Tech Stack
+- **Slim** - HTML templating
+- **Stimulus** - JavaScript interactions
+- **CSS** - Styling
+- **Optics** - CSS styling framework
+
+## Slim Templates
+
+### Conventions
+- Use Ruby 3+ syntax (e.g., keyword arguments with `:`)
+- Keep logic minimal in views
+- Extract complex rendering to helpers or partials
+- Use locals for partial data passing
+- **Always prioritize DRY principles - extract repeated markup into partials**
+- **Extract partials when logic or markup is repeated more than once**
+- **Never use inline styles**
+
+### When to Use Helpers vs Partials
+
+**Use Helper Methods when:**
+- Simple conditional logic that returns HTML with different text/classes
+- Formatting data (dates, currency, durations)
+- Generating single HTML elements with varying attributes
+- Logic is stateless and doesn't need multiple elements
+- Example: `status_badge(status)`, `format_duration(seconds)`
+
+**Use Partials when:**
+- Complex markup structure (multiple nested elements)
+- Reusable UI components with layout
+- Need to render collections
+- Significant HTML that would clutter a helper
+- Example: `_time_entry_row.html.slim`, `_timer_form.html.slim`
+
+**Rule of thumb:** If it's primarily conditional text/classes in a single element, use a helper. If it's a structure/layout, use a partial.
+
+### Partial Extraction Guidelines
+- Extract forms on the `new` and `edit` pages into `_form` partials
+- Extract repeated structures into component partials
+- Use descriptive partial names: `_time_entry_row`, `_project_selector`, `_status_badge`
+- Place partials in same directory as parent view or in `shared/` for cross-feature use
+- Always use keyword arguments for partial locals: `render 'row', time_entry:, show_actions: true`
+
+### Partial Organization
+```
+app/views/
+ time_entries/
+ edit.html.slim # Edit view
+ index.html.slim # Main view
+ new.html.slim # New view
+ show.html.slim # Show view
+ _time_entries.html.slim # Table collection of rows
+ _time_entry.html.slim # Individual row
+ _form.html.slim # Time Entries form
+ shared/
+ _status_badge.html.slim # Reusable badge
+ _empty_state.html.slim # Empty state pattern
+```
+
+### Conditional class names
+Use the rails class_names helper to manage conditional class names in Slim templates.
+```slim
+button.btn class=class_names('btn--active': active) Click Me
+```
+
+### Example
+```slim
+-# locals: (user:, active: false)
+.user-card class=('active' if active)
+ h3 = user.name
+ p = user.email
+```
+
+## Stimulus Controllers
+
+### Structure
+- One controller per behavior
+- Use data attributes for configuration
+- Keep controllers focused and composable
+- Follow naming conventions (kebab-case in HTML)
+
+### Example
+```javascript
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = ["output"]
+ static values = { url: String }
+
+ connect() {
+ // Initialization
+ }
+
+ perform() {
+ // Action logic
+ }
+}
+```
+
+## CSS & Optics
+
+### Guidelines
+- Use Optics utility classes where applicable
+- Keep custom CSS minimal and scoped
+- Follow BEM or similar naming for custom components
+- Avoid inline styles
+
+## Future Topics
+- Turbo Frames and Streams patterns
+- Form styling conventions
+- Icon helper usage
+- Responsive design patterns
+- Animation and transition guidelines
diff --git a/.github/skills/json-typed-attributes/SKILL.md b/.github/skills/json-typed-attributes/SKILL.md
new file mode 100644
index 00000000..601f3dc2
--- /dev/null
+++ b/.github/skills/json-typed-attributes/SKILL.md
@@ -0,0 +1,393 @@
+---
+name: json-typed-attributes
+description: Define typed attributes backed by JSON fields in Rails models. Use when models need flexible data storage with type casting, validations, and form integration. Supports integer, decimal, string, text, boolean, date, and array types.
+---
+
+# JSON Typed Attributes
+
+This skill helps you work with JSON-backed attributes in Rails models using the `StoreJsonAttributes` concern. It provides type casting, validation support, and seamless form integration.
+
+## When to Use
+
+- You need flexible data storage without creating separate database columns
+- You want to store structured data (like configuration, metadata, or dynamic fields) in a JSON column
+- You need proper type casting for JSON attributes (numbers, dates, booleans, arrays)
+- You want to validate JSON-backed attributes like regular ActiveRecord attributes
+- You need JSON attributes to work seamlessly with Rails forms
+
+## Setup
+
+### 1. Ensure JSON Column Exists
+
+Your model must have a JSON column to store the attributes. Common names are `data`, `metadata`, or `settings`:
+
+```ruby
+# In migration
+add_column :table_name, :data, :jsonb, default: {}
+```
+
+### 2. Include the Concern
+
+```ruby
+class YourModel < ApplicationRecord
+ include StoreJsonAttributes
+end
+```
+
+### 3. Define Typed Attributes
+
+Use `store_typed_attributes` to define attributes with automatic type casting:
+
+```ruby
+store_typed_attributes [:attribute_name], type: :type_name, field: :json_column_name
+```
+
+## Supported Types
+
+### String
+
+```ruby
+store_typed_attributes %i[timeline status], type: :string, field: :data
+```
+
+- Casts values to strings
+- Returns `nil` for blank values
+- Example usage:
+ ```ruby
+ record.timeline = "30 Days"
+ record.timeline # => "30 Days"
+ ```
+
+### Integer
+
+```ruby
+store_typed_attributes %i[age count quantity], type: :integer, field: :data
+```
+
+- Casts values to integers
+- Automatically strips commas and spaces from input ("1,000" β 1000)
+- Returns `nil` for invalid values
+- Example usage:
+ ```ruby
+ record.quantity = "1,500"
+ record.quantity # => 1500
+ ```
+
+### Decimal
+
+```ruby
+store_typed_attributes %i[price revenue percentage], type: :decimal, field: :data
+```
+
+- Casts values to BigDecimal
+- Automatically strips commas and spaces from input
+- Preserves precision
+- Example usage:
+ ```ruby
+ record.price = "1,234.56"
+ record.price # => BigDecimal("1234.56")
+ ```
+
+### Boolean
+
+```ruby
+store_typed_attributes %i[active enabled verified], type: :boolean, field: :data
+```
+
+- Creates predicate methods (ending in `?`)
+- Casts truthy/falsy values correctly
+- Example usage:
+ ```ruby
+ record.active = "1"
+ record.active? # => true
+
+ record.active = "0"
+ record.active? # => false
+ ```
+
+### Date
+
+```ruby
+store_typed_attributes %i[started_at completed_at], type: :date, field: :data
+```
+
+- Casts strings to Date objects
+- Handles various date formats
+- Example usage:
+ ```ruby
+ record.started_at = "2026-02-04"
+ record.started_at # => Date object
+ record.started_at.strftime("%B %d, %Y") # => "February 04, 2026"
+ ```
+
+### Array
+
+```ruby
+store_typed_attributes %i[categories tags], type: :array, field: :data
+```
+
+- Always returns an array (empty array if nil)
+- Automatically removes blank values with `compact_blank`
+- Example usage:
+ ```ruby
+ record.categories = ["Revenue Generation", "Operations Management"]
+ record.categories # => ["Revenue Generation", "Operations Management"]
+
+ record.categories = ["", "Valid", nil, "Another"]
+ record.categories # => ["Valid", "Another"]
+ ```
+
+### Text
+
+```ruby
+store_typed_attributes %i[notes description], type: :text, field: :data
+```
+
+- Similar to string but intended for longer content
+- Creates predicate method (ending in `?`)
+- Example usage:
+ ```ruby
+ record.notes = "Long text content..."
+ record.notes? # => true (if present)
+ ```
+
+## Complete Example
+
+```ruby
+# frozen_string_literal: true
+
+class CBPComponents::KeyQuestion < CoreBusinessPresentationComponent
+ CATEGORIES = [
+ 'Revenue Generation',
+ 'Operations Management',
+ 'Organizational Development',
+ 'Financial Management',
+ 'Ministry',
+ 'Personal Issue',
+ ].freeze
+
+ TIMELINES = ['30 Days', '90 Days', '180 Days', '1 Year', 'More than 1 Year'].freeze
+
+ # Define JSON-backed typed attributes
+ store_typed_attributes %i[timeline], type: :string, field: :data
+ store_typed_attributes %i[categories], type: :array, field: :data
+
+ # Add validations like any other attribute
+ validates :timeline, inclusion: { in: TIMELINES, allow_blank: true }
+ validates :categories, inclusion: { in: CATEGORIES }, allow_blank: true
+
+ # Use in strong parameters
+ private
+
+ def base_params
+ super.concat([:timeline, :summary, categories: []])
+ end
+end
+```
+
+## Working with Forms
+
+JSON-backed attributes work seamlessly with Rails form helpers:
+
+### Simple Fields
+
+```slim
+= form.text_field :timeline
+= form.number_field :quantity
+= form.check_box :active
+```
+
+### Array Fields (Checkboxes)
+
+```slim
+- CATEGORIES.each do |category|
+ = form.check_box :categories,
+ { multiple: true, checked: form.object.categories.include?(category) },
+ category,
+ nil
+ = category
+```
+
+### Select Fields
+
+```slim
+= form.select :timeline,
+ options_for_select(TIMELINES, form.object.timeline),
+ { include_blank: "Select timeline" }
+```
+
+## Adding Validations
+
+Validate JSON-backed attributes like regular attributes:
+
+```ruby
+# Presence
+validates :timeline, presence: true
+
+# Inclusion
+validates :timeline, inclusion: { in: TIMELINES }
+
+# Length
+validates :categories, length: { minimum: 1, message: "must select at least one" }
+
+# Custom validation
+validate :categories_must_be_valid
+
+private
+
+def categories_must_be_valid
+ invalid_categories = categories - CATEGORIES
+ if invalid_categories.any?
+ errors.add(:categories, "contains invalid categories: #{invalid_categories.join(', ')}")
+ end
+end
+
+# Numericality
+validates :quantity, numericality: { greater_than: 0, allow_nil: true }
+
+# Format
+validates :status, format: { with: /\A[A-Z][a-z]+\z/, allow_blank: true }
+```
+
+## Strong Parameters
+
+Always include JSON-backed attributes in your strong parameters:
+
+```ruby
+# For simple types (string, integer, decimal, boolean, date)
+params.require(:model).permit(:timeline, :quantity, :active, :started_at)
+
+# For arrays, use array syntax
+params.require(:model).permit(:timeline, categories: [])
+```
+
+## Multiple JSON Fields
+
+You can use different JSON fields for different concerns:
+
+```ruby
+class Product < ApplicationRecord
+ # Pricing data
+ store_typed_attributes %i[base_price discount_percentage], type: :decimal, field: :pricing_data
+
+ # Inventory data
+ store_typed_attributes %i[quantity threshold], type: :integer, field: :inventory_data
+
+ # Feature flags
+ store_typed_attributes %i[featured new_arrival on_sale], type: :boolean, field: :flags
+end
+```
+
+## Common Patterns
+
+### Constants for Validation
+
+Define constants for valid values:
+
+```ruby
+STATUSES = %w[pending approved rejected].freeze
+PRIORITIES = %w[low medium high urgent].freeze
+
+store_typed_attributes %i[status], type: :string, field: :data
+store_typed_attributes %i[priority], type: :string, field: :data
+
+validates :status, inclusion: { in: STATUSES, allow_blank: true }
+validates :priority, inclusion: { in: PRIORITIES, allow_blank: true }
+```
+
+### Default Values
+
+Set defaults in initializer or after_initialize:
+
+```ruby
+after_initialize :set_defaults, if: :new_record?
+
+private
+
+def set_defaults
+ self.categories ||= []
+ self.timeline ||= '90 Days'
+ self.active = true if active.nil?
+end
+```
+
+### Scopes and Queries
+
+Query JSON attributes using PostgreSQL JSON operators:
+
+```ruby
+# Find records with specific value
+scope :with_timeline, ->(timeline) {
+ where("data->>'timeline' = ?", timeline)
+}
+
+# Find records where array contains value
+scope :with_category, ->(category) {
+ where("data->'categories' ? :category", category: category)
+}
+
+# Find records with any of multiple values
+scope :with_any_category, ->(categories) {
+ where("data->'categories' ?| array[:categories]", categories: categories)
+}
+```
+
+## Best Practices
+
+1. **Always specify the field name** - Makes it clear where data is stored
+ ```ruby
+ store_typed_attributes %i[timeline], type: :string, field: :data
+ ```
+
+2. **Use arrays for multi-select data** - Automatically handles blank values
+ ```ruby
+ store_typed_attributes %i[categories], type: :array, field: :data
+ ```
+
+3. **Define constants for valid values** - Makes validations and forms easier
+ ```ruby
+ TIMELINES = ['30 Days', '90 Days', '180 Days'].freeze
+ validates :timeline, inclusion: { in: TIMELINES, allow_blank: true }
+ ```
+
+4. **Add validations** - JSON attributes should be validated like any other attribute
+ ```ruby
+ validates :quantity, numericality: { greater_than: 0, allow_nil: true }
+ ```
+
+5. **Use appropriate types** - Choose the type that matches your data
+ - Use `:decimal` for money/percentages (not `:integer`)
+ - Use `:array` for multi-select (automatically removes blanks)
+ - Use `:boolean` for flags (creates predicate methods)
+
+6. **Include in strong parameters** - Don't forget array syntax for array types
+ ```ruby
+ params.require(:model).permit(:timeline, categories: [])
+ ```
+
+7. **Consider indexing** - For frequently queried JSON attributes, add GIN indexes
+ ```ruby
+ add_index :table_name, :data, using: :gin
+ ```
+
+## Troubleshooting
+
+### Attribute not persisting
+- Ensure the JSON column exists in the database
+- Check that the field name matches: `field: :data`
+- Verify strong parameters include the attribute
+
+### Type casting not working
+- Verify the type is spelled correctly: `:integer`, `:decimal`, `:string`, etc.
+- For arrays, ensure you're setting an array value
+- For booleans, use the predicate method: `record.active?`
+
+### Form not displaying correct values
+- For arrays, check that you're using `multiple: true` and checking inclusion
+- For selects, use `options_for_select` with the current value
+- Ensure the getter method returns the expected type
+
+### Validation failing
+- Check that the attribute is included in strong parameters
+- Verify constants match the expected values exactly
+- For arrays, remember blank values are automatically removed
diff --git a/.github/skills/optics-context/SKILL.md b/.github/skills/optics-context/SKILL.md
new file mode 100644
index 00000000..1479ade8
--- /dev/null
+++ b/.github/skills/optics-context/SKILL.md
@@ -0,0 +1,140 @@
+---
+name: optics-context
+description: Use the Optics design framework for styling applications. Apply Optics classes for layout, spacing, typography, colors, and components. Use when working on CSS, styling views, or implementing design system guidelines.
+metadata:
+ triggers:
+ - slim
+ - css
+ - frontend
+ - design-system
+ - optics
+---
+
+## Discovering Optics Classes
+When you need classes follow these steps:
+
+1. Check `skills/optics-context/assets/components.json` for the appropriate component, modifiers, and attributes.
+ - Often you may need to modify these components to better fit the context or need. Use BEM CSS structure to create modifiers and variants as needed. Use existing tokens when available to ensure consistency.
+2. If you don't find an appropriate component, search in `app/assets/stylesheets` for any relevant components.
+3. If nothing is found, create a new component in a CSS file named after the page that we're on.
+ - If you're creating new classes, always use existing CSS tokens. You can find these in the `skills/optics-context/assets/tokens.json` file.
+
+### Modifying Optics Classes
+When modifying Optics classes, follow these guidelines:
+
+- Always ensure that changes align with the overall design system principles.
+- Follow BEM naming conventions for any new classes.
+- Add changes to the appropriate CSS file in `app/assets/stylesheets/components/overrides/{component.css}`.
+- Ensure that you import any new css files in the `application.scss`
+- Elements and modifiers should be nested under the block
+- Magic numbers should be avoided
+- Classes should have intentional-revealing names.
+
+### Fixing Optics Violations
+As CSS is created or modified, it's important to be looking for Optics violations. These would include:
+- **Hard-coded colors**
+ - `#fff`, `#FFFFFF`, `rgb(...)`, `hsl(...)`, `rgba(...)`, named colors like `white`
+ - Gradients: `linear-gradient(...)` containing literals
+- **Hard-coded spacing/sizing**
+ - `px`, `rem`, `em`, `%` (sometimes), `vh/vw` (maybe allowed depending on policy)
+ - `border-radius`, `gap`, `padding`, `margin`, `width/height` when used as spacing primitives
+- **Hard-coded shadows/borders**
+ - `box-shadow: 0 1px 3px rgba(...)`
+ - `border: 1px solid #ddd`
+- **"Almost token" mistakes**
+ - `var(--op_color_primary...)` (bad separators)
+ - `var(--op-color-primary-plus-one-on)` (segment order wrong)
+ - Missed `--op-` prefix
+
+When violations are found, refactor the CSS to use the appropriate Optics tokens from `skills/optics-context/assets/tokens.json`.
+
+If no appropriate token exists, create a new token following the guidelines below.
+
+### Creating New Optics Tokens
+When creating new Optics tokens, follow these guidelines:
+- Always ensure that new tokens align with the overall design system principles.
+- Follow the established naming conventions for tokens.
+- New tokens created within a project should use a namespace prefix related to the project to avoid conflicts. For example, a project called "Your App" might use the prefix `--ya-` for its tokens.
+- Otherwise, use the standard token format as seen in the `skills/optics-context/assets/tokens.json` file.
+- Occasionally, it may be helpful to create new global tokens to fit into the main Optics token set for your project. In such cases, ensure that the new token is broadly applicable and follows the established naming conventions. Usually, these tokens would be very general, such as new spacing sizes, color shades, or typography styles that could be reused across multiple parts of the application.
+
+## Example CSS class
+```css
+.card {
+ position: relative;
+ border-radius: var(--_op-card-radius);
+ background-color: var(--op-color-background);
+
+ /* Modifiers */
+
+ &.card--padded {
+ padding: var(--op-space-medium);
+ }
+
+
+ /* Elements */
+ .card__header,
+ .card__body,
+ .card__footer {
+ padding: var(--op-space-medium);
+ }
+
+ .card__header {
+ border-start-end-radius: var(--op-radius-medium);
+ border-start-start-radius: var(--op-radius-medium);
+ }
+
+ .card__footer {
+ border-end-end-radius: var(--op-radius-medium);
+ border-end-start-radius: var(--op-radius-medium);
+ }
+}
+```
+
+## Creating Optics Components
+
+To create a new CSS component, follow these steps:
+
+1. Create a CSS file for the new component and import it
+2. Start by defining a css `.{component-name}` selector for the component. This will serve as the base style for the component and all its variants.
+3. Create modifiers for any all variants of the component you defined in the previous step. This ensures that the base style is shared consistently across all variations.
+4. When creating variants of the component, use the following syntax: `.{component-name}--{variant}`. It can be helpful to nest these under the main class with a `&.{component-name}--{variant}` to ensure they only work with that component.
+5. For stylistic tweaks that apply to all variants, use modifiers following the BEM (Block, Element, Modifier) syntax. The modifier class should be in the format: `.{component-name}--{modifier}` just like the other variants.
+
+As a general policy, each CSS component should live in its own file unless very closely related.
+
+To illustrate these concepts, let's consider an example of a button. You can use the following template as a guide:
+
+```css
+/* Define the main component */
+.btn {
+ /* Base styles for the button */
+
+ /* Hover state */
+ &:hover {
+ /*
+ Styles for the hovered button modifier
+ ...
+ */
+ }
+
+ /* Modifier: Large button */
+ &.btn--large {
+ /* Styles for the large button modifier */
+ }
+
+ /* Modifier: Disabled button */
+ &.btn--disabled,
+ &:disabled {
+ /* Styles for the disabled button modifier */
+ }
+}
+
+/* Variant: Primary button */
+.btn.btn--primary {
+ /*
+ Specific styles for the primary button variant
+ ...
+ */
+}
+```
diff --git a/.github/skills/optics-context/assets/components.json b/.github/skills/optics-context/assets/components.json
new file mode 100644
index 00000000..931e2e5f
--- /dev/null
+++ b/.github/skills/optics-context/assets/components.json
@@ -0,0 +1,502 @@
+{
+ "optics_component_classes": {
+ "meta": {
+ "description": "Complete list of Optics Design System CSS component classes. This does not include app specific overrides or app components.",
+ "source_files": [
+ "node_modules/@rolemodel/optics/dist/css/components/accordion.css",
+ "node_modules/@rolemodel/optics/dist/css/components/alert.css",
+ "node_modules/@rolemodel/optics/dist/css/components/avatar.css",
+ "node_modules/@rolemodel/optics/dist/css/components/badge.css",
+ "node_modules/@rolemodel/optics/dist/css/components/breadcrumbs.css",
+ "node_modules/@rolemodel/optics/dist/css/components/button.css",
+ "node_modules/@rolemodel/optics/dist/css/components/button_group.css",
+ "node_modules/@rolemodel/optics/dist/css/components/card.css",
+ "node_modules/@rolemodel/optics/dist/css/components/confirm-dialog.css",
+ "node_modules/@rolemodel/optics/dist/css/components/content-header.css",
+ "node_modules/@rolemodel/optics/dist/css/components/divider.css",
+ "node_modules/@rolemodel/optics/dist/css/components/form.css",
+ "node_modules/@rolemodel/optics/dist/css/components/icon.css",
+ "node_modules/@rolemodel/optics/dist/css/components/modal.css",
+ "node_modules/@rolemodel/optics/dist/css/components/navbar.css",
+ "node_modules/@rolemodel/optics/dist/css/components/pagination.css",
+ "node_modules/@rolemodel/optics/dist/css/components/segmented-control.css",
+ "node_modules/@rolemodel/optics/dist/css/components/side_panel.css",
+ "node_modules/@rolemodel/optics/dist/css/components/sidebar.css",
+ "node_modules/@rolemodel/optics/dist/css/components/spinner.css",
+ "node_modules/@rolemodel/optics/dist/css/components/switch.css",
+ "node_modules/@rolemodel/optics/dist/css/components/tab.css",
+ "node_modules/@rolemodel/optics/dist/css/components/table.css",
+ "node_modules/@rolemodel/optics/dist/css/components/tag.css",
+ "node_modules/@rolemodel/optics/dist/css/components/text_pair.css",
+ "node_modules/@rolemodel/optics/dist/css/components/tooltip.css"
+ ],
+ "total_components": 26,
+ "total_classes": 253,
+ "components": {
+ "accordion": 4,
+ "alert": 13,
+ "avatar": 5,
+ "badge": 10,
+ "breadcrumbs": 5,
+ "button": 15,
+ "button_group": 4,
+ "card": 11,
+ "confirm_dialog": 7,
+ "content_header": 6,
+ "divider": 8,
+ "form": 14,
+ "icon": 16,
+ "modal": 8,
+ "navbar": 10,
+ "pagination": 5,
+ "segmented_control": 8,
+ "side_panel": 31,
+ "sidebar": 15,
+ "spinner": 5,
+ "switch": 3,
+ "tab": 6,
+ "table": 13,
+ "tag": 9,
+ "text_pair": 16,
+ "tooltip": 6
+ },
+ "notes": {
+ "base_classes": "Primary component classes that define the core component styling",
+ "modifiers": "Modifier classes that change the appearance or behavior of components",
+ "elements": "Sub-element classes that style specific parts within a component",
+ "attributes": "Data attributes used for component functionality (like tooltips)"
+ }
+ },
+ "accordion": {
+ "base": [
+ "accordion"
+ ],
+ "modifiers": [
+ "accordion--disable-animation"
+ ],
+ "elements": [
+ "accordion__label",
+ "accordion__marker"
+ ]
+ },
+ "alert": {
+ "base": [
+ "alert"
+ ],
+ "modifiers": [
+ "alert--alert",
+ "alert--danger",
+ "alert--filled",
+ "alert--flash",
+ "alert--info",
+ "alert--muted",
+ "alert--notice",
+ "alert--warning"
+ ],
+ "elements": [
+ "alert__description",
+ "alert__icon",
+ "alert__messages",
+ "alert__title"
+ ]
+ },
+ "avatar": {
+ "base": [
+ "avatar"
+ ],
+ "modifiers": [
+ "avatar--disabled",
+ "avatar--large",
+ "avatar--medium",
+ "avatar--small"
+ ]
+ },
+ "badge": {
+ "base": [
+ "badge",
+ "material-symbols-outlined"
+ ],
+ "modifiers": [
+ "badge--danger",
+ "badge--info",
+ "badge--notice",
+ "badge--notification-left",
+ "badge--notification-right",
+ "badge--pill",
+ "badge--primary",
+ "badge--warning"
+ ]
+ },
+ "breadcrumbs": {
+ "base": [
+ "breadcrumbs"
+ ],
+ "modifiers": [
+ "breadcrumbs--large",
+ "breadcrumbs--small"
+ ],
+ "elements": [
+ "breadcrumbs__link",
+ "breadcrumbs__text"
+ ]
+ },
+ "button": {
+ "base": [
+ "btn"
+ ],
+ "modifiers": [
+ "btn--active",
+ "btn--delete",
+ "btn--destructive",
+ "btn--disabled",
+ "btn--icon",
+ "btn--icon-with-label",
+ "btn--large",
+ "btn--medium",
+ "btn--no-border",
+ "btn--pill",
+ "btn--primary",
+ "btn--small",
+ "btn--warning",
+ "btn--with-badge"
+ ]
+ },
+ "button_group": {
+ "base": [
+ "btn",
+ "btn-group",
+ "btn-group-toolbar"
+ ],
+ "modifiers": [
+ "btn--active"
+ ]
+ },
+ "card": {
+ "base": [
+ "card"
+ ],
+ "modifiers": [
+ "card--condensed",
+ "card--padded",
+ "card--shadow-large",
+ "card--shadow-medium",
+ "card--shadow-small",
+ "card--shadow-x-large",
+ "card--shadow-x-small"
+ ],
+ "elements": [
+ "card__body",
+ "card__footer",
+ "card__header"
+ ]
+ },
+ "confirm_dialog": {
+ "base": [
+ "confirm-dialog",
+ "confirm-dialog-wrapper"
+ ],
+ "modifiers": [
+ "confirm-dialog-wrapper--active"
+ ],
+ "elements": [
+ "confirm-dialog-wrapper__backdrop",
+ "confirm-dialog__body",
+ "confirm-dialog__footer",
+ "confirm-dialog__header"
+ ]
+ },
+ "content_header": {
+ "base": [
+ "content-header"
+ ],
+ "elements": [
+ "content-header__aside",
+ "content-header__context",
+ "content-header__details",
+ "content-header__subline",
+ "content-header__title"
+ ]
+ },
+ "divider": {
+ "base": [
+ "divider"
+ ],
+ "modifiers": [
+ "divider--large",
+ "divider--medium",
+ "divider--small",
+ "divider--spacing-large",
+ "divider--spacing-medium",
+ "divider--spacing-small",
+ "divider--vertical"
+ ]
+ },
+ "form": {
+ "base": [
+ "form-control",
+ "form-error",
+ "form-error-summary",
+ "form-group",
+ "form-hint",
+ "form-label",
+ "segmented-control",
+ "switch"
+ ],
+ "modifiers": [
+ "form-control--large",
+ "form-control--medium",
+ "form-control--no-border",
+ "form-control--small",
+ "form-group--error",
+ "form-group--inline"
+ ]
+ },
+ "icon": {
+ "base": [
+ "custom-icons",
+ "icon",
+ "material-symbols-outlined"
+ ],
+ "modifiers": [
+ "icon--filled",
+ "icon--high-emphasis",
+ "icon--large",
+ "icon--low-emphasis",
+ "icon--medium",
+ "icon--normal-emphasis",
+ "icon--outlined",
+ "icon--small",
+ "icon--weight-bold",
+ "icon--weight-light",
+ "icon--weight-normal",
+ "icon--weight-semi-bold",
+ "icon--x-large"
+ ]
+ },
+ "modal": {
+ "base": [
+ "modal",
+ "modal-wrapper"
+ ],
+ "modifiers": [
+ "modal--closing",
+ "modal-wrapper--active"
+ ],
+ "elements": [
+ "modal-wrapper__backdrop",
+ "modal__body",
+ "modal__footer",
+ "modal__header"
+ ]
+ },
+ "navbar": {
+ "base": [
+ "navbar"
+ ],
+ "modifiers": [
+ "navbar--primary",
+ "navbar__content--justify-center",
+ "navbar__content--justify-end",
+ "navbar__content--justify-start"
+ ],
+ "elements": [
+ "navbar__brand",
+ "navbar__content",
+ "navbar__content--justify-center",
+ "navbar__content--justify-end",
+ "navbar__content--justify-start"
+ ]
+ },
+ "pagination": {
+ "base": [
+ "form-control",
+ "form-group",
+ "form-label",
+ "pagination"
+ ],
+ "elements": [
+ "pagination__divider"
+ ]
+ },
+ "segmented_control": {
+ "base": [
+ "icon",
+ "segmented-control"
+ ],
+ "modifiers": [
+ "segmented-control--full-width",
+ "segmented-control--large",
+ "segmented-control--medium",
+ "segmented-control--small"
+ ],
+ "elements": [
+ "segmented-control__input",
+ "segmented-control__label"
+ ]
+ },
+ "side_panel": {
+ "base": [
+ "side-panel"
+ ],
+ "modifiers": [
+ "side-panel--border-left",
+ "side-panel--border-right",
+ "side-panel__body--padded",
+ "side-panel__body--padded-x",
+ "side-panel__body--padded-y",
+ "side-panel__footer--padded",
+ "side-panel__footer--padded-x",
+ "side-panel__footer--padded-y",
+ "side-panel__header--padded",
+ "side-panel__header--padded-x",
+ "side-panel__header--padded-y",
+ "side-panel__section--padded",
+ "side-panel__section--padded-x",
+ "side-panel__section--padded-y"
+ ],
+ "elements": [
+ "side-panel__body",
+ "side-panel__body--padded",
+ "side-panel__body--padded-x",
+ "side-panel__body--padded-y",
+ "side-panel__footer",
+ "side-panel__footer--padded",
+ "side-panel__footer--padded-x",
+ "side-panel__footer--padded-y",
+ "side-panel__header",
+ "side-panel__header--padded",
+ "side-panel__header--padded-x",
+ "side-panel__header--padded-y",
+ "side-panel__section",
+ "side-panel__section--padded",
+ "side-panel__section--padded-x",
+ "side-panel__section--padded-y"
+ ]
+ },
+ "sidebar": {
+ "base": [
+ "material-symbols-outlined",
+ "sidebar"
+ ],
+ "modifiers": [
+ "sidebar--compact",
+ "sidebar--drawer",
+ "sidebar--padded",
+ "sidebar--primary",
+ "sidebar--rail",
+ "sidebar__content--center",
+ "sidebar__content--end",
+ "sidebar__content--start"
+ ],
+ "elements": [
+ "sidebar__brand",
+ "sidebar__content",
+ "sidebar__content--center",
+ "sidebar__content--end",
+ "sidebar__content--start"
+ ]
+ },
+ "spinner": {
+ "base": [
+ "spinner"
+ ],
+ "modifiers": [
+ "spinner--large",
+ "spinner--medium",
+ "spinner--small",
+ "spinner--x-small"
+ ]
+ },
+ "switch": {
+ "base": [
+ "switch"
+ ],
+ "modifiers": [
+ "switch--large",
+ "switch--small"
+ ]
+ },
+ "tab": {
+ "base": [
+ "tab",
+ "tab-group"
+ ],
+ "modifiers": [
+ "tab--active",
+ "tab--disabled",
+ "tab--large",
+ "tab--small"
+ ]
+ },
+ "table": {
+ "base": [
+ "table"
+ ],
+ "modifiers": [
+ "table--auto-layout",
+ "table--comfortable-density",
+ "table--compact-density",
+ "table--container",
+ "table--danger",
+ "table--default-density",
+ "table--even-striped",
+ "table--fixed-layout",
+ "table--odd-striped",
+ "table--primary",
+ "table--sticky-footer",
+ "table--sticky-header"
+ ]
+ },
+ "tag": {
+ "base": [
+ "tag"
+ ],
+ "modifiers": [
+ "btn--icon",
+ "tag--danger",
+ "tag--info",
+ "tag--notice",
+ "tag--primary",
+ "tag--read-only",
+ "tag--warning"
+ ],
+ "elements": [
+ "tag__label"
+ ]
+ },
+ "text_pair": {
+ "base": [
+ "text-pair"
+ ],
+ "modifiers": [
+ "text-pair--inline",
+ "text-pair__subtitle--large",
+ "text-pair__subtitle--medium",
+ "text-pair__subtitle--small",
+ "text-pair__title--large",
+ "text-pair__title--medium",
+ "text-pair__title--small"
+ ],
+ "elements": [
+ "text-pair__subtitle",
+ "text-pair__subtitle--large",
+ "text-pair__subtitle--medium",
+ "text-pair__subtitle--small",
+ "text-pair__title",
+ "text-pair__title--large",
+ "text-pair__title--medium",
+ "text-pair__title--small"
+ ]
+ },
+ "tooltip": {
+ "attributes": [
+ "[data-tooltip-position='bottom']",
+ "[data-tooltip-position='left']",
+ "[data-tooltip-position='right']",
+ "[data-tooltip-position='top']",
+ "[data-tooltip-position]",
+ "[data-tooltip-text]"
+ ]
+ }
+ }
+}
diff --git a/.github/skills/optics-context/assets/tokens.json b/.github/skills/optics-context/assets/tokens.json
new file mode 100644
index 00000000..5ef8658c
--- /dev/null
+++ b/.github/skills/optics-context/assets/tokens.json
@@ -0,0 +1,614 @@
+{
+ "optics_design_tokens": {
+ "meta": {
+ "description": "Complete list of Optics Design System CSS custom properties (tokens)",
+ "total_tokens": 471,
+ "categories": {
+ "colors": 337,
+ "typography": 25,
+ "spacing": 11,
+ "borders": 17,
+ "shadows": 5,
+ "opacity": 5,
+ "breakpoints": 5,
+ "inputs": 12,
+ "transitions": 7,
+ "animations": 1,
+ "z_index": 9,
+ "encoded_images": 2,
+ "semantic_colors": 4
+ },
+ "color_scale_notes": {
+ "semantic_scales": [
+ "Plus values add luminosity, minus values remove luminosity",
+ "plus_max is the lightest, minus_max is the darkest"
+ ],
+ "on_colors": "Colors designed to be used ON the corresponding color scale (text on background)",
+ "alt_variants": "Alternative color options for enhanced contrast and accessibility"
+ }
+ },
+ "colors": {
+ "basic": [
+ "--op-color-white",
+ "--op-color-black"
+ ],
+ "primary": {
+ "base": [
+ "--op-color-primary-h",
+ "--op-color-primary-s",
+ "--op-color-primary-l",
+ "--op-color-primary-original"
+ ],
+ "scale": [
+ "--op-color-primary-plus-max",
+ "--op-color-primary-plus-eight",
+ "--op-color-primary-plus-seven",
+ "--op-color-primary-plus-six",
+ "--op-color-primary-plus-five",
+ "--op-color-primary-plus-four",
+ "--op-color-primary-plus-three",
+ "--op-color-primary-plus-two",
+ "--op-color-primary-plus-one",
+ "--op-color-primary-base",
+ "--op-color-primary-minus-one",
+ "--op-color-primary-minus-two",
+ "--op-color-primary-minus-three",
+ "--op-color-primary-minus-four",
+ "--op-color-primary-minus-five",
+ "--op-color-primary-minus-six",
+ "--op-color-primary-minus-seven",
+ "--op-color-primary-minus-eight",
+ "--op-color-primary-minus-max"
+ ],
+ "on_scale": [
+ "--op-color-primary-on-plus-max",
+ "--op-color-primary-on-plus-max-alt",
+ "--op-color-primary-on-plus-eight",
+ "--op-color-primary-on-plus-eight-alt",
+ "--op-color-primary-on-plus-seven",
+ "--op-color-primary-on-plus-seven-alt",
+ "--op-color-primary-on-plus-six",
+ "--op-color-primary-on-plus-six-alt",
+ "--op-color-primary-on-plus-five",
+ "--op-color-primary-on-plus-five-alt",
+ "--op-color-primary-on-plus-four",
+ "--op-color-primary-on-plus-four-alt",
+ "--op-color-primary-on-plus-three",
+ "--op-color-primary-on-plus-three-alt",
+ "--op-color-primary-on-plus-two",
+ "--op-color-primary-on-plus-two-alt",
+ "--op-color-primary-on-plus-one",
+ "--op-color-primary-on-plus-one-alt",
+ "--op-color-primary-on-base",
+ "--op-color-primary-on-base-alt",
+ "--op-color-primary-on-minus-one",
+ "--op-color-primary-on-minus-one-alt",
+ "--op-color-primary-on-minus-two",
+ "--op-color-primary-on-minus-two-alt",
+ "--op-color-primary-on-minus-three",
+ "--op-color-primary-on-minus-three-alt",
+ "--op-color-primary-on-minus-four",
+ "--op-color-primary-on-minus-four-alt",
+ "--op-color-primary-on-minus-five",
+ "--op-color-primary-on-minus-five-alt",
+ "--op-color-primary-on-minus-six",
+ "--op-color-primary-on-minus-six-alt",
+ "--op-color-primary-on-minus-seven",
+ "--op-color-primary-on-minus-seven-alt",
+ "--op-color-primary-on-minus-eight",
+ "--op-color-primary-on-minus-eight-alt",
+ "--op-color-primary-on-minus-max",
+ "--op-color-primary-on-minus-max-alt"
+ ]
+ },
+ "neutral": {
+ "base": [
+ "--op-color-neutral-h",
+ "--op-color-neutral-s",
+ "--op-color-neutral-l",
+ "--op-color-neutral-original"
+ ],
+ "scale": [
+ "--op-color-neutral-plus-max",
+ "--op-color-neutral-plus-eight",
+ "--op-color-neutral-plus-seven",
+ "--op-color-neutral-plus-six",
+ "--op-color-neutral-plus-five",
+ "--op-color-neutral-plus-four",
+ "--op-color-neutral-plus-three",
+ "--op-color-neutral-plus-two",
+ "--op-color-neutral-plus-one",
+ "--op-color-neutral-base",
+ "--op-color-neutral-minus-one",
+ "--op-color-neutral-minus-two",
+ "--op-color-neutral-minus-three",
+ "--op-color-neutral-minus-four",
+ "--op-color-neutral-minus-five",
+ "--op-color-neutral-minus-six",
+ "--op-color-neutral-minus-seven",
+ "--op-color-neutral-minus-eight",
+ "--op-color-neutral-minus-max"
+ ],
+ "on_scale": [
+ "--op-color-neutral-on-plus-max",
+ "--op-color-neutral-on-plus-max-alt",
+ "--op-color-neutral-on-plus-eight",
+ "--op-color-neutral-on-plus-eight-alt",
+ "--op-color-neutral-on-plus-seven",
+ "--op-color-neutral-on-plus-seven-alt",
+ "--op-color-neutral-on-plus-six",
+ "--op-color-neutral-on-plus-six-alt",
+ "--op-color-neutral-on-plus-five",
+ "--op-color-neutral-on-plus-five-alt",
+ "--op-color-neutral-on-plus-four",
+ "--op-color-neutral-on-plus-four-alt",
+ "--op-color-neutral-on-plus-three",
+ "--op-color-neutral-on-plus-three-alt",
+ "--op-color-neutral-on-plus-two",
+ "--op-color-neutral-on-plus-two-alt",
+ "--op-color-neutral-on-plus-one",
+ "--op-color-neutral-on-plus-one-alt",
+ "--op-color-neutral-on-base",
+ "--op-color-neutral-on-base-alt",
+ "--op-color-neutral-on-minus-one",
+ "--op-color-neutral-on-minus-one-alt",
+ "--op-color-neutral-on-minus-two",
+ "--op-color-neutral-on-minus-two-alt",
+ "--op-color-neutral-on-minus-three",
+ "--op-color-neutral-on-minus-three-alt",
+ "--op-color-neutral-on-minus-four",
+ "--op-color-neutral-on-minus-four-alt",
+ "--op-color-neutral-on-minus-five",
+ "--op-color-neutral-on-minus-five-alt",
+ "--op-color-neutral-on-minus-six",
+ "--op-color-neutral-on-minus-six-alt",
+ "--op-color-neutral-on-minus-seven",
+ "--op-color-neutral-on-minus-seven-alt",
+ "--op-color-neutral-on-minus-eight",
+ "--op-color-neutral-on-minus-eight-alt",
+ "--op-color-neutral-on-minus-max",
+ "--op-color-neutral-on-minus-max-alt"
+ ]
+ },
+ "alerts": {
+ "warning": {
+ "base": [
+ "--op-color-alerts-warning-h",
+ "--op-color-alerts-warning-s",
+ "--op-color-alerts-warning-l",
+ "--op-color-alerts-warning-original"
+ ],
+ "scale": [
+ "--op-color-alerts-warning-plus-max",
+ "--op-color-alerts-warning-plus-eight",
+ "--op-color-alerts-warning-plus-seven",
+ "--op-color-alerts-warning-plus-six",
+ "--op-color-alerts-warning-plus-five",
+ "--op-color-alerts-warning-plus-four",
+ "--op-color-alerts-warning-plus-three",
+ "--op-color-alerts-warning-plus-two",
+ "--op-color-alerts-warning-plus-one",
+ "--op-color-alerts-warning-base",
+ "--op-color-alerts-warning-minus-one",
+ "--op-color-alerts-warning-minus-two",
+ "--op-color-alerts-warning-minus-three",
+ "--op-color-alerts-warning-minus-four",
+ "--op-color-alerts-warning-minus-five",
+ "--op-color-alerts-warning-minus-six",
+ "--op-color-alerts-warning-minus-seven",
+ "--op-color-alerts-warning-minus-eight",
+ "--op-color-alerts-warning-minus-max"
+ ],
+ "on_scale": [
+ "--op-color-alerts-warning-on-plus-max",
+ "--op-color-alerts-warning-on-plus-max-alt",
+ "--op-color-alerts-warning-on-plus-eight",
+ "--op-color-alerts-warning-on-plus-eight-alt",
+ "--op-color-alerts-warning-on-plus-seven",
+ "--op-color-alerts-warning-on-plus-seven-alt",
+ "--op-color-alerts-warning-on-plus-six",
+ "--op-color-alerts-warning-on-plus-six-alt",
+ "--op-color-alerts-warning-on-plus-five",
+ "--op-color-alerts-warning-on-plus-five-alt",
+ "--op-color-alerts-warning-on-plus-four",
+ "--op-color-alerts-warning-on-plus-four-alt",
+ "--op-color-alerts-warning-on-plus-three",
+ "--op-color-alerts-warning-on-plus-three-alt",
+ "--op-color-alerts-warning-on-plus-two",
+ "--op-color-alerts-warning-on-plus-two-alt",
+ "--op-color-alerts-warning-on-plus-one",
+ "--op-color-alerts-warning-on-plus-one-alt",
+ "--op-color-alerts-warning-on-base",
+ "--op-color-alerts-warning-on-base-alt",
+ "--op-color-alerts-warning-on-minus-one",
+ "--op-color-alerts-warning-on-minus-one-alt",
+ "--op-color-alerts-warning-on-minus-two",
+ "--op-color-alerts-warning-on-minus-two-alt",
+ "--op-color-alerts-warning-on-minus-three",
+ "--op-color-alerts-warning-on-minus-three-alt",
+ "--op-color-alerts-warning-on-minus-four",
+ "--op-color-alerts-warning-on-minus-four-alt",
+ "--op-color-alerts-warning-on-minus-five",
+ "--op-color-alerts-warning-on-minus-five-alt",
+ "--op-color-alerts-warning-on-minus-six",
+ "--op-color-alerts-warning-on-minus-six-alt",
+ "--op-color-alerts-warning-on-minus-seven",
+ "--op-color-alerts-warning-on-minus-seven-alt",
+ "--op-color-alerts-warning-on-minus-eight",
+ "--op-color-alerts-warning-on-minus-eight-alt",
+ "--op-color-alerts-warning-on-minus-max",
+ "--op-color-alerts-warning-on-minus-max-alt"
+ ]
+ },
+ "danger": {
+ "base": [
+ "--op-color-alerts-danger-h",
+ "--op-color-alerts-danger-s",
+ "--op-color-alerts-danger-l",
+ "--op-color-alerts-danger-original"
+ ],
+ "scale": [
+ "--op-color-alerts-danger-plus-max",
+ "--op-color-alerts-danger-plus-eight",
+ "--op-color-alerts-danger-plus-seven",
+ "--op-color-alerts-danger-plus-six",
+ "--op-color-alerts-danger-plus-five",
+ "--op-color-alerts-danger-plus-four",
+ "--op-color-alerts-danger-plus-three",
+ "--op-color-alerts-danger-plus-two",
+ "--op-color-alerts-danger-plus-one",
+ "--op-color-alerts-danger-base",
+ "--op-color-alerts-danger-minus-one",
+ "--op-color-alerts-danger-minus-two",
+ "--op-color-alerts-danger-minus-three",
+ "--op-color-alerts-danger-minus-four",
+ "--op-color-alerts-danger-minus-five",
+ "--op-color-alerts-danger-minus-six",
+ "--op-color-alerts-danger-minus-seven",
+ "--op-color-alerts-danger-minus-eight",
+ "--op-color-alerts-danger-minus-max"
+ ],
+ "on_scale": [
+ "--op-color-alerts-danger-on-plus-max",
+ "--op-color-alerts-danger-on-plus-max-alt",
+ "--op-color-alerts-danger-on-plus-eight",
+ "--op-color-alerts-danger-on-plus-eight-alt",
+ "--op-color-alerts-danger-on-plus-seven",
+ "--op-color-alerts-danger-on-plus-seven-alt",
+ "--op-color-alerts-danger-on-plus-six",
+ "--op-color-alerts-danger-on-plus-six-alt",
+ "--op-color-alerts-danger-on-plus-five",
+ "--op-color-alerts-danger-on-plus-five-alt",
+ "--op-color-alerts-danger-on-plus-four",
+ "--op-color-alerts-danger-on-plus-four-alt",
+ "--op-color-alerts-danger-on-plus-three",
+ "--op-color-alerts-danger-on-plus-three-alt",
+ "--op-color-alerts-danger-on-plus-two",
+ "--op-color-alerts-danger-on-plus-two-alt",
+ "--op-color-alerts-danger-on-plus-one",
+ "--op-color-alerts-danger-on-plus-one-alt",
+ "--op-color-alerts-danger-on-base",
+ "--op-color-alerts-danger-on-base-alt",
+ "--op-color-alerts-danger-on-minus-one",
+ "--op-color-alerts-danger-on-minus-one-alt",
+ "--op-color-alerts-danger-on-minus-two",
+ "--op-color-alerts-danger-on-minus-two-alt",
+ "--op-color-alerts-danger-on-minus-three",
+ "--op-color-alerts-danger-on-minus-three-alt",
+ "--op-color-alerts-danger-on-minus-four",
+ "--op-color-alerts-danger-on-minus-four-alt",
+ "--op-color-alerts-danger-on-minus-five",
+ "--op-color-alerts-danger-on-minus-five-alt",
+ "--op-color-alerts-danger-on-minus-six",
+ "--op-color-alerts-danger-on-minus-six-alt",
+ "--op-color-alerts-danger-on-minus-seven",
+ "--op-color-alerts-danger-on-minus-seven-alt",
+ "--op-color-alerts-danger-on-minus-eight",
+ "--op-color-alerts-danger-on-minus-eight-alt",
+ "--op-color-alerts-danger-on-minus-max",
+ "--op-color-alerts-danger-on-minus-max-alt"
+ ]
+ },
+ "info": {
+ "base": [
+ "--op-color-alerts-info-h",
+ "--op-color-alerts-info-s",
+ "--op-color-alerts-info-l",
+ "--op-color-alerts-info-original"
+ ],
+ "scale": [
+ "--op-color-alerts-info-plus-max",
+ "--op-color-alerts-info-plus-eight",
+ "--op-color-alerts-info-plus-seven",
+ "--op-color-alerts-info-plus-six",
+ "--op-color-alerts-info-plus-five",
+ "--op-color-alerts-info-plus-four",
+ "--op-color-alerts-info-plus-three",
+ "--op-color-alerts-info-plus-two",
+ "--op-color-alerts-info-plus-one",
+ "--op-color-alerts-info-base",
+ "--op-color-alerts-info-minus-one",
+ "--op-color-alerts-info-minus-two",
+ "--op-color-alerts-info-minus-three",
+ "--op-color-alerts-info-minus-four",
+ "--op-color-alerts-info-minus-five",
+ "--op-color-alerts-info-minus-six",
+ "--op-color-alerts-info-minus-seven",
+ "--op-color-alerts-info-minus-eight",
+ "--op-color-alerts-info-minus-max"
+ ],
+ "on_scale": [
+ "--op-color-alerts-info-on-plus-max",
+ "--op-color-alerts-info-on-plus-max-alt",
+ "--op-color-alerts-info-on-plus-eight",
+ "--op-color-alerts-info-on-plus-eight-alt",
+ "--op-color-alerts-info-on-plus-seven",
+ "--op-color-alerts-info-on-plus-seven-alt",
+ "--op-color-alerts-info-on-plus-six",
+ "--op-color-alerts-info-on-plus-six-alt",
+ "--op-color-alerts-info-on-plus-five",
+ "--op-color-alerts-info-on-plus-five-alt",
+ "--op-color-alerts-info-on-plus-four",
+ "--op-color-alerts-info-on-plus-four-alt",
+ "--op-color-alerts-info-on-plus-three",
+ "--op-color-alerts-info-on-plus-three-alt",
+ "--op-color-alerts-info-on-plus-two",
+ "--op-color-alerts-info-on-plus-two-alt",
+ "--op-color-alerts-info-on-plus-one",
+ "--op-color-alerts-info-on-plus-one-alt",
+ "--op-color-alerts-info-on-base",
+ "--op-color-alerts-info-on-base-alt",
+ "--op-color-alerts-info-on-minus-one",
+ "--op-color-alerts-info-on-minus-one-alt",
+ "--op-color-alerts-info-on-minus-two",
+ "--op-color-alerts-info-on-minus-two-alt",
+ "--op-color-alerts-info-on-minus-three",
+ "--op-color-alerts-info-on-minus-three-alt",
+ "--op-color-alerts-info-on-minus-four",
+ "--op-color-alerts-info-on-minus-four-alt",
+ "--op-color-alerts-info-on-minus-five",
+ "--op-color-alerts-info-on-minus-five-alt",
+ "--op-color-alerts-info-on-minus-six",
+ "--op-color-alerts-info-on-minus-six-alt",
+ "--op-color-alerts-info-on-minus-seven",
+ "--op-color-alerts-info-on-minus-seven-alt",
+ "--op-color-alerts-info-on-minus-eight",
+ "--op-color-alerts-info-on-minus-eight-alt",
+ "--op-color-alerts-info-on-minus-max",
+ "--op-color-alerts-info-on-minus-max-alt"
+ ]
+ },
+ "notice": {
+ "base": [
+ "--op-color-alerts-notice-h",
+ "--op-color-alerts-notice-s",
+ "--op-color-alerts-notice-l",
+ "--op-color-alerts-notice-original"
+ ],
+ "scale": [
+ "--op-color-alerts-notice-plus-max",
+ "--op-color-alerts-notice-plus-eight",
+ "--op-color-alerts-notice-plus-seven",
+ "--op-color-alerts-notice-plus-six",
+ "--op-color-alerts-notice-plus-five",
+ "--op-color-alerts-notice-plus-four",
+ "--op-color-alerts-notice-plus-three",
+ "--op-color-alerts-notice-plus-two",
+ "--op-color-alerts-notice-plus-one",
+ "--op-color-alerts-notice-base",
+ "--op-color-alerts-notice-minus-one",
+ "--op-color-alerts-notice-minus-two",
+ "--op-color-alerts-notice-minus-three",
+ "--op-color-alerts-notice-minus-four",
+ "--op-color-alerts-notice-minus-five",
+ "--op-color-alerts-notice-minus-six",
+ "--op-color-alerts-notice-minus-seven",
+ "--op-color-alerts-notice-minus-eight",
+ "--op-color-alerts-notice-minus-max"
+ ],
+ "on_scale": [
+ "--op-color-alerts-notice-on-plus-max",
+ "--op-color-alerts-notice-on-plus-max-alt",
+ "--op-color-alerts-notice-on-plus-eight",
+ "--op-color-alerts-notice-on-plus-eight-alt",
+ "--op-color-alerts-notice-on-plus-seven",
+ "--op-color-alerts-notice-on-plus-seven-alt",
+ "--op-color-alerts-notice-on-plus-six",
+ "--op-color-alerts-notice-on-plus-six-alt",
+ "--op-color-alerts-notice-on-plus-five",
+ "--op-color-alerts-notice-on-plus-five-alt",
+ "--op-color-alerts-notice-on-plus-four",
+ "--op-color-alerts-notice-on-plus-four-alt",
+ "--op-color-alerts-notice-on-plus-three",
+ "--op-color-alerts-notice-on-plus-three-alt",
+ "--op-color-alerts-notice-on-plus-two",
+ "--op-color-alerts-notice-on-plus-two-alt",
+ "--op-color-alerts-notice-on-plus-one",
+ "--op-color-alerts-notice-on-plus-one-alt",
+ "--op-color-alerts-notice-on-base",
+ "--op-color-alerts-notice-on-base-alt",
+ "--op-color-alerts-notice-on-minus-one",
+ "--op-color-alerts-notice-on-minus-one-alt",
+ "--op-color-alerts-notice-on-minus-two",
+ "--op-color-alerts-notice-on-minus-two-alt",
+ "--op-color-alerts-notice-on-minus-three",
+ "--op-color-alerts-notice-on-minus-three-alt",
+ "--op-color-alerts-notice-on-minus-four",
+ "--op-color-alerts-notice-on-minus-four-alt",
+ "--op-color-alerts-notice-on-minus-five",
+ "--op-color-alerts-notice-on-minus-five-alt",
+ "--op-color-alerts-notice-on-minus-six",
+ "--op-color-alerts-notice-on-minus-six-alt",
+ "--op-color-alerts-notice-on-minus-seven",
+ "--op-color-alerts-notice-on-minus-seven-alt",
+ "--op-color-alerts-notice-on-minus-eight",
+ "--op-color-alerts-notice-on-minus-eight-alt",
+ "--op-color-alerts-notice-on-minus-max",
+ "--op-color-alerts-notice-on-minus-max-alt"
+ ]
+ }
+ },
+ "semantic": [
+ "--op-color-background",
+ "--op-color-on-background",
+ "--op-color-on-background-alt",
+ "--op-color-border"
+ ]
+ },
+ "typography": {
+ "font_family": [
+ "--op-font-family",
+ "--op-font-family-alt"
+ ],
+ "font_size": [
+ "--op-font-scale-unit",
+ "--op-font-2x-small",
+ "--op-font-x-small",
+ "--op-font-small",
+ "--op-font-medium",
+ "--op-font-large",
+ "--op-font-x-large",
+ "--op-font-2x-large",
+ "--op-font-3x-large",
+ "--op-font-4x-large",
+ "--op-font-5x-large",
+ "--op-font-6x-large"
+ ],
+ "font_weight": [
+ "--op-font-weight-thin",
+ "--op-font-weight-extra-light",
+ "--op-font-weight-light",
+ "--op-font-weight-normal",
+ "--op-font-weight-medium",
+ "--op-font-weight-semi-bold",
+ "--op-font-weight-bold",
+ "--op-font-weight-extra-bold",
+ "--op-font-weight-black"
+ ],
+ "line_height": [
+ "--op-line-height-none",
+ "--op-line-height-densest",
+ "--op-line-height-denser",
+ "--op-line-height-dense",
+ "--op-line-height-base",
+ "--op-line-height-loose",
+ "--op-line-height-looser",
+ "--op-line-height-loosest"
+ ],
+ "letter_spacing": [
+ "--op-letter-spacing-navigation",
+ "--op-letter-spacing-label"
+ ]
+ },
+ "spacing": {
+ "size_unit": [
+ "--op-size-unit"
+ ],
+ "scale": [
+ "--op-space-scale-unit",
+ "--op-space-3x-small",
+ "--op-space-2x-small",
+ "--op-space-x-small",
+ "--op-space-small",
+ "--op-space-medium",
+ "--op-space-large",
+ "--op-space-x-large",
+ "--op-space-2x-large",
+ "--op-space-3x-large",
+ "--op-space-4x-large"
+ ]
+ },
+ "borders": {
+ "radius": [
+ "--op-radius-small",
+ "--op-radius-medium",
+ "--op-radius-large",
+ "--op-radius-x-large",
+ "--op-radius-2x-large",
+ "--op-radius-circle",
+ "--op-radius-pill"
+ ],
+ "width": [
+ "--op-border-width",
+ "--op-border-width-large",
+ "--op-border-width-x-large"
+ ],
+ "shadow": [
+ "--op-border-none",
+ "--op-border-all",
+ "--op-border-top",
+ "--op-border-right",
+ "--op-border-bottom",
+ "--op-border-left",
+ "--op-border-y",
+ "--op-border-x"
+ ]
+ },
+ "shadows": [
+ "--op-shadow-x-small",
+ "--op-shadow-small",
+ "--op-shadow-medium",
+ "--op-shadow-large",
+ "--op-shadow-x-large"
+ ],
+ "opacity": [
+ "--op-opacity-none",
+ "--op-opacity-overlay",
+ "--op-opacity-disabled",
+ "--op-opacity-half",
+ "--op-opacity-full"
+ ],
+ "breakpoints": [
+ "--op-breakpoint-x-small",
+ "--op-breakpoint-small",
+ "--op-breakpoint-medium",
+ "--op-breakpoint-large",
+ "--op-breakpoint-x-large"
+ ],
+ "inputs": {
+ "heights": [
+ "--op-input-height-small",
+ "--op-input-height-medium",
+ "--op-input-height-large",
+ "--op-input-height-x-large"
+ ],
+ "focus": [
+ "--op-input-inner-focus",
+ "--op-input-outer-focus",
+ "--op-input-focus-primary",
+ "--op-input-focus-neutral",
+ "--op-input-focus-danger",
+ "--op-input-focus-warning",
+ "--op-input-focus-info",
+ "--op-input-focus-notice"
+ ]
+ },
+ "transitions": [
+ "--op-transition-accordion",
+ "--op-transition-accordion-content",
+ "--op-transition-input",
+ "--op-transition-sidebar",
+ "--op-transition-modal",
+ "--op-transition-panel",
+ "--op-transition-tooltip"
+ ],
+ "animations": [
+ "--op-animation-flash"
+ ],
+ "z_index": [
+ "--op-z-index-header",
+ "--op-z-index-footer",
+ "--op-z-index-sidebar",
+ "--op-z-index-dialog",
+ "--op-z-index-dialog-backdrop",
+ "--op-z-index-dialog-content",
+ "--op-z-index-dropdown",
+ "--op-z-index-alert-group",
+ "--op-z-index-tooltip"
+ ],
+ "encoded_images": [
+ "--op-encoded-images-dropdown-arrow",
+ "--op-encoded-images-dropdown-arrow-width"
+ ]
+ }
+}
diff --git a/.github/skills/routing-patterns/SKILL.md b/.github/skills/routing-patterns/SKILL.md
new file mode 100644
index 00000000..e801d859
--- /dev/null
+++ b/.github/skills/routing-patterns/SKILL.md
@@ -0,0 +1,613 @@
+---
+name: routing-patterns
+description: Review, generate, and update Rails routes following professional patterns and best practices. Covers RESTful resource routing, route concerns for code reusability, shallow nesting strategies, and advanced route configurations.
+---
+
+# Routes Best Practices
+
+## Purpose
+This skill helps AI agents review, generate, and update Rails routes following professional patterns and best practices. It covers RESTful resource routing, route concerns for code reusability, shallow nesting strategies, and advanced route configurations that can be applied to any Rails application.
+
+## Context
+This skill covers:
+- **Rails routing** with RESTful conventions
+- **Route concerns** for DRY (Don't Repeat Yourself) principle
+- **Shallow nesting** to avoid overly long URLs
+- **Custom parameters and metadata** for flexible routing
+- **Custom route resolvers** for polymorphic paths
+- **Constraint-based routing** for authorization
+- **Organization patterns** for maintainable routes files
+
+## Best Practices
+
+## 1. Route Concerns for Reusable Behavior
+
+Define concerns at the top of routes.rb for behaviors shared across multiple resources. This keeps your routes DRY and maintainable.
+
+**Example: Commentable Resources**
+```ruby
+concern :commentable do
+ resources :comments, commentable_type: parent_resource.name.classify
+end
+```
+
+**Example: Duplicatable Resources**
+```ruby
+concern :duplicatable do
+ resources :duplications, only: %i[create], resource_type: parent_resource.name.classify
+end
+```
+
+**Example: Dynamic Form Updates (Turbo/AJAX)**
+```ruby
+concern :turbo_fetch do
+ patch :turbo_fetch, on: :collection
+end
+```
+
+**Key Points:**
+- Concerns extract common nested resource patterns into reusable modules
+- Use `parent_resource.name.classify` to dynamically pass the parent context type to controllers
+- Specify `only:` or `except:` to limit actions when appropriate
+- Custom parameters (like `commentable_type`, `resource_type`) are passed as metadata to controllers
+- Controllers can access these via `params[:commentable_type]` or routing metadata
+
+## 2. Applying Concerns to Resources
+
+Apply concerns using the `concerns:` option with an array of symbols:
+
+```ruby
+resources :products, concerns: %i[duplicatable turbo_fetch]
+resources :articles, concerns: %i[commentable duplicatable turbo_fetch]
+```
+
+**Benefits:**
+- Eliminates repetitive nested resource definitions
+- Changes to shared behavior only need to be updated in one place
+- Makes it immediately clear which resources share common functionality
+
+## 3. Shallow Nesting Strategy
+
+Use `scope shallow: true` wrapper to enable shallow nesting for all nested resources. This prevents URLs from becoming unwieldy with deep nesting.
+
+```ruby
+scope shallow: true do
+ resources :projects do
+ resources :tasks do
+ resources :comments
+ # comments routes are shallow - only :index and :create are nested
+ # show/edit/update/destroy use /comments/:id instead of /projects/:project_id/tasks/:task_id/comments/:id
+ end
+ end
+ end
+end
+```
+
+**Benefits:**
+- Shorter, cleaner URLs for member actions (show, edit, update, destroy)
+- Only collection actions (index, create) remain nested under parent
+- Easier to bookmark and share individual resource URLs
+- Can override with `shallow: false` when full nesting is needed
+
+**Generated Routes:**
+```
+# Nested (collection routes)
+GET /projects/:project_id/tasks tasks#index
+POST /projects/:project_id/tasks tasks#create
+GET /projects/:project_id/tasks/new tasks#new
+
+# Shallow (member routes)
+GET /tasks/:id tasks#show
+GET /tasks/:id/edit tasks#edit
+PATCH /tasks/:id tasks#update
+DELETE /tasks/:id tasks#destroy
+```
+
+**Override Example:**
+```ruby
+concern :assembly do
+ # Keep full nesting when parent context is always needed
+ resources :assembly_items, only: %i[show], param: :kind, shallow: false
+end
+```
+
+## 4. Limiting Actions with only: and except:
+
+Always be explicit about which actions a resource provides. This improves security, performance, and code clarity.
+
+```ruby
+# Only specific actions
+resources :webhooks, only: [], concerns: %i[turbo_fetch] # No standard REST actions, only custom
+resources :duplications, only: %i[create] # Only create action needed
+resources :previews, only: %i[show update] # Only show and update
+
+# All except specific actions
+resources :automations, except: %i[show] # All standard actions except show
+resources :notes, except: %i[show] # Create/edit/destroy but no individual view
+resources :widgets, except: %i[index show] # No collection or individual views
+```
+
+**Why This Matters:**
+- Prevents unused routes from cluttering `rails routes` output
+- Blocks access to unimplemented controller actions
+- Documents intent clearly for other developers
+- Reduces attack surface by not exposing unnecessary endpoints
+
+## 5. Nested Resources
+
+For resources that belong to a parent, nest them appropriately:
+
+```ruby
+resources :companies do
+ resources :employees, except: %i[index show]
+end
+
+resources :products do
+ resources :reviews, except: %i[index]
+ resources :variants, except: %i[index show]
+end
+```
+
+**Common Pattern:**
+- Parent resource gets full CRUD by default
+- Nested resources often exclude `index` (displayed on parent's show page)
+- Nested resources often exclude `show` (edited inline or from parent view)
+- Nested create/update/destroy actions work within parent context
+
+## 6. Singular Resources
+
+Use `resource` (singular) for resources where there's only one per parent:
+
+```ruby
+resources :users do
+ resource :profile, only: %i[show edit update] # Only one profile per user
+ resource :settings, only: %i[edit update] # Only one settings per user
+ resource :avatar, only: %i[show update] # Only one avatar per user
+end
+```
+
+**Key Points:**
+- Singular resources don't have an `index` action
+- URLs don't require an `:id` parameter (e.g., `/users/1/profile` not `/users/1/profiles/1`)
+- Perfect for one-to-one relationships or singleton resources
+
+## 7. Collection and Member Routes
+
+Add custom routes using `on: :collection` or `on: :member`:
+
+```ruby
+resources :products do
+ # Collection routes (no :id needed)
+ get :search, on: :collection # /products/search
+ post :bulk_import, on: :collection # /products/bulk_import
+
+ # Member routes (requires :id)
+ post :duplicate, on: :member # /products/:id/duplicate
+ patch :archive, on: :member # /products/:id/archive
+ get :preview, on: :member # /products/:id/preview
+end
+
+# In a concern
+concern :archivable do
+ patch :archive, on: :member
+ patch :unarchive, on: :member
+end
+```
+
+**Key Points:**
+- **Collection routes** act on the entire collection (no `:id` parameter)
+- **Member routes** act on a single resource (requires `:id` parameter)
+- Use appropriate HTTP verbs (GET for reads, POST for creates, PATCH/PUT for updates, DELETE for removes)
+
+## 8. Custom Parameters
+
+Override default parameter names using `param:`:
+
+```ruby
+resources :products, param: :slug # Uses :slug instead of :id
+
+# In concerns
+concern :categorizable do
+ resources :categories, only: %i[show], param: :slug
+end
+```
+
+**Results:**
+- URLs become `/products/:slug` instead of `/products/:id`
+- Controller receives `params[:slug]` instead of `params[:id]`
+- Example: `/products/vintage-leather-jacket` instead of `/products/123`
+- Useful for SEO-friendly URLs or when using non-numeric identifiers
+
+## 9. Default Options
+
+Set default options for a resource:
+
+```ruby
+resources :categories, defaults: { subcategory: false }
+```
+
+These defaults are available in `params[:subcategory]`.
+
+## 10. Custom Route Resolvers
+
+Define custom resolvers for polymorphic path helpers:
+
+```ruby
+resolve 'Bulk::Accessories' do |form|
+ form.persisted? ? [form.accessory] : [form.tank, :accessories]
+end
+
+resolve 'AssemblyItem' do |item|
+ [item.host, item]
+end
+```
+
+**Usage:** These allow `url_for(@form_object)` or `link_to(@assembly_item)` to work correctly.
+
+## 11. Session Routes
+
+Use `controller` block with `scope` for related authentication actions:
+
+```ruby
+controller :sessions do
+ get :login, action: :new
+ delete :logout, action: :destroy
+
+ scope :auth do
+ get :failure
+ match 'ADFS/callback', action: :create, via: %i[get post], as: :adfs_callback
+ end
+end
+```
+
+## 12. Mounting Engines with Constraints
+
+Mount admin engines with authentication constraints:
+
+```ruby
+# Allow access only if user is logged in and is admin
+mount PgHero::Engine, at: :pghero,
+ constraints: -> env {
+ env.session[:user_id].present? &&
+ User.find_by(id: env.session[:user_id])&.admin?
+ }
+
+# Redirect to login if not authenticated
+get :pghero, to: redirect('/login'), anchor: false,
+ constraints: -> env { env.session[:user_id].blank? }
+```
+
+## 13. Health Check Routes
+
+```ruby
+# Production health check
+mount Health::Check::Engine, at: 'health-check' if Rails.env.production?
+
+# Rails 7.1+ health check
+get :up, to: 'rails/health#show', as: :rails_health_check
+```
+
+## Complete Example Structure
+
+Here's a well-organized routes file following all best practices:
+
+```ruby
+Rails.application.routes.draw do
+ # 1. Define concerns first (reusable route patterns)
+ concern :commentable do
+ resources :comments, commentable_type: parent_resource.name.classify
+ end
+
+ concern :archivable do
+ patch :archive, on: :member
+ patch :unarchive, on: :member
+ end
+
+ concern :searchable do
+ get :search, on: :collection
+ end
+
+ # 2. Main routes with shallow nesting
+ scope shallow: true do
+ # Top-level resources
+ resources :users, except: %i[show] do
+ resource :profile, only: %i[show edit update]
+ resource :settings, only: %i[edit update]
+ end
+
+ resources :products, concerns: %i[commentable archivable searchable] do
+ resources :reviews, except: %i[index]
+ resources :variants, except: %i[index show]
+ end
+
+ # Nested resources
+ resources :projects do
+ resources :tasks, concerns: %i[commentable] do
+ resource :assignment, only: %i[create destroy]
+ end
+ end
+ end
+
+ # 3. Session/authentication routes
+ controller :sessions do
+ get :login, action: :new
+ post :login, action: :create
+ delete :logout, action: :destroy
+ end
+
+ # 4. Admin routes with constraints
+ namespace :admin do
+ resources :users
+ resources :settings, only: %i[index update]
+ end
+
+ # 5. Mounted engines (with constraints if needed)
+ mount Sidekiq::Web, at: '/sidekiq', constraints: AdminConstraint.new
+
+ # 6. Custom route resolvers (for polymorphic routing)
+ resolve 'ProjectTask' do |task|
+ [task.project, task]
+ end
+
+ # 7. Health checks
+ get :up, to: 'rails/health#show', as: :rails_health_check
+
+ # 8. Root route
+ root 'dashboard#index'
+end
+```
+
+## Review Checklist
+
+When reviewing or generating routes, verify:
+
+- [ ] Concerns are defined at the top of the file
+- [ ] Concerns are DRY and reusable across multiple resources
+- [ ] Custom parameters (like `commentable_type`) use `parent_resource.name.classify`
+- [ ] Shallow nesting is enabled with `scope shallow: true`
+- [ ] Resources explicitly use `only:` or `except:` to limit actions
+- [ ] Nested resources follow the pattern (often no index/show)
+- [ ] Singular resources use `resource` not `resources`
+- [ ] Collection/member routes use `on:` parameter
+- [ ] Custom parameter names use `param:` when needed
+- [ ] Route resolvers are defined for form objects and polymorphic models
+- [ ] Admin engines have authentication constraints
+- [ ] Root route is defined
+- [ ] Routes are organized logically (concerns β resources β custom β engines β resolvers β root)
+
+## Anti-Patterns to Avoid
+
+β **Don't repeat nested resource patterns:**
+```ruby
+resources :articles do
+ resources :comments, commentable_type: 'Article'
+end
+
+resources :posts do
+ resources :comments, commentable_type: 'Post'
+end
+```
+
+β
**Use concerns instead:**
+```ruby
+concern :commentable do
+ resources :comments, commentable_type: parent_resource.name.classify
+end
+
+resources :articles, concerns: %i[commentable]
+resources :posts, concerns: %i[commentable]
+```
+
+β **Don't use deep nesting without shallow:**
+```ruby
+resources :companies do
+ resources :projects do
+ resources :tasks do
+ # Results in /companies/:company_id/projects/:project_id/tasks/:id/edit
+ end
+ end
+end
+```
+
+β
**Use shallow nesting:**
+```ruby
+scope shallow: true do
+ resources :companies do
+ resources :projects do
+ resources :tasks # edit becomes /tasks/:id/edit
+ end
+ end
+end
+```
+
+β **Don't leave all actions when not needed:**
+```ruby
+resources :duplications # Provides 7 REST actions but only need create
+```
+
+β
**Be explicit:**
+```ruby
+resources :duplications, only: %i[create]
+```
+
+β **Don't use plural for singular resources:**
+```ruby
+resources :profiles, only: %i[show] # There's only one profile per user
+```
+
+β
**Use singular resource:**
+```ruby
+resource :profile, only: %i[show]
+```
+
+β **Don't hardcode context types:**
+```ruby
+concern :commentable do
+ resources :comments, commentable_type: 'Article' # Only works for articles
+end
+```
+
+β
**Use parent_resource:**
+```ruby
+concern :commentable do
+ resources :comments, commentable_type: parent_resource.name.classify
+end
+```
+
+## Advanced Patterns
+
+## Concerns with Nested Concerns
+
+Concerns can reference other concerns for highly reusable routing patterns:
+
+```ruby
+concern :searchable do
+ get :search, on: :collection
+ get :autocomplete, on: :collection
+end
+
+concern :taggable do
+ resources :tags, only: %i[index create destroy],
+ concerns: %i[searchable],
+ taggable_type: parent_resource.name.classify
+end
+
+resources :articles, concerns: %i[taggable]
+# Articles get tags with search and autocomplete functionality
+```
+
+## Multiple Custom Params
+
+You can pass multiple custom parameters to concerns:
+
+```ruby
+concern :versioned do
+ resources :versions,
+ only: %i[index show],
+ param: :version_number,
+ shallow: false,
+ versionable_type: parent_resource.name.classify
+end
+```
+
+These custom parameters are available in the controller as routing metadata:
+```ruby
+# In controller
+def index
+ @versionable_type = request.path_parameters[:versionable_type] # => "Article"
+end
+```
+
+## Context-Specific Route Additions
+
+Add additional routes to specific resources after applying a concern:
+
+```ruby
+resources :articles, concerns: %i[commentable] do
+ # Add article-specific routes beyond the concern
+ get :preview, on: :member
+ post :publish, on: :member
+end
+```
+
+## Conditional Engine Mounting
+
+Mount engines conditionally based on environment:
+
+```ruby
+if Rails.env.production?
+ mount HealthCheck::Engine, at: 'health-check'
+end
+
+unless Rails.env.production?
+ mount LetterOpenerWeb::Engine, at: '/letter_opener'
+end
+```
+
+## Common Routing Patterns
+
+These patterns appear frequently in Rails applications and can be implemented using concerns:
+
+**Commentable Resources:**
+- Pattern: Resources that can have comments
+- Implementation: Nested comments resource with polymorphic association
+- Applied to: Articles, blog posts, products, tasks, etc.
+
+**Duplicatable Resources:**
+- Pattern: Resources that can be cloned/duplicated
+- Implementation: Create-only nested resource for duplication action
+- Applied to: Templates, documents, configurations, etc.
+
+**Archivable Resources:**
+- Pattern: Resources that can be archived/unarchived
+- Implementation: Member routes for archive state changes
+- Applied to: Projects, documents, records, etc.
+
+**Searchable Resources:**
+- Pattern: Resources with search/filter functionality
+- Implementation: Collection routes for search operations
+- Applied to: Products, users, articles, etc.
+
+**Versioned Resources:**
+- Pattern: Resources with version history
+- Implementation: Nested versions resource, often read-only
+- Applied to: Documents, API resources, configurations, etc.
+
+**Taggable Resources:**
+- Pattern: Resources that can be tagged/categorized
+- Implementation: Nested tags with create/destroy actions
+- Applied to: Articles, images, bookmarks, etc.
+
+## Usage Instructions for AI Agents
+
+When asked to **review routes:**
+1. Check routes are organized: concerns β resources β custom β engines β resolvers β root
+2. Verify concerns are properly defined and reusable
+3. Check for deep nesting that should use shallow
+4. Ensure resources use `only:`/`except:` appropriately
+5. Verify singular vs plural resource usage
+6. Check for repeated patterns that should be concerns
+7. Validate custom resolvers match model relationships
+
+When asked to **generate new routes:**
+1. Determine if the behavior is reusable β create/use a concern
+2. Check if resource should be nested under a parent
+3. Enable shallow nesting for nested resources (unless explicitly needed)
+4. Specify `only:` or `except:` based on needed actions
+5. Use `resource` (singular) if there's only one per parent
+6. Add custom collection/member routes if needed
+7. Create route resolver if it's a form object or polymorphic model
+8. Follow existing concern patterns for similar functionality
+
+When asked to **update routes:**
+1. Maintain consistency with existing patterns
+2. If adding similar functionality to multiple resources, refactor to use concerns
+3. Update existing concerns rather than duplicating code
+4. Preserve shallow nesting strategy
+5. Keep routes organized in the established structure
+6. Update custom resolvers if model relationships change
+
+## Testing Routes
+
+Always verify routes after changes:
+
+```bash
+# List all routes
+rails routes
+
+# Search for specific routes
+rails routes | grep products
+
+# Show routes for a specific controller
+rails routes -c products
+
+# Show routes with expanded format
+rails routes --expanded
+
+# Filter by HTTP verb
+rails routes -g POST
+```
+
+---
+
+*Last Updated: February 2026*
diff --git a/.github/skills/stimulus-controllers/SKILL.md b/.github/skills/stimulus-controllers/SKILL.md
new file mode 100644
index 00000000..309eec77
--- /dev/null
+++ b/.github/skills/stimulus-controllers/SKILL.md
@@ -0,0 +1,208 @@
+---
+name: stimulus-controllers
+description: Create and register Stimulus controllers for interactive JavaScript features. Use when adding client-side interactivity, dynamic UI updates, or when the user mentions Stimulus controllers or JavaScript behavior.
+---
+
+# Stimulus Controllers
+
+## Overview
+Stimulus controllers provide modular JavaScript functionality connected to HTML via data attributes. After creating a new controller, you must register it in the index.js file.
+
+## Creating a New Controller
+
+### 1. Create the Controller File
+Create a new controller in `app/javascript/controllers/`:
+
+```javascript
+// app/javascript/controllers/example_controller.js
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = ["element"]
+ static values = { name: String }
+
+ connect() {
+ // Called when controller is connected to DOM
+ }
+
+ disconnect() {
+ // Called when controller is disconnected from DOM
+ }
+
+ // Action methods
+ handleClick(event) {
+ event.preventDefault()
+ // Your logic here
+ }
+}
+```
+
+### 2. Register the Controller
+**CRITICAL**: After creating a new controller, run:
+
+```bash
+bin/rails stimulus:manifest:update
+```
+
+This automatically updates `app/javascript/controllers/index.js` to register your controller.
+
+**Manual Registration** (if needed):
+```javascript
+import ExampleController from "./example_controller"
+application.register("example", ExampleController)
+```
+
+Controller name in HTML uses kebab-case: `data-controller="example"`
+
+### 3. Use in HTML
+Connect the controller to HTML elements:
+
+```slim
+.container data-controller="example" data-example-name-value="test"
+ button data-action="click->example#handleClick" Click Me
+ div data-example-target="element" Target Element
+```
+
+## Naming Conventions
+
+- **File**: `example_controller.js` (snake_case)
+- **Class**: `export default class extends Controller`
+- **Registration**: `"example"` (kebab-case)
+- **HTML**: `data-controller="example"` (kebab-case)
+- **Multi-word**: `bulk_submit_controller.js` β `"bulk-submit"`
+
+## Key Concepts
+
+### Targets
+Reference specific DOM elements:
+
+```javascript
+static targets = ["input", "output"]
+
+// Access in methods:
+this.inputTarget // First matching element
+this.inputTargets // All matching elements
+this.hasInputTarget // Boolean check
+```
+
+### Values
+Type-safe data attributes:
+
+```javascript
+static values = {
+ url: String,
+ count: Number,
+ active: Boolean,
+ items: Array,
+ config: Object
+}
+
+// Access in methods:
+this.urlValue
+this.countValue
+
+// Watch for changes:
+urlValueChanged(newUrl, oldUrl) {
+ // Called when value changes
+}
+```
+
+### Actions
+Connect events to methods:
+
+```html
+
+data-action="click->example#save"
+
+
+data-action="click->example#save submit->example#submit"
+
+
+data-action="example:refresh->example#reload"
+
+
+data-action="submit->example#save:prevent"
+```
+
+### Classes
+Manage CSS classes:
+
+```javascript
+static classes = ["active", "hidden"]
+
+// Use in methods:
+this.element.classList.add(this.activeClass)
+this.element.classList.remove(this.hiddenClass)
+```
+
+## Common Patterns
+
+### Form Validation
+```javascript
+export default class extends Controller {
+ static targets = ["form", "submit"]
+
+ validate() {
+ const isValid = this.formTarget.checkValidity()
+ this.submitTarget.disabled = !isValid
+ }
+}
+```
+
+### Toggle Visibility
+```javascript
+export default class extends Controller {
+ static targets = ["content"]
+ static classes = ["hidden"]
+
+ toggle() {
+ this.contentTarget.classList.toggle(this.hiddenClass)
+ }
+}
+```
+
+### AJAX Updates
+```javascript
+export default class extends Controller {
+ static values = { url: String }
+
+ async refresh() {
+ const response = await fetch(this.urlValue)
+ const html = await response.text()
+ this.element.innerHTML = html
+ }
+}
+```
+
+## Testing
+
+Test Stimulus controllers in system specs:
+
+```ruby
+it 'handles interaction', :js do
+ visit page_path
+
+ click_button 'Toggle'
+
+ expect(page).to have_css('[data-controller="example"]')
+end
+```
+
+## Troubleshooting
+
+**Controller not working?**
+1. Verify controller is registered in `index.js`
+2. Run `bin/rails stimulus:manifest:update`
+3. Check browser console for errors
+4. Verify data attribute spelling (kebab-case)
+5. Ensure JavaScript is enabled in tests (`:js` tag)
+
+**Targets not found?**
+- Check target name in `static targets` matches HTML
+- Use `hasXxxTarget` to verify existence before accessing
+- Ensure target element is in controller scope
+
+## Related Skills
+- [frontend-patterns](../frontend-patterns/SKILL.md) - HTML and CSS patterns
+- [turbo-fetch](../turbo-fetch/SKILL.md) - Dynamic form updates
+- [testing-patterns](../testing-patterns/SKILL.md) - Testing JavaScript features
diff --git a/.github/skills/testing-patterns/SKILL.md b/.github/skills/testing-patterns/SKILL.md
new file mode 100644
index 00000000..51ad0b78
--- /dev/null
+++ b/.github/skills/testing-patterns/SKILL.md
@@ -0,0 +1,119 @@
+---
+name: testing-patterns
+description: Write automated tests using RSpec, Capybara, and FactoryBot for Rails applications. Use when implementing features, fixing bugs, or when the user mentions testing, specs, RSpec, Capybara, or test data. Avoid using rails console or server for testing.
+---
+
+# Testing Patterns
+
+## Overview
+Write automated tests using RSpec and Capybara. Avoid using the Rails console or starting a Rails server for testing.
+
+## Test Command
+```bash
+bundle exec rspec
+```
+
+## Tech Stack
+- **RSpec** - Testing framework
+- **Capybara** - System/integration testing
+- **FactoryBot** - Test data generation
+
+## Best Practices
+
+### General Guidelines
+- Write tests first or alongside implementation
+- Avoid manual testing via console
+- Use factories for test data creation
+- Keep tests focused and readable
+
+### RSpec Conventions
+- Use descriptive context and describe blocks
+- Follow the arrange-act-assert pattern
+- Use let for test data setup
+- Prefer `let` over instance variables
+
+### Validation Testing Pattern
+Test validations explicitly using `build` with invalid data, then verify the model is invalid and check error messages:
+
+```ruby
+describe 'validations' do
+ it 'must have a start date' do
+ membership = build(:membership, start_date: nil)
+
+ expect(membership).not_to be_valid
+ expect(membership.errors.full_messages).to contain_exactly "Start date can't be blank"
+ end
+
+ it 'enforces end date must follow start date' do
+ membership = build(:membership, start_date: 1.year.ago, end_date: 2.years.ago)
+
+ expect(membership).not_to be_valid
+ expect(membership.errors.full_messages).to contain_exactly 'End date must follow start date'
+ end
+
+ it 'permits empty end date' do
+ membership = build(:membership, start_date: 1.year.ago, end_date: nil)
+
+ expect(membership).to be_valid
+ end
+end
+```
+
+Key points:
+- Use `build` instead of `create` to avoid database writes
+- Test both invalid and valid scenarios
+- Verify exact error messages with `full_messages`
+- Use descriptive test names that explain the business rule
+- Don't test associations or enums
+
+### Capybara System Tests
+- Test user-facing functionality
+- Use `data-testid` attributes with `dom_id` for reliable element selection
+- Test happy paths and edge cases
+- Ensure tests are deterministic
+- Avoid sleep statements; use Capybara's waiting mechanisms such as native expectations of elements to appear
+- Use `:js` (e.g. `it 'does something', :js do`) for specs that run javascript such as stimulus controllers
+
+### Element Selection with data-testid
+Use `data-testid` attributes with `dom_id` for stable, reliable element selection that's resistant to UI changes:
+
+**View:**
+```slim
+tbody
+ - @entries.each do |entry|
+ tr data-testid=dom_id(entry)
+ td= entry.name
+```
+
+**Spec:**
+```ruby
+within(data_test(entry1)) do
+ click_button 'Submit'
+end
+```
+
+**Benefits:**
+- Resilient to text changes (descriptions, labels, etc.)
+- Works with dynamic content
+- Self-documenting test intent
+- Easier to refactor views
+
+**Avoid:**
+- Text-based lookups: `within('tr', text: 'Entry 1')`
+- CSS class selectors that may change during styling
+- Overly specific DOM traversal
+
+### FactoryBot
+- Define factories for all models
+- Use traits for variations
+- Keep factories minimal
+- Override attributes in tests as needed
+- Always use `build` or `create` instead of direct model instantiation
+- Use `build` for validation tests to avoid database writes
+- Use `create` when you need persisted records
+
+## Future Topics
+- Mocking and stubbing patterns
+- Test organization strategies
+- Performance testing
+- CI/CD integration
diff --git a/.github/skills/theming-context/SKILL.MD b/.github/skills/theming-context/SKILL.MD
new file mode 100644
index 00000000..1b52102c
--- /dev/null
+++ b/.github/skills/theming-context/SKILL.MD
@@ -0,0 +1,107 @@
+---
+name: theming-context
+description: Using Optics for implementing design system guidelines, theming, and color scales.
+metadata:
+ triggers:
+ - theme
+ - design-system
+ - optics
+---
+# Theming
+To customize the application, a custom theme files that serve as overrides to the existing tokens can be provided. An example implementation of the main project CSS file would look like:
+```css
+@import '@rolemodel/optics';
+
+@import 'stylesheets/theme/my_app_theme';
+```
+Note how the custom theme is imported after the main Optics styles, ensuring that any token overrides take precedence.
+
+A custom theme can change any tokens, including colors, radius, fonts, and even redefine the luminosity and semantic scales.
+
+```css
+@import url('https://fonts.googleapis.com/css2?family=Coming+Soon&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Grandstander:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
+
+:root {
+ /* Colors */
+ --op-color-primary-h: my-new-value;
+ --op-color-primary-s: my-new-value;
+ --op-color-primary-l: my-new-value;
+
+ /* Color Scale */
+ --op-color-primary-plus-two: light-dark(
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 64%),
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 32%)
+ );
+
+ /* Fonts */
+ --op-font-family: 'Coming Soon', sans-serif;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme-mode='light']) {
+ --op-font-family: 'Grandstander', sans-serif;
+ }
+}
+
+:root[data-theme-mode='dark'] {
+ --op-font-family: 'Grandstander', sans-serif;
+}
+```
+
+### Color Scale Overriding
+If you need to override the provided color scales, you should redefine all the variants of that color to ensure consistency across the application. For example, if you are overriding the primary color, you should redefine all its variants like so:
+
+```css
+:root {
+ --op-color-primary-h: 164;
+ --op-color-primary-s: 100%;
+ --op-color-primary-l: 50%;
+
+ /* Main Scale */
+ --op-color-primary-plus-two: light-dark(
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 64%),
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 32%)
+ );
+ --op-color-primary-plus-one: light-dark(
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 45%),
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 35%)
+ );
+
+ /* On Scale */
+ --op-color-primary-on-plus-two: light-dark(
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%),
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%)
+ );
+ --op-color-primary-on-plus-two-alt: light-dark(
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 6%),
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 92%)
+ );
+ --op-color-primary-on-plus-one: light-dark(
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%),
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%)
+ );
+ --op-color-primary-on-plus-one-alt: light-dark(
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 95%),
+ hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%)
+ );
+}
+```
+
+## For More Information
+For more details on theming and token overrides, refer to the Optics documentation using the links below
+- [Optics Repo](https://github.com/RoleModel/optics/)
+
+### Documentation Resources
+For detailed component usage, refer to:
+- **Component API**: https://docs.optics.rolemodel.design/?path=/docs/components-button--docs
+- **Token Reference**: https://docs.optics.rolemodel.design/?path=/docs/overview-tokens--docs
+- **Color System**: https://docs.optics.rolemodel.design/?path=/docs/tokens-color-color-scale--docs
+
+### When You Need More Information
+If the user asks about specific components or advanced features not covered in this skill:
+1. Use `web_fetch` or another method to retrieve the relevant documentation page
+2. Extract the specific information needed
+3. Apply it to the user's request
+
+Example: For button variants, check https://docs.optics.rolemodel.design/?path=/docs/components-button--docs
diff --git a/.github/skills/turbo-fetch/SKILL.md b/.github/skills/turbo-fetch/SKILL.md
new file mode 100644
index 00000000..d9a1a266
--- /dev/null
+++ b/.github/skills/turbo-fetch/SKILL.md
@@ -0,0 +1,210 @@
+---
+name: turbo-fetch
+description: Implement dynamic form updates using Turbo Streams and Stimulus. Use when forms need to update fields based on user selections without full page reloads, such as cascading dropdowns, conditional fields, or dynamic option lists.
+---
+
+# Turbo Fetch Skill
+
+This skill documents the turbo fetch pattern for dynamically updating form fields based on user input using Turbo Streams and Stimulus.
+
+## When to Use
+
+Use turbo fetch when you need to:
+- Update form options based on another field's selection
+- Show/hide conditional fields dynamically
+- Refresh parts of a form without reloading the entire page
+- Implement cascading dropdowns or dependent fields
+
+## Pattern Overview
+
+The turbo fetch pattern consists of four components:
+
+1. **Routes** - A routing concern that adds the turbo_fetch endpoint
+2. **Controller Action** - A backend action that prepares data and renders turbo streams
+3. **Stimulus Controller** - Frontend controller that triggers PATCH requests
+4. **Turbo Stream View** - Template that updates specific DOM elements
+
+## Implementation Steps
+
+### 1. Add Routing Concern (if not already present)
+
+In `config/routes.rb`, ensure the turbo_fetch concern exists:
+
+```ruby
+concern :turbo_fetch do
+ patch :turbo_fetch, on: :collection
+end
+```
+
+Then apply it to your resource:
+
+```ruby
+resources :materials, concerns: %i[turbo_fetch]
+```
+
+This creates a route: `PATCH /materials/turbo_fetch`
+
+### 2. Implement Controller Action
+
+Add a `turbo_fetch` action to your controller:
+
+```ruby
+class MaterialsController < ApplicationController
+ def turbo_fetch
+ @material = authorize Material.new(material_params)
+ # The view will handle the turbo stream responses
+ end
+
+ private
+
+ def material_params
+ params.require(:material).permit(:type, :substance, ...)
+ end
+end
+```
+
+**Key points:**
+- Creates a new instance with submitted params (doesn't save to database)
+- Runs authorization if needed
+- The instance will be used in the turbo stream view to determine updated options
+
+### 3. Create Turbo Stream View
+
+Create `app/views/[resource]/turbo_fetch.turbo_stream.slim`:
+
+```slim
+= simple_form_for @material do |f|
+ = turbo_stream.update 'substance-field' do
+ = f.input :substance, collection: @material.substances
+ = turbo_stream.replace 'material_details', partial: dimension_fields_partial_path, locals: { f: }
+```
+
+**Available turbo stream actions:**
+- `turbo_stream.update` - Replace the content inside an element
+- `turbo_stream.replace` - Replace the entire element
+- `turbo_stream.append` - Add content at the end
+- `turbo_stream.prepend` - Add content at the beginning
+- `turbo_stream.remove` - Remove an element
+
+### 4. Setup Form with Stimulus Controller
+
+Add the Stimulus controller to your form:
+
+```slim
+= simple_form_for resource, data: { controller: 'turbo-fetch', turbo_fetch_url_value: turbo_fetch_materials_url } do |f|
+ .form-row
+ = f.input :type, input_html: { data: { action: "turbo-fetch#perform" } }
+
+ #substance-field.flexible
+ = f.input :substance, collection: f.object.substances
+
+ #material_details
+ = render dimension_fields_partial_path, f: f
+```
+
+**Key attributes:**
+- `data-controller="turbo-fetch"` - Activates the Stimulus controller
+- `data-turbo-fetch-url-value` - The URL to PATCH (defaults to form action + /turbo_fetch)
+- `data-action="turbo-fetch#perform"` - Triggers the fetch on field change
+
+### 5. Verify Stimulus Controller Exists
+
+The `turbo_fetch_controller.js` should exist at `app/javascript/controllers/turbo_fetch_controller.js`:
+
+```javascript
+import { Controller } from '@hotwired/stimulus'
+import { patch } from '@rails/request.js'
+
+export default class extends Controller {
+ static values = { url: String, count: Number }
+
+ async perform({ params: { url: urlParam, query: queryParams } }) {
+ const body = new FormData(this.element)
+
+ if (queryParams) Object.keys(queryParams).forEach(key => body.append(key, queryParams[key]))
+
+ const response = await patch(urlParam || this.urlValue, { body, responseKind: 'turbo-stream' })
+ if (response.ok) this.countValue += 1
+ }
+}
+```
+
+## Examples from Codebase
+
+### Materials Example
+
+**Route:**
+```ruby
+resources :materials, concerns: %i[duplication turbo_fetch]
+```
+
+**Controller:**
+```ruby
+def turbo_fetch
+ @material = authorize Material.new(material_params)
+end
+```
+
+**View (`turbo_fetch.turbo_stream.slim`):**
+```slim
+= simple_form_for @material do |f|
+ = turbo_stream.update 'substance-field' do
+ = f.input :substance, collection: @material.substances, as: :tom_select, allow_create: true
+ = turbo_stream.replace 'material_details', partial: dimension_fields_partial_path, locals: { f: }
+```
+
+**Form:**
+```slim
+= simple_form_for resource, data: { controller: 'turbo-fetch', turbo_fetch_url_value: turbo_fetch_materials_url } do |f|
+ = f.input :type, input_html: { data: { action: "turbo-fetch#perform" } }
+
+ #substance-field.flexible
+ = f.input :substance, collection: f.object.substances
+
+ #material_details
+ = render dimension_fields_partial_path, f: f
+```
+
+## Common Patterns
+
+### Pattern 1: Dependent Dropdown
+When selecting a type, update available options in another field:
+- Trigger field has `data-action="turbo-fetch#perform"`
+- Target field has unique ID (e.g., `#substance-field`)
+- Turbo stream updates the target with new collection
+
+### Pattern 2: Conditional Field Sections
+Show/hide entire form sections based on selection:
+- Use `turbo_stream.replace` to swap out entire sections
+- Render different partials based on the selected value
+
+### Pattern 3: Member vs Collection Routes
+Most turbo_fetch routes are on `:collection`, but for nested resources with IDs:
+
+```ruby
+resources :custom_parts, concerns: %i[turbo_fetch] do
+ patch :turbo_fetch, on: :member # For child items with IDs
+end
+```
+
+## Tips
+
+1. **Target IDs**: Ensure target elements have unique, stable IDs
+2. **Form Context**: The turbo stream view wraps form builder in `simple_form_for` to maintain form context
+3. **Authorization**: Apply same authorization as create/update actions
+4. **Don't Save**: The turbo_fetch action creates instances but never saves them
+5. **Multiple Updates**: You can include multiple turbo_stream updates in one response
+
+## Troubleshooting
+
+**Updates not appearing:**
+- Check that target element ID matches the turbo_stream selector
+- Verify the Stimulus action is firing (check browser console)
+- Ensure turbo_fetch route exists (run `rails routes | grep turbo_fetch`)
+
+**Wrong data in fields:**
+- Verify params are being permitted in `material_params` (or equivalent)
+- Check that the model's computed properties return correct values
+
+**Authorization errors:**
+- Ensure `turbo_fetch` action runs same authorization as `new`/`create`
diff --git a/lib/generators/rolemodel/github/github_generator.rb b/lib/generators/rolemodel/github/github_generator.rb
index 94541335..c6c43616 100644
--- a/lib/generators/rolemodel/github/github_generator.rb
+++ b/lib/generators/rolemodel/github/github_generator.rb
@@ -17,13 +17,31 @@ def remove_rolemodel_rails_version_check
end
def install_copilot_instructions
- copy_file 'instructions/css.instructions.md', '.github/instructions/css.instructions.md'
+ copy_file 'copilot-instructions.md', '.github/copilot-instructions.md'
copy_file 'instructions/js.instructions.md', '.github/instructions/js.instructions.md'
- copy_file 'instructions/project.instructions.md', '.github/instructions/project.instructions.md'
- copy_file 'instructions/ruby.instructions.md', '.github/instructions/ruby.instructions.md'
+ copy_file 'instructions/ruby_model.instructions.md', '.github/instructions/ruby_model.instructions.md'
copy_file 'instructions/slim.instructions.md', '.github/instructions/slim.instructions.md'
end
+ def install_copilot_skills
+ copy_skill 'bem-structure'
+ copy_skill 'controller-patterns'
+ copy_skill 'dynamic-nested-attributes'
+ copy_skill 'form-auto-save'
+ copy_skill 'frontend-patterns'
+ copy_skill 'json-typed-attributes'
+
+ copy_skill 'optics-context'
+ copy_file 'skills/optics-context/assets/components.json', '.github/skills/optics-context/assets/components.json'
+ copy_file 'skills/optics-context/assets/tokens.json', '.github/skills/optics-context/assets/tokens.json'
+
+ copy_skill 'routing-patterns'
+ copy_skill 'stimulus-controllers'
+ copy_skill 'testing-patterns'
+ copy_skill 'theming-context'
+ copy_skill 'turbo-fetch'
+ end
+
def install_ci_yml
copy_file 'templates/ci.yml', '.github/workflows/ci.yml'
end
@@ -39,5 +57,11 @@ def update_database_yml_for_ci
YML
end
end
+
+ private
+
+ def copy_skill(name)
+ copy_file "skills/#{name}/SKILL.md", ".github/skills/#{name}/SKILL.md"
+ end
end
end
diff --git a/lib/generators/rolemodel/testing/rspec/templates/support/helpers/playwright_helper.rb b/lib/generators/rolemodel/testing/rspec/templates/support/helpers/playwright_helper.rb
index e70a485b..9cc075c8 100644
--- a/lib/generators/rolemodel/testing/rspec/templates/support/helpers/playwright_helper.rb
+++ b/lib/generators/rolemodel/testing/rspec/templates/support/helpers/playwright_helper.rb
@@ -58,17 +58,17 @@ def click_on(text = nil, **args)
%w[button link gc-menu-item summary].each do |tag|
loc = scope.get_by_role(tag).get_by_text(text)
- loc = scope.get_by_role(tag).get_by_text(text, exact: true) if loc.count > 1
- loc = scope.locator(tag).get_by_text(text) unless loc.count == 1
- locator = loc if loc.count == 1
+ loc = scope.get_by_role(tag).get_by_text(text, exact: true) if loc.count > 1 # rubocop:disable Style/CollectionQuerying
+ loc = scope.locator(tag).get_by_text(text) unless loc.count == 1 # rubocop:disable Style/CollectionQuerying
+ locator = loc if loc.count == 1 # rubocop:disable Style/CollectionQuerying
break if locator.present?
end
locator = scope.get_by_text(text, exact: true) if locator.blank?
- raise "No element matching text: '#{text}'" if locator.count < 1
+ raise "No element matching text: '#{text}'" if locator.count < 1 # rubocop:disable Style/CollectionQuerying
- if locator.count > 1
+ if locator.count > 1 # rubocop:disable Style/CollectionQuerying
raise "Multiple elements matching text: '#{text}'" unless args[:match] == :first
result = locator.first.click
diff --git a/spec/generators/rolemodel/github_generator_spec.rb b/spec/generators/rolemodel/github_generator_spec.rb
index a9978308..f807fac3 100644
--- a/spec/generators/rolemodel/github_generator_spec.rb
+++ b/spec/generators/rolemodel/github_generator_spec.rb
@@ -10,14 +10,34 @@
assert_file '.github/pull_request_template.md' do |content|
expect(content).not_to include('Update version number in `lib/rolemodel_rails/version.rb`')
end
+ end
- assert_file '.github/instructions/css.instructions.md'
+ it 'creates copilot instructions' do
+ assert_file '.github/copilot-instructions.md'
assert_file '.github/instructions/js.instructions.md'
- assert_file '.github/instructions/project.instructions.md'
- assert_file '.github/instructions/ruby.instructions.md'
+ assert_file '.github/instructions/ruby_model.instructions.md'
assert_file '.github/instructions/slim.instructions.md'
end
+ it 'creates copilot skills' do
+ assert_file '.github/skills/bem-structure/SKILL.md'
+ assert_file '.github/skills/controller-patterns/SKILL.md'
+ assert_file '.github/skills/dynamic-nested-attributes/SKILL.md'
+ assert_file '.github/skills/form-auto-save/SKILL.md'
+ assert_file '.github/skills/frontend-patterns/SKILL.md'
+ assert_file '.github/skills/json-typed-attributes/SKILL.md'
+
+ assert_file '.github/skills/optics-context/SKILL.md'
+ assert_file '.github/skills/optics-context/assets/components.json'
+ assert_file '.github/skills/optics-context/assets/tokens.json'
+
+ assert_file '.github/skills/routing-patterns/SKILL.md'
+ assert_file '.github/skills/stimulus-controllers/SKILL.md'
+ assert_file '.github/skills/testing-patterns/SKILL.md'
+ assert_file '.github/skills/theming-context/SKILL.md'
+ assert_file '.github/skills/turbo-fetch/SKILL.md'
+ end
+
it 'creates ci.yml and updates the database.yml' do
assert_file '.github/workflows/ci.yml' do |content|
expect(content).to include('Linting & Ruby Non-System Tests')